3. Parsing LS-DYNA keyword files with Pytho

Parsing LS-DYNA keyword files with Python 学习卡片

LS-DYNA input decks are plain-text keyword files. Every card begins with an asterisk, and the meaning of each line depends on the currently active keyword block. Before any automation can rename parts, patch material parameters, or extract section data, the deck must be parsed into a structure Python can reason about. This section covers the physical layout of a keyword file, the four constructs that dominate real decks (*KEYWORD, *INCLUDE, *PARAMETER, and *DEFINE), and the text-processing techniques that keep a parser stable across the many dialects LS-DYNA accepts.

Deck structure and the fixed-width convention

A keyword file is a sequence of blocks. Each block starts with a keyword line such as *ELEMENT_SHELL or *MAT_PIECEWISE_LINEAR_PLASTICITY, optionally followed by a title line, then one or more data lines. LS-DYNA historically reads data lines as fixed-width fields of 8 or 10 characters, but modern decks are usually written comma-separated. A robust parser must handle both, because a deck assembled from multiple sources will mix them.

The first non-comment line is normally *KEYWORD, which declares the file format. Two variants matter:

*KEYWORD
*KEYWORD, LOCAL
*KEYWORD, DYNA3D
*KEYWORD, LONG=Y

LONG=Y switches node and element IDs to 64-bit, which changes field widths. If your parser assumes 8-character fields and the deck is long-format, IDs will silently truncate. Read the *KEYWORD line first and set a width policy before parsing anything else.

Handling *INCLUDE

Real production decks are almost never a single file. They use *INCLUDE to pull in sub-assemblies, material libraries, and boundary-condition fragments:

*INCLUDE
../materials/steel_DP780.k
*INCLUDE_PATH
/opt/lsdyna/libraries

Two rules govern correct resolution. First, relative paths in *INCLUDE are resolved relative to the directory of the file that contains the include, not the working directory. Second, *INCLUDE_PATH entries are searched in order when a file is not found locally. A parser that ignores these rules will fail on decks that run fine in LS-DYNA. The practical approach is a recursive loader that tracks the current file's directory and maintains a search path list.

Circular includes are legal in some LS-DYNA workflows but will cause infinite recursion in a naive parser. Track visited absolute paths and raise a clear error when a cycle is detected.

*PARAMETER and *DEFINE substitution

Parameterized decks use *PARAMETER to declare named values and & or *DEFINE to reference them:

*PARAMETER
thk_shell   2.5
mat_steel   1
*DEFINE_CURVE
&curve_id
0.0, 0.0
0.01, 250.0

Substitution happens before the deck is interpreted, so a parser must expand parameters in a pre-pass. The syntax is not uniform: older decks use &name, newer ones use *DEFINE_... with & references, and some use %name%. A safe strategy is to collect all *PARAMETER entries into a dictionary, then run a regex substitution over every subsequent line, iterating until no further replacements occur (to handle parameters defined in terms of other parameters).

Parameter names are case-insensitive in LS-DYNA but the substitution is textual. If a parameter value is 2.5 and it appears inside a quoted title, the substitution will still fire. Restrict substitution to data lines, not title lines, unless you have verified the deck does not rely on that behavior.

A minimal but robust parser skeleton

The following structure handles the common cases without external dependencies. It reads a file, resolves includes, expands parameters, and yields blocks as dictionaries.

import re, os

PARAM_RE = re.compile(r'&([A-Za-z_]\w*)')
INCLUDE_RE = re.compile(r'^\*INCLUDE\b', re.I)

def load_deck(path, search_paths=None, _seen=None):
    search_paths = search_paths or []
    _seen = _seen or set()
    path = os.path.abspath(path)
    if path in _seen:
        raise RuntimeError(f"circular include: {path}")
    _seen.add(path)
    with open(path, 'r', errors='replace') as f:
        lines = f.read().splitlines()
    base = os.path.dirname(path)
    out = []
    for ln in lines:
        if INCLUDE_RE.match(ln):
            continue  # handled by caller
        out.append(ln)
    return out, base

This is deliberately incomplete: it shows the shape of the loader. The include resolution loop, parameter expansion, and block splitting are added next.

Splitting into blocks

Once includes are resolved and parameters expanded, split the flat line list into blocks. A block starts at any line whose first non-space character is *. Everything until the next such line belongs to the current block. The first line is the keyword; the second line, if present and not starting with *, is the title; the rest are data lines.

def split_blocks(lines):
    blocks, cur = [], None
    for ln in lines:
        if ln.lstrip().startswith('*'):
            if cur: blocks.append(cur)
            cur = {'keyword': ln.strip(), 'title': '', 'data': []}
        elif cur is not None:
            if not cur['title'] and not cur['data']:
                cur['title'] = ln
            else:
                cur['data'].append(ln)
    if cur: blocks.append(cur)
    return blocks

This simple splitter is correct for the vast majority of decks. The edge cases are *INCLUDE (already handled), *PARAMETER (should be consumed before splitting), and comment lines beginning with $, which should be dropped or preserved depending on whether you need to round-trip the deck.

Field parsing: comma vs fixed-width

Data lines come in two flavors. Comma-separated lines are trivial: line.split(',') and strip whitespace. Fixed-width lines require slicing at 8- or 10-character boundaries. Detect the format per line: if the line contains a comma, treat it as CSV; otherwise slice. This heuristic fails only for lines that contain a comma inside a quoted string, which is rare in LS-DYNA data lines.

def parse_fields(line, width=8):
    if ',' in line:
        return [f.strip() for f in line.split(',')]
    return [line[i:i+width].strip()
            for i in range(0, len(line), width)]

For long-format decks, pass width=10 or width=20 depending on the keyword. The *KEYWORD, LONG=Y declaration is the authoritative signal.

What the parser should produce

A useful parser output is a list of block dictionaries, each with a normalized keyword (uppercase, no leading *), a title, and a list of parsed field rows. From there, downstream code can index blocks by keyword, extract node coordinates, or rewrite material parameters. The parser should also record the source file and line number for every block, because error messages that say "line 412 of main.k" are far more useful than "parse error".

Keep the raw line alongside the parsed fields. When a downstream tool needs to write the deck back out, preserving the original formatting avoids surprising diffs in version control.

Common failure modes

SymptomLikely causeFix
IDs truncated to 8 digitsLong-format deck parsed with width=8Read *KEYWORD, LONG=Y and set width
Missing materialsRelative *INCLUDE resolved against CWDResolve against the including file's directory
Parameter not substitutedSubstitution ran once, not iterativelyLoop until no &name remains
Infinite loopCircular includeTrack visited absolute paths
Fields shifted by oneTitle line treated as dataSkip the first non-keyword line as title

With the loader, parameter expander, block splitter, and field parser in place, the deck is now a Python data structure. The next step is to build queries on top of it: find all *MAT_ blocks, extract node sets, or rewrite thickness values. Those operations are straightforward once the parsing layer is correct, and they are the subject of the following sections.

LS-DYNA Keyword File Parsing Pipeline main.k *KEYWORD, LONG=Y Include Resolver relative to file dir Parameter Expander &name → value Block Splitter * → new block Parsed Block Structure keyword "MAT_PIECEWISE_LINEAR_ PLASTICITY" title "DP780 steel" data rows (parsed fields) [["1","7850","2.1e11"], ["0.3","250.0","0.01"]] Failure Modes • width=8 on LONG=Y deck → truncated IDs • relative *INCLUDE resolved against CWD • single-pass &name substitution • circular include → infinite recursion Robustness Rules • read *KEYWORD first, set field width • resolve includes against including file • iterate substitution until stable • track visited paths, record line numbers