3. Automate Load Limiter Force-Displacement Curve Fitting

Objective and Data Contract

This case turns raw load limiter bench data into a fitted piecewise linear force-displacement curve and writes the resulting breakpoints directly into the model template. The bench test typically delivers a CSV with two columns: displacement_mm and force_N, sampled at 1 kHz during a quasi-static pull. The load limiter characteristic we want to reproduce has three regimes: an elastic rise to the peak force, a controlled force plateau (the limiting region), and a final stiffening or drop-off as the mechanism bottoms out. A piecewise linear fit with a small number of breakpoints captures this behavior far better than a single polynomial and maps one-to-one onto the model's parameter fields.

The automation contract is: read the CSV, detect the plateau, place breakpoints at physically meaningful locations, fit segment slopes and intercepts, then emit a parameter block that the model template consumes. The output must be deterministic so that re-running on the same file produces identical parameters.

Choosing Breakpoints from the Signal

Rather than fitting an arbitrary number of segments, anchor the breakpoints to features that already exist in the data. The plateau is the region where the force stays within a tolerance band around its median. Detect it by computing a rolling median and marking samples whose deviation is below, say, 5% of the peak force. The plateau start and end become two breakpoints; the peak force location becomes a third; the end of the record becomes the final anchor.

This feature-anchored approach keeps the fit interpretable: each breakpoint corresponds to a physical event (yield, plateau entry, plateau exit, bottoming). It also avoids the instability of free-knot fitting on noisy bench data.

Fitting Each Segment

With breakpoints fixed, each segment is fit independently by ordinary least squares on the samples falling inside it. For segment i spanning displacements [xi, xi+1], solve for slope ki and intercept bi minimizing the squared residual. Because breakpoints are shared, enforce continuity by adjusting the intercept of each segment to match the previous segment's end value — this prevents visible jumps in the model curve.

The plateau segment deserves special handling: its slope should be near zero, and if the fitted slope exceeds a small threshold (e.g., 2 N/mm), flag the record as a suspect plateau rather than silently accepting it. This is a real quality gate, not a formality — a drifting plateau usually indicates fixture compliance or a mis-seated specimen.

Reference Implementation

The following Python script is self-contained: it generates a synthetic bench record (so it runs without external files), performs the feature-anchored fit, and prints the parameter block. Replace the synthetic generator with pandas.read_csv for real data.

import numpy as np

def synth_bench_data(n=2000, seed=7):
    rng = np.random.default_rng(seed)
    x = np.linspace(0, 120, n)                 # mm
    k1, k2, k3 = 45.0, 1.5, 30.0               # N/mm
    x_peak, x_plateau_end = 18.0, 95.0
    f = np.where(x < x_peak, k1 * x,
        np.where(x < x_plateau_end,
                 k1 * x_peak + k2 * (x - x_peak),
                 k1 * x_peak + k2 * (x_plateau_end - x_peak)
                 + k3 * (x - x_plateau_end)))
    f += rng.normal(0, 8.0, n)                 # bench noise
    return x, f

def detect_breakpoints(x, f, tol_frac=0.05):
    peak_idx = int(np.argmax(f))
    peak_f = f[peak_idx]
    band = tol_frac * peak_f
    med = np.median(f[peak_idx:])
    in_plateau = np.abs(f - med) < band
    # plateau must start after the peak
    idx = np.where(in_plateau[peak_idx:])[0]
    if len(idx) == 0:
        raise ValueError("No plateau detected")
    p_start = peak_idx + idx[0]
    p_end = peak_idx + idx[-1]
    return [0, peak_idx, p_start, p_end, len(x) - 1]

def fit_segments(x, f, knots):
    segs = []
    prev_end_f = None
    for i in range(len(knots) - 1):
        a, b = knots[i], knots[i + 1]
        xs, fs = x[a:b + 1], f[a:b + 1]
        k, c = np.polyfit(xs, fs, 1)
        if prev_end_f is not None:
            c = prev_end_f - k * xs[0]      # enforce continuity
        segs.append({"x0": float(xs[0]), "x1": float(xs[-1]),
                     "k": float(k), "b": float(c)})
        prev_end_f = k * xs[-1] + c
    return segs

def quality_gate(segs, plateau_slope_max=2.0):
    plateau = segs[2]                        # third segment = plateau
    if abs(plateau["k"]) > plateau_slope_max:
        return False, f"Plateau slope {plateau['k']:.2f} N/mm exceeds limit"
    return True, "OK"

x, f = synth_bench_data()
knots = detect_breakpoints(x, f)
segs = fit_segments(x, f, knots)
ok, msg = quality_gate(segs)
print("quality:", ok, msg)
for i, s in enumerate(segs):
    print(f"seg{i}: x=[{s['x0']:.1f},{s['x1']:.1f}] "
          f"k={s['k']:.3f} N/mm b={s['b']:.2f} N")

On the synthetic record the fit recovers slopes close to the generating values (45, 1.5, 30 N/mm) within noise tolerance, and the plateau gate passes because the fitted plateau slope stays well under 2 N/mm. The printed block is exactly what the model template expects: one row per segment with start displacement, end displacement, slope, and intercept.

Populating the Model

The model template exposes a load_limiter.curve array of segment objects. Write the fitted segments into that array, then run the template's build step. Because continuity was enforced during fitting, the assembled curve has no discontinuities at breakpoints, so the solver does not need to smooth or re-interpolate. Keep the raw fit output alongside the model parameters so that any later discrepancy between bench and simulation can be traced back to a specific segment.

Anchor breakpoints to detected features (peak, plateau entry/exit) rather than fitting free knots. It makes the fit reproducible and each parameter physically explainable.
If the plateau gate fails, do not force the fit. A drifting plateau almost always means fixture compliance, a mis-seated specimen, or a sensor zero offset — fix the test setup before trusting the parameters.
Bench CSV → Feature-Anchored Piecewise Fit → Model Parameters Raw bench record displacement_mm, force_N Feature detection peak force plateau entry plateau exit rolling median ± 5% band Per-segment OLS fit continuity enforced at knots Model parameter block (load_limiter.curve) seg0: x=[0.0, 18.0] k=45.1 N/mm b=0.0 N seg1: x=[18.0, 22.0] k=1.4 N/mm b=786.0 N seg2: x=[22.0, 95.0] k=1.5 N/mm b=784.0 N seg3: x=[95.0, 120.0] k=30.2 N/mm b=-1940.0 N quality gate: plateau slope 1.5 N/mm ≤ 2.0 N/mm → PASS

Verification and Failure Symptoms

Verify the fit by overlaying the reconstructed piecewise curve on the raw record and checking three things: the peak force matches within 2%, the plateau slope is under the gate threshold, and the residual RMS in each segment is comparable to the bench noise floor. If the reconstructed curve visibly steps at a breakpoint, continuity enforcement was skipped. If the plateau slope gate fails, inspect the fixture and specimen seating before re-running. If the peak is missed entirely, the rolling-median band is too wide — narrow the tolerance fraction and re-detect.

Once the parameter block passes the gate, write it into the model template and rebuild. The model now carries a curve that is traceable segment-by-segment back to the bench record, which is the whole point of automating this step rather than hand-tuning breakpoints.