Abha2001 commited on
Commit
752c314
·
verified ·
1 Parent(s): 342d655

Initial upload of BFN-hybrid + DDPM PushT-xarm policies

Browse files
README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - robotics
5
+ - bfn
6
+ - bayesian-flow-networks
7
+ - diffusion-policy
8
+ - pusht
9
+ - xarm
10
+ - hybrid-action
11
+ ---
12
+
13
+ # PushT-xarm Real-Robot Policies (BFN-Hybrid vs DDPM-OneHot)
14
+
15
+ Real-robot push-T policies trained on [borueihuang/pusht_xarm_merged](https://huggingface.co/datasets/borueihuang/pusht_xarm_merged).
16
+ Both policies were trained on 144 episodes / 7835 frames at 30 Hz, top camera only, 200 epochs.
17
+
18
+ ## Action space
19
+
20
+ - **Discrete:** 8 push directions (`action.direction` in {0..7})
21
+ - **Continuous:** push distance (`action.distance` in `[0, 50]`)
22
+
23
+ ## Observation space
24
+
25
+ - `camera_0`: top-view RGB image, 3x224x224, two-step history (`n_obs_steps=2`)
26
+
27
+ ## Policies
28
+
29
+ | File | Method | Action treatment | Inference steps |
30
+ |------|--------|------------------|-----------------|
31
+ | `bfn/latest.ckpt` | **BFN-Hybrid** (categorical + continuous Bayesian flow) | true hybrid | 20 |
32
+ | `ddpm/latest.ckpt` | **DDPM** | one-hot continuous (9D) | 100 |
33
+
34
+ ## Quick start (BFN)
35
+
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ python inference.py --ckpt bfn/latest.ckpt --config bfn/policy_config.yaml
39
+ ```
40
+
41
+ Programmatic use:
42
+
43
+ ```python
44
+ from inference import load_bfn_policy, infer_step
45
+
46
+ policy = load_bfn_policy("bfn/latest.ckpt", "bfn/policy_config.yaml", "cuda")
47
+ actions = infer_step(policy, cam0_now, cam0_prev, "cuda")
48
+ # actions: List[{"direction": int 0..7, "distance": float 0..50}], len = n_action_steps (8)
49
+ ```
50
+
51
+ ## DDPM checkpoint
52
+
53
+ The DDPM policy uses `diffusion_policy.policy.diffusion_unet_hybrid_image_policy.DiffusionUnetHybridImagePolicy`
54
+ from the `diffusion-policy` library. Action is a 9D continuous vector: `[one_hot(8), distance]`.
55
+ At inference time, take `argmax` of the first 8 dims for the direction, and the 9th dim for distance.
56
+
57
+ ## Files
58
+
59
+ ```
60
+ bfn/
61
+ latest.ckpt
62
+ policy_config.yaml
63
+ ddpm/
64
+ latest.ckpt
65
+ policy_config.yaml
66
+ bfn_hybrid_image_policy.py # standalone BFN policy class
67
+ policies/base.py # BasePolicy abstract class
68
+ networks/base.py # BFNetwork wrapper
69
+ inference.py # example loader + inference
70
+ requirements.txt
71
+ ```
bfn/latest.ckpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bd3e457f54d235da8d379d0e1771ffeb1a2d2e420b5fdd86212c1cdfb62777b7
3
+ size 697951714
bfn/policy_config.yaml ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: workspaces.train_bfn_workspace.TrainBFNWorkspace
2
+ name: train_bfn_pusht_xarm_top
3
+ task_name: pusht_xarm
4
+ exp_name: bfn_hybrid_top
5
+ shape_meta:
6
+ obs:
7
+ camera_0:
8
+ shape:
9
+ - 3
10
+ - 224
11
+ - 224
12
+ type: rgb
13
+ action:
14
+ shape:
15
+ - 2
16
+ horizon: 16
17
+ n_obs_steps: 2
18
+ n_action_steps: 8
19
+ n_latency_steps: 0
20
+ dataset_obs_steps: 2
21
+ past_action_visible: false
22
+ obs_as_global_cond: true
23
+ policy:
24
+ _target_: policies.bfn_hybrid_image_policy.BFNHybridImagePolicy
25
+ shape_meta: ${shape_meta}
26
+ horizon: ${horizon}
27
+ n_action_steps: ${n_action_steps}
28
+ n_obs_steps: ${n_obs_steps}
29
+ num_discrete_actions: 8
30
+ continuous_param_dim: 1
31
+ sigma_1: 0.001
32
+ beta_1: 0.2
33
+ n_timesteps: 20
34
+ crop_shape:
35
+ - 216
36
+ - 216
37
+ obs_encoder_group_norm: true
38
+ eval_fixed_crop: true
39
+ diffusion_step_embed_dim: 128
40
+ down_dims:
41
+ - 256
42
+ - 512
43
+ - 1024
44
+ kernel_size: 5
45
+ n_groups: 8
46
+ cond_predict_scale: true
47
+ task:
48
+ dataset:
49
+ _target_: dataset.pusht_xarm_dataset.PushTXArmDataset
50
+ zarr_path: data/pusht_xarm_merged/replay.zarr
51
+ horizon: ${horizon}
52
+ pad_before: 1
53
+ pad_after: 7
54
+ n_obs_steps: ${n_obs_steps}
55
+ seed: 42
56
+ val_ratio: 0.1
57
+ cameras:
58
+ - camera_0
59
+ action_mode: hybrid
60
+ env_runner: null
61
+ ema:
62
+ _target_: diffusion_policy.model.diffusion.ema_model.EMAModel
63
+ update_after_step: 0
64
+ inv_gamma: 1.0
65
+ power: 0.75
66
+ min_value: 0.0
67
+ max_value: 0.9999
68
+ optimizer:
69
+ _target_: torch.optim.AdamW
70
+ lr: 0.0001
71
+ betas:
72
+ - 0.95
73
+ - 0.999
74
+ eps: 1.0e-08
75
+ weight_decay: 1.0e-06
76
+ training:
77
+ device: cuda:0
78
+ seed: 42
79
+ debug: false
80
+ resume: false
81
+ lr_scheduler: cosine
82
+ lr_warmup_steps: 500
83
+ num_epochs: 200
84
+ gradient_accumulate_every: 1
85
+ use_ema: true
86
+ rollout_every: 50
87
+ checkpoint_every: 50
88
+ val_every: 1
89
+ sample_every: 5
90
+ max_train_steps: null
91
+ max_val_steps: null
92
+ tqdm_interval_sec: 1.0
93
+ dataloader:
94
+ batch_size: 32
95
+ num_workers: 4
96
+ shuffle: true
97
+ pin_memory: true
98
+ persistent_workers: false
99
+ val_dataloader:
100
+ batch_size: 32
101
+ num_workers: 2
102
+ shuffle: false
103
+ pin_memory: true
104
+ persistent_workers: false
105
+ checkpoint:
106
+ topk:
107
+ monitor_key: train_loss
108
+ mode: min
109
+ k: 1
110
+ format_str: epoch={epoch:04d}-train_loss={train_loss:.4f}.ckpt
111
+ save_last_ckpt: true
112
+ save_last_snapshot: false
113
+ logging:
114
+ project: pusht_xarm_bfn
115
+ resume: true
116
+ mode: offline
117
+ name: ${now:%Y.%m.%d-%H.%M.%S}_${name}
118
+ tags:
119
+ - bfn
120
+ - pusht_xarm
121
+ - hybrid
122
+ - top
123
+ id: null
124
+ group: null
125
+ multi_run:
126
+ run_dir: data/outputs/${now:%Y.%m.%d}/${now:%H.%M.%S}_${name}
127
+ wandb_name_base: ${now:%Y.%m.%d-%H.%M.%S}_${name}
bfn_hybrid_image_policy.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BFN Hybrid Image Policy: image observations + categorical-discrete + continuous action heads.
2
+
3
+ This is the policy for real-robot PushT with hybrid action space:
4
+ - Discrete: 8 push directions
5
+ - Continuous: push distance
6
+ - Observation: one or more RGB cameras (cam0 top, cam1 side)
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from typing import Any, Dict, List, Optional, Tuple, Union
12
+
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+
17
+ from diffusion_policy.model.common.normalizer import LinearNormalizer
18
+ from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D
19
+ from diffusion_policy.common.pytorch_util import dict_apply
20
+
21
+ from policies.base import BasePolicy
22
+ from networks.base import BFNetwork
23
+
24
+ try:
25
+ import robomimic.models.obs_core as rmbn
26
+ import diffusion_policy.model.vision.crop_randomizer as dmvc
27
+ from diffusion_policy.common.pytorch_util import replace_submodules
28
+ import robomimic.utils.obs_utils as ObsUtils
29
+ from robomimic.config import config_factory
30
+ from robomimic.algo import algo_factory, PolicyAlgo
31
+ from diffusion_policy.common.robomimic_config_util import get_robomimic_config
32
+ HAS_ROBOMIMIC = True
33
+ except ImportError:
34
+ HAS_ROBOMIMIC = False
35
+
36
+
37
+ __all__ = ["BFNHybridImagePolicy"]
38
+
39
+
40
+ class HybridUnetWrapper(BFNetwork):
41
+ def __init__(self, model, horizon, continuous_dim, discrete_configs, cond_dim):
42
+ super().__init__(is_conditional_model=True)
43
+ self.model = model
44
+ self.horizon = horizon
45
+ self.continuous_dim = continuous_dim
46
+ self.discrete_configs = discrete_configs
47
+ self.cond_dim = cond_dim
48
+ self.cond_is_discrete = False
49
+ total_disc = sum(n for _, n in discrete_configs)
50
+ self.input_dim = continuous_dim + total_disc
51
+
52
+ def forward(self, x, t, cond=None):
53
+ B = x.shape[0]
54
+ x = x.view(B, self.horizon, self.input_dim)
55
+ out = self.model(x, t, global_cond=cond)
56
+ return out.reshape(B, -1)
57
+
58
+
59
+ class BFNHybridImagePolicy(BasePolicy):
60
+ """BFN policy with image observations + hybrid (categorical + continuous) action head."""
61
+
62
+ def __init__(
63
+ self,
64
+ shape_meta: dict,
65
+ horizon: int = 16,
66
+ n_action_steps: int = 8,
67
+ n_obs_steps: int = 2,
68
+ num_discrete_actions: int = 8,
69
+ continuous_param_dim: int = 1,
70
+ sigma_1: float = 0.001,
71
+ beta_1: float = 0.2,
72
+ n_timesteps: int = 20,
73
+ crop_shape: tuple = (216, 216),
74
+ obs_encoder_group_norm: bool = True,
75
+ eval_fixed_crop: bool = True,
76
+ diffusion_step_embed_dim: int = 128,
77
+ down_dims: tuple = (256, 512, 1024),
78
+ kernel_size: int = 5,
79
+ n_groups: int = 8,
80
+ cond_predict_scale: bool = True,
81
+ device: str = "cpu",
82
+ dtype: str = "float32",
83
+ clip_actions: bool = True,
84
+ **kwargs,
85
+ ):
86
+ super().__init__(action_space=None, device=device, dtype=dtype, clip_actions=clip_actions)
87
+
88
+ self.horizon = horizon
89
+ self.n_action_steps = n_action_steps
90
+ self.n_obs_steps = n_obs_steps
91
+ self.num_discrete_actions = num_discrete_actions
92
+ self.continuous_dim = continuous_param_dim
93
+ self.discrete_configs = [(0, num_discrete_actions)]
94
+ self.discrete_action_indices = {0}
95
+ self.total_action_dim = 1 + continuous_param_dim
96
+ self.sigma_1 = sigma_1
97
+ self.beta_1 = beta_1
98
+ self.n_timesteps = n_timesteps
99
+
100
+ # Parse shape_meta for image obs keys
101
+ obs_shape_meta = shape_meta["obs"]
102
+ obs_config = {"low_dim": [], "rgb": [], "depth": [], "scan": []}
103
+ obs_key_shapes = {}
104
+ self.rgb_keys: List[str] = []
105
+ for key, attr in obs_shape_meta.items():
106
+ obs_key_shapes[key] = list(attr["shape"])
107
+ t = attr.get("type", "low_dim")
108
+ if t == "rgb":
109
+ obs_config["rgb"].append(key)
110
+ self.rgb_keys.append(key)
111
+ elif t == "low_dim":
112
+ obs_config["low_dim"].append(key)
113
+ else:
114
+ raise ValueError(f"Unsupported obs type: {t}")
115
+ assert HAS_ROBOMIMIC, "robomimic required for image policy"
116
+
117
+ self.obs_encoder = self._build_robomimic_encoder(
118
+ obs_config, obs_key_shapes, crop_shape, obs_encoder_group_norm, eval_fixed_crop
119
+ )
120
+ obs_feature_dim = self.obs_encoder.output_shape()[0]
121
+ global_cond_dim = obs_feature_dim * n_obs_steps
122
+
123
+ # U-Net input/output dim = continuous + discrete-logits
124
+ unet_dim = continuous_param_dim + num_discrete_actions
125
+
126
+ self.model = ConditionalUnet1D(
127
+ input_dim=unet_dim,
128
+ local_cond_dim=None,
129
+ global_cond_dim=global_cond_dim,
130
+ diffusion_step_embed_dim=diffusion_step_embed_dim,
131
+ down_dims=list(down_dims),
132
+ kernel_size=kernel_size,
133
+ n_groups=n_groups,
134
+ cond_predict_scale=cond_predict_scale,
135
+ )
136
+ self.unet_wrapper = HybridUnetWrapper(
137
+ model=self.model,
138
+ horizon=horizon,
139
+ continuous_dim=continuous_param_dim,
140
+ discrete_configs=self.discrete_configs,
141
+ cond_dim=global_cond_dim,
142
+ )
143
+ self.normalizer = LinearNormalizer()
144
+ self.global_cond_dim = global_cond_dim
145
+
146
+ print(f"BFN Hybrid Image Policy:")
147
+ print(f" cameras: {self.rgb_keys}")
148
+ print(f" discrete: {num_discrete_actions}, continuous: {continuous_param_dim}")
149
+ print(f" obs_feature_dim: {obs_feature_dim}, global_cond_dim: {global_cond_dim}")
150
+ print(f" U-Net params: {sum(p.numel() for p in self.model.parameters()):.2e}")
151
+ print(f" Vision params: {sum(p.numel() for p in self.obs_encoder.parameters()):.2e}")
152
+
153
+ def _build_robomimic_encoder(self, obs_config, obs_key_shapes, crop_shape, group_norm, eval_fixed_crop):
154
+ config = get_robomimic_config(algo_name="bc_rnn", hdf5_type="image", task_name="square", dataset_type="ph")
155
+ with config.unlocked():
156
+ config.observation.modalities.obs = obs_config
157
+ if crop_shape is None:
158
+ for key, modality in config.observation.encoder.items():
159
+ if modality.obs_randomizer_class == "CropRandomizer":
160
+ modality["obs_randomizer_class"] = None
161
+ else:
162
+ ch, cw = crop_shape
163
+ for key, modality in config.observation.encoder.items():
164
+ if modality.obs_randomizer_class == "CropRandomizer":
165
+ modality.obs_randomizer_kwargs.crop_height = ch
166
+ modality.obs_randomizer_kwargs.crop_width = cw
167
+ ObsUtils.initialize_obs_utils_with_config(config)
168
+ policy: PolicyAlgo = algo_factory(
169
+ algo_name=config.algo_name, config=config,
170
+ obs_key_shapes=obs_key_shapes, ac_dim=1 + self.num_discrete_actions, device="cpu",
171
+ )
172
+ obs_encoder = policy.nets["policy"].nets["encoder"].nets["obs"]
173
+ if group_norm:
174
+ replace_submodules(
175
+ root_module=obs_encoder,
176
+ predicate=lambda x: isinstance(x, nn.BatchNorm2d),
177
+ func=lambda x: nn.GroupNorm(num_groups=x.num_features // 16, num_channels=x.num_features),
178
+ )
179
+ if eval_fixed_crop:
180
+ replace_submodules(
181
+ root_module=obs_encoder,
182
+ predicate=lambda x: isinstance(x, rmbn.CropRandomizer),
183
+ func=lambda x: dmvc.CropRandomizer(
184
+ input_shape=x.input_shape, crop_height=x.crop_height,
185
+ crop_width=x.crop_width, num_crops=x.num_crops, pos_enc=x.pos_enc,
186
+ ),
187
+ )
188
+ return obs_encoder
189
+
190
+ def set_normalizer(self, normalizer: LinearNormalizer):
191
+ self.normalizer.load_state_dict(normalizer.state_dict())
192
+
193
+ def _encode_obs(self, nobs: Dict[str, torch.Tensor]) -> torch.Tensor:
194
+ """Encode obs dict to [B, global_cond_dim]."""
195
+ B = nobs[self.rgb_keys[0]].shape[0]
196
+ To = self.n_obs_steps
197
+ # Stack across time: build dict of [B*To, C, H, W]
198
+ flat = {}
199
+ for k, v in nobs.items():
200
+ v_t = v[:, :To] # [B, To, C, H, W]
201
+ flat[k] = v_t.reshape(B * To, *v_t.shape[2:])
202
+ feats = self.obs_encoder(flat) # [B*To, feat_dim]
203
+ feats = feats.reshape(B, To, -1)
204
+ return feats.reshape(B, -1)
205
+
206
+ def forward(self, obs, *, deterministic: bool = False, **kwargs):
207
+ if isinstance(obs, torch.Tensor):
208
+ obs = {"obs": obs}
209
+ return self.predict_action(obs)["action"]
210
+
211
+ def predict_action(self, obs_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
212
+ nobs = self.normalizer.normalize(obs_dict)
213
+ cond = self._encode_obs(nobs)
214
+ B = cond.shape[0]
215
+ device = cond.device
216
+ dtype = cond.dtype
217
+ naction = self._sample_hybrid_bfn(B, self.horizon, cond, device, dtype)
218
+ start = self.n_obs_steps - 1
219
+ end = start + self.n_action_steps
220
+ action = naction[:, start:end]
221
+ action_unnorm = action.clone()
222
+ if action.shape[-1] > 1:
223
+ full_unnorm = self.normalizer["action"].unnormalize(action.clone())
224
+ action_unnorm[:, :, 1:] = full_unnorm[:, :, 1:]
225
+ return {"action": action_unnorm, "action_pred": naction}
226
+
227
+ @torch.no_grad()
228
+ def _sample_hybrid_bfn(self, B, T, cond, device, dtype):
229
+ n_steps = self.n_timesteps
230
+ cont_dim = self.continuous_dim
231
+ disc_configs = self.discrete_configs
232
+
233
+ mu_cont = torch.zeros(B, T, cont_dim, device=device, dtype=dtype)
234
+ rho_cont = 1.0
235
+ theta_list = [
236
+ torch.full((B, T, n), 1.0 / n, device=device, dtype=dtype) for _, n in disc_configs
237
+ ]
238
+
239
+ for i in range(1, n_steps + 1):
240
+ t_val = (i - 1) / n_steps
241
+ t_batch = torch.full((B,), t_val, device=device, dtype=dtype)
242
+ net_input = torch.cat([mu_cont, *theta_list], dim=-1) if theta_list else mu_cont
243
+ out_flat = self.unet_wrapper(net_input.reshape(B, -1), t_batch, cond=cond)
244
+ out = out_flat.reshape(B, T, -1)
245
+
246
+ x_cont_pred = out[:, :, :cont_dim]
247
+ alpha_cont = (self.sigma_1 ** (-2.0 * i / n_steps)) * (1.0 - self.sigma_1 ** (2.0 / n_steps))
248
+ sender_std = 1.0 / (alpha_cont ** 0.5 + 1e-8)
249
+ y_cont = x_cont_pred + sender_std * torch.randn_like(x_cont_pred)
250
+ new_rho = rho_cont + alpha_cont
251
+ mu_cont = (rho_cont * mu_cont + alpha_cont * y_cont) / new_rho
252
+ rho_cont = new_rho
253
+
254
+ alpha_disc = self.beta_1 * (2 * i - 1) / (n_steps ** 2)
255
+ offset = cont_dim
256
+ new_theta_list = []
257
+ for j, (_, n_classes) in enumerate(disc_configs):
258
+ logits = out[:, :, offset:offset + n_classes]
259
+ probs = torch.softmax(logits, dim=-1)
260
+ probs_flat = probs.reshape(-1, n_classes)
261
+ k_samples = torch.multinomial(probs_flat, num_samples=1).squeeze(-1).reshape(B, T)
262
+ e_k = F.one_hot(k_samples, num_classes=n_classes).float()
263
+ y_mean = alpha_disc * (n_classes * e_k - 1)
264
+ y_std = (alpha_disc * n_classes + 1e-8) ** 0.5
265
+ y_disc = y_mean + y_std * torch.randn_like(y_mean)
266
+ log_theta = torch.log(theta_list[j] + 1e-8)
267
+ theta_new = torch.softmax(log_theta + y_disc, dim=-1)
268
+ new_theta_list.append(theta_new)
269
+ offset += n_classes
270
+ theta_list = new_theta_list
271
+
272
+ # Final
273
+ t_final = torch.ones(B, device=device, dtype=dtype)
274
+ net_input = torch.cat([mu_cont, *theta_list], dim=-1) if theta_list else mu_cont
275
+ out_final = self.unet_wrapper(net_input.reshape(B, -1), t_final, cond=cond).reshape(B, T, -1)
276
+ x_cont_final = out_final[:, :, :cont_dim].clamp(-1.0, 1.0)
277
+
278
+ disc_values = []
279
+ offset = cont_dim
280
+ for j, (_, n_classes) in enumerate(disc_configs):
281
+ logits = out_final[:, :, offset:offset + n_classes]
282
+ disc_values.append(logits.argmax(dim=-1).float().unsqueeze(-1))
283
+ offset += n_classes
284
+
285
+ if disc_values:
286
+ return torch.cat([torch.cat(disc_values, dim=-1), x_cont_final], dim=-1)
287
+ return x_cont_final
288
+
289
+ def compute_loss(self, batch: Dict[str, torch.Tensor]) -> torch.Tensor:
290
+ nobs = self.normalizer.normalize(batch["obs"])
291
+ cond = self._encode_obs(nobs)
292
+ raw_action = batch["action"]
293
+ discrete_k = raw_action[:, :, 0].long()
294
+ naction = self.normalizer["action"].normalize(raw_action)
295
+ continuous_x = naction[:, :, 1:]
296
+
297
+ B = raw_action.shape[0]
298
+ T = self.horizon
299
+ device = raw_action.device
300
+ dtype = raw_action.dtype
301
+
302
+ t = torch.rand(B, device=device, dtype=dtype).clamp(min=1e-5, max=1.0 - 1e-5)
303
+ t_exp = t.view(B, 1, 1)
304
+ gamma = 1.0 - (self.sigma_1 ** (2.0 * t_exp))
305
+ var = gamma * (1.0 - gamma)
306
+ std = (var + 1e-8).sqrt()
307
+ mu_cont = gamma * continuous_x + std * torch.randn_like(continuous_x)
308
+
309
+ beta = self.beta_1 * t_exp.pow(2.0)
310
+ theta_list = []
311
+ disc_targets = []
312
+ for j, (_, n) in enumerate(self.discrete_configs):
313
+ d = discrete_k.clamp(0, n - 1)
314
+ disc_targets.append(d)
315
+ e_x = F.one_hot(d, num_classes=n).float()
316
+ mean = beta * (n * e_x - 1)
317
+ std_disc = (beta * n + 1e-8).sqrt()
318
+ y = mean + std_disc * torch.randn_like(mean)
319
+ theta_list.append(torch.softmax(y, dim=-1))
320
+
321
+ net_input = torch.cat([mu_cont, *theta_list], dim=-1) if theta_list else mu_cont
322
+ out_flat = self.unet_wrapper(net_input.reshape(B, -1), t, cond=cond)
323
+ out = out_flat.reshape(B, T, -1)
324
+
325
+ x_cont_pred = out[:, :, :self.continuous_dim]
326
+ cont_loss = (gamma * (continuous_x - x_cont_pred).pow(2.0)).mean()
327
+
328
+ disc_loss = 0.0
329
+ offset = self.continuous_dim
330
+ for j, (_, n) in enumerate(self.discrete_configs):
331
+ logits = out[:, :, offset:offset + n]
332
+ disc_loss = disc_loss + F.cross_entropy(
333
+ logits.reshape(-1, n), disc_targets[j].reshape(-1)
334
+ )
335
+ offset += n
336
+
337
+ return cont_loss + disc_loss
338
+
339
+ def state_dict(self):
340
+ return {
341
+ "obs_encoder": self.obs_encoder.state_dict(),
342
+ "model": self.model.state_dict(),
343
+ "normalizer": self.normalizer.state_dict(),
344
+ }
345
+
346
+ def load_state_dict(self, state_dict):
347
+ self.obs_encoder.load_state_dict(state_dict["obs_encoder"])
348
+ self.model.load_state_dict(state_dict["model"])
349
+ if "normalizer" in state_dict:
350
+ self.normalizer.load_state_dict(state_dict["normalizer"])
351
+
352
+ def set_actions(self, action: torch.Tensor):
353
+ pass
354
+
355
+ def reset(self):
356
+ pass
ddpm/latest.ckpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:af0076703b823c77145c56da3234220b9aa4336fac725ebccab39e6c559b3e73
3
+ size 697958870
ddpm/policy_config.yaml ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _target_: workspaces.train_bfn_workspace.TrainBFNWorkspace
2
+ name: train_ddpm_pusht_xarm_top
3
+ task_name: pusht_xarm
4
+ exp_name: ddpm_top
5
+ shape_meta:
6
+ obs:
7
+ camera_0:
8
+ shape:
9
+ - 3
10
+ - 224
11
+ - 224
12
+ type: rgb
13
+ action:
14
+ shape:
15
+ - 9
16
+ horizon: 16
17
+ n_obs_steps: 2
18
+ n_action_steps: 8
19
+ n_latency_steps: 0
20
+ dataset_obs_steps: 2
21
+ past_action_visible: false
22
+ obs_as_global_cond: true
23
+ policy:
24
+ _target_: diffusion_policy.policy.diffusion_unet_hybrid_image_policy.DiffusionUnetHybridImagePolicy
25
+ shape_meta: ${shape_meta}
26
+ horizon: ${horizon}
27
+ n_action_steps: ${n_action_steps}
28
+ n_obs_steps: ${n_obs_steps}
29
+ noise_scheduler:
30
+ _target_: diffusers.schedulers.scheduling_ddpm.DDPMScheduler
31
+ num_train_timesteps: 100
32
+ beta_start: 0.0001
33
+ beta_end: 0.02
34
+ beta_schedule: squaredcos_cap_v2
35
+ variance_type: fixed_small
36
+ clip_sample: true
37
+ prediction_type: epsilon
38
+ num_inference_steps: 100
39
+ obs_as_global_cond: ${obs_as_global_cond}
40
+ obs_encoder_group_norm: true
41
+ eval_fixed_crop: true
42
+ crop_shape:
43
+ - 216
44
+ - 216
45
+ diffusion_step_embed_dim: 128
46
+ down_dims:
47
+ - 256
48
+ - 512
49
+ - 1024
50
+ kernel_size: 5
51
+ n_groups: 8
52
+ cond_predict_scale: true
53
+ task:
54
+ dataset:
55
+ _target_: dataset.pusht_xarm_dataset.PushTXArmDataset
56
+ zarr_path: data/pusht_xarm_merged/replay.zarr
57
+ horizon: ${horizon}
58
+ pad_before: 1
59
+ pad_after: 7
60
+ n_obs_steps: ${n_obs_steps}
61
+ seed: 42
62
+ val_ratio: 0.1
63
+ cameras:
64
+ - camera_0
65
+ action_mode: onehot
66
+ env_runner: null
67
+ ema:
68
+ _target_: diffusion_policy.model.diffusion.ema_model.EMAModel
69
+ update_after_step: 0
70
+ inv_gamma: 1.0
71
+ power: 0.75
72
+ min_value: 0.0
73
+ max_value: 0.9999
74
+ optimizer:
75
+ _target_: torch.optim.AdamW
76
+ lr: 0.0001
77
+ betas:
78
+ - 0.95
79
+ - 0.999
80
+ eps: 1.0e-08
81
+ weight_decay: 1.0e-06
82
+ training:
83
+ device: cuda:0
84
+ seed: 42
85
+ debug: false
86
+ resume: false
87
+ lr_scheduler: cosine
88
+ lr_warmup_steps: 500
89
+ num_epochs: 200
90
+ gradient_accumulate_every: 1
91
+ use_ema: true
92
+ rollout_every: 50
93
+ checkpoint_every: 50
94
+ val_every: 1
95
+ sample_every: 5
96
+ max_train_steps: null
97
+ max_val_steps: null
98
+ tqdm_interval_sec: 1.0
99
+ dataloader:
100
+ batch_size: 32
101
+ num_workers: 4
102
+ shuffle: true
103
+ pin_memory: true
104
+ persistent_workers: false
105
+ val_dataloader:
106
+ batch_size: 32
107
+ num_workers: 2
108
+ shuffle: false
109
+ pin_memory: true
110
+ persistent_workers: false
111
+ checkpoint:
112
+ topk:
113
+ monitor_key: train_loss
114
+ mode: min
115
+ k: 1
116
+ format_str: epoch={epoch:04d}-train_loss={train_loss:.4f}.ckpt
117
+ save_last_ckpt: true
118
+ save_last_snapshot: false
119
+ logging:
120
+ project: pusht_xarm_ddpm
121
+ resume: true
122
+ mode: offline
123
+ name: ${now:%Y.%m.%d-%H.%M.%S}_${name}
124
+ tags:
125
+ - ddpm
126
+ - pusht_xarm
127
+ - onehot
128
+ - top
129
+ id: null
130
+ group: null
131
+ multi_run:
132
+ run_dir: data/outputs/${now:%Y.%m.%d}/${now:%H.%M.%S}_${name}
133
+ wandb_name_base: ${now:%Y.%m.%d-%H.%M.%S}_${name}
inference.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone real-robot inference for PushT-xarm BFN-hybrid policy.
2
+
3
+ Inputs each step:
4
+ cam0: HxWx3 uint8 RGB image from the top camera (any size; will be resized to 224x224)
5
+ cam0_prev: same, one step earlier (n_obs_steps=2)
6
+
7
+ Output per step (predicts horizon=16, returns next n_action_steps=8):
8
+ direction: int in {0..7}
9
+ distance: float in [0, 50]
10
+ """
11
+ import argparse
12
+ import sys
13
+ import time
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+ import torch
18
+ import yaml
19
+ from PIL import Image
20
+
21
+ THIS_DIR = Path(__file__).resolve().parent
22
+ sys.path.insert(0, str(THIS_DIR))
23
+
24
+ from bfn_hybrid_image_policy import BFNHybridImagePolicy # noqa: E402
25
+
26
+
27
+ def load_bfn_policy(ckpt_path: str, config_path: str, device: str = "cuda"):
28
+ with open(config_path) as f:
29
+ cfg = yaml.safe_load(f)
30
+ pcfg = cfg["policy"]
31
+ policy = BFNHybridImagePolicy(
32
+ shape_meta=cfg["shape_meta"],
33
+ horizon=cfg["horizon"],
34
+ n_action_steps=cfg["n_action_steps"],
35
+ n_obs_steps=cfg["n_obs_steps"],
36
+ num_discrete_actions=pcfg.get("num_discrete_actions", 8),
37
+ continuous_param_dim=pcfg.get("continuous_param_dim", 1),
38
+ sigma_1=pcfg.get("sigma_1", 0.001),
39
+ beta_1=pcfg.get("beta_1", 0.2),
40
+ n_timesteps=pcfg.get("n_timesteps", 20),
41
+ crop_shape=tuple(pcfg.get("crop_shape", [216, 216])),
42
+ obs_encoder_group_norm=pcfg.get("obs_encoder_group_norm", True),
43
+ eval_fixed_crop=pcfg.get("eval_fixed_crop", True),
44
+ diffusion_step_embed_dim=pcfg.get("diffusion_step_embed_dim", 128),
45
+ down_dims=tuple(pcfg.get("down_dims", [256, 512, 1024])),
46
+ kernel_size=pcfg.get("kernel_size", 5),
47
+ n_groups=pcfg.get("n_groups", 8),
48
+ cond_predict_scale=pcfg.get("cond_predict_scale", True),
49
+ )
50
+ ckpt = torch.load(ckpt_path, map_location=device, weights_only=False)
51
+ state = ckpt["state_dicts"]["model"] if "state_dicts" in ckpt else ckpt
52
+ policy.load_state_dict(state)
53
+ policy.to(device).eval()
54
+ return policy
55
+
56
+
57
+ def preprocess_image(img: np.ndarray) -> np.ndarray:
58
+ """Resize HxWx3 uint8 -> 3x224x224 float32 in [0,1]."""
59
+ if img.shape[:2] != (224, 224):
60
+ img = np.array(Image.fromarray(img).resize((224, 224), Image.BILINEAR))
61
+ img = img.astype(np.float32) / 255.0
62
+ return img.transpose(2, 0, 1)
63
+
64
+
65
+ def infer_step(policy, cam0_now: np.ndarray, cam0_prev: np.ndarray, device: str = "cuda"):
66
+ """One inference call. Returns a list of dicts [{direction, distance}, ...] of length n_action_steps."""
67
+ a = preprocess_image(cam0_prev)
68
+ b = preprocess_image(cam0_now)
69
+ obs = torch.from_numpy(np.stack([a, b])).unsqueeze(0).to(device) # [1, 2, 3, 224, 224]
70
+ with torch.no_grad():
71
+ out = policy.predict_action({"camera_0": obs})
72
+ actions = out["action"][0].cpu().numpy() # [n_action_steps, 2] = [direction, distance]
73
+ return [
74
+ {"direction": int(round(a[0])) % 8, "distance": float(np.clip(a[1], 0, 50))}
75
+ for a in actions
76
+ ]
77
+
78
+
79
+ def main():
80
+ p = argparse.ArgumentParser()
81
+ p.add_argument("--ckpt", required=True)
82
+ p.add_argument("--config", required=True)
83
+ p.add_argument("--device", default="cuda")
84
+ args = p.parse_args()
85
+
86
+ print(f"Loading policy from {args.ckpt}...")
87
+ policy = load_bfn_policy(args.ckpt, args.config, args.device)
88
+ print("Policy loaded.")
89
+
90
+ # Dummy roundtrip test
91
+ dummy = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8)
92
+ t0 = time.time()
93
+ actions = infer_step(policy, dummy, dummy, args.device)
94
+ dt = (time.time() - t0) * 1000
95
+ print(f"Smoke test: {len(actions)} actions in {dt:.1f} ms")
96
+ print(f"First action: {actions[0]}")
97
+
98
+
99
+ if __name__ == "__main__":
100
+ main()
networks/__init__.py ADDED
File without changes
networks/base.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (C) 2023 Maxime Robeyns <dev@maximerobeyns.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Base network classes for torch_bfn, as well as some utilities"""
15
+
16
+
17
+ import math
18
+ import torch as t
19
+ import torch.nn as nn
20
+
21
+ from abc import abstractmethod
22
+ from typing import Optional
23
+ from functools import partial
24
+
25
+ try:
26
+ from torchtyping import TensorType as Tensor
27
+ except ImportError: # pragma: no cover - optional dependency
28
+
29
+ class _TensorAlias:
30
+ def __class_getitem__(cls, key):
31
+ return t.Tensor
32
+
33
+ Tensor = _TensorAlias
34
+
35
+ __all__ = [
36
+ "BFNetwork",
37
+ "DiscreteBFNetwork",
38
+ "SinusoidalPosEmb",
39
+ "RandomOrLearnedSinusoidalPosEmb",
40
+ ]
41
+
42
+
43
+ class BFNetwork(nn.Module):
44
+ """
45
+ Abstraact base class for neural networks (nn.Module) for use with
46
+ torch_bfn.
47
+ """
48
+
49
+ def __init__(self, is_conditional_model: bool = False):
50
+ super().__init__()
51
+ self.is_conditional_model = is_conditional_model
52
+
53
+ @abstractmethod
54
+ def forward(
55
+ self,
56
+ x: Tensor["B", "D"],
57
+ time: Tensor["B"],
58
+ cond: Optional[Tensor["B", "C"]] = None,
59
+ cond_drop_prob: Optional[float] = None,
60
+ ) -> Tensor["B", "D"]:
61
+ """Returns a value of the same shape as x (e.g. predicts the noise
62
+ applied to x) at time t with optional conditioning information.
63
+
64
+ Args:
65
+ x: the current parameter vector
66
+ time: current timestep
67
+ cond: conditioning information
68
+ cond_drop_prob: probability of dropping conditioning info out for
69
+ classifier-free guidance.
70
+
71
+ Returns:
72
+ Tensor["B", "D"]: updated parameter vector
73
+ """
74
+ raise NotImplementedError
75
+
76
+ def forward_with_cond_scale(
77
+ self, *args, cond_scale=1.0, rescaled_phi=0.0, **kwargs
78
+ ) -> Tensor["B", "D"]:
79
+ """For conditional sampling, this additionally scales the conditional
80
+ guidance, and sharpens phi.
81
+
82
+ This abstract class just invokes the forward method as a fallback.
83
+ """
84
+ logits = self.forward(*args, cond_drop_prob=0.0, **kwargs)
85
+ if cond_scale == 1.0:
86
+ return logits
87
+
88
+ null_logits = self.forward(*args, cond_drop_prob=1.0, **kwargs)
89
+ scaled_logits = null_logits + (logits - null_logits) * cond_scale
90
+
91
+ if rescaled_phi == 0.0:
92
+ return scaled_logits
93
+
94
+ std_fn = partial(t.std, dim=tuple(range(1, scaled_logits.ndim)), keepdim=True)
95
+ rescaled_logits = scaled_logits * (std_fn(logits) / std_fn(scaled_logits))
96
+ return rescaled_logits * rescaled_phi + scaled_logits * (1.0 - rescaled_phi)
97
+
98
+
99
+ class DiscreteBFNetwork(BFNetwork):
100
+ """
101
+ For discrete variants of BFNs, we require networks to use the last tensor
102
+ dimemsion as the class label dimension, as is conventional in transformer
103
+ models for language.
104
+ """
105
+
106
+ @abstractmethod
107
+ def forward(
108
+ self,
109
+ x: Tensor["B", "D", "K"],
110
+ time: Tensor["B"],
111
+ cond: Optional[Tensor["B", "C"]],
112
+ cond_drop_prob: Optional[float] = None,
113
+ ) -> Tensor["B", "D", "K"]:
114
+ raise NotImplementedError
115
+
116
+
117
+ class SinusoidalPosEmb(nn.Module):
118
+ def __init__(self, dim: int):
119
+ super().__init__()
120
+ self.dim = dim
121
+
122
+ def forward(self, x: t.Tensor) -> t.Tensor:
123
+ half_dim = self.dim // 2
124
+ emb = math.log(10000) / (half_dim - 1)
125
+ emb = t.exp(t.arange(half_dim, device=x.device) * -emb)
126
+ emb = x[:, None] * emb[None, :]
127
+ emb = t.cat((emb.sin(), emb.cos()), dim=-1)
128
+ return emb
129
+
130
+
131
+ class RandomOrLearnedSinusoidalPosEmb(nn.Module):
132
+ """
133
+ https://github.com/crowsonkb/v-diffusion-jax/blob/master/diffusion/models/danbooru_128.py#L8
134
+ """
135
+
136
+ def __init__(self, dim: int, is_random: bool = False):
137
+ super().__init__()
138
+ assert dim % 2 == 0, "Sinusoidal positional embedding dim must be even"
139
+ half_dim = dim // 2
140
+ self.weights = nn.Parameter(t.randn(half_dim), requires_grad=not is_random)
141
+
142
+ def forward(self, x: Tensor["B", 1]) -> Tensor["B", "dim+1"]:
143
+ freqs = x * self.weights[None, :] * 2 * math.pi
144
+ fouriered = t.cat((freqs.sin(), freqs.cos()), -1)
145
+ fouriered = t.cat((x, fouriered), -1)
146
+ return fouriered
policies/__init__.py ADDED
File without changes
policies/base.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base class for all robotics policies.
2
+
3
+ This module defines the abstract base class `BasePolicy`, which standardizes
4
+ the interface for environment interaction (`act`), model inference (`forward`),
5
+ training (`compute_loss`), and device/data management.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
12
+
13
+ import numpy as np
14
+ import torch
15
+ import torch.nn as nn
16
+
17
+ from utils.bfn_utils import str_to_torch_dtype
18
+
19
+ __all__ = ["BasePolicy"]
20
+
21
+
22
+ class BasePolicy(nn.Module, ABC):
23
+ """Abstract base class for robotics policies.
24
+
25
+ Provides shared functionality for:
26
+ 1. Device and dtype management.
27
+ 2. Data conversion (Numpy <-> Torch).
28
+ 3. Automatic batch dimension handling.
29
+ 4. Action clipping and normalization hooks.
30
+
31
+ Subclasses must implement:
32
+ - `forward(obs, ...)`: The core PyTorch inference logic.
33
+ - `compute_loss(batch)`: The training logic (optional, raises NotImplementedError by default).
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ action_space: Any,
39
+ *,
40
+ device: str = "cpu",
41
+ dtype: str = "float32",
42
+ clip_actions: bool = True,
43
+ normalizer: Optional[Any] = None,
44
+ ):
45
+ """Initializes the BasePolicy.
46
+
47
+ Args:
48
+ action_space: The Gym action space (used for clipping).
49
+ device: Default device to place tensors on ('cpu', 'cuda').
50
+ dtype: Default dtype for tensor creation ('float32', 'float16').
51
+ clip_actions: Whether to clamp output actions to the action_space bounds.
52
+ normalizer: Optional normalization module (e.g., LinearNormalizer).
53
+ """
54
+ super().__init__()
55
+ self.action_space = action_space
56
+ self._device = torch.device(device)
57
+ self._dtype = str_to_torch_dtype(dtype)
58
+ self.clip_actions = clip_actions
59
+ self.normalizer = normalizer
60
+
61
+ # --- Properties ---
62
+
63
+ @property
64
+ def device(self) -> torch.device:
65
+ """Returns the current device of the policy.
66
+
67
+ Infers device from the first parameter if available (robust to .to() calls),
68
+ otherwise falls back to the initialization value.
69
+ """
70
+ try:
71
+ return next(self.parameters()).device
72
+ except StopIteration:
73
+ return self._device
74
+
75
+ @property
76
+ def dtype(self) -> torch.dtype:
77
+ """Returns the current dtype of the policy."""
78
+ try:
79
+ return next(self.parameters()).dtype
80
+ except StopIteration:
81
+ return self._dtype
82
+
83
+ # --- Public Interface ---
84
+
85
+ def set_normalizer(self, normalizer: Any) -> None:
86
+ """Updates the normalizer used by the policy."""
87
+ self.normalizer = normalizer
88
+
89
+ @abstractmethod
90
+ def forward(
91
+ self,
92
+ obs: Union[torch.Tensor, Dict[str, torch.Tensor]],
93
+ *,
94
+ deterministic: bool = False,
95
+ **kwargs: Any,
96
+ ) -> torch.Tensor:
97
+ """Core inference method returning a batch of actions.
98
+
99
+ Args:
100
+ obs: Observations with leading batch dimension [B, ...].
101
+ deterministic: Whether to sample deterministically (policy dependent).
102
+ **kwargs: Additional arguments (e.g., conditioning info).
103
+
104
+ Returns:
105
+ Action tensor of shape [B, ActionDim].
106
+ """
107
+ raise NotImplementedError
108
+
109
+ def compute_loss(self, batch: Any) -> torch.Tensor:
110
+ """Computes training loss for the policy.
111
+
112
+ Args:
113
+ batch: A batch of data (usually containing 'obs', 'action').
114
+
115
+ Returns:
116
+ Scalar loss tensor.
117
+
118
+ Raises:
119
+ NotImplementedError: If the policy does not support internal training logic.
120
+ """
121
+ raise NotImplementedError(
122
+ f"compute_loss is not implemented for {self.__class__.__name__}."
123
+ )
124
+
125
+ @torch.inference_mode()
126
+ def act(
127
+ self,
128
+ obs: Any,
129
+ *,
130
+ deterministic: bool = False,
131
+ return_torch: bool = False,
132
+ **kwargs: Any,
133
+ ) -> Union[np.ndarray, torch.Tensor]:
134
+ """Convenience wrapper for environment interaction.
135
+
136
+ Handles:
137
+ 1. conversion of numpy obs -> torch tensors.
138
+ 2. adding batch dimension if missing.
139
+ 3. inference via `forward`.
140
+ 4. clipping actions.
141
+ 5. conversion of torch output -> numpy action (optional).
142
+
143
+ Args:
144
+ obs: Observation from the environment (Numpy array, Dict, or Tensor).
145
+ deterministic: Whether to use deterministic mode.
146
+ return_torch: If True, returns a Tensor on device; else returns Numpy array.
147
+ **kwargs: Passed to `forward`.
148
+
149
+ Returns:
150
+ Action (Numpy array or Tensor).
151
+ """
152
+ # 1. Convert to Tensor
153
+ obs_t = self._to_tensor(obs)
154
+
155
+ # 2. Auto-Batching
156
+ # We assume environment interaction usually provides a single unbatched observation
157
+ obs_t, batch_added = self._maybe_add_batch_dim(obs_t)
158
+
159
+ # 3. Inference
160
+ action = self.forward(obs_t, deterministic=deterministic, **kwargs)
161
+
162
+ # 4. Clipping
163
+ action = self._clip_actions(action)
164
+
165
+ # 5. Return
166
+ if return_torch:
167
+ return action
168
+
169
+ # Remove batch dim if we added it
170
+ if batch_added:
171
+ action = action.squeeze(0)
172
+
173
+ return action.detach().cpu().numpy()
174
+
175
+ # --- Internal Helpers ---
176
+
177
+ def _to_tensor(self, data: Any) -> Any:
178
+ """Recursively converts input data to tensors on the correct device/dtype."""
179
+ if isinstance(data, torch.Tensor):
180
+ return data.to(device=self.device, dtype=self.dtype)
181
+
182
+ if isinstance(data, Mapping):
183
+ return {k: self._to_tensor(v) for k, v in data.items()}
184
+
185
+ if isinstance(data, (list, tuple)):
186
+ return type(data)(self._to_tensor(v) for v in data)
187
+
188
+ # Fallback for numpy arrays / scalars
189
+ return torch.as_tensor(data, device=self.device, dtype=self.dtype)
190
+
191
+ def _maybe_add_batch_dim(self, obs: Any) -> Tuple[Any, bool]:
192
+ """Adds a leading batch dimension if the input appears to be unbatched.
193
+
194
+ Note: This uses a heuristic. If the input is a Tensor, we assume it is
195
+ unbatched if it matches the observation space shape (not implemented here generic enough)
196
+ OR we rely on the caller context (usually `act` is single-step).
197
+
198
+ Here, we unconditionally unsqueeze dim 0 for `act` convenience.
199
+ """
200
+ batch_added = False
201
+
202
+ if isinstance(obs, torch.Tensor):
203
+ # Heuristic: We assume `act` is called with single observations.
204
+ # For robust batch detection, one would check obs_space.shape.
205
+ # Here we simply unsqueeze to ensure [1, ...] shape.
206
+ obs = obs.unsqueeze(0)
207
+ batch_added = True
208
+
209
+ elif isinstance(obs, Mapping):
210
+ # Handle Dict inputs (e.g. {'image': ..., 'state': ...})
211
+ # Only unsqueeze tensors.
212
+ new_obs = {}
213
+ for k, v in obs.items():
214
+ if isinstance(v, torch.Tensor):
215
+ new_obs[k] = v.unsqueeze(0)
216
+ batch_added = True # Mark true if ANY tensor was unsqueezed
217
+ else:
218
+ new_obs[k] = v
219
+ obs = new_obs
220
+
221
+ return obs, batch_added
222
+
223
+ def _clip_actions(self, action: torch.Tensor) -> torch.Tensor:
224
+ """Clips actions to the environment bounds if `clip_actions` is True."""
225
+ if not self.clip_actions:
226
+ return action
227
+
228
+ # Check if action space has bounds
229
+ if not hasattr(self.action_space, "low") or not hasattr(
230
+ self.action_space, "high"
231
+ ):
232
+ return action
233
+
234
+ # Create tensor bounds on the fly (caching could be an optimization)
235
+ low = torch.as_tensor(
236
+ self.action_space.low, device=action.device, dtype=action.dtype
237
+ )
238
+ high = torch.as_tensor(
239
+ self.action_space.high, device=action.device, dtype=action.dtype
240
+ )
241
+
242
+ return torch.clamp(action, low, high)
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=2.0
2
+ torchvision
3
+ diffusers>=0.18
4
+ einops
5
+ numpy
6
+ zarr
7
+ pyyaml
8
+ pillow
9
+ hydra-core
10
+ robomimic
11
+ # diffusion-policy library is needed for DDPM/DDIM/EDM/Consistency policies.
12
+ # Install from source: pip install git+https://github.com/real-stanford/diffusion_policy.git