From 90ac80707dfdd1145e8db728da68ecb8f84fd2a4 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 17 Aug 2026 04:30:04 -0700 Subject: [PATCH] Step the timestep and solver tolerances on a well-tempered log ladder The timestep, the solver tolerances, and impratio span several orders of magnitude, where a linear +/- step is useless. Add ImGui_LogStepper, whose buttons walk a "well-tempered" logarithmic ladder of round numbers that closes each decade on a power of ten (see WellTemperedStep). It lays out like ImGui's own InputScalar stepper -- field, then the - / + buttons, then the label. Apply it to the timestep, Tolerance, the LS/Noslip/CCD/Sleep tolerances, and Imp Ratio. Strictly-positive quantities for which zero is meaningful (the tolerances) take a zero_below floor: the ladder's bottom rung is 0, so stepping down snaps to zero and stepping up off zero returns to the floor. The timestep and impratio, which never want zero, leave it unset. PiperOrigin-RevId: 965888812 Change-Id: I823e60913ecb2be2cade96baa5bf7de9e2334d5a --- simulate/simulate.h | 6 +- src/experimental/platform/CMakeLists.txt | 1 + src/experimental/platform/ux/gui.cc | 14 ++-- src/experimental/platform/ux/gui.h | 6 +- src/experimental/platform/ux/imgui_widgets.h | 53 ++++++++++++++ src/experimental/platform/ux/well_tempered.h | 76 ++++++++++++++++++++ 6 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 src/experimental/platform/ux/well_tempered.h diff --git a/simulate/simulate.h b/simulate/simulate.h index 6bc553b4..40a01da5 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -239,9 +239,9 @@ class Simulate { float measured_slowdown = 1.0; // logarithmically spaced real-time slow-down coefficients (percent) static constexpr float percentRealTime[] = { - 100, 80, 66, 50, 40, 33, 25, 20, 16, 13, - 10, 8, 6.6, 5.0, 4, 3.3, 2.5, 2, 1.6, 1.3, - 1, .8, .66, .5, .4, .33, .25, .2, .16, .13, + 100, 80, 63, 50, 40, 32, 25, 20, 16, 13, + 10, 8, 6.3, 5.0, 4, 3.2, 2.5, 2, 1.6, 1.3, + 1, .8, .63, .5, .4, .32, .25, .2, .16, .13, .1 }; diff --git a/src/experimental/platform/CMakeLists.txt b/src/experimental/platform/CMakeLists.txt index 3ff9285e..56d63b14 100644 --- a/src/experimental/platform/CMakeLists.txt +++ b/src/experimental/platform/CMakeLists.txt @@ -65,6 +65,7 @@ target_sources(${MUJOCO_PLATFORM_TARGET_NAME} ux/plugin.h ux/spec_editor.cc ux/spec_editor.h + ux/well_tempered.h ) if(NOT WIN32) diff --git a/src/experimental/platform/ux/gui.cc b/src/experimental/platform/ux/gui.cc index 6453d13c..86d9cb62 100644 --- a/src/experimental/platform/ux/gui.cc +++ b/src/experimental/platform/ux/gui.cc @@ -1007,16 +1007,16 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui::Combo("Solver", &opt.solver, opts3, IM_ARRAYSIZE(opts3)); if (SectionHeader("Algorithmic Parameters", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui_Input("Timestep", &opt.timestep, {0, 1, 0.01, 0.1}); + ImGui_LogStepper("Timestep", &opt.timestep, {0, 1}); ImGui_Input("Iterations", &opt.iterations, {0, 1000, 1, 10}); - ImGui_Input("Tolerance", &opt.tolerance, {0, 1, 1e-7, 1e-6}); + ImGui_LogStepper("Tolerance", &opt.tolerance, {.min = 0.0, .max = 1.0, .zero_below = 1e-10}); ImGui_Input("LS Iter", &opt.ls_iterations, {0, 100, 1, 0.1}); - ImGui_Input("LS Tol", &opt.ls_tolerance, {0, 0.1, 0.01, 0.1}); + ImGui_LogStepper("LS Tol", &opt.ls_tolerance, {.min = 0.0, .max = 0.1, .zero_below = 1e-4}); ImGui_Input("Noslip Iter", &opt.noslip_iterations, {0, 1000, 1, 100}); - ImGui_Input("Noslip Tol", &opt.noslip_tolerance, {0, 1, 0.01, 0.1}); + ImGui_LogStepper("Noslip Tol", &opt.noslip_tolerance, {.min = 0.0, .max = 1.0, .zero_below = 1e-8}); ImGui_Input("CCD Iter", &opt.ccd_iterations, {0, 1000, 1, 100}); - ImGui_Input("CCD Tol", &opt.ccd_tolerance, {0, 1, 0.01, 0.1}); - ImGui_Input("Sleep Tol", &opt.sleep_tolerance, {0, 1, 0.01, 0.1}); + ImGui_LogStepper("CCD Tol", &opt.ccd_tolerance, {.min = 0.0, .max = 1.0, .zero_below = 1e-8}); + ImGui_LogStepper("Sleep Tol", &opt.sleep_tolerance, {.min = 0.0, .max = 1.0, .zero_below = 1e-6}); ImGui_Input("SDF Iter", &opt.sdf_iterations, {1, 20, 1, 10}); ImGui_Input("SDF Init", &opt.sdf_initpoints, {1, 100, 1, 10}); ImGui::TreePop(); @@ -1052,7 +1052,7 @@ void PhysicsGui(mjModel* model, float min_width) { ImGui_InputN("Magnetic", opt.magnetic, 3); ImGui_Input("Density", &opt.density, {.min = 0.0}); ImGui_Input("Viscosity", &opt.viscosity, {.min = 0.0}); - ImGui_Input("Imp Ratio", &opt.impratio, {.min = 0.0}); + ImGui_LogStepper("Imp Ratio", &opt.impratio, {.min = 0.0}); ImGui::TreePop(); }; diff --git a/src/experimental/platform/ux/gui.h b/src/experimental/platform/ux/gui.h index 9a686edb..fe2a3a4c 100644 --- a/src/experimental/platform/ux/gui.h +++ b/src/experimental/platform/ux/gui.h @@ -73,9 +73,9 @@ ImVec4 ConfigureDockingLayout(bool show_toolbar = true, bool show_status_bar = f // logarithmically spaced real-time slow-down coefficients (percent) // clang-format off static constexpr std::array kPercentRealTime = { -"100.0 ", " 80.0 ", " 66.0 ", " 50.0 ", " 40.0 ", " 33.0 ", " 25.0 ", " 20.0 ", " 16.0 ", " 13.0 ", -" 10.0 ", " 8.0 ", " 6.6 ", " 5.0 ", " 4.0 ", " 3.3 ", " 2.5 ", " 2.0 ", " 1.6 ", " 1.3 ", -" 1.0 ", " 0.8 ", " 0.7 ", " 0.5 ", " 0.4 ", " 0.33", " 0.25", " 0.2 ", " 0.16", " 0.13", +"100.0 ", " 80.0 ", " 63.0 ", " 50.0 ", " 40.0 ", " 32.0 ", " 25.0 ", " 20.0 ", " 16.0 ", " 13.0 ", +" 10.0 ", " 8.0 ", " 6.3 ", " 5.0 ", " 4.0 ", " 3.2 ", " 2.5 ", " 2.0 ", " 1.6 ", " 1.3 ", +" 1.0 ", " 0.8 ", " 0.63", " 0.5 ", " 0.4 ", " 0.32", " 0.25", " 0.2 ", " 0.16", " 0.13", " 0.1 ", }; // clang-format on diff --git a/src/experimental/platform/ux/imgui_widgets.h b/src/experimental/platform/ux/imgui_widgets.h index ed398308..42a82b09 100644 --- a/src/experimental/platform/ux/imgui_widgets.h +++ b/src/experimental/platform/ux/imgui_widgets.h @@ -30,6 +30,7 @@ #include #include #include "experimental/platform/ux/enum_utils.h" +#include "experimental/platform/ux/well_tempered.h" namespace mujoco::platform { @@ -551,6 +552,8 @@ struct ImGuiOpts { std::optional step_fast; std::optional width; const char* format = std::is_floating_point_v ? "%.3g" : "%d"; + // ImGui_LogStepper only: the ladder's bottom rung is 0 below this value. + std::optional zero_below; }; // A compile-time wrapper around ImGui::InputScalarN. This is useful because @@ -606,6 +609,56 @@ bool ImGui_Input(const char* name, T* value, ImGuiOpts opts = {}) { return ImGui_InputN(name, value, 1, opts); } +// A numeric input whose - / + buttons step through a "well-tempered" logarithmic +// ladder (see WellTemperedStep): round numbers spaced evenly in log space that +// close each decade on a power of ten. Typed edits are applied on enter or focus +// change, like ImGui_Input. Suited to quantities spanning several orders of +// magnitude, such as the simulation timestep. The buttons auto-repeat when held. +template +bool ImGui_LogStepper(const char* name, T* value, ImGuiOpts opts = {}) { + const float button = ImGui::GetFrameHeight(); + const float spacing = ImGui::GetStyle().ItemInnerSpacing.x; + float input_width = ImGui::CalcItemWidth() - 2 * (button + spacing); + if (input_width < 1) input_width = 1; + + const double zero_below = + opts.zero_below ? static_cast(*opts.zero_below) : 0.0; + auto clamp = [&](T v) { + if (opts.min && v < *opts.min) v = *opts.min; + if (opts.max && v > *opts.max) v = *opts.max; + return v; + }; + + bool changed = false; + ImGui::PushID(name); + + // Lay out like ImGui's own InputScalar stepper: the field, then the - / + + // buttons to its right, then the label. + ImGuiOpts field = opts; + field.step = std::nullopt; // suppress ImGui's own linear step buttons + field.step_fast = std::nullopt; + field.width = input_width; + if (ImGui_InputN("##value", value, 1, field)) changed = true; + + ImGui::PushButtonRepeat(true); + ImGui::SameLine(0, spacing); + if (ImGui::Button("-", ImVec2(button, button))) { + *value = clamp(static_cast(WellTemperedStep(*value, -1, zero_below))); + changed = true; + } + ImGui::SameLine(0, spacing); + if (ImGui::Button("+", ImVec2(button, button))) { + *value = clamp(static_cast(WellTemperedStep(*value, +1, zero_below))); + changed = true; + } + ImGui::PopButtonRepeat(); + + ImGui::SameLine(0, spacing); + ImGui::TextUnformatted(name); + ImGui::PopID(); + return changed; +} + // Returns true if the given chord is has _just_ been pressed in this frame. // (This is opposed to "Pressed" which means the chord is active, i.e. the user // is holding down the keys.) diff --git a/src/experimental/platform/ux/well_tempered.h b/src/experimental/platform/ux/well_tempered.h new file mode 100644 index 00000000..a1f103a1 --- /dev/null +++ b/src/experimental/platform/ux/well_tempered.h @@ -0,0 +1,76 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_UX_WELL_TEMPERED_H_ +#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_UX_WELL_TEMPERED_H_ + +#include +#include + +namespace mujoco::platform { + +// Steps `value` to the next value in a "well-tempered" logarithmic ladder, in +// the given direction (+1 up, -1 down). The ladder is spaced roughly evenly in +// log space -- like clicks of a slowdown control -- but each rung is snapped to +// a round number so that ten steps multiply by exactly ten, closing the decade +// on a power of ten: +// +// ... 1, 1.3, 1.6, 2, 2.5, 3.2, 4, 5, 6.3, 8, 10, ... +// +// The step always lands strictly on the far side of `value`, so a value between +// two rungs snaps onto the ladder. +// +// `zero_below` gives the ladder a bottom rung of exactly zero: any value below +// it collapses to 0, so stepping down snaps to 0 and stepping up off 0 lands on +// `zero_below`. This lets quantities for which zero is meaningful (e.g. a solver +// tolerance) reach it. Quantities that never want zero leave `zero_below` at 0, +// in which case a non-positive value (or a zero direction) is returned +// unchanged. +inline double WellTemperedStep(double value, int direction, + double zero_below = 0.0) { + static constexpr std::array kLadder = { + 1.0, 1.3, 1.6, 2.0, 2.5, 3.2, 4.0, 5.0, 6.3, 8.0}; + if (direction == 0) { + return value; + } + if (zero_below > 0 && value < zero_below) { + return direction > 0 ? zero_below : 0.0; + } + if (!(value > 0)) { + return value; + } + const double decade = std::floor(std::log10(value) + 1e-9); + const double scale = std::pow(10.0, decade); + const double mantissa = value / scale; // in [1, 10) + if (direction > 0) { + for (double rung : kLadder) { + if (rung > mantissa * (1.0 + 1e-6)) { + return rung * scale; + } + } + return 10.0 * scale; // carry up: 1.0 in the next decade + } + double next = 0.8 * scale; // carry down: 8.0 in the previous decade + for (int i = kLadder.size() - 1; i >= 0; --i) { + if (kLadder[i] < mantissa * (1.0 - 1e-6)) { + next = kLadder[i] * scale; + break; + } + } + return (zero_below > 0 && next < zero_below) ? 0.0 : next; +} + +} // namespace mujoco::platform + +#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_UX_WELL_TEMPERED_H_