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.
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:
| keyword | block_id | title | param_lines |
|---|---|---|---|
| MAT_ELASTIC | 1 | Steel A36 | 2 |
| SECTION_SHELL | 2 | B-pillar inner | 2 |
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.
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_linesbecause thepending_titleflag 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 usedline.startswith("*")instead ofline.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.
Reusable Checklist for This Parser
| Check | Pass condition |
|---|---|
| Comment handling | No $ line reaches title or params |
| Keyword normalization | All keywords upper-case, no leading * |
| Title capture | Exactly one line per block, even if blank |
| ID extraction | Integer from first 10 columns of first non-blank param line |
| Final flush | Last block present in output list |
| Filter correctness | Only 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.