[Real-world active learning] PR 5: observation conversion + twin-sync primitives - #94
Merged
Merged
Conversation
Plan PR 5 of BRIDGE_INTERFACE_PLAN.md. Lands the conversion and synchronization primitives the real-world wrapper needs, and nothing else: no RealWorldEnv, and nothing new in the live execution loop. Simulator knowledge, on PyBulletEnv -- generic, so every future PyBullet-backed real environment inherits it: - sync_to_state(state): _set_state plus explicit velocity zeroing. Adopting a state from OUTSIDE the simulation is the case _set_state does not cover: it writes poses via update_object -> resetBasePositionAndOrientation, which leaves each body's momentum from the previous rollout intact, so the bodies drift on the next stepSimulation. - gripper_joint_layout(): the finger indices and open/closed values the action splitter needs, read off the simulated robot. Domain knowledge, on PyBulletDominoRealEnv -- pure conversions, no hardware, no I/O beyond the scene file, no notion of a robot: - state_from_observation(obs, prev_state): capture id -> slot via _scene_ids (which the env computed and never used until now), base -> world via the existing pose_base_to_world / domino_upright_yaw. Dominoes the cameras did not see keep their last known pose, and the robot's entry -- including the joint positions _set_state trusts -- carries forward untouched. - task_from_observation(obs, train_or_test): the same conversion plus goal semantics. - _build_task_from_scene refactored onto that same path. Both sources are first normalized to _PerceivedDomino, so the captured scene and live re-perception cannot drift apart -- including the start-domino yaw canonicalization, which is easy to apply in one place and forget in the other. A test asserts the two paths build the same task from the same poses. Observations are read structurally (obs.dominoes, .id/.xyz/.quat_xyzw), so the env still imports no babyrobot and the tests run against a plain stub. They do not skip, and that is asserted. One same-value refactor in the existing execution path: _flush_real_actions now calls self.gripper_joint_layout() instead of gripper_joint_layout_from_robot(self._pybullet_robot). Identical value -- the method just delegates -- and it keeps the new method from landing dead. Tested: 19 new tests, plus mutation checks confirming they bite. Removing the velocity zeroing, mapping observation slots by position instead of capture id, or rebuilding the state instead of carrying it forward each fail the corresponding test. (The first needed the domino component's own zeroing stubbed out, which was masking it.) tests/envs + tests/pybullet_helpers: 248 passed / 8 skipped without the submodule, 83 passed with it installed. mypy clean over 700 files, pylint 10.00/10 on the changed files, yapf/isort/ docformatter clean.
amburger66
force-pushed
the
real-robot-observation-conversion
branch
from
July 28, 2026 15:10
2e22a4c to
abb8203
Compare
|
|
||
| # ── State Write (State → PyBullet) ────────────────────────── | ||
|
|
||
| def sync_to_state(self, state: State) -> None: |
Collaborator
There was a problem hiding this comment.
This idea of zeroing the velocities looks interesting. I wonder whether we should apply it more generally in _set_state...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plan PR 5 of
claude_plans/BRIDGE_INTERFACE_PLAN.md(in BabyRobotPredicator). Depends on #93 (PR 4, merged).Lands the conversion and synchronization primitives the real-world wrapper needs — and nothing else. No
RealWorldEnv, and nothing new wired into the live execution loop.Simulator knowledge — on
PyBulletEnvGeneric on purpose, so every future PyBullet-backed real environment inherits it rather than reimplementing it.
sync_to_state(state)—_set_stateplus explicit velocity zeroing. Adopting a state from outside the simulation is exactly the case_set_statedoesn't cover: it writes poses throughupdate_object→resetBasePositionAndOrientation, which leaves each body holding whatever momentum the previous rollout gave it, so the bodies drift on the nextstepSimulation. Robot joints need no equivalent —_set_stateroutes them throughset_joints, which already resets withtargetVelocity=0.gripper_joint_layout()— the finger indices and open/closed values the action splitter needs, read off the simulated robot.Domain knowledge — on
PyBulletDominoRealEnvPure conversions: no hardware, no I/O beyond the scene file, no notion of a robot.
state_from_observation(obs, prev_state)— capture id → slot viaself._scene_ids(which the env has computed since day one and never used; this is what it was for), base → world via the existingpose_base_to_world/domino_upright_yaw. Dominoes the cameras didn't see keep their last known pose, and the robot's entry — including the joint positions_set_statetrusts — carries forward untouched.task_from_observation(obs, train_or_test)— the same conversion plus goal semantics._build_task_from_scenerefactored onto that same path. Both sources normalize to_PerceivedDominofirst, so the captured scene and live re-perception cannot drift apart — including the start-domino yaw canonicalization, which is easy to apply in one place and forget in the other. A test asserts the two paths build the same task from the same poses.Observations are read structurally (
obs.dominoes, each with.id/.xyz/.quat_xyzw), so the env still imports no babyrobot and the tests run against a plain stub. They do not skip, and that is asserted.Two decisions worth a reviewer's eye
state_from_observationdoes not canonicalize the start domino's yaw. That 180° flip exists to orient the opening push, so it belongs to building a task's initial state. Mid-episode the start domino may already have been pushed, and re-canonicalizing would fight what the cameras actually saw. Tested both ways.One same-value refactor in the existing path
_flush_real_actionsnow callsself.gripper_joint_layout()instead ofgripper_joint_layout_from_robot(self._pybullet_robot). Identical value — the method just delegates — and it keeps the new method from landing as dead code. Flagging it because it is the only line this PR touches inside the execution path; happy to revert it if you'd rather PR 5 leave that file's runtime behavior byte-identical.Known limitation, stated plainly
Like
domino_upright_yawitself, these conversions assume the dominoes they are given are standing. Reading a toppled domino's pose back into the twin is not modeled here. The plan doesn't ask for it at this stage, but it is a real gap for the closed loop and someone should decide whether PR 6 needs it.How this was tested
tests/envs/test_pybullet_domino_real.py— the first tests this env has ever had. Scene load,_domino_roleexplicit-vs-id, role counts, the base→world transplant against hand-computed numbers, start-yaw canonicalization (default, no-flip, and explicitstart_push_dir_base), goal isToppled(target), id→slot mapping, carry-forward, robot/joint preservation, unknown ids, and both newPyBulletEnvmethods.sync_to_state's zeroing removed — the domino component zeroes its own blocks at the end ofreset_state, masking it. The test now stubs that hook out, and fails when the zeroing is removed. Three more mutations verified: mapping observation slots by position instead of capture id, and rebuilding the state instead of carrying it forward, each fail the corresponding tests.tests/envs/+tests/pybullet_helpers/: 248 passed / 8 skipped without the submodule, 83 passed with it installed.mypy . --config-file mypy.iniclean over 700 files; pylint 10.00/10 on the changed files; yapf/isort/docformatter clean; submodule working tree untouched.No hardware involved — every conversion here is pure, and the one simulator write is exercised against PyBullet in-process.
🤖 Generated with Claude Code