# PyMJCF IMPORTANT: If you find yourself stuck while using PyMJCF, check out the various IMPORTANT boxes on this page and the [Common gotchas](#common-gotchas) section at the bottom to see if any of them is relevant. This library provides a Python object model for MuJoCo's XML-based [MJCF](http://www.mujoco.org/book/modeling.html) physics modeling language. The goal of the library is to allow users to easily interact with and modify MJCF models in Python, similarly to what the JavaScript DOM does for HTML. A key feature of this library is the ability to easily compose multiple separate MJCF models into a larger one. Disambiguation of duplicated names from different models, or multiple instances of the same model, is handled automatically. The following snippet provides a quick example of this library's typical use case. Here, the `UpperBody` class can simply instantiate two copies of `Arm`, thus reducing code duplication. The names of bodies, joints, or geoms of each `Arm` are automatically prefixed by their parent's names, and so no name collision occurs. ```python from dm_control import mjcf class Arm: def __init__(self, name): self.mjcf_model = mjcf.RootElement(model=name) self.upper_arm = self.mjcf_model.worldbody.add('body', name='upper_arm') self.shoulder = self.upper_arm.add('joint', name='shoulder', type='ball') self.upper_arm.add('geom', name='upper_arm', type='capsule', pos=[0, 0, -0.15], size=[0.045, 0.15]) self.forearm = self.upper_arm.add('body', name='forearm', pos=[0, 0, -0.3]) self.elbow = self.forearm.add('joint', name='elbow', type='hinge', axis=[0, 1, 0]) self.forearm.add('geom', name='forearm', type='capsule', pos=[0, 0, -0.15], size=[0.045, 0.15]) class UpperBody: def __init__(self): self.mjcf_model = mjcf.RootElement() self.mjcf_model.worldbody.add( 'geom', name='torso', type='box', size=[0.15, 0.045, 0.25]) left_shoulder_site = self.mjcf_model.worldbody.add( 'site', size=[1e-6]*3, pos=[-0.15, 0, 0.25]) right_shoulder_site = self.mjcf_model.worldbody.add( 'site', size=[1e-6]*3, pos=[0.15, 0, 0.25]) self.left_arm = Arm(name='left_arm') left_shoulder_site.attach(self.left_arm.mjcf_model) self.right_arm = Arm(name='right_arm') right_shoulder_site.attach(self.right_arm.mjcf_model) body = UpperBody() physics = mjcf.Physics.from_mjcf_model(body.mjcf_model) ``` ## Basic operations ### Creating an MJCF model In PyMJCF, the basic building block of a model is an `mjcf.Element`. This corresponds to an element in the generated XML. However, user code _cannot_ instantiate a generic `mjcf.Element` object directly. A valid model always consists of a single root `` element. This is represented as the special `mjcf.RootElement` type in PyMJCF, which _can_ be instantiated in user code to create an empty model. ```python from dm_control import mjcf mjcf_model = mjcf.RootElement() print(mjcf_model) # MJCF Element: ``` ### Adding new elements Attributes of the new element can be passed as kwargs: ```python my_box = mjcf_model.worldbody.add('geom', name='my_box', type='box', pos=[0, .1, 0]) print(my_box) # MJCF Element: ``` ### Parsing an existing XML document Alternatively, if an existing XML file already exists, PyMJCF can parse it to create a Python object: ```python from dm_control import mjcf # Parse from path mjcf_model = mjcf.from_path(filename) # Parse from file with open(filename) as f: mjcf_model = mjcf.from_file(f) # Parse from string with open(filename) as f: xml_string = f.read() mjcf_model = mjcf.from_xml_string(xml_string) print(type(mjcf_model)) # ``` ### Traversing through a model Consider the following MJCF model: ```xml ``` The child elements and XML attributes of an `Element` object are exposed as Python attributes. These attributes all have the same names as their XML counterparts, with one exception: the `class` XML attribute is named `dclass` in order to avoid a clash with the Python `class` keyword: ```python my_geom = mjcf_model.worldbody.body['foo'].body['bar'].geom['my_geom'] print(isinstance(mjcf_model, mjcf.Element)) # True print(my_geom.name) # 'my_geom' print(my_geom.pos) # np.array([0., 1., 2.], dtype=float) print(my_geom.class) # SyntaxError print(my_geom.dclass) # 'brick' ``` Note that attribute values in the object model are **not** affected by defaults: ```python print(mjcf_model.default.default['brick'].geom.rgba) # [1, 0, 0, 1] print(my_geom.rgba) # None ``` ### Finding elements without traversing We can also find elements directly without having to traverse through the object hierarchy: ```python found_geom = mjcf_model.find('geom', 'my_geom') print(found_geom == my_geom) # True ``` Find all elements of a given type: ```python # Note that is also considered a joint joints = mjcf_model.find_all('joint') print(len(joints)) # 2 print(joints[0] == mjcf_model.worldbody.body['foo'].freejoint) # True print(joints[1] == mjcf_model.worldbody.body['foo'].body['bar'].joint[0]) # True ``` Note that the order of elements returned by `find_all` is the same as the order in which they are declared in the model. ### Modifying XML attributes Attributes can be modified, added, or removed: ```python my_geom.pos = [1, 2, 3] print(my_geom.pos) # np.array([1., 2., 3.], dtype=float) my_geom.quat = [0, 1, 0, 0] print(my_geom.quat) # np.array([0., 1., 0., 0.], dtype=float) del my_geom.quat print(my_geom.quat) # None ``` Schema violations result in errors: ```python print(my_geom.poss) # raise AttributeError (no child or attribute called poss) my_geom.pos = 'invalid' # raise ValueError (assigning string to array) my_geom.pos = [1, 2, 3, 4, 5, 6] # raise ValueError (array length is too long) # raise ValueError (mass is a required attribute of ) del mjcf_model.find('body', 'foo').inertial.mass ``` ### Uniqueness of identifiers PyMJCF enforces the uniqueness of "identifier" attributes within a model. Identifiers consist of the `class` attribute of a ``, and all `name` attributes. Their uniqueness is only enforced within a particular namespace. For example, a `` is allowed to have the same name as a ``, whereas `` and `` actuators cannot have the same name. ```python mjcf_model.worldbody.add('geom', name='my_geom') foo = mjcf_model.worldbody.find('body', 'foo') foo.add('my_geom') # Error, duplicated geom name foo.add('foo') # OK, a geom can have the same name as a body mjcf_model.find('geom', 'foo').name = 'my_geom' # Error, duplicated geom name ``` ### Reference attributes Some attributes are references to other elements. For example, the `joint` attribute of an actuator refers to a `` element in the model. An `mjcf.Element` can be directly assigned to these reference attributes: ```python my_hinge = mjcf_model.find('joint', 'my_hinge') my_actuator = mjcf_model.actuator.add('velocity', joint=my_hinge) ``` This is the recommended way to assign reference attributes, since it guarantees that the reference is not invalidated if the referenced element is renamed. Alternatively, a string can also be assigned to reference attributes. In this case, PyMJCF does **not** attempt to verify that the named element actually exists in the model. IMPORTANT: If the element being referenced is in a different model to the reference attribute (e.g. in an attached model), the reference **must** be created by directly assigning an `mjcf.Element` object to the attribute rather than a string. Strings assigned to reference attributes cannot contain '/', since they are automatically scoped by PyMJCF upon attachment. ## Attaching models In this section we will refer to an `mjcf.RootElement` simply as a "model". Models can be _attached_ to other models in order to create compositional scenes. ```python arena = mjcf.RootElement() arena.worldbody.add('geom', name='ground', type='plane', size=[10, 10, 1]) robot = mjcf.from_xml_file('robot.xml') arena.attach(robot) ``` We refer to `arena` as the _parent model_, and `robot` as the _child model_ (or the _attached model_). ### Attachment frames When a model is attached to a site, an empty body is created in the parent model. This empty body is called an _attachment frame_. The attachment frame is created as a child of the body that contains the attachment site, and it has the same position and orientation as the site. When the XML is generated, the attachment frame's contents shadow the contents of the attached model's ``. The attachment frame's name in the generated XML is the child's `fully/qualified/prefix/`. The trailing slash ensures that the attachment frame's name never collides with a user-defined body. More concretely, if we have the following parent and child models: ```xml ``` Then the final generated XML will be: ```xml ``` IMPORTANT: The attachment frame is created _transparently_ to the user. In particular, it is NOT treated as a regular `body` by PyMJCF. Its name in the generated XML should be considered implementation detail and should NOT be relied on. Having said that, it is sometimes necessary to access the attachment frame, for example to add a joint between the parent and the child model. The easiest way to do this is to hold a reference to the object returned by a call to `attach`: ```python attachment_frame = parent_model.attach('child') attachment_frame.add('freejoint') ``` Alternatively, if a model has already been attached, the `find` function can be used with the `attachment_frame` namespace in order to retrieve the attachment frame. The `get_attachment_frame` convenience function in `mjcf.traversal_utils` can find the child model's attachment frame without needing access to the parent model. ```python frame_1 = parent_model.find('attachment_frame', 'child') # Convenience function: get the attachment frame directly from a child model frame_2 = mjcf.traversal_utils.get_attachment_frame(child_model) print(frame_1 == frame_2) # True ``` IMPORTANT: To encourage good modeling practices, the only allowed direct children of an attachment frame are `` and ``. Other types of elements should instead add be added to the `` of the attached model. ### Element ownership IMPORTANT: Elements of child models do **not** appear when traversing through the parent model. ### Default classes PyMJCF ensures that default classes of a parent model _never_ affect any of its child models. This minimises the possibility that two models become subtly "incompatible", as a model always behaves in the same way regardless of what it is attached to. The way that PyMJCF achieves this in practice is to move everything in a model's global `` context into a default class named `/`. In other words, a PyMJCF-generated model never has anything in the global default context. Instead, the generated model always looks like: ```xml ``` IMPORTANT: This transformation is _transparent_ to the user. Within Python, the above geom rgba setting is accessed as if it were a global default, i.e. `mjcf_model.default.geom.rgba`. Generally speaking, users should never have to worry about PyMJCF's internal handling of defaults. When a model is attached, its `/` default class turns into `fully/qualified/prefix/`. The trailing slash ensures that this transformation never conflicts with a user-named default class. More specifically, if we have the following parent and child models: ```xml ``` Then the final generated XML will be: ```xml ``` ### Global options A model cannot be attached to another model if _any_ of the global options are different. Global options consist of attributes of ``, `