from __future__ import annotations import numpy as np import os import fnmatch from .message import success, warning, info from .robot import Robot, Link, Part, Joint, Closure from .config import Config from .geometry import Box, Cylinder, Sphere, Mesh, Shape from .exporter import Exporter from .exporter_utils import xml_escape, rotation_matrix_to_rpy from transforms3d.quaternions import mat2quat class ExporterMuJoCo(Exporter): def __init__(self, config: Config | None = None): super().__init__() self.config: Config = config self.no_dynamics: bool = False self.additional_xml: str = "" self.meshes: list = [] self.materials: dict = {} if config is not None: self.equalities = self.config.get("equalities", {}) self.no_dynamics = config.no_dynamics additional_xml_file = config.get("additional_xml", None, required=False) if isinstance(additional_xml_file, str): self.add_additional_xml(additional_xml_file) elif isinstance(additional_xml_file, list): for filename in additional_xml_file: self.add_additional_xml(filename) def add_additional_xml(self, xml_file: str): self.additional_xml += f"" with open(self.config.output_directory + "/" + xml_file, "r") as file: self.additional_xml += file.read() def append(self, line: str): self.xml += line def build(self, robot: Robot): self.xml = "" self.append('') self.append("") if self.config: self.append(f"") self.append(f'') self.append( f'' ) # Boilerplate self.default_class = robot.name self.append("") self.append(f'') self.append('') self.append('') self.append('') self.append('') self.append("") self.append('') self.append('') self.append("") self.append("") self.append("") if self.additional_xml: self.append(self.additional_xml) # Adding robot links self.append("") for base_link in robot.base_links: self.add_link(robot, base_link) self.append("") # Asset (mesh & materials) self.append("") for mesh_file in set(self.meshes): self.append(f'') for material_name, color in self.materials.items(): color_str = "%g %g %g %g" % self.config.round(tuple(color)) self.append(f'') self.append("") # Adding actuators self.add_actuators(robot) # Adding equalities (loop closure) self.add_equalities(robot) self.append("") return self.xml def add_actuators(self, robot: Robot): self.append("") for joint in robot.joints: if joint.joint_type == "fixed": continue # Suppose joints with relation equality is not actuated, unless specified guess_actuated = joint.relation is None if ( joint.properties.get("actuated", guess_actuated) and joint.joint_type != Joint.BALL ): type = joint.properties.get("type", "position") actuator_class = joint.properties.get("class", self.default_class) actuator: str = f'<{type} class="{actuator_class}" name="{joint.name}" joint="{joint.name}" ' for key in "kp", "kv", "dampratio": if key in joint.properties: actuator += f'{key}="{joint.properties[key]}" ' if "forcerange" in joint.properties: actuator += f'forcerange="-{joint.properties["forcerange"]} {joint.properties["forcerange"]}" ' joint_limits = joint.properties.get("limits", joint.limits) limits_are_set = joint.properties.get("limits", False) != False if joint_limits and (type == "position" or limits_are_set): if joint.properties.get("range", True) and type == "position": actuator += f'inheritrange="1" ' else: actuator += f'ctrlrange="{joint_limits[0]} {joint_limits[1]}" ' actuator += "/>" self.append(actuator) self.append("") def get_equality_attributes(self, closure: Closure) -> str: all_attributes = {} for name, attributes in self.equalities.items(): if fnmatch.fnmatch(closure.frame1, name) and fnmatch.fnmatch( closure.frame2, name ): all_attributes.update(attributes) if len(all_attributes) > 0: return ( " ".join([f'{key}="{value}"' for key, value in all_attributes.items()]) + " " ) return "" def add_equalities(self, robot: Robot): self.append("") for closure in robot.closures: attributes = self.get_equality_attributes(closure) if closure.closure_type == Closure.FIXED: self.append( f'' ) elif closure.closure_type == Closure.REVOLUTE: self.append( f'' ) elif closure.closure_type == Closure.BALL: self.append( f'' ) else: print( warning( f"Closure type: {closure.closure_type} is not supported with MuJoCo equality constraints" ) ) for joint in robot.joints: if joint.relation is not None: self.append( f'' ) self.append("") def add_inertial(self, mass: float, com: np.ndarray, inertia: np.ndarray): # Ensuring epsilon masses and inertias mass = max(1e-9, mass) inertia[0, 0] = max(1e-9, inertia[0, 0]) inertia[1, 1] = max(1e-9, inertia[1, 1]) inertia[2, 2] = max(1e-9, inertia[2, 2]) # Populating body inertial properties # https://mujoco.readthedocs.io/en/stable/XMLreference.html#body-inertial inertial: str = "") if joint.joint_type == "fixed": self.append(f'') return joint_xml: str = "") T_link_frame = np.linalg.inv(T_world_link) @ T_world_frame site: str = f'") T_parent_link = np.linalg.inv(T_world_parent) @ T_world_link self.append( f'' ) if parent_joint is None: if not link.fixed: self.append(f'') else: self.add_joint(parent_joint) # Adding inertial properties mass, com, inertia = link.get_dynamics(T_world_link) self.add_inertial(mass, com, inertia) # Adding geometry objects for part in link.parts: self.append(f"") self.add_geometries(part, T_world_link) # Adding frames attached to current link for frame, T_world_frame in link.frames.items(): self.add_frame(frame, T_world_link, T_world_frame, group=3) # Adding joints and children links for joint in robot.get_link_joints(link): self.add_link(robot, joint.child, joint, T_world_link) self.append("") def pos_quat(self, matrix: np.ndarray) -> str: """ Turn a transformation matrix into 'pos="..." quat="..."' attributes """ pos = matrix[:3, 3] quat = mat2quat(matrix[:3, :3]) xml = 'pos="%g %g %g" quat="%g %g %g %g"' % self.config.round((*pos, *quat)) return xml def write_xml(self, robot: Robot, filename: str) -> str: super().write_xml(robot, filename) dirname = os.path.dirname(filename) scene_filename = dirname + "/scene.xml" if not os.path.exists(scene_filename): scene_xml: str = ( os.path.dirname(os.path.realpath(__file__)) + "/assets/scene.xml" ) scene_xml = open(scene_xml, "r").read() scene_xml = scene_xml.format(robot_filename=os.path.basename(filename)) with open(scene_filename, "w") as file: file.write(scene_xml) print(success(f"* Writing scene.xml")) else: print(info(f"* scene.xml already exists, not over-writing it"))