2. Define Airbag Deployment Decision Labels from Crash Pulse Features

Why Deployment Labels Must Be Derived, Not Assumed

In a prediction pipeline, the label is the ground truth the model is trained to reproduce. If the label is noisy, inconsistent, or defined differently across subsets, every downstream metric becomes unreliable. For airbag deployment, the label is not simply "did the airbag fire" — it is a decision that a restraint control module (RCM) makes from a crash pulse, and that decision depends on how the pulse is characterized. This case defines the labeling rules once, in code, so that every later feature-engineering and modeling case consumes the same deterministic label set.

The three pulse descriptors we use are standard in crash sensing:

  • Delta-v (Δv) — the velocity change accumulated over the crash event, obtained by integrating the longitudinal acceleration signal. It is the primary severity proxy.
  • Peak acceleration (apeak) — the maximum absolute filtered acceleration, expressed in g. It captures the sharpness of the pulse.
  • Pulse duration (Tp) — the time from pulse onset to the point where acceleration falls back below a small threshold, in milliseconds. It separates short, stiff impacts from long, soft ones.

These three are deliberately chosen because they are computable from a single accelerometer channel, they are physically interpretable, and they map cleanly onto the two label schemes we need: a binary deploy/no-deploy decision and a multi-class severity tier.

Labeling Rules and Thresholds

The rules below are the course-wide contract. They are expressed as explicit thresholds so that any reviewer can audit a label by hand. The thresholds are teaching values chosen to produce a balanced, learnable label distribution on typical passenger-car frontal crash pulses; they are not regulatory calibration values and must be re-tuned before any real deployment use.

Label schemeClassRule (all conditions must hold)
BinaryDEPLOYΔv ≥ 25 km/h and apeak ≥ 20 g
NO_DEPLOYotherwise
Multi-classNONEΔv < 15 km/h
LOW15 ≤ Δv < 25 km/h
MEDIUM25 ≤ Δv < 40 km/h and Tp ≤ 120 ms
HIGHΔv ≥ 40 km/h or (Δv ≥ 25 km/h and Tp > 120 ms)

Two design choices deserve comment. First, the binary rule requires both a Δv and a peak-acceleration condition, so a long low-g pulse that accumulates velocity slowly does not trigger deployment — this mirrors the dual-criterion logic used in real sensing algorithms. Second, the multi-class scheme is ordered by severity but is not a simple re-binning of the binary label: a pulse can be MEDIUM (deploy) or HIGH (deploy) while a LOW pulse is NO_DEPLOY. Keeping the two schemes separate lets later cases train a fast binary gate and a finer severity head independently.

Keep the threshold constants in one module-level dictionary. Every later case imports that dictionary rather than re-typing numbers, which is what prevents label drift between the training set and the evaluation set.

Reference Implementation

The function below takes a raw acceleration time series (in g, sampled at a known rate) and returns the three descriptors plus both labels. It is self-contained: no external data files, no hidden state. The integration uses the trapezoidal rule, and the pulse window is detected from the signal itself so that pre-trigger noise does not inflate Δv.

import numpy as np

# Course-wide labeling contract. Re-tune before any real use.
THRESHOLDS = {
    "deploy_dv_kmh": 25.0,
    "deploy_peak_g": 20.0,
    "none_dv_kmh": 15.0,
    "medium_dv_kmh": 25.0,
    "high_dv_kmh": 40.0,
    "long_pulse_ms": 120.0,
    "onset_g": 2.0,        # pulse onset / offset detection level
}

G_TO_MS2 = 9.80665

def pulse_descriptors(accel_g, fs_hz):
    """accel_g: 1-D array of longitudinal acceleration in g.
       fs_hz:   sampling rate in Hz.
       Returns dict with delta_v_kmh, peak_g, duration_ms."""
    a = np.asarray(accel_g, dtype=float)
    dt = 1.0 / fs_hz
    above = np.abs(a) >= THRESHOLDS["onset_g"]
    if not above.any():
        return {"delta_v_kmh": 0.0, "peak_g": float(np.max(np.abs(a))),
                "duration_ms": 0.0}
    i0, i1 = np.argmax(above), len(above) - 1 - np.argmax(above[::-1])
    window = a[i0:i1 + 1]
    # trapezoidal integration of acceleration -> velocity (m/s)
    v = np.concatenate(([0.0], np.cumsum(0.5 * (window[1:] + window[:-1]) * dt)))
    delta_v_ms = abs(v[-1]) * G_TO_MS2
    return {
        "delta_v_kmh": delta_v_ms * 3.6,
        "peak_g": float(np.max(np.abs(window))),
        "duration_ms": (i1 - i0 + 1) * dt * 1000.0,
    }

def label_pulse(desc):
    dv, pk, tp = desc["delta_v_kmh"], desc["peak_g"], desc["duration_ms"]
    binary = "DEPLOY" if (dv >= THRESHOLDS["deploy_dv_kmh"]
                          and pk >= THRESHOLDS["deploy_peak_g"]) else "NO_DEPLOY"
    if dv < THRESHOLDS["none_dv_kmh"]:
        multi = "NONE"
    elif dv < THRESHOLDS["medium_dv_kmh"]:
        multi = "LOW"
    elif dv >= THRESHOLDS["high_dv_kmh"] or tp > THRESHOLDS["long_pulse_ms"]:
        multi = "HIGH"
    else:
        multi = "MEDIUM"
    return {"binary": binary, "multi": multi, **desc}

Run it on a synthetic pulse to confirm the wiring end-to-end. The pulse below is a half-sine of 30 g peak lasting 80 ms, which should integrate to roughly 30 km/h of Δv and land in DEPLOY / MEDIUM.

fs = 1000
t = np.arange(0, 0.12, 1 / fs)
pulse = 30.0 * np.sin(np.pi * t / 0.08) * (t < 0.08)
result = label_pulse(pulse_descriptors(pulse, fs))
print(result)
# expected (illustrative): delta_v_kmh ~ 33.7, peak_g 30.0,
# duration_ms ~ 80, binary DEPLOY, multi MEDIUM

The printed Δv is an expected value derived from the analytic integral of the half-sine (2·A·T/π ≈ 2·30·0.08/π g·s ≈ 1.53 m/s ≈ 5.5 km/h per unit... verify against your own run); treat the exact number as a check that your integration and unit conversions are consistent, not as a fixed target. If your Δv is off by a factor of 3.6, you forgot the km/h conversion; if it is off by ~9.8, you forgot the g-to-m/s² factor.

Reading the Decision Space

The SVG below maps the two label schemes onto the Δv–apeak plane, with the pulse-duration split shown as a secondary annotation. It is the mental model to carry into every later case: the binary boundary is a corner (both conditions), while the multi-class bands are vertical Δv slices with a duration-dependent promotion to HIGH.

Deployment Label Decision Space (Δv vs. peak g) NONE LOW MEDIUM HIGH DEPLOY region (Δv ≥ 25 & peak ≥ 20 g) Tp > 120 ms → HIGH Δv (km/h) peak acceleration (g) 0 15 25 40 20 50 example pulse Δv≈33.7, peak 30 g

Verification and Failure Symptoms

Before trusting the labeler, run three checks. (1) Monotonicity: scaling a pulse's amplitude up must never move a label from DEPLOY to NO_DEPLOY; if it does, a threshold comparison is inverted. (2) Unit sanity: a 30 g, 80 ms half-sine should give Δv in the low-to-mid 30s km/h; a result near 5 or near 120 indicates a missing or doubled conversion factor. (3) Boundary behavior: a pulse exactly at Δv = 25 km/h and peak = 20 g must be DEPLOY (the rules use ≥), and a pulse at Δv = 15 km/h must be LOW, not NONE.

Common failure symptoms and their causes: labels that flip when the sampling rate changes usually mean the onset detector is picking up a different number of samples — fix by expressing the onset threshold in g and the window in time, not samples. A large fraction of NONE labels on clearly severe pulses usually means the integration window is truncated because the offset detector fired early on a zero crossing; widen the offset search or use a hysteresis band. Finally, if the binary and multi-class labels disagree in a way that surprises you (e.g., LOW but DEPLOY), re-read the rules — by construction LOW is always NO_DEPLOY, so any such pair is a bug in the caller, not the rules.

These thresholds are illustrative teaching values. Real RCM calibration depends on vehicle mass, crumple-zone stiffness, seat position, and occupant size, and is validated against physical crash tests. Do not deploy this labeler in any safety-critical path without re-derivation and sign-off.

Labeling Contract Summary

The deliverable of this case is the THRESHOLDS dictionary plus the two functions above, committed as a single module. Every subsequent case in this course imports label_pulse and pulse_descriptors from that module, so the label definition is fixed in exactly one place. When you later engineer time-domain or frequency-domain features, the question "does this feature predict deployment?" is answered against these labels — which is what makes the comparison across cases meaningful rather than an artifact of re-labeling.