Skip to content

Commit 2d283b5

Browse files
yuvaltassacopybara-github
authored andcommitted
Allow self-attach in MJCF
PiperOrigin-RevId: 941676717 Change-Id: I9ff8fc89716bc14b438a41e2b4075bbe5fdb83cf
1 parent cdc35de commit 2d283b5

4 files changed

Lines changed: 178 additions & 37 deletions

File tree

doc/XMLreference.rst

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3996,16 +3996,18 @@ Associate this body with an :ref:`engine plugin<exPlugin>`. Either :at:`plugin`
39963996
:el-prefix:`body/` |-| **attach** |*|
39973997
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39983998

3999-
The :el:`attach` element is used to insert elements from another (child) model into this (parent) model's kinematic tree.
4000-
Unlike :ref:`include<include>`, which is implemented in the parser and is equivalent to copying and pasting XML from one
4001-
file into another, :el:`attach` is implemented in the model compiler. In order to use this element, the sub-model must
4002-
first be defined as an :ref:`asset<asset-model>`. When creating an attachment, a frame, body or the entire child model in the
4003-
child model is specified, and all referencing elements outside the kinematic tree (e.g., sensors and actuators), are also copied into
4004-
the parent model. Additionally, any elements referenced from within the attached subtree (e.g. defaults and assets)
4005-
will be copied in to the parent model. :el:`attach` is a :ref:`meta-element`, so upon saving all attachments will
4006-
appear in the saved XML file. Note that this element is a subset of the functionality of the procedural
4007-
:ref:`attachment<meAttachment>` functionality. As such, it shares the same limitations as described there. See example `here
4008-
<https://github.com/google-deepmind/mujoco/blob/main/test/xml/testdata/parent.xml>`__.
3999+
The :el:`attach` element is used to insert elements from another (child) model, or from the current model itself
4000+
(self-attachment), into this (parent) model's kinematic tree. Unlike :ref:`include<include>`, which is implemented in
4001+
the parser and is equivalent to copying and pasting XML from one file into another, :el:`attach` is implemented in the
4002+
model compiler. In order to use this element to import from another model, the sub-model must first be defined as an
4003+
:ref:`asset<asset-model>`. When creating an attachment, a frame, body or the entire child model in the child model is
4004+
specified, and all referencing elements outside the kinematic tree (e.g., sensors and actuators), are also copied into
4005+
the parent model. Additionally, any elements referenced from within the attached subtree (e.g. defaults and assets) will
4006+
be copied in to the parent model. For self-attaching within the same model, the :at:`model` attribute is omitted, and a
4007+
body or frame must be specified. :el:`attach` is a :ref:`meta-element`, so upon saving all attachments will appear in
4008+
the saved XML file. Note that this element is a subset of the functionality of the procedural
4009+
:ref:`attachment<meAttachment>` functionality. As such, it shares the same limitations as described there. See example
4010+
`here <https://github.com/google-deepmind/mujoco/blob/main/test/xml/testdata/parent.xml>`__.
40094011

40104012
.. admonition:: Known issues
40114013
:class: note
@@ -4020,8 +4022,9 @@ appear in the saved XML file. Note that this element is a subset of the function
40204022

40214023
.. _body-attach-model:
40224024

4023-
:at:`model`: :at-val:`string, required`
4025+
:at:`model`: :at-val:`string, optional`
40244026
The child model from which to attach a subtree or a frame.
4027+
If omitted, the attachment is performed within the current model (self-attachment).
40254028

40264029
.. _body-attach-body:
40274030

doc/changelog.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ General
99
^^^^^^^
1010
- Added Nesterov momentum extrapolation with adaptive gradient restart (O'Donoghue-Candès) to the PGS solver,
1111
significantly improving convergence. Overall PGS now requires ~2x fewer iterations.
12-
13-
* :ref:`mj_encode` now supports encoding of MJB and TXT files.
12+
- :ref:`mj_encode` now supports encoding of MJB and TXT files.
13+
- The :el:`attach` element now supports self-attachment (attaching elements of the current model to itself) by omitting
14+
the :at:`model` attribute. It also supports attaching a frame via the new :at:`frame` attribute, which is mutually
15+
exclusive with :at:`body`.
1416

1517
.. admonition:: Breaking API changes
1618
:class: attention

src/xml/xml_native_reader.cc

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3942,39 +3942,69 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame,
39423942
// attachment
39433943
else if (name == "attach") {
39443944
string model_name, child_name, prefix;
3945-
ReadAttrTxt(elem, "model", model_name, /*required=*/true);
3945+
bool has_model = ReadAttrTxt(elem, "model", model_name, /*required=*/false);
39463946
bool has_body = ReadAttrTxt(elem, "body", child_name, /*required=*/false);
39473947
bool has_frame = ReadAttrTxt(elem, "frame", child_name, /*required=*/false);
39483948
ReadAttrTxt(elem, "prefix", prefix, /*required=*/true);
39493949

39503950
if (has_body && has_frame) {
39513951
throw mjXError(elem, "only one of body or frame can be specified in attach");
39523952
}
3953-
mjtObj type = has_body ? mjOBJ_BODY : mjOBJ_FRAME;
3953+
mjtObj type = mjOBJ_UNKNOWN;
3954+
if (has_body) type = mjOBJ_BODY;
3955+
else if (has_frame) type = mjOBJ_FRAME;
3956+
3957+
mjsElement* source_elem = nullptr;
3958+
if (!has_model) { // Self-attach
3959+
if (type == mjOBJ_UNKNOWN) {
3960+
throw mjXError(elem, "either 'body' or 'frame' attribute must be specified for self-attach");
3961+
}
39543962

3955-
string full_name = prefix+child_name;
3956-
if (mjs_findElement(spec, type, full_name.c_str())) {
3957-
throw mjXError(elem, "cannot attach: element %s already exists", full_name.c_str());
3958-
}
3963+
// check for name collision in the current spec
3964+
string full_name = prefix + child_name;
3965+
if (mjs_findElement(spec, type, full_name.c_str())) {
3966+
throw mjXError(elem, "cannot self-attach: element %s already exists", full_name.c_str());
3967+
}
3968+
source_elem = mjs_findElement(spec, type, child_name.c_str());
3969+
if (!source_elem) {
3970+
throw mjXError(elem, "%s",
3971+
(string("could not find ") + mju_type2Str(type) + " '" + child_name +
3972+
"' in the current model for self-attachment").c_str());
3973+
}
3974+
} else { // Attach from external model asset
3975+
// Check for name collision in the current spec
3976+
if (!child_name.empty()) {
3977+
string full_name = prefix + child_name;
3978+
if (mjs_findElement(spec, type, full_name.c_str())) {
3979+
throw mjXError(elem, "%s",
3980+
(string("cannot attach: element ") + child_name +
3981+
" already exists with prefix " + prefix).c_str());
3982+
}
3983+
}
39593984

3960-
mjSpec* asset = mjs_findSpec(spec, model_name.c_str());
3961-
if (!asset) {
3962-
throw mjXError(elem, "could not find model '%s'", model_name.c_str());
3963-
}
3985+
mjSpec* asset = mjs_findSpec(spec, model_name.c_str());
3986+
if (!asset) {
3987+
throw mjXError(elem, "could not find model '%s'", model_name.c_str());
3988+
}
39643989

3965-
mjsElement* child;
3966-
if (child_name.empty()) {
3967-
child = asset->element;
3968-
} else {
3969-
child = mjs_findElement(asset, type, child_name.c_str());
3970-
if (!child) {
3971-
throw mjXError(elem, "could not find %s",
3972-
(string(mju_type2Str(type)) + " '" + child_name + "'").c_str());
3990+
if (type == mjOBJ_UNKNOWN) { // Attach world body contents
3991+
source_elem = asset->element;
3992+
} else { // Attach specific body or frame
3993+
source_elem = mjs_findElement(asset, type, child_name.c_str());
3994+
if (!source_elem) {
3995+
throw mjXError(elem, "%s",
3996+
(string("could not find ") + mju_type2Str(type) + " '" + child_name +
3997+
"' in model asset '" + model_name + "'").c_str());
3998+
}
39733999
}
39744000
}
39754001

39764002
mjsFrame* pframe = frame ? frame : mjs_addFrame(body, nullptr);
3977-
if (!mjs_attach(pframe->element, child, prefix.c_str(), "")) {
4003+
// Set default for the new frame from the current context
4004+
mjs_setDefault(pframe->element, mjs_getDefault(frame ? frame->element : body->element));
4005+
mjs_setString(pframe->info, ("line = " + std::to_string(elem->GetLineNum())).c_str());
4006+
4007+
if (!mjs_attach(pframe->element, source_elem, prefix.c_str(), "")) {
39784008
throw mjXError(elem, "%s", stripError(mjs_getError(spec)));
39794009
}
39804010
}

test/xml/xml_native_reader_test.cc

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,8 @@ TEST_F(XMLReaderTest, InvalidDoubleOrientation) {
521521
if (orient1 == orient2) continue;
522522
std::string xml = prefix + field + orient1 + orient2 + suffix;
523523
std::array<char, 1024> error;
524-
MjModelPtr model = LoadModelFromString(xml.c_str(), error.data(), error.size());
524+
MjModelPtr model =
525+
LoadModelFromString(xml.c_str(), error.data(), error.size());
525526
ASSERT_THAT(model.get(), IsNull());
526527
EXPECT_THAT(
527528
error.data(),
@@ -1836,7 +1837,8 @@ TEST_F(XMLReaderTest, AttachSpecAssets) {
18361837
mj_addBufferVFS(vfs.get(), "xml_child.xml", xml_child, sizeof(xml_child));
18371838

18381839
std::array<char, 1024> er;
1839-
MjModelPtr model = LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
1840+
MjModelPtr model =
1841+
LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
18401842
EXPECT_THAT(model.get(), NotNull()) << er.data();
18411843

18421844
MjModelPtr expected = LoadModelFromString(xml_expected, er.data(), er.size());
@@ -1890,7 +1892,8 @@ TEST_F(XMLReaderTest, InvalidAttach) {
18901892
mj_addBufferVFS(vfs.get(), "child.xml", xml_child, sizeof(xml_child));
18911893

18921894
std::array<char, 1024> er;
1893-
MjModelPtr model = LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
1895+
MjModelPtr model =
1896+
LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
18941897

18951898
EXPECT_THAT(model.get(), IsNull()) << er.data();
18961899
EXPECT_THAT(er.data(), HasSubstr("repeated name '_actuator' in actuator"));
@@ -1999,7 +2002,8 @@ TEST_F(XMLReaderTest, ResizeKeyframeAfterParsing) {
19992002
mj_addBufferVFS(vfs.get(), "child.xml", child_xml, sizeof(child_xml));
20002003

20012004
std::array<char, 1024> error;
2002-
MjModelPtr m = LoadModelFromString(parent_xml, error.data(), error.size(), vfs.get());
2005+
MjModelPtr m =
2006+
LoadModelFromString(parent_xml, error.data(), error.size(), vfs.get());
20032007
EXPECT_THAT(m.get(), NotNull()) << error.data();
20042008
mj_deleteVFS(vfs.get());
20052009
}
@@ -3725,7 +3729,7 @@ TEST_F(ActuatorParseTest, ActuatorDelayParsed) {
37253729
ASSERT_EQ(model->nu, 3);
37263730
// actuator_history[2*i] = nsample, actuator_history[2*i+1] = interp
37273731
EXPECT_EQ(model->actuator_history[0], 0); // jnt1 nsample
3728-
EXPECT_EQ(model->actuator_history[1], 0); // jnt1 interp (no buffer, default 0)
3732+
EXPECT_EQ(model->actuator_history[1], 0); // jnt1 interp (no buffer, def 0)
37293733
EXPECT_EQ(model->actuator_history[2], 3); // jnt2 nsample
37303734
EXPECT_EQ(model->actuator_history[3], 0); // jnt2 interp (ZOH)
37313735
EXPECT_EQ(model->actuator_history[4], 10); // jnt3 nsample
@@ -3785,6 +3789,7 @@ TEST_F(ActuatorParseTest, ActuatorDelayRequiresHistory) {
37853789
EXPECT_THAT(error.data(),
37863790
HasSubstr("setting delay > 0 without a history buffer"));
37873791
}
3792+
37883793
TEST_F(ActuatorParseTest, DampingArmatureDefaultsPropagate) {
37893794
static constexpr char xml[] = R"(
37903795
<mujoco>
@@ -3884,5 +3889,106 @@ TEST_F(XMLReaderTest, AttachConflictXMLMergeUnmergableError) {
38843889
HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0"));
38853890
}
38863891

3892+
TEST_F(XMLReaderTest, SelfAttach) {
3893+
static constexpr char xml[] = R"(
3894+
<mujoco model="self-attach-test">
3895+
<worldbody>
3896+
<body name="body1">
3897+
<geom name="geom1" size="1"/>
3898+
</body>
3899+
<body name="body2">
3900+
<attach body="body1" prefix="attached_"/>
3901+
</body>
3902+
</worldbody>
3903+
</mujoco>
3904+
)";
3905+
std::array<char, 1024> error;
3906+
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
3907+
ASSERT_THAT(spec, NotNull()) << error.data();
3908+
3909+
mjModel* m = mj_compile(spec, nullptr);
3910+
ASSERT_THAT(m, NotNull());
3911+
3912+
int body2_id = mj_name2id(m, mjOBJ_BODY, "body2");
3913+
int attached_body1_id = mj_name2id(m, mjOBJ_BODY, "attached_body1");
3914+
3915+
EXPECT_GE(body2_id, 0);
3916+
EXPECT_GE(attached_body1_id, 0);
3917+
EXPECT_EQ(m->body_parentid[attached_body1_id], body2_id);
3918+
3919+
mj_deleteModel(m);
3920+
mj_deleteSpec(spec);
3921+
}
3922+
3923+
TEST_F(XMLReaderTest, SelfAttachCollisionError) {
3924+
static constexpr char xml[] = R"(
3925+
<mujoco model="self-attach-collision">
3926+
<worldbody>
3927+
<body name="body1"/>
3928+
<body name="attached_body1"/>
3929+
<body name="body2">
3930+
<attach body="body1" prefix="attached_"/>
3931+
</body>
3932+
</worldbody>
3933+
</mujoco>
3934+
)";
3935+
std::array<char, 1024> error;
3936+
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
3937+
EXPECT_THAT(spec, IsNull());
3938+
EXPECT_THAT(
3939+
error.data(),
3940+
HasSubstr("cannot self-attach: element attached_body1 already exists"));
3941+
}
3942+
3943+
TEST_F(XMLReaderTest, SelfAttachMissingError) {
3944+
static constexpr char xml[] = R"(
3945+
<mujoco model="self-attach-missing">
3946+
<worldbody>
3947+
<body name="body1">
3948+
<attach body="nonexistent" prefix="attached_"/>
3949+
</body>
3950+
</worldbody>
3951+
</mujoco>
3952+
)";
3953+
std::array<char, 1024> error;
3954+
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
3955+
EXPECT_THAT(spec, IsNull());
3956+
EXPECT_THAT(error.data(), HasSubstr("could not find body 'nonexistent' in "
3957+
"the current model for self-attachment"));
3958+
}
3959+
3960+
TEST_F(XMLReaderTest, SelfAttachFrame) {
3961+
static constexpr char xml[] = R"(
3962+
<mujoco model="self-attach-frame-test">
3963+
<worldbody>
3964+
<frame name="frame1">
3965+
<body name="body1" pos="-1 0 0">
3966+
<geom name="geom1" type="box" size="1 1 1"/>
3967+
</body>
3968+
</frame>
3969+
<body name="body2">
3970+
<attach frame="frame1" prefix="attached_"/>
3971+
</body>
3972+
</worldbody>
3973+
</mujoco>
3974+
)";
3975+
std::array<char, 1024> error;
3976+
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
3977+
ASSERT_THAT(spec, NotNull()) << error.data();
3978+
3979+
mjModel* m = mj_compile(spec, nullptr);
3980+
ASSERT_THAT(m, NotNull()) << mjs_getError(spec);
3981+
3982+
int body2_id = mj_name2id(m, mjOBJ_BODY, "body2");
3983+
int attached_body1_id = mj_name2id(m, mjOBJ_BODY, "attached_body1");
3984+
3985+
EXPECT_GE(body2_id, 0);
3986+
EXPECT_GE(attached_body1_id, 0);
3987+
EXPECT_EQ(m->body_parentid[attached_body1_id], body2_id);
3988+
3989+
mj_deleteModel(m);
3990+
mj_deleteSpec(spec);
3991+
}
3992+
38873993
} // namespace
38883994
} // namespace mujoco

0 commit comments

Comments
 (0)