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.
Cross-Channel Alignment Procedure
- Run the chosen detector independently on the pulse channel and on each occupant channel (head, chest, pelvis accelerometers).
- Compute the per-channel offset
Δt_i = t_onset,i − t_onset,pulse. - Shift each occupant channel by
−Δt_iso all channels share the pulse's time origin. - 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.
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.
| Detector | Typical bias | Noise sensitivity | Best use |
|---|---|---|---|
| Threshold (5 g) | Late by 1–3 ms | Low | Clean, high-SNR pulses; quick sanity check |
| Energy (5× baseline) | Early by 1–2 ms | Medium | Pulses with gradual rise; robust to single spikes |
| Slope (4× baseline) | Near zero | High | Sharp-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_icomputed 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.