From 410c73168c87c17720f7695c6c7c05926eecf348 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 17 Jun 2026 03:34:10 -0700 Subject: [PATCH] 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 --- doc/APIreference/APItypes.rst | 9 + doc/XMLreference.rst | 21 + doc/XMLschema.rst | 3 + doc/changelog.rst | 11 + doc/includes/references.h | 6 + doc/programming/modeledit.rst | 61 ++ include/mujoco/mjspec.h | 6 + include/mujoco/mjspecmacro.h | 42 +- python/mujoco/introspect/enums.py | 10 + python/mujoco/introspect/structs.py | 5 + python/mujoco/specs_test.py | 131 ++- python/mujoco/specs_wrapper.cc | 1 + src/user/CMakeLists.txt | 2 + src/user/user_api.cc | 63 +- src/user/user_model.cc | 19 + src/user/user_model.h | 2 + src/user/user_resolver.cc | 449 ++++++++ src/user/user_resolver.h | 34 + src/xml/xml_base.h | 3 +- src/xml/xml_native_reader.cc | 990 +++++++++++------- src/xml/xml_native_writer.cc | 2 + test/fixture.cc | 1 + test/user/user_api_test.cc | 109 +- test/user/user_recompile_test.cc | 5 +- test/user/user_resolver_test.cc | 918 ++++++++++++++++ test/xml/mjz/mjz_encoder_test.cc | 2 + test/xml/testdata/child_mergable.xml | 10 + test/xml/testdata/child_unmergable.xml | 10 + test/xml/testdata/parent_error.xml | 13 + test/xml/testdata/parent_merge.xml | 13 + test/xml/testdata/parent_merge_unmergable.xml | 13 + test/xml/testdata/parent_warn.xml | 15 + test/xml/xml_native_reader_test.cc | 75 ++ test/xml/xml_write_read_test.cc | 53 +- unity/Runtime/Bindings/MjBindings.cs | 6 + wasm/codegen/generated/bindings.cc | 11 + wasm/tests/bindings_test.ts | 30 + 37 files changed, 2705 insertions(+), 449 deletions(-) create mode 100644 src/user/user_resolver.cc create mode 100644 src/user/user_resolver.h create mode 100644 test/user/user_resolver_test.cc create mode 100644 test/xml/testdata/child_mergable.xml create mode 100644 test/xml/testdata/child_unmergable.xml create mode 100644 test/xml/testdata/parent_error.xml create mode 100644 test/xml/testdata/parent_merge.xml create mode 100644 test/xml/testdata/parent_merge_unmergable.xml create mode 100644 test/xml/testdata/parent_warn.xml diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index 143c6b11..15c82ff9 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -915,6 +915,15 @@ Type of built-in procedural mesh. .. mujoco-include:: mjtMeshBuiltin +.. _mjtConflict: + +mjtConflict +~~~~~~~~~~~ + +Conflict resolution mode for attach. + +.. mujoco-include:: mjtConflict + .. _mjtCTimer: diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index bcf33364..72842f40 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -900,6 +900,27 @@ has any effect. The settings here are global and apply to the entire model. :at:`saveinertial`: :at-val:`[false, true], "false"` If set to "true", the compiler will save explicit :ref:`inertial ` clauses for all bodies. +.. _compiler-conflict: + +:at:`conflict`: :at-val:`[warning, merge, error], "warning"` + This attribute controls how conflicting global attributes (physics options, sizes, visual settings) are resolved when + a child spec is attached to a parent using :ref:`mjs_attach`. A conflict occurs when both the parent and child specify + authored values for the same field and those values differ. See :ref:`Attribute Merging ` for + details and a per-field table. + + :at-val:`warning` + Parent values take precedence. When a conflict is detected, a warning is emitted but the parent value is not + modified. This is the default and preserves the pre-existing attachment behavior. + + :at-val:`merge` + Fields are merged using field-specific strategies (minimum, maximum, OR, or error), depending on the field's + semantics. When only the child specifies an authored value, it is adopted by the parent. See the + :ref:`merging table ` for per-field details. + + :at-val:`error` + Any conflict between authored values results in a compile error. This is the strictest mode and is useful for + detecting unintended attribute mismatches. + .. _compiler-lengthrange: :el-prefix:`compiler/` |-| **lengthrange** |?| diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 6507fbdd..f15c15f7 100755 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -254,6 +254,9 @@ .. grid-item:: :ref:`alignfree` + .. grid-item:: + :ref:`conflict` + .. dropdown:: :ref:`lengthrange` :octicon:`dot` diff --git a/doc/changelog.rst b/doc/changelog.rst index 4beda810..bb2fbb0b 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -22,6 +22,17 @@ General - Added :ref:`mjs_numWarnings` and :ref:`mjs_getWarning` for retrieving all warnings accumulated during model compilation and attachment. Deprecated :ref:`mjs_isWarning` in favor of ``mjs_numWarnings(s) > 0``. +- Added the :ref:`compiler/conflict` attribute for controlling how conflicting global attributes + are resolved during :ref:`attachment`. Possible values are "warning" (default: parent values take + precedence, warnings emitted on conflicts), "merge" (per-field min/max/error strategy), and "error" (any + conflict raises an error). See :ref:`Attribute Merging ` for details. + + .. admonition:: Future breaking API changes + :class: warning + + The current default conflict resolution policy "warn" (ignore the child model) is backward compatible. + However, the default policy will change to "merge" in a future release. + - Improved primal solver convergence under float32. Improvements initially proposed by :github:user:`n3b` in :issue:`2313` and :github:user:`denzeler-nvidia` in :doc:`MJWarp ` pull request `1374 `__. diff --git a/doc/includes/references.h b/doc/includes/references.h index 66c834c4..1ebccb8b 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1549,6 +1549,11 @@ typedef enum mjtOrientation_ { // type of orientation specifier mjORIENTATION_ZAXIS, // z axis (minimal rotation) mjORIENTATION_EULER, // Euler angles } mjtOrientation; +typedef enum mjtConflict_ { // conflict resolution for attach + mjCONFLICT_WARNING = 0, // keep parent, warn on conflict + mjCONFLICT_MERGE, // merge: min/max/error per field + mjCONFLICT_ERROR, // error on any conflict +} mjtConflict; typedef enum mjtCTimer_ { // compiler timing categories // top-level timers (wall-clock) mjCTIMER_TOTAL = 0, // total compile time @@ -1585,6 +1590,7 @@ typedef struct mjsCompiler_ { // compiler options int inertiagrouprange[2]; // range of geom groups used to compute inertia mjtByte saveinertial; // save explicit inertial clause for all bodies to XML int alignfree; // align free joints with inertial frame + int conflict; // conflict resolution for attach (mjtConflict) mjLROpt LRopt; // options for lengthrange computation mjString* meshdir; // mesh and hfield directory mjString* texturedir; // texture directory diff --git a/doc/programming/modeledit.rst b/doc/programming/modeledit.rst index 509747e2..a1ff806a 100644 --- a/doc/programming/modeledit.rst +++ b/doc/programming/modeledit.rst @@ -171,6 +171,67 @@ Note also that once a child is attached by reference to a parent, the child cann finalized. If a second attachment is performed without compilation, the keyframes from the first attachment will be lost. +.. _meAttributeMerging: + +Attribute Merging +^^^^^^^^^^^^^^^^^ + +When attaching a child spec (or an element from a child spec) to a parent spec using :ref:`mjs_attach`, global +attributes from the child may conflict with those in the parent. A conflict occurs when both the parent and child +specify authored values for the same field and those values differ. Note that for XML-based models, explicitly writing a +value (even if it matches the default value) counts as authoring and can trigger conflicts. The +:ref:`compiler/conflict` attribute controls how such conflicts are resolved. Fields where only one +side specifies an authored value never conflict. + +:at-val:`warning` (default) + Parent values take precedence. Whenever a conflict is detected, a warning is emitted but the parent value is not + modified. This preserves the pre-existing attachment behavior. + +:at-val:`merge` + Attribute values are merged using a per-field strategy as described in the table below. When only the child specifies + an authored value, it is adopted by the parent. + +:at-val:`error` + Any conflict results in a compile error. No values are modified. + +The table below describes the per-field merge strategy used in :at-val:`merge` mode. + +.. list-table:: Attribute Merging Behavior (:at-val:`merge` mode) + :widths: 15 60 25 + :header-rows: 1 + :name: meAttributeMergingTable + + * - Behavior + - Fields + - Justification + * - **Minimum** + - **option**: :ref:`timestep`, :ref:`tolerance`, :ref:`ls_tolerance`, + :ref:`noslip_tolerance`, :ref:`ccd_tolerance`, + :ref:`sleep_tolerance`, + |br| **visual**: :ref:`znear`, :ref:`realtime` + - Preserves precision and stability. + * - **Maximum** + - **option**: :ref:`iterations`, :ref:`ls_iterations`, + :ref:`noslip_iterations`, :ref:`ccd_iterations`, + :ref:`sdf_iterations`, :ref:`sdf_initpoints`, + |br| **size**: :ref:`memory`, :ref:`nkey`, :ref:`nuserdata`, + :ref:`nuser_body`, :ref:`nuser_jnt`, :ref:`nuser_geom`, :ref:`nuser_site`, + :ref:`nuser_cam`, :ref:`nuser_tendon`, :ref:`nuser_actuator`, :ref:`nuser_sensor` + |br| **visual**: :ref:`zfar` + - Ensures sufficient resources and limits. + * - **OR (union)** + - **option**: :ref:`disableflags`, :ref:`enableflags`, + :ref:`disableactuator` + - Flags from both models are combined. + * - **Error** + - **option**: :ref:`gravity`, :ref:`wind`, :ref:`magnetic`, + :ref:`density`, :ref:`viscosity`, :ref:`integrator`, + :ref:`cone`, :ref:`jacobian`, :ref:`solver`, + :ref:`impratio`, + :ref:`o_margin`, :ref:`o_solref`, :ref:`o_solimp`, + :ref:`o_friction` + - Raised if non-default values conflict. + .. _meDefault: Default classes diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index f8deaecb..6d9d6237 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -128,6 +128,11 @@ typedef enum mjtOrientation_ { // type of orientation specifier mjORIENTATION_EULER, // Euler angles } mjtOrientation; +typedef enum mjtConflict_ { // conflict resolution for attach + mjCONFLICT_WARNING = 0, // keep parent, warn on conflict + mjCONFLICT_MERGE, // merge: min/max/error per field + mjCONFLICT_ERROR, // error on any conflict +} mjtConflict; typedef enum mjtCTimer_ { // compiler timing categories // top-level timers (wall-clock) @@ -171,6 +176,7 @@ typedef struct mjsCompiler_ { // compiler options int inertiagrouprange[2]; // range of geom groups used to compute inertia mjtByte saveinertial; // save explicit inertial clause for all bodies to XML int alignfree; // align free joints with inertial frame + int conflict; // conflict resolution for attach (mjtConflict) mjLROpt LRopt; // options for lengthrange computation mjString* meshdir; // mesh and hfield directory mjString* texturedir; // texture directory diff --git a/include/mujoco/mjspecmacro.h b/include/mujoco/mjspecmacro.h index fe3f6804..b5ff4f9e 100644 --- a/include/mujoco/mjspecmacro.h +++ b/include/mujoco/mjspecmacro.h @@ -25,27 +25,27 @@ //-------------------------------- mjsCompiler ----------------------------------------------------- -#define MJSCOMPILER_FIELDS \ - X ( mjtByte, autolimits, 1 ) \ - X ( double, boundmass, 1 ) \ - X ( double, boundinertia, 1 ) \ - X ( double, settotalmass, 1 ) \ - X ( mjtByte, balanceinertia, 1 ) \ - X ( mjtByte, fitaabb, 1 ) \ - X ( mjtByte, degree, 1 ) \ - XVEC( char, eulerseq, 3 ) \ - X ( mjtByte, discardvisual, 1 ) \ - X ( mjtByte, usethread, 1 ) \ - X ( mjtByte, fusestatic, 1 ) \ - X ( int, inertiafromgeom, 1 ) \ - XVEC( int, inertiagrouprange, 2 ) \ - X ( mjtByte, saveinertial, 1 ) \ - X ( int, alignfree, 1 ) \ - X ( mjLROpt, LRopt, 1 ) \ - X ( mjString*, meshdir, 1 ) \ - X ( mjString*, texturedir, 1 ) \ - X ( uint64_t, authored, 1 ) - +#define MJSCOMPILER_FIELDS \ + X(mjtByte, autolimits, 1) \ + X(double, boundmass, 1) \ + X(double, boundinertia, 1) \ + X(double, settotalmass, 1) \ + X(mjtByte, balanceinertia, 1) \ + X(mjtByte, fitaabb, 1) \ + X(mjtByte, degree, 1) \ + XVEC(char, eulerseq, 3) \ + X(mjtByte, discardvisual, 1) \ + X(mjtByte, usethread, 1) \ + X(mjtByte, fusestatic, 1) \ + X(int, inertiafromgeom, 1) \ + XVEC(int, inertiagrouprange, 2) \ + X(mjtByte, saveinertial, 1) \ + X(int, alignfree, 1) \ + X(int, conflict, 1) \ + X(mjLROpt, LRopt, 1) \ + X(mjString*, meshdir, 1) \ + X(mjString*, texturedir, 1) \ + X(uint64_t, authored, 1) //-------------------------------- mjSpec ---------------------------------------------------------- diff --git a/python/mujoco/introspect/enums.py b/python/mujoco/introspect/enums.py index f5b21971..05d8f525 100644 --- a/python/mujoco/introspect/enums.py +++ b/python/mujoco/introspect/enums.py @@ -731,6 +731,16 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjORIENTATION_EULER', 4), ]), )), + ('mjtConflict', + EnumDecl( + name='mjtConflict', + declname='enum mjtConflict_', + values=dict([ + ('mjCONFLICT_WARNING', 0), + ('mjCONFLICT_MERGE', 1), + ('mjCONFLICT_ERROR', 2), + ]), + )), ('mjtCTimer', EnumDecl( name='mjtCTimer', diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 478e564c..39dd1815 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -6977,6 +6977,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='align free joints with inertial frame', ), + StructFieldDecl( + name='conflict', + type=ValueType(name='int'), + doc='conflict resolution for attach (mjtConflict)', + ), StructFieldDecl( name='LRopt', type=ValueType(name='mjLROpt'), diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 282a0482..1a725097 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -20,6 +20,7 @@ import math import os import textwrap import typing +import warnings import zipfile # pylint: disable=unused-import from absl import flags @@ -1160,7 +1161,7 @@ class SpecsTest(absltest.TestCase): spec = mujoco.MjSpec() material = spec.add_material(name='mat') - texture = spec.add_texture( + spec.add_texture( name='tex', builtin=mujoco.mjtBuiltin.mjBUILTIN_FLAT, width=2, height=2 ) @@ -1795,7 +1796,9 @@ class SpecsTest(absltest.TestCase): sidesite = body.add_site(name='sidesite', pos=[2, 0, -5]) site4 = body.add_site(name='site4', pos=[0, 1, -6]) - sphere = spec.worldbody.add_geom(name='sphere', size=[.2, 0, 0], pos=[0, 0, -2]) + sphere = spec.worldbody.add_geom( + name='sphere', size=[0.2, 0, 0], pos=[0, 0, -2] + ) cylinder = spec.worldbody.add_geom( name='cylinder', @@ -2217,5 +2220,129 @@ class SpecsTest(absltest.TestCase): self.assertEqual(spec.authored.visual_quality, 0) self.assertEqual(spec.authored.visual_map, 0) + def test_attach_conflict_merge_procedural(self): + # Procedural attach with merge mode: min/max fields are merged. + parent = mujoco.MjSpec() + parent.compiler.conflict = mujoco.mjtConflict.mjCONFLICT_MERGE + parent.option.timestep = 0.005 + parent.option.iterations = 150 + parent.worldbody.add_geom().size[0] = 1 + + child = mujoco.MjSpec() + child.option.timestep = 0.001 # smaller -> wins (min-merge) + child.option.iterations = 200 # larger -> wins (max-merge) + child_body = child.worldbody.add_body() + child_body.add_geom().size[0] = 1 + + frame = parent.worldbody.add_frame() + frame.attach_body(child_body, prefix='child_') + + # merged values are applied immediately at attach time + self.assertEqual(parent.option.timestep, 0.001) + self.assertEqual(parent.option.iterations, 200) + + # compile succeeds with the merged values + model = parent.compile() + self.assertIsNotNone(model) + self.assertEqual(model.opt.timestep, 0.001) + self.assertEqual(model.opt.iterations, 200) + + def test_attach_conflict_error_procedural(self): + # Procedural attach with unmergeable conflict: raises ValueError, + # parent spec is unchanged. + parent = mujoco.MjSpec() + parent.compiler.conflict = mujoco.mjtConflict.mjCONFLICT_MERGE + parent.option.timestep = 0.005 + parent.option.integrator = mujoco.mjtIntegrator.mjINT_RK4 + + child = mujoco.MjSpec() + child.option.timestep = 0.001 + child.option.integrator = mujoco.mjtIntegrator.mjINT_IMPLICIT + child_body = child.worldbody.add_body() + child_body.add_geom().size[0] = 1 + + frame = parent.worldbody.add_frame() + + with self.assertRaisesRegex(ValueError, 'integrator'): + frame.attach_body(child_body, prefix='child_') + + # parent spec should be unchanged (two-pass guarantee) + self.assertEqual(parent.option.timestep, 0.005) + self.assertEqual(parent.option.integrator, mujoco.mjtIntegrator.mjINT_RK4) + + def test_attach_conflict_merge_xml(self): + # XML-based attach with merge mode: child timestep wins (min). + parent_xml = textwrap.dedent("""\ + + + + """) + + child_xml = textwrap.dedent("""\ + + + """) + + spec = mujoco.MjSpec.from_string( + parent_xml, + include={'child.xml': child_xml.encode()}, + ) + model = spec.compile() + self.assertIsNotNone(model) + self.assertEqual(model.opt.timestep, 0.001) # min + self.assertEqual(model.opt.iterations, 150) # max + + def test_attach_conflict_error_xml(self): + # XML-based attach with error mode: any conflict raises ValueError. + parent_xml = textwrap.dedent("""\ + + + + """) + + child_xml = textwrap.dedent("""\ + + + """) + + with self.assertRaisesRegex(ValueError, 'timestep'): + mujoco.MjSpec.from_string( + parent_xml, + include={'child.xml': child_xml.encode()}, + ) + + if __name__ == '__main__': absltest.main() diff --git a/python/mujoco/specs_wrapper.cc b/python/mujoco/specs_wrapper.cc index 049199e1..1b8efbc5 100644 --- a/python/mujoco/specs_wrapper.cc +++ b/python/mujoco/specs_wrapper.cc @@ -146,6 +146,7 @@ raw::MjModel* MjSpec::Compile(mjVFS* vfs) { warnings.attr("warn")(mjs_getWarning(ptr, i)); } } + return m; } diff --git a/src/user/CMakeLists.txt b/src/user/CMakeLists.txt index 67025afc..233c20b1 100644 --- a/src/user/CMakeLists.txt +++ b/src/user/CMakeLists.txt @@ -27,6 +27,8 @@ set(MUJOCO_USER_SRCS user_model.h user_objects.cc user_objects.h + user_resolver.cc + user_resolver.h user_resource.cc user_resource.h user_threadpool.cc diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 432be2f7..d5e61f7b 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -20,7 +20,7 @@ #include #include #include -#include +#include // NOLINT #include #include #include @@ -30,15 +30,16 @@ #include #include -#include #include #include +#include #include "engine/engine_support.h" #include "engine/engine_util_errmem.h" #include "user/user_cache.h" #include "user/user_flexcomp.h" #include "user/user_model.h" #include "user/user_objects.h" +#include "user/user_resolver.h" #include "user/user_resource.h" #include "user/user_util.h" @@ -382,7 +383,6 @@ static mjsElement* attachFrameToSite(mjCSite* parent, const mjCFrame* child, return attached_frame; } - mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child, const char* prefix, const char* suffix) { if (!parent) { @@ -394,6 +394,31 @@ mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child, return nullptr; } mjCModel* model = static_cast(mjs_getSpec(parent)->element); + const mjSpec* child_spec = nullptr; + if (child->elemtype == mjOBJ_MODEL) { + child_spec = &(static_cast(child)->spec); + } else { + child_spec = &(static_cast(child)->model->spec); + } + + // handle global attribute conflicts + if (child_spec && child_spec != &model->spec) { + std::string error_msg, warning_subject, warning_body; + bool success = mujoco::ResolveConflicts( + &model->spec, child_spec, + static_cast(model->spec.compiler.conflict), &error_msg, + &warning_subject, &warning_body); + + if (!success) { + model->SetError(mjCError(0, "%s", error_msg.c_str())); + return nullptr; + } + + if (!warning_body.empty()) { + model->AddGroupedWarning(warning_subject, warning_body); + } + } + if (child->elemtype == mjOBJ_MODEL) { mjCModel* child_model = static_cast((mjsElement*)child); mjsBody* worldbody = mjs_findBody(&child_model->spec, "world"); @@ -411,11 +436,12 @@ mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child, SetFrame(worldbody, mjOBJ_CAMERA, worldframe); child = worldframe->element; } + mjsElement* result = nullptr; switch (parent->elemtype) { case mjOBJ_FRAME: if (child->elemtype == mjOBJ_BODY) { - return attachBody(static_cast(parent), - static_cast(child), prefix, suffix); + result = attachBody(static_cast(parent), + static_cast(child), prefix, suffix); } else if (child->elemtype == mjOBJ_FRAME) { mjsBody* parent_body = mjs_getParent(parent); if (!parent_body) { @@ -429,35 +455,46 @@ mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child, if (mjs_setFrame(attached_frame, &frame->spec)) { return nullptr; } - return attached_frame; + result = attached_frame; } else { model->SetError(mjCError(0, "child element is not a body or frame")); return nullptr; } + break; case mjOBJ_BODY: if (child->elemtype == mjOBJ_FRAME) { - return attachFrame(static_cast(parent), - static_cast(child), prefix, suffix); + result = + attachFrame(static_cast(parent), + static_cast(child), prefix, suffix); } else { model->SetError(mjCError(0, "child element is not a frame")); return nullptr; } + break; case mjOBJ_SITE: if (child->elemtype == mjOBJ_BODY) { - return attachToSite(static_cast(parent), - static_cast(child), prefix, suffix); + result = + attachToSite(static_cast(parent), + static_cast(child), prefix, suffix); } else if (child->elemtype == mjOBJ_FRAME) { - return attachFrameToSite(static_cast(parent), - static_cast(child), prefix, suffix); + result = attachFrameToSite(static_cast(parent), + static_cast(child), prefix, + suffix); } else { model->SetError(mjCError(0, "child element is not a body or frame")); return nullptr; } + break; default: model->SetError(mjCError(0, "parent element is not a frame, body or site")); return nullptr; } - return nullptr; + + // mark all warnings accumulated so far as attach-phase + if (result) { + model->SetAttachWarningBoundary(); + } + return result; } diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 10eb4db2..e3867a41 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -1525,6 +1525,22 @@ void mjCModel::AddWarning(std::string msg, const mjCBase* obj) { warnings_.push_back(std::move(msg)); } +// add grouped warning with subject/body split (immediate delivery outside +// compile) +void mjCModel::AddGroupedWarning(const std::string& subject, + const std::string& body) { + std::string full = body.empty() ? subject : subject + "\n" + body; + warnings_.push_back(full); + + // outside compile: deliver immediately via structured log message + if (!compiling_) { + mjLogMessage m = {.level = mjLOG_WARNING}; + snprintf(m.subject, sizeof(m.subject), "%s", subject.c_str()); + m.body = body.empty() ? nullptr : body.c_str(); + mju_message(&m); + } +} + // pointer to world body mjCBody* mjCModel::GetWorld() { return bodies_[0]; @@ -5026,6 +5042,9 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { } disable_usethread(compiler.usethread); #endif + // clear compile-phase warnings from previous compile, keep attach warnings + ClearCompileWarnings(); + using Clock = std::chrono::steady_clock; using Seconds = std::chrono::duration; for (int i=0; i < mjNCTIMER; i++) { diff --git a/src/user/user_model.h b/src/user/user_model.h index b7dc3e2b..e72773c2 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -251,6 +251,8 @@ class mjCModel : public mjCModel_, private mjSpec { void SetError(const mjCError& error) { errInfo = error; } // set value of error object void AddWarning(std::string msg, // add warning to vector const mjCBase* obj = nullptr); + void AddGroupedWarning(const std::string& subject, // add grouped warning + const std::string& body); const std::vector& GetWarnings() const { // get accumulated warnings return warnings_; diff --git a/src/user/user_resolver.cc b/src/user/user_resolver.cc new file mode 100644 index 00000000..621afc26 --- /dev/null +++ b/src/user/user_resolver.cc @@ -0,0 +1,449 @@ +// 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. + +#include "user/user_resolver.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "user/user_api.h" + +namespace mujoco { +namespace { + +// format a numeric value for conflict messages +template +std::string fmtVal(T val) { + if constexpr (std::is_same_v) { + char buf[32]; + snprintf(buf, sizeof(buf), "%g", val); + return buf; + } else { + return std::to_string(val); + } +} + +// format an array of numeric values for conflict messages +std::string fmtArr(const mjtNum* val, int n) { + std::string s; + for (int i = 0; i < n; i++) { + if (i) s += ' '; + s += fmtVal(val[i]); + } + return s; +} + +// merge strategy for scalar conflict resolution +enum ResolveMerge { kMergeMin, kMergeMax, kMergeError }; + +// single-pass conflict resolver: accumulates errors, warnings, and deferred +// mutations; commits mutations only if no errors were found +struct Resolver { + mjtConflict mode; + mjSpec* parent; + const mjSpec* child; + std::vector errs; + std::vector warnings; + std::vector> ops; + + Resolver(mjtConflict mode, mjSpec* parent, const mjSpec* child) + : mode(mode), parent(parent), child(child) {} + + // resolve a scalar field conflict + template + void operator()(const char* name, T& pval, const T& cval, T dval, + ResolveMerge merge = kMergeError) { + if (pval == cval) return; + + // check authored status + bool parent_authored = mjs_isAuthored(parent, &pval); + bool child_authored = mjs_isAuthored(child, &cval); + + // fall back to default comparison for fields without authored tracking + if (!parent_authored && !child_authored) { + parent_authored = (pval != dval); + child_authored = (cval != dval); + } + + if (!child_authored) return; + + // "FIELD: parent has X, child has Y" + auto prefix = [&]() { + return std::string(name) + ": parent has " + fmtVal(pval) + + ", child has " + fmtVal(cval); + }; + + // only child authored: adopt or keep + if (!parent_authored) { + std::string p = prefix(); + if (mode == mjCONFLICT_MERGE) { + ops.push_back([&pval, cval]() { pval = cval; }); + warnings.push_back(p + ", adopting child value"); + } else { + warnings.push_back(p + ", keeping parent value"); + } + return; + } + + // both authored: dispatch by mode + switch (mode) { + case mjCONFLICT_WARNING: + warnings.push_back(prefix() + ", keeping parent value"); + break; + case mjCONFLICT_MERGE: + if (merge == kMergeError) { + errs.push_back(prefix()); + break; + } + { + T merged = + merge == kMergeMin ? std::min(pval, cval) : std::max(pval, cval); + const char* desc = merge == kMergeMin ? "the minimum" : "the maximum"; + warnings.push_back( + prefix() + + (merged == cval + ? std::string(", taking ") + desc + : std::string(", keeping parent value (") + desc + ")")); + ops.push_back([&pval, merged]() { pval = merged; }); + break; + } + case mjCONFLICT_ERROR: + errs.push_back(prefix()); + break; + } + } + + // resolve an unmergeable array field + template + void operator()(const char* name, mjtNum (&pval)[N], const mjtNum (&cval)[N], + const mjtNum (&dval)[N]) { + bool equal = true; + for (int i = 0; i < N; i++) { + if (pval[i] != cval[i]) { + equal = false; + break; + } + } + if (equal) return; + + // check authored status + bool parent_authored = mjs_isAuthored(parent, pval); + bool child_authored = mjs_isAuthored(child, cval); + + // fall back to default comparison for fields without authored tracking + if (!parent_authored && !child_authored) { + for (int i = 0; i < N; i++) { + if (pval[i] != dval[i]) { + parent_authored = true; + break; + } + } + for (int i = 0; i < N; i++) { + if (cval[i] != dval[i]) { + child_authored = true; + break; + } + } + } + + if (!child_authored) return; + + // "FIELD: parent has X Y Z, child has X Y Z" + auto prefix = [&]() { + return std::string(name) + ": parent has " + fmtArr(pval, N) + + ", child has " + fmtArr(cval, N); + }; + + // only child authored: adopt or keep + if (!parent_authored) { + std::string p = prefix(); + if (mode == mjCONFLICT_MERGE) { + std::array vals; + for (int i = 0; i < N; i++) vals[i] = cval[i]; + ops.push_back([&pval, vals]() { + for (int i = 0; i < N; i++) pval[i] = vals[i]; + }); + warnings.push_back(p + ", adopting child value"); + } else { + warnings.push_back(p + ", keeping parent value"); + } + return; + } + + // both authored: dispatch by mode + switch (mode) { + case mjCONFLICT_WARNING: + warnings.push_back(prefix() + ", keeping parent value"); + break; + case mjCONFLICT_MERGE: + case mjCONFLICT_ERROR: + errs.push_back(prefix()); + break; + } + } + + // resolve bitfield conflicts + void operator()(int& pval, int cval, const char* const names[], int nbit, + int pauth, int cauth) { + // fall back to treating all bits as authored if no tracking available + if (!pauth && !cauth) { + pauth = ~0; + cauth = ~0; + } + + // only consider bits that the child actually authored + int child_authored = cval & cauth; + if (!child_authored) return; + if (mode == mjCONFLICT_MERGE) { + int added = child_authored & ~pval; + for (int i = 0; i < nbit; i++) { + if ((added >> i) & 1) { + warnings.push_back(std::string("flag '") + names[i] + + "': added from child"); + } + } + ops.push_back([&pval, child_authored]() { pval |= child_authored; }); + return; + } + + // only report conflicts for bits where both parent and child authored + int both = pauth & cauth; + int diff = (pval ^ cval) & both; + for (int i = 0; i < nbit; i++) { + if ((diff >> i) & 1) { + bool parent_set = (pval >> i) & 1; + std::string msg = std::string("flag '") + names[i] + "': parent " + + (parent_set ? "set" : "unset") + ", child " + + (parent_set ? "unset" : "set"); + if (mode == mjCONFLICT_ERROR) { + errs.push_back(msg); + } else { + warnings.push_back(msg + ", keeping parent"); + } + } + } + } + + // resolve disableactuator bitfield conflicts + void operator()(int& pval, int cval, int pauth, int cauth) { + // fall back to treating all bits as authored if no tracking available + if (!pauth && !cauth) { + pauth = ~0; + cauth = ~0; + } + + // only consider bits that the child actually authored + int child_authored = cval & cauth; + if (!child_authored) return; + if (mode == mjCONFLICT_MERGE) { + for (int i = 0; i < mjNGROUP; i++) { + if (((child_authored & ~pval) >> i) & 1) { + char buf[64]; + snprintf(buf, sizeof(buf), + "disableactuator group %d: added from child", i); + warnings.push_back(buf); + } + } + ops.push_back([&pval, child_authored]() { pval |= child_authored; }); + return; + } + int both = pauth & cauth; + int diff = (pval ^ cval) & both; + for (int i = 0; i < mjNGROUP; i++) { + if ((diff >> i) & 1) { + char buf[128]; + bool parent_set = (pval >> i) & 1; + snprintf(buf, sizeof(buf), + "disableactuator group %d: parent %s, child %s", i, + parent_set ? "set" : "unset", parent_set ? "unset" : "set"); + if (mode == mjCONFLICT_ERROR) { + errs.push_back(buf); + } else { + warnings.push_back(std::string(buf) + ", keeping parent"); + } + } + } + } + + // apply deferred mutations (only if no errors) + bool Apply() { + if (!errs.empty()) return false; + for (auto& op : ops) op(); + return true; + } +}; + +// enumerate all conflictable fields, dispatching each to the resolver +void VisitConflicts(mjSpec* parent, const mjSpec* child, Resolver& r) { + // ==== mjOption ==== + mjOption d; + mj_defaultOption(&d); + mjOption& po = parent->option; + const mjOption& co = child->option; + + // min-merge fields + r("timestep", po.timestep, co.timestep, d.timestep, kMergeMin); + r("tolerance", po.tolerance, co.tolerance, d.tolerance, kMergeMin); + r("ls_tolerance", po.ls_tolerance, co.ls_tolerance, d.ls_tolerance, + kMergeMin); + r("noslip_tolerance", po.noslip_tolerance, co.noslip_tolerance, + d.noslip_tolerance, kMergeMin); + r("ccd_tolerance", po.ccd_tolerance, co.ccd_tolerance, d.ccd_tolerance, + kMergeMin); + r("sleep_tolerance", po.sleep_tolerance, co.sleep_tolerance, + d.sleep_tolerance, kMergeMin); + + // max-merge fields + r("iterations", po.iterations, co.iterations, d.iterations, kMergeMax); + r("ls_iterations", po.ls_iterations, co.ls_iterations, d.ls_iterations, + kMergeMax); + r("noslip_iterations", po.noslip_iterations, co.noslip_iterations, + d.noslip_iterations, kMergeMax); + r("ccd_iterations", po.ccd_iterations, co.ccd_iterations, d.ccd_iterations, + kMergeMax); + r("sdf_iterations", po.sdf_iterations, co.sdf_iterations, d.sdf_iterations, + kMergeMax); + r("sdf_initpoints", po.sdf_initpoints, co.sdf_initpoints, d.sdf_initpoints, + kMergeMax); + + // unmergeable scalars + r("impratio", po.impratio, co.impratio, d.impratio); + r("density", po.density, co.density, d.density); + r("viscosity", po.viscosity, co.viscosity, d.viscosity); + r("o_margin", po.o_margin, co.o_margin, d.o_margin); + r("integrator", po.integrator, co.integrator, d.integrator); + r("cone", po.cone, co.cone, d.cone); + r("jacobian", po.jacobian, co.jacobian, d.jacobian); + r("solver", po.solver, co.solver, d.solver); + + // unmergeable arrays + r("gravity", po.gravity, co.gravity, d.gravity); + r("wind", po.wind, co.wind, d.wind); + r("magnetic", po.magnetic, co.magnetic, d.magnetic); + r("o_solref", po.o_solref, co.o_solref, d.o_solref); + r("o_solimp", po.o_solimp, co.o_solimp, d.o_solimp); + r("o_friction", po.o_friction, co.o_friction, d.o_friction); + + // bitfields (pass authored bitmasks) + const mjsAuthored& pa = parent->authored; + const mjsAuthored& ca = child->authored; + r(po.disableflags, co.disableflags, mjDISABLESTRING, mjNDISABLE, + pa.disableflags, ca.disableflags); + r(po.enableflags, co.enableflags, mjENABLESTRING, mjNENABLE, pa.enableflags, + ca.enableflags); + r(po.disableactuator, co.disableactuator, pa.disableactuator, + ca.disableactuator); + + // ==== mjVisual ==== + mjVisual dv; + mj_defaultVisual(&dv); + mjVisual& pv = parent->visual; + const mjVisual& cv = child->visual; + + r("znear", pv.map.znear, cv.map.znear, dv.map.znear, kMergeMin); + r("realtime", pv.global.realtime, cv.global.realtime, dv.global.realtime, + kMergeMin); + r("zfar", pv.map.zfar, cv.map.zfar, dv.map.zfar, kMergeMax); + + // ==== mjSpec sizes (no authored tracking, uses default fallback) ==== + mjSpec ds; + mjs_defaultSpec(&ds); + mjSpec& ps = *parent; + const mjSpec& cs = *child; + + r("memory", ps.memory, cs.memory, ds.memory, kMergeMax); + r("njmax", ps.njmax, cs.njmax, ds.njmax, kMergeMax); + r("nconmax", ps.nconmax, cs.nconmax, ds.nconmax, kMergeMax); + r("nuserdata", ps.nuserdata, cs.nuserdata, ds.nuserdata, kMergeMax); + r("nkey", ps.nkey, cs.nkey, ds.nkey, kMergeMax); + r("nuser_body", ps.nuser_body, cs.nuser_body, ds.nuser_body, kMergeMax); + r("nuser_jnt", ps.nuser_jnt, cs.nuser_jnt, ds.nuser_jnt, kMergeMax); + r("nuser_geom", ps.nuser_geom, cs.nuser_geom, ds.nuser_geom, kMergeMax); + r("nuser_site", ps.nuser_site, cs.nuser_site, ds.nuser_site, kMergeMax); + r("nuser_cam", ps.nuser_cam, cs.nuser_cam, ds.nuser_cam, kMergeMax); + r("nuser_tendon", ps.nuser_tendon, cs.nuser_tendon, ds.nuser_tendon, + kMergeMax); + r("nuser_actuator", ps.nuser_actuator, cs.nuser_actuator, ds.nuser_actuator, + kMergeMax); + r("nuser_sensor", ps.nuser_sensor, cs.nuser_sensor, ds.nuser_sensor, + kMergeMax); +} + +// compose subject line for conflict messages +std::string ConflictSubject(const mjSpec* parent, const mjSpec* child) { + const char* mode_str = parent->compiler.conflict == mjCONFLICT_MERGE ? "merge" + : parent->compiler.conflict == mjCONFLICT_ERROR + ? "error" + : "warning"; + std::string pname = *parent->modelname; + std::string cname = *child->modelname; + bool has_parent = (pname != "MuJoCo Model" && !pname.empty()); + bool has_child = (cname != "MuJoCo Model" && !cname.empty()); + if (has_child && has_parent) { + return "Attach conflict when attaching '" + cname + "' to '" + pname + + "', policy is '" + mode_str + "'"; + } else if (has_child) { + return "Attach conflict when attaching '" + cname + "', policy is '" + + mode_str + "'"; + } else if (has_parent) { + return "Attach conflict when attaching to '" + pname + "', policy is '" + + mode_str + "'"; + } + return std::string("Attach conflict on attach, policy is '") + mode_str + "'"; +} + +} // namespace + +bool ResolveConflicts(mjSpec* parent, const mjSpec* child, mjtConflict mode, + std::string* error_msg, std::string* warning_subject, + std::string* warning_body) { + Resolver r(mode, parent, child); + VisitConflicts(parent, child, r); + + if (!r.Apply()) { + if (error_msg) { + *error_msg = ConflictSubject(parent, child); + for (const auto& e : r.errs) { + error_msg->append("\n"); + error_msg->append(e); + } + } + return false; + } + + if (!r.warnings.empty()) { + if (warning_subject) { + *warning_subject = ConflictSubject(parent, child); + } + if (warning_body) { + warning_body->clear(); + for (size_t i = 0; i < r.warnings.size(); ++i) { + if (i > 0) { + warning_body->append("\n"); + } + warning_body->append(r.warnings[i]); + } + } + } + return true; +} + +} // namespace mujoco diff --git a/src/user/user_resolver.h b/src/user/user_resolver.h new file mode 100644 index 00000000..e88c2c1a --- /dev/null +++ b/src/user/user_resolver.h @@ -0,0 +1,34 @@ +// 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. + +#ifndef MUJOCO_SRC_USER_USER_RESOLVER_H_ +#define MUJOCO_SRC_USER_USER_RESOLVER_H_ + +#include + +#include + +namespace mujoco { + +// Resolves global attribute conflicts between parent and child specs. +// Returns true on success (no errors). If errors are found, mutations are not +// applied, and 'error_msg' will contain the summary. If warnings are generated +// during resolution, 'warning_subject' and 'warning_body' will be populated. +bool ResolveConflicts(mjSpec* parent, const mjSpec* child, mjtConflict mode, + std::string* error_msg, std::string* warning_subject, + std::string* warning_body); + +} // namespace mujoco + +#endif // MUJOCO_SRC_USER_USER_RESOLVER_H_ diff --git a/src/xml/xml_base.h b/src/xml/xml_base.h index af636702..d3026376 100644 --- a/src/xml/xml_base.h +++ b/src/xml/xml_base.h @@ -48,6 +48,7 @@ extern const int stage_sz; extern const int datatype_sz; extern const int camout_sz; extern const int reduce_sz; +extern const int conflict_sz; extern const mjMap angle_map[]; extern const mjMap enable_map[]; extern const mjMap bool_map[]; @@ -86,7 +87,7 @@ extern const mjMap meshtype_map[]; extern const mjMap meshinertia_map[]; extern const mjMap flexself_map[]; extern const mjMap elastic2d_map[]; - +extern const mjMap conflict_map[]; //---------------------------------- Base XML class ------------------------------------------------ diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 817f8607..6b396efc 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -167,433 +167,664 @@ static void UpdateString(string& psuffix, int count, int i) { //---------------------------------- MJCF schema --------------------------------------------------- std::vector MJCF[nMJCF] = { -{"mujoco", "!", "model"}, -{"<"}, - {"compiler", "*", "autolimits", "boundmass", "boundinertia", "settotalmass", - "balanceinertia", "strippath", "coordinate", "angle", "fitaabb", "eulerseq", - "meshdir", "texturedir", "discardvisual", "usethread", "fusestatic", "inertiafromgeom", - "inertiagrouprange", "saveinertial", "assetdir", "alignfree"}, + {"mujoco", "!", "model"}, {"<"}, - {"lengthrange", "?", "mode", "useexisting", "uselimit", - "accel", "maxforce", "timeconst", "timestep", - "inttotal", "interval", "tolrange"}, + {"compiler", "*", + "autolimits", "boundmass", + "boundinertia", "settotalmass", + "balanceinertia", "strippath", + "coordinate", "angle", + "fitaabb", "eulerseq", + "meshdir", "texturedir", + "discardvisual", "usethread", + "fusestatic", "inertiafromgeom", + "inertiagrouprange", "saveinertial", + "assetdir", "alignfree", + "conflict"}, + {"<"}, + {"lengthrange", "?", "mode", "useexisting", "uselimit", "accel", "maxforce", + "timeconst", "timestep", "inttotal", "interval", "tolrange"}, {">"}, - {"option", "*", - "timestep", "impratio", "tolerance", "ls_tolerance", "noslip_tolerance", - "ccd_tolerance", "sleep_tolerance", "gravity", "wind", "magnetic", "density", "viscosity", - "o_margin", "o_solref", "o_solimp", "o_friction", - "integrator", "cone", "jacobian", - "solver", "iterations", "ls_iterations", "noslip_iterations", "ccd_iterations", - "sdf_iterations", "sdf_initpoints", "actuatorgroupdisable"}, + {"option", + "*", + "timestep", + "impratio", + "tolerance", + "ls_tolerance", + "noslip_tolerance", + "ccd_tolerance", + "sleep_tolerance", + "gravity", + "wind", + "magnetic", + "density", + "viscosity", + "o_margin", + "o_solref", + "o_solimp", + "o_friction", + "integrator", + "cone", + "jacobian", + "solver", + "iterations", + "ls_iterations", + "noslip_iterations", + "ccd_iterations", + "sdf_iterations", + "sdf_initpoints", + "actuatorgroupdisable"}, {"<"}, - {"flag", "?", "constraint", "equality", "frictionloss", "limit", "contact", - "spring", "damper", "gravity", "clampctrl", "warmstart", "filterparent", "actuation", - "refsafe", "sensor", "midphase", "eulerdamp", "autoreset", "nativeccd", "island", - "override", "energy", "fwdinv", "invdiscrete", "multiccd", "sleep", - "diagexact"}, + {"flag", "?", "constraint", "equality", "frictionloss", + "limit", "contact", "spring", "damper", "gravity", + "clampctrl", "warmstart", "filterparent", "actuation", "refsafe", + "sensor", "midphase", "eulerdamp", "autoreset", "nativeccd", + "island", "override", "energy", "fwdinv", "invdiscrete", + "multiccd", "sleep", "diagexact"}, {">"}, {"size", "*", "memory", "njmax", "nconmax", "nstack", "nuserdata", "nkey", - "nuser_body", "nuser_jnt", "nuser_geom", "nuser_site", "nuser_cam", - "nuser_tendon", "nuser_actuator", "nuser_sensor"}, + "nuser_body", "nuser_jnt", "nuser_geom", "nuser_site", "nuser_cam", + "nuser_tendon", "nuser_actuator", "nuser_sensor"}, {"visual", "*"}, {"<"}, - {"global", "?", "cameraid", "orthographic", "fovy", "ipd", "azimuth", "elevation", - "linewidth", "glow", "offwidth", "offheight", "realtime", "ellipsoidinertia", - "bvactive"}, - {"quality", "?", "shadowsize", "offsamples", "numslices", "numstacks", - "numquads"}, - {"headlight", "?", "ambient", "diffuse", "specular", "active"}, - {"map", "?", "stiffness", "stiffnessrot", "force", "torque", "alpha", - "fogstart", "fogend", "znear", "zfar", "haze", "shadowclip", "shadowscale", - "actuatortendon"}, - {"scale", "?", "forcewidth", "contactwidth", "contactheight", "connect", "com", - "camera", "light", "selectpoint", "jointlength", "jointwidth", "actuatorlength", - "actuatorwidth", "framelength", "framewidth", "constraint", "slidercrank", "frustum"}, - {"rgba", "?", "fog", "haze", "force", "inertia", "joint", - "actuator", "actuatornegative", "actuatorpositive", "com", - "camera", "light", "selectpoint", "connect", "contactpoint", "contactforce", - "contactfriction", "contacttorque", "contactgap", "rangefinder", - "constraint", "slidercrank", "crankbroken", "frustum", "bv", "bvactive"}, + {"global", "?", "cameraid", "orthographic", "fovy", "ipd", "azimuth", + "elevation", "linewidth", "glow", "offwidth", "offheight", "realtime", + "ellipsoidinertia", "bvactive"}, + {"quality", "?", "shadowsize", "offsamples", "numslices", "numstacks", + "numquads"}, + {"headlight", "?", "ambient", "diffuse", "specular", "active"}, + {"map", "?", "stiffness", "stiffnessrot", "force", "torque", "alpha", + "fogstart", "fogend", "znear", "zfar", "haze", "shadowclip", "shadowscale", + "actuatortendon"}, + {"scale", "?", "forcewidth", "contactwidth", "contactheight", "connect", + "com", "camera", "light", "selectpoint", "jointlength", "jointwidth", + "actuatorlength", "actuatorwidth", "framelength", "framewidth", + "constraint", "slidercrank", "frustum"}, + {"rgba", + "?", + "fog", + "haze", + "force", + "inertia", + "joint", + "actuator", + "actuatornegative", + "actuatorpositive", + "com", + "camera", + "light", + "selectpoint", + "connect", + "contactpoint", + "contactforce", + "contactfriction", + "contacttorque", + "contactgap", + "rangefinder", + "constraint", + "slidercrank", + "crankbroken", + "frustum", + "bv", + "bvactive"}, {">"}, - {"statistic", "*", "meaninertia", "meanmass", "meansize", "extent", "center"}, + {"statistic", "*", "meaninertia", "meanmass", "meansize", "extent", + "center"}, {"default", "R", "class"}, {"<"}, - {"mesh", "?", "scale", "maxhullvert", "inertia"}, - {"material", "?", "texture", "emission", "specular", "shininess", - "reflectance", "metallic", "roughness", "rgba", "texrepeat", "texuniform"}, - {"<"}, - {"layer", "*", "texture", "role"}, - {">"}, - {"joint", "?", "type", "group", "pos", "axis", "springdamper", - "limited", "actuatorfrclimited", "solreflimit", "solimplimit", - "solreffriction", "solimpfriction", "stiffness", "range", "actuatorfrcrange", - "actuatorgravcomp", "margin", "ref", "springref", "armature", "damping", - "frictionloss", "user"}, - {"geom", "?", "type", "pos", "quat", "contype", "conaffinity", "condim", - "group", "priority", "size", "material", "friction", "mass", "density", - "shellinertia", "solmix", "solref", "solimp", - "margin", "gap", "fromto", "axisangle", "xyaxes", "zaxis", "euler", - "hfield", "mesh", "fitscale", "rgba", "fluidshape", "fluidcoef", "user"}, - {"site", "?", "type", "group", "pos", "quat", "material", - "size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"}, - {"camera", "?", "projection", "fovy", "ipd", "resolution", "output", "pos", "quat", - "axisangle", "xyaxes", "zaxis", "euler", "mode", "focal", "focalpixel", - "principal", "principalpixel", "sensorsize", "user"}, - {"light", "?", "pos", "dir", "bulbradius", "intensity", "range", - "directional", "type", "castshadow", "active", "attenuation", "cutoff", "exponent", - "ambient", "diffuse", "specular", "mode"}, - {"pair", "?", "condim", "friction", "solref", "solreffriction", "solimp", - "gap", "margin"}, - {"equality", "?", "active", "solref", "solimp"}, - {"tendon", "?", "group", "limited", "range", - "solreflimit", "solimplimit", "solreffriction", "solimpfriction", - "frictionloss", "springlength", "width", "material", - "margin", "stiffness", "damping", "rgba", "user"}, - {"general", "?", "ctrllimited", "forcelimited", "actlimited", "ctrlrange", "forcerange", - "actrange", "gear", "damping", "armature", "cranklength", "user", "group", "nsample", - "interp", "delay", "actdim", "dyntype", "gaintype", "biastype", "dynprm", "gainprm", - "biasprm", "actearly"}, - {"motor", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", - "gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay"}, - {"position", "?", "ctrllimited", "forcelimited", "ctrlrange", "inheritrange", "forcerange", - "gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", - "delay", "kp", "kv", "dampratio", "timeconst"}, - {"velocity", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "gear", - "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay", "kv"}, - {"intvelocity", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "actrange", - "inheritrange", "gear", "damping", "armature", "cranklength", "user", "group", - "nsample", "interp", "delay", "kp", "kv", "dampratio"}, - {"damper", "?", "forcelimited", "ctrlrange", "forcerange", - "gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay", "kv"}, - {"cylinder", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", - "gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay", - "timeconst", "area", "diameter", "bias"}, - {"muscle", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", - "gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay", - "timeconst", "range", "force", "scale", - "lmin", "lmax", "vmax", "fpmax", "fvmax"}, - {"adhesion", "?", "forcelimited", "ctrlrange", "forcerange", - "gain", "user", "group", "nsample", "interp", "delay"}, - {"dcmotor", "?", "ctrllimited", "ctrlrange", - "gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay", - "motorconst", "resistance", "nominal", "saturation", - "inductance", "cogging", "controller", "input", "thermal", "lugre"}, + {"mesh", "?", "scale", "maxhullvert", "inertia"}, + {"material", "?", "texture", "emission", "specular", "shininess", + "reflectance", "metallic", "roughness", "rgba", "texrepeat", "texuniform"}, + {"<"}, + {"layer", "*", "texture", "role"}, + {">"}, + {"joint", + "?", + "type", + "group", + "pos", + "axis", + "springdamper", + "limited", + "actuatorfrclimited", + "solreflimit", + "solimplimit", + "solreffriction", + "solimpfriction", + "stiffness", + "range", + "actuatorfrcrange", + "actuatorgravcomp", + "margin", + "ref", + "springref", + "armature", + "damping", + "frictionloss", + "user"}, + {"geom", "?", "type", "pos", "quat", + "contype", "conaffinity", "condim", "group", "priority", + "size", "material", "friction", "mass", "density", + "shellinertia", "solmix", "solref", "solimp", "margin", + "gap", "fromto", "axisangle", "xyaxes", "zaxis", + "euler", "hfield", "mesh", "fitscale", "rgba", + "fluidshape", "fluidcoef", "user"}, + {"site", "?", "type", "group", "pos", "quat", "material", "size", "fromto", + "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"}, + {"camera", "?", "projection", + "fovy", "ipd", "resolution", + "output", "pos", "quat", + "axisangle", "xyaxes", "zaxis", + "euler", "mode", "focal", + "focalpixel", "principal", "principalpixel", + "sensorsize", "user"}, + {"light", "?", "pos", "dir", "bulbradius", "intensity", "range", + "directional", "type", "castshadow", "active", "attenuation", "cutoff", + "exponent", "ambient", "diffuse", "specular", "mode"}, + {"pair", "?", "condim", "friction", "solref", "solreffriction", "solimp", + "gap", "margin"}, + {"equality", "?", "active", "solref", "solimp"}, + {"tendon", "?", "group", "limited", "range", "solreflimit", "solimplimit", + "solreffriction", "solimpfriction", "frictionloss", "springlength", + "width", "material", "margin", "stiffness", "damping", "rgba", "user"}, + {"general", "?", "ctrllimited", "forcelimited", "actlimited", + "ctrlrange", "forcerange", "actrange", "gear", "damping", + "armature", "cranklength", "user", "group", "nsample", + "interp", "delay", "actdim", "dyntype", "gaintype", + "biastype", "dynprm", "gainprm", "biasprm", "actearly"}, + {"motor", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", + "gear", "damping", "armature", "cranklength", "user", "group", "nsample", + "interp", "delay"}, + {"position", "?", "ctrllimited", "forcelimited", "ctrlrange", + "inheritrange", "forcerange", "gear", "damping", "armature", + "cranklength", "user", "group", "nsample", "interp", + "delay", "kp", "kv", "dampratio", "timeconst"}, + {"velocity", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", + "gear", "damping", "armature", "cranklength", "user", "group", "nsample", + "interp", "delay", "kv"}, + {"intvelocity", "?", "ctrllimited", "forcelimited", "ctrlrange", + "forcerange", "actrange", "inheritrange", "gear", "damping", + "armature", "cranklength", "user", "group", "nsample", + "interp", "delay", "kp", "kv", "dampratio"}, + {"damper", "?", "forcelimited", "ctrlrange", "forcerange", "gear", + "damping", "armature", "cranklength", "user", "group", "nsample", "interp", + "delay", "kv"}, + {"cylinder", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", + "gear", "damping", "armature", "cranklength", "user", "group", "nsample", + "interp", "delay", "timeconst", "area", "diameter", "bias"}, + {"muscle", "?", "ctrllimited", "forcelimited", "ctrlrange", + "forcerange", "gear", "damping", "armature", "cranklength", + "user", "group", "nsample", "interp", "delay", + "timeconst", "range", "force", "scale", "lmin", + "lmax", "vmax", "fpmax", "fvmax"}, + {"adhesion", "?", "forcelimited", "ctrlrange", "forcerange", "gain", "user", + "group", "nsample", "interp", "delay"}, + {"dcmotor", "?", "ctrllimited", "ctrlrange", "gear", + "damping", "armature", "cranklength", "user", "group", + "nsample", "interp", "delay", "motorconst", "resistance", + "nominal", "saturation", "inductance", "cogging", "controller", + "input", "thermal", "lugre"}, {">"}, {"extension", "*"}, {"<"}, - {"plugin", "*", "plugin"}, - {"<"}, - {"instance", "*", "name"}, - {"<"}, - {"config", "*", "key", "value"}, - {">"}, - {">"}, + {"plugin", "*", "plugin"}, + {"<"}, + {"instance", "*", "name"}, + {"<"}, + {"config", "*", "key", "value"}, + {">"}, + {">"}, {">"}, {"custom", "*"}, {"<"}, - {"numeric", "*", "name", "size", "data"}, - {"text", "*", "name", "data"}, - {"tuple", "*", "name"}, - {"<"}, - {"element", "*", "objtype", "objname", "prm"}, - {">"}, + {"numeric", "*", "name", "size", "data"}, + {"text", "*", "name", "data"}, + {"tuple", "*", "name"}, + {"<"}, + {"element", "*", "objtype", "objname", "prm"}, + {">"}, {">"}, {"asset", "*"}, {"<"}, - {"mesh", "*", "name", "class", "content_type", "file", "vertex", "normal", - "texcoord", "face", "refpos", "refquat", "scale", "smoothnormal", - "maxhullvert", "inertia", "builtin", "params", "material"}, - {"<"}, - {"plugin", "*", "plugin", "instance"}, - {"<"}, - {"config", "*", "key", "value"}, - {">"}, - {">"}, - {"hfield", "*", "name", "content_type", "file", "nrow", "ncol", "size", "elevation"}, - {"skin", "*", "name", "file", "material", "rgba", "inflate", - "vertex", "texcoord", "face", "group"}, - {"<"}, - {"bone", "*", "body", "bindpos", "bindquat", "vertid", "vertweight"}, - {">"}, - {"texture", "*", "name", "type", "colorspace", "content_type", "file", "gridsize", - "gridlayout", "fileright", "fileleft", "fileup", "filedown", "filefront", "fileback", - "builtin", "rgb1", "rgb2", "mark", "markrgb", "random", "width", "height", - "hflip", "vflip", "nchannel"}, - {"material", "*", "name", "class", "texture", "texrepeat", "texuniform", - "emission", "specular", "shininess", "reflectance", "metallic", "roughness", "rgba"}, - {"<"}, - {"layer", "*", "texture", "role"}, - {">"}, - {"model", "*", "name", "file", "content_type"}, + {"mesh", "*", "name", "class", "content_type", "file", "vertex", "normal", + "texcoord", "face", "refpos", "refquat", "scale", "smoothnormal", + "maxhullvert", "inertia", "builtin", "params", "material"}, + {"<"}, + {"plugin", "*", "plugin", "instance"}, + {"<"}, + {"config", "*", "key", "value"}, + {">"}, + {">"}, + {"hfield", "*", "name", "content_type", "file", "nrow", "ncol", "size", + "elevation"}, + {"skin", "*", "name", "file", "material", "rgba", "inflate", "vertex", + "texcoord", "face", "group"}, + {"<"}, + {"bone", "*", "body", "bindpos", "bindquat", "vertid", "vertweight"}, + {">"}, + {"texture", "*", "name", "type", "colorspace", + "content_type", "file", "gridsize", "gridlayout", "fileright", + "fileleft", "fileup", "filedown", "filefront", "fileback", + "builtin", "rgb1", "rgb2", "mark", "markrgb", + "random", "width", "height", "hflip", "vflip", + "nchannel"}, + {"material", "*", "name", "class", "texture", "texrepeat", "texuniform", + "emission", "specular", "shininess", "reflectance", "metallic", + "roughness", "rgba"}, + {"<"}, + {"layer", "*", "texture", "role"}, + {">"}, + {"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", "user"}, {"<"}, - {"inertial", "?", "pos", "quat", "mass", "diaginertia", - "axisangle", "xyaxes", "zaxis", "euler", "fullinertia"}, - {"joint", "*", "name", "class", "type", "group", "pos", "axis", - "springdamper", "limited", "actuatorfrclimited", - "solreflimit", "solimplimit", "solreffriction", "solimpfriction", - "stiffness", "range", "actuatorfrcrange", "actuatorgravcomp", "margin", "ref", - "springref", "armature", "damping", "frictionloss", "user"}, - {"freejoint", "*", "name", "group", "align"}, - {"geom", "*", "name", "class", "type", "contype", "conaffinity", "condim", - "group", "priority", "size", "material", "friction", "mass", "density", - "shellinertia", "solmix", "solref", "solimp", - "margin", "gap", "fromto", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", - "hfield", "mesh", "fitscale", "rgba", "fluidshape", "fluidcoef", "user"}, - {"<"}, - {"plugin", "*", "plugin", "instance"}, - {"<"}, - {"config", "*", "key", "value"}, - {">"}, - {">"}, - {"attach", "*", "model", "body", "prefix"}, - {"site", "*", "name", "class", "type", "group", "pos", "quat", - "material", "size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"}, - {"camera", "*", "name", "class", "projection", "fovy", "ipd", "resolution", "output", "pos", - "quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "target", - "focal", "focalpixel", "principal", "principalpixel", "sensorsize", "user"}, - {"light", "*", "name", "class", "directional", "type", "castshadow", "active", - "pos", "dir", "bulbradius", "intensity", "range", "attenuation", "cutoff", - "exponent", "ambient", "diffuse", "specular", "mode", "target", "texture"}, - {"plugin", "*", "plugin", "instance"}, - {"<"}, - {"config", "*", "key", "value"}, - {">"}, - {"composite", "*", "prefix", "type", "count", "offset", - "vertex", "initial", "curve", "size", "quat"}, - {"<"}, - {"joint", "*", "kind", "group", "stiffness", "damping", "armature", - "solreffix", "solimpfix", "type", "axis", - "limited", "range", "margin", "solreflimit", "solimplimit", - "frictionloss", "solreffriction", "solimpfriction"}, - {"skin", "?", "texcoord", "material", "group", "rgba", "inflate", "subgrid"}, - {"geom", "?", "type", "contype", "conaffinity", "condim", - "group", "priority", "size", "material", "rgba", "friction", "mass", - "density", "solmix", "solref", "solimp", "margin", "gap"}, - {"site", "?", "group", "size", "material", "rgba"}, - {"plugin", "*", "plugin", "instance"}, - {"<"}, - {"config", "*", "key", "value"}, - {">"}, - {">"}, - {"flexcomp", "*", "name", "type", "group", "dim", "dof", - "count", "cellcount", "spacing", "radius", "rigid", "mass", "inertiabox", - "scale", "file", "point", "element", "texcoord", "material", "rgba", - "flatskin", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "origin"}, - {"<"}, - {"edge", "?", "equality", "solref", "solimp", "stiffness", "damping"}, - {"elasticity", "?", "young", "poisson", "damping", "thickness", "elastic2d"}, - {"contact", "?", "contype", "conaffinity", "condim", "priority", - "friction", "solmix", "solref", "solimp", "margin", "gap", - "internal", "selfcollide", "activelayers", "passive"}, - {"pin", "*", "id", "range", "grid", "gridrange"}, - {"plugin", "*", "plugin", "instance"}, - {"<"}, - {"config", "*", "key", "value"}, - {">"}, - {">"}, + {"inertial", "?", "pos", "quat", "mass", "diaginertia", "axisangle", + "xyaxes", "zaxis", "euler", "fullinertia"}, + {"joint", + "*", + "name", + "class", + "type", + "group", + "pos", + "axis", + "springdamper", + "limited", + "actuatorfrclimited", + "solreflimit", + "solimplimit", + "solreffriction", + "solimpfriction", + "stiffness", + "range", + "actuatorfrcrange", + "actuatorgravcomp", + "margin", + "ref", + "springref", + "armature", + "damping", + "frictionloss", + "user"}, + {"freejoint", "*", "name", "group", "align"}, + {"geom", "*", "name", "class", "type", + "contype", "conaffinity", "condim", "group", "priority", + "size", "material", "friction", "mass", "density", + "shellinertia", "solmix", "solref", "solimp", "margin", + "gap", "fromto", "pos", "quat", "axisangle", + "xyaxes", "zaxis", "euler", "hfield", "mesh", + "fitscale", "rgba", "fluidshape", "fluidcoef", "user"}, + {"<"}, + {"plugin", "*", "plugin", "instance"}, + {"<"}, + {"config", "*", "key", "value"}, + {">"}, + {">"}, + {"attach", "*", "model", "body", "prefix"}, + {"site", "*", "name", "class", "type", "group", "pos", "quat", "material", + "size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"}, + {"camera", "*", "name", "class", "projection", + "fovy", "ipd", "resolution", "output", "pos", + "quat", "axisangle", "xyaxes", "zaxis", "euler", + "mode", "target", "focal", "focalpixel", "principal", + "principalpixel", "sensorsize", "user"}, + {"light", "*", "name", "class", "directional", + "type", "castshadow", "active", "pos", "dir", + "bulbradius", "intensity", "range", "attenuation", "cutoff", + "exponent", "ambient", "diffuse", "specular", "mode", + "target", "texture"}, + {"plugin", "*", "plugin", "instance"}, + {"<"}, + {"config", "*", "key", "value"}, + {">"}, + {"composite", "*", "prefix", "type", "count", "offset", "vertex", "initial", + "curve", "size", "quat"}, + {"<"}, + {"joint", "*", "kind", "group", "stiffness", "damping", "armature", + "solreffix", "solimpfix", "type", "axis", "limited", "range", "margin", + "solreflimit", "solimplimit", "frictionloss", "solreffriction", + "solimpfriction"}, + {"skin", "?", "texcoord", "material", "group", "rgba", "inflate", + "subgrid"}, + {"geom", "?", "type", "contype", "conaffinity", "condim", "group", + "priority", "size", "material", "rgba", "friction", "mass", "density", + "solmix", "solref", "solimp", "margin", "gap"}, + {"site", "?", "group", "size", "material", "rgba"}, + {"plugin", "*", "plugin", "instance"}, + {"<"}, + {"config", "*", "key", "value"}, + {">"}, + {">"}, + {"flexcomp", "*", "name", "type", "group", "dim", + "dof", "count", "cellcount", "spacing", "radius", "rigid", + "mass", "inertiabox", "scale", "file", "point", "element", + "texcoord", "material", "rgba", "flatskin", "pos", "quat", + "axisangle", "xyaxes", "zaxis", "euler", "origin"}, + {"<"}, + {"edge", "?", "equality", "solref", "solimp", "stiffness", "damping"}, + {"elasticity", "?", "young", "poisson", "damping", "thickness", + "elastic2d"}, + {"contact", "?", "contype", "conaffinity", "condim", "priority", "friction", + "solmix", "solref", "solimp", "margin", "gap", "internal", "selfcollide", + "activelayers", "passive"}, + {"pin", "*", "id", "range", "grid", "gridrange"}, + {"plugin", "*", "plugin", "instance"}, + {"<"}, + {"config", "*", "key", "value"}, + {">"}, + {">"}, {">"}, {"deformable", "*"}, {"<"}, - {"flex", "*", "name", "group", "dim", "radius", "material", "rgba", "flatskin", "body", - "vertex", "element", "texcoord", "elemtexcoord", "node", "cellcount", "dof"}, - {"<"}, - {"contact", "?", "contype", "conaffinity", "condim", "priority", - "friction", "solmix", "solref", "solimp", "margin", "gap", - "internal", "selfcollide", "activelayers", "passive"}, - {"edge", "?", "stiffness", "damping"}, - {"elasticity", "?", "young", "poisson", "damping", "thickness", "elastic2d"}, - {">"}, - {"skin", "*", "name", "file", "material", "rgba", "inflate", - "vertex", "texcoord", "face", "group"}, - {"<"}, - {"bone", "*", "body", "bindpos", "bindquat", "vertid", "vertweight"}, - {">"}, + {"flex", "*", "name", "group", "dim", "radius", "material", "rgba", + "flatskin", "body", "vertex", "element", "texcoord", "elemtexcoord", + "node", "cellcount", "dof"}, + {"<"}, + {"contact", "?", "contype", "conaffinity", "condim", "priority", "friction", + "solmix", "solref", "solimp", "margin", "gap", "internal", "selfcollide", + "activelayers", "passive"}, + {"edge", "?", "stiffness", "damping"}, + {"elasticity", "?", "young", "poisson", "damping", "thickness", + "elastic2d"}, + {">"}, + {"skin", "*", "name", "file", "material", "rgba", "inflate", "vertex", + "texcoord", "face", "group"}, + {"<"}, + {"bone", "*", "body", "bindpos", "bindquat", "vertid", "vertweight"}, + {">"}, {">"}, {"contact", "*"}, {"<"}, - {"pair", "*", "name", "class", "geom1", "geom2", "condim", "friction", - "solref", "solreffriction", "solimp", "gap", "margin"}, - {"exclude", "*", "name", "body1", "body2"}, + {"pair", "*", "name", "class", "geom1", "geom2", "condim", "friction", + "solref", "solreffriction", "solimp", "gap", "margin"}, + {"exclude", "*", "name", "body1", "body2"}, {">"}, {"equality", "*"}, {"<"}, - {"connect", "*", "name", "class", "body1", "body2", "anchor", - "site1", "site2", "active", "solref", "solimp"}, - {"weld", "*", "name", "class", "body1", "body2", "relpose", "anchor", - "site1", "site2", "active", "solref", "solimp", "torquescale"}, - {"joint", "*", "name", "class", "joint1", "joint2", "polycoef", - "active", "solref", "solimp"}, - {"tendon", "*", "name", "class", "tendon1", "tendon2", "polycoef", - "active", "solref", "solimp"}, - {"flex", "*", "name", "class", "flex", - "active", "solref", "solimp"}, - {"flexvert", "*", "name", "class", "flex", - "active", "solref", "solimp"}, - {"flexstrain", "*", "name", "class", "flex", "cell", - "active", "solref", "solimp"}, + {"connect", "*", "name", "class", "body1", "body2", "anchor", "site1", + "site2", "active", "solref", "solimp"}, + {"weld", "*", "name", "class", "body1", "body2", "relpose", "anchor", + "site1", "site2", "active", "solref", "solimp", "torquescale"}, + {"joint", "*", "name", "class", "joint1", "joint2", "polycoef", "active", + "solref", "solimp"}, + {"tendon", "*", "name", "class", "tendon1", "tendon2", "polycoef", "active", + "solref", "solimp"}, + {"flex", "*", "name", "class", "flex", "active", "solref", "solimp"}, + {"flexvert", "*", "name", "class", "flex", "active", "solref", "solimp"}, + {"flexstrain", "*", "name", "class", "flex", "cell", "active", "solref", + "solimp"}, {">"}, {"tendon", "*"}, {"<"}, - {"spatial", "*", "name", "class", "group", "limited", "actuatorfrclimited", "range", - "actuatorfrcrange", "solreflimit", "solimplimit", "solreffriction", "solimpfriction", - "frictionloss", "springlength", "width", "material", - "margin", "stiffness", "damping", "armature", "rgba", "user"}, - {"<"}, - {"site", "*", "site"}, - {"geom", "*", "geom", "sidesite"}, - {"pulley", "*", "divisor"}, - {">"}, - {"fixed", "*", "name", "class", "group", "limited", "actuatorfrclimited", "range", - "actuatorfrcrange", "solreflimit", "solimplimit", "solreffriction", "solimpfriction", - "frictionloss", "springlength", "margin", "stiffness", "damping", "armature", "user"}, - {"<"}, - {"joint", "*", "joint", "coef"}, - {">"}, + {"spatial", + "*", + "name", + "class", + "group", + "limited", + "actuatorfrclimited", + "range", + "actuatorfrcrange", + "solreflimit", + "solimplimit", + "solreffriction", + "solimpfriction", + "frictionloss", + "springlength", + "width", + "material", + "margin", + "stiffness", + "damping", + "armature", + "rgba", + "user"}, + {"<"}, + {"site", "*", "site"}, + {"geom", "*", "geom", "sidesite"}, + {"pulley", "*", "divisor"}, + {">"}, + {"fixed", + "*", + "name", + "class", + "group", + "limited", + "actuatorfrclimited", + "range", + "actuatorfrcrange", + "solreflimit", + "solimplimit", + "solreffriction", + "solimpfriction", + "frictionloss", + "springlength", + "margin", + "stiffness", + "damping", + "armature", + "user"}, + {"<"}, + {"joint", "*", "joint", "coef"}, + {">"}, {">"}, {"actuator", "*"}, {"<"}, - {"general", "*", "name", "class", "group", "nsample", "interp", "delay", - "ctrllimited", "forcelimited", "actlimited", "ctrlrange", "forcerange", "actrange", - "lengthrange", "gear", "damping", "armature", "cranklength", "user", - "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", - "body", "actdim", "dyntype", "gaintype", "biastype", "dynprm", "gainprm", "biasprm", - "actearly"}, - {"motor", "*", "name", "class", "group", "nsample", "interp", "delay", - "ctrllimited", "forcelimited", "ctrlrange", "forcerange", - "lengthrange", "gear", "damping", "armature", "cranklength", "user", - "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite"}, - {"position", "*", "name", "class", "group", "nsample", "interp", "delay", - "ctrllimited", "forcelimited", "ctrlrange", "inheritrange", "forcerange", - "lengthrange", "gear", "damping", "armature", "cranklength", "user", - "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", - "kp", "kv", "dampratio", "timeconst"}, - {"velocity", "*", "name", "class", "group", "nsample", "interp", "delay", - "ctrllimited", "forcelimited", "ctrlrange", "forcerange", - "lengthrange", "gear", "damping", "armature", "cranklength", "user", - "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", - "kv"}, - {"intvelocity", "*", "name", "class", "group", "nsample", "interp", "delay", - "ctrllimited", "forcelimited", - "ctrlrange", "forcerange", "actrange", "inheritrange", "lengthrange", - "gear", "damping", "armature", "cranklength", "user", - "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", - "kp", "kv", "dampratio"}, - {"damper", "*", "name", "class", "group", "nsample", "interp", "delay", - "forcelimited", "ctrlrange", "forcerange", - "lengthrange", "gear", "damping", "armature", "cranklength", "user", - "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", - "kv"}, - {"cylinder", "*", "name", "class", "group", "nsample", "interp", "delay", - "ctrllimited", "forcelimited", "ctrlrange", "forcerange", - "lengthrange", "gear", "damping", "armature", "cranklength", "user", - "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", - "timeconst", "area", "diameter", "bias"}, - {"muscle", "*", "name", "class", "group", "nsample", "interp", "delay", - "ctrllimited", "forcelimited", "ctrlrange", "forcerange", - "lengthrange", "gear", "damping", "armature", "cranklength", "user", - "joint", "jointinparent", "tendon", "slidersite", "cranksite", - "timeconst", "tausmooth", "range", "force", "scale", - "lmin", "lmax", "vmax", "fpmax", "fvmax"}, - {"adhesion", "*", "name", "class", "group", "nsample", "interp", "delay", - "forcelimited", "ctrlrange", "forcerange", "user", "body", "gain"}, - {"dcmotor", "*", "name", "class", "group", "nsample", "interp", "delay", - "ctrllimited", "ctrlrange", - "lengthrange", "gear", "damping", "armature", "cranklength", "user", - "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", - "motorconst", "resistance", "nominal", "saturation", - "inductance", "cogging", "controller", "thermal", "lugre", "input"}, - {"plugin", "*", "name", "class", "plugin", "instance", "group", "nsample", "interp", "delay", - "ctrllimited", "forcelimited", "actlimited", "ctrlrange", "forcerange", "actrange", - "lengthrange", "gear", "damping", "armature", "cranklength", "joint", "jointinparent", - "site", "actdim", "dyntype", "dynprm", "tendon", "cranksite", "slidersite", "user", - "actearly"}, - {"<"}, - {"config", "*", "key", "value"}, - {">"}, + {"general", "*", "name", + "class", "group", "nsample", + "interp", "delay", "ctrllimited", + "forcelimited", "actlimited", "ctrlrange", + "forcerange", "actrange", "lengthrange", + "gear", "damping", "armature", + "cranklength", "user", "joint", + "jointinparent", "tendon", "slidersite", + "cranksite", "site", "refsite", + "body", "actdim", "dyntype", + "gaintype", "biastype", "dynprm", + "gainprm", "biasprm", "actearly"}, + {"motor", "*", "name", "class", "group", + "nsample", "interp", "delay", "ctrllimited", "forcelimited", + "ctrlrange", "forcerange", "lengthrange", "gear", "damping", + "armature", "cranklength", "user", "joint", "jointinparent", + "tendon", "slidersite", "cranksite", "site", "refsite"}, + {"position", "*", "name", + "class", "group", "nsample", + "interp", "delay", "ctrllimited", + "forcelimited", "ctrlrange", "inheritrange", + "forcerange", "lengthrange", "gear", + "damping", "armature", "cranklength", + "user", "joint", "jointinparent", + "tendon", "slidersite", "cranksite", + "site", "refsite", "kp", + "kv", "dampratio", "timeconst"}, + {"velocity", "*", "name", "class", "group", + "nsample", "interp", "delay", "ctrllimited", "forcelimited", + "ctrlrange", "forcerange", "lengthrange", "gear", "damping", + "armature", "cranklength", "user", "joint", "jointinparent", + "tendon", "slidersite", "cranksite", "site", "refsite", + "kv"}, + {"intvelocity", "*", + "name", "class", + "group", "nsample", + "interp", "delay", + "ctrllimited", "forcelimited", + "ctrlrange", "forcerange", + "actrange", "inheritrange", + "lengthrange", "gear", + "damping", "armature", + "cranklength", "user", + "joint", "jointinparent", + "tendon", "slidersite", + "cranksite", "site", + "refsite", "kp", + "kv", "dampratio"}, + {"damper", "*", "name", "class", "group", + "nsample", "interp", "delay", "forcelimited", "ctrlrange", + "forcerange", "lengthrange", "gear", "damping", "armature", + "cranklength", "user", "joint", "jointinparent", "tendon", + "slidersite", "cranksite", "site", "refsite", "kv"}, + {"cylinder", "*", "name", "class", "group", + "nsample", "interp", "delay", "ctrllimited", "forcelimited", + "ctrlrange", "forcerange", "lengthrange", "gear", "damping", + "armature", "cranklength", "user", "joint", "jointinparent", + "tendon", "slidersite", "cranksite", "site", "refsite", + "timeconst", "area", "diameter", "bias"}, + {"muscle", "*", "name", "class", "group", + "nsample", "interp", "delay", "ctrllimited", "forcelimited", + "ctrlrange", "forcerange", "lengthrange", "gear", "damping", + "armature", "cranklength", "user", "joint", "jointinparent", + "tendon", "slidersite", "cranksite", "timeconst", "tausmooth", + "range", "force", "scale", "lmin", "lmax", + "vmax", "fpmax", "fvmax"}, + {"adhesion", "*", "name", "class", "group", "nsample", "interp", "delay", + "forcelimited", "ctrlrange", "forcerange", "user", "body", "gain"}, + {"dcmotor", "*", "name", "class", "group", + "nsample", "interp", "delay", "ctrllimited", "ctrlrange", + "lengthrange", "gear", "damping", "armature", "cranklength", + "user", "joint", "jointinparent", "tendon", "slidersite", + "cranksite", "site", "refsite", "motorconst", "resistance", + "nominal", "saturation", "inductance", "cogging", "controller", + "thermal", "lugre", "input"}, + {"plugin", "*", "name", "class", + "plugin", "instance", "group", "nsample", + "interp", "delay", "ctrllimited", "forcelimited", + "actlimited", "ctrlrange", "forcerange", "actrange", + "lengthrange", "gear", "damping", "armature", + "cranklength", "joint", "jointinparent", "site", + "actdim", "dyntype", "dynprm", "tendon", + "cranksite", "slidersite", "user", "actearly"}, + {"<"}, + {"config", "*", "key", "value"}, + {">"}, {">"}, {"sensor", "*"}, {"<"}, - {"touch", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"accelerometer", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"velocimeter", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"gyro", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"force", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"torque", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"magnetometer", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"camprojection", "*", "name", "site", "camera", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"rangefinder", "*", "name", "site", "camera", "data", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"jointpos", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"jointvel", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"tendonpos", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"tendonvel", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"actuatorpos", "*", "name", "actuator", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"actuatorvel", "*", "name", "actuator", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"actuatorfrc", "*", "name", "actuator", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"jointactuatorfrc", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"tendonactuatorfrc", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"ballquat", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"ballangvel", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"jointlimitpos", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"jointlimitvel", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"jointlimitfrc", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"tendonlimitpos", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"tendonlimitvel", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"tendonlimitfrc", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"framepos", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"framequat", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"framexaxis", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"frameyaxis", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"framezaxis", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"framelinvel", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"frameangvel", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"framelinacc", "*", "name", "objtype", "objname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"frameangacc", "*", "name", "objtype", "objname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"subtreecom", "*", "name", "body", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"subtreelinvel", "*", "name", "body", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"subtreeangmom", "*", "name", "body", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"insidesite", "*", "name", "site", "objtype", "objname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"distance", "*", "name", "geom1", "geom2", "body1", "body2", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"normal", "*", "name", "geom1", "geom2", "body1", "body2", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"fromto", "*", "name", "geom1", "geom2", "body1", "body2", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"contact", "*", "name", "geom1", "geom2", "body1", "body2", "subtree1", "subtree2", "site", - "num", "data", "reduce", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"e_potential", "*", "name", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"e_kinetic", "*", "name", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"clock", "*", "name", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, - {"tactile", "*", "name", "geom", "mesh", "nsample", "interp", "delay", "interval", "user"}, - {"user", "*", "name", "objtype", "objname", "datatype", "needstage", - "dim", "cutoff", "noise", "user"}, - {"plugin", "*", "name", "plugin", "instance", "cutoff", "objtype", "objname", "reftype", "refname", - "user"}, - {"<"}, - {"config", "*", "key", "value"}, - {">"}, + {"touch", "*", "name", "site", "nsample", "interp", "delay", "interval", + "cutoff", "noise", "user"}, + {"accelerometer", "*", "name", "site", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"velocimeter", "*", "name", "site", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"gyro", "*", "name", "site", "nsample", "interp", "delay", "interval", + "cutoff", "noise", "user"}, + {"force", "*", "name", "site", "nsample", "interp", "delay", "interval", + "cutoff", "noise", "user"}, + {"torque", "*", "name", "site", "nsample", "interp", "delay", "interval", + "cutoff", "noise", "user"}, + {"magnetometer", "*", "name", "site", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"camprojection", "*", "name", "site", "camera", "nsample", "interp", + "delay", "interval", "cutoff", "noise", "user"}, + {"rangefinder", "*", "name", "site", "camera", "data", "nsample", "interp", + "delay", "interval", "cutoff", "noise", "user"}, + {"jointpos", "*", "name", "joint", "nsample", "interp", "delay", "interval", + "cutoff", "noise", "user"}, + {"jointvel", "*", "name", "joint", "nsample", "interp", "delay", "interval", + "cutoff", "noise", "user"}, + {"tendonpos", "*", "name", "tendon", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"tendonvel", "*", "name", "tendon", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"actuatorpos", "*", "name", "actuator", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"actuatorvel", "*", "name", "actuator", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"actuatorfrc", "*", "name", "actuator", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"jointactuatorfrc", "*", "name", "joint", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"tendonactuatorfrc", "*", "name", "tendon", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"ballquat", "*", "name", "joint", "nsample", "interp", "delay", "interval", + "cutoff", "noise", "user"}, + {"ballangvel", "*", "name", "joint", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"jointlimitpos", "*", "name", "joint", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"jointlimitvel", "*", "name", "joint", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"jointlimitfrc", "*", "name", "joint", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"tendonlimitpos", "*", "name", "tendon", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"tendonlimitvel", "*", "name", "tendon", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"tendonlimitfrc", "*", "name", "tendon", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"framepos", "*", "name", "objtype", "objname", "reftype", "refname", + "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"framequat", "*", "name", "objtype", "objname", "reftype", "refname", + "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"framexaxis", "*", "name", "objtype", "objname", "reftype", "refname", + "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"frameyaxis", "*", "name", "objtype", "objname", "reftype", "refname", + "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"framezaxis", "*", "name", "objtype", "objname", "reftype", "refname", + "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"framelinvel", "*", "name", "objtype", "objname", "reftype", "refname", + "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"frameangvel", "*", "name", "objtype", "objname", "reftype", "refname", + "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"framelinacc", "*", "name", "objtype", "objname", "nsample", "interp", + "delay", "interval", "cutoff", "noise", "user"}, + {"frameangacc", "*", "name", "objtype", "objname", "nsample", "interp", + "delay", "interval", "cutoff", "noise", "user"}, + {"subtreecom", "*", "name", "body", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"subtreelinvel", "*", "name", "body", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"subtreeangmom", "*", "name", "body", "nsample", "interp", "delay", + "interval", "cutoff", "noise", "user"}, + {"insidesite", "*", "name", "site", "objtype", "objname", "nsample", + "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"distance", "*", "name", "geom1", "geom2", "body1", "body2", "nsample", + "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"normal", "*", "name", "geom1", "geom2", "body1", "body2", "nsample", + "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"fromto", "*", "name", "geom1", "geom2", "body1", "body2", "nsample", + "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"contact", "*", "name", "geom1", "geom2", "body1", "body2", + "subtree1", "subtree2", "site", "num", "data", "reduce", "nsample", + "interp", "delay", "interval", "cutoff", "noise", "user"}, + {"e_potential", "*", "name", "nsample", "interp", "delay", "interval", + "cutoff", "noise", "user"}, + {"e_kinetic", "*", "name", "nsample", "interp", "delay", "interval", + "cutoff", "noise", "user"}, + {"clock", "*", "name", "nsample", "interp", "delay", "interval", "cutoff", + "noise", "user"}, + {"tactile", "*", "name", "geom", "mesh", "nsample", "interp", "delay", + "interval", "user"}, + {"user", "*", "name", "objtype", "objname", "datatype", "needstage", "dim", + "cutoff", "noise", "user"}, + {"plugin", "*", "name", "plugin", "instance", "cutoff", "objtype", + "objname", "reftype", "refname", "user"}, + {"<"}, + {"config", "*", "key", "value"}, + {">"}, {">"}, {"keyframe", "*"}, {"<"}, - {"key", "*", "name", "time", "qpos", "qvel", "act", "mpos", "mquat", "ctrl"}, + {"key", "*", "name", "time", "qpos", "qvel", "act", "mpos", "mquat", + "ctrl"}, {">"}, -{">"} -}; + {">"}}; @@ -918,6 +1149,11 @@ const mjMap reduce_map[reduce_sz] = { {"netforce", 3} }; +// conflict resolution type +const int conflict_sz = 3; +const mjMap conflict_map[conflict_sz] = {{"warning", mjCONFLICT_WARNING}, + {"merge", mjCONFLICT_MERGE}, + {"error", mjCONFLICT_ERROR}}; // LR mode const int lrmode_sz = 4; @@ -1226,6 +1462,8 @@ void mjXReader::Compiler(XMLElement* section, mjSpec* s) { read("inertiagrouprange", 2, s->compiler.inertiagrouprange); read("alignfree", s->compiler.alignfree, bool_map, 2); read("saveinertial", s->compiler.saveinertial, bool_map, 2); + MapValue(section, "conflict", &s->compiler.conflict, conflict_map, + conflict_sz); // lengthrange subelement XMLElement* elem = FindSubElem(section, "lengthrange"); @@ -3636,7 +3874,13 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { } } - +// strip the "Error: " prefix from compiler/spec error messages +static const char* stripError(const char* err) { + if (err && std::strncmp(err, "Error: ", 7) == 0) { + return err + 7; + } + return err; +} // body/world section parser; recursive void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame, @@ -3863,13 +4107,13 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame, // attach to parent if (!mjs_attach(body->element, pframe->element, /*prefix=*/"", suffix.c_str())) { - throw mjXError(elem, "%s", mjs_getError(spec)); + throw mjXError(elem, "%s", stripError(mjs_getError(spec))); } } // delete subtree if (mjs_delete(spec, subtree->element)) { - throw mjXError(elem, "%s", mjs_getError(spec)); + throw mjXError(elem, "%s", stripError(mjs_getError(spec))); } } @@ -3950,12 +4194,12 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame, child = child_body->element; } if (!mjs_attach(pframe->element, child, prefix.c_str(), "")) { - throw mjXError(elem, "%s", mjs_getError(spec)); + throw mjXError(elem, "%s", stripError(mjs_getError(spec))); } } else { // only set frame to existing body if (mjs_setFrame(child_body->element, pframe)) { - throw mjXError(elem, "%s", mjs_getError(spec)); + throw mjXError(elem, "%s", stripError(mjs_getError(spec))); } } } diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index d541872a..ba9e9485 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -1034,6 +1034,8 @@ void mjXWriter::Compiler(XMLElement* root) { if (!model->compiler.autolimits) { WriteAttrTxt(section, "autolimits", "false"); } + WriteAttrKey(section, "conflict", conflict_map, conflict_sz, + model->compiler.conflict, mjCONFLICT_WARNING); } diff --git a/test/fixture.cc b/test/fixture.cc index 26d0d4d1..4e720651 100644 --- a/test/fixture.cc +++ b/test/fixture.cc @@ -51,6 +51,7 @@ using ::testing::Truly; bool IsBenignWarning(const std::string& msg) { static const char* const kBenignWarnings[] = { "is not rigid and has no equality constraints", + "Attach conflict", }; for (const char* warning : kBenignWarnings) { if (absl::StrContains(msg, warning)) { diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index cf0de3a8..04e4a4ff 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -2668,18 +2668,17 @@ void AttachNestedKeyframe(bool compile) { // compile required before further attachment mjModel* m_child = compile ? mj_compile(child, 0) : nullptr; - // check warning is issued, empty for a compiled model - static char warning[1024]; - warning[0] = '\0'; - mju_user_warning = [](const char* msg) { - util::strcpy_arr(warning, msg); - }; - // attach child to parent mjs_attach(mjs_findFrame(parent, "frame")->element, mjs_findBody(child, "body")->element, "child-", ""); - EXPECT_THAT(warning, HasSubstr(compile ? "" : "model has pending keyframes")); + if (compile) { + EXPECT_EQ(mjs_numWarnings(parent), 0); + } else { + EXPECT_GE(mjs_numWarnings(parent), 1); + EXPECT_THAT(mjs_getWarning(parent, 0), + HasSubstr("model has pending keyframes")); + } // compare models mjtNum tol = 0; @@ -2702,6 +2701,7 @@ void AttachNestedKeyframe(bool compile) { } TEST_F(MujocoTest, TestAttachNestedKeyframe) { + mock_warning_handler.ExpectWarnings(); AttachNestedKeyframe(/*compile=*/true); AttachNestedKeyframe(/*compile=*/false); } @@ -3454,5 +3454,98 @@ TEST_F(MujocoTest, CompileWarningChainedToHandler) { mj_deleteSpec(spec); } +TEST_F(MujocoTest, WarningAccumulationAndRetrieval) { + mock_warning_handler.ExpectWarnings(); + static constexpr char xml_parent[] = R"( + + + + + + )"; + + static constexpr char xml_child[] = R"( + + + + + + + + )"; + + static constexpr char xml_gchild[] = R"( + + + + + + + + + )"; + + std::array er; + mjSpec* parent = mj_parseXMLString(xml_parent, 0, er.data(), er.size()); + ASSERT_THAT(parent, NotNull()) << er.data(); + mjSpec* child = mj_parseXMLString(xml_child, 0, er.data(), er.size()); + ASSERT_THAT(child, NotNull()) << er.data(); + mjSpec* gchild = mj_parseXMLString(xml_gchild, 0, er.data(), er.size()); + ASSERT_THAT(gchild, NotNull()) << er.data(); + + mjs_setDeepCopy(parent, true); + mjs_setDeepCopy(child, true); + mjs_setDeepCopy(gchild, true); + + mjs_attach(mjs_findFrame(child, "child_frame")->element, + mjs_findBody(gchild, "gchild")->element, "gchild-", ""); + + mjs_attach(mjs_findFrame(parent, "parent")->element, + mjs_findBody(child, "child")->element, "child-", ""); + + EXPECT_EQ(mjs_numWarnings(parent), 1); + EXPECT_THAT(mjs_getWarning(parent, 0), + HasSubstr("Child model has pending keyframes")); + EXPECT_THAT(mjs_getWarning(parent, 1), IsNull()); + + mj_deleteSpec(parent); + mj_deleteSpec(child); + mj_deleteSpec(gchild); +} + +TEST_F(MujocoTest, CompilationWarningsClearedOnRecompile) { + mock_warning_handler.ExpectWarnings(); + // flex with no edge stiffness or equality triggers passive forces warning + static constexpr char xml[] = R"( + + + + + + )"; + + std::array er; + mjSpec* spec = mj_parseXMLString(xml, 0, er.data(), er.size()); + ASSERT_THAT(spec, NotNull()) << er.data(); + + // first compile: should generate flex warning + mjModel* m = mj_compile(spec, nullptr); + ASSERT_THAT(m, NotNull()); + int n1 = mjs_numWarnings(spec); + EXPECT_EQ(n1, 1); + EXPECT_THAT(mjs_getWarning(spec, 0), + HasSubstr("no equality constraints or passive forces")); + + // recompile: warnings should be cleared and regenerated + mj_deleteModel(m); + m = mj_compile(spec, nullptr); + ASSERT_THAT(m, NotNull()); + EXPECT_EQ(mjs_numWarnings(spec), n1); + + mj_deleteModel(m); + mj_deleteSpec(spec); +} + } // namespace } // namespace mujoco diff --git a/test/user/user_recompile_test.cc b/test/user/user_recompile_test.cc index f01c306e..e9a8c286 100644 --- a/test/user/user_recompile_test.cc +++ b/test/user/user_recompile_test.cc @@ -47,8 +47,9 @@ std::vector GetRecompileTestModels() { if (absl::StrContains(xml, "malformed_") || absl::StrContains(xml, "_fail") || absl::StrContains(xml, "touch_grid") || - absl::StrContains(xml, "perf") || - absl::StrContains(xml, "cow")) { + absl::StrContains(xml, "perf") || absl::StrContains(xml, "cow") || + // exclude conflict test assets (designed to fail compile) + absl::StrContains(xml, "xml/testdata/parent_")) { continue; } models.push_back(xml); diff --git a/test/user/user_resolver_test.cc b/test/user/user_resolver_test.cc new file mode 100644 index 00000000..7bbcf346 --- /dev/null +++ b/test/user/user_resolver_test.cc @@ -0,0 +1,918 @@ +// 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. + +#include +#include + +#include +#include +#include +#include +#include "src/xml/xml_api.h" +#include "test/fixture.h" + +namespace mujoco { +namespace { + +using ::testing::HasSubstr; +using ::testing::IsNull; +using ::testing::NotNull; + +TEST_F(MujocoTest, AttachWarningsSurviveRecompile) { + mock_warning_handler.ExpectWarnings(); + // attach warnings should persist through recompilation + static constexpr char xml_parent[] = R"( + + + )"; + + static constexpr char xml_child[] = R"( + + + )"; + + std::array er; + mjSpec* parent = mj_parseXMLString(xml_parent, 0, er.data(), er.size()); + ASSERT_THAT(parent, NotNull()) << er.data(); + mjSpec* child = mj_parseXMLString(xml_child, 0, er.data(), er.size()); + ASSERT_THAT(child, NotNull()) << er.data(); + + mjsFrame* frame = mjs_findFrame(parent, "attachment"); + ASSERT_THAT(frame, NotNull()); + mjsElement* attached = mjs_attach( + frame->element, mjs_findBody(child, "child")->element, "child-", ""); + ASSERT_THAT(attached, NotNull()); + + // attach warning should be present + int attach_warnings = mjs_numWarnings(parent); + EXPECT_GE(attach_warnings, 1); + EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep")); + + // compile + mjModel* m = mj_compile(parent, nullptr); + ASSERT_THAT(m, NotNull()) << mjs_getError(parent); + + // attach warnings should survive compilation + EXPECT_GE(mjs_numWarnings(parent), attach_warnings); + EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep")); + + // recompile + mj_deleteModel(m); + m = mj_compile(parent, nullptr); + ASSERT_THAT(m, NotNull()) << mjs_getError(parent); + + // attach warnings should still be there after recompile + EXPECT_GE(mjs_numWarnings(parent), attach_warnings); + EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep")); + + mj_deleteModel(m); + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictWarningDefault) { + mock_warning_handler.ExpectWarnings(); + // default mode (warning): conflicting values should keep parent, set warning + + mjSpec* parent = mj_makeSpec(); + parent->option.timestep = 0.005; + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + // parent value should be unchanged + EXPECT_EQ(parent->option.timestep, mjtNum(0.005)); + + // one grouped warning per attach + EXPECT_EQ(mjs_numWarnings(parent), 1); + std::string w = mjs_getWarning(parent, 0); + EXPECT_THAT(w, HasSubstr("policy is 'warning'")); + EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.001," + " keeping parent value")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictWarningNoConflict) { + mock_warning_handler.ExpectWarnings(); + // warning mode: child has non-default, parent is default → warn + keep parent + mjSpec* parent = mj_makeSpec(); + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + // warning produced when child is non-default + EXPECT_TRUE(mjs_isWarning(parent)); + EXPECT_EQ(mjs_numWarnings(parent), 1); + EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictMergeMin) { + mock_warning_handler.ExpectWarnings(); + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_MERGE; + parent->option.timestep = 0.005; + parent->option.tolerance = 1e-10; + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; + child->option.tolerance = 1e-12; + child->option.sleep_tolerance = 0.005; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + // min-fields should take the smaller value + EXPECT_EQ(parent->option.timestep, mjtNum(0.001)); + EXPECT_EQ(parent->option.tolerance, mjtNum(1e-12)); + // one side is default -> copy and warn + EXPECT_EQ(parent->option.sleep_tolerance, mjtNum(0.005)); + + // one grouped warning containing all three fields + EXPECT_EQ(mjs_numWarnings(parent), 1); + std::string w = mjs_getWarning(parent, 0); + EXPECT_THAT(w, HasSubstr("policy is 'merge'")); + EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.001," + " taking the minimum")); + EXPECT_THAT(w, HasSubstr("tolerance")); + EXPECT_THAT(w, HasSubstr("sleep_tolerance")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictMergeMax) { + mock_warning_handler.ExpectWarnings(); + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_MERGE; + parent->option.iterations = 150; + parent->nkey = 5; + parent->nuser_body = 2; + + mjSpec* child = mj_makeSpec(); + child->option.iterations = 200; + child->nkey = 8; + child->nuser_body = 4; + + mjsBody* child_body = mjs_addBody(mjs_findBody(child, "world"), 0); + mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr); + + mjsElement* attached = + mjs_attach(frame->element, child_body->element, "child_", ""); + ASSERT_THAT(attached, NotNull()); + + // max-fields should take the larger value + EXPECT_EQ(parent->option.iterations, 200); + EXPECT_EQ(parent->nkey, 8); + EXPECT_EQ(parent->nuser_body, 4); + + // one grouped warning containing all three fields + EXPECT_EQ(mjs_numWarnings(parent), 1); + std::string w = mjs_getWarning(parent, 0); + EXPECT_THAT(w, HasSubstr("iterations: parent has 150, child has 200," + " taking the maximum")); + EXPECT_THAT(w, HasSubstr("nkey")); + EXPECT_THAT(w, HasSubstr("nuser_body")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictMergeError) { + // merge mode: conflicting error-fields should produce an error + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_MERGE; + parent->option.integrator = mjINT_RK4; + + mjSpec* child = mj_makeSpec(); + child->option.integrator = mjINT_IMPLICIT; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + + // error-fields should fail on conflict + EXPECT_THAT(attached, IsNull()); + EXPECT_THAT(mjs_getError(parent), HasSubstr("integrator: parent has")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictErrorMode) { + // error mode: any conflict -> error + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_ERROR; + parent->option.timestep = 0.005; + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + + EXPECT_THAT(attached, IsNull()); + std::string error = mjs_getError(parent); + EXPECT_THAT(error, HasSubstr("policy is 'error'")); + EXPECT_THAT(error, HasSubstr("timestep: parent has 0.005, child has 0.001")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictErrorNoConflict) { + // error mode: no conflict (one side default) -> succeeds + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_ERROR; + parent->option.timestep = 0.005; + + mjSpec* child = mj_makeSpec(); + // child timestep is default -> no conflict + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + // parent unchanged + EXPECT_EQ(parent->option.timestep, mjtNum(0.005)); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictMergeFromBody) { + mock_warning_handler.ExpectWarnings(); + // merge mode: attach from body (not spec) still merges + + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_MERGE; + parent->option.timestep = 0.005; + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; + mjsBody* child_body = mjs_addBody(mjs_findBody(child, "world"), 0); + + mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr); + + mjsElement* attached = + mjs_attach(frame->element, child_body->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + EXPECT_EQ(parent->option.timestep, mjtNum(0.001)); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictMergeGravityError) { + // merge mode: conflicting gravity -> error + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_MERGE; + parent->option.gravity[2] = -10.0; + + mjSpec* child = mj_makeSpec(); + child->option.gravity[2] = -1.62; + + mjsBody* child_body = mjs_addBody(mjs_findBody(child, "world"), 0); + mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr); + + mjs_attach(frame->element, child_body->element, "child_", ""); + + EXPECT_THAT(mjs_getError(parent), + HasSubstr("gravity: parent has 0 0 -10, child has 0 0 -1.62")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictMergeRealtimeMin) { + mock_warning_handler.ExpectWarnings(); + // merge mode: conflicting realtime -> take min + + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_MERGE; + parent->visual.global.realtime = 2.0f; + + mjSpec* child = mj_makeSpec(); + child->visual.global.realtime = 0.5f; + + mjsBody* child_body = mjs_addBody(mjs_findBody(child, "world"), 0); + mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr); + + mjsElement* attached = + mjs_attach(frame->element, child_body->element, "child_", ""); + ASSERT_THAT(attached, NotNull()); + + EXPECT_EQ(parent->visual.global.realtime, 0.5f); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictWarningBitfields) { + mock_warning_handler.ExpectWarnings(); + // warning mode: conflicting disable flags report individual flag names + + mjSpec* parent = mj_makeSpec(); + parent->option.disableflags = mjDSBL_GRAVITY | mjDSBL_CONTACT; + + mjSpec* child = mj_makeSpec(); + child->option.disableflags = mjDSBL_GRAVITY | mjDSBL_ACTUATION; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + // Contact and Actuation differ, Gravity agrees -> 1 grouped warning + EXPECT_TRUE(mjs_isWarning(parent)); + EXPECT_EQ(mjs_numWarnings(parent), 1); + std::string w = mjs_getWarning(parent, 0); + EXPECT_THAT(w, HasSubstr("flag 'Contact': parent set, child unset," + " keeping parent")); + EXPECT_THAT(w, HasSubstr("flag 'Actuation': parent unset, child set," + " keeping parent")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictMergeMultipleWarnings) { + mock_warning_handler.ExpectWarnings(); + // merge mode: multiple min/max conflicts should be in one grouped warning + + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_MERGE; + parent->option.timestep = 0.005; + parent->option.iterations = 150; + parent->visual.global.realtime = 2.0f; + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; // min conflict + child->option.iterations = 200; // max conflict + child->visual.global.realtime = 0.5f; // min conflict + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + // values should be merged + EXPECT_EQ(parent->option.timestep, mjtNum(0.001)); + EXPECT_EQ(parent->option.iterations, 200); + EXPECT_EQ(parent->visual.global.realtime, 0.5f); + + // one grouped warning containing all three fields + EXPECT_EQ(mjs_numWarnings(parent), 1); + std::string w = mjs_getWarning(parent, 0); + EXPECT_THAT(w, HasSubstr("policy is 'merge'")); + EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.001," + " taking the minimum")); + EXPECT_THAT(w, HasSubstr("iterations: parent has 150, child has 200," + " taking the maximum")); + EXPECT_THAT(w, HasSubstr("realtime")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictWarningBitfieldsNoConflict) { + // warning mode: one side has zero disableflags -> no conflict + mjSpec* parent = mj_makeSpec(); + parent->option.disableflags = mjDSBL_GRAVITY; + + mjSpec* child = mj_makeSpec(); + // child disableflags is 0 (default) -> no conflict + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + EXPECT_FALSE(mjs_isWarning(parent)); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictMergeBitfields) { + mock_warning_handler.ExpectWarnings(); + // merge mode: flags are ORed together + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_MERGE; + parent->option.disableflags = mjDSBL_GRAVITY | mjDSBL_CONTACT; + parent->option.enableflags = mjENBL_OVERRIDE; + + mjSpec* child = mj_makeSpec(); + child->option.disableflags = mjDSBL_GRAVITY | mjDSBL_ACTUATION; + child->option.enableflags = mjENBL_ENERGY; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + // union of flags + EXPECT_EQ(parent->option.disableflags, + mjDSBL_GRAVITY | mjDSBL_CONTACT | mjDSBL_ACTUATION); + EXPECT_EQ(parent->option.enableflags, mjENBL_OVERRIDE | mjENBL_ENERGY); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictErrorBitfields) { + // error mode: differing flags -> per-flag error + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_ERROR; + parent->option.disableflags = mjDSBL_GRAVITY; + + mjSpec* child = mj_makeSpec(); + child->option.disableflags = mjDSBL_CONTACT; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + + EXPECT_THAT(attached, IsNull()); + // should mention the specific flag name, not "disableflags" + std::string error = mjs_getError(parent); + EXPECT_THAT(error, HasSubstr("flag 'Contact'")); + EXPECT_THAT(error, HasSubstr("flag 'Gravity'")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictErrorNoMutation) { + // failed attach should not mutate the parent spec + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_MERGE; + parent->option.timestep = 0.005; + parent->option.iterations = 150; + parent->option.integrator = mjINT_RK4; + parent->option.gravity[2] = -10.0; + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; // min-mergeable + child->option.iterations = 200; // max-mergeable + child->option.integrator = mjINT_IMPLICIT; // unmergeable -> error + child->option.gravity[2] = -1.62; // unmergeable array -> error + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + + // attach should fail + EXPECT_THAT(attached, IsNull()); + + // parent should be completely unchanged + EXPECT_EQ(parent->option.timestep, mjtNum(0.005)); + EXPECT_EQ(parent->option.iterations, 150); + EXPECT_EQ(parent->option.integrator, mjINT_RK4); + EXPECT_EQ(parent->option.gravity[2], mjtNum(-10.0)); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictMultipleErrors) { + // error mode: multiple conflicts should all be reported in the error message + mjSpec* parent = mj_makeSpec(); + parent->compiler.conflict = mjCONFLICT_ERROR; + parent->option.timestep = 0.005; + parent->option.iterations = 150; + parent->option.gravity[2] = -10.0; + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; + child->option.iterations = 200; + child->option.gravity[2] = -1.62; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + + EXPECT_THAT(attached, IsNull()); + // all three conflicting fields should appear in the error message + std::string error = mjs_getError(parent); + EXPECT_THAT(error, HasSubstr("timestep")); + EXPECT_THAT(error, HasSubstr("iterations")); + EXPECT_THAT(error, HasSubstr("gravity")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictWarningArray) { + mock_warning_handler.ExpectWarnings(); + // warning mode: conflicting array -> keep parent, warn + mjSpec* parent = mj_makeSpec(); + parent->option.gravity[2] = -10.0; + + mjSpec* child = mj_makeSpec(); + child->option.gravity[2] = -1.62; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + // parent gravity should be unchanged + EXPECT_EQ(parent->option.gravity[2], mjtNum(-10.0)); + + // one grouped warning containing gravity + EXPECT_TRUE(mjs_isWarning(parent)); + EXPECT_EQ(mjs_numWarnings(parent), 1); + EXPECT_THAT(mjs_getWarning(parent, 0), + HasSubstr("gravity: parent has 0 0 -10, child has 0 0 -1.62," + " keeping parent value")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictSubjectNames) { + mock_warning_handler.ExpectWarnings(); + + // Test case 1: both custom names + { + mjSpec* parent = mj_makeSpec(); + mjs_setString(parent->modelname, "parent_model"); + parent->option.timestep = 0.005; + + mjSpec* child = mj_makeSpec(); + mjs_setString(child->modelname, "child_model"); + child->option.timestep = 0.001; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + EXPECT_EQ(mjs_numWarnings(parent), 1); + std::string w = mjs_getWarning(parent, 0); + EXPECT_THAT(w, HasSubstr("Attach conflict when attaching 'child_model' to " + "'parent_model', policy is 'warning'")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); + } + + // Test case 2: only child custom name + { + mjSpec* parent = mj_makeSpec(); + parent->option.timestep = 0.005; + + mjSpec* child = mj_makeSpec(); + mjs_setString(child->modelname, "child_model"); + child->option.timestep = 0.001; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + EXPECT_EQ(mjs_numWarnings(parent), 1); + std::string w = mjs_getWarning(parent, 0); + EXPECT_THAT(w, HasSubstr("Attach conflict when attaching 'child_model', " + "policy is 'warning'")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); + } + + // Test case 3: only parent custom name + { + mjSpec* parent = mj_makeSpec(); + mjs_setString(parent->modelname, "parent_model"); + parent->option.timestep = 0.005; + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + EXPECT_EQ(mjs_numWarnings(parent), 1); + std::string w = mjs_getWarning(parent, 0); + EXPECT_THAT(w, HasSubstr("Attach conflict when attaching to 'parent_model'," + " policy is 'warning'")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); + } + + // Test case 4: no custom name (both "MuJoCo Model") + { + mjSpec* parent = mj_makeSpec(); + parent->option.timestep = 0.005; + + mjSpec* child = mj_makeSpec(); + child->option.timestep = 0.001; + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = mjs_attach(world->element, child->element, "", ""); + ASSERT_THAT(attached, NotNull()); + + EXPECT_EQ(mjs_numWarnings(parent), 1); + std::string w = mjs_getWarning(parent, 0); + EXPECT_THAT(w, HasSubstr("Attach conflict on attach, " + "policy is 'warning'")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); + } +} + +TEST_F(MujocoTest, AttachConflictAuthoredDefault) { + // child explicitly sets timestep to its default value (0.002) in XML, + // which should still conflict with a non-default parent timestep + static constexpr char parent_xml[] = R"( + + + + )"; + + static constexpr char child_xml[] = R"( + + + )"; + + std::array err; + mjSpec* parent = + mj_parseXMLString(parent_xml, nullptr, err.data(), err.size()); + ASSERT_THAT(parent, NotNull()) << err.data(); + mjSpec* child = mj_parseXMLString(child_xml, nullptr, err.data(), err.size()); + ASSERT_THAT(child, NotNull()) << err.data(); + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = + mjs_attach(world->element, child->element, "child_", ""); + + // attach should fail: child authored timestep=0.002 (the default), + // but parent has timestep=0.005 — authored tracking catches this + EXPECT_THAT(attached, IsNull()); + EXPECT_THAT(mjs_getError(parent), HasSubstr("timestep")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictNonAuthoredDefault) { + // child does NOT set timestep in XML, parent has non-default timestep. + // no conflict should be detected (child didn't author the field) + static constexpr char parent_xml[] = R"( + + + + )"; + + static constexpr char child_xml[] = R"( + + + + + + )"; + + std::array err; + mjSpec* parent = + mj_parseXMLString(parent_xml, nullptr, err.data(), err.size()); + ASSERT_THAT(parent, NotNull()) << err.data(); + mjSpec* child = mj_parseXMLString(child_xml, nullptr, err.data(), err.size()); + ASSERT_THAT(child, NotNull()) << err.data(); + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = + mjs_attach(world->element, child->element, "child_", ""); + + // attach should succeed: child didn't author timestep + EXPECT_THAT(attached, NotNull()) << mjs_getError(parent); + // parent timestep should be unchanged + EXPECT_EQ(parent->option.timestep, mjtNum(0.005)); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictXMLMergeAuthoredDefault) { + mock_warning_handler.ExpectWarnings(); + + static constexpr char parent_xml[] = R"( + + + + )"; + + static constexpr char child_xml[] = R"( + + + )"; + + std::array error; + mjSpec* parent = + mj_parseXMLString(parent_xml, nullptr, error.data(), error.size()); + ASSERT_THAT(parent, NotNull()) << error.data(); + + mjSpec* child = + mj_parseXMLString(child_xml, nullptr, error.data(), error.size()); + ASSERT_THAT(child, NotNull()) << error.data(); + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = + mjs_attach(world->element, child->element, "child_", ""); + ASSERT_THAT(attached, NotNull()) << "Error details: " << mjs_getError(parent); + + // Child explicitly authored timestep=0.002. Parent has timestep=0.005. So + // they conflict on both-authored. Under merge mode, min-merge applies -> + // child wins. + EXPECT_EQ(parent->option.timestep, mjtNum(0.002)); + EXPECT_EQ(mjs_numWarnings(parent), 1); + EXPECT_THAT(mjs_getWarning(parent, 0), + HasSubstr("timestep: parent has 0.005, child has 0.002, " + "taking the minimum")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachConflictXMLBitfieldSilentAdoption) { + mock_warning_handler.ExpectWarnings(); + + static constexpr char parent_xml[] = R"( + + + + + )"; + + static constexpr char child_xml[] = R"( + + + + + )"; + + std::array error; + mjSpec* parent = + mj_parseXMLString(parent_xml, nullptr, error.data(), error.size()); + ASSERT_THAT(parent, NotNull()) << error.data(); + + mjSpec* child = + mj_parseXMLString(child_xml, nullptr, error.data(), error.size()); + ASSERT_THAT(child, NotNull()) << error.data(); + + mjsBody* world = mjs_findBody(parent, "world"); + mjsElement* attached = + mjs_attach(world->element, child->element, "child_", ""); + ASSERT_THAT(attached, NotNull()) << "Error details: " << mjs_getError(parent); + + // Parent should adopt child's constraint disable flag since parent didn't + // restrict flags. In merge mode, this logging counts as 1 warning. + EXPECT_EQ(parent->option.disableflags, mjDSBL_CONSTRAINT); + EXPECT_EQ(mjs_numWarnings(parent), 1); + EXPECT_THAT(mjs_getWarning(parent, 0), + HasSubstr("flag 'Constraint': added from child")); + + mj_deleteSpec(parent); + mj_deleteSpec(child); +} + +TEST_F(MujocoTest, AttachWarningBoundaryAndPreservation) { + mock_warning_handler.ExpectWarnings(); + + // 1. Build a parent XML that generates a compile warning + static constexpr char parent_xml[] = R"( + + + + )"; + + // 2. Child 1 has conflicting timestep (0.001) + static constexpr char child1_xml[] = R"( + + + )"; + + // 3. Child 2 has conflicting sleep_tolerance (0.005) + static constexpr char child2_xml[] = R"( + + + )"; + + std::array error; + mjSpec* parent = + mj_parseXMLString(parent_xml, nullptr, error.data(), error.size()); + ASSERT_THAT(parent, NotNull()) << error.data(); + + mjSpec* child1 = + mj_parseXMLString(child1_xml, nullptr, error.data(), error.size()); + ASSERT_THAT(child1, NotNull()) << error.data(); + + mjSpec* child2 = + mj_parseXMLString(child2_xml, nullptr, error.data(), error.size()); + ASSERT_THAT(child2, NotNull()) << error.data(); + + mjsBody* world = mjs_findBody(parent, "world"); + + // Perform Attach 1 + mjsElement* attached1 = + mjs_attach(world->element, child1->element, "c1_", ""); + ASSERT_THAT(attached1, NotNull()) + << "Error details: " << mjs_getError(parent); + EXPECT_EQ(mjs_numWarnings(parent), 1); + EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep")); + + // Perform Attach 2 + mjsElement* attached2 = + mjs_attach(world->element, child2->element, "c2_", ""); + ASSERT_THAT(attached2, NotNull()) + << "Error details: " << mjs_getError(parent); + EXPECT_EQ(mjs_numWarnings(parent), 2); + EXPECT_THAT(mjs_getWarning(parent, 1), HasSubstr("sleep_tolerance")); + + // Compile 1: compiles and generates a compile warning (flex is not rigid) + mjModel* model1 = mj_compile(parent, nullptr); + ASSERT_THAT(model1, NotNull()); + + // Warnings should include both attach warnings (indices 0 and 1) AND the + // compile warning (at index 2) + int num_warnings_after_compile = mjs_numWarnings(parent); + EXPECT_GE(num_warnings_after_compile, 3); + EXPECT_THAT(mjs_getWarning(parent, 2), HasSubstr("not rigid")); + + // Compile 2 (Recompile): Recompiling should CLEAR compile warnings but + // PRESERVE all attach warnings! + mjModel* model2 = mj_compile(parent, nullptr); + ASSERT_THAT(model2, NotNull()); + + // Attach warnings must still be preserved and compile warnings refreshed + EXPECT_EQ(mjs_numWarnings(parent), num_warnings_after_compile); + EXPECT_THAT(mjs_getWarning(parent, 0), HasSubstr("timestep")); + EXPECT_THAT(mjs_getWarning(parent, 1), HasSubstr("sleep_tolerance")); + EXPECT_THAT(mjs_getWarning(parent, 2), HasSubstr("not rigid")); + + mj_deleteModel(model1); + mj_deleteModel(model2); + mj_deleteSpec(parent); + mj_deleteSpec(child1); + mj_deleteSpec(child2); +} + +} // namespace +} // namespace mujoco diff --git a/test/xml/mjz/mjz_encoder_test.cc b/test/xml/mjz/mjz_encoder_test.cc index 21786bfc..c6a6999d 100644 --- a/test/xml/mjz/mjz_encoder_test.cc +++ b/test/xml/mjz/mjz_encoder_test.cc @@ -99,6 +99,8 @@ std::vector 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; diff --git a/test/xml/testdata/child_mergable.xml b/test/xml/testdata/child_mergable.xml new file mode 100644 index 00000000..d409cbc7 --- /dev/null +++ b/test/xml/testdata/child_mergable.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/test/xml/testdata/child_unmergable.xml b/test/xml/testdata/child_unmergable.xml new file mode 100644 index 00000000..64662eb4 --- /dev/null +++ b/test/xml/testdata/child_unmergable.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/test/xml/testdata/parent_error.xml b/test/xml/testdata/parent_error.xml new file mode 100644 index 00000000..3011d72b --- /dev/null +++ b/test/xml/testdata/parent_error.xml @@ -0,0 +1,13 @@ + + + diff --git a/test/xml/testdata/parent_merge.xml b/test/xml/testdata/parent_merge.xml new file mode 100644 index 00000000..04065586 --- /dev/null +++ b/test/xml/testdata/parent_merge.xml @@ -0,0 +1,13 @@ + + + diff --git a/test/xml/testdata/parent_merge_unmergable.xml b/test/xml/testdata/parent_merge_unmergable.xml new file mode 100644 index 00000000..b73b6d32 --- /dev/null +++ b/test/xml/testdata/parent_merge_unmergable.xml @@ -0,0 +1,13 @@ + + + diff --git a/test/xml/testdata/parent_warn.xml b/test/xml/testdata/parent_warn.xml new file mode 100644 index 00000000..ee4e6687 --- /dev/null +++ b/test/xml/testdata/parent_warn.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index 8bd977ef..0f840419 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -3897,5 +3897,80 @@ TEST_F(ActuatorParseTest, DampingArmatureDefaultsPropagate) { mj_deleteModel(model); } +TEST_F(XMLReaderTest, AttachConflictXMLWarning) { + mock_warning_handler.ExpectWarnings(); + std::array 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 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 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 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 diff --git a/test/xml/xml_write_read_test.cc b/test/xml/xml_write_read_test.cc index 0c250d20..2968f13f 100644 --- a/test/xml/xml_write_read_test.cc +++ b/test/xml/xml_write_read_test.cc @@ -45,29 +45,30 @@ std::vector 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; diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 0da6ee5b..ebf97afe 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -583,6 +583,11 @@ public enum mjtOrientation : int{ mjORIENTATION_ZAXIS = 3, mjORIENTATION_EULER = 4, } +public enum mjtConflict : int{ + mjCONFLICT_WARNING = 0, + mjCONFLICT_MERGE = 1, + mjCONFLICT_ERROR = 2, +} public enum mjtCTimer : int{ mjCTIMER_TOTAL = 0, mjCTIMER_ASSETS = 1, @@ -5865,6 +5870,7 @@ public unsafe struct mjsCompiler_ { public fixed int inertiagrouprange[2]; public byte saveinertial; public int alignfree; + public int conflict; public mjLROpt_ LRopt; public void* meshdir; public void* texturedir; diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index 5232bc98..bcd95bd0 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -2331,6 +2331,12 @@ struct MjsCompiler { void set_alignfree(int value) { ptr_->alignfree = value; } + int conflict() const { + return ptr_->conflict; + } + void set_conflict(int value) { + ptr_->conflict = value; + } mjString meshdir() const { return (ptr_ && ptr_->meshdir) ? *(ptr_->meshdir) : ""; } @@ -11312,6 +11318,10 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { enum_("mjtCone") .value("mjCONE_PYRAMIDAL", mjCONE_PYRAMIDAL) .value("mjCONE_ELLIPTIC", mjCONE_ELLIPTIC); + enum_("mjtConflict") + .value("mjCONFLICT_WARNING", mjCONFLICT_WARNING) + .value("mjCONFLICT_MERGE", mjCONFLICT_MERGE) + .value("mjCONFLICT_ERROR", mjCONFLICT_ERROR); enum_("mjtConstraint") .value("mjCNSTR_EQUALITY", mjCNSTR_EQUALITY) .value("mjCNSTR_FRICTION_DOF", mjCNSTR_FRICTION_DOF) @@ -12993,6 +13003,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .property("balanceinertia", &MjsCompiler::balanceinertia, &MjsCompiler::set_balanceinertia, reference()) .property("boundinertia", &MjsCompiler::boundinertia, &MjsCompiler::set_boundinertia, reference()) .property("boundmass", &MjsCompiler::boundmass, &MjsCompiler::set_boundmass, reference()) + .property("conflict", &MjsCompiler::conflict, &MjsCompiler::set_conflict, reference()) .property("degree", &MjsCompiler::degree, &MjsCompiler::set_degree, reference()) .property("discardvisual", &MjsCompiler::discardvisual, &MjsCompiler::set_discardvisual, reference()) .property("eulerseq", &MjsCompiler::eulerseq) diff --git a/wasm/tests/bindings_test.ts b/wasm/tests/bindings_test.ts index 94fd6d01..7e259af8 100644 --- a/wasm/tests/bindings_test.ts +++ b/wasm/tests/bindings_test.ts @@ -1803,6 +1803,36 @@ describe('MuJoCo WASM Bindings', () => { } }); + it('should compile a spec and emit warnings', () => { + const xml = ` + + + + + `; + let spec = null; + let model = null; + const warnSpy = spyOn(console, 'warn'); + try { + spec = mujoco.parseXMLString(xml); + expect(spec).not.toBeNull(); + + model = mujoco.mj_compile(spec); + expect(model).not.toBeNull(); + expect(warnSpy).toHaveBeenCalled(); + const warningCall = warnSpy.calls.mostRecent(); + expect(warningCall.args[0]).toContain("no equality constraints or passive forces"); + } finally { + if (spec) { + spec.delete(); + } + if (model) { + model.delete(); + } + } + }); + it('should compile a spec from XML string with no assets', () => { let spec = null; let model = null;