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 block | Purpose in this model | Driven by config key |
|---|---|---|
*KEYWORD | Deck header / version marker | — |
*CONTROL_TERMINATION | Sets ENDTIM for the explicit run | termination_time |
*CONTROL_TIMESTEP | Optional TSSFAC scaling | timestep_scale |
*DATABASE_BINARY_D3PLOT | Output interval for animation | d3plot_interval |
*PART | Single belt part referencing section + material | part_id, secid, mid |
*SECTION_SEATBELT | Webbing cross-section / thickness | belt_thickness |
*MAT_ELASTIC | Webbing density, Young's modulus, Poisson ratio | density, youngs_modulus, poisson |
*NODE | Belt path nodes from geometry list | belt_nodes |
*ELEMENT_SEATBELT | 1D belt elements connecting consecutive nodes | belt_nodes (derived) |
*SET_NODE_LIST | Anchor node set for boundary conditions | anchor_node_ids |
*BOUNDARY_SPC_SET | Fixes anchor nodes in all DOF | anchor_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.
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_SEATBELTlines. A mismatch means thezip(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_idsreferences a node not present inbelt_nodes,*BOUNDARY_SPC_SETwill 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 withE-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 of1.0e-9inflates the mass by 1000× and produces an absurdly small stable timestep.
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.