2. Parameterize Pretensioner Stroke Profile from Test Data

From Bench Trace to Model Parameters

Raw pretensioner bench data arrives as a two-column trace: time in milliseconds and stroke in millimeters, sampled at 1–10 kHz. The model template, however, expects a compact parameter set — stroke_length, pull_in_time, and force_plateau — expressed in SI units and normalized to the template's reference frame. The task in this case is to build a parser that reads the trace, extracts those three parameters deterministically, and writes them into the template's parameter block without manual editing.

Two conventions must be fixed before any code is written, because every downstream calculation depends on them. First, stroke is measured positive in the pull-in direction (the direction the buckle moves as the pyrotechnic charge fires); a trace that reports negative displacement is sign-flipped at load time, not mid-analysis. Second, time zero is the trigger edge, defined as the first sample where stroke exceeds 0.5 mm — this removes trigger latency jitter between runs and makes pull_in_time comparable across specimens.

Defining the Extraction Rules

The three parameters are not read off single samples; each is derived from a defined feature of the trace. The table below is the contract the parser implements, and it is also the specification you should hand to whoever supplies the test data.

ParameterDefinitionUnitTypical range (example)
stroke_lengthMaximum stroke after the plateau settles, taken as the 99th percentile of the post-plateau window to reject single-sample spikesmm60–120
pull_in_timeTime from trigger edge (stroke > 0.5 mm) until stroke first reaches 90 % of stroke_lengthms8–25
force_plateauMean force over the window between 90 % stroke and stroke end, from the load cell channelN2000–6000

The 90 % threshold and the 99th-percentile spike rejection are deliberate choices: a raw max() on stroke is corrupted by electrical noise on the LVDT, and a raw argmax on the derivative is corrupted by the pyrotechnic shock. Both rules are cheap to implement and stable across the specimen population.

If the trace contains fewer than 50 samples after the trigger edge, or if stroke never exceeds 0.5 mm, the parser must raise a validation error rather than emit a parameter set. Silent fallback to defaults is the most common cause of a model that "runs" but produces a physically wrong deployment curve.

Parser Implementation

The parser is a single module with one public function. It takes a CSV path and returns a dictionary matching the template's parameter schema, so it can be dropped into the automation entry point established in the baseline framework without changing the calling convention.

import csv
import statistics

TRIGGER_STROKE_MM = 0.5
PULL_IN_FRACTION = 0.90
MIN_SAMPLES = 50

def parse_pretensioner_trace(csv_path):
    t, s, f = [], [], []
    with open(csv_path, newline="") as fh:
        for row in csv.DictReader(fh):
            t.append(float(row["time_ms"]))
            s.append(float(row["stroke_mm"]))
            f.append(float(row["force_n"]))

    # Sign convention: stroke positive in pull-in direction
    if s[-1] < s[0]:
        s = [-v for v in s]

    # Trigger edge: first sample above threshold
    trig = next((i for i, v in enumerate(s) if v > TRIGGER_STROKE_MM), None)
    if trig is None or len(s) - trig < MIN_SAMPLES:
        raise ValueError(f"{csv_path}: insufficient post-trigger data")

    t0 = t[trig]
    t_rel = [ti - t0 for ti in t[trig:]]
    s_rel = s[trig:]
    f_rel = f[trig:]

    # Stroke length: 99th percentile rejects LVDT spikes
    stroke_length = sorted(s_rel)[int(0.99 * (len(s_rel) - 1))]

    # Pull-in time: first crossing of 90 % stroke
    target = PULL_IN_FRACTION * stroke_length
    pull_in_time = next(tr for tr, sv in zip(t_rel, s_rel) if sv >= target)

    # Force plateau: mean force between 90 % stroke and end
    plateau_idx = next(i for i, sv in enumerate(s_rel) if sv >= target)
    force_plateau = statistics.fmean(f_rel[plateau_idx:])

    return {
        "stroke_length": round(stroke_length, 2),
        "pull_in_time": round(pull_in_time, 2),
        "force_plateau": round(force_plateau, 1),
    }

Note that t_rel is built from the trigger index, so pull_in_time is already relative to the trigger edge and needs no further offset. The rounding at return time matches the template's parameter precision; keeping full float precision in the intermediate values avoids accumulating error in the 90 % crossing search.

Injecting Into the Template

The template stores parameters in a delimited block so the parser can rewrite it without a full YAML or JSON round-trip that would reorder unrelated keys. The block is bounded by marker comments, and the injection step replaces only the lines between them.

import re

PARAM_BLOCK = re.compile(
    r"(# BEGIN PRETENSIONER PARAMS\n)(.*?)(# END PRETENSIONER PARAMS)",
    re.DOTALL,
)

def inject_params(template_path, out_path, params):
    with open(template_path) as fh:
        text = fh.read()
    body = "".join(f"{k} = {v}\n" for k, v in params.items())
    new_text, n = PARAM_BLOCK.subn(rf"\g<1>{body}\g<3>", text)
    if n != 1:
        raise RuntimeError("parameter block markers not found exactly once")
    with open(out_path, "w") as fh:
        fh.write(new_text)

The n != 1 guard is intentional: a template with duplicated or missing markers would otherwise be silently half-updated, and the resulting model would mix old and new parameters. Failing loudly here is cheaper than debugging a plausible-looking but wrong deployment curve later.

Stroke trace → parameter extraction → template injection time (ms) stroke 0.5 mm trigger 90 % stroke t₀ pull_in_time force_plateau window stroke_length Normalized parameter set stroke_length = 92.4 mm pull_in_time = 14.2 ms force_plateau = 3850 N (example values) Template param block # BEGIN … # END markers

Verification and Failure Symptoms

Run the parser against a known trace and check three things before trusting the output. First, pull_in_time must be strictly less than the time at which stroke reaches its maximum — if it is not, the sign convention was applied incorrectly or the trigger threshold was never crossed. Second, force_plateau should be positive and within the load cell's calibrated range; a negative value almost always means the force channel was sign-flipped along with stroke. Third, re-running the parser on the same file must produce byte-identical output — any variation indicates a non-deterministic sort or a floating-point comparison that should be a tolerance check.

Two failure modes are worth recognizing on sight. A ValueError on "insufficient post-trigger data" usually means the trigger threshold is set above the actual first-motion amplitude for a low-energy specimen; inspect the first 5 ms of the trace before changing the threshold. A RuntimeError on "parameter block markers not found exactly once" means the template was edited by hand and a marker was duplicated or deleted — restore the markers rather than loosening the regex, because the guard is what prevents partial injection.

Keep the raw CSV alongside the generated parameter set in the run output directory. When a model later produces an unexpected deployment curve, the first diagnostic step is to re-run the parser on the archived CSV and confirm the parameters still match — this separates a data problem from a template problem in one step.