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_