5e99ca6cb8
The writer consumes the same generated rows as the reader. mjXWriter::WriteAttrTable drives the mechanical attributes of an element from its mjXAttr rows: each bound field is compared against the class default at the same offset -- the default object is the same struct type, so the rows carry no comparison values -- and attributes equal to their default are skipped. A null default object means the element has no defaults, and every defined value is written. Ranged-arity rows write with trailing-default trimming, which the reader makes round-trip exact by refilling from the same default. Call sites upcast to the private mjs base (the friend declarations permit it; mjCMesh gains the friendship its siblings had); the comparison object is def->X().spec, a freshly-defaulted struct for the sections, or zero-initialized for size, whose spec defaults (-1, auto) are resolved by compilation. Converted: pair, geom, site, joint, camera, light, material, the equality family, both tendon types (the fixed rows are the spatial rows without appearance attributes -- exactly the tag difference), the actuator, flex with its three sub-elements, mesh, skin, option, the six visual sub-sections, statistic and size. The remnants keep names, files, resolved reference strings (the mjC classes null their private base's string pointers; resolved names live behind accessors), and the writing=custom policies the schema declares: compile directives never saved (fromto, springdamper, fitscale), type-dependent lengths and attributes (sizes, joint pos/axis/limited, shellinertia), and alternatives (mass/density, fovy-versus-intrinsics, the plugin-gated gain/bias family). Compiler keeps its write-if-nonzero policy; keyframe keeps its model-sized vectors. Saved files are canonical: attributes follow schema declaration order with remnants trailing, and sections follow the schema's dependency order (statistic before visual, deformable before the contact and equality sections that name flexes, tendon before the equality constraints that name tendons, custom demoted to the data tail). Uniform behavior fixes fall out: default-equal positionals are dropped, dynprm is trimmed like every other ranged vector, and mesh material -- read into the spec but never written -- now survives save/load round trips. Changelog entries ride along. Verified: full suite, doc_test, and the two-tier A/B harness -- saved XML reorders attributes, and every corpus model reloads to a byte-identical binary. PiperOrigin-RevId: 958255003 Change-Id: I5fe7346014450db88b2f3f8680a8f616f7d31266
2365 lines
81 KiB
Python
2365 lines
81 KiB
Python
# Copyright 2024 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.
|
|
# ==============================================================================
|
|
"""Tests for mjSpec bindings."""
|
|
|
|
import gc
|
|
import inspect
|
|
import math
|
|
import os
|
|
import textwrap
|
|
import typing
|
|
import zipfile # pylint: disable=unused-import
|
|
|
|
from absl import flags
|
|
from absl.testing import absltest
|
|
from etils import epath
|
|
import mujoco
|
|
import numpy as np
|
|
|
|
|
|
def get_linenumber():
|
|
cf = inspect.currentframe()
|
|
return cf.f_back.f_lineno
|
|
|
|
|
|
class SpecsTest(absltest.TestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
# Mark flags as parsed to avoid pytest errors about unparsed flags.
|
|
# This is needed for `create_tempdir()` calls below.
|
|
flags.FLAGS.mark_as_parsed()
|
|
|
|
def test_typing(self):
|
|
spec = mujoco.MjSpec()
|
|
self.assertIsInstance(spec, mujoco.MjSpec)
|
|
self.assertIsInstance(spec.worldbody, mujoco.MjsBody)
|
|
self.assertIsInstance(spec.worldbody, typing.get_args(mujoco.MjStruct))
|
|
|
|
def test_timer(self):
|
|
xml = """
|
|
<mujoco>
|
|
<asset>
|
|
<texture name="grid" type="2d" builtin="checker" width="300" height="300" rgb1=".1 .2 .3" rgb2=".2 .3 .4"/>
|
|
<material name="grid" texture="grid"/>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="plane" size="1 1 1" material="grid"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
spec = mujoco.MjSpec.from_string(xml)
|
|
spec.compile()
|
|
self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TOTAL], 0)
|
|
self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_ASSETS], 0)
|
|
self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TEXTURE], 0)
|
|
|
|
def test_basic(self):
|
|
# Create a spec.
|
|
spec = mujoco.MjSpec()
|
|
|
|
# Check that euler sequence order is set correctly.
|
|
self.assertEqual(spec.compiler.eulerseq[0], 'x')
|
|
spec.compiler.eulerseq = ['z', 'y', 'x']
|
|
self.assertEqual(spec.compiler.eulerseq[0], 'z')
|
|
|
|
# Change single elements of euler sequence.
|
|
spec.compiler.eulerseq[0] = 'y'
|
|
spec.compiler.eulerseq[1] = 'z'
|
|
self.assertEqual(spec.compiler.eulerseq[0], 'y')
|
|
self.assertEqual(spec.compiler.eulerseq[1], 'z')
|
|
|
|
# eulerseq is iterable
|
|
self.assertEqual('yzx', ''.join(spec.compiler.eulerseq))
|
|
|
|
# supports `len`
|
|
self.assertLen(spec.compiler.eulerseq, 3)
|
|
|
|
# field checks for out-of-bound access on read and on write
|
|
with self.assertRaises(IndexError):
|
|
spec.compiler.eulerseq[3] = 'x'
|
|
|
|
with self.assertRaises(IndexError):
|
|
spec.compiler.eulerseq[-1] = 'x'
|
|
|
|
# Add a body, check that it has default orientation.
|
|
body = spec.worldbody.add_body()
|
|
self.assertEqual(body.name, '')
|
|
np.testing.assert_array_equal(body.quat, [1, 0, 0, 0])
|
|
|
|
# Change the name of the body and read it back twice.
|
|
body.name = 'foobar'
|
|
self.assertEqual(body.name, 'foobar')
|
|
body.name = 'baz'
|
|
self.assertEqual(body.name, 'baz')
|
|
|
|
# Change the position of the body and read it back.
|
|
body.pos = [4, 2, 3]
|
|
np.testing.assert_array_equal(body.pos, [4, 2, 3])
|
|
self.assertEqual(body.pos.shape, (3,))
|
|
|
|
# Change single element of position.
|
|
body.pos[0] = 1
|
|
np.testing.assert_array_equal(body.pos, [1, 2, 3])
|
|
|
|
# Change the orientation of the body and read it back.
|
|
body.quat = [0, 1, 0, 0]
|
|
np.testing.assert_array_equal(body.quat, [0, 1, 0, 0])
|
|
self.assertEqual(body.quat.shape, (4,))
|
|
|
|
# Add a site to the body with user data and read it back.
|
|
site = body.add_site()
|
|
site.name = 'sitename'
|
|
site.type = mujoco.mjtGeom.mjGEOM_BOX
|
|
site.userdata = [7, 2, 3, 4, 5, 6]
|
|
self.assertEqual(site.name, 'sitename')
|
|
self.assertEqual(site.type, mujoco.mjtGeom.mjGEOM_BOX)
|
|
np.testing.assert_array_equal(site.userdata, [7, 2, 3, 4, 5, 6])
|
|
|
|
# Modify a single element of userdata.
|
|
site.userdata[0] = 1
|
|
np.testing.assert_array_equal(site.userdata, [1, 2, 3, 4, 5, 6])
|
|
|
|
# Compile the spec and check for expected values in the model.
|
|
model = spec.compile()
|
|
data = mujoco.MjData(model)
|
|
mujoco.mj_forward(model, data)
|
|
self.assertEqual(model.nbody, 2) # 2 bodies, including the world body
|
|
np.testing.assert_array_equal(model.bind(body).pos, [1, 2, 3])
|
|
np.testing.assert_array_equal(model.bind(body).quat, [0, 1, 0, 0])
|
|
np.testing.assert_array_equal(data.bind(body).xpos, [1, 2, 3])
|
|
self.assertEqual(model.nsite, 1)
|
|
self.assertEqual(model.nuser_site, 6)
|
|
np.testing.assert_array_equal(model.site_user[0], [1, 2, 3, 4, 5, 6])
|
|
|
|
xml = textwrap.dedent("""\
|
|
<mujoco model="MuJoCo Model">
|
|
<compiler angle="radian"/>
|
|
|
|
<size nuser_site="6"/>
|
|
|
|
<worldbody>
|
|
<body name="baz" pos="1 2 3" quat="0 1 0 0">
|
|
<site name="sitename" type="box" user="1 2 3 4 5 6"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
""")
|
|
self.assertEqual(spec.to_xml(), xml)
|
|
|
|
def test_resolve_orientation(self):
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body(euler=[0, 0, 90])
|
|
quat = mujoco.MjSpec.resolve_orientation(
|
|
degree=spec.compiler.degree,
|
|
sequence=spec.compiler.eulerseq,
|
|
orientation=body.alt,
|
|
)
|
|
np.testing.assert_array_almost_equal(
|
|
quat, [math.sqrt(2) / 2, 0, 0, math.sqrt(2) / 2]
|
|
)
|
|
|
|
def test_kwarg(self):
|
|
# Create a spec.
|
|
spec = mujoco.MjSpec()
|
|
|
|
# Add material.
|
|
material = spec.add_material(texrepeat=[1, 2], emission=-1)
|
|
np.testing.assert_array_equal(material.texrepeat, [1, 2])
|
|
self.assertEqual(material.emission, -1)
|
|
|
|
# Add mesh.
|
|
mesh = spec.add_mesh(refpos=[1, 2, 3])
|
|
np.testing.assert_array_equal(mesh.refpos, [1, 2, 3])
|
|
|
|
# Add pair.
|
|
pair = spec.add_pair(gap=0.1)
|
|
self.assertEqual(pair.gap, 0.1)
|
|
|
|
# Add equality.
|
|
equality = spec.add_equality(objtype=mujoco.mjtObj.mjOBJ_SITE)
|
|
self.assertEqual(equality.objtype, mujoco.mjtObj.mjOBJ_SITE)
|
|
|
|
# Add tendon.
|
|
tendon = spec.add_tendon(stiffness=2, springlength=[0.1, 0.2])
|
|
np.testing.assert_array_equal(tendon.stiffness, [2, 0, 0])
|
|
np.testing.assert_array_equal(tendon.springlength, [0.1, 0.2])
|
|
|
|
# Add actuator.
|
|
actuator = spec.add_actuator(actdim=10, ctrlrange=[-1, 10])
|
|
self.assertEqual(actuator.actdim, 10)
|
|
np.testing.assert_array_equal(actuator.ctrlrange, [-1, 10])
|
|
|
|
# Add skin.
|
|
skin = spec.add_skin(inflate=2.0, vertid=[[1, 1], [2, 2]])
|
|
self.assertEqual(skin.inflate, 2.0)
|
|
np.testing.assert_array_equal(skin.vertid, [[1, 1], [2, 2]])
|
|
|
|
# Add texture.
|
|
texture = spec.add_texture(builtin=0, nchannel=3)
|
|
self.assertEqual(texture.builtin, 0)
|
|
self.assertEqual(texture.nchannel, 3)
|
|
|
|
# Add text.
|
|
text = spec.add_text(data='data', info='info')
|
|
self.assertEqual(text.data, 'data')
|
|
self.assertEqual(text.info, 'info')
|
|
|
|
# Add tuple.
|
|
tuple_ = spec.add_tuple(objprm=[2.0, 3.0, 5.0], objname=['obj'])
|
|
np.testing.assert_array_equal(tuple_.objprm, [2.0, 3.0, 5.0])
|
|
self.assertEqual(tuple_.objname[0], 'obj')
|
|
|
|
# Add flex.
|
|
flex = spec.add_flex(friction=[1, 2, 3], texcoord=[1.0, 2.0, 3.0])
|
|
np.testing.assert_array_equal(flex.friction, [1, 2, 3])
|
|
np.testing.assert_array_equal(flex.texcoord, [1.0, 2.0, 3.0])
|
|
|
|
# Add hfield.
|
|
hfield = spec.add_hfield(nrow=2, content_type='type')
|
|
self.assertEqual(hfield.nrow, 2)
|
|
self.assertEqual(hfield.content_type, 'type')
|
|
|
|
# Add key.
|
|
key = spec.add_key(time=1.2, qpos=[1.0, 2.0])
|
|
self.assertEqual(key.time, 1.2)
|
|
np.testing.assert_array_equal(key.qpos, [1.0, 2.0])
|
|
|
|
# Add numeric.
|
|
numeric = spec.add_numeric(data=[1.0, 1.1, 1.2], size=2)
|
|
np.testing.assert_array_equal(numeric.data, [1.0, 1.1, 1.2])
|
|
self.assertEqual(numeric.size, 2)
|
|
|
|
# Add exclude.
|
|
exclude = spec.add_exclude(bodyname2='body2')
|
|
self.assertEqual(exclude.bodyname2, 'body2')
|
|
|
|
# Add sensor.
|
|
sensor = spec.add_sensor(
|
|
needstage=mujoco.mjtStage.mjSTAGE_ACC, objtype=mujoco.mjtObj.mjOBJ_SITE
|
|
)
|
|
self.assertEqual(sensor.needstage, mujoco.mjtStage.mjSTAGE_ACC)
|
|
self.assertEqual(sensor.objtype, mujoco.mjtObj.mjOBJ_SITE)
|
|
|
|
# Add plugin.
|
|
plugin = spec.add_plugin(
|
|
name='instance_name',
|
|
plugin_name='mujoco.plugin',
|
|
active=True,
|
|
info='info',
|
|
)
|
|
self.assertEqual(plugin.name, 'instance_name')
|
|
self.assertEqual(plugin.plugin_name, 'mujoco.plugin')
|
|
self.assertEqual(plugin.active, True)
|
|
self.assertEqual(plugin.info, 'info')
|
|
|
|
# Add a body.
|
|
body = spec.worldbody.add_body(
|
|
name='body', pos=[1, 2, 3], quat=[0, 0, 0, 1]
|
|
)
|
|
self.assertEqual(body.name, 'body')
|
|
np.testing.assert_array_equal(body.pos, [1, 2, 3])
|
|
np.testing.assert_array_equal(body.quat, [0, 0, 0, 1])
|
|
|
|
# Add a body with a plugin.
|
|
body_with_plugin = spec.worldbody.add_body(plugin=plugin)
|
|
self.assertEqual(body_with_plugin.plugin.name, 'instance_name')
|
|
self.assertEqual(body_with_plugin.plugin.plugin_name, 'mujoco.plugin')
|
|
self.assertEqual(body_with_plugin.plugin.active, True)
|
|
self.assertEqual(body_with_plugin.plugin.info, 'info')
|
|
|
|
# Add a geom.
|
|
geom = body.add_geom(
|
|
name='geom',
|
|
pos=[3, 2, 1],
|
|
fromto=[1, 2, 3, 4, 5, 6],
|
|
contype=3,
|
|
)
|
|
|
|
self.assertEqual(geom.name, 'geom')
|
|
np.testing.assert_array_equal(geom.pos, [3, 2, 1])
|
|
np.testing.assert_array_equal(geom.fromto, [1, 2, 3, 4, 5, 6])
|
|
self.assertEqual(geom.contype, 3)
|
|
|
|
# Add a site to the body with user data and read it back.
|
|
site = body.add_site(
|
|
name='sitename',
|
|
pos=[0, 1, 2],
|
|
quat=[1, 0, 0, 0],
|
|
fromto=[0, 1, 2, 3, 4, 5],
|
|
size=[3, 2, 1],
|
|
type=mujoco.mjtGeom.mjGEOM_BOX,
|
|
material='material',
|
|
group=7,
|
|
rgba=[1, 1, 1, 0.5],
|
|
userdata=[1, 2, 3, 4, 5, 6],
|
|
info='info',
|
|
)
|
|
self.assertEqual(site.name, 'sitename')
|
|
np.testing.assert_array_equal(site.pos, [0, 1, 2])
|
|
np.testing.assert_array_equal(site.quat, [1, 0, 0, 0])
|
|
np.testing.assert_array_equal(site.fromto, [0, 1, 2, 3, 4, 5])
|
|
np.testing.assert_array_equal(site.size, [3, 2, 1])
|
|
self.assertEqual(site.type, mujoco.mjtGeom.mjGEOM_BOX)
|
|
self.assertEqual(site.material, 'material')
|
|
self.assertEqual(site.group, 7)
|
|
np.testing.assert_array_equal(site.rgba, [1, 1, 1, 0.5])
|
|
np.testing.assert_array_equal(site.userdata, [1, 2, 3, 4, 5, 6])
|
|
self.assertEqual(site.info, 'info')
|
|
|
|
# Add camera.
|
|
cam = body.add_camera(
|
|
proj=mujoco.mjtProjection.mjPROJ_ORTHOGRAPHIC, resolution=[10, 20]
|
|
)
|
|
self.assertEqual(cam.proj, 1)
|
|
np.testing.assert_array_equal(cam.resolution, [10, 20])
|
|
|
|
# Add frame.
|
|
framea0 = body.add_frame(name='framea', pos=[1, 2, 3], quat=[0, 0, 0, 1])
|
|
np.testing.assert_array_equal(framea0.pos, [1, 2, 3])
|
|
np.testing.assert_array_equal(framea0.quat, [0, 0, 0, 1])
|
|
|
|
frameb0 = body.add_frame(
|
|
framea0, name='frameb', pos=[4, 5, 6], quat=[0, 1, 0, 0]
|
|
)
|
|
|
|
framea1 = body.first_frame()
|
|
frameb1 = body.next_frame(framea1)
|
|
|
|
self.assertEqual(framea1.name, framea0.name)
|
|
self.assertEqual(frameb1.name, frameb0.name)
|
|
np.testing.assert_array_equal(framea1.pos, framea0.pos)
|
|
np.testing.assert_array_equal(framea1.quat, framea0.quat)
|
|
np.testing.assert_array_equal(frameb1.pos, frameb0.pos)
|
|
np.testing.assert_array_equal(frameb1.quat, frameb0.quat)
|
|
|
|
# Add frame in frame.
|
|
framec0 = frameb0.add_frame(name='framec', pos=[7, 8, 9], quat=[0, 0, 0, 1])
|
|
self.assertEqual(framec0.name, 'framec')
|
|
self.assertEqual(framec0.parent, frameb0.parent)
|
|
self.assertEqual(framec0.frame, frameb0)
|
|
np.testing.assert_array_equal(framec0.pos, [7, 8, 9])
|
|
np.testing.assert_array_equal(framec0.quat, [0, 0, 0, 1])
|
|
|
|
# Add joint.
|
|
joint = body.add_joint(type=mujoco.mjtJoint.mjJNT_HINGE, axis=[0, 1, 0])
|
|
self.assertEqual(joint.type, mujoco.mjtJoint.mjJNT_HINGE)
|
|
np.testing.assert_array_equal(joint.axis, [0, 1, 0])
|
|
|
|
# Add freejoint.
|
|
freejoint = body.add_freejoint()
|
|
self.assertEqual(freejoint.type, mujoco.mjtJoint.mjJNT_FREE)
|
|
freejoint_align = body.add_freejoint(align=True)
|
|
self.assertEqual(freejoint_align.align, True)
|
|
|
|
with self.assertRaises(TypeError) as cm:
|
|
body.add_freejoint(axis=[1, 2, 3])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'Invalid axis keyword argument. Valid options are: align, group, name.',
|
|
)
|
|
|
|
# Add light.
|
|
light = body.add_light(attenuation=[1, 2, 3])
|
|
np.testing.assert_array_equal(light.attenuation, [1, 2, 3])
|
|
|
|
# Add light in a frame.
|
|
light_in_frame = framea0.add_light(cutoff=10)
|
|
self.assertEqual(light_in_frame.cutoff, 10)
|
|
self.assertEqual(light_in_frame.parent, framea0.parent)
|
|
self.assertEqual(light_in_frame.frame, framea0)
|
|
|
|
# Invalid input for valid keyword argument.
|
|
with self.assertRaises(TypeError):
|
|
body.add_geom(pos='pos')
|
|
|
|
with self.assertRaises(ValueError) as cm:
|
|
body.add_geom(pos=[0, 1])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'pos should be a list/array of size 3.',
|
|
)
|
|
|
|
with self.assertRaises(TypeError):
|
|
body.add_geom(type='type')
|
|
|
|
with self.assertRaises(TypeError):
|
|
body.add_geom(userdata='')
|
|
|
|
# Invalid keyword argument.
|
|
with self.assertRaises(TypeError):
|
|
body.add_geom(vel='vel')
|
|
|
|
# Orientation keyword arguments.
|
|
geom_axisangle = body.add_geom(axisangle=[1, 2, 3, 4])
|
|
geom_xyaxes = body.add_geom(xyaxes=[1, 2, 3, 4, 5, 6])
|
|
geom_zaxis = body.add_geom(zaxis=[1, 2, 3])
|
|
geom_euler = body.add_geom(euler=[1, 2, 3])
|
|
|
|
self.assertEqual(
|
|
geom_axisangle.alt.type, mujoco.mjtOrientation.mjORIENTATION_AXISANGLE
|
|
)
|
|
self.assertEqual(
|
|
geom_xyaxes.alt.type, mujoco.mjtOrientation.mjORIENTATION_XYAXES
|
|
)
|
|
self.assertEqual(
|
|
geom_zaxis.alt.type, mujoco.mjtOrientation.mjORIENTATION_ZAXIS
|
|
)
|
|
self.assertEqual(
|
|
geom_euler.alt.type, mujoco.mjtOrientation.mjORIENTATION_EULER
|
|
)
|
|
np.testing.assert_array_equal(geom_axisangle.alt.axisangle, [1, 2, 3, 4])
|
|
np.testing.assert_array_equal(geom_xyaxes.alt.xyaxes, [1, 2, 3, 4, 5, 6])
|
|
np.testing.assert_array_equal(geom_zaxis.alt.zaxis, [1, 2, 3])
|
|
np.testing.assert_array_equal(geom_euler.alt.euler, [1, 2, 3])
|
|
|
|
body_iaxisangle = spec.worldbody.add_body(iaxisangle=[1, 2, 3, 4])
|
|
body_ixyaxes = spec.worldbody.add_body(ixyaxes=[1, 2, 3, 4, 5, 6])
|
|
body_izaxis = spec.worldbody.add_body(izaxis=[1, 2, 3])
|
|
body_ieuler = spec.worldbody.add_body(ieuler=[1, 2, 3])
|
|
body_euler_ieuler = spec.worldbody.add_body(
|
|
euler=[1, 2, 3], ieuler=[4, 5, 6]
|
|
)
|
|
np.testing.assert_array_equal(body_iaxisangle.ialt.axisangle, [1, 2, 3, 4])
|
|
np.testing.assert_array_equal(body_ixyaxes.ialt.xyaxes, [1, 2, 3, 4, 5, 6])
|
|
np.testing.assert_array_equal(body_izaxis.ialt.zaxis, [1, 2, 3])
|
|
np.testing.assert_array_equal(body_ieuler.ialt.euler, [1, 2, 3])
|
|
np.testing.assert_array_equal(body_euler_ieuler.alt.euler, [1, 2, 3])
|
|
np.testing.assert_array_equal(body_euler_ieuler.ialt.euler, [4, 5, 6])
|
|
|
|
# Test invalid orientation keyword arguments.
|
|
with self.assertRaises(ValueError) as cm:
|
|
body.add_geom(axisangle=[1, 2, 3])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'axisangle should be a list/array of size 4.',
|
|
)
|
|
with self.assertRaises(ValueError) as cm:
|
|
body.add_geom(xyaxes=[1, 2, 3, 4, 5])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'xyaxes should be a list/array of size 6.',
|
|
)
|
|
with self.assertRaises(ValueError) as cm:
|
|
body.add_geom(zaxis=[1, 2, 3, 4])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'zaxis should be a list/array of size 3.',
|
|
)
|
|
with self.assertRaises(ValueError) as cm:
|
|
body.add_geom(euler=[1])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'euler should be a list/array of size 3.',
|
|
)
|
|
with self.assertRaises(ValueError) as cm:
|
|
body.add_geom(axisangle=[1, 2, 3, 4], euler=[1, 2, 3])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'Only one of: axisangle, xyaxes, zaxis, or euler can be set.',
|
|
)
|
|
with self.assertRaises(ValueError) as cm:
|
|
spec.worldbody.add_body(iaxisangle=[1, 2, 3, 4], ieuler=[1, 2, 3])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'Only one of: iaxisangle, ixyaxes, izaxis, or ieuler can be set.',
|
|
)
|
|
|
|
def test_size_kwarg_variable_length(self):
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body()
|
|
|
|
geom_size1 = body.add_geom(size=[0.5])
|
|
np.testing.assert_array_equal(geom_size1.size, [0.5, 0, 0])
|
|
|
|
geom_size2 = body.add_geom(size=[0.5, 0.3])
|
|
np.testing.assert_array_equal(geom_size2.size, [0.5, 0.3, 0])
|
|
|
|
geom_size3 = body.add_geom(size=[0.5, 0.3, 0.1])
|
|
np.testing.assert_array_equal(geom_size3.size, [0.5, 0.3, 0.1])
|
|
|
|
site_size1 = body.add_site(size=[0.2])
|
|
np.testing.assert_array_equal(site_size1.size, [0.2, 0, 0])
|
|
|
|
site_size2 = body.add_site(size=[0.2, 0.1])
|
|
np.testing.assert_array_equal(site_size2.size, [0.2, 0.1, 0])
|
|
|
|
site_size3 = body.add_site(size=[0.2, 0.1, 0.05])
|
|
np.testing.assert_array_equal(site_size3.size, [0.2, 0.1, 0.05])
|
|
|
|
with self.assertRaises(ValueError) as cm:
|
|
body.add_geom(size=[])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'size should be a list/array of size 1 to 3.',
|
|
)
|
|
|
|
with self.assertRaises(ValueError) as cm:
|
|
body.add_geom(size=[1, 2, 3, 4])
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'size should be a list/array of size 1 to 3.',
|
|
)
|
|
|
|
def test_load_xml(self):
|
|
file_path = epath.resource_path("mujoco") / "testdata" / "model.xml"
|
|
filename = file_path.as_posix()
|
|
state_type = mujoco.mjtState.mjSTATE_INTEGRATION
|
|
|
|
# Load from file.
|
|
spec1 = mujoco.MjSpec.from_file(filename)
|
|
model1 = spec1.compile()
|
|
data1 = mujoco.MjData(model1)
|
|
mujoco.mj_step(model1, data1)
|
|
size1 = mujoco.mj_stateSize(model1, state_type)
|
|
state1 = np.empty(size1, np.float64)
|
|
mujoco.mj_getState(model1, data1, state1, state_type)
|
|
|
|
# Load from string.
|
|
with open(filename, 'r') as file:
|
|
spec2 = mujoco.MjSpec.from_string(file.read().rstrip())
|
|
model2 = spec2.compile()
|
|
data2 = mujoco.MjData(model2)
|
|
mujoco.mj_step(model2, data2)
|
|
size2 = mujoco.mj_stateSize(model2, state_type)
|
|
state2 = np.empty(size2, np.float64)
|
|
mujoco.mj_getState(model2, data2, state2, state_type)
|
|
|
|
# Check that the state is the same.
|
|
np.testing.assert_array_equal(state1, state2)
|
|
|
|
def test_parses_urdf(self):
|
|
file_path = epath.resource_path("mujoco") / "testdata" / "model.urdf"
|
|
filename = file_path.as_posix()
|
|
|
|
# Load from file.
|
|
spec1 = mujoco.MjSpec.from_file(filename)
|
|
self.assertIsNotNone(spec1)
|
|
|
|
def test_make_mesh(self):
|
|
spec = mujoco.MjSpec()
|
|
|
|
mesh = spec.add_mesh(name='wedge')
|
|
mesh.make_wedge(resolution=[25, 25], fov=[90, 45], gamma=0)
|
|
|
|
mesh = spec.add_mesh(name='prism')
|
|
mesh.make_cone(nedge=5, radius=1)
|
|
|
|
mesh = spec.add_mesh(name='cone')
|
|
mesh.make_cone(nedge=6, radius=0)
|
|
|
|
mesh = spec.add_mesh(name='hemisphere')
|
|
mesh.make_hemisphere(resolution=4)
|
|
|
|
mesh = spec.add_mesh(name='sphere')
|
|
mesh.make_sphere(subdivision=2)
|
|
|
|
mesh = spec.add_mesh(name='supertorus')
|
|
mesh.make_supertorus(resolution=10, radius=0.5, s=1, t=1)
|
|
|
|
mesh = spec.add_mesh(name='supersphere')
|
|
mesh.make_supersphere(resolution=20, e=2, n=1)
|
|
|
|
model = spec.compile()
|
|
self.assertEqual(model.nmesh, 7)
|
|
self.assertEqual(model.mesh_vertnum[0], 25 * 25)
|
|
self.assertEqual(model.mesh_vertnum[1], 10)
|
|
self.assertEqual(model.mesh_vertnum[2], 7)
|
|
self.assertEqual(model.mesh_vertnum[3], 2 * (4 + 1) * (4 + 2) + 2)
|
|
self.assertEqual(model.mesh_vertnum[4], 2 + 10 * 4**2)
|
|
self.assertEqual(model.mesh_vertnum[5], 100)
|
|
self.assertEqual(model.mesh_vertnum[6], 20 * (20 -1) + 2)
|
|
|
|
def test_compile_errors_with_line_info(self):
|
|
spec = mujoco.MjSpec()
|
|
|
|
added_on_line = get_linenumber() + 1
|
|
geom = spec.worldbody.add_geom()
|
|
geom.name = 'MyGeom'
|
|
geom.info = f'geom added on line {added_on_line}'
|
|
|
|
# Try to compile, get error.
|
|
expected_error = (
|
|
'Error: size 0 must be positive in geom\n'
|
|
+ f"Element name 'MyGeom', id 0, geom added on line {added_on_line}"
|
|
)
|
|
with self.assertRaisesRegex(ValueError, expected_error):
|
|
spec.compile()
|
|
|
|
def test_compile_warnings(self):
|
|
xml = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<flexcomp name="my_flex" type="grid" count="3 3 1" spacing=".05 .05 .05" radius=".01" dim="2"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
spec = mujoco.MjSpec.from_string(xml)
|
|
with self.assertWarnsRegex(UserWarning, 'is not rigid'):
|
|
spec.compile()
|
|
|
|
def test_recompile(self):
|
|
# Create a spec.
|
|
spec = mujoco.MjSpec()
|
|
|
|
# Add movable body1.
|
|
body1 = spec.worldbody.add_body()
|
|
geom = body1.add_geom()
|
|
geom.size[0] = 1
|
|
geom.pos = [1, 1, 0]
|
|
joint = body1.add_joint()
|
|
joint.type = mujoco.mjtJoint.mjJNT_BALL
|
|
|
|
# Compile model, make data.
|
|
model = spec.compile()
|
|
data = mujoco.MjData(model)
|
|
|
|
# Simulate for 1 second.
|
|
while data.time < 1:
|
|
mujoco.mj_step(model, data)
|
|
|
|
# Add movable body2.
|
|
body2 = spec.worldbody.add_body()
|
|
body2.pos[1] = 3
|
|
geom = body2.add_geom()
|
|
geom.size[0] = 1
|
|
geom.pos = [0, 1, 0]
|
|
joint = body2.add_joint()
|
|
joint.type = mujoco.mjtJoint.mjJNT_BALL
|
|
|
|
# Recompile model and data while maintaining the state.
|
|
model_new, data_new = spec.recompile(model, data)
|
|
|
|
# Check that the state is preserved.
|
|
np.testing.assert_array_equal(model_new.body_pos[1], model.body_pos[1])
|
|
np.testing.assert_array_equal(data_new.qpos[:4], data.qpos)
|
|
np.testing.assert_array_equal(data_new.qvel[:3], data.qvel)
|
|
|
|
def test_uncompiled_spec_can_be_written(self):
|
|
spec = mujoco.MjSpec()
|
|
spec.to_xml()
|
|
|
|
def test_modelname_default_class(self):
|
|
xml = textwrap.dedent("""\
|
|
<mujoco model="test">
|
|
<compiler angle="radian"/>
|
|
|
|
<default>
|
|
<geom size="2 0 0"/>
|
|
<default class="def1">
|
|
<geom size="3 0 0"/>
|
|
</default>
|
|
</default>
|
|
|
|
<worldbody>
|
|
<geom class="def1"/>
|
|
<geom/>
|
|
</worldbody>
|
|
</mujoco>
|
|
""")
|
|
|
|
spec = mujoco.MjSpec()
|
|
spec.modelname = 'test'
|
|
|
|
main = spec.default
|
|
main.geom.size[0] = 2
|
|
|
|
def1 = spec.add_default('def1', main)
|
|
def1.geom.size[0] = 3
|
|
|
|
spec.worldbody.add_geom(def1)
|
|
spec.worldbody.add_geom(main)
|
|
|
|
spec.compile()
|
|
self.assertEqual(spec.to_xml(), xml)
|
|
spec = mujoco.MjSpec()
|
|
spec.modelname = 'test'
|
|
|
|
main = spec.default
|
|
main.geom.size[0] = 2
|
|
def1 = spec.add_default('def1', main)
|
|
def1.geom.size[0] = 3
|
|
|
|
geom1 = spec.worldbody.add_geom(def1)
|
|
geom2 = spec.worldbody.add_geom()
|
|
self.assertEqual(geom1.classname.name, 'def1')
|
|
self.assertEqual(geom2.classname.name, 'main')
|
|
|
|
spec.compile()
|
|
self.assertEqual(spec.to_xml(), xml)
|
|
|
|
spec = mujoco.MjSpec()
|
|
spec.modelname = 'test'
|
|
|
|
main = spec.default
|
|
main.geom.size[0] = 2
|
|
def1 = spec.add_default('def1', main)
|
|
def1.geom.size[0] = 3
|
|
|
|
geom1 = spec.worldbody.add_geom(size=[3, 0, 0])
|
|
geom2 = spec.worldbody.add_geom(size=[2, 0, 0])
|
|
geom1.classname = def1
|
|
geom2.classname = main # actually redundant, since main is always applied
|
|
|
|
spec.compile()
|
|
self.assertEqual(spec.to_xml(), xml)
|
|
|
|
# test delete default
|
|
def1 = spec.find_default('def1')
|
|
spec.delete(def1)
|
|
def1 = spec.find_default('def1')
|
|
self.assertIsNone(def1)
|
|
|
|
def test_element_list(self):
|
|
spec = mujoco.MjSpec()
|
|
sensor1 = spec.add_sensor()
|
|
sensor2 = spec.add_sensor()
|
|
sensor3 = spec.add_sensor()
|
|
actuator1 = spec.add_actuator()
|
|
actuator2 = spec.add_actuator()
|
|
actuator3 = spec.add_actuator()
|
|
sensor1.name = 'sensor1'
|
|
sensor2.name = 'sensor2'
|
|
sensor3.name = 'sensor3'
|
|
actuator1.name = 'actuator1'
|
|
actuator2.name = 'actuator2'
|
|
actuator3.name = 'actuator3'
|
|
self.assertLen(spec.sensors, 3)
|
|
self.assertLen(spec.actuators, 3)
|
|
self.assertEqual(spec.sensors[0].name, 'sensor1')
|
|
self.assertEqual(spec.sensors[1].name, 'sensor2')
|
|
self.assertEqual(spec.sensors[2].name, 'sensor3')
|
|
self.assertEqual(spec.actuators[0].name, 'actuator1')
|
|
self.assertEqual(spec.actuators[1].name, 'actuator2')
|
|
self.assertEqual(spec.actuators[2].name, 'actuator3')
|
|
self.assertEqual(spec.sensor('sensor1'), sensor1)
|
|
self.assertEqual(spec.sensor('sensor2'), sensor2)
|
|
self.assertEqual(spec.sensor('sensor3'), sensor3)
|
|
self.assertEqual(spec.actuator('actuator1'), actuator1)
|
|
self.assertEqual(spec.actuator('actuator2'), actuator2)
|
|
self.assertEqual(spec.actuator('actuator3'), actuator3)
|
|
|
|
def test_body_list(self):
|
|
main_xml = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="body1">
|
|
<site name="site1"/>
|
|
<body name="body2">
|
|
<site name="site4"/>
|
|
</body>
|
|
<geom name="geom1" size="1"/>
|
|
<geom name="geom2" size="1"/>
|
|
<site name="site2"/>
|
|
<site name="site3"/>
|
|
<geom name="geom3" size="1"/>
|
|
</body>
|
|
<body name="body3">
|
|
<site name="site5"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
spec = mujoco.MjSpec.from_string(main_xml)
|
|
bodytype = mujoco.mjtObj.mjOBJ_BODY
|
|
self.assertLen(spec.bodies, 4)
|
|
self.assertLen(spec.sites, 5)
|
|
self.assertLen(spec.worldbody.find_all('body'), 3)
|
|
self.assertLen(spec.worldbody.find_all('site'), 5)
|
|
self.assertLen(spec.worldbody.find_all('geom'), 3)
|
|
self.assertEqual(spec.bodies[1].name, 'body1')
|
|
self.assertEqual(spec.bodies[2].name, 'body2')
|
|
self.assertEqual(spec.bodies[3].name, 'body3')
|
|
self.assertEqual(spec.bodies[1].parent, spec.bodies[0])
|
|
self.assertEqual(spec.bodies[2].parent, spec.bodies[1])
|
|
self.assertEqual(spec.bodies[3].parent, spec.bodies[0])
|
|
self.assertLen(spec.worldbody.find_all(bodytype), 3)
|
|
self.assertLen(spec.bodies[1].find_all(bodytype), 1)
|
|
self.assertEmpty(spec.bodies[3].find_all(bodytype))
|
|
self.assertEqual(spec.bodies[1].find_all('body')[0].name, 'body2')
|
|
self.assertEmpty(spec.bodies[3].find_all('body'))
|
|
self.assertEmpty(spec.bodies[2].find_all('body'))
|
|
for i, body in enumerate(spec.worldbody.find_all('body')):
|
|
self.assertEqual(body.name, 'body' + str(i + 1))
|
|
for i, site in enumerate(spec.worldbody.find_all('site')):
|
|
self.assertEqual(site.name, 'site' + str(i + 1))
|
|
self.assertLen(spec.bodies[1].sites, 3)
|
|
self.assertLen(spec.bodies[2].sites, 1)
|
|
self.assertLen(spec.bodies[3].sites, 1)
|
|
self.assertEqual(spec.bodies[1].sites[0].name, 'site1')
|
|
self.assertEqual(spec.bodies[1].sites[1].name, 'site2')
|
|
self.assertEqual(spec.bodies[1].sites[2].name, 'site3')
|
|
self.assertEqual(spec.bodies[2].sites[0].name, 'site4')
|
|
self.assertEqual(spec.bodies[3].sites[0].name, 'site5')
|
|
for body in spec.bodies:
|
|
for site in body.sites:
|
|
self.assertEqual(site.parent, body)
|
|
with self.assertRaises(ValueError) as cm:
|
|
spec.worldbody.find_all('actuator')
|
|
self.assertEqual(
|
|
str(cm.exception),
|
|
'body.find_all supports the types: body, frame, geom, site,'
|
|
' joint, light, camera.',
|
|
)
|
|
body3 = spec.worldbody.find_all('body')[2]
|
|
body3.name = 'body3_new'
|
|
self.assertEqual(spec.bodies[3].name, 'body3_new')
|
|
|
|
def test_geom_list(self):
|
|
main_xml = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="body1"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
spec = mujoco.MjSpec.from_string(main_xml)
|
|
geom1 = spec.worldbody.add_geom(name='geom1')
|
|
geom2 = spec.worldbody.add_geom(name='geom2')
|
|
geom3 = spec.body('body1').add_geom(name='geom3')
|
|
|
|
self.assertEqual(spec.geoms, [geom1, geom2, geom3])
|
|
self.assertEqual(spec.geom('geom1'), geom1)
|
|
self.assertEqual(spec.geom('geom2'), geom2)
|
|
self.assertEqual(spec.geom('geom3'), geom3)
|
|
|
|
def test_iterators(self):
|
|
spec = mujoco.MjSpec()
|
|
geom1 = spec.worldbody.add_geom()
|
|
geom2 = spec.worldbody.add_geom()
|
|
geom3 = spec.worldbody.add_geom()
|
|
geom1.name = 'geom1'
|
|
geom2.name = 'geom2'
|
|
geom3.name = 'geom3'
|
|
geom = spec.worldbody.first_geom()
|
|
i = 1
|
|
while geom:
|
|
self.assertEqual(geom.name, 'geom' + str(i))
|
|
geom = spec.worldbody.next_geom(geom)
|
|
i += 1
|
|
|
|
def test_assets(self):
|
|
cube = """
|
|
v -1 -1 1
|
|
v 1 -1 1
|
|
v -1 1 1
|
|
v 1 1 1
|
|
v -1 1 -1
|
|
v 1 1 -1
|
|
v -1 -1 -1
|
|
v 1 -1 -1"""
|
|
spec = mujoco.MjSpec()
|
|
spec.modelname = 'test'
|
|
mesh = spec.add_mesh()
|
|
mesh.name = 'cube'
|
|
mesh.file = 'cube.obj'
|
|
geom = spec.worldbody.add_geom()
|
|
geom.type = mujoco.mjtGeom.mjGEOM_MESH
|
|
geom.meshname = 'cube'
|
|
spec.assets = {'cube.obj': cube}
|
|
model = spec.compile()
|
|
self.assertEqual(model.nmeshvert, 8)
|
|
self.assertEqual(spec.assets['cube.obj'], cube)
|
|
self.assertIs(
|
|
spec.assets['cube.obj'], cube,
|
|
'Asset dict should contain a reference, not a copy'
|
|
)
|
|
|
|
xml = """
|
|
<mujoco model="test">
|
|
<compiler angle="radian"/>
|
|
|
|
<asset>
|
|
<mesh name="cube" file="cube.obj"/>
|
|
</asset>
|
|
|
|
<worldbody>
|
|
<geom type="mesh" mesh="cube"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
assets = {'cube.obj': cube}
|
|
spec = mujoco.MjSpec.from_string(xml, assets=assets)
|
|
model = spec.compile()
|
|
self.assertEqual(model.nmeshvert, 8)
|
|
self.assertEqual(spec.assets['cube.obj'], cube)
|
|
self.assertIs(
|
|
spec.assets['cube.obj'], cube,
|
|
'Asset dict should contain a reference, not a copy'
|
|
)
|
|
del assets
|
|
gc.collect()
|
|
self.assertEqual(spec.assets['cube.obj'], cube)
|
|
|
|
def test_include(self):
|
|
included_xml = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom type="box" size="1 1 1"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
spec = mujoco.MjSpec.from_string(
|
|
textwrap.dedent("""
|
|
<mujoco model="MuJoCo Model">
|
|
<include file="included.xml"/>
|
|
</mujoco>
|
|
"""),
|
|
include={'included.xml': included_xml.encode('utf-8')},
|
|
)
|
|
self.assertEqual(
|
|
spec.worldbody.first_body().first_geom().type, mujoco.mjtGeom.mjGEOM_BOX
|
|
)
|
|
|
|
def test_delete(self):
|
|
file_path = epath.resource_path("mujoco") / "testdata" / "model.xml"
|
|
spec = mujoco.MjSpec.from_file(file_path.as_posix())
|
|
|
|
model = spec.compile()
|
|
self.assertIsNotNone(model)
|
|
self.assertEqual(model.nsite, 11)
|
|
self.assertEqual(model.nsensor, 11)
|
|
|
|
head = spec.body('head')
|
|
self.assertIsNotNone(head)
|
|
site = head.first_site()
|
|
self.assertIsNotNone(site)
|
|
self.assertEqual(site, spec.site('head'))
|
|
|
|
spec.delete(site)
|
|
spec.delete(spec.sensors[-1])
|
|
spec.delete(spec.sensors[-1])
|
|
|
|
model = spec.compile()
|
|
self.assertIsNotNone(model)
|
|
self.assertEqual(model.nsite, 10)
|
|
self.assertEqual(model.nsensor, 9)
|
|
|
|
def test_plugin(self):
|
|
spec = mujoco.MjSpec()
|
|
spec.activate_plugin('mujoco.elasticity.cable')
|
|
plugin = spec.add_plugin(
|
|
name='instance_name',
|
|
plugin_name='mujoco.elasticity.cable',
|
|
active=True,
|
|
info='info'
|
|
)
|
|
plugin.config = {'twist': '10', 'bend': '1'}
|
|
self.assertEqual(plugin.config, {'twist': '10', 'bend': '1'})
|
|
|
|
body = spec.worldbody.add_body()
|
|
body.plugin = plugin
|
|
body.plugin.name = 'instance_name'
|
|
body.plugin.active = True
|
|
|
|
geom = body.add_geom()
|
|
geom.type = mujoco.mjtGeom.mjGEOM_BOX
|
|
geom.size[0] = 1
|
|
geom.size[1] = 1
|
|
geom.size[2] = 1
|
|
|
|
model = spec.compile()
|
|
self.assertIsNotNone(model)
|
|
self.assertEqual(model.nplugin, 1)
|
|
self.assertEqual(model.npluginattr, 7)
|
|
self.assertEqual(model.body_plugin[1], 0)
|
|
attributes = (''.join([chr(i) for i in model.plugin_attr]).split(chr(0)))
|
|
self.assertEqual(attributes[:2], ['10', '1'])
|
|
|
|
copy = spec.copy() # before assigning the new config
|
|
wrong_config = {'wrong': '10', 'bend': '1'}
|
|
for s in [spec, copy]:
|
|
s.plugins[0].config = wrong_config
|
|
with self.assertRaisesRegex(
|
|
ValueError, "Error: unrecognized attribute 'plugin:wrong'"
|
|
):
|
|
s.compile()
|
|
|
|
def test_geom_and_mesh_plugin(self):
|
|
"""Test that geom.plugin and mesh.plugin are accessible."""
|
|
spec = mujoco.MjSpec()
|
|
|
|
# Verify mesh has plugin attribute
|
|
mesh = spec.add_mesh(name='test_mesh')
|
|
self.assertTrue(hasattr(mesh, 'plugin'))
|
|
|
|
# Verify geom has plugin attribute
|
|
body = spec.worldbody.add_body()
|
|
geom = body.add_geom()
|
|
self.assertTrue(hasattr(geom, 'plugin'))
|
|
|
|
# Verify we can access and modify plugin properties
|
|
spec.activate_plugin('mujoco.sdf.torus')
|
|
plugin = spec.add_plugin(name='inst', plugin_name='mujoco.sdf.torus')
|
|
|
|
# Assign plugin to geom
|
|
geom.plugin = plugin
|
|
self.assertEqual(geom.plugin.name, 'inst')
|
|
self.assertEqual(geom.plugin.plugin_name, 'mujoco.sdf.torus')
|
|
|
|
# Assign plugin to mesh
|
|
mesh.plugin = plugin
|
|
self.assertEqual(mesh.plugin.name, 'inst')
|
|
self.assertEqual(mesh.plugin.plugin_name, 'mujoco.sdf.torus')
|
|
|
|
def test_duplicate_name_error(self):
|
|
main_xml = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<body>
|
|
<geom size="0.1"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
|
|
spec = mujoco.MjSpec.from_string(main_xml)
|
|
spec.add_material().name = 'yellow'
|
|
with self.assertRaisesRegex(
|
|
ValueError, "Error: repeated name 'yellow' in material"
|
|
):
|
|
spec.add_material().name = 'yellow'
|
|
|
|
def test_duplicate_name_error_when_adding_specs_with_kwargs(self):
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body(name='body')
|
|
body.add_geom(
|
|
type=mujoco.mjtGeom.mjGEOM_BOX, size=[0.1, 1, 1], name='dup'
|
|
)
|
|
with self.assertRaisesRegex(
|
|
ValueError, "Error: repeated name 'dup' in geom"
|
|
):
|
|
body.add_geom(
|
|
type=mujoco.mjtGeom.mjGEOM_BOX, size=[1, 0.1, 1], name='dup'
|
|
)
|
|
|
|
spec2 = mujoco.MjSpec()
|
|
spec2.add_material(name='yellow')
|
|
with self.assertRaisesRegex(
|
|
ValueError, "Error: repeated name 'yellow' in material"
|
|
):
|
|
spec2.add_material(name='yellow')
|
|
|
|
def test_delete_unused_plugin(self):
|
|
spec = mujoco.MjSpec.from_string("""
|
|
<mujoco model="MuJoCo Model">
|
|
<extension>
|
|
<plugin plugin="mujoco.pid">
|
|
<instance name="pid1">
|
|
<config key="kp" value="4.0"/>
|
|
</instance>
|
|
</plugin>
|
|
</extension>
|
|
|
|
<worldbody>
|
|
<body>
|
|
<geom size="1"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
""")
|
|
plugin = spec.plugins[0]
|
|
self.assertIsNotNone(plugin)
|
|
spec.delete(plugin)
|
|
|
|
model = spec.compile()
|
|
self.assertIsNotNone(model)
|
|
self.assertEqual(model.nplugin, 0)
|
|
|
|
def testPluginAssignment(self):
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body()
|
|
body.name = 'test_body'
|
|
|
|
plugin = spec.add_plugin(
|
|
name='test_instance', plugin_name='mujoco.elasticity.cable', active=True
|
|
)
|
|
plugin.config = {'twist': '1e2', 'bend': '4e1'}
|
|
|
|
# Assignment should copy active flag and names
|
|
body.plugin = plugin
|
|
|
|
self.assertTrue(body.plugin.active)
|
|
self.assertEqual(body.plugin.name, 'test_instance')
|
|
self.assertEqual(body.plugin.plugin_name, 'mujoco.elasticity.cable')
|
|
|
|
def test_access_option_stat_visual(self):
|
|
spec = mujoco.MjSpec.from_string("""
|
|
<mujoco model="MuJoCo Model">
|
|
<option timestep="0.001"/>
|
|
<statistic meansize="0.05"/>
|
|
<visual>
|
|
<quality shadowsize="4096"/>
|
|
<headlight active="0"/>
|
|
<rgba camera="0 0 0 0"/>
|
|
</visual>
|
|
</mujoco>
|
|
""")
|
|
self.assertEqual(spec.option.timestep, 0.001)
|
|
self.assertEqual(spec.stat.meansize, 0.05)
|
|
self.assertEqual(spec.visual.quality.shadowsize, 4096)
|
|
self.assertEqual(spec.visual.headlight.active, 0)
|
|
self.assertEqual(spec.visual.global_, getattr(spec.visual, 'global'))
|
|
np.testing.assert_array_equal(spec.visual.rgba.camera, [0, 0, 0, 0])
|
|
|
|
spec.option.timestep = 0.002
|
|
spec.stat.meansize = 0.06
|
|
spec.visual.quality.shadowsize = 8192
|
|
spec.visual.headlight.active = 1
|
|
spec.visual.rgba.camera = [1, 1, 1, 1]
|
|
|
|
model = spec.compile()
|
|
|
|
self.assertEqual(model.opt.timestep, 0.002)
|
|
self.assertEqual(model.stat.meansize, 0.06)
|
|
self.assertEqual(model.vis.quality.shadowsize, 8192)
|
|
self.assertEqual(model.vis.headlight.active, 1)
|
|
np.testing.assert_array_equal(model.vis.rgba.camera, [1, 1, 1, 1])
|
|
|
|
def test_assign_list_element(self):
|
|
spec = mujoco.MjSpec()
|
|
material = spec.add_material()
|
|
texture_index = mujoco.mjtTextureRole.mjTEXROLE_RGB
|
|
|
|
# Assign a string to a list element.
|
|
material.textures[texture_index] = 'texture_name'
|
|
self.assertEqual(material.textures[texture_index], 'texture_name')
|
|
|
|
# Assign a complete list (must be mjNTEXROLE = 10 elements)
|
|
material.textures = ['', 'new_name', '', '', '', '', '', '', '', '']
|
|
self.assertEqual(material.textures[texture_index], 'new_name')
|
|
|
|
# textures is iterable
|
|
self.assertEqual('new_name', ''.join(material.textures))
|
|
|
|
# supports `len` - should always be mjNTEXROLE (10)
|
|
self.assertLen(material.textures, mujoco.mjtTextureRole.mjNTEXROLE)
|
|
|
|
# field checks for out-of-bound access on read and on write
|
|
with self.assertRaises(IndexError):
|
|
material.textures[10] = 'x'
|
|
|
|
with self.assertRaises(IndexError):
|
|
material.textures[-1] = 'x'
|
|
|
|
def test_textures(self):
|
|
"""Tests that partial texture list assignment raises ValueError."""
|
|
|
|
spec = mujoco.MjSpec()
|
|
material = spec.add_material(name='mat')
|
|
spec.add_texture(
|
|
name='tex', builtin=mujoco.mjtBuiltin.mjBUILTIN_FLAT, width=2, height=2
|
|
)
|
|
|
|
# Should raise ValueError for incorrect size (only 1 element instead of 10)
|
|
with self.assertRaises(ValueError) as cm:
|
|
material.textures = ['tex']
|
|
self.assertIn('must have exactly 10 elements', str(cm.exception))
|
|
self.assertIn('got 1', str(cm.exception))
|
|
|
|
# Should succeed with correct size (mjNTEXROLE = 10)
|
|
material.textures = ['tex', '', '', '', '', '', '', '', '', '']
|
|
spec.worldbody.add_geom(size=[0.1, 0.1, 0.1], material='mat')
|
|
model = spec.compile()
|
|
self.assertIsNotNone(model)
|
|
|
|
def test_assign_texture(self):
|
|
spec = mujoco.MjSpec()
|
|
texture = spec.add_texture(name='texture', height=2, width=2)
|
|
texture.data = np.zeros((2, 2, 3), dtype=np.uint8).tobytes()
|
|
spec.compile()
|
|
|
|
def test_read_texture(self):
|
|
spec = mujoco.MjSpec()
|
|
texture = spec.add_texture(name='texture', height=1, width=2, nchannel=3)
|
|
texture.data = bytes([1, 2, 3, 4, 5, 6])
|
|
read_bytes = texture.data
|
|
self.assertEqual(read_bytes, bytes([1, 2, 3, 4, 5, 6]))
|
|
|
|
def test_modify_texture(self):
|
|
# Assign red, green and blue pixels, then make the first pixel yellow.
|
|
spec = mujoco.MjSpec()
|
|
texture = spec.add_texture(name='texture', height=1, width=3, nchannel=3)
|
|
texture.data = bytes([255, 0, 0, 0, 255, 0, 0, 0, 255])
|
|
data_array = bytearray(texture.data)
|
|
data_array[1] = 255
|
|
texture.data = bytes(data_array)
|
|
self.assertEqual(
|
|
texture.data, bytes([255, 255, 0, 0, 255, 0, 0, 0, 255])
|
|
)
|
|
|
|
# Assigning values outside the range [0, 255] should raise an error.
|
|
with self.assertRaises(ValueError):
|
|
data_array[0] = 256
|
|
with self.assertRaises(ValueError):
|
|
data_array[0] = -1
|
|
|
|
def test_find_unnamed_asset(self):
|
|
spec = mujoco.MjSpec()
|
|
texture_file = spec.add_texture(file='file.png')
|
|
texture_name = spec.add_texture(name='name')
|
|
mesh_file = spec.add_mesh(file='file.obj')
|
|
mesh_name = spec.add_mesh(name='mesh')
|
|
self.assertEqual(spec.texture('file'), texture_file)
|
|
self.assertEqual(spec.texture('name'), texture_name)
|
|
self.assertEqual(spec.mesh('file'), mesh_file)
|
|
self.assertEqual(spec.mesh('mesh'), mesh_name)
|
|
self.assertIsNone(spec.texture('none'))
|
|
self.assertIsNone(spec.mesh('none'))
|
|
|
|
def test_texture_gridlayout(self):
|
|
spec = mujoco.MjSpec()
|
|
|
|
texture = spec.add_texture(name='test', gridlayout='.U..LFRB.D..')
|
|
self.assertEqual(list(texture.gridlayout), list('.U..LFRB.D..'))
|
|
|
|
texture2 = spec.add_texture(name='test2', gridlayout=list('.U..LFRB.D..'))
|
|
self.assertEqual(list(texture2.gridlayout), list('.U..LFRB.D..'))
|
|
|
|
with self.assertRaises(ValueError) as cm:
|
|
spec.add_texture(name='test3', gridlayout='.U..')
|
|
self.assertIn('should have length 12', str(cm.exception))
|
|
|
|
with self.assertRaises(ValueError) as cm:
|
|
spec.add_texture(name='test4', gridlayout=['.', 'U', '.', '.'])
|
|
self.assertIn('should have length 12', str(cm.exception))
|
|
|
|
with self.assertRaises(ValueError) as cm:
|
|
spec.add_texture(name='test5', gridlayout=['..', 'U'] + ['.'] * 10)
|
|
self.assertIn('list elements must be single characters', str(cm.exception))
|
|
|
|
def test_attach_units(self):
|
|
child = mujoco.MjSpec()
|
|
parent = mujoco.MjSpec()
|
|
parent.compiler.degree = not child.compiler.degree
|
|
body = child.worldbody.add_body(euler=[90, 0, 0])
|
|
frame = parent.worldbody.add_frame(euler=[-mujoco.mjPI / 2, 0, 0])
|
|
frame.attach_body(body, prefix='child-')
|
|
model = parent.compile()
|
|
np.testing.assert_almost_equal(model.body_quat[1], [1, 0, 0, 0])
|
|
|
|
def test_compiler_from_element(self):
|
|
child = mujoco.MjSpec()
|
|
child.meshdir = '/child/meshes'
|
|
child.texturedir = '/child/textures'
|
|
child_body = child.worldbody.add_body()
|
|
child_geom = child_body.add_geom()
|
|
child_geom.size[0] = 1
|
|
child_site = child_body.add_site()
|
|
|
|
parent = mujoco.MjSpec()
|
|
parent.meshdir = '/parent/meshes'
|
|
parent.texturedir = '/parent/textures'
|
|
parent_geom = parent.worldbody.add_geom()
|
|
parent_geom.size[0] = 1
|
|
parent_site = parent.worldbody.add_site()
|
|
|
|
self.assertEqual(parent_geom.compiler.meshdir, '/parent/meshes')
|
|
self.assertEqual(parent_geom.compiler.texturedir, '/parent/textures')
|
|
self.assertEqual(child_geom.compiler.meshdir, '/child/meshes')
|
|
self.assertEqual(child_site.compiler.meshdir, '/child/meshes')
|
|
|
|
frame = parent.worldbody.add_frame()
|
|
frame.attach_body(child_body, prefix='child-')
|
|
|
|
self.assertEqual(parent_geom.compiler.meshdir, '/parent/meshes')
|
|
self.assertEqual(parent_site.compiler.meshdir, '/parent/meshes')
|
|
self.assertEqual(child_geom.compiler.meshdir, '/child/meshes')
|
|
self.assertEqual(child_geom.compiler.texturedir, '/child/textures')
|
|
self.assertEqual(child_site.compiler.meshdir, '/child/meshes')
|
|
|
|
def test_attach_to_site(self):
|
|
parent = mujoco.MjSpec()
|
|
parent.assets = {'path/cube.obj': 'cube_content'}
|
|
site = parent.worldbody.add_site(pos=[1, 2, 3], quat=[0, 0, 0, 1])
|
|
site.name = 'site'
|
|
|
|
# Attach body to site and compile.
|
|
child1 = mujoco.MjSpec()
|
|
child1.assets = {'cube1.obj': 'cube1_content'}
|
|
body1 = child1.worldbody.add_body()
|
|
self.assertIs(body1, site.attach_body(body1, prefix='_'))
|
|
self.assertIsNotNone(child1.worldbody)
|
|
body1.pos = [1, 1, 1]
|
|
model1 = parent.compile()
|
|
self.assertIsNotNone(model1)
|
|
self.assertEqual(model1.nbody, 2)
|
|
np.testing.assert_array_equal(model1.body_pos[1], [0, 1, 4])
|
|
np.testing.assert_array_equal(model1.body_quat[1], [0, 0, 0, 1])
|
|
self.assertEqual(parent.assets['path/cube.obj'], 'cube_content')
|
|
|
|
# Attach entire spec to site and compile again.
|
|
child2 = mujoco.MjSpec()
|
|
child2.assets = {'path/cube2.obj': 'cube2_content'}
|
|
body2 = child2.worldbody.add_body(name='body')
|
|
self.assertIsNotNone(parent.attach(child2, site=site, suffix='-child2'))
|
|
self.assertIsNotNone(child2.worldbody)
|
|
self.assertEqual(child2.parent, parent)
|
|
body2.pos = [-1, -1, -1]
|
|
model2 = parent.compile()
|
|
self.assertIsNotNone(model2)
|
|
self.assertEqual(model2.nbody, 3)
|
|
np.testing.assert_array_equal(model2.body_pos[1], [0, 1, 4])
|
|
np.testing.assert_array_equal(model2.body_pos[2], [2, 3, 2])
|
|
np.testing.assert_array_equal(model2.body_quat[1], [0, 0, 0, 1])
|
|
np.testing.assert_array_equal(model2.body_quat[2], [0, 0, 0, 1])
|
|
self.assertEqual(parent.assets['path/cube.obj'], 'cube_content')
|
|
self.assertEqual(parent.assets['path//cube2-child2.obj'], 'cube2_content')
|
|
|
|
# Attach another spec to site (referenced by name) and compile again.
|
|
child3 = mujoco.MjSpec()
|
|
child3.assets = {'path/cube3.obj': 'cube3_content'}
|
|
body3 = child3.worldbody.add_body(name='body')
|
|
self.assertIsNotNone(parent.attach(child3, site='site', prefix='child3-'))
|
|
self.assertIsNotNone(child3.worldbody)
|
|
self.assertEqual(child3.parent, parent)
|
|
body3.pos = [-2, -2, -2]
|
|
model3 = parent.compile()
|
|
self.assertIsNotNone(model3)
|
|
self.assertEqual(model3.nbody, 4)
|
|
np.testing.assert_array_equal(model3.body_pos[1], [0, 1, 4])
|
|
np.testing.assert_array_equal(model3.body_pos[2], [2, 3, 2])
|
|
np.testing.assert_array_equal(model3.body_pos[3], [3, 4, 1])
|
|
np.testing.assert_array_equal(model3.body_quat[1], [0, 0, 0, 1])
|
|
np.testing.assert_array_equal(model3.body_quat[2], [0, 0, 0, 1])
|
|
np.testing.assert_array_equal(model3.body_quat[3], [0, 0, 0, 1])
|
|
self.assertEqual(parent.assets['path/cube.obj'], 'cube_content')
|
|
self.assertEqual(parent.assets['path//cube2-child2.obj'], 'cube2_content')
|
|
self.assertEqual(parent.assets['path/child3-cube3.obj'], 'cube3_content')
|
|
|
|
# Fail to attach to a site that does not exist.
|
|
child4 = mujoco.MjSpec()
|
|
with self.assertRaisesRegex(ValueError, 'Site not found.'):
|
|
parent.attach(child4, site='invalid_site', prefix='child3-')
|
|
|
|
def test_body_to_frame(self):
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body(pos=[1, 2, 3])
|
|
spec.compile()
|
|
frame = body.to_frame()
|
|
np.testing.assert_array_equal(frame.pos, [1, 2, 3])
|
|
|
|
def test_get_frame(self):
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body()
|
|
frame = body.add_frame()
|
|
geom = body.add_geom()
|
|
geom.set_frame(frame)
|
|
self.assertIsNotNone(frame)
|
|
self.assertIs(geom.frame, frame)
|
|
|
|
def test_attach_to_frame(self):
|
|
parent = mujoco.MjSpec()
|
|
parent.assets = {'path/cube.obj': 'cube_content'}
|
|
frame = parent.worldbody.add_frame(pos=[1, 2, 3], quat=[0, 0, 0, 1])
|
|
frame.name = 'frame'
|
|
|
|
# Attach body to frame and compile.
|
|
child1 = mujoco.MjSpec()
|
|
child1.assets = {'path/cube1.obj': 'cube1_content'}
|
|
body1 = child1.worldbody.add_body()
|
|
self.assertIs(body1, frame.attach_body(body1, prefix='_'))
|
|
self.assertIsNotNone(child1.worldbody)
|
|
body1.pos = [1, 1, 1]
|
|
model1 = parent.compile()
|
|
self.assertIsNotNone(model1)
|
|
self.assertEqual(model1.nbody, 2)
|
|
np.testing.assert_array_equal(model1.body_pos[1], [0, 1, 4])
|
|
np.testing.assert_array_equal(model1.body_quat[1], [0, 0, 0, 1])
|
|
self.assertEqual(parent.assets['path/cube.obj'], 'cube_content')
|
|
|
|
# Attach entire spec to frame and compile again.
|
|
child2 = mujoco.MjSpec()
|
|
child2.assets = {'path/cube2.obj': 'cube2_content'}
|
|
body2 = child2.worldbody.add_body(name='body')
|
|
body2.set_frame(child2.worldbody.add_frame(pos=[-1, -1, 1]))
|
|
self.assertIsNotNone(parent.attach(child2, frame=frame, suffix='-child'))
|
|
self.assertIsNotNone(child2.worldbody)
|
|
self.assertEqual(child2.parent, parent)
|
|
body2.pos = [-1, -1, -1]
|
|
model2 = parent.compile()
|
|
self.assertIsNotNone(model2)
|
|
self.assertEqual(model2.nbody, 3)
|
|
np.testing.assert_array_equal(model2.body_pos[1], [0, 1, 4])
|
|
np.testing.assert_array_equal(model2.body_pos[2], [3, 4, 3])
|
|
np.testing.assert_array_equal(model2.body_quat[1], [0, 0, 0, 1])
|
|
np.testing.assert_array_equal(model2.body_quat[2], [0, 0, 0, 1])
|
|
self.assertEqual(parent.assets['path/cube.obj'], 'cube_content')
|
|
self.assertEqual(parent.assets['path//cube2-child.obj'], 'cube2_content')
|
|
|
|
# Attach another spec to frame (referenced by name) and compile again.
|
|
child3 = mujoco.MjSpec()
|
|
child3.assets = {'path/cube2.obj': 'new_content'}
|
|
body3 = child3.worldbody.add_body(name='body')
|
|
body3.set_frame(child3.worldbody.add_frame(pos=[-1, -1, 1]))
|
|
self.assertIsNotNone(parent.attach(child3, frame='frame', prefix='child3-'))
|
|
self.assertIsNotNone(child3.worldbody)
|
|
self.assertEqual(child3.parent, parent)
|
|
body3.pos = [-2, -2, -2]
|
|
model3 = parent.compile()
|
|
self.assertIsNotNone(model3)
|
|
self.assertEqual(model3.nbody, 4)
|
|
np.testing.assert_array_equal(model3.body_pos[1], [0, 1, 4])
|
|
np.testing.assert_array_equal(model3.body_pos[2], [3, 4, 3])
|
|
np.testing.assert_array_equal(model3.body_pos[3], [4, 5, 2])
|
|
np.testing.assert_array_equal(model3.body_quat[1], [0, 0, 0, 1])
|
|
np.testing.assert_array_equal(model3.body_quat[2], [0, 0, 0, 1])
|
|
np.testing.assert_array_equal(model3.body_quat[3], [0, 0, 0, 1])
|
|
self.assertEqual(parent.assets['path/cube.obj'], 'cube_content')
|
|
self.assertEqual(parent.assets['path/child3-cube2.obj'], 'new_content')
|
|
|
|
# Fail to attach to a frame that does not exist.
|
|
child4 = mujoco.MjSpec()
|
|
with self.assertRaisesRegex(ValueError, 'Frame not found.'):
|
|
parent.attach(child4, frame='invalid_frame', prefix='child3-')
|
|
|
|
def test_delete_from_attached_spec_error(self):
|
|
parent = mujoco.MjSpec()
|
|
child = mujoco.MjSpec()
|
|
body = child.worldbody.add_body(name='child_body')
|
|
geom = body.add_geom(name='child_geom')
|
|
|
|
frame = parent.worldbody.add_frame()
|
|
parent.attach(child, frame=frame, prefix='child_')
|
|
|
|
# Now child spec is attached. Deleting from it should raise ValueError.
|
|
with self.assertRaisesRegex(
|
|
ValueError, 'Cannot delete element from an attached mjSpec.'
|
|
):
|
|
child.delete(geom)
|
|
|
|
def test_attach_valid_child_lists(self):
|
|
xml1 = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="b1">
|
|
<geom name="g1"/>
|
|
<joint name="j1" type="hinge"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
|
|
xml2 = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="b2">
|
|
<geom name="g2"/>
|
|
<joint name="j2" type="hinge"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
|
|
parent = mujoco.MjSpec.from_string(xml1)
|
|
child = mujoco.MjSpec.from_string(xml2)
|
|
self.assertLen(child.joints, 1)
|
|
self.assertLen(child.geoms, 1)
|
|
frame = parent.worldbody.add_frame()
|
|
parent.attach(child, prefix='', frame=frame)
|
|
self.assertLen(child.joints, 1)
|
|
self.assertLen(child.geoms, 1)
|
|
|
|
def test_bind(self):
|
|
spec = mujoco.MjSpec.from_string("""
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="main">
|
|
<geom name="main" size="0.15 0.15 0.15" mass="1" type="box"/>
|
|
<freejoint/>
|
|
<body name="box">
|
|
<joint name="box" type="hinge" range="-1 +1"/>
|
|
<geom name="box" size="0.15 0.15 0.15" mass="1" type="box"/>
|
|
</body>
|
|
<body name="sphere">
|
|
<joint name="sphere" type="hinge" range="-1 +1"/>
|
|
<geom name="sphere" size="0.15 0.15 0.15" mass="1" type="box"/>
|
|
</body>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
""")
|
|
joint_box = spec.joint('box')
|
|
joint_sphere = spec.joint('sphere')
|
|
joints = [joint_box, joint_sphere]
|
|
mj_model = spec.compile()
|
|
mj_data = mujoco.MjData(mj_model)
|
|
np.testing.assert_array_equal(mj_data.bind(joint_box).qpos, 0)
|
|
np.testing.assert_array_equal(mj_model.bind(joint_box).qposadr, 7)
|
|
np.testing.assert_array_equal(mj_data.bind(joints).qpos, [0, 0])
|
|
np.testing.assert_array_equal(mj_model.bind(joints).qposadr, [7, 8])
|
|
np.testing.assert_array_equal(mj_data.bind([]).qpos, [])
|
|
np.testing.assert_array_equal(mj_model.bind([]).qposadr, [])
|
|
mj_data.bind(joints).qpos = np.array([1, 2])
|
|
np.testing.assert_array_equal(mj_data.bind(joints).qpos, [1, 2])
|
|
with self.assertRaisesRegex(
|
|
AttributeError, "object has no attribute 'invalid'"
|
|
):
|
|
print(mj_model.bind(joints).invalid)
|
|
invalid_spec = mujoco.MjSpec()
|
|
invalid_spec.worldbody.add_body(name='main')
|
|
with self.assertRaisesRegex(
|
|
ValueError,
|
|
'The mjSpec does not match mjModel. Please recompile the mjSpec.',
|
|
):
|
|
print(mj_model.bind(invalid_spec.body('main')))
|
|
|
|
def test_incorrect_hfield_size(self):
|
|
nrow = 300
|
|
ncol = 400
|
|
hdata = np.random.uniform(size=(1, 1))
|
|
model_spec = mujoco.MjSpec()
|
|
model_spec.add_hfield(
|
|
name='hfield',
|
|
size=[1, 1, 1, 1e-3],
|
|
ncol=ncol,
|
|
nrow=nrow,
|
|
userdata=hdata.flatten(),
|
|
)
|
|
model_spec.worldbody.add_geom(
|
|
name='hfield',
|
|
type=mujoco.mjtGeom.mjGEOM_HFIELD,
|
|
pos=np.array([0, 0, 1]),
|
|
hfieldname='hfield',
|
|
)
|
|
with self.assertRaisesRegex(
|
|
ValueError, r"Error: elevation data length must match nrow\*ncol\n"
|
|
"Element name 'hfield', id 0",
|
|
):
|
|
model_spec.compile()
|
|
|
|
def test_address(self):
|
|
spec1 = mujoco.MjSpec()
|
|
spec2 = mujoco.MjSpec()
|
|
spec3 = spec1.copy()
|
|
|
|
self.assertGreater(spec1._address, 0)
|
|
self.assertGreater(spec2._address, 0)
|
|
self.assertGreater(spec3._address, 0)
|
|
self.assertLen({spec1._address, spec2._address, spec3._address}, 3)
|
|
|
|
def test_actuator_shortname(self):
|
|
spec = mujoco.MjSpec()
|
|
actuator = spec.add_actuator(
|
|
gainprm=np.zeros((10,)),
|
|
dyntype=mujoco.mjtDyn.mjDYN_FILTER,
|
|
gaintype=mujoco.mjtGain.mjGAIN_AFFINE,
|
|
biastype=mujoco.mjtBias.mjBIAS_AFFINE,
|
|
)
|
|
actuator.set_to_motor()
|
|
self.assertEqual(actuator.gainprm[0], 1)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED)
|
|
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_NONE)
|
|
|
|
actuator.set_to_position(kp=2.0, kv=3.0, timeconst=4.0, inheritrange=True)
|
|
self.assertEqual(actuator.gainprm[0], 2)
|
|
self.assertEqual(actuator.biasprm[1], -2)
|
|
self.assertEqual(actuator.biasprm[2], -3)
|
|
self.assertEqual(actuator.dynprm[0], 4)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_FILTEREXACT)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED)
|
|
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_AFFINE)
|
|
self.assertEqual(actuator.inheritrange, True)
|
|
|
|
actuator.set_to_intvelocity(
|
|
kp=2.0, kv=3.0, timeconst=4.0, inheritrange=True
|
|
)
|
|
self.assertEqual(actuator.gainprm[0], 2)
|
|
self.assertEqual(actuator.biasprm[1], -2)
|
|
self.assertEqual(actuator.biasprm[2], -3)
|
|
self.assertEqual(actuator.dynprm[0], 4)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_INTEGRATOR)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED)
|
|
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_AFFINE)
|
|
self.assertEqual(actuator.inheritrange, True)
|
|
|
|
actuator.set_to_orientation(kp=2.0, dampratio=1.0)
|
|
self.assertEqual(actuator.gainprm[0], 2)
|
|
self.assertEqual(actuator.biasprm[1], -2)
|
|
self.assertEqual(actuator.biasprm[2], 1)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_SO3)
|
|
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_SO3)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE)
|
|
|
|
actuator.set_to_pid(kp=2.0, kv=3.0, ki=0.5, imax=1.5, slewmax=4.0)
|
|
self.assertEqual(actuator.biasprm[1], -2)
|
|
self.assertEqual(actuator.biasprm[2], -3)
|
|
self.assertEqual(actuator.gainprm[0], 0.5)
|
|
self.assertEqual(actuator.dynprm[0], 1.5)
|
|
self.assertEqual(actuator.dynprm[1], 4.0)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_PID)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_PID)
|
|
|
|
actuator.set_to_velocity(kv=5.0)
|
|
self.assertEqual(actuator.gainprm[0], 5)
|
|
self.assertEqual(actuator.biasprm[2], -5)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED)
|
|
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_AFFINE)
|
|
|
|
actuator.set_to_damper(kv=6.0)
|
|
self.assertEqual(actuator.gainprm[0], 0)
|
|
self.assertEqual(actuator.gainprm[2], -6)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_AFFINE)
|
|
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_NONE)
|
|
|
|
actuator.set_to_adhesion(gain=7.0)
|
|
self.assertEqual(actuator.gainprm[0], 7)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED)
|
|
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_NONE)
|
|
|
|
actuator.set_to_dcmotor(motorconst=[0.05, 0.05], resistance=2.0)
|
|
self.assertEqual(actuator.gainprm[0], 2.0)
|
|
self.assertEqual(actuator.gainprm[1], 0.05)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_DCMOTOR)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_DCMOTOR)
|
|
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_DCMOTOR)
|
|
|
|
actuator = spec.add_actuator()
|
|
actuator.set_to_muscle(tausmooth=0.1)
|
|
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_MUSCLE)
|
|
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_MUSCLE)
|
|
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_MUSCLE)
|
|
self.assertEqual(actuator.dynprm[2], 0.1)
|
|
|
|
actuator.set_to_muscle(
|
|
timeconst=[0.02, 0.05],
|
|
tausmooth=0.2,
|
|
range=[0.8, 1.2],
|
|
force=5.0,
|
|
scale=250.0,
|
|
lmin=0.6,
|
|
lmax=1.7,
|
|
vmax=1.8,
|
|
fpmax=1.4,
|
|
fvmax=1.5,
|
|
)
|
|
self.assertEqual(actuator.dynprm[0], 0.02)
|
|
self.assertEqual(actuator.dynprm[1], 0.05)
|
|
self.assertEqual(actuator.dynprm[2], 0.2)
|
|
self.assertEqual(actuator.gainprm[0], 0.8)
|
|
self.assertEqual(actuator.gainprm[1], 1.2)
|
|
self.assertEqual(actuator.gainprm[2], 5.0)
|
|
self.assertEqual(actuator.gainprm[3], 250.0)
|
|
self.assertEqual(actuator.gainprm[4], 0.6)
|
|
self.assertEqual(actuator.gainprm[5], 1.7)
|
|
self.assertEqual(actuator.gainprm[6], 1.8)
|
|
self.assertEqual(actuator.gainprm[7], 1.4)
|
|
self.assertEqual(actuator.gainprm[8], 1.5)
|
|
|
|
def test_bad_contact_sensor(self):
|
|
test_cases = [
|
|
dict(
|
|
expected_error='num (intprm[2]) must be positive in sensor',
|
|
sensor_params=dict(
|
|
type=mujoco.mjtSensor.mjSENS_CONTACT,
|
|
objtype=mujoco.mjtObj.mjOBJ_GEOM,
|
|
objname='sphere1',
|
|
intprm=[1, 0, 0],
|
|
),
|
|
),
|
|
dict(
|
|
expected_error='data spec (intprm[0]) must be positive, got 0',
|
|
sensor_params=dict(
|
|
type=mujoco.mjtSensor.mjSENS_CONTACT,
|
|
objtype=mujoco.mjtObj.mjOBJ_GEOM,
|
|
objname='sphere1',
|
|
intprm=[0, 0, 1],
|
|
),
|
|
),
|
|
dict(
|
|
expected_error=(
|
|
'data spec intprm[0]=1024 must have at least one bit set of the'
|
|
' first mjNCONDATA bits'
|
|
),
|
|
sensor_params=dict(
|
|
type=mujoco.mjtSensor.mjSENS_CONTACT,
|
|
objtype=mujoco.mjtObj.mjOBJ_GEOM,
|
|
objname='sphere1',
|
|
intprm=[1 << 10, 0, 1],
|
|
),
|
|
),
|
|
dict(
|
|
expected_error=(
|
|
'data spec intprm[0]=1025 has bits set beyond the first'
|
|
' mjNCONDATA bits'
|
|
),
|
|
sensor_params=dict(
|
|
type=mujoco.mjtSensor.mjSENS_CONTACT,
|
|
objtype=mujoco.mjtObj.mjOBJ_GEOM,
|
|
objname='sphere1',
|
|
intprm=[(1 << 10) | 1, 0, 1],
|
|
),
|
|
),
|
|
dict(
|
|
expected_error='unknown reduction criterion. got 4',
|
|
sensor_params=dict(
|
|
type=mujoco.mjtSensor.mjSENS_CONTACT,
|
|
objtype=mujoco.mjtObj.mjOBJ_GEOM,
|
|
objname='sphere1',
|
|
intprm=[1, 4, 1],
|
|
),
|
|
),
|
|
dict(
|
|
expected_error=(
|
|
'first matching criterion: if set, must be'
|
|
' (x)body, geom or site'
|
|
),
|
|
sensor_params=dict(
|
|
type=mujoco.mjtSensor.mjSENS_CONTACT,
|
|
objtype=mujoco.mjtObj.mjOBJ_CAMERA,
|
|
objname='cam',
|
|
),
|
|
),
|
|
dict(
|
|
expected_error=(
|
|
'second matching criterion: if set, must be (x)body or geom'
|
|
),
|
|
sensor_params=dict(
|
|
type=mujoco.mjtSensor.mjSENS_CONTACT,
|
|
reftype=mujoco.mjtObj.mjOBJ_CAMERA,
|
|
refname='cam',
|
|
),
|
|
),
|
|
]
|
|
|
|
for params in test_cases:
|
|
expected_error = params.get('expected_error')
|
|
with self.subTest(expected_error):
|
|
spec = mujoco.MjSpec()
|
|
spec.worldbody.add_geom(name='sphere1', size=[.2, 0, 0], pos=[0, 0, 1])
|
|
body = spec.worldbody.add_body(name='body')
|
|
non_root = body.add_body(name='non_root')
|
|
non_root.add_geom(name='sphere3', size=[.3, 0, 0], pos=[1, 0, 1])
|
|
spec.worldbody.add_camera(name='cam')
|
|
spec.add_sensor(**params['sensor_params'])
|
|
error_predicate = lambda e, expected=expected_error: expected in str(e)
|
|
with self.assertRaisesWithPredicateMatch(ValueError, error_predicate):
|
|
spec.compile()
|
|
|
|
def test_sensor_data_size(self):
|
|
spec = mujoco.MjSpec()
|
|
quat = spec.add_sensor(
|
|
name='framequat',
|
|
type=mujoco.mjtSensor.mjSENS_FRAMEQUAT,
|
|
objtype=mujoco.mjtObj.mjOBJ_BODY,
|
|
objname='world',
|
|
)
|
|
self.assertEqual(quat.get_data_size(), 4)
|
|
clock = spec.add_sensor(
|
|
name='clock',
|
|
type=mujoco.mjtSensor.mjSENS_CLOCK,
|
|
)
|
|
self.assertEqual(clock.get_data_size(), 1)
|
|
mj_model = spec.compile()
|
|
self.assertEqual(mj_model.sensor_dim[0], 4)
|
|
self.assertEqual(mj_model.sensor_dim[1], 1)
|
|
|
|
def test_mesh_material(self):
|
|
spec = mujoco.MjSpec()
|
|
|
|
spec.add_material(name='red', rgba=(1, 0, 0, 1))
|
|
spec.add_material(name='green', rgba=(0, 1, 0, 1))
|
|
|
|
mesh = spec.add_mesh(name='sphere')
|
|
mesh.make_sphere(subdivision=1)
|
|
mesh.material = 'red'
|
|
|
|
geom = spec.worldbody.add_geom()
|
|
geom.type = mujoco.mjtGeom.mjGEOM_MESH
|
|
geom.meshname = 'sphere'
|
|
|
|
geom_2 = spec.worldbody.add_geom()
|
|
geom_2.type = mujoco.mjtGeom.mjGEOM_MESH
|
|
geom_2.meshname = 'sphere'
|
|
geom_2.material = 'green'
|
|
|
|
model = spec.compile()
|
|
|
|
self.assertEqual(model.geom_matid[0], 0)
|
|
self.assertEqual(model.geom_matid[1], 1)
|
|
|
|
mesh.material = 'green'
|
|
model = spec.compile()
|
|
|
|
self.assertEqual(model.geom_matid[0], 1)
|
|
|
|
def test_tendon_path(self):
|
|
spec = mujoco.MjSpec()
|
|
|
|
body = spec.worldbody.add_body(name='body')
|
|
|
|
body.add_geom(name='body_geom', pos=[0, 0, 0], size=[.1, 0, 0])
|
|
site1 = body.add_site(name='site1', pos=[0, 0, 0])
|
|
site2 = body.add_site(name='site2', pos=[0, 0, -1])
|
|
site3 = body.add_site(name='site3', pos=[0, 0, -4])
|
|
sidesite = body.add_site(name='sidesite', pos=[2, 0, -5])
|
|
site4 = body.add_site(name='site4', pos=[0, 1, -6])
|
|
|
|
sphere = spec.worldbody.add_geom(
|
|
name='sphere', size=[0.2, 0, 0], pos=[0, 0, -2]
|
|
)
|
|
|
|
cylinder = spec.worldbody.add_geom(
|
|
name='cylinder',
|
|
type=mujoco.mjtGeom.mjGEOM_CYLINDER,
|
|
size=[0.1, 0.2, 0.3],
|
|
pos=[0, 0, -5]
|
|
)
|
|
|
|
joint1 = body.add_joint(
|
|
name='joint1', type=mujoco.mjtJoint.mjJNT_HINGE, axis=[0, 1, 0]
|
|
)
|
|
|
|
body2 = spec.worldbody.add_body(name='body2', pos=[2, 0, 0])
|
|
body2.add_geom(name='body2_geom', pos=[0, 0, 0], size=[.1, 0, 0])
|
|
joint2 = body2.add_joint(
|
|
name='joint2', type=mujoco.mjtJoint.mjJNT_HINGE, axis=[0, 1, 0]
|
|
)
|
|
|
|
spatial_tendon = spec.add_tendon()
|
|
fixed_tendon = spec.add_tendon()
|
|
|
|
wrap_site1 = spatial_tendon.wrap_site('site1')
|
|
wrap_site2 = spatial_tendon.wrap_site('site2')
|
|
wrap_pulley1 = spatial_tendon.wrap_pulley(2.0)
|
|
wrap_site3_1 = spatial_tendon.wrap_site('site3')
|
|
wrap_sphere = spatial_tendon.wrap_geom('sphere', '')
|
|
wrap_site4_1 = spatial_tendon.wrap_site('site4')
|
|
wrap_pulley2 = spatial_tendon.wrap_pulley(2.0)
|
|
wrap_site3_2 = spatial_tendon.wrap_site('site3')
|
|
wrap_cylinder = spatial_tendon.wrap_geom('cylinder', 'sidesite')
|
|
wrap_site4_2 = spatial_tendon.wrap_site('site4')
|
|
|
|
wrap_joint1 = fixed_tendon.wrap_joint('joint1', 1.0)
|
|
wrap_joint2 = fixed_tendon.wrap_joint('joint2', 2.0)
|
|
|
|
self.assertListEqual(
|
|
list(spatial_tendon.path),
|
|
[
|
|
wrap_site1,
|
|
wrap_site2,
|
|
wrap_pulley1,
|
|
wrap_site3_1,
|
|
wrap_sphere,
|
|
wrap_site4_1,
|
|
wrap_pulley2,
|
|
wrap_site3_2,
|
|
wrap_cylinder,
|
|
wrap_site4_2,
|
|
],
|
|
)
|
|
self.assertListEqual(
|
|
[w.target for w in spatial_tendon.path],
|
|
[
|
|
site1,
|
|
site2,
|
|
None, # Pulley wraps have no targets
|
|
site3,
|
|
sphere,
|
|
site4,
|
|
None, # Pulley wraps have no targets
|
|
site3,
|
|
cylinder,
|
|
site4,
|
|
],
|
|
)
|
|
self.assertEqual(spatial_tendon.path[8].sidesite, sidesite)
|
|
self.assertIsNone(spatial_tendon.path[7].sidesite)
|
|
|
|
self.assertListEqual(list(fixed_tendon.path), [wrap_joint1, wrap_joint2])
|
|
self.assertListEqual(
|
|
[w.target for w in fixed_tendon.path],
|
|
[joint1, joint2]
|
|
)
|
|
|
|
# Wrap type for geom is only set during compilation.
|
|
spec.compile()
|
|
|
|
self.assertEqual(wrap_site1.type, mujoco.mjtWrap.mjWRAP_SITE)
|
|
self.assertEqual(wrap_site2.type, mujoco.mjtWrap.mjWRAP_SITE)
|
|
self.assertEqual(wrap_site3_1.type, mujoco.mjtWrap.mjWRAP_SITE)
|
|
self.assertEqual(wrap_site4_1.type, mujoco.mjtWrap.mjWRAP_SITE)
|
|
self.assertEqual(wrap_site3_2.type, mujoco.mjtWrap.mjWRAP_SITE)
|
|
self.assertEqual(wrap_site4_2.type, mujoco.mjtWrap.mjWRAP_SITE)
|
|
self.assertEqual(wrap_pulley1.type, mujoco.mjtWrap.mjWRAP_PULLEY)
|
|
self.assertEqual(wrap_pulley1.divisor, 2.0)
|
|
self.assertEqual(wrap_sphere.type, mujoco.mjtWrap.mjWRAP_SPHERE)
|
|
self.assertEqual(wrap_cylinder.type, mujoco.mjtWrap.mjWRAP_CYLINDER)
|
|
self.assertEqual(wrap_pulley2.type, mujoco.mjtWrap.mjWRAP_PULLEY)
|
|
self.assertEqual(wrap_pulley2.divisor, 2.0)
|
|
|
|
self.assertEqual(wrap_joint1.type, mujoco.mjtWrap.mjWRAP_JOINT)
|
|
self.assertEqual(wrap_joint1.coef, 1.0)
|
|
self.assertEqual(wrap_joint2.type, mujoco.mjtWrap.mjWRAP_JOINT)
|
|
self.assertEqual(wrap_joint2.coef, 2.0)
|
|
|
|
def test_from_zip(self):
|
|
"""Tests that the assets are correctly parsed from a zip file."""
|
|
model_path_root = (
|
|
epath.resource_path("mujoco") / "testdata" / "MJCF_Root.zip"
|
|
)
|
|
model_path_no_root = (
|
|
epath.resource_path("mujoco") / "testdata" / "MJCF_NoRoot.zip"
|
|
)
|
|
filenames = [model_path_root.as_posix(), model_path_no_root.as_posix()]
|
|
|
|
for filename in filenames:
|
|
with self.subTest(filename):
|
|
spec = mujoco.MjSpec.from_zip(filename)
|
|
spec.compile()
|
|
assets = spec.assets
|
|
xml_string = spec.to_xml()
|
|
string_spec = mujoco.MjSpec.from_string(xml_string, assets=assets)
|
|
string_spec.compile()
|
|
self.assertEqual(spec.to_xml(), string_spec.to_xml())
|
|
|
|
def test_rangefinder_sensor(self):
|
|
"""Test rangefinder sensor with mjSpec, iterative model building."""
|
|
# Raydata field enum values for dataspec bitfield
|
|
rd = mujoco.mjtRayDataField
|
|
dist_val = int(rd.mjRAYDATA_DIST)
|
|
dir_val = int(rd.mjRAYDATA_DIR)
|
|
origin_val = int(rd.mjRAYDATA_ORIGIN)
|
|
point_val = int(rd.mjRAYDATA_POINT)
|
|
normal_val = int(rd.mjRAYDATA_NORMAL)
|
|
depth_val = int(rd.mjRAYDATA_DEPTH)
|
|
|
|
# Step 1: Create a rangefinder sensor attached to a site, no dataspec set.
|
|
# Note: site goes on a child body because rangefinder excludes the site's
|
|
# parent body from ray casting.
|
|
spec = mujoco.MjSpec()
|
|
sensor_body = spec.worldbody.add_body(name='sensor_body', pos=[0, 0, 1])
|
|
sensor_body.add_site(name='rf_site', zaxis=[0, 0, -1])
|
|
rf_sensor = spec.add_sensor(
|
|
name='rf',
|
|
type=mujoco.mjtSensor.mjSENS_RANGEFINDER,
|
|
objtype=mujoco.mjtObj.mjOBJ_SITE,
|
|
objname='rf_site',
|
|
)
|
|
|
|
# This should fail: data spec (intprm[0]) must be positive
|
|
with self.assertRaisesWithPredicateMatch(
|
|
ValueError,
|
|
lambda e: 'data spec (intprm[0]) must be positive' in str(e)
|
|
):
|
|
spec.compile()
|
|
|
|
# Step 2: Set dataspec to just mjRAYDATA_DIST
|
|
rf_sensor.intprm[0] = 1 << dist_val
|
|
model = spec.compile()
|
|
data = mujoco.MjData(model)
|
|
mujoco.mj_forward(model, data)
|
|
|
|
# With no geometry, the ray should miss: dist = -1
|
|
self.assertEqual(model.nsensordata, 1)
|
|
self.assertEqual(data.bind(rf_sensor).data[0], -1)
|
|
|
|
# Step 3: Add all raydata fields and check no-hit values
|
|
all_fields = (
|
|
(1 << dist_val) | (1 << dir_val) | (1 << origin_val) |
|
|
(1 << point_val) | (1 << normal_val) | (1 << depth_val)
|
|
)
|
|
rf_sensor.intprm[0] = all_fields
|
|
model = spec.compile()
|
|
data = mujoco.MjData(model)
|
|
mujoco.mj_forward(model, data)
|
|
|
|
# Expected size: dist(1) + dir(3) + origin(3) + point(3) + normal(3) +
|
|
# depth(1) = 14
|
|
self.assertEqual(model.nsensordata, 14)
|
|
|
|
# No-hit values
|
|
sd = data.bind(rf_sensor).data
|
|
self.assertEqual(sd[0], -1) # dist
|
|
np.testing.assert_allclose(sd[1:4], [0, 0, 0]) # dir
|
|
np.testing.assert_allclose(sd[4:7], [0, 0, 1]) # origin
|
|
np.testing.assert_allclose(sd[7:10], [0, 0, 0]) # point
|
|
np.testing.assert_allclose(sd[10:13], [0, 0, 0]) # normal
|
|
self.assertEqual(sd[13], -1) # depth
|
|
|
|
# Step 4: Add a floor plane, now the ray should hit
|
|
spec.worldbody.add_geom(
|
|
name='floor',
|
|
type=mujoco.mjtGeom.mjGEOM_PLANE,
|
|
size=[10, 10, 0.1],
|
|
)
|
|
model = spec.compile()
|
|
data = mujoco.MjData(model)
|
|
mujoco.mj_forward(model, data)
|
|
|
|
# Ray starts at z=1 pointing down, hits floor at z=0
|
|
# For site sensor, depth = dist
|
|
sd = data.bind(rf_sensor).data
|
|
self.assertAlmostEqual(sd[0], 1.0, places=6) # dist
|
|
np.testing.assert_allclose(sd[1:4], [0, 0, -1], atol=1e-10) # dir
|
|
np.testing.assert_allclose(sd[4:7], [0, 0, 1], atol=1e-10) # origin
|
|
np.testing.assert_allclose(sd[7:10], [0, 0, 0], atol=1e-10) # point
|
|
np.testing.assert_allclose(sd[10:13], [0, 0, 1], atol=1e-10) # normal
|
|
self.assertAlmostEqual(sd[13], 1.0, places=6) # depth
|
|
|
|
# Step 5: Add a camera-based rangefinder sensor
|
|
# Camera also on child body so it doesn't exclude the floor
|
|
cam_body = spec.worldbody.add_body(name='cam_body', pos=[0, 0, 2])
|
|
cam_body.add_camera(
|
|
name='rf_cam',
|
|
xyaxes=[1, 0, 0, 0, 1, 0], # z=[0,0,1], looks along -z (down)
|
|
resolution=[3, 3],
|
|
fovy=90,
|
|
)
|
|
cam_sensor = spec.add_sensor(
|
|
name='rf_cam_sensor',
|
|
type=mujoco.mjtSensor.mjSENS_RANGEFINDER,
|
|
objtype=mujoco.mjtObj.mjOBJ_CAMERA,
|
|
objname='rf_cam',
|
|
intprm=[(1 << dist_val) | (1 << depth_val), 0, 0],
|
|
)
|
|
|
|
model = spec.compile()
|
|
data = mujoco.MjData(model)
|
|
mujoco.mj_forward(model, data)
|
|
|
|
# Site sensor: 14 values, Camera sensor: (1+1)*9 = 18 values
|
|
self.assertEqual(model.nsensordata, 14 + 18)
|
|
|
|
# Check camera sensor data using bind
|
|
cam_sd = data.bind(cam_sensor).data
|
|
stride = 2 # dist + depth per pixel
|
|
center_pixel = 4 # center of 3x3 = row 1, col 1
|
|
|
|
# Center pixel: ray straight down from z=2 to z=0
|
|
self.assertAlmostEqual(cam_sd[center_pixel * stride], 2.0, places=6)
|
|
self.assertAlmostEqual(cam_sd[center_pixel * stride + 1], 2.0, places=6)
|
|
|
|
# Corner pixel: off-axis ray, dist > depth
|
|
self.assertGreater(cam_sd[0], cam_sd[1]) # dist > depth
|
|
self.assertAlmostEqual(cam_sd[1], 2.0, places=6) # depth is still 2.0
|
|
|
|
def test_encode_xml(self):
|
|
# Create a simple spec and compile.
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body()
|
|
geom = body.add_geom()
|
|
geom.size[0] = 1
|
|
model = spec.compile()
|
|
|
|
# Encode to XML.
|
|
filename = os.path.join(self.create_tempdir().full_path, 'output.xml')
|
|
nbytes = spec.encode(filename, model)
|
|
self.assertGreater(nbytes, 0)
|
|
|
|
# Verify the output is valid XML that can be loaded.
|
|
reloaded = mujoco.MjSpec.from_file(filename)
|
|
reloaded_model = reloaded.compile()
|
|
self.assertEqual(reloaded_model.ngeom, model.ngeom)
|
|
|
|
def test_encode_xml_without_model(self):
|
|
# Create a simple spec and compile so XML can be written.
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body()
|
|
geom = body.add_geom()
|
|
geom.size[0] = 1
|
|
spec.compile()
|
|
|
|
# Encode to XML without passing a model explicitly.
|
|
filename = os.path.join(self.create_tempdir().full_path, 'output.xml')
|
|
nbytes = spec.encode(filename)
|
|
self.assertGreater(nbytes, 0)
|
|
|
|
def test_encode_no_encoder_raises(self):
|
|
# Create a simple spec and compile.
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body()
|
|
geom = body.add_geom()
|
|
geom.size[0] = 1
|
|
model = spec.compile()
|
|
|
|
# Encode with an unknown extension should fail.
|
|
filename = os.path.join(self.create_tempdir().full_path, 'output.unknown')
|
|
with self.assertRaises(mujoco.FatalError):
|
|
spec.encode(filename, model)
|
|
|
|
def test_make_flex_grid(self):
|
|
# Create a spec with a flexcomp grid.
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body(name='flex_body')
|
|
flex = body.make_flex(
|
|
name='test_flex',
|
|
type='grid',
|
|
dim=3,
|
|
count=[4, 4, 4],
|
|
spacing=[0.05, 0.05, 0.05],
|
|
mass=0.5,
|
|
equality=1,
|
|
)
|
|
self.assertIsNotNone(flex)
|
|
model = spec.compile()
|
|
self.assertIsNotNone(model)
|
|
self.assertGreater(model.nflex, 0)
|
|
|
|
# Verify elastic2d is forwarded to Make() and produces shell-mode
|
|
# strain constraints (equality=3 + elastic2d=2 triggers shell path).
|
|
spec2 = mujoco.MjSpec()
|
|
body2 = spec2.worldbody.add_body(name='shell_body')
|
|
flex2 = body2.make_flex(
|
|
name='shell_flex',
|
|
type='grid',
|
|
dim=3,
|
|
count=[3, 3, 3],
|
|
spacing=[0.1, 0.1, 0.1],
|
|
dof='trilinear',
|
|
cellcount=[2, 2, 1],
|
|
mass=0.5,
|
|
equality=3, # strain
|
|
elastic2d=2, # bend
|
|
)
|
|
flex2.young = 1e3
|
|
flex2.thickness = 0.01
|
|
flex2.selfcollide = mujoco.mjtFlexSelf.mjFLEXSELF_NONE
|
|
self.assertIsNotNone(flex2)
|
|
model2 = spec2.compile()
|
|
self.assertIsNotNone(model2)
|
|
# Shell mode creates face-based constraints; verify they exist.
|
|
self.assertGreater(model2.neq, 0)
|
|
|
|
def test_make_flex_defaults(self):
|
|
# Create a spec with minimal flexcomp args (defaults).
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body(name='flex_body')
|
|
flex = body.make_flex(name='default_flex', equality=1)
|
|
self.assertIsNotNone(flex)
|
|
model = spec.compile()
|
|
self.assertIsNotNone(model)
|
|
|
|
def test_make_flex_with_pos_quat(self):
|
|
# Create a spec with flexcomp that has a pose.
|
|
spec = mujoco.MjSpec()
|
|
body = spec.worldbody.add_body(name='flex_body')
|
|
flex = body.make_flex(
|
|
name='posed_flex',
|
|
type='grid',
|
|
dim=2,
|
|
count=[3, 3, 1],
|
|
spacing=[0.1, 0.1, 0.1],
|
|
pos=[1.0, 2.0, 3.0],
|
|
quat=[1.0, 0.0, 0.0, 0.0],
|
|
equality=1,
|
|
)
|
|
self.assertIsNotNone(flex)
|
|
model = spec.compile()
|
|
self.assertIsNotNone(model)
|
|
|
|
def test_authored_struct(self):
|
|
spec = mujoco.MjSpec()
|
|
# authored struct should be accessible with correct fields
|
|
self.assertEqual(spec.authored.option, 0)
|
|
self.assertEqual(spec.authored.disableflags, 0)
|
|
self.assertEqual(spec.authored.enableflags, 0)
|
|
self.assertEqual(spec.authored.disableactuator, 0)
|
|
self.assertEqual(spec.authored.visual_global, 0)
|
|
self.assertEqual(spec.authored.visual_quality, 0)
|
|
self.assertEqual(spec.authored.visual_headlight, 0)
|
|
self.assertEqual(spec.authored.visual_map, 0)
|
|
self.assertEqual(spec.authored.visual_scale, 0)
|
|
self.assertEqual(spec.authored.visual_rgba, 0)
|
|
# compiler authored should be zero
|
|
self.assertEqual(spec.compiler.authored, 0)
|
|
|
|
def test_authored_flags_from_xml(self):
|
|
spec = mujoco.MjSpec.from_string("""
|
|
<mujoco>
|
|
<option timestep="0.01">
|
|
<flag constraint="disable" energy="enable"/>
|
|
</option>
|
|
<compiler boundmass="1"/>
|
|
<visual>
|
|
<global fovy="60"/>
|
|
<quality shadowsize="1024"/>
|
|
</visual>
|
|
<worldbody/>
|
|
</mujoco>
|
|
""")
|
|
# disable/enable flags should be tracked
|
|
self.assertNotEqual(
|
|
spec.authored.disableflags & mujoco.mjtDisableBit.mjDSBL_CONSTRAINT, 0)
|
|
self.assertEqual(
|
|
spec.authored.disableflags & mujoco.mjtDisableBit.mjDSBL_CONTACT, 0)
|
|
self.assertNotEqual(
|
|
spec.authored.enableflags & mujoco.mjtEnableBit.mjENBL_ENERGY, 0)
|
|
self.assertEqual(
|
|
spec.authored.enableflags & mujoco.mjtEnableBit.mjENBL_OVERRIDE, 0)
|
|
|
|
# option authored bitmask should be nonzero (timestep was authored)
|
|
self.assertNotEqual(spec.authored.option, 0)
|
|
|
|
# compiler authored bitmask should be nonzero (boundmass was authored)
|
|
self.assertNotEqual(spec.compiler.authored, 0)
|
|
|
|
# visual authored bitmask should be nonzero (fovy, shadowsize were authored)
|
|
self.assertNotEqual(spec.authored.visual_global, 0)
|
|
self.assertNotEqual(spec.authored.visual_quality, 0)
|
|
|
|
# visual sections that were not authored should be zero
|
|
self.assertEqual(spec.authored.visual_headlight, 0)
|
|
self.assertEqual(spec.authored.visual_map, 0)
|
|
self.assertEqual(spec.authored.visual_scale, 0)
|
|
self.assertEqual(spec.authored.visual_rgba, 0)
|
|
|
|
def test_authored_defaults_zero(self):
|
|
spec = mujoco.MjSpec.from_string("""
|
|
<mujoco>
|
|
<worldbody/>
|
|
</mujoco>
|
|
""")
|
|
# nothing authored in an empty model
|
|
self.assertEqual(spec.authored.option, 0)
|
|
self.assertEqual(spec.authored.disableflags, 0)
|
|
self.assertEqual(spec.authored.enableflags, 0)
|
|
self.assertEqual(spec.compiler.authored, 0)
|
|
self.assertEqual(spec.authored.visual_global, 0)
|
|
self.assertEqual(spec.authored.visual_quality, 0)
|
|
self.assertEqual(spec.authored.visual_map, 0)
|
|
|
|
def test_attach_conflict_merge_procedural(self):
|
|
# Procedural attach with merge mode: min/max fields are merged.
|
|
parent = mujoco.MjSpec()
|
|
parent.compiler.conflict = mujoco.mjtConflict.mjCONFLICT_MERGE
|
|
parent.option.timestep = 0.005
|
|
parent.option.iterations = 150
|
|
parent.worldbody.add_geom().size[0] = 1
|
|
|
|
child = mujoco.MjSpec()
|
|
child.option.timestep = 0.001 # smaller -> wins (min-merge)
|
|
child.option.iterations = 200 # larger -> wins (max-merge)
|
|
child_body = child.worldbody.add_body()
|
|
child_body.add_geom().size[0] = 1
|
|
|
|
frame = parent.worldbody.add_frame()
|
|
frame.attach_body(child_body, prefix='child_')
|
|
|
|
# merged values are applied immediately at attach time
|
|
self.assertEqual(parent.option.timestep, 0.001)
|
|
self.assertEqual(parent.option.iterations, 200)
|
|
|
|
# compile succeeds with the merged values
|
|
model = parent.compile()
|
|
self.assertIsNotNone(model)
|
|
self.assertEqual(model.opt.timestep, 0.001)
|
|
self.assertEqual(model.opt.iterations, 200)
|
|
|
|
def test_attach_conflict_error_procedural(self):
|
|
# Procedural attach with unmergeable conflict: raises ValueError,
|
|
# parent spec is unchanged.
|
|
parent = mujoco.MjSpec()
|
|
parent.compiler.conflict = mujoco.mjtConflict.mjCONFLICT_MERGE
|
|
parent.option.timestep = 0.005
|
|
parent.option.integrator = mujoco.mjtIntegrator.mjINT_RK4
|
|
|
|
child = mujoco.MjSpec()
|
|
child.option.timestep = 0.001
|
|
child.option.integrator = mujoco.mjtIntegrator.mjINT_IMPLICIT
|
|
child_body = child.worldbody.add_body()
|
|
child_body.add_geom().size[0] = 1
|
|
|
|
frame = parent.worldbody.add_frame()
|
|
|
|
with self.assertRaisesRegex(ValueError, 'integrator'):
|
|
frame.attach_body(child_body, prefix='child_')
|
|
|
|
# parent spec should be unchanged (two-pass guarantee)
|
|
self.assertEqual(parent.option.timestep, 0.005)
|
|
self.assertEqual(parent.option.integrator, mujoco.mjtIntegrator.mjINT_RK4)
|
|
|
|
def test_attach_conflict_merge_xml(self):
|
|
# XML-based attach with merge mode: child timestep wins (min).
|
|
parent_xml = textwrap.dedent("""\
|
|
<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__':
|
|
absltest.main()
|