mujoco_worldgen/parser/parser.py (154 lines of code) (raw):
from collections import OrderedDict
from decimal import getcontext
from os.path import abspath, dirname, join, exists
from mujoco_worldgen.transforms import closure_transform
import numpy as np
import xmltodict
import os
from mujoco_worldgen.util.types import accepts, returns
from mujoco_worldgen.util.path import worldgen_path
from mujoco_worldgen.parser.normalize import normalize, stringify
getcontext().prec = 4
'''
This directory should contain all XML string processing.
No other files should be manually converting types for XML processing.
API:
parse_file() - takes in path to mujoco file and returns normalized dictionary
unparse_dict() - takes in an xml dictionary and returns an XML string
NOTE FOR TRANSFORMS:
The internal xml_dict layout passed into transforms is the one returned by
normalize() -- see that docstring for more details on its layout.
Every other method should be considered internal!
'''
@accepts(str, bool)
@returns(OrderedDict)
def parse_file(xml_path, enforce_validation=True):
'''
Reads xml from xml_path, consolidates all includes in xml, and returns
a normalized xml dictionary. See preprocess()
'''
# TODO: use XSS or DTD checking to verify XML structure
with open(xml_path) as f:
xml_string = f.read()
xml_doc_dict = xmltodict.parse(xml_string.strip())
assert 'mujoco' in xml_doc_dict, "XML must contain <mujoco> node"
xml_dict = xml_doc_dict['mujoco']
assert isinstance(xml_dict, OrderedDict), \
"Invalid node type {}".format(type(xml_dict))
preprocess(xml_dict, xml_path, enforce_validation=enforce_validation)
return xml_dict
@accepts(OrderedDict)
@returns(str)
def unparse_dict(xml_dict):
'''
Convert a normalized XML dictionary into a XML string. See stringify().
Note: this modifies xml_dict in place to have strings instead of values.
'''
stringify(xml_dict)
xml_doc_dict = OrderedDict(mujoco=xml_dict)
return xmltodict.unparse(xml_doc_dict, pretty=True)
@accepts(OrderedDict, str, bool)
def preprocess(xml_dict, root_xml_path, enforce_validation=True):
'''
All the steps to turn XML into Worldgen readable form:
- normalize: changes strings to floats / vectors / bools, and
turns consistently nodes to OrderedDict and List
- name_meshes: some meshes are missing names. Here we give default names.
- rename_defaults: some defaults are global, we give them names so
they won't be anymore.
- extract_includes: recursively, we extract includes and merge them.
- validate: we apply few final checks on the structure.
'''
normalize(xml_dict)
set_absolute_paths(xml_dict, root_xml_path)
extract_includes(xml_dict, root_xml_path, enforce_validation=enforce_validation)
if enforce_validation:
validate(xml_dict)
@accepts(OrderedDict, str)
def set_absolute_paths(xml_dict, root_xml_path):
dirnames = ["@meshdir", "@texturedir"]
if "compiler" in xml_dict:
for drname in dirnames:
if drname in xml_dict["compiler"]:
asset_dir = worldgen_path('assets') + '/'
path = xml_dict["compiler"][drname]
if path[0] != "/":
relative_path = os.path.dirname(root_xml_path) + "/" + path
xml_dict["compiler"][drname] = os.path.abspath(relative_path)
elif path.find(asset_dir) > -1:
xml_dict["compiler"][drname] = worldgen_path(
'assets', path.split(asset_dir)[-1])
@accepts(OrderedDict, str, bool)
def extract_includes(xml_dict, root_xml_path, enforce_validation=True):
'''
extracts "include" xmls and substitutes them.
'''
def transform_include(node):
if "include" in node:
if isinstance(node["include"], OrderedDict):
node["include"] = [node["include"]]
include_xmls = []
for include_dict in node["include"]:
include_path = include_dict["@file"]
if not exists(include_path):
include_path = join(dirname(abspath(root_xml_path)), include_path)
assert exists(include_path), "Cannot include file: %s" % include_path
with open(include_path) as f:
include_string = f.read()
include_xml = xmltodict.parse(include_string.strip())
closure_transform(transform_include)(include_xml)
assert "mujocoinclude" in include_xml, "Missing <mujocoinclude>."
include_xmls.append(include_xml["mujocoinclude"])
del node["include"]
for include_xml in include_xmls:
preprocess(include_xml, root_xml_path, enforce_validation=enforce_validation)
update_mujoco_dict(node, include_xml)
closure_transform(transform_include)(xml_dict)
@accepts(OrderedDict, OrderedDict)
@returns(None.__class__)
def update_mujoco_dict(dict_a, dict_b):
'''
Update mujoco dict_a with the contents of another mujoco dict_b.
'''
other = (str, int, float, np.ndarray, tuple)
for key, value in dict_b.items():
if key not in dict_a:
dict_a[key] = value
elif isinstance(dict_a[key], list):
assert isinstance(value, list), "Expected %s to be a list" % value
dict_a[key] += value
elif isinstance(value, other):
assert(isinstance(dict_a[key], other))
assert dict_a[key] == value, "key=%s\n,Trying to merge dictionaries. " \
"They don't agree on value: %s vs %s" % (key, dict_a[key], value)
else:
assert isinstance(dict_a[key], OrderedDict), "dict_a = %s\nkey=%s\nExpected dict_a[key] to be a OrderedDict." % (dict_a, key)
assert(isinstance(value, OrderedDict))
update_mujoco_dict(dict_a[key], value)
@accepts(OrderedDict)
def validate(xml_dict):
'''
If we make assumptions elsewhere in XML processing, then they should be
enforced here.
'''
# Assumption: radians for angles, "xyz" euler angle sequence, etc.
values = {'@coordinate': 'local',
'@angle': 'radian',
'@eulerseq': 'xyz'}
for key, value in values.items():
if key in xml_dict:
assert value == xml_dict[key], 'Invalid value for \"%s\". We support only \"%s\"' % (key, value)
# Assumption: all meshes have name
if "asset" in xml_dict and "mesh" in xml_dict["asset"]:
for mesh in xml_dict["asset"]["mesh"]:
assert "@name" in mesh, "%s is missing name" % mesh
# Assumption: none all the default classes is global.
if "default" in xml_dict:
for key, value in xml_dict["default"].items():
assert key == "default", "Dont use global variables in default %s %s" % (key, value)
# Assumption: all joints have name.
def assert_joint_names(node):
if "joint" in node:
for joint in node["joint"]:
assert "@name" in joint, "Missing name for %s" % joint
if "worldbody" in xml_dict:
closure_transform(assert_joint_names)(xml_dict["worldbody"])