2. Define Submarining Detection Metrics from Belt and Pelvis Kinematics
Why a Single Metric Is Not Enough
Submarining is a kinematic event, not a force event. The lap belt initially loads the anterior superior iliac spine (ASIS) region; when the belt slips superiorly past the iliac crest and compresses the abdomen, the pelvis has already begun rotating under the belt. Any detection scheme that relies on one signal alone will either fire late (belt force drop lags migration by tens of milliseconds) or fire falsely (pelvis rotation occurs in non-submarining pulses too). The practical approach is to define three independent, physically interpretable metrics, evaluate each on its own threshold, and then combine them with a persistence rule so that a single noisy frame cannot trigger a flag.
The three metrics used here are:
- Belt-to-iliac-crest migration — the superior displacement of the lap-belt centerline relative to a pelvis-fixed iliac crest marker, measured along the pelvis vertical axis.
- Pelvis rotation — the pitch angle of the pelvis about the lateral (Y) axis, referenced to the initial seated posture, positive in the direction of posterior pelvic tilt that accompanies belt ride-up.
- Lap-belt force drop — the normalized reduction in lap-belt tension after its peak, computed as (F_peak − F(t)) / F_peak over a trailing window.
Defining the Metrics Quantitatively
All three metrics must be computed in a consistent reference frame. Define the pelvis local frame from three nodes: the sacrum (origin), the right ASIS, and the left ASIS. The pelvis vertical axis e_z points from the sacrum toward the midpoint of the two ASIS nodes; the lateral axis e_y points from left to right ASIS; e_x = e_y × e_z completes a right-handed frame. Migration is then the projection of the belt-center displacement onto e_z, which makes the metric insensitive to whole-body forward excursion.
Pelvis pitch is extracted from the same frame: θ = atan2(−e_x · g, e_z · g), where g is the global gravity unit vector. This gives a signed angle that is zero in the baseline posture and increases as the pelvis rotates under the belt. Using the frame rather than raw nodal coordinates avoids contaminating the angle with rigid-body translation of the occupant.
Belt force drop is computed from the lap-belt seatbelt element force output. Because belt force is noisy during the loading phase, the peak is taken as the maximum over the loading window, and the drop is evaluated only after the peak time. A trailing 10 ms mean is applied before the ratio is formed.
Threshold Selection and the Persistence Rule
Thresholds should be derived from the baseline run rather than assumed. Run the baseline model from Case 1, extract the three time histories, and record the maximum value each metric reaches before any visible belt penetration of the abdomen. Those pre-submarining maxima define the noise floor. Set each threshold at the noise floor plus a margin that reflects the metric's sensitivity:
| Metric | Symbol | Typical pre-submarining max (example) | Proposed threshold | Rationale |
|---|---|---|---|---|
| Belt-to-iliac-crest migration | Δz_belt | 8 mm | 15 mm | Ride-up beyond the crest is geometrically irreversible past ~15 mm |
| Pelvis pitch | θ_pelvis | 6° | 12° | Posterior tilt of 12° corresponds to loss of ASIS purchase |
| Lap-belt force drop | ΔF/F_peak | 0.10 | 0.25 | 25% unloading indicates the belt has slipped off the load path |
These numbers are illustrative for a specific model and must be re-derived for any new occupant or belt configuration. The persistence rule is what makes the criteria robust: a metric is considered "active" only if it exceeds its threshold continuously for at least 5 ms. Submarining onset is flagged at the first time at which at least two of the three metrics are simultaneously active. Requiring two-of-three suppresses single-channel false positives (e.g., a force spike during pretensioner firing) while still catching genuine ride-up, which always produces migration plus either rotation or unloading.
Post-Processing Script
The script below reads three CSV exports (node coordinates, belt centerline, belt force) and produces a single onset time plus a per-frame active mask. It is self-contained: it defines its own frame construction, smoothing, persistence logic, and threshold constants, so it can be run directly against the baseline model output without depending on earlier chapters.
import numpy as np
import pandas as pd
# ---- Thresholds (re-derive per model; values are illustrative) ----
THRESH_MIGRATION_MM = 15.0
THRESH_PITCH_DEG = 12.0
THRESH_FORCE_DROP = 0.25
PERSIST_MS = 5.0
DT_MS = 0.1
def build_pelvis_frame(sacrum, asis_l, asis_r):
"""Return orthonormal pelvis axes (e_x, e_y, e_z) for each frame.
sacrum, asis_l, asis_r: (N,3) arrays in global coords."""
e_z = (asis_l + asis_r) / 2.0 - sacrum
e_z /= np.linalg.norm(e_z, axis=1, keepdims=True)
e_y = asis_r - asis_l
e_y /= np.linalg.norm(e_y, axis=1, keepdims=True)
e_x = np.cross(e_y, e_z)
e_x /= np.linalg.norm(e_x, axis=1, keepdims=True)
return e_x, e_y, e_z
def pelvis_pitch_deg(e_x, e_z, gravity=(0.0, 0.0, -1.0)):
g = np.asarray(gravity, dtype=float)
num = -np.einsum('ij,j->i', e_x, g)
den = np.einsum('ij,j->i', e_z, g)
return np.degrees(np.arctan2(num, den))
def trailing_mean(x, n):
return pd.Series(x).rolling(window=n, min_periods=1).mean().to_numpy()
def persist(mask, n_frames):
"""True only where mask has been continuously True for n_frames."""
out = np.zeros_like(mask, dtype=bool)
run = 0
for i, m in enumerate(mask):
run = run + 1 if m else 0
out[i] = run >= n_frames
return out
def detect_submarining(nodes_csv, belt_csv, force_csv):
nodes = pd.read_csv(nodes_csv)
belt = pd.read_csv(belt_csv)
force = pd.read_csv(force_csv)
t = nodes['time_ms'].to_numpy()
sacrum = nodes[['sac_x','sac_y','sac_z']].to_numpy()
asis_l = nodes[['asisL_x','asisL_y','asisL_z']].to_numpy()
asis_r = nodes[['asisR_x','asisR_y','asisR_z']].to_numpy()
crest = nodes[['crest_x','crest_y','crest_z']].to_numpy()
belt_c = belt[['belt_x','belt_y','belt_z']].to_numpy()
e_x, e_y, e_z = build_pelvis_frame(sacrum, asis_l, asis_r)
# Metric 1: migration of belt center along pelvis vertical, relative to crest
rel = belt_c - crest
migration_mm = np.einsum('ij,ij->i', rel, e_z)
# Metric 2: pelvis pitch
pitch_deg = pelvis_pitch_deg(e_x, e_z)
# Metric 3: normalized lap-belt force drop after peak
F = force['lap_belt_N'].to_numpy()
F_smooth = trailing_mean(F, int(10.0 / DT_MS))
i_peak = int(np.argmax(F_smooth))
F_peak = F_smooth[i_peak]
drop = np.zeros_like(F_smooth)
drop[i_peak:] = (F_peak - F_smooth[i_peak:]) / F_peak
n_persist = int(round(PERSIST_MS / DT_MS))
a_mig = persist(migration_mm > THRESH_MIGRATION_MM, n_persist)
a_pit = persist(pitch_deg > THRESH_PITCH_DEG, n_persist)
a_frc = persist(drop > THRESH_FORCE_DROP, n_persist)
active_count = a_mig.astype(int) + a_pit.astype(int) + a_frc.astype(int)
flagged = active_count >= 2
onset_ms = float(t[np.argmax(flagged)]) if flagged.any() else None
return pd.DataFrame({
'time_ms': t,
'migration_mm': migration_mm,
'pitch_deg': pitch_deg,
'force_drop': drop,
'active_migration': a_mig,
'active_pitch': a_pit,
'active_force': a_frc,
'active_count': active_count,
'submarining_flag': flagged,
}), onset_ms
if __name__ == "__main__":
result, onset = detect_submarining(
"nodes.csv", "belt_centerline.csv", "lap_belt_force.csv")
result.to_csv("submarining_metrics.csv", index=False)
print(f"Submarining onset: {onset} ms" if onset is not None
else "No submarining detected")
Verifying the Flag Against the Baseline
Run the script on the Case 1 baseline output and inspect three things. First, confirm that onset_ms falls after the lap-belt force peak — if it precedes the peak, the force-drop channel is being evaluated in the wrong window and the peak index must be recomputed. Second, plot migration_mm and pitch_deg together; in a genuine submarining event the two curves rise within a few milliseconds of each other, whereas a false positive typically shows one channel rising while the other stays flat. Third, check the active mask around onset: a clean event shows active_count stepping from 1 to 2 and staying at 2, not oscillating between 1 and 2.
If the baseline produces no flag at all, the thresholds are too high for that model — lower them incrementally until the known ride-up is captured, then re-check that a non-submarining pulse (e.g., a pure frontal sled run without belt slip) still returns no flag. If the baseline flags immediately at t = 0, the persistence window is too short or the force-drop window includes the pretensioner spike; increase PERSIST_MS and start the drop evaluation after the pretensioner has fired.