# 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 from absl.testing import absltest from absl.testing import parameterized from dm_control import mjcf from dm_control.mjcf import element from dm_control.mjcf import namescope from dm_control.mjcf import parser from dm_control.mujoco.wrapper import util import lxml import numpy as np 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') class ElementTest(parameterized.TestCase): def assertIsSame(self, mjcf_model, other): self.assertTrue(mjcf_model.is_same_as(other)) self.assertTrue(other.is_same_as(mjcf_model)) def assertIsNotSame(self, mjcf_model, other): self.assertFalse(mjcf_model.is_same_as(other)) self.assertFalse(other.is_same_as(mjcf_model)) def assertHasAttr(self, obj, attrib): self.assertTrue(hasattr(obj, attrib)) 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) 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') 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) 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) def testChildren(self): mujoco = element.RootElement(model='test') self._test_children(mujoco, recursive=True) 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) 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, ' child already exists'): body_foo.add('inertial', **body_foo_inertial_attributes) # non-repeated, non-on-demand elements with self.assertRaisesRegex(ValueError, ' child already exists'): mujoco.add('compiler') self.assertIsNotNone(mujoco.compiler) with self.assertRaisesRegex(ValueError, ' child already exists'): mujoco.add('default') self.assertIsNotNone(mujoco.default) 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]) 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')) 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) 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) 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) 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)) 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], '') self.assertEqual( subsubmodel_frame.to_xml_string(precision=5).split('\n')[0], '') 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 = ( '') self.assertEqual( mujoco.contact.to_xml_string(pretty_print=False), '' + base_contact_content.format('') + base_contact_content.format('submodel/') + base_contact_content.format('submodel/subsubmodel/') + '') actuators_template = ( '' '') self.assertEqual( mujoco.actuator.to_xml_string(pretty_print=False), '' + actuators_template.format('/', '') + actuators_template.format('submodel/', 'submodel/') + actuators_template.format('submodel/subsubmodel/', 'submodel/subsubmodel/') + '') 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], '') self.assertEqual(default_xml_lines[1], '') self.assertEqual(default_xml_lines[4], '') self.assertEqual(default_xml_lines[6], '') self.assertEqual(default_xml_lines[7], '') self.assertEqual(default_xml_lines[8], '') self.assertEqual(default_xml_lines[11], '') self.assertEqual(default_xml_lines[13], '') self.assertEqual(default_xml_lines[14], '') self.assertEqual(default_xml_lines[15], '') self.assertEqual(default_xml_lines[18], '') self.assertEqual(default_xml_lines[-3], '') self.assertEqual(default_xml_lines[-2], '') self.assertEqual(default_xml_lines[-1], '') 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) 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) 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 submujoco_frame = mujoco.find('attachment_frame', 'submodel') self.assertStartsWith(submujoco_frame.to_xml_string(pretty_print=False), '') self.assertEqual(submujoco_frame.full_identifier, 'submodel/') free_joint = submujoco_frame.add('freejoint') self.assertEqual(free_joint.to_xml_string(pretty_print=False), '') self.assertEqual(free_joint.full_identifier, 'submodel/') # attachments elsewhere cannot have a 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, '') 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, '') self.assertEqual(hinge_joint.full_identifier, 'submodel/subsubmodel/') 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/') 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), '') 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), '') 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), '