Source code for rofunc.utils.robolab.formatter.mjcf_parser.element_test

# Copyright 2018 The dm_control Authors.
#
# 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 `dm_control.mjcf.element`."""

import copy
import hashlib
import itertools
import os
import sys
import traceback

import lxml
import numpy as np
from absl.testing import absltest
from absl.testing import parameterized
from rofunc.utils.robolab.formatter import mjcf_parser as mjcf
from rofunc.utils.robolab.formatter.mjcf_parser import util

from rofunc.utils.robolab.formatter.mjcf_parser import element
from rofunc.utils.robolab.formatter.mjcf_parser import namescope
from rofunc.utils.robolab.formatter.mjcf_parser import parser

etree = lxml.etree

_ASSETS_DIR = os.path.join(os.path.dirname(__file__), 'test_assets')
_TEST_MODEL_XML = os.path.join(_ASSETS_DIR, 'test_model.xml')
_TEXTURE_PATH = os.path.join(_ASSETS_DIR, 'textures/deepmind.png')
_MESH_PATH = os.path.join(_ASSETS_DIR, 'meshes/cube.stl')
_MODEL_WITH_INCLUDE_PATH = os.path.join(_ASSETS_DIR, 'model_with_include.xml')

_MODEL_WITH_INVALID_FILENAMES = os.path.join(
    _ASSETS_DIR, 'model_with_invalid_filenames.xml')
_INCLUDED_WITH_INVALID_FILENAMES = os.path.join(
    _ASSETS_DIR, 'included_with_invalid_filenames.xml')
_MODEL_WITH_NAMELESS_ASSETS = os.path.join(
    _ASSETS_DIR, 'model_with_nameless_assets.xml')


[docs]class ElementTest(parameterized.TestCase):
[docs] def assertIsSame(self, mjcf_model, other): self.assertTrue(mjcf_model.is_same_as(other)) self.assertTrue(other.is_same_as(mjcf_model))
[docs] def assertIsNotSame(self, mjcf_model, other): self.assertFalse(mjcf_model.is_same_as(other)) self.assertFalse(other.is_same_as(mjcf_model))
[docs] def assertHasAttr(self, obj, attrib): self.assertTrue(hasattr(obj, attrib))
[docs] def assertNotHasAttr(self, obj, attrib): self.assertFalse(hasattr(obj, attrib))
def _test_properties(self, mjcf_element, parent, root, recursive=False): self.assertEqual(mjcf_element.tag, mjcf_element.spec.name) self.assertEqual(mjcf_element.parent, parent) self.assertEqual(mjcf_element.root, root) self.assertEqual(mjcf_element.namescope, root.namescope) for child_name, child_spec in mjcf_element.spec.children.items(): if not (child_spec.repeated or child_spec.on_demand): child = getattr(mjcf_element, child_name) self.assertEqual(child.tag, child_name) self.assertEqual(child.spec, child_spec) if recursive: self._test_properties(child, parent=mjcf_element, root=root, recursive=True)
[docs] def testAttributeError(self): mjcf_model = element.RootElement(model='test') mjcf_model.worldbody._spec = None try: _ = mjcf_model.worldbody.tag except AttributeError: _, err, tb = sys.exc_info() else: self.fail('AttributeError was not raised.') # Test that the error comes from the fact that we've set `_spec = None`. self.assertEqual(str(err), '\'NoneType\' object has no attribute \'name\'') _, _, func_name, _ = traceback.extract_tb(tb)[-1] # Test that the error comes from the `root` property, not `__getattr__`. self.assertEqual(func_name, 'tag')
[docs] def testProperties(self): mujoco = element.RootElement(model='test') self.assertIsInstance(mujoco.namescope, namescope.NameScope) self._test_properties(mujoco, parent=None, root=mujoco, recursive=True)
def _test_attributes(self, mjcf_element, expected_values=None, recursive=False): attributes = mjcf_element.get_attributes() self.assertNotIn('class', attributes) for attribute_name in mjcf_element.spec.attributes.keys(): if attribute_name == 'class': attribute_name = 'dclass' self.assertHasAttr(mjcf_element, attribute_name) self.assertIn(attribute_name, dir(mjcf_element)) attribute_value = getattr(mjcf_element, attribute_name) if attribute_value is not None: self.assertIn(attribute_name, attributes) else: self.assertNotIn(attribute_name, attributes) if expected_values: if attribute_name in expected_values: expected_value = expected_values[attribute_name] np.testing.assert_array_equal(attribute_value, expected_value) else: self.assertIsNone(attribute_value) if recursive: for child in mjcf_element.all_children(): self._test_attributes(child, recursive=True)
[docs] def testAttributes(self): mujoco = element.RootElement(model='test') mujoco.default.dclass = 'main' self._test_attributes(mujoco, recursive=True)
def _test_children(self, mjcf_element, recursive=False): children = mjcf_element.all_children() for child_name, child_spec in mjcf_element.spec.children.items(): if not (child_spec.repeated or child_spec.on_demand): self.assertHasAttr(mjcf_element, child_name) self.assertIn(child_name, dir(mjcf_element)) child = getattr(mjcf_element, child_name) self.assertIn(child, children) with self.assertRaisesRegex(AttributeError, 'can\'t set attribute'): setattr(mjcf_element, child_name, 'value') if recursive: self._test_children(child, recursive=True)
[docs] def testChildren(self): mujoco = element.RootElement(model='test') self._test_children(mujoco, recursive=True)
[docs] def testInvalidAttr(self): mujoco = element.RootElement(model='test') invalid_attrib_name = 'foobar' def test_invalid_attr_recursively(mjcf_element): self.assertNotHasAttr(mjcf_element, invalid_attrib_name) self.assertNotIn(invalid_attrib_name, dir(mjcf_element)) with self.assertRaisesRegex(AttributeError, 'object has no attribute'): getattr(mjcf_element, invalid_attrib_name) with self.assertRaisesRegex(AttributeError, 'can\'t set attribute'): setattr(mjcf_element, invalid_attrib_name, 'value') with self.assertRaisesRegex(AttributeError, 'object has no attribute'): delattr(mjcf_element, invalid_attrib_name) for child in mjcf_element.all_children(): test_invalid_attr_recursively(child) test_invalid_attr_recursively(mujoco)
[docs] def testAdd(self): mujoco = element.RootElement(model='test') # repeated elements body_foo_attributes = dict(name='foo', pos=[0, 1, 0], quat=[0, 1, 0, 0]) body_foo = mujoco.worldbody.add('body', **body_foo_attributes) self.assertEqual(body_foo.tag, 'body') joint_foo_attributes = dict(name='foo', type='free') joint_foo = body_foo.add('joint', **joint_foo_attributes) self.assertEqual(joint_foo.tag, 'joint') self._test_properties(body_foo, parent=mujoco.worldbody, root=mujoco) self._test_attributes(body_foo, expected_values=body_foo_attributes) self._test_children(body_foo) self._test_properties(joint_foo, parent=body_foo, root=mujoco) self._test_attributes(joint_foo, expected_values=joint_foo_attributes) self._test_children(joint_foo) # non-repeated, on-demand elements self.assertIsNone(body_foo.inertial) body_foo_inertial_attributes = dict(mass=1.0, pos=[0, 0, 0]) body_foo_inertial = body_foo.add('inertial', **body_foo_inertial_attributes) self._test_properties(body_foo_inertial, parent=body_foo, root=mujoco) self._test_attributes(body_foo_inertial, expected_values=body_foo_inertial_attributes) self._test_children(body_foo_inertial) with self.assertRaisesRegex(ValueError, '<inertial> child already exists'): body_foo.add('inertial', **body_foo_inertial_attributes) # non-repeated, non-on-demand elements with self.assertRaisesRegex(ValueError, '<compiler> child already exists'): mujoco.add('compiler') self.assertIsNotNone(mujoco.compiler) with self.assertRaisesRegex(ValueError, '<default> child already exists'): mujoco.add('default') self.assertIsNotNone(mujoco.default)
[docs] def testInsert(self): mujoco = element.RootElement(model='test') # add in order mujoco.worldbody.add('body', name='0') mujoco.worldbody.add('body', name='1') mujoco.worldbody.add('body', name='2') # insert into position 0, check order mujoco.worldbody.insert('body', name='foo', position=0) expected_order = ['foo', '0', '1', '2'] for i, child in enumerate(mujoco.worldbody._children): self.assertEqual(child.name, expected_order[i]) # insert into position -1, check order mujoco.worldbody.insert('body', name='bar', position=-1) expected_order = ['foo', '0', '1', 'bar', '2'] for i, child in enumerate(mujoco.worldbody._children): self.assertEqual(child.name, expected_order[i])
[docs] def testAddWithInvalidAttribute(self): mujoco = element.RootElement(model='test') with self.assertRaisesRegex(AttributeError, 'not a valid attribute'): mujoco.worldbody.add('body', name='foo', invalid_attribute='some_value') self.assertFalse(mujoco.worldbody.body) self.assertIsNone(mujoco.worldbody.find('body', 'foo'))
[docs] def testSameness(self): mujoco = element.RootElement(model='test') body_1 = mujoco.worldbody.add('body', pos=[0, 1, 2], quat=[0, 1, 0, 1]) site_1 = body_1.add('site', pos=[0, 1, 2], quat=[0, 1, 0, 1]) geom_1 = body_1.add('geom', pos=[0, 1, 2], quat=[0, 1, 0, 1]) for elem in (body_1, site_1, geom_1): self.assertIsSame(elem, elem) # strict ordering NOT required: adding geom and site is different order body_2 = mujoco.worldbody.add('body', pos=[0, 1, 2], quat=[0, 1, 0, 1]) geom_2 = body_2.add('geom', pos=[0, 1, 2], quat=[0, 1, 0, 1]) site_2 = body_2.add('site', pos=[0, 1, 2], quat=[0, 1, 0, 1]) elems_1 = (body_1, site_1, geom_1) elems_2 = (body_2, site_2, geom_2) for i, j in itertools.product(range(len(elems_1)), range(len(elems_2))): if i == j: self.assertIsSame(elems_1[i], elems_2[j]) else: self.assertIsNotSame(elems_1[i], elems_2[j]) # on-demand child body_1.add('inertial', pos=[0, 0, 0], mass=1) self.assertIsNotSame(body_1, body_2) body_2.add('inertial', pos=[0, 0, 0], mass=1) self.assertIsSame(body_1, body_2) # different number of children subbody_1 = body_1.add('body', pos=[0, 0, 1]) self.assertIsNotSame(body_1, body_2) # attribute mismatch subbody_2 = body_2.add('body') self.assertIsNotSame(subbody_1, subbody_2) self.assertIsNotSame(body_1, body_2) subbody_2.pos = [0, 0, 1] self.assertIsSame(subbody_1, subbody_2) self.assertIsSame(body_1, body_2) # grandchild attribute mismatch subbody_1.add('joint', type='hinge') subbody_2.add('joint', type='ball') self.assertIsNotSame(body_1, body_2)
[docs] def testTendonSameness(self): mujoco = element.RootElement(model='test') spatial_1 = mujoco.tendon.add('spatial') spatial_1.add('site', site='foo') spatial_1.add('geom', geom='bar') spatial_2 = mujoco.tendon.add('spatial') spatial_2.add('site', site='foo') spatial_2.add('geom', geom='bar') self.assertIsSame(spatial_1, spatial_2) # strict ordering is required spatial_3 = mujoco.tendon.add('spatial') spatial_3.add('site', site='foo') spatial_3.add('geom', geom='bar') spatial_4 = mujoco.tendon.add('spatial') spatial_4.add('geom', geom='bar') spatial_4.add('site', site='foo') self.assertIsNotSame(spatial_3, spatial_4)
[docs] def testCopy(self): mujoco = parser.from_path(_TEST_MODEL_XML) self.assertIsSame(mujoco, mujoco) copy_mujoco = copy.copy(mujoco) copy_mujoco.model = 'copied_model' self.assertIsSame(copy_mujoco, mujoco) self.assertNotEqual(copy_mujoco, mujoco) deepcopy_mujoco = copy.deepcopy(mujoco) deepcopy_mujoco.model = 'deepcopied_model' self.assertIsSame(deepcopy_mujoco, mujoco) self.assertNotEqual(deepcopy_mujoco, mujoco) self.assertIsSame(deepcopy_mujoco, copy_mujoco) self.assertNotEqual(deepcopy_mujoco, copy_mujoco)
[docs] def testWorldBodyFullIdentifier(self): mujoco = parser.from_path(_TEST_MODEL_XML) mujoco.model = 'model' self.assertEqual(mujoco.worldbody.full_identifier, 'world') submujoco = copy.copy(mujoco) submujoco.model = 'submodel' self.assertEqual(submujoco.worldbody.full_identifier, 'world') mujoco.attach(submujoco) self.assertEqual(mujoco.worldbody.full_identifier, 'world') self.assertEqual(submujoco.worldbody.full_identifier, 'submodel/') self.assertNotIn('name', mujoco.worldbody.to_xml_string(self_only=True)) self.assertNotIn('name', submujoco.worldbody.to_xml_string(self_only=True))
[docs] def testAttach(self): mujoco = parser.from_path(_TEST_MODEL_XML) mujoco.model = 'model' submujoco = copy.copy(mujoco) submujoco.model = 'submodel' subsubmujoco = copy.copy(mujoco) subsubmujoco.model = 'subsubmodel' with self.assertRaisesRegex(ValueError, 'Cannot merge a model to itself'): mujoco.attach(mujoco) attachment_site = submujoco.find('site', 'attachment') attachment_site.attach(subsubmujoco) subsubmodel_frame = submujoco.find('attachment_frame', 'subsubmodel') for attribute_name in ('pos', 'axisangle', 'xyaxes', 'zaxis', 'euler'): np.testing.assert_array_equal( getattr(subsubmodel_frame, attribute_name), getattr(attachment_site, attribute_name)) self._test_properties(subsubmodel_frame, parent=attachment_site.parent, root=submujoco) self.assertEqual( subsubmodel_frame.to_xml_string().split('\n')[0], '<body ' 'pos="0.10000000000000001 0.10000000000000001 0.10000000000000001" ' 'quat="0 1 0 0" ' 'name="subsubmodel/">') self.assertEqual( subsubmodel_frame.to_xml_string(precision=5).split('\n')[0], '<body ' 'pos="0.1 0.1 0.1" ' 'quat="0 1 0 0" ' 'name="subsubmodel/">') self.assertEqual(subsubmodel_frame.all_children(), subsubmujoco.worldbody.all_children()) with self.assertRaisesRegex(ValueError, 'already attached elsewhere'): mujoco.attach(subsubmujoco) with self.assertRaisesRegex(ValueError, 'Expected a mjcf.RootElement'): mujoco.attach(submujoco.contact) submujoco.option.flag.gravity = 'enable' with self.assertRaisesRegex( ValueError, 'Conflicting values for attribute `gravity`'): mujoco.attach(submujoco) submujoco.option.flag.gravity = 'disable' mujoco.attach(submujoco) self.assertEqual(subsubmujoco.parent_model, submujoco) self.assertEqual(submujoco.parent_model, mujoco) self.assertEqual(subsubmujoco.root_model, mujoco) self.assertEqual(submujoco.root_model, mujoco) self.assertEqual(submujoco.full_identifier, 'submodel/') self.assertEqual(subsubmujoco.full_identifier, 'submodel/subsubmodel/') merged_children = ('contact', 'actuator') for child_name in merged_children: for grandchild in getattr(submujoco, child_name).all_children(): self.assertIn(grandchild, getattr(mujoco, child_name).all_children()) for grandchild in getattr(subsubmujoco, child_name).all_children(): self.assertIn(grandchild, getattr(mujoco, child_name).all_children()) self.assertIn(grandchild, getattr(submujoco, child_name).all_children()) base_contact_content = ( '<exclude name="{0}exclude" body1="{0}b_0" body2="{0}b_1"/>') self.assertEqual( mujoco.contact.to_xml_string(pretty_print=False), '<contact>' + base_contact_content.format('') + base_contact_content.format('submodel/') + base_contact_content.format('submodel/subsubmodel/') + '</contact>') actuators_template = ( '<velocity name="{1}b_0_0" class="{0}" joint="{1}b_0_0"/>' '<velocity name="{1}b_1_0" class="{0}" joint="{1}b_1_0"/>') self.assertEqual( mujoco.actuator.to_xml_string(pretty_print=False), '<actuator>' + actuators_template.format('/', '') + actuators_template.format('submodel/', 'submodel/') + actuators_template.format('submodel/subsubmodel/', 'submodel/subsubmodel/') + '</actuator>') self.assertEqual(mujoco.default.full_identifier, '/') self.assertEqual(mujoco.default.default[0].full_identifier, 'big_and_green') self.assertEqual(submujoco.default.full_identifier, 'submodel/') self.assertEqual(submujoco.default.default[0].full_identifier, 'submodel/big_and_green') self.assertEqual(subsubmujoco.default.full_identifier, 'submodel/subsubmodel/') self.assertEqual(subsubmujoco.default.default[0].full_identifier, 'submodel/subsubmodel/big_and_green') default_xml_lines = (mujoco.default.to_xml_string(pretty_print=False) .replace('><', '>><<').split('><')) self.assertEqual(default_xml_lines[0], '<default>') self.assertEqual(default_xml_lines[1], '<default class="/">') self.assertEqual(default_xml_lines[4], '<default class="big_and_green">') self.assertEqual(default_xml_lines[6], '</default>') self.assertEqual(default_xml_lines[7], '</default>') self.assertEqual(default_xml_lines[8], '<default class="submodel/">') self.assertEqual(default_xml_lines[11], '<default class="submodel/big_and_green">') self.assertEqual(default_xml_lines[13], '</default>') self.assertEqual(default_xml_lines[14], '</default>') self.assertEqual(default_xml_lines[15], '<default class="submodel/subsubmodel/">') self.assertEqual(default_xml_lines[18], '<default class="submodel/subsubmodel/big_and_green">') self.assertEqual(default_xml_lines[-3], '</default>') self.assertEqual(default_xml_lines[-2], '</default>') self.assertEqual(default_xml_lines[-1], '</default>')
[docs] def testDetach(self): root = parser.from_path(_TEST_MODEL_XML) root.model = 'model' submodel = copy.copy(root) submodel.model = 'submodel' unattached_xml_1 = root.to_xml_string() root.attach(submodel) attached_xml_1 = root.to_xml_string() submodel.detach() unattached_xml_2 = root.to_xml_string() root.attach(submodel) attached_xml_2 = root.to_xml_string() self.assertEqual(unattached_xml_1, unattached_xml_2) self.assertEqual(attached_xml_1, attached_xml_2)
[docs] def testRenameAttachedModel(self): root = parser.from_path(_TEST_MODEL_XML) root.model = 'model' submodel = copy.copy(root) submodel.model = 'submodel' geom = submodel.worldbody.add( 'geom', name='geom', type='sphere', size=[0.1]) frame = root.attach(submodel) submodel.model = 'renamed' self.assertEqual(frame.full_identifier, 'renamed/') self.assertIsSame(root.find('geom', 'renamed/geom'), geom)
[docs] def testAttachmentFrames(self): mujoco = parser.from_path(_TEST_MODEL_XML) mujoco.model = 'model' submujoco = copy.copy(mujoco) submujoco.model = 'submodel' subsubmujoco = copy.copy(mujoco) subsubmujoco.model = 'subsubmodel' attachment_site = submujoco.find('site', 'attachment') attachment_site.attach(subsubmujoco) mujoco.attach(submujoco) # attachments directly on worldbody can have a <freejoint> submujoco_frame = mujoco.find('attachment_frame', 'submodel') self.assertStartsWith(submujoco_frame.to_xml_string(pretty_print=False), '<body name="submodel/">') self.assertEqual(submujoco_frame.full_identifier, 'submodel/') free_joint = submujoco_frame.add('freejoint') self.assertEqual(free_joint.to_xml_string(pretty_print=False), '<freejoint name="submodel/"/>') self.assertEqual(free_joint.full_identifier, 'submodel/') # attachments elsewhere cannot have a <freejoint> subsubmujoco_frame = submujoco.find('attachment_frame', 'subsubmodel') subsubmujoco_frame_xml = subsubmujoco_frame.to_xml_string( pretty_print=False, prefix_root=mujoco.namescope) self.assertStartsWith( subsubmujoco_frame_xml, '<body ' 'pos="0.10000000000000001 0.10000000000000001 0.10000000000000001" ' 'quat="0 1 0 0" ' 'name="submodel/subsubmodel/">') self.assertEqual(subsubmujoco_frame.full_identifier, 'submodel/subsubmodel/') with self.assertRaisesRegex(AttributeError, 'not a valid child'): subsubmujoco_frame.add('freejoint') hinge_joint = subsubmujoco_frame.add('joint', type='hinge', axis=[1, 2, 3]) hinge_joint_xml = hinge_joint.to_xml_string( pretty_print=False, prefix_root=mujoco.namescope) self.assertEqual( hinge_joint_xml, '<joint class="submodel/" type="hinge" axis="1 2 3" ' 'name="submodel/subsubmodel/"/>') self.assertEqual(hinge_joint.full_identifier, 'submodel/subsubmodel/')
[docs] def testDuplicateAttachmentFrameJointIdentifiers(self): mujoco = parser.from_path(_TEST_MODEL_XML) mujoco.model = 'model' submujoco_1 = copy.copy(mujoco) submujoco_1.model = 'submodel_1' submujoco_2 = copy.copy(mujoco) submujoco_2.model = 'submodel_2' frame_1 = mujoco.attach(submujoco_1) frame_2 = mujoco.attach(submujoco_2) joint_1 = frame_1.add('joint', type='slide', name='root_x', axis=[1, 0, 0]) joint_2 = frame_2.add('joint', type='slide', name='root_x', axis=[1, 0, 0]) self.assertEqual(joint_1.full_identifier, 'submodel_1/root_x/') self.assertEqual(joint_2.full_identifier, 'submodel_2/root_x/')
[docs] def testAttachmentFrameReference(self): root_1 = mjcf.RootElement('model_1') root_2 = mjcf.RootElement('model_2') root_2_frame = root_1.attach(root_2) sensor = root_1.sensor.add( 'framelinacc', name='root_2', objtype='body', objname=root_2_frame) self.assertEqual( sensor.to_xml_string(pretty_print=False), '<framelinacc name="root_2" objtype="body" objname="model_2/"/>')
[docs] def testAttachmentFrameChildReference(self): root_1 = mjcf.RootElement('model_1') root_2 = mjcf.RootElement('model_2') root_2_frame = root_1.attach(root_2) root_2_joint = root_2_frame.add( 'joint', name='root_x', type='slide', axis=[1, 0, 0]) actuator = root_1.actuator.add( 'position', name='root_x', joint=root_2_joint) self.assertEqual( actuator.to_xml_string(pretty_print=False), '<position name="root_x" class="/" joint="model_2/root_x/"/>')
[docs] def testDeletion(self): mujoco = parser.from_path(_TEST_MODEL_XML) mujoco.model = 'model' submujoco = copy.copy(mujoco) submujoco.model = 'submodel' subsubmujoco = copy.copy(mujoco) subsubmujoco.model = 'subsubmodel' submujoco.find('site', 'attachment').attach(subsubmujoco) mujoco.attach(submujoco) with self.assertRaisesRegex( ValueError, r'use remove\(affect_attachments=True\)'): del mujoco.option mujoco.option.remove(affect_attachments=True) for root in (mujoco, submujoco, subsubmujoco): self.assertIsNotNone(root.option.flag) self.assertEqual( root.option.to_xml_string(pretty_print=False), '<option/>') self.assertIsNotNone(root.option.flag) self.assertEqual( root.option.flag.to_xml_string(pretty_print=False), '<flag/>') with self.assertRaisesRegex( ValueError, r'use remove\(affect_attachments=True\)'): del mujoco.contact mujoco.contact.remove(affect_attachments=True) for root in (mujoco, submujoco, subsubmujoco): self.assertEqual( root.contact.to_xml_string(pretty_print=False), '<contact/>') b_0 = mujoco.find('body', 'b_0') b_0_inertial = b_0.inertial self.assertEqual(b_0_inertial.mass, 1) self.assertIsNotNone(b_0.inertial) del b_0.inertial self.assertIsNone(b_0.inertial)
[docs] def testRemoveElementWithRequiredAttribute(self): root = mjcf.RootElement() body = root.worldbody.add('body') # `objtype` is a required attribute. sensor = root.sensor.add('framepos', objtype='body', objname=body) self.assertIn(sensor, root.sensor.all_children()) sensor.remove() self.assertNotIn(sensor, root.sensor.all_children())
[docs] def testRemoveWithChildren(self): root = mjcf.RootElement() body = root.worldbody.add('body') subbodies = [] for _ in range(5): subbodies.append(body.add('body')) body.remove() for subbody in subbodies: self.assertTrue(subbody.is_removed)
[docs] def testFind(self): mujoco = parser.from_path(_TEST_MODEL_XML, resolve_references=False) mujoco.model = 'model' submujoco = copy.copy(mujoco) submujoco.model = 'submodel' subsubmujoco = copy.copy(mujoco) subsubmujoco.model = 'subsubmodel' submujoco.find('site', 'attachment').attach(subsubmujoco) mujoco.attach(submujoco) self.assertIsNotNone(mujoco.find('geom', 'b_0_0')) self.assertIsNotNone(mujoco.find('body', 'b_0').find('geom', 'b_0_0')) self.assertIsNone(mujoco.find('body', 'b_1').find('geom', 'b_0_0')) self.assertIsNone(mujoco.find('geom', 'nonexistent')) self.assertIsNone(mujoco.find('geom', 'nonexistent/b_0_0')) self.assertEqual(mujoco.find('geom', 'submodel/b_0_0'), submujoco.find('geom', 'b_0_0')) self.assertEqual(mujoco.find('geom', 'submodel/subsubmodel/b_0_0'), submujoco.find('geom', 'subsubmodel/b_0_0')) self.assertEqual(submujoco.find('geom', 'subsubmodel/b_0_0'), subsubmujoco.find('geom', 'b_0_0')) subsubmujoco.find('geom', 'b_0_0').name = 'foo' self.assertIsNone(mujoco.find('geom', 'submodel/subsubmodel/b_0_0')) self.assertIsNone(submujoco.find('geom', 'subsubmodel/b_0_0')) self.assertIsNone(subsubmujoco.find('geom', 'b_0_0')) self.assertEqual(mujoco.find('geom', 'submodel/subsubmodel/foo'), submujoco.find('geom', 'subsubmodel/foo')) self.assertEqual(submujoco.find('geom', 'subsubmodel/foo'), subsubmujoco.find('geom', 'foo')) self.assertEqual(mujoco.find('actuator', 'b_0_0').root, mujoco) self.assertEqual(mujoco.find('actuator', 'b_0_0').tag, 'velocity') self.assertEqual(mujoco.find('actuator', 'b_0_0').joint, 'b_0_0') self.assertEqual(mujoco.find('actuator', 'submodel/b_0_0').root, submujoco) self.assertEqual(mujoco.find('actuator', 'submodel/b_0_0').tag, 'velocity') self.assertEqual(mujoco.find('actuator', 'submodel/b_0_0').joint, 'b_0_0')
[docs] def testFindInvalidNamespace(self): mjcf_model = mjcf.RootElement() with self.assertRaisesRegex(ValueError, 'not a valid namespace'): mjcf_model.find('jiont', 'foo') with self.assertRaisesRegex(ValueError, 'not a valid namespace'): mjcf_model.find_all('goem')
[docs] def testEnterScope(self): mujoco = parser.from_path(_TEST_MODEL_XML, resolve_references=False) mujoco.model = 'model' self.assertIsNone(mujoco.enter_scope('submodel')) submujoco = copy.copy(mujoco) submujoco.model = 'submodel' subsubmujoco = copy.copy(mujoco) subsubmujoco.model = 'subsubmodel' submujoco.find('site', 'attachment').attach(subsubmujoco) mujoco.attach(submujoco) self.assertIsNotNone(mujoco.enter_scope('submodel')) self.assertEqual(mujoco.enter_scope('submodel').find('geom', 'b_0_0'), submujoco.find('geom', 'b_0_0')) self.assertEqual( mujoco.enter_scope('submodel/subsubmodel/').find('geom', 'b_0_0'), subsubmujoco.find('geom', 'b_0_0')) self.assertEqual(mujoco.enter_scope('submodel').enter_scope( 'subsubmodel').find('geom', 'b_0_0'), subsubmujoco.find('geom', 'b_0_0')) self.assertEqual( mujoco.enter_scope('submodel').find('actuator', 'b_0_0').root, submujoco) self.assertEqual( mujoco.enter_scope('submodel').find('actuator', 'b_0_0').tag, 'velocity') self.assertEqual( mujoco.enter_scope('submodel').find('actuator', 'b_0_0').joint, 'b_0_0')
[docs] def testDefaultIdentifier(self): mujoco = element.RootElement(model='test') body = mujoco.worldbody.add('body') joint_0 = body.add('freejoint') joint_1 = body.add('joint', type='hinge') self.assertIsNone(body.name) self.assertIsNone(joint_0.name) self.assertIsNone(joint_1.name) self.assertEqual(str(body), '<body>...</body>') self.assertEqual(str(joint_0), '<freejoint/>') self.assertEqual(str(joint_1), '<joint class="/" type="hinge"/>') self.assertEqual(body.full_identifier, '//unnamed_body_0') self.assertStartsWith(body.to_xml_string(pretty_print=False), '<body name="{:s}">'.format(body.full_identifier)) self.assertEqual(joint_0.full_identifier, '//unnamed_joint_0') self.assertEqual(joint_0.to_xml_string(pretty_print=False), '<freejoint name="{:s}"/>'.format(joint_0.full_identifier)) self.assertEqual(joint_1.full_identifier, '//unnamed_joint_1') self.assertEqual(joint_1.to_xml_string(pretty_print=False), '<joint name="{:s}" class="/" type="hinge"/>'.format( joint_1.full_identifier)) submujoco = copy.copy(mujoco) submujoco.model = 'submodel' mujoco.attach(submujoco) submujoco_body = submujoco.worldbody.body[0] self.assertEqual(submujoco_body.full_identifier, 'submodel//unnamed_body_0') self.assertEqual(submujoco_body.freejoint.full_identifier, 'submodel//unnamed_joint_0') self.assertEqual(submujoco_body.joint[0].full_identifier, 'submodel//unnamed_joint_1')
[docs] def testFindAll(self): mujoco = parser.from_path(_TEST_MODEL_XML) mujoco.model = 'model' submujoco = copy.copy(mujoco) submujoco.model = 'submodel' subsubmujoco = copy.copy(mujoco) subsubmujoco.model = 'subsubmodel' submujoco.find('site', 'attachment').attach(subsubmujoco) mujoco.attach(submujoco) geoms = mujoco.find_all('geom') self.assertLen(geoms, 6) self.assertEqual(geoms[0].root, mujoco) self.assertEqual(geoms[1].root, mujoco) self.assertEqual(geoms[2].root, submujoco) self.assertEqual(geoms[3].root, subsubmujoco) self.assertEqual(geoms[4].root, subsubmujoco) self.assertEqual(geoms[5].root, submujoco) b_0 = submujoco.find('body', 'b_0') self.assertLen(b_0.find_all('joint'), 6) self.assertLen(b_0.find_all('joint', immediate_children_only=True), 1) self.assertLen(b_0.find_all('joint', exclude_attachments=True), 2)
[docs] def testFindAllFrameJoints(self): root_model = parser.from_path(_TEST_MODEL_XML) root_model.model = 'model' submodel = copy.copy(root_model) submodel.model = 'submodel' frame = root_model.attach(submodel) joint_x = frame.add('joint', type='slide', axis=[1, 0, 0]) joint_y = frame.add('joint', type='slide', axis=[0, 1, 0]) joints = frame.find_all('joint', immediate_children_only=True) self.assertListEqual(joints, [joint_x, joint_y])
[docs] def testDictLikeInterface(self): mujoco = element.RootElement(model='test') elem = mujoco.worldbody.add('body') with self.assertRaisesRegex(TypeError, 'object is not subscriptable'): _ = elem['foo'] with self.assertRaisesRegex(TypeError, 'does not support item assignment'): elem['foo'] = 'bar' with self.assertRaisesRegex(TypeError, 'does not support item deletion'): del elem['foo']
[docs] def testSetAndGetAttributes(self): mujoco = element.RootElement(model='test') foo_attribs = dict(name='foo', pos=[1, 2, 3, 4], quat=[0, 1, 0, 0]) with self.assertRaisesRegex(ValueError, 'no more than 3 entries'): foo = mujoco.worldbody.add('body', **foo_attribs) # failed creationg should not cause the identifier 'foo' to be registered with self.assertRaises(KeyError): mujoco.namescope.get('body', 'foo') foo_attribs['pos'] = [1, 2, 3] foo = mujoco.worldbody.add('body', **foo_attribs) self._test_attributes(foo, expected_values=foo_attribs) foo_attribs['name'] = 'bar' foo_attribs['pos'] = [1, 2, 3, 4] foo_attribs['childclass'] = 'klass' with self.assertRaisesRegex(ValueError, 'no more than 3 entries'): foo.set_attributes(**foo_attribs) # failed assignment should not cause the identifier 'bar' to be registered with self.assertRaises(KeyError): mujoco.namescope.get('body', 'bar') foo_attribs['pos'] = [1, 2, 3] foo.set_attributes(**foo_attribs) self._test_attributes(foo, expected_values=foo_attribs) actual_foo_attribs = foo.get_attributes() for attribute_name, value in foo_attribs.items(): np.testing.assert_array_equal( actual_foo_attribs.pop(attribute_name), value) for value in actual_foo_attribs.values(): self.assertIsNone(value)
[docs] def testResolveReferences(self): resolved_model = parser.from_path(_TEST_MODEL_XML) self.assertIs( resolved_model.find('geom', 'b_1_0').material, resolved_model.find('material', 'mat_texture')) unresolved_model = parser.from_path( _TEST_MODEL_XML, resolve_references=False) self.assertEqual( unresolved_model.find('geom', 'b_1_0').material, 'mat_texture') unresolved_model.resolve_references() self.assertIs( unresolved_model.find('geom', 'b_1_0').material, unresolved_model.find('material', 'mat_texture'))
@parameterized.named_parameters( ('WithoutInclude', _TEST_MODEL_XML), ('WithInclude', _MODEL_WITH_INCLUDE_PATH)) def testParseFromString(self, model_path): with open(model_path) as xml_file: xml_string = xml_file.read() model_dir, _ = os.path.split(model_path) parser.from_xml_string(xml_string, model_dir=model_dir) @parameterized.named_parameters( ('WithoutInclude', _TEST_MODEL_XML), ('WithInclude', _MODEL_WITH_INCLUDE_PATH)) def testParseFromFile(self, model_path): model_dir, _ = os.path.split(model_path) with open(model_path) as xml_file: parser.from_file(xml_file, model_dir=model_dir) @parameterized.named_parameters( ('WithoutInclude', _TEST_MODEL_XML), ('WithInclude', _MODEL_WITH_INCLUDE_PATH)) def testParseFromPath(self, model_path): parser.from_path(model_path)
[docs] def testGetAssetFromFile(self): with open(_TEXTURE_PATH, 'rb') as f: contents = f.read() _, filename = os.path.split(_TEXTURE_PATH) prefix, extension = os.path.splitext(filename) vfs_filename = prefix + '-' + hashlib.sha1(contents).hexdigest() + extension mujoco = parser.from_path(_TEST_MODEL_XML) self.assertDictEqual({vfs_filename: contents}, mujoco.get_assets())
[docs] def testGetAssetFromPlaceholder(self): mujoco = parser.from_path(_TEST_MODEL_XML) # Add an extra texture asset from a placeholder. contents = b'I am a texture bytestring' extension = '.png' vfs_filename = hashlib.sha1(contents).hexdigest() + extension placeholder = mjcf.Asset(contents=contents, extension=extension) mujoco.asset.add('texture', name='fake_texture', file=placeholder) self.assertContainsSubset(set([(vfs_filename, contents)]), set(mujoco.get_assets().items()))
[docs] def testGetAssetsFromDict(self): with open(_MODEL_WITH_INVALID_FILENAMES, 'rb') as f: xml_string = f.read() with open(_TEXTURE_PATH, 'rb') as f: texture_contents = f.read() with open(_MESH_PATH, 'rb') as f: mesh_contents = f.read() with open(_INCLUDED_WITH_INVALID_FILENAMES, 'rb') as f: included_xml_contents = f.read() assets = { 'invalid_texture_name.png': texture_contents, 'invalid_mesh_name.stl': mesh_contents, 'invalid_included_name.xml': included_xml_contents, } # The paths specified in the main and included XML files are deliberately # invalid, so the parser should fail unless the pre-loaded assets are passed # in as a dict. with self.assertRaises(IOError): parser.from_xml_string(xml_string=xml_string) mujoco = parser.from_xml_string(xml_string=xml_string, assets=assets) expected_assets = {} for path, contents in assets.items(): _, filename = os.path.split(path) prefix, extension = os.path.splitext(filename) if extension != '.xml': vfs_filename = ''.join( [prefix, '-', hashlib.sha1(contents).hexdigest(), extension]) expected_assets[vfs_filename] = contents self.assertDictEqual(expected_assets, mujoco.get_assets())
[docs] def testAssetsCanBeCopied(self): with open(_TEXTURE_PATH, 'rb') as f: contents = f.read() _, filename = os.path.split(_TEXTURE_PATH) prefix, extension = os.path.splitext(filename) vfs_filename = prefix + '-' + hashlib.sha1(contents).hexdigest() + extension mujoco = parser.from_path(_TEST_MODEL_XML) mujoco_copy = copy.copy(mujoco) expected = {vfs_filename: contents} self.assertDictEqual(expected, mujoco.get_assets()) self.assertDictEqual(expected, mujoco_copy.get_assets())
[docs] def testParseModelWithNamelessAssets(self): mujoco = parser.from_path(path=_MODEL_WITH_NAMELESS_ASSETS) expected_names_derived_from_filenames = [ ('mesh', 'cube'), # ./test_assets/meshes/cube.stl ('texture', 'deepmind'), # ./test_assets/textures/deepmind.png ('hfield', 'deepmind'), # ./test_assets/textures/deepmind.png ] with self.subTest('Expected asset names are present in the parsed model'): for namespace, name in expected_names_derived_from_filenames: self.assertIsNotNone(mujoco.find(namespace, name)) with self.subTest('Can compile and step the simulation'): physics = mjcf.Physics.from_mjcf_model(mujoco) physics.step()
[docs] def testAssetInheritance(self): parent = element.RootElement(model='parent') child = element.RootElement(model='child') grandchild = element.RootElement(model='grandchild') ext = '.png' parent_str = b'I belong to the parent' child_str = b'I belong to the child' grandchild_str = b'I belong to the grandchild' parent_vfs_name, child_vfs_name, grandchild_vfs_name = ( hashlib.sha1(s).hexdigest() + ext for s in (parent_str, child_str, grandchild_str)) parent_ph = mjcf.Asset(contents=parent_str, extension=ext) child_ph = mjcf.Asset(contents=child_str, extension=ext) grandchild_ph = mjcf.Asset(contents=grandchild_str, extension=ext) parent.asset.add('texture', name='parent_tex', file=parent_ph) child.asset.add('texture', name='child_tex', file=child_ph) grandchild.asset.add('texture', name='grandchild_tex', file=grandchild_ph) parent.attach(child) child.attach(grandchild) # The grandchild should only return its own assets. self.assertDictEqual( {grandchild_vfs_name: grandchild_str}, grandchild.get_assets()) # The child should return its own assets plus those of the grandchild. self.assertDictEqual( {child_vfs_name: child_str, grandchild_vfs_name: grandchild_str}, child.get_assets()) # The parent should return everything. self.assertDictEqual( {parent_vfs_name: parent_str, child_vfs_name: child_str, grandchild_vfs_name: grandchild_str}, parent.get_assets())
[docs] def testActuatorReordering(self): def make_model_with_mixed_actuators(name): actuators = [] root = mjcf.RootElement(model=name) body = root.worldbody.add('body') body.add('geom', type='sphere', size=[0.1]) slider = body.add('joint', type='slide', name='slide_joint') # Third-order `general` actuator. actuators.append( root.actuator.add( 'general', dyntype='integrator', biastype='affine', dynprm=[1, 0, 0], joint=slider, name='general_act')) # Cylinder actuators are also third-order. actuators.append( root.actuator.add('cylinder', joint=slider, name='cylinder_act')) # A second-order actuator, added after the third-order actuators. actuators.append( root.actuator.add('velocity', joint=slider, name='velocity_act')) return root, actuators child_1, actuators_1 = make_model_with_mixed_actuators(name='child_1') child_2, actuators_2 = make_model_with_mixed_actuators(name='child_2') child_3, actuators_3 = make_model_with_mixed_actuators(name='child_3') parent = mjcf.RootElement() parent.attach(child_1) parent.attach(child_2) child_2.attach(child_3) # Check that the generated XML contains all of the actuators that we expect # it to have. expected_xml_strings = [ actuator.to_xml_string(prefix_root=parent.namescope) for actuator in actuators_1 + actuators_2 + actuators_3 ] xml_strings = [ util.to_native_string(etree.tostring(node, pretty_print=True)) for node in parent.to_xml().find('actuator').getchildren() ] self.assertSameElements(expected_xml_strings, xml_strings) # MuJoCo requires that all 3rd-order actuators (i.e. those with internal # dynamics) come after all 2nd-order actuators in the XML. Attempting to # compile this model will result in an error unless PyMJCF internally # reorders the actuators so that the 3rd-order actuator comes last in the # generated XML. _ = mjcf.Physics.from_mjcf_model(child_1) # Actuator re-ordering should also work in cases where there are multiple # attached submodels with mixed 2nd- and 3rd-order actuators. _ = mjcf.Physics.from_mjcf_model(parent)
[docs] def testMaxConflictingValues(self): model_1 = mjcf.RootElement() model_1.size.nconmax = 123 model_1.size.njmax = 456 model_2 = mjcf.RootElement() model_2.size.nconmax = 345 model_2.size.njmax = 234 model_1.attach(model_2) self.assertEqual(model_1.size.nconmax, 345) self.assertEqual(model_1.size.njmax, 456)
[docs] def testMaxBytesConflictingValues(self): model_1 = mjcf.RootElement() model_1.size.memory = '10000' model_2 = mjcf.RootElement() model_2.size.memory = '1M' model_1.attach(model_2) self.assertEqual(model_1.size.memory, '1048576')
if __name__ == '__main__': absltest.main()