Add policies for global attribute conflict resolution upon attach
For example when loading parent_merge.xml: ``` WARNING: Attach conflict when attaching 'child' to 'parent_merge', policy is 'merge' timestep: parent has 0.005, child has 0.002, taking the minimum iterations: parent has 50, child has 100, taking the maximum flag 'Damper': added from child ``` When loading parent_error.xml: ``` XML Error: Attach conflict when attaching 'child' to 'parent_error', policy is 'error' timestep: parent has 0.005, child has 0.002 iterations: parent has 50, child has 100 Element 'attach', line 10 ``` PiperOrigin-RevId: 933620810 Change-Id: Ib477863b5ef763474d27fb4be5a4148be1d5d500
This commit is contained in:
committed by
Copybara-Service
parent
30357c38a3
commit
410c73168c
@@ -915,6 +915,15 @@ Type of built-in procedural mesh.
|
|||||||
|
|
||||||
.. mujoco-include:: mjtMeshBuiltin
|
.. mujoco-include:: mjtMeshBuiltin
|
||||||
|
|
||||||
|
.. _mjtConflict:
|
||||||
|
|
||||||
|
mjtConflict
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Conflict resolution mode for attach.
|
||||||
|
|
||||||
|
.. mujoco-include:: mjtConflict
|
||||||
|
|
||||||
|
|
||||||
.. _mjtCTimer:
|
.. _mjtCTimer:
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
:at:`saveinertial`: :at-val:`[false, true], "false"`
|
||||||
If set to "true", the compiler will save explicit :ref:`inertial <body-inertial>` clauses for all bodies.
|
If set to "true", the compiler will save explicit :ref:`inertial <body-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 <meAttributeMerging>` 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 <meAttributeMergingTable>` 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:
|
.. _compiler-lengthrange:
|
||||||
|
|
||||||
:el-prefix:`compiler/` |-| **lengthrange** |?|
|
:el-prefix:`compiler/` |-| **lengthrange** |?|
|
||||||
|
|||||||
@@ -254,6 +254,9 @@
|
|||||||
.. grid-item::
|
.. grid-item::
|
||||||
:ref:`alignfree<compiler-alignfree>`
|
:ref:`alignfree<compiler-alignfree>`
|
||||||
|
|
||||||
|
.. grid-item::
|
||||||
|
:ref:`conflict<compiler-conflict>`
|
||||||
|
|
||||||
|
|
||||||
.. dropdown:: :ref:`lengthrange<compiler-lengthrange>` :octicon:`dot`
|
.. dropdown:: :ref:`lengthrange<compiler-lengthrange>` :octicon:`dot`
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ General
|
|||||||
|
|
||||||
- Added :ref:`mjs_numWarnings` and :ref:`mjs_getWarning` for retrieving all warnings accumulated during model
|
- 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``.
|
compilation and attachment. Deprecated :ref:`mjs_isWarning` in favor of ``mjs_numWarnings(s) > 0``.
|
||||||
|
- Added the :ref:`compiler/conflict<compiler-conflict>` attribute for controlling how conflicting global attributes
|
||||||
|
are resolved during :ref:`attachment<mjs_attach>`. 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 <meAttributeMerging>` 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
|
- Improved primal solver convergence under float32. Improvements initially proposed by :github:user:`n3b` in
|
||||||
:issue:`2313` and :github:user:`denzeler-nvidia` in :doc:`MJWarp <mjwarp/index>` pull request
|
:issue:`2313` and :github:user:`denzeler-nvidia` in :doc:`MJWarp <mjwarp/index>` pull request
|
||||||
`1374 <https://github.com/google-deepmind/mujoco_warp/pull/1374>`__.
|
`1374 <https://github.com/google-deepmind/mujoco_warp/pull/1374>`__.
|
||||||
|
|||||||
@@ -1549,6 +1549,11 @@ typedef enum mjtOrientation_ { // type of orientation specifier
|
|||||||
mjORIENTATION_ZAXIS, // z axis (minimal rotation)
|
mjORIENTATION_ZAXIS, // z axis (minimal rotation)
|
||||||
mjORIENTATION_EULER, // Euler angles
|
mjORIENTATION_EULER, // Euler angles
|
||||||
} mjtOrientation;
|
} 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
|
typedef enum mjtCTimer_ { // compiler timing categories
|
||||||
// top-level timers (wall-clock)
|
// top-level timers (wall-clock)
|
||||||
mjCTIMER_TOTAL = 0, // total compile time
|
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
|
int inertiagrouprange[2]; // range of geom groups used to compute inertia
|
||||||
mjtByte saveinertial; // save explicit inertial clause for all bodies to XML
|
mjtByte saveinertial; // save explicit inertial clause for all bodies to XML
|
||||||
int alignfree; // align free joints with inertial frame
|
int alignfree; // align free joints with inertial frame
|
||||||
|
int conflict; // conflict resolution for attach (mjtConflict)
|
||||||
mjLROpt LRopt; // options for lengthrange computation
|
mjLROpt LRopt; // options for lengthrange computation
|
||||||
mjString* meshdir; // mesh and hfield directory
|
mjString* meshdir; // mesh and hfield directory
|
||||||
mjString* texturedir; // texture directory
|
mjString* texturedir; // texture directory
|
||||||
|
|||||||
@@ -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
|
finalized. If a second attachment is performed without compilation, the keyframes from the first attachment will be
|
||||||
lost.
|
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<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<option-timestep>`, :ref:`tolerance<option-tolerance>`, :ref:`ls_tolerance<option-ls_tolerance>`,
|
||||||
|
:ref:`noslip_tolerance<option-noslip_tolerance>`, :ref:`ccd_tolerance<option-ccd_tolerance>`,
|
||||||
|
:ref:`sleep_tolerance<option-sleep_tolerance>`,
|
||||||
|
|br| **visual**: :ref:`znear<visual-map-znear>`, :ref:`realtime<visual-global-realtime>`
|
||||||
|
- Preserves precision and stability.
|
||||||
|
* - **Maximum**
|
||||||
|
- **option**: :ref:`iterations<option-iterations>`, :ref:`ls_iterations<option-ls_iterations>`,
|
||||||
|
:ref:`noslip_iterations<option-noslip_iterations>`, :ref:`ccd_iterations<option-ccd_iterations>`,
|
||||||
|
:ref:`sdf_iterations<option-sdf_iterations>`, :ref:`sdf_initpoints<option-sdf_initpoints>`,
|
||||||
|
|br| **size**: :ref:`memory<size-memory>`, :ref:`nkey<size-nkey>`, :ref:`nuserdata<size-nuserdata>`,
|
||||||
|
:ref:`nuser_body<size-nuser_body>`, :ref:`nuser_jnt<size-nuser_jnt>`, :ref:`nuser_geom<size-nuser_geom>`, :ref:`nuser_site<size-nuser_site>`,
|
||||||
|
:ref:`nuser_cam<size-nuser_cam>`, :ref:`nuser_tendon<size-nuser_tendon>`, :ref:`nuser_actuator<size-nuser_actuator>`, :ref:`nuser_sensor<size-nuser_sensor>`
|
||||||
|
|br| **visual**: :ref:`zfar<visual-map-zfar>`
|
||||||
|
- Ensures sufficient resources and limits.
|
||||||
|
* - **OR (union)**
|
||||||
|
- **option**: :ref:`disableflags<option-flag>`, :ref:`enableflags<option-flag>`,
|
||||||
|
:ref:`disableactuator<option-actuatorgroupdisable>`
|
||||||
|
- Flags from both models are combined.
|
||||||
|
* - **Error**
|
||||||
|
- **option**: :ref:`gravity<option-gravity>`, :ref:`wind<option-wind>`, :ref:`magnetic<option-magnetic>`,
|
||||||
|
:ref:`density<option-density>`, :ref:`viscosity<option-viscosity>`, :ref:`integrator<option-integrator>`,
|
||||||
|
:ref:`cone<option-cone>`, :ref:`jacobian<option-jacobian>`, :ref:`solver<option-solver>`,
|
||||||
|
:ref:`impratio<option-impratio>`,
|
||||||
|
:ref:`o_margin<option-o_margin>`, :ref:`o_solref<option-o_solref>`, :ref:`o_solimp<option-o_solimp>`,
|
||||||
|
:ref:`o_friction<option-o_friction>`
|
||||||
|
- Raised if non-default values conflict.
|
||||||
|
|
||||||
.. _meDefault:
|
.. _meDefault:
|
||||||
|
|
||||||
Default classes
|
Default classes
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ typedef enum mjtOrientation_ { // type of orientation specifier
|
|||||||
mjORIENTATION_EULER, // Euler angles
|
mjORIENTATION_EULER, // Euler angles
|
||||||
} mjtOrientation;
|
} 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
|
typedef enum mjtCTimer_ { // compiler timing categories
|
||||||
// top-level timers (wall-clock)
|
// 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
|
int inertiagrouprange[2]; // range of geom groups used to compute inertia
|
||||||
mjtByte saveinertial; // save explicit inertial clause for all bodies to XML
|
mjtByte saveinertial; // save explicit inertial clause for all bodies to XML
|
||||||
int alignfree; // align free joints with inertial frame
|
int alignfree; // align free joints with inertial frame
|
||||||
|
int conflict; // conflict resolution for attach (mjtConflict)
|
||||||
mjLROpt LRopt; // options for lengthrange computation
|
mjLROpt LRopt; // options for lengthrange computation
|
||||||
mjString* meshdir; // mesh and hfield directory
|
mjString* meshdir; // mesh and hfield directory
|
||||||
mjString* texturedir; // texture directory
|
mjString* texturedir; // texture directory
|
||||||
|
|||||||
@@ -41,12 +41,12 @@
|
|||||||
XVEC(int, inertiagrouprange, 2) \
|
XVEC(int, inertiagrouprange, 2) \
|
||||||
X(mjtByte, saveinertial, 1) \
|
X(mjtByte, saveinertial, 1) \
|
||||||
X(int, alignfree, 1) \
|
X(int, alignfree, 1) \
|
||||||
|
X(int, conflict, 1) \
|
||||||
X(mjLROpt, LRopt, 1) \
|
X(mjLROpt, LRopt, 1) \
|
||||||
X(mjString*, meshdir, 1) \
|
X(mjString*, meshdir, 1) \
|
||||||
X(mjString*, texturedir, 1) \
|
X(mjString*, texturedir, 1) \
|
||||||
X(uint64_t, authored, 1)
|
X(uint64_t, authored, 1)
|
||||||
|
|
||||||
|
|
||||||
//-------------------------------- mjSpec ----------------------------------------------------------
|
//-------------------------------- mjSpec ----------------------------------------------------------
|
||||||
|
|
||||||
#define MJSPEC_FIELDS \
|
#define MJSPEC_FIELDS \
|
||||||
|
|||||||
@@ -731,6 +731,16 @@ ENUMS: Mapping[str, EnumDecl] = dict([
|
|||||||
('mjORIENTATION_EULER', 4),
|
('mjORIENTATION_EULER', 4),
|
||||||
]),
|
]),
|
||||||
)),
|
)),
|
||||||
|
('mjtConflict',
|
||||||
|
EnumDecl(
|
||||||
|
name='mjtConflict',
|
||||||
|
declname='enum mjtConflict_',
|
||||||
|
values=dict([
|
||||||
|
('mjCONFLICT_WARNING', 0),
|
||||||
|
('mjCONFLICT_MERGE', 1),
|
||||||
|
('mjCONFLICT_ERROR', 2),
|
||||||
|
]),
|
||||||
|
)),
|
||||||
('mjtCTimer',
|
('mjtCTimer',
|
||||||
EnumDecl(
|
EnumDecl(
|
||||||
name='mjtCTimer',
|
name='mjtCTimer',
|
||||||
|
|||||||
@@ -6977,6 +6977,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([
|
|||||||
type=ValueType(name='int'),
|
type=ValueType(name='int'),
|
||||||
doc='align free joints with inertial frame',
|
doc='align free joints with inertial frame',
|
||||||
),
|
),
|
||||||
|
StructFieldDecl(
|
||||||
|
name='conflict',
|
||||||
|
type=ValueType(name='int'),
|
||||||
|
doc='conflict resolution for attach (mjtConflict)',
|
||||||
|
),
|
||||||
StructFieldDecl(
|
StructFieldDecl(
|
||||||
name='LRopt',
|
name='LRopt',
|
||||||
type=ValueType(name='mjLROpt'),
|
type=ValueType(name='mjLROpt'),
|
||||||
|
|||||||
+129
-2
@@ -20,6 +20,7 @@ import math
|
|||||||
import os
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
import typing
|
import typing
|
||||||
|
import warnings
|
||||||
import zipfile # pylint: disable=unused-import
|
import zipfile # pylint: disable=unused-import
|
||||||
|
|
||||||
from absl import flags
|
from absl import flags
|
||||||
@@ -1160,7 +1161,7 @@ class SpecsTest(absltest.TestCase):
|
|||||||
|
|
||||||
spec = mujoco.MjSpec()
|
spec = mujoco.MjSpec()
|
||||||
material = spec.add_material(name='mat')
|
material = spec.add_material(name='mat')
|
||||||
texture = spec.add_texture(
|
spec.add_texture(
|
||||||
name='tex', builtin=mujoco.mjtBuiltin.mjBUILTIN_FLAT, width=2, height=2
|
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])
|
sidesite = body.add_site(name='sidesite', pos=[2, 0, -5])
|
||||||
site4 = body.add_site(name='site4', pos=[0, 1, -6])
|
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(
|
cylinder = spec.worldbody.add_geom(
|
||||||
name='cylinder',
|
name='cylinder',
|
||||||
@@ -2217,5 +2220,129 @@ class SpecsTest(absltest.TestCase):
|
|||||||
self.assertEqual(spec.authored.visual_quality, 0)
|
self.assertEqual(spec.authored.visual_quality, 0)
|
||||||
self.assertEqual(spec.authored.visual_map, 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("""\
|
||||||
|
<mujoco>
|
||||||
|
<compiler conflict="merge"/>
|
||||||
|
<option timestep="0.005" iterations="50"/>
|
||||||
|
<asset>
|
||||||
|
<model name="child" file="child.xml"/>
|
||||||
|
</asset>
|
||||||
|
<worldbody>
|
||||||
|
<body name="parent_body">
|
||||||
|
<geom size="1"/>
|
||||||
|
<attach model="child" body="child_body" prefix="child/"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
""")
|
||||||
|
|
||||||
|
child_xml = textwrap.dedent("""\
|
||||||
|
<mujoco>
|
||||||
|
<option timestep="0.001" iterations="150"/>
|
||||||
|
<worldbody>
|
||||||
|
<body name="child_body">
|
||||||
|
<geom size="1"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
""")
|
||||||
|
|
||||||
|
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("""\
|
||||||
|
<mujoco>
|
||||||
|
<compiler conflict="error"/>
|
||||||
|
<option timestep="0.005"/>
|
||||||
|
<asset>
|
||||||
|
<model name="child" file="child.xml"/>
|
||||||
|
</asset>
|
||||||
|
<worldbody>
|
||||||
|
<body name="parent_body">
|
||||||
|
<geom size="1"/>
|
||||||
|
<attach model="child" body="child_body" prefix="child/"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
""")
|
||||||
|
|
||||||
|
child_xml = textwrap.dedent("""\
|
||||||
|
<mujoco>
|
||||||
|
<option timestep="0.001"/>
|
||||||
|
<worldbody>
|
||||||
|
<body name="child_body">
|
||||||
|
<geom size="1"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
""")
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(ValueError, 'timestep'):
|
||||||
|
mujoco.MjSpec.from_string(
|
||||||
|
parent_xml,
|
||||||
|
include={'child.xml': child_xml.encode()},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
absltest.main()
|
absltest.main()
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ raw::MjModel* MjSpec::Compile(mjVFS* vfs) {
|
|||||||
warnings.attr("warn")(mjs_getWarning(ptr, i));
|
warnings.attr("warn")(mjs_getWarning(ptr, i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ set(MUJOCO_USER_SRCS
|
|||||||
user_model.h
|
user_model.h
|
||||||
user_objects.cc
|
user_objects.cc
|
||||||
user_objects.h
|
user_objects.h
|
||||||
|
user_resolver.cc
|
||||||
|
user_resolver.h
|
||||||
user_resource.cc
|
user_resource.cc
|
||||||
user_resource.h
|
user_resource.h
|
||||||
user_threadpool.cc
|
user_threadpool.cc
|
||||||
|
|||||||
+47
-10
@@ -20,7 +20,7 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem> // NOLINT
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -30,15 +30,16 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <mujoco/mujoco.h>
|
|
||||||
#include <mujoco/mjspecmacro.h>
|
#include <mujoco/mjspecmacro.h>
|
||||||
#include <mujoco/mjxmacro.h>
|
#include <mujoco/mjxmacro.h>
|
||||||
|
#include <mujoco/mujoco.h>
|
||||||
#include "engine/engine_support.h"
|
#include "engine/engine_support.h"
|
||||||
#include "engine/engine_util_errmem.h"
|
#include "engine/engine_util_errmem.h"
|
||||||
#include "user/user_cache.h"
|
#include "user/user_cache.h"
|
||||||
#include "user/user_flexcomp.h"
|
#include "user/user_flexcomp.h"
|
||||||
#include "user/user_model.h"
|
#include "user/user_model.h"
|
||||||
#include "user/user_objects.h"
|
#include "user/user_objects.h"
|
||||||
|
#include "user/user_resolver.h"
|
||||||
#include "user/user_resource.h"
|
#include "user/user_resource.h"
|
||||||
#include "user/user_util.h"
|
#include "user/user_util.h"
|
||||||
|
|
||||||
@@ -382,7 +383,6 @@ static mjsElement* attachFrameToSite(mjCSite* parent, const mjCFrame* child,
|
|||||||
return attached_frame;
|
return attached_frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child,
|
mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child,
|
||||||
const char* prefix, const char* suffix) {
|
const char* prefix, const char* suffix) {
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
@@ -394,6 +394,31 @@ mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child,
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
mjCModel* model = static_cast<mjCModel*>(mjs_getSpec(parent)->element);
|
mjCModel* model = static_cast<mjCModel*>(mjs_getSpec(parent)->element);
|
||||||
|
const mjSpec* child_spec = nullptr;
|
||||||
|
if (child->elemtype == mjOBJ_MODEL) {
|
||||||
|
child_spec = &(static_cast<const mjCModel*>(child)->spec);
|
||||||
|
} else {
|
||||||
|
child_spec = &(static_cast<const mjCBase*>(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<mjtConflict>(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) {
|
if (child->elemtype == mjOBJ_MODEL) {
|
||||||
mjCModel* child_model = static_cast<mjCModel*>((mjsElement*)child);
|
mjCModel* child_model = static_cast<mjCModel*>((mjsElement*)child);
|
||||||
mjsBody* worldbody = mjs_findBody(&child_model->spec, "world");
|
mjsBody* worldbody = mjs_findBody(&child_model->spec, "world");
|
||||||
@@ -411,10 +436,11 @@ mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child,
|
|||||||
SetFrame(worldbody, mjOBJ_CAMERA, worldframe);
|
SetFrame(worldbody, mjOBJ_CAMERA, worldframe);
|
||||||
child = worldframe->element;
|
child = worldframe->element;
|
||||||
}
|
}
|
||||||
|
mjsElement* result = nullptr;
|
||||||
switch (parent->elemtype) {
|
switch (parent->elemtype) {
|
||||||
case mjOBJ_FRAME:
|
case mjOBJ_FRAME:
|
||||||
if (child->elemtype == mjOBJ_BODY) {
|
if (child->elemtype == mjOBJ_BODY) {
|
||||||
return attachBody(static_cast<mjCFrame*>(parent),
|
result = attachBody(static_cast<mjCFrame*>(parent),
|
||||||
static_cast<const mjCBody*>(child), prefix, suffix);
|
static_cast<const mjCBody*>(child), prefix, suffix);
|
||||||
} else if (child->elemtype == mjOBJ_FRAME) {
|
} else if (child->elemtype == mjOBJ_FRAME) {
|
||||||
mjsBody* parent_body = mjs_getParent(parent);
|
mjsBody* parent_body = mjs_getParent(parent);
|
||||||
@@ -429,35 +455,46 @@ mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child,
|
|||||||
if (mjs_setFrame(attached_frame, &frame->spec)) {
|
if (mjs_setFrame(attached_frame, &frame->spec)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return attached_frame;
|
result = attached_frame;
|
||||||
} else {
|
} else {
|
||||||
model->SetError(mjCError(0, "child element is not a body or frame"));
|
model->SetError(mjCError(0, "child element is not a body or frame"));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
case mjOBJ_BODY:
|
case mjOBJ_BODY:
|
||||||
if (child->elemtype == mjOBJ_FRAME) {
|
if (child->elemtype == mjOBJ_FRAME) {
|
||||||
return attachFrame(static_cast<mjCBody*>(parent),
|
result =
|
||||||
|
attachFrame(static_cast<mjCBody*>(parent),
|
||||||
static_cast<const mjCFrame*>(child), prefix, suffix);
|
static_cast<const mjCFrame*>(child), prefix, suffix);
|
||||||
} else {
|
} else {
|
||||||
model->SetError(mjCError(0, "child element is not a frame"));
|
model->SetError(mjCError(0, "child element is not a frame"));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
case mjOBJ_SITE:
|
case mjOBJ_SITE:
|
||||||
if (child->elemtype == mjOBJ_BODY) {
|
if (child->elemtype == mjOBJ_BODY) {
|
||||||
return attachToSite(static_cast<mjCSite*>(parent),
|
result =
|
||||||
|
attachToSite(static_cast<mjCSite*>(parent),
|
||||||
static_cast<const mjCBody*>(child), prefix, suffix);
|
static_cast<const mjCBody*>(child), prefix, suffix);
|
||||||
} else if (child->elemtype == mjOBJ_FRAME) {
|
} else if (child->elemtype == mjOBJ_FRAME) {
|
||||||
return attachFrameToSite(static_cast<mjCSite*>(parent),
|
result = attachFrameToSite(static_cast<mjCSite*>(parent),
|
||||||
static_cast<const mjCFrame*>(child), prefix, suffix);
|
static_cast<const mjCFrame*>(child), prefix,
|
||||||
|
suffix);
|
||||||
} else {
|
} else {
|
||||||
model->SetError(mjCError(0, "child element is not a body or frame"));
|
model->SetError(mjCError(0, "child element is not a body or frame"));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
model->SetError(mjCError(0, "parent element is not a frame, body or site"));
|
model->SetError(mjCError(0, "parent element is not a frame, body or site"));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return nullptr;
|
|
||||||
|
// mark all warnings accumulated so far as attach-phase
|
||||||
|
if (result) {
|
||||||
|
model->SetAttachWarningBoundary();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1525,6 +1525,22 @@ void mjCModel::AddWarning(std::string msg, const mjCBase* obj) {
|
|||||||
warnings_.push_back(std::move(msg));
|
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
|
// pointer to world body
|
||||||
mjCBody* mjCModel::GetWorld() {
|
mjCBody* mjCModel::GetWorld() {
|
||||||
return bodies_[0];
|
return bodies_[0];
|
||||||
@@ -5026,6 +5042,9 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) {
|
|||||||
} disable_usethread(compiler.usethread);
|
} disable_usethread(compiler.usethread);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// clear compile-phase warnings from previous compile, keep attach warnings
|
||||||
|
ClearCompileWarnings();
|
||||||
|
|
||||||
using Clock = std::chrono::steady_clock;
|
using Clock = std::chrono::steady_clock;
|
||||||
using Seconds = std::chrono::duration<double>;
|
using Seconds = std::chrono::duration<double>;
|
||||||
for (int i=0; i < mjNCTIMER; i++) {
|
for (int i=0; i < mjNCTIMER; i++) {
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ class mjCModel : public mjCModel_, private mjSpec {
|
|||||||
void SetError(const mjCError& error) { errInfo = error; } // set value of error object
|
void SetError(const mjCError& error) { errInfo = error; } // set value of error object
|
||||||
void AddWarning(std::string msg, // add warning to vector
|
void AddWarning(std::string msg, // add warning to vector
|
||||||
const mjCBase* obj = nullptr);
|
const mjCBase* obj = nullptr);
|
||||||
|
void AddGroupedWarning(const std::string& subject, // add grouped warning
|
||||||
|
const std::string& body);
|
||||||
const std::vector<std::string>& GetWarnings()
|
const std::vector<std::string>& GetWarnings()
|
||||||
const { // get accumulated warnings
|
const { // get accumulated warnings
|
||||||
return warnings_;
|
return warnings_;
|
||||||
|
|||||||
@@ -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 <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <mujoco/mjspec.h>
|
||||||
|
#include <mujoco/mujoco.h>
|
||||||
|
#include "user/user_api.h"
|
||||||
|
|
||||||
|
namespace mujoco {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// format a numeric value for conflict messages
|
||||||
|
template <typename T>
|
||||||
|
std::string fmtVal(T val) {
|
||||||
|
if constexpr (std::is_same_v<T, mjtNum>) {
|
||||||
|
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<std::string> errs;
|
||||||
|
std::vector<std::string> warnings;
|
||||||
|
std::vector<std::function<void()>> ops;
|
||||||
|
|
||||||
|
Resolver(mjtConflict mode, mjSpec* parent, const mjSpec* child)
|
||||||
|
: mode(mode), parent(parent), child(child) {}
|
||||||
|
|
||||||
|
// resolve a scalar field conflict
|
||||||
|
template <typename T>
|
||||||
|
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 <int N>
|
||||||
|
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<mjtNum, N> 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
|
||||||
@@ -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 <string>
|
||||||
|
|
||||||
|
#include <mujoco/mjspec.h>
|
||||||
|
|
||||||
|
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_
|
||||||
+2
-1
@@ -48,6 +48,7 @@ extern const int stage_sz;
|
|||||||
extern const int datatype_sz;
|
extern const int datatype_sz;
|
||||||
extern const int camout_sz;
|
extern const int camout_sz;
|
||||||
extern const int reduce_sz;
|
extern const int reduce_sz;
|
||||||
|
extern const int conflict_sz;
|
||||||
extern const mjMap angle_map[];
|
extern const mjMap angle_map[];
|
||||||
extern const mjMap enable_map[];
|
extern const mjMap enable_map[];
|
||||||
extern const mjMap bool_map[];
|
extern const mjMap bool_map[];
|
||||||
@@ -86,7 +87,7 @@ extern const mjMap meshtype_map[];
|
|||||||
extern const mjMap meshinertia_map[];
|
extern const mjMap meshinertia_map[];
|
||||||
extern const mjMap flexself_map[];
|
extern const mjMap flexself_map[];
|
||||||
extern const mjMap elastic2d_map[];
|
extern const mjMap elastic2d_map[];
|
||||||
|
extern const mjMap conflict_map[];
|
||||||
|
|
||||||
//---------------------------------- Base XML class ------------------------------------------------
|
//---------------------------------- Base XML class ------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
+506
-262
@@ -169,29 +169,59 @@ static void UpdateString(string& psuffix, int count, int i) {
|
|||||||
std::vector<const char*> MJCF[nMJCF] = {
|
std::vector<const char*> MJCF[nMJCF] = {
|
||||||
{"mujoco", "!", "model"},
|
{"mujoco", "!", "model"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"compiler", "*", "autolimits", "boundmass", "boundinertia", "settotalmass",
|
{"compiler", "*",
|
||||||
"balanceinertia", "strippath", "coordinate", "angle", "fitaabb", "eulerseq",
|
"autolimits", "boundmass",
|
||||||
"meshdir", "texturedir", "discardvisual", "usethread", "fusestatic", "inertiafromgeom",
|
"boundinertia", "settotalmass",
|
||||||
"inertiagrouprange", "saveinertial", "assetdir", "alignfree"},
|
"balanceinertia", "strippath",
|
||||||
|
"coordinate", "angle",
|
||||||
|
"fitaabb", "eulerseq",
|
||||||
|
"meshdir", "texturedir",
|
||||||
|
"discardvisual", "usethread",
|
||||||
|
"fusestatic", "inertiafromgeom",
|
||||||
|
"inertiagrouprange", "saveinertial",
|
||||||
|
"assetdir", "alignfree",
|
||||||
|
"conflict"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"lengthrange", "?", "mode", "useexisting", "uselimit",
|
{"lengthrange", "?", "mode", "useexisting", "uselimit", "accel", "maxforce",
|
||||||
"accel", "maxforce", "timeconst", "timestep",
|
"timeconst", "timestep", "inttotal", "interval", "tolrange"},
|
||||||
"inttotal", "interval", "tolrange"},
|
|
||||||
{">"},
|
{">"},
|
||||||
|
|
||||||
{"option", "*",
|
{"option",
|
||||||
"timestep", "impratio", "tolerance", "ls_tolerance", "noslip_tolerance",
|
"*",
|
||||||
"ccd_tolerance", "sleep_tolerance", "gravity", "wind", "magnetic", "density", "viscosity",
|
"timestep",
|
||||||
"o_margin", "o_solref", "o_solimp", "o_friction",
|
"impratio",
|
||||||
"integrator", "cone", "jacobian",
|
"tolerance",
|
||||||
"solver", "iterations", "ls_iterations", "noslip_iterations", "ccd_iterations",
|
"ls_tolerance",
|
||||||
"sdf_iterations", "sdf_initpoints", "actuatorgroupdisable"},
|
"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",
|
{"flag", "?", "constraint", "equality", "frictionloss",
|
||||||
"spring", "damper", "gravity", "clampctrl", "warmstart", "filterparent", "actuation",
|
"limit", "contact", "spring", "damper", "gravity",
|
||||||
"refsafe", "sensor", "midphase", "eulerdamp", "autoreset", "nativeccd", "island",
|
"clampctrl", "warmstart", "filterparent", "actuation", "refsafe",
|
||||||
"override", "energy", "fwdinv", "invdiscrete", "multiccd", "sleep",
|
"sensor", "midphase", "eulerdamp", "autoreset", "nativeccd",
|
||||||
"diagexact"},
|
"island", "override", "energy", "fwdinv", "invdiscrete",
|
||||||
|
"multiccd", "sleep", "diagexact"},
|
||||||
{">"},
|
{">"},
|
||||||
|
|
||||||
{"size", "*", "memory", "njmax", "nconmax", "nstack", "nuserdata", "nkey",
|
{"size", "*", "memory", "njmax", "nconmax", "nstack", "nuserdata", "nkey",
|
||||||
@@ -200,26 +230,50 @@ std::vector<const char*> MJCF[nMJCF] = {
|
|||||||
|
|
||||||
{"visual", "*"},
|
{"visual", "*"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"global", "?", "cameraid", "orthographic", "fovy", "ipd", "azimuth", "elevation",
|
{"global", "?", "cameraid", "orthographic", "fovy", "ipd", "azimuth",
|
||||||
"linewidth", "glow", "offwidth", "offheight", "realtime", "ellipsoidinertia",
|
"elevation", "linewidth", "glow", "offwidth", "offheight", "realtime",
|
||||||
"bvactive"},
|
"ellipsoidinertia", "bvactive"},
|
||||||
{"quality", "?", "shadowsize", "offsamples", "numslices", "numstacks",
|
{"quality", "?", "shadowsize", "offsamples", "numslices", "numstacks",
|
||||||
"numquads"},
|
"numquads"},
|
||||||
{"headlight", "?", "ambient", "diffuse", "specular", "active"},
|
{"headlight", "?", "ambient", "diffuse", "specular", "active"},
|
||||||
{"map", "?", "stiffness", "stiffnessrot", "force", "torque", "alpha",
|
{"map", "?", "stiffness", "stiffnessrot", "force", "torque", "alpha",
|
||||||
"fogstart", "fogend", "znear", "zfar", "haze", "shadowclip", "shadowscale",
|
"fogstart", "fogend", "znear", "zfar", "haze", "shadowclip", "shadowscale",
|
||||||
"actuatortendon"},
|
"actuatortendon"},
|
||||||
{"scale", "?", "forcewidth", "contactwidth", "contactheight", "connect", "com",
|
{"scale", "?", "forcewidth", "contactwidth", "contactheight", "connect",
|
||||||
"camera", "light", "selectpoint", "jointlength", "jointwidth", "actuatorlength",
|
"com", "camera", "light", "selectpoint", "jointlength", "jointwidth",
|
||||||
"actuatorwidth", "framelength", "framewidth", "constraint", "slidercrank", "frustum"},
|
"actuatorlength", "actuatorwidth", "framelength", "framewidth",
|
||||||
{"rgba", "?", "fog", "haze", "force", "inertia", "joint",
|
"constraint", "slidercrank", "frustum"},
|
||||||
"actuator", "actuatornegative", "actuatorpositive", "com",
|
{"rgba",
|
||||||
"camera", "light", "selectpoint", "connect", "contactpoint", "contactforce",
|
"?",
|
||||||
"contactfriction", "contacttorque", "contactgap", "rangefinder",
|
"fog",
|
||||||
"constraint", "slidercrank", "crankbroken", "frustum", "bv", "bvactive"},
|
"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"},
|
{"default", "R", "class"},
|
||||||
{"<"},
|
{"<"},
|
||||||
@@ -229,60 +283,92 @@ std::vector<const char*> MJCF[nMJCF] = {
|
|||||||
{"<"},
|
{"<"},
|
||||||
{"layer", "*", "texture", "role"},
|
{"layer", "*", "texture", "role"},
|
||||||
{">"},
|
{">"},
|
||||||
{"joint", "?", "type", "group", "pos", "axis", "springdamper",
|
{"joint",
|
||||||
"limited", "actuatorfrclimited", "solreflimit", "solimplimit",
|
"?",
|
||||||
"solreffriction", "solimpfriction", "stiffness", "range", "actuatorfrcrange",
|
"type",
|
||||||
"actuatorgravcomp", "margin", "ref", "springref", "armature", "damping",
|
"group",
|
||||||
"frictionloss", "user"},
|
"pos",
|
||||||
{"geom", "?", "type", "pos", "quat", "contype", "conaffinity", "condim",
|
"axis",
|
||||||
"group", "priority", "size", "material", "friction", "mass", "density",
|
"springdamper",
|
||||||
"shellinertia", "solmix", "solref", "solimp",
|
"limited",
|
||||||
"margin", "gap", "fromto", "axisangle", "xyaxes", "zaxis", "euler",
|
"actuatorfrclimited",
|
||||||
"hfield", "mesh", "fitscale", "rgba", "fluidshape", "fluidcoef", "user"},
|
"solreflimit",
|
||||||
{"site", "?", "type", "group", "pos", "quat", "material",
|
"solimplimit",
|
||||||
"size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"},
|
"solreffriction",
|
||||||
{"camera", "?", "projection", "fovy", "ipd", "resolution", "output", "pos", "quat",
|
"solimpfriction",
|
||||||
"axisangle", "xyaxes", "zaxis", "euler", "mode", "focal", "focalpixel",
|
"stiffness",
|
||||||
"principal", "principalpixel", "sensorsize", "user"},
|
"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",
|
{"light", "?", "pos", "dir", "bulbradius", "intensity", "range",
|
||||||
"directional", "type", "castshadow", "active", "attenuation", "cutoff", "exponent",
|
"directional", "type", "castshadow", "active", "attenuation", "cutoff",
|
||||||
"ambient", "diffuse", "specular", "mode"},
|
"exponent", "ambient", "diffuse", "specular", "mode"},
|
||||||
{"pair", "?", "condim", "friction", "solref", "solreffriction", "solimp",
|
{"pair", "?", "condim", "friction", "solref", "solreffriction", "solimp",
|
||||||
"gap", "margin"},
|
"gap", "margin"},
|
||||||
{"equality", "?", "active", "solref", "solimp"},
|
{"equality", "?", "active", "solref", "solimp"},
|
||||||
{"tendon", "?", "group", "limited", "range",
|
{"tendon", "?", "group", "limited", "range", "solreflimit", "solimplimit",
|
||||||
"solreflimit", "solimplimit", "solreffriction", "solimpfriction",
|
"solreffriction", "solimpfriction", "frictionloss", "springlength",
|
||||||
"frictionloss", "springlength", "width", "material",
|
"width", "material", "margin", "stiffness", "damping", "rgba", "user"},
|
||||||
"margin", "stiffness", "damping", "rgba", "user"},
|
{"general", "?", "ctrllimited", "forcelimited", "actlimited",
|
||||||
{"general", "?", "ctrllimited", "forcelimited", "actlimited", "ctrlrange", "forcerange",
|
"ctrlrange", "forcerange", "actrange", "gear", "damping",
|
||||||
"actrange", "gear", "damping", "armature", "cranklength", "user", "group", "nsample",
|
"armature", "cranklength", "user", "group", "nsample",
|
||||||
"interp", "delay", "actdim", "dyntype", "gaintype", "biastype", "dynprm", "gainprm",
|
"interp", "delay", "actdim", "dyntype", "gaintype",
|
||||||
"biasprm", "actearly"},
|
"biastype", "dynprm", "gainprm", "biasprm", "actearly"},
|
||||||
{"motor", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
{"motor", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
||||||
"gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay"},
|
"gear", "damping", "armature", "cranklength", "user", "group", "nsample",
|
||||||
{"position", "?", "ctrllimited", "forcelimited", "ctrlrange", "inheritrange", "forcerange",
|
"interp", "delay"},
|
||||||
"gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp",
|
{"position", "?", "ctrllimited", "forcelimited", "ctrlrange",
|
||||||
|
"inheritrange", "forcerange", "gear", "damping", "armature",
|
||||||
|
"cranklength", "user", "group", "nsample", "interp",
|
||||||
"delay", "kp", "kv", "dampratio", "timeconst"},
|
"delay", "kp", "kv", "dampratio", "timeconst"},
|
||||||
{"velocity", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "gear",
|
{"velocity", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
||||||
"damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay", "kv"},
|
"gear", "damping", "armature", "cranklength", "user", "group", "nsample",
|
||||||
{"intvelocity", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "actrange",
|
"interp", "delay", "kv"},
|
||||||
"inheritrange", "gear", "damping", "armature", "cranklength", "user", "group",
|
{"intvelocity", "?", "ctrllimited", "forcelimited", "ctrlrange",
|
||||||
"nsample", "interp", "delay", "kp", "kv", "dampratio"},
|
"forcerange", "actrange", "inheritrange", "gear", "damping",
|
||||||
{"damper", "?", "forcelimited", "ctrlrange", "forcerange",
|
"armature", "cranklength", "user", "group", "nsample",
|
||||||
"gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay", "kv"},
|
"interp", "delay", "kp", "kv", "dampratio"},
|
||||||
|
{"damper", "?", "forcelimited", "ctrlrange", "forcerange", "gear",
|
||||||
|
"damping", "armature", "cranklength", "user", "group", "nsample", "interp",
|
||||||
|
"delay", "kv"},
|
||||||
{"cylinder", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
{"cylinder", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
||||||
"gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay",
|
"gear", "damping", "armature", "cranklength", "user", "group", "nsample",
|
||||||
"timeconst", "area", "diameter", "bias"},
|
"interp", "delay", "timeconst", "area", "diameter", "bias"},
|
||||||
{"muscle", "?", "ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
{"muscle", "?", "ctrllimited", "forcelimited", "ctrlrange",
|
||||||
"gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay",
|
"forcerange", "gear", "damping", "armature", "cranklength",
|
||||||
"timeconst", "range", "force", "scale",
|
"user", "group", "nsample", "interp", "delay",
|
||||||
"lmin", "lmax", "vmax", "fpmax", "fvmax"},
|
"timeconst", "range", "force", "scale", "lmin",
|
||||||
{"adhesion", "?", "forcelimited", "ctrlrange", "forcerange",
|
"lmax", "vmax", "fpmax", "fvmax"},
|
||||||
"gain", "user", "group", "nsample", "interp", "delay"},
|
{"adhesion", "?", "forcelimited", "ctrlrange", "forcerange", "gain", "user",
|
||||||
{"dcmotor", "?", "ctrllimited", "ctrlrange",
|
"group", "nsample", "interp", "delay"},
|
||||||
"gear", "damping", "armature", "cranklength", "user", "group", "nsample", "interp", "delay",
|
{"dcmotor", "?", "ctrllimited", "ctrlrange", "gear",
|
||||||
"motorconst", "resistance", "nominal", "saturation",
|
"damping", "armature", "cranklength", "user", "group",
|
||||||
"inductance", "cogging", "controller", "input", "thermal", "lugre"},
|
"nsample", "interp", "delay", "motorconst", "resistance",
|
||||||
|
"nominal", "saturation", "inductance", "cogging", "controller",
|
||||||
|
"input", "thermal", "lugre"},
|
||||||
{">"},
|
{">"},
|
||||||
|
|
||||||
{"extension", "*"},
|
{"extension", "*"},
|
||||||
@@ -317,40 +403,67 @@ std::vector<const char*> MJCF[nMJCF] = {
|
|||||||
{"config", "*", "key", "value"},
|
{"config", "*", "key", "value"},
|
||||||
{">"},
|
{">"},
|
||||||
{">"},
|
{">"},
|
||||||
{"hfield", "*", "name", "content_type", "file", "nrow", "ncol", "size", "elevation"},
|
{"hfield", "*", "name", "content_type", "file", "nrow", "ncol", "size",
|
||||||
{"skin", "*", "name", "file", "material", "rgba", "inflate",
|
"elevation"},
|
||||||
"vertex", "texcoord", "face", "group"},
|
{"skin", "*", "name", "file", "material", "rgba", "inflate", "vertex",
|
||||||
|
"texcoord", "face", "group"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"bone", "*", "body", "bindpos", "bindquat", "vertid", "vertweight"},
|
{"bone", "*", "body", "bindpos", "bindquat", "vertid", "vertweight"},
|
||||||
{">"},
|
{">"},
|
||||||
{"texture", "*", "name", "type", "colorspace", "content_type", "file", "gridsize",
|
{"texture", "*", "name", "type", "colorspace",
|
||||||
"gridlayout", "fileright", "fileleft", "fileup", "filedown", "filefront", "fileback",
|
"content_type", "file", "gridsize", "gridlayout", "fileright",
|
||||||
"builtin", "rgb1", "rgb2", "mark", "markrgb", "random", "width", "height",
|
"fileleft", "fileup", "filedown", "filefront", "fileback",
|
||||||
"hflip", "vflip", "nchannel"},
|
"builtin", "rgb1", "rgb2", "mark", "markrgb",
|
||||||
|
"random", "width", "height", "hflip", "vflip",
|
||||||
|
"nchannel"},
|
||||||
{"material", "*", "name", "class", "texture", "texrepeat", "texuniform",
|
{"material", "*", "name", "class", "texture", "texrepeat", "texuniform",
|
||||||
"emission", "specular", "shininess", "reflectance", "metallic", "roughness", "rgba"},
|
"emission", "specular", "shininess", "reflectance", "metallic",
|
||||||
|
"roughness", "rgba"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"layer", "*", "texture", "role"},
|
{"layer", "*", "texture", "role"},
|
||||||
{">"},
|
{">"},
|
||||||
{"model", "*", "name", "file", "content_type"},
|
{"model", "*", "name", "file", "content_type"},
|
||||||
{">"},
|
{">"},
|
||||||
|
|
||||||
{"body", "R", "name", "childclass", "pos", "quat", "mocap",
|
{"body", "R", "name", "childclass", "pos", "quat", "mocap", "axisangle",
|
||||||
"axisangle", "xyaxes", "zaxis", "euler", "gravcomp", "sleep", "user"},
|
"xyaxes", "zaxis", "euler", "gravcomp", "sleep", "user"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"inertial", "?", "pos", "quat", "mass", "diaginertia",
|
{"inertial", "?", "pos", "quat", "mass", "diaginertia", "axisangle",
|
||||||
"axisangle", "xyaxes", "zaxis", "euler", "fullinertia"},
|
"xyaxes", "zaxis", "euler", "fullinertia"},
|
||||||
{"joint", "*", "name", "class", "type", "group", "pos", "axis",
|
{"joint",
|
||||||
"springdamper", "limited", "actuatorfrclimited",
|
"*",
|
||||||
"solreflimit", "solimplimit", "solreffriction", "solimpfriction",
|
"name",
|
||||||
"stiffness", "range", "actuatorfrcrange", "actuatorgravcomp", "margin", "ref",
|
"class",
|
||||||
"springref", "armature", "damping", "frictionloss", "user"},
|
"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"},
|
{"freejoint", "*", "name", "group", "align"},
|
||||||
{"geom", "*", "name", "class", "type", "contype", "conaffinity", "condim",
|
{"geom", "*", "name", "class", "type",
|
||||||
"group", "priority", "size", "material", "friction", "mass", "density",
|
"contype", "conaffinity", "condim", "group", "priority",
|
||||||
"shellinertia", "solmix", "solref", "solimp",
|
"size", "material", "friction", "mass", "density",
|
||||||
"margin", "gap", "fromto", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler",
|
"shellinertia", "solmix", "solref", "solimp", "margin",
|
||||||
"hfield", "mesh", "fitscale", "rgba", "fluidshape", "fluidcoef", "user"},
|
"gap", "fromto", "pos", "quat", "axisangle",
|
||||||
|
"xyaxes", "zaxis", "euler", "hfield", "mesh",
|
||||||
|
"fitscale", "rgba", "fluidshape", "fluidcoef", "user"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"plugin", "*", "plugin", "instance"},
|
{"plugin", "*", "plugin", "instance"},
|
||||||
{"<"},
|
{"<"},
|
||||||
@@ -358,45 +471,52 @@ std::vector<const char*> MJCF[nMJCF] = {
|
|||||||
{">"},
|
{">"},
|
||||||
{">"},
|
{">"},
|
||||||
{"attach", "*", "model", "body", "prefix"},
|
{"attach", "*", "model", "body", "prefix"},
|
||||||
{"site", "*", "name", "class", "type", "group", "pos", "quat",
|
{"site", "*", "name", "class", "type", "group", "pos", "quat", "material",
|
||||||
"material", "size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"},
|
"size", "fromto", "axisangle", "xyaxes", "zaxis", "euler", "rgba", "user"},
|
||||||
{"camera", "*", "name", "class", "projection", "fovy", "ipd", "resolution", "output", "pos",
|
{"camera", "*", "name", "class", "projection",
|
||||||
"quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "target",
|
"fovy", "ipd", "resolution", "output", "pos",
|
||||||
"focal", "focalpixel", "principal", "principalpixel", "sensorsize", "user"},
|
"quat", "axisangle", "xyaxes", "zaxis", "euler",
|
||||||
{"light", "*", "name", "class", "directional", "type", "castshadow", "active",
|
"mode", "target", "focal", "focalpixel", "principal",
|
||||||
"pos", "dir", "bulbradius", "intensity", "range", "attenuation", "cutoff",
|
"principalpixel", "sensorsize", "user"},
|
||||||
"exponent", "ambient", "diffuse", "specular", "mode", "target", "texture"},
|
{"light", "*", "name", "class", "directional",
|
||||||
|
"type", "castshadow", "active", "pos", "dir",
|
||||||
|
"bulbradius", "intensity", "range", "attenuation", "cutoff",
|
||||||
|
"exponent", "ambient", "diffuse", "specular", "mode",
|
||||||
|
"target", "texture"},
|
||||||
{"plugin", "*", "plugin", "instance"},
|
{"plugin", "*", "plugin", "instance"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"config", "*", "key", "value"},
|
{"config", "*", "key", "value"},
|
||||||
{">"},
|
{">"},
|
||||||
{"composite", "*", "prefix", "type", "count", "offset",
|
{"composite", "*", "prefix", "type", "count", "offset", "vertex", "initial",
|
||||||
"vertex", "initial", "curve", "size", "quat"},
|
"curve", "size", "quat"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"joint", "*", "kind", "group", "stiffness", "damping", "armature",
|
{"joint", "*", "kind", "group", "stiffness", "damping", "armature",
|
||||||
"solreffix", "solimpfix", "type", "axis",
|
"solreffix", "solimpfix", "type", "axis", "limited", "range", "margin",
|
||||||
"limited", "range", "margin", "solreflimit", "solimplimit",
|
"solreflimit", "solimplimit", "frictionloss", "solreffriction",
|
||||||
"frictionloss", "solreffriction", "solimpfriction"},
|
"solimpfriction"},
|
||||||
{"skin", "?", "texcoord", "material", "group", "rgba", "inflate", "subgrid"},
|
{"skin", "?", "texcoord", "material", "group", "rgba", "inflate",
|
||||||
{"geom", "?", "type", "contype", "conaffinity", "condim",
|
"subgrid"},
|
||||||
"group", "priority", "size", "material", "rgba", "friction", "mass",
|
{"geom", "?", "type", "contype", "conaffinity", "condim", "group",
|
||||||
"density", "solmix", "solref", "solimp", "margin", "gap"},
|
"priority", "size", "material", "rgba", "friction", "mass", "density",
|
||||||
|
"solmix", "solref", "solimp", "margin", "gap"},
|
||||||
{"site", "?", "group", "size", "material", "rgba"},
|
{"site", "?", "group", "size", "material", "rgba"},
|
||||||
{"plugin", "*", "plugin", "instance"},
|
{"plugin", "*", "plugin", "instance"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"config", "*", "key", "value"},
|
{"config", "*", "key", "value"},
|
||||||
{">"},
|
{">"},
|
||||||
{">"},
|
{">"},
|
||||||
{"flexcomp", "*", "name", "type", "group", "dim", "dof",
|
{"flexcomp", "*", "name", "type", "group", "dim",
|
||||||
"count", "cellcount", "spacing", "radius", "rigid", "mass", "inertiabox",
|
"dof", "count", "cellcount", "spacing", "radius", "rigid",
|
||||||
"scale", "file", "point", "element", "texcoord", "material", "rgba",
|
"mass", "inertiabox", "scale", "file", "point", "element",
|
||||||
"flatskin", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "origin"},
|
"texcoord", "material", "rgba", "flatskin", "pos", "quat",
|
||||||
|
"axisangle", "xyaxes", "zaxis", "euler", "origin"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"edge", "?", "equality", "solref", "solimp", "stiffness", "damping"},
|
{"edge", "?", "equality", "solref", "solimp", "stiffness", "damping"},
|
||||||
{"elasticity", "?", "young", "poisson", "damping", "thickness", "elastic2d"},
|
{"elasticity", "?", "young", "poisson", "damping", "thickness",
|
||||||
{"contact", "?", "contype", "conaffinity", "condim", "priority",
|
"elastic2d"},
|
||||||
"friction", "solmix", "solref", "solimp", "margin", "gap",
|
{"contact", "?", "contype", "conaffinity", "condim", "priority", "friction",
|
||||||
"internal", "selfcollide", "activelayers", "passive"},
|
"solmix", "solref", "solimp", "margin", "gap", "internal", "selfcollide",
|
||||||
|
"activelayers", "passive"},
|
||||||
{"pin", "*", "id", "range", "grid", "gridrange"},
|
{"pin", "*", "id", "range", "grid", "gridrange"},
|
||||||
{"plugin", "*", "plugin", "instance"},
|
{"plugin", "*", "plugin", "instance"},
|
||||||
{"<"},
|
{"<"},
|
||||||
@@ -407,17 +527,19 @@ std::vector<const char*> MJCF[nMJCF] = {
|
|||||||
|
|
||||||
{"deformable", "*"},
|
{"deformable", "*"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"flex", "*", "name", "group", "dim", "radius", "material", "rgba", "flatskin", "body",
|
{"flex", "*", "name", "group", "dim", "radius", "material", "rgba",
|
||||||
"vertex", "element", "texcoord", "elemtexcoord", "node", "cellcount", "dof"},
|
"flatskin", "body", "vertex", "element", "texcoord", "elemtexcoord",
|
||||||
|
"node", "cellcount", "dof"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"contact", "?", "contype", "conaffinity", "condim", "priority",
|
{"contact", "?", "contype", "conaffinity", "condim", "priority", "friction",
|
||||||
"friction", "solmix", "solref", "solimp", "margin", "gap",
|
"solmix", "solref", "solimp", "margin", "gap", "internal", "selfcollide",
|
||||||
"internal", "selfcollide", "activelayers", "passive"},
|
"activelayers", "passive"},
|
||||||
{"edge", "?", "stiffness", "damping"},
|
{"edge", "?", "stiffness", "damping"},
|
||||||
{"elasticity", "?", "young", "poisson", "damping", "thickness", "elastic2d"},
|
{"elasticity", "?", "young", "poisson", "damping", "thickness",
|
||||||
|
"elastic2d"},
|
||||||
{">"},
|
{">"},
|
||||||
{"skin", "*", "name", "file", "material", "rgba", "inflate",
|
{"skin", "*", "name", "file", "material", "rgba", "inflate", "vertex",
|
||||||
"vertex", "texcoord", "face", "group"},
|
"texcoord", "face", "group"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"bone", "*", "body", "bindpos", "bindquat", "vertid", "vertweight"},
|
{"bone", "*", "body", "bindpos", "bindquat", "vertid", "vertweight"},
|
||||||
{">"},
|
{">"},
|
||||||
@@ -432,36 +554,70 @@ std::vector<const char*> MJCF[nMJCF] = {
|
|||||||
|
|
||||||
{"equality", "*"},
|
{"equality", "*"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"connect", "*", "name", "class", "body1", "body2", "anchor",
|
{"connect", "*", "name", "class", "body1", "body2", "anchor", "site1",
|
||||||
"site1", "site2", "active", "solref", "solimp"},
|
"site2", "active", "solref", "solimp"},
|
||||||
{"weld", "*", "name", "class", "body1", "body2", "relpose", "anchor",
|
{"weld", "*", "name", "class", "body1", "body2", "relpose", "anchor",
|
||||||
"site1", "site2", "active", "solref", "solimp", "torquescale"},
|
"site1", "site2", "active", "solref", "solimp", "torquescale"},
|
||||||
{"joint", "*", "name", "class", "joint1", "joint2", "polycoef",
|
{"joint", "*", "name", "class", "joint1", "joint2", "polycoef", "active",
|
||||||
"active", "solref", "solimp"},
|
"solref", "solimp"},
|
||||||
{"tendon", "*", "name", "class", "tendon1", "tendon2", "polycoef",
|
{"tendon", "*", "name", "class", "tendon1", "tendon2", "polycoef", "active",
|
||||||
"active", "solref", "solimp"},
|
"solref", "solimp"},
|
||||||
{"flex", "*", "name", "class", "flex",
|
{"flex", "*", "name", "class", "flex", "active", "solref", "solimp"},
|
||||||
"active", "solref", "solimp"},
|
{"flexvert", "*", "name", "class", "flex", "active", "solref", "solimp"},
|
||||||
{"flexvert", "*", "name", "class", "flex",
|
{"flexstrain", "*", "name", "class", "flex", "cell", "active", "solref",
|
||||||
"active", "solref", "solimp"},
|
"solimp"},
|
||||||
{"flexstrain", "*", "name", "class", "flex", "cell",
|
|
||||||
"active", "solref", "solimp"},
|
|
||||||
{">"},
|
{">"},
|
||||||
|
|
||||||
{"tendon", "*"},
|
{"tendon", "*"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"spatial", "*", "name", "class", "group", "limited", "actuatorfrclimited", "range",
|
{"spatial",
|
||||||
"actuatorfrcrange", "solreflimit", "solimplimit", "solreffriction", "solimpfriction",
|
"*",
|
||||||
"frictionloss", "springlength", "width", "material",
|
"name",
|
||||||
"margin", "stiffness", "damping", "armature", "rgba", "user"},
|
"class",
|
||||||
|
"group",
|
||||||
|
"limited",
|
||||||
|
"actuatorfrclimited",
|
||||||
|
"range",
|
||||||
|
"actuatorfrcrange",
|
||||||
|
"solreflimit",
|
||||||
|
"solimplimit",
|
||||||
|
"solreffriction",
|
||||||
|
"solimpfriction",
|
||||||
|
"frictionloss",
|
||||||
|
"springlength",
|
||||||
|
"width",
|
||||||
|
"material",
|
||||||
|
"margin",
|
||||||
|
"stiffness",
|
||||||
|
"damping",
|
||||||
|
"armature",
|
||||||
|
"rgba",
|
||||||
|
"user"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"site", "*", "site"},
|
{"site", "*", "site"},
|
||||||
{"geom", "*", "geom", "sidesite"},
|
{"geom", "*", "geom", "sidesite"},
|
||||||
{"pulley", "*", "divisor"},
|
{"pulley", "*", "divisor"},
|
||||||
{">"},
|
{">"},
|
||||||
{"fixed", "*", "name", "class", "group", "limited", "actuatorfrclimited", "range",
|
{"fixed",
|
||||||
"actuatorfrcrange", "solreflimit", "solimplimit", "solreffriction", "solimpfriction",
|
"*",
|
||||||
"frictionloss", "springlength", "margin", "stiffness", "damping", "armature", "user"},
|
"name",
|
||||||
|
"class",
|
||||||
|
"group",
|
||||||
|
"limited",
|
||||||
|
"actuatorfrclimited",
|
||||||
|
"range",
|
||||||
|
"actuatorfrcrange",
|
||||||
|
"solreflimit",
|
||||||
|
"solimplimit",
|
||||||
|
"solreffriction",
|
||||||
|
"solimpfriction",
|
||||||
|
"frictionloss",
|
||||||
|
"springlength",
|
||||||
|
"margin",
|
||||||
|
"stiffness",
|
||||||
|
"damping",
|
||||||
|
"armature",
|
||||||
|
"user"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"joint", "*", "joint", "coef"},
|
{"joint", "*", "joint", "coef"},
|
||||||
{">"},
|
{">"},
|
||||||
@@ -469,61 +625,89 @@ std::vector<const char*> MJCF[nMJCF] = {
|
|||||||
|
|
||||||
{"actuator", "*"},
|
{"actuator", "*"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"general", "*", "name", "class", "group", "nsample", "interp", "delay",
|
{"general", "*", "name",
|
||||||
"ctrllimited", "forcelimited", "actlimited", "ctrlrange", "forcerange", "actrange",
|
"class", "group", "nsample",
|
||||||
"lengthrange", "gear", "damping", "armature", "cranklength", "user",
|
"interp", "delay", "ctrllimited",
|
||||||
"joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite",
|
"forcelimited", "actlimited", "ctrlrange",
|
||||||
"body", "actdim", "dyntype", "gaintype", "biastype", "dynprm", "gainprm", "biasprm",
|
"forcerange", "actrange", "lengthrange",
|
||||||
"actearly"},
|
"gear", "damping", "armature",
|
||||||
{"motor", "*", "name", "class", "group", "nsample", "interp", "delay",
|
"cranklength", "user", "joint",
|
||||||
"ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
"jointinparent", "tendon", "slidersite",
|
||||||
"lengthrange", "gear", "damping", "armature", "cranklength", "user",
|
"cranksite", "site", "refsite",
|
||||||
"joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite"},
|
"body", "actdim", "dyntype",
|
||||||
{"position", "*", "name", "class", "group", "nsample", "interp", "delay",
|
"gaintype", "biastype", "dynprm",
|
||||||
"ctrllimited", "forcelimited", "ctrlrange", "inheritrange", "forcerange",
|
"gainprm", "biasprm", "actearly"},
|
||||||
"lengthrange", "gear", "damping", "armature", "cranklength", "user",
|
{"motor", "*", "name", "class", "group",
|
||||||
"joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite",
|
"nsample", "interp", "delay", "ctrllimited", "forcelimited",
|
||||||
"kp", "kv", "dampratio", "timeconst"},
|
"ctrlrange", "forcerange", "lengthrange", "gear", "damping",
|
||||||
{"velocity", "*", "name", "class", "group", "nsample", "interp", "delay",
|
"armature", "cranklength", "user", "joint", "jointinparent",
|
||||||
"ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
"tendon", "slidersite", "cranksite", "site", "refsite"},
|
||||||
"lengthrange", "gear", "damping", "armature", "cranklength", "user",
|
{"position", "*", "name",
|
||||||
"joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite",
|
"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"},
|
"kv"},
|
||||||
{"intvelocity", "*", "name", "class", "group", "nsample", "interp", "delay",
|
{"intvelocity", "*",
|
||||||
|
"name", "class",
|
||||||
|
"group", "nsample",
|
||||||
|
"interp", "delay",
|
||||||
"ctrllimited", "forcelimited",
|
"ctrllimited", "forcelimited",
|
||||||
"ctrlrange", "forcerange", "actrange", "inheritrange", "lengthrange",
|
"ctrlrange", "forcerange",
|
||||||
"gear", "damping", "armature", "cranklength", "user",
|
"actrange", "inheritrange",
|
||||||
"joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite",
|
"lengthrange", "gear",
|
||||||
"kp", "kv", "dampratio"},
|
"damping", "armature",
|
||||||
{"damper", "*", "name", "class", "group", "nsample", "interp", "delay",
|
"cranklength", "user",
|
||||||
"forcelimited", "ctrlrange", "forcerange",
|
"joint", "jointinparent",
|
||||||
"lengthrange", "gear", "damping", "armature", "cranklength", "user",
|
"tendon", "slidersite",
|
||||||
"joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite",
|
"cranksite", "site",
|
||||||
"kv"},
|
"refsite", "kp",
|
||||||
{"cylinder", "*", "name", "class", "group", "nsample", "interp", "delay",
|
"kv", "dampratio"},
|
||||||
"ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
{"damper", "*", "name", "class", "group",
|
||||||
"lengthrange", "gear", "damping", "armature", "cranklength", "user",
|
"nsample", "interp", "delay", "forcelimited", "ctrlrange",
|
||||||
"joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite",
|
"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"},
|
"timeconst", "area", "diameter", "bias"},
|
||||||
{"muscle", "*", "name", "class", "group", "nsample", "interp", "delay",
|
{"muscle", "*", "name", "class", "group",
|
||||||
"ctrllimited", "forcelimited", "ctrlrange", "forcerange",
|
"nsample", "interp", "delay", "ctrllimited", "forcelimited",
|
||||||
"lengthrange", "gear", "damping", "armature", "cranklength", "user",
|
"ctrlrange", "forcerange", "lengthrange", "gear", "damping",
|
||||||
"joint", "jointinparent", "tendon", "slidersite", "cranksite",
|
"armature", "cranklength", "user", "joint", "jointinparent",
|
||||||
"timeconst", "tausmooth", "range", "force", "scale",
|
"tendon", "slidersite", "cranksite", "timeconst", "tausmooth",
|
||||||
"lmin", "lmax", "vmax", "fpmax", "fvmax"},
|
"range", "force", "scale", "lmin", "lmax",
|
||||||
|
"vmax", "fpmax", "fvmax"},
|
||||||
{"adhesion", "*", "name", "class", "group", "nsample", "interp", "delay",
|
{"adhesion", "*", "name", "class", "group", "nsample", "interp", "delay",
|
||||||
"forcelimited", "ctrlrange", "forcerange", "user", "body", "gain"},
|
"forcelimited", "ctrlrange", "forcerange", "user", "body", "gain"},
|
||||||
{"dcmotor", "*", "name", "class", "group", "nsample", "interp", "delay",
|
{"dcmotor", "*", "name", "class", "group",
|
||||||
"ctrllimited", "ctrlrange",
|
"nsample", "interp", "delay", "ctrllimited", "ctrlrange",
|
||||||
"lengthrange", "gear", "damping", "armature", "cranklength", "user",
|
"lengthrange", "gear", "damping", "armature", "cranklength",
|
||||||
"joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite",
|
"user", "joint", "jointinparent", "tendon", "slidersite",
|
||||||
"motorconst", "resistance", "nominal", "saturation",
|
"cranksite", "site", "refsite", "motorconst", "resistance",
|
||||||
"inductance", "cogging", "controller", "thermal", "lugre", "input"},
|
"nominal", "saturation", "inductance", "cogging", "controller",
|
||||||
{"plugin", "*", "name", "class", "plugin", "instance", "group", "nsample", "interp", "delay",
|
"thermal", "lugre", "input"},
|
||||||
"ctrllimited", "forcelimited", "actlimited", "ctrlrange", "forcerange", "actrange",
|
{"plugin", "*", "name", "class",
|
||||||
"lengthrange", "gear", "damping", "armature", "cranklength", "joint", "jointinparent",
|
"plugin", "instance", "group", "nsample",
|
||||||
"site", "actdim", "dyntype", "dynprm", "tendon", "cranksite", "slidersite", "user",
|
"interp", "delay", "ctrllimited", "forcelimited",
|
||||||
"actearly"},
|
"actlimited", "ctrlrange", "forcerange", "actrange",
|
||||||
|
"lengthrange", "gear", "damping", "armature",
|
||||||
|
"cranklength", "joint", "jointinparent", "site",
|
||||||
|
"actdim", "dyntype", "dynprm", "tendon",
|
||||||
|
"cranksite", "slidersite", "user", "actearly"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"config", "*", "key", "value"},
|
{"config", "*", "key", "value"},
|
||||||
{">"},
|
{">"},
|
||||||
@@ -531,58 +715,105 @@ std::vector<const char*> MJCF[nMJCF] = {
|
|||||||
|
|
||||||
{"sensor", "*"},
|
{"sensor", "*"},
|
||||||
{"<"},
|
{"<"},
|
||||||
{"touch", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"touch", "*", "name", "site", "nsample", "interp", "delay", "interval",
|
||||||
{"accelerometer", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"cutoff", "noise", "user"},
|
||||||
{"velocimeter", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"accelerometer", "*", "name", "site", "nsample", "interp", "delay",
|
||||||
{"gyro", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"force", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"velocimeter", "*", "name", "site", "nsample", "interp", "delay",
|
||||||
{"torque", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"magnetometer", "*", "name", "site", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"gyro", "*", "name", "site", "nsample", "interp", "delay", "interval",
|
||||||
{"camprojection", "*", "name", "site", "camera", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"cutoff", "noise", "user"},
|
||||||
{"rangefinder", "*", "name", "site", "camera", "data", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"force", "*", "name", "site", "nsample", "interp", "delay", "interval",
|
||||||
{"jointpos", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"cutoff", "noise", "user"},
|
||||||
{"jointvel", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"torque", "*", "name", "site", "nsample", "interp", "delay", "interval",
|
||||||
{"tendonpos", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"cutoff", "noise", "user"},
|
||||||
{"tendonvel", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"magnetometer", "*", "name", "site", "nsample", "interp", "delay",
|
||||||
{"actuatorpos", "*", "name", "actuator", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"actuatorvel", "*", "name", "actuator", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"camprojection", "*", "name", "site", "camera", "nsample", "interp",
|
||||||
{"actuatorfrc", "*", "name", "actuator", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"delay", "interval", "cutoff", "noise", "user"},
|
||||||
{"jointactuatorfrc", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"rangefinder", "*", "name", "site", "camera", "data", "nsample", "interp",
|
||||||
{"tendonactuatorfrc", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"delay", "interval", "cutoff", "noise", "user"},
|
||||||
{"ballquat", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"jointpos", "*", "name", "joint", "nsample", "interp", "delay", "interval",
|
||||||
{"ballangvel", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"cutoff", "noise", "user"},
|
||||||
{"jointlimitpos", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"jointvel", "*", "name", "joint", "nsample", "interp", "delay", "interval",
|
||||||
{"jointlimitvel", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"cutoff", "noise", "user"},
|
||||||
{"jointlimitfrc", "*", "name", "joint", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"tendonpos", "*", "name", "tendon", "nsample", "interp", "delay",
|
||||||
{"tendonlimitpos", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"tendonlimitvel", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"tendonvel", "*", "name", "tendon", "nsample", "interp", "delay",
|
||||||
{"tendonlimitfrc", "*", "name", "tendon", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"framepos", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"actuatorpos", "*", "name", "actuator", "nsample", "interp", "delay",
|
||||||
{"framequat", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"framexaxis", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"actuatorvel", "*", "name", "actuator", "nsample", "interp", "delay",
|
||||||
{"frameyaxis", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"framezaxis", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"actuatorfrc", "*", "name", "actuator", "nsample", "interp", "delay",
|
||||||
{"framelinvel", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"frameangvel", "*", "name", "objtype", "objname", "reftype", "refname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"jointactuatorfrc", "*", "name", "joint", "nsample", "interp", "delay",
|
||||||
{"framelinacc", "*", "name", "objtype", "objname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"frameangacc", "*", "name", "objtype", "objname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"tendonactuatorfrc", "*", "name", "tendon", "nsample", "interp", "delay",
|
||||||
{"subtreecom", "*", "name", "body", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"subtreelinvel", "*", "name", "body", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"ballquat", "*", "name", "joint", "nsample", "interp", "delay", "interval",
|
||||||
{"subtreeangmom", "*", "name", "body", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"cutoff", "noise", "user"},
|
||||||
{"insidesite", "*", "name", "site", "objtype", "objname", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"ballangvel", "*", "name", "joint", "nsample", "interp", "delay",
|
||||||
{"distance", "*", "name", "geom1", "geom2", "body1", "body2", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"normal", "*", "name", "geom1", "geom2", "body1", "body2", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"jointlimitpos", "*", "name", "joint", "nsample", "interp", "delay",
|
||||||
{"fromto", "*", "name", "geom1", "geom2", "body1", "body2", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"contact", "*", "name", "geom1", "geom2", "body1", "body2", "subtree1", "subtree2", "site",
|
{"jointlimitvel", "*", "name", "joint", "nsample", "interp", "delay",
|
||||||
"num", "data", "reduce", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"e_potential", "*", "name", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"jointlimitfrc", "*", "name", "joint", "nsample", "interp", "delay",
|
||||||
{"e_kinetic", "*", "name", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"clock", "*", "name", "nsample", "interp", "delay", "interval", "cutoff", "noise", "user"},
|
{"tendonlimitpos", "*", "name", "tendon", "nsample", "interp", "delay",
|
||||||
{"tactile", "*", "name", "geom", "mesh", "nsample", "interp", "delay", "interval", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"user", "*", "name", "objtype", "objname", "datatype", "needstage",
|
{"tendonlimitvel", "*", "name", "tendon", "nsample", "interp", "delay",
|
||||||
"dim", "cutoff", "noise", "user"},
|
"interval", "cutoff", "noise", "user"},
|
||||||
{"plugin", "*", "name", "plugin", "instance", "cutoff", "objtype", "objname", "reftype", "refname",
|
{"tendonlimitfrc", "*", "name", "tendon", "nsample", "interp", "delay",
|
||||||
"user"},
|
"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"},
|
{"config", "*", "key", "value"},
|
||||||
{">"},
|
{">"},
|
||||||
@@ -590,10 +821,10 @@ std::vector<const char*> MJCF[nMJCF] = {
|
|||||||
|
|
||||||
{"keyframe", "*"},
|
{"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}
|
{"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
|
// LR mode
|
||||||
const int lrmode_sz = 4;
|
const int lrmode_sz = 4;
|
||||||
@@ -1226,6 +1462,8 @@ void mjXReader::Compiler(XMLElement* section, mjSpec* s) {
|
|||||||
read("inertiagrouprange", 2, s->compiler.inertiagrouprange);
|
read("inertiagrouprange", 2, s->compiler.inertiagrouprange);
|
||||||
read("alignfree", s->compiler.alignfree, bool_map, 2);
|
read("alignfree", s->compiler.alignfree, bool_map, 2);
|
||||||
read("saveinertial", s->compiler.saveinertial, bool_map, 2);
|
read("saveinertial", s->compiler.saveinertial, bool_map, 2);
|
||||||
|
MapValue(section, "conflict", &s->compiler.conflict, conflict_map,
|
||||||
|
conflict_sz);
|
||||||
|
|
||||||
// lengthrange subelement
|
// lengthrange subelement
|
||||||
XMLElement* elem = FindSubElem(section, "lengthrange");
|
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
|
// body/world section parser; recursive
|
||||||
void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame,
|
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
|
// attach to parent
|
||||||
if (!mjs_attach(body->element, pframe->element, /*prefix=*/"", suffix.c_str())) {
|
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
|
// delete subtree
|
||||||
if (mjs_delete(spec, subtree->element)) {
|
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;
|
child = child_body->element;
|
||||||
}
|
}
|
||||||
if (!mjs_attach(pframe->element, child, prefix.c_str(), "")) {
|
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 {
|
} else {
|
||||||
// only set frame to existing body
|
// only set frame to existing body
|
||||||
if (mjs_setFrame(child_body->element, pframe)) {
|
if (mjs_setFrame(child_body->element, pframe)) {
|
||||||
throw mjXError(elem, "%s", mjs_getError(spec));
|
throw mjXError(elem, "%s", stripError(mjs_getError(spec)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1034,6 +1034,8 @@ void mjXWriter::Compiler(XMLElement* root) {
|
|||||||
if (!model->compiler.autolimits) {
|
if (!model->compiler.autolimits) {
|
||||||
WriteAttrTxt(section, "autolimits", "false");
|
WriteAttrTxt(section, "autolimits", "false");
|
||||||
}
|
}
|
||||||
|
WriteAttrKey(section, "conflict", conflict_map, conflict_sz,
|
||||||
|
model->compiler.conflict, mjCONFLICT_WARNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ using ::testing::Truly;
|
|||||||
bool IsBenignWarning(const std::string& msg) {
|
bool IsBenignWarning(const std::string& msg) {
|
||||||
static const char* const kBenignWarnings[] = {
|
static const char* const kBenignWarnings[] = {
|
||||||
"is not rigid and has no equality constraints",
|
"is not rigid and has no equality constraints",
|
||||||
|
"Attach conflict",
|
||||||
};
|
};
|
||||||
for (const char* warning : kBenignWarnings) {
|
for (const char* warning : kBenignWarnings) {
|
||||||
if (absl::StrContains(msg, warning)) {
|
if (absl::StrContains(msg, warning)) {
|
||||||
|
|||||||
+101
-8
@@ -2668,18 +2668,17 @@ void AttachNestedKeyframe(bool compile) {
|
|||||||
// compile required before further attachment
|
// compile required before further attachment
|
||||||
mjModel* m_child = compile ? mj_compile(child, 0) : nullptr;
|
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
|
// attach child to parent
|
||||||
mjs_attach(mjs_findFrame(parent, "frame")->element,
|
mjs_attach(mjs_findFrame(parent, "frame")->element,
|
||||||
mjs_findBody(child, "body")->element, "child-", "");
|
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
|
// compare models
|
||||||
mjtNum tol = 0;
|
mjtNum tol = 0;
|
||||||
@@ -2702,6 +2701,7 @@ void AttachNestedKeyframe(bool compile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(MujocoTest, TestAttachNestedKeyframe) {
|
TEST_F(MujocoTest, TestAttachNestedKeyframe) {
|
||||||
|
mock_warning_handler.ExpectWarnings();
|
||||||
AttachNestedKeyframe(/*compile=*/true);
|
AttachNestedKeyframe(/*compile=*/true);
|
||||||
AttachNestedKeyframe(/*compile=*/false);
|
AttachNestedKeyframe(/*compile=*/false);
|
||||||
}
|
}
|
||||||
@@ -3454,5 +3454,98 @@ TEST_F(MujocoTest, CompileWarningChainedToHandler) {
|
|||||||
mj_deleteSpec(spec);
|
mj_deleteSpec(spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(MujocoTest, WarningAccumulationAndRetrieval) {
|
||||||
|
mock_warning_handler.ExpectWarnings();
|
||||||
|
static constexpr char xml_parent[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<worldbody>
|
||||||
|
<frame name="parent"/>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
static constexpr char xml_child[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<worldbody>
|
||||||
|
<body name="child">
|
||||||
|
<frame name="child_frame"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
static constexpr char xml_gchild[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<worldbody>
|
||||||
|
<body name="gchild"/>
|
||||||
|
</worldbody>
|
||||||
|
<keyframe>
|
||||||
|
<key name="k1"/>
|
||||||
|
</keyframe>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::array<char, 1024> 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"(
|
||||||
|
<mujoco>
|
||||||
|
<worldbody>
|
||||||
|
<flexcomp name="test" type="grid" count="4 4 1" spacing=".2 .2 .2"
|
||||||
|
dim="2" radius=".1"/>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::array<char, 1024> 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
|
||||||
} // namespace mujoco
|
} // namespace mujoco
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ std::vector<std::string> GetRecompileTestModels() {
|
|||||||
if (absl::StrContains(xml, "malformed_") ||
|
if (absl::StrContains(xml, "malformed_") ||
|
||||||
absl::StrContains(xml, "_fail") ||
|
absl::StrContains(xml, "_fail") ||
|
||||||
absl::StrContains(xml, "touch_grid") ||
|
absl::StrContains(xml, "touch_grid") ||
|
||||||
absl::StrContains(xml, "perf") ||
|
absl::StrContains(xml, "perf") || absl::StrContains(xml, "cow") ||
|
||||||
absl::StrContains(xml, "cow")) {
|
// exclude conflict test assets (designed to fail compile)
|
||||||
|
absl::StrContains(xml, "xml/testdata/parent_")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
models.push_back(xml);
|
models.push_back(xml);
|
||||||
|
|||||||
@@ -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 <array>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <mujoco/mjspec.h>
|
||||||
|
#include <mujoco/mujoco.h>
|
||||||
|
#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"(
|
||||||
|
<mujoco>
|
||||||
|
<option timestep="0.005"/>
|
||||||
|
<worldbody>
|
||||||
|
<frame name="attachment"/>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
static constexpr char xml_child[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<option timestep="0.001"/>
|
||||||
|
<worldbody>
|
||||||
|
<body name="child">
|
||||||
|
<geom size="1"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::array<char, 1024> 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"(
|
||||||
|
<mujoco>
|
||||||
|
<compiler conflict="error"/>
|
||||||
|
<option timestep="0.005"/>
|
||||||
|
<worldbody>
|
||||||
|
<body name="parent_body"><geom size="1"/></body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
static constexpr char child_xml[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<option timestep="0.002"/>
|
||||||
|
<worldbody>
|
||||||
|
<body name="child_body"><geom size="1"/></body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::array<char, 1024> 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"(
|
||||||
|
<mujoco>
|
||||||
|
<compiler conflict="error"/>
|
||||||
|
<option timestep="0.005"/>
|
||||||
|
<worldbody>
|
||||||
|
<body name="parent_body"><geom size="1"/></body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
static constexpr char child_xml[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<worldbody>
|
||||||
|
<body name="child_body"><geom size="1"/></body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::array<char, 1024> 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"(
|
||||||
|
<mujoco>
|
||||||
|
<compiler conflict="merge"/>
|
||||||
|
<option timestep="0.005"/>
|
||||||
|
<worldbody/>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
static constexpr char child_xml[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<option timestep="0.002"/>
|
||||||
|
<worldbody/>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::array<char, 1024> 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"(
|
||||||
|
<mujoco>
|
||||||
|
<compiler conflict="merge"/>
|
||||||
|
<worldbody/>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
static constexpr char child_xml[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<option>
|
||||||
|
<flag constraint="disable"/>
|
||||||
|
</option>
|
||||||
|
<worldbody/>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::array<char, 1024> 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"(
|
||||||
|
<mujoco>
|
||||||
|
<compiler conflict="warning"/>
|
||||||
|
<option timestep="0.005"/>
|
||||||
|
<worldbody>
|
||||||
|
<body name="parent">
|
||||||
|
<flexcomp name="grid" type="grid" count="3 3 1" spacing="0.1 0.1 0.1"
|
||||||
|
dim="2" radius="0.01">
|
||||||
|
<contact internal="false"/>
|
||||||
|
</flexcomp>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
// 2. Child 1 has conflicting timestep (0.001)
|
||||||
|
static constexpr char child1_xml[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<option timestep="0.001"/>
|
||||||
|
<worldbody/>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
// 3. Child 2 has conflicting sleep_tolerance (0.005)
|
||||||
|
static constexpr char child2_xml[] = R"(
|
||||||
|
<mujoco>
|
||||||
|
<option sleep_tolerance="0.005"/>
|
||||||
|
<worldbody/>
|
||||||
|
</mujoco>
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::array<char, 1024> 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
|
||||||
@@ -99,6 +99,8 @@ std::vector<std::string> GetWriteReadTestModels() {
|
|||||||
// exclude files that fail since we do not save pinned flex nodes
|
// exclude files that fail since we do not save pinned flex nodes
|
||||||
absl::StrContains(xml, "gripper_trilinear") ||
|
absl::StrContains(xml, "gripper_trilinear") ||
|
||||||
absl::StrContains(xml, "strain") ||
|
absl::StrContains(xml, "strain") ||
|
||||||
|
// exclude conflict test assets (designed to fail compile)
|
||||||
|
absl::StrContains(xml, "xml/testdata/parent_") ||
|
||||||
// exclude mjz testdata with VFS files
|
// exclude mjz testdata with VFS files
|
||||||
absl::StrContains(xml, "mixed_test")) {
|
absl::StrContains(xml, "mixed_test")) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<mujoco model="child_mergable">
|
||||||
|
<option timestep="0.002" iterations="100" gravity="0 0 -10">
|
||||||
|
<flag damper="disable"/>
|
||||||
|
</option>
|
||||||
|
<worldbody>
|
||||||
|
<body name="child_body">
|
||||||
|
<geom name="child_geom" size="1"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<mujoco model="child_unmergable">
|
||||||
|
<option timestep="0.005" iterations="50" gravity="0 0 0">
|
||||||
|
<flag damper="enable" constraint="disable"/>
|
||||||
|
</option>
|
||||||
|
<worldbody>
|
||||||
|
<body name="child_body">
|
||||||
|
<geom name="child_geom" size="1"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<mujoco model="parent_error">
|
||||||
|
<compiler conflict="error"/>
|
||||||
|
<option timestep="0.005" iterations="50" gravity="0 0 -10"/>
|
||||||
|
<asset>
|
||||||
|
<model name="child" file="child_mergable.xml"/>
|
||||||
|
</asset>
|
||||||
|
<worldbody>
|
||||||
|
<body name="parent_body">
|
||||||
|
<geom name="parent_geom" size="2"/>
|
||||||
|
<attach model="child" body="child_body" prefix="child/"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<mujoco model="parent_merge">
|
||||||
|
<compiler conflict="merge"/>
|
||||||
|
<option timestep="0.005" iterations="50" gravity="0 0 -10"/>
|
||||||
|
<asset>
|
||||||
|
<model name="child" file="child_mergable.xml"/>
|
||||||
|
</asset>
|
||||||
|
<worldbody>
|
||||||
|
<body name="parent_body">
|
||||||
|
<geom name="parent_geom" size="2"/>
|
||||||
|
<attach model="child" body="child_body" prefix="child/"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<mujoco model="parent_merge_unmergable">
|
||||||
|
<compiler conflict="merge"/>
|
||||||
|
<option timestep="0.005" iterations="50" gravity="0 0 -10"/>
|
||||||
|
<asset>
|
||||||
|
<model name="child" file="child_unmergable.xml"/>
|
||||||
|
</asset>
|
||||||
|
<worldbody>
|
||||||
|
<body name="parent_body">
|
||||||
|
<geom name="parent_geom" size="2"/>
|
||||||
|
<attach model="child" body="child_body" prefix="child/"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
<mujoco model="parent_warn">
|
||||||
|
<compiler conflict="warning"/>
|
||||||
|
<option timestep="0.005" iterations="50" gravity="0 0 -10">
|
||||||
|
<flag damper="disable"/>
|
||||||
|
</option>
|
||||||
|
<asset>
|
||||||
|
<model name="child" file="child_unmergable.xml"/>
|
||||||
|
</asset>
|
||||||
|
<worldbody>
|
||||||
|
<body name="parent_body">
|
||||||
|
<geom name="parent_geom" size="2"/>
|
||||||
|
<attach model="child" body="child_body" prefix="child/"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>
|
||||||
@@ -3897,5 +3897,80 @@ TEST_F(ActuatorParseTest, DampingArmatureDefaultsPropagate) {
|
|||||||
mj_deleteModel(model);
|
mj_deleteModel(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(XMLReaderTest, AttachConflictXMLWarning) {
|
||||||
|
mock_warning_handler.ExpectWarnings();
|
||||||
|
std::array<char, 1024> error;
|
||||||
|
std::string path = GetTestDataFilePath("xml/testdata/parent_warn.xml");
|
||||||
|
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
|
||||||
|
ASSERT_THAT(spec, NotNull()) << error.data();
|
||||||
|
|
||||||
|
// one grouped warning per attach, containing gravity and damper flag
|
||||||
|
// (constraint is only authored by child, not a conflict)
|
||||||
|
EXPECT_EQ(mjs_numWarnings(spec), 1);
|
||||||
|
std::string w = mjs_getWarning(spec, 0);
|
||||||
|
EXPECT_THAT(w, HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0,"
|
||||||
|
" keeping parent value"));
|
||||||
|
EXPECT_THAT(w, HasSubstr("flag 'Damper'"));
|
||||||
|
|
||||||
|
mjModel* m = mj_compile(spec, nullptr);
|
||||||
|
ASSERT_THAT(m, NotNull());
|
||||||
|
// Gravity should be parent's value (0 0 -10)
|
||||||
|
EXPECT_MJTNUM_EQ(m->opt.gravity[0], 0);
|
||||||
|
EXPECT_MJTNUM_EQ(m->opt.gravity[1], 0);
|
||||||
|
EXPECT_MJTNUM_EQ(m->opt.gravity[2], -10);
|
||||||
|
|
||||||
|
mj_deleteModel(m);
|
||||||
|
mj_deleteSpec(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(XMLReaderTest, AttachConflictXMLMerge) {
|
||||||
|
mock_warning_handler.ExpectWarnings();
|
||||||
|
std::array<char, 1024> error;
|
||||||
|
std::string path = GetTestDataFilePath("xml/testdata/parent_merge.xml");
|
||||||
|
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
|
||||||
|
ASSERT_THAT(spec, NotNull()) << error.data();
|
||||||
|
|
||||||
|
// one grouped warning per attach, containing timestep, iterations, and flag
|
||||||
|
EXPECT_GE(mjs_numWarnings(spec), 1);
|
||||||
|
std::string w = mjs_getWarning(spec, 0);
|
||||||
|
EXPECT_THAT(w, HasSubstr("timestep: parent has 0.005, child has 0.002,"
|
||||||
|
" taking the minimum"));
|
||||||
|
EXPECT_THAT(w, HasSubstr("iterations: parent has 50, child has 100,"
|
||||||
|
" taking the maximum"));
|
||||||
|
|
||||||
|
mjModel* m = mj_compile(spec, nullptr);
|
||||||
|
ASSERT_THAT(m, NotNull());
|
||||||
|
// Timestep should be min (0.002)
|
||||||
|
EXPECT_MJTNUM_EQ(m->opt.timestep, 0.002);
|
||||||
|
// Iterations should be max (100)
|
||||||
|
EXPECT_EQ(m->opt.iterations, 100);
|
||||||
|
|
||||||
|
mj_deleteModel(m);
|
||||||
|
mj_deleteSpec(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(XMLReaderTest, AttachConflictXMLError) {
|
||||||
|
std::array<char, 1024> error;
|
||||||
|
std::string path = GetTestDataFilePath("xml/testdata/parent_error.xml");
|
||||||
|
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
|
||||||
|
|
||||||
|
// Should fail to parse because of conflict in timestep in error mode
|
||||||
|
EXPECT_THAT(spec, IsNull());
|
||||||
|
EXPECT_THAT(error.data(),
|
||||||
|
HasSubstr("timestep: parent has 0.005, child has 0.002"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(XMLReaderTest, AttachConflictXMLMergeUnmergableError) {
|
||||||
|
std::array<char, 1024> error;
|
||||||
|
std::string path =
|
||||||
|
GetTestDataFilePath("xml/testdata/parent_merge_unmergable.xml");
|
||||||
|
mjSpec* spec = mj_parseXML(path.c_str(), nullptr, error.data(), error.size());
|
||||||
|
|
||||||
|
// Should fail to parse because gravity is unmergeable in merge mode
|
||||||
|
EXPECT_THAT(spec, IsNull());
|
||||||
|
EXPECT_THAT(error.data(),
|
||||||
|
HasSubstr("gravity: parent has 0 0 -10, child has 0 0 0"));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace mujoco
|
} // namespace mujoco
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ std::vector<std::string> GetWriteReadTestModels() {
|
|||||||
absl::StrContains(xml, "malformed_") ||
|
absl::StrContains(xml, "malformed_") ||
|
||||||
absl::StrContains(xml, "_fail") ||
|
absl::StrContains(xml, "_fail") ||
|
||||||
// exclude files that are too slow to load
|
// exclude files that are too slow to load
|
||||||
absl::StrContains(xml, "cow") ||
|
absl::StrContains(xml, "cow") || absl::StrContains(xml, "gmsh_") ||
|
||||||
absl::StrContains(xml, "gmsh_") ||
|
|
||||||
absl::StrContains(xml, "shark_") ||
|
absl::StrContains(xml, "shark_") ||
|
||||||
absl::StrContains(xml, "perf") ||
|
absl::StrContains(xml, "perf") ||
|
||||||
// exclude files that fail the comparison test
|
// exclude files that fail the comparison test
|
||||||
@@ -67,7 +66,9 @@ std::vector<std::string> GetWriteReadTestModels() {
|
|||||||
absl::StrContains(xml, "cube_3x3x3") ||
|
absl::StrContains(xml, "cube_3x3x3") ||
|
||||||
// exclude files that fail since we do not save pinned flex nodes
|
// exclude files that fail since we do not save pinned flex nodes
|
||||||
absl::StrContains(xml, "gripper_trilinear") ||
|
absl::StrContains(xml, "gripper_trilinear") ||
|
||||||
absl::StrContains(xml, "strain")) {
|
absl::StrContains(xml, "strain") ||
|
||||||
|
// exclude conflict tests (known option conflict warnings/errors)
|
||||||
|
absl::StrContains(xml, "xml/testdata/parent_")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
models.push_back(xml);
|
models.push_back(xml);
|
||||||
@@ -84,12 +85,6 @@ class WriteReadCompareTest : public XMLWriterTest,
|
|||||||
TEST_P(WriteReadCompareTest, WriteReadCompare) {
|
TEST_P(WriteReadCompareTest, WriteReadCompare) {
|
||||||
std::string xml = GetParam();
|
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
|
// full precision float printing
|
||||||
FullFloatPrecision increase_precision;
|
FullFloatPrecision increase_precision;
|
||||||
|
|
||||||
|
|||||||
@@ -583,6 +583,11 @@ public enum mjtOrientation : int{
|
|||||||
mjORIENTATION_ZAXIS = 3,
|
mjORIENTATION_ZAXIS = 3,
|
||||||
mjORIENTATION_EULER = 4,
|
mjORIENTATION_EULER = 4,
|
||||||
}
|
}
|
||||||
|
public enum mjtConflict : int{
|
||||||
|
mjCONFLICT_WARNING = 0,
|
||||||
|
mjCONFLICT_MERGE = 1,
|
||||||
|
mjCONFLICT_ERROR = 2,
|
||||||
|
}
|
||||||
public enum mjtCTimer : int{
|
public enum mjtCTimer : int{
|
||||||
mjCTIMER_TOTAL = 0,
|
mjCTIMER_TOTAL = 0,
|
||||||
mjCTIMER_ASSETS = 1,
|
mjCTIMER_ASSETS = 1,
|
||||||
@@ -5865,6 +5870,7 @@ public unsafe struct mjsCompiler_ {
|
|||||||
public fixed int inertiagrouprange[2];
|
public fixed int inertiagrouprange[2];
|
||||||
public byte saveinertial;
|
public byte saveinertial;
|
||||||
public int alignfree;
|
public int alignfree;
|
||||||
|
public int conflict;
|
||||||
public mjLROpt_ LRopt;
|
public mjLROpt_ LRopt;
|
||||||
public void* meshdir;
|
public void* meshdir;
|
||||||
public void* texturedir;
|
public void* texturedir;
|
||||||
|
|||||||
@@ -2331,6 +2331,12 @@ struct MjsCompiler {
|
|||||||
void set_alignfree(int value) {
|
void set_alignfree(int value) {
|
||||||
ptr_->alignfree = value;
|
ptr_->alignfree = value;
|
||||||
}
|
}
|
||||||
|
int conflict() const {
|
||||||
|
return ptr_->conflict;
|
||||||
|
}
|
||||||
|
void set_conflict(int value) {
|
||||||
|
ptr_->conflict = value;
|
||||||
|
}
|
||||||
mjString meshdir() const {
|
mjString meshdir() const {
|
||||||
return (ptr_ && ptr_->meshdir) ? *(ptr_->meshdir) : "";
|
return (ptr_ && ptr_->meshdir) ? *(ptr_->meshdir) : "";
|
||||||
}
|
}
|
||||||
@@ -11312,6 +11318,10 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
|
|||||||
enum_<mjtCone>("mjtCone")
|
enum_<mjtCone>("mjtCone")
|
||||||
.value("mjCONE_PYRAMIDAL", mjCONE_PYRAMIDAL)
|
.value("mjCONE_PYRAMIDAL", mjCONE_PYRAMIDAL)
|
||||||
.value("mjCONE_ELLIPTIC", mjCONE_ELLIPTIC);
|
.value("mjCONE_ELLIPTIC", mjCONE_ELLIPTIC);
|
||||||
|
enum_<mjtConflict>("mjtConflict")
|
||||||
|
.value("mjCONFLICT_WARNING", mjCONFLICT_WARNING)
|
||||||
|
.value("mjCONFLICT_MERGE", mjCONFLICT_MERGE)
|
||||||
|
.value("mjCONFLICT_ERROR", mjCONFLICT_ERROR);
|
||||||
enum_<mjtConstraint>("mjtConstraint")
|
enum_<mjtConstraint>("mjtConstraint")
|
||||||
.value("mjCNSTR_EQUALITY", mjCNSTR_EQUALITY)
|
.value("mjCNSTR_EQUALITY", mjCNSTR_EQUALITY)
|
||||||
.value("mjCNSTR_FRICTION_DOF", mjCNSTR_FRICTION_DOF)
|
.value("mjCNSTR_FRICTION_DOF", mjCNSTR_FRICTION_DOF)
|
||||||
@@ -12993,6 +13003,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
|
|||||||
.property("balanceinertia", &MjsCompiler::balanceinertia, &MjsCompiler::set_balanceinertia, reference())
|
.property("balanceinertia", &MjsCompiler::balanceinertia, &MjsCompiler::set_balanceinertia, reference())
|
||||||
.property("boundinertia", &MjsCompiler::boundinertia, &MjsCompiler::set_boundinertia, reference())
|
.property("boundinertia", &MjsCompiler::boundinertia, &MjsCompiler::set_boundinertia, reference())
|
||||||
.property("boundmass", &MjsCompiler::boundmass, &MjsCompiler::set_boundmass, 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("degree", &MjsCompiler::degree, &MjsCompiler::set_degree, reference())
|
||||||
.property("discardvisual", &MjsCompiler::discardvisual, &MjsCompiler::set_discardvisual, reference())
|
.property("discardvisual", &MjsCompiler::discardvisual, &MjsCompiler::set_discardvisual, reference())
|
||||||
.property("eulerseq", &MjsCompiler::eulerseq)
|
.property("eulerseq", &MjsCompiler::eulerseq)
|
||||||
|
|||||||
@@ -1803,6 +1803,36 @@ describe('MuJoCo WASM Bindings', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should compile a spec and emit warnings', () => {
|
||||||
|
const xml = `
|
||||||
|
<mujoco>
|
||||||
|
<worldbody>
|
||||||
|
<flexcomp name="test" type="grid" count="4 4 1" spacing=".2 .2 .2"
|
||||||
|
dim="2" radius=".1"/>
|
||||||
|
</worldbody>
|
||||||
|
</mujoco>`;
|
||||||
|
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', () => {
|
it('should compile a spec from XML string with no assets', () => {
|
||||||
let spec = null;
|
let spec = null;
|
||||||
let model = null;
|
let model = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user