Add body/simple attribute to control simple body optimization.

PiperOrigin-RevId: 942164766
Change-Id: I1e83bb99b6a1955917eb9b2e9bed0dc7b8e6a161
This commit is contained in:
Yuval Tassa
2026-07-03 09:08:42 -07:00
committed by Copybara-Service
parent 4ecfe5c1f3
commit 5618666a7d
19 changed files with 205 additions and 10 deletions
+25
View File
@@ -2152,6 +2152,31 @@ defined. Its body name is automatically defined as "world".
See :ref:`implementation notes<siSleep>` for more details.
.. _body-simple:
:at:`simple`: :at-val:`[false, auto], "auto"`
Controls the *simple body* optimization. When a body qualifies as "simple", its inertial matrix block in the mass
matrix is diagonal, representing independent translational and rotational degrees of freedom. The optimization
omits storing of the zero-valued off-diagonal entries, reducing memory footprint and computation.
A body qualifies for this optimization if it satisfies all of the following:
- **Inertial frame alignment**: The body's inertial frame coincides with its body frame.
- **Kinematic root**: The body's parent is either the world body or a static body.
- **Leaf body**: The body is a leaf node in the kinematic tree (it has no child bodies).
- **Origin-centered joints**: All joints belonging to this body must reside at the body's origin.
- **Aligned joint axes**: Any hinge or slide joint axes must be aligned with the local coordinate axes, and at most
one joint with rotational degrees of freedom (hinge or ball) is permitted.
- **No inertia-bearing tendons**: The body must not contain sites or geoms used as wrap objects by any tendon that
has non-zero :ref:`armature<tendon-spatial-armature>`.
Setting this attribute to :at-val:`false` disables the optimization for this body. This is necessary for domain
randomization workflows where model parameters (such as joint/inertial offsets or angles) are perturbed dynamically
during simulation and updated via :ref:`mj_setConst`. Because a body compiled with the simple optimization active
cannot dynamically lose its simple state at runtime (which would require reallocation of sparse matrix structures),
any runtime parameter change that violates the simple conditions will trigger a validation error unless
``simple="false"`` was explicitly declared in the XML.
.. _body-user:
:at:`user`: :at-val:`real(nbody_user), "0 0 ..."`
+3
View File
@@ -713,6 +713,9 @@
.. grid-item::
:ref:`sleep<body-sleep>`
.. grid-item::
:ref:`simple<body-simple>`
.. grid-item::
:ref:`user<body-user>`
+2
View File
@@ -12,6 +12,8 @@ General
- :ref:`mj_encode` now supports encoding of MJB and TXT files.
- :ref:`mj_setConst` now recomputes the ``mjModel.{body,geom,site}_sameframe`` flags, to account for changes in
body/geom/site frames after compilation.
- Added :ref:`body/simple<body-simple>` attribute ("false"/"auto") to disable the *simple body* mass matrix
optimization. This is useful for domain randomization, where model parameters may change post-compilation.
- The :el:`attach` element now supports self-attachment (attaching elements of the current model to itself) by omitting
the :at:`model` attribute. It also supports attaching a frame via the new :at:`frame` attribute, which is mutually
exclusive with :at:`body`.
+1
View File
@@ -1661,6 +1661,7 @@ typedef struct mjsBody_ { // body specification
mjtByte mocap; // is this a mocap body
double gravcomp; // gravity compensation
mjtSleepPolicy sleep; // sleep policy
mjtByte simple; // simple body optimization (0: false, 1: auto)
mjDoubleVec* userdata; // user data
mjtByte explicitinertial; // whether to save the body with explicit inertial clause
mjsPlugin plugin; // passive force plugin
+1
View File
@@ -279,6 +279,7 @@ typedef struct mjsBody_ { // body specification
mjtByte mocap; // is this a mocap body
double gravcomp; // gravity compensation
mjtSleepPolicy sleep; // sleep policy
mjtByte simple; // simple body optimization (0: false, 1: auto)
mjDoubleVec* userdata; // user data
mjtByte explicitinertial; // whether to save the body with explicit inertial clause
mjsPlugin plugin; // passive force plugin
+1
View File
@@ -114,6 +114,7 @@
X ( mjtByte, mocap, 1 ) \
X ( double, gravcomp, 1 ) \
X ( mjtSleepPolicy, sleep, 1 ) \
X ( mjtByte, simple, 1 ) \
X ( mjDoubleVec*, userdata, 1 ) \
X ( mjtByte, explicitinertial, 1 ) \
X ( mjsPlugin, plugin, 1 ) \
+5
View File
@@ -7391,6 +7391,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([
type=ValueType(name='mjtSleepPolicy'),
doc='sleep policy',
),
StructFieldDecl(
name='simple',
type=ValueType(name='mjtByte'),
doc='simple body optimization (0: false, 1: auto)',
),
StructFieldDecl(
name='userdata',
type=PointerType(
+8
View File
@@ -1324,6 +1324,14 @@ void mj_setConst(mjModel* m, mjData* d) {
// recompute sameframe flags from current model geometry
setSameframe(m);
// error if simple body lost sameframe (user must set simple="false")
for (int i = 1; i < m->nbody; i++) {
if (m->body_simple[i] > 0 && m->body_sameframe[i] != mjSAMEFRAME_BODY) {
mjERROR("body %d is compiled as simple but sameframe no longer holds, "
"use body/simple='false'", i);
}
}
// set fixed quantities
setFixed(m, d);
+3
View File
@@ -82,6 +82,9 @@ void mjs_defaultBody(mjsBody* body) {
body->ipos[0] = mjNAN;
body->iquat[0] = 1;
body->fullinertia[0] = mjNAN;
// simple optimization: auto
body->simple = 1;
}
+10
View File
@@ -1002,6 +1002,11 @@ void mjCModel::ComputeSparseSizes() {
bodies_[parentid]->parent &&
bodies_[parentid]->parent->id == 0 &&
bodies_[parentid]->dofnum == 0)));
// user override: disable simple optimization
if (!pb->simple) {
body_simple_pre[i] = 0;
}
}
// a parent body is never simple (unless world)
@@ -2816,6 +2821,11 @@ void mjCModel::CopyTree(mjModel* m) {
(m->body_parentid[parentid] == 0 &&
m->body_dofnum[parentid] == 0)));
// user override: disable simple optimization
if (!pb->simple) {
m->body_simple[i] = 0;
}
// a parent body is never simple (unless world)
if (m->body_parentid[i] > 0) {
m->body_simple[m->body_parentid[i]] = 0;
+1
View File
@@ -54,6 +54,7 @@ extern const mjMap enable_map[];
extern const mjMap bool_map[];
extern const mjMap fluid_map[];
extern const mjMap TFAuto_map[];
extern const mjMap FAuto_map[];
extern const mjMap joint_map[];
extern const mjMap bodysleep_map[];
extern const mjMap geom_map[];
+12 -3
View File
@@ -335,9 +335,8 @@ std::vector<const char*> MJCF[nMJCF] = {
{">"},
{"model", "*", "name", "file", "content_type"},
{">"},
{"body", "R", "name", "childclass", "pos", "quat", "mocap",
"axisangle", "xyaxes", "zaxis", "euler", "gravcomp", "sleep", "user"},
{"body", "R", "name", "childclass", "pos", "quat", "mocap", "axisangle",
"xyaxes", "zaxis", "euler", "gravcomp", "sleep", "simple", "user"},
{"<"},
{"inertial", "?", "pos", "quat", "mass", "diaginertia",
"axisangle", "xyaxes", "zaxis", "euler", "fullinertia"},
@@ -644,6 +643,13 @@ const mjMap TFAuto_map[3] = {
};
// FAuto type
const mjMap FAuto_map[2] = {
{"false", 0},
{"auto", 1}
};
// body sleep type
const int bodysleep_sz = 4;
const mjMap bodysleep_map[bodysleep_sz] = {
@@ -3926,6 +3932,9 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame,
if (MapValue(elem, "sleep", &n, bodysleep_map, bodysleep_sz)) {
child->sleep = (mjtSleepPolicy) n;
}
if (MapValue(elem, "simple", &n, FAuto_map, 2)) {
child->simple = (mjtByte) n;
}
// read userdata
std::vector<double> userdata;
+3
View File
@@ -1726,6 +1726,9 @@ void mjXWriter::Body(XMLElement* elem, mjCBody* body, mjCFrame* frame, string_vi
WriteAttrKey(elem, "sleep", bodysleep_map, bodysleep_sz, body->sleep);
}
// simple optimization
WriteAttrKey(elem, "simple", FAuto_map, 2, body->simple, 1);
// userdata
WriteVector(elem, "user", body->get_userdata());
+37 -6
View File
@@ -329,10 +329,12 @@ TEST_F(SetConstTest, TendonTreeId) {
int t_static_id = mj_name2id(model.get(), mjOBJ_TENDON, "T_static");
int t_tree1_id = mj_name2id(model.get(), mjOBJ_TENDON, "T_tree1");
int t_intertree12_id = mj_name2id(model.get(), mjOBJ_TENDON, "T_intertree12");
int t_intertree123_id = mj_name2id(model.get(), mjOBJ_TENDON, "T_intertree123");
int b1_1_treeid = model->body_treeid[mj_name2id(model.get(), mjOBJ_BODY, "B1_1")];
int b2_1_treeid = model->body_treeid[mj_name2id(model.get(), mjOBJ_BODY, "B2_1")];
int t_intertree123_id =
mj_name2id(model.get(), mjOBJ_TENDON, "T_intertree123");
int b1_1_treeid =
model->body_treeid[mj_name2id(model.get(), mjOBJ_BODY, "B1_1")];
int b2_1_treeid =
model->body_treeid[mj_name2id(model.get(), mjOBJ_BODY, "B2_1")];
// Tendon 1: Not associated with any tree
EXPECT_EQ(model->tendon_treenum[t_static_id], 0);
@@ -461,7 +463,7 @@ TEST_F(SetConstTest, BodySameframeRecomputed) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="B1">
<body name="B1" simple="false">
<joint type="slide"/>
<geom size=".1"/>
</body>
@@ -570,7 +572,7 @@ TEST_F(SetConstTest, SameframeKinematicsCorrect) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="B1" pos="1 0 0">
<body name="B1" pos="1 0 0" simple="false">
<joint type="slide" axis="1 0 0"/>
<geom name="G1" size=".1"/>
<site name="S1"/>
@@ -604,5 +606,34 @@ TEST_F(SetConstTest, SameframeKinematicsCorrect) {
EXPECT_NEAR(d->geom_xpos[3*g+2], 0.3, MjTol(1e-10, 1e-6));
}
TEST_F(SetConstTest, SimpleBodyLostSameframeError) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="B1">
<joint type="slide"/>
<geom size=".1"/>
</body>
</worldbody>
</mujoco>
)";
char error[1024];
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), NotNull()) << error;
MjDataPtr d(mj_makeData(m.get()));
int b = mj_name2id(m.get(), mjOBJ_BODY, "B1");
// confirm body is compiled as simple
EXPECT_GT(m->body_simple[b], 0);
// perturb body_ipos, breaking sameframe; calling mj_setConst should fail
m->body_ipos[3*b+0] = 1.0;
std::string err = MjuErrorMessageFrom(mj_setConst)(m.get(), d.get());
EXPECT_THAT(err, HasSubstr("body 1 is compiled as simple but "
"sameframe no longer holds"));
}
} // namespace
} // namespace mujoco
+1 -1
View File
@@ -140,7 +140,7 @@ CHECK_STRUCT(mjsDefault, MJSDEFAULT_FIELDS)
#undef XIMPL
TEST(MjspecmacroTest, CompileTimeChecks) {
// Verifies that the test binary compiled successfully and all static_asserts passed.
// Verifies that the test compiled successfully and all static_asserts passed.
EXPECT_TRUE(true);
}
+49
View File
@@ -1287,6 +1287,55 @@ TEST_F(MjCJointTest, AlignFree) {
mj_deleteSpec(s);
}
TEST_F(MjCJointTest, BodySimpleFalse) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="B1">
<joint type="slide"/>
<geom size=".1"/>
</body>
</worldbody>
</mujoco>
)";
constexpr char xml_nosimple[] = R"(
<mujoco>
<worldbody>
<body name="B1" simple="false">
<joint type="slide"/>
<geom size=".1"/>
</body>
</worldbody>
</mujoco>
)";
char error[1024];
// compile default model: body should be simple
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), NotNull()) << error;
int b = mj_name2id(m.get(), mjOBJ_BODY, "B1");
EXPECT_GT(m->body_simple[b], 0);
EXPECT_GT(m->dof_simplenum[0], 0);
// compile with simple="false": body should not be simple
MjModelPtr m_ns = LoadModelFromString(xml_nosimple, error, sizeof(error));
ASSERT_THAT(m_ns.get(), NotNull()) << error;
EXPECT_EQ(m_ns->body_simple[b], 0);
EXPECT_EQ(m_ns->dof_simplenum[0], 0);
// forward kinematics should produce identical results
MjDataPtr d = MakeData(m);
MjDataPtr d_ns = MakeData(m_ns);
d->qpos[0] = d_ns->qpos[0] = 0.5;
mj_forward(m.get(), d.get());
mj_forward(m_ns.get(), d_ns.get());
EXPECT_THAT(d_ns->xpos[3*b+0], MjNear(d->xpos[3*b+0], 1e-10, 1e-6));
EXPECT_THAT(d_ns->xpos[3*b+1], MjNear(d->xpos[3*b+1], 1e-10, 1e-6));
EXPECT_THAT(d_ns->xpos[3*b+2], MjNear(d->xpos[3*b+2], 1e-10, 1e-6));
}
// ------------- test height fields --------------------------------------------
using MjCHFieldTest = MujocoTest;
+36
View File
@@ -1590,5 +1590,41 @@ TEST_F(XMLWriterTest, WritesActuatorDelayHistory) {
EXPECT_THAT(saved_xml, HasSubstr("interp=\"cubic\""));
}
TEST_F(XMLWriterTest, BodySimpleRoundtrip) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="B1" simple="false">
<joint type="ball"/>
<geom size=".1"/>
</body>
</worldbody>
</mujoco>
)";
MjModelPtr model = LoadModelFromString(xml);
ASSERT_THAT(model.get(), NotNull());
std::string saved_xml = SaveAndReadXml(model.get());
EXPECT_THAT(saved_xml, HasSubstr("simple=\"false\""));
// auto should not be written
static constexpr char xml_auto[] = R"(
<mujoco>
<worldbody>
<body name="B1">
<joint type="ball"/>
<geom size=".1"/>
</body>
</worldbody>
</mujoco>
)";
MjModelPtr model_auto = LoadModelFromString(xml_auto);
ASSERT_THAT(model_auto.get(), NotNull());
std::string saved_auto = SaveAndReadXml(model_auto.get());
EXPECT_THAT(saved_auto, Not(HasSubstr("simple=")));
// nC should increase when simple is disabled
EXPECT_GT(model->nC, model_auto->nC);
}
} // namespace
} // namespace mujoco
+1
View File
@@ -5593,6 +5593,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.property("plugin", &MjsBody::plugin, reference())
.property("pos", &MjsBody::pos)
.property("quat", &MjsBody::quat)
.property("simple", &MjsBody::simple, &MjsBody::set_simple, reference())
.property("sleep", &MjsBody::sleep, &MjsBody::set_sleep, reference())
.property("userdata", &MjsBody::userdata, reference());
emscripten::class_<MjsCamera>("MjsCamera")
+6
View File
@@ -6087,6 +6087,12 @@ struct MjsBody {
void set_sleep(mjtSleepPolicy value) {
ptr_->sleep = value;
}
mjtByte simple() const {
return ptr_->simple;
}
void set_simple(mjtByte value) {
ptr_->simple = value;
}
mjDoubleVec &userdata() const {
return *(ptr_->userdata);
}