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_ELASTIC line causes E to be read where RO was 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.

Keep the template file under version control alongside your generator. When you add a new material model, add its expected field counts to 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.

Card Generator emits *MAT_ lines from parameter dict MAT_TEMPLATES expected field counts Validator split on commas compare count vs template check required fields counts match? LS-DYNA submit deck Reject report line + counts yes no

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 None for 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 Violation dataclass already carries every field needed for either format.
Do not treat a passing field-count check as proof the card is physically correct. The validator confirms structural conformance to the template, not that E and PR are in a physically sensible range. Pair it with a units-and-range check for full coverage.

Verification Checklist

CheckPass conditionFailure symptom
Field count per data lineActual equals template valueValidator prints "field count mismatch"
Required first-line fieldsRO, E, PR non-empty"required field … is empty"
Extra data linesNo line beyond template length"extra data line beyond template"
Clean deckNo output, exit code 0Any 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.