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 = ""
self.append(inertial)
def add_mesh(self, part: Part, class_: str, T_world_link: np.ndarray, mesh: Mesh):
"""
Add a mesh node (e.g. STL) to the MuJoCo file
"""
# Retrieving mesh file and material name
mesh_file = os.path.relpath(self.config.output_directory + "/" + mesh.filename, self.config.asset_path(""))
mesh_file_no_ext = ".".join(os.path.basename(mesh_file).split(".")[:-1])
material_name = mesh_file_no_ext + "_material"
# Relative frame
T_link_part = np.linalg.inv(T_world_link) @ part.T_world_part
# Adding the geom node
geom = f'"
# Adding the mesh and material to appear in the assets section
self.meshes.append(mesh_file)
self.materials[material_name] = mesh.color
self.append(geom)
def add_shape(
self, part: Part, class_: str, T_world_link: np.ndarray, shape: Shape
):
"""
Add pure shape geometry.
"""
geom = f'"
self.append(geom)
def add_geometries(self, part: Part, T_world_link: np.ndarray):
"""
Add a part geometries
"""
for shape in part.shapes:
if shape.visual:
self.add_shape(part, "visual", T_world_link, shape)
if shape.collision:
self.add_shape(part, "collision", T_world_link, shape)
for mesh in part.meshes:
if mesh.visual:
self.add_mesh(part, "visual", T_world_link, mesh)
if mesh.collision:
self.add_mesh(part, "collision", T_world_link, mesh)
def add_joint(self, joint: Joint):
self.append(f"")
if joint.joint_type == "fixed":
self.append(f'')
return
joint_xml: str = ""
self.append(joint_xml)
def add_frame(
self,
frame: str,
T_world_link: np.ndarray,
T_world_frame: np.ndarray,
group: int = 0,
):
self.append(f"")
T_link_frame = np.linalg.inv(T_world_link) @ T_world_frame
site: str = f'"
self.append(site)
def add_link(
self,
robot: Robot,
link: Link,
parent_joint: Joint | None = None,
T_world_parent: np.ndarray = np.eye(4),
):
"""
Adds a link recursively to the URDF file
"""
if parent_joint is None:
T_world_link = np.eye(4)
else:
T_world_link = parent_joint.T_world_joint
childclass = ""
if parent_joint is None:
childclass = f'childclass="{self.default_class}" '
self.append(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"))