def convert_key_item(self, key_item): """Converts a named indexing expression to a numpy-friendly index.""" _validate_key_item(key_item) if isinstance(key_item, str): key_item = self._names_to_offsets[util.to_native_string(key_item)] elif isinstance(key_item, (list, np.ndarray)): # Cast lists to numpy arrays. key_item = np.array(key_item, copy=False) original_shape = key_item.shape # We assume that either all or none of the items in the array are strings # representing names. If there is a mix, we will let NumPy throw an error # when trying to index with the returned item. if isinstance(key_item.flat[0], str): key_item = np.array([ self._names_to_offsets[util.to_native_string(k)] for k in key_item.flat ]) # Ensure the output shape is the same as that of the input. key_item.shape = original_shape return key_item
def _load_xml(filename, vfs_or_none): """Invokes `mj_loadXML` with logging/error handling.""" error_buf = ctypes.create_string_buffer(_ERROR_BUFSIZE) model_ptr = mjlib.mj_loadXML(util.to_binary_string(filename), vfs_or_none, error_buf, _ERROR_BUFSIZE) if not model_ptr: raise Error(util.to_native_string(error_buf.value)) elif error_buf.value: logging.warning(util.to_native_string(error_buf.value)) # Free resources when the ctypes pointer is garbage collected. _create_finalizer(model_ptr, mjlib.mj_deleteModel) return model_ptr
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)
def _get_size_name_to_element_names(model): """Returns a dict that maps size names to element names. Args: model: An instance of `mjbindings.mjModelWrapper`. Returns: A `dict` mapping from a size name (e.g. `'nbody'`) to a list of element names. """ names = model.names[:model.nnames] size_name_to_element_names = {} for field_name in dir(model): if not _is_name_pointer(field_name): continue # Get addresses of element names in `model.names` array, e.g. # field name: `name_nbodyadr` and name_addresses: `[86, 92, 101]`, and skip # when there are no elements for this type in the model. name_addresses = getattr(model, field_name).ravel() if not name_addresses.size: continue # Get the element names. element_names = [] for start_index in name_addresses: name = names[start_index:names.find(b'\0', start_index)] element_names.append(util.to_native_string(name)) # String identifier for the size of the first dimension, e.g. 'nbody'. size_name = _get_size_name(field_name) size_name_to_element_names[size_name] = element_names # Add custom element names for certain columns. for size_name, element_names in six.iteritems(_COLUMN_NAMES): size_name_to_element_names[size_name] = element_names # "Ragged" axes inherit their element names from other "non-ragged" axes. # For example, the element names for "nv" axis come from "njnt". for size_name, address_field_name in six.iteritems(_RAGGED_ADDRS): donor = 'n' + address_field_name.split('_')[0] if donor in size_name_to_element_names: size_name_to_element_names[size_name] = size_name_to_element_names[donor] # 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']): body_mocapid = model.body_mocapid[body_id] if body_mocapid != -1: mocap_body_names[body_mocapid] = body_name assert None not in mocap_body_names size_name_to_element_names['nmocap'] = mocap_body_names return size_name_to_element_names
def to_xml_string(self, prefix_root=None): # pylint: disable=unused-argument if self._value is None: return None else: out = six.BytesIO() # 17 decimal digits is sufficient to represent a double float without loss # of precision. # https://en.wikipedia.org/wiki/IEEE_754#Character_representation np.savetxt(out, self._value, fmt='%.17g', newline=' ') return util.to_native_string(out.getvalue())[:-1] # Strip trailing space.
def convert_key_item(self, key): """Converts a named indexing expression to a numpy-friendly index.""" _validate_key_item(key) if isinstance(key, str): key = self._names_to_slices[util.to_native_string(key)] elif isinstance(key, (list, np.ndarray)): # We assume that either all or none of the items in the sequence are # strings representing names. If there is a mix, we will let NumPy throw # an error when trying to index with the returned key. if isinstance(key[0], str): new_key = [] for k in key: idx = self._names_to_indices[util.to_native_string(k)] if isinstance(idx, int): new_key.append(idx) else: new_key.extend(idx) key = new_key return key
def test_export_model(self, xml_path, model_name): """Save processed MJCF model.""" out_dir = self.create_tempdir().full_path mjcf_model = mjcf.from_path(xml_path) mjcf.export_with_assets_as_zip(mjcf_model, out_dir=out_dir, model_name=model_name) # Read the .zip file in the output directory. # Check that the only directory is named `model_name`/, and put all the # contents under any directory in a dict a directory in a dict. zip_file_contents = {} zip_filename = os.path.join(out_dir, (model_name + '.zip')) self.assertTrue(zipfile.is_zipfile(zip_filename)) with zipfile.ZipFile(zip_filename, 'r') as zip_file: for zip_info in zip_file.infolist(): # Note: zip_info.is_dir() is not Python 2 compatible, but directories # inside a ZipFile are guaranteed to end with '/'. if not zip_info.filename.endswith(os.path.sep): with zip_file.open(zip_info.filename) as f: zip_file_contents[zip_info.filename] = f.read() else: self.assertEqual(os.path.join(model_name), zip_info.filename) # Check that the output directory contains an XML file of the correct name. xml_filename = os.path.join(model_name, model_name) + '.xml' self.assertIn(xml_filename, zip_file_contents) # Check that its contents match the output of `mjcf_model.to_xml_string()`. xml_contents = util.to_native_string( zip_file_contents.pop(xml_filename)) expected_xml_contents = mjcf_model.to_xml_string() self.assertEqual(xml_contents, expected_xml_contents) # Check that the other files in the directory match the contents of the # model's `assets` dict. assets = mjcf_model.get_assets() for asset_name, asset_contents in assets.items(): self.assertEqual( asset_contents, zip_file_contents[os.path.join(model_name, asset_name)])
def id2name(self, object_id, object_type): """Returns the name associated with a MuJoCo object ID, if there is one. Args: object_id: Integer ID. object_type: The type of the object. Can be either a lowercase string (e.g. 'body', 'geom') or an `mjtObj` enum value. Returns: A string containing the object name, or an empty string if the object ID either doesn't exist or has no name. Raises: Error: If `object_type` is not a valid MuJoCo object type. """ if not isinstance(object_type, int): object_type = _str2type(object_type) name_ptr = mjlib.mj_id2name(self.ptr, object_type, object_id) if not name_ptr: return "" return util.to_native_string(ctypes.string_at(name_ptr))
def test_export_model(self, xml_path, out_xml_name): """Save processed MJCF model.""" out_dir = self.create_tempdir().full_path mjcf_model = mjcf.from_path(xml_path) mjcf.export_with_assets(mjcf_model, out_dir=out_dir, out_file_name=out_xml_name) # Read the files in the output directory and put their contents in a dict. out_dir_contents = {} for filename in os.listdir(out_dir): with open(os.path.join(out_dir, filename), 'rb') as f: out_dir_contents[filename] = f.read() # Check that the output directory contains an XML file of the correct name. self.assertIn(out_xml_name, out_dir_contents) # Check that its contents match the output of `mjcf_model.to_xml_string()`. xml_contents = util.to_native_string( out_dir_contents.pop(out_xml_name)) expected_xml_contents = mjcf_model.to_xml_string() self.assertEqual(xml_contents, expected_xml_contents) # Check that the other files in the directory match the contents of the # model's `assets` dict. assets = mjcf_model.get_assets() 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)) # Check that this model is identical to one constructed directly from # `mjcf_model`. from_mjcf = wrapper.MjModel.from_xml_string(expected_xml_contents, assets=assets) self.assertEqual(from_exported.to_bytes(), from_mjcf.to_bytes())
def __repr__(self): """Returns a pretty string representation of the `FieldIndexer`.""" def get_name_arr_and_len(dim_idx): """Returns a string array of element names and the max name length.""" axis = self._axes[dim_idx] size = self._field.shape[dim_idx] try: name_len = max(len(name) for name in axis.names) name_arr = np.zeros(size, dtype='S{}'.format(name_len)) for name in axis.names: if name: # Use the `Axis` object to convert the name into a numpy index, then # use this index to write into name_arr. name_arr[axis.convert_key_item(name)] = name except AttributeError: name_arr = np.zeros( size, dtype='S0') # An array of zero-length strings name_len = 0 return name_arr, name_len row_name_arr, row_name_len = get_name_arr_and_len(0) if self._field.ndim > 1: col_name_arr, col_name_len = get_name_arr_and_len(1) else: col_name_arr, col_name_len = np.zeros(1, dtype='S0'), 0 idx_len = int(np.log10(max(self._field.shape[0], 1))) + 1 cls_template = '{class_name:}({field_name:}):' col_template = '{padding:}{col_names:}' row_template = '{idx:{idx_len:}} {row_name:>{row_name_len:}} {row_vals:}' lines = [] # Write the class name and field name. lines.append( cls_template.format(class_name=self.__class__.__name__, field_name=self._field_name)) # Write a header line containing the column names (if there are any). if col_name_len: col_width = max(col_name_len, 9) + 1 extra_indent = 4 padding = ' ' * (idx_len + row_name_len + extra_indent) col_names = ''.join('{name:<{width:}}'.format( name=util.to_native_string(name), width=col_width) for name in col_name_arr) lines.append( col_template.format(padding=padding, col_names=col_names)) # Write the row names (if there are any) and the formatted array values. if not self._field.shape[0]: lines.append('(empty)') else: for idx, row in enumerate(self._field): row_vals = np.array2string( np.atleast_1d(row), suppress_small=True, formatter={'float_kind': '{: < 9.3g}'.format}) lines.append( row_template.format(idx=idx, idx_len=idx_len, row_name=util.to_native_string( row_name_arr[idx]), row_name_len=row_name_len, row_vals=row_vals)) return '\n'.join(lines)
def _error_callback(message): logging.fatal(util.to_native_string(message))
def _warning_callback(message): logging.warning(util.to_native_string(message))
def name(self): """Returns the name of the model.""" # The model name is the first null-terminated string in the `names` buffer. return util.to_native_string( ctypes.string_at(ctypes.addressof(self.names.contents)))