Bigenlight commited on
Commit
c1dfd46
·
verified ·
1 Parent(s): 8023979

Upload folder using huggingface_hub

Browse files
.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
+ ![training curves](assets/training_curves.png)
81
+
82
+ ![confusion matrix](assets/confusion_matrix.png)
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
+ ![FK validation](assets/fk_validation.png)
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

  • SHA256: 4d8e46865ae4b0aa84f591168272cd8e64e4af82bdc81f6adfca7f98308b68e5
  • Pointer size: 131 Bytes
  • Size of remote file: 266 kB
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>