Replace the flex metric factorization with a block preconditioner

Every step, the flex block of the implicit effective metric M + K was
factorized by sparse Cholesky, because K depends on the configuration. On
model/flex/bag.xml, added here, that is roughly half the step, against a
comparable share for the constraint solve it exists to accelerate.

Keep only the metric's per-vertex 3x3 diagonal blocks, prefactored. Neither
consumer needs the exact inverse: the CG constraint solver only wants a
preconditioner, and qacc_smooth can come from an iterative solve using those
blocks. They are O(n) to build and to apply, but weaker, so CG runs about twice
the iterations and qacc_smooth becomes an iteration rather than a direct solve.
Net, the bag model steps roughly twice as fast.

The preconditioner, by metric state. Inactive, meaning no flex elasticity or an
explicit integrator: M^-1, unchanged. Bending only (nefmK == 0): M^-1 plus the
exact constant bending factor from mj_setConst on the dofs it covers,
unchanged; that factor is built at model compile time and costs nothing per
step. Per-step stiffness: M^-1 plus the 3x3 blocks, where before it was a
per-step sparse Cholesky, or, when M couples across the flex block, an inner
PCG of up to 50 iterations run once per outer CG iteration.

Only models carrying per-step stretch stiffness change in wall-clock. Both
ponchos hold their timing and take slightly fewer CG iterations than before,
because the preconditioner is now symmetric: it applies M^-1 and the covered
blocks to disjoint sets of dofs, where previously the two overlapped and the
operator was not symmetric, which PCG requires.

mjd_effSolve is the accurate solve of (M + K)x = b; what used to carry that
name only preconditions and is now mjd_effPrec. Its CG guarded the division by
pAp with mjMINVAL, an absolute floor on a quantity that scales with the square
of the right-hand side, so a small b aborted the solve while the curvature was
healthy: four flex models were quietly left short of tolerance. For an SPD
metric the guard is positivity, and with that the same solves converge. The qacc_smooth call site in
mj_fwdAcceleration is textually unchanged but now reaches the iterative solve,
which converges on opt.tolerance rather than a hardcoded threshold, floored in
mjUSESINGLE builds where the squared target is unreachable in float. Reaching
the iteration cap names the ill-conditioned flex stiffness and then reports it
through mjWARN_INERTIA, rather than returning an under-converged result.
Covered dofs are located by walking the covered rows of the stiffness matrix,
as they need not be 3-aligned from dof 0: any joint declared before a flexcomp
shifts them.

mjData.efm_L_rownnz, efm_L_rowadr and efm_L_colind described the sparsity of
the deleted factorization and are removed: left NULL with nonzero mjxmacro
extents they made the Python bindings hand back uninitialized arrays.
efm_active loses the value 2 for the same reason, nothing selects a solve path
on preconditioner exactness any more. Both are recorded under breaking changes.

model/flex/bag.xml is added because no shipped model carried per-step stretch
stiffness. The ponchos are bending-only and trampoline.xml uses an explicit
integrator, so the metric never activates there. It is excluded from
WriteReadCompareTest: stretch stiffness amplifies rest geometry that XML rounds
on save.
This commit is contained in:
Alessio
2026-07-29 14:19:58 +01:00
parent d400914e97
commit 55d13aec5f
16 changed files with 4914 additions and 485 deletions
File diff suppressed because it is too large Load Diff
+74
View File
@@ -0,0 +1,74 @@
<!-- 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.
-->
<mujoco model="Bag">
<compiler angle="radian" meshdir="asset"/>
<option timestep="0.001" jacobian="sparse" integrator="implicitfast" solver="CG"
iterations="2000" tolerance="1e-4"/>
<statistic extent="2.5" center="0 0 1"/>
<visual>
<map force="0.1" zfar="30"/>
<global offwidth="2560" offheight="1440" elevation="-20" azimuth="120"/>
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0"
width="512" height="512"/>
<texture name="texplane" type="2d" builtin="checker" rgb1=".2 .3 .4" rgb2=".1 0.15 0.2"
width="512" height="512" mark="cross" markrgb=".8 .8 .8"/>
<material name="matplane" reflectance="0.3" texture="texplane" texrepeat="10 10"
texuniform="true"/>
<model name="humanoid" file="../humanoid/humanoid.xml"/>
</asset>
<worldbody>
<geom name="ground" type="plane" size="0 0 1" material="matplane" condim="1"/>
<!-- Both lights are directional. A positional light inside the bag's closed volume (as in
model/flex/scene.xml, which puts one at z=2) blacks out the cloth around it. -->
<light directional="true" diffuse=".7 .7 .7" specular=".1 .1 .1" pos="0 0 4" dir="0 0 -1"/>
<light directional="true" diffuse=".4 .4 .4" specular=".1 .1 .1" pos="3 -3 4" dir="-.6 .6 -.6"/>
<!-- The bag, scaled up so the humanoid fits and held open by pinning the ring of vertices
around its mouth. It hangs well above the floor, so the cloth catches the humanoid. -->
<body name="bag_container" euler="1.57 0 0" pos="0 0 1.5">
<flexcomp name="bag" type="mesh" file="bag.obj" dim="2" scale="3.0 3.09 3.0"
radius="0.003" mass="5" rgba="0.6 0.6 0.62 0.6">
<elasticity young="3e6" poisson="0.3" thickness="0.002" elastic2d="stretch" damping="5e-3"/>
<pin id="193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
213 214 215 216 239 240 241 242 243 244 245 246 247 248 249 250
251 252 253 254 255 256 257 258 259 260 267 268 269 270 271 272
645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
661 662 663 664 665 666 667 668 671 672 673 675 677 678 679 682
687 688 689 690 711 712 713 714 715 716 717 718 719 720 721 722
723 724 725 726 727 728 729 730 755 756 758 761 762 764 766 770
772 773 803 804 805 806 813 815 819 820 821 822 1003 1005 1006
1009 1010 1013 1014 1015 1017 1019 1021 1024 1025 1051 1052
1054 1056 1059 1061 1063 1065 1066 1069 1070"/>
<!-- priority=1: the flex parameters win outright instead of averaging with the
humanoid's softer solref/solimp, which let limbs sink ~35mm into the cloth. -->
<contact selfcollide="none" contype="1" conaffinity="1" priority="1"
solref="0.004 1" solimp="0.99 0.999 0.001"/>
</flexcomp>
</body>
<!-- The humanoid, dropped above the bag's mouth; its torso sits at z=1.282 in its
own model. It is the stock 40kg humanoid, hence the heavy, stiff cloth. -->
<frame pos="0 0 1.618">
<attach model="humanoid" body="torso" prefix="humanoid_"/>
</frame>
</worldbody>
</mujoco>