1. Audit LS-DYNA Keyword Deck for Dataset Readiness
Why an audit precedes extraction
A dataset built from an LS-DYNA keyword deck is only as trustworthy as the deck's internal consistency. Before any parser writes a single row, you need to know what the deck actually contains: which parts exist, which materials and sections they reference, how contacts are defined, and which control cards govern the run. The audit produces a structured inventory — a machine-readable map of the deck's entities and their cross-references — plus a list of readiness flags for entries that are missing, duplicated, or ambiguous. Those flags become the dataset's provenance metadata: a downstream model trained on a part with an unresolved material reference is silently learning from a defect.
This case works on a supplied deck and delivers an inventory table plus a flag register. It does not parse the deck into typed records (that is a later concern) and it does not extract per-part mass or names into a normalized dataset. The deliverable here is the audit: what is present, what is referenced, what is broken.
What the audit must capture
An LS-DYNA deck is a sequence of keyword blocks. Each block begins with a *KEYWORD line, optionally followed by fixed-format data cards. For dataset readiness, five block families matter most:
- Parts —
*PARTcards carry a part ID (PID), a section ID (SECID), and a material ID (MID). These three IDs are the backbone of every per-part dataset row. - Materials —
*MAT_*blocks define MID and material parameters. The specific*MATsubtype (e.g.*MAT_PIECEWISE_LINEAR_PLASTICITY) determines which fields are extractable. - Sections —
*SECTION_*blocks define SECID and thickness/integration data. Shell and solid sections expose different fields. - Contacts —
*CONTACT_*blocks define contact definitions, often referencing part sets or segment sets rather than raw PIDs. - Control —
*CONTROL_*blocks (termination, timestep, energy, output) set global run parameters that become dataset-level context fields.
The audit's job is to enumerate each of these, record the IDs and key attributes, and then resolve the cross-references: does every SECID and MID referenced by a *PART actually exist? Does every part belong to at least one contact, or is it unconstrained? Are there duplicate IDs that will collide when the deck is flattened into a table?
Reading the deck without a full parser
You do not need a complete LS-DYNA parser to audit. A line-oriented scan that tracks the current keyword and collects the first data card of each block is sufficient for the inventory. The following Python snippet is self-contained and runs against any deck file; it produces the inventory and the flag register in one pass.
import re
from collections import defaultdict
KEYWORD_RE = re.compile(r"^\*([A-Z0-9_]+)", re.IGNORECASE)
def audit_deck(path):
inventory = {
"parts": {}, # pid -> {secid, mid, title}
"materials": {}, # mid -> subtype
"sections": {}, # secid -> subtype
"contacts": [], # list of contact keyword names
"controls": [], # list of control keyword names
}
flags = []
current = None
pending_title = None
with open(path, "r", errors="replace") as fh:
for lineno, raw in enumerate(fh, 1):
line = raw.rstrip("\n")
if not line.strip() or line.lstrip().startswith("$"):
continue
m = KEYWORD_RE.match(line.strip())
if m:
current = m.group(1).upper()
if current == "PART":
pending_title = None
elif current.startswith("MAT_"):
inventory["materials"].setdefault("_pending", []).append(current)
elif current.startswith("SECTION_"):
inventory["sections"].setdefault("_pending", []).append(current)
elif current.startswith("CONTACT_"):
inventory["contacts"].append(current)
elif current.startswith("CONTROL_"):
inventory["controls"].append(current)
continue
# data card under current keyword
if current == "PART":
fields = line.split()
if len(fields) >= 3 and fields[0].isdigit():
pid, secid, mid = int(fields[0]), int(fields[1]), int(fields[2])
if pid in inventory["parts"]:
flags.append(f"DUPLICATE_PART pid={pid} line={lineno}")
inventory["parts"][pid] = {"secid": secid, "mid": mid}
elif current and current.startswith("MAT_"):
fields = line.split()
if fields and fields[0].isdigit():
mid = int(fields[0])
inventory["materials"][mid] = current
elif current and current.startswith("SECTION_"):
fields = line.split()
if fields and fields[0].isdigit():
secid = int(fields[0])
inventory["sections"][secid] = current
# resolve cross-references
for pid, refs in inventory["parts"].items():
if refs["mid"] not in inventory["materials"]:
flags.append(f"MISSING_MATERIAL pid={pid} mid={refs['mid']}")
if refs["secid"] not in inventory["sections"]:
flags.append(f"MISSING_SECTION pid={pid} secid={refs['secid']}")
if not inventory["contacts"]:
flags.append("NO_CONTACT_DEFINED")
if not inventory["controls"]:
flags.append("NO_CONTROL_CARDS")
inventory["materials"].pop("_pending", None)
inventory["sections"].pop("_pending", None)
return inventory, flags
if __name__ == "__main__":
inv, flags = audit_deck("model.k")
print(f"parts={len(inv['parts'])} materials={len(inv['materials'])} "
f"sections={len(inv['sections'])} contacts={len(inv['contacts'])}")
for f in flags:
print("FLAG:", f)
Run it as python audit.py with the deck named model.k in the working directory. The output is a one-line summary followed by one FLAG: line per issue. On a clean deck the summary line is the only output; any flag line is a readiness defect that must be resolved or explicitly annotated before extraction.
$) and blank lines. LS-DYNA decks are frequently padded with comment banners; skipping them keeps the ID collection clean and makes line numbers in flags point at real data cards.Interpreting the inventory and flags
The inventory is a dictionary keyed by entity type. For each part you have its secid and mid; for each material and section you have the subtype keyword, which tells you which fields are extractable later. The flag register is the audit's verdict. Four flag classes cover the common readiness failures:
| Flag | Meaning | Typical cause | Action before extraction |
|---|---|---|---|
MISSING_MATERIAL | A part references a MID with no matching *MAT_* block | Material block deleted or MID typo | Restore the material block or correct the PID's MID; do not extract the part until resolved |
MISSING_SECTION | A part references a SECID with no matching *SECTION_* block | Section block omitted from the include set | Add the missing include or fix the SECID |
DUPLICATE_PART | Two *PART cards share a PID | Merged decks with overlapping ID ranges | Renumber one part and update all references; duplicates will collide in the dataset table |
NO_CONTACT_DEFINED | No *CONTACT_* block found | Contact defined in an un-included file | Confirm the include chain; a deck without contacts is not a crash model |
Two further ambiguities deserve explicit handling even though they are not hard errors. First, a *PART card whose title field is blank leaves the part unnamed — the dataset row will have an empty name, which is legal but should be recorded as AMBIGUOUS_NAME so downstream consumers know the label is synthetic. Second, a *CONTACT that references a part set (*SET_PART) rather than raw PIDs means the part-to-contact mapping cannot be resolved from the *PART cards alone; record the contact as SET_BASED and defer resolution to the set-expansion step.
How the audit maps to dataset fields
The inventory is not the dataset — it is the schema contract for the dataset. Each entity type becomes a field group, and each flag becomes a provenance column. The diagram below shows how the five block families feed the inventory and how the cross-reference resolution produces the flag register that gates extraction.
The gate at the bottom is the operational point: extraction should not begin until every flag is either resolved (the deck is corrected) or annotated (the defect is recorded as a known limitation of the dataset). Annotated flags travel with the dataset as provenance columns — for example, a material_resolved boolean and a flag_notes text field — so a model consumer can filter or weight rows accordingly.
Verification and common failure symptoms
Verify the audit against the deck by spot-checking three things. First, the part count in the summary line should match the number of *PART cards you can count with a text search; a mismatch means the scanner is misreading a data card as a keyword or vice versa. Second, every MISSING_MATERIAL flag should correspond to a MID that genuinely has no *MAT_* block — confirm by searching the deck for that MID. Third, the contact and control lists should be non-empty for any real crash model; an empty list almost always means the relevant blocks live in an include file that was not passed to the scanner.
Typical failure symptoms and their causes: a summary line reporting zero parts usually means the deck uses a different keyword spelling or the file path is wrong; a flood of MISSING_SECTION flags on an otherwise clean deck usually means the section blocks use a subtype the scanner's prefix match missed (check the exact *SECTION_ spelling); and duplicate part flags clustered at a specific line range point to a merged include with overlapping ID ranges, which must be renumbered before extraction.
Audit checklist
- Deck path confirmed and file readable; scanner runs without exceptions.
- Part count matches manual
*PARTcount; no zero-part summary. - Every part's
SECIDandMIDresolves to an existing block, or the miss is flagged. - No duplicate PIDs; any duplicates renumbered before extraction.
- At least one
*CONTACT_*and one*CONTROL_*block present, or absence flagged. - Set-based contacts recorded as
SET_BASEDand deferred to set expansion. - Blank part titles recorded as
AMBIGUOUS_NAME. - Flag register versioned with the dataset; every flag resolved or annotated.