4. Interfacing with CAE solvers

Interfacing with CAE solvers 学习卡片

Automotive CAE workflows rarely involve a single interactive solver run. A typical crash, NVH, or durability study requires dozens to hundreds of load cases, each producing large result files that must be post-processed and compared. Python becomes valuable at exactly this boundary: it orchestrates the solver, generates the input decks, and extracts the numbers that engineers actually need. This chapter focuses on the three mechanical layers of that interface — launching solvers in batch mode, writing valid input decks programmatically, and parsing solver output reliably.

Batch Execution: Controlling the Solver Process

Most automotive solvers (Abaqus, LS-DYNA, Optistruct, Nastran, Pam-Crash) are driven from the command line. Python's subprocess module is the standard tool for invoking them. The key decision is whether to use subprocess.run() (blocking, simple) or subprocess.Popen() (non-blocking, allows streaming and parallel dispatch).

import subprocess, pathlib

def run_abaqus(job_name: str, cpus: int = 4, workdir: pathlib.Path = pathlib.Path(".")):
    cmd = ["abaqus", "job=" + job_name, f"cpus={cpus}", "interactive", "scratch=./tmp"]
    result = subprocess.run(
        cmd,
        cwd=workdir,
        capture_output=True,
        text=True,
        timeout=3600,
    )
    return result.returncode, result.stdout, result.stderr

Three practical points matter here. First, always set cwd explicitly; solvers write scratch files relative to the working directory, and mixing runs in one folder causes silent file collisions. Second, capture both stdout and stderr — many solvers report fatal errors on stderr while returning exit code 0. Third, use a timeout so a hung solver does not block an overnight batch.

For parallel dispatch across load cases, concurrent.futures.ProcessPoolExecutor is usually the right abstraction. Each worker calls run_abaqus() with a different job name. The number of workers should match the number of solver licenses and available cores, not the number of load cases.

Never assume exit code 0 means success. LS-DYNA, for example, can terminate with "Normal termination" in the message file while the process returns a non-zero code, or vice versa. Always verify by inspecting the solver's own status file.

Input Deck Generation: Templates and Parameter Substitution

An input deck is a text file with a rigid syntax. Generating it programmatically means separating the invariant structure (element definitions, material cards, boundary conditions) from the varying parameters (thickness, material properties, load magnitudes, output requests).

The most maintainable approach is a template file with named placeholders, rendered by Python's string.Template or Jinja2. Avoid f-strings for large decks — they become unreadable and hard to diff.

from string import Template

DECK_TEMPLATE = Template("""
*HEADING
$job_name - thickness $thickness mm
*MATERIAL, NAME=STEEL
*ELASTIC
210000., 0.30
*SHELL SECTION, ELSET=panel, MATERIAL=STEEL
$thickness,
*STEP
*STATIC
*BOUNDARY
$bc_set, 1, 6
*END STEP
""")

def render_deck(job_name, thickness, bc_set):
    return DECK_TEMPLATE.substitute(
        job_name=job_name, thickness=f"{thickness:.3f}", bc_set=bc_set
    )

Two rules prevent most deck-generation bugs. First, format numeric values explicitly (f"{thickness:.3f}") — solvers are sensitive to field width and scientific notation. Second, validate the rendered deck before submission: check that every required keyword appears, that no placeholder remains unresolved, and that the element count matches the mesh. A lightweight validator that greps for $ characters catches the most common failure.

For parametric studies, keep a manifest — a CSV or JSON file listing each job name and its parameter set. The manifest is the single source of truth; the deck generator reads it, and the post-processor writes results back against the same job names.

Output Parsing: From Raw Files to Engineering Numbers

Solver output comes in three flavors: plain-text status files (.sta, .msg, d3hsp), structured result databases (.odb, .d3plot, .h3d), and tabular ASCII (.dat, .csv). Each needs a different parsing strategy.

For text status files, line-oriented parsing with regular expressions is sufficient and fast. The goal is usually a small set of scalars: maximum displacement, peak stress, energy balance, termination status.

import re

def parse_sta(sta_path):
    pattern = re.compile(r"^\s*(\d+)\s+([\d.E+-]+)\s+([\d.E+-]+)")
    rows = []
    with open(sta_path) as f:
        for line in f:
            m = pattern.match(line)
            if m:
                rows.append((int(m.group(1)), float(m.group(2)), float(m.group(3))))
    return rows

For binary result databases, use the solver vendor's Python API — odbAccess for Abaqus, lasso or d3plot readers for LS-DYNA, h5py for HDF5-based formats. These APIs expose node sets, element sets, and time steps directly, avoiding fragile byte-level parsing.

from odbAccess import openOdb

def peak_mises(odb_path, set_name):
    odb = openOdb(odb_path, readOnly=True)
    step = odb.steps[odb.steps.keys()[-1]]
    frame = step.frames[-1]
    field = frame.fieldOutputs["S"]
    subset = field.getSubset(region=odb.rootAssembly.nodeSets[set_name])
    return max(v.mises for v in subset.values)

Regardless of format, three parsing practices pay off. First, parse incrementally and write results to a structured store (SQLite, Parquet, or CSV) as each job finishes — do not hold everything in memory until the end. Second, record provenance: solver version, deck hash, and timestamp alongside each result. Third, treat missing or malformed output as a first-class outcome, not an exception to swallow. A job that produced no .sta file is a different failure than one that produced a .sta with zero increments.

CAE Solver Interface Pipeline Manifest job_name, params Deck Generator Template + substitute validate placeholders Batch Solver subprocess.run / Popen cwd, timeout, cpus Solver Outputs .sta / .msg (text) .odb / .d3plot (binary) .dat / .csv (tabular) Text Parser regex line scan status + scalars Binary Parser odbAccess / h5py field outputs Result Store SQLite / Parquet + provenance next parameter set

Putting It Together: A Minimal Batch Driver

The three layers compose into a driver that reads a manifest, generates decks, dispatches solver runs, and parses results. A compact version looks like this:

import csv, pathlib
from concurrent.futures import ProcessPoolExecutor

def process_case(row, workdir):
    deck = render_deck(row["job"], float(row["thickness"]), row["bc_set"])
    (workdir / f"{row['job']}.inp").write_text(deck)
    rc, out, err = run_abaqus(row["job"], cpus=4, workdir=workdir)
    if not (workdir / f"{row['job']}.sta").exists():
        return {"job": row["job"], "status": "no_output", "peak": None}
    peak = peak_mises(workdir / f"{row['job']}.odb", "PANEL")
    return {"job": row["job"], "status": "ok", "peak": peak}

with open("manifest.csv") as f:
    rows = list(csv.DictReader(f))

with ProcessPoolExecutor(max_workers=4) as pool:
    results = list(pool.map(process_case, rows, [pathlib.Path("runs")] * len(rows)))

This driver is deliberately small, but it already handles the failure modes that matter: missing output, parallel dispatch, and structured result collection. From here, the natural extensions are retry logic for transient license failures, per-case logging, and a summary report that flags any case whose peak stress exceeds a threshold.

Keep the manifest, the deck template, and the parser in the same repository. When a solver version changes and a keyword is renamed, you want the diff to show up in one place, not scattered across scripts.

The interface between Python and a CAE solver is not glamorous, but it is where most automation time is saved. Batch execution removes manual clicking, deck generation removes copy-paste errors, and output parsing removes the spreadsheet shuffle. Together they turn a solver into a callable function that a CAE team can build real workflows on top of.