4. Python file I/O for simulation data

PAM-CRASH workflows generate and consume a large amount of text-based data. Input decks (.pc, .inp), result files (.dsy, .th, .erfh5), and metadata files (run logs, material summaries, job status) all need to be read, filtered, transformed, or generated programmatically. Python's pathlib and csv modules provide a robust foundation for these tasks without pulling in heavy dependencies. This section focuses on practical patterns for handling each file category correctly.

Path Handling with pathlib

Hard-coded string paths break when simulations move between Linux clusters and Windows workstations. pathlib.Path solves this by treating paths as objects with methods rather than raw strings. A typical PAM-CRASH project directory might look like this:

from pathlib import Path

project = Path("/sim/projects/crash_2024")
deck_dir = project / "decks"
result_dir = project / "results"
meta_dir = project / "metadata"

# Iterate over all .pc decks in a directory
for deck in sorted(deck_dir.glob("*.pc")):
    print(deck.name, deck.stat().st_size)

# Find result files matching a pattern
for dsy in result_dir.glob("run_*/d3plot"):
    print(dsy.parent.name, "->", dsy)

Key advantages: glob() and rglob() replace os.walk with cleaner syntax; Path.suffix, Path.stem, and Path.parent eliminate manual string splitting; and Path.mkdir(parents=True, exist_ok=True) safely creates output directories. When reading large decks, always open with explicit encoding — PAM-CRASH decks are typically ASCII, but metadata may contain UTF-8 comments:

with open(deck_path, "r", encoding="ascii", errors="replace") as f:
    for line_no, line in enumerate(f, start=1):
        if line.startswith("$"):
            continue  # skip comment lines
        # process keyword line

Reading PAM-CRASH Input Decks

PAM-CRASH input decks are keyword-driven. Each block begins with a keyword line (e.g., *PART, *MAT, *SECTION) followed by parameter lines. A common automation task is extracting all part IDs and their associated material IDs to verify consistency before submission. The following pattern reads a deck and builds a mapping:

def parse_parts(deck_path):
    parts = {}
    current_part = None
    with open(deck_path, "r", encoding="ascii", errors="replace") as f:
        for line in f:
            stripped = line.strip()
            if stripped.startswith("*PART"):
                current_part = None
            elif stripped.startswith("*MAT"):
                current_part = None
            elif current_part is None and stripped and not stripped.startswith("$"):
                # first data line after *PART contains part ID
                tokens = stripped.split()
                if len(tokens) >= 2:
                    current_part = tokens[0]
                    parts[current_part] = {"mat_id": tokens[1]}
    return parts

This is a simplified example. Real decks may have fixed-width fields, continuation lines, and include files (*INCLUDE). For production use, consider a line-oriented state machine that tracks the active keyword and accumulates fields until the next keyword. Always validate that referenced material IDs exist in the *MAT section — a missing material is a common cause of solver termination.

Writing Modified Decks

When parameter studies require changing thickness, material properties, or boundary conditions, you can write a new deck by copying the original and replacing specific lines. A safe approach is to read all lines into memory, modify targeted lines, and write to a new file:

def set_shell_thickness(src, dst, part_id, new_thickness):
    lines = Path(src).read_text(encoding="ascii").splitlines()
    out = []
    in_target = False
    for line in lines:
        if line.strip().startswith("*PART"):
            in_target = False
        if in_target and line.strip().startswith(part_id):
            # replace thickness field (example: column 5)
            fields = line.split()
            fields[4] = f"{new_thickness:.3f}"
            line = " ".join(fields)
            in_target = False
        if line.strip().startswith("*PART"):
            in_target = True
        out.append(line)
    Path(dst).write_text("\n".join(out), encoding="ascii")
Never overwrite the original deck in place during a parameter sweep. Write to a new path and keep the source immutable. This preserves reproducibility and allows you to diff decks when a run fails.

Reading Result Files and Time-History Data

PAM-CRASH result files come in several formats. Time-history files (.th) are often ASCII tables with a header line and columns of time versus response. These are straightforward to parse with csv.reader after skipping comment lines:

import csv

def read_th_file(path):
    data = {"time": [], "values": []}
    with open(path, "r", encoding="ascii", errors="replace") as f:
        reader = csv.reader(f, delimiter=" ", skipinitialspace=True)
        for row in reader:
            if not row or row[0].startswith("#"):
                continue
            try:
                t = float(row[0])
                v = float(row[1])
            except (ValueError, IndexError):
                continue
            data["time"].append(t)
            data["values"].append(v)
    return data

For binary result files such as .dsy or .erfh5, Python alone is insufficient; you typically call a converter (e.g., pam2csv or the PAM-CRASH Python API) to produce ASCII, then parse the output. The key point is to isolate the conversion step and treat the resulting text as the stable interface for your automation scripts.

Metadata and CSV Logging

Simulation campaigns produce metadata: run ID, deck hash, solver version, wall-clock time, peak memory, and status. Storing this in CSV makes it easy to filter and summarize with csv.DictReader and csv.DictWriter:

import csv
from pathlib import Path

def log_run(meta_path, run_id, deck, status, wall_time):
    meta_path = Path(meta_path)
    write_header = not meta_path.exists()
    with open(meta_path, "a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=[
            "run_id", "deck", "status", "wall_time_s"
        ])
        if write_header:
            writer.writeheader()
        writer.writerow({
            "run_id": run_id,
            "deck": str(deck),
            "status": status,
            "wall_time_s": f"{wall_time:.1f}",
        })

Reading back the log allows quick status checks:

with open("run_log.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        if row["status"] != "completed":
            print(row["run_id"], row["status"])

Choosing the Right Tool for Each File Type

File Type Typical Format Recommended Python Approach
Input deck (.pc, .inp) Keyword-driven ASCII Line-by-line state machine; pathlib for paths
Time history (.th) Column ASCII csv.reader with delimiter and comment skip
Binary results (.dsy, .erfh5) Binary External converter → ASCII → csv/numpy
Run metadata CSV csv.DictReader / csv.DictWriter
Use Path.read_text() and Path.write_text() for small files under a few megabytes. For large decks or result files, stream with open() and iterate line by line to keep memory usage predictable.

Error Handling and Validation

File I/O in simulation automation must handle missing files, permission errors, and malformed lines gracefully. Wrap file operations in try/except and log failures with enough context to diagnose:

def safe_read(path):
    try:
        return Path(path).read_text(encoding="ascii", errors="replace")
    except FileNotFoundError:
        print(f"Missing file: {path}")
        return None
    except PermissionError:
        print(f"Permission denied: {path}")
        return None

For deck parsing, validate that required keywords exist and that referenced IDs are defined. A simple check after parsing can prevent submitting a deck that will fail immediately in the solver:

def validate_deck(parts, materials):
    missing = [pid for pid, info in parts.items()
               if info["mat_id"] not in materials]
    if missing:
        raise ValueError(f"Parts with undefined materials: {missing}")

These patterns — pathlib for path manipulation, csv for tabular data, and explicit encoding plus validation — form the backbone of reliable PAM-CRASH file automation. The next step is to combine them into reusable modules that can be called from batch scripts or workflow managers.

PAM-CRASH File I/O Pipeline with pathlib and csv Input Deck .pc / .inp Keyword-driven ASCII *PART, *MAT, *SECTION Result Files .th / .dsy / .erfh5 Time-history / binary Column or converted ASCII Metadata run_log.csv run_id, deck, status wall_time_s Python Layer pathlib.Path csv.reader / DictReader Validation & Error Handling Outputs Modified deck Parsed time-history Updated run_log.csv Validation report Input deck Result file Metadata Processing Output