3. Python environment setup for LS-DYNA automation
Before a single DOE case can be launched, the automation environment must be deterministic. LS-DYNA automation fails in subtle ways when the Python interpreter, the solver executable, and the working directory are not pinned down: a case may run with the wrong solver version, write results into an unexpected folder, or silently reuse a stale d3plot from a previous run. This chapter establishes a reproducible environment layout, the minimal library set, and a thin execution wrapper that every later script in the course will reuse.
Interpreter and dependency baseline
Use a dedicated virtual environment per project rather than the system interpreter. The reason is practical: LS-DYNA automation scripts depend on a small, stable set of packages, and isolating them prevents version drift when other projects upgrade numpy or pandas. A Python 3.10+ interpreter is recommended because the type-hint syntax and pathlib behavior used in later chapters are stable there.
python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux / macOS
source .venv/bin/activate
pip install numpy pandas matplotlib pyyaml lxml
The library roles are distinct and worth stating explicitly, because choosing the wrong one for a task creates fragile code:
numpy— numeric arrays for parameter sweeps and for post-processing result vectors (e.g., acceleration time histories).pandas— tabular DOE matrices, run-status logs, and injury-metric summary tables; its CSV and Excel I/O is the backbone of reporting.matplotlib— quick diagnostic plots of response curves before formal report generation.pyyaml— human-editable configuration files for solver paths, case templates, and DOE definitions.lxml— parsing LS-DYNA keyword files and XML-based result exports when regex is too brittle.
Note what is deliberately absent: no LS-DYNA Python API is assumed. LS-DYNA is driven as an external process through its command-line executable, and Python's role is orchestration, file manipulation, and post-processing. This keeps the automation portable across solver versions and operating systems.
Directory conventions
A fixed directory contract is the single most effective defense against "which run produced this file?" confusion. The layout below separates immutable inputs from generated outputs, so a run can be deleted and regenerated without touching source data.
project/
├── config/
│ ├── environment.yaml # solver path, license settings, thread count
│ └── doe_matrix.csv # parameter combinations
├── templates/
│ └── base_model.k # keyword template with placeholders
├── cases/
│ └── case_0001/ # one directory per DOE run
│ ├── model.k
│ ├── run.log
│ └── results/
├── scripts/
│ ├── env_setup.py
│ ├── runner.py
│ └── postprocess.py
└── reports/
└── summary.xlsx
Two rules make this layout work. First, every case directory is self-contained: the generated model.k lives beside its outputs, so archiving or deleting a case is a single folder operation. Second, case identifiers are zero-padded (case_0001, not case_1) so lexical sorting matches numeric order in logs and reports. The environment.yaml file centralizes machine-specific paths, which means the same scripts run on a workstation and a cluster node without edits:
solver:
executable: "C:/LSDYNA/lsdyna.exe" # or /opt/ls-dyna/lsdyna
ncpu: 4
memory_words: 500000000
paths:
project_root: "."
template: "templates/base_model.k"
The execution wrapper
Calling the solver directly from every script duplicates error handling and makes logging inconsistent. A single wrapper function solves this. It builds the command line, redirects stdout/stderr to run.log, waits for completion, and returns a structured status rather than a bare exit code.
import subprocess, pathlib, datetime
def run_lsdyna(case_dir: pathlib.Path, exe: str, ncpu: int = 4) -> dict:
model = case_dir / "model.k"
log = case_dir / "run.log"
cmd = [exe, f"i={model.name}", f"ncpu={ncpu}"]
start = datetime.datetime.now()
with open(log, "w") as fh:
proc = subprocess.run(cmd, cwd=case_dir, stdout=fh,
stderr=subprocess.STDOUT)
return {
"case": case_dir.name,
"returncode": proc.returncode,
"elapsed_s": (datetime.datetime.now() - start).total_seconds(),
"log": str(log),
}
The cwd=case_dir argument is essential: LS-DYNA writes d3plot, glstat, and messag files into the current working directory, so running from the case folder guarantees outputs land in the right place. The wrapper returns a dictionary that the DOE driver can append directly to a run-status table.
messag and glstat for error keywords before trusting results — this check is added in the post-processing chapter.Environment verification
Before launching a DOE, run a smoke test that confirms the solver is reachable and the wrapper behaves. A minimal single-element model or a short existing case is sufficient. The expected observable outcome is a run.log containing the LS-DYNA version banner and a normal termination message, plus a non-empty d3plot in the case directory. Typical failure symptoms and their causes:
| Symptom | Likely cause | Fix |
|---|---|---|
FileNotFoundError on executable | Wrong path in environment.yaml | Use absolute path; verify with Path(exe).exists() |
| Log empty, return code non-zero | License checkout failure | Check license server env vars; test solver manually |
| Outputs in project root, not case dir | cwd not set | Pass cwd=case_dir to subprocess.run |
| Stale results reused | Old d3plot present | Clear results/ before each run |
The diagram below summarizes how configuration, the wrapper, and the case directory interact during a single run.
With this environment in place, every subsequent chapter can assume three things: a known interpreter with pinned libraries, a case directory that owns its own inputs and outputs, and a wrapper that returns a uniform status record. Those three assumptions are what make a 50-case DOE reproducible rather than a collection of manual runs.