4. Calculate 3 ms Chest Acceleration Clip and Overlay Limit Curves

Why the 3 ms Clip Is Not a Simple Peak

The 3 ms chest acceleration criterion answers a different question than a raw peak: it asks how much acceleration the chest can sustain continuously for a 3 millisecond window. A short spike of 90 g lasting 0.4 ms is far less injurious than a sustained 65 g plateau spanning 5 ms, yet a naive max(a) call ranks the spike higher. The clip value is therefore defined as the maximum, over all sliding 3 ms windows, of the window average:

clip3ms = max over t of [ (1/Δt) · ∫(t → t+3ms) a(τ) dτ ]

With sampled data at uniform step dt, the integral becomes a moving sum. For a window of N = round(0.003 / dt) samples, the clip is max(cumsum[i+N] − cumsum[i]) / (N · dt). Two practical details dominate accuracy: the signal must already be filtered to the correct CFC class (CFC 600 for chest acceleration per SAE J211), and the window length must be expressed in seconds, not samples, so that a change of sampling rate does not silently change the criterion.

Reference Signal and Expected Clip

To make the calculation verifiable, use a synthetic pulse with a known answer. A rectangular plateau of 60 g held for exactly 4 ms, sampled at 10 kHz, must produce a clip of 60 g: every fully contained 3 ms window averages 60 g. A triangular pulse peaking at 80 g with a 6 ms base gives a lower clip, because no 3 ms window contains the full peak. These two cases let you confirm the implementation before trusting it on simulation output.

import numpy as np

def clip_3ms(acc_g, dt):
    """Maximum 3 ms moving average of a chest acceleration signal.

    acc_g : 1-D array of acceleration in g (already CFC-filtered)
    dt    : sample interval in seconds
    """
    n = int(round(0.003 / dt))
    if n < 1:
        raise ValueError("sampling rate too low for a 3 ms window")
    c = np.concatenate(([0.0], np.cumsum(acc_g)))
    # window sums for every fully contained window
    sums = c[n:] - c[:-n]
    return float(sums.max() / (n * dt))

# --- verification on synthetic pulses ---
dt = 1e-4                      # 10 kHz
t = np.arange(0, 0.020, dt)

plateau = np.where((t >= 0.005) & (t < 0.009), 60.0, 0.0)
tri = np.where((t >= 0.005) & (t <= 0.011),
               80.0 * (1 - np.abs(t - 0.008) / 0.003), 0.0)

print("plateau clip:", clip_3ms(plateau, dt))   # expect 60.0
print("triangle clip:", clip_3ms(tri, dt))      # expect < 80.0

The plateau result should print 60.0 to within floating-point noise. The triangle result is a useful sanity check rather than a fixed number: it must be strictly below 80 g, and it should be close to the average of the triangle over its steepest 3 ms span. If the plateau returns something like 59.7 or 60.3, the window length is off by one sample — a classic off-by-one in the c[n:] − c[:-n] slicing.

Do not compute the clip on unfiltered data. Raw accelerometer channels often carry high-frequency content that inflates short windows; the regulatory comparison assumes CFC 600 filtering. If your pipeline stores both raw and filtered channels, assert that the channel name or metadata confirms the filter class before clipping.

Overlaying Against the Limit Line

The regulatory limit for chest acceleration is conventionally drawn as a horizontal line at 60 g for the 3 ms criterion. The overlay is not the acceleration trace itself but a clip-versus-load-case chart: one marker per simulation, positioned by its clip value, with the limit line as a reference. This makes pass/fail readable at a glance across a matrix of load cases, which a stack of time histories cannot do.

import matplotlib.pyplot as plt

LIMIT_G = 60.0
cases = ["Frontal 48 km/h", "Frontal 56 km/h", "Oblique 32 km/h",
         "Side 27 km/h", "Rear 32 km/h"]
clips = [52.4, 63.1, 47.8, 58.9, 41.2]   # example values, g

fig, ax = plt.subplots(figsize=(8, 4.2))
colors = ["#2e7d32" if v <= LIMIT_G else "#c62828" for v in clips]
bars = ax.barh(cases, clips, color=colors, edgecolor="white")
ax.axvline(LIMIT_G, color="#e65100", lw=2, ls="--", label="3 ms limit (60 g)")

for bar, v in zip(bars, clips):
    status = "PASS" if v <= LIMIT_G else "FAIL"
    ax.text(v + 0.8, bar.get_y() + bar.get_height()/2,
            f"{v:.1f} g  {status}", va="center", fontsize=9)

ax.set_xlabel("3 ms chest acceleration clip (g)")
ax.set_xlim(0, 75)
ax.legend(loc="lower right")
ax.set_title("3 ms clip vs regulatory limit — example load cases")
plt.tight_layout()
plt.savefig("clip_overlay.png", dpi=150)

In this example dataset the 56 km/h frontal case exceeds the limit at 63.1 g and is annotated FAIL; the remaining four cases pass. The annotation text is generated from the same comparison that colors the bar, so the label and the color can never disagree — a small but important guard against reporting errors.

From time history to clip value to pass/fail overlay Chest accel (CFC 600) 3 ms window time → moving avg Clip = max window avg N = 0.003 / dt sum = c[i+N] − c[i] avg = sum / (N·dt) clip = max(avg) plateau 60 g → clip 60.0 g ✓ Clip vs limit (g) limit 60 g FAIL PASS Result interpretation per load case clip ≤ 60 g PASS clip > 60 g FAIL clip within 2 g of limit REVIEW plateau check ≠ 60 g window off-by-one Filter class must be CFC 600 before clipping; window length in seconds, not samples. Annotate every case from the same comparison that sets its color.

Batch Processing and the Review Band

Real studies run dozens of load cases, so the clip function should be applied in a loop over a results directory, with the channel name and filter class read from metadata rather than hard-coded. A practical addition is a review band: any case whose clip falls within about 2 g of the limit is flagged REVIEW rather than PASS, because filtering choices, sampling rate, and channel calibration can shift the value by that much. The decision strip in the diagram encodes exactly this three-way outcome, and the purple branch captures the most common implementation defect — a plateau test that does not return 60 g, which almost always means the window length is off by one sample.

Keep the synthetic plateau and triangle tests in the same module as clip_3ms and run them on every pipeline change. They cost milliseconds and catch the two failure modes that silently corrupt every downstream pass/fail call: wrong window length and unfiltered input.

Deliverable: Clip Summary Table

The following table is the artifact to hand to the design team. It carries the clip value, the limit, the margin, and the status, with the status derived from the margin so the table is internally consistent.

Load caseChannelFilterClip (g)Limit (g)Margin (g)Status
Frontal 48 km/hChest_Accel_ResCFC 60052.460.0+7.6PASS
Frontal 56 km/hChest_Accel_ResCFC 60063.160.0−3.1FAIL
Oblique 32 km/hChest_Accel_ResCFC 60047.860.0+12.2PASS
Side 27 km/hChest_Accel_ResCFC 60058.960.0+1.1REVIEW
Rear 32 km/hChest_Accel_ResCFC 60041.260.0+18.8PASS

Values above are illustrative example data for the workflow, not measured results. The side-impact case at 58.9 g sits inside the review band and should be re-examined for filter class and channel calibration before it is reported as a pass. Once the table and the overlay chart agree case-for-case, the clip analysis is complete and ready to feed the injury-criteria summary.