3. Resolve Duplicate Keyword Definitions Across INCLUDE Files

Why duplicates appear in modular decks

When a flat deck is split into logical INCLUDE files, the same keyword block can easily end up in more than one file. A typical pattern is a shared materials.k that is included both directly by the main deck and again by a parts.k that was written to be self-contained. LS-DYNA does not silently merge these blocks: it reads the deck sequentially, and a second definition of the same ID produces a fatal error such as *** Error duplicate definition for part ID 101 or a warning that a material ID is already in use. Because the error message reports the ID and the keyword type but not always the file that introduced the first copy, locating the overlap by hand in a large include tree is slow and error-prone.

The practical task in this case is therefore twofold: detect every duplicated *PART, *SECTION, and *MAT definition across the include tree, and remove the redundancy without changing the model. Detection must be done on the assembled deck, because a single include file in isolation cannot reveal that another file defines the same ID.

Assembling the deck for inspection

LS-DYNA resolves *INCLUDE statements recursively, so the reliable way to see what the solver actually reads is to let it expand the tree. Two approaches work well in practice:

  • Solver-side expansion. Run a short initialization-only job (for example with *CONTROL_TERMINATION set to a tiny end time, or by using the NLOUT/listing options) and read the echoed input in d3hsp. The d3hsp file lists the keywords in the order they were processed and reports duplicate-ID errors with the offending keyword.
  • Pre-processing expansion. Use a small script to walk the include tree and concatenate the files in resolution order, then scan the result. This is faster to iterate on and gives you a single text artifact you can grep and diff.

The script below performs the second approach. It resolves *INCLUDE (and *INCLUDE_PATH prefixes) relative to the including file, records the origin file and line for every keyword block, and reports any *PART, *SECTION, or *MAT ID that is defined more than once. It is deliberately conservative: it treats the first field after the keyword as the ID, which is correct for the standard *PART, *SECTION, and *MAT cards.

import os, re, sys
from collections import defaultdict

KEYWORDS = ("*PART", "*SECTION", "*MAT_", "*MAT,")
INCLUDE_RE = re.compile(r"^\*INCLUDE(_PATH)?\s*$", re.IGNORECASE)

def expand(path, base=None, seen=None, out=None):
    if seen is None: seen = set()
    if out is None: out = []
    path = os.path.normpath(path)
    if path in seen:
        out.append((path, 0, "*INCLUDE", "RECURSION"))
        return out
    seen.add(path)
    with open(path, "r", errors="replace") as fh:
        lines = fh.readlines()
    i = 0
    while i < len(lines):
        raw = lines[i].rstrip("\n")
        stripped = raw.strip()
        if stripped.startswith("*"):
            kw = stripped.split()[0].upper()
            if INCLUDE_RE.match(kw):
                # next non-blank line holds the filename
                j = i + 1
                while j < len(lines) and not lines[j].strip():
                    j += 1
                target = lines[j].strip().strip("'\"")
                target = os.path.join(os.path.dirname(path), target)
                expand(target, seen=seen, out=out)
                i = j + 1
                continue
            out.append((path, i + 1, kw, stripped))
        i += 1
    return out

def audit(root):
    blocks = expand(root)
    defs = defaultdict(list)
    for path, line, kw, text in blocks:
        if kw.startswith("*PART") or kw.startswith("*SECTION") or kw.startswith("*MAT"):
            parts = text.split()
            if len(parts) >= 2:
                defs[(kw.split("_")[0].split(",")[0], parts[1])].append((path, line))
    dupes = {k: v for k, v in defs.items() if len(v) > 1}
    for (kw, ident), locs in sorted(dupes.items()):
        print(f"DUPLICATE {kw} ID {ident}")
        for p, l in locs:
            print(f"    {p}:{l}")
    if not dupes:
        print("No duplicate *PART/*SECTION/*MAT IDs found.")
    return dupes

if __name__ == "__main__":
    audit(sys.argv[1])

Run it against the top-level deck, for example python audit_dupes.py main.k. A representative output for a deck where parts.k re-includes materials.k looks like this:

DUPLICATE *MAT ID 2
    model/materials.k:1
    model/materials.k:1
DUPLICATE *PART ID 101
    model/parts.k:12
    model/parts_extra.k:8

The first entry is the classic double-include: the same file is reached twice through different paths, so every block inside it is reported twice. The second is a genuine overlap between two different part files. Distinguishing these two cases matters, because the fix is different.

Reading the audit result

The SVG below shows the include tree for a small model and how the two duplicate classes arise. The left branch reaches materials.k twice (once directly, once via parts.k), producing a whole-file duplication. The right branch shows two distinct part files that both define part ID 101, producing a single-block collision.

Include tree and duplicate origin main.k materials.k parts.k materials.k (again) parts_extra.k Whole-file duplication materials.k reached twice → every *MAT block reported twice Single-block collision parts.k and parts_extra.k both define *PART ID 101 *MAT ID 2 *PART ID 101

Eliminating the duplicates

For whole-file duplication, the fix is structural: remove the redundant *INCLUDE so each file is reached exactly once. In the example, parts.k should not include materials.k if main.k already includes it before parts.k. The rule to adopt is that shared dependencies are included once, at the highest level that needs them, and leaf files never include their own dependencies. After editing, re-run the audit; the *MAT ID 2 entry should disappear entirely.

For single-block collisions, decide which definition is authoritative and delete the other. If parts_extra.k was a scratch file that was never meant to be included, remove its *INCLUDE line. If both files are legitimate but one part was copied by mistake, delete the copied *PART block and its now-orphaned *SECTION and *MAT if those are unique to it. Never renumber an ID to "make the error go away" unless you have checked every reference: *SET_PART, *CONTACT slave/master lists, *INITIAL_VELOCITY, and rigid body definitions all reference part IDs, and renumbering silently breaks those links.

Deleting a duplicate *MAT that is still referenced by a surviving *PART produces a different fatal error (material ID not defined). Always confirm that the definition you keep is the one the part actually points to, and that the definition you delete is not the only copy referenced elsewhere.

Verification

After the edits, the deck should pass three checks before submission:

  1. Audit is clean. Re-run the script; it should print No duplicate *PART/*SECTION/*MAT IDs found.
  2. Solver initialization succeeds. Run the deck with a minimal termination time. The d3hsp file should contain no duplicate definition messages, and the part/material/section counts reported at initialization should match the expected totals.
  3. Model behavior is unchanged. Compare the initial kinetic and internal energy reported in glstat or d3hsp against the pre-edit run. Identical values confirm that removing the redundant definitions did not alter the assembled model.

A quick way to keep this from recurring is to add the audit script to the deck's pre-submission checklist and to enforce the "leaf files do not include dependencies" convention in review. Both are cheap and catch the problem before the solver does.