Replaying episodes¶
piper-replay plays a recorded episode back on the real arm, open-loop, and
then prints a report of how closely the arm reproduced the original
trajectory. It reads the episode straight from the dataset's parquet files —
no teleop hardware (Quest or SpaceMouse) is needed.
Why replay¶
Replay closes the loop on data collection in three ways:
- Sanity-check a dataset physically. Watching the arm re-execute a demonstration is the fastest way to confirm that what you recorded is what actually happened — correct motion, correct gripper timing, no glitches.
- Verify the arm can reproduce demonstrations. The recorded actions are absolute EEF targets (see Dataset format). If the arm cannot track them accurately in open loop, a policy trained on them will inherit that error floor.
- Regression-test control changes. After changing gains, firmware, or the control backend, replay a known-good episode and compare the report numbers against earlier runs.
Run a replay¶
Home the arm first
After every arm power-on, run make home to unfold the arm from the
factory folded pose into the working pose. Starting a replay from the
folded pose fails with TARGET_POS_EXCEEDS_LIMIT(4).
Flags¶
| Flag | Default | Meaning |
|---|---|---|
--root <ds> |
required | Dataset root directory (the folder containing data/ and meta/) |
--episode N |
0 |
Episode index to replay |
--speed X |
1.0 |
Time scale; 0.5 = half speed |
--config FILE |
none | Config overlay applied on top of default.yaml (see Configuration) |
--sim |
off | Dry run on the fake backend: parses the episode and exercises the timing loop with no hardware |
--position |
off | Force the piper firmware position backend instead of the default MIT impedance backend |
--arm-side {left,right} |
right |
Dual-arm (modality-schema) datasets: which arm's channel to replay |
--relative / --absolute |
auto | Override the replay anchoring (see below) |
--gripper-max X |
episode max | Raw gripper value meaning "fully open", for datasets whose gripper is not in meters |
--joints |
off | Replay the recorded joint trajectory instead of EEF actions (native datasets, or modality sets exposing <side>_arm_joint; a pre-flight check warns if recorded joints exceed this arm's limits) |
--viz |
off | Live rerun 3D view of the arm and target during playback |
--anchor X,Y,Z[,R,P,Y] |
arm's current pose | Anchor relative replay to this fixed pose (m, RPY in deg, base frame) — deterministic: same trajectory every run |
--scale X |
1.0 |
Scale translation deltas in relative replay (shrink motions recorded on a larger robot to fit the workspace) |
EEF vs joint space¶
By default the recorded action.arm_eef targets are replayed through the
same EEF command path teleop uses (host IK in impedance mode, firmware IK in
--position mode). With --joints, the recorded observation.arm_joint
trajectory is streamed directly — impedance mode streams MIT joint targets,
position mode streams move_j — which removes IK from the loop and
reproduces the exact joint-space motion, including null-space posture. The
report then shows joint-space error in degrees. Modality datasets (e.g.
RoboMIND) carry no joint channel, so --joints is refused for them.
External and dual-arm datasets¶
piper-replay also plays back GR00T/modality-style LeRobot datasets that were
recorded on other robots — for example the RoboMIND agilex_cobot_magic
dual-arm sets, whose action is a 16-dim vector described by
meta/modality.json. The schema is auto-detected; pick the arm with
--arm-side:
Because such data lives in a different robot's frame convention, it is
replayed relative by default: the recorded motion — every pose expressed
as a delta from the episode's first action pose — is applied on top of this
arm's current pose, and the approach phase is a no-op. Home the arm first
so the motion unfolds from the middle of the workspace. Raw-unit gripper
values are rescaled linearly to the configured gripper.max_width
(override the full-open raw value with --gripper-max).
For a deterministic playback that does not depend on where the arm happens
to be, pass --anchor (a fixed base-frame pose the episode's first action is
pinned to) and, if the source robot's motion span exceeds this arm's
workspace, --scale to shrink the translations:
piper-replay --root data/50_pourtea --episode 53 --arm-side right \
--anchor "0.40,0,0.30" --scale 0.7
Target positions are always clamped to the configured workspace box; if any targets get clamped, the run prints how many, so you know where the reproduction was distorted. If the requested episode's parquet is missing (sampled subsets ship only a few episodes), the error lists the episode indices actually present.
Cross-robot replay reproduces motion, not the task
The trajectory is re-anchored to your arm and clipped to your workspace — useful for inspecting data quality and exercising the control stack, but scene geometry, object positions, and the second arm's contribution are not reproduced.
Dry-run first
piper-replay --root <ds> --sim checks that the episode loads and the
timing loop runs before you involve the real arm.
What happens, step by step¶
- Load. The tool reads
data/chunk-XXX/episode_XXXXXX.parquetfrom the dataset root and extracts the absoluteaction.arm_eeftargets,action.gripperopenings,observation.arm_eefmeasurements, and timestamps. It prints a summary: step count, recorded duration, playback duration at your--speed, and the arm backend in use. - Connect. It brings up the arm backend (MIT impedance by default), waits 2 s for mode entry to settle, and verifies EEF feedback is valid.
- Approach. It interpolates linearly from the arm's current measured
pose to the episode's first action pose over 3 s, printing how far away
that start pose is in millimetres — with a
stand clear!warning. - Playback. It steps through the actions paced by the recorded
timestamps (scaled by
--speed), which makes playback robust to tick jitter in the data. Each step commands the absolute EEF pose and gripper opening, and samples the arm's measured EEF position for the report. Any arm fault aborts playback immediately and stops the arm. - Report. After the last step it prints the reproduction-error report (below) and stops the arm.
Safety notes¶
The arm follows the recording blindly
Replay is open-loop: there is no perception and no reaction to obstacles. Clear the workspace of everything that was not present during recording — including your hands — before starting. The approach phase alone can sweep a large arc if the arm starts far from the episode's first pose.
- Replay commands the same arm backends as teleop, so the per-tick step clamps come from the backend and any arm fault aborts the run — the same safety envelope described in Control modes.
- Start at reduced speed (for example
--speed 0.7or--speed 0.5) the first time you replay a dataset, and only move to1.0once the motion looks right. - Keep in mind that
--positionmakes the arm stiff: the firmware position servo tracks tightly but is not compliant on contact.
Reading the report¶
At the end of playback you get:
=============== REPLAY REPORT ===============
vs RECORDED observation : RMS ... mm max ... mm
vs COMMANDED action : RMS ... mm
=============================================
| Line | What it measures |
|---|---|
vs RECORDED observation |
Distance between the EEF positions measured during replay and the positions measured during the original recording, at matching steps — RMS and worst-case max. This is the headline "did the arm do the same thing again" number. |
vs COMMANDED action |
Distance between the replayed measured positions and the commanded action targets — pure tracking error of the current controller, independent of how well the recording itself tracked. |
What is normal¶
Expect the numbers to sit in the same ballpark as the measured tracking benchmarks for the control mode you are using (Piper-X, firmware S-V1.8-9; see Benchmarks):
- Position mode (
--position): about 1.3 mm dynamic RMS. - Impedance mode (default, shipped soft gains): about 14 mm dynamic RMS, with an 11 mm static hold error.
So an impedance-mode replay reporting an RMS in the low tens of millimetres is normal, not a bug.
What a large error suggests¶
If the report is far above those ballparks:
- The controller changed since recording. Different impedance gains, a
different firmware, or a different backend than the one used during
collection all shift the error. The dataset's
meta/collection_meta.jsonrecords the backend and gains used at recording time (see Dataset format), so compare it against your current config. - You are replaying in a different mode than recorded. For example,
replaying an impedance-mode recording with
--position(or the reverse) changes the tracking behaviour — see Control modes for how the two modes differ.
Once a dataset replays cleanly, the next step is closing the loop with a policy — see Running inference.