roostField / Lab
Curriculum
Phase 05Lesson 5 of 5
70 min
Engineer the real-time loopWeeks 15–18

Motion quality: smoothness, jerk, and recovery

Success rate is a one-bit verdict with terrible statistical power. This lesson builds the second axis of the capstone's evaluation: noise-aware jerk and spectral smoothness, a seam metric for chunk boundaries, safety-filter clip rates, and a reproducible perturbation-recovery protocol.

After this lesson you can
  • Compute RMS jerk and spectral arc length from logged 50 Hz joint trajectories without letting encoder noise dominate the estimate.
  • Define and measure a chunk-seam discontinuity metric that isolates scheduling artifacts from the policy's intrinsic motion style.
  • Instrument a velocity/acceleration safety filter so that its clip fraction doubles as a policy-aggressiveness metric.
  • Design a pre-registered perturbation-recovery protocol and assemble the complete dependent-variable suite the capstone ablations will report.

You would never sign off on a serving deployment because the eval said the answers were correct — the launch review also wants p99 latency, throughput per GPU, and behavior under a traffic spike, because a change that leaves accuracy flat can quietly wreck every other column. Manipulation research has a bad habit of shipping on one column: success rate, a single bit per episode, usually over twenty episodes. The previous lesson explained where every millisecond of your 150 ms pipeline goes; this lesson builds the rest of the capstone's dashboard — how violently the arm moved, how continuously across chunk boundaries, how often the safety layer intervened, and how fast the system recovers when the world refuses to cooperate.

Foundations: From Torque to Noise Variance

Before evaluating motion quality, we must establish why jerk is the correct physical proxy for mechanical stress and how sensor noise corrupts its estimation. For a rigid robotic link, the actuator torque τ\tau is governed by the equation of motion τ=Iq¨+C(q,q˙)q˙+G(q)\tau = I\ddot{q} + C(q, \dot{q})\dot{q} + G(q), where II is the moment of inertia, CC represents Coriolis and centrifugal forces, and GG is gravity. While torque depends on acceleration q¨\ddot{q}, the rate of change of torque demand is driven by jerk q...\dddot{q}. High jerk implies rapid changes in acceleration, which force the motor current to slew quickly. This rapid current change excites structural resonances and causes thermal spikes in the gearbox, making jerk the primary metric for mechanical wear.

To quantify the noise problem, consider the finite-difference stencil for jerk: jk=(qk3qk1+3qk2qk3)/Δt3j_k = (q_k - 3q_{k-1} + 3q_{k-2} - q_{k-3})/\Delta t^3. If the measured position qkq_k contains independent white noise nkn_k with variance σ2\sigma^2, the variance of the estimated jerk is the sum of the squared coefficients times the noise variance, divided by Δt6\Delta t^6. This is a direct application of the variance of a linear combination of independent random variables.

Var(jk)=12+(3)2+32+(1)2Δt6σ2=20σ2Δt6\operatorname{Var}(j_k) = \frac{1^2 + (-3)^2 + 3^2 + (-1)^2}{\Delta t^6} \sigma^2 = \frac{20 \sigma^2}{\Delta t^6}
The variance multiplier is 20, derived from the sum of squared binomial coefficients.

This derivation reveals why raw jerk is unusable: the Δt6\Delta t^6 in the denominator is extremely small for high sampling rates. For Δt=0.02\Delta t = 0.02 s and σ=0.001\sigma = 0.001 rad, the standard deviation of the noise floor is 200.001/(0.02)3559\sqrt{20} \cdot 0.001 / (0.02)^3 \approx 559 rad/s³. This is two orders of magnitude larger than the signal jerk of a smooth motion. Therefore, any jerk metric must be preceded by a low-pass filter to attenuate the high-frequency noise before differentiation.

Finally, we define the baseline for 'smooth' motion: the minimum-jerk profile. This is a quintic polynomial trajectory that minimizes the integral of squared jerk subject to boundary conditions. For a displacement AA over time TT, the peak jerk is 60A/T360A/T^3. This analytic form provides a physical reference: if your measured jerk is close to this value, the motion is as smooth as theoretically possible for that displacement and duration.

Worked Example: Quantifying the Noise Trap

Consider a single joint moving linearly from 0 to 0.1 rad in 4 ticks (Δt=0.02\Delta t = 0.02 s). The true velocity is constant, so the true jerk is zero. However, the encoder adds noise. Let the raw positions be q=[0.000,0.025,0.050,0.076,0.100]q = [0.000, 0.025, 0.050, 0.076, 0.100] rad, where the third sample has a noise spike of +0.001+0.001 rad.

TickRaw Position (rad)Raw Jerk (rad/s³)Filtered Position (rad)Filtered Jerk (rad/s³)
30.076-375.00.0752-12.5
40.1000.00.10000.0
Raw vs. Filtered Jerk Calculation

Calculating the raw jerk at tick 3 using the stencil: j3=(0.1003(0.076)+3(0.050)0.025)/(0.02)3=0.003/0.000008=375j_3 = (0.100 - 3(0.076) + 3(0.050) - 0.025) / (0.02)^3 = -0.003 / 0.000008 = -375 rad/s³. This is a massive spike caused by a single 1 mrad noise error. After applying an 8 Hz Butterworth filter, the noise is attenuated, and the jerk drops to approximately -12.5 rad/s³, which is much closer to the true signal (0 rad/s³) and within the range of the minimum-jerk baseline for this short duration.

Checkpoint 01

Why does the raw jerk estimate show a spike of -375 rad/s³ when the true motion is linear (zero jerk)?

Success rate is a one-bit verdict

Two policies can post identical success on a pick-and-place task and be wildly different systems. One glides to the object along something close to a minimum-jerk path; the other oscillates on approach, saws at the grasp, and slams into the joint-velocity limiter twice per episode — and still succeeds, because a static object on a table is forgiving. The difference matters three ways: mechanical stress (gearbox wear and thermal load scale with torque transients, which scale with jerk), observation quality (a ringing wrist camera feeds motion-blurred views back to the policy), and robustness (the aggressive policy fails first when latency or the scene shifts).

There is also a colder, statistical reason to distrust the single bit: it has almost no power at episode counts you can afford on hardware. Success over nn episodes is binomial, with standard error shrinking only as 1/n1/\sqrt{n}:

SE(p^)=p^(1p^)nn=20, p^=0.75:SE0.097,95% CI[0.56, 0.94]\mathrm{SE}(\hat p) = \sqrt{\frac{\hat p\,(1-\hat p)}{n}} \qquad n = 20,\ \hat p = 0.75:\quad \mathrm{SE} \approx 0.097,\quad 95\%\ \mathrm{CI} \approx [0.56,\ 0.94]
Twenty episodes at 75% success gives a confidence interval nearly forty points wide. A 75% policy and a 90% policy are statistically indistinguishable at this budget.

Twenty real-robot episodes is an honest afternoon of work — resets, battery, occasional E-stops — and it buys a forty-point-wide interval. Yet each of those episodes also produced roughly 1,500 ticks of 50 Hz joint telemetry, every tick a sample for a continuous motion-quality metric. Continuous metrics separate conditions at sample sizes where the binomial bit cannot. For latency-aware chunking they are also the more sensitive instrument: a modestly stale chunk on a static task rarely flips success, but it reliably shows up as a velocity discontinuity at the swap tick — measurable hundreds of episodes before the success column could prove anything.

Smoothness you can defend: jerk, noise, and the spectral alternative

Jerk is the third time-derivative of position, j(t)=q...(t)j(t) = \dddot q(t), and it is the standard smoothness quantity for two convergent reasons: mechanically, jerk tracks the rate of change of actuator torque, so jerk spikes are exactly the transients that excite structural resonance and chew gearboxes; behaviorally, Flash and Hogan's classic result is that unconstrained human reaches are well described as minimum-jerk trajectories, so jerk measures distance from the motion style your teleoperated demonstrations exhibit. Your logs give joint positions qkq_k at Δt=20\Delta t = 20 ms; differentiating three times by finite differences is mechanical:

vk=qkqk1Δt,ak=vkvk1Δt=qk2qk1+qk2Δt2,jk=qk3qk1+3qk2qk3Δt3v_k = \frac{q_k - q_{k-1}}{\Delta t}, \qquad a_k = \frac{v_k - v_{k-1}}{\Delta t} = \frac{q_k - 2q_{k-1} + q_{k-2}}{\Delta t^2}, \qquad j_k = \frac{q_k - 3q_{k-1} + 3q_{k-2} - q_{k-3}}{\Delta t^3}
Composing three first differences yields the third-difference stencil with coefficients (1, −3, 3, −1) — the binomial coefficients with alternating signs.

Now the trap. Encoder readings are not the true position: qk=qˉk+nkq_k = \bar q_k + n_k, with nkn_k approximately white. The WidowX AI streams joint positions from each actuator's magnetic encoder; the raw resolution is not published, but a locked-joint log — quantization plus timing jitter plus structural vibration — typically shows an effective per-sample noise around σ=1\sigma = 1 mrad (measure yours the same way and substitute it). The stencil is linear, so the noise passes through independently of the signal, with variance the sum of squared coefficients over Δt6\Delta t^6:

Var ⁣[jknoise]=(12+32+32+12)σ2Δt6=20σ2Δt6σj=20σΔt3=4.47×103(0.02)3559 rad/s3\operatorname{Var}\!\left[j_k^{\text{noise}}\right] = \frac{\left(1^2 + 3^2 + 3^2 + 1^2\right)\sigma^2}{\Delta t^6} = \frac{20\,\sigma^2}{\Delta t^6} \quad\Rightarrow\quad \sigma_j = \frac{\sqrt{20}\,\sigma}{\Delta t^3} = \frac{4.47 \times 10^{-3}}{(0.02)^3} \approx 559\ \mathrm{rad/s^3}
One milliradian of encoder noise becomes 559 rad/s³ of jerk noise at 50 Hz — before the arm has moved at all.

Compare that to the signal: a calm 1-radian reach executed over 2 seconds as a minimum-jerk profile peaks at 60A/T3=7.560A/T^3 = 7.5 rad/s³ — the noise floor sits nearly two orders of magnitude above the thing you are measuring. The frequency-domain view says why: each derivative multiplies the spectrum by ω\omega, so jerk multiplies by ω3\omega^3, a ferocious high-pass. Arm motion lives below roughly 5 Hz; encoder noise is flat out to the 25 Hz Nyquist limit, and ω3\omega^3 hands the entire budget to the noise. A naive jerk pipeline produces the same number for every policy: the number is the sensor, not the robot.

Checkpoint 02

You compute jerk by triple-differencing raw 50 Hz WidowX AI encoder positions (about 1 mrad of noise per sample) with no filtering. Every policy — smooth or violent — scores an RMS jerk near 600 rad/s³. Why?

The fix: low-pass filter before differentiating — a 4th-order Butterworth at 8 Hz, applied zero-phase (forward-backward, filtfilt) so it adds no lag bias. 8 Hz keeps everything the arm can mechanically do while cutting the band where ω3\omega^3 does its damage. But notice what you just did: the filter is now part of the metric — RMS jerk after an 8 Hz cutoff and after a 4 Hz cutoff can differ by 2–3× on the same log.

With a defensible jerk signal, two summary statistics. RMS jerk is the workhorse — interpretable units, sensitive — but it scales with movement speed and duration: a policy that finishes twice as fast posts higher jerk for identical motion shape. When conditions change speed (and latency ablations do), normalize it away. The log dimensionless jerk (LDLJ) multiplies the integrated squared jerk by T3/vpeak2T^3/v_{\text{peak}}^2, which cancels units exactly:

JRMS=1T0Tj(t)2dt,LDLJ=ln ⁣(T3vpeak20Tj(t)2dt)J_{\mathrm{RMS}} = \sqrt{\frac{1}{T}\int_0^T \lVert j(t)\rVert^2\, dt}, \qquad \mathrm{LDLJ} = -\ln\!\left(\frac{T^3}{v_{\mathrm{peak}}^2}\int_0^T \lVert j(t)\rVert^2\, dt\right)
Check the units: ∫‖j‖²dt carries rad²/s⁵; T³/v²peak carries s⁵/rad² — the product is a pure number. Less negative = smoother; slow and fast executions of the same shape score alike.

A third option sidesteps differentiation entirely — the one the motor-control literature converged on for noisy field data: spectral arc length (the SPARC variant). Take the speed profile — end-effector speed from forward kinematics is conventional — compute its Fourier magnitude spectrum, normalize amplitude and frequency, and measure the arc length of the curve up to a fixed cutoff:

SAL=0ωc(1ωc)2+(dV^(ω)dω)2 dω,V^(ω)=V(ω)V(0)\mathrm{SAL} = -\int_0^{\omega_c} \sqrt{\left(\frac{1}{\omega_c}\right)^{2} + \left(\frac{d\hat V(\omega)}{d\omega}\right)^{2}}\ d\omega, \qquad \hat V(\omega) = \frac{|V(\omega)|}{|V(0)|}
A smooth movement concentrates its speed spectrum at low frequency — a short, simple curve, SAL near −1.4. Jerky motion spreads energy across the band, lengthening the arc and driving SAL more negative.

The intuition is spectral: smoothness is the absence of high-frequency content in the velocity profile, so read the spectrum directly instead of estimating a third derivative that amplifies exactly the band you distrust. No differentiation means no ω3\omega^3 amplification; the fixed cutoff ωc\omega_c (10–20 Hz is conventional) excludes the noise band by construction; the double normalization makes the number dimensionless and roughly bounded. Report SPARC as the headline smoothness number, with RMS jerk (and its pipeline) alongside for physical interpretability.

MetricComputationNoise behaviorCross-condition comparability
RMS jerkTriple differentiation of low-pass-filtered positionsFragile: ω³ amplification; the filter cutoff becomes part of the metricPhysical units (rad/s³), but scales with speed and duration — compare only matched executions
LDLJIntegrated squared jerk normalized by T³/v²peak, log-compressedInherits jerk's noise sensitivity; same filtering requiredDimensionless; slow and fast executions of the same shape score alike
Spectral arc length (SPARC)Arc length of the normalized speed spectrum below a fixed cutoffRobust: no differentiation; noise band excluded by the cutoffDimensionless and roughly bounded; the default for cross-condition claims
Three smoothness metrics and when to trust them
smoothness.py — noise-aware jerk and spectral arc length from 50 Hz logspython
import numpy as np
from scipy.signal import butter, filtfilt

def filtered_jerk(q, dt=0.02, cutoff_hz=8.0):
    """q: (T, J) joint positions at 50 Hz. Returns RMS jerk and LDLJ.

    The filter parameters are part of the metric: freeze them and
    report them next to every number.
    """
    nyq = 0.5 / dt
    b, a = butter(4, cutoff_hz / nyq, btype="low")
    qf = filtfilt(b, a, q, axis=0)            # zero-phase: no lag bias
    v = np.gradient(qf, dt, axis=0)
    acc = np.gradient(v, dt, axis=0)
    jrk = np.gradient(acc, dt, axis=0)
    jerk_sq = np.sum(jrk ** 2, axis=1)        # squared norm across joints
    T_total = len(q) * dt
    rms_jerk = float(np.sqrt(jerk_sq.mean()))
    v_peak = float(np.linalg.norm(v, axis=1).max())
    ldlj = float(-np.log((T_total ** 3 / v_peak ** 2) * np.sum(jerk_sq) * dt))
    return {"rms_jerk": round(rms_jerk, 2), "ldlj": round(ldlj, 3)}

def sparc(speed, dt=0.02, cutoff_hz=10.0):
    """Spectral arc length of a 1-D speed profile. No differentiation.

    speed: end-effector speed from forward kinematics, one value per tick.
    """
    n_fft = int(2 ** np.ceil(np.log2(len(speed)) + 2))   # pad for resolution
    mag = np.abs(np.fft.rfft(speed, n_fft))
    freq = np.fft.rfftfreq(n_fft, d=dt)
    mag = mag / mag[0]                        # amplitude-normalize
    band = freq <= cutoff_hz
    f_hat = freq[band] / cutoff_hz            # frequency-normalize to [0, 1]
    arc = np.sqrt(np.diff(f_hat) ** 2 + np.diff(mag[band]) ** 2)
    return float(-np.sum(arc))

The seam metric: metering the chunk boundary

The real-time chunking lesson built an executor that swaps in a fresh 50-action chunk while the old one is still running. Every swap is a potential discontinuity: the new chunk was computed from an observation now L150L \approx 150 ms old, the old chunk has advanced six or seven ticks since, and the first command of the new chunk can disagree with the last command of the old by an amount the policy never intended. That disagreement is a scheduling artifact — it lives in the executor, not the weights — and it deserves its own metric, measured where it is injected: the commanded stream, conditioned on switch ticks.

sk=qkcmdqk1cmdΔt,Rseam=p95 ⁣({sk}switch ticks)p95 ⁣({sk}within-chunk ticks)s_{k^*} = \frac{\left\lVert q^{\mathrm{cmd}}_{k^*} - q^{\mathrm{cmd}}_{k^*-1}\right\rVert}{\Delta t}, \qquad R_{\mathrm{seam}} = \frac{\mathrm{p95}\!\left(\{s_{k^*}\}_{\text{switch ticks}}\right)}{\mathrm{p95}\!\left(\{s_k\}_{\text{within-chunk ticks}}\right)}
The seam is the implied command velocity across a chunk switch. Dividing by the within-chunk p95 self-calibrates against the policy's own velocity scale: R ≈ 1 means the boundary is invisible in the command stream.

Concrete numbers from the stack you profiled: π₀-class chunks of 50 actions at 50 Hz mean a switch roughly every second. A naive swap under 150 ms of capture-to-first-action latency typically lands the first command about 0.05 rad from the previous tick's — an implied 2.5 rad/s step where within-chunk steps run 0.2–0.4 rad/s. That is Rseam7R_{\text{seam}} \approx 7: once per second, a velocity spike seven times anything the policy asked for; at a 0.5 m working radius (well inside the arm's 0.769 m reach), a 1.25 m/s end-effector jump demanded in a single tick. The blended handoff exists to push RseamR_{\text{seam}} back toward 1 — and this metric is how the capstone ablation table proves it did.

seam.py — chunk-boundary discontinuity from the logged command streampython
import numpy as np

def seam_report(q_cmd, chunk_id, dt=0.02):
    """q_cmd: (T, J) commanded positions, pre-safety-filter.
    chunk_id: (T,) integer label of the chunk that owned each tick.
    """
    step = np.linalg.norm(np.diff(q_cmd, axis=0), axis=1) / dt
    switch = np.diff(chunk_id) != 0        # step[i] spans tick i -> i+1
    seam = step[switch]
    within = step[~switch]
    p95_within = float(np.percentile(within, 95))
    p95_seam = float(np.percentile(seam, 95))
    return {
        "n_seams": int(seam.size),
        "seam_p50_rad_s": round(float(np.percentile(seam, 50)), 3),
        "seam_p95_rad_s": round(p95_seam, 3),
        "within_p95_rad_s": round(p95_within, 3),
        "seam_ratio": round(p95_seam / p95_within, 2),
    }

Note what the metric deliberately ignores: the encoders. The seam is defined on commands, and the choice is load-bearing. The joint's inner control loop plus the arm's inertia act as a mechanical low-pass — a one-tick 2.5 rad/s commanded step produces a smaller, smeared-out wiggle in the measured trajectory, often below the filtered-jerk noise floor. Measured smoothness also blends two causes you must keep separate: the policy's intrinsic style and the executor's handoff. Jerk answers "how does this system move"; the seam ratio, computed on commands and conditioned on switch ticks, answers "what did the scheduler add" — and the capstone needs both answers separately.

Checkpoint 03

Why does the seam metric use the commanded joint stream at chunk switches rather than the measured encoder stream?

The safety envelope: protection that doubles as measurement

Nothing computed by the policy should reach the arm unclamped. The minimal envelope is a per-joint velocity and acceleration limiter at the 50 Hz control rate: with vmax=1.5v_{\max} = 1.5 rad/s and amax=8a_{\max} = 8 rad/s² — deliberately far inside the WidowX AI's published 360–540°/s joint velocity limits — no command may move more than 0.03 rad past the previous one or change the implied velocity by more than 0.16 rad/s per tick. That 2.5 rad/s seam spike? The envelope catches it — the arm never sees it. Which is exactly why you log both sides of the filter: pre-clip is what the policy and scheduler wanted, post-clip is what physics received. Compute the seam metric pre-clip, or the envelope silently launders the evidence.

safety_filter.py — a clamp that countspython
import numpy as np

class SafetyFilter:
    """Per-joint velocity/acceleration clamp at the control rate.

    Also a meter: clip_fraction is a per-condition metric of how
    hard the policy pushes against its physical envelope.
    """

    def __init__(self, n_joints, v_max=1.5, a_max=8.0, dt=0.02):
        self.v_max, self.a_max, self.dt = v_max, a_max, dt
        self.v_prev = np.zeros(n_joints)
        self.ticks = 0
        self.clipped_ticks = 0

    def __call__(self, q_target, q_now):
        v_des = (q_target - q_now) / self.dt
        a_des = (v_des - self.v_prev) / self.dt
        a_lim = np.clip(a_des, -self.a_max, self.a_max)
        v_lim = np.clip(self.v_prev + a_lim * self.dt,
                        -self.v_max, self.v_max)
        self.ticks += 1
        if not np.allclose(v_lim, v_des, atol=1e-9):
            self.clipped_ticks += 1
        self.v_prev = v_lim
        return q_now + v_lim * self.dt   # log q_target AND this, every tick

    @property
    def clip_fraction(self):
        return self.clipped_ticks / max(self.ticks, 1)

The counter is the point. Clip fraction — the share of ticks on which any joint hit a limit — measures what no other metric does: how aggressively the policy-plus-scheduler system leans on its physical envelope. A well-behaved stack on a nominal task clips well under 1% of ticks; a stack running stale chunks under injected latency climbs to several percent, because stale actions keep demanding corrections the envelope refuses to deliver at once. It is a leading indicator: in a latency-injection sweep, clip fraction rises before success rate falls, because the envelope absorbs the early damage. Report clipped magnitude too: a 0.01 rad/s trim and a 1.0 rad/s trim share a flag.

Perturbation, recovery, and the assembled suite

Everything so far measures nominal episodes, and nominal episodes share a blind spot: they never force the closed loop to close. A policy that memorized a trajectory distribution and a policy that genuinely reacts look identical while the world behaves. For a chunked executor the blind spot is structural: within a chunk the system is open-loop by design, so reactivity is exactly the property that chunk length, staleness, and scheduling trade away. The capstone manipulates that trade; the evaluation must measure reactivity directly, not hope nominal success implies it.

The protocol's enemy is casualness. "I nudged the cup sometimes" produces anecdotes; statistics need every perturbation to be the same event. Three properties make it reproducible. A scripted trigger: the perturbation fires when a logged, machine-checkable condition occurs — the first tick at which forward kinematics puts the gripper within 10 cm of the object centroid — never when a human feels like it. A standardized displacement: a fixed 5 cm translation along a direction drawn from a seeded, pre-registered schedule. A human hand guided by a printed template and an audible cue is repeatable to about a centimeter, which is adequate; a cheap servo-driven slider under the object removes the human entirely. Pre-registered outcome definitions: decided before running either experimental condition, so the numbers stay comparable across conditions, days, and reruns.

  • Outcomes: recovery = task completion within a 10 s post-trigger timeout; time-to-recovery = trigger tick to completion tick; timeouts logged as censored, not discarded.
  • Sample size: at least 30 perturbed trials per condition — binary recovery has the same binomial weakness as success, but time-to-recovery is continuous and separates conditions far sooner.
  • Report: recovery fraction with a 95% interval, plus median and IQR of time-to-recovery; the median stays honest under censoring as long as more than half the trials recover.

Now assemble the suite. Phase 05 built each instrument in sequence: the trace gave per-stage latency and staleness, the delay lesson deadline misses and the injection rig, the chunking lesson the executor variants, the profiling lesson the GPU-to-loop timing link, and this lesson the motion side. Every capstone ablation — chunk length, executor type, scheduler policy, injected latency — reports exactly these columns, computed by one shared analysis pipeline:

MetricQuestion it answersSource signalReport as
Success rateDid the task complete?Episode outcome labelsFraction with 95% CI
Task timeHow fast, when it works?Episode timestampsMedian seconds per successful episode
Observation stalenessHow old was the acted-on evidence?Trace spans, per actionp50 / p99 ms plus the sawtooth profile
Deadline miss rateDoes the loop hold its 50 Hz cadence?Executor tick logMisses per 1,000 ticks
RMS jerk + SPARCHow violently does it move?50 Hz encoder stream, frozen filter pipelinerad/s³ (with pipeline) and SAL
Seam ratioWhat does the chunk boundary add?Pre-clip command stream + chunk idsp95 seam over p95 within-chunk
Clip fractionHow hard does it lean on the envelope?Pre/post safety-filter streamsFraction of ticks, plus clipped magnitude
Recovery rate + time-to-recoveryDoes it react when the world changes?Perturbed trials with logged triggersFraction, plus median seconds (censored at timeout)
The Phase 05 metric suite — the dependent variables for every capstone ablation
Studio exercise 01

Two executors, one motion-quality report

Take the synchronous executor and the asynchronous (blended-handoff) executor from the real-time chunking lesson, running the same π₀-class policy on the same pick-and-place task. Run 20 nominal episodes per executor, logging the 50 Hz encoder stream, the pre- and post-safety-filter command streams, and chunk ids per tick. Then run 10 perturbed trials per executor using a scripted trigger (gripper within 10 cm of the object) and a fixed 5 cm displacement. Produce one table — success, RMS jerk (state your filter), SPARC, seam p95 and seam ratio, clip fraction, recovery rate, median time-to-recovery — plus a five-sentence interpretation naming which metrics separated the executors and which did not.

Need a hint?

Build the extraction as pure functions over logged .npz episodes so both conditions pass through byte-identical analysis code, and pre-register the perturbation schedule first. If seam ratio comes back near 1.0 where you expected worse, check that chunk_id flips on the tick where the executor swaps command sources, not where inference returns — an off-by-one absorbs the seam into the within-chunk pool. Compute jerk on encoders and the seam on pre-clip commands; swapping those streams is the classic silent bug.

Where this goes next: the previous lesson, “Profiling the loop: NVTX, Nsight, and the robot twist,” instrumented the causes — where the milliseconds are born. This lesson instrumented the effects — what those milliseconds do to metal, as a metric suite with real statistical power. “From system to science: the falsifiable hypothesis” is where the halves meet: an independent variable you can manipulate, the dependent variables you just built, and the discipline to turn “my stack feels smoother” into a claim precise enough to be wrong — the first sentence of the capstone.