4. Writing valid LS-DYNA keyword files from Python

Generating an LS-DYNA input deck from Python is not simply a matter of concatenating strings. The solver's keyword reader is a fixed-format parser: it expects specific field widths, specific column positions, and specific ordering conventions. A single misplaced character can shift every subsequent field on a card, producing a deck that either fails to parse or, worse, parses silently into the wrong values. This section covers the formatting rules you must respect, how to handle continuation lines, how keyword block ordering affects the model, and what validation checks to run before handing the deck to the solver.

Fixed-Field Formatting Rules

LS-DYNA keyword cards come in two flavors: fixed-field (the traditional format) and free-field (comma-separated). Fixed-field cards are the default and the most common source of Python-generated errors. Each field occupies exactly 10 columns, and each card is 80 columns wide. Numeric fields are right-justified within their 10-column slot; integer fields follow the same rule. If a value overflows its field, the parser reads only the first 10 characters, silently truncating the number.

Consider a typical *ELEMENT_SOLID card. The node connectivity fields are integers, and the element ID is an integer. If you write 12345678901 into a 10-column field, the solver reads 1234567890 and discards the trailing 1. This is a classic silent failure: the deck runs, but the element references the wrong node. The fix is to validate field widths before writing, not after the solver rejects the deck.

A practical Python helper enforces the width contract:

def fixed_field(value, width=10):
    """Format a value into a fixed-width LS-DYNA field."""
    if isinstance(value, float):
        text = f"{value:.6g}"
    else:
        text = str(value)
    if len(text) > width:
        raise ValueError(f"Value '{text}' exceeds {width}-column field")
    return text.rjust(width)

Using rjust rather than ljust matters: LS-DYNA expects right-justified numerics. Left-justified numbers can be misread when the parser scans for the leading digit. For string fields such as titles, left-justification is acceptable, but the field must still be padded to its full width so the next field starts at the correct column.

Continuation Lines and Card Boundaries

Many LS-DYNA keywords accept more than eight fields per logical card. When a card exceeds the 80-column limit, the data continues on a continuation line. The continuation line is still 80 columns wide and still uses 10-column fields, but it carries no keyword header. The parser knows to expect it because the preceding card declared more fields than fit on one line.

For example, *ELEMENT_SOLID with eight nodes uses two lines: the first holds the element ID and the first four node IDs, the second holds the remaining four node IDs. If you write all eight nodes on one line, the parser reads only the first four and treats the rest as a new card, corrupting the element definition.

The safest approach is to build each card as a list of fields, then chunk that list into groups of eight and emit one line per chunk. This guarantees correct continuation behavior regardless of how many fields the keyword requires:

def emit_card(fields, per_line=8):
    lines = []
    for i in range(0, len(fields), per_line):
        chunk = fields[i:i + per_line]
        lines.append("".join(fixed_field(f) for f in chunk))
    return "\n".join(lines)

Note that some keywords use a different number of fields per line. *NODE uses four fields (ID, x, y, z), while *DEFINE_CURVE uses two fields per line (abscissa, ordinate). Always check the keyword's card definition in the LS-DYNA Keyword User's Manual rather than assuming eight fields per line.

Keyword Block Ordering

LS-DYNA does not require keywords to appear in a strict global order, but certain dependencies must be satisfied. Nodes must be defined before elements that reference them. Parts must be defined before sections and materials that they reference. Contact definitions must appear after the parts they connect. If you emit a *PART block before its *SECTION_SHELL block, the solver may still parse the deck but will report a missing section ID at initialization.

A reliable ordering convention is to emit blocks in this sequence: control cards, nodes, elements, sections, materials, parts, contact, boundary conditions, loads, and finally termination. This mirrors the dependency graph and avoids most ordering-related errors. The diagram below shows the dependency relationships that drive this ordering.

Keyword Block Dependency and Emission Order *CONTROL_* *NODE *ELEMENT_* *SECTION_* *MAT_* *PART *CONTACT_* *BOUNDARY_* / *LOAD_* *TERMINATION references references references

Validation Checks Before Solver Submission

Before submitting a generated deck, run a set of programmatic checks that catch the most common formatting and consistency errors. These checks are cheap to implement and save hours of solver debugging.

  • Field width validation: Every numeric field must fit within its 10-column slot. Reject any value whose formatted length exceeds the width.
  • Line length validation: Every emitted line must be exactly 80 columns or fewer. Lines longer than 80 columns are truncated by the parser.
  • Reference integrity: Every node ID referenced by an element must exist in the node table. Every section and material ID referenced by a part must exist.
  • Duplicate ID detection: Node, element, part, and material IDs must be unique within their respective namespaces.
  • Keyword block completeness: Each keyword block must have the correct number of cards. For example, a *PART block must have exactly one card after the keyword line.

A minimal validation pass in Python might look like this:

def validate_deck(lines):
    errors = []
    for i, line in enumerate(lines, 1):
        if len(line) > 80:
            errors.append(f"Line {i}: exceeds 80 columns ({len(line)})")
    return errors

For reference integrity, build sets of defined IDs as you emit each block, then check references against those sets. This catches the ordering problem described earlier: if a part references a section that has not yet been emitted, the reference check fails and you know to reorder the blocks.

Common pitfall: Python's default str(float) can produce values like 1e-05 or 0.30000000000000004. LS-DYNA expects fixed-point notation in most fields. Always format floats with a controlled precision, such as f"{value:.6g}", and verify the result fits the field width.

Putting It Together

A robust Python writer combines three responsibilities: formatting each field to its exact width, chunking fields into correctly sized lines, and emitting keyword blocks in dependency order. Validation runs as a separate pass over the generated lines, checking widths, lengths, and reference integrity. When these three layers are in place, the generated deck parses cleanly on the first submission, and any remaining errors are model-level issues rather than formatting artifacts.

The practical payoff is significant. A deck that fails to parse costs a solver startup cycle and a debugging session. A deck that parses but contains silently truncated values costs far more: the simulation runs to completion and produces results that look plausible but are wrong. Enforcing field widths and validating references before submission eliminates both classes of failure.