Instructions to use Bigenlight/banana_in_pot_hilserl with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LeRobot
How to use Bigenlight/banana_in_pot_hilserl with LeRobot:
- Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- HILSERL_RUNBOOK.md +183 -0
- README.md +185 -0
- assets/confusion_matrix.png +0 -0
- assets/fk_validation.png +3 -0
- assets/training_curves.png +0 -0
- build_offline_buffer.py +335 -0
- config/train_hilserl_ur7e.json +196 -0
- joint_to_ee.py +235 -0
- reward_classifier/checkpoint/config.json +45 -0
- reward_classifier/checkpoint/model.safetensors +3 -0
- ur7e.urdf +297 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/fk_validation.png filter=lfs diff=lfs merge=lfs -text
|
HILSERL_RUNBOOK.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HIL-SERL Online Training Runbook — "put right banana in the pot" (UR7e)
|
| 2 |
+
|
| 3 |
+
Everything offline is already built and frozen (see `HILSERL_PREP_RESULTS.md`). This runbook is
|
| 4 |
+
the exact sequence to start **online** HIL-SERL actor–learner training once the **UR7e** is
|
| 5 |
+
physically connected. All robot-only steps are marked **[ROBOT]**.
|
| 6 |
+
|
| 7 |
+
- **Shared train config (learner + actor):**
|
| 8 |
+
`hilserl/config/train_hilserl_ur7e.json`
|
| 9 |
+
- **Offline demo buffer:** `theo/banana_in_pot_rl` at `hilserl/banana_rl_lerobot` (51 ep / 21 524 frames)
|
| 10 |
+
- **Reward classifier:** `hilserl/reward_classifier/checkpoint` (`success_threshold=0.7`)
|
| 11 |
+
- **UR7e URDF (pre-generated):** `hilserl/ur7e.urdf` (IK frame `tool0`)
|
| 12 |
+
- **venv:** `lr_env/bin/python` · env vars: `HF_HUB_OFFLINE=1`, `TORCH_HOME=scratchpad/torch_home`,
|
| 13 |
+
`HF_LEROBOT_HOME=<root>/hilserl`
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 0. Robot-PC installs (one-time) **[ROBOT]**
|
| 18 |
+
|
| 19 |
+
The offline env (`lr_env`) is missing the online/transport/hardware deps on purpose. On the machine
|
| 20 |
+
that will talk to the arm and run the actor+learner, install:
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
# gRPC transport (actor<->learner). NOTE: the offline uv cache only has a cp310 wheel; you
|
| 24 |
+
# need a wheel matching lr_env's Python (3.12). Requires network.
|
| 25 |
+
uv pip install --python lr_env/bin/python 'lerobot[grpcio-dep]' # grpcio
|
| 26 |
+
|
| 27 |
+
# HIL-SERL extra: gym-hil + placo (IK) + transformers, etc.
|
| 28 |
+
uv pip install --python lr_env/bin/python 'lerobot[hilserl]' # pulls placo, gym-hil
|
| 29 |
+
|
| 30 |
+
# placo (IK solver, required by lerobot/model/kinematics.py:59). If not pulled above:
|
| 31 |
+
uv pip install --python lr_env/bin/python placo
|
| 32 |
+
|
| 33 |
+
# ur_rtde — UR robot I/O. NOT installed anywhere yet; required for the UR robot driver.
|
| 34 |
+
uv pip install --python lr_env/bin/python ur_rtde
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Verify: `lr_env/bin/python -c "import grpc, placo, rtde_control; print('ok')"`.
|
| 38 |
+
|
| 39 |
+
> Why these are not already installed: they are hardware/transport-only. The entire offline prep
|
| 40 |
+
> (buffer, classifier, EE conversion, config, dry-run) was completed without them. The learner
|
| 41 |
+
> module import fails fast at `lerobot/transport/__init__.py:27` (`require_package("grpcio")`) until
|
| 42 |
+
> grpcio is present — this is the only thing blocking a full end-to-end module launch offline.
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## 1. (Already done, re-verify) UR7e URDF
|
| 47 |
+
|
| 48 |
+
`hilserl/ur7e.urdf` is already generated and referenced by the config
|
| 49 |
+
(`env.processor.inverse_kinematics.urdf_path`). To regenerate:
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
source /opt/ros/jazzy/setup.bash
|
| 53 |
+
xacro /opt/ros/jazzy/share/ur_description/urdf/ur.urdf.xacro \
|
| 54 |
+
ur_type:=ur7e name:=ur7e > hilserl/ur7e.urdf
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
IK target frame = `tool0` (offset-free flange; Agent D validated recorded TCP == flange). If you
|
| 58 |
+
mount a gripper whose TCP differs, add a `gripper_frame_link` to the URDF and set
|
| 59 |
+
`env.processor.inverse_kinematics.target_frame_name` accordingly.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## 2. Fill the robot + teleop blocks in the config **[ROBOT]**
|
| 64 |
+
|
| 65 |
+
In `hilserl/config/train_hilserl_ur7e.json`, `env.robot` and `env.teleop` are currently `null`
|
| 66 |
+
(robot-free dry-run). Set them for the live arm, e.g.:
|
| 67 |
+
|
| 68 |
+
```jsonc
|
| 69 |
+
"robot": { "type": "<ur_follower_type>", "ip": "<UR7e_IP>", /* motors, cameras cam1/cam2 */ },
|
| 70 |
+
"teleop": { "type": "gamepad", "use_gripper": true }
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
`motor_names = list(env.robot.bus.motors.keys())` feeds the kinematics solver
|
| 74 |
+
(gym_manipulator.py:414-420), so the robot config's motor order must match the 6 UR joints + gripper.
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## 3. Tune the workspace-specific values against the real arm **[ROBOT]**
|
| 79 |
+
|
| 80 |
+
These are **TODO placeholders** in the config — draft values are in there, but they MUST be verified
|
| 81 |
+
with the arm before letting the policy move:
|
| 82 |
+
|
| 83 |
+
| Field | Config path | Current placeholder | How to tune |
|
| 84 |
+
|---|---|---|---|
|
| 85 |
+
| EE safety bounds | `env.processor.inverse_kinematics.end_effector_bounds` | `min[-0.6,-0.6,0.0] max[0.6,0.6,0.6]` (m, base frame) | Jog the arm to the reachable corners of the banana/pot workspace; set a tight box that contains the task but clamps runaways. |
|
| 86 |
+
| Reset pose | `env.processor.reset.fixed_reset_joint_positions` | `[3.05,-1.60,1.90,-1.85,-1.55,-3.30,0.02]` (rad, from dataset joint-range mid) | Set to a safe, repeatable pre-grasp home; verify the arm returns there each episode. |
|
| 87 |
+
| EE step sizes | `env.processor.inverse_kinematics.end_effector_step_sizes` | `{x:0.05,y:0.05,z:0.05}` (m) | **Keep at 0.05** — the offline action = TCP-delta ÷ 0.05, so changing this desyncs the demo actions. |
|
| 88 |
+
| Episode length | `env.processor.reset.control_time_s` | `20.0` s (→ `max_episode_steps = 20*30 = 600`) | Match a comfortable single-attempt duration. |
|
| 89 |
+
|
| 90 |
+
The reward classifier decision boundary around the release moment is uncalibrated (Agent B caveat);
|
| 91 |
+
`success_threshold=0.7` adds margin. Consider recording a few real **failure/near-miss** episodes
|
| 92 |
+
early to harden it.
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## 4. (Optional) Crop ROI — only if you crop online **[ROBOT]**
|
| 97 |
+
|
| 98 |
+
The offline buffer images are **full-frame resized to 128×128, no crop**
|
| 99 |
+
(`image_preprocessing.resize_size=[128,128]`, `crop_params_dict=null`). If you decide to crop online
|
| 100 |
+
to focus on the workspace, you MUST keep offline and online identical:
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
# 1) find the ROI interactively on a recorded dataset
|
| 104 |
+
lr_env/bin/python -m lerobot.rl.crop_dataset_roi --repo-id theo/banana_in_pot_rl
|
| 105 |
+
# 2) put the returned crop_params_dict into env.processor.image_preprocessing.crop_params_dict
|
| 106 |
+
# 3) RE-RUN the crop on the OFFLINE buffer too (hilserl/banana_rl_lerobot) so the demo images
|
| 107 |
+
# match the online cropped+resized size — otherwise the encoder sees two different distributions.
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
If you do not crop, skip this entirely (the default is consistent already).
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
## 5. Start the LEARNER (terminal 1) **[ROBOT]**
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
export HF_HUB_OFFLINE=1
|
| 118 |
+
export TORCH_HOME=<root>/scratchpad/torch_home
|
| 119 |
+
export HF_LEROBOT_HOME=<root>/hilserl
|
| 120 |
+
lr_env/bin/python -m lerobot.rl.learner \
|
| 121 |
+
--config_path hilserl/config/train_hilserl_ur7e.json
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
The learner: builds the SAC policy + critics, loads the offline demo buffer via
|
| 125 |
+
`ReplayBuffer.from_lerobot_dataset`, opens a gRPC server on `127.0.0.1:50051`, then **idles at the
|
| 126 |
+
online-buffer gate** (`learner.py:412-413`) until the actor sends transitions. This idle state was
|
| 127 |
+
validated offline (see §7 / `HILSERL_PREP_RESULTS.md`).
|
| 128 |
+
|
| 129 |
+
> **RAM WARNING — full offline buffer ≈ 25 GB host RAM.**
|
| 130 |
+
> `from_lerobot_dataset` eagerly materializes **every** transition (decoded float32 state +
|
| 131 |
+
> next-state images) into a Python list before filling storage; the full 21 524-frame ×
|
| 132 |
+
> 2×(3×128×128) set peaks at **~25 GB** and was OOM-killed on the 31 GB box (Agent C). Storage
|
| 133 |
+
> itself at `offline_buffer_capacity=25000` adds ~9.8 GB (`optimize_memory=True` aliases next_state).
|
| 134 |
+
> **Mitigations** (pick one):
|
| 135 |
+
> - Run on a machine with **≥ 48 GB RAM** (or add swap).
|
| 136 |
+
> - Subset the demos: add `--dataset.episodes='[0,1,...,N]'` (fewer episodes → proportional RAM).
|
| 137 |
+
> - Lower `policy.offline_buffer_capacity` toward the frame count you actually load.
|
| 138 |
+
|
| 139 |
+
---
|
| 140 |
+
|
| 141 |
+
## 6. Start the ACTOR (terminal 2, same config) **[ROBOT]**
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
export HF_HUB_OFFLINE=1 TORCH_HOME=<root>/scratchpad/torch_home HF_LEROBOT_HOME=<root>/hilserl
|
| 145 |
+
lr_env/bin/python -m lerobot.rl.actor \
|
| 146 |
+
--config_path hilserl/config/train_hilserl_ur7e.json
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
The actor connects to the learner over gRPC, opens cam1/cam2, builds the EE-delta → IK action
|
| 150 |
+
pipeline (`MapTensorToDeltaActionDict → MapDeltaActionToRobotAction → EEReferenceAndDelta →
|
| 151 |
+
EEBoundsAndSafety → GripperVelocityToJoint → InverseKinematicsRLStep`), runs the policy on the arm,
|
| 152 |
+
and streams transitions back. Once ≥ `online_step_before_learning` (100) online transitions arrive,
|
| 153 |
+
the learner starts SAC updates with a 50/50 online/offline RLPD mix (`online_ratio=0.5`).
|
| 154 |
+
|
| 155 |
+
---
|
| 156 |
+
|
| 157 |
+
## 7. Human-in-the-loop interventions **[ROBOT]**
|
| 158 |
+
|
| 159 |
+
- Press the **upper-right trigger** on the gamepad (or **`space`** on the keyboard) to take over and
|
| 160 |
+
provide a corrective demonstration; release to hand control back to the policy.
|
| 161 |
+
- Intervene heavily at the start, then taper — a healthy run shows the intervention rate dropping as
|
| 162 |
+
the policy improves (watch it in wandb if `wandb.enable=true`).
|
| 163 |
+
- The success reward comes from the vision classifier (`reward=1` when `prob>0.7`); with
|
| 164 |
+
`terminate_on_success=true` the episode ends on the first success frame.
|
| 165 |
+
|
| 166 |
+
---
|
| 167 |
+
|
| 168 |
+
## 8. Key hyperparameters to tune (config: `hilserl/config/train_hilserl_ur7e.json`)
|
| 169 |
+
|
| 170 |
+
- `algorithm.temperature_init` (SAC entropy temp) — start `0.01`; too high makes interventions
|
| 171 |
+
ineffective.
|
| 172 |
+
- `policy.actor_learner_config.policy_parameters_push_frequency` — seconds between weight pushes
|
| 173 |
+
(default 4; drop to 1–2 for fresher actor weights).
|
| 174 |
+
- `policy.storage_device` — keep `"cpu"` here (12 GB GPU can't hold the offline image buffer). Set
|
| 175 |
+
`"cuda"` only if you move to a big-VRAM box.
|
| 176 |
+
- `algorithm.utd_ratio` (2) / `algorithm.num_critics` (2) — raise UTD for more updates per step.
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
### Offline validation already passed (no robot)
|
| 181 |
+
`hilserl/config/dryrun_validate_learner.py` reproduced the learner setup path (config parse →
|
| 182 |
+
`make_policy` → `make_algorithm` (SAC) → offline buffer load+sample → gate). It reached the idle gate
|
| 183 |
+
`len(online_buffer)=0 < online_step_before_learning=100`. Full evidence in `HILSERL_PREP_RESULTS.md`.
|
README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: lerobot
|
| 4 |
+
tags:
|
| 5 |
+
- hil-serl
|
| 6 |
+
- reinforcement-learning
|
| 7 |
+
- sac
|
| 8 |
+
- rlpd
|
| 9 |
+
- robotics
|
| 10 |
+
- ur7e
|
| 11 |
+
- lerobot
|
| 12 |
+
pipeline_tag: robotics
|
| 13 |
+
datasets:
|
| 14 |
+
- Bigenlight/banana_in_pot
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
# HIL-SERL offline prep bundle — Put the right banana in the pot (UR7e)
|
| 18 |
+
|
| 19 |
+
This repository is the **offline-prepared base for HIL-SERL** (Human-in-the-Loop Sample-Efficient
|
| 20 |
+
Robotic reinforcement Learning) on the *"put the right banana in the pot"* task with a
|
| 21 |
+
**Universal Robots UR7e**. It contains **everything up to online RL** — the vision reward
|
| 22 |
+
classifier, the SAC/RLPD training config, the UR7e kinematics (URDF + FK), the offline demo
|
| 23 |
+
buffer builder, and the runbook. The **online phase itself is robot-only** and is not included
|
| 24 |
+
here (it requires the physical arm plus `ur_rtde` + `placo` + a gRPC actor/learner).
|
| 25 |
+
|
| 26 |
+
Built from [`Bigenlight/banana_in_pot`](https://huggingface.co/datasets/Bigenlight/banana_in_pot)
|
| 27 |
+
(51 teleop demos / 21,524 frames) using [LeRobot](https://github.com/huggingface/lerobot)'s
|
| 28 |
+
HIL-SERL stack.
|
| 29 |
+
|
| 30 |
+
## Data & hardware setup
|
| 31 |
+
|
| 32 |
+
| Component | Detail |
|
| 33 |
+
|---|---|
|
| 34 |
+
| Robot | **Universal Robots UR7e** — 6-DOF collaborative arm, joints in radians. |
|
| 35 |
+
| Teleoperation (demos) | **GELLO** low-cost 3D-printed leader arm. |
|
| 36 |
+
| Camera 1 | **Intel RealSense D435** — RGB only |
|
| 37 |
+
| Camera 2 | **Intel RealSense D435if** — RGB only |
|
| 38 |
+
| Camera streams | 1280×720 @ 30 fps, color only (**no depth / IR**). HIL-SERL uses the two RGB views **resized to 128×128**. |
|
| 39 |
+
| Task | *"put the right banana in the pot"* — distractors + silver pot; success = correct banana in the pot. |
|
| 40 |
+
|
| 41 |
+
## What's in this bundle
|
| 42 |
+
|
| 43 |
+
```
|
| 44 |
+
banana_in_pot_hilserl/
|
| 45 |
+
├── reward_classifier/
|
| 46 |
+
│ └── checkpoint/ # trained success classifier (config.json + model.safetensors)
|
| 47 |
+
├── config/
|
| 48 |
+
│ └── train_hilserl_ur7e.json # shared SAC/RLPD learner+actor config (paths relative)
|
| 49 |
+
├── build_offline_buffer.py # rebuilds the 54 MB RL demo buffer from the dataset
|
| 50 |
+
├── joint_to_ee.py # UR7e forward kinematics (Joint → EE), placo-free, validated
|
| 51 |
+
├── ur7e.urdf # pre-generated UR7e URDF (IK target frame `tool0`)
|
| 52 |
+
├── HILSERL_RUNBOOK.md # exact online start sequence (robot-only steps marked [ROBOT])
|
| 53 |
+
└── assets/ # figures embedded below
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
> **Offline demo buffer is NOT shipped** (it is ~54 MB of video/state transitions). Rebuild it
|
| 57 |
+
> from the dataset with `build_offline_buffer.py` — see [Rebuilding the demo buffer](#rebuilding-the-offline-demo-buffer).
|
| 58 |
+
|
| 59 |
+
## 1. Reward / success classifier
|
| 60 |
+
|
| 61 |
+
A vision-based binary success detector trained offline, in the exact LeRobot
|
| 62 |
+
`reward_classifier` format so it drops straight into HIL-SERL.
|
| 63 |
+
|
| 64 |
+
- **Encoder:** frozen `lerobot/resnet10` (CNN), per-camera `SpatialLearnedEmbeddings` pool +
|
| 65 |
+
Linear→LayerNorm→Tanh. Following the official LeRobot behavior, the encoder runs under
|
| 66 |
+
`no_grad` so **only the classifier head trains** (~2.36 M trainable params).
|
| 67 |
+
- **Cameras:** `observation.images.cam1` + `observation.images.cam2`, 128×128, MEAN_STD-normalized.
|
| 68 |
+
- **Labeling** (all 51 demos are successes, so negatives are synthesized): POSITIVE = last 15%
|
| 69 |
+
of frames **and** gripper re-opened (release into pot); NEGATIVE = first 55% of frames; the
|
| 70 |
+
55–85% transport margin is excluded. Split **by episode** (val = eps [4,14,24,34,44]).
|
| 71 |
+
- **Deployed with `success_threshold = 0.7`** for release-boundary margin.
|
| 72 |
+
|
| 73 |
+
| metric | value |
|
| 74 |
+
|---|---|
|
| 75 |
+
| Val accuracy | **99.49%** |
|
| 76 |
+
| Val precision / recall / F1 | 0.979 / 0.991 / 0.985 |
|
| 77 |
+
| Train accuracy (balanced) | 100% |
|
| 78 |
+
| Confusion (val, thr 0.5) | TP=231, TN=1138, FP=5, FN=2 |
|
| 79 |
+
|
| 80 |
+

|
| 81 |
+
|
| 82 |
+

|
| 83 |
+
|
| 84 |
+
## 2. Action spec — EE-delta
|
| 85 |
+
|
| 86 |
+
The demos are absolute joints, but the HIL-SERL policy acts in **end-effector delta** space.
|
| 87 |
+
|
| 88 |
+
- **Action = base-frame TCP delta ÷ step_size (0.05 m)**, computed by running **FK on the dataset
|
| 89 |
+
joints** (identical to what online deploy uses as its per-step reference). The gripper is a
|
| 90 |
+
discrete class `{0=close, 1=stay, 2=open}`.
|
| 91 |
+
- The offline buffer stores a 4-dim action (continuous xyz + discrete gripper); the SAC critic
|
| 92 |
+
slices `actions[:, :-1]` for the continuous part and a separate discrete critic handles the
|
| 93 |
+
gripper. Hence `output_features["action"].shape = [3]` with `num_discrete_actions = 3`.
|
| 94 |
+
- Action stats (21,524 frames): p99 ≈ 0.2 in tanh space, `|Δ|>1` = **0.0%** — comfortably inside
|
| 95 |
+
the `[-1, 1]` range.
|
| 96 |
+
|
| 97 |
+
### Joint → EE forward kinematics (validated)
|
| 98 |
+
|
| 99 |
+
`joint_to_ee.py` implements UR7e FK directly from the URDF joint origins (placo-free) and was
|
| 100 |
+
validated against the recorded TCP pose:
|
| 101 |
+
|
| 102 |
+
| subset | pos err median | rot err median |
|
| 103 |
+
|---|---|---|
|
| 104 |
+
| near-static (‖q̇‖<0.02, n=1884) | **0.85 mm** | **0.16°** |
|
| 105 |
+
| all samples (n=42,833) | 28.0 mm | — |
|
| 106 |
+
|
| 107 |
+
Sub-mm / sub-0.2° error while static confirms the kinematic chain is accurate; the larger
|
| 108 |
+
moving-sample error is timing jitter between the two async logging streams (joint vs. TCP),
|
| 109 |
+
**not** an FK error. Online deploy IK uses `placo` (Pinocchio), frame `tool0`.
|
| 110 |
+
|
| 111 |
+

|
| 112 |
+
|
| 113 |
+
## 3. Training config (SAC + RLPD)
|
| 114 |
+
|
| 115 |
+
`config/train_hilserl_ur7e.json` is the shared `TrainRLServerPipelineConfig` for the learner and
|
| 116 |
+
actor, fully consistent with the offline buffer:
|
| 117 |
+
|
| 118 |
+
| block | key values |
|
| 119 |
+
|---|---|
|
| 120 |
+
| `algorithm` (SAC) | `num_critics=2`, `utd_ratio=2`, `discount=0.99`, `temperature_init=0.01`, `grad_clip_norm=10` |
|
| 121 |
+
| mixer | `online_offline`, `online_ratio=0.5` (RLPD 50/50 online/offline) |
|
| 122 |
+
| `policy` (gaussian_actor) | `vision_encoder=lerobot/resnet10` (frozen), `num_discrete_actions=3`, `online_step_before_learning=100`, `storage_device=cpu` |
|
| 123 |
+
| input features | state[7] + cam1[3,128,128] + cam2[3,128,128] |
|
| 124 |
+
| output features | `action[3]` (continuous xyz; gripper via discrete head) |
|
| 125 |
+
| `env` (gym_manipulator) | `resize_size=[128,128]`, EE `step_sizes=0.05`, IK `urdf=ur7e.urdf`/`tool0`, `reward_classifier` thr 0.7, `robot`/`teleop = null` |
|
| 126 |
+
|
| 127 |
+
The learner setup was validated end-to-end offline: `python -m lerobot.rl.learner` builds the SAC
|
| 128 |
+
policy (2.76 M trainable / 7.67 M total params), loads the offline demo buffer via
|
| 129 |
+
`ReplayBuffer.from_lerobot_dataset`, starts its gRPC server, and **idles at the online-buffer gate**
|
| 130 |
+
(`len(online_buffer) < online_step_before_learning=100`) waiting for the actor — which is exactly
|
| 131 |
+
the "ready for online RL" state.
|
| 132 |
+
|
| 133 |
+
## 4. The online phase (robot-only, NOT in this repo)
|
| 134 |
+
|
| 135 |
+
See `HILSERL_RUNBOOK.md` for the exact sequence. In short, on the machine wired to the arm:
|
| 136 |
+
|
| 137 |
+
1. Install the hardware/transport deps that the offline env intentionally omits: `grpcio`
|
| 138 |
+
(py3.12 wheel), `placo` (IK), `ur_rtde` (UR I/O), `lerobot[hilserl]`.
|
| 139 |
+
2. Fill `env.robot` / `env.teleop` in the config (UR7e IP + motors + cameras; gamepad teleop).
|
| 140 |
+
3. Tune the workspace placeholders against the real arm: `end_effector_bounds`,
|
| 141 |
+
`fixed_reset_joint_positions`, episode length. **Keep `end_effector_step_sizes = 0.05`** — the
|
| 142 |
+
offline action = TCP-delta ÷ 0.05, so changing it desyncs the demo actions.
|
| 143 |
+
4. Start the **learner** (terminal 1) and **actor** (terminal 2) with the same config. The learner
|
| 144 |
+
idles at the gate until the actor supplies ≥100 online transitions, then SAC updates begin with
|
| 145 |
+
the 50/50 RLPD mix.
|
| 146 |
+
5. **Human interventions:** gamepad trigger / `space` to take over and give corrective demos; taper
|
| 147 |
+
the intervention rate as the policy improves.
|
| 148 |
+
|
| 149 |
+
> **RAM note:** the full offline buffer (`from_lerobot_dataset`) materializes every transition
|
| 150 |
+
> eagerly and peaks at **~25 GB**. Use ≥48 GB RAM, or subset episodes via
|
| 151 |
+
> `--dataset.episodes='[...]'`, or lower `offline_buffer_capacity`.
|
| 152 |
+
|
| 153 |
+
## Rebuilding the offline demo buffer
|
| 154 |
+
|
| 155 |
+
The 54 MB RL demo buffer is not shipped. Rebuild it from the LeRobot dataset with the included
|
| 156 |
+
script (it converts absolute-joint demos to the EE-delta + reward + done schema):
|
| 157 |
+
|
| 158 |
+
```bash
|
| 159 |
+
python build_offline_buffer.py # writes ./banana_rl_lerobot (repo_id theo/banana_in_pot_rl)
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
Then point `dataset.root` in `config/train_hilserl_ur7e.json` at the rebuilt directory (the config
|
| 163 |
+
ships with a relative `./banana_rl_lerobot`).
|
| 164 |
+
|
| 165 |
+
## Related repositories
|
| 166 |
+
|
| 167 |
+
- **Dataset:** [`Bigenlight/banana_in_pot`](https://huggingface.co/datasets/Bigenlight/banana_in_pot)
|
| 168 |
+
- **ACT imitation-learning policy** for the same task:
|
| 169 |
+
[`Bigenlight/act_banana_in_pot`](https://huggingface.co/Bigenlight/act_banana_in_pot)
|
| 170 |
+
|
| 171 |
+
## Caveats & limitations
|
| 172 |
+
|
| 173 |
+
- **No true failure episodes.** The classifier's negatives are *early-task* frames (approach/grasp),
|
| 174 |
+
not genuine failed attempts. It reliably separates "task complete" from "task in progress" but has
|
| 175 |
+
never seen a real failure of a completed-looking state — expect over-confidence on OOD near-miss
|
| 176 |
+
end states. **Record a handful of real failure/near-miss episodes early in online HIL-SERL** to
|
| 177 |
+
harden it.
|
| 178 |
+
- The excluded 55–85% transport margin means the **decision boundary around the release moment is
|
| 179 |
+
uncalibrated**; `success_threshold=0.7` adds margin.
|
| 180 |
+
- **EE safety bounds and reset pose in the config are placeholders** — they MUST be tuned against
|
| 181 |
+
the real arm before letting the policy move.
|
| 182 |
+
- Only the classifier head trains (frozen-encoder LeRobot quirk) — fine for this easy visual task,
|
| 183 |
+
the first thing to revisit if a harder task underperforms.
|
| 184 |
+
- All validation here is **offline** (config parse → policy/critic build → buffer load → gate). No
|
| 185 |
+
online RL results are included; that is the robot-only next step.
|
assets/confusion_matrix.png
ADDED
|
assets/fk_validation.png
ADDED
|
Git LFS Details
|
assets/training_curves.png
ADDED
|
build_offline_buffer.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""
|
| 3 |
+
build_offline_buffer.py (Agent C)
|
| 4 |
+
|
| 5 |
+
Convert the teleop demo dataset `banana_in_pot_lerobot` into the HIL-SERL OFFLINE
|
| 6 |
+
DEMO BUFFER: a standard LeRobot v3 dataset that already carries the RL columns the
|
| 7 |
+
SAC learner ingests via ReplayBuffer.from_lerobot_dataset:
|
| 8 |
+
|
| 9 |
+
observation.state float32 [7] (ur_q1..6 + grip_pos) -- MATCHES online obs
|
| 10 |
+
observation.images.cam1 video 3x128x128 (full-frame, resized)
|
| 11 |
+
observation.images.cam2 video 3x128x128
|
| 12 |
+
action float32 [4] [delta_x, delta_y, delta_z, gripper]
|
| 13 |
+
next.reward float32 [1] (Agent B success classifier, thr=0.7)
|
| 14 |
+
next.done bool [1] (success onset OR episode end)
|
| 15 |
+
|
| 16 |
+
WHY these choices (citations in hilserl/rl_dataset_schema.json + notes md):
|
| 17 |
+
- action = base-frame TCP displacement / end_effector_step_sizes (Agent D rule).
|
| 18 |
+
We derive the TCP position via FORWARD KINEMATICS of the dataset's OWN joint state
|
| 19 |
+
(observation.state[:6]), NOT the raw-h5 tcp_pose. Reason: (a) the online deploy
|
| 20 |
+
reference is `FK(current joints)` recomputed every step (EEReferenceAndDelta,
|
| 21 |
+
use_latched_reference=False, gym_manipulator.py:507), so FK(joints) is exactly the
|
| 22 |
+
quantity whose per-step delta the policy must reproduce; (b) it is perfectly
|
| 23 |
+
frame-aligned to the images/state the policy observes (no async-stream matching,
|
| 24 |
+
no episode->take mapping). Agent D validated FK == recorded tcp_pose to 0.85 mm at
|
| 25 |
+
rest across all 51 takes, so this is equivalent to the recorded TCP.
|
| 26 |
+
- gripper action = discrete class {0=close, 1=stay, 2=open} from grip_pos transitions
|
| 27 |
+
(robot_kinematic_processor.py:408-412 semantics; grip_cmd is NaN-prone so grip_pos).
|
| 28 |
+
- observation.state kept as the raw 7-d joint state: the online env base observation is
|
| 29 |
+
`agent_pos` = motor-bus joint positions (gym_manipulator.py:173-177) with all
|
| 30 |
+
ObservationConfig add_* flags defaulting False (envs/configs.py:266-268) -> 7-dim.
|
| 31 |
+
- images 3x128x128 to match Agent B's reward classifier (trained at 128x128); the
|
| 32 |
+
online env must set image_preprocessing.resize_size=[128,128] to match (documented).
|
| 33 |
+
"""
|
| 34 |
+
from __future__ import annotations
|
| 35 |
+
|
| 36 |
+
import json
|
| 37 |
+
import os
|
| 38 |
+
import sys
|
| 39 |
+
|
| 40 |
+
import numpy as np
|
| 41 |
+
import torch
|
| 42 |
+
import torch.nn.functional as F
|
| 43 |
+
|
| 44 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 45 |
+
ROOT = os.path.dirname(HERE)
|
| 46 |
+
sys.path.insert(0, HERE)
|
| 47 |
+
|
| 48 |
+
from lerobot.datasets.lerobot_dataset import LeRobotDataset # noqa: E402
|
| 49 |
+
from lerobot.rewards.classifier.modeling_classifier import Classifier # noqa: E402
|
| 50 |
+
|
| 51 |
+
# ---------------------------------------------------------------- config
|
| 52 |
+
SRC_ROOT = os.path.join(ROOT, "banana_in_pot_lerobot")
|
| 53 |
+
OUT_ROOT = os.path.join(HERE, "banana_rl_lerobot")
|
| 54 |
+
REPO_ID = "theo/banana_in_pot_rl"
|
| 55 |
+
CKPT = os.path.join(HERE, "reward_classifier", "checkpoint")
|
| 56 |
+
|
| 57 |
+
STEP_SIZE = 0.05 # metres per unit; end_effector_step_sizes x=y=z
|
| 58 |
+
SUCCESS_THRESHOLD = 0.7 # Agent B caveat
|
| 59 |
+
SUCCESS_REWARD = 1.0
|
| 60 |
+
IMG = 128
|
| 61 |
+
GRIP_EPS = 0.03 # deadzone on Δgrip_pos for discrete gripper class
|
| 62 |
+
GRIPPER_CLOSE, GRIPPER_STAY, GRIPPER_OPEN = 0.0, 1.0, 2.0
|
| 63 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 64 |
+
|
| 65 |
+
# ---------------------------------------------------------------- UR7e FK
|
| 66 |
+
# scipy-free re-implementation of Agent D's validated FK (hilserl/joint_to_ee.py):
|
| 67 |
+
# same default_kinematics.yaml joint origins, same RPY(xyz, URDF/extrinsic) + Rz(theta)
|
| 68 |
+
# chain shoulder->wrist_3, base_link frame, no tool offset (recorded TCP == flange).
|
| 69 |
+
import yaml # noqa: E402
|
| 70 |
+
|
| 71 |
+
_KIN_YAML = "/opt/ros/jazzy/share/ur_description/config/ur7e/default_kinematics.yaml"
|
| 72 |
+
_LINK_ORDER = ["shoulder", "upper_arm", "forearm", "wrist_1", "wrist_2", "wrist_3"]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _Rx(a):
|
| 76 |
+
c, s = np.cos(a), np.sin(a); return np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _Ry(a):
|
| 80 |
+
c, s = np.cos(a), np.sin(a); return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _Rz(a):
|
| 84 |
+
c, s = np.cos(a), np.sin(a); return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _rpyxyz_to_T(p):
|
| 88 |
+
T = np.eye(4)
|
| 89 |
+
T[:3, :3] = _Rz(p["yaw"]) @ _Ry(p["pitch"]) @ _Rx(p["roll"]) # URDF RPY = extrinsic xyz
|
| 90 |
+
T[:3, 3] = [p["x"], p["y"], p["z"]]
|
| 91 |
+
return T
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
with open(_KIN_YAML) as _f:
|
| 95 |
+
_KIN = yaml.safe_load(_f)["kinematics"]
|
| 96 |
+
_FIXED = [_rpyxyz_to_T(_KIN[k]) for k in _LINK_ORDER]
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _Rz4(a):
|
| 100 |
+
T = np.eye(4); T[:3, :3] = _Rz(a); return T
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def fk_batch(q_rad_seq):
|
| 104 |
+
"""(N,6) joint angles [rad] -> (N,4,4) TCP pose in base_link frame."""
|
| 105 |
+
out = np.zeros((len(q_rad_seq), 4, 4))
|
| 106 |
+
for n, q in enumerate(np.asarray(q_rad_seq, dtype=float)):
|
| 107 |
+
T = np.eye(4)
|
| 108 |
+
for i in range(6):
|
| 109 |
+
T = T @ _FIXED[i] @ _Rz4(q[i])
|
| 110 |
+
out[n] = T
|
| 111 |
+
return out
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def resize_uint8_hwc(chw_float01: torch.Tensor) -> np.ndarray:
|
| 115 |
+
"""(3,H,W) float[0,1] -> (128,128,3) uint8 HWC (matches Agent B cache + LeRobot video convention)."""
|
| 116 |
+
x = F.interpolate(chw_float01.unsqueeze(0), size=(IMG, IMG), mode="bilinear", align_corners=False)
|
| 117 |
+
return (x[0].permute(1, 2, 0).clamp(0, 1).numpy() * 255).astype(np.uint8)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def norm_cam(uint8_hwc: np.ndarray, mean: torch.Tensor, std: torch.Tensor) -> torch.Tensor:
|
| 121 |
+
"""(N,128,128,3) uint8 -> (N,3,128,128) MEAN_STD-normalized (exactly Agent B inference)."""
|
| 122 |
+
t = torch.from_numpy(uint8_hwc.astype(np.float32) / 255.0).permute(0, 3, 1, 2)
|
| 123 |
+
return (t - mean[None, :, None, None]) / std[None, :, None, None]
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def discrete_gripper(grip: np.ndarray) -> np.ndarray:
|
| 127 |
+
"""grip_pos (N,) -> discrete gripper command per frame (N,).
|
| 128 |
+
grip_pos increases when closing (grasp ~0.55), decreases when opening (~0.01).
|
| 129 |
+
Online: 0=close, 1=stay, 2=open. Command at frame t reproduces grip[t]->grip[t+1]."""
|
| 130 |
+
n = len(grip)
|
| 131 |
+
cls = np.full(n, GRIPPER_STAY, dtype=np.float32)
|
| 132 |
+
dg = np.diff(grip) # (n-1,)
|
| 133 |
+
cls[:-1] = np.where(dg > GRIP_EPS, GRIPPER_CLOSE, np.where(dg < -GRIP_EPS, GRIPPER_OPEN, GRIPPER_STAY))
|
| 134 |
+
return cls
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def main():
|
| 138 |
+
print(f"device={DEVICE} src={SRC_ROOT}\n")
|
| 139 |
+
ds = LeRobotDataset(REPO_ID, root=SRC_ROOT)
|
| 140 |
+
task_str = ds.meta.tasks.index[0] if hasattr(ds.meta.tasks, "index") else list(ds.meta.tasks)[0]
|
| 141 |
+
print("task string:", task_str)
|
| 142 |
+
|
| 143 |
+
# per-camera normalization stats (same source as Agent B)
|
| 144 |
+
stats = json.load(open(os.path.join(SRC_ROOT, "meta", "stats.json")))
|
| 145 |
+
mean1 = torch.tensor(np.array(stats["observation.images.cam1"]["mean"]).reshape(3), dtype=torch.float32)
|
| 146 |
+
std1 = torch.tensor(np.array(stats["observation.images.cam1"]["std"]).reshape(3), dtype=torch.float32)
|
| 147 |
+
mean2 = torch.tensor(np.array(stats["observation.images.cam2"]["mean"]).reshape(3), dtype=torch.float32)
|
| 148 |
+
std2 = torch.tensor(np.array(stats["observation.images.cam2"]["std"]).reshape(3), dtype=torch.float32)
|
| 149 |
+
|
| 150 |
+
clf = Classifier.from_pretrained(CKPT).to(DEVICE).eval()
|
| 151 |
+
|
| 152 |
+
# ---- output dataset schema (RL columns) ----
|
| 153 |
+
features = {
|
| 154 |
+
"observation.state": {"dtype": "float32", "shape": [7],
|
| 155 |
+
"names": ["ur_q1", "ur_q2", "ur_q3", "ur_q4", "ur_q5", "ur_q6", "grip_pos"]},
|
| 156 |
+
"observation.images.cam1": {"dtype": "video", "shape": [IMG, IMG, 3],
|
| 157 |
+
"names": ["height", "width", "channels"]},
|
| 158 |
+
"observation.images.cam2": {"dtype": "video", "shape": [IMG, IMG, 3],
|
| 159 |
+
"names": ["height", "width", "channels"]},
|
| 160 |
+
"action": {"dtype": "float32", "shape": [4], "names": ["delta_x", "delta_y", "delta_z", "gripper"]},
|
| 161 |
+
"next.reward": {"dtype": "float32", "shape": [1], "names": None},
|
| 162 |
+
"next.done": {"dtype": "bool", "shape": [1], "names": None},
|
| 163 |
+
}
|
| 164 |
+
if os.path.exists(OUT_ROOT):
|
| 165 |
+
import shutil
|
| 166 |
+
shutil.rmtree(OUT_ROOT)
|
| 167 |
+
out = LeRobotDataset.create(repo_id=REPO_ID, fps=ds.fps, root=OUT_ROOT,
|
| 168 |
+
features=features, use_videos=True)
|
| 169 |
+
|
| 170 |
+
# ---- group frames by episode (single ordered pass) ----
|
| 171 |
+
from_idx = ds.meta.episodes["dataset_from_index"]
|
| 172 |
+
to_idx = ds.meta.episodes["dataset_to_index"]
|
| 173 |
+
n_ep = ds.meta.total_episodes
|
| 174 |
+
|
| 175 |
+
all_actions = []
|
| 176 |
+
grip_class_counts = np.zeros(3, dtype=int)
|
| 177 |
+
n_success_ep = 0
|
| 178 |
+
reward_timeline_ep0 = None
|
| 179 |
+
prob_timeline_ep0 = None
|
| 180 |
+
|
| 181 |
+
for ep in range(n_ep):
|
| 182 |
+
lo, hi = int(from_idx[ep]), int(to_idx[ep])
|
| 183 |
+
idxs = list(range(lo, hi))
|
| 184 |
+
n = len(idxs)
|
| 185 |
+
|
| 186 |
+
states = np.zeros((n, 7), dtype=np.float32)
|
| 187 |
+
c1 = np.zeros((n, IMG, IMG, 3), dtype=np.uint8)
|
| 188 |
+
c2 = np.zeros((n, IMG, IMG, 3), dtype=np.uint8)
|
| 189 |
+
for j, i in enumerate(idxs):
|
| 190 |
+
f = ds[i]
|
| 191 |
+
states[j] = f["observation.state"].numpy()
|
| 192 |
+
c1[j] = resize_uint8_hwc(f["observation.images.cam1"])
|
| 193 |
+
c2[j] = resize_uint8_hwc(f["observation.images.cam2"])
|
| 194 |
+
|
| 195 |
+
# --- action: FK(joints) -> base-frame TCP; delta / step_size ---
|
| 196 |
+
pos = fk_batch(states[:, :6])[:, :3, 3] # (n,3) metres
|
| 197 |
+
actions = np.zeros((n, 4), dtype=np.float32)
|
| 198 |
+
actions[:-1, :3] = np.diff(pos, axis=0) / STEP_SIZE
|
| 199 |
+
actions[:, 3] = discrete_gripper(states[:, 6])
|
| 200 |
+
all_actions.append(actions)
|
| 201 |
+
for cval in (GRIPPER_CLOSE, GRIPPER_STAY, GRIPPER_OPEN):
|
| 202 |
+
grip_class_counts[int(cval)] += int((actions[:, 3] == cval).sum())
|
| 203 |
+
|
| 204 |
+
# --- reward: Agent B classifier over frames (batched) ---
|
| 205 |
+
probs = np.zeros(n, dtype=np.float32)
|
| 206 |
+
with torch.no_grad():
|
| 207 |
+
for s in range(0, n, 256):
|
| 208 |
+
e = min(n, s + 256)
|
| 209 |
+
b1 = norm_cam(c1[s:e], mean1, std1).to(DEVICE)
|
| 210 |
+
b2 = norm_cam(c2[s:e], mean2, std2).to(DEVICE)
|
| 211 |
+
probs[s:e] = clf.predict([b1, b2]).probabilities.cpu().numpy()
|
| 212 |
+
reward = (probs > SUCCESS_THRESHOLD).astype(np.float32)
|
| 213 |
+
|
| 214 |
+
# --- done: success onset OR episode end ---
|
| 215 |
+
done = np.zeros(n, dtype=bool)
|
| 216 |
+
succ = np.where(reward > 0.5)[0]
|
| 217 |
+
if len(succ):
|
| 218 |
+
n_success_ep += 1
|
| 219 |
+
done[succ[0]] = True # success onset
|
| 220 |
+
done[-1] = True # episode end
|
| 221 |
+
|
| 222 |
+
if ep == 0:
|
| 223 |
+
reward_timeline_ep0 = reward.copy()
|
| 224 |
+
prob_timeline_ep0 = probs.copy()
|
| 225 |
+
|
| 226 |
+
for j in range(n):
|
| 227 |
+
out.add_frame({
|
| 228 |
+
"observation.state": states[j],
|
| 229 |
+
"observation.images.cam1": c1[j],
|
| 230 |
+
"observation.images.cam2": c2[j],
|
| 231 |
+
"action": actions[j],
|
| 232 |
+
"next.reward": np.array([reward[j]], dtype=np.float32),
|
| 233 |
+
"next.done": np.array([done[j]], dtype=bool),
|
| 234 |
+
"task": task_str,
|
| 235 |
+
})
|
| 236 |
+
out.save_episode()
|
| 237 |
+
print(f"ep {ep:02d}: n={n:4d} success_onset={'-' if not len(succ) else succ[0]:>4} "
|
| 238 |
+
f"reward_frames={int(reward.sum()):4d} |Δp|/step max={np.abs(actions[:,:3]).max():.3f}")
|
| 239 |
+
|
| 240 |
+
out.finalize()
|
| 241 |
+
|
| 242 |
+
# ---------------------------------------------------------- stats report
|
| 243 |
+
A = np.concatenate(all_actions, axis=0)
|
| 244 |
+
cont = A[:, :3]
|
| 245 |
+
rep = {
|
| 246 |
+
"total_frames": int(A.shape[0]),
|
| 247 |
+
"n_episodes": int(n_ep),
|
| 248 |
+
"n_episodes_with_success_onset": int(n_success_ep),
|
| 249 |
+
"step_size_m": STEP_SIZE,
|
| 250 |
+
"success_threshold": SUCCESS_THRESHOLD,
|
| 251 |
+
"action_continuous_xyz": {
|
| 252 |
+
"min": cont.min(0).tolist(), "max": cont.max(0).tolist(),
|
| 253 |
+
"q01": np.quantile(cont, 0.01, axis=0).tolist(),
|
| 254 |
+
"q50": np.quantile(cont, 0.50, axis=0).tolist(),
|
| 255 |
+
"q99": np.quantile(cont, 0.99, axis=0).tolist(),
|
| 256 |
+
"frac_abs_gt_1": float((np.abs(cont) > 1.0).mean()),
|
| 257 |
+
},
|
| 258 |
+
"gripper_class_counts": {"close(0)": int(grip_class_counts[0]),
|
| 259 |
+
"stay(1)": int(grip_class_counts[1]),
|
| 260 |
+
"open(2)": int(grip_class_counts[2])},
|
| 261 |
+
}
|
| 262 |
+
print("\n==== ACTION / REWARD STATS ====")
|
| 263 |
+
print(json.dumps(rep, indent=2))
|
| 264 |
+
json.dump(rep, open(os.path.join(HERE, "action_reward_stats.json"), "w"), indent=2)
|
| 265 |
+
|
| 266 |
+
# ---------------------------------------------------------- plots
|
| 267 |
+
try:
|
| 268 |
+
import matplotlib
|
| 269 |
+
matplotlib.use("Agg")
|
| 270 |
+
import matplotlib.pyplot as plt
|
| 271 |
+
fig, ax = plt.subplots(1, 3, figsize=(16, 4))
|
| 272 |
+
for d, name in zip(range(3), ["Δx", "Δy", "Δz"]):
|
| 273 |
+
ax[0].hist(cont[:, d], bins=120, alpha=0.5, label=name)
|
| 274 |
+
ax[0].axvline(-1, color="k", ls="--", lw=0.8); ax[0].axvline(1, color="k", ls="--", lw=0.8)
|
| 275 |
+
ax[0].set_title(f"EE-delta action (÷ step={STEP_SIZE} m)"); ax[0].set_xlabel("normalized delta"); ax[0].legend()
|
| 276 |
+
ax[1].bar(["close(0)", "stay(1)", "open(2)"], grip_class_counts, color=["#d62728", "#7f7f7f", "#2ca02c"])
|
| 277 |
+
ax[1].set_title("gripper discrete class counts")
|
| 278 |
+
ax[2].plot(prob_timeline_ep0, label="classifier prob")
|
| 279 |
+
ax[2].plot(reward_timeline_ep0, label="next.reward", lw=2)
|
| 280 |
+
ax[2].axhline(SUCCESS_THRESHOLD, color="k", ls="--", lw=0.8, label=f"thr={SUCCESS_THRESHOLD}")
|
| 281 |
+
ax[2].set_title("episode 0: reward timeline"); ax[2].set_xlabel("frame"); ax[2].legend()
|
| 282 |
+
fig.tight_layout()
|
| 283 |
+
fig.savefig(os.path.join(HERE, "rl_dataset_action_reward.png"), dpi=110)
|
| 284 |
+
print("saved rl_dataset_action_reward.png")
|
| 285 |
+
except Exception as e:
|
| 286 |
+
print("plot skipped:", e)
|
| 287 |
+
|
| 288 |
+
# ---------------------------------------------------------- schema json
|
| 289 |
+
schema = {
|
| 290 |
+
"repo_id": REPO_ID,
|
| 291 |
+
"root": OUT_ROOT,
|
| 292 |
+
"codebase_version": "v3.0",
|
| 293 |
+
"fps": int(ds.fps),
|
| 294 |
+
"total_episodes": int(n_ep),
|
| 295 |
+
"total_frames": int(A.shape[0]),
|
| 296 |
+
"rl_columns": {
|
| 297 |
+
"action": {"dtype": "float32", "shape": [4],
|
| 298 |
+
"names": ["delta_x", "delta_y", "delta_z", "gripper"],
|
| 299 |
+
"semantics": "xyz = base-frame TCP delta / end_effector_step_sizes(=0.05 m); "
|
| 300 |
+
"gripper = discrete class {0=close,1=stay,2=open}"},
|
| 301 |
+
"next.reward": {"dtype": "float32", "shape": [1],
|
| 302 |
+
"semantics": f"Agent B success classifier prob>{SUCCESS_THRESHOLD} -> {SUCCESS_REWARD}"},
|
| 303 |
+
"next.done": {"dtype": "bool", "shape": [1],
|
| 304 |
+
"semantics": "True at success onset AND at episode end"},
|
| 305 |
+
},
|
| 306 |
+
"state_keys_for_replay_buffer": ["observation.state",
|
| 307 |
+
"observation.images.cam1", "observation.images.cam2"],
|
| 308 |
+
"observation.state": {"dtype": "float32", "shape": [7],
|
| 309 |
+
"names": ["ur_q1", "ur_q2", "ur_q3", "ur_q4", "ur_q5", "ur_q6", "grip_pos"],
|
| 310 |
+
"note": "matches online gym_manipulator agent_pos (all ObservationConfig add_* False)"},
|
| 311 |
+
"observation.images.cam1": {"dtype": "video", "stored_shape_hwc": [IMG, IMG, 3],
|
| 312 |
+
"decoded_shape_chw": [3, IMG, IMG], "range": "float[0,1]"},
|
| 313 |
+
"observation.images.cam2": {"dtype": "video", "stored_shape_hwc": [IMG, IMG, 3],
|
| 314 |
+
"decoded_shape_chw": [3, IMG, IMG], "range": "float[0,1]"},
|
| 315 |
+
"policy_output_features": {"action": {"type": "ACTION", "shape": [4]}},
|
| 316 |
+
"policy_input_features": {
|
| 317 |
+
"observation.state": {"type": "STATE", "shape": [7]},
|
| 318 |
+
"observation.images.cam1": {"type": "VISUAL", "shape": [3, IMG, IMG]},
|
| 319 |
+
"observation.images.cam2": {"type": "VISUAL", "shape": [3, IMG, IMG]},
|
| 320 |
+
},
|
| 321 |
+
"online_env_requirements": {
|
| 322 |
+
"image_preprocessing.resize_size": [IMG, IMG],
|
| 323 |
+
"inverse_kinematics.end_effector_step_sizes": {"x": STEP_SIZE, "y": STEP_SIZE, "z": STEP_SIZE},
|
| 324 |
+
"policy.num_discrete_actions": 3,
|
| 325 |
+
"reward_classifier.success_threshold": SUCCESS_THRESHOLD,
|
| 326 |
+
"note": "crop_dataset_roi may be re-applied online; if ROI crop is used, re-run it on THIS "
|
| 327 |
+
"dataset too so offline images match the online cropped size.",
|
| 328 |
+
},
|
| 329 |
+
}
|
| 330 |
+
json.dump(schema, open(os.path.join(HERE, "rl_dataset_schema.json"), "w"), indent=2)
|
| 331 |
+
print("saved rl_dataset_schema.json")
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
if __name__ == "__main__":
|
| 335 |
+
main()
|
config/train_hilserl_ur7e.json
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"output_dir": "hilserl/outputs/banana_sac",
|
| 3 |
+
"job_name": "banana_hilserl_ur7e",
|
| 4 |
+
"seed": 1,
|
| 5 |
+
"resume": false,
|
| 6 |
+
"num_workers": 4,
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"steps": 100000,
|
| 9 |
+
"log_freq": 500,
|
| 10 |
+
"save_freq": 5000,
|
| 11 |
+
"save_checkpoint": true,
|
| 12 |
+
|
| 13 |
+
"mixer": "online_offline",
|
| 14 |
+
"online_ratio": 0.5,
|
| 15 |
+
|
| 16 |
+
"dataset": {
|
| 17 |
+
"repo_id": "theo/banana_in_pot_rl",
|
| 18 |
+
"root": "./banana_rl_lerobot",
|
| 19 |
+
"use_imagenet_stats": false
|
| 20 |
+
},
|
| 21 |
+
|
| 22 |
+
"algorithm": {
|
| 23 |
+
"type": "sac",
|
| 24 |
+
"actor_lr": 3e-4,
|
| 25 |
+
"critic_lr": 3e-4,
|
| 26 |
+
"temperature_lr": 3e-4,
|
| 27 |
+
"discount": 0.99,
|
| 28 |
+
"use_backup_entropy": true,
|
| 29 |
+
"critic_target_update_weight": 0.005,
|
| 30 |
+
"num_critics": 2,
|
| 31 |
+
"num_subsample_critics": null,
|
| 32 |
+
"critic_network_kwargs": {
|
| 33 |
+
"hidden_dims": [256, 256],
|
| 34 |
+
"activate_final": true,
|
| 35 |
+
"final_activation": null
|
| 36 |
+
},
|
| 37 |
+
"discrete_critic_network_kwargs": {
|
| 38 |
+
"hidden_dims": [256, 256],
|
| 39 |
+
"activate_final": true,
|
| 40 |
+
"final_activation": null
|
| 41 |
+
},
|
| 42 |
+
"temperature_init": 0.01,
|
| 43 |
+
"target_entropy": null,
|
| 44 |
+
"utd_ratio": 2,
|
| 45 |
+
"policy_update_freq": 1,
|
| 46 |
+
"grad_clip_norm": 10.0,
|
| 47 |
+
"use_torch_compile": false
|
| 48 |
+
},
|
| 49 |
+
|
| 50 |
+
"policy": {
|
| 51 |
+
"type": "gaussian_actor",
|
| 52 |
+
"push_to_hub": false,
|
| 53 |
+
"device": "cuda",
|
| 54 |
+
"storage_device": "cpu",
|
| 55 |
+
"vision_encoder_name": "lerobot/resnet10",
|
| 56 |
+
"freeze_vision_encoder": true,
|
| 57 |
+
"image_encoder_hidden_dim": 32,
|
| 58 |
+
"shared_encoder": true,
|
| 59 |
+
"num_discrete_actions": 3,
|
| 60 |
+
"image_embedding_pooling_dim": 8,
|
| 61 |
+
"state_encoder_hidden_dim": 256,
|
| 62 |
+
"latent_dim": 256,
|
| 63 |
+
|
| 64 |
+
"online_steps": 1000000,
|
| 65 |
+
"online_buffer_capacity": 100000,
|
| 66 |
+
"offline_buffer_capacity": 25000,
|
| 67 |
+
"async_prefetch": false,
|
| 68 |
+
"online_step_before_learning": 100,
|
| 69 |
+
|
| 70 |
+
"normalization_mapping": {
|
| 71 |
+
"VISUAL": "MEAN_STD",
|
| 72 |
+
"STATE": "MIN_MAX",
|
| 73 |
+
"ENV": "MIN_MAX",
|
| 74 |
+
"ACTION": "MIN_MAX"
|
| 75 |
+
},
|
| 76 |
+
|
| 77 |
+
"input_features": {
|
| 78 |
+
"observation.state": { "type": "STATE", "shape": [7] },
|
| 79 |
+
"observation.images.cam1": { "type": "VISUAL", "shape": [3, 128, 128] },
|
| 80 |
+
"observation.images.cam2": { "type": "VISUAL", "shape": [3, 128, 128] }
|
| 81 |
+
},
|
| 82 |
+
"output_features": {
|
| 83 |
+
"action": { "type": "ACTION", "shape": [3] }
|
| 84 |
+
},
|
| 85 |
+
|
| 86 |
+
"dataset_stats": {
|
| 87 |
+
"observation.images.cam1": {
|
| 88 |
+
"mean": [0.485, 0.456, 0.406],
|
| 89 |
+
"std": [0.229, 0.224, 0.225]
|
| 90 |
+
},
|
| 91 |
+
"observation.images.cam2": {
|
| 92 |
+
"mean": [0.485, 0.456, 0.406],
|
| 93 |
+
"std": [0.229, 0.224, 0.225]
|
| 94 |
+
},
|
| 95 |
+
"observation.state": {
|
| 96 |
+
"min": [2.506379, -2.391092, 1.200254, -3.141332, -2.102934, -4.914474, 0.0118],
|
| 97 |
+
"max": [3.561876, -1.021552, 2.487616, -1.266623, -1.415739, -1.831519, 0.898]
|
| 98 |
+
},
|
| 99 |
+
"action": {
|
| 100 |
+
"min": [-1.0, -1.0, -1.0],
|
| 101 |
+
"max": [1.0, 1.0, 1.0]
|
| 102 |
+
}
|
| 103 |
+
},
|
| 104 |
+
|
| 105 |
+
"actor_network_kwargs": {
|
| 106 |
+
"hidden_dims": [256, 256],
|
| 107 |
+
"activate_final": true
|
| 108 |
+
},
|
| 109 |
+
"policy_kwargs": {
|
| 110 |
+
"use_tanh_squash": true,
|
| 111 |
+
"std_min": -5,
|
| 112 |
+
"std_max": 2,
|
| 113 |
+
"init_final": 0.05
|
| 114 |
+
},
|
| 115 |
+
"actor_learner_config": {
|
| 116 |
+
"learner_host": "127.0.0.1",
|
| 117 |
+
"learner_port": 50051,
|
| 118 |
+
"policy_parameters_push_frequency": 4
|
| 119 |
+
},
|
| 120 |
+
"concurrency": {
|
| 121 |
+
"actor": "processes",
|
| 122 |
+
"learner": "processes"
|
| 123 |
+
}
|
| 124 |
+
},
|
| 125 |
+
|
| 126 |
+
"env": {
|
| 127 |
+
"type": "gym_manipulator",
|
| 128 |
+
"name": "real_robot",
|
| 129 |
+
"fps": 30,
|
| 130 |
+
"task": "put right banana in the pot",
|
| 131 |
+
|
| 132 |
+
"robot": null,
|
| 133 |
+
"teleop": null,
|
| 134 |
+
|
| 135 |
+
"processor": {
|
| 136 |
+
"control_mode": "gamepad",
|
| 137 |
+
"max_gripper_pos": 100.0,
|
| 138 |
+
"observation": {
|
| 139 |
+
"display_cameras": false,
|
| 140 |
+
"add_joint_velocity_to_observation": false,
|
| 141 |
+
"add_current_to_observation": false,
|
| 142 |
+
"add_ee_pose_to_observation": false
|
| 143 |
+
},
|
| 144 |
+
"image_preprocessing": {
|
| 145 |
+
"crop_params_dict": null,
|
| 146 |
+
"resize_size": [128, 128]
|
| 147 |
+
},
|
| 148 |
+
"gripper": {
|
| 149 |
+
"use_gripper": true,
|
| 150 |
+
"gripper_penalty": 0.0
|
| 151 |
+
},
|
| 152 |
+
"reset": {
|
| 153 |
+
"fixed_reset_joint_positions": [3.05, -1.60, 1.90, -1.85, -1.55, -3.30, 0.02],
|
| 154 |
+
"reset_time_s": 5.0,
|
| 155 |
+
"control_time_s": 20.0,
|
| 156 |
+
"terminate_on_success": true
|
| 157 |
+
},
|
| 158 |
+
"inverse_kinematics": {
|
| 159 |
+
"urdf_path": "./ur7e.urdf",
|
| 160 |
+
"target_frame_name": "tool0",
|
| 161 |
+
"end_effector_bounds": {
|
| 162 |
+
"min": [-0.6, -0.6, 0.0],
|
| 163 |
+
"max": [0.6, 0.6, 0.6]
|
| 164 |
+
},
|
| 165 |
+
"end_effector_step_sizes": {
|
| 166 |
+
"x": 0.05,
|
| 167 |
+
"y": 0.05,
|
| 168 |
+
"z": 0.05
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
"reward_classifier": {
|
| 172 |
+
"pretrained_path": "./reward_classifier/checkpoint",
|
| 173 |
+
"success_threshold": 0.7,
|
| 174 |
+
"success_reward": 1.0
|
| 175 |
+
}
|
| 176 |
+
},
|
| 177 |
+
|
| 178 |
+
"features": {
|
| 179 |
+
"observation.state": { "type": "STATE", "shape": [7] },
|
| 180 |
+
"observation.images.cam1": { "type": "VISUAL", "shape": [3, 128, 128] },
|
| 181 |
+
"observation.images.cam2": { "type": "VISUAL", "shape": [3, 128, 128] },
|
| 182 |
+
"action": { "type": "ACTION", "shape": [3] }
|
| 183 |
+
},
|
| 184 |
+
"features_map": {
|
| 185 |
+
"observation.state": "observation.state",
|
| 186 |
+
"observation.images.cam1": "observation.images.cam1",
|
| 187 |
+
"observation.images.cam2": "observation.images.cam2",
|
| 188 |
+
"action": "action"
|
| 189 |
+
}
|
| 190 |
+
},
|
| 191 |
+
|
| 192 |
+
"wandb": {
|
| 193 |
+
"enable": false,
|
| 194 |
+
"project": "banana_in_pot_hilserl"
|
| 195 |
+
}
|
| 196 |
+
}
|
joint_to_ee.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""
|
| 3 |
+
joint_to_ee.py — UR7e joint <-> end-effector conversion for HIL-SERL prep.
|
| 4 |
+
|
| 5 |
+
WHAT HIL-SERL EXPECTS (see notes below / joint_to_ee_notes.md):
|
| 6 |
+
The RL policy emits a flat action vector [delta_x, delta_y, delta_z, gripper]
|
| 7 |
+
(4-d when use_gripper=True). delta_{x,y,z} are DIMENSIONLESS, expressed in the
|
| 8 |
+
robot BASE frame, and are converted to metres inside gym_manipulator's action
|
| 9 |
+
pipeline by multiplying with `end_effector_step_sizes[{x,y,z}]`
|
| 10 |
+
(lerobot/robots/so_follower/robot_kinematic_processor.py:124-135). The policy
|
| 11 |
+
never commands rotation (target_wx/wy/wz are hard-wired to 0.0 in
|
| 12 |
+
lerobot/processor/delta_action_processor.py:114-116), so orientation is held at
|
| 13 |
+
the current (reference) EE orientation. `gripper` is a velocity-like scalar.
|
| 14 |
+
|
| 15 |
+
Reference pose in the RL pipeline is FK(current joints) recomputed EVERY step
|
| 16 |
+
(use_latched_reference=False, gym_manipulator.py:507). Therefore the absolute
|
| 17 |
+
target each step is: target_pos = FK(q_t).pos + step_size * policy_delta,
|
| 18 |
+
and the learner's action is exactly the base-frame displacement of the TCP
|
| 19 |
+
between consecutive steps, divided by the step size.
|
| 20 |
+
|
| 21 |
+
This module provides:
|
| 22 |
+
(a) fk(q_rad) -> 4x4 TCP pose (base frame) from 6 UR joints.
|
| 23 |
+
(b) joint_traj_to_ee_delta_actions(...) -> the [dx,dy,dz,gripper] action seq.
|
| 24 |
+
Prefers RECORDED tcp_pose for the EE (exact); FK is the fallback / validator.
|
| 25 |
+
(c) IK direction for deploy is documented in joint_to_ee_notes.md (placo).
|
| 26 |
+
|
| 27 |
+
Validated: FK(ur_joint_states) vs recorded tcp_pose = 0.85 mm median error at
|
| 28 |
+
near-static frames across all 51 takes -> the kinematic chain is exact and the
|
| 29 |
+
recorded TCP == robot flange (no tool offset configured). See validate_fk().
|
| 30 |
+
"""
|
| 31 |
+
from __future__ import annotations
|
| 32 |
+
|
| 33 |
+
import glob
|
| 34 |
+
import os
|
| 35 |
+
|
| 36 |
+
import numpy as np
|
| 37 |
+
import yaml
|
| 38 |
+
from scipy.spatial.transform import Rotation as R
|
| 39 |
+
|
| 40 |
+
# ----------------------------------------------------------------------------
|
| 41 |
+
# UR7e forward kinematics, built directly from the ur_description URDF joint
|
| 42 |
+
# origins (default_kinematics.yaml). Each joint i is a fixed RPY+XYZ transform
|
| 43 |
+
# followed by a rotation about the joint's local Z axis.
|
| 44 |
+
# ----------------------------------------------------------------------------
|
| 45 |
+
DEFAULT_KIN_YAML = "/opt/ros/jazzy/share/ur_description/config/ur7e/default_kinematics.yaml"
|
| 46 |
+
_LINK_ORDER = ["shoulder", "upper_arm", "forearm", "wrist_1", "wrist_2", "wrist_3"]
|
| 47 |
+
UR_JOINT_NAMES = [
|
| 48 |
+
"shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint",
|
| 49 |
+
"wrist_1_joint", "wrist_2_joint", "wrist_3_joint",
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _rpyxyz_to_T(p: dict) -> np.ndarray:
|
| 54 |
+
T = np.eye(4)
|
| 55 |
+
T[:3, :3] = R.from_euler("xyz", [p["roll"], p["pitch"], p["yaw"]]).as_matrix()
|
| 56 |
+
T[:3, 3] = [p["x"], p["y"], p["z"]]
|
| 57 |
+
return T
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _load_fixed_transforms(kin_yaml: str = DEFAULT_KIN_YAML) -> list[np.ndarray]:
|
| 61 |
+
with open(kin_yaml) as f:
|
| 62 |
+
kin = yaml.safe_load(f)["kinematics"]
|
| 63 |
+
return [_rpyxyz_to_T(kin[k]) for k in _LINK_ORDER]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
_FIXED = _load_fixed_transforms()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _rotz(a: float) -> np.ndarray:
|
| 70 |
+
c, s = np.cos(a), np.sin(a)
|
| 71 |
+
T = np.eye(4)
|
| 72 |
+
T[0, 0] = c; T[0, 1] = -s; T[1, 0] = s; T[1, 1] = c
|
| 73 |
+
return T
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def fk(q_rad, tool: np.ndarray | None = None) -> np.ndarray:
|
| 77 |
+
"""Forward kinematics of the UR7e TCP (flange) in the robot BASE frame.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
q_rad: 6 joint angles [shoulder_pan..wrist_3] in RADIANS (URDF order,
|
| 81 |
+
which is exactly ur_joint_states q1..q6 in our h5).
|
| 82 |
+
tool: optional 4x4 flange->TCP offset. Our dataset has NO tool offset
|
| 83 |
+
(recorded tcp_pose == flange, validated to 0.85 mm), so leave None.
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
4x4 homogeneous transform (base_link frame == UR RTDE base frame).
|
| 87 |
+
"""
|
| 88 |
+
q = np.asarray(q_rad, dtype=float)
|
| 89 |
+
T = np.eye(4)
|
| 90 |
+
for i in range(6):
|
| 91 |
+
T = T @ _FIXED[i] @ _rotz(q[i])
|
| 92 |
+
if tool is not None:
|
| 93 |
+
T = T @ tool
|
| 94 |
+
return T
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def fk_batch(q_rad_seq: np.ndarray) -> np.ndarray:
|
| 98 |
+
"""FK over an (N,6) array of joint configs -> (N,4,4)."""
|
| 99 |
+
return np.stack([fk(q) for q in np.asarray(q_rad_seq, dtype=float)], axis=0)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def pose_to_pos_quat(T: np.ndarray):
|
| 103 |
+
"""4x4 -> (pos[3], quat[x,y,z,w])."""
|
| 104 |
+
return T[:3, 3].copy(), R.from_matrix(T[:3, :3]).as_quat()
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# ----------------------------------------------------------------------------
|
| 108 |
+
# (b) absolute-joint / TCP trajectory -> HIL-SERL EE-delta action sequence
|
| 109 |
+
# ----------------------------------------------------------------------------
|
| 110 |
+
def joint_traj_to_ee_delta_actions(
|
| 111 |
+
tcp_pos: np.ndarray,
|
| 112 |
+
tcp_quat: np.ndarray | None = None,
|
| 113 |
+
gripper: np.ndarray | None = None,
|
| 114 |
+
end_effector_step_sizes: dict | None = None,
|
| 115 |
+
gripper_max: float = 1.0,
|
| 116 |
+
include_rotation: bool = False,
|
| 117 |
+
):
|
| 118 |
+
"""Convert a per-timestep TCP trajectory into the HIL-SERL policy action seq.
|
| 119 |
+
|
| 120 |
+
HIL-SERL action at step t is the BASE-frame TCP displacement between the pose
|
| 121 |
+
at t and t+1, divided by the per-axis step size (so the policy operates in a
|
| 122 |
+
normalised, roughly [-1,1] action space). Rotation is NOT part of the policy
|
| 123 |
+
action (gym_manipulator holds orientation fixed), but a rotvec delta is
|
| 124 |
+
returned as extra info when include_rotation=True.
|
| 125 |
+
|
| 126 |
+
Prefer passing the RECORDED tcp_pose (x,y,z,quat) for `tcp_pos`/`tcp_quat` —
|
| 127 |
+
it is the exact robot EE. Use fk_batch(q)[:, :3, 3] only if TCP wasn't logged.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
tcp_pos: (N,3) TCP positions in metres, base frame.
|
| 131 |
+
tcp_quat: (N,4) TCP quaternions [x,y,z,w] (only needed if include_rotation).
|
| 132 |
+
gripper: (N,) gripper position (e.g. grip_pos). Aligned to `tcp_pos`.
|
| 133 |
+
end_effector_step_sizes: {"x":sx,"y":sy,"z":sz} metres-per-unit. If None,
|
| 134 |
+
deltas are returned in METRES (step size = 1). Choose sx.. so the max
|
| 135 |
+
per-frame delta maps to <=1 (our data: max ~0.053 m/frame at TCP rate).
|
| 136 |
+
gripper_max: normaliser for gripper -> action[:,3] = gripper / gripper_max.
|
| 137 |
+
include_rotation: also return per-step rotvec delta (base frame), (N-1,3).
|
| 138 |
+
|
| 139 |
+
Returns:
|
| 140 |
+
actions: (N-1, 4) float32 -> [delta_x, delta_y, delta_z, gripper], the
|
| 141 |
+
vector MapTensorToDeltaActionDictStep consumes. gripper column is
|
| 142 |
+
0 if `gripper` is None.
|
| 143 |
+
rot_delta: (N-1,3) rotvec deltas if include_rotation else None.
|
| 144 |
+
"""
|
| 145 |
+
tcp_pos = np.asarray(tcp_pos, dtype=float)
|
| 146 |
+
n = len(tcp_pos)
|
| 147 |
+
if n < 2:
|
| 148 |
+
raise ValueError("need >=2 timesteps to form deltas")
|
| 149 |
+
|
| 150 |
+
dpos = np.diff(tcp_pos, axis=0) # (N-1,3) base-frame metres
|
| 151 |
+
if end_effector_step_sizes is not None:
|
| 152 |
+
s = np.array([end_effector_step_sizes["x"],
|
| 153 |
+
end_effector_step_sizes["y"],
|
| 154 |
+
end_effector_step_sizes["z"]], dtype=float)
|
| 155 |
+
dpos = dpos / s
|
| 156 |
+
|
| 157 |
+
actions = np.zeros((n - 1, 4), dtype=np.float32)
|
| 158 |
+
actions[:, :3] = dpos
|
| 159 |
+
if gripper is not None:
|
| 160 |
+
g = np.asarray(gripper, dtype=float)[:n]
|
| 161 |
+
# policy gripper is applied at step t; use the value at the *target* step
|
| 162 |
+
actions[:, 3] = (g[1:] / gripper_max).astype(np.float32)
|
| 163 |
+
|
| 164 |
+
rot_delta = None
|
| 165 |
+
if include_rotation:
|
| 166 |
+
if tcp_quat is None:
|
| 167 |
+
raise ValueError("tcp_quat required when include_rotation=True")
|
| 168 |
+
Rmats = R.from_quat(np.asarray(tcp_quat, dtype=float))
|
| 169 |
+
rot_delta = np.zeros((n - 1, 3), dtype=np.float32)
|
| 170 |
+
for t in range(n - 1):
|
| 171 |
+
dR = Rmats[t].inv() * Rmats[t + 1] # relative rotation in EE frame
|
| 172 |
+
rot_delta[t] = dR.as_rotvec()
|
| 173 |
+
return actions, rot_delta
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
# ----------------------------------------------------------------------------
|
| 177 |
+
# h5 helpers (reuse recorded tcp_pose; align async streams by timestamp)
|
| 178 |
+
# ----------------------------------------------------------------------------
|
| 179 |
+
def load_take(h5_path: str, interp_joints_to_tcp: bool = True):
|
| 180 |
+
"""Load a take. ur_joint_states (609) and tcp_pose (608) are logged on
|
| 181 |
+
SEPARATE clocks; we interpolate joints onto the tcp_pose timestamps so every
|
| 182 |
+
returned row is time-consistent. Returns a dict of aligned arrays."""
|
| 183 |
+
import h5py
|
| 184 |
+
with h5py.File(h5_path, "r") as h:
|
| 185 |
+
q = np.stack([h[f"ur_joint_states/q{i}"][:] for i in range(1, 7)], 1)
|
| 186 |
+
qd = np.stack([h[f"ur_joint_states/qd{i}"][:] for i in range(1, 7)], 1)
|
| 187 |
+
tq = h["ur_joint_states/t_rel_s"][:]
|
| 188 |
+
pos = np.stack([h[f"tcp_pose/{a}"][:] for a in "xyz"], 1)
|
| 189 |
+
quat = np.stack([h[f"tcp_pose/{a}"][:] for a in ["qx", "qy", "qz", "qw"]], 1)
|
| 190 |
+
tp = h["tcp_pose/t_rel_s"][:]
|
| 191 |
+
gpos = h["gripper/grip_pos"][:]
|
| 192 |
+
tg = h["gripper/t_rel_s"][:]
|
| 193 |
+
if interp_joints_to_tcp:
|
| 194 |
+
q = np.stack([np.interp(tp, tq, q[:, j]) for j in range(6)], 1)
|
| 195 |
+
qd = np.stack([np.interp(tp, tq, qd[:, j]) for j in range(6)], 1)
|
| 196 |
+
t = tp
|
| 197 |
+
else:
|
| 198 |
+
t = tq
|
| 199 |
+
gpos_i = np.interp(tp, tg, gpos)
|
| 200 |
+
return {"q": q, "qd": qd, "tcp_pos": pos, "tcp_quat": quat,
|
| 201 |
+
"gripper": gpos_i, "t": t}
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# ----------------------------------------------------------------------------
|
| 205 |
+
# (validator) FK vs recorded tcp_pose
|
| 206 |
+
# ----------------------------------------------------------------------------
|
| 207 |
+
def validate_fk(dataset_glob: str, tool: np.ndarray | None = None):
|
| 208 |
+
"""Return per-sample position (mm) / orientation (deg) errors + joint speed."""
|
| 209 |
+
files = sorted(glob.glob(dataset_glob))
|
| 210 |
+
perr, oerr, speed = [], [], []
|
| 211 |
+
for f in files:
|
| 212 |
+
d = load_take(f)
|
| 213 |
+
for k in range(len(d["tcp_pos"])):
|
| 214 |
+
T = fk(d["q"][k], tool=tool)
|
| 215 |
+
perr.append(np.linalg.norm(T[:3, 3] - d["tcp_pos"][k]) * 1000.0)
|
| 216 |
+
Rr = R.from_quat(d["tcp_quat"][k]).as_matrix()
|
| 217 |
+
oerr.append(np.degrees(np.linalg.norm(
|
| 218 |
+
R.from_matrix(T[:3, :3].T @ Rr).as_rotvec())))
|
| 219 |
+
speed.append(np.linalg.norm(d["qd"][k]))
|
| 220 |
+
return np.array(perr), np.array(oerr), np.array(speed), files
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
if __name__ == "__main__":
|
| 224 |
+
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 225 |
+
g = os.path.join(root, "Put_right_banana_in_the_pot", "take_*", "vectors.h5")
|
| 226 |
+
perr, oerr, speed, files = validate_fk(g)
|
| 227 |
+
st = speed < 0.02
|
| 228 |
+
print(f"{len(files)} takes, {len(perr)} samples")
|
| 229 |
+
print(f"POS err mm ALL: med={np.median(perr):.2f} mean={perr.mean():.2f} "
|
| 230 |
+
f"p95={np.percentile(perr,95):.2f}")
|
| 231 |
+
print(f"POS err mm static(|qd|<0.02, n={st.sum()}): "
|
| 232 |
+
f"med={np.median(perr[st]):.2f} mean={perr[st].mean():.2f} "
|
| 233 |
+
f"p95={np.percentile(perr[st],95):.2f}")
|
| 234 |
+
print(f"ROT err deg static: med={np.median(oerr[st]):.2f} "
|
| 235 |
+
f"mean={oerr[st].mean():.2f}")
|
reward_classifier/checkpoint/config.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"type": "reward_classifier",
|
| 3 |
+
"input_features": {
|
| 4 |
+
"observation.images.cam1": {
|
| 5 |
+
"type": "VISUAL",
|
| 6 |
+
"shape": [
|
| 7 |
+
3,
|
| 8 |
+
128,
|
| 9 |
+
128
|
| 10 |
+
]
|
| 11 |
+
},
|
| 12 |
+
"observation.images.cam2": {
|
| 13 |
+
"type": "VISUAL",
|
| 14 |
+
"shape": [
|
| 15 |
+
3,
|
| 16 |
+
128,
|
| 17 |
+
128
|
| 18 |
+
]
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
"output_features": {},
|
| 22 |
+
"device": "cuda",
|
| 23 |
+
"pretrained_path": null,
|
| 24 |
+
"pretrained_revision": null,
|
| 25 |
+
"push_to_hub": false,
|
| 26 |
+
"repo_id": null,
|
| 27 |
+
"license": null,
|
| 28 |
+
"tags": null,
|
| 29 |
+
"private": null,
|
| 30 |
+
"name": "reward_classifier",
|
| 31 |
+
"num_classes": 2,
|
| 32 |
+
"hidden_dim": 256,
|
| 33 |
+
"latent_dim": 256,
|
| 34 |
+
"image_embedding_pooling_dim": 8,
|
| 35 |
+
"dropout_rate": 0.1,
|
| 36 |
+
"model_name": "lerobot/resnet10",
|
| 37 |
+
"model_type": "cnn",
|
| 38 |
+
"num_cameras": 2,
|
| 39 |
+
"learning_rate": 0.0001,
|
| 40 |
+
"weight_decay": 0.01,
|
| 41 |
+
"grad_clip_norm": 1.0,
|
| 42 |
+
"normalization_mapping": {
|
| 43 |
+
"VISUAL": "MEAN_STD"
|
| 44 |
+
}
|
| 45 |
+
}
|
reward_classifier/checkpoint/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2174329c4ff4143f27176d818c3bfb87386ad3eb39336aa552682bf2edea2cf9
|
| 3 |
+
size 29085260
|
ur7e.urdf
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" ?>
|
| 2 |
+
<!-- =================================================================================== -->
|
| 3 |
+
<!-- | This document was autogenerated by xacro from /opt/ros/jazzy/share/ur_description/urdf/ur.urdf.xacro | -->
|
| 4 |
+
<!-- | EDITING THIS FILE BY HAND IS NOT RECOMMENDED | -->
|
| 5 |
+
<!-- =================================================================================== -->
|
| 6 |
+
<robot name="ur7e">
|
| 7 |
+
<!--
|
| 8 |
+
Base UR robot series xacro macro.
|
| 9 |
+
|
| 10 |
+
NOTE this is NOT a URDF. It cannot directly be loaded by consumers
|
| 11 |
+
expecting a flattened '.urdf' file. See the top-level '.xacro' for that
|
| 12 |
+
(but note that .xacro must still be processed by the xacro command).
|
| 13 |
+
|
| 14 |
+
This file models the base kinematic chain of a UR robot, which then gets
|
| 15 |
+
parameterised by various configuration files to convert it into a UR3(e),
|
| 16 |
+
UR5(e), UR10(e) or UR16e, etc.
|
| 17 |
+
|
| 18 |
+
NOTE the default kinematic parameters (i.e., link lengths, frame locations,
|
| 19 |
+
offsets, etc) do not correspond to any particular robot. They are defaults
|
| 20 |
+
only. There WILL be non-zero offsets between the Forward Kinematics results
|
| 21 |
+
in TF (i.e., robot_state_publisher) and the values reported by the Teach
|
| 22 |
+
Pendant.
|
| 23 |
+
|
| 24 |
+
For accurate (and robot-specific) transforms, the 'kinematics_parameters_file'
|
| 25 |
+
parameter MUST point to a .yaml file containing the appropriate values for
|
| 26 |
+
the targeted robot.
|
| 27 |
+
|
| 28 |
+
If using the UniversalRobots/Universal_Robots_ROS_Driver, follow the steps
|
| 29 |
+
described in the readme of that repository to extract the kinematic
|
| 30 |
+
calibration from the controller and generate the required .yaml file.
|
| 31 |
+
|
| 32 |
+
Main author of the migration to yaml configs Ludovic Delval.
|
| 33 |
+
|
| 34 |
+
Contributors to previous versions (in no particular order)
|
| 35 |
+
|
| 36 |
+
- Denis Stogl
|
| 37 |
+
- Lovro Ivanov
|
| 38 |
+
- Felix Messmer
|
| 39 |
+
- Kelsey Hawkins
|
| 40 |
+
- Wim Meeussen
|
| 41 |
+
- Shaun Edwards
|
| 42 |
+
- Nadia Hammoudeh Garcia
|
| 43 |
+
- Dave Hershberger
|
| 44 |
+
- G. vd. Hoorn
|
| 45 |
+
- Philip Long
|
| 46 |
+
- Dave Coleman
|
| 47 |
+
- Miguel Prada
|
| 48 |
+
- Mathias Luedtke
|
| 49 |
+
- Marcel Schnirring
|
| 50 |
+
- Felix von Drigalski
|
| 51 |
+
- Felix Exner
|
| 52 |
+
- Jimmy Da Silva
|
| 53 |
+
- Ajit Krisshna N L
|
| 54 |
+
- Muhammad Asif Rana
|
| 55 |
+
-->
|
| 56 |
+
<!-- create link fixed to the "world" -->
|
| 57 |
+
<link name="world"/>
|
| 58 |
+
<!-- links - main serial chain -->
|
| 59 |
+
<!-- base_link serves as the root of the robot's kinematic tree. It follows REP-103
|
| 60 |
+
conventions (i.e., X+ forward, Y+ left, Z+ up).
|
| 61 |
+
Since some kinematic solvers forbid having inertia values attached to a root link, we've
|
| 62 |
+
added the base_link_inertia link that carries the visual, collision and inertia
|
| 63 |
+
information.
|
| 64 |
+
-->
|
| 65 |
+
<link name="base_link"/>
|
| 66 |
+
<link name="base_link_inertia">
|
| 67 |
+
<visual>
|
| 68 |
+
<origin rpy="0 0 3.141592653589793" xyz="0 0 0"/>
|
| 69 |
+
<geometry>
|
| 70 |
+
<mesh filename="package://ur_description/meshes/ur5e/visual/base.dae"/>
|
| 71 |
+
</geometry>
|
| 72 |
+
</visual>
|
| 73 |
+
<collision>
|
| 74 |
+
<origin rpy="0 0 3.141592653589793" xyz="0 0 0"/>
|
| 75 |
+
<geometry>
|
| 76 |
+
<mesh filename="package://ur_description/meshes/ur5e/collision/base.stl"/>
|
| 77 |
+
</geometry>
|
| 78 |
+
</collision>
|
| 79 |
+
<inertial>
|
| 80 |
+
<mass value="4.0"/>
|
| 81 |
+
<origin rpy="0 0 0" xyz="0 0 0"/>
|
| 82 |
+
<inertia ixx="0.00443333156" ixy="0.0" ixz="0.0" iyy="0.00443333156" iyz="0.0" izz="0.0072"/>
|
| 83 |
+
</inertial>
|
| 84 |
+
</link>
|
| 85 |
+
<link name="shoulder_link">
|
| 86 |
+
<visual>
|
| 87 |
+
<origin rpy="0 0 3.141592653589793" xyz="0 0 0"/>
|
| 88 |
+
<geometry>
|
| 89 |
+
<mesh filename="package://ur_description/meshes/ur5e/visual/shoulder.dae"/>
|
| 90 |
+
</geometry>
|
| 91 |
+
</visual>
|
| 92 |
+
<collision>
|
| 93 |
+
<origin rpy="0 0 3.141592653589793" xyz="0 0 0"/>
|
| 94 |
+
<geometry>
|
| 95 |
+
<mesh filename="package://ur_description/meshes/ur5e/collision/shoulder.stl"/>
|
| 96 |
+
</geometry>
|
| 97 |
+
</collision>
|
| 98 |
+
<inertial>
|
| 99 |
+
<mass value="3.761"/>
|
| 100 |
+
<origin rpy="1.570796326794897 0 0" xyz="0.0 -0.00193 -0.02561"/>
|
| 101 |
+
<inertia ixx="0.0070021" ixy="7.3e-07" ixz="-1.053e-05" iyy="0.00648091" iyz="0.00049994" izz="0.00657286"/>
|
| 102 |
+
</inertial>
|
| 103 |
+
</link>
|
| 104 |
+
<link name="upper_arm_link">
|
| 105 |
+
<visual>
|
| 106 |
+
<origin rpy="1.5707963267948966 0 -1.5707963267948966" xyz="0 0 0.138"/>
|
| 107 |
+
<geometry>
|
| 108 |
+
<mesh filename="package://ur_description/meshes/ur5e/visual/upperarm.dae"/>
|
| 109 |
+
</geometry>
|
| 110 |
+
</visual>
|
| 111 |
+
<collision>
|
| 112 |
+
<origin rpy="1.5707963267948966 0 -1.5707963267948966" xyz="0 0 0.138"/>
|
| 113 |
+
<geometry>
|
| 114 |
+
<mesh filename="package://ur_description/meshes/ur5e/collision/upperarm.stl"/>
|
| 115 |
+
</geometry>
|
| 116 |
+
</collision>
|
| 117 |
+
<inertial>
|
| 118 |
+
<mass value="8.058"/>
|
| 119 |
+
<origin rpy="0 0 0" xyz="-0.2125 0.0 0.11336"/>
|
| 120 |
+
<inertia ixx="0.01505885" ixy="-5.4e-05" ixz="5.63e-06" iyy="0.33388086" iyz="-1.81e-06" izz="0.33247207"/>
|
| 121 |
+
</inertial>
|
| 122 |
+
</link>
|
| 123 |
+
<link name="forearm_link">
|
| 124 |
+
<visual>
|
| 125 |
+
<origin rpy="1.5707963267948966 0 -1.5707963267948966" xyz="0 0 0.007"/>
|
| 126 |
+
<geometry>
|
| 127 |
+
<mesh filename="package://ur_description/meshes/ur5e/visual/forearm.dae"/>
|
| 128 |
+
</geometry>
|
| 129 |
+
</visual>
|
| 130 |
+
<collision>
|
| 131 |
+
<origin rpy="1.5707963267948966 0 -1.5707963267948966" xyz="0 0 0.007"/>
|
| 132 |
+
<geometry>
|
| 133 |
+
<mesh filename="package://ur_description/meshes/ur5e/collision/forearm.stl"/>
|
| 134 |
+
</geometry>
|
| 135 |
+
</collision>
|
| 136 |
+
<inertial>
|
| 137 |
+
<mass value="2.846"/>
|
| 138 |
+
<origin rpy="0 0 0" xyz="-0.2422 0.0 0.0265"/>
|
| 139 |
+
<inertia ixx="0.00399632" ixy="-1.365e-05" ixz="0.00137272" iyy="0.07879254" iyz="-6.6e-06" izz="0.0784851"/>
|
| 140 |
+
</inertial>
|
| 141 |
+
</link>
|
| 142 |
+
<link name="wrist_1_link">
|
| 143 |
+
<visual>
|
| 144 |
+
<origin rpy="1.5707963267948966 0 0" xyz="0 0 -0.127"/>
|
| 145 |
+
<geometry>
|
| 146 |
+
<mesh filename="package://ur_description/meshes/ur5e/visual/wrist1.dae"/>
|
| 147 |
+
</geometry>
|
| 148 |
+
</visual>
|
| 149 |
+
<collision>
|
| 150 |
+
<origin rpy="1.5707963267948966 0 0" xyz="0 0 -0.127"/>
|
| 151 |
+
<geometry>
|
| 152 |
+
<mesh filename="package://ur_description/meshes/ur5e/collision/wrist1.stl"/>
|
| 153 |
+
</geometry>
|
| 154 |
+
</collision>
|
| 155 |
+
<inertial>
|
| 156 |
+
<mass value="1.37"/>
|
| 157 |
+
<origin rpy="1.570796326794897 0 0" xyz="0.0 -0.01634 -0.0018"/>
|
| 158 |
+
<inertia ixx="0.00165491" ixy="-2.82e-06" ixz="-4.38e-06" iyy="0.00135962" iyz="0.00010157" izz="0.00126279"/>
|
| 159 |
+
</inertial>
|
| 160 |
+
</link>
|
| 161 |
+
<link name="wrist_2_link">
|
| 162 |
+
<visual>
|
| 163 |
+
<origin rpy="0 0 0" xyz="0 0 -0.0997"/>
|
| 164 |
+
<geometry>
|
| 165 |
+
<mesh filename="package://ur_description/meshes/ur5e/visual/wrist2.dae"/>
|
| 166 |
+
</geometry>
|
| 167 |
+
</visual>
|
| 168 |
+
<collision>
|
| 169 |
+
<origin rpy="0 0 0" xyz="0 0 -0.0997"/>
|
| 170 |
+
<geometry>
|
| 171 |
+
<mesh filename="package://ur_description/meshes/ur5e/collision/wrist2.stl"/>
|
| 172 |
+
</geometry>
|
| 173 |
+
</collision>
|
| 174 |
+
<inertial>
|
| 175 |
+
<mass value="1.3"/>
|
| 176 |
+
<origin rpy="-1.570796326794897 0 0" xyz="0.0 0.01634 -0.0018"/>
|
| 177 |
+
<inertia ixx="0.00135617" ixy="-2.74e-06" ixz="4.44e-06" iyy="0.00127827" iyz="-5.048e-05" izz="0.00096614"/>
|
| 178 |
+
</inertial>
|
| 179 |
+
</link>
|
| 180 |
+
<link name="wrist_3_link">
|
| 181 |
+
<visual>
|
| 182 |
+
<origin rpy="1.5707963267948966 0 0" xyz="0 -0.0005 -0.0989"/>
|
| 183 |
+
<geometry>
|
| 184 |
+
<mesh filename="package://ur_description/meshes/ur5e/visual/wrist3.dae"/>
|
| 185 |
+
</geometry>
|
| 186 |
+
</visual>
|
| 187 |
+
<collision>
|
| 188 |
+
<origin rpy="1.5707963267948966 0 0" xyz="0 -0.0005 -0.0989"/>
|
| 189 |
+
<geometry>
|
| 190 |
+
<mesh filename="package://ur_description/meshes/ur5e/collision/wrist3.stl"/>
|
| 191 |
+
</geometry>
|
| 192 |
+
</collision>
|
| 193 |
+
<inertial>
|
| 194 |
+
<mass value="0.365"/>
|
| 195 |
+
<origin rpy="0 0 0" xyz="0.0 0.0 -0.001159"/>
|
| 196 |
+
<inertia ixx="0.00018694" ixy="6e-08" ixz="-1.7e-07" iyy="0.00018908" iyz="-9.2e-07" izz="0.00025756"/>
|
| 197 |
+
</inertial>
|
| 198 |
+
</link>
|
| 199 |
+
<!-- base_joint fixes base_link to the environment -->
|
| 200 |
+
<joint name="base_joint" type="fixed">
|
| 201 |
+
<origin rpy="0 0 0" xyz="0 0 0"/>
|
| 202 |
+
<parent link="world"/>
|
| 203 |
+
<child link="base_link"/>
|
| 204 |
+
</joint>
|
| 205 |
+
<!-- joints - main serial chain -->
|
| 206 |
+
<joint name="base_link-base_link_inertia" type="fixed">
|
| 207 |
+
<parent link="base_link"/>
|
| 208 |
+
<child link="base_link_inertia"/>
|
| 209 |
+
<!-- 'base_link' is REP-103 aligned (so X+ forward), while the internal
|
| 210 |
+
frames of the robot/controller have X+ pointing backwards.
|
| 211 |
+
Use the joint between 'base_link' and 'base_link_inertia' (a dummy
|
| 212 |
+
link/frame) to introduce the necessary rotation over Z (of pi rad).
|
| 213 |
+
-->
|
| 214 |
+
<origin rpy="0 0 3.141592653589793" xyz="0 0 0"/>
|
| 215 |
+
</joint>
|
| 216 |
+
<joint name="shoulder_pan_joint" type="revolute">
|
| 217 |
+
<parent link="base_link_inertia"/>
|
| 218 |
+
<child link="shoulder_link"/>
|
| 219 |
+
<origin rpy="0 0 0" xyz="0 0 0.1625"/>
|
| 220 |
+
<axis xyz="0 0 1"/>
|
| 221 |
+
<limit effort="150.0" lower="-6.283185307179586" upper="6.283185307179586" velocity="3.141592653589793"/>
|
| 222 |
+
<dynamics damping="0" friction="0"/>
|
| 223 |
+
</joint>
|
| 224 |
+
<joint name="shoulder_lift_joint" type="revolute">
|
| 225 |
+
<parent link="shoulder_link"/>
|
| 226 |
+
<child link="upper_arm_link"/>
|
| 227 |
+
<origin rpy="1.570796327 0 0" xyz="0 0 0"/>
|
| 228 |
+
<axis xyz="0 0 1"/>
|
| 229 |
+
<limit effort="150.0" lower="-6.283185307179586" upper="6.283185307179586" velocity="3.141592653589793"/>
|
| 230 |
+
<dynamics damping="0" friction="0"/>
|
| 231 |
+
</joint>
|
| 232 |
+
<joint name="elbow_joint" type="revolute">
|
| 233 |
+
<parent link="upper_arm_link"/>
|
| 234 |
+
<child link="forearm_link"/>
|
| 235 |
+
<origin rpy="0 0 0" xyz="-0.425 0 0"/>
|
| 236 |
+
<axis xyz="0 0 1"/>
|
| 237 |
+
<limit effort="150.0" lower="-3.141592653589793" upper="3.141592653589793" velocity="3.141592653589793"/>
|
| 238 |
+
<dynamics damping="0" friction="0"/>
|
| 239 |
+
</joint>
|
| 240 |
+
<joint name="wrist_1_joint" type="revolute">
|
| 241 |
+
<parent link="forearm_link"/>
|
| 242 |
+
<child link="wrist_1_link"/>
|
| 243 |
+
<origin rpy="0 0 0" xyz="-0.3922 0 0.1333"/>
|
| 244 |
+
<axis xyz="0 0 1"/>
|
| 245 |
+
<limit effort="28.0" lower="-6.283185307179586" upper="6.283185307179586" velocity="3.141592653589793"/>
|
| 246 |
+
<dynamics damping="0" friction="0"/>
|
| 247 |
+
</joint>
|
| 248 |
+
<joint name="wrist_2_joint" type="revolute">
|
| 249 |
+
<parent link="wrist_1_link"/>
|
| 250 |
+
<child link="wrist_2_link"/>
|
| 251 |
+
<origin rpy="1.570796327 0 0" xyz="0 -0.0997 -2.044881182297852e-11"/>
|
| 252 |
+
<axis xyz="0 0 1"/>
|
| 253 |
+
<limit effort="28.0" lower="-6.283185307179586" upper="6.283185307179586" velocity="3.141592653589793"/>
|
| 254 |
+
<dynamics damping="0" friction="0"/>
|
| 255 |
+
</joint>
|
| 256 |
+
<joint name="wrist_3_joint" type="revolute">
|
| 257 |
+
<parent link="wrist_2_link"/>
|
| 258 |
+
<child link="wrist_3_link"/>
|
| 259 |
+
<origin rpy="1.570796326589793 3.141592653589793 3.141592653589793" xyz="0 0.0996 -2.042830148012698e-11"/>
|
| 260 |
+
<axis xyz="0 0 1"/>
|
| 261 |
+
<limit effort="28.0" lower="-6.283185307179586" upper="6.283185307179586" velocity="3.141592653589793"/>
|
| 262 |
+
<dynamics damping="0" friction="0"/>
|
| 263 |
+
</joint>
|
| 264 |
+
<link name="ft_frame"/>
|
| 265 |
+
<joint name="wrist_3_link-ft_frame" type="fixed">
|
| 266 |
+
<parent link="wrist_3_link"/>
|
| 267 |
+
<child link="ft_frame"/>
|
| 268 |
+
<origin rpy="3.141592653589793 0 0" xyz="0 0 0"/>
|
| 269 |
+
</joint>
|
| 270 |
+
<!-- ROS-Industrial 'base' frame - base_link to UR 'Base' Coordinates transform -->
|
| 271 |
+
<link name="base"/>
|
| 272 |
+
<joint name="base_link-base_fixed_joint" type="fixed">
|
| 273 |
+
<!-- Note the rotation over Z of pi radians - as base_link is REP-103
|
| 274 |
+
aligned (i.e., has X+ forward, Y+ left and Z+ up), this is needed
|
| 275 |
+
to correctly align 'base' with the 'Base' coordinate system of
|
| 276 |
+
the UR controller.
|
| 277 |
+
-->
|
| 278 |
+
<origin rpy="0 0 3.141592653589793" xyz="0 0 0"/>
|
| 279 |
+
<parent link="base_link"/>
|
| 280 |
+
<child link="base"/>
|
| 281 |
+
</joint>
|
| 282 |
+
<!-- ROS-Industrial 'flange' frame - attachment point for EEF models -->
|
| 283 |
+
<link name="flange"/>
|
| 284 |
+
<joint name="wrist_3-flange" type="fixed">
|
| 285 |
+
<parent link="wrist_3_link"/>
|
| 286 |
+
<child link="flange"/>
|
| 287 |
+
<origin rpy="0 -1.5707963267948966 -1.5707963267948966" xyz="0 0 0"/>
|
| 288 |
+
</joint>
|
| 289 |
+
<!-- ROS-Industrial 'tool0' frame - all-zeros tool frame -->
|
| 290 |
+
<link name="tool0"/>
|
| 291 |
+
<joint name="flange-tool0" type="fixed">
|
| 292 |
+
<!-- default toolframe - X+ left, Y+ up, Z+ front -->
|
| 293 |
+
<origin rpy="1.5707963267948966 0 1.5707963267948966" xyz="0 0 0"/>
|
| 294 |
+
<parent link="flange"/>
|
| 295 |
+
<child link="tool0"/>
|
| 296 |
+
</joint>
|
| 297 |
+
</robot>
|