Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.

Commit 143d39b

Browse files
committed
finish widowx eval code
1 parent 3670ed6 commit 143d39b

8 files changed

Lines changed: 527 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,4 @@ demo_data/demos25
237237

238238
demo_data/libero_spatial_no_noops_1.0.0_lerobot
239239
experiments/test
240+
dev/

.gitmodules

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
[submodule "experiments/7_franka/deoxys_control"]
22
path = experiments/7_franka/deoxys_control
33
url = https://github.com/UT-Austin-RPL/deoxys_control.git
4+
[submodule "experiments/5_widowx/bridge_data_robot"]
5+
path = experiments/5_widowx/bridge_data_robot
6+
url = https://github.com/HaomingSong/bridge_data_robot.git
7+
[submodule "experiments/5_widowx/edgeml"]
8+
path = experiments/5_widowx/edgeml
9+
url = https://github.com/youliangtan/edgeml.git

experiments/5_widowx/README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# WidowX 250s with EO-1
2+
3+
This directory contains the implementation for controlling WidowX 250s robots using the EO-1 model. The system enables real-time robot manipulation tasks through vision-language-action integration.
4+
5+
## 🚀 Quick Start
6+
7+
### Prerequisites
8+
9+
**Hardware Requirements:**
10+
11+
- WidowX 250s robot arm
12+
- RealSense D435 camera (or compatible RGB camera)
13+
- Compute options:
14+
- Single GPU workstation (runs both ROS control and model inference)
15+
- OR: NUC + GPU workstation (NUC for arm control, workstation for model inference)
16+
17+
**Software Requirements:**
18+
19+
- Ubuntu 20.04+ with CUDA support
20+
- Python 3.10+
21+
- Docker (recommended for running the WidowX ROS control node on a workstation in single-machine mode)
22+
- BridgeData WidowX controller stack properly configured
23+
24+
Notes on architecture:
25+
26+
- `Single-machine mode`: Run the WidowX ROS control node in Docker on the same GPU workstation used for EO-1 inference.
27+
- `Dual-machine mode`: Use a NUC for robot control and a GPU workstation for model inference. For WidowX, the NUC does not require a real-time kernel in this setup.
28+
29+
### Installation
30+
31+
1. **Setup submodules:**
32+
33+
```bash
34+
git submodule update --init --recursive experiments/5_widowx/bridge_data_robot
35+
git submodule update --init --recursive experiments/5_widowx/edgeml
36+
```
37+
38+
2. **Configure robot control system:**
39+
Follow the BridgeData WidowX controller setup in [bridge_data_robot](https://github.com/HaomingSong/bridge_data_robot?tab=readme-ov-file#setup) to configure your NUC/workstation for WidowX 250s control:
40+
41+
3. **Install dependencies on workstation**
42+
43+
```bash
44+
# Create conda environment
45+
conda create -n eo python=3.10
46+
conda activate eo
47+
48+
# Install WidowX envs for workstation
49+
pip install -e experiments/5_widowx/bridge_data_robot/widowx_envs
50+
pip install -e experiments/5_widowx/edgeml
51+
52+
# Install additional requirements
53+
pip install -r experiments/5_widowx/requirements.txt
54+
```
55+
56+
**Note**: In dual-machine mode, ensure the workstation can reach the control host (robot IP/port) over the network. In single-machine mode, ensure Docker has access to USB and camera devices.
57+
58+
## 🤖 Running Robot Control
59+
60+
### Basic Usage
61+
62+
```bash
63+
python experiments/5_widowx/eval_widowx.py \
64+
--model-path "path/to/your/model" \
65+
--repo-id libero_spatial_no_noops_1.0.0_lerobot \
66+
--default-instruction "Put the eggplant in the basket" \
67+
--robot-ip 10.6.8.122 \
68+
--robot-port 5556 \
69+
--max-timesteps 120
70+
```
71+
72+
### Parameters
73+
74+
| Parameter | Description | Default |
75+
| ----------------------- | ----------------------------------------- | -------------------------------- |
76+
| `--model-path` | Path to the trained EO-1 model checkpoint | Required |
77+
| `--repo-id` | Dataset/repo ID for task specification | Required |
78+
| `--default-instruction` | Default natural language instruction | "Put the eggplant in the basket" |
79+
| `--roll-out-path` | Directory to save rollouts/videos | experiments/5_widowx/logs |
80+
| `--max-timesteps` | Maximum number of control steps | 120 |
81+
| `--im-size` | Image size for model input | 224 |
82+
| `--action-horizon` | Receding-horizon (RHC) execution steps | 2 |
83+
| `--blocking` | Use blocking control for step execution | False |
84+
| `--robot-ip` | Robot/control host IP | 10.6.8.122 |
85+
| `--robot-port` | Robot/control host port | 5556 |
86+
87+
### Camera Configuration
88+
89+
- Default color topic for RealSense D435 is `/D435/color/image_raw` (see `CAMERA_TOPICS` in `eval_widowx.py`).
90+
- Mount and wire the D435 according to the hardware guide: [BridgeData V2 Hardware Setup](https://docs.google.com/document/d/1si-6cTElTWTgflwcZRPfgHU7-UwfCUkEztkH3ge5CGc/edit?tab=t.0).
91+
- If your camera topic differs, update `CAMERA_TOPICS` or the controller configuration accordingly.
92+
93+
## 🔒 Safety Considerations
94+
95+
- Always ensure proper workspace setup and clear the workspace before operation.
96+
- Monitor robot movements and be ready to use the emergency stop.
97+
- Verify camera positioning and exposure for optimal visual coverage.
98+
99+
## 📝 Notes
100+
101+
- This setup uses a single external D435 stream by default; wrist camera is optional.
102+
- Model performance depends on lighting, viewpoint, and calibration quality.
103+
- Regular calibration of the robot and camera(s) is recommended.
104+
- Rollouts and videos are saved under `--roll-out-path`.
Submodule bridge_data_robot added at b841131

experiments/5_widowx/edgeml

Submodule edgeml added at b4b8495
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
"""
2+
This script shows how we evaluated a finetuned EO-1 on a real WidowX robot, which is adapted from https://github.com/octo-models/octo/blob/main/examples/04_eval_finetuned_on_robot.py.
3+
While the exact specifics may not be applicable to your use case, this script serves as a didactic example of how to use EO-1 in a real-world setting.
4+
5+
If you wish, you may reproduce these results by [reproducing the robot setup](https://rail-berkeley.github.io/bridgedata/)
6+
and installing [the robot controller](https://github.com/HaomingSong/bridge_data_robot.git)
7+
"""
8+
9+
import os
10+
11+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
12+
import dataclasses
13+
import pathlib
14+
import time
15+
from datetime import datetime
16+
17+
import cv2
18+
import imageio
19+
import numpy as np
20+
import pandas as pd
21+
import torch
22+
import tqdm
23+
import tyro
24+
from PIL import Image
25+
from transformers import AutoModel, AutoProcessor
26+
from widowx_env import RHCWrapper, WidowXGym
27+
from widowx_envs.widowx_env_service import WidowXConfigs
28+
29+
30+
@dataclasses.dataclass
31+
class Args:
32+
#################################################################################################################
33+
# Model parameters
34+
#################################################################################################################
35+
im_size: int = 224
36+
action_horizon: int = 2
37+
model_path: str = ""
38+
repo_id: str = ""
39+
40+
#################################################################################################################
41+
# WidowX environment-specific parameters
42+
#################################################################################################################
43+
robot_ip: str = "10.6.8.122" # IP address of the robot
44+
robot_port: int = 5556 # Port of the robot
45+
initial_eep: tuple[float, float, float] = (0.3, 0.0, 0.25) # Initial position
46+
# initial_eep: tuple[float, float, float] = (0.15, 0.0, 0.1) # Initial position
47+
blocking: bool = False # Use the blocking controller
48+
max_timesteps: int = 120 # Number of timesteps to run
49+
default_instruction: str = "Put the eggplant in the basket" # Default instruction
50+
51+
#################################################################################################################
52+
# Utils
53+
#################################################################################################################
54+
show_image: bool = False # Show image
55+
roll_out_path: pathlib.Path = pathlib.Path("experiments/5_widowx/logs") # Path to save videos
56+
57+
58+
##############################################################################
59+
STEP_DURATION_MESSAGE = """
60+
Bridge data was collected with non-blocking control and a step duration of 0.2s.
61+
However, we relabel the actions to make it look like the data was collected with
62+
blocking control and we evaluate with blocking control.
63+
Be sure to use a step duration of 0.2 if evaluating with non-blocking control.
64+
"""
65+
STEP_DURATION = 0.2
66+
STICKY_GRIPPER_NUM_STEPS = 1
67+
WORKSPACE_BOUNDS = [[0.1, -0.15, -0.01, -1.57, 0], [0.45, 0.25, 0.25, 1.57, 0]]
68+
CAMERA_TOPICS = [{"name": "/D435/color/image_raw"}]
69+
ENV_PARAMS = {
70+
"camera_topics": CAMERA_TOPICS,
71+
"override_workspace_boundaries": WORKSPACE_BOUNDS,
72+
"move_duration": STEP_DURATION,
73+
}
74+
75+
##############################################################################
76+
77+
78+
def eval_bridge(args: Args) -> None:
79+
curr_time = datetime.now().strftime("%Y_%m_%d_%H:%M:%S")
80+
base_save_path = args.roll_out_path / pathlib.Path(args.default_instruction.replace(" ", "_")) / curr_time
81+
82+
# set up the widowx client
83+
start_state = np.concatenate([args.initial_eep, (0, 0, 0, 1)])
84+
env_params = WidowXConfigs.DefaultEnvParams.copy()
85+
env_params.update(ENV_PARAMS)
86+
env_params["start_state"] = list(start_state)
87+
88+
env = WidowXGym(
89+
env_params,
90+
host=args.robot_ip,
91+
port=args.robot_port,
92+
im_size=args.im_size,
93+
blocking=args.blocking,
94+
sticky_gripper_num_steps=STICKY_GRIPPER_NUM_STEPS,
95+
)
96+
if not args.blocking:
97+
assert STEP_DURATION == 0.2, STEP_DURATION_MESSAGE
98+
results_df = pd.DataFrame(columns=["success", "duration", "video_filename"])
99+
100+
model = (
101+
AutoModel.from_pretrained(args.model_path, dtype=torch.bfloat16, trust_remote_code=True).eval().cuda()
102+
)
103+
104+
processor = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True)
105+
106+
# switch TemporalEnsembleWrapper with RHCWrapper for receding horizon control
107+
env = RHCWrapper(env, args.action_horizon)
108+
109+
while True:
110+
# reset env
111+
obs, _ = env.reset()
112+
time.sleep(2.0)
113+
114+
if input(f"Use default instruction: {args.default_instruction}? (default y) [y/n]").lower() == "n":
115+
instruction = input("Enter instruction: ")
116+
else:
117+
instruction = args.default_instruction
118+
119+
# do rollout
120+
images = []
121+
images.append(obs["full_image"])
122+
last_tstep = time.time()
123+
bar = tqdm.tqdm(
124+
range(args.max_timesteps),
125+
position=0,
126+
leave=True,
127+
ncols=80,
128+
desc="Rollout steps",
129+
)
130+
131+
for t_step in bar:
132+
try:
133+
bar.set_description(f"Step {t_step}/{args.max_timesteps}")
134+
if args.show_image:
135+
cv2.imshow("img_view", obs["full_image"])
136+
cv2.waitKey(1)
137+
138+
# prepare observation
139+
# image = torch.from_numpy(obs["image_primary"] / 255).permute(2, 0, 1)
140+
# [::-1, ::-1]
141+
image = cv2.resize(obs["full_image"], (256, 256), interpolation=cv2.INTER_LINEAR)
142+
# image = np.ascontiguousarray(obs["image_primary"])
143+
144+
# print("image",image.shape)
145+
img = Image.fromarray(image)
146+
batch = {
147+
"observation.images.image": [img],
148+
"observation.images.wrist_image": [img],
149+
"observation.state": [obs["proprio"]],
150+
"task": [str(instruction)],
151+
"repo_id": [args.repo_id],
152+
}
153+
ov_out = processor.select_action(model, batch)
154+
action_chunk = ov_out.action.squeeze(0).numpy()
155+
156+
assert len(action_chunk) >= args.action_horizon, (
157+
f"We want to replan every {args.action_horizon} steps, but policy only predicts {len(action_chunk)} steps."
158+
)
159+
160+
# perform environment step
161+
obs, _, _, truncated, infos = env.step(action_chunk)
162+
163+
# recording history images
164+
for history_obs in infos["observations"]:
165+
image = history_obs["full_image"]
166+
images.append(image)
167+
if truncated:
168+
break
169+
170+
# match the step duration
171+
elapsed_time = time.time() - last_tstep
172+
if elapsed_time < STEP_DURATION:
173+
time.sleep(STEP_DURATION - elapsed_time)
174+
175+
except KeyboardInterrupt:
176+
break
177+
time.sleep(0.2)
178+
179+
# logging rollouts
180+
success: str | float | None = None
181+
while not isinstance(success, float):
182+
success = input(
183+
"Did the rollout succeed? (enter y for 100%, n for 0%, a float value 0-1, or a numeric value 0-100 based on the evaluation spec)"
184+
)
185+
try:
186+
if success == "y":
187+
success = 1.0
188+
elif success == "n":
189+
success = 0.0
190+
else:
191+
success = float(success)
192+
except Exception:
193+
success = 0.0
194+
195+
video_save_path = (
196+
base_save_path
197+
/ "videos"
198+
/ f"{datetime.now().strftime('%Y_%m_%d-%H_%M_%S')}_success_{success:.2f}.mp4"
199+
)
200+
201+
if not (0 <= success <= 1):
202+
print(f"Success must be a number in [0, 100] but got: {success * 100}")
203+
204+
results_df = pd.concat(
205+
[
206+
results_df,
207+
pd.DataFrame(
208+
[
209+
{
210+
"instruction": instruction,
211+
"success": success,
212+
"duration": t_step,
213+
"video_filename": video_save_path,
214+
"model_path": args.model_path,
215+
"repo_id": args.repo_id,
216+
}
217+
]
218+
),
219+
],
220+
ignore_index=True,
221+
)
222+
223+
# saving video
224+
video = np.stack(images)
225+
video_save_path.parent.mkdir(parents=True, exist_ok=True)
226+
imageio.mimsave(video_save_path, video, fps=1.0 / STEP_DURATION * 3)
227+
228+
if (
229+
input(f"Already eval {len(results_df)} rollouts. Do one more eval (default y)? [y/n]").lower()
230+
== "n"
231+
):
232+
break
233+
234+
# save results
235+
csv_filename = base_save_path / "results.csv"
236+
results_df.to_csv(csv_filename, index=False)
237+
print(f"Results saved to {csv_filename}")
238+
# print avg
239+
print(f"Avg success: {results_df['success'].mean()}")
240+
241+
242+
if __name__ == "__main__":
243+
import logging
244+
245+
logging.basicConfig(level=logging.INFO)
246+
args: Args = tyro.cli(Args)
247+
eval_bridge(args)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
gym
2+
funcsigs
3+
numpy==1.24.3

0 commit comments

Comments
 (0)