Do not update qacc_warmstart at the end of the solver call; instead, update it at the same time as all other state variables. This change makes mj_forward idempotent.
See changelog herein for motivation and discussion. PiperOrigin-RevId: 797392841 Change-Id: If50e9f09e1d07e8363169d3589c32097aeca2432
This commit is contained in:
committed by
Copybara-Service
parent
bf04103b6f
commit
c7a82c32dc
+23
-2
@@ -7,8 +7,29 @@ Upcoming version (not yet released)
|
|||||||
|
|
||||||
General
|
General
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
.. admonition:: Breaking API changes
|
||||||
|
:class: attention
|
||||||
|
|
||||||
|
- The update of ``mjData.qacc_warmstart`` was moved from the end of the solver call (:ref:`mj_fwdConstraint`) to the
|
||||||
|
end of :ref:`mj_step`, and is now updated with all other state variables. This change makes :ref:`mj_forward`
|
||||||
|
fully idempotent.
|
||||||
|
|
||||||
|
Before this change, calling :ref:`mj_forward` repeatedly would make the constraint solver converge,
|
||||||
|
since each subsequent call would start from the previously updated ``qacc_warmstart`` value.
|
||||||
|
Indeed, this is precisely what happened in the viewer, which calls :ref:`mj_forward` repeatedly in PAUSE mode.
|
||||||
|
|
||||||
|
**Migration:** If your code depended on this behavior, you can recover it by updating manually after each
|
||||||
|
:ref:`mj_forward`: ``qacc_warmstart ← qacc``. The behavior is available in :ref:`simulate<saSimulate>` by
|
||||||
|
clicking the "Pause update" toggle (off by default).
|
||||||
|
|
||||||
|
Furthermore, this change has a numerical impact on the output of the :ref:`RK4 <geIntegrators>` integrator. Before
|
||||||
|
this change, due to the ``qacc_warmstart`` update occurring after each of the four Runge-Kutta substeps, the solver
|
||||||
|
convergence of RK4 was faster, at the cost of unprincipled integration. This change makes the RK4 integration
|
||||||
|
principled and well-defined. Since this change to RK4 is effectively a bug fix, migration to the previous behavior
|
||||||
|
is not provided.
|
||||||
|
|
||||||
- Added support for shells with a curved reference configuration. See this `example
|
- Added support for shells with a curved reference configuration. See this `example
|
||||||
<https://github.com/google-deepmind/mujoco/blob/main/model/flex/basket.xml>`__
|
<https://github.com/google-deepmind/mujoco/blob/main/model/flex/basket.xml>`__.
|
||||||
|
|
||||||
MJX
|
MJX
|
||||||
^^^
|
^^^
|
||||||
@@ -21,7 +42,7 @@ MJX
|
|||||||
|
|
||||||
|
|
||||||
Version 3.3.5 (August 8, 2025)
|
Version 3.3.5 (August 8, 2025)
|
||||||
-----------------------------------
|
------------------------------
|
||||||
|
|
||||||
General
|
General
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|||||||
@@ -78,11 +78,7 @@ class SolverTest(parameterized.TestCase):
|
|||||||
mjx_cost = ctx.cost - ctx.gauss
|
mjx_cost = ctx.cost - ctx.gauss
|
||||||
_assert_eq(mj_cost, mjx_cost, 'cost')
|
_assert_eq(mj_cost, mjx_cost, 'cost')
|
||||||
|
|
||||||
# mj_forward overwrites qacc_warmstart, so let's restore it to what it was
|
|
||||||
# before the step so that MJX does not have a trivial solution
|
|
||||||
warmstart = d.qacc_warmstart.copy()
|
|
||||||
mujoco.mj_forward(m, d)
|
mujoco.mj_forward(m, d)
|
||||||
d.qacc_warmstart = warmstart
|
|
||||||
dx = jax.jit(mjx.solve)(mjx.put_model(m), mjx.put_data(m, d))
|
dx = jax.jit(mjx.solve)(mjx.put_model(m), mjx.put_data(m, d))
|
||||||
|
|
||||||
# MJX finds very similar solutions with the newton solver
|
# MJX finds very similar solutions with the newton solver
|
||||||
@@ -120,11 +116,7 @@ class SolverTest(parameterized.TestCase):
|
|||||||
# significant constraint forces keyframe 2
|
# significant constraint forces keyframe 2
|
||||||
mujoco.mj_resetDataKeyframe(m, d, 2)
|
mujoco.mj_resetDataKeyframe(m, d, 2)
|
||||||
|
|
||||||
# mj_forward overwrites qacc_warmstart, so let's restore it to what it was
|
|
||||||
# at the beginning of the step so that MJX does not have a trivial solution
|
|
||||||
warmstart = d.qacc_warmstart.copy()
|
|
||||||
mujoco.mj_forward(m, d)
|
mujoco.mj_forward(m, d)
|
||||||
d.qacc_warmstart = warmstart
|
|
||||||
|
|
||||||
dx = jax.jit(mjx.solve)(mjx.put_model(m), mjx.put_data(m, d))
|
dx = jax.jit(mjx.solve)(mjx.put_model(m), mjx.put_data(m, d))
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ def fixture(
|
|||||||
mjd.mocap_quat = mocap_quat
|
mjd.mocap_quat = mocap_quat
|
||||||
|
|
||||||
mujoco.mj_forward(mjm, mjd)
|
mujoco.mj_forward(mjm, mjd)
|
||||||
|
mjd.qacc_warmstart = mjd.qacc
|
||||||
m = io.put_model(mjm)
|
m = io.put_model(mjm)
|
||||||
if ls_parallel is not None:
|
if ls_parallel is not None:
|
||||||
m.opt.ls_parallel = ls_parallel
|
m.opt.ls_parallel = ls_parallel
|
||||||
|
|||||||
@@ -192,14 +192,6 @@ class ForwardTest(parameterized.TestCase):
|
|||||||
tu.assert_attr_eq(dx, d, 'qfrc_smooth')
|
tu.assert_attr_eq(dx, d, 'qfrc_smooth')
|
||||||
tu.assert_attr_eq(dx, d, 'qacc_smooth')
|
tu.assert_attr_eq(dx, d, 'qacc_smooth')
|
||||||
|
|
||||||
# solve
|
|
||||||
np.testing.assert_allclose(
|
|
||||||
dx.qacc_warmstart,
|
|
||||||
d.qacc_warmstart,
|
|
||||||
err_msg='qacc_warmstart',
|
|
||||||
rtol=1e-5,
|
|
||||||
atol=1.0,
|
|
||||||
)
|
|
||||||
np.testing.assert_allclose(
|
np.testing.assert_allclose(
|
||||||
dx.qacc, d.qacc, err_msg='qacc', rtol=1e-5, atol=1.0
|
dx.qacc, d.qacc, err_msg='qacc', rtol=1e-5, atol=1.0
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-1
@@ -285,7 +285,7 @@ mjModel* LoadModel(const char* file, mj::Simulate& sim) {
|
|||||||
|
|
||||||
// simulate in background thread (while rendering in main thread)
|
// simulate in background thread (while rendering in main thread)
|
||||||
void PhysicsLoop(mj::Simulate& sim) {
|
void PhysicsLoop(mj::Simulate& sim) {
|
||||||
// cpu-sim syncronization point
|
// cpu-sim synchronization point
|
||||||
std::chrono::time_point<mj::Simulate::Clock> syncCPU;
|
std::chrono::time_point<mj::Simulate::Clock> syncCPU;
|
||||||
mjtNum syncSim = 0;
|
mjtNum syncSim = 0;
|
||||||
|
|
||||||
@@ -438,6 +438,9 @@ void PhysicsLoop(mj::Simulate& sim) {
|
|||||||
else {
|
else {
|
||||||
// run mj_forward, to update rendering and joint sliders
|
// run mj_forward, to update rendering and joint sliders
|
||||||
mj_forward(m, d);
|
mj_forward(m, d);
|
||||||
|
if (sim.pause_update) {
|
||||||
|
mju_copy(d->qacc_warmstart, d->qacc, m->nv);
|
||||||
|
}
|
||||||
sim.speed_changed = true;
|
sim.speed_changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1922,8 +1922,8 @@ void Simulate::Sync(bool state_only) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool update_profiler = this->profiler && (this->pause_update || this->run);
|
bool update_profiler = this->profiler;
|
||||||
bool update_sensor = this->sensor && (this->pause_update || this->run);
|
bool update_sensor = this->sensor;
|
||||||
|
|
||||||
for (int i = 0; i < m_->njnt; ++i) {
|
for (int i = 0; i < m_->njnt; ++i) {
|
||||||
std::optional<std::pair<mjtNum, mjtNum>> range;
|
std::optional<std::pair<mjtNum, mjtNum>> range;
|
||||||
|
|||||||
+1
-1
@@ -193,7 +193,7 @@ class Simulate {
|
|||||||
int info = 0;
|
int info = 0;
|
||||||
int profiler = 0;
|
int profiler = 0;
|
||||||
int sensor = 0;
|
int sensor = 0;
|
||||||
int pause_update = 1;
|
int pause_update = 0;
|
||||||
int fullscreen = 0;
|
int fullscreen = 0;
|
||||||
int vsync = 1;
|
int vsync = 1;
|
||||||
int busywait = 0;
|
int busywait = 0;
|
||||||
|
|||||||
@@ -709,7 +709,6 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) {
|
|||||||
// no constraints: copy unconstrained acc, clear forces, return
|
// no constraints: copy unconstrained acc, clear forces, return
|
||||||
if (!nefc) {
|
if (!nefc) {
|
||||||
mju_copy(d->qacc, d->qacc_smooth, nv);
|
mju_copy(d->qacc, d->qacc_smooth, nv);
|
||||||
mju_copy(d->qacc_warmstart, d->qacc_smooth, nv);
|
|
||||||
mju_zeroInt(d->solver_niter, mjNISLAND);
|
mju_zeroInt(d->solver_niter, mjNISLAND);
|
||||||
TM_END(mjTIMER_CONSTRAINT);
|
TM_END(mjTIMER_CONSTRAINT);
|
||||||
return;
|
return;
|
||||||
@@ -783,9 +782,6 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// save result for next step warmstart
|
|
||||||
mju_copy(d->qacc_warmstart, d->qacc, nv);
|
|
||||||
|
|
||||||
// run noslip solver if enabled
|
// run noslip solver if enabled
|
||||||
if (m->opt.noslip_iterations > 0) {
|
if (m->opt.noslip_iterations > 0) {
|
||||||
mj_solNoSlip(m, d, m->opt.noslip_iterations);
|
mj_solNoSlip(m, d, m->opt.noslip_iterations);
|
||||||
@@ -837,6 +833,9 @@ static void mj_advance(const mjModel* m, mjData* d,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// save qacc for next step warmstart
|
||||||
|
mju_copy(d->qacc_warmstart, d->qacc, m->nv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Euler integrator, semi-implicit in velocity, possibly skipping factorisation
|
// Euler integrator, semi-implicit in velocity, possibly skipping factorisation
|
||||||
|
|||||||
Reference in New Issue
Block a user