Allow self-attach in MJCF

PiperOrigin-RevId: 941676717
Change-Id: I9ff8fc89716bc14b438a41e2b4075bbe5fdb83cf
This commit is contained in:
Yuval Tassa
2026-07-02 06:51:04 -07:00
committed by Copybara-Service
parent cdc35dec38
commit 2d283b5e9d
4 changed files with 178 additions and 37 deletions
+111 -5
View File
@@ -521,7 +521,8 @@ TEST_F(XMLReaderTest, InvalidDoubleOrientation) {
if (orient1 == orient2) continue;
std::string xml = prefix + field + orient1 + orient2 + suffix;
std::array<char, 1024> error;
MjModelPtr model = LoadModelFromString(xml.c_str(), error.data(), error.size());
MjModelPtr model =
LoadModelFromString(xml.c_str(), error.data(), error.size());
ASSERT_THAT(model.get(), IsNull());
EXPECT_THAT(
error.data(),
@@ -1836,7 +1837,8 @@ TEST_F(XMLReaderTest, AttachSpecAssets) {
mj_addBufferVFS(vfs.get(), "xml_child.xml", xml_child, sizeof(xml_child));
std::array<char, 1024> er;
MjModelPtr model = LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
MjModelPtr model =
LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
EXPECT_THAT(model.get(), NotNull()) << er.data();
MjModelPtr expected = LoadModelFromString(xml_expected, er.data(), er.size());
@@ -1890,7 +1892,8 @@ TEST_F(XMLReaderTest, InvalidAttach) {
mj_addBufferVFS(vfs.get(), "child.xml", xml_child, sizeof(xml_child));
std::array<char, 1024> er;
MjModelPtr model = LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
MjModelPtr model =
LoadModelFromString(xml_parent, er.data(), er.size(), vfs.get());
EXPECT_THAT(model.get(), IsNull()) << er.data();
EXPECT_THAT(er.data(), HasSubstr("repeated name '_actuator' in actuator"));
@@ -1999,7 +2002,8 @@ TEST_F(XMLReaderTest, ResizeKeyframeAfterParsing) {
mj_addBufferVFS(vfs.get(), "child.xml", child_xml, sizeof(child_xml));
std::array<char, 1024> error;
MjModelPtr m = LoadModelFromString(parent_xml, error.data(), error.size(), vfs.get());
MjModelPtr m =
LoadModelFromString(parent_xml, error.data(), error.size(), vfs.get());
EXPECT_THAT(m.get(), NotNull()) << error.data();
mj_deleteVFS(vfs.get());
}
@@ -3725,7 +3729,7 @@ TEST_F(ActuatorParseTest, ActuatorDelayParsed) {
ASSERT_EQ(model->nu, 3);
// actuator_history[2*i] = nsample, actuator_history[2*i+1] = interp
EXPECT_EQ(model->actuator_history[0], 0); // jnt1 nsample
EXPECT_EQ(model->actuator_history[1], 0); // jnt1 interp (no buffer, default 0)
EXPECT_EQ(model->actuator_history[1], 0); // jnt1 interp (no buffer, def 0)
EXPECT_EQ(model->actuator_history[2], 3); // jnt2 nsample
EXPECT_EQ(model->actuator_history[3], 0); // jnt2 interp (ZOH)
EXPECT_EQ(model->actuator_history[4], 10); // jnt3 nsample
@@ -3785,6 +3789,7 @@ TEST_F(ActuatorParseTest, ActuatorDelayRequiresHistory) {
EXPECT_THAT(error.data(),
HasSubstr("setting delay > 0 without a history buffer"));
}
TEST_F(ActuatorParseTest, DampingArmatureDefaultsPropagate) {
static constexpr char xml[] = R"(
<mujoco>
@@ -3884,5 +3889,106 @@ TEST_F(XMLReaderTest, AttachConflictXMLMergeUnmergableError) {
HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0"));
}
TEST_F(XMLReaderTest, SelfAttach) {
static constexpr char xml[] = R"(
<mujoco model="self-attach-test">
<worldbody>
<body name="body1">
<geom name="geom1" size="1"/>
</body>
<body name="body2">
<attach body="body1" prefix="attached_"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
ASSERT_THAT(spec, NotNull()) << error.data();
mjModel* m = mj_compile(spec, nullptr);
ASSERT_THAT(m, NotNull());
int body2_id = mj_name2id(m, mjOBJ_BODY, "body2");
int attached_body1_id = mj_name2id(m, mjOBJ_BODY, "attached_body1");
EXPECT_GE(body2_id, 0);
EXPECT_GE(attached_body1_id, 0);
EXPECT_EQ(m->body_parentid[attached_body1_id], body2_id);
mj_deleteModel(m);
mj_deleteSpec(spec);
}
TEST_F(XMLReaderTest, SelfAttachCollisionError) {
static constexpr char xml[] = R"(
<mujoco model="self-attach-collision">
<worldbody>
<body name="body1"/>
<body name="attached_body1"/>
<body name="body2">
<attach body="body1" prefix="attached_"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
EXPECT_THAT(spec, IsNull());
EXPECT_THAT(
error.data(),
HasSubstr("cannot self-attach: element attached_body1 already exists"));
}
TEST_F(XMLReaderTest, SelfAttachMissingError) {
static constexpr char xml[] = R"(
<mujoco model="self-attach-missing">
<worldbody>
<body name="body1">
<attach body="nonexistent" prefix="attached_"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
EXPECT_THAT(spec, IsNull());
EXPECT_THAT(error.data(), HasSubstr("could not find body 'nonexistent' in "
"the current model for self-attachment"));
}
TEST_F(XMLReaderTest, SelfAttachFrame) {
static constexpr char xml[] = R"(
<mujoco model="self-attach-frame-test">
<worldbody>
<frame name="frame1">
<body name="body1" pos="-1 0 0">
<geom name="geom1" type="box" size="1 1 1"/>
</body>
</frame>
<body name="body2">
<attach frame="frame1" prefix="attached_"/>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjSpec* spec = mj_parseXMLString(xml, nullptr, error.data(), error.size());
ASSERT_THAT(spec, NotNull()) << error.data();
mjModel* m = mj_compile(spec, nullptr);
ASSERT_THAT(m, NotNull()) << mjs_getError(spec);
int body2_id = mj_name2id(m, mjOBJ_BODY, "body2");
int attached_body1_id = mj_name2id(m, mjOBJ_BODY, "attached_body1");
EXPECT_GE(body2_id, 0);
EXPECT_GE(attached_body1_id, 0);
EXPECT_EQ(m->body_parentid[attached_body1_id], body2_id);
mj_deleteModel(m);
mj_deleteSpec(spec);
}
} // namespace
} // namespace mujoco