4. Programmatic generation of PAM-CRASH input decks

PAM-CRASH input decks are plain-text files organized into keyword blocks. Each block begins with a keyword line (for example *NODE, *ELEMENT_SHELL, or *MATERIAL) followed by fixed-format or free-format data lines. Because the format is line-oriented and column-sensitive, it is an ideal target for programmatic generation: once the model data lives in Python structures, the deck becomes a deterministic serialization problem rather than a manual editing task.

This section focuses on the three foundational card families — nodes, elements, and materials — and shows how to map Python data structures onto them while preserving the strict formatting rules PAM-CRASH expects.

4.1 Representing Model Data in Python

The first design decision is how to hold the model in memory. A practical and readable approach is to use dictionaries keyed by entity ID, with each value being a small record. This gives O(1) lookup, natural deduplication, and easy iteration in ascending ID order.

nodes = {
    1: {"x": 0.0,  "y": 0.0,  "z": 0.0},
    2: {"x": 50.0, "y": 0.0,  "z": 0.0},
    3: {"x": 50.0, "y": 50.0, "z": 0.0},
    4: {"x": 0.0,  "y": 50.0, "z": 0.0},
}

shells = {
    101: {"nodes": (1, 2, 3, 4), "pid": 10},
}

materials = {
    10: {"name": "STEEL_DP600", "type": "ELASTIC_PLASTIC",
         "rho": 7.85e-9, "E": 210000.0, "nu": 0.3,
         "sig_y": 420.0, "hard": 1200.0},
}

Two conventions matter here. First, IDs are integers and must be unique within their entity class. Second, units must be consistent across the whole deck; the values above assume a millimeter–tonne–second (mm-t–s) system, so density is expressed in t/mm³ and stress in MPa. Mixing unit systems is the single most common cause of a deck that runs but produces physically meaningless results.

4.2 Writing Node Cards

The *NODE block lists node IDs and coordinates. A minimal writer iterates the dictionary in sorted order and emits one line per node:

def write_nodes(fh, nodes):
    fh.write("*NODE\n")
    for nid in sorted(nodes):
        n = nodes[nid]
        fh.write(f"{nid:8d}{n['x']:16.6f}{n['y']:16.6f}{n['z']:16.6f}\n")

The format specifiers are not cosmetic. PAM-CRASH readers expect integer fields to occupy a defined width and floating-point fields to be right-aligned with a fixed number of decimals. Using {:8d} for the ID and {:16.6f} for each coordinate produces a stable, column-aligned layout that both the solver and human reviewers can parse reliably. If a coordinate exceeds the field width, the value is not truncated — Python widens the field — but the resulting line may violate the reader's expectations, so it is worth validating coordinate magnitude before writing.

4.3 Writing Shell Element Cards

Shell elements reference their nodes and a part ID. The connectivity order defines the element normal via the right-hand rule, so the node sequence must be consistent (typically counter-clockwise when viewed from the positive normal direction).

def write_shells(fh, shells):
    fh.write("*ELEMENT_SHELL\n")
    for eid in sorted(shells):
        e = shells[eid]
        n1, n2, n3, n4 = e["nodes"]
        fh.write(f"{eid:8d}{e['pid']:8d}"
                 f"{n1:8d}{n2:8d}{n3:8d}{n4:8d}\n")

Before writing, it is good practice to verify that every referenced node ID exists. A dangling reference will cause the solver to abort during input processing, and the error message often points to the element line rather than the missing node, which can slow debugging. A simple set-difference check catches this early:

missing = {n for e in shells.values() for n in e["nodes"]} - set(nodes)
if missing:
    raise ValueError(f"Shells reference undefined nodes: {sorted(missing)}")

4.4 Writing Material Cards

Material cards are the most format-sensitive part of the deck because each material type has its own parameter set and ordering. A robust pattern is to keep a small dispatch table that maps a material type string to a writer function, so adding a new material does not require touching the main loop.

def write_mat_elastic_plastic(fh, mid, m):
    fh.write("*MATERIAL, NAME=ELASTIC_PLASTIC\n")
    fh.write(f"{mid:8d}{m['rho']:16.6e}{m['E']:16.6e}"
             f"{m['nu']:16.6f}{m['sig_y']:16.6e}{m['hard']:16.6e}\n")

MAT_WRITERS = {
    "ELASTIC_PLASTIC": write_mat_elastic_plastic,
}

def write_materials(fh, materials):
    for mid in sorted(materials):
        m = materials[mid]
        MAT_WRITERS[m["type"]](fh, mid, m)

Note the use of scientific notation ({:16.6e}) for density, modulus, and yield stress. These quantities span several orders of magnitude, and fixed-point notation either loses precision or overflows the field. Scientific notation keeps the significant digits intact and matches the convention used in most published PAM-CRASH decks.

4.5 Assembling the Deck

With the three writers in place, the top-level function simply opens a file and calls them in the order the solver expects: control cards, materials, nodes, then elements. The order of node and element blocks is flexible, but materials must be defined before the elements that reference them.

def build_deck(path, nodes, shells, materials):
    with open(path, "w") as fh:
        write_materials(fh, materials)
        write_nodes(fh, nodes)
        write_shells(fh, shells)

The diagram below summarizes how the Python data structures flow into the three card families and how cross-references between them are resolved at write time.

From Python Data Structures to PAM-CRASH Cards nodes dict id → x, y, z sorted by id shells dict id → nodes, pid connectivity order materials dict id → type, params dispatch by type write_nodes() *NODE block write_shells() *ELEMENT_SHELL block write_materials() *MATERIAL block deck.k *MATERIAL *NODE *ELEMENT_SHELL 1 0.0 0.0 0.0 101 10 1 2 3 4 10 7.85e-9 2.1e5 ... Cross-reference check: shell node IDs must exist in nodes; shell pid must exist in materials

4.6 Validation and Common Failure Modes

Generating a deck is only half the task; confirming it is well-formed saves solver time. Three checks catch the majority of problems:

  • ID uniqueness and positivity. Duplicate IDs silently overwrite earlier entries in some readers. Assert that len(set(ids)) == len(ids) and that all IDs are positive integers.
  • Referential integrity. Every shell node ID must exist in the node dictionary, and every shell part ID must exist in the material dictionary. The set-difference check shown earlier handles the first; a similar check handles the second.
  • Field width overflow. If a coordinate or parameter exceeds its field width, the line shifts and the reader misinterprets columns. Validate that abs(value) < 10**(width - decimals - 2) before writing, or widen the field deliberately.
A deck that parses without error is not necessarily correct. A shell with reversed node ordering will run but produce an inverted normal, leading to wrong contact behavior. Always verify connectivity orientation against the intended geometry, not just against the absence of parser errors.

4.7 A Minimal End-to-End Example

Combining the pieces, a complete generation run looks like this:

nodes = {1: {"x":0,"y":0,"z":0}, 2: {"x":50,"y":0,"z":0},
         3: {"x":50,"y":50,"z":0}, 4: {"x":0,"y":50,"z":0}}
shells = {101: {"nodes": (1,2,3,4), "pid": 10}}
materials = {10: {"name":"STEEL_DP600","type":"ELASTIC_PLASTIC",
                  "rho":7.85e-9,"E":210000.0,"nu":0.3,
                  "sig_y":420.0,"hard":1200.0}}

build_deck("plate.k", nodes, shells, materials)

The resulting plate.k contains a material block, a four-node block, and a single shell element. Opening it in a PAM-CRASH preprocessor should show a 50 × 50 mm quad with the DP600 material assigned. If the quad appears with the wrong orientation or the material is missing, the cause is almost always a connectivity order issue or a part-ID mismatch — both of which the validation checks above are designed to surface before the solver ever runs.

Keep the writer functions pure: they should take data and a file handle, and return nothing. This makes them trivial to unit-test by writing to an io.StringIO buffer and comparing the output string against an expected literal.

With nodes, elements, and materials under programmatic control, the deck becomes a reproducible artifact: change a parameter in Python, regenerate, and the deck updates consistently. The next step is extending this pattern to boundary conditions, loads, and control cards, which follow the same keyword-block structure but introduce time-dependent and set-based references.