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 MISSING status.

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
The 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.

Input deck (line-ordered) *MAT_PLASTIC_KINEMATIC card 0: rho E PR SIGY 7.85e-6 210.0 0.30 0.35 *SECTION_SHELL card 0: secid elform shrf nip t 1 2 1.0 5 2.0 *CONTROL_TERMINATION card 0: endtim 50.0 Factor anchors (keyword, card, field) rho → MAT, c0, f0 E → MAT, c0, f1 PR → MAT, c0, f2 SIGY → MAT, c0, f3 THICK → SEC, c0, f3 TEND → CTRL, c0, f0 Baseline configuration factor value unit status rho7.85e-06kg/mm^3OK E210.0GPaOK PR0.30-OK SIGY0.35GPaOK THICK2.0mmOK TEND50.0msOK Resolution outcomes OK — token parsed to float MISSING — keyword/card/field absent NON_NUMERIC — token not a number Any non-OK row blocks DOE generation. Baseline table is the single source of truth for factor bounds in the next stage. Coordinates (keyword, card, field) prevent cross-card value collisions.

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:

StatusTypical causeCorrective action
MISSINGKeyword 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_NUMERICField 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 OKMultiple 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.
Do not silently coerce 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:

factorkeywordcardfieldbaselineunitstatus
rho*MAT_PLASTIC_KINEMATIC007.85e-06kg/mm^3OK
E*MAT_PLASTIC_KINEMATIC01210.0GPaOK
PR*MAT_PLASTIC_KINEMATIC020.30-OK
SIGY*MAT_PLASTIC_KINEMATIC030.35GPaOK
THICK*SECTION_SHELL032.0mmOK
TEND*CONTROL_TERMINATION0050.0msOK

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.