petkopetkov commited on
Commit
020cdbc
·
verified ·
1 Parent(s): 84539e3

Upload sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED (arch=paligemma_hlc, verify=FAIL_STRICT_LOAD)

Browse files
Files changed (13) hide show
  1. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/RECONSTRUCTION_NOTES.txt +14 -0
  2. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/checkpoints/checkpoint-020000.pt +3 -0
  3. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/checkpoints/model-checkpoint-020000.pt +3 -0
  4. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/__pycache__/common_pizero_fm_paligemma.cpython-310.pyc +0 -0
  5. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/__pycache__/configuration_pizero_fm_paligemma.cpython-310.pyc +0 -0
  6. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/__pycache__/modeling_pizero_fm_paligemma.cpython-310.pyc +0 -0
  7. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/__pycache__/processing_pizero_fm_paligemma.cpython-310.pyc +0 -0
  8. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/common_pizero_fm_paligemma.py +571 -0
  9. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/configuration_pizero_fm_paligemma.py +366 -0
  10. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/format.log +24 -0
  11. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/model_config.yaml +120 -0
  12. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/modeling_pizero_fm_paligemma.py +0 -0
  13. sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/processing_pizero_fm_paligemma.py +1849 -0
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/RECONSTRUCTION_NOTES.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Session reconstruction notes (uploaded with _UNVERIFIED suffix).
2
+
3
+ This session's original hf_export was not available in any local archive or scratch
4
+ location. The hf_export here is BORROWED from a sibling session of the same model
5
+ architecture, to allow load via make_hf_model. As such, the resulting model may
6
+ load successfully (strict state_dict match) BUT downstream eval-specific config
7
+ (action chunks, dataset stats, processor parameters) MAY DIFFER from the
8
+ original training run. Do not assume the model produces correct actions for
9
+ the original task without re-evaluation.
10
+
11
+ architecture: paligemma_hlc
12
+ borrowed_hf_export: sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm
13
+ empirical_verify: FAIL_STRICT_LOAD
14
+ build_date: 2026-05-14T13:32:26Z
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/checkpoints/checkpoint-020000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4490ad1f86cfa756bdc33b65dffaf733d256de56e8c4acd0674dd70bbf8ce688
3
+ size 15874194950
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/checkpoints/model-checkpoint-020000.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cb9b518ecde9692cc9b4cd6740e62814f72550e6e99a862c33041170132a6cea
3
+ size 13801201742
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/__pycache__/common_pizero_fm_paligemma.cpython-310.pyc ADDED
Binary file (24.2 kB). View file
 
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/__pycache__/configuration_pizero_fm_paligemma.cpython-310.pyc ADDED
Binary file (16.2 kB). View file
 
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/__pycache__/modeling_pizero_fm_paligemma.cpython-310.pyc ADDED
Binary file (100 kB). View file
 
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/__pycache__/processing_pizero_fm_paligemma.cpython-310.pyc ADDED
Binary file (62.5 kB). View file
 
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/common_pizero_fm_paligemma.py ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import cached_property
2
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type
3
+
4
+ import torch
5
+ import torch.distributed.fsdp
6
+ import torch.distributed.tensor
7
+ import torch.nn.attention.flex_attention
8
+ import transformers
9
+ from backports.strenum import StrEnum
10
+ from databib.dataclasses import Dataclass, dataclass
11
+ from databib.dataclasses.dataclass import DataclassT
12
+ from databib.utils.classproperty import classproperty
13
+
14
+
15
+ class ReferenceFrame(StrEnum):
16
+ """
17
+ Indicates the frame frame w.r.t. which translation or rotation is expressed.
18
+ Note that each of translation and rotation has its own (possibly different) ReferenceFrame value.
19
+
20
+ WORLD: Only for completeness, not yet used. Will become relevant when navigation is introduced.
21
+ ROBOT_BASE: Translation/rotation expressed in absolute robot base frame
22
+ ROBOT_BASE_DELTA:
23
+ - Translation expressed as delta value w.r.t. the previous EEF translation pose
24
+ The delta value is defined in the robot base frame (rather than in the current EEF frame)
25
+ - Rotation expressed as w.r.t. the previous rotation pose
26
+ The axis of rotation is defined in the robot base frame (rather than in the current EEF frame)
27
+ ROBOT_BASE_RELATIVE: Same as ROBOT_BASE_DELTA, but the sequence is expressed w.r.t.the 0-th element
28
+ instead of the previous element
29
+ EEF: Translation/rotation expressed in the current end-effector frame
30
+ EEF_DELTA:
31
+ - Translation expressed as delta value w.r.t. the previous EEF translation pose
32
+ The delta value is defined in the current EEF frame (rather than in the robot base frame)
33
+ - Rotation expressed as w.r.t. the previous rotation pose
34
+ The axis of rotation is defined in the current EEF frame (rather than in the robot base frame)
35
+ """
36
+
37
+ ROBOT_BASE = 'robot_base'
38
+ ROBOT_BASE_DELTA = 'robot_base_delta'
39
+ ROBOT_BASE_RELATIVE = 'robot_base_relative'
40
+ EEF_RELATIVE = EEF = 'eef_relative'
41
+ EEF_DELTA = 'eef_delta'
42
+ CAMERA = 'camera'
43
+ UNKNOWN = 'unknown'
44
+
45
+ @classproperty
46
+ def robot_frames(cls) -> set['ReferenceFrame']:
47
+ return {
48
+ ReferenceFrame.ROBOT_BASE,
49
+ ReferenceFrame.ROBOT_BASE_DELTA,
50
+ ReferenceFrame.ROBOT_BASE_RELATIVE,
51
+ }
52
+
53
+ @classproperty
54
+ def eef_frames(cls) -> set['ReferenceFrame']:
55
+ return {ReferenceFrame.EEF, ReferenceFrame.EEF_RELATIVE, ReferenceFrame.EEF_DELTA}
56
+
57
+ @classproperty
58
+ def delta_frames(cls) -> set['ReferenceFrame']:
59
+ return {ReferenceFrame.ROBOT_BASE_DELTA, ReferenceFrame.EEF_DELTA}
60
+
61
+ @classproperty
62
+ def relative_frames(cls) -> set['ReferenceFrame']:
63
+ return {ReferenceFrame.ROBOT_BASE_RELATIVE, ReferenceFrame.EEF_RELATIVE}
64
+
65
+ @classproperty
66
+ def core_frames(cls) -> set['ReferenceFrame']:
67
+ return {ReferenceFrame.ROBOT_BASE, ReferenceFrame.EEF}
68
+
69
+ def to_relative(self) -> 'ReferenceFrame':
70
+ if self in self.robot_frames:
71
+ return self.ROBOT_BASE_RELATIVE
72
+ if self in self.eef_frames:
73
+ return self.EEF_RELATIVE
74
+ raise ValueError(f'Cannot convert frame {self} to relative frame')
75
+
76
+ def to_delta(self) -> 'ReferenceFrame':
77
+ if self in self.robot_frames:
78
+ return self.ROBOT_BASE_DELTA
79
+ if self in self.eef_frames:
80
+ return self.EEF_DELTA
81
+ raise ValueError(f'Cannot convert frame {self} to delta frame')
82
+
83
+ def to_core(self) -> 'ReferenceFrame':
84
+ if self in self.robot_frames:
85
+ return self.ROBOT_BASE
86
+ if self in self.eef_frames:
87
+ return self.EEF
88
+ raise ValueError(f'Cannot convert frame {self} to relative frame')
89
+
90
+
91
+ class RotationFormat(StrEnum):
92
+ """Determines how rotations will be encoded in the loaded batch"""
93
+
94
+ EULER = 'euler'
95
+ QUATERNION = 'quaternion'
96
+ ROTMAT = 'rotmat'
97
+
98
+
99
+ class ResizeMode(StrEnum):
100
+ """
101
+ Different modes for resizing images.
102
+ """
103
+
104
+ MATCH_WIDTH = 'match_width'
105
+ MATCH_HEIGHT = 'match_height'
106
+ MATCH_MAX = 'match_max'
107
+ NAIVE = 'naive'
108
+ SMART = 'smart'
109
+ PAD = 'pad'
110
+ CROP = 'crop'
111
+
112
+
113
+ def expand_dims(tensor: torch.Tensor, ndim: int, order: Sequence[int]) -> torch.Tensor:
114
+ """
115
+ Expand the dimensions of `tensor` to `ndim` such that all new dimensions have size of 1
116
+ Args:
117
+ tensor: torch.Tensor of any shape
118
+ ndim: Number of output dimensions. Must be >= `tensor.ndim`
119
+ order: Sequence of size `tensor.ndim + 1`. Contains only values of 1 and a single value of -1,
120
+ indicating where the new `ndim - tensor.ndim` dimensions will be inserted
121
+ Returns:
122
+ torch.Tensor with dimensions `ndim`, a view of `tensor`
123
+
124
+ Ex:
125
+ expand_dims(torch.ones([2, 3, 4]), ndim=5, order=[1, -1, 1, 1]).shape -> [2, 1, 1, 3, 4]
126
+ expand_dims(torch.ones([2, 3, 4]), ndim=5, order=[-1, 1, 1, 1]).shape -> [1, 1, 2, 3, 4]
127
+ expand_dims(torch.ones([2, 3, 4]), ndim=5, order=[1, 1, 1, -1]).shape -> [2, 3, 4, 1, 1]
128
+ """
129
+ assert tensor.ndim <= ndim, f'{tensor.ndim} > {ndim}; shape={tensor.shape}'
130
+ assert len(order) == tensor.ndim + 1, f'{len(order)} != {tensor.ndim + 1}; shape={tensor.shape}'
131
+ order = list(order)
132
+ assert order.count(-1) == 1, 'Order must have exactly one value of -1'
133
+ assert order.count(1) == len(order) - 1, 'Order must have exactly len(order) - 1 values of 1'
134
+ if tensor.ndim == ndim:
135
+ return tensor
136
+ insert_index = order.index(-1)
137
+ view = list(tensor.shape[:insert_index]) + [1] * (ndim - tensor.ndim) + list(tensor.shape[insert_index:])
138
+ tensor = tensor.view(view)
139
+ return tensor
140
+
141
+
142
+ def compare_dicts(dict_0: Dict[str, Any], dict_1: Dict[str, Any], comparison_function: Callable) -> bool:
143
+ if set(dict_0.keys()) != set(dict_1.keys()):
144
+ return False
145
+ for key, _ in dict_0.items():
146
+ if type(dict_0[key]) != type(dict_1[key]):
147
+ return False
148
+ if isinstance(dict_0[key], dict):
149
+ result = compare_dicts(dict_0[key], dict_1[key], comparison_function)
150
+ else:
151
+ result = comparison_function(dict_0[key], dict_1[key])
152
+ if isinstance(result, torch.Tensor):
153
+ result = bool(result.all())
154
+ if not result:
155
+ return False
156
+ return True
157
+
158
+
159
+ def tensor_size_bytes(tensor: Optional[torch.Tensor]) -> int:
160
+ if tensor is None:
161
+ return 0
162
+ if not isinstance(tensor, torch.Tensor):
163
+ raise RuntimeError('Provided data is not a torch.Tensor: ', tensor)
164
+ bytes_per_element = tensor.element_size()
165
+ return bytes_per_element * tensor.numel()
166
+
167
+
168
+ def tensor_dataclass(cls: Type[DataclassT], **kwargs) -> Type[DataclassT]:
169
+ cls = dataclass(cls, eq=False, **kwargs)
170
+ return cls
171
+
172
+
173
+ @tensor_dataclass
174
+ class TensorDataclass(Dataclass):
175
+ """
176
+ Extends Dataclass with common torch.Tensor utilities.
177
+ - Can contain non-tensor fields, but some member functions might ignore these fields
178
+ or explicitly raise errors.
179
+ - Useful for packing batches, input and output data for ML models
180
+ - When using for input / output data for ML models, it's recommended to keep only torch.Tensor
181
+ fields to allow for supporting functionality such as torch.jit.script
182
+ """
183
+
184
+ def __eq__(self, other) -> bool:
185
+ if type(other) is not type(self):
186
+ return False
187
+ return compare_dicts(self.as_json(), other.as_json(), lambda x, y: x == y)
188
+
189
+ def __ne__(self, other) -> bool:
190
+ return not self == other
191
+
192
+ def __hash__(self):
193
+ raise ValueError(f'Hash function not implemented for {self.__class__.__name__}.')
194
+
195
+ def calc_size_bytes(self) -> int:
196
+ return sum(
197
+ (
198
+ tensor_size_bytes(value)
199
+ for (_, value) in self.items(recursive=True)
200
+ if isinstance(value, torch.Tensor)
201
+ )
202
+ )
203
+
204
+ def calc_size_megabytes(self) -> float:
205
+ return self.calc_size_bytes() / 2**20
206
+
207
+ def cpu(self) -> 'TensorDataclass':
208
+ return self.to(device='cpu')
209
+
210
+ def to(self, *, device=None, dtype=None, copy=False, non_blocking=False) -> 'TensorDataclass':
211
+ assert device is not None or dtype is not None
212
+ return self.apply(
213
+ lambda value: value.to(device=device, dtype=dtype, copy=copy, non_blocking=non_blocking)
214
+ if isinstance(value, torch.Tensor)
215
+ else value
216
+ )
217
+
218
+ def float32(self) -> 'TensorDataclass':
219
+ return self.apply(
220
+ lambda value: value.to(dtype=torch.float32)
221
+ if isinstance(value, torch.Tensor) and value.dtype.is_floating_point
222
+ else value
223
+ )
224
+
225
+ def detach(self) -> 'TensorDataclass':
226
+ return self.apply(lambda value: value.detach() if isinstance(value, torch.Tensor) else value)
227
+
228
+ def __getitem__(self, index) -> 'TensorDataclass':
229
+ def extract(obj):
230
+ if obj is None:
231
+ return None
232
+ if isinstance(obj, torch.Tensor):
233
+ return obj[index]
234
+ raise ValueError(f'Cannot slice {obj.__class__.__name__} object')
235
+
236
+ return self.apply(extract)
237
+
238
+ @property
239
+ def device(self) -> Optional[torch.device]:
240
+ """
241
+ Returns the device on which tensors in this dataclass reside. If tensors are on
242
+ different devices, raises RuntimeError. If no tensors in the class, returns None
243
+ """
244
+ devices = [
245
+ value.device
246
+ for (key, value) in self.items()
247
+ if isinstance(value, (TensorDataclass, torch.Tensor))
248
+ ]
249
+ devices = [d for d in devices if d is not None]
250
+ if len(devices) == 0:
251
+ return None
252
+ if len(set(devices)) == 1:
253
+ return devices[0]
254
+ (key, device) = (None, None)
255
+ for k, value in self.items():
256
+ if value is None:
257
+ continue
258
+ if device is None:
259
+ device = value.device
260
+ key = k
261
+ elif device != value.device:
262
+ raise RuntimeError(
263
+ f'Inconsistent device for instance of {self.__class__.__name__}. Device of field {key} is {device}, while device of field {k} is {value.device}'
264
+ )
265
+ raise RuntimeError
266
+
267
+ def to_shared_memory(self) -> 'TensorDataclass':
268
+ """Move all tensors in the dataclass to shared memory"""
269
+ return self.apply(lambda value: value.share_memory_() if isinstance(value, torch.Tensor) else value)
270
+
271
+ def pin_memory(self) -> 'TensorDataclass':
272
+ """Used for pinning memory during dataloading. Do not modify the name of the function"""
273
+ return self.apply(lambda value: value.pin_memory() if isinstance(value, torch.Tensor) else value)
274
+
275
+
276
+ @tensor_dataclass
277
+ class ModelTarget(TensorDataclass):
278
+ """
279
+ Only relevant for supervised learning.
280
+ Packs regression / classification target values that we input in the loss
281
+ """
282
+
283
+
284
+ @tensor_dataclass
285
+ class RoboticsTarget(ModelTarget):
286
+ control_tokens_ids: Optional[torch.Tensor]
287
+ text_tokens_ids: Optional[torch.Tensor]
288
+ translation: torch.Tensor
289
+ rotation: torch.Tensor
290
+ gripper: torch.Tensor
291
+ valid_mask: torch.Tensor
292
+
293
+
294
+ @tensor_dataclass
295
+ class PolicyControlPlan(TensorDataclass):
296
+ """
297
+ Abstraction class relevant for control tasks. Note that `ModelOutput` might not contain the actual
298
+ controls we want to use on the robot in the environment. Examples:
299
+ - `ModelOutput` contains logits, since computing losses on logits is more numerically stable.
300
+ We need to convert these logits to actual controls for the actual robot
301
+ - `ModelOutput` contains an entire costmap from which we need to extract waypoints
302
+ - `ModelOutput` contains unnormalized quaternion or rotation matrix that need to be normalized
303
+ - `ModelOutput` contains 2D/3D positions from which we need to extract speed and steering
304
+ `PolicyControlPlan`
305
+ - Extracts actual physical representation from `ModelOutput` that we can use to dervie the controls
306
+ - Doesn't necessarily contain the controls themselves, but they can be derived from this data
307
+ - **Interpretable control plan which we can visualize, interpret and compare to the real data**
308
+ - Ex: Controls might be in speed and steering, but we likely want to compare 2D/3D positions
309
+ instead of controls for metrics and visualizations
310
+ - Ex: Robot control is usually a single timestep, while `PolicyControlPlan` contains
311
+ controls over multiple timesteps
312
+ - Can have different abstractions, e.g.
313
+ - End effector 3D translation and rotation (positional control)
314
+ - Speed and steering for a vehicle (actuator control)
315
+ - 3D waypoints for a path to be followed
316
+ - Usually **unnormalized** values into physical units (vs normalized `ModelOutput`)
317
+ Main purpose: (Human) Interpretable control plans and metadata that can be used for visualization,
318
+ metrics and debugging
319
+ """
320
+
321
+
322
+ @tensor_dataclass
323
+ class RoboticsControlPlan(PolicyControlPlan):
324
+ translation_m: torch.Tensor
325
+ rotmat: torch.Tensor
326
+ gripper_prob: torch.Tensor
327
+ valid_mask: torch.Tensor
328
+
329
+ def __post_init__(self):
330
+ super().__post_init__()
331
+ assert self.translation_m.ndim == 3, self.translation_m.shape
332
+ assert self.rotmat.ndim == 3, self.rotmat.shape
333
+ assert self.gripper_prob.ndim == 3, self.gripper_prob.shape
334
+
335
+
336
+ @tensor_dataclass
337
+ class ModelOutput(TensorDataclass):
338
+ """
339
+ Packs data which an NN model outputs. Note this can contain a lot of metadata
340
+ such as intermediate outputs, probabilities, visualizations, etc
341
+ In the case of robot control, the action class is not guaranteed to be part of this
342
+ class, but we must be able to derive an action from the data in this class
343
+ """
344
+
345
+
346
+ @tensor_dataclass
347
+ class RoboticsInput(TensorDataclass):
348
+ images: Dict[str, torch.Tensor]
349
+ input_ids: torch.Tensor
350
+ attn_mask: torch.Tensor
351
+ ee_pose_translation: torch.Tensor
352
+ ee_pose_rotation: torch.Tensor
353
+ gripper: torch.Tensor
354
+ joints: torch.Tensor
355
+ control_tokens_ids: Optional[torch.Tensor]
356
+
357
+ @property
358
+ def inputs_embeds(self) -> Optional[torch.Tensor]:
359
+ return None
360
+
361
+ @property
362
+ def past_key_values(self) -> Optional[List[torch.Tensor]]:
363
+ return None
364
+
365
+ @cached_property
366
+ def multimodal_indices(self) -> torch.Tensor:
367
+ """
368
+ Returns a torch.Tensor containing only the indices of the batch examples which are multimodal.
369
+ Return shape is [B]
370
+ """
371
+ return torch.arange(self.input_ids.shape[0], dtype=torch.int64, device=self.input_ids.device)
372
+
373
+ @cached_property
374
+ def unimodal_indices(self) -> torch.Tensor:
375
+ """
376
+ Returns a torch.Tensor containing only the indices of the batch examples which are unimodal.
377
+ Return shape is [B]
378
+ """
379
+ return torch.tensor([], dtype=torch.int64, device=self.input_ids.device)
380
+
381
+
382
+ @tensor_dataclass
383
+ class FlowInput(TensorDataclass):
384
+ timestep: torch.Tensor
385
+ translation_t: torch.Tensor
386
+ rotation_t: torch.Tensor
387
+ gripper_t: torch.Tensor
388
+ translation_t0: torch.Tensor
389
+ rotation_t0: torch.Tensor
390
+ gripper_t0: torch.Tensor
391
+
392
+
393
+ @tensor_dataclass
394
+ class RoboticsFlowInput(RoboticsInput):
395
+ """Input to the entire Robotics VLM"""
396
+
397
+ flow_input: FlowInput
398
+
399
+
400
+ @tensor_dataclass
401
+ class DiffusionInput(TensorDataclass):
402
+ timestep: torch.Tensor
403
+ noised_translation: torch.Tensor
404
+ noised_rotation: torch.Tensor
405
+ noised_gripper: torch.Tensor
406
+
407
+
408
+ @tensor_dataclass
409
+ class LLMOutput(TensorDataclass):
410
+ """Fork of transformers.modeling_outputs.CausalLMOutputWithPast"""
411
+
412
+ input_ids: torch.Tensor
413
+ logits: Optional[torch.Tensor]
414
+ output_ids: Optional[torch.Tensor]
415
+ loss: Optional[torch.Tensor]
416
+ past_key_values: List[Tuple[torch.Tensor, torch.Tensor]]
417
+ hidden_states: List[torch.Tensor]
418
+ text_mask: torch.Tensor
419
+ image_mask: torch.Tensor
420
+
421
+ @classmethod
422
+ def from_transformers(
423
+ cls,
424
+ input_ids: torch.Tensor,
425
+ llm_output: transformers.modeling_outputs.CausalLMOutputWithPast,
426
+ text_mask: torch.Tensor,
427
+ image_mask: torch.Tensor,
428
+ ) -> 'LLMOutput':
429
+ return LLMOutput(
430
+ input_ids=input_ids,
431
+ logits=getattr(llm_output, 'logits', None),
432
+ output_ids=None,
433
+ loss=getattr(llm_output, 'loss', None),
434
+ past_key_values=list(llm_output.past_key_values)
435
+ if llm_output.past_key_values is not None
436
+ else [],
437
+ hidden_states=list(llm_output.hidden_states) if llm_output.hidden_states is not None else [],
438
+ text_mask=text_mask,
439
+ image_mask=image_mask,
440
+ )
441
+
442
+ def compress(self, ignore_index: int = -100) -> 'LLMOutput':
443
+ """
444
+ Compress the data contained in the class so it can be moved between CPU and GPU or concatenated
445
+ much faster:
446
+ - hidden_states - huge tensors; take a lot of CPU time to move across devices or concat
447
+ - past_key_values - huge tensors; take a lot of CPU time to move across devices or concat
448
+ - logits - huge last dimension; takes a lot of CPU time to move across devices or concat
449
+ """
450
+ replace: Dict[str, Any] = {'hidden_states': [], 'past_key_values': [], 'loss': None}
451
+ if self.logits is not None:
452
+ replace['logits'] = None
453
+ if self.output_ids is None:
454
+ assert (
455
+ self.text_mask is not None
456
+ ), 'text_mask is required to compute output_ids when output_ids is None'
457
+ assert (
458
+ self.logits.shape[:2] == self.text_mask.shape
459
+ ), 'logits and text_mask batch and sequence dimensions must match to compute output_ids'
460
+ predicted_ids = self.logits.argmax(dim=-1)
461
+ output_ids = torch.where(self.text_mask, predicted_ids, ignore_index)
462
+ replace['output_ids'] = output_ids
463
+ return self.replace(**replace)
464
+
465
+
466
+ @tensor_dataclass
467
+ class RoboticsOutput(ModelOutput):
468
+ translation: Optional[torch.Tensor]
469
+ rotation: Optional[torch.Tensor]
470
+ gripper: Optional[torch.Tensor]
471
+ token_logits: Optional[torch.Tensor]
472
+ token_ids: Optional[torch.Tensor]
473
+ llm_output: LLMOutput
474
+
475
+ def compress(self, ignore_index: int = -100) -> 'RoboticsOutput':
476
+ """
477
+ Compress output and drop unnecessary components to speed up transfer GPU <-> CPU.
478
+ Note that LLM logits can be extremely expensive since their size is [B, S, vocab_size], which
479
+ can reach millions or billions of values for large vocab_size
480
+ """
481
+ replace: Dict[str, Any] = {
482
+ 'llm_output': self.llm_output.compress(ignore_index=ignore_index),
483
+ 'token_logits': None,
484
+ }
485
+ if self.token_logits is not None and self.token_ids is None:
486
+ replace['token_ids'] = torch.argmax(self.token_logits, dim=-1)
487
+ return self.replace(**replace)
488
+
489
+
490
+ @tensor_dataclass
491
+ class VLMOutput(TensorDataclass):
492
+ llm_output: LLMOutput
493
+ vit_tokens: Optional[torch.Tensor]
494
+ attn_mask: torch.Tensor
495
+
496
+ def compress(self, ignore_index: int = -100) -> 'VLMOutput':
497
+ """
498
+ Compress output and drop unnecessary components to speed up transfer GPU <-> CPU.
499
+ Note that LLM logits can be extremely expensive since their size is [B, S, vocab_size], which
500
+ can reach millions or billions of values for large vocab_size
501
+ """
502
+ return self.replace(llm_output=self.llm_output.compress(ignore_index=ignore_index))
503
+
504
+
505
+ def is_quaternion(quaternion: torch.Tensor) -> bool:
506
+ return quaternion.shape[-1] == 4
507
+
508
+
509
+ def quaternion_half_cover(quaternion: torch.Tensor) -> torch.Tensor:
510
+ """
511
+ Flip quaternions so they cover only a half the space. If the q_w is negative, flip the quaternion.
512
+ If q_w is 0, then choose such that the first non-zero component is positive. Note that geometrically,
513
+ this doesn't correspond to a single hemisphere of the unit sphere. Follows
514
+ https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.as_quat.html#scipy.spatial.transform.Rotation.as_quat
515
+ """
516
+ assert is_quaternion(quaternion), quaternion.shape
517
+ with torch.no_grad():
518
+ is_zero = quaternion == 0
519
+ flip_condition = (
520
+ (quaternion[..., -1:] < 0)
521
+ | is_zero[..., -1:] & (quaternion[..., 0:1] < 0)
522
+ | is_zero[..., -1:] & is_zero[..., 0:1] & (quaternion[..., 1:2] < 0)
523
+ | is_zero[..., -1:] & is_zero[..., 0:1] & is_zero[..., 1:2] & (quaternion[..., 2:3] < 0)
524
+ )
525
+ quaternion = torch.where(flip_condition, -quaternion, quaternion)
526
+ return quaternion
527
+
528
+
529
+ def is_rotmat_3x3(rotmat: torch.Tensor) -> bool:
530
+ return rotmat.shape[-2:] == torch.Size([3, 3])
531
+
532
+
533
+ def is_rotmat_9(rotmat: torch.Tensor) -> bool:
534
+ return rotmat.shape[-1] == 9
535
+
536
+
537
+ def rotmat_as_9(rotmat: torch.Tensor) -> torch.Tensor:
538
+ """Convert any rotmat input to [..., 9] shape"""
539
+ if is_rotmat_9(rotmat):
540
+ return rotmat
541
+ if is_rotmat_3x3(rotmat):
542
+ return rotmat.reshape(*rotmat.shape[:-2], 9)
543
+ raise ValueError(f"Can't convert tensor of shape {rotmat.shape} to a 3x3 rotation matrix")
544
+
545
+
546
+ def is_rotmat(rotmat: torch.Tensor) -> bool:
547
+ """
548
+ Checks if the tensor shape matches that of a rotmat. However, it's not guaranteed the data is a
549
+ valid rotmat. `is_orthonormal_rotmat` performs this additional check.
550
+ NOTE: This might incorrectly return True if the underlying data is euler angles and accidentally
551
+ `rotmat.shape[-2:] == [3, 3]`. This would happen very rarely, but use with caution
552
+ """
553
+ return is_rotmat_3x3(rotmat) or is_rotmat_9(rotmat)
554
+
555
+
556
+ def rotmat_as_3x3(rotmat: torch.Tensor) -> torch.Tensor:
557
+ """Convert any rotmat input to [..., 3, 3] shape"""
558
+ if rotmat.shape[-1] == 9:
559
+ return rotmat.reshape(*rotmat.shape[:-1], 3, 3)
560
+ if rotmat.shape[-2:] == torch.Size([3, 3]):
561
+ return rotmat
562
+ raise ValueError(f"Can't convert tensor of shape {rotmat.shape} to a 3x3 rotation matrix")
563
+
564
+
565
+ def rotmat_inverse(rotation: torch.Tensor) -> torch.Tensor:
566
+ assert is_rotmat(rotation), f'Expected a rotation matrix, but got shape {rotation.shape}'
567
+ rotmat = rotmat_as_3x3(rotation)
568
+ rotmat = rotmat.transpose(-1, -2)
569
+ if is_rotmat_9(rotation):
570
+ rotmat = rotmat_as_9(rotmat)
571
+ return rotmat
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/configuration_pizero_fm_paligemma.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ from databib.config import Config
4
+
5
+ from .common_pizero_fm_paligemma import ReferenceFrame, ResizeMode, RotationFormat
6
+
7
+
8
+ class ConfigurableModuleConfig(Config):
9
+ @property
10
+ def pretrained(self) -> bool:
11
+ return not self.pretrain_config.empty
12
+
13
+
14
+ class FourierFeaturesProjectorConfig(ConfigurableModuleConfig):
15
+ in_features: int
16
+ num_features: int = 256
17
+ layers: List[int] = [256, 512, 256]
18
+ activation: str = 'GELU'
19
+ norm: Optional[str] = None
20
+
21
+
22
+ class RotaryPositionalEncodingConfig(ConfigurableModuleConfig):
23
+ num_embeddings: int
24
+ embedding_dim: int
25
+ base: int = 10000
26
+ cached: bool = True
27
+
28
+
29
+ class PiZeroFlowMatchingDecoderBlockConfig(ConfigurableModuleConfig):
30
+ feature_size: int
31
+ head_dim: int = 128
32
+ num_heads: int = 32
33
+ num_kv_heads: int = 1
34
+ hidden_size: int
35
+ activation: str = 'GELU'
36
+ activation_kwargs: Dict[str, Any] = {}
37
+ norm: str = 'RMSNorm'
38
+ dropout: float = 0.0
39
+ attn_implementation: str = 'sdpa'
40
+ position_embed_config: RotaryPositionalEncodingConfig
41
+
42
+
43
+ class PiZeroFlowMatchingDecoderConfig(ConfigurableModuleConfig):
44
+ num_blocks: int
45
+ block_config: PiZeroFlowMatchingDecoderBlockConfig
46
+
47
+
48
+ class RobotStateProjectorConfig(ConfigurableModuleConfig):
49
+ layers: List[int] = []
50
+ mode: str
51
+ activation: str = 'GELU'
52
+ fourier: bool = False
53
+
54
+ def __post_init__(self):
55
+ super().__post_init__()
56
+ assert self.mode in [
57
+ 'ee_pose',
58
+ 'ee_pose_gripper',
59
+ 'ee_pose_joints',
60
+ 'joints',
61
+ 'all',
62
+ 'none',
63
+ ], self.mode
64
+
65
+
66
+ class FourierFeaturesConfig(ConfigurableModuleConfig):
67
+ num_features: int = 256
68
+ learnable_features: bool = False
69
+ max_period: float = 10000.0
70
+ layers: List[int] = [256, 512, 256]
71
+ activation: str = 'SiLU'
72
+ norm: Optional[str] = None
73
+
74
+
75
+ class NoisedControlProjectorConfig(ConfigurableModuleConfig):
76
+ time_embed: FourierFeaturesConfig
77
+ layers: List[int] = []
78
+ activation: str = 'SiLU'
79
+ norm: Optional[str] = None
80
+
81
+
82
+ class PiZeroFlowMatchingModuleConfig(ConfigurableModuleConfig):
83
+ token_size: int = 1024
84
+ noised_control_proj_config: NoisedControlProjectorConfig
85
+ robot_state_proj_config: RobotStateProjectorConfig
86
+ control_decoder_config: PiZeroFlowMatchingDecoderConfig
87
+ rotation_components: int = 3
88
+
89
+
90
+ class VLMConfig(ConfigurableModuleConfig):
91
+ pass
92
+
93
+
94
+ class InputSequencingConfig(Config):
95
+ """
96
+ past_frames_sequence_length: number of past images needed in a single robot state
97
+ past_scalars_sequence_length: number of past scalar state data, e.g. actions, poses, etc,
98
+ needed in a single robot state
99
+ past_frames_stride_sec: sampling rate, determines how far apart in time each point in the sequence
100
+ is. If None, ignored and takes the default data collection frequency from the dataset
101
+ past_scalars_stride_sec: similar to past_frames_stride_sec
102
+
103
+ sequence_frames: number of temporally-sequential points in a single example in the batch
104
+ sequence_frames_stride_sec: sampling rate
105
+
106
+ Understanding sequence_frames:
107
+ TODO: sequences are possibly useful in some rare cases, maybe sequence modeling problems,
108
+ but yet to be confirmed. Keeping for now, but could be removed if proved unnecessary
109
+
110
+ - past_scalars_sequence_length, past_frames_sequence_length, future_controls_sequence_length,
111
+ future_frames_sequence_length are hyperparameters refering to a SINGLE dataset example / 'state'.
112
+ It is assumed that `past_scalars_sequence_length` and `past_frames_sequence_length` are the min
113
+ number of observations that comprise a single 'state'
114
+ - sequence_frames is a hyperparameter refering to the entire learning process. It controls the size
115
+ of the sequence dimension in the batch. It's treated similarly to the batch dimension, with the
116
+ difference that points in the sequence dimensions are temporally aligned. Unlike `past_*`
117
+ attributes, in supervised learning a label is loaded for every point in the sequence dimension
118
+ and the loss usually computed over the entire sequence dimension.
119
+ """
120
+
121
+ past_scalars_sequence_length: int = 1
122
+ past_frames_sequence_length: int = 1
123
+ past_scalars_stride_sec: Optional[float] = None
124
+ past_frames_stride_sec: Optional[float] = None
125
+ sequence_frames: int = 1
126
+ sequence_frames_stride_sec: Optional[float] = None
127
+
128
+ def __post_init__(self):
129
+ super().__post_init__()
130
+ assert self.past_scalars_sequence_length >= 1, self.past_scalars_sequence_length
131
+ assert self.past_frames_sequence_length >= 1, self.past_frames_sequence_length
132
+ assert self.sequence_frames >= 1, self.sequence_frames
133
+ if self.past_frames_stride_sec is not None:
134
+ assert self.past_frames_stride_sec >= 0.0, self.past_frames_stride_sec
135
+ if self.past_scalars_stride_sec is not None:
136
+ assert self.past_scalars_stride_sec >= 0.0, self.past_scalars_stride_sec
137
+ if self.sequence_frames_stride_sec is not None:
138
+ assert self.sequence_frames_stride_sec >= 0.0, self.sequence_frames_stride_sec
139
+
140
+ def assert_same_past(self) -> None:
141
+ assert (
142
+ self.past_frames_stride_sec == self.past_scalars_stride_sec
143
+ ), f'{self.past_frames_stride_sec} != {self.past_scalars_stride_sec}'
144
+ assert (
145
+ self.past_frames_sequence_length == self.past_scalars_sequence_length
146
+ ), f'{self.past_frames_sequence_length} != {self.past_scalars_sequence_length}'
147
+
148
+
149
+ class OutputSequencingConfig(Config):
150
+ """
151
+ future_controls_sequence_length: number of control steps in the future the model predicts
152
+ future_frames_sequence_length: number of future frames the model predicts
153
+ (only relevant for neural networks that learn some sort of a world model)
154
+
155
+ future_controls_sequence_stride_sec / future_frames_sequence_stride_sec: sampling rate
156
+ that determines how far apart in time each point in the sequence is. If None,
157
+ ignored and takes the default data collection frequency from the dataset
158
+
159
+ future_control_offset_sec: time interval between the last observation and the first
160
+ point at which control is predicted. Serves as a 'causality hyperparameter', allowing
161
+ for predicting controls slightly further into the future in environments with dynamics
162
+ where the observed effects of an action appear slightly later
163
+ """
164
+
165
+ future_controls_sequence_length: int = 1
166
+ future_controls_sequence_stride_sec: Optional[float] = None
167
+ future_frames_sequence_length: int = 1
168
+ future_frames_sequence_stride_sec: Optional[float] = None
169
+ future_control_offset_sec: float = 0.0
170
+
171
+ def __post_init__(self):
172
+ super().__post_init__()
173
+ assert self.future_controls_sequence_length >= 1, self.future_controls_sequence_length
174
+ assert self.future_frames_sequence_length >= 1, self.future_frames_sequence_length
175
+ assert self.future_control_offset_sec >= 0.0, self.future_control_offset_sec
176
+ if self.future_controls_sequence_stride_sec is not None:
177
+ assert self.future_controls_sequence_stride_sec >= 0.0, self.future_controls_sequence_stride_sec
178
+ if self.future_frames_sequence_stride_sec is not None:
179
+ assert self.future_frames_sequence_stride_sec >= 0.0, self.future_frames_sequence_stride_sec
180
+
181
+
182
+ class ControlDataIOConfig(InputSequencingConfig, OutputSequencingConfig):
183
+ pass
184
+
185
+
186
+ class NormalizerConfig(Config):
187
+ pass
188
+
189
+
190
+ class RotationPowermapNormalizerConfig(NormalizerConfig):
191
+ exponent: float
192
+
193
+
194
+ class IdentityNormalizerConfig(NormalizerConfig):
195
+ pass
196
+
197
+
198
+ class DatasetStatsNormalizerConfig(NormalizerConfig):
199
+ stats_filepath: str
200
+ stats_key: str = ''
201
+ component_name: str
202
+ mode: str
203
+
204
+ def __post_init__(self):
205
+ super().__post_init__()
206
+ assert self.mode in {'mean', 'bounds', 'bounds_q99'}, self.mode
207
+
208
+
209
+ class BoundsNormalizerConfig(NormalizerConfig):
210
+ low: List[float]
211
+ high: List[float]
212
+
213
+ def __post_init__(self):
214
+ super().__post_init__()
215
+ if len(self.low) != len(self.high):
216
+ raise ValueError(
217
+ f'Low and high bounds must have the same length, but got {self.low} and {self.high}'
218
+ )
219
+ for low, high in zip(self.low, self.high, strict=True):
220
+ assert low < high, f'Low bound {low} must be less than high bound {high}'
221
+
222
+
223
+ class ControlTokenizerConfig(Config):
224
+ pass
225
+
226
+
227
+ class EmptyTokenizerConfig(ControlTokenizerConfig):
228
+ pass
229
+
230
+
231
+ class VLAMProcessorConfig(Config):
232
+ control_io_config: ControlDataIOConfig
233
+ joints_obs_norm: BoundsNormalizerConfig
234
+ translation_obs_norm: DatasetStatsNormalizerConfig
235
+ rotation_obs_norm: IdentityNormalizerConfig
236
+ translation_control_norm: BoundsNormalizerConfig
237
+ rotation_control_norm: RotationPowermapNormalizerConfig
238
+ translation_obs_frame: ReferenceFrame = ReferenceFrame.ROBOT_BASE
239
+ rotation_obs_frame: ReferenceFrame = ReferenceFrame.ROBOT_BASE
240
+ translation_control_frame: ReferenceFrame = ReferenceFrame.ROBOT_BASE_DELTA
241
+ rotation_control_frame: ReferenceFrame = ReferenceFrame.EEF_DELTA
242
+ rotation_format: RotationFormat
243
+ image_resize: ResizeMode = ResizeMode.SMART
244
+ control_tokenizer_config: EmptyTokenizerConfig
245
+
246
+ def __post_init__(self):
247
+ super().__post_init__()
248
+ if (
249
+ self.rotation_obs_frame != ReferenceFrame.ROBOT_BASE
250
+ or self.translation_obs_frame != ReferenceFrame.ROBOT_BASE
251
+ ):
252
+ raise NotImplementedError()
253
+
254
+ @property
255
+ def delta_controls(self) -> bool:
256
+ translation_is_delta = self.translation_control_frame in (
257
+ ReferenceFrame.ROBOT_BASE_DELTA,
258
+ ReferenceFrame.EEF_DELTA,
259
+ )
260
+ rotation_is_delta = self.rotation_control_frame in (
261
+ ReferenceFrame.ROBOT_BASE_DELTA,
262
+ ReferenceFrame.EEF_DELTA,
263
+ )
264
+ if translation_is_delta != rotation_is_delta:
265
+ raise NotImplementedError(
266
+ 'Delta controls for only one of translation or rotation not yet supported'
267
+ )
268
+ return translation_is_delta
269
+
270
+
271
+ class RegressionProcessorConfig(VLAMProcessorConfig):
272
+ pass
273
+
274
+
275
+ class PiZeroFlowProcessorConfig(RegressionProcessorConfig):
276
+ num_inference_steps: int
277
+ r0_distribution: str = 'uniform'
278
+ timestep_distribution: str
279
+ distribution_hyperparams: Dict[str, Any] = {}
280
+ sig_min: float = 0.001
281
+
282
+ def __post_init__(self):
283
+ super().__post_init__()
284
+ assert self.r0_distribution in ['normal', 'uniform']
285
+ if self.rotation_obs_frame != ReferenceFrame.ROBOT_BASE:
286
+ raise NotImplementedError()
287
+
288
+
289
+ class VLMProcessorConfig(Config):
290
+ pass
291
+
292
+
293
+ class ImageSizeConfig(Config):
294
+ width: int
295
+ height: int
296
+
297
+
298
+ class PaliGemmaProcessorConfig(VLMProcessorConfig):
299
+ image_token: str = '<image>'
300
+ image_sizes: Dict[str, ImageSizeConfig] = {'main': ImageSizeConfig(width=224, height=224)}
301
+ max_language_tokens: int = -1
302
+
303
+ def __post_init__(self):
304
+ super().__post_init__()
305
+ for camera_name, camera_image_size in self.image_sizes.items():
306
+ assert camera_image_size.height % 14 == 0, f'{camera_name}: {camera_image_size}'
307
+ assert camera_image_size.width % 14 == 0, f'{camera_name}: {camera_image_size}'
308
+
309
+ @property
310
+ def num_image_tokens(self) -> Dict[str, int]:
311
+ return {
312
+ camera_name: camera_image_size.height // 14 * (camera_image_size.width // 14)
313
+ for (camera_name, camera_image_size) in self.image_sizes.items()
314
+ }
315
+
316
+ @property
317
+ def is_single_image_size(self) -> bool:
318
+ return (
319
+ len(self.image_sizes) == 1
320
+ or len(set(((image_size.height, image_size.width) for image_size in self.image_sizes.values())))
321
+ == 1
322
+ )
323
+
324
+ @property
325
+ def camera_names(self) -> List[str]:
326
+ return list(self.image_sizes.keys())
327
+
328
+
329
+ class PaliGemmaVLMConfig(VLMConfig):
330
+ model_id: str = 'google/paligemma-3b-mix-224'
331
+ attn_implementation: str = 'flash_attention_2'
332
+ processor_config: PaliGemmaProcessorConfig
333
+ lm_head: bool = False
334
+ paligemma_3d_config: Dict[str, Any] = {}
335
+ depth_tokens: int = 0
336
+ train_only_depth_tokens: bool = False
337
+ mean_resizing: bool = False
338
+
339
+ def __post_init__(self):
340
+ super().__post_init__()
341
+ if self.train_only_depth_tokens:
342
+ assert self.depth_tokens > 0, self.depth_tokens
343
+ if self.paligemma_3d_config.get('mask_prob', 0.0) != 0.0:
344
+ raise NotImplementedError(
345
+ f"Masking is deprecated, but got mask_prob={self.paligemma_3d_config['mask_prob']}"
346
+ )
347
+
348
+ @property
349
+ def paligemma_3d_config_dict(self) -> Dict[str, Any]:
350
+ config = dict(self.paligemma_3d_config)
351
+ config['depth_config'] = dict(config['depth_config'])
352
+ config['depth_config']['image_sizes'] = dict(self.processor_config.image_sizes.as_json())
353
+ return config
354
+
355
+ @property
356
+ def with_depth(self) -> bool:
357
+ return len(self.paligemma_3d_config) > 0
358
+
359
+
360
+ class VLAMConfig(ConfigurableModuleConfig):
361
+ processor_config: PiZeroFlowProcessorConfig
362
+ vlm_config: PaliGemmaVLMConfig
363
+ control_module_config: PiZeroFlowMatchingModuleConfig
364
+
365
+
366
+ MainModelConfig = VLAMConfig
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/format.log ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /scratch/petko_petkov/.cache/bazel/_bazel_petko_petkov/4351ef1ae7995270349c07accca69930/execroot/_main/bazel-out/k8-opt/bin/barrel/train/pipes/vlams/train.runfiles/rules_python++python+python_3_10_x86_64-unknown-linux-gnu/bin/python3 -m autoflake --in-place --remove-all-unused-imports /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/modeling_pizero_fm_paligemma.py /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/configuration_pizero_fm_paligemma.py /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/common_pizero_fm_paligemma.py /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/processing_pizero_fm_paligemma.py
2
+ /scratch/petko_petkov/.cache/bazel/_bazel_petko_petkov/4351ef1ae7995270349c07accca69930/execroot/_main/bazel-out/k8-opt/bin/barrel/train/pipes/vlams/train.runfiles/rules_python++python+python_3_10_x86_64-unknown-linux-gnu/bin/python3 -m isort /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/modeling_pizero_fm_paligemma.py /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/configuration_pizero_fm_paligemma.py /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/common_pizero_fm_paligemma.py /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/processing_pizero_fm_paligemma.py
3
+ /scratch/petko_petkov/.cache/bazel/_bazel_petko_petkov/4351ef1ae7995270349c07accca69930/execroot/_main/bazel-out/k8-opt/bin/barrel/train/pipes/vlams/train.runfiles/rules_python++python+python_3_10_x86_64-unknown-linux-gnu/bin/python3 -m black --config /scratch/petko_petkov/barrel/pyproject.toml /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/modeling_pizero_fm_paligemma.py /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/configuration_pizero_fm_paligemma.py /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/common_pizero_fm_paligemma.py /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/processing_pizero_fm_paligemma.py
4
+
5
+
6
+ Fixing /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/modeling_pizero_fm_paligemma.py
7
+ Fixing /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/configuration_pizero_fm_paligemma.py
8
+ Fixing /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/common_pizero_fm_paligemma.py
9
+ Fixing /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/processing_pizero_fm_paligemma.py
10
+
11
+ reformatted /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/configuration_pizero_fm_paligemma.py
12
+ reformatted /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/common_pizero_fm_paligemma.py
13
+ reformatted /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/processing_pizero_fm_paligemma.py
14
+ reformatted /home/petko_petkov/experiments/vlams/control/sess_2026_03_10_17_30_15_msp3-6_petko_petkov_pizero_fm/hf_export/sanguine-kestrel-violet/src/modeling_pizero_fm_paligemma.py
15
+
16
+ All done! ✨ 🍰 ✨
17
+ 4 files reformatted.
18
+
19
+
20
+ TO RERUN:
21
+
22
+ bazel run //tools:autoflake -- /scratch/petko_petkov/.cache/bazel/_bazel_petko_petkov/4351ef1ae7995270349c07accca69930/execroot/_main/bazel-out/k8-opt/bin/barrel/train/pipes/vlams/train.runfiles/rules_python++python+python_3_10_x86_64-unknown-linux-gnu/bin/python3 -m autoflake
23
+ bazel run //tools:isort -- /scratch/petko_petkov/.cache/bazel/_bazel_petko_petkov/4351ef1ae7995270349c07accca69930/execroot/_main/bazel-out/k8-opt/bin/barrel/train/pipes/vlams/train.runfiles/rules_python++python+python_3_10_x86_64-unknown-linux-gnu/bin/python3 -m isort
24
+ bazel run //tools:black -- /scratch/petko_petkov/.cache/bazel/_bazel_petko_petkov/4351ef1ae7995270349c07accca69930/execroot/_main/bazel-out/k8-opt/bin/barrel/train/pipes/vlams/train.runfiles/rules_python++python+python_3_10_x86_64-unknown-linux-gnu/bin/python3 -m black
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/model_config.yaml ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ control_module_config:
2
+ control_decoder_config:
3
+ block_config:
4
+ activation: GELU
5
+ activation_kwargs:
6
+ approximate: tanh
7
+ attn_implementation: sdpa
8
+ dropout: 0.0
9
+ feature_size: 1024
10
+ head_dim: 256
11
+ hidden_size: 4096
12
+ norm: RMSNorm
13
+ num_heads: 8
14
+ num_kv_heads: 1
15
+ position_embed_config:
16
+ base: 10000
17
+ cached: true
18
+ embedding_dim: 256
19
+ num_embeddings: 512
20
+ num_blocks: 18
21
+ noised_control_proj_config:
22
+ activation: SiLU
23
+ layers:
24
+ - 8
25
+ - 2048
26
+ - 1024
27
+ - 1024
28
+ norm: null
29
+ time_embed:
30
+ activation: SiLU
31
+ layers: []
32
+ learnable_features: false
33
+ max_period: 10000.0
34
+ norm: null
35
+ num_features: 1024
36
+ robot_state_proj_config:
37
+ activation: SiLU
38
+ fourier: false
39
+ layers:
40
+ - 8
41
+ - 1024
42
+ mode: ee_pose_gripper
43
+ rotation_components: 4
44
+ token_size: 1024
45
+ processor_config:
46
+ control_io_config:
47
+ future_control_offset_sec: 0.0
48
+ future_controls_sequence_length: 5
49
+ future_controls_sequence_stride_sec: 0.2
50
+ future_frames_sequence_length: 1
51
+ future_frames_sequence_stride_sec: null
52
+ past_frames_sequence_length: 1
53
+ past_frames_stride_sec: null
54
+ past_scalars_sequence_length: 1
55
+ past_scalars_stride_sec: null
56
+ sequence_frames: 1
57
+ sequence_frames_stride_sec: null
58
+ control_tokenizer_config: {}
59
+ distribution_hyperparams:
60
+ alpha: 1.5
61
+ beta: 1.0
62
+ image_resize: naive
63
+ joints_obs_norm:
64
+ high:
65
+ - 3.141592653589793
66
+ - 3.141592653589793
67
+ - 3.141592653589793
68
+ - 3.141592653589793
69
+ - 3.141592653589793
70
+ - 3.141592653589793
71
+ - 3.141592653589793
72
+ low:
73
+ - -3.141592653589793
74
+ - -3.141592653589793
75
+ - -3.141592653589793
76
+ - -3.141592653589793
77
+ - -3.141592653589793
78
+ - -3.141592653589793
79
+ - -3.141592653589793
80
+ num_inference_steps: 10
81
+ r0_distribution: uniform
82
+ rotation_control_frame: eef_delta
83
+ rotation_control_norm:
84
+ exponent: 0.5
85
+ rotation_format: quaternion
86
+ rotation_obs_frame: robot_base
87
+ rotation_obs_norm: {}
88
+ sig_min: 0.001
89
+ timestep_distribution: beta
90
+ translation_control_frame: robot_base_delta
91
+ translation_control_norm:
92
+ high:
93
+ - 0.04
94
+ - 0.04
95
+ - 0.04
96
+ low:
97
+ - -0.04
98
+ - -0.04
99
+ - -0.04
100
+ translation_obs_frame: robot_base
101
+ translation_obs_norm:
102
+ component_name: translation
103
+ mode: bounds_q99
104
+ stats_filepath: barrel/train/pipes/vlams/types/observation_stats.yaml
105
+ stats_key: ''
106
+ vlm_config:
107
+ attn_implementation: sdpa
108
+ depth_tokens: 0
109
+ lm_head: false
110
+ mean_resizing: false
111
+ model_id: google/paligemma-3b-mix-224
112
+ paligemma_3d_config: {}
113
+ processor_config:
114
+ image_sizes:
115
+ main:
116
+ height: 224
117
+ width: 224
118
+ image_token: <image>
119
+ max_language_tokens: -1
120
+ train_only_depth_tokens: false
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/modeling_pizero_fm_paligemma.py ADDED
The diff for this file is too large to render. See raw diff
 
sess_2026_03_17_21_12_26_msp3-2_petko_petkov_paligemma_hlc_bridge_steering_UNVERIFIED/hf_export/sanguine-kestrel-violet/src/processing_pizero_fm_paligemma.py ADDED
@@ -0,0 +1,1849 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import warnings
2
+ from abc import abstractmethod
3
+ from functools import cached_property
4
+ from typing import Dict, List, Optional, Tuple, TypeVar
5
+
6
+ import numpy as np
7
+ import PIL.Image
8
+ import roma
9
+ import torch
10
+ import torchvision.transforms.v2
11
+ import transformers
12
+ from databib.config import Configurable
13
+ from databib.template import Template
14
+
15
+ from .common_pizero_fm_paligemma import (
16
+ FlowInput,
17
+ ReferenceFrame,
18
+ ResizeMode,
19
+ RoboticsControlPlan,
20
+ RoboticsFlowInput,
21
+ RoboticsInput,
22
+ RoboticsOutput,
23
+ RoboticsTarget,
24
+ RotationFormat,
25
+ expand_dims,
26
+ is_quaternion,
27
+ is_rotmat,
28
+ is_rotmat_3x3,
29
+ is_rotmat_9,
30
+ quaternion_half_cover,
31
+ rotmat_as_3x3,
32
+ rotmat_as_9,
33
+ rotmat_inverse,
34
+ )
35
+ from .configuration_pizero_fm_paligemma import (
36
+ BoundsNormalizerConfig,
37
+ ControlDataIOConfig,
38
+ ControlTokenizerConfig,
39
+ DatasetStatsNormalizerConfig,
40
+ EmptyTokenizerConfig,
41
+ IdentityNormalizerConfig,
42
+ ImageSizeConfig,
43
+ NormalizerConfig,
44
+ PiZeroFlowProcessorConfig,
45
+ RegressionProcessorConfig,
46
+ RotationPowermapNormalizerConfig,
47
+ VLAMProcessorConfig,
48
+ VLMProcessorConfig,
49
+ )
50
+
51
+ ControlTokenizerConfigT = TypeVar('ControlTokenizerConfigT', bound=ControlTokenizerConfig)
52
+
53
+
54
+ class ControlTokenizer(Configurable[ControlTokenizerConfigT], Template[ControlTokenizerConfigT]):
55
+ @abstractmethod
56
+ def __call__(self, *args, **kwargs) -> str:
57
+ """Given GT actions and possibly other information, output text control. Gets appened to the prompt"""
58
+
59
+
60
+ class EmptyTokenizer(ControlTokenizer[EmptyTokenizerConfig]):
61
+ """
62
+ Takes the LLM hidden states from `llm_layer_indices` and concatenates them to produce the
63
+ desired result. Includes the hidden states for the image tokens.
64
+ """
65
+
66
+ def __init__(self, config, tokenizer: transformers.PreTrainedTokenizerBase) -> None:
67
+ super().__init__(config)
68
+ self.tokenizer = tokenizer
69
+
70
+ def __call__(self, *_) -> str:
71
+ return ''
72
+
73
+
74
+ NormalizerConfigT = TypeVar('NormalizerConfigT', bound=NormalizerConfig)
75
+
76
+
77
+ class Normalizer(Configurable[NormalizerConfigT], Template[NormalizerConfigT]):
78
+ @abstractmethod
79
+ def normalize(self, value: torch.Tensor, **kwargs) -> torch.Tensor:
80
+ """
81
+ Normalize the input value.
82
+
83
+ Args:
84
+ value: Tensor to be normalized
85
+ **kwargs: Implmentation-specific arguments for normalization
86
+ Returns:
87
+ Normalized tensor of the same shape as input
88
+ """
89
+
90
+ @abstractmethod
91
+ def unnormalize(self, value: torch.Tensor, **kwargs) -> torch.Tensor:
92
+ """
93
+ Unnormalize the input value.
94
+
95
+ Args:
96
+ value: Tensor to be normalized
97
+ **kwargs: Implmentation-specific arguments for normalization
98
+ Returns:
99
+ Unnormalized tensor of the same shape as input
100
+ """
101
+
102
+
103
+ class IdentityNormalizer(Normalizer[IdentityNormalizerConfig]):
104
+ def normalize(self, value: torch.Tensor, **kwargs) -> torch.Tensor:
105
+ del kwargs
106
+ return value
107
+
108
+ def unnormalize(self, value: torch.Tensor, **kwargs) -> torch.Tensor:
109
+ del kwargs
110
+ return value
111
+
112
+
113
+ def np_unique(data: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
114
+ """
115
+ Compute unique elements in data and corresponding indices.
116
+
117
+ np.unique returns the values in a sorted order, even if the source is not sorted. Thus, if you simply
118
+ run np.unique on unsorted data, the indices you will get will be invalid.
119
+
120
+ """
121
+ (_, indices, inverse) = np.unique(data, return_index=True, return_inverse=True)
122
+ (_, indices_of_first_occurence, inverse_indices, counts) = np.unique(
123
+ indices[inverse], return_index=True, return_inverse=True, return_counts=True
124
+ )
125
+ unique_ids = data[indices_of_first_occurence]
126
+ return unique_ids, indices_of_first_occurence, inverse_indices, counts
127
+
128
+
129
+ def _broadcast_shapes(
130
+ value: torch.Tensor, low: torch.Tensor, high: torch.Tensor
131
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
132
+ """
133
+ Broadcast shapes for normalization:
134
+ Args:
135
+ value: torch.Tensor of shape [..., num_components]. The entire shape might be:
136
+ - [num_components]: `value` has no batch dimension
137
+ - [num_datasets, num_components]: `value` contains entries *aligned* with the dataset bounds
138
+ contained in `low` and `high`
139
+ - [num_datasets, ..., num_components]: `value` contains entries *aligned* with the dataset bounds
140
+ contained in `low` and `high`
141
+ - [..., num_components]: `value` contains multiple dimensions. In this case, `low` and `high`
142
+ must be for a single dataset, i.e. `num_datasets = 1`
143
+
144
+ low: torch.Tensor, shape [num_datasets, num_components], where `num_datasets` can be 1 when `low`
145
+ contains normalization bounds for a single dataset
146
+ high: torch.Tensor, shape [num_datasets, num_components], where `num_datasets` can be 1 when `high`
147
+ contains normalization bounds for a single dataset
148
+ Returns:
149
+ Tuple of torch.Tensors (low, high), where `low` and `high` have the same number of dimensions as `value`
150
+ """
151
+ assert low.ndim == high.ndim == 2, f'{low.shape} != {high.shape} or ndim != 2'
152
+ assert value.shape[-1] == low.shape[-1] == high.shape[-1], f'{value.shape} != {low.shape} / {high.shape}'
153
+ if value.ndim == low.ndim == high.ndim:
154
+ return low, high
155
+ if value.ndim < low.ndim:
156
+ assert low.ndim == high.ndim == 2, f'{low.shape}, {high.shape}'
157
+ assert low.shape[0] == high.shape[0] == 1, f'{low.shape}, {high.shape}'
158
+ (low, high) = (low.view(-1), high.view(-1))
159
+ return low, high
160
+ if low.shape[0] == high.shape[0] == 1:
161
+ low = expand_dims(low.view(-1), ndim=value.ndim, order=[-1, 1])
162
+ high = expand_dims(high.view(-1), ndim=value.ndim, order=[-1, 1])
163
+ else:
164
+ assert value.shape[0] == low.shape[0] == high.shape[0], f'{value.shape} != {low.shape} / {high.shape}'
165
+ low = expand_dims(low, ndim=value.ndim, order=[1, -1, 1])
166
+ high = expand_dims(high, ndim=value.ndim, order=[1, -1, 1])
167
+ return low, high
168
+
169
+
170
+ def normalize_gripper_by_bounds(
171
+ value: torch.Tensor, low: torch.Tensor, high: torch.Tensor, binary: bool = True
172
+ ) -> torch.Tensor:
173
+ """
174
+ If binary, normalize to [0, 1], otherwise normalize to [-1, 1]
175
+ """
176
+ (low, high) = _broadcast_shapes(value, low, high)
177
+ (low, high) = (low.to(device=value.device), high.to(device=value.device))
178
+ if binary:
179
+ return torch.clamp((value - low) / torch.clamp(high - low, min=1e-08), min=0.0, max=1.0)
180
+ return torch.clamp(2 * (value - low) / torch.clamp(high - low, min=1e-08) - 1, min=-1.0, max=1.0)
181
+
182
+
183
+ def unnormalize_by_moments(value: torch.Tensor, mean: torch.Tensor, std: torch.Tensor) -> torch.Tensor:
184
+ (mean, std) = _broadcast_shapes(value, mean, std)
185
+ (mean, std) = (mean.to(device=value.device), std.to(device=value.device))
186
+ return value * (std + 1e-08) + mean
187
+
188
+
189
+ def normalize_by_moments(value: torch.Tensor, mean: torch.Tensor, std: torch.Tensor) -> torch.Tensor:
190
+ (mean, std) = _broadcast_shapes(value, mean, std)
191
+ (mean, std) = (mean.to(device=value.device), std.to(device=value.device))
192
+ return (value - mean) / (std + 1e-08)
193
+
194
+
195
+ def unnormalize_by_bounds(value: torch.Tensor, low: torch.Tensor, high: torch.Tensor) -> torch.Tensor:
196
+ (low, high) = _broadcast_shapes(value, low, high)
197
+ (low, high) = (low.to(device=value.device), high.to(device=value.device))
198
+ return 0.5 * (value + 1) * (high - low) + low
199
+
200
+
201
+ def normalize_by_bounds(value: torch.Tensor, low: torch.Tensor, high: torch.Tensor) -> torch.Tensor:
202
+ (low, high) = _broadcast_shapes(value, low, high)
203
+ (low, high) = (low.to(device=value.device), high.to(device=value.device))
204
+ return torch.clamp(2 * (value - low) / torch.clamp(high - low, min=1e-08) - 1, min=-1.0, max=1.0)
205
+
206
+
207
+ class DatasetStatsNormalizer(Normalizer[DatasetStatsNormalizerConfig]):
208
+ def __init__(self, config: DatasetStatsNormalizerConfig):
209
+ super().__init__(config)
210
+ self._norm_stats = self._load_norm_stats()
211
+
212
+ def _load_norm_stats(self) -> Dict[str, Dict[str, Dict[str, torch.Tensor]]]:
213
+ norm_stats = {
214
+ 'austin_buds_dataset': {
215
+ 'low': [0.3499317765235901, -0.2854413390159607, 0.010516085661947727],
216
+ 'high': [0.7243335843086243, 0.20652863383293152, 0.3218296766281128],
217
+ },
218
+ 'austin_sailor_dataset': {
219
+ 'low': [0.387094110250473, -0.3164229393005371, 0.024492919445037842],
220
+ 'high': [0.6869593262672424, 0.2086469978094101, 0.2551962733268738],
221
+ },
222
+ 'austin_sirius_dataset': {
223
+ 'low': [0.0, -0.11814527958631516, 0.0],
224
+ 'high': [0.532875120639801, 0.26084619760513306, 0.27225059270858765],
225
+ },
226
+ 'bc_z': {
227
+ 'low': [-0.3956047296524048, -0.11924505233764648, 0.601338267326355],
228
+ 'high': [0.332028865814209, 0.3088575601577759, 0.98329097032547],
229
+ },
230
+ 'berkeley_autolab_ur5': {
231
+ 'low': [0.3020566999912262, -0.21297279000282288, -0.18836002051830292],
232
+ 'high': [0.6132073998451233, 0.30656182765960693, 0.12212439626455307],
233
+ },
234
+ 'berkeley_cable_routing': {
235
+ 'low': [0.4641263782978058, -0.2806571424007416, 0.030183622613549232],
236
+ 'high': [0.6452807784080505, 0.28204888105392456, 0.1557157188653946],
237
+ },
238
+ 'berkeley_fanuc_manipulation': {
239
+ 'low': [0.3718133866786957, -0.4071895182132721, 0.01847645826637745],
240
+ 'high': [0.7200658321380615, 0.3128541111946106, 0.5413243770599365],
241
+ },
242
+ 'bridge': {
243
+ 'low': [0.1711955964565277, -0.15639324486255646, -0.048255354166030884],
244
+ 'high': [0.4604376256465912, 0.24112474918365479, 0.18886254727840424],
245
+ },
246
+ 'bridge_orig': {
247
+ 'low': [0.1711955964565277, -0.15639324486255646, -0.048255354166030884],
248
+ 'high': [0.4604376256465912, 0.24112474918365479, 0.18886254727840424],
249
+ },
250
+ 'cmu_stretch': {
251
+ 'low': [0.017430847510695457, 0.0, 0.46050605177879333],
252
+ 'high': [0.33094948530197144, 0.0, 1.0952961444854736],
253
+ },
254
+ 'dlr_edan_shared_control': {
255
+ 'low': [-0.729511022567749, 0.077408567070961, 0.2658006250858307],
256
+ 'high': [-0.13719859719276428, 0.5719971060752869, 0.7898909449577332],
257
+ },
258
+ 'droid': {
259
+ 'low': [0.26669958233833313, -0.43774399161338806, -0.048167888075113297],
260
+ 'high': [0.7774086594581604, 0.42832574248313904, 0.7760910391807556],
261
+ },
262
+ 'fmb': {
263
+ 'low': [0.3655048608779907, -0.28729698061943054, 0.033201027661561966],
264
+ 'high': [0.6782684326171875, 0.209969624876976, 0.3331448435783386],
265
+ },
266
+ 'fractal20220817_data': {
267
+ 'low': [0.3249714970588684, -0.2818704843521118, 0.1410011649131775],
268
+ 'high': [0.8754204511642456, 0.21279653906822205, 1.071526288986206],
269
+ },
270
+ 'furniture_bench_dataset': {
271
+ 'low': [0.36915361881256104, -0.180975541472435, 0.0058300793170928955],
272
+ 'high': [0.6652880311012268, 0.1772783100605011, 0.18316447734832764],
273
+ },
274
+ 'iamlab_cmu_pickup_insert': {
275
+ 'low': [0.31449857354164124, -0.20315787196159363, 0.06785127520561218],
276
+ 'high': [0.6472027897834778, 0.20840713381767273, 0.3700340986251831],
277
+ },
278
+ 'jaco_play': {
279
+ 'low': [-0.3789186179637909, -0.6194459795951843, 0.16865813732147217],
280
+ 'high': [0.21203258633613586, -0.26914602518081665, 0.38958534598350525],
281
+ },
282
+ 'kuka': {
283
+ 'low': [0.4765772819519043, -0.14815208315849304, 0.06674224138259888],
284
+ 'high': [0.6515637040138245, 0.2447487711906433, 0.28018367290496826],
285
+ },
286
+ 'language_table': {
287
+ 'low': [0.19237099587917328, -0.2962527573108673, 0.0],
288
+ 'high': [0.6171894669532776, 0.30645298957824707, 0.0],
289
+ },
290
+ 'nyu_franka_play_dataset': {
291
+ 'low': [0.13936959207057953, 0.07645522058010101, 0.19364508986473083],
292
+ 'high': [0.5920727252960205, 0.6584802269935608, 0.8056891560554504],
293
+ },
294
+ 'roboset': {
295
+ 'low': [0.18437016010284424, -0.25699371099472046, 0.15134164690971375],
296
+ 'high': [0.543661892414093, 0.29646238684654236, 0.6682320833206177],
297
+ },
298
+ 'roboturk': {
299
+ 'low': [0.28454264998435974, -0.3288349509239197, -0.09349551796913147],
300
+ 'high': [0.8773894309997559, 0.2857522964477539, 0.32863926887512207],
301
+ },
302
+ 'stanford_hydra_dataset': {
303
+ 'low': [0.23737286031246185, -0.26521679759025574, 0.09069013595581055],
304
+ 'high': [0.7124238014221191, 0.25299057364463806, 0.49505406618118286],
305
+ },
306
+ 'taco_play': {
307
+ 'low': [0.1368357390165329, -0.4297449290752411, 0.20516259968280792],
308
+ 'high': [0.6700438857078552, 0.5943909883499146, 0.5966404676437378],
309
+ },
310
+ 'toto': {
311
+ 'low': [-0.09177927672863007, -0.3571659028530121, 0.2196546494960785],
312
+ 'high': [0.6757593750953674, 0.2889021635055542, 0.5011094212532043],
313
+ },
314
+ 'ucsd_kitchen_dataset': {
315
+ 'low': [0.18739914894104004, -0.18234309554100037, 0.04897069185972214],
316
+ 'high': [0.6410437822341919, 0.20632223784923553, 0.5983893275260925],
317
+ },
318
+ 'utaustin_mutex': {
319
+ 'low': [0.3217194080352783, -0.4733337163925171, 0.014122226275503635],
320
+ 'high': [0.5321439504623413, 0.3733823001384735, 0.5785381197929382],
321
+ },
322
+ 'viola': {
323
+ 'low': [0.40061360597610474, -0.25196850299835205, 0.010269512422382832],
324
+ 'high': [0.6458418369293213, 0.17776551842689514, 0.4456312954425812],
325
+ },
326
+ }
327
+ return {
328
+ dataset_name: {
329
+ key: torch.tensor(value, dtype=torch.float32) for (key, value) in dataset_stats.items()
330
+ }
331
+ for (dataset_name, dataset_stats) in norm_stats.items()
332
+ }
333
+
334
+ def _broadcast_norm_stats_to_dataset_name(
335
+ self, dataset_name: np.ndarray
336
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
337
+ """
338
+ Create an array of normalization bounds corresponding to dataset names
339
+ Args:
340
+ dataset_name: Array of shape [B] of dataset names for which to fetch normalization stats.
341
+ Note the values can be repeated
342
+ Returns:
343
+ Tuple of (low, high) or (norm, std) stats, each of shape [B, -1]
344
+ """
345
+ if self.config.mode == 'mean':
346
+ (stats_key_1, stats_key_2) = ('mean', 'std')
347
+ else:
348
+ (stats_key_1, stats_key_2) = ('low', 'high')
349
+ (unique_names, _, inverse_indices, _) = np_unique(dataset_name)
350
+ stats_1 = np.zeros([len(unique_names), self._component_size], dtype=np.float32)
351
+ stats_2 = np.zeros([len(unique_names), self._component_size], dtype=np.float32)
352
+ for i, ds_name in enumerate(unique_names):
353
+ stats_1[i] = self._norm_stats[ds_name][stats_key_1].numpy()
354
+ stats_2[i] = self._norm_stats[ds_name][stats_key_2].numpy()
355
+ stats_1 = stats_1[inverse_indices]
356
+ stats_2 = stats_2[inverse_indices]
357
+ return torch.from_numpy(stats_1), torch.from_numpy(stats_2)
358
+
359
+ @property
360
+ def _component_size(self) -> int:
361
+ return list(list(self._norm_stats.values())[0].values())[0].shape[-1]
362
+
363
+ def normalize(self, value: torch.Tensor, dataset_name: np.ndarray, **kwargs) -> torch.Tensor:
364
+ del kwargs
365
+ if self.config.mode == 'mean':
366
+ (mean, std) = self._broadcast_norm_stats_to_dataset_name(dataset_name)
367
+ output = normalize_by_moments(value, mean=mean, std=std)
368
+ else:
369
+ (low, high) = self._broadcast_norm_stats_to_dataset_name(dataset_name)
370
+ output = normalize_by_bounds(value, low=low, high=high)
371
+ return output
372
+
373
+ def unnormalize(self, value: torch.Tensor, dataset_name: np.ndarray, **kwargs) -> torch.Tensor:
374
+ del kwargs
375
+ if self.config.mode == 'mean':
376
+ (mean, std) = self._broadcast_norm_stats_to_dataset_name(dataset_name)
377
+ output = unnormalize_by_moments(value, mean=mean, std=std)
378
+ else:
379
+ (low, high) = self._broadcast_norm_stats_to_dataset_name(dataset_name)
380
+ output = unnormalize_by_bounds(value, low=low, high=high)
381
+ return output
382
+
383
+
384
+ class BoundsNormalizer(Normalizer[BoundsNormalizerConfig]):
385
+ def __init__(self, config: BoundsNormalizerConfig):
386
+ super().__init__(config)
387
+ self.low = torch.tensor(self.config.low, dtype=torch.float32).view(1, -1)
388
+ self.high = torch.tensor(self.config.high, dtype=torch.float32).view(1, -1)
389
+
390
+ def normalize(self, value: torch.Tensor, **kwargs) -> torch.Tensor:
391
+ del kwargs
392
+ return normalize_by_bounds(value, low=self.low, high=self.high)
393
+
394
+ def unnormalize(self, value: torch.Tensor, **kwargs) -> torch.Tensor:
395
+ del kwargs
396
+ return unnormalize_by_bounds(value, low=self.low, high=self.high)
397
+
398
+
399
+ def euler_to_rotmat(angles: torch.Tensor) -> torch.Tensor:
400
+ """
401
+ Args:
402
+ angles: Euler angles in radians in the format 'xyz', shape [..., 3]
403
+ Returns:
404
+ torch.Tensor of shape [..., 3, 3] containing rotation matrices
405
+ """
406
+ return roma.euler_to_rotmat(convention='xyz', angles=angles, degrees=False)
407
+
408
+
409
+ def euler_to_unit_quaternion(angles: torch.Tensor) -> torch.Tensor:
410
+ """
411
+ Args:
412
+ angles: Euler angles in radians in the format 'xyz', shape [..., 3]
413
+ Returns:
414
+ torch.Tensor of shape [..., 4] containing unit quaternions
415
+ """
416
+ return roma.euler_to_unitquat(convention='xyz', angles=angles, degrees=False, normalize=True)
417
+
418
+
419
+ def normalize_quaternion(quaternion: torch.Tensor, eps: float = 1e-08) -> torch.Tensor:
420
+ """
421
+ Args:
422
+ quaternion: Unnormalized quaternion, torch.Tensor of shape [..., 4]
423
+ eps: Small constant to prevent division by zero
424
+ Returns:
425
+ torch.Tensor of shape [..., 4] of unit quaternions
426
+ """
427
+ return quaternion / (quaternion.norm(dim=-1, keepdim=True).detach() + eps)
428
+
429
+
430
+ def quaternion_to_euler(quaternion: torch.Tensor) -> torch.Tensor:
431
+ """
432
+ Args:
433
+ quaternion: torch.Tensor of shape [..., 4]; Can be non-normalized
434
+ Returns:
435
+ torch.Tensor of shape [..., 3, 3] containing rotation matrices in SO(3)
436
+ """
437
+ unit_quat = normalize_quaternion(quaternion)
438
+ rotmat = roma.unitquat_to_euler(convention='xyz', quat=unit_quat, as_tuple=False, degrees=False)
439
+ return rotmat
440
+
441
+
442
+ def quaternion_to_rotmat(quaternion: torch.Tensor) -> torch.Tensor:
443
+ """
444
+ Args:
445
+ quaternion: torch.Tensor of shape [..., 4]; Can be non-normalized
446
+ Returns:
447
+ torch.Tensor of shape [..., 3, 3] containing rotation matrices in SO(3)
448
+ """
449
+ unit_quat = normalize_quaternion(quaternion)
450
+ rotmat = roma.unitquat_to_rotmat(unit_quat)
451
+ return rotmat
452
+
453
+
454
+ def rotmat_to_unit_quaternion(rotmat: torch.Tensor) -> torch.Tensor:
455
+ """
456
+ Args:
457
+ rotmat: Batch of rotation matrices, shape [..., 3, 3]
458
+ Returns:
459
+ Batch of unit quaternions, shape [..., 4]
460
+ """
461
+ rotmat = rotmat_as_3x3(rotmat)
462
+ return roma.rotmat_to_unitquat(rotmat)
463
+
464
+
465
+ def rotmat_to_euler(rotmat: torch.Tensor) -> torch.Tensor:
466
+ """
467
+ Args:
468
+ rotmat: Batch of rotation matrices, shape [..., 3, 3]
469
+ Returns:
470
+ Batch of Euler angles in radiant, shape [..., 3]
471
+ """
472
+ rotmat = rotmat_as_3x3(rotmat)
473
+ return roma.rotmat_to_euler(convention='xyz', rotmat=rotmat, as_tuple=False, degrees=False)
474
+
475
+
476
+ def symmetric_orthogonalization(x: torch.Tensor) -> torch.Tensor:
477
+ """
478
+ Maps 9D input vectors onto SO(3) via symmetric orthogonalization.
479
+ - Let SVD(M) = U \Sigma V^T
480
+ - Returned value is SVD+(M) = U diag(1, 1, det(UV^T)) V^T
481
+ - det(UV^T) ensures that det(SVD+(M)) = 1
482
+ - The return value is a rotation matrix (ortonormal) with the least-squares distance to M
483
+
484
+ Args:
485
+ x: Input matrices, not necessarily orthonormal, shape [..., 9] or [..., 3, 3]
486
+ Returns:
487
+ torch.Tensor with the same shape as x, where each inner 3x3 matrix is in SO(3)
488
+ """
489
+ with warnings.catch_warnings():
490
+ warnings.filterwarnings(
491
+ 'ignore', message='In CPU autocast, but the target dtype is not supported. Disabling autocast.'
492
+ )
493
+ with torch.autocast(device_type=x.device.type, dtype=torch.float32):
494
+ matrices = x.view(-1, 3, 3)
495
+ matrices = matrices.to(dtype=torch.float32)
496
+ (u, s, v) = torch.svd(matrices)
497
+ vt = torch.transpose(v, 1, 2)
498
+ det = torch.det(torch.matmul(u, vt)).view(-1, 1, 1)
499
+ diag_vt = torch.cat((vt[:, :2, :], vt[:, -1:, :] * det), dim=1)
500
+ result = torch.matmul(u, diag_vt)
501
+ result = result.view(*x.shape)
502
+ result = result.to(dtype=x.dtype)
503
+ return result
504
+
505
+
506
+ def is_rotmat_orthonormal(
507
+ rotmat: torch.Tensor, epsilon: float = 1e-06, reduction: str = 'none'
508
+ ) -> torch.Tensor | bool:
509
+ """
510
+ Check if a rotation matrix is orthonormal or not.
511
+ Args:
512
+ rotmat: torch.Tensor of shape [..., 3, 3] or [..., 9]
513
+ epsilon: Tolerance for numerical comparisons. Bigger values allow for more freedom. Generally,
514
+ anything smaller than 1e-6 might incorrectly detect some otrhonormal matrices as not
515
+ reduction:
516
+ 'none' - returns torch.Tensor of bools with the same batch shape
517
+ 'all' - returns a bool, True is ALL matrices in the batch are orthonormal
518
+ Returns:
519
+ torch.Tensor with the same batch shape or bool
520
+ """
521
+ assert is_rotmat(rotmat)
522
+ rotmat = rotmat_as_3x3(rotmat.to(dtype=torch.float32))
523
+ is_orthonormal = roma.is_orthonormal_matrix(rotmat, epsilon=epsilon)
524
+ if reduction == 'none':
525
+ return is_orthonormal
526
+ if reduction == 'all':
527
+ return bool(torch.all(is_orthonormal).item())
528
+ raise ValueError(f'Unknown reduction mode {reduction}')
529
+
530
+
531
+ def is_orthonormal_rotmat(rotmat: torch.Tensor, epsilon=0.01, reduction='none') -> bool:
532
+ """
533
+ Checks if the tensor shape matches that of a rotmat. If the last dimensions of shape are 3x3,
534
+ also checks if the data is a valid rotmat. This is to avoid a possible clash with euler angles
535
+ when accidentally `rotmat.shape[-2:] == [3, 3]`
536
+ """
537
+ return (
538
+ is_rotmat_9(rotmat)
539
+ or is_rotmat_3x3(rotmat)
540
+ and is_rotmat_orthonormal(rotmat, epsilon=epsilon, reduction=reduction)
541
+ )
542
+
543
+
544
+ def is_euler(euler: torch.Tensor) -> bool:
545
+ return euler.shape[-1] == 3 and not is_orthonormal_rotmat(euler, reduction='all')
546
+
547
+
548
+ def normalize_rotation(rotation: torch.Tensor) -> torch.Tensor:
549
+ if is_quaternion(rotation):
550
+ return normalize_quaternion(rotation)
551
+ if is_euler(rotation):
552
+ return rotation
553
+ if is_rotmat(rotation):
554
+ is_flat = is_rotmat_9(rotation)
555
+ rotation = rotmat_as_3x3(rotation) if is_flat else rotation
556
+ rotmat = roma.special_gramschmidt(rotation)
557
+ rotmat = rotmat_as_9(rotmat) if is_flat else rotmat
558
+ return rotmat
559
+ raise ValueError(f'Unknown rotation format: {rotation.shape}')
560
+
561
+
562
+ def rotation_format_from_tensor(rotation) -> RotationFormat:
563
+ if is_quaternion(rotation):
564
+ return RotationFormat.QUATERNION
565
+ if is_orthonormal_rotmat(rotation, reduction='all'):
566
+ return RotationFormat.ROTMAT
567
+ if is_euler(rotation):
568
+ return RotationFormat.EULER
569
+ raise ValueError(f'Tensor shape {rotation.shape} is not a valid rotation format')
570
+
571
+
572
+ def is_unit_quaternion(
573
+ quaternion: torch.Tensor, epsilon: float = 1e-08, reduction: str = 'none'
574
+ ) -> torch.Tensor | bool:
575
+ """
576
+ Check if a quternion is normalized or not.
577
+ Args:
578
+ quaternion: torch.Tensor of shape [..., 4]
579
+ tolerance: Tolerance for numerical comparisons
580
+ reduction:
581
+ 'none' - returns torch.Tensor of bools with the same batch shape
582
+ 'all' - returns a bool, True if ALL quaternions in the batch are normalized
583
+ Returns:
584
+ torch.Tensor with the same batch shape or bool
585
+ """
586
+ if not is_quaternion(quaternion):
587
+ return False
588
+ is_norm = torch.isclose(
589
+ quaternion.norm(dim=-1, keepdim=True),
590
+ torch.tensor(1.0, dtype=quaternion.dtype, device=quaternion.device),
591
+ atol=epsilon,
592
+ )
593
+ if reduction == 'none':
594
+ return is_norm
595
+ if reduction == 'all':
596
+ return bool(torch.all(is_norm).item())
597
+ raise ValueError(f'Unknown reduction mode {reduction}')
598
+
599
+
600
+ def convert_rotation(
601
+ rotation: torch.Tensor | np.ndarray,
602
+ output_format: RotationFormat,
603
+ autonorm: bool = True,
604
+ half_cover: bool = True,
605
+ ) -> torch.Tensor | np.ndarray:
606
+ is_np = isinstance(rotation, np.ndarray)
607
+ if is_np:
608
+ rotation = torch.from_numpy(rotation)
609
+ if is_quaternion(rotation):
610
+ if autonorm and not is_unit_quaternion(rotation, reduction='all'):
611
+ rotation = normalize_quaternion(rotation)
612
+ if output_format == RotationFormat.QUATERNION:
613
+ output = rotation
614
+ elif output_format == RotationFormat.ROTMAT:
615
+ output = rotmat_as_9(quaternion_to_rotmat(rotation))
616
+ elif output_format == RotationFormat.EULER:
617
+ output = quaternion_to_euler(rotation)
618
+ else:
619
+ raise NotImplementedError(f'Unsupported rotation format: {output_format}')
620
+ elif is_orthonormal_rotmat(rotation, reduction='all'):
621
+ if autonorm and not is_rotmat_orthonormal(rotation, epsilon=0.01, reduction='all'):
622
+ rotation = symmetric_orthogonalization(rotation)
623
+ if output_format == RotationFormat.QUATERNION:
624
+ output = rotmat_to_unit_quaternion(rotation)
625
+ elif output_format == RotationFormat.ROTMAT:
626
+ output = rotmat_as_9(rotation)
627
+ elif output_format == RotationFormat.EULER:
628
+ output = rotmat_to_euler(rotation)
629
+ else:
630
+ raise NotImplementedError(f'Unsupported rotation format: {output_format}')
631
+ elif is_euler(rotation):
632
+ if output_format == RotationFormat.QUATERNION:
633
+ output = euler_to_unit_quaternion(rotation)
634
+ elif output_format == RotationFormat.ROTMAT:
635
+ output = rotmat_as_9(euler_to_rotmat(rotation))
636
+ elif output_format == RotationFormat.EULER:
637
+ output = rotation
638
+ else:
639
+ raise NotImplementedError(f'Unsupported rotation format: {output_format}')
640
+ else:
641
+ raise ValueError(f'Unknown rotation encoding with shape {rotation.shape}')
642
+ if output_format == RotationFormat.QUATERNION and half_cover:
643
+ output = quaternion_half_cover(output)
644
+ if is_np:
645
+ output = output.numpy()
646
+ return output
647
+
648
+
649
+ def apply_rotation(rotation: torch.Tensor, value: torch.Tensor) -> torch.Tensor:
650
+ """
651
+ Rotate `value` by `rotation`
652
+ Args:
653
+ rotation: torch.Tensor, euler, quaternion or rotmat. Any batch shape that can be expanded
654
+ such that it broadcasts to `value`
655
+ value: torch.Tensor. Supported shapes:
656
+ - Rotmat: [B, ..., 3, 3] or [B, ..., 9]
657
+ - Quaternion: [B, ..., 4]
658
+ - 3D vector: [B, ..., 3]
659
+ Returns:
660
+ torch.Tensor of the same shape as `value`
661
+ """
662
+ rotation = rotmat_as_3x3(convert_rotation(rotation, RotationFormat.ROTMAT))
663
+ quaternion = is_quaternion(value)
664
+ if quaternion:
665
+ value = convert_rotation(value, RotationFormat.ROTMAT)
666
+ if is_orthonormal_rotmat(value, reduction='all'):
667
+ if is_rotmat_9(value):
668
+ assert rotation.ndim <= value.ndim + 1, f'{rotation.shape}, {value.shape}'
669
+ if rotation.ndim > 2:
670
+ rotation = expand_dims(
671
+ rotation, ndim=value.ndim + 1, order=[1, -1] + [1] * (rotation.ndim - 3) + [1, 1]
672
+ )
673
+ value = rotmat_as_9(torch.matmul(rotation, rotmat_as_3x3(value)))
674
+ else:
675
+ assert rotation.ndim <= value.ndim, f'{rotation.shape}, {value.shape}'
676
+ if rotation.ndim > 2:
677
+ rotation = expand_dims(
678
+ rotation, ndim=value.ndim, order=[1, -1] + [1] * (rotation.ndim - 3) + [1, 1]
679
+ )
680
+ value = torch.matmul(rotation, value)
681
+ else:
682
+ assert value.shape[-1] == 3, f'Expected a 3-dim vector in last dim, but got shape: {value.shape}'
683
+ assert rotation.ndim <= value.ndim + 1, f'{rotation.shape}, {value.shape}'
684
+ if rotation.ndim > 2:
685
+ rotation = expand_dims(
686
+ rotation, ndim=value.ndim + 1, order=[1, -1] + [1] * (rotation.ndim - 3) + [1, 1]
687
+ )
688
+ value = torch.matmul(rotation, value.unsqueeze(-1)).squeeze(-1)
689
+ if quaternion:
690
+ value = convert_rotation(value, RotationFormat.QUATERNION)
691
+ return value
692
+
693
+
694
+ def relative_to_delta_rotations(
695
+ rotation_sequence: torch.Tensor, encoding_frame: ReferenceFrame
696
+ ) -> torch.Tensor:
697
+ """
698
+ Transform a sequence of rotation representations encoded w.r.t. the same reference frame to delta
699
+ rotations where each element is encoded w.r.t. the PREVIOUS rotation frame in the sequence.
700
+ The first element in the sequence remains the same.
701
+
702
+ Ex:
703
+ Sequence of points (rotations): R_1, R_2, R_3, R_4
704
+ `rotation_sequence` contains the rotations: R_01, R_02, R_03, R_04, where 0 is the reference frame
705
+ and R_01 is the pose of R1 frame in the reference frame 0, i.e. R_10 converts from reference
706
+ frame to R1 frame
707
+ Output: R_01, R_12, R_23, R_34, i.e. the rotation poses of R_1 in 0 frame, of R_2 in R1 frame, etc
708
+
709
+ Args:
710
+ rotation_sequence: torch.Tensor of shape [..., S, 9], [..., S, 3, 3] or [..., S, 4], containing
711
+ either rotation matrices (R_01, R_12, R_23, R_34, ...) or quaternions, where S corresponds
712
+ to the sequence dimension
713
+ encoding_frame: Indicates the frame w.r.t. which the input rotations are expressed.
714
+ - EEF: Input rotations are fully expressed w.r.t. 0-th reference frame,
715
+ (i.e. the axis of rotation is defined in 0-th reference frame)
716
+ R_12 = R_01^-1 @ R_02
717
+ R_23 = R_12^-1 @ R_03
718
+ - ROBOT_BASE: Input rotations are still relative, but the
719
+ axis of rotation is defined in robot base frame
720
+ R_12 = R_01^-1 @ R_02
721
+ R_23 = R_12^-1 @ R_03
722
+ - All other EEF or ROBOT_BASE frames treated accordingly
723
+ Returns:
724
+ torch.Tensor of the same shape as rotation_sequence, containing delta rotations
725
+ """
726
+ assert rotation_sequence.ndim >= 3, rotation_sequence.shape
727
+ rotation_format: RotationFormat = rotation_format_from_tensor(rotation_sequence)
728
+ rotation_sequence = convert_rotation(rotation_sequence, RotationFormat.QUATERNION)
729
+ reference_sequence = torch.roll(rotation_sequence, 1, dims=-2).clone()
730
+ reference_sequence[..., 0, :] = roma.identity_quat()
731
+ reference_sequence = roma.quat_inverse(reference_sequence)
732
+ if encoding_frame in ReferenceFrame.eef_frames:
733
+ delta_rotations = roma.quat_product(reference_sequence, rotation_sequence)
734
+ elif encoding_frame in ReferenceFrame.robot_frames:
735
+ delta_rotations = roma.quat_product(rotation_sequence, reference_sequence)
736
+ else:
737
+ raise NotImplementedError(f'Encoding frame {encoding_frame} not implemented')
738
+ delta_rotations = convert_rotation(delta_rotations, rotation_format)
739
+ return delta_rotations
740
+
741
+
742
+ def delta_to_relative_rotations(
743
+ rotation_sequence: torch.Tensor, encoding_frame: ReferenceFrame
744
+ ) -> torch.Tensor:
745
+ """
746
+ Transform a sequence of rotation representations encoded w.r.t. the PREVIOUS rotation frame in the
747
+ sequence to the 0-th element preceding the sequence
748
+
749
+ Ex:
750
+ `rotation_sequence` contains the rotations: R_01, R_12, R_23, R_34, where R0 is the base frame,
751
+ implicitly encoded in R_01 and R_10 converts from R0 frame to R1 frame
752
+ Output: R_01, R_02, R_03, R_04
753
+
754
+ Args:
755
+ rotation_sequence: torch.Tensor of shape [..., S, 9], [..., S, 3, 3] or [..., S, 4], containing
756
+ either rotation matrices (R_01, R_12, R_23, R_34, ...) or quaternions
757
+ Returns:
758
+ torch.Tensor of shape [..., S, 9], [..., S, 3, 3] or [..., S, 4] containing transformed rotations
759
+ (R_01, R_02, R_03, R_04, ...)
760
+ """
761
+ assert rotation_sequence.ndim >= 3, rotation_sequence.shape
762
+ rotation_format: RotationFormat = rotation_format_from_tensor(rotation_sequence)
763
+ rotation_sequence = convert_rotation(rotation_sequence, RotationFormat.QUATERNION)
764
+ rotation_sequence = rotation_sequence.clone()
765
+ cumulative = rotation_sequence[..., :1, :]
766
+ delta_rotations = [cumulative]
767
+ for i in range(2, rotation_sequence.shape[-2] + 1):
768
+ if encoding_frame in ReferenceFrame.eef_frames:
769
+ cumulative = roma.quat_product(cumulative, rotation_sequence[..., i - 1 : i, :])
770
+ elif encoding_frame in ReferenceFrame.robot_frames:
771
+ cumulative = roma.quat_product(rotation_sequence[..., i - 1 : i, :], cumulative)
772
+ else:
773
+ raise NotImplementedError(f'Encoding frame {encoding_frame} not implemented')
774
+ delta_rotations.append(cumulative)
775
+ delta_rotations = torch.cat(delta_rotations, dim=-2)
776
+ delta_rotations = convert_rotation(delta_rotations, rotation_format)
777
+ return delta_rotations
778
+
779
+
780
+ def world_to_relative_rotations(
781
+ rotation_sequence: torch.Tensor, reference_rotation: torch.Tensor, encoding_frame: ReferenceFrame
782
+ ) -> torch.Tensor:
783
+ """
784
+ Transform a sequence of rotations expressed w.r.t. WORLD frame to relative rotations w.r.t.
785
+ `reference_rotation`, where `reference_rotation` is provided w.r.t. WORLD frame.
786
+
787
+ Ex:
788
+ Sequence of points (rotations): R_0, R_1, R_2, R_3, R_4
789
+ `rotation_sequence` contains the rotations: R_W1, R_W2, R_W3, R_W4, where W is the world frame
790
+ and R_W1 is the pose of R1 frame in world frame, i.e. R_1W converts from world frame to R1 frame
791
+ `reference_rotation`: R_W0
792
+ Output: R_01, R_02, R_03, R_04 -> the rotation poses of R_1, R_2, R_3, R_4 expressed in R_0 frame
793
+
794
+ Args:
795
+ rotation_sequence: torch.Tensor of shape [..., S, 9], [..., S, 3, 3] or [..., S, 4], containing
796
+ either rotation matrices (R_W1, R_W2, R_W3, R_W4, ...) or quaternions
797
+ reference_rotation: torch.Tensor, shape [..., 9], [..., 3, 3] or [..., 4] and the SAME number of BATCH
798
+ dims as `rotation_sequence`. The new reference frame, provided w.r.t. WORLD coordinate frame R_W0
799
+ encoding_frame: Indicates the frame w.r.t. which the output rotations would be encoded - the fixed
800
+ world frame (ROBOT_BASE) or the local reference_frame (EEF)
801
+ - EEF: Output rotations are fully expressed w.r.t. reference_rotation
802
+ (i.e. the axis of rotation is defined in reference frame)
803
+ R_W1 = R_W0 @ R_01 <=> R_01 = R_0W @ R_W1
804
+ - ROBOT_BASE: Output rotations are still relative, but
805
+ the axis of rotation is defined in robot base frame
806
+ R_W1 = R_01 @ R_W0 <=> R_01 = R_W1 @ R_0W
807
+ - All other EEF or ROBOT_BASE frames treated accordingly
808
+ Returns:
809
+ torch.Tensor of shape [..., S, 9], [..., S, 3, 3] or [..., S, 4] containing transformed rotations
810
+ (R_01, R_02, R_03, R_04, ...)
811
+ """
812
+ assert rotation_sequence.ndim >= 3, rotation_sequence.shape
813
+ rotation_format: RotationFormat = rotation_format_from_tensor(rotation_sequence)
814
+ reference_rotation = rotmat_as_3x3(convert_rotation(reference_rotation, RotationFormat.ROTMAT))
815
+ rotation_sequence = rotmat_as_3x3(convert_rotation(rotation_sequence, RotationFormat.ROTMAT))
816
+ if reference_rotation.ndim != rotation_sequence.ndim:
817
+ raise ValueError(
818
+ f'Cannot broadcast reference_rotation of shape {reference_rotation.shape} to rotation_sequence of shape {rotation_sequence.shape}. Provide tensors with the same number of batch dimensions'
819
+ )
820
+ R_0W = rotmat_as_3x3(rotmat_inverse(reference_rotation))
821
+ if encoding_frame in ReferenceFrame.eef_frames:
822
+ relative_rotations = torch.matmul(R_0W, rotation_sequence)
823
+ elif encoding_frame in ReferenceFrame.robot_frames:
824
+ relative_rotations = torch.matmul(rotation_sequence, R_0W)
825
+ else:
826
+ raise NotImplementedError(f'Encoding frame {encoding_frame} not implemented')
827
+ relative_rotations = convert_rotation(relative_rotations, rotation_format)
828
+ return relative_rotations
829
+
830
+
831
+ def rotation_to_target_frame(
832
+ rotation: torch.Tensor,
833
+ source_frame: ReferenceFrame,
834
+ target_frame: ReferenceFrame,
835
+ ee_pose_rotation: Optional[torch.Tensor] = None,
836
+ ) -> torch.Tensor:
837
+ """
838
+ Convert rotation sequence from source_frame to target_frame
839
+ Args:
840
+ rotation: torch.Tensor of shape [..., S, 9 | 4 | 3 x 3], containing
841
+ the rotations, where S corresponds to the sequence dimension
842
+ source_frame: indicates the frame w.r.t. which `rotation` is expressed
843
+ target_frame: indicates the frame w.r.t. which the output rotation should be expressed
844
+ ee_pose_rotation: torch.Tensor of shape [..., 9 | 4 | 3 x 3], containing the rotation of the
845
+ current end-effector pose w.r.t. ROBOT_BASE frame. Required only when source_frame and
846
+ target_frame have different core reference frames.
847
+ Returns:
848
+ torch.Tensor of the same shape as rotation, containing the converted rotations
849
+ """
850
+ if source_frame == target_frame:
851
+ return rotation
852
+ assert source_frame in ReferenceFrame.robot_frames | ReferenceFrame.eef_frames, source_frame
853
+ assert target_frame in ReferenceFrame.robot_frames | ReferenceFrame.eef_frames, target_frame
854
+ if ee_pose_rotation is not None:
855
+ ee_pose_rotation = rotmat_as_3x3(convert_rotation(ee_pose_rotation, RotationFormat.ROTMAT))
856
+ if source_frame.to_core() != target_frame.to_core():
857
+ assert ee_pose_rotation is not None, f'{source_frame}, {target_frame}'
858
+ if source_frame in ReferenceFrame.delta_frames:
859
+ rotation = delta_to_relative_rotations(rotation, encoding_frame=source_frame)
860
+ source_frame = source_frame.to_relative()
861
+ if target_frame in ReferenceFrame.robot_frames:
862
+ assert source_frame == ReferenceFrame.EEF_RELATIVE, source_frame
863
+ rotation = world_to_relative_rotations(
864
+ rotation, reference_rotation=rotmat_inverse(ee_pose_rotation), encoding_frame=source_frame
865
+ )
866
+ source_frame = ReferenceFrame.ROBOT_BASE
867
+ elif target_frame in ReferenceFrame.eef_frames:
868
+ assert source_frame in ReferenceFrame.robot_frames, source_frame
869
+ if source_frame == ReferenceFrame.ROBOT_BASE_RELATIVE:
870
+ rotation = world_to_relative_rotations(
871
+ rotation, reference_rotation=rotmat_inverse(ee_pose_rotation), encoding_frame=source_frame
872
+ )
873
+ source_frame = ReferenceFrame.ROBOT_BASE
874
+ rotation = world_to_relative_rotations(
875
+ rotation, reference_rotation=ee_pose_rotation, encoding_frame=target_frame
876
+ )
877
+ source_frame = target_frame.to_relative()
878
+ assert source_frame.to_core() == target_frame.to_core(), f'{source_frame}, {target_frame}'
879
+ if source_frame == target_frame:
880
+ return rotation
881
+ if (
882
+ source_frame in ReferenceFrame.delta_frames
883
+ and target_frame in ReferenceFrame.relative_frames | ReferenceFrame.core_frames
884
+ ):
885
+ rotation = delta_to_relative_rotations(rotation, encoding_frame=source_frame)
886
+ source_frame = source_frame.to_relative()
887
+ elif source_frame == ReferenceFrame.ROBOT_BASE:
888
+ assert ee_pose_rotation is not None
889
+ rotation = world_to_relative_rotations(
890
+ rotation, reference_rotation=ee_pose_rotation, encoding_frame=source_frame
891
+ )
892
+ source_frame = ReferenceFrame.ROBOT_BASE_RELATIVE
893
+ assert source_frame in ReferenceFrame.relative_frames, source_frame
894
+ if target_frame in ReferenceFrame.delta_frames:
895
+ rotation = relative_to_delta_rotations(rotation, encoding_frame=source_frame)
896
+ source_frame = source_frame.to_delta()
897
+ elif target_frame == ReferenceFrame.ROBOT_BASE:
898
+ rotation = world_to_relative_rotations(
899
+ rotation, reference_rotation=rotmat_inverse(ee_pose_rotation), encoding_frame=source_frame
900
+ )
901
+ source_frame = ReferenceFrame.ROBOT_BASE
902
+ assert source_frame == target_frame, f'{source_frame}, {target_frame}'
903
+ return rotation
904
+
905
+
906
+ def power_map_quaternion(quaternion: torch.Tensor, power: float, inverse: bool, eps=1e-12) -> torch.Tensor:
907
+ """
908
+ Forward or inverse 1-1 quaternion remapping on S^3 using a power-law angle map.
909
+ Forward map:
910
+ theta' = pi * (theta / pi)^power
911
+ Inverse map:
912
+ theta = pi * (theta' / pi)^(1/power)
913
+
914
+ Args:
915
+ quaternion: torch.Tensor of shape [..., 4], input quaternion
916
+ power: float, power parameter (<1 spreads small angles, >1 compresses)
917
+ inverse: bool, if True apply the inverse mapping, otherwise apply the forward mapping
918
+ eps: float, small epsilon to stabilize division near zero
919
+
920
+ Returns:
921
+ torh.Tensor of shape [..., 4], mapped quaternion
922
+ """
923
+ assert is_quaternion(quaternion), f'{quaternion.shape} not a quaternion'
924
+ rotvec = roma.unitquat_to_rotvec(quaternion)
925
+ theta = torch.norm(rotvec, dim=-1, keepdim=True)
926
+ power_eff = 1.0 / power if inverse else power
927
+ theta_prime = torch.pi * torch.pow(theta / 2 / torch.pi, power_eff) * 2
928
+ rotvec = rotvec / torch.max(theta, torch.tensor(eps)) * theta_prime
929
+ quaternion_output = roma.rotvec_to_unitquat(rotvec)
930
+ return quaternion_output
931
+
932
+
933
+ def power_map_rotation(rotation: torch.Tensor, power: float, inverse: bool, eps=1e-12) -> torch.Tensor:
934
+ if power == 1.0:
935
+ return rotation
936
+ rotation_format = rotation_format_from_tensor(rotation)
937
+ is_3x3 = is_rotmat_3x3(rotation)
938
+ rotation = convert_rotation(rotation, RotationFormat.QUATERNION, autonorm=False, half_cover=True)
939
+ rotation = power_map_quaternion(rotation, power, inverse, eps=eps)
940
+ rotation = convert_rotation(rotation, rotation_format, autonorm=False, half_cover=True)
941
+ if is_3x3:
942
+ rotation = rotmat_as_3x3(rotation)
943
+ return rotation
944
+
945
+
946
+ class RotationPowermapNormalizer(Normalizer[RotationPowermapNormalizerConfig]):
947
+ def normalize(self, value: torch.Tensor, **kwargs) -> torch.Tensor:
948
+ del kwargs
949
+ return power_map_rotation(value, power=self.config.exponent, inverse=False)
950
+
951
+ def unnormalize(self, value: torch.Tensor, **kwargs) -> torch.Tensor:
952
+ del kwargs
953
+ return power_map_rotation(value, power=self.config.exponent, inverse=True)
954
+
955
+
956
+ def assert_np_hwc_or_hw_image(image: np.ndarray | PIL.Image.Image) -> np.ndarray:
957
+ """Make sure image is of type np.ndarray and HWC format"""
958
+ if isinstance(image, PIL.Image.Image):
959
+ image = np.asarray(image)
960
+ assert isinstance(image, np.ndarray), type(image)
961
+ assert image.ndim in [2, 3], image.shape
962
+ if image.ndim == 3:
963
+ assert image.shape[-1] <= 4, image.shape
964
+ return image
965
+
966
+
967
+ def hw_from_image(image: PIL.Image.Image | np.ndarray) -> tuple[int, int]:
968
+ if isinstance(image, np.ndarray):
969
+ (height, width) = image.shape[:2]
970
+ else:
971
+ (width, height) = image.size
972
+ return height, width
973
+
974
+
975
+ def pad_image(
976
+ image: PIL.Image.Image | np.ndarray,
977
+ target_size: dict[str, int],
978
+ pad_value: tuple[int, int, int] | tuple[float, float, float] | int | float = 0,
979
+ ) -> PIL.Image.Image | np.ndarray:
980
+ """Pad image adding a symmetric border around the height/width."""
981
+ assert isinstance(image, (PIL.Image.Image, np.ndarray)), type(image)
982
+ (height, width) = hw_from_image(image)
983
+ (target_width, target_height) = (target_size['width'], target_size['height'])
984
+ if width == target_width and height == target_height:
985
+ return image
986
+ assert target_width >= width, f"Can't pad image of width {width} to {target_width}"
987
+ assert target_height >= height, f"Can't pad image of height {height} to {target_height}"
988
+ (horizontal_pad, vertical_pad) = (int((target_width - width) / 2), int((target_height - height) / 2))
989
+ if isinstance(image, np.ndarray):
990
+ padding = ((vertical_pad, vertical_pad), (horizontal_pad, horizontal_pad)) + ((0, 0),) * (
991
+ image.ndim - 2
992
+ )
993
+ image = np.pad(image, padding, mode='constant', constant_values=pad_value)
994
+ else:
995
+ padding = (horizontal_pad, vertical_pad, horizontal_pad, vertical_pad)
996
+ image = torchvision.transforms.v2.functional.pad(
997
+ image, padding=padding, fill=pad_value, padding_mode='constant'
998
+ )
999
+ return image
1000
+
1001
+
1002
+ def pad_image_to_ratio(
1003
+ image: PIL.Image.Image | np.ndarray,
1004
+ target_wh_ratio: float,
1005
+ pad_value: tuple[int, int, int] | tuple[float, float, float] | int | float = 0,
1006
+ ) -> PIL.Image.Image | np.ndarray:
1007
+ """Pad image to a target aspect ratio."""
1008
+ (height, width) = hw_from_image(image)
1009
+ wh_ratio = width / height
1010
+ if target_wh_ratio >= wh_ratio:
1011
+ pad_size = {'width': round(height * target_wh_ratio), 'height': height}
1012
+ else:
1013
+ pad_size = {'width': width, 'height': round(width / target_wh_ratio)}
1014
+ image = pad_image(image, target_size=pad_size, pad_value=pad_value)
1015
+ return image
1016
+
1017
+
1018
+ def crop_image(
1019
+ image: np.ndarray | PIL.Image.Image,
1020
+ start_height: int,
1021
+ start_width: int,
1022
+ target_height: int,
1023
+ target_width: int,
1024
+ ) -> np.ndarray | PIL.Image.Image:
1025
+ np_image = assert_np_hwc_or_hw_image(image)
1026
+ (height, width) = hw_from_image(image)
1027
+ assert target_width <= width, f"Can't crop image of width {width} to {target_width}"
1028
+ assert target_height <= height, f"Can't crop image of width {height} to {target_height}"
1029
+ (start_height, start_width) = (round(start_height), round(start_width))
1030
+ (target_height, target_width) = (round(target_height), round(target_width))
1031
+ np_image = np_image[
1032
+ start_height : start_height + target_height, start_width : start_width + target_width, ...
1033
+ ]
1034
+ image = PIL.Image.fromarray(np_image) if isinstance(image, PIL.Image.Image) else np_image
1035
+ return image
1036
+
1037
+
1038
+ def crop_image_center(
1039
+ image: np.ndarray | PIL.Image.Image, target_size: dict[str, int]
1040
+ ) -> np.ndarray | PIL.Image.Image:
1041
+ np_image = assert_np_hwc_or_hw_image(image)
1042
+ (height, width) = np_image.shape[:2]
1043
+ (target_height, target_width) = (target_size['height'], target_size['width'])
1044
+ assert target_width <= width, f"Can't crop image of width {width} to {target_width}"
1045
+ assert target_height <= height, f"Can't crop image of width {height} to {target_height}"
1046
+ top = (height - target_height) // 2
1047
+ left = (width - target_width) // 2
1048
+ np_image = crop_image(np_image, top, left, target_height, target_width)
1049
+ image = PIL.Image.fromarray(np_image) if isinstance(image, PIL.Image.Image) else np_image
1050
+ return image
1051
+
1052
+
1053
+ def crop_image_to_ratio(
1054
+ image: PIL.Image.Image | np.ndarray, target_wh_ratio: float
1055
+ ) -> PIL.Image.Image | np.ndarray:
1056
+ """Pad image to a target aspect ratio."""
1057
+ (height, width) = hw_from_image(image)
1058
+ wh_ratio = width / height
1059
+ if target_wh_ratio >= wh_ratio:
1060
+ crop_size = {'width': width, 'height': round(width / target_wh_ratio)}
1061
+ else:
1062
+ crop_size = {'width': round(height * target_wh_ratio), 'height': height}
1063
+ image = crop_image_center(image, target_size=crop_size)
1064
+ return image
1065
+
1066
+
1067
+ def crop_and_pad_image_to_ratio(
1068
+ image: PIL.Image.Image | np.ndarray,
1069
+ target_wh_ratio: float,
1070
+ mode: ResizeMode | str,
1071
+ pad_value: tuple[int, int, int] | tuple[float, float, float] | int | float = 0,
1072
+ ) -> PIL.Image.Image | np.ndarray:
1073
+ """
1074
+ Crop and pad an image to a target size depending on the mode.
1075
+ It's expected that the source image and target size have different aspect ratios.
1076
+
1077
+ Args:
1078
+ image: The image to crop and pad.
1079
+ target_size: The target size to crop and pad the image to.
1080
+ mode: The mode to use for cropping and padding.
1081
+ """
1082
+ (height, width) = hw_from_image(image)
1083
+ wh_ratio = width / height
1084
+ if np.isclose(wh_ratio, target_wh_ratio, rtol=0.01, atol=0.0001):
1085
+ return image
1086
+ if mode == ResizeMode.SMART:
1087
+ aspect_ratio = max(width, height) / min(width, height)
1088
+ target_ratio = max(target_wh_ratio, 1 / target_wh_ratio)
1089
+ if aspect_ratio == 1:
1090
+ if target_ratio >= 4 / 3 - 0.01:
1091
+ crop_wh_ratio = 4 / 3 if target_wh_ratio >= 1.0 else 3 / 4
1092
+ image = crop_image_to_ratio(image, crop_wh_ratio)
1093
+ else:
1094
+ pass
1095
+ elif aspect_ratio <= 4 / 3 + 0.01:
1096
+ if wh_ratio >= 1.0 != (target_wh_ratio >= 1.0):
1097
+ image = crop_image_to_ratio(image, 1.0)
1098
+ elif wh_ratio >= 1.0 != (target_wh_ratio >= 1.0):
1099
+ image = crop_image_to_ratio(image, 1.0)
1100
+ elif target_ratio >= 4 / 3 + 0.01:
1101
+ pass
1102
+ else:
1103
+ crop_wh_ratio = 4 / 3 if target_wh_ratio >= 1.0 else 3 / 4
1104
+ image = crop_image_to_ratio(image, crop_wh_ratio)
1105
+ image = pad_image_to_ratio(image, target_wh_ratio, pad_value=pad_value)
1106
+ elif mode == ResizeMode.PAD:
1107
+ image = pad_image_to_ratio(image, target_wh_ratio, pad_value=pad_value)
1108
+ elif mode == ResizeMode.CROP:
1109
+ image = crop_image_to_ratio(image, target_wh_ratio)
1110
+ else:
1111
+ raise ValueError(f'Mode {mode} not supported')
1112
+ return image
1113
+
1114
+
1115
+ def is_single_channel_image(image: np.ndarray | PIL.Image.Image) -> bool:
1116
+ if isinstance(image, PIL.Image.Image):
1117
+ return image.mode in ['1', 'L', 'LA', 'La', 'P', 'PA', 'F', 'I', 'I;16', 'I;16L', 'I;16B', 'I;16N']
1118
+ if isinstance(image, np.ndarray):
1119
+ return image.ndim == 2 or image.ndim == 3 and image.shape[2] == 1
1120
+ raise ValueError(f'Unsupported image type: {type(image)}')
1121
+
1122
+
1123
+ def is_binary_mask(image: np.ndarray | PIL.Image.Image) -> bool:
1124
+ image = np.asarray(image)
1125
+ return image.dtype in [np.uint8, np.bool_] and np.max(image) == 1
1126
+
1127
+
1128
+ def resize_image(
1129
+ image: PIL.Image.Image | np.ndarray,
1130
+ target_size: dict[str, int],
1131
+ mode: ResizeMode | str,
1132
+ resample: PIL.Image.Resampling | str = 'auto',
1133
+ pad_value: tuple[int, int, int] | tuple[float, float, float] | int | float = 0,
1134
+ ) -> PIL.Image.Image | np.ndarray:
1135
+ (target_width, target_height) = (target_size['width'], target_size['height'])
1136
+ (height, width) = hw_from_image(image)
1137
+ if height == target_height and width == target_width:
1138
+ return image
1139
+ if resample == 'auto':
1140
+ if is_single_channel_image(image):
1141
+ resample = PIL.Image.Resampling.BILINEAR
1142
+ else:
1143
+ resample = PIL.Image.Resampling.LANCZOS
1144
+ else:
1145
+ assert isinstance(resample, PIL.Image.Resampling), resample
1146
+ if is_single_channel_image(image) and resample not in [
1147
+ PIL.Image.Resampling.BILINEAR,
1148
+ PIL.Image.Resampling.BICUBIC,
1149
+ ]:
1150
+ raise ValueError(
1151
+ f'Single channel images must be resized with bilinear or bicubic, but got {resample}'
1152
+ )
1153
+ if is_bin_mask := is_binary_mask(image):
1154
+ image = np.asarray(image).astype(np.uint8) * 255
1155
+ if mode == ResizeMode.SMART:
1156
+ image = crop_and_pad_image_to_ratio(
1157
+ image, target_wh_ratio=target_width / target_height, mode=mode, pad_value=pad_value
1158
+ )
1159
+ pil_image = PIL.Image.fromarray(image) if isinstance(image, np.ndarray) else image
1160
+ if mode in [ResizeMode.NAIVE, ResizeMode.SMART]:
1161
+ pil_image = pil_image.resize((target_width, target_height), resample=resample)
1162
+ else:
1163
+ raise NotImplementedError(f'Mode {mode} not supported')
1164
+ image = np.asarray(pil_image) if isinstance(image, np.ndarray) else pil_image
1165
+ if is_bin_mask:
1166
+ image = image.astype(np.uint8) > 127
1167
+ return image
1168
+
1169
+
1170
+ def invert_gripper(gripper: np.ndarray, low: float, high: float) -> np.ndarray:
1171
+ if low < 0.0:
1172
+ return np.clip(-gripper, low, high)
1173
+ return high - np.clip(gripper, low, high)
1174
+
1175
+
1176
+ GRIPPER_BOUNDS = {
1177
+ 'austin_buds_dataset': (0.0, 0.08),
1178
+ 'austin_sailor_dataset': (0.0, 0.08),
1179
+ 'austin_sirius_dataset': (0.0, 0.08),
1180
+ 'bc_z': (0.0, 1.0),
1181
+ 'berkeley_autolab_ur5': (0.0, 1.0),
1182
+ 'berkeley_cable_routing': (0.0, 1.0),
1183
+ 'berkeley_fanuc_manipulation': (0.0, 1.0),
1184
+ 'bridge': (0.0, 1.0),
1185
+ 'bridge_orig': (0.0, 1.0),
1186
+ 'cmu_stretch': (-3.0, 3.0),
1187
+ 'dlr_edan_shared_control': (0.0, 1.0),
1188
+ 'droid': (0.0, 1.0),
1189
+ 'fmb': (0.0, 1.0),
1190
+ 'fractal20220817_data': (0.0, 1.0),
1191
+ 'furniture_bench_dataset': (0.0, 0.08),
1192
+ 'iamlab_cmu_pickup_insert': (0.0, 1.0),
1193
+ 'jaco_play': (0.0, 1.4),
1194
+ 'kuka': (0.0, 1.0),
1195
+ 'language_table': (0.0, 1.0),
1196
+ 'nyu_franka_play_dataset': (0.0, 1.0),
1197
+ 'roboset': (0.0, 1.0),
1198
+ 'roboturk': (0.0, 1.0),
1199
+ 'stanford_hydra_dataset': (0.0, 0.08),
1200
+ 'taco_play': (0.0, 0.08),
1201
+ 'toto': (0.0, 1.0),
1202
+ 'ucsd_kitchen_dataset': (0.0, 1.0),
1203
+ 'utaustin_mutex': (0.0, 0.08),
1204
+ 'viola': (0.0, 0.08),
1205
+ }
1206
+
1207
+
1208
+ def preprocess_gripper_observation(
1209
+ gripper: np.ndarray, dataset_name: str | np.ndarray, binary: bool = True
1210
+ ) -> np.ndarray:
1211
+ """
1212
+ Preprocess gripper observation depending on dataset. Input is the raw gripper observation from the dataset
1213
+ or from the robot and output is normalized continuous value.
1214
+ - if `binary`, output is in [0, 1], with 0 = closed and 1 = open.
1215
+ - otherwise, output is in [-1, 1], with -1 = closed and 1 = open.
1216
+
1217
+ Dataset-specific gripper observations:
1218
+ austin_buds_dataset: continuous; ~[0=closed; 0.08=open] (franka gripper)
1219
+ austin_sailor_dataset: continuous; ~[0=closed; 0.08=open] (franka gripper)
1220
+ austin_sirius_dataset: continuous; ~[0=closed; 0.08=open] (franka gripper)
1221
+ bc_z: continuous; [0=open; 1=closed]
1222
+ berkeley_autolab_ur5: binary; [0=open; 1=closed]
1223
+ berkeley_cable_routing: constant (closed)
1224
+ berkeley_fanuc_manipulation: binary; [0=open; 1=closed]
1225
+ bridge: continuous; ~[0=closed; 1=open]
1226
+ bridge_orig: continuous; ~[0=closed; 1=open]
1227
+ cmu_stretch: continuous; [-3=closed; 3=open]
1228
+ dlr_edan_shared_control: missing
1229
+ droid: continuous; [0=open, 1=closed]
1230
+ fmb: binary; [0=open; 1=closed]
1231
+ fractal20220817_data: continuous; [0=open; 1=closed]
1232
+ furniture_bench_dataset: continuous; ~[0=closed; 0.08=open] (franka gripper)
1233
+ iamlab_cmu_pickup_insert: binary; [0=closed; 1=open]
1234
+ jaco_play: continuous; [0=open; 1.4=closed]
1235
+ kuka: binary; [0=open; 1=closed]
1236
+ language_table: constant (no gripper)
1237
+ nyu_franka_play_dataset: missing
1238
+ roboset: continuous; [0=open, 1=closed]
1239
+ roboturk: continuous; [0=closed, 0.04=open]
1240
+ stanford_hydra_dataset: continuous; ~[0=closed; 0.08=open] (franka gripper)
1241
+ taco_play: continuous; ~[0=closed; 0.08=open] (franka gripper)
1242
+ toto: constant (closed)
1243
+ ucsd_kitchen_dataset: missing
1244
+ utaustin_mutex: continuous; ~[0=closed; 0.08=open] (franka gripper)
1245
+ viola: continuous; ~[0=closed; 0.08=open] (franka gripper)
1246
+
1247
+ """
1248
+ if isinstance(dataset_name, np.ndarray):
1249
+ assert np.unique(dataset_name).size == 1, dataset_name
1250
+ dataset_name = str(dataset_name[0])
1251
+ if dataset_name in [
1252
+ 'berkeley_cable_routing',
1253
+ 'dlr_edan_shared_control',
1254
+ 'language_table',
1255
+ 'nyu_franka_play_dataset',
1256
+ 'toto',
1257
+ 'ucsd_kitchen_dataset',
1258
+ ]:
1259
+ gripper = normalize_gripper_by_bounds(
1260
+ torch.from_numpy(gripper),
1261
+ low=torch.full(gripper.shape, GRIPPER_BOUNDS[dataset_name][0], dtype=torch.float32),
1262
+ high=torch.full(gripper.shape, GRIPPER_BOUNDS[dataset_name][1], dtype=torch.float32),
1263
+ binary=binary,
1264
+ ).numpy()
1265
+ elif dataset_name in [
1266
+ 'bc_z',
1267
+ 'berkeley_autolab_ur5',
1268
+ 'berkeley_fanuc_manipulation',
1269
+ 'droid',
1270
+ 'fmb',
1271
+ 'fractal20220817_data',
1272
+ 'jaco_play',
1273
+ 'kuka',
1274
+ 'roboset',
1275
+ ]:
1276
+ (low, high) = GRIPPER_BOUNDS[dataset_name]
1277
+ gripper = normalize_gripper_by_bounds(
1278
+ torch.from_numpy(invert_gripper(gripper, low=low, high=high)),
1279
+ low=torch.full(gripper.shape, GRIPPER_BOUNDS[dataset_name][0], dtype=torch.float32),
1280
+ high=torch.full(gripper.shape, GRIPPER_BOUNDS[dataset_name][1], dtype=torch.float32),
1281
+ binary=binary,
1282
+ ).numpy()
1283
+ elif dataset_name in [
1284
+ 'austin_buds_dataset',
1285
+ 'austin_sailor_dataset',
1286
+ 'austin_sirius_dataset',
1287
+ 'bridge',
1288
+ 'bridge_orig',
1289
+ 'cmu_stretch',
1290
+ 'furniture_bench_dataset',
1291
+ 'iamlab_cmu_pickup_insert',
1292
+ 'roboturk',
1293
+ 'stanford_hydra_dataset',
1294
+ 'taco_play',
1295
+ 'utaustin_mutex',
1296
+ 'viola',
1297
+ ]:
1298
+ (low, high) = GRIPPER_BOUNDS[dataset_name]
1299
+ gripper = normalize_gripper_by_bounds(
1300
+ torch.from_numpy(gripper),
1301
+ low=torch.full(gripper.shape, low, dtype=torch.float32),
1302
+ high=torch.full(gripper.shape, high, dtype=torch.float32),
1303
+ binary=binary,
1304
+ ).numpy()
1305
+ else:
1306
+ raise NotImplementedError(f'Unknown dataset: {dataset_name}')
1307
+ return gripper
1308
+
1309
+
1310
+ VLMProcessorConfigT = TypeVar('VLMProcessorConfigT', bound=VLMProcessorConfig)
1311
+
1312
+
1313
+ class VLMProcessor(Configurable[VLMProcessorConfigT], Template[VLMProcessorConfigT]):
1314
+ @abstractmethod
1315
+ def preprocess_inputs(
1316
+ self, chat: List[str], images: Dict[str, List[PIL.Image.Image]]
1317
+ ) -> Dict[str, torch.Tensor | Dict[str, torch.Tensor]]:
1318
+ ...
1319
+
1320
+ @property
1321
+ @abstractmethod
1322
+ def tokenizer(self) -> transformers.PreTrainedTokenizerBase:
1323
+ pass
1324
+
1325
+ @property
1326
+ @abstractmethod
1327
+ def image_sizes(self) -> Dict[str, ImageSizeConfig]:
1328
+ pass
1329
+
1330
+ @property
1331
+ @abstractmethod
1332
+ def ignore_index(self) -> int:
1333
+ pass
1334
+
1335
+
1336
+ VLAMProcessorConfigT = TypeVar('VLAMProcessorConfigT', bound=VLAMProcessorConfig)
1337
+
1338
+
1339
+ class VLAMProcessor(Configurable[VLAMProcessorConfigT], Template[VLAMProcessorConfigT]):
1340
+ def __init__(self, config: VLAMProcessorConfigT, vlm_processor: VLMProcessor):
1341
+ super().__init__(config)
1342
+ self.vlm_processor = vlm_processor
1343
+ self.control_tokenizer = EmptyTokenizer(
1344
+ config=self.config.control_tokenizer_config, tokenizer=self.tokenizer
1345
+ )
1346
+ self.translation_obs_norm = DatasetStatsNormalizer(self.config.translation_obs_norm)
1347
+ self.rotation_obs_norm = IdentityNormalizer(self.config.rotation_obs_norm)
1348
+ self.translation_control_norm = BoundsNormalizer(self.config.translation_control_norm)
1349
+ self.rotation_control_norm = RotationPowermapNormalizer(self.config.rotation_control_norm)
1350
+ self.joints_obs_norm = BoundsNormalizer(self.config.joints_obs_norm)
1351
+
1352
+ @property
1353
+ def tokenizer(self) -> transformers.PreTrainedTokenizerBase:
1354
+ return self.vlm_processor.tokenizer
1355
+
1356
+ @property
1357
+ def image_sizes(self) -> Dict[str, ImageSizeConfig]:
1358
+ return self.vlm_processor.image_sizes
1359
+
1360
+ @property
1361
+ def camera_names(self) -> List[str]:
1362
+ return list(self.vlm_processor.image_sizes.keys())
1363
+
1364
+ @property
1365
+ def ignore_index(self) -> int:
1366
+ return self.vlm_processor.ignore_index
1367
+
1368
+ @property
1369
+ def control_io_config(self) -> ControlDataIOConfig:
1370
+ return self.config.control_io_config
1371
+
1372
+ @cached_property
1373
+ def rotation_components(self) -> int:
1374
+ if self.config.rotation_format == RotationFormat.EULER:
1375
+ return 3
1376
+ if self.config.rotation_format == RotationFormat.QUATERNION:
1377
+ return 4
1378
+ if self.config.rotation_format == RotationFormat.ROTMAT:
1379
+ return 9
1380
+ raise NotImplementedError(self.config.rotation_format)
1381
+
1382
+ @abstractmethod
1383
+ def policy_control_plan_from_model_target(
1384
+ self, target: RoboticsTarget, dataset_name: np.ndarray
1385
+ ) -> RoboticsControlPlan:
1386
+ """
1387
+ Produce a RoboticsControlPlan from `model_output`. Unnormalizes the outputs, runs any
1388
+ model-specific postprocessing and converts to the desired target reference frame.
1389
+ See `policy_control_plan_from_model_output` for details on arguments.
1390
+ """
1391
+
1392
+ @abstractmethod
1393
+ def policy_control_plan_from_model_output(
1394
+ self, model_output: RoboticsOutput, dataset_name: np.ndarray, valid_mask: torch.Tensor
1395
+ ) -> RoboticsControlPlan:
1396
+ """
1397
+ Produce a RoboticsControlPlan from `model_output`. Unnormalizes the outputs and runs any
1398
+ model-specific postprocessing. Translation and rotation outputs are always in a RELATIVE
1399
+ frame w.r.t. the currrent end-effector pose, where the reference frame used during learning
1400
+ (ROBOT_BASE vs EEF) is preserved for each component. In other words, if translation_control_frame
1401
+ is ROBOT_BASE_DELTA, and rotation_control_frame is EEF_DELTA, then the output translation will be
1402
+ in ROBOT_BASE_RELATIVE frame and rotation in EEF_RELATIVE frame.
1403
+
1404
+ We explicitly avoid any conversions which require the EE pose. The EE pose needs to be in
1405
+ ROBOT_BASE frame, but there are many easy sources of error. For example, it's easy to mistakenly
1406
+ provide the EE pose, which was input to the model and is not guaranteed to be in ROBOT_BASE.
1407
+ It's also easy to provide normalized EE pose, which also leads to incorrect results. Instead,
1408
+ if further conversions are required, it's recommended to call translation_to_target_frame and
1409
+ rotation_to_target_frame outside this function, where the user has full control over.
1410
+
1411
+ Args:
1412
+ model_output: RoboticsOutput from the model of shape [B, num_timesteps, ...]
1413
+ dataset_name: np.ndarray of shape [B] with dataset names for each batch example
1414
+ valid_mask: torch.Tensor of shape [B, num_timesteps] indicating valid control steps
1415
+ Returns:
1416
+ RoboticsControlPlan with **UNNORMALIZED** controls in the desired target frame
1417
+ """
1418
+
1419
+ def resize_image(
1420
+ self, camera_name: str, image: PIL.Image.Image | np.ndarray
1421
+ ) -> PIL.Image.Image | np.ndarray:
1422
+ return resize_image(
1423
+ image,
1424
+ target_size={
1425
+ 'width': self.image_sizes[camera_name].width,
1426
+ 'height': self.image_sizes[camera_name].height,
1427
+ },
1428
+ mode=self.config.image_resize,
1429
+ resample=PIL.Image.Resampling.LANCZOS,
1430
+ )
1431
+
1432
+ def preprocess_inputs(
1433
+ self,
1434
+ chat: List[str],
1435
+ images: Dict[str, PIL.Image.Image | List[PIL.Image.Image]],
1436
+ ee_pose_translation: np.ndarray,
1437
+ ee_pose_rotation: np.ndarray,
1438
+ gripper: np.ndarray,
1439
+ joints: np.ndarray,
1440
+ dataset_name: np.ndarray,
1441
+ inference_mode: bool,
1442
+ control_target: Optional[RoboticsTarget] = None,
1443
+ ) -> Dict[str, torch.Tensor | Dict[str, torch.Tensor]]:
1444
+ """
1445
+ Preprocess the inputs for a single example
1446
+ Args:
1447
+ instruction: Language instruction
1448
+ images: History of input images with increasing timestamps
1449
+ ee_pose_translation: np.ndarray, shape [..., num_past_scalars, 3]
1450
+ ee_pose_rotation: np.ndarray, shape [..., num_past_scalars, 3 | 4 | 9]
1451
+ joints: np.ndarray, shape [..., num_past_scalars, <= 7]
1452
+ dataset_name: 1D np.ndarray
1453
+ inference_mode: If True, prepare the input for inference (e.g. don't include target
1454
+ any tokens in the input if relevant). If control_target is available, it should
1455
+ still be preprocessed for test dataset comparison
1456
+ control_target: RoboticsTarget, each component of shape
1457
+ [..., num_control_steps, num_control_components]. Provided only when available, usually
1458
+ during training and dataset test
1459
+ Returns:
1460
+ Dict containing torch.Tensor with inputs
1461
+ """
1462
+ del control_target, inference_mode
1463
+ inputs = self.vlm_processor.preprocess_inputs(chat=chat, images=images)
1464
+ images: Dict[str, torch.Tensor] = inputs['images']
1465
+ input_ids: torch.Tensor = inputs['input_ids'][..., : self.tokenizer.model_max_length]
1466
+ target_text_tokens_ids: torch.Tensor = inputs['target_ids'][..., : self.tokenizer.model_max_length]
1467
+ attn_mask = torch.ones(input_ids.shape, dtype=torch.bool)
1468
+ ee_pose_translation = torch.tensor(ee_pose_translation, dtype=torch.float32)
1469
+ ee_pose_rotation = torch.tensor(ee_pose_rotation, dtype=torch.float32)
1470
+ ee_pose_rotation = convert_rotation(ee_pose_rotation, self.config.rotation_format, autonorm=True)
1471
+ gripper = preprocess_gripper_observation(gripper, dataset_name)
1472
+ gripper = torch.tensor(gripper, dtype=torch.float32)
1473
+ ee_pose_translation = self.normalize(
1474
+ ee_pose_translation, dataset_name=dataset_name, key='translation_obs'
1475
+ )
1476
+ ee_pose_rotation = self.normalize(ee_pose_rotation, dataset_name=dataset_name, key='rotation_obs')
1477
+ joints = torch.tensor(joints, dtype=torch.float32)
1478
+ if joints.shape[-1] < 7:
1479
+ missing_size = 7 - joints.shape[-1]
1480
+ joints = torch.cat([joints, torch.zeros([*joints.shape[:-1], missing_size])], dim=-1)
1481
+ joints = self.normalize(joints, dataset_name=dataset_name, key='joints_obs')
1482
+ outputs = {
1483
+ 'images': images,
1484
+ 'input_ids': input_ids,
1485
+ 'target_text_tokens_ids': target_text_tokens_ids,
1486
+ 'attn_mask': attn_mask,
1487
+ 'ee_pose_translation': ee_pose_translation,
1488
+ 'ee_pose_rotation': ee_pose_rotation,
1489
+ 'gripper': gripper,
1490
+ 'joints': joints,
1491
+ 'control_tokens_ids': None,
1492
+ 'target_control_tokens_ids': None,
1493
+ }
1494
+ return outputs
1495
+
1496
+ def create_input(
1497
+ self,
1498
+ chat: List[str],
1499
+ images: Dict[str, List[PIL.Image.Image]],
1500
+ ee_pose_translation: np.ndarray,
1501
+ ee_pose_rotation: np.ndarray,
1502
+ gripper: np.ndarray,
1503
+ joints: np.ndarray,
1504
+ dataset_name: np.ndarray,
1505
+ inference_mode: bool,
1506
+ control_target: Optional[RoboticsTarget] = None,
1507
+ ) -> RoboticsInput:
1508
+ inputs = self.preprocess_inputs(
1509
+ chat=chat,
1510
+ images=images,
1511
+ ee_pose_translation=ee_pose_translation,
1512
+ ee_pose_rotation=ee_pose_rotation,
1513
+ gripper=gripper,
1514
+ joints=joints,
1515
+ dataset_name=dataset_name,
1516
+ inference_mode=inference_mode,
1517
+ control_target=control_target,
1518
+ )
1519
+ inputs.pop('target_text_tokens_ids')
1520
+ inputs.pop('target_control_tokens_ids')
1521
+ return RoboticsInput(**inputs)
1522
+
1523
+ def normalize(self, value: torch.Tensor, dataset_name: np.ndarray, key: str) -> torch.Tensor:
1524
+ normalizer = getattr(self, f'{key}_norm')
1525
+ return normalizer.normalize(value, dataset_name=dataset_name)
1526
+
1527
+ def unnormalize(self, value: torch.Tensor, dataset_name: np.ndarray, key: str) -> torch.Tensor:
1528
+ normalizer = getattr(self, f'{key}_norm')
1529
+ return normalizer.unnormalize(value, dataset_name=dataset_name)
1530
+
1531
+ @property
1532
+ def _stats_horizon_key(self) -> str:
1533
+ if self.config.delta_controls:
1534
+ if self.control_io_config.future_controls_sequence_stride_sec is None:
1535
+ horizon = 0.0
1536
+ else:
1537
+ horizon = self.control_io_config.future_controls_sequence_stride_sec
1538
+ elif self.control_io_config.future_controls_sequence_stride_sec is None:
1539
+ if self.control_io_config.future_controls_sequence_length == 1:
1540
+ horizon = 0.0
1541
+ else:
1542
+ raise NotImplementedError()
1543
+ else:
1544
+ horizon = (
1545
+ self.control_io_config.future_controls_sequence_length
1546
+ * self.control_io_config.future_controls_sequence_stride_sec
1547
+ )
1548
+ key = f'horizon_{round(horizon, 2)}s'
1549
+ return key
1550
+
1551
+
1552
+ def world_to_relative_translations(
1553
+ translation_sequence: torch.Tensor, reference_frame: torch.Tensor
1554
+ ) -> torch.Tensor:
1555
+ """
1556
+ Transform a sequence of translation vectors encoded w.r.t. WORLD frame to encoding w.r.t.
1557
+ `reference_frame`, where `reference_frame` is provided w.r.t. WORLD frame
1558
+ Ex:
1559
+ Sequence of points: T1, T2, T3, T4
1560
+ `translation_sequence` contains the vectors: WT1, WT2, WT3, WT4, where W is the world frame
1561
+ Output: T0T1, T0T2, T0T3, T0T4, where T0 is the reference frame
1562
+
1563
+ Args:
1564
+ translation_sequence: torch.Tensor of shape [..., S, 3], containing the translation vectors, where S
1565
+ corresponds to the sequence dimension
1566
+ reference_frame: torch.Tensor, shape [..., 1, 3] and the SAME number of BATCH dims as
1567
+ `translation_sequence`. The new reference frame, provided w.r.t. WORLD coordinate frame
1568
+ Returns:
1569
+ torch.Tensor of the same shape as translation_sequence, containing delta translations
1570
+ """
1571
+ assert translation_sequence.ndim >= 3, translation_sequence.shape
1572
+ if reference_frame.ndim != translation_sequence.ndim:
1573
+ raise ValueError(
1574
+ f'Cannot broadcast reference_frame of shape {reference_frame.shape} to translation_sequence of shape {translation_sequence.shape}. Provide tensors with the same number of batch dimensions'
1575
+ )
1576
+ delta_translations = translation_sequence - reference_frame
1577
+ return delta_translations
1578
+
1579
+
1580
+ def delta_to_relative_translations(translation_sequence: torch.Tensor) -> torch.Tensor:
1581
+ """
1582
+ Transform a sequence of translation vectors encoded w.r.t. PREVIOUS frame in the sequence to encoding
1583
+ w.r.t. the 0-th element preceding the sequence
1584
+ Ex:
1585
+ Sequence of points: T1, T2, T3, T4
1586
+ `translation_sequence` contains the vectors: T0T1, T1T2, T2T3, T3T4, where T0 is the base frame,
1587
+ implicitly encoded in T0T1
1588
+ Output: T0T1, T0T2, T0T3, T0T4
1589
+
1590
+ Args:
1591
+ translation_sequence: torch.Tensor of shape [..., S, 3], containing the translation vectors, where S
1592
+ corresponds to the sequence dimension
1593
+ Returns:
1594
+ torch.Tensor of the same shape as translation_sequence, containing delta translations
1595
+ """
1596
+ assert translation_sequence.ndim >= 3, translation_sequence.shape
1597
+ delta_translations = torch.cumsum(translation_sequence, dim=-2)
1598
+ return delta_translations
1599
+
1600
+
1601
+ def relative_to_delta_translations(translation_sequence: torch.Tensor) -> torch.Tensor:
1602
+ """
1603
+ Transform a sequence of translation vectors encoded w.r.t. the same reference frame to delta translation
1604
+ vectors where each value is encoded w.r.t. the PREVIOUS frame in the sequence. The first element in
1605
+ the sequence remains the same.
1606
+ Ex:
1607
+ Sequence of points: T1, T2, T3, T4
1608
+ `translation_sequence` contains the vectors: RT1, RT2, RT3, RT4, where R is the reference frame
1609
+ Output: RT1, T1T2, T2T3, T3T4
1610
+
1611
+ Args:
1612
+ translation_sequence: torch.Tensor of shape [..., S, 3], containing the translation vectors, where S
1613
+ corresponds to the sequence dimension
1614
+ Returns:
1615
+ torch.Tensor of the same shape as translation_sequence, containing delta translations
1616
+ """
1617
+ assert translation_sequence.ndim >= 3, translation_sequence.shape
1618
+ reference_frames = torch.roll(translation_sequence, 1, dims=-2).clone()
1619
+ reference_frames[..., 0, :] = 0
1620
+ delta_translations = translation_sequence - reference_frames
1621
+ return delta_translations
1622
+
1623
+
1624
+ def translation_to_target_frame(
1625
+ translation: torch.Tensor,
1626
+ source_frame: ReferenceFrame,
1627
+ target_frame: ReferenceFrame,
1628
+ ee_pose_translation: Optional[torch.Tensor] = None,
1629
+ ee_pose_rotation: Optional[torch.Tensor] = None,
1630
+ ) -> torch.Tensor:
1631
+ """
1632
+ Convert translation sequence from source_frame to target_frame
1633
+ Args:
1634
+ translation: torch.Tensor of shape [..., S, 3], containing the translation vectors, where S
1635
+ corresponds to the sequence dimension
1636
+ source_frame: indicates the frame w.r.t. which `translation` is expressed
1637
+ target_frame: indicates the frame w.r.t. which the output translation should be expressed
1638
+ ee_pose_translation: torch.Tensor of shape [B, ..., 3], containing the translation of the current
1639
+ end-effector pose. Required only if target_frame is ROBOT_BASE and source_frame isn't.
1640
+ ee_pose_rotation: torch.Tensor of shape [..., 9 | 4 | 3 x 3], containing the rotation of the
1641
+ current end-effector pose w.r.t. ROBOT_BASE frame. Required only when source_frame and
1642
+ target_frame have different core reference frames.
1643
+ Returns:
1644
+ torch.Tensor of the same shape as translation, containing the converted translations
1645
+ """
1646
+ if source_frame == target_frame:
1647
+ return translation
1648
+ assert source_frame in ReferenceFrame.robot_frames | ReferenceFrame.eef_frames, source_frame
1649
+ assert target_frame in ReferenceFrame.robot_frames | ReferenceFrame.eef_frames, target_frame
1650
+ if ee_pose_rotation is not None:
1651
+ ee_pose_rotation = rotmat_as_3x3(convert_rotation(ee_pose_rotation, RotationFormat.ROTMAT))
1652
+ if source_frame.to_core() != target_frame.to_core():
1653
+ assert ee_pose_rotation is not None, f'{source_frame}, {target_frame}'
1654
+ if source_frame in ReferenceFrame.delta_frames:
1655
+ translation = delta_to_relative_translations(translation)
1656
+ source_frame = source_frame.to_relative()
1657
+ if target_frame in ReferenceFrame.robot_frames:
1658
+ assert source_frame == ReferenceFrame.EEF_RELATIVE, source_frame
1659
+ translation = apply_rotation(rotation=ee_pose_rotation, value=translation)
1660
+ source_frame = ReferenceFrame.ROBOT_BASE_RELATIVE
1661
+ elif target_frame in ReferenceFrame.eef_frames:
1662
+ assert source_frame in ReferenceFrame.robot_frames, source_frame
1663
+ if source_frame == ReferenceFrame.ROBOT_BASE:
1664
+ assert ee_pose_translation is not None
1665
+ translation = world_to_relative_translations(translation, reference_frame=ee_pose_translation)
1666
+ source_frame = ReferenceFrame.ROBOT_BASE_RELATIVE
1667
+ assert source_frame in ReferenceFrame.relative_frames, source_frame
1668
+ translation = apply_rotation(rotation=rotmat_inverse(ee_pose_rotation), value=translation)
1669
+ source_frame = ReferenceFrame.EEF_RELATIVE
1670
+ assert source_frame.to_core() == target_frame.to_core(), f'{source_frame}, {target_frame}'
1671
+ if source_frame == target_frame:
1672
+ return translation
1673
+ if (
1674
+ source_frame in ReferenceFrame.delta_frames
1675
+ and target_frame in ReferenceFrame.relative_frames | ReferenceFrame.core_frames
1676
+ ):
1677
+ translation = delta_to_relative_translations(translation)
1678
+ source_frame = source_frame.to_relative()
1679
+ elif source_frame == ReferenceFrame.ROBOT_BASE:
1680
+ assert ee_pose_translation is not None
1681
+ translation = world_to_relative_translations(translation, reference_frame=ee_pose_translation)
1682
+ source_frame = ReferenceFrame.ROBOT_BASE_RELATIVE
1683
+ assert source_frame in ReferenceFrame.relative_frames, source_frame
1684
+ if target_frame in ReferenceFrame.delta_frames:
1685
+ translation = relative_to_delta_translations(translation)
1686
+ source_frame = source_frame.to_delta()
1687
+ elif target_frame == ReferenceFrame.ROBOT_BASE:
1688
+ translation = world_to_relative_translations(translation, reference_frame=-ee_pose_translation)
1689
+ source_frame = ReferenceFrame.ROBOT_BASE
1690
+ assert source_frame == target_frame, f'{source_frame}, {target_frame}'
1691
+ return translation
1692
+
1693
+
1694
+ class RegressionProcessor(VLAMProcessor[RegressionProcessorConfig]):
1695
+ def policy_control_plan_from_model_target(
1696
+ self, target: RoboticsTarget, dataset_name: np.ndarray
1697
+ ) -> RoboticsControlPlan:
1698
+ """See VLAMProcessor.policy_control_plan_from_model_target for arguments"""
1699
+ translation_m = self.unnormalize(
1700
+ target.translation, dataset_name=dataset_name, key='translation_control'
1701
+ )
1702
+ rotation = self.unnormalize(target.rotation, dataset_name=dataset_name, key='rotation_control')
1703
+ rotmat = convert_rotation(rotation, RotationFormat.ROTMAT)
1704
+ gripper_prob = target.gripper
1705
+ if self.config.translation_control_frame != ReferenceFrame.ROBOT_BASE:
1706
+ translation_m = translation_to_target_frame(
1707
+ translation_m,
1708
+ source_frame=self.config.translation_control_frame,
1709
+ target_frame=self.config.translation_control_frame.to_relative(),
1710
+ )
1711
+ if self.config.rotation_control_frame != ReferenceFrame.ROBOT_BASE:
1712
+ rotmat = rotation_to_target_frame(
1713
+ rotmat,
1714
+ source_frame=self.config.rotation_control_frame,
1715
+ target_frame=self.config.rotation_control_frame.to_relative(),
1716
+ )
1717
+ return RoboticsControlPlan(
1718
+ translation_m=translation_m,
1719
+ rotmat=rotmat,
1720
+ gripper_prob=gripper_prob,
1721
+ valid_mask=target.valid_mask,
1722
+ )
1723
+
1724
+ def policy_control_plan_from_model_output(
1725
+ self, model_output: RoboticsOutput, dataset_name: np.ndarray, valid_mask: torch.Tensor
1726
+ ) -> RoboticsControlPlan:
1727
+ """
1728
+ Called during inference to create control plan from model output
1729
+ See VLAMProcessor.policy_control_plan_from_model_output for arguments
1730
+ """
1731
+ translation_m = self.unnormalize(
1732
+ model_output.translation, dataset_name=dataset_name, key='translation_control'
1733
+ )
1734
+ rotation = self.unnormalize(model_output.rotation, dataset_name=dataset_name, key='rotation_control')
1735
+ rotmat = convert_rotation(rotation, RotationFormat.ROTMAT, autonorm=True)
1736
+ gripper_prob = torch.sigmoid(model_output.gripper)
1737
+ if self.config.translation_control_frame != ReferenceFrame.ROBOT_BASE:
1738
+ translation_m = translation_to_target_frame(
1739
+ translation_m,
1740
+ source_frame=self.config.translation_control_frame,
1741
+ target_frame=self.config.translation_control_frame.to_relative(),
1742
+ )
1743
+ if self.config.rotation_control_frame != ReferenceFrame.ROBOT_BASE:
1744
+ rotmat = rotation_to_target_frame(
1745
+ rotmat,
1746
+ source_frame=self.config.rotation_control_frame,
1747
+ target_frame=self.config.rotation_control_frame.to_relative(),
1748
+ )
1749
+ return RoboticsControlPlan(
1750
+ translation_m=translation_m, rotmat=rotmat, gripper_prob=gripper_prob, valid_mask=valid_mask
1751
+ )
1752
+
1753
+
1754
+ class PiZeroFlowMatchingProcessor(Configurable[PiZeroFlowProcessorConfig], RegressionProcessor):
1755
+ def __init__(self, **kwargs):
1756
+ super().__init__(**kwargs)
1757
+ self.generator: torch.Generator = torch.Generator()
1758
+
1759
+ @cached_property
1760
+ def beta_distribution(self) -> torch.distributions.Beta:
1761
+ return torch.distributions.Beta(
1762
+ self.config.distribution_hyperparams.get('alpha', 1.5),
1763
+ self.config.distribution_hyperparams.get('beta', 1.0),
1764
+ )
1765
+
1766
+ def create_input(self, *args, **kwargs) -> RoboticsFlowInput:
1767
+ """In practice used only during inference"""
1768
+ inputs = super().create_input(*args, **kwargs)
1769
+ flow_input: FlowInput = self.sample_t0_input(batch_size=1, device=torch.device('cpu'))
1770
+ inputs = RoboticsFlowInput(**inputs.as_json(), flow_input=flow_input[0, ...])
1771
+ return inputs
1772
+
1773
+ def sample_timestep(self, batch_size: int) -> torch.Tensor:
1774
+ if self.config.timestep_distribution.lower() == 'uniform':
1775
+ eps = 1e-05
1776
+ sample = (torch.rand(1, generator=self.generator) + torch.arange(batch_size) / batch_size) % (
1777
+ 1 - eps
1778
+ )
1779
+ elif self.config.timestep_distribution.lower() == 'beta':
1780
+ sample = self.beta_distribution.sample([batch_size, 1, 1])
1781
+ sample = (1 - self.config.sig_min) * (1 - sample)
1782
+ else:
1783
+ raise NotImplementedError(self.config.timestep_distribution)
1784
+ sample = sample.view(batch_size, 1, 1)
1785
+ return sample
1786
+
1787
+ def _psi_t(self, timestep: torch.Tensor, x_0: torch.Tensor, x_1: torch.Tensor) -> torch.Tensor:
1788
+ return (1 - (1 - self.config.sig_min) * timestep) * x_0 + timestep * x_1
1789
+
1790
+ def _dpsi_dt(self, x_0: torch.Tensor, x_1: torch.Tensor) -> torch.Tensor:
1791
+ return x_1 - (1 - self.config.sig_min) * x_0
1792
+
1793
+ def sample_t0_input(self, batch_size: int, device: torch.device) -> FlowInput:
1794
+ if self.config.r0_distribution == 'normal':
1795
+ controls_t0 = torch.randn(
1796
+ [
1797
+ batch_size,
1798
+ self.config.control_io_config.future_controls_sequence_length,
1799
+ 3 + self.rotation_components + 1,
1800
+ ],
1801
+ generator=self.generator,
1802
+ ).to(device=device)
1803
+ (translation_t0, rotation_t0, gripper_t0) = torch.split(
1804
+ controls_t0, [3, self.rotation_components, 1], dim=-1
1805
+ )
1806
+ rotation_t0 = normalize_rotation(rotation_t0)
1807
+ elif self.config.r0_distribution == 'uniform':
1808
+ controls_t0 = torch.randn(
1809
+ [batch_size, self.config.control_io_config.future_controls_sequence_length, 4],
1810
+ generator=self.generator,
1811
+ ).to(device=device)
1812
+ (translation_t0, gripper_t0) = torch.split(controls_t0, [3, 1], dim=-1)
1813
+ rotation_t0 = convert_rotation(
1814
+ roma.random_unitquat(
1815
+ (batch_size, self.config.control_io_config.future_controls_sequence_length), device=device
1816
+ ),
1817
+ self.config.rotation_format,
1818
+ )
1819
+ else:
1820
+ raise NotImplementedError(self.config.r0_distribution)
1821
+ if self.config.rotation_format == RotationFormat.QUATERNION:
1822
+ rotation_t0 = quaternion_half_cover(rotation_t0)
1823
+ timestep = torch.zeros([batch_size, 1, 1], device=device)
1824
+ return FlowInput(
1825
+ timestep=timestep,
1826
+ translation_t0=translation_t0,
1827
+ rotation_t0=rotation_t0,
1828
+ gripper_t0=gripper_t0,
1829
+ translation_t=None,
1830
+ rotation_t=None,
1831
+ gripper_t=None,
1832
+ )
1833
+
1834
+ def policy_control_plan_from_model_output(
1835
+ self, model_output: RoboticsOutput, dataset_name: np.ndarray, valid_mask: torch.Tensor
1836
+ ) -> RoboticsControlPlan:
1837
+ """
1838
+ Called during inference to create control plan from model output
1839
+ See VLAMProcessor.policy_control_plan_from_model_output for arguments
1840
+ """
1841
+ model_output = model_output.replace(
1842
+ translation=torch.clamp(model_output.translation, -1, 1),
1843
+ rotation=torch.clamp(model_output.rotation, -1, 1),
1844
+ )
1845
+ control_plan = super().policy_control_plan_from_model_output(
1846
+ model_output=model_output, dataset_name=dataset_name, valid_mask=valid_mask
1847
+ )
1848
+ control_plan = control_plan.replace(gripper_prob=torch.clamp(model_output.gripper, 0, 1))
1849
+ return control_plan