4. Programmatic generation of LS-DYNA input decks

Hand-editing a *KEYWORD deck for every design variant is slow, error-prone, and impossible to audit. A parametric study with 40 runs, each differing only in shell thickness and material curve, should not require 40 manual edits of the same file. This chapter shows how to build a Python generator that turns a single Jinja2 template plus a parameter set into a complete, syntactically valid LS-DYNA input deck.

Why Jinja2 Fits Keyword Files

LS-DYNA keyword files are line-oriented, fixed-format in places, and heavily repetitive: the same *PART, *SECTION_SHELL, and *MAT_* blocks repeat with different IDs and values. Jinja2 gives you three primitives that map directly onto this structure:

  • Variable substitution — {{ thickness }} replaces a literal value.
  • Loops — {% for part in parts %} emits one keyword block per part.
  • Conditionals — {% if contact_enabled %} includes or omits an entire keyword card.

The critical discipline is that the template must remain a valid LS-DYNA deck when rendered with a known-good parameter set. Treat the template as the single source of truth for card structure; treat the parameter file as the only thing that changes between runs.

Template Structure and Card Alignment

LS-DYNA is sensitive to column positions on many cards. When you substitute a value, the rendered line must still satisfy the field widths. The safest approach is to keep the template's whitespace fixed and substitute only the numeric token, letting Python format the value to the correct width before rendering.

*KEYWORD
*TITLE
{{ title }}
*CONTROL_TERMINATION
{{ "%10.3f"|format(termination_time) }}
*CONTROL_TIMESTEP
{{ "%10.3f"|format(dt_init) }} {{ "%10.3f"|format(tssfac) }}
*DATABASE_BINARY_D3PLOT
{{ "%10.3f"|format(d3plot_interval) }}
*PART
part_{{ part.id }}
{{ "%10d"|format(part.secid) }} {{ "%10d"|format(part.mid) }} {{ "%10d"|format(part.eosid) }}
*SECTION_SHELL
{{ "%10d"|format(part.secid) }}
{{ "%10d"|format(part.elform) }} {{ "%10.3f"|format(part.shear) }} {{ "%10.3f"|format(part.nip) }} {{ "%10.3f"|format(part.propt) }}
{{ "%10.3f"|format(part.t1) }} {{ "%10.3f"|format(part.t2) }} {{ "%10.3f"|format(part.t3) }} {{ "%10.3f"|format(part.t4) }}
*MAT_PIECEWISE_LINEAR_PLASTICITY
{{ "%10d"|format(part.mid) }} {{ "%10.3f"|format(part.rho) }} {{ "%10.3f"|format(part.e) }} {{ "%10.3f"|format(part.pr) }} {{ "%10.3f"|format(part.sigy) }} {{ "%10.3f"|format(part.etan) }} {{ "%10.3f"|format(part.fail) }} {{ "%10.3f"|format(part.tdel) }}
{% for lc in part.lc %}{{ "%10.3f"|format(lc.strain) }} {{ "%10.3f"|format(lc.stress) }}
{% endfor %}{% endfor %}
*END

Note the use of |format(...) filters rather than raw {{ value }}. This guarantees that a thickness of 1.2 renders as 1.200 and not 1.2, which would shift every subsequent field on the card. This single habit eliminates the most common class of generated-deck errors.

Parameter Substitution from a Data Source

Parameters should live outside the template. A YAML or JSON file is the natural choice because it is human-readable and diff-friendly. The generator loads it, validates required keys, and passes the resulting dictionary into template.render().

import yaml
from jinja2 import Environment, FileSystemLoader

env = Environment(
    loader=FileSystemLoader("templates"),
    trim_blocks=True,
    lstrip_blocks=True,
)
tmpl = env.get_template("deck.j2")

with open("params/run_017.yaml") as f:
    params = yaml.safe_load(f)

deck = tmpl.render(**params)
with open("runs/run_017/main.k", "w") as f:
    f.write(deck)

The trim_blocks and lstrip_blocks options are not cosmetic. Without them, Jinja2 leaves blank lines where control tags appear, and LS-DYNA will reject a deck containing stray empty lines between keyword cards. Enable both from the start.

Conditional Keyword Blocks

Real studies toggle features: a contact definition, an initial velocity card, a *MAT_ADD_EROSION block, or a restart segment. Conditionals keep one template serving many configurations.

{% if contact.enabled %}
*CONTACT_AUTOMATIC_SINGLE_SURFACE
{{ "%10d"|format(contact.ssid) }} {{ "%10d"|format(contact.msid) }} {{ "%10d"|format(contact.sstyp) }} {{ "%10d"|format(contact.mstyp) }}
{{ "%10.3f"|format(contact.fs) }} {{ "%10.3f"|format(contact.fd) }} {{ "%10.3f"|format(contact.dc) }} {{ "%10.3f"|format(contact.vc) }}
{% endif %}
{% if initial_velocity is defined %}
*INITIAL_VELOCITY_GENERATION
{{ "%10d"|format(initial_velocity.nsid) }} {{ "%10d"|format(initial_velocity.styp) }} {{ "%10.3f"|format(initial_velocity.omega) }} {{ "%10.3f"|format(initial_velocity.vx) }} {{ "%10.3f"|format(initial_velocity.vy) }} {{ "%10.3f"|format(initial_velocity.vz) }}
{% endif %}

Use is defined for optional blocks and an explicit boolean for feature toggles. Mixing the two — for example, relying on a missing key to disable contact — makes the parameter file ambiguous and hard to validate.

Validation before rendering. Check that every required key exists and that numeric values fall within physical ranges (positive thickness, Young's modulus in the expected order of magnitude) before calling render(). A generator that silently emits a deck with thickness = 0 wastes an entire solver run.

Workflow Overview

Parametric Deck Generation Pipeline Parameter Source run_017.yaml thickness, mat, contact Validator required keys range checks Jinja2 Template deck.j2 loops + conditionals Renderer template.render() format filters Generated Deck runs/run_017/main.k *KEYWORD ... *END LS-DYNA Solver lsdyna i=main.k iterate parameters

Verifying the Generated Deck

Never submit a generated deck directly to the solver without a structural check. Three verification layers catch nearly all generator bugs:

  1. Keyword balance. Count *KEYWORD and *END occurrences; the deck must open and close exactly once. A missing *END usually means a conditional swallowed the terminator.
  2. Card field count. For each keyword block, confirm the number of whitespace-separated tokens per line matches the expected card width. A shifted field is the signature of a formatting filter that was omitted.
  3. Diff against a reference. Render the template with a known-good parameter set and diff the output against a hand-verified deck. Any difference is either an intended parameter change or a template defect — there is no third possibility.
Common failure symptom. If LS-DYNA reports "unknown keyword" or reads a card with wrong values, the cause is almost always a blank line or a misaligned field introduced by the template. Check trim_blocks/lstrip_blocks first, then verify every numeric substitution uses a width-preserving format filter.

Practical Deliverable: Parameter Schema

The following schema documents the minimum parameter set for the template above. It doubles as the validator's contract and as the interface for anyone running the study.

KeyTypeConstraintMaps to
titlestringnon-empty*TITLE
termination_timefloat> 0*CONTROL_TERMINATION
dt_initfloat> 0*CONTROL_TIMESTEP
d3plot_intervalfloat> 0*DATABASE_BINARY_D3PLOT
parts[].idintunique*PART name
parts[].t1..t4float> 0*SECTION_SHELL
parts[].lc[]liststrain ascending*MAT_PIECEWISE_LINEAR_PLASTICITY
contact.enabledbool—conditional block
initial_velocityobjectoptionalconditional block

With this structure in place, generating 40 variants is a loop over 40 YAML files. The template stays fixed, the parameter files are the experimental record, and every generated deck is reproducible from its inputs. That reproducibility is the real payoff: when a run produces an unexpected result, you can regenerate the exact deck that was solved and inspect it line by line.