2. Python environment setup for PAM-CRASH automation

PAM-CRASH input decks are plain-text files organized into keyword blocks: *PART, *MAT, *SECTION, *NODE, *ELEMENT, *CONTACT, and so on. Because the deck is text, almost any automation task reduces to three operations: read the file, locate and modify the relevant keyword blocks, and write the file back without corrupting the rest of the model. A correctly prepared Python environment makes those three operations reliable and repeatable. This section covers the interpreter and library choices, the file I/O patterns that matter for large decks, and a working example that parses and rewrites a keyword block.

Interpreter and dependency baseline

Use CPython 3.10 or newer. The standard library alone covers most deck-editing work: pathlib for path handling, re for keyword and value matching, csv for parameter tables, and shutil for backups. Third-party packages become worthwhile once you need structured data or batch execution:

  • pandas — reading and writing parameter tables (material cards, thickness maps, run matrices) as CSV or Excel.
  • numpy — numeric post-processing of extracted results, e.g. section forces or nodal coordinates.
  • lxml or xml.etree.ElementTree — only if your workflow exchanges data with XML-based tooling; PAM-CRASH decks themselves are not XML.
  • pytest — regression tests for deck transformations, so a refactor cannot silently change output.
  • click or argparse — command-line entry points for batch scripts.

Create an isolated environment so that a solver-adjacent machine does not accumulate conflicting packages:

python -m venv .venv
.venv\Scripts\activate        # Windows
source .venv/bin/activate     # Linux / macOS
python -m pip install --upgrade pip
pip install pandas numpy pytest click
pip freeze > requirements.txt

Pin versions in requirements.txt and commit it alongside the automation scripts. Deck transformations are sensitive to formatting details, so a reproducible environment is not optional once more than one engineer runs the same script.

Reading and writing text decks safely

PAM-CRASH decks are typically ASCII, but comments and titles may contain non-ASCII characters. Always specify an encoding explicitly rather than relying on the platform default:

from pathlib import Path

deck_path = Path("model_v3.key")
text = deck_path.read_text(encoding="utf-8", errors="replace")
lines = text.splitlines(keepends=True)

Two details matter here. First, splitlines(keepends=True) preserves the original line terminators, so a round-trip write does not silently convert CRLF to LF or vice versa. Second, errors="replace" prevents a single stray byte from aborting a batch run; log the affected line numbers instead so the anomaly can be inspected.

Before any in-place modification, write a backup and use an atomic replace so a crash mid-write cannot leave a truncated deck:

import shutil, os, tempfile

backup = deck_path.with_suffix(deck_path.suffix + ".bak")
shutil.copy2(deck_path, backup)

tmp_fd, tmp_name = tempfile.mkstemp(dir=deck_path.parent, suffix=".tmp")
with os.fdopen(tmp_fd, "w", encoding="utf-8", newline="") as fh:
    fh.writelines(lines)
os.replace(tmp_name, deck_path)

The newline="" argument disables Python's automatic newline translation, which is essential when the deck uses fixed line endings. os.replace is atomic on the same filesystem, so the deck is either the old version or the new version, never a partial file.

Locating keyword blocks

A keyword block starts with a line whose first non-blank character is * and continues until the next such line. A robust parser records the start and end index of every block rather than assuming fixed line numbers:

import re

KEYWORD_RE = re.compile(r"^\s*\*([A-Za-z_]+)")

def index_blocks(lines):
    blocks = []
    current = None
    for i, line in enumerate(lines):
        m = KEYWORD_RE.match(line)
        if m:
            if current:
                current["end"] = i
                blocks.append(current)
            current = {"keyword": m.group(1).upper(), "start": i, "end": len(lines)}
    if current:
        blocks.append(current)
    return blocks

With this index you can answer questions such as "how many *MAT cards exist", "which *PART references material ID 12", or "does this deck contain a *CONTACT block at all" without scanning the file repeatedly. For decks in the hundreds of megabytes, build the index once and reuse it.

PAM-CRASH keyword names are case-insensitive in most solver versions, but the deck convention is uppercase. Normalize to uppercase when comparing, and preserve the original spelling when writing back so diffs stay minimal.

Worked example: rewriting a material parameter

Suppose a study varies the yield stress of a *MAT card across a batch of runs. The material block has the form:

*MAT
$ mid  rho    E      nu     sigy
  12   7.85e-9 210000 0.30   350.0

The transformation reads the deck, finds the target material ID, replaces the yield-stress field, and writes the result to a new file. Keeping the original untouched makes the batch reproducible:

def set_yield_stress(lines, mid, new_sigy):
    blocks = index_blocks(lines)
    for b in blocks:
        if b["keyword"] != "MAT":
            continue
        for i in range(b["start"] + 1, b["end"]):
            fields = lines[i].split()
            if len(fields) >= 5 and fields[0] == str(mid):
                fields[4] = f"{new_sigy:.1f}"
                lines[i] = "  " + "  ".join(fields) + "\n"
                return True
    return False

Run it for a small sweep and verify the output:

for sigy in (320.0, 350.0, 380.0):
    out = Path(f"model_sigy{int(sigy)}.key")
    work = list(lines)
    ok = set_yield_stress(work, 12, sigy)
    assert ok, "material 12 not found"
    out.write_text("".join(work), encoding="utf-8", newline="")
    print(out.name, "written")

Expected observable result: three decks that differ only in the fifth field of the material-12 line. Verify with a diff or by re-parsing the output and asserting the value. Typical failure symptoms and their causes:

  • Material not found — the ID column is not the first field, or the card uses continuation lines. Inspect the actual block layout before assuming a fixed column order.
  • Deck rejected by the solver — field widths or separators were changed. PAM-CRASH is generally whitespace-tolerant, but some cards expect fixed columns; preserve the original spacing when in doubt.
  • Line endings changed — newline="" was omitted, or the file was opened in text mode without it.
  • Silent corruption of other blocks — the block index was built on a stale copy of lines after an earlier edit shifted line numbers. Rebuild the index after structural changes.
Never write directly over the source deck during development. Work on a copy, diff the result against the original, and only promote the script to in-place operation once the transformation is covered by a regression test.

Environment and workflow at a glance

The diagram below summarizes how the interpreter, libraries, and file-handling steps fit together for a typical deck-automation script.

Python Environment for PAM-CRASH Deck Automation Interpreter & venv CPython 3.10+ .venv isolated requirements.txt pinned pip install pandas numpy pytest, click Libraries pathlib, re, csv, shutil pandas — parameter tables numpy — result post-processing pytest — regression tests argparse / click — CLI Deck I/O read_text(encoding="utf-8") splitlines(keepends=True) backup via shutil.copy2 atomic os.replace newline="" on write Deck Processing Pipeline read → index keyword blocks → locate target card → modify fields → write to new file → diff / re-parse Keyword Block Index KEYWORD_RE = ^\s*\*([A-Za-z_]+) blocks = [{keyword, start, end}, ...] *MAT → lines 10–14 *PART → lines 15–22 *NODE → lines 23–400 *CONTACT → lines 401–430 Verification & Failure Checks ✓ diff output vs original ✓ re-parse and assert field value ✗ material not found → check column order ✗ solver rejects → field width changed ✗ line endings → newline="" missing ✗ stale index → rebuild after edits

With this environment in place, the remaining automation work is mostly domain logic: knowing which keyword blocks carry which parameters, and how a change propagates through the model. The next step is to build reusable parsers for the specific card types your studies touch most often.