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")
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 |
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.