diff --git a/dm_control/autowrap/binding_generator.py b/dm_control/autowrap/binding_generator.py index 370a543e..38c81459 100644 --- a/dm_control/autowrap/binding_generator.py +++ b/dm_control/autowrap/binding_generator.py @@ -159,6 +159,11 @@ def parse_hints(self, xmacro_src): parser = header_parsing.XMACRO for tokens, _, _ in parser.scanString(xmacro_src): for xmacro in tokens: + if not ( + xmacro.name.startswith("MJMODEL") + or xmacro.name.startswith("MJDATA") + ): + continue for member in xmacro.members: if not hasattr(member, "name") or not member.name: continue diff --git a/dm_control/blender/fake_core/bpy.py b/dm_control/blender/fake_core/bpy.py index 5ea814e3..46ddac6e 100644 --- a/dm_control/blender/fake_core/bpy.py +++ b/dm_control/blender/fake_core/bpy.py @@ -99,7 +99,7 @@ def select_set(self, _): def to_mesh(self): pass - def evaluated_get(self, _) -> types.Object: + def evaluated_get(self, _) -> types.Object: # pyrefly: ignore[bad-return] pass @property @@ -137,7 +137,7 @@ class bpy_struct: class Context: @property - def scene(self) -> types.Scene: + def scene(self) -> types.Scene: # pyrefly: ignore[bad-return] pass class Light: diff --git a/dm_control/blender/fake_core/mathutils.py b/dm_control/blender/fake_core/mathutils.py index a48c21e9..16f0c51b 100644 --- a/dm_control/blender/fake_core/mathutils.py +++ b/dm_control/blender/fake_core/mathutils.py @@ -67,7 +67,7 @@ def w(self) -> float: def __eq__(self, rhs) -> bool: if not isinstance(rhs, Vector): return False - return np.linalg.norm(self._coords - rhs._coords) < 1e-6 + return np.linalg.norm(self._coords - rhs._coords) < 1e-6 # pyrefly: ignore[bad-return] def __str__(self) -> str: return 'Vector({:.2f}, {:.2f}, {:.2f})'.format(self.x, self.y, self.z) diff --git a/dm_control/blender/mujoco_exporter/blender_scene.py b/dm_control/blender/mujoco_exporter/blender_scene.py index 3004fdfe..ca50a537 100644 --- a/dm_control/blender/mujoco_exporter/blender_scene.py +++ b/dm_control/blender/mujoco_exporter/blender_scene.py @@ -122,8 +122,8 @@ def is_none(self) -> bool: @property def is_armature(self) -> bool: - return ( # pytype: disable=bad-return-type - self.native_obj + return ( + self.native_obj # pyrefly: ignore[bad-return] and self.native_obj.type == _ARMATURE and not self.native_bone ) @@ -138,19 +138,19 @@ def is_bone(self) -> bool: @property def is_mesh(self) -> bool: - return self.native_obj and self.native_obj.type == _MESH # pytype: disable=bad-return-type + return self.native_obj and self.native_obj.type == _MESH # pyrefly: ignore[bad-return] @property def is_light(self) -> bool: - return self.native_obj and self.native_obj.type == _LIGHT # pytype: disable=bad-return-type + return self.native_obj and self.native_obj.type == _LIGHT # pyrefly: ignore[bad-return] @property def is_camera(self) -> bool: - return self.native_obj and self.native_obj.type == _CAMERA # pytype: disable=bad-return-type + return self.native_obj and self.native_obj.type == _CAMERA # pyrefly: ignore[bad-return] @property def is_empty(self) -> bool: - return self.native_obj and self.native_obj.type == _EMPTY # pytype: disable=bad-return-type + return self.native_obj and self.native_obj.type == _EMPTY # pyrefly: ignore[bad-return] @property def name(self) -> str: @@ -171,7 +171,7 @@ def get_bone_local_mtx(bone: bpy.types.Bone) -> mathutils.Matrix: """Derives a local matrix of an armature bone.""" assert isinstance(bone, bpy.types.Bone) if bone.parent: - return bone.parent.matrix_local.inverted() @ bone.matrix_local + return bone.parent.matrix_local.inverted() @ bone.matrix_local # pyrefly: ignore[missing-attribute] else: return bone.matrix_local @@ -179,7 +179,7 @@ def get_object_local_mtx(obj: bpy.types.Object) -> mathutils.Matrix: """Derives a local matrix of an object, such as a mesh or armature.""" assert isinstance(obj, bpy.types.Object) if obj.parent: - local_mtx = obj.parent.matrix_world.inverted() @ obj.matrix_world + local_mtx = obj.parent.matrix_world.inverted() @ obj.matrix_world # pyrefly: ignore[missing-attribute] if obj.parent_bone: assert obj.parent.type == _ARMATURE armature = obj.parent @@ -191,9 +191,9 @@ def get_object_local_mtx(obj: bpy.types.Object) -> mathutils.Matrix: return obj.matrix_world if self.is_bone: - local_mtx = get_bone_local_mtx(self.native_bone) + local_mtx = get_bone_local_mtx(self.native_bone) # pyrefly: ignore[bad-argument-type] else: - local_mtx = get_object_local_mtx(self.native_obj) + local_mtx = get_object_local_mtx(self.native_obj) # pyrefly: ignore[bad-argument-type] rot_quat = local_mtx.to_quaternion() pos = local_mtx.translation @@ -373,8 +373,8 @@ def map_blender_tree( """Returns a list of scene objects in the breadth-first order.""" # Collect all nodes to explore - objects and bones alike assert context.scene is not None - to_explore = [ObjectRef.new_object(o) for o in context.scene.objects] - armatures = [o for o in context.scene.objects if o.type == 'ARMATURE'] + to_explore = [ObjectRef.new_object(o) for o in context.scene.objects] # pyrefly: ignore[missing-attribute] + armatures = [o for o in context.scene.objects if o.type == 'ARMATURE'] # pyrefly: ignore[missing-attribute] for armature in armatures: for bone in armature.data.bones: to_explore.append(ObjectRef.new_bone(armature, bone)) diff --git a/dm_control/blender/mujoco_exporter/mujoco_assets.py b/dm_control/blender/mujoco_exporter/mujoco_assets.py index 86b45d20..6c96453a 100644 --- a/dm_control/blender/mujoco_exporter/mujoco_assets.py +++ b/dm_control/blender/mujoco_exporter/mujoco_assets.py @@ -102,7 +102,7 @@ def mesh_asset_builder( """Exports a mesh associated with the object and creates an 'asset' node.""" mat_names = blender_scene.map_materials(lambda m: m.name, materials) twosidedness = blender_scene.map_materials( - lambda m: not m.use_backface_culling, materials + lambda m: not m.use_backface_culling, materials # pyrefly: ignore[missing-attribute] ) mat_names = mat_names or [''] twosidedness = twosidedness or [False] diff --git a/dm_control/blender/mujoco_exporter/mujoco_scene.py b/dm_control/blender/mujoco_exporter/mujoco_scene.py index c2d75549..e633492c 100644 --- a/dm_control/blender/mujoco_exporter/mujoco_scene.py +++ b/dm_control/blender/mujoco_exporter/mujoco_scene.py @@ -74,7 +74,7 @@ def light_builder( el.setAttribute('pos', vec_to_mjcf(transform.pos)) el.setAttribute('dir', vec_to_mjcf(transform.rot @ _OZ)) el.setAttribute('directional', directional) - el.setAttribute('castshadow', bool_to_mjcf(light.use_shadow)) # pytype: disable=wrong-arg-types + el.setAttribute('castshadow', bool_to_mjcf(light.use_shadow)) el.setAttribute('diffuse', color_to_mjcf(light.color)) el.setAttribute('attenuation', attenuation) return el diff --git a/dm_control/blender/mujoco_exporter/testing.py b/dm_control/blender/mujoco_exporter/testing.py index c325c38a..7df4014f 100644 --- a/dm_control/blender/mujoco_exporter/testing.py +++ b/dm_control/blender/mujoco_exporter/testing.py @@ -70,7 +70,7 @@ def build_armature( parent: Optional[Any] = None, parent_bone: Optional[str] = None, bones: Optional[Any] = None, -) -> ...: +): """TBD.""" obj = mock.MagicMock(spec=bpy.types.Object, type='ARMATURE') obj.name = name @@ -154,7 +154,8 @@ def build_mesh_object( name: str, mesh: Optional[Any] = None, parent: Optional[Any] = None, - parent_bone: Optional[str] = None) -> ...: + parent_bone: Optional[str] = None, +): """Builds a mock object with a mesh assigned to it.""" obj = mock.MagicMock(spec=bpy.types.Object, type='MESH') obj.name = name diff --git a/dm_control/composer/initializers/tcp_initializer_test.py b/dm_control/composer/initializers/tcp_initializer_test.py index 6571b7af..002ac8f6 100644 --- a/dm_control/composer/initializers/tcp_initializer_test.py +++ b/dm_control/composer/initializers/tcp_initializer_test.py @@ -53,10 +53,10 @@ def assertEntitiesInContact(self, physics, first, second): second_geom_ids = physics.bind( second.mjcf_model.find_all('geom')).element_id contact = physics.data.contact - first_to_second = (np.in1d(contact.geom1, first_geom_ids) & - np.in1d(contact.geom2, second_geom_ids)) - second_to_first = (np.in1d(contact.geom1, second_geom_ids) & - np.in1d(contact.geom2, first_geom_ids)) + first_to_second = (np.isin(contact.geom1, first_geom_ids).ravel() & + np.isin(contact.geom2, second_geom_ids).ravel()) + second_to_first = (np.isin(contact.geom1, second_geom_ids).ravel() & + np.isin(contact.geom2, first_geom_ids).ravel()) touching = contact.dist <= 0 valid_contact = touching & (first_to_second | second_to_first) self.assertTrue(np.any(valid_contact), msg='Entities are not in contact.') diff --git a/dm_control/entities/props/duplo/duplo2x4.xml b/dm_control/entities/props/duplo/duplo2x4.xml index 324502bc..77f5811e 100644 --- a/dm_control/entities/props/duplo/duplo2x4.xml +++ b/dm_control/entities/props/duplo/duplo2x4.xml @@ -1,6 +1,6 @@ diff --git a/dm_control/locomotion/mocap/trajectory.py b/dm_control/locomotion/mocap/trajectory.py index 6d070acd..c09739e1 100644 --- a/dm_control/locomotion/mocap/trajectory.py +++ b/dm_control/locomotion/mocap/trajectory.py @@ -118,7 +118,7 @@ def prop_field(timestep, i=i, field_name=field_name): self._create_all_items(self._dict) for k in self._dict: # make trajectory immutable by default - self._dict[k].flags.writeable = False # pytype: disable=attribute-error + self._dict[k].flags.writeable = False return {k: v[self._start_step:self._end_step] for k, v in self._dict.items()} @@ -130,8 +130,8 @@ def _create_single_item(self, get_field_in_timestep): values = get_field_in_timestep(timestep) if i == 0: array = np.empty((len(self._proto.timesteps),) + values.shape) - array[i, :] = values - return array + array[i, :] = values # pyrefly: ignore[unbound-name] + return array # pyrefly: ignore[unbound-name] def _create_all_items(self, dictionary): for key, value in dictionary.items(): diff --git a/dm_control/locomotion/mocap/walkers.py b/dm_control/locomotion/mocap/walkers.py index a5fdbab9..afa29bd2 100644 --- a/dm_control/locomotion/mocap/walkers.py +++ b/dm_control/locomotion/mocap/walkers.py @@ -92,6 +92,6 @@ def add_marker_sites(self, walker, size=0.01, rgba=(0., 0., 1., .3), pos = np.zeros(3) sites.append( body.add( - 'site', name=str(marker_info.name), pos=pos, dclass=mocap_class)) + 'site', name=str(marker_info.name), pos=pos, dclass=mocap_class)) # pyrefly: ignore[unbound-name] walker.list_of_site_names = [site.name for site in sites] return sites diff --git a/dm_control/locomotion/props/target_sphere.py b/dm_control/locomotion/props/target_sphere.py index 85ace41a..c42715ca 100644 --- a/dm_control/locomotion/props/target_sphere.py +++ b/dm_control/locomotion/props/target_sphere.py @@ -58,7 +58,7 @@ def _build(self, self._material = self._mjcf_root.asset.add( 'material', name='target_sphere', texture=self._texture) self._geom = self._mjcf_root.worldbody.add( - 'geom', type='sphere', name='geom', gap=2*radius, + 'geom', type='sphere', name='geom', margin=-2*radius, gap=2*radius, pos=[0, 0, height_above_ground], size=[radius], material=self._material) self._geom_id = -1 self._activated = False @@ -157,7 +157,7 @@ def _build(self, self._material = self._mjcf_root.asset.add( 'material', name='target_sphere_init', texture=self._texture_initial) self._geom = self._mjcf_root.worldbody.add( - 'geom', type='sphere', name='geom', gap=2*radius, + 'geom', type='sphere', name='geom', margin=-2*radius, gap=2*radius, pos=[0, 0, height_above_ground], size=[radius], material=self._material) self._geom_id = -1 diff --git a/dm_control/locomotion/soccer/camera.py b/dm_control/locomotion/soccer/camera.py index 6a3f1f85..4e6932a4 100644 --- a/dm_control/locomotion/soccer/camera.py +++ b/dm_control/locomotion/soccer/camera.py @@ -104,16 +104,16 @@ def _get_target_camera_pose(self, entity_positions): def initialize_episode(self, entity_positions): """Begin the episode with the camera set to its target pose.""" target_pose = self._get_target_camera_pose(entity_positions) - self._camera.set_pose(*target_pose) + self._camera.set_pose(*target_pose) # pyrefly: ignore[missing-attribute] def after_step(self, entity_positions): """Move camera toward its target poses.""" target_pose = self._get_target_camera_pose(entity_positions) - cur_pose = self._camera.get_pose() + cur_pose = self._camera.get_pose() # pyrefly: ignore[missing-attribute] smoothing_update_speed = self._smoothing_update_speed filtered_pose = [ target_val * smoothing_update_speed + \ current_val * (1 - smoothing_update_speed) for target_val, current_val in zip(target_pose, cur_pose) ] - self._camera.set_pose(*filtered_pose) + self._camera.set_pose(*filtered_pose) # pyrefly: ignore[missing-attribute] diff --git a/dm_control/locomotion/soccer/humanoid.py b/dm_control/locomotion/soccer/humanoid.py index e066b5b4..6ad70da1 100644 --- a/dm_control/locomotion/soccer/humanoid.py +++ b/dm_control/locomotion/soccer/humanoid.py @@ -108,7 +108,7 @@ class Visual(enum.Enum): GEOM = 1 JERSEY = 2 - def _build(self, # pytype: disable=signature-mismatch # overriding-parameter-count-checks + def _build(self, # pyrefly: ignore[bad-override] visual, marker_rgba, walker_id=None, @@ -167,12 +167,12 @@ def _build(self, # pytype: disable=signature-mismatch # overriding-parameter-c tracking_light.remove() if visual == Humanoid.Visual.JERSEY: - shirt_number = walker_id + 1 + shirt_number = walker_id + 1 # pyrefly: ignore[unsupported-operation] self._mjcf_root.asset.add( 'texture', name='skin', type='2d', - file=os.path.join(_ASSETS_PATH, f'{team}_{walker_id + 1:02d}.png')) + file=os.path.join(_ASSETS_PATH, f'{team}_{walker_id + 1:02d}.png')) # pyrefly: ignore[unbound-name, unsupported-operation] self._mjcf_root.asset.add('material', name='skin', texture='skin') self._mjcf_root.asset.add( 'skin', diff --git a/dm_control/locomotion/tasks/reference_pose/tracking.py b/dm_control/locomotion/tasks/reference_pose/tracking.py index 3c28287b..f6fd4eb9 100644 --- a/dm_control/locomotion/tasks/reference_pose/tracking.py +++ b/dm_control/locomotion/tasks/reference_pose/tracking.py @@ -81,7 +81,7 @@ def _strip_reference_prefix(dictionary: Mapping[Text, Any], The dictionary with the modified keys and original values (and unchanged keys specified by keep_prefixes). """ - keep_prefixes = keep_prefixes or [] + keep_prefixes = keep_prefixes or [] # pyrefly: ignore[bad-assignment] new_dictionary = dict() for key in dictionary: if key.startswith(prefix): @@ -89,7 +89,7 @@ def _strip_reference_prefix(dictionary: Mapping[Text, Any], # note that this will not copy the underlying array. new_dictionary[key_without_prefix] = dictionary[key] else: - for keep_prefix in keep_prefixes: + for keep_prefix in keep_prefixes: # pyrefly: ignore[not-iterable] if key.startswith(keep_prefix): new_dictionary[key] = dictionary[key] @@ -266,10 +266,10 @@ def __init__( self._reset_reward_channels() def _strip_reference_prefix(self): - self._clip_reference_features = _strip_reference_prefix( # pytype: disable=wrong-arg-types + self._clip_reference_features = _strip_reference_prefix( self._clip_reference_features, 'walker/', - keep_prefixes=self._prop_prefixes) + keep_prefixes=self._prop_prefixes) # pyrefly: ignore[bad-argument-type] positions = [] quaternions = [] @@ -315,7 +315,7 @@ def _load_reference_data(self, ref_path, proto_modifier, clip_id, start_step=clip_start_step, end_step=_MAX_END_STEP) for clip_id, clip_start_step in zip( - self._dataset.ids, self._dataset.start_steps) + self._dataset.ids, self._dataset.start_steps) # pyrefly: ignore[bad-argument-type] ] # infer clip end steps to set sampling distribution self._dataset.end_steps = tuple(clip.end_step for clip in self._all_clips) @@ -395,7 +395,7 @@ def _get_possible_starts(self): self._start_probabilities = [] dataset = self._dataset for clip_number, (start, end, weight) in enumerate( - zip(dataset.start_steps, dataset.end_steps, dataset.weights)): + zip(dataset.start_steps, dataset.end_steps, dataset.weights)): # pyrefly: ignore[bad-argument-type] # length - required lookahead - minimum number of steps last_possible_start = end - self._max_ref_step - self._min_steps @@ -458,8 +458,8 @@ def _get_clip_to_track(self, random_state: np.random.RandomState): logging.info('Loading clip %s', clip_id) self._all_clips[self._current_clip_index] = self._loader.get_trajectory( clip_id, - start_step=self._dataset.start_steps[self._current_clip_index], - end_step=self._dataset.end_steps[self._current_clip_index], + start_step=self._dataset.start_steps[self._current_clip_index], # pyrefly: ignore[unsupported-operation] + end_step=self._dataset.end_steps[self._current_clip_index], # pyrefly: ignore[unsupported-operation] zero_out_velocities=False) self._current_clip = self._all_clips[self._current_clip_index] self._clip_reference_features = self._current_clip.as_dict() @@ -469,9 +469,9 @@ def _get_clip_to_track(self, random_state: np.random.RandomState): # clip_start_step:clip_end_step. However start_step is in # [clip_start_step:clip_end_step]. Hence we subtract clip_start_step to # obtain a valid index for the reference features. - self._time_step = start_step - self._dataset.start_steps[ + self._time_step = start_step - self._dataset.start_steps[ # pyrefly: ignore[unsupported-operation] self._current_clip_index] - self._current_start_time = (start_step - self._dataset.start_steps[ + self._current_start_time = (start_step - self._dataset.start_steps[ # pyrefly: ignore[unsupported-operation] self._current_clip_index]) * self._current_clip.dt self._last_step = len( self._clip_reference_features['joints']) - self._max_ref_step - 1 @@ -488,7 +488,7 @@ def initialize_episode(self, physics: 'mjcf.Physics', physics, self._walker, props=self._props) self._walker_features_prev = self._walker_features.copy() - self._walker_joints = np.array(physics.bind(self._walker.mocap_joints).qpos) # pytype: disable=attribute-error + self._walker_joints = np.array(physics.bind(self._walker.mocap_joints).qpos) # compute initial error self._compute_termination_error() @@ -779,14 +779,14 @@ def _update_ghost(self, physics: 'mjcf.Physics'): def action_spec(self, physics: 'mjcf.Physics'): """Action spec of the walker only.""" - ctrl = physics.bind(self._walker.actuators).ctrl # pytype: disable=attribute-error + ctrl = physics.bind(self._walker.actuators).ctrl shape = ctrl.shape dtype = ctrl.dtype minimum = [] maximum = [] for actuator in self._walker.actuators: - if physics.bind(actuator).ctrllimited: # pytype: disable=attribute-error - ctrlrange = physics.bind(actuator).ctrlrange # pytype: disable=attribute-error + if physics.bind(actuator).ctrllimited: + ctrlrange = physics.bind(actuator).ctrlrange minimum.append(ctrlrange[0]) maximum.append(ctrlrange[1]) else: @@ -797,7 +797,7 @@ def action_spec(self, physics: 'mjcf.Physics'): dtype=dtype, minimum=np.asarray(minimum, dtype=dtype), maximum=np.asarray(maximum, dtype=dtype), - name='\t'.join(actuator.full_identifier # pytype: disable=attribute-error + name='\t'.join(actuator.full_identifier for actuator in self._walker.actuators)) @property @@ -879,7 +879,7 @@ def __init__( termination_error_threshold=termination_error_threshold, prop_termination_error_threshold=prop_termination_error_threshold, min_steps=min_steps, - dataset=dataset, + dataset=dataset, # pyrefly: ignore[bad-argument-type] reward_type=reward_type, physics_timestep=physics_timestep, always_init_at_clip_start=always_init_at_clip_start, @@ -903,7 +903,7 @@ def after_step(self, physics: 'mjcf.Physics', random_state): self._walker_features = utils.get_features( physics, self._walker, props=self._props) # features for default error - self._walker_joints = np.array(physics.bind(self._walker.mocap_joints).qpos) # pytype: disable=attribute-error + self._walker_joints = np.array(physics.bind(self._walker.mocap_joints).qpos) self._current_reference_features = { k: v[self._time_step].copy() @@ -956,7 +956,7 @@ def __init__(self, def _get_clip_to_track(self, random_state: np.random.RandomState): self._current_clip_index = (self._current_clip_index + 1) % self._num_clips - start_step = self._dataset.start_steps[self._current_clip_index] + start_step = self._dataset.start_steps[self._current_clip_index] # pyrefly: ignore[unsupported-operation] clip_id = self._dataset.ids[self._current_clip_index] logging.info('Showing clip %d of %d, clip id %s', self._current_clip_index+1, self._num_clips, clip_id) @@ -966,21 +966,21 @@ def _get_clip_to_track(self, random_state: np.random.RandomState): logging.info('Loading clip %s', clip_id) self._all_clips[self._current_clip_index] = self._loader.get_trajectory( clip_id, - start_step=self._dataset.start_steps[self._current_clip_index], - end_step=self._dataset.end_steps[self._current_clip_index], + start_step=self._dataset.start_steps[self._current_clip_index], # pyrefly: ignore[unsupported-operation] + end_step=self._dataset.end_steps[self._current_clip_index], # pyrefly: ignore[unsupported-operation] zero_out_velocities=False) self._current_clip = self._all_clips[self._current_clip_index] - self._clip_reference_features = self._current_clip.as_dict() + self._clip_reference_features = self._current_clip.as_dict() # pyrefly: ignore[missing-attribute] self._clip_reference_features = _strip_reference_prefix( self._clip_reference_features, 'walker/') # The reference features are already restricted to # clip_start_step:clip_end_step. However start_step is in # [clip_start_step:clip_end_step]. Hence we subtract clip_start_step to # obtain a valid index for the reference features. - self._time_step = start_step - self._dataset.start_steps[ + self._time_step = start_step - self._dataset.start_steps[ # pyrefly: ignore[unsupported-operation] self._current_clip_index] - self._current_start_time = (start_step - self._dataset.start_steps[ - self._current_clip_index]) * self._current_clip.dt + self._current_start_time = (start_step - self._dataset.start_steps[ # pyrefly: ignore[unsupported-operation] + self._current_clip_index]) * self._current_clip.dt # pyrefly: ignore[missing-attribute] self._last_step = len( self._clip_reference_features['joints']) - self._max_ref_step - 1 logging.info('Mocap %s at step %d with remaining length %d.', clip_id, diff --git a/dm_control/mjcf/attribute.py b/dm_control/mjcf/attribute.py index 1605f639..c2ae0b2e 100644 --- a/dm_control/mjcf/attribute.py +++ b/dm_control/mjcf/attribute.py @@ -231,6 +231,8 @@ def to_xml_string(self, prefix_root=None, return util.to_native_string(out.getvalue())[:-1] # Strip trailing space. def _check_shape(self, array): + if array.ndim == 0: + array = array.reshape(1) actual_length = array.shape[0] if len(array.shape) > 1: raise ValueError('Expect one-dimensional array: got {}'.format(array)) diff --git a/dm_control/mjcf/export_with_assets_test.py b/dm_control/mjcf/export_with_assets_test.py index a23e7b47..1cc6b31e 100644 --- a/dm_control/mjcf/export_with_assets_test.py +++ b/dm_control/mjcf/export_with_assets_test.py @@ -13,8 +13,6 @@ # limitations under the License. # ============================================================================ -"""Tests for `dm_control.mjcf.export_with_assets`.""" - import os from absl import flags from absl.testing import absltest @@ -74,9 +72,15 @@ def test_export_model(self, xml_path, out_xml_name): self.assertDictEqual(out_dir_contents, assets) # Check that we can construct an MjModel instance using the path to the - # output file. - from_exported = wrapper.MjModel.from_xml_path( - os.path.join(out_dir, out_xml_name)) + # output file. Load relative to `out_dir` so `modelfiledir` is empty, + # matching `from_xml_string` below when resolved paths are stored in + # `mjModel.paths`. + old_cwd = os.getcwd() + try: + os.chdir(out_dir) + from_exported = wrapper.MjModel.from_xml_path(out_xml_name) + finally: + os.chdir(old_cwd) # Check that this model is identical to one constructed directly from # `mjcf_model`. diff --git a/dm_control/mjcf/physics.py b/dm_control/mjcf/physics.py index ef6a9402..8922bbf3 100644 --- a/dm_control/mjcf/physics.py +++ b/dm_control/mjcf/physics.py @@ -92,7 +92,8 @@ def _get_attributes(size_names, strip_prefixes): _ATTRIBUTES = { - 'actuator': _get_attributes(['na', 'nu'], strip_prefixes=['actuator']), + 'actuator': _get_attributes(['na', 'nu', 'nactuator', 'nout'], + strip_prefixes=['actuator']), 'body': _get_attributes(['nbody'], strip_prefixes=['body']), 'mocap_body': _get_attributes(['nbody', 'nmocap'], strip_prefixes=['body']), 'camera': _get_attributes(['ncam'], strip_prefixes=['cam']), diff --git a/dm_control/mjcf/physics_test.py b/dm_control/mjcf/physics_test.py index b10f42bb..e2c4a608 100644 --- a/dm_control/mjcf/physics_test.py +++ b/dm_control/mjcf/physics_test.py @@ -13,8 +13,6 @@ # limitations under the License. # ============================================================================ -"""Tests for `dm_control.mjcf.physics`.""" - import copy import os import pickle @@ -579,33 +577,6 @@ def test_error_when_pickling_synchronizing_array_wrapper(self): mjcf_physics._PICKLING_NOT_SUPPORTED.format(type=type(xpos_view))): pickle.dumps(xpos_view) - def test_plugins_elasticity(self): - root = mjcf.RootElement() - root.extension.add('plugin', plugin='mujoco.elasticity.cable') - - # Replicate example in mujoco/model/plugin/elasticity/cable.xml - composite = root.worldbody.add( - 'composite', - type='cable', - curve='s', - count=[41, 1, 1], - size=[1, 0, 0], - offset=[-0.3, 0, 0.6], - initial='none', - ) - plugin = composite.add('plugin', plugin='mujoco.elasticity.cable') - plugin.add('config', key='twist', value='1e7') - plugin.add('config', key='bend', value='4e6') - plugin.add('config', key='vmax', value='0.05') - composite.add('joint', kind='main', damping=0.015) - composite.geom.type = 'capsule' - composite.geom.size = [0.005, 0, 0] - composite.geom.rgba = [0.8, 0.2, 0.1, 1] - composite.geom.condim = 1 - - physics = mjcf.Physics.from_mjcf_model(root) - physics.step() - def test_plugins_sdf(self): root = mjcf.RootElement() root.option.sdf_iterations = 10 @@ -616,7 +587,7 @@ def test_plugins_sdf(self): instance.add('config', key='radius1', value='0.35') instance.add('config', key='radius2', value='0.15') - # Replicate example in mujoco/model/plugin/elasticity/torus.xml + # Replicate example in mujoco/model/plugin/sdf/torus.xml mesh = root.asset.add('mesh', name='torus') mesh.add('plugin', instance='torus') diff --git a/dm_control/mjcf/schema.xml b/dm_control/mjcf/schema.xml index 7f293d5b..11aea4eb 100644 --- a/dm_control/mjcf/schema.xml +++ b/dm_control/mjcf/schema.xml @@ -1,3 +1,7 @@ + @@ -5,98 +9,109 @@ - + - + - + - - + - - + + + + + + + - - + + - + - - - - - + + + + + - - - - + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -104,568 +119,291 @@ - - - + + + - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + - - - - - + + + + + - - - - + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + + - + - - + + + + + + + + + + + + + + + + + + + + + + - - + + - + + + + + + + + + + + + - - + - - - - - - - + + + + + + + + + - + - - - - - + + + + - - - + + - + - - - + + + + + - - + + - - - + + + + + - + - + - - - - - + + + + + + + - - - - - - + + + + + + + - + @@ -676,34 +414,35 @@ - - - - - - - - - - - - - - + + + + + - + + + + + + + + + - + - - - - - + + + + + + + @@ -715,407 +454,380 @@ - + + + + - + + - + + + + + - - - + - - + + + + + + + + - + + + + - - - - - - - + + + + + + + + + + - + - - + + + + + + + - + + + - - - - - + - - + + + + + + + + - + + + - - - + - - + + + + + + + - + + + - - + - - + + + + + + + + - + + + + + + + + + + + + + + + + + + - + - + - + + + + + + - + + + - - + - - + + + + + + + - + + + - - + - + + + - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - + - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + - - - - - - - - - - - - - - + - - - - - - + + + - - - - - - - - + + - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + + - - + + - + + + - - + + - - - - - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + - + - - - + + + + + - - + + - - - + + + + + - + - - - - - - - - - - - - - - - - - + - - - + - - - - - - + + + + + + + - + - - - - - - - - + + + + + + + - + @@ -1124,135 +836,324 @@ - + - - - - - - - - - - - - - - - - + + + + + - + + + + + + + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + - + @@ -1260,113 +1161,135 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + - + + + + + - - - + + + + + + - - - - - - - - - - - - - - - - + + + + - - @@ -1374,28 +1297,29 @@ - + - + - - - - - - - - + + + + + + + + + + - + - @@ -1409,40 +1333,43 @@ - - - - + + + + - - - + + - + - - - + + + + + - - + + - - - + + + + + - + - + @@ -1450,7 +1377,7 @@ - + @@ -1462,18 +1389,19 @@ - + - - - - - - + + + + + + + @@ -1481,17 +1409,19 @@ - - - - - - + + + + + + + - + + @@ -1500,206 +1430,357 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - + + @@ -1713,52 +1794,57 @@ - - - - - - - - - + + + + + + + + + - - + + + + + - - - - - - - + - - - + + + - - - - + + + + - - - - - + + + + + + + + + - + - + + + + + @@ -1767,22 +1853,22 @@ + + + - - - - - - - - + + + + + @@ -1795,23 +1881,103 @@ - - - - - + + + + + - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1821,9 +1987,9 @@ - - - + + + @@ -1835,9 +2001,9 @@ - - - + + + @@ -1851,10 +2017,10 @@ - - - - + + + + @@ -1863,10 +2029,10 @@ - - - - + + + + @@ -1875,84 +2041,22 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - + + + + + - - - - - - - - @@ -1963,30 +2067,39 @@ - - - - + + + + + + + - + + + - + - - - - - + - - - - - - + + + + + + + + + + + + + + @@ -1996,21 +2109,26 @@ - - + + + + + + - + + + - + - - + - + + - @@ -2018,26 +2136,31 @@ - - + + + + + + + - + + + - + - - + - + + - - @@ -2045,21 +2168,26 @@ - - + + + + + + - + + + - + - - + - + + - @@ -2068,26 +2196,53 @@ - - + + + + + + + + - + + + - + - - + - + + + + + + + + + + + + + + + + + + + + + - + @@ -2095,20 +2250,25 @@ - + + + + + - + + + - + - - + - + + - @@ -2117,21 +2277,26 @@ - - + + + + + + - + + + - + - - + - + + - @@ -2143,21 +2308,26 @@ - - + + + + + + - + + + - + - - + - - + + @@ -2173,11 +2343,14 @@ - + + + - - + + + @@ -2185,34 +2358,39 @@ - - - - - + + + + + + + + + - - - - + + + - + - - + + + + + - - + @@ -2225,462 +2403,722 @@ - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + - + + + + + - + + - + - + + + + + - - + + + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + + + + + + + + + + + + + + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + + - - - - + + + + - + + + + + - - - - + + + + - + + + + + - - - - + + + + - + + + + + - - - - + + + + - + + + + + - - - - + + + + - + + + + + - - - - + + + + - + + + + + - - - - + + + + - + + + + + - - + + - + + + + + - - + + - + + + + + - + - + + + + + - + - + + + + + - + - + + + + - - - - - - - - - - - - - - - + + + + - + - - + + + + - - + + + + + - - - - - - - - - + + + + + + - - + + + + + + - - + + + + + + - + + + + + + + + + + + - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2693,9 +3131,10 @@ - + + diff --git a/dm_control/mjcf/schema_test.py b/dm_control/mjcf/schema_test.py new file mode 100644 index 00000000..1eda0e8c --- /dev/null +++ b/dm_control/mjcf/schema_test.py @@ -0,0 +1,191 @@ +# Copyright 2026 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. +# ============================================================================ +"""Validates dm_control's schema.xml against dm_control itself.""" + +import os +import xml.etree.ElementTree as ET + +from absl.testing import absltest +from dm_control import mjcf +from dm_control.mjcf import schema + +_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), 'schema.xml') + +# attributes whose defaults are not settable through PyMJCF descriptors +# (multi-token strings PyMJCF types more strictly than the runtime) +_DEFAULT_SWEEP_SKIP = set() + + +class SchemaTest(absltest.TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.spec = schema.parse_schema(_SCHEMA_PATH) + + def test_namespace_universe_is_closed(self): + """Every reference namespace is populated by some identified element.""" + namespaces = schema.collect_namespaces(self.spec) + dangling = [] + + def visit(spec, seen): + if id(spec) in seen: + return + seen.add(id(spec)) + for attr_spec in spec.attributes.values(): + kwargs = getattr(attr_spec, 'other_kwargs', {}) or {} + ns = kwargs.get('reference_namespace') + if ns and not ns.startswith('attrib:') and ns not in namespaces: + dangling.append(f'{spec.name}.{attr_spec.name} -> {ns}') + for child in spec.children.values(): + visit(child, seen) + + visit(self.spec, set()) + self.assertEqual(dangling, []) + + def test_singleton_api_surface(self): + """The attribute-vs-list-view surface PyMJCF users rely on.""" + root = mjcf.RootElement(model='m') + # singletons are elements, directly usable + root.option.timestep = 0.001 + root.compiler.angle = 'radian' + root.visual.map.znear = 0.01 + self.assertTrue(hasattr(root.worldbody, 'add')) + # the top-level default is a singleton... + root.default.geom.margin = 0.001 + # ...whose nested defaults are repeated + root.default.add('default', dclass='a') + root.default.add('default', dclass='b') + # repeated elements are list views + root.worldbody.add('body', name='b1') + root.worldbody.add('body', name='b2') + self.assertLen(root.worldbody.body, 2) + + def test_full_pymjcf_exercise(self): + """Build, class, attach twice, bind, compile, round-trip.""" + arm = mjcf.RootElement(model='arm') + arm.default.joint.damping = 2.0 + strong = arm.default.add('default', dclass='strong') + strong.joint.damping = 5.0 + upper = arm.worldbody.add('body', name='upper') + upper.add('joint', name='shoulder', type='ball') + upper.add('geom', name='upper_geom', type='capsule', size=[.05], + fromto=[0, 0, 0, 0, 0, -.3]) + lower = upper.add('body', name='lower', pos=[0, 0, -.3]) + elbow = lower.add('joint', name='elbow', type='hinge', axis=[0, 1, 0], + dclass='strong') + lower.add('geom', name='lower_geom', type='capsule', size=[.04], + fromto=[0, 0, 0, 0, 0, -.25]) + tip = lower.add('site', name='tip', pos=[0, 0, -.25]) + arm.actuator.add('position', name='act', joint=elbow, kp=10) + arm.sensor.add('jointpos', name='sense', joint=elbow) + arm.contact.add('exclude', name='ex', body1='upper', body2='lower') + + hand = mjcf.RootElement(model='hand') + hand.worldbody.add('geom', name='palm', type='box', + size=[.02, .02, .02]) + tip.attach(hand) + + scene = mjcf.RootElement(model='scene') + scene.worldbody.add('geom', name='floor', type='plane', size=[1, 1, .1]) + scene.worldbody.add('light', pos=[0, 0, 3]) + scene.attach(arm) + + physics = mjcf.Physics.from_mjcf_model(scene) + self.assertEqual(physics.model.nu, 1) + self.assertGreater(physics.model.nq, 4) + # binding resolves through two attachment levels + self.assertEqual( + scene.find_all('sensor')[0].joint.full_identifier, 'arm/elbow') + bound = physics.bind(scene.find_all('joint')) + self.assertEqual(bound.qpos.shape[0], physics.model.nq) + # serialization is deterministic; PyMJCF output is not re-parseable by + # design (the '/' scope markers reject), so the parser direction is + # exercised on ordinary MJCF instead + self.assertEqual(scene.to_xml_string(), scene.to_xml_string()) + plain = mjcf.from_xml_string(""" + + + + + + + + + + + + """) + self.assertEqual(plain.find('geom', 'g').dclass.dclass, 'soft') + plain_physics = mjcf.Physics.from_mjcf_model(plain) + self.assertEqual(plain_physics.model.nu, 1) + + def test_every_declared_default_validates(self): + """Setting each attribute to its declared default passes validation. + + The schema file carries default= metadata; PyMJCF's descriptors + validate on assignment, so this sweeps every keyword set, arity and + numeric type through dm_control's own machinery. + """ + tree = ET.parse(_SCHEMA_PATH) + + def constructible(spec_node, path): + """Depth-first: yield (path, element node) below construction roots.""" + for child in spec_node.findall('./children/element'): + name = child.get('name') + yield path + (name,), child + yield from constructible(child, path + (name,)) + + root = mjcf.RootElement(model='sweep') + checked, failures = 0, [] + for path, node in constructible(tree.getroot(), ()): + for attr in node.findall('./attributes/attribute'): + default = attr.get('default') + if default is None: + continue + if (path[-1], attr.get('name')) in _DEFAULT_SWEEP_SKIP: + continue + # walk to the element, constructing along the way where possible + parent = root + ok = True + for tag in path: + existing = getattr(parent, tag, None) + if isinstance(existing, mjcf.Element) or ( + existing is not None + and type(existing).__name__ == '_ElementImpl'): + parent = existing + continue + try: + parent = parent.add(tag) + except Exception: # pylint: disable=broad-exception-caught + ok = False + break + if not ok: + continue + try: + setattr(parent, attr.get('name'), default) + checked += 1 + except Exception as e: # pylint: disable=broad-exception-caught + failures.append(f'{"/".join(path)}@{attr.get("name")}=' + f'{default!r}: {e}') + self.assertGreater(checked, 200) + if failures: + self.fail(f'{len(failures)} defaults rejected by PyMJCF:\n' + + '\n'.join(failures[:20])) + + +if __name__ == '__main__': + absltest.main() diff --git a/dm_control/mujoco/engine_test.py b/dm_control/mujoco/engine_test.py index c4197a83..5471eee0 100644 --- a/dm_control/mujoco/engine_test.py +++ b/dm_control/mujoco/engine_test.py @@ -406,37 +406,27 @@ def testSetGetPhysicsState(self): ) def testSetGetPhysicsStateWithPlugin(self): - # Model copied from mujoco/test/plugin/elasticity/elasticity_test.cc - model_with_cable_plugin = """ + model_with_pid_plugin = """ - """ - physics = engine.Physics.from_xml_string(model_with_cable_plugin) + physics = engine.Physics.from_xml_string(model_with_pid_plugin) physics_state = physics.get_state() # qpos, qvel, act, plugin_state @@ -549,7 +539,10 @@ def assert_warning(): @parameterized.named_parameters( ('_copy', lambda x: x.copy()), ('_deepcopy', copy.deepcopy), - ('_pickle_and_unpickle', lambda x: pickle.loads(pickle.dumps(x))), + ( + '_pickle_and_unpickle', + lambda x: pickle.loads(pickle.dumps(x)), # pylint: disable=g-unsafe-pickle-load + ), ) def testCopyOrPicklePhysics(self, func): for _ in range(10): @@ -573,7 +566,10 @@ def testCopyOrPicklePhysics(self, func): @parameterized.named_parameters( ('_copy', lambda x: x.copy()), - ('_pickle_and_unpickle', lambda x: pickle.loads(pickle.dumps(x))), + ( + '_pickle_and_unpickle', + lambda x: pickle.loads(pickle.dumps(x)), # pylint: disable=g-unsafe-pickle-load + ), ) def testSuppressErrorsAfterCopyOrPicklePhysics(self, func): # Regression test for a problem that used to exist where diff --git a/dm_control/mujoco/index.py b/dm_control/mujoco/index.py index 112e4a78..76e534cc 100644 --- a/dm_control/mujoco/index.py +++ b/dm_control/mujoco/index.py @@ -219,11 +219,17 @@ def _get_size_name_to_element_names(model): # For example, the element names for "nv" axis come from "njnt". for size_name, address_field_name in _RAGGED_ADDRS.items(): donor = 'n' + address_field_name.split('_')[0] - if donor == 'nactuator': - donor = 'nu' if donor in size_name_to_element_names: size_name_to_element_names[size_name] = size_name_to_element_names[donor] + # nu (controls) and nout (force outputs) inherit actuator names from + # nactuator. For SISO models nu == nactuator == nout; for MIMO models this + # gives each control/output the name of its owning actuator. + if 'nactuator' in size_name_to_element_names: + for alias in ('nu', 'nout'): + size_name_to_element_names[alias] = ( + size_name_to_element_names['nactuator']) + # Mocap bodies are a special subset of bodies. mocap_body_names = [None] * model.nmocap for body_id, body_name in enumerate(size_name_to_element_names['nbody']): diff --git a/dm_control/mujoco/index_test.py b/dm_control/mujoco/index_test.py index a5d1176d..3eaeae53 100644 --- a/dm_control/mujoco/index_test.py +++ b/dm_control/mujoco/index_test.py @@ -13,8 +13,6 @@ # limitations under the License. # ============================================================================ -"""Tests for index.""" - import collections from absl.testing import absltest @@ -34,10 +32,10 @@ FIELD_REPR = { 'act': ('FieldIndexer(act):\n' '(empty)'), - 'qM': ('FieldIndexer(qM):\n' - '0 [ 0 ]\n' - '1 [ 1 ]\n' - '2 [ 2 ]'), + 'qLD': ('FieldIndexer(qLD):\n' + '0 [ 0 ]\n' + '1 [ 1 ]\n' + '2 [ 2 ]'), 'sensordata': ('FieldIndexer(sensordata):\n' '0 accelerometer [ 0 ]\n' '1 accelerometer [ 1 ]\n' @@ -270,7 +268,7 @@ def testBadIndexItems(self, bad_index_item, exception_regexp): with self.assertRaisesRegex(IndexError, exception_regexp): _ = indexer[expression] - @parameterized.parameters('act', 'qM', 'sensordata', 'xpos') # field name + @parameterized.parameters('act', 'qLD', 'sensordata', 'xpos') # field name def testFieldIndexerRepr(self, field_name): indexer = getattr(self._data_indexers, field_name) diff --git a/dm_control/mujoco/tutorial.ipynb b/dm_control/mujoco/tutorial.ipynb index caf73cc6..e577e4c2 100644 --- a/dm_control/mujoco/tutorial.ipynb +++ b/dm_control/mujoco/tutorial.ipynb @@ -25,9 +25,9 @@ }, "cell_type": "markdown", "source": [ - "\u003e \u003cp\u003e\u003csmall\u003e\u003csmall\u003eCopyright 2021 The dm_control Authors.\u003c/small\u003e\u003c/p\u003e\n", - "\u003e \u003cp\u003e\u003csmall\u003e\u003csmall\u003eLicensed 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 \u003ca href=\"http://www.apache.org/licenses/LICENSE-2.0\"\u003ehttp://www.apache.org/licenses/LICENSE-2.0\u003c/a\u003e.\u003c/small\u003e\u003c/small\u003e\u003c/p\u003e\n", - "\u003e \u003cp\u003e\u003csmall\u003e\u003csmall\u003eUnless 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.\u003c/small\u003e\u003c/small\u003e\u003c/p\u003e" + ">

Copyright 2021 The dm_control Authors.

\n", + ">

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.

\n", + ">

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.

" ] }, { @@ -38,7 +38,7 @@ "source": [ "This notebook provides an overview tutorial of the **MuJoCo** physics simulator, using the `dm_control` Python bindings. It is similar to the notebook in `dm_control/tutorial.ipynb`, but focuses on teaching MuJoCo itself, rather than the additional features provided by the Python package. \n", "\n", - "**A Colab runtime with GPU acceleration is required.** If you're using a CPU-only runtime, you can switch using the menu \"Runtime \u003e Change runtime type\"." + "**A Colab runtime with GPU acceleration is required.** If you're using a CPU-only runtime, you can switch using the menu \"Runtime > Change runtime type\"." ] }, { @@ -91,7 +91,7 @@ "\"\"\")\n", "\n", "print('Installing dm_control...')\n", - "!pip install -q dm_control\u003e=1.0.35\n", + "!pip install -q dm_control>=1.0.51\n", "\n", "# Configure dm_control to use the EGL rendering backend (requires GPU)\n", "%env MUJOCO_GL=egl\n", @@ -267,13 +267,13 @@ "#@title A static model {vertical-output: true}\n", "\n", "static_model = \"\"\"\n", - "\u003cmujoco\u003e\n", - " \u003cworldbody\u003e\n", - " \u003clight name=\"top\" pos=\"0 0 1\"/\u003e\n", - " \u003cgeom name=\"red_box\" type=\"box\" size=\".2 .2 .2\" rgba=\"1 0 0 1\"/\u003e\n", - " \u003cgeom name=\"green_sphere\" pos=\".2 .2 .2\" size=\".1\" rgba=\"0 1 0 1\"/\u003e\n", - " \u003c/worldbody\u003e\n", - "\u003c/mujoco\u003e\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", "\"\"\"\n", "physics = mujoco.Physics.from_xml_string(static_model)\n", "pixels = physics.render()\n", @@ -310,16 +310,16 @@ "#@title A child body with a joint { vertical-output: true }\n", "\n", "swinging_body = \"\"\"\n", - "\u003cmujoco\u003e\n", - " \u003cworldbody\u003e\n", - " \u003clight name=\"top\" pos=\"0 0 1\"/\u003e\n", - " \u003cbody name=\"box_and_sphere\" euler=\"0 0 -30\"\u003e\n", - " \u003cjoint name=\"swing\" type=\"hinge\" axis=\"1 -1 0\" pos=\"-.2 -.2 -.2\"/\u003e\n", - " \u003cgeom name=\"red_box\" type=\"box\" size=\".2 .2 .2\" rgba=\"1 0 0 1\"/\u003e\n", - " \u003cgeom name=\"green_sphere\" pos=\".2 .2 .2\" size=\".1\" rgba=\"0 1 0 1\"/\u003e\n", - " \u003c/body\u003e\n", - " \u003c/worldbody\u003e\n", - "\u003c/mujoco\u003e\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", "\"\"\"\n", "physics = mujoco.Physics.from_xml_string(swinging_body)\n", "# Visualize the joint axis.\n", @@ -362,9 +362,9 @@ "# Simulate and display video.\n", "frames = []\n", "physics.reset() # Reset state and time\n", - "while physics.data.time \u003c duration:\n", + "while physics.data.time < duration:\n", " physics.step()\n", - " if len(frames) \u003c physics.data.time * framerate:\n", + " if len(frames) < physics.data.time * framerate:\n", " pixels = physics.render(scene_option=scene_option)\n", " frames.append(pixels)\n", "display_video(frames, framerate)" @@ -422,7 +422,7 @@ "# Shift nearest values to the origin.\n", "depth -= depth.min()\n", "# Scale by 2 mean distances of near rays.\n", - "depth /= 2*depth[depth \u003c= 1].mean()\n", + "depth /= 2*depth[depth <= 1].mean()\n", "# Scale to [0, 255]\n", "pixels = 255*np.clip(depth, 0, 1)\n", "PIL.Image.fromarray(pixels.astype(np.uint8))" @@ -512,7 +512,7 @@ "\n", "def add_visual_capsule(scene, point1, point2, radius, rgba):\n", " \"\"\"Adds one capsule to an mjvScene.\"\"\"\n", - " if scene.ngeom \u003e= scene.maxgeom:\n", + " if scene.ngeom >= scene.maxgeom:\n", " return\n", " scene.ngeom += 1 # increment ngeom\n", " # initialise a new capsule, add it to the scene using mjv_connector\n", @@ -531,7 +531,7 @@ "\n", "def scene_callback(physics, scn):\n", " \"\"\"Draw position trace, speed modifies width and colours.\"\"\"\n", - " if len(positions) \u003e 1:\n", + " if len(positions) > 1:\n", " for i in range(len(positions)-1):\n", " rgba=np.array((np.clip(speeds[i]/10, 0, 1),\n", " np.clip(1-speeds[i]/10, 0, 1),\n", @@ -547,13 +547,13 @@ "# Simulate and display video.\n", "frames = []\n", "physics.reset() # Reset state and time\n", - "while physics.data.time \u003c duration:\n", + "while physics.data.time < duration:\n", " # append data to the traces\n", " positions.append(physics.named.data.geom_xpos[\"green_sphere\"].copy())\n", " times.append(physics.data.time)\n", " speeds.append(get_geom_speed(physics, \"green_sphere\"))\n", " physics.step()\n", - " if len(frames) \u003c physics.data.time * framerate:\n", + " if len(frames) < physics.data.time * framerate:\n", " camera = mujoco.Camera(physics, max_geom=10000,\n", " scene_callback=scene_callback)\n", " pixels = camera.render()\n", @@ -841,32 +841,32 @@ "#@title The \"tippe-top\" model{vertical-output: true}\n", "\n", "tippe_top = \"\"\"\n", - "\u003cmujoco model=\"tippe top\"\u003e\n", - " \u003coption integrator=\"RK4\"/\u003e\n", - "\n", - " \u003casset\u003e\n", - " \u003ctexture name=\"grid\" type=\"2d\" builtin=\"checker\" rgb1=\".1 .2 .3\"\n", - " rgb2=\".2 .3 .4\" width=\"300\" height=\"300\"/\u003e\n", - " \u003cmaterial name=\"grid\" texture=\"grid\" texrepeat=\"8 8\" reflectance=\".2\"/\u003e\n", - " \u003c/asset\u003e\n", - "\n", - " \u003cworldbody\u003e\n", - " \u003cgeom size=\".2 .2 .01\" type=\"plane\" material=\"grid\"/\u003e\n", - " \u003clight pos=\"0 0 .6\"/\u003e\n", - " \u003ccamera name=\"closeup\" pos=\"0 -.1 .07\" xyaxes=\"1 0 0 0 1 2\"/\u003e\n", - " \u003cbody name=\"top\" pos=\"0 0 .02\"\u003e\n", - " \u003cfreejoint/\u003e\n", - " \u003cgeom name=\"ball\" type=\"sphere\" size=\".02\" /\u003e\n", - " \u003cgeom name=\"stem\" type=\"cylinder\" pos=\"0 0 .02\" size=\"0.004 .008\"/\u003e\n", - " \u003cgeom name=\"ballast\" type=\"box\" size=\".023 .023 0.005\" pos=\"0 0 -.015\"\n", - " contype=\"0\" conaffinity=\"0\" group=\"3\"/\u003e\n", - " \u003c/body\u003e\n", - " \u003c/worldbody\u003e\n", - "\n", - " \u003ckeyframe\u003e\n", - " \u003ckey name=\"spinning\" qpos=\"0 0 0.02 1 0 0 0\" qvel=\"0 0 0 0 1 200\" /\u003e\n", - " \u003c/keyframe\u003e\n", - "\u003c/mujoco\u003e\n", + "\n", + " \n", "\"\"\"\n", "physics = mujoco.Physics.from_xml_string(tippe_top)\n", "PIL.Image.fromarray(physics.render(camera_id='closeup'))" @@ -881,12 +881,12 @@ "cell_type": "markdown", "source": [ "Note several new features of this model definition:\n", - "0. The free joint is added with the `\u003cfreejoint/\u003e` clause, which is similar to `\u003cjoint type=\"free\"/\u003e`, but prohibits unphysical attributes like friction or stiffness.\n", - "1. We use the `\u003coption/\u003e` clause to set the integrator to the more accurate Runge Kutta 4th order.\n", - "2. We define the floor's grid material inside the `\u003casset/\u003e` clause and reference it in the floor geom. \n", + "0. The free joint is added with the `` clause, which is similar to ``, but prohibits unphysical attributes like friction or stiffness.\n", + "1. We use the `