4. Validate Material Card Field Counts Agai
Why Field Counts Break Before Submission
LS-DYNA keyword input is comma-delimited, but the parser is far less forgiving than a spreadsheet. A *MAT_ card that looks visually aligned can still be rejected or, worse, silently misinterpreted when the number of comma-separated fields on a data line does not match the field layout the solver expects for that specific material model. Two failure modes dominate:
- Missing fields — a trailing parameter is omitted, so the parser shifts subsequent values left or fills defaults you did not intend. For example, dropping the density field on a
*MAT_ELASTICline causesEto be read whereROwas expected. - Extra fields — an appended comment or a stray comma adds a column the model does not define. Depending on the card, this triggers a warning, an error, or silent truncation.
The reliable defense is a template-driven validator: for each material keyword you generate, you declare the expected field count per data line, then compare the actual comma-separated field count against that template before the deck is submitted. This chapter builds that validator as a small, self-contained Python module and demonstrates it against real *MAT_ELASTIC and *MAT_PLASTIC_KINEMATIC cards.
Template Model: Keyword → Line → Expected Fields
The validator needs a compact, declarative description of each card. A template maps a keyword to an ordered list of expected field counts, one entry per data line. Blank lines and comment lines ($) are ignored. The template below covers the two cards used in this example; extend it as you add material models.
# mat_templates.py
# Expected comma-separated field counts per data line, in order.
# A value of None means "variable length, do not enforce".
MAT_TEMPLATES = {
"*MAT_ELASTIC": [8], # RO, E, PR, DA, DB, (unused x3)
"*MAT_PLASTIC_KINEMATIC": [8, 8, 8], # 3 data lines, 8 fields each
"*MAT_RIGID": [8, 8, 8],
}
# Fields that must be non-empty on the first data line (quick sanity check).
REQUIRED_FIRST_LINE = {
"*MAT_ELASTIC": ["RO", "E", "PR"],
"*MAT_PLASTIC_KINEMATIC": ["RO", "E", "PR", "SIGY", "ETAN"],
}
Note the deliberate choice of 8 for *MAT_ELASTIC: the card defines eight comma-separated columns on its single data line, even though only the first six are commonly populated. Enforcing eight catches both a dropped trailing comma and an accidental extra column.
The Validator Implementation
The validator walks the deck line by line, tracks the currently active keyword, and checks each data line against the template. It reports every violation with file, line number, keyword, expected count, and actual count so the fix is unambiguous.
# mat_validator.py
from dataclasses import dataclass
from mat_templates import MAT_TEMPLATES, REQUIRED_FIRST_LINE
@dataclass
class Violation:
line_no: int
keyword: str
expected: int
actual: int
reason: str
def _split_fields(line: str):
# Preserve empty fields: "1.0,,0.3" -> ["1.0", "", "0.3"]
return [f.strip() for f in line.rstrip("\n").split(",")]
def validate_deck(path: str):
violations = []
with open(path, "r", encoding="utf-8") as fh:
lines = fh.readlines()
active_kw = None
data_line_idx = 0 # index into the template's per-line list
for i, raw in enumerate(lines, start=1):
stripped = raw.strip()
if not stripped or stripped.startswith("$"):
continue
if stripped.startswith("*"):
active_kw = stripped.upper()
data_line_idx = 0
continue
if active_kw not in MAT_TEMPLATES:
continue # not a material card we validate
expected_list = MAT_TEMPLATES[active_kw]
if data_line_idx >= len(expected_list):
violations.append(Violation(
i, active_kw, 0, len(_split_fields(raw)),
"extra data line beyond template"))
data_line_idx += 1
continue
expected = expected_list[data_line_idx]
fields = _split_fields(raw)
actual = len(fields)
if expected is not None and actual != expected:
violations.append(Violation(
i, active_kw, expected, actual,
"field count mismatch"))
# Required-field check on the first data line only.
if data_line_idx == 0 and active_kw in REQUIRED_FIRST_LINE:
for pos, name in enumerate(REQUIRED_FIRST_LINE[active_kw]):
if pos < len(fields) and fields[pos] == "":
violations.append(Violation(
i, active_kw, expected, actual,
f"required field {name} is empty"))
data_line_idx += 1
return violations
if __name__ == "__main__":
import sys
for v in validate_deck(sys.argv[1]):
print(f"line {v.line_no}: {v.keyword} "
f"expected {v.expected} fields, got {v.actual} "
f"({v.reason})")
Two design points matter for correctness. First, _split_fields keeps empty strings, so 1.0,,0.3 counts as three fields rather than two — this is what catches a missing middle value. Second, the validator tracks data_line_idx per keyword so multi-line cards like *MAT_PLASTIC_KINEMATIC are checked line by line rather than against a single count.
Worked Example: Catching Real Defects
Consider a deck fragment with three intentional defects:
*MAT_ELASTIC
$ RO E PR
7.85e-9 210000.0 0.30
*MAT_PLASTIC_KINEMATIC
7.85e-9 210000.0 0.30 355.0 1200.0
0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Running python mat_validator.py deck.k produces:
line 3: *MAT_ELASTIC expected 8 fields, got 3 (field count mismatch)
line 5: *MAT_PLASTIC_KINEMATIC expected 8 fields, got 5 (field count mismatch)
line 6: *MAT_PLASTIC_KINEMATIC expected 8 fields, got 3 (field count mismatch)
The first violation is the classic case: the *MAT_ELASTIC line was written with only the three populated values, so the parser would read DA, DB, and the three unused columns as absent. The fix is to pad to eight fields with trailing commas. The second and third violations show the same problem on the multi-line plastic card. After padding, re-running the validator returns no output, which is the pass condition.
MAT_TEMPLATES in the same commit — the validator then enforces the new card automatically.How the Validator Fits the Generation Pipeline
The diagram below shows where the validator sits relative to the generator and the solver. The key idea is that validation is a gate: a card that fails the field-count check never reaches the submission step.
Extending and Hardening the Check
The template approach scales cleanly. A few practical refinements worth adopting:
- Variable-length cards. Some keywords accept a variable number of continuation lines. Set the template entry to
Nonefor those lines and rely on the required-field check instead of a strict count. - Numeric sanity. Field count alone does not catch a non-numeric token. Add a per-field type check (float or integer) driven by a parallel template if your generator can emit malformed values.
- CI integration. Return a non-zero exit code when violations exist so the validator can run as a pre-commit hook or CI step. A one-line change to
__main__—sys.exit(1 if violations else 0)— is enough. - Reporting. Emit violations as JSON for machine consumption, or as a plain table for human review. The
Violationdataclass already carries every field needed for either format.
E and PR are in a physically sensible range. Pair it with a units-and-range check for full coverage.Verification Checklist
| Check | Pass condition | Failure symptom |
|---|---|---|
| Field count per data line | Actual equals template value | Validator prints "field count mismatch" |
| Required first-line fields | RO, E, PR non-empty | "required field … is empty" |
| Extra data lines | No line beyond template length | "extra data line beyond template" |
| Clean deck | No output, exit code 0 | Any printed violation |
With the template, validator, and this checklist in place, every generated *MAT_ card is structurally verified before it reaches the solver, and any defect is reported with the exact line number and field counts needed to fix it.