2. Automated Extraction of Baseline Model Parameters
Why a Baseline Extraction Pass Matters
Before a design space can be defined, the DOE driver must know what the model currently contains. An LS-DYNA input deck is a flat sequence of keyword blocks, and the values that matter for a DOE study are scattered across *MAT_*, *SECTION_*, *PART_*, *CONTROL_*, and *INITIAL_VELOCITY cards. Extracting those values by hand is error-prone and does not scale. The goal of this case is a small, deterministic parser that reads an existing deck, locates every candidate factor, and emits a baseline table that later stages (factor-level definition, matrix generation, batch substitution) can consume without ambiguity.
The parser must be position-aware, not just value-aware. LS-DYNA keyword cards are fixed-field in many cases, and the same numeric value can appear in different columns with different meanings. A robust extractor therefore records, for each factor: the keyword block, the card index within the block, the field position, the raw token, and the parsed numeric value. That record is the baseline configuration.
Deck Structure and the Extraction Contract
An LS-DYNA deck is line-oriented. A keyword line begins with *; continuation and data cards follow until the next keyword line. Comments begin with $. The extractor's contract is deliberately narrow so it stays testable:
- Read the deck as text, preserving line order and line numbers.
- Track the current keyword block and the data-card index inside it.
- For each candidate factor, match on
(keyword, card_index, field_index)and return the token at that position. - Emit one row per factor with the raw token and a parsed float, or a clear
MISSINGstatus.
This contract avoids the common failure of "search the whole file for a number" — which silently picks up the wrong card when a model has multiple materials or sections.
Reference Implementation
The following Python script is self-contained and runnable. It defines a small candidate-factor specification, parses a sample deck embedded as a string (so the example is reproducible without external files), and prints the baseline table. In practice you point deck_path at the real .k file.
import re
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class FactorSpec:
name: str
keyword: str # e.g. "*MAT_PLASTIC_KINEMATIC"
card_index: int # 0-based data card within the block
field_index: int # 0-based whitespace/comma token on that card
unit: str = ""
@dataclass
class BaselineRow:
name: str
keyword: str
card_index: int
field_index: int
raw: Optional[str]
value: Optional[float]
unit: str
status: str
CANDIDATES = [
FactorSpec("rho", "*MAT_PLASTIC_KINEMATIC", 0, 0, "kg/mm^3"),
FactorSpec("E", "*MAT_PLASTIC_KINEMATIC", 0, 1, "GPa"),
FactorSpec("PR", "*MAT_PLASTIC_KINEMATIC", 0, 2, "-"),
FactorSpec("SIGY", "*MAT_PLASTIC_KINEMATIC", 0, 3, "GPa"),
FactorSpec("THICK", "*SECTION_SHELL", 0, 3, "mm"),
FactorSpec("TEND", "*CONTROL_TERMINATION", 0, 0, "ms"),
]
def tokenize(line: str):
# LS-DYNA data cards are comma- or whitespace-separated; strip trailing comments
line = line.split("$", 1)[0]
return [t for t in re.split(r"[,\s]+", line.strip()) if t]
def parse_deck(text: str):
"""Return list of (keyword, card_index, tokens) for every data card."""
blocks = []
current_kw = None
card_idx = 0
for raw in text.splitlines():
s = raw.strip()
if not s or s.startswith("$"):
continue
if s.startswith("*"):
current_kw = s.upper()
card_idx = 0
continue
if current_kw is None:
continue
blocks.append((current_kw, card_idx, tokenize(s)))
card_idx += 1
return blocks
def extract(blocks, specs):
rows = []
for spec in specs:
hit = None
for kw, ci, toks in blocks:
if kw == spec.keyword.upper() and ci == spec.card_index:
hit = toks
break
if hit is None or spec.field_index >= len(hit):
rows.append(BaselineRow(spec.name, spec.keyword, spec.card_index,
spec.field_index, None, None, spec.unit, "MISSING"))
continue
raw = hit[spec.field_index]
try:
val = float(raw)
status = "OK"
except ValueError:
val, status = None, "NON_NUMERIC"
rows.append(BaselineRow(spec.name, spec.keyword, spec.card_index,
spec.field_index, raw, val, spec.unit, status))
return rows
SAMPLE_DECK = """
$ baseline sample deck
*KEYWORD
*MAT_PLASTIC_KINEMATIC
$ rho E PR SIGY
7.85e-6 210.0 0.30 0.35
*SECTION_SHELL
$ secid elform shrf nip t
1 2 1.0 5 2.0
*CONTROL_TERMINATION
$ endtim
50.0
*END
"""
if __name__ == "__main__":
blocks = parse_deck(SAMPLE_DECK)
for r in extract(blocks, CANDIDATES):
print(f"{r.name:6s} {r.keyword:24s} card={r.card_index} "
f"field={r.field_index} raw={r.raw!s:8s} "
f"value={r.value!s:10s} unit={r.unit:8s} {r.status}")
Running this against the embedded sample deck produces a baseline table with six rows. The expected output shape is:
rho *MAT_PLASTIC_KINEMATIC card=0 field=0 raw=7.85e-6 value=7.85e-06 unit=kg/mm^3 OK
E *MAT_PLASTIC_KINEMATIC card=0 field=1 raw=210.0 value=210.0 unit=GPa OK
PR *MAT_PLASTIC_KINEMATIC card=0 field=2 raw=0.30 value=0.3 unit=- OK
SIGY *MAT_PLASTIC_KINEMATIC card=0 field=3 raw=0.35 value=0.35 unit=GPa OK
THICK *SECTION_SHELL card=0 field=3 raw=2.0 value=2.0 unit=mm OK
TEND *CONTROL_TERMINATION card=0 field=0 raw=50.0 value=50.0 unit=ms OK
field_index is zero-based on the tokenized card, not on the fixed-column layout. For decks that rely on strict 10-column fields (common in older LS-DYNA models), replace tokenize with a fixed-width slicer and keep the same FactorSpec contract — the rest of the pipeline is unchanged.Reading the Baseline Map
The diagram below shows how a single deck is decomposed into keyword blocks, how each candidate factor is anchored to a specific (keyword, card, field) coordinate, and how the extractor resolves each anchor to either a numeric baseline value or an explicit status. The right-hand column is the artifact that downstream DOE stages consume.
Verification and Failure Modes
Verification is straightforward because the extractor is deterministic. Run the script against the sample deck and confirm that all six rows report OK with the values shown above. Then run it against the real deck and inspect the status column:
| Status | Typical cause | Corrective action |
|---|---|---|
MISSING | Keyword name differs (e.g. *MAT_PLASTIC_KINEMATIC_TITLE) or the factor lives on a different card index. | Inspect the deck around the keyword; adjust keyword or card_index in the spec. |
NON_NUMERIC | Field is a title string, an ID reference, or a blank placeholder. | Re-check field_index; confirm the card layout against the LS-DYNA keyword manual for that version. |
Wrong value, status OK | Multiple blocks share the keyword (e.g. several *MAT_* cards); the first match is not the intended one. | Extend FactorSpec with a part or material ID selector and filter blocks by that ID. |
NON_NUMERIC tokens to zero. A zero baseline will propagate into the DOE matrix and produce physically meaningless runs. Treat any non-OK row as a hard stop until the spec is corrected.Deliverable: Baseline Factor Table
The output of this case is a CSV-ready table that the next stage consumes directly. The columns below are the minimum needed to define a design space without re-reading the deck:
| factor | keyword | card | field | baseline | unit | status |
|---|---|---|---|---|---|---|
| rho | *MAT_PLASTIC_KINEMATIC | 0 | 0 | 7.85e-06 | kg/mm^3 | OK |
| E | *MAT_PLASTIC_KINEMATIC | 0 | 1 | 210.0 | GPa | OK |
| PR | *MAT_PLASTIC_KINEMATIC | 0 | 2 | 0.30 | - | OK |
| SIGY | *MAT_PLASTIC_KINEMATIC | 0 | 3 | 0.35 | GPa | OK |
| THICK | *SECTION_SHELL | 0 | 3 | 2.0 | mm | OK |
| TEND | *CONTROL_TERMINATION | 0 | 0 | 50.0 | ms | OK |
Once every row is OK, the baseline is established and the design space can be defined around these values with confidence that the anchors are correct. The same FactorSpec list becomes the substitution map used later during batch model preparation, so the extraction pass and the injection pass stay consistent by construction.