Calibrating Acceleration Limits for Realistic Vehicle Paths

Synthetic vehicle paths that respect a speed cap but ignore how fast that speed is allowed to change produce trajectories that teleport between velocities, and the fix is to bound acceleration, deceleration, jerk, and turn rate against distributions measured from real telemetry.

Part of Physics-Based Path Generation: that stage builds trajectories from an explicit motion model rather than pure interpolation, and this page focuses on the one calibration step that most often lets an unrealistic path slip through — the kinematic envelope that constrains not just where a vehicle is but how violently its motion state may transition between samples.

Root Cause: An Unbounded Second and Third Derivative

A speed-limited path generator enforces vtvmax\lvert v_t \rvert \le v_{\max} at every sample but says nothing about the derivatives of velocity. The result is a trajectory that is valid position-by-position yet impossible frame-to-frame: it jumps from 5 m/s to 25 m/s across a single 1 s tick, implying an acceleration no road vehicle can produce, and it snaps between headings implying an instantaneous lateral force. Downstream models trained on such data learn a physics that does not exist, and any evaluator that reconstructs acceleration will flag the batch.

Acceleration is the first time-derivative of velocity and jerk is the second:

at=vtvt1Δt,jt=atat1Δt=vt2vt1+vt2Δt2.a_t = \frac{v_t - v_{t-1}}{\Delta t}, \qquad j_t = \frac{a_t - a_{t-1}}{\Delta t} = \frac{v_t - 2v_{t-1} + v_{t-2}}{\Delta t^2}.

Real drivers keep jerk small because passengers feel it directly — comfortable longitudinal jerk stays under roughly 0.9 m/s³ and emergency maneuvers rarely exceed 2 m/s³. A generator that bounds only ata_t still produces square-wave acceleration profiles: the acceleration is legal at every instant but switches sign discontinuously, which is a jerk spike of unbounded magnitude. Lateral motion has the same trap — a heading change Δψ\Delta\psi over Δt\Delta t implies a turn rate ω=Δψ/Δt\omega = \Delta\psi / \Delta t and a lateral acceleration alat=vωa_{\text{lat}} = v\,\omega that must also stay inside the friction ellipse. Calibration means fitting all four bounds — acceleration, deceleration, jerk, turn rate — to reference distributions rather than guessing round numbers, then rejecting or repairing any generated segment that leaves the envelope. Because the noise stage layered on later by adding realistic GPS noise to synthetic vehicle trajectories will itself perturb finite-difference derivatives, the clean path must sit comfortably inside the bound, not on its edge.

Kinematic envelope: bounding acceleration, jerk, and turn rate for a synthetic vehicle path Left panel plots two speed-versus-time profiles sampled at fixed ticks. A dashed accent profile jumps between speeds in single steps, producing square-wave acceleration and unbounded jerk spikes flagged with markers. A solid primary profile ramps smoothly with rounded transitions, keeping acceleration and jerk inside the calibrated band shaded behind it. Right panel shows a lateral acceleration friction ellipse in the longitudinal-lateral acceleration plane: a primary-soft filled ellipse bounded by a_x maximum and a_lat maximum, with an in-bounds point on the solid path and an out-of-bounds point from the unbounded path lying outside the ellipse. Speed profile at fixed ticks Lateral friction ellipse time (ticks) speed v calibrated jerk spikes aₓ (long.) a_lat feasible smooth path rejected
A speed cap alone permits square-wave acceleration and instantaneous turns; calibrated acceleration, jerk, and turn-rate bounds keep every state transition inside a physically feasible envelope.

Prerequisite Check: Reconstruct the Derivatives and Look for Spikes

Before calibrating anything, confirm the failure exists by reconstructing acceleration and jerk from a generated batch and comparing their tails to a reference set. Pin the toolchain so finite-difference results are identical across machines.

# requirements.txt — pin majors, let patch releases float
numpy==1.26.*
scipy==1.11.*
pandas==2.*
python
import numpy as np

def kinematic_summary(xy: np.ndarray, dt: float) -> dict:
    """xy: (N, 2) metric coordinates (project to UTM first). Returns derivative tails."""
    v = np.diff(xy, axis=0) / dt                       # velocity vectors
    speed = np.linalg.norm(v, axis=1)
    accel = np.diff(speed) / dt                         # longitudinal accel (m/s^2)
    jerk = np.diff(accel) / dt                          # jerk (m/s^3)
    heading = np.unwrap(np.arctan2(v[:, 1], v[:, 0]))   # unwrap avoids +/-pi jumps
    turn_rate = np.diff(heading) / dt                   # rad/s
    return {
        "accel_p99": float(np.percentile(np.abs(accel), 99)),
        "jerk_p99": float(np.percentile(np.abs(jerk), 99)),
        "turn_rate_p99": float(np.percentile(np.abs(turn_rate), 99)),
    }

# A speed-capped-only generator typically returns something like:
# {'accel_p99': 11.4, 'jerk_p99': 47.9, 'turn_rate_p99': 3.1}
# Reference passenger-car telemetry: accel_p99 ~ 3.0, jerk_p99 ~ 1.8, turn_rate_p99 ~ 0.5

A jerk_p99 an order of magnitude above the reference is the signature of square-wave acceleration. Coordinates must be metric — reconstruct derivatives in a UTM zone or EPSG:6933, never in raw EPSG:4326 degrees, or the m/s² units are meaningless.

Fix: Fit the Bounds, Then Rate-Limit the Motion State

Calibration is two moves: estimate each bound from reference telemetry as a high percentile (not the max, which is dominated by GPS artifacts), then enforce them during generation by clamping the change in each motion-state variable per tick rather than the variable itself.

Step 1 — Fit bounds from reference distributions

python
import numpy as np

def fit_envelope(ref_batches: list[np.ndarray], dt: float, q: float = 99.5) -> dict:
    """Aggregate per-vehicle derivative percentiles into calibrated bounds.

    q=99.5 rejects sensor outliers while keeping genuine hard maneuvers.
    """
    accels, decels, jerks, turns = [], [], [], []
    for xy in ref_batches:
        v = np.diff(xy, axis=0) / dt
        speed = np.linalg.norm(v, axis=1)
        a = np.diff(speed) / dt
        accels.append(a[a > 0]); decels.append(-a[a < 0])
        jerks.append(np.abs(np.diff(a) / dt))
        h = np.unwrap(np.arctan2(v[:, 1], v[:, 0]))
        turns.append(np.abs(np.diff(h) / dt))
    cat = lambda xs: np.concatenate(xs) if xs else np.array([0.0])
    return {
        "a_max": float(np.percentile(cat(accels), q)),      # e.g. 3.0 m/s^2
        "d_max": float(np.percentile(cat(decels), q)),      # e.g. 6.0 m/s^2 (braking)
        "j_max": float(np.percentile(cat(jerks), q)),       # e.g. 1.8 m/s^3
        "omega_max": float(np.percentile(cat(turns), q)),   # e.g. 0.5 rad/s
    }

Step 2 — Enforce the envelope with a jerk-aware rate limiter

Clamp acceleration toward its target so the jerk implied by the change stays under j_max, then integrate. This is the step that eliminates the square wave: acceleration can no longer switch sign discontinuously because its own rate of change is bounded.

python
import numpy as np

def rate_limit(target_speed: np.ndarray, dt: float, env: dict,
               v0: float = 0.0) -> np.ndarray:
    """Produce a speed profile that honours a_max, d_max and j_max."""
    v = np.empty_like(target_speed, dtype=float)
    v[0], a_prev = v0, 0.0
    for t in range(1, len(target_speed)):
        want = target_speed[t]
        a_des = (want - v[t - 1]) / dt
        a_des = np.clip(a_des, -env["d_max"], env["a_max"])   # accel / brake caps
        # limit change in acceleration so |jerk| <= j_max
        dj = np.clip(a_des - a_prev, -env["j_max"] * dt, env["j_max"] * dt)
        a = a_prev + dj
        v[t] = max(0.0, v[t - 1] + a * dt)                    # no negative speed
        a_prev = a
    return v

def limit_turn_rate(heading: np.ndarray, dt: float, omega_max: float) -> np.ndarray:
    """Slew-limit heading so per-tick turn rate stays feasible."""
    out = np.copy(heading)
    for t in range(1, len(heading)):
        step = np.clip(heading[t] - out[t - 1], -omega_max * dt, omega_max * dt)
        out[t] = out[t - 1] + step
    return out

Apply limit_turn_rate in concert with the speed limiter so lateral acceleration alat=vωa_{\text{lat}} = v\,\omega stays inside the friction ellipse — at high speed the turn-rate cap must tighten, which you can enforce by scaling omega_max by min(1, a_lat_max / (v * omega_max)). Keeping the geometry feasible here is what lets preventing trajectory self-intersection in physics paths work with a smooth curve rather than fighting kinks the motion model injected.

Verification Step: A Jerk-Limit CI Gate

Wire the envelope as a pytest gate so a regenerated batch cannot regress into teleporting motion. Give the gate a small slack over the fitted bound to absorb finite-difference noise from the downstream perturbation stage.

python
import numpy as np

def test_batch_inside_kinematic_envelope(batch, dt, env, slack=1.10):
    """batch: iterable of (N,2) metric paths. Fails if any derivative tail exceeds bound."""
    worst = {"accel": 0.0, "jerk": 0.0, "turn": 0.0}
    for xy in batch:
        v = np.diff(xy, axis=0) / dt
        speed = np.linalg.norm(v, axis=1)
        a = np.diff(speed) / dt
        j = np.diff(a) / dt
        h = np.unwrap(np.arctan2(v[:, 1], v[:, 0]))
        w = np.diff(h) / dt
        worst["accel"] = max(worst["accel"], np.abs(a).max(initial=0.0))
        worst["jerk"] = max(worst["jerk"], np.abs(j).max(initial=0.0))
        worst["turn"] = max(worst["turn"], np.abs(w).max(initial=0.0))
    assert worst["jerk"] <= env["j_max"] * slack, f"jerk {worst['jerk']:.2f} > gate"
    assert worst["accel"] <= max(env["a_max"], env["d_max"]) * slack
    assert worst["turn"] <= env["omega_max"] * slack

The jerk assertion is the load-bearing one: acceleration and turn-rate caps are easy to satisfy trivially, but a batch that passes the jerk gate cannot contain the square-wave artifact that a speed cap alone permits. Run the same gate against a held-out reference batch to confirm the bounds are not so tight that real maneuvers would fail.

Edge Cases & Gotchas

Low sample rate hides jerk violations. At dt = 5 s a genuine 1.8 m/s³ jerk is smeared into a single acceleration step and the finite-difference reconstruction underreports it, so the gate passes on a coarse trajectory that is actually infeasible. Calibrate and gate at the finest available tick, and if you must downsample, do it after the rate limiter so the enforced profile is preserved rather than aliased away.

Heading wrap at the antimeridian and at rest. arctan2 returns values in (π,π](-\pi, \pi], so a vehicle crossing due west flips heading by 2π2\pi and the naive turn-rate difference reports an impossible slew. Always np.unwrap the heading series before differencing. Separately, when speed is near zero the heading is numerically undefined — gate turn rate only on samples above a small speed floor, or a parked vehicle’s GPS jitter manufactures phantom turns.

Braking is not symmetric with acceleration. A single a_max used for both throttle and brake produces vehicles that accelerate as hard as they stop, which no reference distribution supports. Fit d_max separately — deceleration percentiles run roughly twice the acceleration percentiles for passenger cars — and clamp the two directions independently as the reproducer does.

Frequently Asked Questions

Should I use the maximum observed acceleration or a percentile as the bound?
Use a high percentile such as the 99.5th, not the maximum. Reference telemetry maxima are dominated by GPS position spikes and dropped samples that imply superhuman accelerations, so fitting to them makes the envelope useless. A high percentile captures genuine hard maneuvers while discarding sensor artifacts, and you can validate the choice by confirming a held-out real batch passes the resulting gate.
Why bound jerk at all if acceleration is already capped?
An acceleration cap permits acceleration to jump between its extremes on consecutive ticks, which is a square-wave profile with unbounded jerk at every corner. Passengers feel jerk directly and real drivers keep it small, so a jerk bound is what forces the smooth ramp-in and ramp-out that distinguishes a plausible speed profile from a legal-but-impossible one. It is also the single most discriminating signal an evaluator can reconstruct from position data.
Do acceleration limits need to change with speed?
Yes for lateral acceleration and often for longitudinal. Turn rate must tighten as speed rises so lateral acceleration stays inside the tire friction ellipse, otherwise a feasible low-speed turn becomes an impossible high-speed one. Longitudinal acceleration also falls at high speed because available engine force drops, so a speed-dependent a_max fit from binned reference data is more realistic than a single constant.
How does GPS noise added later interact with these bounds?
Perturbing positions inflates finite-difference derivatives, so a clean path sitting exactly on the jerk bound will fail the gate once noise is applied. Keep the calibrated path comfortably inside the envelope and give the CI gate a small slack factor over the fitted bound to absorb that inflation. Order matters: enforce the kinematic envelope on the clean path first, then add noise, then gate with slack.