1. Parse LS-DYNA Keyword Files for Material and Section Blocks

Why a Keyword Parser Is the Foundation of Automation

Every downstream automation task in this course — generating material cards, rewriting section thicknesses, auditing IDs, or building a parameter database — depends on one capability: reliably reading an existing LS-DYNA keyword file and turning its text into structured data. LS-DYNA input decks (.k files) are line-oriented, fixed-format, and keyword-driven. A parser that understands this structure lets you stop treating the deck as opaque text and start treating it as a queryable object model.

The task for this case is narrow and concrete: read a .k file, locate every *MAT_ and *SECTION_ keyword block, and extract each block's keyword name, its ID, its title, and its raw parameter lines into an in-memory representation. We deliberately do not interpret the numeric fields yet — that belongs to later cases. Here we build the structural skeleton that everything else will hang on.

How LS-DYNA Keyword Blocks Are Structured

A keyword block begins with a line whose first non-blank character is *. The keyword name follows, case-insensitively, and may carry a suffix such as *MAT_ELASTIC or *SECTION_SHELL. Immediately after the keyword line comes a title line (free text, often blank), then one or more parameter lines in fixed-width fields. A block ends when the next line starting with * appears, or at end of file.

Two practical details matter for a robust parser:

  • Comments and blanks. Lines beginning with $ are comments and must be skipped. Blank lines inside a block are legal and should be preserved as empty parameter lines rather than treated as terminators.
  • ID location. For most *MAT_ and *SECTION_ keywords the ID is the first field of the first parameter line (columns 1–10 in fixed format). We extract it as a string and convert to an integer, tolerating leading/trailing whitespace.
LS-DYNA is case-insensitive for keywords but case-sensitive for nothing else in the deck. Normalize keyword names to upper case during parsing so downstream comparisons are stable.

Parser Design: A Two-Pass Line Scanner

The cleanest approach is a single forward scan that accumulates lines into the current block and flushes the block whenever a new keyword line or EOF is reached. This avoids regex gymnastics over the whole file and keeps memory proportional to the number of blocks, not the file size.

The data model below captures exactly what the goal requires — keyword, ID, title, and parameter lines — plus a source line number for diagnostics.

from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class KeywordBlock:
    keyword: str                 # e.g. "MAT_ELASTIC", "SECTION_SHELL"
    line_no: int                 # 1-based line where the *KEYWORD appeared
    title: str = ""
    param_lines: List[str] = field(default_factory=list)
    block_id: Optional[int] = None

    def __repr__(self):
        return (f"KeywordBlock({self.keyword!r}, id={self.block_id}, "
                f"title={self.title!r}, params={len(self.param_lines)})")

def _extract_id(param_lines: List[str]) -> Optional[int]:
    """First field of the first parameter line, fixed-width 10 chars."""
    for line in param_lines:
        if line.strip() == "":
            continue
        field0 = line[:10].strip()
        if field0:
            try:
                return int(field0)
            except ValueError:
                return None
    return None

def parse_keyword_file(path: str) -> List[KeywordBlock]:
    blocks: List[KeywordBlock] = []
    current: Optional[KeywordBlock] = None
    pending_title = False

    with open(path, "r", encoding="utf-8", errors="replace") as fh:
        for lineno, raw in enumerate(fh, start=1):
            line = raw.rstrip("\n").rstrip("\r")

            # Skip comment lines entirely
            if line.lstrip().startswith("$"):
                continue

            # New keyword block starts here
            if line.lstrip().startswith("*"):
                if current is not None:
                    current.block_id = _extract_id(current.param_lines)
                    blocks.append(current)
                name = line.lstrip()[1:].strip().upper()
                current = KeywordBlock(keyword=name, line_no=lineno)
                pending_title = True
                continue

            if current is None:
                continue  # preamble before first keyword

            if pending_title:
                current.title = line.strip()
                pending_title = False
            else:
                current.param_lines.append(line)

    if current is not None:
        current.block_id = _extract_id(current.param_lines)
        blocks.append(current)

    return blocks

Two design choices are worth calling out. First, the title is consumed on the first non-comment line after the keyword, which matches LS-DYNA's convention that the title line is mandatory even when empty. Second, ID extraction is deferred until the block is flushed, so it always sees the complete parameter list and can skip leading blank lines.

Filtering to Materials and Sections

The goal asks specifically for *MAT_ and *SECTION_ blocks. Rather than hard-code a filter inside the scanner, keep the parser general and filter at the call site — this keeps the same function reusable for *PART, *CONTACT, or any other keyword family later.

def select_material_and_section(blocks):
    return [b for b in blocks
            if b.keyword.startswith("MAT_") or b.keyword.startswith("SECTION_")]

if __name__ == "__main__":
    all_blocks = parse_keyword_file("model.k")
    targets = select_material_and_section(all_blocks)
    for b in targets:
        print(b)

Worked Example and Expected Output

Consider a minimal deck fragment:

$ material definition
*MAT_ELASTIC
Steel A36
         1     7.85e-9    210000.0       0.300
$ shell section
*SECTION_SHELL
B-pillar inner
         2         2         1         0
       1.2       1.2       1.2       1.2

Running the parser on this file should yield two blocks. The expected structured result is:

keywordblock_idtitleparam_lines
MAT_ELASTIC1Steel A362
SECTION_SHELL2B-pillar inner2

Note that the comment lines are gone, the title is separated from the parameters, and the ID is an integer. This is exactly the shape downstream generators need: they can look up a material by ID, read its title for reporting, and hand the raw parameter lines to a field-level interpreter.

Visualizing the Parse Flow

The diagram below shows how a single line is classified and routed during the scan, and how blocks accumulate and flush. It is the mental model you should hold when debugging a parser that mis-assigns titles or drops parameter lines.

Line-by-line classification during keyword parsing model.k line iterator starts with $ or * ? skip comment line flush current block start new KeywordBlock first line after keyword? → title, else → param_lines KeywordBlock accumulator keyword · line_no · title · param_lines · block_id block_id = int(param_lines[0][:10]) List[KeywordBlock] → filter MAT_ / SECTION_ ready for downstream automation

Verification and Common Failure Symptoms

You can verify the parser without any LS-DYNA solver by checking three invariants on the worked example: (1) the number of returned blocks equals the number of *MAT_/*SECTION_ keyword lines; (2) every block's block_id matches the first field of its first parameter line; (3) no $ comment text appears in any title or parameter line. A quick assertion harness:

blocks = select_material_and_section(parse_keyword_file("model.k"))
assert [b.keyword for b in blocks] == ["MAT_ELASTIC", "SECTION_SHELL"]
assert [b.block_id for b in blocks] == [1, 2]
assert all("$" not in b.title for b in blocks)

When the parser misbehaves, the symptoms are usually diagnostic:

  • Titles shifted into parameters. The title line was blank and got appended to param_lines because the pending_title flag was cleared too early. Confirm the flag is only cleared after consuming exactly one line.
  • Missing blocks. A keyword line had leading whitespace before * and the check used line.startswith("*") instead of line.lstrip().startswith("*").
  • Wrong IDs. The deck uses comma-separated rather than fixed-width fields; the [:10] slice then grabs the wrong characters. Detect this by checking whether the first parameter line contains commas and switch to a split-based extractor.
  • Trailing block lost. The final block was never flushed because the loop ended without a following keyword line — the post-loop flush handles this, so verify it is present.
Do not attempt to interpret parameter values in this parser. Mixing structural parsing with field-level semantics makes both harder to test and is the single most common source of silent corruption in keyword automation.

Reusable Checklist for This Parser

CheckPass condition
Comment handlingNo $ line reaches title or params
Keyword normalizationAll keywords upper-case, no leading *
Title captureExactly one line per block, even if blank
ID extractionInteger from first 10 columns of first non-blank param line
Final flushLast block present in output list
Filter correctnessOnly MAT_* and SECTION_* returned

With this parser in place, the next automation steps become straightforward: you can iterate over KeywordBlock objects, look up materials by ID, and hand parameter lines to a field interpreter — all without re-reading the file or re-parsing text.