3. Pulse Onset Detection and Time-Zero Alignment

Why Time-Zero Alignment Matters

Every downstream kinematic quantity — relative displacement, occupant velocity, injury-metric timing — is referenced to a time origin. If the pulse channel and the occupant sensor channel disagree on where t = 0 sits, the reconstruction inherits a constant phase error that propagates through both integrations. A 2 ms misalignment on a 60 g, 80 ms pulse is not cosmetic: it shifts the integrated velocity by roughly the area of the pulse over that window, and it desynchronizes occupant response peaks from the pulse peak that produced them. The task here is therefore twofold: pick a defensible onset definition, and apply it consistently across channels.

Three Onset Detectors on the Same Pulse

We work with a synthetic but realistic teaching pulse so the ground truth is known. The signal is a half-sine of 60 g peak and 80 ms duration, sampled at 10 kHz, with a 20 ms pre-trigger window, additive Gaussian noise (σ = 0.4 g), and a deliberate 3 ms delay injected into the occupant channel to simulate a sensor/DAQ latency. The true onset is defined at the first sample where the ideal pulse exceeds 0.5 g.

import numpy as np

fs = 10_000.0                 # Hz
dt = 1.0 / fs
pre_ms, dur_ms = 20.0, 80.0
t = np.arange(-pre_ms, dur_ms, dt * 1e3) / 1e3   # seconds, pre-trigger negative
rng = np.random.default_rng(7)

peak_g, half = 60.0, dur_ms / 1e3 / 2.0
ideal = np.where((t >= 0) & (t <= 2 * half),
                 peak_g * np.sin(np.pi * t / (2 * half)), 0.0)
pulse = ideal + rng.normal(0.0, 0.4, t.size)      # accelerometer channel
occupant = np.interp(t - 3e-3, t, pulse)          # 3 ms latency, same shape

def onset_threshold(x, thr):
    idx = np.flatnonzero(np.abs(x) >= thr)
    return t[idx[0]] if idx.size else np.nan

def onset_energy(x, win=20, k=5.0):
    e = np.convolve(x**2, np.ones(win) / win, mode="same")
    base = np.median(e[: int(pre_ms / 1e3 * fs)])
    idx = np.flatnonzero(e > k * base)
    return t[idx[0]] if idx.size else np.nan

def onset_slope(x, win=10, k=4.0):
    d = np.gradient(x, dt)
    s = np.convolve(np.abs(d), np.ones(win) / win, mode="same")
    base = np.median(s[: int(pre_ms / 1e3 * fs)])
    idx = np.flatnonzero(s > k * base)
    return t[idx[0]] if idx.size else np.nan

print("threshold 5g :", onset_threshold(pulse, 5.0))
print("energy 5x    :", onset_energy(pulse))
print("slope 4x     :", onset_slope(pulse))

On this dataset the threshold detector at 5 g lands late (it waits for the pulse to climb), the energy detector lands a few samples early because the moving-average window smears energy backward, and the slope detector typically lands closest to the true onset but is the most noise-sensitive. The exact numbers depend on the seed and thresholds; what matters is the relative bias each method carries, because that bias is what you must remove before cross-channel alignment.

Treat the onset estimate as a measurement with a known bias, not as truth. Calibrate each detector against a synthetic pulse with a known start, record its bias and jitter, and subtract the bias when applying it to real data.

Cross-Channel Alignment Procedure

  1. Run the chosen detector independently on the pulse channel and on each occupant channel (head, chest, pelvis accelerometers).
  2. Compute the per-channel offset Δt_i = t_onset,i − t_onset,pulse.
  3. Shift each occupant channel by −Δt_i so all channels share the pulse's time origin.
  4. Verify by cross-correlation: the lag at maximum correlation between the aligned occupant channel and the pulse should be near zero for a rigidly coupled sensor, or match the expected structural delay for a compliant mount.
def align(x, t, dt_shift):
    return np.interp(t - dt_shift, t, x)

t0_pulse = onset_slope(pulse)
t0_occ   = onset_slope(occupant)
shift    = t0_occ - t0_pulse
occ_aligned = align(occupant, t, shift)

lag = np.argmax(np.correlate(occ_aligned, pulse, mode="full")) - (pulse.size - 1)
print(f"shift applied: {shift*1e3:.2f} ms, residual lag: {lag*dt*1e3:.2f} ms")

A residual lag within ±1 sample (±0.1 ms at 10 kHz) is acceptable for most occupant-kinematics work; anything larger means the detector bias differs between channels or the channels were sampled on unsynchronized clocks. If the residual lag is stable but nonzero, it is a real physical delay (mount compliance, filter group delay) and should be documented rather than zeroed out.

Do not align by eye on the plotted peak. Peak alignment is dominated by amplitude and shape differences between channels and routinely produces 1–3 ms errors — exactly the phase error this procedure exists to eliminate.
Onset Detection Bias and Cross-Channel Alignment Pulse channel (accel, 60 g / 80 ms) true t=0 thr 5 g (late) energy (early) slope (near) Occupant channel (3 ms latency) detected onset pulse t=0 Δt = 3 ms After alignment: shift occupant by −Δt, then verify residual lag residual lag ≤ ±1 sample (±0.1 ms @ 10 kHz) → accept stable nonzero lag → physical delay (mount/filter), document it Accept |lag| ≤ 1 sample bias removed, channels synced Investigate stable lag > 1 sample check mount / filter delay Reject unstable / drifting lag unsynced clocks or bad channel

Choosing a Detector for Production

No single detector wins everywhere. The table below summarizes the trade-offs observed on the teaching pulse and gives the practical selection rule.

DetectorTypical biasNoise sensitivityBest use
Threshold (5 g)Late by 1–3 msLowClean, high-SNR pulses; quick sanity check
Energy (5× baseline)Early by 1–2 msMediumPulses with gradual rise; robust to single spikes
Slope (4× baseline)Near zeroHighSharp-onset pulses; requires pre-filtering

For a mixed fleet of pulses, the defensible choice is the slope detector with a short moving-average pre-filter (5–10 samples) to suppress noise, calibrated against a synthetic reference to remove its residual bias. Apply the same detector and the same pre-filter to every channel — a detector mismatch between pulse and occupant channels reintroduces exactly the phase error the alignment step is meant to remove.

Verification Checklist

  • Onset detector bias measured on a synthetic pulse with known start; bias value recorded.
  • Same detector and pre-filter applied to pulse and all occupant channels.
  • Per-channel Δt_i computed and applied; residual lag reported.
  • Residual lag within ±1 sample, or explained as a documented physical delay.
  • Aligned channels re-plotted against a common time axis before any integration.

Once the channels share a verified time origin, the kinematic reconstruction in later steps inherits a clean phase reference. Skipping this step does not produce an obvious failure — it produces curves that look plausible but are shifted, and every peak timing and injury-metric comparison built on them is quietly wrong.