3. Parsing MADYMO input files with Python

Parsing MADYMO input files with Python 学习卡片

MADYMO input decks are XML documents, but they are not arbitrary XML. They follow a strict schema in which every physical object — a rigid body, a joint, a belt segment, a contact — is expressed as an element with a fixed set of attributes, and every simulation control is expressed as a keyword block. Before any automation can be trusted, you need a parser that understands this structure rather than one that merely happens to read the file. This section builds that parser incrementally: first the document skeleton, then the keyword-block layer, then extraction patterns that survive real-world decks.

The document skeleton

A MADYMO input file opens with an XML declaration and a root element, conventionally <MADYMO>, which carries a version attribute. Inside the root, the deck is divided into a small number of top-level sections. The most important are <MODEL>, which holds the physical system, and <CONTROL>, which holds solver settings. A minimal skeleton looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<MADYMO version="7.7">
  <MODEL>
    <BODY id="1" name="torso" mass="32.0">
      <INITIAL.POSITION x="0.0" y="0.0" z="0.55"/>
    </BODY>
    <JOINT id="2" name="hip" type="spherical">
      <BODY.REF body1="1" body2="3"/>
    </JOINT>
  </MODEL>
  <CONTROL>
    <TIME t_end="0.12" dt="1.0e-4"/>
  </CONTROL>
</MADYMO>

Two conventions matter immediately. First, element names frequently contain dots — INITIAL.POSITION, BODY.REF, CONTACT.FRICTION. These are legal XML names and must be matched literally; a parser that strips or normalizes dots will silently miss data. Second, identity is carried by the id attribute, not by document order. Two bodies may appear in any order, and references between them (body1, body2) point at those ids. Any extraction logic that assumes "the third BODY is the pelvis" is fragile; resolve by id instead.

Keyword blocks versus object elements

It helps to separate the deck into two conceptual layers. Object elements describe the model: BODY, JOINT, BELT, CONTACT, SYSTEM. They are nested, they reference each other by id, and they are the target of most data extraction. Keyword blocks describe how the solver behaves: TIME, INTEGRATOR, OUTPUT, SWITCH. They are typically flat, live under <CONTROL>, and are read as configuration rather than as geometry.

The practical consequence is that a single generic walk over the tree is rarely the right tool. Object extraction wants to build an index keyed by id and type; keyword extraction wants a flat dictionary of settings. Keeping these two passes separate makes both simpler and makes failures easier to localize.

MADYMO decks are frequently generated by pre-processors and may contain comments, processing instructions, or namespace declarations. Use a parser that preserves element names exactly and does not silently drop nodes it does not recognize.

Building an id-indexed object map

The core extraction pattern is a single pass that records every object element by its id and type. Using xml.etree.ElementTree, which ships with the standard library and is sufficient for decks of this size:

import xml.etree.ElementTree as ET

OBJECT_TAGS = {"BODY", "JOINT", "BELT", "CONTACT", "SYSTEM"}

def index_objects(path):
    root = ET.parse(path).getroot()
    index = {}
    for elem in root.iter():
        tag = elem.tag
        if tag in OBJECT_TAGS:
            oid = elem.get("id")
            if oid is None:
                continue
            index[(tag, oid)] = elem
    return root, index

After this pass, index[("BODY", "1")] returns the torso element directly, regardless of where it appeared. This is the foundation for every cross-reference check that follows.

Extracting attributes with type coercion and defaults

Attributes in MADYMO decks are strings. Numeric attributes such as mass, x, t_end, and dt must be converted, and missing optional attributes must fall back to documented defaults rather than raising. A small helper keeps this consistent:

def get_float(elem, name, default=None):
    raw = elem.get(name)
    if raw is None:
        return default
    try:
        return float(raw)
    except ValueError:
        raise ValueError(
            f"{elem.tag} id={elem.get('id')}: "
            f"attribute '{name}' is not numeric: {raw!r}"
        )

The error message deliberately includes the element tag and id. When a deck has hundreds of bodies, "attribute 'mass' is not numeric" is useless; "BODY id=17: attribute 'mass' is not numeric: '32,0'" points straight at a locale-formatted decimal comma, which is a common defect in exported decks.

Resolving references and validating the graph

Once objects are indexed, reference integrity becomes a cheap check. Joints reference bodies through BODY.REF; contacts reference two bodies; belts reference attachment points. Each of these is a lookup that either succeeds or reveals a dangling id:

def check_joint_refs(index):
    problems = []
    for (tag, oid), elem in index.items():
        if tag != "JOINT":
            continue
        ref = elem.find("BODY.REF")
        if ref is None:
            problems.append(f"JOINT id={oid}: missing BODY.REF")
            continue
        for attr in ("body1", "body2"):
            target = ref.get(attr)
            if target is None:
                problems.append(f"JOINT id={oid}: missing {attr}")
            elif ("BODY", target) not in index:
                problems.append(
                    f"JOINT id={oid}: {attr}={target} not found"
                )
    return problems

Running this before a solver invocation converts a class of runtime failures into a short, readable list. In practice, dangling references are the single most common defect in hand-edited or script-generated decks, and catching them here costs milliseconds.

Reading keyword blocks as configuration

Keyword blocks are extracted differently: they are read as a flat settings map, with the block name as the key and its attributes as values. This makes it trivial to compare two decks or to assert that a batch of runs shares the same time step:

def read_control(root):
    control = root.find("CONTROL")
    if control is None:
        return {}
    settings = {}
    for block in control:
        settings[block.tag] = dict(block.attrib)
    return settings

# settings["TIME"]["t_end"] -> "0.12"
# settings["TIME"]["dt"]    -> "1.0e-4"

Note that values remain strings here. Coercion is applied only where a numeric value is actually needed, which keeps the configuration map faithful to the file and avoids surprising rounding during comparison.

Choosing a parser: ElementTree versus lxml

For decks up to a few megabytes, ElementTree is fast enough and has no external dependency, which matters for reproducible automation environments. lxml becomes worthwhile when you need XPath queries across large decks, when you must preserve comments and processing instructions for round-tripping, or when you want schema validation against the MADYMO XSD. A reasonable rule: start with ElementTree; switch to lxml only when a concrete requirement — XPath, comment preservation, or validation — appears.

Do not use regular expressions to extract attributes from MADYMO decks. Attribute order is not guaranteed, values may contain whitespace, and nested elements with the same tag name will produce ambiguous matches. Parse the XML; do not pattern-match it.

What the extraction layer should produce

A well-designed extraction layer returns three things: an id-indexed object map, a flat control-settings map, and a list of validation problems. Everything downstream — parameter sweeps, batch generation, result post-processing — consumes these three structures rather than re-parsing the file. The diagram below summarizes how a raw deck flows through these stages and where each failure mode is caught.

MADYMO deck .xml input file MODEL + CONTROL XML parse ElementTree / lxml preserve tag names Layer split object vs keyword elements Object index key = (tag, id) BODY / JOINT / BELT CONTACT / SYSTEM Control settings flat dict per block TIME / INTEGRATOR OUTPUT / SWITCH Validation & extraction output dangling id check · numeric coercion · missing BODY.REF returns: object map + settings map + problem list Caught early decimal comma missing id dangling body ref

Practical notes on real decks

  • Encoding. Declare and honor UTF-8. Decks exported from Windows tools occasionally carry a BOM; strip it before parsing or the XML declaration will be misread.
  • Large decks. For decks above roughly 50 MB, use iterparse and clear elements after processing to keep memory bounded.
  • Id uniqueness. Ids are unique per element type, not globally. A BODY and a JOINT may both have id="1", which is why the index key is the pair (tag, id).
  • Attribute case. XML attribute names are case-sensitive. mass and Mass are different attributes; match exactly as documented.

With the extraction layer in place, the deck is no longer a text file to be edited by hand but a queryable data structure. That structure is what makes the next stage — programmatic modification and batch generation — both safe and reviewable.