4. Generating MADYMO input decks programmatically
MADYMO input decks are plain-text XML files. That single fact is what makes automation possible: if a deck is text, Python can read it, modify it, and write it back. In practice, however, hand-editing XML for every load case is slow and error-prone. A frontal ODB matrix with three restraint variants, two occupant sizes, and four pulse severities already produces twenty-four decks, each differing only in a handful of numeric values. This section shows how to generate those decks from a single template using Python's string.Template and xml.etree.ElementTree, and how to validate the result before it reaches the solver.
Why templating beats string concatenation
The naive approach is to build the deck with f-strings or + concatenation. This breaks as soon as the deck contains literal braces, XML entities, or repeated blocks. A better pattern is to keep one authoritative template file and substitute only the values that change. Two mechanisms are worth knowing:
- Text-level substitution with
string.Template— fast, readable, and safe for placeholders like$pulse_scale. It does not understand XML structure, so it cannot validate that a substituted value is a legal attribute. - Tree-level substitution with
xml.etree.ElementTree— parses the deck into elements, lets you address nodes by tag or XPath, and writes valid XML back. Slower, but structurally safe.
Use text-level substitution for values that appear in many places (pulse scale factors, gravity, time step), and tree-level substitution when you need to add, remove, or reorder elements (extra CONTACT definitions, additional BELT segments).
A minimal template
Consider a simplified deck fragment with three placeholders: the simulation end time, the pulse scale factor, and the initial velocity.
<?xml version="1.0" encoding="UTF-8"?>
<MADYMO>
<CONTROL>
<TIME T_END="$t_end" />
</CONTROL>
<INITIAL>
<VELOCITY V_X="$v_init" />
</INITIAL>
<LOAD>
<ACCELERATION SCALE="$pulse_scale" FILE="pulse.tab" />
</LOAD>
</MADYMO>
The Python driver reads the template, substitutes the three values, and writes a case-specific file. The safe_substitute method is preferred during development because it leaves unknown placeholders untouched instead of raising KeyError — useful when a template is shared across several case families.
from string import Template
from pathlib import Path
tpl = Template(Path("deck_template.xml").read_text())
cases = [
{"name": "odb_50kph_driver", "t_end": "0.120", "v_init": "-13.89", "pulse_scale": "1.00"},
{"name": "odb_64kph_driver", "t_end": "0.120", "v_init": "-17.78", "pulse_scale": "1.25"},
{"name": "odb_64kph_pass", "t_end": "0.120", "v_init": "-17.78", "pulse_scale": "1.25"},
]
out_dir = Path("decks"); out_dir.mkdir(exist_ok=True)
for c in cases:
text = tpl.safe_substitute(c)
(out_dir / f"{c['name']}.xml").write_text(text)
Note the sign convention: V_X is negative because the vehicle travels in the +X direction and the occupant reference frame is fixed to the vehicle, so the initial velocity applied to the occupant is opposite to the travel direction. Keep this convention consistent across every generated deck; mixing signs between cases is one of the most common sources of non-physical results.
Parameter substitution at the tree level
When a case needs a different number of elements — for example, a second belt segment for a pretensioner study — text substitution is awkward. Parse the template once, deep-copy the tree per case, and mutate attributes directly:
import copy, xml.etree.ElementTree as ET
base = ET.parse("deck_template.xml")
for c in cases:
tree = copy.deepcopy(base)
root = tree.getroot()
root.find("./CONTROL/TIME").set("T_END", c["t_end"])
root.find("./INITIAL/VELOCITY").set("V_X", c["v_init"])
root.find("./LOAD/ACCELERATION").set("SCALE", c["pulse_scale"])
tree.write(out_dir / f"{c['name']}.xml", encoding="UTF-8", xml_declaration=True)
This approach guarantees well-formed XML output and lets you assert structural invariants (for example, that exactly one TIME element exists) before writing.
Validation before the solver sees the deck
Generation without validation just moves the error from your editor to the solver log. A three-layer check catches most problems cheaply:
- Well-formedness —
ET.parse()raisesParseErroron malformed XML. Run it on every generated file. - Schema and required attributes — verify that mandatory attributes such as
T_END,V_X, andSCALEare present and numeric. A small dictionary of expected types is enough. - Physical plausibility — check ranges:
T_ENDbetween 0.05 and 0.30 s,|V_X|between 5 and 25 m/s,SCALEbetween 0.5 and 2.0. Values outside these bands are almost always unit or sign mistakes.
def validate(path):
tree = ET.parse(path) # layer 1
root = tree.getroot()
t_end = float(root.find("./CONTROL/TIME").get("T_END")) # layer 2
v_x = float(root.find("./INITIAL/VELOCITY").get("V_X"))
scale = float(root.find("./LOAD/ACCELERATION").get("SCALE"))
assert 0.05 <= t_end <= 0.30, f"T_END out of range: {t_end}" # layer 3
assert 5.0 <= abs(v_x) <= 25.0, f"V_X out of range: {v_x}"
assert 0.5 <= scale <= 2.0, f"SCALE out of range: {scale}"
return True
Typical failure symptoms map cleanly onto these layers. A ParseError usually means an unescaped & or a stray placeholder that was never substituted. A KeyError or AttributeError means the template and the case dictionary have drifted apart — often after someone renamed a placeholder. A solver abort with "negative time step" or "zero mass" almost always traces back to a plausibility failure that the range check would have caught.
Practical checklist for a generation script
| Stage | Action | Failure signal |
|---|---|---|
| Template load | Read once, keep in memory | FileNotFoundError |
| Substitution | safe_substitute or ET deepcopy | Unreplaced $name in output |
| Write | UTF-8, xml_declaration=True | Encoding mojibake in solver log |
| Validate L1 | ET.parse() | ParseError with line number |
| Validate L2 | Required attribute check | AttributeError / None |
| Validate L3 | Range assertion | AssertionError with value |
With templating, substitution, and three-layer validation in place, the generation step becomes deterministic and auditable. The next step — driving the solver and collecting results — builds directly on the clean, validated deck set produced here.