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:

  1. Well-formedness — ET.parse() raises ParseError on malformed XML. Run it on every generated file.
  2. Schema and required attributes — verify that mandatory attributes such as T_END, V_X, and SCALE are present and numeric. A small dictionary of expected types is enough.
  3. Physical plausibility — check ranges: T_END between 0.05 and 0.30 s, |V_X| between 5 and 25 m/s, SCALE between 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.

Deck Generation and Validation Pipeline deck_template.xml $t_end $v_init $scale Case table name / t_end v_init / scale (list of dicts) Substitution Template.safe_substitute or ET deepcopy + set() → per-case XML L1: ET.parse() well-formedness L2: required attrs present + numeric L3: range check physical plausibility decks/*.xml ready for solver Failure symptoms ParseError → bad XML KeyError → template drift solver abort → range fail

Practical checklist for a generation script

StageActionFailure signal
Template loadRead once, keep in memoryFileNotFoundError
Substitutionsafe_substitute or ET deepcopyUnreplaced $name in output
WriteUTF-8, xml_declaration=TrueEncoding mojibake in solver log
Validate L1ET.parse()ParseError with line number
Validate L2Required attribute checkAttributeError / None
Validate L3Range assertionAssertionError with value
Keep the template under version control and treat the case table as data, not code. When a placeholder is renamed, the L2 check fails immediately for every case, which is exactly the feedback you want before launching a batch.
Never write generated decks directly into the directory the solver watches. Write to a staging folder, run validation, and only then move validated files into the run directory. This prevents a partially written deck from being picked up by a batch scheduler.

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.