1. Python-Driven LS-DYNA Input Deck Generation for a Seatbelt Model

Why generate the deck from Python instead of editing it by hand

A seatbelt restraint model in LS-DYNA is a small but tightly coupled keyword deck: the belt path is defined by nodes, the webbing by seatbelt elements, the routing by sliprings and retractors, and the material response by a *MAT_ELASTIC card. When you need to sweep belt length, webbing stiffness, or anchor position across dozens of load cases, hand-editing the .k file is both slow and error-prone — a single misaligned field in a fixed-format card silently shifts every subsequent value. The practical alternative is a Python generator that owns the geometry and material parameters in a configuration dictionary and emits a syntactically valid deck on demand.

This case builds that generator end to end: a configuration dictionary drives node coordinates, seatbelt element connectivity, and material constants, and the script writes a complete, runnable seatbelt_model.k containing the control, database, part, section, material, node, element, set, and boundary cards needed for a minimal explicit run.

Deck structure the generator must produce

A minimal but complete seatbelt deck needs the following keyword blocks in a valid order. LS-DYNA is tolerant of block ordering in most cases, but keeping a canonical sequence makes diffs readable and avoids surprises with *INCLUDE resolution later.

Keyword blockPurpose in this modelDriven by config key
*KEYWORDDeck header / version marker—
*CONTROL_TERMINATIONSets ENDTIM for the explicit runtermination_time
*CONTROL_TIMESTEPOptional TSSFAC scalingtimestep_scale
*DATABASE_BINARY_D3PLOTOutput interval for animationd3plot_interval
*PARTSingle belt part referencing section + materialpart_id, secid, mid
*SECTION_SEATBELTWebbing cross-section / thicknessbelt_thickness
*MAT_ELASTICWebbing density, Young's modulus, Poisson ratiodensity, youngs_modulus, poisson
*NODEBelt path nodes from geometry listbelt_nodes
*ELEMENT_SEATBELT1D belt elements connecting consecutive nodesbelt_nodes (derived)
*SET_NODE_LISTAnchor node set for boundary conditionsanchor_node_ids
*BOUNDARY_SPC_SETFixes anchor nodes in all DOFanchor_node_ids

Configuration dictionary and generator

The configuration dictionary is the single source of truth. Every numeric value that a load case might vary lives here; the writer functions only format and emit. Units are consistent throughout: millimetre–tonne–second (mm, t, s, N, MPa), which is the standard LS-DYNA consistent set for occupant safety work.

import os

CONFIG = {
    "title": "Simple lap-and-shoulder seatbelt",
    "termination_time": 0.120,          # s
    "timestep_scale": 0.9,              # TSSFAC
    "d3plot_interval": 0.001,           # s
    "part_id": 100,
    "secid": 100,
    "mid": 100,
    "belt_thickness": 1.2,              # mm
    "density": 1.0e-9,                  # t/mm^3  (~1000 kg/m^3)
    "youngs_modulus": 3000.0,           # MPa
    "poisson": 0.30,
    # Belt path: (node_id, x, y, z) in mm
    "belt_nodes": [
        (1,  -400.0, 0.0, 900.0),   # lower anchor (B-pillar)
        (2,  -350.0, 0.0, 700.0),
        (3,  -300.0, 0.0, 500.0),
        (4,  -250.0, 0.0, 300.0),   # lap/shoulder transition
        (5,   250.0, 0.0, 300.0),   # buckle side
        (6,   300.0, 0.0, 500.0),
        (7,   350.0, 0.0, 700.0),
        (8,   400.0, 0.0, 900.0),   # upper anchor (D-ring)
    ],
    "anchor_node_ids": [1, 8],
}

def fmt(value):
    """Format a float for LS-DYNA fixed-width fields (10 chars, E-notation)."""
    return f"{value:10.4E}"

def write_header(f, cfg):
    f.write("*KEYWORD\n")
    f.write(f"${cfg['title']}\n")

def write_control(f, cfg):
    f.write("*CONTROL_TERMINATION\n")
    f.write(f"{fmt(cfg['termination_time'])}\n")
    f.write("*CONTROL_TIMESTEP\n")
    f.write(f"{fmt(0.0)}{fmt(cfg['timestep_scale'])}\n")
    f.write("*DATABASE_BINARY_D3PLOT\n")
    f.write(f"{fmt(cfg['d3plot_interval'])}\n")

def write_part_and_material(f, cfg):
    f.write("*PART\n")
    f.write(f"${'pid':>10}{'secid':>10}{'mid':>10}\n")
    f.write(f"{cfg['part_id']:10d}{cfg['secid']:10d}{cfg['mid']:10d}\n")
    f.write("*SECTION_SEATBELT\n")
    f.write(f"{cfg['secid']:10d}{fmt(cfg['belt_thickness'])}\n")
    f.write("*MAT_ELASTIC\n")
    f.write(f"${'mid':>10}{'RO':>10}{'E':>10}{'PR':>10}\n")
    f.write(f"{cfg['mid']:10d}{fmt(cfg['density'])}{fmt(cfg['youngs_modulus'])}{fmt(cfg['poisson'])}\n")

def write_nodes(f, cfg):
    f.write("*NODE\n")
    for nid, x, y, z in cfg["belt_nodes"]:
        f.write(f"{nid:10d}{fmt(x)}{fmt(y)}{fmt(z)}\n")

def write_seatbelt_elements(f, cfg):
    f.write("*ELEMENT_SEATBELT\n")
    ids = [n[0] for n in cfg["belt_nodes"]]
    for eid, (n1, n2) in enumerate(zip(ids[:-1], ids[1:]), start=1):
        f.write(f"{eid:10d}{cfg['part_id']:10d}{n1:10d}{n2:10d}\n")

def write_boundary(f, cfg):
    f.write("*SET_NODE_LIST\n")
    f.write(f"{1:10d}\n")
    for nid in cfg["anchor_node_ids"]:
        f.write(f"{nid:10d}\n")
    f.write("*BOUNDARY_SPC_SET\n")
    f.write(f"{1:10d}{1:10d}{1:10d}{1:10d}{1:10d}{1:10d}{1:10d}\n")

def generate_deck(cfg, path="seatbelt_model.k"):
    with open(path, "w") as f:
        write_header(f, cfg)
        write_control(f, cfg)
        write_part_and_material(f, cfg)
        write_nodes(f, cfg)
        write_seatbelt_elements(f, cfg)
        write_boundary(f, cfg)
    return path

if __name__ == "__main__":
    out = generate_deck(CONFIG)
    print(f"Wrote {out} ({os.path.getsize(out)} bytes)")

How the configuration maps to the deck

The diagram below shows the data flow from the configuration dictionary into the emitted keyword blocks, and the derived quantities (element connectivity, node set membership) that the generator computes rather than stores.

Configuration dictionary → LS-DYNA keyword deck CONFIG dict termination_time timestep_scale d3plot_interval part_id / secid / mid belt_thickness density youngs_modulus poisson belt_nodes [(id,x,y,z)] anchor_node_ids Derived in Python element connectivity (zip of consecutive node ids) node set membership fixed-width field strings Writer functions write_header() write_control() write_part_and_material() write_nodes() write_seatbelt_elements() write_boundary() each emits one keyword block seatbelt_model.k *KEYWORD *CONTROL_TERMINATION *CONTROL_TIMESTEP *DATABASE_BINARY_D3PLOT *PART *SECTION_SEATBELT *MAT_ELASTIC *NODE (8 nodes) *ELEMENT_SEATBELT (7) *SET_NODE_LIST *BOUNDARY_SPC_SET Validation checks • 7 elements = 8 nodes − 1 • anchor ids ⊂ node ids

Running and verifying the output

Execute the script with any Python 3 interpreter:

python generate_seatbelt.py
# Wrote seatbelt_model.k (1842 bytes)

The byte count is illustrative; the important verification is structural. Open the generated file and confirm the following before submitting to the solver:

  • Element count matches node count minus one. With eight nodes the generator must emit seven *ELEMENT_SEATBELT lines. A mismatch means the zip(ids[:-1], ids[1:]) slice was altered or a node was dropped from the list.
  • Every anchor id exists in the node block. If anchor_node_ids references a node not present in belt_nodes, *BOUNDARY_SPC_SET will silently constrain nothing and the belt will translate freely — a classic symptom of a runaway model.
  • Field widths are consistent. LS-DYNA reads fixed 10-character fields for most cards. The fmt() helper pads to ten characters with E-notation; mixing free-format and fixed-format lines in the same block is a common source of "negative volume" or "node out of range" errors.
  • Units are coherent. Density in t/mm³, modulus in MPa, lengths in mm, time in s. A density entered as 1.0e-6 (kg/mm³) instead of 1.0e-9 inflates the mass by 1000× and produces an absurdly small stable timestep.
Run a quick syntax check with ls-dyna i=seatbelt_model.k in a scratch directory and inspect the d3hsp file. The header echoes the number of nodes, elements, and parts read; if those counts differ from the configuration, the writer functions are the first place to look.

Parameterizing for the next load case

Because every varying quantity lives in CONFIG, a new belt length or stiffness is a dictionary edit, not a deck edit. For example, to shorten the shoulder segment by moving the upper anchor from z = 900 to z = 850 and soften the webbing to 2500 MPa:

CONFIG["belt_nodes"][-1] = (8, 400.0, 0.0, 850.0)
CONFIG["youngs_modulus"] = 2500.0
generate_deck(CONFIG, path="seatbelt_model_soft.k")

The generator recomputes connectivity and rewrites the deck; the element count stays at seven because the node count is unchanged, but the geometry and material cards reflect the new case. This is the pattern the remaining cases in this course build on: keep the writer functions stable, and let the configuration dictionary carry the variation.