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
This commit is contained in:
committed by
Copybara-Service
parent
ba3f8cf180
commit
90ac80707d
+3
-3
@@ -239,9 +239,9 @@ class Simulate {
|
|||||||
float measured_slowdown = 1.0;
|
float measured_slowdown = 1.0;
|
||||||
// logarithmically spaced real-time slow-down coefficients (percent)
|
// logarithmically spaced real-time slow-down coefficients (percent)
|
||||||
static constexpr float percentRealTime[] = {
|
static constexpr float percentRealTime[] = {
|
||||||
100, 80, 66, 50, 40, 33, 25, 20, 16, 13,
|
100, 80, 63, 50, 40, 32, 25, 20, 16, 13,
|
||||||
10, 8, 6.6, 5.0, 4, 3.3, 2.5, 2, 1.6, 1.3,
|
10, 8, 6.3, 5.0, 4, 3.2, 2.5, 2, 1.6, 1.3,
|
||||||
1, .8, .66, .5, .4, .33, .25, .2, .16, .13,
|
1, .8, .63, .5, .4, .32, .25, .2, .16, .13,
|
||||||
.1
|
.1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ target_sources(${MUJOCO_PLATFORM_TARGET_NAME}
|
|||||||
ux/plugin.h
|
ux/plugin.h
|
||||||
ux/spec_editor.cc
|
ux/spec_editor.cc
|
||||||
ux/spec_editor.h
|
ux/spec_editor.h
|
||||||
|
ux/well_tempered.h
|
||||||
)
|
)
|
||||||
|
|
||||||
if(NOT WIN32)
|
if(NOT WIN32)
|
||||||
|
|||||||
@@ -1007,16 +1007,16 @@ void PhysicsGui(mjModel* model, float min_width) {
|
|||||||
ImGui::Combo("Solver", &opt.solver, opts3, IM_ARRAYSIZE(opts3));
|
ImGui::Combo("Solver", &opt.solver, opts3, IM_ARRAYSIZE(opts3));
|
||||||
|
|
||||||
if (SectionHeader("Algorithmic Parameters", ImGuiTreeNodeFlags_DefaultOpen)) {
|
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("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 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 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 Iter", &opt.ccd_iterations, {0, 1000, 1, 100});
|
||||||
ImGui_Input("CCD Tol", &opt.ccd_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_Input("Sleep Tol", &opt.sleep_tolerance, {0, 1, 0.01, 0.1});
|
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 Iter", &opt.sdf_iterations, {1, 20, 1, 10});
|
||||||
ImGui_Input("SDF Init", &opt.sdf_initpoints, {1, 100, 1, 10});
|
ImGui_Input("SDF Init", &opt.sdf_initpoints, {1, 100, 1, 10});
|
||||||
ImGui::TreePop();
|
ImGui::TreePop();
|
||||||
@@ -1052,7 +1052,7 @@ void PhysicsGui(mjModel* model, float min_width) {
|
|||||||
ImGui_InputN("Magnetic", opt.magnetic, 3);
|
ImGui_InputN("Magnetic", opt.magnetic, 3);
|
||||||
ImGui_Input("Density", &opt.density, {.min = 0.0});
|
ImGui_Input("Density", &opt.density, {.min = 0.0});
|
||||||
ImGui_Input("Viscosity", &opt.viscosity, {.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();
|
ImGui::TreePop();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ ImVec4 ConfigureDockingLayout(bool show_toolbar = true, bool show_status_bar = f
|
|||||||
// logarithmically spaced real-time slow-down coefficients (percent)
|
// logarithmically spaced real-time slow-down coefficients (percent)
|
||||||
// clang-format off
|
// clang-format off
|
||||||
static constexpr std::array<const char*, 31> kPercentRealTime = {
|
static constexpr std::array<const char*, 31> kPercentRealTime = {
|
||||||
"100.0 ", " 80.0 ", " 66.0 ", " 50.0 ", " 40.0 ", " 33.0 ", " 25.0 ", " 20.0 ", " 16.0 ", " 13.0 ",
|
"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.6 ", " 5.0 ", " 4.0 ", " 3.3 ", " 2.5 ", " 2.0 ", " 1.6 ", " 1.3 ",
|
" 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.7 ", " 0.5 ", " 0.4 ", " 0.33", " 0.25", " 0.2 ", " 0.16", " 0.13",
|
" 1.0 ", " 0.8 ", " 0.63", " 0.5 ", " 0.4 ", " 0.32", " 0.25", " 0.2 ", " 0.16", " 0.13",
|
||||||
" 0.1 ",
|
" 0.1 ",
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
#include <implot.h>
|
#include <implot.h>
|
||||||
#include <mujoco/mujoco.h>
|
#include <mujoco/mujoco.h>
|
||||||
#include "experimental/platform/ux/enum_utils.h"
|
#include "experimental/platform/ux/enum_utils.h"
|
||||||
|
#include "experimental/platform/ux/well_tempered.h"
|
||||||
|
|
||||||
namespace mujoco::platform {
|
namespace mujoco::platform {
|
||||||
|
|
||||||
@@ -551,6 +552,8 @@ struct ImGuiOpts {
|
|||||||
std::optional<T> step_fast;
|
std::optional<T> step_fast;
|
||||||
std::optional<float> width;
|
std::optional<float> width;
|
||||||
const char* format = std::is_floating_point_v<T> ? "%.3g" : "%d";
|
const char* format = std::is_floating_point_v<T> ? "%.3g" : "%d";
|
||||||
|
// ImGui_LogStepper only: the ladder's bottom rung is 0 below this value.
|
||||||
|
std::optional<T> zero_below;
|
||||||
};
|
};
|
||||||
|
|
||||||
// A compile-time wrapper around ImGui::InputScalarN. This is useful because
|
// A compile-time wrapper around ImGui::InputScalarN. This is useful because
|
||||||
@@ -606,6 +609,56 @@ bool ImGui_Input(const char* name, T* value, ImGuiOpts<T> opts = {}) {
|
|||||||
return ImGui_InputN(name, value, 1, 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 <typename T>
|
||||||
|
bool ImGui_LogStepper(const char* name, T* value, ImGuiOpts<T> 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<double>(*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<T> 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<T>(WellTemperedStep(*value, -1, zero_below)));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0, spacing);
|
||||||
|
if (ImGui::Button("+", ImVec2(button, button))) {
|
||||||
|
*value = clamp(static_cast<T>(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.
|
// 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
|
// (This is opposed to "Pressed" which means the chord is active, i.e. the user
|
||||||
// is holding down the keys.)
|
// is holding down the keys.)
|
||||||
|
|||||||
@@ -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 <array>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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<double, 10> 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_
|
||||||
Reference in New Issue
Block a user