Source code for myoconverter.xml.converter

""" This module contains the OpenSim-to-MuJoCo model (XML file) convertion main access points.

You can convert an OpenSim XML model file to MuJoCo by calling the :py:func:`convert` function, or by running this
module.

Example:

  in a command shell::

    python converter.py /path/to/opensim/model.xml /output/directory/for/mujoco/model/


  or, in a Python file::

    from myoconverter.xml.converter import convert
    convert(xml_file, output_folder, **kwargs)

"""

import argparse
from loguru import logger
from lxml import etree

from myoconverter.xml import config as cfg
from myoconverter.xml.utils import find_element_by_name, split_name, create_keyframe


[docs]def convert(xml_file, output_folder, **kwargs): """ Convert an OpenSim XML model file to MuJoCo XMl model file. :param xml_file: Path to the OpenSim XML model file :param output_folder: Path to folder where converted model is saved :param kwargs: geometry_folder (str), add_ground_geom (bool), treat_as_normal_path_point (bool) :return: Path to the MuJoCo XML file """ # First, initialise config (read and parse OpenSim and MuJoCo XML files, set variables etc.) cfg.initialise(xml_file, output_folder, **kwargs) logger.info(f"Commencing the conversion procedure!") # Parse ground. The ground may have attached geometries and wrapping objects _parse_ground() # Parse constraints. The constraints need to be parsed before joints, because we might need to update the polycoefs # of some joint constraints when parsing joints _parse_constraints() # Parse bodies and joints _parse_bodies_and_joints() # Parse forces _parse_forces() # Parse markers -- needed for later optimization procedures _parse_markers() # Set keyframe _set_keyframe() # Copy credits from the OpenSim model _copy_credits() # Finally, save the MuJoCo model into XML file logger.info(f"Writing the converted model into {cfg.OUTPUT_XML_FILE}") etree.ElementTree(cfg.MUJOCO).write(cfg.OUTPUT_XML_FILE, pretty_print=True) logger.info("All good, conversion procedure is finished") # Return path to converted file return cfg.OUTPUT_XML_FILE
[docs]def _parse_bodies_and_joints(): """ Parse OpenSim `BodySet` and `JointSet`. :return: None """ # Start from ground and go through joints recursively (depth-first), and add bodies, geoms, joints to MuJoCo model logger.info("Starting to parse bodies, wrapping objects, and joints") _add_bodies_and_joints(f"/{cfg.O_GROUND.attrib['name']}", cfg.M_WORLDBODY, root_body=True)
[docs]def _add_bodies_and_joints(parent_name, current_body, root_body=False): """ Add OpenSim `Body` and related `Joint`s to MuJoCo model in a recurrent fashion. :param parent_name: Name of parent `Body` :param current_body: Pointer to current body (XML element) in the MuJoCo XML file :param root_body: Boolean to indicate whether current body is the root of the kinematic chain or not :return: None """ # Find ALL frames with 'parent_name' as socket parent. This will contain some incorrect frames as well, # we'll need to cut them by checking whether they are socket_parent_frames or socket_child_frames socket_parent = cfg.O_JOINTSET.xpath(f".//socket_parent[text()='{parent_name}']") for s in socket_parent: # Get parent until WeldJoint, CustomJoint, etc frames = s.getparent().getparent() joint = frames.getparent() # Get parent and child socket frames (can be PhysicalOffsetFrame, or PhysicalFrame? Let's use wildcard # in the find so we don't need to care about the tag) socket_parent_frame = frames.find(f".//*[@name='{joint.find('socket_parent_frame').text}']") socket_child_frame = frames.find(f".//*[@name='{joint.find('socket_child_frame').text}']") # Ignore child socket frames if socket_child_frame.find("socket_parent").text == parent_name: continue # Find child body of joint child_body = find_element_by_name(cfg.OPENSIM, split_name(socket_child_frame.find("socket_parent").text)) # Parse body next_body = cfg.BODY_PARSER.parse(child_body, socket_parent_frame=socket_parent_frame, socket_child_frame=socket_child_frame, current_body=current_body, root_body=root_body) # Parse joint cfg.JOINT_PARSER.parse(joint, socket_parent_frame=socket_parent_frame, socket_child_frame=socket_child_frame, pointer=next_body, root_body=root_body) # Move to next joint parent_name = socket_child_frame.find('socket_parent').text _add_bodies_and_joints(parent_name, next_body)
[docs]def _parse_ground(): """ Parse OpenSim `Ground`. :return: None """ logger.info("Parsing the ground") cfg.BODY_PARSER.parse(cfg.O_GROUND, add_ground_geom=cfg.ADD_GROUND_GEOM)
[docs]def _parse_constraints(): """ Parse OpenSim `ConstraintSet`. :return: None """ logger.info("Starting to parse constraints") cfg.CONSTRAINT_PARSER.parse_all(cfg.O_CONSTRAINTSET)
[docs]def _parse_forces(): """ Parse OpenSim `ForceSet`. :return: None """ logger.info("Starting to parse forces, including path points and wrap paths") cfg.FORCE_PARSER.parse_all(cfg.O_FORCESET)
[docs]def _parse_markers(): """ Parse OpenSim `MarkerSet`. :return: None """ logger.info("Starting to parse markers") cfg.MARKER_PARSER.parse_all(cfg.O_MARKERSET)
[docs]def _set_keyframe(): """ Create a keyframe for the MuJoCo model. The keyframe defines a default pose, where all dependent joints have been properly initialised. :return: None """ logger.info("Setting the default keyframe") create_keyframe(cfg.MUJOCO, cfg.M_WORLDBODY, cfg.M_EQUALITY)
[docs]def _copy_credits(): """ Copy credits from the OpenSim model. :return: None """ # Get credits credits = cfg.OPENSIM.find("credits") # Add as comment if they exist if credits is not None: comment = etree.Comment(f" Credits from the original OpenSim model. Note! If the following has any license information, it applies to the original OpenSim model. Credits: {credits.text} ") cfg.MUJOCO.insert(0, comment) # Add our credits as a comment too comment = etree.Comment(" This model has been converted from an OpenSim model. Model conversion by MyoConverter https://github.com/MyoHub/myoconverter. This model is licensed under Apache 2.0. ") cfg.MUJOCO.insert(0, comment)
if __name__ == "__main__":
[docs] argparser = argparse.ArgumentParser(description='Convert an OpenSim model into a MuJoCo model.' 'Only Works with OpenSim v4 models.')
""" ArgumentParser: Parse arguments when running module as a Python script. """ argparser.add_argument('xml_file', type=str, help='Path to an OpenSim model XML file') argparser.add_argument('output_folder', type=str, help="Path to an output folder. The converted model will be saved here.") argparser.add_argument('--geometry_folder', type=str, default=None, help='Path to the Geometry folder (by default uses folder of given OpenSim file)') argparser.add_argument('--add_ground_geom', default=False, action="store_true", help="If true, a geom (of type plane) is added to the MuJoCo model as ground") argparser.add_argument('--treat_as_normal_path_point', default=False, action="store_true", help="If true, MovingPathPoints and ConditionalPathPoints will be treated as normal " "PathPoints") args = argparser.parse_args() # Do the conversion convert(**vars(args))