Add policies for global attribute conflict resolution upon attach

For example when loading parent_merge.xml:

```
WARNING: Attach conflict when attaching 'child' to 'parent_merge', policy is 'merge'
timestep: parent has 0.005, child has 0.002, taking the minimum
iterations: parent has 50, child has 100, taking the maximum
flag 'Damper': added from child
```

When loading parent_error.xml:

```
XML Error: Attach conflict when attaching 'child' to 'parent_error', policy is 'error'
timestep: parent has 0.005, child has 0.002
iterations: parent has 50, child has 100
Element 'attach', line 10
```
PiperOrigin-RevId: 933620810
Change-Id: Ib477863b5ef763474d27fb4be5a4148be1d5d500
This commit is contained in:
Yuval Tassa
2026-06-17 03:34:10 -07:00
committed by Copybara-Service
parent 30357c38a3
commit 410c73168c
37 changed files with 2705 additions and 449 deletions
+2
View File
@@ -99,6 +99,8 @@ std::vector<std::string> GetWriteReadTestModels() {
// exclude files that fail since we do not save pinned flex nodes
absl::StrContains(xml, "gripper_trilinear") ||
absl::StrContains(xml, "strain") ||
// exclude conflict test assets (designed to fail compile)
absl::StrContains(xml, "xml/testdata/parent_") ||
// exclude mjz testdata with VFS files
absl::StrContains(xml, "mixed_test")) {
continue;
+10
View File
@@ -0,0 +1,10 @@
<mujoco model="child_mergable">
<option timestep="0.002" iterations="100" gravity="0 0 -10">
<flag damper="disable"/>
</option>
<worldbody>
<body name="child_body">
<geom name="child_geom" size="1"/>
</body>
</worldbody>
</mujoco>
+10
View File
@@ -0,0 +1,10 @@
<mujoco model="child_unmergable">
<option timestep="0.005" iterations="50" gravity="0 0 0">
<flag damper="enable" constraint="disable"/>
</option>
<worldbody>
<body name="child_body">
<geom name="child_geom" size="1"/>
</body>
</worldbody>
</mujoco>
+13
View File
@@ -0,0 +1,13 @@
<mujoco model="parent_error">
<compiler conflict="error"/>
<option timestep="0.005" iterations="50" gravity="0 0 -10"/>
<asset>
<model name="child" file="child_mergable.xml"/>
</asset>
<worldbody>
<body name="parent_body">
<geom name="parent_geom" size="2"/>
<attach model="child" body="child_body" prefix="child/"/>
</body>
</worldbody>
</mujoco>
+13
View File
@@ -0,0 +1,13 @@
<mujoco model="parent_merge">
<compiler conflict="merge"/>
<option timestep="0.005" iterations="50" gravity="0 0 -10"/>
<asset>
<model name="child" file="child_mergable.xml"/>
</asset>
<worldbody>
<body name="parent_body">
<geom name="parent_geom" size="2"/>
<attach model="child" body="child_body" prefix="child/"/>
</body>
</worldbody>
</mujoco>
+13
View File
@@ -0,0 +1,13 @@
<mujoco model="parent_merge_unmergable">
<compiler conflict="merge"/>
<option timestep="0.005" iterations="50" gravity="0 0 -10"/>
<asset>
<model name="child" file="child_unmergable.xml"/>
</asset>
<worldbody>
<body name="parent_body">
<geom name="parent_geom" size="2"/>
<attach model="child" body="child_body" prefix="child/"/>
</body>
</worldbody>
</mujoco>
+15
View File
@@ -0,0 +1,15 @@
<mujoco model="parent_warn">
<compiler conflict="warning"/>
<option timestep="0.005" iterations="50" gravity="0 0 -10">
<flag damper="disable"/>
</option>
<asset>
<model name="child" file="child_unmergable.xml"/>
</asset>
<worldbody>
<body name="parent_body">
<geom name="parent_geom" size="2"/>
<attach model="child" body="child_body" prefix="child/"/>
</body>
</worldbody>
</mujoco>
+75
View File
@@ -3897,5 +3897,80 @@ TEST_F(ActuatorParseTest, DampingArmatureDefaultsPropagate) {
mj_deleteModel(model);
}
TEST_F(XMLReaderTest, AttachConflictXMLWarning) {
mock_warning_handler.ExpectWarnings();
std::array<char, 1024> error;
std::string path = GetTestDataFilePath("xml/testdata/parent_warn.xml");
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
ASSERT_THAT(spec, NotNull()) << error.data();
// one grouped warning per attach, containing gravity and damper flag
// (constraint is only authored by child, not a conflict)
EXPECT_EQ(mjs_numWarnings(spec), 1);
std::string w = mjs_getWarning(spec, 0);
EXPECT_THAT(w, HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0,"
" keeping parent value"));
EXPECT_THAT(w, HasSubstr("flag 'Damper'"));
mjModel* m = mj_compile(spec, nullptr);
ASSERT_THAT(m, NotNull());
// Gravity should be parent's value (0 0 -10)
EXPECT_MJTNUM_EQ(m->opt.gravity[0], 0);
EXPECT_MJTNUM_EQ(m->opt.gravity[1], 0);
EXPECT_MJTNUM_EQ(m->opt.gravity[2], -10);
mj_deleteModel(m);
mj_deleteSpec(spec);
}
TEST_F(XMLReaderTest, AttachConflictXMLMerge) {
mock_warning_handler.ExpectWarnings();
std::array<char, 1024> error;
std::string path = GetTestDataFilePath("xml/testdata/parent_merge.xml");
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
ASSERT_THAT(spec, NotNull()) << error.data();
// one grouped warning per attach, containing timestep, iterations, and flag
EXPECT_GE(mjs_numWarnings(spec), 1);
std::string w = mjs_getWarning(spec, 0);
EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.002,"
" taking the minimum"));
EXPECT_THAT(w, HasSubstr("iterations: parent has 50, child has 100,"
" taking the maximum"));
mjModel* m = mj_compile(spec, nullptr);
ASSERT_THAT(m, NotNull());
// Timestep should be min (0.002)
EXPECT_MJTNUM_EQ(m->opt.timestep, 0.002);
// Iterations should be max (100)
EXPECT_EQ(m->opt.iterations, 100);
mj_deleteModel(m);
mj_deleteSpec(spec);
}
TEST_F(XMLReaderTest, AttachConflictXMLError) {
std::array<char, 1024> error;
std::string path = GetTestDataFilePath("xml/testdata/parent_error.xml");
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
// Should fail to parse because of conflict in timestep in error mode
EXPECT_THAT(spec, IsNull());
EXPECT_THAT(error.data(),
HasSubstr("timestep: parent has 0.005, child has 0.002"));
}
TEST_F(XMLReaderTest, AttachConflictXMLMergeUnmergableError) {
std::array<char, 1024> error;
std::string path =
GetTestDataFilePath("xml/testdata/parent_merge_unmergable.xml");
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
// Should fail to parse because gravity is unmergeable in merge mode
EXPECT_THAT(spec, IsNull());
EXPECT_THAT(error.data(),
HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0"));
}
} // namespace
} // namespace mujoco
+24 -29
View File
@@ -45,29 +45,30 @@ std::vector<std::string> GetWriteReadTestModels() {
if (p.path().extension() == ext) {
std::string xml = p.path().string();
if ( // if file is meant to fail, skip it
absl::StrContains(xml, "malformed_") ||
absl::StrContains(xml, "_fail") ||
// exclude files that are too slow to load
absl::StrContains(xml, "cow") ||
absl::StrContains(xml, "gmsh_") ||
absl::StrContains(xml, "shark_") ||
absl::StrContains(xml, "perf") ||
// exclude files that fail the comparison test
absl::StrContains(xml, "rfcamera") ||
absl::StrContains(xml, "tactile") ||
absl::StrContains(xml, "makemesh") ||
absl::StrContains(xml, "many_dependencies") ||
absl::StrContains(xml, "usd") ||
absl::StrContains(xml, "torus_maxhull") ||
absl::StrContains(xml, "fitmesh_") ||
absl::StrContains(xml, "lengthrange") ||
absl::StrContains(xml, "hfield_xml") ||
absl::StrContains(xml, "fromto_convex") ||
absl::StrContains(xml, "cube_skin") ||
absl::StrContains(xml, "cube_3x3x3") ||
// exclude files that fail since we do not save pinned flex nodes
absl::StrContains(xml, "gripper_trilinear") ||
absl::StrContains(xml, "strain")) {
absl::StrContains(xml, "malformed_") ||
absl::StrContains(xml, "_fail") ||
// exclude files that are too slow to load
absl::StrContains(xml, "cow") || absl::StrContains(xml, "gmsh_") ||
absl::StrContains(xml, "shark_") ||
absl::StrContains(xml, "perf") ||
// exclude files that fail the comparison test
absl::StrContains(xml, "rfcamera") ||
absl::StrContains(xml, "tactile") ||
absl::StrContains(xml, "makemesh") ||
absl::StrContains(xml, "many_dependencies") ||
absl::StrContains(xml, "usd") ||
absl::StrContains(xml, "torus_maxhull") ||
absl::StrContains(xml, "fitmesh_") ||
absl::StrContains(xml, "lengthrange") ||
absl::StrContains(xml, "hfield_xml") ||
absl::StrContains(xml, "fromto_convex") ||
absl::StrContains(xml, "cube_skin") ||
absl::StrContains(xml, "cube_3x3x3") ||
// exclude files that fail since we do not save pinned flex nodes
absl::StrContains(xml, "gripper_trilinear") ||
absl::StrContains(xml, "strain") ||
// exclude conflict tests (known option conflict warnings/errors)
absl::StrContains(xml, "xml/testdata/parent_")) {
continue;
}
models.push_back(xml);
@@ -84,12 +85,6 @@ class WriteReadCompareTest : public XMLWriterTest,
TEST_P(WriteReadCompareTest, WriteReadCompare) {
std::string xml = GetParam();
// If this is the flex_line_obj model, expect the 'is not rigid' warning
if (absl::StrContains(xml, "flex_line_obj")) {
EXPECT_CALL(mock_warning_handler, Warn(testing::HasSubstr("is not rigid")))
.WillRepeatedly(testing::Return());
}
// full precision float printing
FullFloatPrecision increase_precision;