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) |
|---|---|---|---|
| Length | mm | m | × 1e-3 |
| Mass | tonne (t) | kg | × 1e3 |
| Time | s | s | × 1 |
| Velocity | mm/s | m/s | × 1e-3 |
| Force | N | N | × 1 (both give N) |
| Stress / pressure | MPa | Pa | × 1e6 |
| Density | t/mm³ | kg/m³ | × 1e12 |
| Energy | mJ | J | × 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.
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.