4. Python Script for Parametric Seatbelt Routing with *SET_SEATBELT

Why *SET_SEATBELT Deserves Its Own Generator

In an LS-DYNA restraint model, *SET_SEATBELT is the card that tells the solver which seatbelt elements belong to a given belt system and how they are ordered along the belt path. The ordering matters: LS-DYNA uses the sequence of element IDs in the set to establish the belt slip direction, so a set that is correct in membership but scrambled in order will produce a belt that feeds through the retractor and slip ring in the wrong direction, or that fails to slip at all. Manual editing of this card is therefore error-prone in exactly the way that parametric studies amplify: every routing variant needs a consistent, ordered element list, and a single transposed digit silently changes the model.

This case builds a small, self-contained Python generator that takes a parameterized description of belt segments — each segment carrying an ordered list of *ELEMENT_SEATBELT IDs — and emits a valid *SET_SEATBELT block. The generator is deliberately narrow: it does not create elements (that was the previous case's job) and does not write material cards. It owns one responsibility, the set definition, and it owns it completely, including ID ordering, continuation-line formatting, and the eight-entries-per-line rule that LS-DYNA keyword files require.

The *SET_SEATBELT Card Layout

The card has a fixed structure that the generator must reproduce exactly. The first data line carries the set ID and a title, and the second line carries the set type and the count of entities. The entity IDs then follow, eight per line, with the standard LS-DYNA continuation convention: a trailing + in column 80 of a full line signals that the next line continues the list. Getting the continuation right is the single most common source of "set not found" or truncated-set errors when sets are written by hand or by naive string concatenation.

LineField 1Field 2Field 3Field 4Field 5Field 6Field 7Field 8
Card 1SIDTITLE——————
Card 2TYPECOUNT——————
DataEID1EID2EID3EID4EID5EID6EID7EID8

For a seatbelt set, TYPE is SEATBELT and COUNT is the total number of element IDs across all segments. The generator must compute COUNT from the segment data rather than accept it as an input, because a hand-supplied count that disagrees with the actual list is another silent failure mode.

Segment Model and Ordering Contract

The input to the generator is a list of segments, each with a name and an ordered list of element IDs. The ordering contract is explicit and must be documented in the generator itself: segments are concatenated in the order given, and within each segment the IDs are emitted in the order supplied. This mirrors the physical belt path — retractor, shoulder run, slip ring, lap run, buckle stalk — so that the resulting set reads along the belt in the direction of slip. The generator does not sort IDs; sorting would destroy the path order and is precisely the mistake the tool exists to prevent.

A segment may legitimately be empty (for example, a lap segment that is disabled in a two-point configuration), and the generator must handle that without emitting a stray blank line or miscounting. Duplicate IDs across segments are a modeling error, not a formatting error, so the generator detects them and raises rather than silently deduplicating, because deduplication would change the belt path length.

Input: ordered belt segments retractor [1001, 1002, 1003] shoulder_run [1004 … 1011] slip_ring [1012, 1013] lap_run [1014 … 1021] buckle_stalk [1022, 1023] write_set_seatbelt() • concatenate in segment order • reject duplicate EIDs • COUNT = len(flat list) • 8 IDs per line, pad to 10 cols • trailing '+' on continued lines • no sorting of IDs emit *SET_SEATBELT $ sid, title 9001,DRIVER_BELT $ type, count SEATBELT,23 $ eids, 8 per line 1001,1002,1003,1004, 1005,1006,1007,1008, 1009,1010,1011,1012, 1013,1014,1015,1016, 1017,1018,1019,1020, 1021,1022,1023 Order preserved along belt path; COUNT derived, not supplied. Continued lines carry '+' in col 80.

Implementing the Generator

The implementation below is complete and runnable as a standalone module. It defines a Segment dataclass, a validation step, and the writer. The writer uses fixed-width field formatting rather than simple comma joining, because LS-DYNA keyword readers are column-sensitive in some preprocessors even when the file is nominally free-format.

from dataclasses import dataclass, field
from typing import List

@dataclass
class Segment:
    name: str
    eids: List[int] = field(default_factory=list)

def flatten_segments(segments: List[Segment]) -> List[int]:
    """Concatenate segment EIDs in the given order; reject duplicates."""
    flat: List[int] = []
    seen = set()
    for seg in segments:
        for eid in seg.eids:
            if eid in seen:
                raise ValueError(
                    f"duplicate EID {eid} in segment '{seg.name}'"
                )
            seen.add(eid)
            flat.append(eid)
    return flat

def _fmt_ids(eids: List[int]) -> List[str]:
    """Format EIDs 8 per line, 10-char fields, '+' continuation marker."""
    lines = []
    for i in range(0, len(eids), 8):
        chunk = eids[i:i + 8]
        body = "".join(f"{eid:>10d}" for eid in chunk)
        if i + 8 < len(eids):
            body = body.ljust(79) + "+"
        lines.append(body.rstrip())
    return lines

def write_set_seatbelt(sid: int, title: str,
                       segments: List[Segment]) -> str:
    eids = flatten_segments(segments)
    if not eids:
        raise ValueError("seatbelt set would be empty")
    out = ["*SET_SEATBELT"]
    out.append(f"{sid:>10d}{title}")
    out.append(f"{'SEATBELT':>10s}{len(eids):>10d}")
    out.extend(_fmt_ids(eids))
    return "\n".join(out) + "\n"

if __name__ == "__main__":
    segs = [
        Segment("retractor",    [1001, 1002, 1003]),
        Segment("shoulder_run", list(range(1004, 1012))),
        Segment("slip_ring",    [1012, 1013]),
        Segment("lap_run",      list(range(1014, 1022))),
        Segment("buckle_stalk", [1022, 1023]),
    ]
    print(write_set_seatbelt(9001, "DRIVER_BELT", segs))

Two details in the writer are worth calling out. First, _fmt_ids pads each ID to ten characters with right alignment, matching the fixed-field convention used throughout LS-DYNA keyword files; this keeps the output readable in any text editor and avoids column drift when IDs grow from four to five digits. Second, the continuation marker is placed at column 80 only when more IDs follow, so the final line of the set is never marked as continued — a trailing + on the last line causes some readers to expect another line and either error out or consume the next keyword.

Running and Verifying the Output

Executing the module prints the block shown in the SVG. Verification is a three-step check that can be scripted or done by inspection:

  1. Count check. The COUNT field must equal the number of IDs actually emitted. In the example, five segments contribute 3 + 8 + 2 + 8 + 2 = 23 IDs, and the card reports 23.
  2. Order check. Read the emitted IDs left to right and confirm they follow the physical belt path: retractor first, buckle stalk last. If the order is scrambled, the generator has been modified to sort — revert that change.
  3. Continuation check. Every line except the last must end with + at column 80; the last line must not. A quick awk or Python check on line lengths catches this.

A useful negative test is to feed two segments that share an element ID. The generator should raise ValueError naming the duplicate and the segment where it was found. If it instead produces a set with the ID listed twice, the duplicate guard has been bypassed and the resulting model will double-count that belt element.

Keep the segment names identical to the names used in your geometry generator. When a routing variant disables a segment, pass an empty eids list rather than removing the segment from the list; the generator handles empty segments correctly, and the segment name then still appears in any downstream reporting.
Do not compute COUNT from the number of segments or from a cached value. It must be derived from the flattened ID list on every call. A stale count is the most common cause of a set that loads but behaves as if truncated.

Reusable Parameter Table for Routing Variants

The generator becomes genuinely useful when the segment lists are driven from a small table rather than hard-coded. The table below is a fillable template for a driver-belt routing study; each row is one variant, and the segment columns hold the ordered ID ranges. The generator consumes one row at a time and emits one *SET_SEATBELT block per variant, with the set ID taken from the first column.

Set IDTitleretractorshoulder_runslip_ringlap_runbuckle_stalkExpected COUNT
9001DRIVER_BELT_BASE1001–10031004–10111012–10131014–10211022–102323
9002DRIVER_BELT_HIGH_ANCHOR1001–10031004–10111012–10131014–10211022–102323
9003DRIVER_BELT_NO_LAP1001–10031004–10111012–1013(empty)1022–102315

The third row demonstrates the empty-segment case: the lap run is disabled, the generator emits 15 IDs, and the resulting set still reads correctly along the remaining belt path. This is the pattern to reuse when a routing variant removes a segment without renumbering the others.

What This Generator Deliberately Does Not Do

It does not renumber elements, does not validate that the referenced IDs exist in the model, and does not write the *ELEMENT_SEATBELT cards themselves. Those responsibilities belong to the geometry generator and to a separate cross-reference check. Keeping the set writer narrow means that when a routing variant fails, the failure is localized: either the segment table is wrong, or the set writer is wrong, and the two can be tested independently. That separation is what makes rapid variation of belt routing configurations practical without manual keyword editing.