4. Unit Consistency Check for Crash Assembl

Why unit consistency is a release gate, not a formatting detail

In a crash assembly, every subsystem deck is authored independently, and each authoring team may have chosen its own consistent unit system. A body-in-white model exported from a CAD-oriented preprocessor is frequently built in mm-ton-s, while a barrier or a restraint model imported from a supplier may arrive in m-kg-s. Each deck is internally consistent, so nothing looks wrong when the subsystems are inspected one at a time. The failure appears only after assembly, when the solver combines a mass expressed in tonnes with a length expressed in metres and a velocity expressed in mm/ms.

The consequence is not a crash — it is a silently wrong answer. LS-DYNA does not carry unit metadata; it evaluates the numbers it is given. If a 1.5 t body is declared as 1.5 in a deck whose length unit is metres, the solver reads 1.5 kg. The resulting kinetic energy is off by a factor of 1000, the initial velocity produces a completely different strain rate regime, and contact stiffnesses derived from *CONTACT penalties are mismatched against the shell thickness. The run terminates normally and the animation looks plausible, which is exactly why this check must be automated and must run before any result is trusted.

The three coherent systems and their conversion factors

Only a small number of unit systems are used in production crash work. The table below gives the conversion factors you need to move a quantity from one system to the other. The rule that matters: a unit system is defined by three base choices — length, mass, time — and every derived unit follows from them. You cannot mix and match.

Quantity mm-ton-s m-kg-s Conversion (mm-ton-s → m-kg-s)
Lengthmmm× 1e-3
Masstonne (t)kg× 1e3
Timess× 1
Velocitymm/sm/s× 1e-3
ForceNN× 1 (both give N)
Stress / pressureMPaPa× 1e6
Densityt/mm³kg/m³× 1e12
EnergymJJ× 1e-3
Acceleration (gravity)9810 mm/s²9.81 m/s²× 1e-3

Note the two traps in that table. First, force in newtons is identical in both systems — this is a coincidence of the tonne/metre pairing and it is why a mixed deck can survive a casual force check while mass and energy are wrong. Second, density differs by twelve orders of magnitude, so a steel density of 7.85e-9 (t/mm³) placed in a metre-based deck produces a model that is effectively massless.

Where mismatches hide in an assembled deck

Unit errors do not distribute uniformly. In practice they cluster in a predictable set of cards, and a scan that targets these locations finds almost everything:

  • *PART / *PART_INERTIA — mass and inertia entries, the primary mass carrier.
  • *MAT_* — density (RO) and, for rate-sensitive materials, the strain-rate curve abscissa, which is in 1/s and therefore unit-free, but the stress ordinate is not.
  • *SECTION_SHELL / *SECTION_SOLID — thickness and element formulation scaling.
  • *INITIAL_VELOCITY — the velocity vector, the single most damaging mismatch because it scales energy quadratically.
  • *LOAD_BODY / gravity — the gravity constant, 9810 vs 9.81.
  • *CONTACT — penalty stiffness and thickness offsets, which inherit length units.
  • *DEFINE_CURVE — force-displacement or acceleration-time curves imported from test data, where the ordinate unit is whatever the test house used.
Unit consistency scan across an assembled crash model Subsystem decks in mm-ton-s and m-kg-s feed a scan that classifies each card as consistent, convertible, or blocking, and routes blocking mismatches to correction or flagging before release. Subsystem decks (declared unit system) Body-in-white mm-ton-s Chassis mm-ton-s Barrier m-kg-s Restraints m-kg-s Powertrain mm-ton-s Unit scan parse cards compare magnitudes against expected ranges per system classify each card Classification CONSISTENT magnitude matches declared system e.g. RO = 7.85e-9 t/mm³, v = 15000 mm/s CONVERTIBLE single subsystem off-system, factor known apply factor, log the change, re-scan BLOCKING ambiguous or mixed within one card flag to author, do not release Release gate PASS only when zero BLOCKING and every CONVERTIBLE has a logged factor mass, velocity and energy sanity checks must agree with the manifest Sanity anchors used by the scan Steel density: 7.85e-9 t/mm³ or 7850 kg/m³ · Gravity: 9810 mm/s² or 9.81 m/s² · Frontal velocity: 15000 mm/s or 15 m/s Vehicle mass: 1.5 t or 1500 kg · Energy at 15 m/s: 168.75 kJ (both systems must agree)

Running the scan: a reproducible procedure

The scan is a text-level pass over the assembled deck, not a solver run. It works because each card type has a narrow expected magnitude range in each unit system, and the ranges do not overlap. A density of 7.85e-9 is unambiguously t/mm³; a density of 7850 is unambiguously kg/m³; a density of 7.85 is neither and must be flagged.

The following Python script implements the scan. It reads a flat LS-DYNA deck, extracts the cards that carry unit-bearing quantities, and classifies each one. It is deliberately conservative: anything it cannot classify with confidence becomes BLOCKING rather than being silently converted.

import re
from dataclasses import dataclass, field

# Expected magnitude windows per quantity, per unit system.
# A value inside exactly one window is classified; inside none or both -> BLOCKING.
WINDOWS = {
    "density":   {"mm-ton-s": (1e-10, 1e-7),  "m-kg-s": (1e2, 1e4)},
    "velocity":  {"mm-ton-s": (1e2, 1e5),    "m-kg-s": (1e-1, 1e2)},
    "gravity":   {"mm-ton-s": (9e3, 1e4),     "m-kg-s": (9.0, 10.0)},
    "thickness": {"mm-ton-s": (0.5, 20.0),    "m-kg-s": (5e-4, 2e-2)},
}

@dataclass
class Finding:
    card: str
    line_no: int
    quantity: str
    value: float
    system: str          # detected system, or "ambiguous"
    status: str          # CONSISTENT / CONVERTIBLE / BLOCKING
    note: str = ""

def classify(quantity, value, declared):
    lo_hi = WINDOWS[quantity]
    hits = [sys for sys, (lo, hi) in lo_hi.items() if lo <= abs(value) <= hi]
    if len(hits) != 1:
        return "ambiguous", "BLOCKING", f"value {value:g} fits {len(hits)} systems"
    detected = hits[0]
    if detected == declared:
        return detected, "CONSISTENT", ""
    return detected, "CONVERTIBLE", f"{detected} -> {declared}"

def scan(deck_path, declared="mm-ton-s"):
    findings = []
    current = None
    with open(deck_path) as fh:
        for n, raw in enumerate(fh, 1):
            line = raw.rstrip("\n")
            if line.startswith("*"):
                current = line.strip().upper()
                continue
            if not line.strip() or line.lstrip().startswith("$"):
                continue
            fields = [f.strip() for f in line.split(",")]
            try:
                nums = [float(f) for f in fields if f]
            except ValueError:
                continue
            if current and current.startswith("*MAT_") and len(nums) >= 1:
                sys, status, note = classify("density", nums[0], declared)
                findings.append(Finding(current, n, "density", nums[0], sys, status, note))
            elif current == "*INITIAL_VELOCITY" and len(nums) >= 3:
                speed = (nums[0]**2 + nums[1]**2 + nums[2]**2) ** 0.5
                sys, status, note = classify("velocity", speed, declared)
                findings.append(Finding(current, n, "velocity", speed, sys, status, note))
            elif current == "*LOAD_BODY" and len(nums) >= 2:
                sys, status, note = classify("gravity", nums[1], declared)
                findings.append(Finding(current, n, "gravity", nums[1], sys, status, note))
    return findings

def report(findings):
    blocking = [f for f in findings if f.status == "BLOCKING"]
    convertible = [f for f in findings if f.status == "CONVERTIBLE"]
    for f in findings:
        print(f"{f.status:12s} line {f.line_no:5d} {f.card:22s} "
              f"{f.quantity:9s} {f.value:12.4g} {f.system:10s} {f.note}")
    print(f"\n{len(blocking)} blocking, {len(convertible)} convertible, "
          f"{len(findings) - len(blocking) - len(convertible)} consistent")
    return len(blocking) == 0

if __name__ == "__main__":
    ok = report(scan("assembly_final.k"))
    raise SystemExit(0 if ok else 1)

Run it against the assembled deck with python unit_scan.py. The exit code is the release gate: zero means no blocking findings, non-zero means the deck must not be released. A representative output for a deck with one supplier subsystem in the wrong system looks like this:

CONSISTENT   line   412 *MAT_PIECEWISE_LINEAR_PLASTICITY density     7.85e-09 mm-ton-s
CONSISTENT   line   418 *MAT_PIECEWISE_LINEAR_PLASTICITY density     7.85e-09 mm-ton-s
CONVERTIBLE  line  1204 *MAT_RIGID                 density          7850 m-kg-s    m-kg-s -> mm-ton-s
BLOCKING     line  1206 *MAT_RIGID                 density          7.85 ambiguous  value 7.85 fits 0 systems
CONSISTENT   line  2011 *INITIAL_VELOCITY          velocity        15000 mm-ton-s
CONSISTENT   line  2012 *LOAD_BODY_Z               gravity          9810 mm-ton-s

1 blocking, 1 convertible, 4 consistent

The BLOCKING line above is the important one. A density of 7.85 is not a unit error you can fix by multiplying — it is a value that fits no coherent system, which usually means the author entered the density in g/cm³ (a third system) or truncated a scientific-notation field. Converting it automatically would produce a plausible but wrong mass. Flag it back to the subsystem owner and require a corrected deck.

Correcting versus flagging

The scan distinguishes two remediation paths, and the distinction is a release-policy decision, not a technical one:

  • Convertible — the subsystem is internally consistent but in the wrong system. Apply the conversion factors from the table to the affected cards, record the original and converted values in the assembly manifest, and re-run the scan. This is safe because the factor is exact and the subsystem's internal consistency is preserved.
  • Blocking — the value is ambiguous, mixed within a single card, or fits no system. Do not convert. Flag the card, the line number, and the subsystem revision to the authoring team, and hold the release.

After conversion, the scan must be re-run to confirm zero blocking findings, and the mass and energy sanity anchors in the SVG legend must be checked against the manifest. A 1.5 t vehicle at 15 m/s carries 168.75 kJ; if the assembled deck reports a kinetic energy three orders of magnitude away from that, a unit mismatch survived the scan and the conversion factors were applied in the wrong direction.

Keep the declared unit system for each subsystem in the assembly manifest alongside its revision lock. When the scan reports a CONVERTIBLE finding, the manifest tells you immediately whether the subsystem was expected to be off-system (a known supplier convention) or whether it drifted during a revision update. This turns a unit finding into an actionable ownership question rather than a manual investigation.

Verification criteria

The unit consistency check passes when all of the following hold on the assembled deck: the scan reports zero BLOCKING findings; every CONVERTIBLE finding has a corresponding logged conversion in the manifest; the total mass derived from *PART entries agrees with the release target within the tolerance set for the mass reconciliation step; and the kinetic energy computed from the initial velocity card and the total mass agrees with the hand calculation to within rounding. Any one of these failing means the deck is not ready for the mass and inertia reconciliation that follows, because that step would be reconciling numbers expressed in incompatible systems.