4. Interfacing Python with MADYMO

MADYMO is a solver-centric application. Although it ships with a graphical user interface (MADYMO Commander / XMADgic), the most reliable and automation-friendly way to drive it from Python is through its command-line executable. Every parametric study ultimately reduces to the same primitive operation: launch the solver on a specific input deck, wait for it to finish, inspect whether it succeeded, and collect the outputs. This chapter explains how to perform that primitive operation correctly, how to scale it into batch runs, and how to interpret the process return code so that a failed run never silently corrupts your study results.

The MADYMO command-line executable

On a typical installation the solver binary is named madymo (Linux) or madymo.exe (Windows), located under the installation's bin directory. The minimal invocation is:

madymo -i model.xml -o run01

Here -i specifies the input deck (an XML file in MADYMO's native format, or a legacy .dat file), and -o sets the output base name. The solver then produces a family of files sharing that base name: run01.log (solver log), run01.xml or run01.lsda (time-history results), and possibly run01.ps or run01.erg depending on the requested output. Additional switches matter for automation:

  • -nothreads / thread control — pin the number of threads so that timing and numerical results stay reproducible across runs.
  • -licwait <seconds> — wait for a license instead of failing immediately when all seats are busy; essential in batch environments.
  • -timeout <seconds> — abort a run that exceeds a wall-clock budget, preventing a single divergent model from stalling the whole study.

Because these flags differ slightly between MADYMO versions, always confirm them with madymo -help on the target machine before hard-coding them into a script.

Launching the solver from Python

The subprocess module is the correct tool. Avoid os.system, which gives you no access to the exit status in a portable way. A robust single-run wrapper looks like this:

import subprocess, pathlib

def run_madymo(deck: pathlib.Path, outbase: str, workdir: pathlib.Path,
               timeout: int = 3600) -> subprocess.CompletedProcess:
    cmd = ["madymo", "-i", str(deck), "-o", outbase,
           "-licwait", "600", "-timeout", str(timeout)]
    return subprocess.run(
        cmd,
        cwd=workdir,
        capture_output=True,
        text=True,
        timeout=timeout + 60,   # Python-side guard, slightly larger
    )

Three details deserve attention. First, cwd=workdir isolates each run in its own directory, so output files from different parameter combinations never overwrite one another. Second, capture_output=True keeps stdout and stderr in memory for logging; for long runs you may prefer redirecting to a file to avoid memory growth. Third, the Python-level timeout is a safety net that is deliberately set larger than the solver's own -timeout, so the solver gets the chance to shut down cleanly and write its log before Python kills the process.

Reading the return code correctly

The CompletedProcess.returncode attribute is the single most important signal in the whole workflow. Its meaning follows the usual operating-system convention:

Return codeMeaningTypical cause
0Normal terminationSolver completed the requested simulation time
1Solver-reported errorInput deck error, convergence failure, invalid keyword
2Usage / argument errorUnknown flag, missing -i file
NegativeKilled by signalSegmentation fault, out-of-memory, external kill

A return code of 0 is necessary but not sufficient. MADYMO can terminate "normally" while still reporting warnings or an early stop in the log. Always pair the return code with a scan of the log file for the solver's own termination banner and for lines containing ERROR or WARNING. A practical check is:

def classify(result, log_path):
    if result.returncode != 0:
        return "FAILED", result.returncode
    text = log_path.read_text(errors="ignore")
    if "ERROR" in text:
        return "FAILED_LOG", 0
    if "WARNING" in text:
        return "WARN", 0
    return "OK", 0

This two-level classification prevents the classic parametric-study bug in which a run that crashed mid-simulation is treated as a valid data point, skewing every downstream trend curve.

Scaling to batch execution

A parametric study is a batch of independent runs. The simplest correct pattern is a sequential loop that records the outcome of each run:

import json, pathlib

results = []
for i, params in enumerate(cases):
    workdir = pathlib.Path(f"runs/case_{i:03d}")
    workdir.mkdir(parents=True, exist_ok=True)
    deck = write_deck(params, workdir / "model.xml")
    proc = run_madymo(deck, "run", workdir)
    status, code = classify(proc, workdir / "run.log")
    results.append({"case": i, "params": params,
                    "status": status, "rc": code})
    if status.startswith("FAILED"):
        (workdir / "stderr.txt").write_text(proc.stderr)

pathlib.Path("study_summary.json").write_text(json.dumps(results, indent=2))

Sequential execution is easy to reason about and is the right default when runs are short or when license seats are scarce. When you have many cores and enough licenses, replace the loop with a process pool:

from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor(max_workers=4) as pool:
    futures = [pool.submit(run_case, i, p) for i, p in enumerate(cases)]
    results = [f.result() for f in futures]

Keep max_workers at or below the number of available license seats; oversubscribing causes runs to block on -licwait and destroys the throughput benefit. Also ensure each worker writes to its own directory — concurrent writes to a shared output base name will corrupt results.

Failure handling and reproducibility

Two failure modes dominate real studies. The first is a solver crash (negative return code) caused by an unstable model at an extreme parameter value; capture stderr and the tail of the log, mark the case as failed, and continue — do not abort the whole study. The second is a silent license exhaustion, where every run returns code 2 or blocks indefinitely; detect this by checking for a license-related message in stderr and by monitoring wall-clock time per run. A per-run timeout, a per-case log, and a machine-readable summary file together make the study reproducible: anyone can re-run the script and obtain the same case-to-status mapping.

Log the exact command line, the working directory, and the MADYMO version for every run. When a result looks anomalous months later, this metadata is often the only way to reconstruct what actually executed.
Python → MADYMO Batch Execution and Return-Code Decision Path Python driver subprocess.run() cwd = case dir madymo -i model.xml -o run -licwait 600 -timeout 3600 MADYMO solver run.log run.xml / .lsda Outputs time-history log files Inspect returncode CompletedProcess.returncode rc == 0 scan log for ERROR → OK / WARN rc == 1 or 2 solver / usage error → FAILED rc < 0 killed by signal → CRASH study_summary.json — case, params, status, rc

The diagram above summarizes the control flow: the Python driver builds the command, the solver writes its outputs, and the return code plus a log scan determine whether a case is usable. The summary file at the bottom is what makes the study auditable — every case carries its parameters, status, and return code, so a later analysis script can filter out failures before computing trends.

Never assume a run succeeded because the output file exists. MADYMO may create a partial results file before crashing. Always gate downstream analysis on the recorded status, not on file presence.

With command-line execution, batch orchestration, and return-code handling in place, you have the mechanical foundation for any parametric study. The next step is to generate the input decks themselves programmatically, which is where templating and parameter injection come into play.