Skip to content

real robot: a fixed-plan explorer, and a record of every look - #115

Merged
amburger66 merged 5 commits into
masterfrom
stage6-loop-tooling
Aug 5, 2026
Merged

real robot: a fixed-plan explorer, and a record of every look#115
amburger66 merged 5 commits into
masterfrom
stage6-loop-tooling

Conversation

@amburger66

@amburger66 amburger66 commented Aug 4, 2026

Copy link
Copy Markdown

Why

Stage 6 (real active learning) is the first stage where the loop runs for hours, and its cost is wall-clock rather than compute: every episode pays for a planning explorer, and every human reset pays for a person. Testing the loop shouldn't require paying for the planner. Separately, a run today leaves almost no record of what perception actually saw.

1. A fixed_plan explorer

The explorer is already a plug point (explorer: in the approach config). The shipped cheap ones — random_options, random_nsrts, no_explore — are all random, so they can't reproduce a specific failure.

fixed_plan replays one plan file every episode. It reads replay_plan.py's format, so a plan produced by probe_real_scene --dump-plan, watched in simulation and already shipped to the arm through replay_plan, can be handed straight to the online loop without retyping anything.

Grounded per episode rather than once, because a human reset rebuilds the episode's task and hands back fresh Object instances.

2. Every look is logged, not just the ones over tolerance

TwinCorrector.absorb only logged when divergence exceeded real_robot_divergence_atol. A run whose looks all behaved therefore said nothing — indistinguishable from nobody having looked.

It now logs every look, with the per-object breakdown. That distinction matters: _max_position_divergence answers "how bad", which is what the tolerance is checked against, but only "which object" tells a single knocked domino apart from a table-height offset shared by all of them.

3. Looks can be dumped to JSON

real_robot_observation_dump_dir (empty = off) writes one file per look, recording what was perceived beside what the twin predicted, plus the per-object distances. That makes a session re-examinable offline, without the robot and without the cameras.

Write failures are logged and swallowed — losing a dump must never take the arm down mid-episode.

4. Two bugs the rehearsal found, and the fix for each

Running the rehearsal is what surfaced these. Both concern Wait, whose terminal is stateful: it counts consecutive settled steps in option.memory, and its own option policy consults it once per step.

OptionBoundaryBuffer was consulting it a second time. Two callers were driving one tally, so Wait judged the scene settled in a third of the steps it really takes. The boundary check now snapshots and restores the option's memory, making it a pure query.

A twin correction was being counted as the scene moving. Writing perception into the twin replaces object poses without anything having moved. Since a correction only happens when a chunk ships — i.e. exactly at the boundary — it zeroed the tally at precisely the moment it was about to pay off, every cycle. Wait therefore never terminated, the episode ran to the 500-step cap instead of ending when the plan did, and it burned a look every few steps on the way. note_external_state_change keeps the tally and moves the comparison point past the jump.

Measured on the rehearsal, same config and same plan, before → after:

before after
Wait never terminated terminates after 11 steps
episode ends by 500-step cap plan exhausted after 3 options
episode length ~500 steps 75 steps
looks per episode 35 3
actions dropped mid-option 7 per episode 0

5. A rehearsal config

predicatorv3/stage6_rehearsal_domino_real.yaml runs the whole loop shape — explore, learn, test, reset — with the arm powered down and the cameras unplugged: real_robot_dry so nothing moves, scene_file perception so no camera opens, and the fixed_plan explorer so an episode costs nothing and does the same thing every time.

It does not replace the learner, which is separately expensive; that is called out in the config's own header.

Testing

pytest tests/pybullet_helpers/ tests/explorers/test_fixed_plan_explorer.py tests/envs/test_real_robot_execution.py tests/test_skill_factories.py — 163 passed. Full suite (excluding the network-dependent approaches/agent_sdk dirs) 855 passed. pylint (repo rcfile), yapf, isort, docformatter and mypy all clean.

New tests cover: the plan grammar including the # header those files carry and the -> {...} subgoal tail; the explorer replaying the file rather than searching; a missing path failing loudly rather than quietly exploring some other way; per-object divergence naming and ordering; a dumped look recording both sides of the comparison; dumping staying off by default; the boundary check leaving a stateful terminal untouched; and a settle tally surviving an external state change.

Exercised end to end through the rehearsal, twice. Not exercised on hardware — that is Stage 6 itself.

Scope

The learner is deliberately untouched. Swapping the explorer removes the exploration cost, but agent_po_predicate_invention_al still invents predicates, which is separately expensive. A cheap stand-in there would be agent_sim_learning with agent_sim_learn_oracle_sim_program and agent_sim_learn_oracle_sim_params both on — handing the learner the oracle artifacts instead of synthesising them — but that is a separate change and unverified.

One thing this PR does not contain: the rehearsal also surfaced a constant 4 mm z error in captured scenes, traced to the pose-estimation scripts defaulting to a table height that was superseded when it was measured. That fix belongs to BabyRobotPredicator and is on its domino-table-z-4mm branch.

Three pieces for exercising the online loop on hardware, where the cost is
wall-clock rather than compute.

fixed_plan explorer. The planning explorers cost minutes per episode, so a
full cycle is expensive to run for the sake of testing the loop around it.
This one replays a plan file every episode and costs nothing, so a run that
goes wrong is the loop's fault and not the planner's. It reads replay_plan's
format, which means a plan dumped by probe_real_scene --dump-plan and already
verified through replay_plan can be handed straight to the loop. Grounded per
episode, because a human reset rebuilds the task and hands back fresh Objects.

Every look is logged, not only the ones over tolerance. A run whose looks all
behaved previously said nothing at all, so there was no way to distinguish
"perception was healthy" from "nobody checked". The line carries the
per-object breakdown, which is what tells a single knocked domino apart from a
table-height offset shared by all of them -- the max alone cannot.

Looks can be dumped to JSON (real_robot_observation_dump_dir, off by default).
Each file records what was perceived beside what the twin predicted, so a
session can be re-examined offline without the robot. Write failures are
logged and swallowed: losing a dump must not take the arm down mid-episode.

The learner is untouched.
Stage 6 proper is exp_domino_real.yaml -- live cameras, a live arm, a human
reset per episode, a planning explorer costing minutes an episode. That is
what to run once the loop is trusted, and the wrong thing to debug the loop
with.

This keeps the loop shape (explore, learn, test) and makes one pass
survivable: the fixed_plan explorer replays a verified plan instead of
planning, nothing moves, no camera opens, and every look is dumped. It runs
with the robot powered down.

It does not replace the learner, which is separately expensive; the header
says so.
Wait counts consecutive settled steps in its own memory, and its option
policy already consults it once per step. OptionBoundaryBuffer consulted
it a second time, so Wait judged the scene settled in a third of the
steps it really takes.

The check now restores the option's memory, making it a pure query.
Wait ends once the scene holds still for several consecutive steps.
Writing perception into the twin moves objects without the scene having
moved, so Wait counted every look as motion, zeroed its tally, and never
saw the scene settle -- the episode then ran to the step cap instead of
ending when the plan did, spending a look every few steps on the way.

rebaseline_quiescence re-seeds the baseline and keeps the tally, so the
jolt is skipped rather than counted.
The old name described the mechanism, so a reader had to already know
what a quiescence baseline was to guess what the call did. The new one
names the event instead: the state was set from outside rather than
moved into, which is the thing the caller actually knows.
@amburger66
amburger66 marked this pull request as ready for review August 5, 2026 18:01

@yichao-liang yichao-liang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@amburger66
amburger66 merged commit 5437b60 into master Aug 5, 2026
14 checks passed
@amburger66 amburger66 self-assigned this Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants