Repository navigation
fix(dual-franka): align RLinf v0.4 deployment and control diagnostics (v0.1) #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+823
−106
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
2aae8e7
fix(dual-franka): rotation-hold review slice
codex a85d370
fix(dual-franka): rlinf-v04 review slice
codex a747738
fix(dual-franka): manual-vla review slice
codex 34f70ee
test(vla): focus manual rollout coverage on operator-visible behavior
codex 796a1f7
test(control): use representative motion outcomes instead of paramete…
codex ee1cbcf
test(rlinf): retain the official environment-to-worker contract only
codex ead897f
Merge branch 'nieyi/split-01-rlinf-v04' into nieyi/split-02-rotation-…
codex f5762ae
Merge branch 'nieyi/split-01-rlinf-v04' into nieyi/split-03-manual-vla
codex face067
test(control): keep one full rotation control regression
codex f687f5b
test(vla): retain startup and continuous policy flow coverage
codex ae10d42
test(control): drop deployment-specific override permutations
codex 99d3245
test(vla): reuse the existing diagnostic test module
codex 7d8769f
Merge branch 'nieyi/split-03-manual-vla' into nieyi/compress-franka-r…
codex f3e20d3
refactor(dual-franka): trim unused control trace and manual rollout p…
codex c0fd6a8
fix(dual-franka): propagate protective rotation abort during recovery
codex 5069d54
fix(dual-franka): preserve official dummy worker observations
codex 4ad43f9
test(dual-franka): separate schema checks from prompt fixture edits
codex 98504cb
test: narrow offline regressions to essential workflows
codex 1481f56
fix(dual-franka): restore move_delta accuracy result contract
codex 777bb10
fix(dual-franka): restore skill results and simplify rotation
codex 282dfc4
fix(dual-franka): preserve recovery accuracy diagnostics
codex f814943
fix(dual-franka): include task 6 for default manual rollout
codex b8eed29
test(dual-franka): place full VLA diagnostic under e2e
codex 73dcdbb
fix(dual-franka): finish rotation references before tolerance checks
codex dc9931b
docs(dual-franka): document rotation drift guard default
codex 336ed83
fix(dual-franka): simplify deployment adapters and diagnostics
codex 8f61c15
fix(dual-franka): compensate rotation drift and document its limit
codex 3b6e256
refactor(dual-franka): name VLA-only diagnostic and simplify docs
codex 1c1d393
fix(dual-franka): use vla_only diagnostic name
codex 6cda640
Merge branch 'main' into nieyi/split-01-rlinf-v04
NewYork424 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
Large diffs are not rendered by default.
Oops, something went wrong.
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| # Copyright 2026 The RPent Authors. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # https://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """RPent observations on top of the official RLinf dual-Franka environment.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import copy | ||
| import queue | ||
| from dataclasses import replace | ||
| from typing import Any | ||
|
|
||
| import cv2 | ||
| import gymnasium as gym | ||
| import numpy as np | ||
| from gymnasium.envs.registration import register | ||
| from rlinf.envs.real.franka.dual_franka_tcp import DualFrankaTCPEnv | ||
| from rlinf.envs.real.wrappers import build_stack | ||
| from rlinf.robotics.parts.cameras import Camera, CameraInfo, RealSenseCamera | ||
|
|
||
| from robots.franka.rpent_env import realsense_color_intrinsics | ||
|
|
||
| _CAMERA_FRAME_TIMEOUT_S = 0.5 | ||
| _REALSENSE_BACKEND = "rpent_realsense" | ||
|
|
||
|
|
||
| @Camera.register(_REALSENSE_BACKEND) | ||
| class RPentRealSenseCamera(RealSenseCamera): | ||
| """Expose projection metadata from the camera's existing SDK connection.""" | ||
|
|
||
| def get_color_intrinsics(self) -> dict[str, Any]: | ||
| """Read serializable intrinsics on the node that owns the camera.""" | ||
| return realsense_color_intrinsics(self) | ||
|
|
||
|
|
||
| class RPentDualFrankaEnv(DualFrankaTCPEnv): | ||
| """Preserve policy observations and retain their uncropped RGB-D for RPent.""" | ||
|
|
||
| def __init__(self, *args: Any, **kwargs: Any) -> None: | ||
| self._rpent_camera_snapshot: dict[str, dict[str, np.ndarray]] | None = None | ||
| super().__init__(*args, **kwargs) | ||
|
|
||
| def _camera_infos(self) -> list[CameraInfo]: | ||
| # A public method on this driver is also forwarded by RLinf's remote | ||
| # camera handle; SDK profile objects themselves cannot cross that boundary. | ||
| return [ | ||
| replace(info, camera_type=_REALSENSE_BACKEND) | ||
| if info.camera_type == "realsense" | ||
| else info | ||
| for info in super()._camera_infos() | ||
| ] | ||
|
|
||
| def _get_camera_observation( | ||
| self, | ||
| ) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray]]: | ||
| """Collect once for policy and perception, invalidating stale raw data.""" | ||
| self._rpent_camera_snapshot = None | ||
| frames: dict[str, np.ndarray] = {} | ||
| depths: dict[str, np.ndarray] = {} | ||
| raw_frames: dict[str, np.ndarray] = {} | ||
| raw_depths: dict[str, np.ndarray] = {} | ||
| display_frames: dict[str, np.ndarray] = {} | ||
| all_fresh = True | ||
|
|
||
| for name, camera in self._cameras.items(): | ||
| try: | ||
| reading = camera.get_observation( | ||
| timeout=_CAMERA_FRAME_TIMEOUT_S, | ||
| max_age=_CAMERA_FRAME_TIMEOUT_S, | ||
| ) | ||
| except queue.Empty: | ||
| # Preserve RLinf's policy fallback during camera recovery, but | ||
| # never present its old image as a fresh projection observation. | ||
| cached = self._last_camera_frame.get(name) | ||
| if cached is None: | ||
| raise RuntimeError( | ||
| f"Camera {name} stalled with no cached frame to fall back to." | ||
| ) from None | ||
| self._logger.error("Camera %s stalled; using the last frame.", name) | ||
| reading = cached | ||
| all_fresh = False | ||
|
|
||
| raw_frames[name] = reading["frame"][..., ::-1].copy() | ||
| reshape_size = self.observation_space["frames"][name].shape[:2][::-1] | ||
| cropped, resized = self._crop_frame(reading["frame"], reshape_size) | ||
| frames[name] = resized[..., ::-1] | ||
| display_frames[name] = resized | ||
| display_frames[f"{name}_full"] = cropped | ||
| if "depth" in reading: | ||
| raw_depths[name] = reading["depth"].copy() | ||
| _, depths[name] = self._crop_frame( | ||
| reading["depth"], reshape_size, interpolation=cv2.INTER_NEAREST | ||
| ) | ||
| self._last_camera_frame[name] = reading | ||
|
|
||
| self.camera_player.put_frame(display_frames) | ||
| if all_fresh: | ||
| self._rpent_camera_snapshot = { | ||
| "raw_frames": raw_frames, | ||
| "raw_depths": raw_depths, | ||
| } | ||
| return frames, depths | ||
|
|
||
| def get_raw_camera_metadata(self) -> dict[str, dict[str, Any]]: | ||
| """Describe the complete, uncropped snapshot and supported calibration.""" | ||
| if self.config.is_dummy: | ||
| return {} | ||
| snapshot = self._rpent_camera_snapshot | ||
| if snapshot is None: | ||
| raise RuntimeError( | ||
| "No fresh complete camera snapshot; acquire an observation first." | ||
| ) | ||
| metadata = {} | ||
| for name, camera in self._cameras.items(): | ||
| info = camera.camera_info | ||
| depth = snapshot["raw_depths"].get(name) | ||
| intrinsics = ( | ||
| camera.get_color_intrinsics() | ||
| if isinstance(camera, RPentRealSenseCamera) | ||
| else None | ||
| ) | ||
| frame = snapshot["raw_frames"][name] | ||
| height, width = frame.shape[:2] | ||
| metadata[name] = { | ||
| "name": name, | ||
| "serial_number": info.serial_number, | ||
| "camera_type": "realsense" | ||
| if info.camera_type == _REALSENSE_BACKEND | ||
| else info.camera_type, | ||
| "raw_resolution": [width, height], | ||
| "output_resolution": [width, height], | ||
| "crop_bounds_xyxy": [0, 0, width, height], | ||
| "crop_region": None, | ||
| "depth_scale": float(camera.depth_scale), | ||
| "depth_unit": "m" if depth is not None else None, | ||
| "depth_aligned_to_color": bool(info.enable_depth), | ||
| "extrinsic_cam2base": None, | ||
| "extrinsic_cam2ee": None, | ||
| "raw_color_intrinsics": intrinsics, | ||
| "intrinsic_K": ( | ||
| [ | ||
| [intrinsics["fx"], 0.0, intrinsics["ppx"]], | ||
| [0.0, intrinsics["fy"], intrinsics["ppy"]], | ||
| [0.0, 0.0, 1.0], | ||
| ] | ||
| if intrinsics is not None | ||
| else None | ||
| ), | ||
| "depth_enabled": bool(info.enable_depth), | ||
| "depth_available": depth is not None, | ||
| "projection_supported": depth is not None and intrinsics is not None, | ||
| "rgb_shape": list(frame.shape), | ||
| "depth_shape": None if depth is None else list(depth.shape), | ||
| } | ||
| return metadata | ||
|
|
||
| def get_raw_camera_snapshot(self) -> dict[str, Any]: | ||
| """Copy the preceding acquisition, or return empty physical views in dummy mode.""" | ||
| if self.config.is_dummy: | ||
| return {"raw_frames": {}, "raw_depths": {}, "camera_meta": {}} | ||
| metadata = self.get_raw_camera_metadata() | ||
| return { | ||
| **copy.deepcopy(self._rpent_camera_snapshot), | ||
| "camera_meta": metadata, | ||
| } | ||
|
|
||
|
|
||
| def create_rpent_dual_franka_env( | ||
| override_cfg: dict, | ||
| worker_info: object, | ||
| robot_info: object, | ||
| env_idx: int, | ||
| env_cfg: dict, | ||
| ) -> gym.Env: | ||
| """Create the dual-Franka adapter with RLinf's standard wrapper stack.""" | ||
| env = RPentDualFrankaEnv( | ||
| override_cfg=override_cfg, | ||
| worker_info=worker_info, | ||
| robot_info=robot_info, | ||
| env_idx=env_idx, | ||
| ) | ||
| try: | ||
| return build_stack(env, env_cfg) | ||
| except BaseException: | ||
| env.close() | ||
| raise | ||
|
|
||
|
|
||
| def register_rpent_dual_franka_env() -> None: | ||
| """Register the RPent dual-Franka adapter with Gymnasium.""" | ||
| if "RPentDualFrankaTCPEnv-v1" not in gym.registry: | ||
| register( | ||
| id="RPentDualFrankaTCPEnv-v1", | ||
| entry_point="robots.dual_franka.rpent_env:create_rpent_dual_franka_env", | ||
| ) |
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.