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.
| Parameter | Definition | Unit | Typical range (example) |
|---|---|---|---|
stroke_length | Maximum stroke after the plateau settles, taken as the 99th percentile of the post-plateau window to reject single-sample spikes | mm | 60–120 |
pull_in_time | Time from trigger edge (stroke > 0.5 mm) until stroke first reaches 90 % of stroke_length | ms | 8–25 |
force_plateau | Mean force over the window between 90 % stroke and stroke end, from the load cell channel | N | 2000–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.
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.
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.