ZibinDong commited on
Commit
895e804
·
verified ·
1 Parent(s): 7c09a0c

Publish audited ActionCodec2 release files

Browse files
Files changed (40) hide show
  1. runtime/physical/semantic.py +377 -0
  2. runtime/physical/trajectory.py +130 -0
  3. runtime/processing_actioncodec2.py +429 -0
  4. runtime/routing/__init__.py +1 -0
  5. runtime/routing/cache.py +41 -0
  6. runtime/routing/defaults.py +609 -0
  7. runtime/routing/presets.py +802 -0
  8. runtime/routing/routing.py +745 -0
  9. runtime/tokenization/__init__.py +1 -0
  10. runtime/tokenization/bpe.py +397 -0
  11. runtime/tokenization/bpe_training.py +235 -0
  12. runtime/tokenization/bpe_vocabulary.py +159 -0
  13. runtime/tokenization/generation.py +122 -0
  14. runtime/tokenization/grammar.py +209 -0
  15. runtime/tokenization/setbpe_spatial.py +177 -0
  16. runtime/tokenization/setbpe_spatial_training.py +311 -0
  17. runtime/tokenization/setbpe_temporal.py +258 -0
  18. runtime/tokenization/setbpe_temporal_training.py +649 -0
  19. runtime/training/__init__.py +1 -0
  20. runtime/training/fitting.py +179 -0
  21. runtime/training/training.py +698 -0
  22. setup.py +17 -0
  23. variants/extended-1st-10mm-16k/config.json +4 -0
  24. variants/extended-1st-10mm-16k/processor_config.json +11 -0
  25. variants/extended-1st-10mm-16k/profiles/eef/bpe.json +0 -0
  26. variants/extended-1st-10mm-16k/profiles/eef/config.json +4 -0
  27. variants/extended-1st-10mm-16k/profiles/eef/config.yaml +74 -0
  28. variants/extended-1st-10mm-16k/profiles/eef/primitives.npz +3 -0
  29. variants/extended-1st-10mm-16k/profiles/eef/processor_config.json +3 -0
  30. variants/extended-1st-10mm-16k/profiles/joint/bpe.json +0 -0
  31. variants/extended-1st-10mm-16k/profiles/joint/config.json +4 -0
  32. variants/extended-1st-10mm-16k/profiles/joint/config.yaml +59 -0
  33. variants/extended-1st-10mm-16k/profiles/joint/primitives.npz +3 -0
  34. variants/extended-1st-10mm-16k/profiles/joint/processor_config.json +3 -0
  35. variants/extended-1st-10mm-16k/router_config.yaml +346 -0
  36. variants/extended-1st-10mm-1k/config.json +4 -0
  37. variants/extended-1st-10mm-1k/processor_config.json +11 -0
  38. variants/extended-1st-10mm-1k/profiles/eef/bpe.json +4222 -0
  39. variants/extended-1st-10mm-1k/profiles/eef/config.json +4 -0
  40. variants/extended-1st-10mm-1k/profiles/eef/config.yaml +74 -0
runtime/physical/semantic.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Canonical physical preprocessing for routed ActionCodec2 profiles."
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ import numpy as np
8
+ from scipy.interpolate import CubicSpline, PchipInterpolator
9
+ from scipy.spatial.transform import Rotation, RotationSpline
10
+
11
+ from ._so3 import euler_xyz_to_quaternion as _euler_xyz_to_quaternion
12
+ from ._so3 import multiply_quaternions as _quaternion_multiply
13
+ from ._so3 import normalize_quaternion as _normalize_quaternion
14
+ from ._so3 import quaternion_to_rotvec as _quaternion_to_rotvec
15
+ from ._so3 import rotvec_to_quaternion as _rotvec_to_quaternion
16
+
17
+ _INTERPOLATION_METHODS = {"linear", "pchip", "cubic"}
18
+
19
+
20
+ def _validate_rate(value: float, name: str) -> float:
21
+ rate = float(value)
22
+ if not math.isfinite(rate) or rate <= 0.0:
23
+ raise ValueError(f"{name} must be positive and finite")
24
+ return rate
25
+
26
+
27
+ def _quaternion_slerp(
28
+ left: np.ndarray, right: np.ndarray, weight: np.ndarray
29
+ ) -> np.ndarray:
30
+
31
+ first = _normalize_quaternion(np.asarray(left, dtype=np.float64))
32
+ second = _normalize_quaternion(np.asarray(right, dtype=np.float64))
33
+ dot = np.sum(first * second, axis=1)
34
+ flip = dot < 0.0
35
+ second[flip] *= -1.0
36
+ dot = np.clip(np.abs(dot), 0.0, 1.0)
37
+ output = np.empty_like(first)
38
+ near = dot > 0.9995
39
+ output[near] = first[near] + weight[near, None] * (second[near] - first[near])
40
+ regular = ~near
41
+ if np.any(regular):
42
+ angle = np.arccos(dot[regular])
43
+ denominator = np.sin(angle)
44
+ w = weight[regular]
45
+ output[regular] = (
46
+ np.sin((1.0 - w) * angle)[:, None] / denominator[:, None] * first[regular]
47
+ + np.sin(w * angle)[:, None] / denominator[:, None] * second[regular]
48
+ )
49
+ return _normalize_quaternion(output)
50
+
51
+
52
+ def resampled_horizon(length: int, *, source_hz: float, target_hz: float) -> int:
53
+ "Return the target control-step count for a regular source horizon."
54
+ if (
55
+ isinstance(length, bool)
56
+ or not isinstance(length, (int, np.integer))
57
+ or length < 1
58
+ ):
59
+ raise ValueError("a trajectory must contain at least one sample")
60
+ source_rate = _validate_rate(source_hz, "source_hz")
61
+ target_rate = _validate_rate(target_hz, "target_hz")
62
+ scaled = int(math.floor(int(length) * target_rate / source_rate + 0.5))
63
+ return max(1, scaled)
64
+
65
+
66
+ def _target_times(
67
+ length: int,
68
+ source_hz: float,
69
+ target_hz: float,
70
+ *,
71
+ target_length: int | None,
72
+ ) -> np.ndarray:
73
+ count = (
74
+ resampled_horizon(length, source_hz=source_hz, target_hz=target_hz)
75
+ if target_length is None
76
+ else target_length
77
+ )
78
+ if isinstance(count, bool) or not isinstance(count, (int, np.integer)) or count < 1:
79
+ raise ValueError("target_length must be a positive integer")
80
+ return np.arange(int(count), dtype=np.float64) / target_hz
81
+
82
+
83
+ def _guarded_natural_cubic(
84
+ source_time: np.ndarray,
85
+ values: np.ndarray,
86
+ target_time: np.ndarray,
87
+ ) -> np.ndarray:
88
+
89
+ sampled = np.asarray(
90
+ CubicSpline(source_time, values, axis=0, bc_type="natural")(target_time),
91
+ dtype=np.float64,
92
+ )
93
+ lower = np.min(values, axis=0)
94
+ upper = np.max(values, axis=0)
95
+
96
+
97
+ tolerance = 1e-9 + 0.05 * (upper - lower)
98
+ fallback_axes = np.any(
99
+ (sampled < lower[None] - tolerance[None])
100
+ | (sampled > upper[None] + tolerance[None]),
101
+ axis=0,
102
+ )
103
+ if np.any(fallback_axes):
104
+ sampled[:, fallback_axes] = PchipInterpolator(
105
+ source_time, values[:, fallback_axes], axis=0
106
+ )(target_time)
107
+ return sampled
108
+
109
+
110
+ def resample_uniform(
111
+ values: np.ndarray,
112
+ *,
113
+ source_hz: float,
114
+ target_hz: float,
115
+ kind: str = "linear",
116
+ target_length: int | None = None,
117
+ method: str = "linear",
118
+ ) -> np.ndarray:
119
+ "Resample one regular control-horizon trajectory onto a target clock."
120
+ source_rate = _validate_rate(source_hz, "source_hz")
121
+ target_rate = _validate_rate(target_hz, "target_hz")
122
+ array = np.asarray(values, dtype=np.float64)
123
+ if array.ndim != 2 or len(array) < 1 or not np.all(np.isfinite(array)):
124
+ raise ValueError("values must be a non-empty finite [T, D] array")
125
+ if kind not in {"linear", "hold", "pose_xyzw", "pose_euler_xyz"}:
126
+ raise ValueError("unsupported resampling kind")
127
+ if method not in _INTERPOLATION_METHODS:
128
+ raise ValueError(f"method must be one of {sorted(_INTERPOLATION_METHODS)}")
129
+ if kind == "pose_xyzw" and array.shape[1] != 7:
130
+ raise ValueError("pose_xyzw values must have shape [T, 7]")
131
+ if kind == "pose_euler_xyz" and array.shape[1] != 6:
132
+ raise ValueError("pose_euler_xyz values must have shape [T, 6]")
133
+ standard_length = resampled_horizon(
134
+ len(array), source_hz=source_rate, target_hz=target_rate
135
+ )
136
+ if source_rate == target_rate and target_length in {None, standard_length}:
137
+ if kind == "pose_euler_xyz":
138
+ array = np.concatenate(
139
+ (array[:, :3], _euler_xyz_to_quaternion(array[:, 3:6])), axis=1
140
+ )
141
+ return np.asarray(array, dtype=np.float32)
142
+
143
+ target_time = _target_times(
144
+ len(array), source_rate, target_rate, target_length=target_length
145
+ )
146
+ source_time = np.arange(len(array), dtype=np.float64) / source_rate
147
+ interpolation_time = np.minimum(target_time, source_time[-1])
148
+ source_position = target_time * source_rate
149
+ left = np.minimum(np.floor(source_position).astype(np.int64), len(array) - 1)
150
+ right = np.minimum(left + 1, len(array) - 1)
151
+ weight = source_position - left
152
+ if kind == "hold":
153
+ result = array[left]
154
+ else:
155
+ pose = kind in {"pose_xyzw", "pose_euler_xyz"}
156
+ continuous = array[:, :3] if pose else array
157
+ if len(array) == 1:
158
+ sampled = np.repeat(continuous, len(target_time), axis=0)
159
+ elif method == "pchip":
160
+ sampled = PchipInterpolator(source_time, continuous, axis=0)(
161
+ interpolation_time
162
+ )
163
+ elif method == "cubic":
164
+ sampled = _guarded_natural_cubic(
165
+ source_time, continuous, interpolation_time
166
+ )
167
+ else:
168
+ sampled = array[left, : continuous.shape[1]] + weight[:, None] * (
169
+ array[right, : continuous.shape[1]] - array[left, : continuous.shape[1]]
170
+ )
171
+ if not pose:
172
+ result = sampled
173
+ else:
174
+ quaternion = (
175
+ array[:, 3:7]
176
+ if kind == "pose_xyzw"
177
+ else _euler_xyz_to_quaternion(array[:, 3:6])
178
+ )
179
+ if len(array) == 1:
180
+ rotation = np.repeat(quaternion, len(target_time), axis=0)
181
+ elif method == "linear":
182
+ rotation = _quaternion_slerp(
183
+ quaternion[left], quaternion[right], weight
184
+ )
185
+ else:
186
+ rotation = RotationSpline(source_time, Rotation.from_quat(quaternion))(
187
+ interpolation_time
188
+ ).as_quat()
189
+ result = np.concatenate((sampled, rotation), axis=1)
190
+ return np.asarray(result, dtype=np.float32)
191
+
192
+
193
+ def eef_pose_absolute(poses: np.ndarray) -> np.ndarray:
194
+ """Convert canonical ``xyz + xyzw`` state to ``xyz + rotation_vector``."""
195
+ values = np.asarray(poses, dtype=np.float64)
196
+ if values.ndim != 2 or values.shape[1] != 7 or not np.all(np.isfinite(values)):
197
+ raise ValueError("poses must be a finite [T, 7] array")
198
+ return np.asarray(
199
+ np.concatenate((values[:, :3], _quaternion_to_rotvec(values[:, 3:7])), axis=1),
200
+ dtype=np.float32,
201
+ )
202
+
203
+
204
+ def eef_pose_components(poses: np.ndarray, representation: str) -> np.ndarray:
205
+ """Convert canonical ``xyz + xyzw`` poses to one registered source layout."""
206
+ values = np.asarray(poses, dtype=np.float64)
207
+ if values.ndim != 2 or values.shape[1] != 7 or not np.all(np.isfinite(values)):
208
+ raise ValueError("poses must be a finite [T, 7] array")
209
+ quaternion = _normalize_quaternion(values[:, 3:7])
210
+ if representation == "rotation_vector":
211
+ rotation = _quaternion_to_rotvec(quaternion)
212
+ elif representation == "euler_xyz":
213
+ rotation = Rotation.from_quat(quaternion).as_euler("xyz")
214
+ elif representation == "quaternion_xyzw":
215
+ rotation = quaternion
216
+ else:
217
+ raise ValueError("unsupported EEF rotation representation")
218
+ return np.asarray(
219
+ np.concatenate((values[:, :3], rotation), axis=1), dtype=np.float32
220
+ )
221
+
222
+
223
+ def eef_pose_from_components(
224
+ position: np.ndarray,
225
+ rotation: np.ndarray,
226
+ representation: str,
227
+ ) -> np.ndarray:
228
+ "Build canonical ``xyz + xyzw`` poses from an explicit source layout."
229
+ translation = np.asarray(position, dtype=np.float64)
230
+ orientation = np.asarray(rotation, dtype=np.float64)
231
+ if translation.ndim != 2 or translation.shape[1] != 3 or len(translation) < 1:
232
+ raise ValueError("position must have non-empty shape [T, 3]")
233
+ if len(orientation) != len(translation) or not np.all(np.isfinite(translation)):
234
+ raise ValueError("position and rotation must have matching finite trajectories")
235
+ if representation == "rotation_vector":
236
+ if orientation.ndim != 2 or orientation.shape[1] != 3:
237
+ raise ValueError("rotation_vector must have shape [T, 3]")
238
+ quaternion = _rotvec_to_quaternion(orientation)
239
+ elif representation == "euler_xyz":
240
+ if orientation.ndim != 2 or orientation.shape[1] != 3:
241
+ raise ValueError("euler_xyz must have shape [T, 3]")
242
+ quaternion = _euler_xyz_to_quaternion(orientation)
243
+ elif representation == "quaternion_xyzw":
244
+ if orientation.ndim != 2 or orientation.shape[1] != 4:
245
+ raise ValueError("quaternion_xyzw must have shape [T, 4]")
246
+ quaternion = _normalize_quaternion(orientation)
247
+ else:
248
+ raise ValueError("unsupported EEF rotation representation")
249
+ if not np.all(np.isfinite(orientation)):
250
+ raise ValueError("position and rotation must be finite")
251
+ return np.asarray(
252
+ np.concatenate((translation, quaternion), axis=1), dtype=np.float32
253
+ )
254
+
255
+
256
+ def eef_pose_local_absolute(
257
+ poses: np.ndarray, *, initial_pose: np.ndarray | None = None
258
+ ) -> np.ndarray:
259
+ "Express measured poses relative to an explicit or inferred initial pose."
260
+ values = np.asarray(poses, dtype=np.float64)
261
+ if (
262
+ values.ndim != 2
263
+ or values.shape[1] != 7
264
+ or len(values) < 1
265
+ or not np.all(np.isfinite(values))
266
+ ):
267
+ raise ValueError("poses must be a finite non-empty [T, 7] array")
268
+ if initial_pose is None:
269
+ initial = values[:1]
270
+ else:
271
+ initial = np.asarray(initial_pose, dtype=np.float64)
272
+ if initial.shape == (7,):
273
+ initial = initial[None]
274
+ if initial.shape != (1, 7) or not np.all(np.isfinite(initial)):
275
+ raise ValueError("initial_pose must be a finite [7] pose")
276
+ quaternion = _normalize_quaternion(values[:, 3:7])
277
+ initial_quaternion = _normalize_quaternion(initial[:, 3:7])
278
+ inverse_start = initial_quaternion.copy()
279
+ inverse_start[:, :3] *= -1.0
280
+ relative = _quaternion_multiply(
281
+ np.broadcast_to(inverse_start, quaternion.shape), quaternion
282
+ )
283
+ return np.asarray(
284
+ np.concatenate(
285
+ (values[:, :3] - initial[:, :3], _quaternion_to_rotvec(relative)), axis=1
286
+ ),
287
+ dtype=np.float32,
288
+ )
289
+
290
+
291
+ def eef_pose_deltas(poses: np.ndarray) -> np.ndarray:
292
+ "Differentiate a local ``xyz + xyzw`` path from zero/identity."
293
+ values = np.asarray(poses, dtype=np.float64)
294
+ if (
295
+ values.ndim != 2
296
+ or values.shape[1] != 7
297
+ or len(values) < 1
298
+ or not np.all(np.isfinite(values))
299
+ ):
300
+ raise ValueError("poses must be a finite non-empty [T, 7] array")
301
+ quaternion = _normalize_quaternion(values[:, 3:7])
302
+ previous_position = np.concatenate((np.zeros((1, 3)), values[:-1, :3]), axis=0)
303
+ identity = np.asarray([[0.0, 0.0, 0.0, 1.0]], dtype=np.float64)
304
+ previous_quaternion = np.concatenate((identity, quaternion[:-1]), axis=0)
305
+ inverse_previous = previous_quaternion.copy()
306
+ inverse_previous[:, :3] *= -1.0
307
+ increment = _quaternion_multiply(inverse_previous, quaternion)
308
+ return np.asarray(
309
+ np.concatenate(
310
+ (values[:, :3] - previous_position, _quaternion_to_rotvec(increment)),
311
+ axis=1,
312
+ ),
313
+ dtype=np.float32,
314
+ )
315
+
316
+
317
+ def eef_pose_from_local_absolute(
318
+ poses: np.ndarray, *, initial_pose: np.ndarray
319
+ ) -> np.ndarray:
320
+ """Attach an external initial pose to a local ``xyz + xyzw`` path."""
321
+ values = np.asarray(poses, dtype=np.float64)
322
+ initial = np.asarray(initial_pose, dtype=np.float64)
323
+ if (
324
+ values.ndim != 2
325
+ or values.shape[1] != 7
326
+ or len(values) < 1
327
+ or not np.all(np.isfinite(values))
328
+ ):
329
+ raise ValueError("poses must be a finite non-empty [T, 7] array")
330
+ if initial.shape == (7,):
331
+ initial = initial[None]
332
+ if initial.shape != (1, 7) or not np.all(np.isfinite(initial)):
333
+ raise ValueError("initial_pose must be a finite [7] pose")
334
+ local_quaternion = _normalize_quaternion(values[:, 3:7])
335
+ initial_quaternion = _normalize_quaternion(initial[:, 3:7])
336
+ quaternion = _quaternion_multiply(
337
+ np.broadcast_to(initial_quaternion, local_quaternion.shape), local_quaternion
338
+ )
339
+ return np.asarray(
340
+ np.concatenate((values[:, :3] + initial[:, :3], quaternion), axis=1),
341
+ dtype=np.float32,
342
+ )
343
+
344
+
345
+ def integrate_eef_deltas(deltas: np.ndarray) -> np.ndarray:
346
+ "Integrate ``[dx,dy,dz,body-rotvec]`` from a zero/identity pose."
347
+ values = np.asarray(deltas, dtype=np.float64)
348
+ if values.ndim != 2 or values.shape[1] != 6 or not np.all(np.isfinite(values)):
349
+ raise ValueError("deltas must be a finite [T, 6] array")
350
+ if len(values) < 1:
351
+ raise ValueError("deltas must contain at least one step")
352
+ translation = np.cumsum(values[:, :3], axis=0)
353
+ increments = _rotvec_to_quaternion(values[:, 3:6])
354
+ orientation = np.empty((len(values), 4), dtype=np.float64)
355
+ current = np.asarray([[0.0, 0.0, 0.0, 1.0]], dtype=np.float64)
356
+ for index, increment in enumerate(increments):
357
+ current = _quaternion_multiply(current, increment[None])
358
+ orientation[index] = current[0]
359
+ return np.asarray(
360
+ np.concatenate((translation, orientation), axis=1), dtype=np.float32
361
+ )
362
+
363
+
364
+ def binarize_gripper_actions(
365
+ values: np.ndarray,
366
+ *,
367
+ threshold: float,
368
+ open_is_high: bool,
369
+ ) -> np.ndarray:
370
+ """Map one controller gripper channel to canonical ``0=closed, 1=open``."""
371
+ array = np.asarray(values, dtype=np.float32)
372
+ if array.ndim != 2 or array.shape[1] != 1 or not np.all(np.isfinite(array)):
373
+ raise ValueError("gripper action must be a finite [T, 1] array")
374
+ if not math.isfinite(threshold):
375
+ raise ValueError("gripper threshold must be finite")
376
+ opened = array >= threshold if open_is_high else array <= threshold
377
+ return opened.astype(np.float32)
runtime/physical/trajectory.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Variable-width trajectory contract shared by statistics and fitting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+
8
+ import numpy as np
9
+
10
+ from ..common._validation import active_dimensions
11
+ from .configuration import ActionCodec2Config
12
+
13
+ __all__ = ["ActiveTrajectory", "TrajectoryLike", "prepare_trajectory"]
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ActiveTrajectory:
18
+ "One complete trajectory over an explicit subset of codebook dimensions."
19
+
20
+ values: np.ndarray
21
+ active_dims: tuple[int, ...]
22
+ previous_first_order: np.ndarray | None = None
23
+
24
+ def __init__(
25
+ self,
26
+ values: np.ndarray,
27
+ active_dims: Sequence[int],
28
+ *,
29
+ previous_first_order: np.ndarray | None = None,
30
+ ) -> None:
31
+ object.__setattr__(self, "values", np.asarray(values))
32
+ object.__setattr__(self, "active_dims", tuple(active_dims))
33
+ previous = (
34
+ None
35
+ if previous_first_order is None
36
+ else np.asarray(previous_first_order, dtype=np.float32)
37
+ )
38
+ if previous is not None and (
39
+ previous.shape != (len(self.active_dims),)
40
+ or not np.all(np.isfinite(previous))
41
+ ):
42
+ raise ValueError(
43
+ "previous_first_order must be finite with shape [D_active]"
44
+ )
45
+ object.__setattr__(self, "previous_first_order", previous)
46
+
47
+ @classmethod
48
+ def from_padded(
49
+ cls,
50
+ values: np.ndarray,
51
+ *,
52
+ active_dims: Sequence[int],
53
+ previous_first_order: np.ndarray | None = None,
54
+ ) -> ActiveTrajectory:
55
+ "Drop engineering-only dense padding before the semantic boundary."
56
+ array = np.asarray(values)
57
+ dims = tuple(int(value) for value in active_dims)
58
+ if array.ndim != 2 or not dims or min(dims) < 0 or max(dims) >= array.shape[1]:
59
+ raise ValueError(
60
+ "padded values and active_dims do not define a valid trajectory"
61
+ )
62
+ previous = (
63
+ None
64
+ if previous_first_order is None
65
+ else np.take(previous_first_order, dims)
66
+ )
67
+ return cls(np.take(array, dims, axis=1), dims, previous_first_order=previous)
68
+
69
+
70
+ def trajectory_boundary(
71
+ trajectory: TrajectoryLike, config: ActionCodec2Config, active_dims: tuple[int, ...]
72
+ ) -> np.ndarray | None:
73
+ """Expand explicit compact state to ``[D]`` for fitting and validation."""
74
+ previous = getattr(trajectory, "previous_first_order", None)
75
+ active = set(active_dims)
76
+ required = any(
77
+ c.primitive_order == 2 and active.intersection(c.dims)
78
+ for c in config.physical.components.values()
79
+ )
80
+ if previous is None and required:
81
+ raise ValueError(
82
+ "second-order trajectories require explicit previous_first_order"
83
+ )
84
+ if previous is None and not any(
85
+ c.primitive_order == 2 for c in config.physical.components.values()
86
+ ):
87
+ return None
88
+ dense = np.zeros(config.physical.input_dim, dtype=np.float32)
89
+ if previous is not None:
90
+ dense[list(active_dims)] = previous
91
+ return dense
92
+
93
+
94
+ TrajectoryLike = np.ndarray | ActiveTrajectory
95
+
96
+
97
+ def prepare_trajectory(
98
+ trajectory: TrajectoryLike,
99
+ *,
100
+ dimension: int,
101
+ index: int,
102
+ dtype: np.dtype | type = np.float32,
103
+ ) -> tuple[np.ndarray, tuple[int, ...]]:
104
+ "Validate a trajectory and return compact values plus global dimension ids."
105
+ if isinstance(trajectory, ActiveTrajectory):
106
+ raw = trajectory.values
107
+ active_dims = trajectory.active_dims
108
+ else:
109
+ raw = trajectory
110
+ array = np.asarray(raw)
111
+ active_dims = tuple(range(array.shape[1])) if array.ndim == 2 else ()
112
+
113
+ values = np.asarray(raw, dtype=dtype)
114
+ if values.ndim != 2 or len(values) < 1 or values.shape[1] < 1:
115
+ raise ValueError(
116
+ f"trajectory {index} must have non-empty shape [T, D_active], got {values.shape}"
117
+ )
118
+ if len(active_dims) != values.shape[1]:
119
+ raise ValueError(
120
+ f"trajectory {index} active_dims has {len(active_dims)} entries for "
121
+ f"width {values.shape[1]}"
122
+ )
123
+ if any(isinstance(item, bool) or not isinstance(item, int) for item in active_dims):
124
+ raise ValueError(f"trajectory {index} active_dims must contain integers")
125
+ active_dims = active_dimensions(
126
+ active_dims, dimension, subject=f"trajectory {index} active_dims"
127
+ )
128
+ if not np.all(np.isfinite(values)):
129
+ raise ValueError(f"trajectory {index} contains a non-finite value")
130
+ return np.ascontiguousarray(values), active_dims
runtime/processing_actioncodec2.py ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face processor over the shared ActionCodec2 frontend implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ import torch
12
+ from transformers.feature_extraction_utils import BatchFeature
13
+ from transformers.processing_utils import ProcessorMixin
14
+
15
+ from .routing.defaults import CodecBuilder, ProfileBundleBuilder, builtin_action_spaces
16
+ from .integration.hf import (
17
+ PROCESSOR_CONFIG_NAME,
18
+ ROUTER_ALLOW_PATTERNS,
19
+ ActionCodec2FrontendHFConfig,
20
+ resolve_pretrained_directory,
21
+ save_huggingface_metadata,
22
+ save_model_card,
23
+ upload_pretrained_directory,
24
+ )
25
+ from .routing.presets import PresetConfig
26
+ from .routing.routing import ActionCodec2Router
27
+ from .frontend.tokenizer import ActionCodec2Tokenizer
28
+
29
+
30
+ def _saved_metadata(directory: Path) -> dict[str, Any]:
31
+ metadata = directory / PROCESSOR_CONFIG_NAME
32
+ return (
33
+ json.loads(metadata.read_text(encoding="utf-8")) if metadata.is_file() else {}
34
+ )
35
+
36
+
37
+ def _fitted_status(profiles) -> str:
38
+ status = ", ".join(sorted(profiles)) or "none; call codec.fit(episodes)"
39
+ return f"Fitted profiles: {status}\n"
40
+
41
+
42
+ class ActionCodec2(ActionCodec2Tokenizer, ProcessorMixin):
43
+ "Action-space-bound processor with lossless token serialization."
44
+
45
+ model_type = ActionCodec2FrontendHFConfig.model_type
46
+ model_input_names = ["input_ids"]
47
+
48
+ attributes = []
49
+
50
+ def __init__(
51
+ self,
52
+ backend: ActionCodec2Router | None = None,
53
+ *,
54
+ action_space: str,
55
+ primitive_order: int = 1,
56
+ bpe_scheme: str | None = None,
57
+ profile_configs: Mapping[str, object] | None = None,
58
+ ) -> None:
59
+ fresh = backend is None
60
+ if fresh:
61
+ backend = CodecBuilder(
62
+ primitive_order=primitive_order,
63
+ bpe_scheme=bpe_scheme,
64
+ profile_configs=profile_configs,
65
+ ).build()
66
+ elif (
67
+ primitive_order != 1
68
+ or bpe_scheme is not None
69
+ or profile_configs is not None
70
+ ):
71
+ raise ValueError(
72
+ "profile construction options cannot accompany an existing backend"
73
+ )
74
+ super().__init__(backend, action_space=action_space)
75
+ ProcessorMixin.__init__(self)
76
+ self._fitted_profiles = frozenset() if fresh else frozenset(backend.profiles)
77
+ self.training_report = None
78
+ self._source_directory = Path(__file__).parent
79
+
80
+ def __dir__(self):
81
+ inapplicable = {
82
+ "action_tokenizer",
83
+ "attributes",
84
+ "audio_tokenizer",
85
+ "chat_template",
86
+ "check_argument_for_proper_class",
87
+ "encode_kwargs",
88
+ "feature_extractor_class",
89
+ "from_args_and_dict",
90
+ "get_possibly_dynamic_module",
91
+ "get_processor_dict",
92
+ "model_input_names",
93
+ "model_type",
94
+ "optional_attributes",
95
+ "optional_call_args",
96
+ "apply_chat_template",
97
+ "post_process_image_text_to_text",
98
+ "pad_action_id",
99
+ "bos_blk_id",
100
+ "parts_meta",
101
+ "BAR_SENTINEL_TOKENS",
102
+ "push_to_hub",
103
+ "register_for_auto_class",
104
+ "tokenizer_class",
105
+ "to_dict",
106
+ "to_json_file",
107
+ "to_json_string",
108
+ "valid_processor_kwargs",
109
+ "validate_init_kwargs",
110
+ }
111
+ return [name for name in super().__dir__() if name not in inapplicable]
112
+
113
+ def __repr__(self) -> str:
114
+ return (
115
+ f"ActionCodec2(action_space={self.action_space.name!r}, "
116
+ f"D={self.action_dim}, vocab={self.action_tokenizer.vocab_size})"
117
+ )
118
+
119
+ @staticmethod
120
+ def builtin_action_spaces():
121
+ """Return the immutable named layout definitions shipped in the package."""
122
+ return builtin_action_spaces()
123
+
124
+ @classmethod
125
+ def from_profiles(cls, joint, eef, *, action_space: str, token_budgets=None):
126
+ "Bind an artifact assembled from existing joint/EEF profile directories."
127
+ router = ProfileBundleBuilder(joint, eef, token_budgets=token_budgets).build()
128
+ return cls(router, action_space=action_space)
129
+
130
+ def logits_processor(
131
+ self,
132
+ horizon: int,
133
+ *,
134
+ fps: float,
135
+ prompt_length: int,
136
+ eos_token_id: int,
137
+ token_offset: int = 0,
138
+ pad_token_id=None,
139
+ ):
140
+ "Create a Transformers LogitsProcessor for a fixed action horizon."
141
+ from .tokenization.generation import ActionCodec2LogitsProcessor
142
+
143
+ return ActionCodec2LogitsProcessor(
144
+ self,
145
+ horizon,
146
+ fps=fps,
147
+ prompt_length=prompt_length,
148
+ eos_token_id=eos_token_id,
149
+ token_offset=token_offset,
150
+ pad_token_id=pad_token_id,
151
+ )
152
+
153
+ def batch_decode(
154
+ self,
155
+ token_rows,
156
+ *,
157
+ fps=None,
158
+ current_state=None,
159
+ previous_first_order=None,
160
+ errors="raise",
161
+ ):
162
+ "Decode independent token rows, including different valid horizons."
163
+ self._require_fitted()
164
+ if errors not in ("raise", "return"):
165
+ raise ValueError("errors must be 'raise' or 'return'")
166
+ rows = list(token_rows)
167
+ current = (
168
+ None
169
+ if current_state is None
170
+ else np.asarray(
171
+ current_state.detach().cpu()
172
+ if isinstance(current_state, torch.Tensor)
173
+ else current_state
174
+ )
175
+ )
176
+ if current is not None and current.shape != (len(rows), self.action_dim):
177
+ raise ValueError(
178
+ f"current_state must have shape {(len(rows), self.action_dim)}"
179
+ )
180
+ previous = self._numpy_boundary(previous_first_order)
181
+ if previous is not None and any(
182
+ np.ndim(v) != 2 or len(v) != len(rows) for v in previous.values()
183
+ ):
184
+ raise ValueError(
185
+ "previous_first_order arrays must have batch dimension equal to token rows"
186
+ )
187
+ result = []
188
+ for index, row in enumerate(rows):
189
+ try:
190
+ decoded = self.decode(
191
+ [row],
192
+ fps=fps,
193
+ current_state=None
194
+ if current is None
195
+ else current[index : index + 1],
196
+ previous_first_order=None
197
+ if previous is None
198
+ else {k: v[index : index + 1] for k, v in previous.items()},
199
+ )
200
+ except ValueError as error:
201
+ error = ValueError(f"token row {index}: {error}")
202
+ if errors == "raise":
203
+ raise error from None
204
+ decoded = error
205
+ result.append(decoded)
206
+ return result
207
+
208
+ @property
209
+ def fitted_profiles(self) -> frozenset[str]:
210
+ """Names of profiles fitted on data; persisted by save_pretrained()."""
211
+ return self._fitted_profiles
212
+
213
+ @property
214
+ def is_fitted(self) -> bool:
215
+ """Whether every profile used by the selected action space is fitted."""
216
+ return all(
217
+ c.space in self.fitted_profiles for c in self.action_space.components
218
+ )
219
+
220
+ def _require_fitted(self):
221
+ missing = {c.space for c in self.action_space.components} - self.fitted_profiles
222
+ if missing:
223
+ raise RuntimeError(
224
+ f"Unfitted profiles {sorted(missing)}; call codec.fit(episodes) first"
225
+ )
226
+
227
+ def _describe_action_spaces(self, action_dim: int | None = None) -> str:
228
+ return _fitted_status(self.fitted_profiles) + super()._describe_action_spaces(
229
+ action_dim
230
+ )
231
+
232
+ def for_action_space(self, name: str) -> ActionCodec2:
233
+ """Share current weights with a new binding; subsequent fit replaces only that instance."""
234
+ codec = super().for_action_space(name)
235
+ codec._fitted_profiles = self.fitted_profiles
236
+ codec.training_report = self.training_report
237
+ codec._source_directory = self._source_directory
238
+ return codec
239
+
240
+ def fit(
241
+ self,
242
+ episodes,
243
+ *,
244
+ fps: float | None = None,
245
+ current_state=None,
246
+ previous_first_order=None,
247
+ bootstrap_context: bool = False,
248
+ backend: str = "auto",
249
+ threads: int = 0,
250
+ batch_size: int = 256,
251
+ progress: bool = False,
252
+ max_cell_bytes: int | None = None,
253
+ work_dir: str | Path | None = None,
254
+ ) -> ActionCodec2:
255
+ "Fit physical vocabularies from actions in declared source layouts."
256
+
257
+
258
+ from .training.fitting import ActionCodec2Fitter
259
+
260
+ router, report = ActionCodec2Fitter(
261
+ self, bootstrap_context=bootstrap_context
262
+ ).fit(
263
+ episodes,
264
+ work_dir=work_dir,
265
+ metadata=dict(
266
+ fps=fps,
267
+ current_state=current_state,
268
+ previous_first_order=previous_first_order,
269
+ ),
270
+ backend=backend,
271
+ threads=threads,
272
+ batch_size=batch_size,
273
+ progress=progress,
274
+ max_cell_bytes=max_cell_bytes,
275
+ )
276
+ fitted = self.fitted_profiles | report["profiles"].keys()
277
+ replacement = type(self)(router, action_space=self.action_space.name)
278
+ replacement._fitted_profiles = frozenset(fitted)
279
+ replacement.training_report = report
280
+ replacement._source_directory = self._source_directory
281
+ replacement._warned_default_fps = self._warned_default_fps
282
+ self.__dict__.update(replacement.__dict__)
283
+ return self
284
+
285
+ def _encode_action_indices(self, action, encode_kwargs=None):
286
+ self._require_fitted()
287
+ return super()._encode_action_indices(action, encode_kwargs)
288
+
289
+ def _decode_action_indices(self, action_indices, **kwargs):
290
+ self._require_fitted()
291
+ return super()._decode_action_indices(action_indices, **kwargs)
292
+
293
+ def grammar(self, horizon: int, *, fps: float | None = None):
294
+ self._require_fitted()
295
+ return super().grammar(horizon, fps=fps)
296
+
297
+ def final_first_order(
298
+ self, action_token_ids, *, previous_first_order, executed_steps=None
299
+ ):
300
+ self._require_fitted()
301
+ return super().final_first_order(
302
+ action_token_ids,
303
+ previous_first_order=previous_first_order,
304
+ executed_steps=executed_steps,
305
+ )
306
+
307
+ @classmethod
308
+ def describe_pretrained(
309
+ cls,
310
+ pretrained_model_name_or_path: str | Path,
311
+ *,
312
+ action_dim: int | None = None,
313
+ **kwargs: Any,
314
+ ) -> str:
315
+ "Inspect a local/Hub artifact before choosing an action space."
316
+ directory = resolve_pretrained_directory(
317
+ pretrained_model_name_or_path,
318
+ allow_patterns=ROUTER_ALLOW_PATTERNS,
319
+ **kwargs,
320
+ )
321
+ router = ActionCodec2Router.from_pretrained(directory)
322
+ metadata = _saved_metadata(directory)
323
+ return _fitted_status(
324
+ metadata.get("fitted_profiles", router.profiles)
325
+ ) + router.describe_action_spaces(
326
+ selected=metadata.get("action_space"), action_dim=action_dim
327
+ )
328
+
329
+ @classmethod
330
+ def from_pretrained(
331
+ cls,
332
+ pretrained_model_name_or_path: str | Path,
333
+ *,
334
+ action_space: str | Path | PresetConfig | Mapping[str, object] | None = None,
335
+ **kwargs: Any,
336
+ ) -> ActionCodec2:
337
+ "Load a local/Hub artifact and restore or override its action space."
338
+ directory = resolve_pretrained_directory(
339
+ pretrained_model_name_or_path,
340
+ allow_patterns=ROUTER_ALLOW_PATTERNS,
341
+ **kwargs,
342
+ )
343
+ if action_space is None:
344
+ action_space = _saved_metadata(directory).get("action_space")
345
+ if action_space is None:
346
+ raise ValueError(
347
+ "action_space is required when loading an unbound router artifact. "
348
+ "First inspect its layouts with print(ActionCodec2.describe_pretrained(path)), "
349
+ "then pass action_space='NAME'; shape alone cannot identify physical meaning."
350
+ )
351
+ codec = super().from_pretrained(directory, action_space=action_space)
352
+ metadata = _saved_metadata(directory)
353
+ fitted = metadata.get("fitted_profiles", list(codec.action_tokenizer.profiles))
354
+ if not isinstance(fitted, list) or any(
355
+ not isinstance(name, str) or name not in codec.action_tokenizer.profiles
356
+ for name in fitted
357
+ ):
358
+ raise ValueError("invalid fitted_profiles in processor_config.json")
359
+ codec._fitted_profiles = frozenset(fitted)
360
+ codec._source_directory = directory
361
+ report_path = directory / "fit_report.json"
362
+ if report_path.is_file():
363
+ codec.training_report = json.loads(report_path.read_text(encoding="utf-8"))
364
+ return codec
365
+
366
+ def to_dict(self, legacy_serialization: bool = True) -> dict[str, object]:
367
+ "Return lightweight discovery metadata."
368
+ del legacy_serialization
369
+ return {
370
+ "processor_class": type(self).__name__,
371
+ "model_type": self.model_type,
372
+ "action_space": self.action_space.name,
373
+ "fitted_profiles": sorted(self.fitted_profiles),
374
+ }
375
+
376
+ def save_pretrained(
377
+ self, save_directory: str | Path, push_to_hub: bool = False, **kwargs: Any
378
+ ) -> list[str]:
379
+ """Save the complete router and the selected action-space binding."""
380
+ if not push_to_hub and kwargs:
381
+ raise TypeError(
382
+ f"unused save_pretrained keyword(s): {', '.join(sorted(kwargs))}"
383
+ )
384
+ from .integration.exporting import ArtifactExporter
385
+
386
+ directory = Path(save_directory)
387
+ save_model_card(directory, str(self))
388
+ files = self.action_tokenizer.save_pretrained(directory, include_code=False)
389
+ files.extend(
390
+ ArtifactExporter(
391
+ directory, source_directory=self._source_directory
392
+ ).export()
393
+ )
394
+ save_huggingface_metadata(
395
+ directory,
396
+ **self.to_dict(),
397
+ auto_map={"AutoProcessor": "processing_actioncodec2.ActionCodec2"},
398
+ )
399
+ report_path = directory / "fit_report.json"
400
+ if self.training_report is not None:
401
+ report_path.write_text(
402
+ json.dumps(self.training_report, indent=2) + "\n", encoding="utf-8"
403
+ )
404
+ files.append(str(report_path))
405
+ elif report_path.exists():
406
+ report_path.unlink()
407
+ if push_to_hub:
408
+ upload_pretrained_directory(directory, **kwargs)
409
+ return files
410
+
411
+ def __call__(
412
+ self,
413
+ action: torch.Tensor | np.ndarray,
414
+ *,
415
+ fps: float | None = None,
416
+ current_state: torch.Tensor | np.ndarray | None = None,
417
+ previous_first_order: Mapping[str, torch.Tensor | np.ndarray] | None = None,
418
+ encode_kwargs: dict | None = None,
419
+ return_tensors: str | None = None,
420
+ ) -> BatchFeature:
421
+
422
+ rows = self.encode(
423
+ action,
424
+ fps=fps,
425
+ current_state=current_state,
426
+ previous_first_order=previous_first_order,
427
+ encode_kwargs=encode_kwargs,
428
+ )
429
+ return BatchFeature({"input_ids": rows}, tensor_type=return_tensors)
runtime/routing/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Internal routing components for ActionCodec2."""
runtime/routing/cache.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small per-instance LRU caches; no global references to fitted codecs."""
2
+
3
+ from collections import OrderedDict
4
+ from collections.abc import MutableMapping
5
+ from threading import RLock
6
+
7
+
8
+ class BoundedCache(MutableMapping):
9
+ "Evict least-recently-used entries above a fixed capacity."
10
+
11
+ def __init__(self, capacity):
12
+ if type(capacity) is not int or capacity < 1:
13
+ raise ValueError("cache capacity must be a positive integer")
14
+ self.capacity = capacity
15
+ self._items = OrderedDict()
16
+ self._lock = RLock()
17
+
18
+ def __getitem__(self, key):
19
+ with self._lock:
20
+ value = self._items[key]
21
+ self._items.move_to_end(key)
22
+ return value
23
+
24
+ def __setitem__(self, key, value):
25
+ with self._lock:
26
+ self._items[key] = value
27
+ self._items.move_to_end(key)
28
+ while len(self._items) > self.capacity:
29
+ self._items.popitem(last=False)
30
+
31
+ def __delitem__(self, key):
32
+ with self._lock:
33
+ del self._items[key]
34
+
35
+ def __iter__(self):
36
+ with self._lock:
37
+ return iter(tuple(self._items))
38
+
39
+ def __len__(self):
40
+ with self._lock:
41
+ return len(self._items)
runtime/routing/defaults.py ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Package-owned configuration templates for fresh ActionCodec2 instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+ from ..tokenization.bpe import BPESerializer, SetBPESerializer
8
+ from ..physical.configuration import ActionCodec2Config
9
+ from ..physical.physical_processor import ActionCodec2Processor
10
+ from .presets import PresetConfig
11
+ from .routing import ActionCodec2Router
12
+
13
+
14
+ class CodecBuilder:
15
+ "Build unfitted physical profiles from packaged defaults or explicit configs."
16
+
17
+ def __init__(self, *, primitive_order=1, bpe_scheme=None, profile_configs=None):
18
+ if type(primitive_order) is not int or primitive_order not in (1, 2):
19
+ raise ValueError("primitive_order must be 1 or 2")
20
+ if bpe_scheme not in (None, "set_bpe", "bpe"):
21
+ raise ValueError("bpe_scheme must be 'set_bpe' or 'bpe'")
22
+ if profile_configs is not None and not isinstance(profile_configs, Mapping):
23
+ raise TypeError(
24
+ "profile_configs must map 'joint'/'eef' to physical configurations"
25
+ )
26
+ self.profile_configs = dict(profile_configs or {})
27
+ if set(self.profile_configs) - {"joint", "eef"}:
28
+ raise ValueError("profile_configs keys must be 'joint' or 'eef'")
29
+ self.primitive_order = primitive_order
30
+ self.bpe_scheme = bpe_scheme
31
+
32
+ def _config(self, name: str) -> ActionCodec2Config:
33
+ suffix = "" if self.primitive_order == 1 else "_second_order_pretrain_v2"
34
+ raw = self.profile_configs.get(name, _PROFILE_DEFAULTS[f"{name}{suffix}"])
35
+ if isinstance(raw, ActionCodec2Config):
36
+ config = raw
37
+ elif isinstance(raw, Mapping):
38
+ config = ActionCodec2Config.from_dict(raw)
39
+ else:
40
+ config = ActionCodec2Config.from_yaml(raw)
41
+ if config.train is None:
42
+ raise ValueError(f"{name} profile config requires a train section")
43
+ if self.bpe_scheme is not None:
44
+ payload = config.to_dict()
45
+ payload["set_bpe"]["scheme"] = self.bpe_scheme
46
+ if self.bpe_scheme == "bpe":
47
+ payload["train"]["bpe"]["spatial_vocab_size"] = payload["train"]["bpe"][
48
+ "vocab_size"
49
+ ]
50
+ config = ActionCodec2Config.from_dict(payload)
51
+ return config
52
+
53
+ def build(self) -> ActionCodec2Router:
54
+ """Create primitive-only profiles; the public facade tracks fitted state."""
55
+ processors = {}
56
+ budgets = {}
57
+ for name in ("joint", "eef"):
58
+ config = self._config(name)
59
+
60
+
61
+ from ..training.training import build_primitives
62
+
63
+ primitives = build_primitives(config)
64
+ if config.set_bpe.scheme == "bpe":
65
+ serializer = BPESerializer(
66
+ bins=primitives.bins, lookahead=config.set_bpe.lookahead, merges=()
67
+ )
68
+ else:
69
+ serializer = SetBPESerializer(
70
+ bins=primitives.bins,
71
+ lookahead=config.set_bpe.lookahead,
72
+ spatial_merges=(),
73
+ temporal_merges=(),
74
+ )
75
+ processors[name] = ActionCodec2Processor(config, primitives, serializer)
76
+ budgets[name] = config.train.bpe.vocab_size
77
+ presets = list(builtin_action_spaces().values())
78
+ return ActionCodec2Router.from_processors(
79
+ processors, token_budgets=budgets, presets=presets
80
+ )
81
+
82
+
83
+ def builtin_action_spaces():
84
+ """Return a read-only name-to-preset mapping shipped in the installed package."""
85
+ from types import MappingProxyType
86
+
87
+ presets = [PresetConfig.from_dict(value) for value in _PRESET_DEFAULTS.values()]
88
+ return MappingProxyType({preset.name: preset for preset in presets})
89
+
90
+
91
+ class ProfileBundleBuilder:
92
+ "Assemble two existing physical artifacts using their recorded vocabulary budgets."
93
+
94
+ def __init__(self, joint, eef, *, token_budgets=None):
95
+ self.paths = {"joint": joint, "eef": eef}
96
+ self.budgets = dict(token_budgets or {})
97
+ if self.budgets.keys() - self.paths.keys():
98
+ raise ValueError("token_budgets accepts only 'joint' and 'eef'")
99
+
100
+ def build(self):
101
+ processors = {
102
+ name: ActionCodec2Processor.from_pretrained(path)
103
+ for name, path in self.paths.items()
104
+ }
105
+ budgets = {
106
+ name: self.budgets.get(
107
+ name,
108
+ processor.config.train.bpe.vocab_size
109
+ if processor.config.train is not None
110
+ else processor.vocab_size,
111
+ )
112
+ for name, processor in processors.items()
113
+ }
114
+ return ActionCodec2Router.from_processors(
115
+ processors,
116
+ token_budgets=budgets,
117
+ presets=list(builtin_action_spaces().values()),
118
+ )
119
+
120
+
121
+ _PROFILE_DEFAULTS = {
122
+ "eef": {
123
+ "model_type": "actioncodec2_unified",
124
+ "config_version": 1,
125
+ "physical": {
126
+ "input_dim": 7,
127
+ "components": {
128
+ "translation_m": {
129
+ "dims": {"start": 0, "stop": 3},
130
+ "space": "additive",
131
+ "input_order": 0,
132
+ "primitive_order": 1,
133
+ "input_transform": {"scale": 1.0, "clip": False},
134
+ "constraints": {
135
+ "order_0": {"min": -2.0, "max": 2.0},
136
+ "order_1": {"min": -0.08, "max": 0.08},
137
+ },
138
+ "distortion_limit": 0.005,
139
+ "primitive_selection": "smooth",
140
+ },
141
+ "orientation_so3": {
142
+ "dims": {"start": 3, "stop": 6},
143
+ "space": "so3",
144
+ "input_order": 0,
145
+ "primitive_order": 1,
146
+ "input_transform": {"scale": 1.0, "clip": False},
147
+ "constraints": {
148
+ "order_0": {
149
+ "min": -3.141592653589793,
150
+ "max": 3.141592653589793,
151
+ },
152
+ "order_1": {"min": -0.25, "max": 0.25},
153
+ },
154
+ "distortion_limit": 0.02,
155
+ "primitive_selection": "smooth",
156
+ "representation": "rotation_vector",
157
+ "multiplication": "right",
158
+ },
159
+ "gripper_open_command": {
160
+ "dims": [6],
161
+ "space": "additive",
162
+ "input_order": 0,
163
+ "primitive_order": 0,
164
+ "input_transform": {"scale": 1.0, "clip": True},
165
+ "constraints": {"order_0": {"min": 0.0, "max": 1.0}},
166
+ "distortion_limit": 0.5,
167
+ "primitive_selection": "nearest",
168
+ },
169
+ },
170
+ },
171
+ "set_bpe": {"lookahead": 8, "allow_cross_component_merge": False},
172
+ "train": {
173
+ "primitives": {
174
+ "zero_primitive_components": ["translation_m", "orientation_so3"]
175
+ },
176
+ "bpe": {"spatial_vocab_size": 2048, "vocab_size": 4096},
177
+ "quality_control": {"hard_order_1_scale": 3.0, "min_segment_length": 2},
178
+ },
179
+ },
180
+ "eef_second_order_pretrain_v2": {
181
+ "model_type": "actioncodec2_unified",
182
+ "config_version": 1,
183
+ "physical": {
184
+ "input_dim": 7,
185
+ "components": {
186
+ "translation_m": {
187
+ "dims": {"start": 0, "stop": 3},
188
+ "space": "additive",
189
+ "input_order": 0,
190
+ "primitive_order": 2,
191
+ "input_transform": {"scale": 1.0, "clip": False},
192
+ "constraints": {
193
+ "order_0": {"min": -2.0, "max": 2.0},
194
+ "order_2": {"min": -0.025, "max": 0.025},
195
+ "order_1": {"min": -0.08, "max": 0.08},
196
+ },
197
+ "distortion_limit": 0.005,
198
+ "primitive_selection": "smooth",
199
+ },
200
+ "orientation_so3": {
201
+ "dims": {"start": 3, "stop": 6},
202
+ "space": "so3",
203
+ "input_order": 0,
204
+ "primitive_order": 2,
205
+ "input_transform": {"scale": 1.0, "clip": False},
206
+ "constraints": {
207
+ "order_0": {
208
+ "min": -3.141592653589793,
209
+ "max": 3.141592653589793,
210
+ },
211
+ "order_2": {"min": -0.08, "max": 0.08},
212
+ "order_1": {"min": -0.25, "max": 0.25},
213
+ },
214
+ "distortion_limit": 0.02,
215
+ "primitive_selection": "smooth",
216
+ "representation": "rotation_vector",
217
+ "multiplication": "right",
218
+ },
219
+ "gripper_open_command": {
220
+ "dims": [6],
221
+ "space": "additive",
222
+ "input_order": 0,
223
+ "primitive_order": 0,
224
+ "input_transform": {"scale": 1.0, "clip": True},
225
+ "constraints": {"order_0": {"min": 0.0, "max": 1.0}},
226
+ "distortion_limit": 0.5,
227
+ "primitive_selection": "nearest",
228
+ },
229
+ },
230
+ },
231
+ "set_bpe": {"lookahead": 8, "allow_cross_component_merge": False},
232
+ "train": {
233
+ "primitives": {
234
+ "zero_primitive_components": ["translation_m", "orientation_so3"]
235
+ },
236
+ "bpe": {"spatial_vocab_size": 2048, "vocab_size": 4096},
237
+ "quality_control": {"hard_order_1_scale": 3.0, "min_segment_length": 2},
238
+ },
239
+ },
240
+ "joint": {
241
+ "model_type": "actioncodec2_unified",
242
+ "config_version": 1,
243
+ "physical": {
244
+ "input_dim": 8,
245
+ "components": {
246
+ "generalized_joint_position": {
247
+ "dims": {"start": 0, "stop": 7},
248
+ "space": "additive",
249
+ "input_order": 0,
250
+ "primitive_order": 1,
251
+ "input_transform": {"scale": 1.0, "clip": False},
252
+ "constraints": {
253
+ "order_0": {
254
+ "min": -6.283185307179586,
255
+ "max": 6.283185307179586,
256
+ },
257
+ "order_1": {"min": -0.35, "max": 0.35},
258
+ },
259
+ "distortion_limit": [
260
+ 0.004,
261
+ 0.004,
262
+ 0.006,
263
+ 0.0075,
264
+ 0.015,
265
+ 0.02,
266
+ 0.02,
267
+ ],
268
+ "primitive_selection": "smooth",
269
+ },
270
+ "gripper_open_command": {
271
+ "dims": [7],
272
+ "space": "additive",
273
+ "input_order": 0,
274
+ "primitive_order": 0,
275
+ "input_transform": {"scale": 1.0, "clip": True},
276
+ "constraints": {"order_0": {"min": 0.0, "max": 1.0}},
277
+ "distortion_limit": 0.5,
278
+ "primitive_selection": "nearest",
279
+ },
280
+ },
281
+ },
282
+ "set_bpe": {"lookahead": 8, "allow_cross_component_merge": False},
283
+ "train": {
284
+ "primitives": {"zero_primitive_components": ["generalized_joint_position"]},
285
+ "bpe": {"spatial_vocab_size": 2048, "vocab_size": 4096},
286
+ "quality_control": {"hard_order_1_scale": 3.0, "min_segment_length": 2},
287
+ },
288
+ },
289
+ "joint_second_order_pretrain_v2": {
290
+ "model_type": "actioncodec2_unified",
291
+ "config_version": 1,
292
+ "physical": {
293
+ "input_dim": 8,
294
+ "components": {
295
+ "generalized_joint_position": {
296
+ "dims": {"start": 0, "stop": 7},
297
+ "space": "additive",
298
+ "input_order": 0,
299
+ "primitive_order": 2,
300
+ "input_transform": {"scale": 1.0, "clip": False},
301
+ "constraints": {
302
+ "order_0": {
303
+ "min": -6.283185307179586,
304
+ "max": 6.283185307179586,
305
+ },
306
+ "order_2": {"min": -0.25, "max": 0.25},
307
+ "order_1": {"min": -0.35, "max": 0.35},
308
+ },
309
+ "distortion_limit": [
310
+ 0.004,
311
+ 0.004,
312
+ 0.006,
313
+ 0.0075,
314
+ 0.015,
315
+ 0.02,
316
+ 0.02,
317
+ ],
318
+ "primitive_selection": "smooth",
319
+ },
320
+ "gripper_open_command": {
321
+ "dims": [7],
322
+ "space": "additive",
323
+ "input_order": 0,
324
+ "primitive_order": 0,
325
+ "input_transform": {"scale": 1.0, "clip": True},
326
+ "constraints": {"order_0": {"min": 0.0, "max": 1.0}},
327
+ "distortion_limit": 0.5,
328
+ "primitive_selection": "nearest",
329
+ },
330
+ },
331
+ },
332
+ "set_bpe": {"lookahead": 8, "allow_cross_component_merge": False},
333
+ "train": {
334
+ "primitives": {"zero_primitive_components": ["generalized_joint_position"]},
335
+ "bpe": {"spatial_vocab_size": 2048, "vocab_size": 4096},
336
+ "quality_control": {"hard_order_1_scale": 3.0, "min_segment_length": 2},
337
+ },
338
+ },
339
+ }
340
+
341
+ _PRESET_DEFAULTS = {
342
+ "dual_eef_abs": {
343
+ "name": "dual_eef_abs",
344
+ "input_dim": 14,
345
+ "input": "absolute",
346
+ "source_hz": 15.0,
347
+ "target_hz": 15.0,
348
+ "interpolation": "cubic",
349
+ "components": {
350
+ "left_arm": {
351
+ "space": "eef",
352
+ "position_dims": [0, 1, 2],
353
+ "rotation_dims": [3, 4, 5],
354
+ "rotation_format": "rotation_vector",
355
+ "gripper_dim": 6,
356
+ "gripper_threshold": 0.8,
357
+ "gripper_open_is_high": True,
358
+ },
359
+ "right_arm": {
360
+ "space": "eef",
361
+ "position_dims": [7, 8, 9],
362
+ "rotation_dims": [10, 11, 12],
363
+ "rotation_format": "rotation_vector",
364
+ "gripper_dim": 13,
365
+ "gripper_threshold": 0.8,
366
+ "gripper_open_is_high": True,
367
+ },
368
+ },
369
+ },
370
+ "dual_eef_delta": {
371
+ "name": "dual_eef_delta",
372
+ "input_dim": 14,
373
+ "input": "delta",
374
+ "source_hz": 15.0,
375
+ "target_hz": 15.0,
376
+ "interpolation": "cubic",
377
+ "components": {
378
+ "left_arm": {
379
+ "space": "eef",
380
+ "position_dims": [0, 1, 2],
381
+ "rotation_dims": [3, 4, 5],
382
+ "rotation_format": "rotation_vector",
383
+ "gripper_dim": 6,
384
+ "gripper_threshold": 0.8,
385
+ "gripper_open_is_high": True,
386
+ },
387
+ "right_arm": {
388
+ "space": "eef",
389
+ "position_dims": [7, 8, 9],
390
+ "rotation_dims": [10, 11, 12],
391
+ "rotation_format": "rotation_vector",
392
+ "gripper_dim": 13,
393
+ "gripper_threshold": 0.8,
394
+ "gripper_open_is_high": True,
395
+ },
396
+ },
397
+ },
398
+ "dual_joint6_abs": {
399
+ "name": "dual_joint6_abs",
400
+ "input_dim": 14,
401
+ "input": "absolute",
402
+ "source_hz": 15.0,
403
+ "target_hz": 15.0,
404
+ "interpolation": "cubic",
405
+ "components": {
406
+ "left_arm": {
407
+ "space": "joint",
408
+ "joint_dims": [0, 1, 2, 3, 4, 5],
409
+ "gripper_dim": 6,
410
+ "gripper_threshold": 0.8,
411
+ "gripper_open_is_high": True,
412
+ },
413
+ "right_arm": {
414
+ "space": "joint",
415
+ "joint_dims": [7, 8, 9, 10, 11, 12],
416
+ "gripper_dim": 13,
417
+ "gripper_threshold": 0.8,
418
+ "gripper_open_is_high": True,
419
+ },
420
+ },
421
+ },
422
+ "dual_joint6_delta": {
423
+ "name": "dual_joint6_delta",
424
+ "input_dim": 14,
425
+ "input": "delta",
426
+ "source_hz": 15.0,
427
+ "target_hz": 15.0,
428
+ "interpolation": "cubic",
429
+ "components": {
430
+ "left_arm": {
431
+ "space": "joint",
432
+ "joint_dims": [0, 1, 2, 3, 4, 5],
433
+ "gripper_dim": 6,
434
+ "gripper_threshold": 0.8,
435
+ "gripper_open_is_high": True,
436
+ },
437
+ "right_arm": {
438
+ "space": "joint",
439
+ "joint_dims": [7, 8, 9, 10, 11, 12],
440
+ "gripper_dim": 13,
441
+ "gripper_threshold": 0.8,
442
+ "gripper_open_is_high": True,
443
+ },
444
+ },
445
+ },
446
+ "dual_joint7_abs": {
447
+ "name": "dual_joint7_abs",
448
+ "input_dim": 16,
449
+ "input": "absolute",
450
+ "source_hz": 15.0,
451
+ "target_hz": 15.0,
452
+ "interpolation": "cubic",
453
+ "components": {
454
+ "left_arm": {
455
+ "space": "joint",
456
+ "joint_dims": [0, 1, 2, 3, 4, 5, 6],
457
+ "gripper_dim": 7,
458
+ "gripper_threshold": 0.8,
459
+ "gripper_open_is_high": True,
460
+ },
461
+ "right_arm": {
462
+ "space": "joint",
463
+ "joint_dims": [8, 9, 10, 11, 12, 13, 14],
464
+ "gripper_dim": 15,
465
+ "gripper_threshold": 0.8,
466
+ "gripper_open_is_high": True,
467
+ },
468
+ },
469
+ },
470
+ "dual_joint7_delta": {
471
+ "name": "dual_joint7_delta",
472
+ "input_dim": 16,
473
+ "input": "delta",
474
+ "source_hz": 15.0,
475
+ "target_hz": 15.0,
476
+ "interpolation": "cubic",
477
+ "components": {
478
+ "left_arm": {
479
+ "space": "joint",
480
+ "joint_dims": [0, 1, 2, 3, 4, 5, 6],
481
+ "gripper_dim": 7,
482
+ "gripper_threshold": 0.8,
483
+ "gripper_open_is_high": True,
484
+ },
485
+ "right_arm": {
486
+ "space": "joint",
487
+ "joint_dims": [8, 9, 10, 11, 12, 13, 14],
488
+ "gripper_dim": 15,
489
+ "gripper_threshold": 0.8,
490
+ "gripper_open_is_high": True,
491
+ },
492
+ },
493
+ },
494
+ "single_eef_abs": {
495
+ "name": "single_eef_abs",
496
+ "input_dim": 7,
497
+ "input": "absolute",
498
+ "source_hz": 15.0,
499
+ "target_hz": 15.0,
500
+ "interpolation": "cubic",
501
+ "components": {
502
+ "arm": {
503
+ "space": "eef",
504
+ "position_dims": [0, 1, 2],
505
+ "rotation_dims": [3, 4, 5],
506
+ "rotation_format": "rotation_vector",
507
+ "gripper_dim": 6,
508
+ "gripper_threshold": 0.8,
509
+ "gripper_open_is_high": True,
510
+ }
511
+ },
512
+ },
513
+ "single_eef_delta": {
514
+ "name": "single_eef_delta",
515
+ "input_dim": 7,
516
+ "input": "delta",
517
+ "source_hz": 15.0,
518
+ "target_hz": 15.0,
519
+ "interpolation": "cubic",
520
+ "components": {
521
+ "arm": {
522
+ "space": "eef",
523
+ "position_dims": [0, 1, 2],
524
+ "rotation_dims": [3, 4, 5],
525
+ "rotation_format": "rotation_vector",
526
+ "gripper_dim": 6,
527
+ "gripper_threshold": 0.8,
528
+ "gripper_open_is_high": True,
529
+ }
530
+ },
531
+ },
532
+ "single_joint6_abs": {
533
+ "name": "single_joint6_abs",
534
+ "input_dim": 7,
535
+ "input": "absolute",
536
+ "source_hz": 15.0,
537
+ "target_hz": 15.0,
538
+ "interpolation": "cubic",
539
+ "components": {
540
+ "arm": {
541
+ "space": "joint",
542
+ "joint_dims": [0, 1, 2, 3, 4, 5],
543
+ "gripper_dim": 6,
544
+ "gripper_threshold": 0.8,
545
+ "gripper_open_is_high": True,
546
+ }
547
+ },
548
+ },
549
+ "single_joint6_abs_continuous_gripper": {
550
+ "name": "single_joint6_abs_continuous_gripper",
551
+ "input_dim": 6,
552
+ "input": "absolute",
553
+ "source_hz": 15.0,
554
+ "target_hz": 15.0,
555
+ "interpolation": "cubic",
556
+ "components": {"arm": {"space": "joint", "joint_dims": [0, 1, 2, 3, 4, 5]}},
557
+ },
558
+ "single_joint6_delta": {
559
+ "name": "single_joint6_delta",
560
+ "input_dim": 7,
561
+ "input": "delta",
562
+ "source_hz": 15.0,
563
+ "target_hz": 15.0,
564
+ "interpolation": "cubic",
565
+ "components": {
566
+ "arm": {
567
+ "space": "joint",
568
+ "joint_dims": [0, 1, 2, 3, 4, 5],
569
+ "gripper_dim": 6,
570
+ "gripper_threshold": 0.8,
571
+ "gripper_open_is_high": True,
572
+ }
573
+ },
574
+ },
575
+ "single_joint7_abs": {
576
+ "name": "single_joint7_abs",
577
+ "input_dim": 8,
578
+ "input": "absolute",
579
+ "source_hz": 15.0,
580
+ "target_hz": 15.0,
581
+ "interpolation": "cubic",
582
+ "components": {
583
+ "arm": {
584
+ "space": "joint",
585
+ "joint_dims": [0, 1, 2, 3, 4, 5, 6],
586
+ "gripper_dim": 7,
587
+ "gripper_threshold": 0.8,
588
+ "gripper_open_is_high": True,
589
+ }
590
+ },
591
+ },
592
+ "single_joint7_delta": {
593
+ "name": "single_joint7_delta",
594
+ "input_dim": 8,
595
+ "input": "delta",
596
+ "source_hz": 15.0,
597
+ "target_hz": 15.0,
598
+ "interpolation": "cubic",
599
+ "components": {
600
+ "arm": {
601
+ "space": "joint",
602
+ "joint_dims": [0, 1, 2, 3, 4, 5, 6],
603
+ "gripper_dim": 7,
604
+ "gripper_threshold": 0.8,
605
+ "gripper_open_is_high": True,
606
+ }
607
+ },
608
+ },
609
+ }
runtime/routing/presets.py ADDED
@@ -0,0 +1,802 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """User-facing action layouts compiled into shared joint/EEF trajectories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping, Sequence
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+ import yaml
12
+
13
+ from ..common._validation import optional_finite_array, validate_dimension_partition
14
+ from ..physical.configuration import (
15
+ ConfigError,
16
+ _integer,
17
+ _mapping,
18
+ _number,
19
+ _reject_unknown,
20
+ _required,
21
+ _UniqueKeyLoader,
22
+ )
23
+ from ..physical.semantic import (
24
+ binarize_gripper_actions,
25
+ eef_pose_absolute,
26
+ eef_pose_components,
27
+ eef_pose_deltas,
28
+ eef_pose_from_components,
29
+ eef_pose_from_local_absolute,
30
+ eef_pose_local_absolute,
31
+ integrate_eef_deltas,
32
+ resample_uniform,
33
+ )
34
+ from ..physical.trajectory import ActiveTrajectory
35
+
36
+ _NAME = re.compile(r"[a-z][a-z0-9_]*\Z")
37
+ _SPACES = {"eef", "joint"}
38
+ _INPUTS = {"absolute", "delta"}
39
+ _ROTATIONS = {"rotation_vector", "euler_xyz", "quaternion_xyzw"}
40
+ _INTERPOLATIONS = {"linear", "pchip", "cubic"}
41
+ _GRIPPER_DIM = {"eef": 6, "joint": 7}
42
+
43
+
44
+ def _format_dims(dims: Sequence[int]) -> str:
45
+ values = tuple(dims)
46
+ if len(values) == 1:
47
+ return str(values[0])
48
+ if values == tuple(range(values[0], values[-1] + 1)):
49
+ return f"{values[0]}..{values[-1]}"
50
+ return ",".join(map(str, values))
51
+
52
+
53
+ def _dims(value: object, path: str) -> tuple[int, ...]:
54
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
55
+ raise ConfigError(f"{path} must be a list of dimensions")
56
+ output = tuple(
57
+ _integer(item, f"{path}[{index}]") for index, item in enumerate(value)
58
+ )
59
+ if not output:
60
+ raise ConfigError(f"{path} must not be empty")
61
+ if len(set(output)) != len(output):
62
+ raise ConfigError(f"{path} contains a repeated dimension")
63
+ return output
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class ComponentSpec:
68
+ "One independently encoded arm/body component in an input tensor."
69
+
70
+ name: str
71
+ space: str
72
+ joint_dims: tuple[int, ...] | None = None
73
+ position_dims: tuple[int, ...] | None = None
74
+ rotation_dims: tuple[int, ...] | None = None
75
+ rotation_format: str = "rotation_vector"
76
+ gripper_dim: int | None = None
77
+ gripper_threshold: float | None = None
78
+ gripper_open_is_high: bool = True
79
+
80
+ def __post_init__(self) -> None:
81
+ if not isinstance(self.name, str) or _NAME.fullmatch(self.name) is None:
82
+ raise ConfigError(f"component name must match {_NAME.pattern!r}")
83
+ if self.space not in _SPACES:
84
+ raise ConfigError(f"component {self.name!r}.space must be joint or eef")
85
+ if self.rotation_format not in _ROTATIONS:
86
+ raise ConfigError(
87
+ f"component {self.name!r}.rotation_format must be one of {sorted(_ROTATIONS)}"
88
+ )
89
+ for label, dims in (
90
+ ("joint_dims", self.joint_dims),
91
+ ("position_dims", self.position_dims),
92
+ ("rotation_dims", self.rotation_dims),
93
+ ):
94
+ if dims is not None:
95
+ _dims(dims, f"component {self.name}.{label}")
96
+ self._validate_motion_dims()
97
+ raw_dims = self.source_dims
98
+ if self.gripper_dim is not None:
99
+ _integer(self.gripper_dim, f"component {self.name}.gripper_dim")
100
+ if self.gripper_dim in raw_dims:
101
+ raise ConfigError(
102
+ f"component {self.name!r}.gripper_dim overlaps motion dims"
103
+ )
104
+ if not isinstance(self.gripper_open_is_high, bool):
105
+ raise ConfigError(
106
+ f"component {self.name!r}.gripper_open_is_high must be boolean"
107
+ )
108
+ threshold = self.gripper_threshold
109
+ if threshold is None:
110
+ threshold = 0.8 if self.gripper_open_is_high else 0.2
111
+ object.__setattr__(self, "gripper_threshold", threshold)
112
+ _number(threshold, f"component {self.name}.gripper_threshold")
113
+
114
+ def _validate_motion_dims(self) -> None:
115
+ if self.space == "joint":
116
+ if self.joint_dims is None or not 1 <= len(self.joint_dims) <= 7:
117
+ raise ConfigError(
118
+ f"joint component {self.name!r} requires 1..7 joint_dims"
119
+ )
120
+ if self.position_dims is not None or self.rotation_dims is not None:
121
+ raise ConfigError(
122
+ f"joint component {self.name!r} cannot define EEF dims"
123
+ )
124
+ return
125
+ if self.joint_dims is not None:
126
+ raise ConfigError(f"EEF component {self.name!r} cannot define joint_dims")
127
+ if self.position_dims is None or len(self.position_dims) != 3:
128
+ raise ConfigError(
129
+ f"EEF component {self.name!r} requires three position_dims"
130
+ )
131
+ expected = 4 if self.rotation_format == "quaternion_xyzw" else 3
132
+ if self.rotation_dims is None or len(self.rotation_dims) != expected:
133
+ raise ConfigError(
134
+ f"EEF component {self.name!r} requires {expected} rotation_dims for "
135
+ f"{self.rotation_format}"
136
+ )
137
+ if set(self.position_dims).intersection(self.rotation_dims):
138
+ raise ConfigError(f"EEF component {self.name!r} reuses a pose dimension")
139
+
140
+ @property
141
+ def source_dims(self) -> tuple[int, ...]:
142
+ """All source motion columns in canonical component order."""
143
+ if self.space == "joint":
144
+ assert self.joint_dims is not None
145
+ return self.joint_dims
146
+ assert self.position_dims is not None and self.rotation_dims is not None
147
+ return (*self.position_dims, *self.rotation_dims)
148
+
149
+ @property
150
+ def all_source_dims(self) -> tuple[int, ...]:
151
+ """Motion plus optional binary-gripper source columns."""
152
+ return (
153
+ self.source_dims
154
+ if self.gripper_dim is None
155
+ else (*self.source_dims, self.gripper_dim)
156
+ )
157
+
158
+ @property
159
+ def active_dims(self) -> tuple[int, ...]:
160
+ """Shared-profile slots emitted by this component."""
161
+ motion_width = len(self.joint_dims or ()) if self.space == "joint" else 6
162
+ dims = tuple(range(motion_width))
163
+ return dims if self.gripper_dim is None else (*dims, _GRIPPER_DIM[self.space])
164
+
165
+ @classmethod
166
+ def from_dict(cls, name: str, payload: object, *, path: str) -> ComponentSpec:
167
+ values = _mapping(payload, path)
168
+ allowed = {
169
+ "space",
170
+ "joint_dims",
171
+ "position_dims",
172
+ "rotation_dims",
173
+ "rotation_format",
174
+ "gripper_dim",
175
+ "gripper_threshold",
176
+ "gripper_open_is_high",
177
+ }
178
+ _reject_unknown(values, allowed, path)
179
+ gripper_dim = values.get("gripper_dim")
180
+ if gripper_dim is not None:
181
+ gripper_dim = _integer(gripper_dim, f"{path}.gripper_dim")
182
+ return cls(
183
+ name=name,
184
+ space=str(_required(values, "space", path)),
185
+ joint_dims=None
186
+ if "joint_dims" not in values
187
+ else _dims(values["joint_dims"], f"{path}.joint_dims"),
188
+ position_dims=None
189
+ if "position_dims" not in values
190
+ else _dims(values["position_dims"], f"{path}.position_dims"),
191
+ rotation_dims=None
192
+ if "rotation_dims" not in values
193
+ else _dims(values["rotation_dims"], f"{path}.rotation_dims"),
194
+ rotation_format=str(values.get("rotation_format", "rotation_vector")),
195
+ gripper_dim=gripper_dim,
196
+ gripper_threshold=(
197
+ None
198
+ if values.get("gripper_threshold") is None
199
+ else _number(values["gripper_threshold"], f"{path}.gripper_threshold")
200
+ ),
201
+ gripper_open_is_high=values.get("gripper_open_is_high", True),
202
+ )
203
+
204
+ def to_dict(self) -> dict[str, object]:
205
+ payload: dict[str, object] = {"space": self.space}
206
+ if self.space == "joint":
207
+ payload["joint_dims"] = list(self.joint_dims or ())
208
+ else:
209
+ payload.update(
210
+ {
211
+ "position_dims": list(self.position_dims or ()),
212
+ "rotation_dims": list(self.rotation_dims or ()),
213
+ "rotation_format": self.rotation_format,
214
+ }
215
+ )
216
+ if self.gripper_dim is not None:
217
+ payload.update(
218
+ {
219
+ "gripper_dim": self.gripper_dim,
220
+ "gripper_threshold": self.gripper_threshold,
221
+ "gripper_open_is_high": self.gripper_open_is_high,
222
+ }
223
+ )
224
+ return payload
225
+
226
+
227
+ @dataclass(frozen=True)
228
+ class PresetConfig:
229
+ "A dataset- and embodiment-independent physical ``[B,T,D]`` contract."
230
+
231
+ name: str
232
+ input_dim: int
233
+ input: str
234
+ components: tuple[ComponentSpec, ...]
235
+ source_hz: float = 15.0
236
+ target_hz: float = 15.0
237
+ interpolation: str = "cubic"
238
+
239
+ def __post_init__(self) -> None:
240
+ if not isinstance(self.name, str) or _NAME.fullmatch(self.name) is None:
241
+ raise ConfigError(f"preset.name must match {_NAME.pattern!r}")
242
+ _integer(self.input_dim, "preset.input_dim", positive=True)
243
+ if self.input not in _INPUTS:
244
+ raise ConfigError("preset.input must be absolute or delta")
245
+ if not self.components:
246
+ raise ConfigError("preset.components must not be empty")
247
+ if not all(
248
+ isinstance(component, ComponentSpec) for component in self.components
249
+ ):
250
+ raise ConfigError("preset.components must contain ComponentSpec values")
251
+ names = [component.name for component in self.components]
252
+ if len(set(names)) != len(names):
253
+ raise ConfigError("preset.components contains a duplicate name")
254
+ self._validate_source_partition()
255
+ for rate, path in (
256
+ (self.source_hz, "source_hz"),
257
+ (self.target_hz, "target_hz"),
258
+ ):
259
+ checked = _number(rate, f"preset.{path}")
260
+ if checked <= 0.0:
261
+ raise ConfigError(f"preset.{path} must be positive and finite")
262
+ if self.interpolation not in _INTERPOLATIONS:
263
+ raise ConfigError(
264
+ f"preset.interpolation must be one of {sorted(_INTERPOLATIONS)}"
265
+ )
266
+ if self.input == "delta" and any(
267
+ component.space == "eef" and component.rotation_format != "rotation_vector"
268
+ for component in self.components
269
+ ):
270
+ raise ConfigError(
271
+ "EEF delta components require rotation_format=rotation_vector"
272
+ )
273
+
274
+ def _validate_source_partition(self) -> None:
275
+ validate_dimension_partition(
276
+ (
277
+ (component.name, component.all_source_dims)
278
+ for component in self.components
279
+ ),
280
+ self.input_dim,
281
+ label="input dim",
282
+ error_type=ConfigError,
283
+ )
284
+
285
+ @classmethod
286
+ def from_dict(cls, payload: object, *, path: str = "preset") -> PresetConfig:
287
+ values = _mapping(payload, path)
288
+ allowed = {
289
+ "name",
290
+ "input_dim",
291
+ "input",
292
+ "source_hz",
293
+ "target_hz",
294
+ "interpolation",
295
+ "components",
296
+ }
297
+ _reject_unknown(values, allowed, path)
298
+ raw_components = _mapping(
299
+ _required(values, "components", path), f"{path}.components"
300
+ )
301
+ components = tuple(
302
+ ComponentSpec.from_dict(name, raw, path=f"{path}.components.{name}")
303
+ for name, raw in raw_components.items()
304
+ )
305
+ return cls(
306
+ name=str(_required(values, "name", path)),
307
+ input_dim=_integer(
308
+ _required(values, "input_dim", path), f"{path}.input_dim", positive=True
309
+ ),
310
+ input=str(_required(values, "input", path)),
311
+ components=components,
312
+ source_hz=_number(values.get("source_hz", 15.0), f"{path}.source_hz"),
313
+ target_hz=_number(values.get("target_hz", 15.0), f"{path}.target_hz"),
314
+ interpolation=str(values.get("interpolation", "cubic")),
315
+ )
316
+
317
+ @classmethod
318
+ def from_yaml(cls, path: str | Path) -> PresetConfig:
319
+ """Load one strict user preset without executing arbitrary code."""
320
+ source = Path(path)
321
+ try:
322
+ payload = yaml.load(
323
+ source.read_text(encoding="utf-8"), Loader=_UniqueKeyLoader
324
+ )
325
+ except yaml.YAMLError as error:
326
+ raise ConfigError(f"cannot parse {source}: {error}") from error
327
+ return cls.from_dict(payload, path=str(source))
328
+
329
+ def save_yaml(self, path: str | Path) -> Path:
330
+ """Save the complete reproducible preset and return its path."""
331
+ target = Path(path)
332
+ target.parent.mkdir(parents=True, exist_ok=True)
333
+ target.write_text(
334
+ yaml.safe_dump(self.to_dict(), sort_keys=False), encoding="utf-8"
335
+ )
336
+ return target
337
+
338
+ def to_dict(self) -> dict[str, object]:
339
+ payload = {
340
+ "name": self.name,
341
+ "input_dim": self.input_dim,
342
+ "input": self.input,
343
+ "source_hz": self.source_hz,
344
+ "target_hz": self.target_hz,
345
+ "interpolation": self.interpolation,
346
+ "components": {
347
+ component.name: component.to_dict() for component in self.components
348
+ },
349
+ }
350
+ return payload
351
+
352
+ def summary(self) -> str:
353
+ """Return a compact action-space label for discovery UIs."""
354
+ spaces = "+".join(
355
+ dict.fromkeys(component.space for component in self.components)
356
+ )
357
+ return f"{self.name} (D={self.input_dim}, {self.input}, {spaces}, {self.source_hz:g} Hz input)"
358
+
359
+ def describe(self) -> str:
360
+ """Describe every source tensor column and its physical interpretation."""
361
+ motion = "position" if self.input == "absolute" else "per-step delta"
362
+ lines = [self.summary()]
363
+ for component in self.components:
364
+ if component.space == "joint":
365
+ assert component.joint_dims is not None
366
+ lines.append(
367
+ f" dims {_format_dims(component.joint_dims)}: {component.name} "
368
+ f"generalized-joint {motion} [rad or registered canonical unit]"
369
+ )
370
+ else:
371
+ assert component.position_dims is not None
372
+ assert component.rotation_dims is not None
373
+ lines.append(
374
+ f" dims {_format_dims(component.position_dims)}: {component.name} "
375
+ f"EEF translation {motion} (x, y, z in listed order) [m]"
376
+ )
377
+ rotation_motion = (
378
+ "orientation" if self.input == "absolute" else "increment"
379
+ )
380
+ rotation_unit = (
381
+ "unit xyzw quaternion"
382
+ if component.rotation_format == "quaternion_xyzw"
383
+ else "rad"
384
+ )
385
+ lines.append(
386
+ f" dims {_format_dims(component.rotation_dims)}: {component.name} EEF "
387
+ f"{rotation_motion} [{component.rotation_format}, {rotation_unit}]"
388
+ )
389
+ if self.input == "delta":
390
+ lines.append(
391
+ " rotation_vector = axis * angle [rad], right-multiplicative "
392
+ "body-frame increment: R_next = R_previous @ Exp(rotvec)"
393
+ )
394
+ elif component.rotation_format == "rotation_vector":
395
+ lines.append(
396
+ " rotation_vector = axis * angle [rad], not Euler angles"
397
+ )
398
+ elif component.rotation_format == "euler_xyz":
399
+ lines.append(" euler_xyz = extrinsic x, y, z Euler angles [rad]")
400
+ if component.gripper_dim is not None:
401
+ comparison = ">=" if component.gripper_open_is_high else "<="
402
+ lines.append(
403
+ f" dim {component.gripper_dim}: {component.name} gripper command "
404
+ f"(open if value {comparison} {component.gripper_threshold:g}, otherwise closed; "
405
+ "decoded as absolute command: 1=open, 0=closed)"
406
+ )
407
+ else:
408
+ lines.append(
409
+ f" {component.name}: no binary gripper channel; all listed columns are motion"
410
+ )
411
+ if self.input == "absolute":
412
+ lines.append(
413
+ " encode/decode require the same current_state (B,D), immediately before row 0"
414
+ )
415
+ else:
416
+ lines.append(
417
+ " encode/decode: omit current_state; deltas are per-step increments, not velocities"
418
+ )
419
+ lines.append(
420
+ f" resampling: {self.source_hz:g} Hz default -> {self.target_hz:g} Hz codec "
421
+ f"clock ({self.interpolation})"
422
+ )
423
+ return "\n".join(lines)
424
+
425
+ def canonicalize(
426
+ self,
427
+ values: np.ndarray,
428
+ *,
429
+ source_hz: float | None = None,
430
+ current_state: np.ndarray | None = None,
431
+ ) -> dict[str, list[ActiveTrajectory]]:
432
+ "Convert a batch into local paths for the two shared profiles."
433
+ array = np.asarray(values, dtype=np.float32)
434
+ if array.ndim != 3 or len(array) < 1 or array.shape[1] < 1:
435
+ raise ValueError("preset input must have non-empty shape [B, T, D]")
436
+ if array.shape[2] != self.input_dim:
437
+ raise ValueError(
438
+ f"preset {self.name!r} expects input_dim={self.input_dim}, got {array.shape[2]}"
439
+ )
440
+ if not np.all(np.isfinite(array)):
441
+ raise ValueError("preset input contains a non-finite value")
442
+ states: list[np.ndarray | None]
443
+ if current_state is None:
444
+ states = [None] * len(array)
445
+ else:
446
+ state_array = optional_finite_array(
447
+ current_state, shape=(len(array), self.input_dim)
448
+ )
449
+ assert state_array is not None
450
+ states = list(state_array)
451
+ return {
452
+ component.name: [
453
+ self._canonicalize_component(
454
+ episode,
455
+ component,
456
+ source_hz=self.source_hz if source_hz is None else source_hz,
457
+ current_state=state,
458
+ )
459
+ for episode, state in zip(array, states, strict=True)
460
+ ]
461
+ for component in self.components
462
+ }
463
+
464
+ def canonicalize_episode(
465
+ self,
466
+ values: np.ndarray,
467
+ *,
468
+ source_hz: float | None = None,
469
+ current_state: np.ndarray | None = None,
470
+ ) -> dict[str, ActiveTrajectory]:
471
+ """Canonicalize one variable-length episode and optional ``(D,)`` anchor."""
472
+ episode = np.asarray(values, dtype=np.float32)
473
+ if episode.ndim != 2 or len(episode) < 1 or episode.shape[1] != self.input_dim:
474
+ raise ValueError(
475
+ f"preset {self.name!r} expects a non-empty [T, {self.input_dim}] episode"
476
+ )
477
+ if not np.all(np.isfinite(episode)):
478
+ raise ValueError("preset input contains a non-finite value")
479
+ state = optional_finite_array(current_state, shape=(self.input_dim,))
480
+ return {
481
+ component.name: self._canonicalize_component(
482
+ episode,
483
+ component,
484
+ source_hz=self.source_hz if source_hz is None else source_hz,
485
+ current_state=state,
486
+ )
487
+ for component in self.components
488
+ }
489
+
490
+ def profile_trajectories(
491
+ self, values: np.ndarray, *, current_state: np.ndarray | None = None
492
+ ) -> dict[str, list[ActiveTrajectory]]:
493
+ """Canonicalize one episode and group its components by joint/EEF profile."""
494
+ canonical = self.canonicalize_episode(values, current_state=current_state)
495
+ output: dict[str, list[ActiveTrajectory]] = {}
496
+ for component in self.components:
497
+ output.setdefault(component.space, []).append(canonical[component.name])
498
+ return output
499
+
500
+ def _canonicalize_component(
501
+ self,
502
+ episode: np.ndarray,
503
+ component: ComponentSpec,
504
+ *,
505
+ source_hz: float,
506
+ current_state: np.ndarray | None,
507
+ ) -> ActiveTrajectory:
508
+ if component.space == "joint":
509
+ motion = self._joint_motion(
510
+ episode, component, source_hz=source_hz, current_state=current_state
511
+ )
512
+ else:
513
+ motion = self._eef_motion(
514
+ episode, component, source_hz=source_hz, current_state=current_state
515
+ )
516
+ if component.gripper_dim is None:
517
+ return ActiveTrajectory(motion, component.active_dims)
518
+ gripper = resample_uniform(
519
+ episode[:, component.gripper_dim : component.gripper_dim + 1],
520
+ source_hz=source_hz,
521
+ target_hz=self.target_hz,
522
+ kind="hold",
523
+ method=self.interpolation,
524
+ )
525
+ binary = binarize_gripper_actions(
526
+ gripper,
527
+ threshold=component.gripper_threshold,
528
+ open_is_high=component.gripper_open_is_high,
529
+ )
530
+ if len(binary) != len(motion):
531
+ raise RuntimeError("motion and gripper produced different target horizons")
532
+ return ActiveTrajectory(
533
+ np.concatenate((motion, binary), axis=1), component.active_dims
534
+ )
535
+
536
+ def _joint_motion(
537
+ self,
538
+ episode: np.ndarray,
539
+ component: ComponentSpec,
540
+ *,
541
+ source_hz: float,
542
+ current_state: np.ndarray | None,
543
+ ) -> np.ndarray:
544
+ assert component.joint_dims is not None
545
+ source = np.take(episode, component.joint_dims, axis=1)
546
+ if self.input == "delta":
547
+ source = np.cumsum(source, axis=0, dtype=np.float32)
548
+ anchor = np.zeros((1, source.shape[1]), dtype=np.float32)
549
+ else:
550
+ raw_anchor = (
551
+ source[:1]
552
+ if current_state is None
553
+ else np.take(current_state, component.joint_dims)[None]
554
+ )
555
+ unwrapped = np.unwrap(np.concatenate((raw_anchor, source), axis=0), axis=0)
556
+ anchor = np.asarray(unwrapped[:1], dtype=np.float32)
557
+ source = np.asarray(unwrapped[1:], dtype=np.float32)
558
+ sampled = resample_uniform(
559
+ source,
560
+ source_hz=source_hz,
561
+ target_hz=self.target_hz,
562
+ kind="linear",
563
+ method=self.interpolation,
564
+ )
565
+ return np.asarray(sampled - anchor, dtype=np.float32)
566
+
567
+ def _eef_motion(
568
+ self,
569
+ episode: np.ndarray,
570
+ component: ComponentSpec,
571
+ *,
572
+ source_hz: float,
573
+ current_state: np.ndarray | None,
574
+ ) -> np.ndarray:
575
+ assert (
576
+ component.position_dims is not None and component.rotation_dims is not None
577
+ )
578
+ position = np.take(episode, component.position_dims, axis=1)
579
+ rotation = np.take(episode, component.rotation_dims, axis=1)
580
+ if self.input == "delta":
581
+ pose = integrate_eef_deltas(np.concatenate((position, rotation), axis=1))
582
+ pose = resample_uniform(
583
+ pose,
584
+ source_hz=source_hz,
585
+ target_hz=self.target_hz,
586
+ kind="pose_xyzw",
587
+ method=self.interpolation,
588
+ )
589
+ return eef_pose_absolute(pose)
590
+ pose = eef_pose_from_components(position, rotation, component.rotation_format)
591
+ if current_state is None:
592
+ initial_pose = pose[0]
593
+ else:
594
+ initial_pose = eef_pose_from_components(
595
+ np.take(current_state, component.position_dims)[None],
596
+ np.take(current_state, component.rotation_dims)[None],
597
+ component.rotation_format,
598
+ )[0]
599
+ pose = resample_uniform(
600
+ pose,
601
+ source_hz=source_hz,
602
+ target_hz=self.target_hz,
603
+ kind="pose_xyzw",
604
+ method=self.interpolation,
605
+ )
606
+ return eef_pose_local_absolute(pose, initial_pose=initial_pose)
607
+
608
+
609
+ def format_action_spaces(
610
+ presets: Mapping[str, PresetConfig],
611
+ *,
612
+ selected: str | None = None,
613
+ action_dim: int | None = None,
614
+ second_order_dims: Mapping[str, Sequence[int]] | None = None,
615
+ ) -> str:
616
+ "Explain registered tensor layouts without inferring semantics from shape."
617
+ if action_dim is not None:
618
+ action_dim = _integer(action_dim, "action_dim", positive=True)
619
+ if not presets:
620
+ return "No action spaces are registered."
621
+ candidates = sorted(
622
+ (
623
+ preset
624
+ for preset in presets.values()
625
+ if action_dim is None or preset.input_dim == action_dim
626
+ ),
627
+ key=lambda preset: (preset.input_dim, preset.name),
628
+ )
629
+ lines = [
630
+ "ActionCodec2 — action layout guide",
631
+ f"Selected action_space: {selected or 'none (choose a layout before encoding)'}",
632
+ "",
633
+ "Input: NumPy arrays or Torch tensors, (T,D) or (B,T,D).",
634
+ " T = timesteps, D = columns per action, B = batch size; column indices start at 0.",
635
+ " Any positive T is accepted. A batch needs equal T; encode unequal-length episodes separately.",
636
+ " decode returns (B,T,D), including B=1 for a single episode; values are quantized, not lossless.",
637
+ "",
638
+ "How to choose:",
639
+ " joint = robot joint coordinates; EEF = end-effector (tool) position and orientation.",
640
+ " absolute = target coordinates; delta = per-step motion increments, not velocities.",
641
+ " Match column order, units, rotation convention, gripper commands and controller fps.",
642
+ " Shape alone cannot identify physical meaning: layouts sharing D are only candidates.",
643
+ " Consult your dataset/controller documentation if these meanings are unknown.",
644
+ " Supply physical units, not normalized [-1,1] features; convert degrees to radians.",
645
+ " Non-radian joint coordinates need an explicit mapping to the registered canonical units.",
646
+ " Pass the actual sampling rate as fps=...; defaults and codec rates appear below.",
647
+ " Resampling can change decoded T; token coverage determines the returned length.",
648
+ "",
649
+ f"Registered configurations: {len(candidates)} shown / {len(presets)} total"
650
+ + (
651
+ ""
652
+ if action_dim is None
653
+ else f" (D={action_dim} candidates, not automatic selection)"
654
+ ),
655
+ ]
656
+ if not candidates:
657
+ widths = ", ".join(
658
+ map(str, sorted({preset.input_dim for preset in presets.values()}))
659
+ )
660
+ lines.extend(
661
+ [
662
+ f" No registered action space matches D={action_dim}. Available D: {widths}.",
663
+ " Confirm your column meanings and register a custom layout; do not pad/truncate to force a match.",
664
+ ]
665
+ )
666
+ for preset in candidates:
667
+ name = preset.name
668
+ marker = " [selected]" if name == selected else ""
669
+ description = preset.describe().splitlines()
670
+ lines.extend(["", f"- {description[0]}{marker}"])
671
+ lines.extend(f" {line.strip()}" for line in description[1:])
672
+ if second_order_dims is not None:
673
+ boundaries = [
674
+ f"{component.name!r}: (B,{len(component.active_dims)})"
675
+ for component in preset.components
676
+ if set(component.active_dims).intersection(
677
+ second_order_dims.get(component.space, ())
678
+ )
679
+ ]
680
+ lines.append(
681
+ " previous_first_order: "
682
+ + (
683
+ "required in encode/decode, {" + ", ".join(boundaries) + "}; "
684
+ f"reconstructed increments at {preset.target_hz:g} Hz, B=1 for a single episode"
685
+ if boundaries
686
+ else "not required by this artifact for this layout"
687
+ )
688
+ )
689
+ lines.extend(
690
+ [
691
+ "",
692
+ "Next steps:",
693
+ " Load: ActionCodec2.from_pretrained(path, action_space='NAME')",
694
+ " Switch: codec = codec.for_action_space('NAME')",
695
+ " Filter: codec.print_action_spaces(action_dim=D)",
696
+ " Config: codec.action_spaces['NAME'].to_dict() # exact settings for a listed layout",
697
+ " Custom: codec.register_action_space(preset) # explicit column definitions, then switch",
698
+ " Custom layouts can reorder columns; each joint component supports 1..7 coordinates.",
699
+ " EEF absolute rotations support rotation_vector, euler_xyz or quaternion_xyzw (unit x,y,z,w).",
700
+ " EEF delta rotations require rotation_vector with the convention shown above.",
701
+ " For absolute layouts, current_state is (B,D), even when actions are (T,D).",
702
+ ]
703
+ )
704
+ return "\n".join(lines)
705
+
706
+
707
+ def resample_canonical_trajectory(
708
+ trajectory: ActiveTrajectory,
709
+ component: ComponentSpec,
710
+ *,
711
+ source_hz: float,
712
+ target_hz: float,
713
+ target_length: int | None = None,
714
+ method: str = "linear",
715
+ ) -> ActiveTrajectory:
716
+ """Rate-convert one decoded canonical component without changing slots."""
717
+ values = np.asarray(trajectory.values, dtype=np.float32)
718
+ motion_width = len(component.joint_dims or ()) if component.space == "joint" else 6
719
+ motion = values[:, :motion_width]
720
+ if component.space == "eef":
721
+ pose = eef_pose_from_components(
722
+ motion[:, :3], motion[:, 3:6], "rotation_vector"
723
+ )
724
+ sampled_pose = resample_uniform(
725
+ pose,
726
+ source_hz=source_hz,
727
+ target_hz=target_hz,
728
+ kind="pose_xyzw",
729
+ target_length=target_length,
730
+ method=method,
731
+ )
732
+ sampled_motion = eef_pose_absolute(sampled_pose)
733
+ else:
734
+ sampled_motion = resample_uniform(
735
+ motion,
736
+ source_hz=source_hz,
737
+ target_hz=target_hz,
738
+ kind="linear",
739
+ target_length=target_length,
740
+ method=method,
741
+ )
742
+ if component.gripper_dim is None:
743
+ return ActiveTrajectory(sampled_motion, trajectory.active_dims)
744
+ gripper = resample_uniform(
745
+ values[:, motion_width : motion_width + 1],
746
+ source_hz=source_hz,
747
+ target_hz=target_hz,
748
+ kind="hold",
749
+ target_length=target_length,
750
+ method=method,
751
+ )
752
+ return ActiveTrajectory(
753
+ np.concatenate((sampled_motion, gripper), axis=1), trajectory.active_dims
754
+ )
755
+
756
+
757
+ def externalize_canonical_trajectory(
758
+ trajectory: ActiveTrajectory,
759
+ component: ComponentSpec,
760
+ *,
761
+ current_state: np.ndarray | None,
762
+ ) -> np.ndarray:
763
+ "Return deltas, or absolute values when an external anchor is supplied."
764
+ values = np.asarray(trajectory.values, dtype=np.float32)
765
+ motion_width = len(component.joint_dims or ()) if component.space == "joint" else 6
766
+ motion = values[:, :motion_width]
767
+ if component.space == "joint":
768
+ if current_state is None:
769
+ previous = np.concatenate((np.zeros_like(motion[:1]), motion[:-1]), axis=0)
770
+ output_motion = motion - previous
771
+ else:
772
+ assert component.joint_dims is not None
773
+ output_motion = motion + np.take(current_state, component.joint_dims)[None]
774
+ else:
775
+ local_pose = eef_pose_from_components(
776
+ motion[:, :3], motion[:, 3:6], "rotation_vector"
777
+ )
778
+ if current_state is None:
779
+ output_motion = eef_pose_deltas(local_pose)
780
+ else:
781
+ assert (
782
+ component.position_dims is not None
783
+ and component.rotation_dims is not None
784
+ )
785
+ initial_pose = eef_pose_from_components(
786
+ np.take(current_state, component.position_dims)[None],
787
+ np.take(current_state, component.rotation_dims)[None],
788
+ component.rotation_format,
789
+ )[0]
790
+ output_motion = eef_pose_components(
791
+ eef_pose_from_local_absolute(local_pose, initial_pose=initial_pose),
792
+ component.rotation_format,
793
+ )
794
+ if component.gripper_dim is None:
795
+ return np.asarray(output_motion, dtype=np.float32)
796
+ gripper = values[:, motion_width : motion_width + 1]
797
+ if not component.gripper_open_is_high:
798
+ gripper = 1.0 - gripper
799
+ return np.asarray(
800
+ np.concatenate((output_motion, gripper), axis=1),
801
+ dtype=np.float32,
802
+ )
runtime/routing/routing.py ADDED
@@ -0,0 +1,745 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Required-preset routing over the two shared ActionCodec2 processors."
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping, Sequence
7
+ from dataclasses import asdict, dataclass
8
+ from pathlib import Path
9
+ from types import MappingProxyType
10
+ from typing import TYPE_CHECKING
11
+
12
+ import numpy as np
13
+ import yaml
14
+
15
+ from ..common._validation import optional_finite_array
16
+ from .cache import BoundedCache
17
+ from ..physical.configuration import (
18
+ ConfigError,
19
+ _integer,
20
+ _mapping,
21
+ _reject_unknown,
22
+ _required,
23
+ _UniqueKeyLoader,
24
+ )
25
+ from ..integration.hf import (
26
+ ROUTER_ALLOW_PATTERNS,
27
+ ROUTER_CONFIG_NAME,
28
+ ROUTER_MODEL_TYPE,
29
+ resolve_pretrained_directory,
30
+ save_huggingface_metadata,
31
+ save_model_card,
32
+ )
33
+ from ..physical.physical_processor import ActionCodec2Processor
34
+ from .presets import (
35
+ ComponentSpec,
36
+ PresetConfig,
37
+ externalize_canonical_trajectory,
38
+ format_action_spaces,
39
+ resample_canonical_trajectory,
40
+ )
41
+ from ..physical.semantic import resampled_horizon
42
+ from ..physical.trajectory import ActiveTrajectory
43
+
44
+ if TYPE_CHECKING:
45
+ from .processing_actioncodec2 import ActionCodec2
46
+
47
+
48
+ ROUTER_CONFIG_VERSION = 3
49
+ _NAME = re.compile(r"[a-z][a-z0-9_]*\Z")
50
+ _PROFILES = {"eef", "joint"}
51
+ _PROFILE_WIDTHS = {"eef": 7, "joint": 8}
52
+
53
+
54
+ def _current_state_rows(
55
+ current_state: np.ndarray | None, *, count: int, input_dim: int
56
+ ) -> list[np.ndarray | None]:
57
+ if current_state is None:
58
+ return [None] * count
59
+ values = optional_finite_array(current_state, shape=(count, input_dim))
60
+ assert values is not None
61
+ return list(values)
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class ProfileSpec:
66
+ """Token namespace metadata for one shared physical processor."""
67
+
68
+ name: str
69
+ token_offset: int
70
+ token_budget: int
71
+ artifact_fingerprint: str
72
+
73
+ def __post_init__(self) -> None:
74
+ if _NAME.fullmatch(self.name) is None:
75
+ raise ConfigError(f"profile.name must match {_NAME.pattern!r}")
76
+ _integer(self.token_offset, "profile.token_offset")
77
+ _integer(self.token_budget, "profile.token_budget", positive=True)
78
+ if re.fullmatch(r"[0-9a-f]{64}", self.artifact_fingerprint) is None:
79
+ raise ConfigError(
80
+ "profile.artifact_fingerprint must be a SHA-256 hex digest"
81
+ )
82
+
83
+ @classmethod
84
+ def from_dict(cls, payload: object, *, path: str) -> ProfileSpec:
85
+ values = _mapping(payload, path)
86
+ allowed = {"name", "token_offset", "token_budget", "artifact_fingerprint"}
87
+ _reject_unknown(values, allowed, path)
88
+ return cls(
89
+ name=str(_required(values, "name", path)),
90
+ token_offset=_integer(
91
+ _required(values, "token_offset", path), f"{path}.token_offset"
92
+ ),
93
+ token_budget=_integer(
94
+ _required(values, "token_budget", path),
95
+ f"{path}.token_budget",
96
+ positive=True,
97
+ ),
98
+ artifact_fingerprint=str(_required(values, "artifact_fingerprint", path)),
99
+ )
100
+
101
+ def to_dict(self) -> dict[str, object]:
102
+ return asdict(self)
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class _ProfileRuntime:
107
+ spec: ProfileSpec
108
+ processor: ActionCodec2Processor
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class _Segment:
113
+ component: ComponentSpec
114
+ profile: _ProfileRuntime
115
+
116
+
117
+ class RoutedActionCodec2Grammar:
118
+ """Constrained grammar over a preset's ordered, namespaced segments."""
119
+
120
+ def __init__(
121
+ self, router: ActionCodec2Router, *, preset: str, horizon: int
122
+ ) -> None:
123
+ horizon = _integer(horizon, "horizon", positive=True)
124
+ self.segments = router._segments(router.preset(preset))
125
+ self.vocab_size = router.vocab_size
126
+ self._grammars = tuple(
127
+ segment.profile.processor.grammar(
128
+ horizon, active_dims=segment.component.active_dims
129
+ )
130
+ for segment in self.segments
131
+ )
132
+ initial = self._normalize_state(0, self._grammars[0]._initial_state())
133
+ self._initial_state = initial
134
+ self._state_cache = BoundedCache(256)
135
+ self._state_cache[()] = initial
136
+
137
+ def _normalize_state(
138
+ self, segment_index: int, state: tuple[int, ...]
139
+ ) -> tuple[int, tuple[int, ...]]:
140
+ while segment_index < len(self.segments) and self._grammars[
141
+ segment_index
142
+ ]._state_is_complete(state):
143
+ segment_index += 1
144
+ if segment_index < len(self.segments):
145
+ state = self._grammars[segment_index]._initial_state()
146
+ return segment_index, state
147
+
148
+ def _advance_routed_state(
149
+ self,
150
+ routed_state: tuple[int, tuple[int, ...]],
151
+ token: int,
152
+ ) -> tuple[int, tuple[int, ...]] | None:
153
+ segment_index, state = routed_state
154
+ if segment_index == len(self.segments):
155
+ return None
156
+ segment = self.segments[segment_index]
157
+ local_token = token - segment.profile.spec.token_offset
158
+ if not 0 <= local_token < segment.profile.processor.vocab_size:
159
+ return None
160
+ next_state = self._grammars[segment_index]._advance(state, local_token)
161
+ if next_state is None:
162
+ return None
163
+ return self._normalize_state(segment_index, next_state)
164
+
165
+ def _consume(self, tokens: Sequence[int]) -> tuple[int, tuple[int, ...]] | None:
166
+ checked: list[int] = []
167
+ for raw_token in tokens:
168
+ if isinstance(raw_token, bool) or not isinstance(
169
+ raw_token, (int, np.integer)
170
+ ):
171
+ return None
172
+ checked.append(int(raw_token))
173
+ key = tuple(checked)
174
+ if not key:
175
+ return self._initial_state
176
+ missing = object()
177
+ cached = self._state_cache.get(key, missing)
178
+ if cached is not missing:
179
+ return cached
180
+ parent = key[:-1]
181
+ state = self._state_cache.get(parent, missing)
182
+ if state is missing:
183
+ state = self._initial_state
184
+ for token in parent:
185
+ if state is None:
186
+ break
187
+ state = self._advance_routed_state(state, token)
188
+ self._state_cache[parent] = state
189
+ if state is not None:
190
+ state = self._advance_routed_state(state, key[-1])
191
+ self._state_cache[key] = state
192
+ return state
193
+
194
+ def next_token_mask(self, tokens: Sequence[int]) -> np.ndarray:
195
+ """Return a global namespace mask for the next preset token."""
196
+ state = self._consume(tokens)
197
+ if state is None:
198
+ raise ValueError("token prefix is not legal for this routed preset")
199
+ segment_index, local_state = state
200
+ output = np.zeros(self.vocab_size, dtype=bool)
201
+ if segment_index == len(self.segments):
202
+ return output
203
+ segment = self.segments[segment_index]
204
+ mask = self._grammars[segment_index]._next_token_mask_from_state(local_state)
205
+ start = segment.profile.spec.token_offset
206
+ output[start : start + len(mask)] = mask
207
+ return output
208
+
209
+ def is_complete(self, tokens: Sequence[int]) -> bool:
210
+ """Whether the prefix completes every segment exactly once."""
211
+ state = self._consume(tokens)
212
+ return state is not None and state[0] == len(self.segments)
213
+
214
+
215
+ class ActionCodec2Router:
216
+ "Route heterogeneous dataset states into physical token namespaces."
217
+
218
+ model_type = ROUTER_MODEL_TYPE
219
+
220
+ def __init__(
221
+ self,
222
+ profiles: Sequence[tuple[ProfileSpec, ActionCodec2Processor]],
223
+ presets: Sequence[PresetConfig],
224
+ ) -> None:
225
+ runtimes: dict[str, _ProfileRuntime] = {}
226
+ expected_offset = 0
227
+ for spec, processor in profiles:
228
+ if spec.name in runtimes:
229
+ raise ConfigError(f"duplicate physical profile {spec.name!r}")
230
+ if spec.token_offset != expected_offset:
231
+ raise ConfigError(
232
+ "profile token namespaces must be contiguous and ordered"
233
+ )
234
+ if processor.vocab_size > spec.token_budget:
235
+ raise ConfigError(
236
+ f"profile {spec.name!r} uses {processor.vocab_size} tokens, "
237
+ f"exceeding budget {spec.token_budget}"
238
+ )
239
+ if processor.artifact_fingerprint != spec.artifact_fingerprint:
240
+ raise ConfigError(
241
+ f"profile {spec.name!r} artifact fingerprint does not match its processor"
242
+ )
243
+ if processor.config.set_bpe.allow_cross_component_merge:
244
+ raise ConfigError(
245
+ "allow_cross_component_merge=true is not supported by the v1 router; "
246
+ "a component-aware Set-BPE serializer and grammar are required"
247
+ )
248
+ runtimes[spec.name] = _ProfileRuntime(spec, processor)
249
+ expected_offset += spec.token_budget
250
+ if not runtimes or not set(runtimes) <= _PROFILES:
251
+ raise ConfigError(
252
+ "router requires joint and/or eef physical profiles"
253
+ )
254
+ for name, runtime in runtimes.items():
255
+ if runtime.processor.action_dim != _PROFILE_WIDTHS[name]:
256
+ raise ConfigError(
257
+ f"profile {name!r} must have width {_PROFILE_WIDTHS[name]}, "
258
+ f"got {runtime.processor.action_dim}"
259
+ )
260
+ self.profiles = MappingProxyType(runtimes)
261
+ self._source_directory = Path(__file__).resolve().parent.parent
262
+ self._grammar_cache = BoundedCache(8)
263
+ self._action_spaces: dict[str, PresetConfig] = {}
264
+ for preset in presets:
265
+ self.register_action_space(preset)
266
+ self.vocab_size = expected_offset
267
+
268
+ @property
269
+ def action_spaces(self) -> Mapping[str, PresetConfig]:
270
+ """Read-only registered built-in and user-created action spaces."""
271
+ return MappingProxyType(self._action_spaces)
272
+
273
+ def __str__(self) -> str:
274
+ return self.describe_action_spaces()
275
+
276
+ def describe_action_spaces(
277
+ self, *, selected: str | None = None, action_dim: int | None = None
278
+ ) -> str:
279
+ """Explain all layouts, or source-width candidates, using this artifact's state requirements."""
280
+ second_order_dims = {
281
+ name: tuple(
282
+ dimension
283
+ for component in runtime.processor.config.physical.components.values()
284
+ if component.primitive_order == 2
285
+ for dimension in component.dims
286
+ )
287
+ for name, runtime in self.profiles.items()
288
+ }
289
+ return format_action_spaces(
290
+ self.action_spaces,
291
+ selected=selected,
292
+ action_dim=action_dim,
293
+ second_order_dims=second_order_dims,
294
+ )
295
+
296
+ @classmethod
297
+ def from_processors(
298
+ cls,
299
+ processors: Mapping[str, ActionCodec2Processor],
300
+ *,
301
+ token_budgets: Mapping[str, int],
302
+ presets: Sequence[PresetConfig],
303
+ ) -> ActionCodec2Router:
304
+ """Assign stable contiguous namespaces in canonical joint/eef order."""
305
+ if set(processors) != set(token_budgets):
306
+ raise ConfigError("processors and token_budgets must share keys")
307
+ if set(processors) != _PROFILES:
308
+ raise ConfigError(
309
+ "processors must contain exactly the joint and eef profiles"
310
+ )
311
+ offset = 0
312
+ entries = []
313
+ for name in ("joint", "eef"):
314
+ processor = processors[name]
315
+ budget = _integer(
316
+ token_budgets[name], f"token_budgets.{name}", positive=True
317
+ )
318
+ spec = ProfileSpec(
319
+ name,
320
+ offset,
321
+ budget,
322
+ processor.artifact_fingerprint,
323
+ )
324
+ entries.append((spec, processor))
325
+ offset += budget
326
+ return cls(entries, presets)
327
+
328
+ def preset(self, name: str) -> PresetConfig:
329
+ """Resolve one preset or fail with the available names."""
330
+ try:
331
+ return self._action_spaces[name]
332
+ except KeyError as error:
333
+ raise KeyError(
334
+ f"unknown preset {name!r}; available: {sorted(self._action_spaces)}"
335
+ ) from error
336
+
337
+ def register_action_space(
338
+ self,
339
+ preset: PresetConfig | Mapping[str, object] | str | Path,
340
+ ) -> PresetConfig:
341
+ "Register a validated action-space config, mapping, or YAML file."
342
+ if isinstance(preset, Mapping):
343
+ preset = PresetConfig.from_dict(preset)
344
+ elif isinstance(preset, (str, Path)):
345
+ preset = PresetConfig.from_yaml(preset)
346
+ if not isinstance(preset, PresetConfig):
347
+ raise TypeError(
348
+ "register_action_space expects PresetConfig, a mapping, or a preset YAML path"
349
+ )
350
+ existing = self._action_spaces.get(preset.name)
351
+ if existing is not None and existing != preset:
352
+ raise ConfigError(
353
+ f"action-space name {preset.name!r} is already registered differently"
354
+ )
355
+ self._action_spaces[preset.name] = preset
356
+ return preset
357
+
358
+ def _segments(self, preset: PresetConfig) -> tuple[_Segment, ...]:
359
+ return tuple(
360
+ _Segment(component, self.profiles[component.space])
361
+ for component in preset.components
362
+ )
363
+
364
+ def encode_batch(
365
+ self,
366
+ inputs: np.ndarray,
367
+ *,
368
+ preset: str,
369
+ fps: float | None = None,
370
+ current_state: np.ndarray | None = None,
371
+ previous_first_order: Mapping[str, np.ndarray] | None = None,
372
+ ) -> list[list[int]]:
373
+ "Encode one regular-rate ``[B,T,D]`` tensor at ``fps``."
374
+ preset_config = self.preset(preset)
375
+ self._validate_boundary_keys(previous_first_order, preset_config)
376
+ canonical = preset_config.canonicalize(
377
+ inputs, source_hz=fps, current_state=current_state
378
+ )
379
+ batch = len(next(iter(canonical.values())))
380
+ rows: list[list[int]] = [[] for _ in range(batch)]
381
+ for segment in self._segments(preset_config):
382
+ values = canonical[segment.component.name]
383
+ streams = segment.profile.processor.encode_batch(
384
+ np.stack([trajectory.values for trajectory in values]),
385
+ active_dims=segment.component.active_dims,
386
+ previous_first_order=None
387
+ if previous_first_order is None
388
+ else previous_first_order.get(segment.component.name),
389
+ )
390
+ offset = segment.profile.spec.token_offset
391
+ for row, stream in zip(rows, streams, strict=True):
392
+ row.extend(offset + token for token in stream)
393
+ return rows
394
+
395
+ def route_trajectories(
396
+ self,
397
+ inputs: np.ndarray,
398
+ *,
399
+ preset: str,
400
+ current_state: np.ndarray | None = None,
401
+ fps: float | None = None,
402
+ previous_first_order: Mapping[str, np.ndarray] | None = None,
403
+ ) -> dict[str, list[ActiveTrajectory]]:
404
+ "Expose canonical complete trajectories grouped by physical profile."
405
+ preset_config = self.preset(preset)
406
+ self._validate_boundary_keys(previous_first_order, preset_config)
407
+ canonical = preset_config.canonicalize(
408
+ inputs, current_state=current_state, source_hz=fps
409
+ )
410
+ output: dict[str, list[ActiveTrajectory]] = {}
411
+ for component in preset_config.components:
412
+ trajectories = canonical[component.name]
413
+ processor = self.profiles[component.space].processor
414
+ previous = (
415
+ None
416
+ if previous_first_order is None
417
+ else previous_first_order.get(component.name)
418
+ )
419
+ dense = processor._dense_previous_first_order(
420
+ previous, component.active_dims, len(trajectories)
421
+ )
422
+ rows = (
423
+ [None] * len(trajectories)
424
+ if dense is None
425
+ else np.take(dense, component.active_dims, axis=1)
426
+ )
427
+ output.setdefault(component.space, []).extend(
428
+ ActiveTrajectory(t.values, t.active_dims, previous_first_order=p)
429
+ for t, p in zip(trajectories, rows, strict=True)
430
+ )
431
+ return output
432
+
433
+ def _validate_boundary_keys(
434
+ self,
435
+ previous_first_order: Mapping[str, np.ndarray] | None,
436
+ preset: PresetConfig,
437
+ ) -> None:
438
+ if previous_first_order is None:
439
+ previous_first_order = {}
440
+ if not isinstance(previous_first_order, Mapping):
441
+ raise ValueError(
442
+ "previous_first_order must map component names to canonical [B,D_active] boundaries"
443
+ )
444
+ unknown = set(previous_first_order) - {c.name for c in preset.components}
445
+ if unknown:
446
+ raise ValueError(
447
+ f"unknown previous_first_order component(s): {sorted(unknown)}"
448
+ )
449
+ required = {
450
+ c.name: f"(B, {len(c.active_dims)})"
451
+ for c in preset.components
452
+ if any(
453
+ p.primitive_order == 2 and set(c.active_dims).intersection(p.dims)
454
+ for p in self.profiles[
455
+ c.space
456
+ ].processor.config.physical.components.values()
457
+ )
458
+ }
459
+ missing = set(required) - previous_first_order.keys()
460
+ if missing:
461
+ raise ValueError(
462
+ "second-order tokenization requires previous_first_order with arrays "
463
+ f"{required}; missing {sorted(missing)}. Use canonical {preset.target_hz:g} Hz "
464
+ "reconstructed increments. codec.first_order_template(batch_size) explicitly "
465
+ "creates zeros; use zeros only if the preceding increment is known to be zero."
466
+ )
467
+
468
+ def final_first_order(
469
+ self,
470
+ streams: Sequence[Sequence[int]],
471
+ *,
472
+ preset: str,
473
+ previous_first_order: Mapping[str, np.ndarray],
474
+ executed_steps: int | None = None,
475
+ ) -> dict[str, np.ndarray]:
476
+ "Return component ``[B,D_active]`` boundaries at the canonical clock."
477
+ preset_config = self.preset(preset)
478
+ if previous_first_order is None:
479
+ raise ValueError("final_first_order requires previous_first_order")
480
+ self._validate_boundary_keys(previous_first_order, preset_config)
481
+ segments = self._segments(preset_config)
482
+ steps = self._batch_steps(streams, segments)
483
+ split = [self._split_stream(stream, segments, steps) for stream in streams]
484
+ output = {}
485
+ for index, segment in enumerate(segments):
486
+ processor = segment.profile.processor
487
+ if not processor.requires_previous_first_order:
488
+ continue
489
+ output[segment.component.name] = processor.final_first_order(
490
+ [row[index] for row in split],
491
+ previous_first_order=previous_first_order.get(segment.component.name),
492
+ active_dims=segment.component.active_dims,
493
+ executed_steps=executed_steps,
494
+ )
495
+ return output
496
+
497
+ def _split_stream(
498
+ self, tokens: Sequence[int], segments: Sequence[_Segment], steps: int
499
+ ) -> list[list[int]]:
500
+ cursor = 0
501
+ output = []
502
+ for segment in segments:
503
+ grammar = segment.profile.processor.grammar(
504
+ steps, active_dims=segment.component.active_dims
505
+ )
506
+ state = grammar._initial_state()
507
+ local: list[int] = []
508
+ while cursor < len(tokens) and not grammar._state_is_complete(state):
509
+ raw_token = tokens[cursor]
510
+ if isinstance(raw_token, bool) or not isinstance(
511
+ raw_token, (int, np.integer)
512
+ ):
513
+ raise ValueError(f"tokens[{cursor}] must be an integer")
514
+ token = int(raw_token) - segment.profile.spec.token_offset
515
+ if not 0 <= token < segment.profile.processor.vocab_size:
516
+ raise ValueError(
517
+ "token belongs to the wrong routed profile namespace"
518
+ )
519
+ next_state = grammar._advance(state, token)
520
+ if next_state is None:
521
+ raise ValueError(
522
+ f"Action token stream has invalid coverage for component {segment.component.name!r}. Constrain generation with codec.grammar(H, fps=...).next_token_mask(prefix)."
523
+ )
524
+ state = next_state
525
+ local.append(token)
526
+ cursor += 1
527
+ if not grammar._state_is_complete(state):
528
+ raise ValueError(
529
+ f"Incomplete action token stream for component {segment.component.name!r}. Check codec.grammar(H, fps=...).is_complete(tokens); discard or regenerate this row."
530
+ )
531
+ output.append(local)
532
+ if cursor != len(tokens):
533
+ raise ValueError(
534
+ "Action token stream has extra tokens after complete coverage; remove model prompt/EOS/padding before codec.decode()."
535
+ )
536
+ return output
537
+
538
+ def _token_cell_count(self, raw_token: object, *, index: int) -> int:
539
+
540
+ if isinstance(raw_token, bool) or not isinstance(raw_token, (int, np.integer)):
541
+ raise ValueError(f"tokens[{index}] must be an integer")
542
+ token = int(raw_token)
543
+ for runtime in self.profiles.values():
544
+ local = token - runtime.spec.token_offset
545
+ if 0 <= local < runtime.processor.vocab_size:
546
+ return int(runtime.processor.serializer.token_cell_counts[local])
547
+ raise ValueError(f"tokens[{index}] is outside the routed profile vocabularies")
548
+
549
+ def _decoded_steps(
550
+ self, tokens: Sequence[int], segments: Sequence[_Segment]
551
+ ) -> int:
552
+
553
+ if not tokens:
554
+ raise ValueError("token stream must not be empty")
555
+ cells_per_step = sum(len(segment.component.active_dims) for segment in segments)
556
+ represented_cells = sum(
557
+ self._token_cell_count(token, index=index)
558
+ for index, token in enumerate(tokens)
559
+ )
560
+ steps, remainder = divmod(represented_cells, cells_per_step)
561
+ if steps < 1 or remainder:
562
+ raise ValueError(
563
+ "Incomplete action token stream: "
564
+ f"{cells_per_step - remainder} more coordinate values are needed to finish a step. "
565
+ "Constrain generation with codec.grammar(H, fps=...).next_token_mask(prefix) "
566
+ "and check is_complete(tokens) for the intended H before decoding. "
567
+ "Use codec.batch_decode(..., errors='return') to isolate invalid rows; "
568
+ "do not pad truncated tokens with zeros."
569
+ )
570
+ return steps
571
+
572
+ def _batch_steps(
573
+ self,
574
+ streams: Sequence[Sequence[int]],
575
+ segments: Sequence[_Segment],
576
+ ) -> int:
577
+
578
+ if not streams:
579
+ raise ValueError("streams must not be empty")
580
+ lengths = [self._decoded_steps(stream, segments) for stream in streams]
581
+ if len(set(lengths)) != 1:
582
+ raise ValueError(
583
+ f"all token streams must decode to the same length, got {lengths}; use codec.batch_decode(...) for independent variable-length outputs"
584
+ )
585
+ return lengths[0]
586
+
587
+ def decode_batch(
588
+ self,
589
+ streams: Sequence[Sequence[int]],
590
+ *,
591
+ preset: str,
592
+ fps: float | None = None,
593
+ current_state: np.ndarray | None = None,
594
+ previous_first_order: Mapping[str, np.ndarray] | None = None,
595
+ ) -> dict[str, np.ndarray]:
596
+ "Decode rows to deltas, or absolute components with ``current_state``."
597
+ preset_config = self.preset(preset)
598
+ self._validate_boundary_keys(previous_first_order, preset_config)
599
+ segments = self._segments(preset_config)
600
+ internal_steps = self._batch_steps(streams, segments)
601
+ states = _current_state_rows(
602
+ current_state, count=len(streams), input_dim=preset_config.input_dim
603
+ )
604
+ output_fps = preset_config.source_hz if fps is None else fps
605
+ output_steps = resampled_horizon(
606
+ internal_steps,
607
+ source_hz=preset_config.target_hz,
608
+ target_hz=output_fps,
609
+ )
610
+ output: dict[str, list[np.ndarray]] = {
611
+ component.name: [] for component in preset_config.components
612
+ }
613
+ split_streams = [
614
+ self._split_stream(stream, segments, internal_steps) for stream in streams
615
+ ]
616
+ for segment_index, segment in enumerate(segments):
617
+ decoded_batch = segment.profile.processor.decode_batch(
618
+ [split[segment_index] for split in split_streams],
619
+ active_dims=segment.component.active_dims,
620
+ previous_first_order=None
621
+ if previous_first_order is None
622
+ else previous_first_order.get(segment.component.name),
623
+ )
624
+ for decoded, state in zip(decoded_batch, states, strict=True):
625
+ restored = resample_canonical_trajectory(
626
+ ActiveTrajectory(decoded, segment.component.active_dims),
627
+ segment.component,
628
+ source_hz=preset_config.target_hz,
629
+ target_hz=output_fps,
630
+ target_length=output_steps,
631
+ method=preset_config.interpolation,
632
+ )
633
+ output[segment.component.name].append(
634
+ externalize_canonical_trajectory(
635
+ restored, segment.component, current_state=state
636
+ )
637
+ )
638
+ return {name: np.stack(values) for name, values in output.items()}
639
+
640
+ def grammar(self, *, preset: str, horizon: int) -> RoutedActionCodec2Grammar:
641
+ """Build the global constrained grammar for one preset and target horizon."""
642
+ horizon = _integer(horizon, "horizon", positive=True)
643
+ self.preset(preset)
644
+ key = (preset, horizon)
645
+ grammar = self._grammar_cache.get(key)
646
+ if grammar is None:
647
+ grammar = RoutedActionCodec2Grammar(self, preset=preset, horizon=horizon)
648
+ self._grammar_cache[key] = grammar
649
+ return grammar
650
+
651
+ @property
652
+ def new_action_tokens(self) -> list[str]:
653
+ """Stable physical token names, including reserved profile budget slots."""
654
+ return [
655
+ f"<action_{name}_{index:04d}>"
656
+ for name, runtime in self.profiles.items()
657
+ for index in range(runtime.spec.token_budget)
658
+ ]
659
+
660
+ def save_pretrained(
661
+ self, directory: str | Path, *, include_code: bool = True
662
+ ) -> list[str]:
663
+ """Save one manifest plus isolated physical processor subdirectories."""
664
+ root = Path(directory)
665
+ files = []
666
+ for runtime in self.profiles.values():
667
+ files.extend(
668
+ runtime.processor.save_pretrained(root / "profiles" / runtime.spec.name)
669
+ )
670
+ manifest = {
671
+ "model_type": self.model_type,
672
+ "config_version": ROUTER_CONFIG_VERSION,
673
+ "profiles": [runtime.spec.to_dict() for runtime in self.profiles.values()],
674
+ "presets": [preset.to_dict() for preset in self.action_spaces.values()],
675
+ }
676
+ path = root / ROUTER_CONFIG_NAME
677
+ path.parent.mkdir(parents=True, exist_ok=True)
678
+ path.write_text(yaml.safe_dump(manifest, sort_keys=False), encoding="utf-8")
679
+ metadata = save_huggingface_metadata(
680
+ root,
681
+ model_type=self.model_type,
682
+ processor_class=type(self).__name__,
683
+ **(
684
+ {"auto_map": {"AutoProcessor": "processing_actioncodec2.ActionCodec2"}}
685
+ if include_code
686
+ else {}
687
+ ),
688
+ )
689
+ card = save_model_card(root, str(self))
690
+ if include_code:
691
+ from ..integration.exporting import ArtifactExporter
692
+
693
+ files.extend(
694
+ ArtifactExporter(root, source_directory=self._source_directory).export()
695
+ )
696
+ return [str(path), str(card), *map(str, metadata), *files]
697
+
698
+ @classmethod
699
+ def from_pretrained(
700
+ cls,
701
+ pretrained_model_name_or_path: str | Path,
702
+ **kwargs: object,
703
+ ) -> ActionCodec2Router | ActionCodec2:
704
+ "Load a router, or bind its public processor when given ``action_space``."
705
+ if "action_space" in kwargs:
706
+ from .processing_actioncodec2 import ActionCodec2
707
+
708
+ return ActionCodec2.from_pretrained(pretrained_model_name_or_path, **kwargs)
709
+ root = resolve_pretrained_directory(
710
+ pretrained_model_name_or_path,
711
+ allow_patterns=ROUTER_ALLOW_PATTERNS,
712
+ **kwargs,
713
+ )
714
+ path = root / ROUTER_CONFIG_NAME
715
+ payload = _mapping(
716
+ yaml.load(path.read_text(encoding="utf-8"), Loader=_UniqueKeyLoader),
717
+ "router",
718
+ )
719
+ _reject_unknown(
720
+ payload, {"model_type", "config_version", "profiles", "presets"}, "router"
721
+ )
722
+ if payload.get("model_type") != ROUTER_MODEL_TYPE:
723
+ raise ConfigError(f"router.model_type must be {ROUTER_MODEL_TYPE!r}")
724
+ if payload.get("config_version") != ROUTER_CONFIG_VERSION:
725
+ raise ConfigError(
726
+ f"unsupported router config_version {payload.get('config_version')!r}"
727
+ )
728
+ raw_profiles = payload.get("profiles")
729
+ raw_presets = payload.get("presets")
730
+ if not isinstance(raw_profiles, list) or not isinstance(raw_presets, list):
731
+ raise ConfigError("router.profiles and router.presets must be lists")
732
+ profiles = []
733
+ for index, raw in enumerate(raw_profiles):
734
+ spec = ProfileSpec.from_dict(raw, path=f"router.profiles[{index}]")
735
+ processor = ActionCodec2Processor.from_pretrained(
736
+ root / "profiles" / spec.name
737
+ )
738
+ profiles.append((spec, processor))
739
+ presets = [
740
+ PresetConfig.from_dict(raw, path=f"router.presets[{index}]")
741
+ for index, raw in enumerate(raw_presets)
742
+ ]
743
+ router = cls(profiles, presets)
744
+ router._source_directory = root
745
+ return router
runtime/tokenization/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Internal tokenization components for ActionCodec2."""
runtime/tokenization/bpe.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Strict ``bpe.json`` I/O for both tokenization schemes."
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Mapping, Sequence
7
+ from numbers import Integral
8
+ from pathlib import Path
9
+
10
+ import numpy as np
11
+
12
+ from ..common._validation import active_dimensions
13
+ from .bpe_vocabulary import BPEVocabulary
14
+ from ..physical.configuration import ActionCodec2Config, ConfigError
15
+ from .setbpe_spatial import SpatialVocabulary
16
+ from .setbpe_temporal import TemporalVocabulary
17
+
18
+ BPE_NAME = "bpe.json"
19
+
20
+
21
+ def _integer(value: object, path: str, *, positive: bool = False) -> int:
22
+ if isinstance(value, bool) or not isinstance(value, int):
23
+ raise ConfigError(f"{path} must be an integer")
24
+ minimum = 1 if positive else 0
25
+ if value < minimum:
26
+ qualifier = "positive" if positive else "non-negative"
27
+ raise ConfigError(f"{path} must be {qualifier}")
28
+ return int(value)
29
+
30
+
31
+ def _rules(payload: object, width: int, path: str) -> tuple[tuple[int, ...], ...]:
32
+
33
+ if not isinstance(payload, Sequence) or isinstance(
34
+ payload, (str, bytes, bytearray)
35
+ ):
36
+ raise ConfigError(f"{path} must be a list")
37
+ output = []
38
+ for rank, raw_rule in enumerate(payload):
39
+ rule_path = f"{path}[{rank}]"
40
+ if not isinstance(raw_rule, Sequence) or isinstance(
41
+ raw_rule, (str, bytes, bytearray)
42
+ ):
43
+ raise ConfigError(f"{rule_path} must be a list of {width} integers")
44
+ if len(raw_rule) != width:
45
+ raise ConfigError(f"{rule_path} must contain exactly {width} integers")
46
+ output.append(
47
+ tuple(
48
+ _integer(value, f"{rule_path}[{index}]")
49
+ for index, value in enumerate(raw_rule)
50
+ )
51
+ )
52
+ return tuple(output)
53
+
54
+
55
+ def _unique_json_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
56
+ output: dict[str, object] = {}
57
+ for key, value in pairs:
58
+ if key in output:
59
+ raise ConfigError(f"{BPE_NAME} contains duplicate field {key!r}")
60
+ output[key] = value
61
+ return output
62
+
63
+
64
+ def _validate_rule_references(
65
+ *,
66
+ atom_count: int,
67
+ lookahead: int,
68
+ spatial_merges: tuple[tuple[int, int], ...],
69
+ temporal_merges: tuple[tuple[int, int, int], ...],
70
+ ) -> None:
71
+
72
+ for rank, (left, right) in enumerate(spatial_merges):
73
+ child = atom_count + rank
74
+ if left >= child or right >= child:
75
+ raise ConfigError(
76
+ f"spatial_merges[{rank}] must reference already-created tokens"
77
+ )
78
+ spatial_size = atom_count + len(spatial_merges)
79
+ for rank, (left, right, offset) in enumerate(temporal_merges):
80
+ child = spatial_size + rank
81
+ if left >= child or right >= child:
82
+ raise ConfigError(
83
+ f"temporal_merges[{rank}] must reference already-created tokens"
84
+ )
85
+ if not 1 <= offset < lookahead:
86
+ raise ConfigError(
87
+ f"temporal_merges[{rank}] offset must be positive and smaller than lookahead"
88
+ )
89
+
90
+
91
+ def _read_payload(directory: str | Path) -> dict[str, object]:
92
+
93
+ path = Path(directory) / BPE_NAME
94
+ try:
95
+ return json.loads(
96
+ path.read_text(encoding="utf-8"), object_pairs_hook=_unique_json_object
97
+ )
98
+ except OSError:
99
+ raise
100
+ except json.JSONDecodeError as error:
101
+ raise ConfigError(f"cannot parse {path}: {error}") from error
102
+
103
+
104
+ def _dense_cells(
105
+ cells: np.ndarray,
106
+ bins: Sequence[int],
107
+ active_dims: Sequence[int] | None,
108
+ ) -> tuple[np.ndarray, tuple[int, ...] | None]:
109
+
110
+ raw = np.asarray(cells)
111
+ if raw.dtype.kind not in {"i", "u"}:
112
+ raise ValueError("cells must contain integer indices")
113
+ values = np.asarray(raw, dtype=np.int64)
114
+ if values.ndim != 2 or len(values) < 1 or values.shape[1] < 1:
115
+ raise ValueError("cells must have non-empty shape [T, D_active]")
116
+ if active_dims is None:
117
+ if values.shape[1] > len(bins):
118
+ raise ValueError(f"cells width exceeds codebook dimension {len(bins)}")
119
+ if values.shape[1] < len(bins):
120
+ dimensions = tuple(range(values.shape[1]))
121
+ dense = np.full((len(values), len(bins)), -1, dtype=np.int64)
122
+ dense[:, : values.shape[1]] = values
123
+ values = dense
124
+ else:
125
+ dimensions = None
126
+ else:
127
+ dimensions = active_dimensions(active_dims, len(bins))
128
+ if len(dimensions) != values.shape[1]:
129
+ raise ValueError(
130
+ "active_dims must have one entry per compact cell coordinate"
131
+ )
132
+ if np.any(values < 0):
133
+ raise ValueError("compact cells must not contain -1")
134
+ dense = np.full((len(values), len(bins)), -1, dtype=np.int64)
135
+ dense[:, dimensions] = values
136
+ values = dense
137
+ counts = np.asarray(tuple(int(value) for value in bins), dtype=np.int64)
138
+ if np.any(values < -1) or np.any(values >= counts[None, :]):
139
+ raise ValueError("cells contain an invalid active or absent index")
140
+ return values, dimensions
141
+
142
+
143
+ class SetBPESerializer:
144
+ "Lossless mapping between ``[T,D]`` physical cells and token IDs."
145
+
146
+ scheme = "set_bpe"
147
+
148
+ def __init__(
149
+ self,
150
+ *,
151
+ bins: Sequence[int],
152
+ lookahead: int,
153
+ spatial_merges: Sequence[Sequence[int]],
154
+ temporal_merges: Sequence[Sequence[int]],
155
+ ) -> None:
156
+ self.lookahead = _integer(lookahead, "lookahead", positive=True)
157
+ self.bins = tuple(
158
+ _integer(value, f"bins[{index}]", positive=True)
159
+ for index, value in enumerate(bins)
160
+ )
161
+ if not self.bins:
162
+ raise ConfigError("bins must contain at least one cell slot")
163
+ self.spatial_merges = _rules(spatial_merges, 2, "spatial_merges")
164
+ self.temporal_merges = _rules(temporal_merges, 3, "temporal_merges")
165
+ _validate_rule_references(
166
+ atom_count=sum(self.bins),
167
+ lookahead=self.lookahead,
168
+ spatial_merges=self.spatial_merges,
169
+ temporal_merges=self.temporal_merges,
170
+ )
171
+ try:
172
+ self.spatial = SpatialVocabulary(self.bins, self.spatial_merges)
173
+ self.vocabulary = TemporalVocabulary(
174
+ self.spatial, self.lookahead, self.temporal_merges
175
+ )
176
+ except ValueError as error:
177
+ raise ConfigError(str(error)) from error
178
+ self.patterns = self.vocabulary.patterns
179
+ self.root = self.vocabulary.root
180
+ self.vocab_size = self.vocabulary.vocab_size
181
+ self.token_cell_counts = np.asarray(
182
+ [
183
+ sum(
184
+ int(self.spatial.support[spatial_token]).bit_count()
185
+ for _, spatial_token in self.patterns[token]
186
+ )
187
+ for token in range(self.vocab_size)
188
+ ],
189
+ dtype=np.int64,
190
+ )
191
+
192
+ def encode(
193
+ self,
194
+ cells: np.ndarray,
195
+ *,
196
+ active_dims: Sequence[int] | None = None,
197
+ ) -> list[int]:
198
+ "Serialize one non-empty trajectory without emitting absent slots."
199
+ values, _ = _dense_cells(cells, self.bins, active_dims)
200
+ return self.vocabulary.encode(values)
201
+
202
+ def decode(
203
+ self,
204
+ tokens: Sequence[int],
205
+ *,
206
+ active_dims: Sequence[int] | None = None,
207
+ ) -> np.ndarray:
208
+ """Recover dense cells, writing ``-1`` in semantically absent slots."""
209
+ if len(tokens) < 1:
210
+ raise ValueError("token stream must not be empty")
211
+ checked = []
212
+ for index, token in enumerate(tokens):
213
+ if isinstance(token, bool) or not isinstance(token, Integral):
214
+ raise ValueError(f"tokens[{index}] must be an integer")
215
+ value = int(token)
216
+ if not 0 <= value < self.vocab_size:
217
+ raise ValueError(f"tokens[{index}] is outside the vocabulary")
218
+ checked.append(value)
219
+ return self.vocabulary.decode(checked, active_dims=active_dims)
220
+
221
+ def to_dict(self) -> dict[str, object]:
222
+ """Return the intentionally minimal, path-independent BPE artifact."""
223
+ return {
224
+ "spatial_merges": [list(rule) for rule in self.spatial_merges],
225
+ "temporal_merges": [list(rule) for rule in self.temporal_merges],
226
+ }
227
+
228
+ @classmethod
229
+ def from_dict(
230
+ cls,
231
+ payload: object,
232
+ *,
233
+ bins: Sequence[int],
234
+ lookahead: int,
235
+ ) -> SetBPESerializer:
236
+ if not isinstance(payload, Mapping) or not all(
237
+ isinstance(key, str) for key in payload
238
+ ):
239
+ raise ConfigError(f"{BPE_NAME} must contain one JSON object")
240
+ allowed = {"spatial_merges", "temporal_merges"}
241
+ unknown = sorted(set(payload) - allowed)
242
+ if unknown:
243
+ raise ConfigError(
244
+ f"{BPE_NAME} contains unknown field(s): {', '.join(unknown)}"
245
+ )
246
+ missing = sorted(allowed - set(payload))
247
+ if missing:
248
+ raise ConfigError(f"{BPE_NAME} is missing field(s): {', '.join(missing)}")
249
+ return cls(
250
+ bins=bins,
251
+ lookahead=lookahead,
252
+ spatial_merges=payload["spatial_merges"],
253
+ temporal_merges=payload["temporal_merges"],
254
+ )
255
+
256
+ @classmethod
257
+ def from_pretrained(
258
+ cls,
259
+ directory: str | Path,
260
+ *,
261
+ bins: Sequence[int],
262
+ lookahead: int,
263
+ ) -> SetBPESerializer:
264
+ return cls.from_dict(_read_payload(directory), bins=bins, lookahead=lookahead)
265
+
266
+ def save_pretrained(self, directory: str | Path) -> Path:
267
+ """Write ``bpe.json`` and return its path."""
268
+ path = Path(directory) / BPE_NAME
269
+ path.parent.mkdir(parents=True, exist_ok=True)
270
+ path.write_text(json.dumps(self.to_dict(), indent=2) + "\n", encoding="utf-8")
271
+ return path
272
+
273
+
274
+ class BPESerializer:
275
+ "Lossless mapping between ``[T,D]`` physical cells and plain-BPE ids."
276
+
277
+ scheme = "bpe"
278
+
279
+ def __init__(
280
+ self,
281
+ *,
282
+ bins: Sequence[int],
283
+ lookahead: int,
284
+ merges: Sequence[Sequence[int]],
285
+ ) -> None:
286
+ self.lookahead = _integer(lookahead, "lookahead", positive=True)
287
+ self.bins = tuple(
288
+ _integer(value, f"bins[{index}]", positive=True)
289
+ for index, value in enumerate(bins)
290
+ )
291
+ if not self.bins:
292
+ raise ConfigError("bins must contain at least one cell slot")
293
+ self.merges = _rules(merges, 2, "merges")
294
+ atom_count = sum(self.bins)
295
+ for rank, (left, right) in enumerate(self.merges):
296
+ child = atom_count + rank
297
+ if left >= child or right >= child:
298
+ raise ConfigError(
299
+ f"merges[{rank}] must reference already-created tokens"
300
+ )
301
+ try:
302
+ self.vocabulary = BPEVocabulary(self.bins, self.merges)
303
+ except ValueError as error:
304
+ raise ConfigError(str(error)) from error
305
+ self.max_token_cells = self.lookahead * len(self.bins)
306
+ self.token_cell_counts = self.vocabulary.token_cells
307
+ oversized = int(np.count_nonzero(self.token_cell_counts > self.max_token_cells))
308
+ if oversized:
309
+ raise ConfigError(
310
+ f"{oversized} merge rule(s) cover more than the {self.max_token_cells} "
311
+ "primitive cells allowed by lookahead"
312
+ )
313
+ self.vocab_size = self.vocabulary.vocab_size
314
+
315
+ def encode(
316
+ self,
317
+ cells: np.ndarray,
318
+ *,
319
+ active_dims: Sequence[int] | None = None,
320
+ ) -> list[int]:
321
+ "Serialize one non-empty trajectory without emitting absent slots."
322
+ values, _ = _dense_cells(cells, self.bins, active_dims)
323
+ return self.vocabulary.encode(values)
324
+
325
+ def decode(
326
+ self,
327
+ tokens: Sequence[int],
328
+ *,
329
+ active_dims: Sequence[int] | None = None,
330
+ ) -> np.ndarray:
331
+ """Recover dense cells, writing ``-1`` in semantically absent slots."""
332
+ if len(tokens) < 1:
333
+ raise ValueError("token stream must not be empty")
334
+ checked = []
335
+ for index, token in enumerate(tokens):
336
+ if isinstance(token, bool) or not isinstance(token, Integral):
337
+ raise ValueError(f"tokens[{index}] must be an integer")
338
+ value = int(token)
339
+ if not 0 <= value < self.vocab_size:
340
+ raise ValueError(f"tokens[{index}] is outside the vocabulary")
341
+ checked.append(value)
342
+ return self.vocabulary.decode(checked, active_dims=active_dims)
343
+
344
+ def to_dict(self) -> dict[str, object]:
345
+ """Return the intentionally minimal, path-independent BPE artifact."""
346
+ return {"merges": [list(rule) for rule in self.merges]}
347
+
348
+ @classmethod
349
+ def from_dict(
350
+ cls,
351
+ payload: object,
352
+ *,
353
+ bins: Sequence[int],
354
+ lookahead: int,
355
+ ) -> BPESerializer:
356
+ if not isinstance(payload, Mapping) or not all(
357
+ isinstance(key, str) for key in payload
358
+ ):
359
+ raise ConfigError(f"{BPE_NAME} must contain one JSON object")
360
+ unknown = sorted(set(payload) - {"merges"})
361
+ if unknown:
362
+ raise ConfigError(
363
+ f"{BPE_NAME} contains unknown field(s): {', '.join(unknown)}"
364
+ )
365
+ if "merges" not in payload:
366
+ raise ConfigError(f"{BPE_NAME} is missing field(s): merges")
367
+ return cls(bins=bins, lookahead=lookahead, merges=payload["merges"])
368
+
369
+ @classmethod
370
+ def from_pretrained(
371
+ cls,
372
+ directory: str | Path,
373
+ *,
374
+ bins: Sequence[int],
375
+ lookahead: int,
376
+ ) -> BPESerializer:
377
+ return cls.from_dict(_read_payload(directory), bins=bins, lookahead=lookahead)
378
+
379
+ def save_pretrained(self, directory: str | Path) -> Path:
380
+ """Write ``bpe.json`` and return its path."""
381
+ path = Path(directory) / BPE_NAME
382
+ path.parent.mkdir(parents=True, exist_ok=True)
383
+ path.write_text(json.dumps(self.to_dict(), indent=2) + "\n", encoding="utf-8")
384
+ return path
385
+
386
+
387
+ SERIALIZERS: dict[str, type[SetBPESerializer] | type[BPESerializer]] = {
388
+ SetBPESerializer.scheme: SetBPESerializer,
389
+ BPESerializer.scheme: BPESerializer,
390
+ }
391
+
392
+
393
+ def serializer_class(
394
+ config: ActionCodec2Config,
395
+ ) -> type[SetBPESerializer] | type[BPESerializer]:
396
+ """Return the serializer the configured tokenization scheme requires."""
397
+ return SERIALIZERS[config.set_bpe.scheme]
runtime/tokenization/bpe_training.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Plain-BPE fitting implementations."
2
+
3
+ from __future__ import annotations
4
+
5
+ import heapq
6
+ from collections import defaultdict
7
+ from collections.abc import Iterable, Iterator, Sequence
8
+
9
+ import numpy as np
10
+
11
+ from .bpe_vocabulary import BPEVocabulary
12
+ from ..backend.native_backend import require_native, resolve_backend
13
+
14
+ __all__ = ["train_bpe", "cell_chunks"]
15
+
16
+
17
+ def cell_chunks(
18
+ episodes: Iterable[np.ndarray],
19
+ *,
20
+ max_chunk_rows: int = 1_000_000,
21
+ ) -> Iterator[tuple[np.ndarray, np.ndarray]]:
22
+ "Yield ``(cells, episode_offsets)`` blocks for the native trainer."
23
+ chunks = getattr(episodes, "chunks", None)
24
+ offsets = getattr(episodes, "offsets", None)
25
+ if getattr(episodes, "_actioncodec2_validated", False) and chunks is not None:
26
+ for index, (codes, bounds) in enumerate(zip(chunks, offsets, strict=True)):
27
+ if codes is None:
28
+ raise RuntimeError("cell corpus chunk has already been consumed")
29
+ yield (
30
+ np.ascontiguousarray(codes),
31
+ np.ascontiguousarray(bounds, dtype=np.int64),
32
+ )
33
+ episodes.release_chunk(index)
34
+ return
35
+ pending: list[np.ndarray] = []
36
+ lengths: list[int] = []
37
+ rows = 0
38
+
39
+ def flush() -> tuple[np.ndarray, np.ndarray]:
40
+ block = np.concatenate(pending, axis=0)
41
+ bounds = np.concatenate(([0], np.cumsum(lengths, dtype=np.int64)))
42
+ pending.clear()
43
+ lengths.clear()
44
+ return np.ascontiguousarray(block), bounds
45
+
46
+ for episode in episodes:
47
+ values = np.asarray(episode)
48
+ if values.dtype.kind not in {"i", "u"}:
49
+ raise ValueError("episodes must contain integer cell indices")
50
+ if values.ndim != 2 or len(values) < 1:
51
+ raise ValueError("every episode must have non-empty shape [T, D]")
52
+ pending.append(values)
53
+ lengths.append(len(values))
54
+ rows += len(values)
55
+ if rows >= max_chunk_rows:
56
+ yield flush()
57
+ rows = 0
58
+ if pending:
59
+ yield flush()
60
+
61
+
62
+ def _flatten(
63
+ chunks: Iterable[tuple[np.ndarray, np.ndarray]], bins: Sequence[int]
64
+ ) -> tuple[list[list[int]], int]:
65
+
66
+ counts = np.asarray(tuple(int(value) for value in bins), dtype=np.int64)
67
+ offsets = np.concatenate(([0], np.cumsum(counts)))
68
+ streams: list[list[int]] = []
69
+ actions = 0
70
+ for codes, bounds in chunks:
71
+ values = np.asarray(codes, dtype=np.int64)
72
+ if values.ndim != 2 or values.shape[1] != len(counts):
73
+ raise ValueError("cells must have one column per bin count")
74
+ if np.any(values < -1) or np.any(values >= counts[None, :]):
75
+ raise ValueError("cells must contain -1 or an in-range cell index")
76
+ for start, stop in zip(bounds[:-1], bounds[1:], strict=True):
77
+ episode = values[int(start) : int(stop)]
78
+ if not len(episode):
79
+ raise ValueError("every episode must be non-empty")
80
+ support = episode >= 0
81
+ if np.any(support != support[:1]):
82
+ raise ValueError(
83
+ "active dimensions must remain constant within an episode"
84
+ )
85
+ present = np.flatnonzero(support[0])
86
+ if not len(present):
87
+ raise ValueError(
88
+ "every control step needs at least one active coordinate"
89
+ )
90
+ shifted = episode[:, present] + offsets[present][None, :]
91
+ streams.append(shifted.reshape(-1).tolist())
92
+ actions += len(episode)
93
+ if not streams:
94
+ raise ValueError("BPE training requires at least one complete trajectory")
95
+ return streams, actions
96
+
97
+
98
+ def _train_bpe_python(
99
+ chunks: Iterable[tuple[np.ndarray, np.ndarray]],
100
+ bins: Sequence[int],
101
+ *,
102
+ vocab_size: int,
103
+ max_token_cells: int,
104
+ progress: bool = False,
105
+ ) -> tuple[BPEVocabulary, dict[str, object]]:
106
+
107
+ atoms = int(sum(int(value) for value in bins))
108
+ if vocab_size < atoms:
109
+ raise ValueError(
110
+ f"vocab_size {vocab_size} is below the {atoms} primitive atoms"
111
+ )
112
+ if max_token_cells < 1:
113
+ raise ValueError("max_token_cells must be positive")
114
+ streams, actions = _flatten(chunks, bins)
115
+ initial_length = sum(len(stream) for stream in streams)
116
+ length = initial_length
117
+
118
+ counts: dict[tuple[int, int], int] = defaultdict(int)
119
+ carriers: dict[tuple[int, int], set[int]] = defaultdict(set)
120
+ for index, stream in enumerate(streams):
121
+ for pair in zip(stream[:-1], stream[1:], strict=True):
122
+ counts[pair] += 1
123
+ carriers[pair].add(index)
124
+ heap = [(-value, pair) for pair, value in counts.items()]
125
+ heapq.heapify(heap)
126
+
127
+ token_cells = [1] * atoms
128
+ merges: list[tuple[int, int]] = []
129
+ oversized: set[tuple[int, int]] = set()
130
+ observed_cells = 1
131
+ while len(merges) + atoms < vocab_size and heap:
132
+ negative, pair = heapq.heappop(heap)
133
+ frequency = counts.get(pair, 0)
134
+ if frequency <= 0 or frequency != -negative or pair in oversized:
135
+ continue
136
+ span = token_cells[pair[0]] + token_cells[pair[1]]
137
+ if span > max_token_cells:
138
+ oversized.add(pair)
139
+ continue
140
+ child = atoms + len(merges)
141
+ token_cells.append(span)
142
+ observed_cells = max(observed_cells, span)
143
+ touched: set[tuple[int, int]] = set()
144
+ for index in sorted(carriers.pop(pair, ())):
145
+ stream = streams[index]
146
+ output: list[int] = []
147
+ position = 0
148
+ while position < len(stream):
149
+ if (
150
+ position + 1 < len(stream)
151
+ and stream[position] == pair[0]
152
+ and stream[position + 1] == pair[1]
153
+ ):
154
+ if output:
155
+ counts[(output[-1], pair[0])] -= 1
156
+ touched.add((output[-1], pair[0]))
157
+ counts[(output[-1], child)] += 1
158
+ carriers[(output[-1], child)].add(index)
159
+ touched.add((output[-1], child))
160
+ if position + 2 < len(stream):
161
+ follower = stream[position + 2]
162
+ counts[(pair[1], follower)] -= 1
163
+ touched.add((pair[1], follower))
164
+ counts[(child, follower)] += 1
165
+ carriers[(child, follower)].add(index)
166
+ touched.add((child, follower))
167
+ counts[pair] -= 1
168
+ length -= 1
169
+ output.append(child)
170
+ position += 2
171
+ else:
172
+ output.append(stream[position])
173
+ position += 1
174
+ streams[index] = output
175
+ merges.append(pair)
176
+ for member in touched:
177
+ value = counts.get(member, 0)
178
+ if value > 0:
179
+ heapq.heappush(heap, (-value, member))
180
+ if progress and len(merges) % 256 == 0:
181
+ print(
182
+ f" bpe merge {len(merges):>5} tokens/action {length / actions:.4f}",
183
+ flush=True,
184
+ )
185
+
186
+ vocabulary = BPEVocabulary(tuple(int(value) for value in bins), tuple(merges))
187
+ report: dict[str, object] = {
188
+ "episodes": len(streams),
189
+ "actions": actions,
190
+ "atoms": atoms,
191
+ "merges": len(merges),
192
+ "vocab_size": vocabulary.vocab_size,
193
+ "cells": initial_length,
194
+ "tokens_per_action_initial": initial_length / actions,
195
+ "tokens_per_action": length / actions,
196
+ "compression": initial_length / length,
197
+ "max_token_cells": int(max_token_cells),
198
+ "max_token_cells_observed": observed_cells,
199
+ "oversized_pairs_skipped": len(oversized),
200
+ "trainer": "python_sparse_exact",
201
+ }
202
+ return vocabulary, report
203
+
204
+
205
+ def train_bpe(
206
+ episodes: Iterable[np.ndarray],
207
+ bins: Sequence[int],
208
+ *,
209
+ vocab_size: int,
210
+ max_token_cells: int,
211
+ backend: str = "auto",
212
+ progress: bool = False,
213
+ threads: int = 0,
214
+ ) -> tuple[BPEVocabulary, dict[str, object]]:
215
+ "Fit plain BPE on the row-major flattened primitive-cell stream."
216
+ if resolve_backend(backend) == "python":
217
+ return _train_bpe_python(
218
+ cell_chunks(episodes),
219
+ bins,
220
+ vocab_size=vocab_size,
221
+ max_token_cells=max_token_cells,
222
+ progress=progress,
223
+ )
224
+ merges, report = require_native().train_bpe(
225
+ cell_chunks(episodes),
226
+ [int(count) for count in bins],
227
+ int(vocab_size),
228
+ int(max_token_cells),
229
+ int(threads),
230
+ )
231
+ vocabulary = BPEVocabulary(
232
+ tuple(int(count) for count in bins),
233
+ tuple((int(left), int(right)) for left, right in merges),
234
+ )
235
+ return vocabulary, dict(report)
runtime/tokenization/bpe_vocabulary.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Runtime plain-BPE vocabulary over the row-major flattened cell stream."
2
+
3
+ from __future__ import annotations
4
+
5
+ import heapq
6
+ from collections.abc import Sequence
7
+ from dataclasses import dataclass
8
+
9
+ import numpy as np
10
+
11
+ from ..common._validation import active_dimensions
12
+
13
+ __all__ = ["BPEVocabulary"]
14
+
15
+
16
+ @dataclass
17
+ class BPEVocabulary:
18
+ """Primitive atoms plus ordered, adjacency-based merge rules."""
19
+
20
+ bins: tuple[int, ...]
21
+ merges: tuple[tuple[int, int], ...]
22
+
23
+ def __post_init__(self) -> None:
24
+ if not self.bins or any(count < 1 for count in self.bins):
25
+ raise ValueError("every dimension needs at least one bin")
26
+ self.offsets = np.concatenate(
27
+ ([0], np.cumsum(np.asarray(self.bins, dtype=np.int64)))
28
+ )
29
+ self.atom_count = int(self.offsets[-1])
30
+ self.vocab_size = self.atom_count + len(self.merges)
31
+ atom_dims = np.repeat(
32
+ np.arange(len(self.bins), dtype=np.int64),
33
+ np.asarray(self.bins, dtype=np.int64),
34
+ )
35
+
36
+
37
+ expansions: list[tuple[int, ...]] = [
38
+ (token,) for token in range(self.atom_count)
39
+ ]
40
+ for rank, (left, right) in enumerate(self.merges):
41
+ token = self.atom_count + rank
42
+ if not 0 <= left < token or not 0 <= right < token:
43
+ raise ValueError("a merge rule must reference already-created tokens")
44
+ expansions.append(expansions[left] + expansions[right])
45
+ self.expansions = tuple(expansions)
46
+ self.token_dims = tuple(
47
+ tuple(int(atom_dims[atom]) for atom in expansion)
48
+ for expansion in expansions
49
+ )
50
+ self.token_cells = np.asarray(
51
+ [len(expansion) for expansion in expansions], dtype=np.int64
52
+ )
53
+ self._rules = {
54
+ (int(left), int(right)): (self.atom_count + rank, rank)
55
+ for rank, (left, right) in enumerate(self.merges)
56
+ }
57
+
58
+ @property
59
+ def dimension(self) -> int:
60
+ return len(self.bins)
61
+
62
+ def atom(self, dimension: int, value: int) -> int:
63
+ return int(self.offsets[dimension]) + int(value)
64
+
65
+ def _flatten(self, values: np.ndarray, dimensions: tuple[int, ...]) -> list[int]:
66
+
67
+ counts = np.asarray(self.bins, dtype=np.int64)
68
+ if np.any(values < -1) or np.any(values >= counts[None, :]):
69
+ raise ValueError("cells contain an invalid active or absent index")
70
+ support = values >= 0
71
+ if np.any(support != support[:1]):
72
+ raise ValueError(
73
+ "active dimensions must remain constant within a trajectory"
74
+ )
75
+ present = tuple(int(dim) for dim in np.flatnonzero(support[0]))
76
+ if present != dimensions:
77
+ raise ValueError("cells do not activate exactly the requested dimensions")
78
+ if not present:
79
+ raise ValueError("every control step needs at least one active coordinate")
80
+ active = values[:, list(present)]
81
+ base = np.asarray([int(self.offsets[dim]) for dim in present], dtype=np.int64)
82
+ return (active + base[None, :]).reshape(-1).tolist()
83
+
84
+ def encode(self, cells: np.ndarray) -> list[int]:
85
+ "Apply the merges greedily in creation-rank order to one trajectory."
86
+ values = np.asarray(cells, dtype=np.int64)
87
+ if values.ndim != 2 or len(values) < 1 or values.shape[1] < 1:
88
+ raise ValueError("cells must have non-empty shape [T, D]")
89
+ dimensions = tuple(int(dim) for dim in np.flatnonzero(values[0] >= 0))
90
+ stream = self._flatten(values, dimensions)
91
+ count = len(stream)
92
+ previous = list(range(-1, count - 1))
93
+ following = list(range(1, count + 1))
94
+ following[-1] = -1
95
+ alive = [True] * count
96
+ rules = self._rules
97
+ heap: list[tuple[int, int]] = []
98
+ for index in range(count - 1):
99
+ rule = rules.get((stream[index], stream[index + 1]))
100
+ if rule is not None:
101
+ heap.append((rule[1], index))
102
+ heapq.heapify(heap)
103
+ while heap:
104
+ rank, index = heapq.heappop(heap)
105
+ if not alive[index]:
106
+ continue
107
+ follower = following[index]
108
+ if follower < 0 or not alive[follower]:
109
+ continue
110
+ rule = rules.get((stream[index], stream[follower]))
111
+ if rule is None or rule[1] != rank:
112
+ continue
113
+ stream[index] = rule[0]
114
+ alive[follower] = False
115
+ after = following[follower]
116
+ following[index] = after
117
+ if after >= 0:
118
+ previous[after] = index
119
+ successor = rules.get((stream[index], stream[after]))
120
+ if successor is not None:
121
+ heapq.heappush(heap, (successor[1], index))
122
+ before = previous[index]
123
+ if before >= 0:
124
+ predecessor = rules.get((stream[before], stream[index]))
125
+ if predecessor is not None:
126
+ heapq.heappush(heap, (predecessor[1], before))
127
+ return [stream[index] for index in range(count) if alive[index]]
128
+
129
+ def decode(
130
+ self,
131
+ tokens: Sequence[int],
132
+ *,
133
+ active_dims: Sequence[int] | None = None,
134
+ ) -> np.ndarray:
135
+ """Expand tokens to dense cells, writing ``-1`` in absent coordinates."""
136
+ dimensions = active_dimensions(active_dims, self.dimension)
137
+ atoms: list[int] = []
138
+ for index, raw_token in enumerate(tokens):
139
+ token = int(raw_token)
140
+ if not 0 <= token < self.vocab_size:
141
+ raise ValueError(f"tokens[{index}] is outside the vocabulary")
142
+ atoms.extend(self.expansions[token])
143
+ width = len(dimensions)
144
+ if not atoms or len(atoms) % width:
145
+ raise ValueError(
146
+ "token stream does not cover a whole number of routed control steps"
147
+ )
148
+ values = np.asarray(atoms, dtype=np.int64).reshape(-1, width)
149
+ starts = np.asarray(
150
+ [int(self.offsets[dim]) for dim in dimensions], dtype=np.int64
151
+ )
152
+ stops = np.asarray(
153
+ [int(self.offsets[dim + 1]) for dim in dimensions], dtype=np.int64
154
+ )
155
+ if np.any(values < starts[None, :]) or np.any(values >= stops[None, :]):
156
+ raise ValueError("token stream does not follow the routed dimension order")
157
+ cells = np.full((len(values), self.dimension), -1, dtype=np.int64)
158
+ cells[:, list(dimensions)] = values - starts[None, :]
159
+ return cells
runtime/tokenization/generation.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Explicit action-token constraints for Hugging Face generate()."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ from transformers import LogitsProcessor
7
+
8
+ from ..routing.cache import BoundedCache
9
+
10
+
11
+ class ActionCodec2LogitsProcessor(LogitsProcessor):
12
+ "Constrain generated action IDs, then allow EOS only at complete coverage."
13
+
14
+ def __init__(
15
+ self,
16
+ codec,
17
+ horizon,
18
+ *,
19
+ fps,
20
+ prompt_length,
21
+ eos_token_id,
22
+ token_offset=0,
23
+ pad_token_id=None,
24
+ ):
25
+ for name, value in {
26
+ "prompt_length": prompt_length,
27
+ "eos_token_id": eos_token_id,
28
+ "token_offset": token_offset,
29
+ }.items():
30
+ if type(value) is not int or value < 0:
31
+ raise ValueError(f"{name} must be a nonnegative integer")
32
+ if pad_token_id is not None and (
33
+ type(pad_token_id) is not int or pad_token_id < 0
34
+ ):
35
+ raise ValueError("pad_token_id must be a nonnegative integer")
36
+ self.grammar = codec.grammar(horizon, fps=fps)
37
+ self.prompt_length = prompt_length
38
+ self.eos_token_id = eos_token_id
39
+ self.pad_token_id = pad_token_id
40
+ self.token_offset = token_offset
41
+ stop = token_offset + self.grammar.vocab_size
42
+ if token_offset <= eos_token_id < stop:
43
+ raise ValueError("eos_token_id must be outside the action token interval")
44
+ if pad_token_id is not None and token_offset <= pad_token_id < stop:
45
+ raise ValueError("pad_token_id must be outside the action token interval")
46
+ self._mask_cache = BoundedCache(64)
47
+
48
+ def _state(self, row):
49
+ if self.eos_token_id in row:
50
+ end = row.index(self.eos_token_id)
51
+ if any(t not in (self.eos_token_id, self.pad_token_id) for t in row[end:]):
52
+ raise ValueError(
53
+ "only EOS/padding may follow completed action generation"
54
+ )
55
+ tokens = [t - self.token_offset for t in row[:end]]
56
+ if not self.grammar.is_complete(tokens):
57
+ raise ValueError("EOS occurred before action coverage was complete")
58
+ else:
59
+ tokens = [t - self.token_offset for t in row]
60
+ state = self.grammar._consume(tokens)
61
+ if state is None:
62
+ raise ValueError(
63
+ "generated prefix violates the action grammar; check prompt_length and token_offset"
64
+ )
65
+ return state
66
+
67
+ def _mask(self, state, scores):
68
+ key = (state, str(scores.device), scores.shape[-1])
69
+ mask = self._mask_cache.get(key)
70
+ if mask is None:
71
+ mask = torch.zeros(scores.shape[-1], device=scores.device, dtype=torch.bool)
72
+ index, local = state
73
+ if index == len(self.grammar.segments):
74
+ mask[self.eos_token_id] = True
75
+ else:
76
+ segment = self.grammar.segments[index]
77
+ allowed = self.grammar._grammars[index]._next_token_mask_from_state(
78
+ local
79
+ )
80
+ start = self.token_offset + segment.profile.spec.token_offset
81
+ mask[start : start + len(allowed)] = torch.as_tensor(
82
+ allowed, device=scores.device
83
+ )
84
+ self._mask_cache[key] = mask
85
+ return mask
86
+
87
+ def __call__(
88
+ self, input_ids: torch.LongTensor, scores: torch.FloatTensor
89
+ ) -> torch.FloatTensor:
90
+ if (
91
+ input_ids.ndim != 2
92
+ or scores.ndim != 2
93
+ or input_ids.shape[0] != scores.shape[0]
94
+ ):
95
+ raise ValueError(
96
+ "expected input_ids(B,L) and scores(B,V) with the same batch size"
97
+ )
98
+ if input_ids.shape[1] < self.prompt_length:
99
+ raise ValueError("prompt_length exceeds the supplied input length")
100
+ required = max(
101
+ self.token_offset + self.grammar.vocab_size,
102
+ self.eos_token_id + 1,
103
+ 0 if self.pad_token_id is None else self.pad_token_id + 1,
104
+ )
105
+ if scores.shape[1] < required:
106
+ raise ValueError(
107
+ "model vocabulary does not cover configured action/EOS/padding IDs"
108
+ )
109
+ rows = input_ids[:, self.prompt_length :].detach().cpu().tolist()
110
+ output = scores.clone()
111
+ groups = {}
112
+ for index, row in enumerate(rows):
113
+ groups.setdefault(self._state(row), []).append(index)
114
+ for state, indices in groups.items():
115
+ output[indices] = scores[indices].masked_fill(
116
+ ~self._mask(state, scores), -torch.inf
117
+ )
118
+ if not torch.isfinite(output).any(dim=1).all():
119
+ raise ValueError(
120
+ "all legal action logits were suppressed; check other generation processors"
121
+ )
122
+ return output
runtime/tokenization/grammar.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Constrained-generation grammars for the two ActionCodec2 BPE schemes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from numbers import Integral
7
+
8
+ import numpy as np
9
+
10
+ from ..common._validation import active_dimensions
11
+ from .bpe import BPESerializer, SetBPESerializer
12
+ from ..routing.cache import BoundedCache
13
+
14
+
15
+ class _PrefixGrammar:
16
+
17
+
18
+ def next_token_mask(self, tokens: Sequence[int]) -> np.ndarray:
19
+ """Return a boolean vocabulary mask for the next generated token."""
20
+ state = self._state(tokens)
21
+ if state is None:
22
+ raise ValueError("token prefix is not a legal ActionCodec2 stream")
23
+ return self._next_token_mask_from_state(state)
24
+
25
+ def is_complete(self, tokens: Sequence[int]) -> bool:
26
+ """Whether ``tokens`` is exactly one complete trajectory of this horizon."""
27
+ state = self._state(tokens)
28
+ return state is not None and self._state_is_complete(state)
29
+
30
+
31
+ class ActionCodec2Grammar(_PrefixGrammar):
32
+ "Prefix grammar for one exact, fixed-horizon rooted token stream."
33
+
34
+ def __init__(
35
+ self,
36
+ serializer: SetBPESerializer,
37
+ *,
38
+ horizon: int,
39
+ active_dims: Sequence[int] | None = None,
40
+ ) -> None:
41
+ if isinstance(horizon, bool) or not isinstance(horizon, int) or horizon < 1:
42
+ raise ValueError("horizon must be a positive integer")
43
+ self.horizon = horizon
44
+ self.vocab_size = serializer.vocab_size
45
+ dims = active_dimensions(active_dims, serializer.spatial.dimension)
46
+ self._full = sum(1 << dimension for dimension in dims)
47
+ self._root_anchor = np.asarray(
48
+ [
49
+ serializer.spatial.anchor[serializer.root[token]]
50
+ for token in range(self.vocab_size)
51
+ ],
52
+ dtype=np.int64,
53
+ )
54
+ self._patterns = tuple(
55
+ tuple(
56
+ (offset, int(serializer.spatial.support[spatial_token]))
57
+ for offset, spatial_token in serializer.patterns[token]
58
+ )
59
+ for token in range(self.vocab_size)
60
+ )
61
+ self._mask_cache = BoundedCache(64)
62
+ self._state_cache = BoundedCache(256)
63
+ self._state_cache[()] = self._initial_state()
64
+
65
+ def _advance(self, state: tuple[int, ...], token: int) -> tuple[int, ...] | None:
66
+ if not 0 <= token < self.vocab_size:
67
+ return None
68
+ coverage = list(state)
69
+ front = next(
70
+ (index for index, mask in enumerate(coverage) if mask != self._full),
71
+ self.horizon,
72
+ )
73
+ if front == self.horizon:
74
+ return None
75
+ uncovered = self._full & ~coverage[front]
76
+ expected_anchor = int(uncovered & -uncovered).bit_length() - 1
77
+ if int(self._root_anchor[token]) != expected_anchor:
78
+ return None
79
+ for offset, support in self._patterns[token]:
80
+ step = front + offset
81
+ if step >= self.horizon or coverage[step] & support:
82
+ return None
83
+ coverage[step] |= support
84
+ return tuple(coverage)
85
+
86
+ def _initial_state(self) -> tuple[int, ...]:
87
+
88
+ return (0,) * self.horizon
89
+
90
+ def _state_is_complete(self, state: tuple[int, ...]) -> bool:
91
+
92
+ return all(mask == self._full for mask in state)
93
+
94
+ def _state(self, tokens: Sequence[int]) -> tuple[int, ...] | None:
95
+ checked: list[int] = []
96
+ for raw_token in tokens:
97
+ if isinstance(raw_token, bool) or not isinstance(raw_token, Integral):
98
+ return None
99
+ checked.append(int(raw_token))
100
+ key = tuple(checked)
101
+ if not key:
102
+ return self._initial_state()
103
+ missing = object()
104
+ cached = self._state_cache.get(key, missing)
105
+ if cached is not missing:
106
+ return cached
107
+ parent = key[:-1]
108
+ state = self._state_cache.get(parent, missing)
109
+ if state is missing:
110
+ state = self._initial_state()
111
+ for token in parent:
112
+ state = self._advance(state, token)
113
+ if state is None:
114
+ break
115
+ self._state_cache[parent] = state
116
+ if state is not None:
117
+ state = self._advance(state, key[-1])
118
+ self._state_cache[key] = state
119
+ return state
120
+
121
+ def _next_token_mask_from_state(self, state: tuple[int, ...]) -> np.ndarray:
122
+
123
+ if self._state_is_complete(state):
124
+ return np.zeros(self.vocab_size, dtype=bool)
125
+ cached = self._mask_cache.get(state)
126
+ if cached is not None:
127
+ return cached.copy()
128
+ front = next(index for index, mask in enumerate(state) if mask != self._full)
129
+ uncovered = self._full & ~state[front]
130
+ expected_anchor = int(uncovered & -uncovered).bit_length() - 1
131
+ mask = self._root_anchor == expected_anchor
132
+ for token in np.flatnonzero(mask):
133
+ for offset, support in self._patterns[int(token)]:
134
+ step = front + offset
135
+ if step >= self.horizon or state[step] & support:
136
+ mask[token] = False
137
+ break
138
+ self._mask_cache[state] = mask
139
+ return mask.copy()
140
+
141
+
142
+ class ActionCodec2BPEGrammar(_PrefixGrammar):
143
+ "Prefix grammar for one exact, fixed-horizon plain-BPE token stream."
144
+
145
+ def __init__(
146
+ self,
147
+ serializer: BPESerializer,
148
+ *,
149
+ horizon: int,
150
+ active_dims: Sequence[int] | None = None,
151
+ ) -> None:
152
+ if isinstance(horizon, bool) or not isinstance(horizon, int) or horizon < 1:
153
+ raise ValueError("horizon must be a positive integer")
154
+ self.horizon = horizon
155
+ self.vocab_size = serializer.vocab_size
156
+ dims = active_dimensions(active_dims, serializer.vocabulary.dimension)
157
+ self._width = len(dims)
158
+ self._cells = horizon * self._width
159
+ self._token_cells = np.asarray(serializer.token_cell_counts, dtype=np.int64)
160
+ token_dims = serializer.vocabulary.token_dims
161
+ self._phase = np.zeros((self._width, self.vocab_size), dtype=bool)
162
+ for phase in range(self._width):
163
+ for token in range(self.vocab_size):
164
+ expansion = token_dims[token]
165
+ self._phase[phase, token] = all(
166
+ dimension == dims[(phase + offset) % self._width]
167
+ for offset, dimension in enumerate(expansion)
168
+ )
169
+ self._mask_cache = BoundedCache(64)
170
+
171
+ def _initial_state(self) -> tuple[int, ...]:
172
+ return (0,)
173
+
174
+ def _state_is_complete(self, state: tuple[int, ...]) -> bool:
175
+ return state[0] == self._cells
176
+
177
+ def _advance(self, state: tuple[int, ...], token: int) -> tuple[int, ...] | None:
178
+ if not 0 <= token < self.vocab_size:
179
+ return None
180
+ covered = state[0]
181
+ length = int(self._token_cells[token])
182
+ if covered + length > self._cells:
183
+ return None
184
+ if not self._phase[covered % self._width, token]:
185
+ return None
186
+ return (covered + length,)
187
+
188
+ def _state(self, tokens: Sequence[int]) -> tuple[int, ...] | None:
189
+ state: tuple[int, ...] | None = self._initial_state()
190
+ for raw_token in tokens:
191
+ if isinstance(raw_token, bool) or not isinstance(raw_token, Integral):
192
+ return None
193
+ assert state is not None
194
+ state = self._advance(state, int(raw_token))
195
+ if state is None:
196
+ return None
197
+ return state
198
+
199
+ def _next_token_mask_from_state(self, state: tuple[int, ...]) -> np.ndarray:
200
+ covered = state[0]
201
+ if covered == self._cells:
202
+ return np.zeros(self.vocab_size, dtype=bool)
203
+ cached = self._mask_cache.get(covered)
204
+ if cached is None:
205
+ cached = self._phase[covered % self._width] & (
206
+ self._token_cells <= self._cells - covered
207
+ )
208
+ self._mask_cache[covered] = cached
209
+ return cached.copy()
runtime/tokenization/setbpe_spatial.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runtime spatial Set-BPE vocabulary and lossless row serialization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import heapq
6
+ from collections.abc import Iterable, Sequence
7
+ from dataclasses import dataclass
8
+
9
+ import numpy as np
10
+
11
+ from ..common._validation import active_dimensions
12
+
13
+ __all__ = ["SpatialVocabulary", "aggregate_records"]
14
+
15
+
16
+ def aggregate_records(
17
+ codes: np.ndarray, bins: Sequence[int] | None = None
18
+ ) -> tuple[np.ndarray, np.ndarray]:
19
+ "Lossless micro-aggregation of identical quantized actions."
20
+ values = np.asarray(codes, dtype=np.int64)
21
+ if values.ndim != 2:
22
+ raise ValueError("codes must have shape [U, D]")
23
+ if bins is not None:
24
+ dimensions = tuple(int(count) for count in bins)
25
+ if len(dimensions) != values.shape[1]:
26
+ raise ValueError("bins must have one entry per code coordinate")
27
+ counts = np.asarray(dimensions, dtype=np.int64)
28
+ valid = values >= 0
29
+ if np.any(values < -1) or np.any(values >= counts[None, :]):
30
+ raise ValueError("codes must contain -1 or an in-range cell index")
31
+ cardinality = int(np.prod(np.asarray(dimensions, dtype=object)))
32
+ if np.all(valid) and cardinality <= np.iinfo(np.int64).max:
33
+ keys = np.ravel_multi_index(values.T, dimensions)
34
+ unique_keys, counts = np.unique(keys, return_counts=True)
35
+ unique = np.stack(np.unravel_index(unique_keys, dimensions), axis=1)
36
+ return unique.astype(np.int64, copy=False), counts.astype(np.int64)
37
+ unique, counts = np.unique(values, axis=0, return_counts=True)
38
+ return unique, counts.astype(np.int64)
39
+
40
+
41
+ @dataclass
42
+ class SpatialVocabulary:
43
+ """Atoms, merge rules and the decoder metadata they induce."""
44
+
45
+ bins: tuple[int, ...]
46
+ merges: tuple[tuple[int, int], ...]
47
+
48
+ def __post_init__(self) -> None:
49
+ if not self.bins or any(count < 1 for count in self.bins):
50
+ raise ValueError("every dimension needs at least one bin")
51
+ self.offsets = np.concatenate(
52
+ ([0], np.cumsum(np.asarray(self.bins, dtype=np.int64)))
53
+ )
54
+ self.atom_count = int(self.offsets[-1])
55
+ self.vocab_size = self.atom_count + len(self.merges)
56
+ dimension = len(self.bins)
57
+ support = np.zeros(self.vocab_size, dtype=np.int64)
58
+ expansion = np.full((self.vocab_size, dimension), -1, dtype=np.int64)
59
+ for index, count in enumerate(self.bins):
60
+ for value in range(count):
61
+ token = int(self.offsets[index]) + value
62
+ support[token] = 1 << index
63
+ expansion[token, index] = value
64
+ for rank, (left, right) in enumerate(self.merges):
65
+ token = self.atom_count + rank
66
+ if left >= token or right >= token:
67
+ raise ValueError("a merge rule must reference already-created tokens")
68
+ if support[left] & support[right]:
69
+ raise ValueError("a merge rule joins two overlapping supports")
70
+ support[token] = support[left] | support[right]
71
+ merged = np.maximum(expansion[left], expansion[right])
72
+ expansion[token] = merged
73
+ self.support = support
74
+ self.expansion = expansion
75
+ self.anchor = np.asarray(
76
+ [int(mask & -mask).bit_length() - 1 for mask in support], dtype=np.int64
77
+ )
78
+ self.full_support = (1 << dimension) - 1
79
+ self._rules: dict[tuple[int, int], tuple[int, int]] = {}
80
+ for rank, (left, right) in enumerate(self.merges):
81
+ key = (min(left, right), max(left, right))
82
+ self._rules[key] = (self.atom_count + rank, rank)
83
+
84
+ @property
85
+ def dimension(self) -> int:
86
+ return len(self.bins)
87
+
88
+ def atom(self, dimension: int, value: int) -> int:
89
+ return int(self.offsets[dimension]) + int(value)
90
+
91
+ def encode(
92
+ self,
93
+ code: Sequence[int],
94
+ *,
95
+ active_dims: Sequence[int] | None = None,
96
+ ) -> list[int]:
97
+ "Deterministic online Set-BPE encoding (Algorithm 8)."
98
+ compact = [int(value) for value in code]
99
+ if active_dims is None:
100
+ if len(compact) != self.dimension:
101
+ raise ValueError(
102
+ f"a dense quantized action must have {self.dimension} slots"
103
+ )
104
+ pairs = enumerate(compact)
105
+ else:
106
+ dimensions = active_dimensions(active_dims, self.dimension)
107
+ if len(dimensions) != len(compact):
108
+ raise ValueError(
109
+ "active_dims must have one entry per compact coordinate"
110
+ )
111
+ pairs = zip(dimensions, compact, strict=True)
112
+ tokens = set()
113
+ for index, value in pairs:
114
+ if value == -1:
115
+ if active_dims is not None:
116
+ raise ValueError("compact coordinates must not contain -1")
117
+ continue
118
+ if not 0 <= value < self.bins[index]:
119
+ raise ValueError(f"bin {value} is out of range for dimension {index}")
120
+ tokens.add(self.atom(index, value))
121
+ if not tokens:
122
+ raise ValueError(
123
+ "a quantized action must contain at least one active coordinate"
124
+ )
125
+ heap: list[tuple[int, int, int, int]] = []
126
+ ordered = sorted(tokens)
127
+ for position, left in enumerate(ordered):
128
+ for right in ordered[position + 1 :]:
129
+ rule = self._rules.get((left, right))
130
+ if rule is not None:
131
+ heapq.heappush(heap, (rule[1], rule[0], left, right))
132
+ while heap:
133
+ _, child, left, right = heapq.heappop(heap)
134
+ if left not in tokens or right not in tokens:
135
+ continue
136
+ tokens.discard(left)
137
+ tokens.discard(right)
138
+ for other in tokens:
139
+ key = (min(child, other), max(child, other))
140
+ rule = self._rules.get(key)
141
+ if rule is not None:
142
+ heapq.heappush(heap, (rule[1], rule[0], key[0], key[1]))
143
+ tokens.add(child)
144
+ return self.serialize(tokens)
145
+
146
+ def serialize(self, tokens: Iterable[int]) -> list[int]:
147
+ "Canonical order for autoregressive supervision."
148
+ return sorted(tokens, key=lambda token: (int(self.anchor[token]), int(token)))
149
+
150
+ def decode(
151
+ self,
152
+ tokens: Sequence[int],
153
+ *,
154
+ active_dims: Sequence[int] | None = None,
155
+ ) -> np.ndarray:
156
+ """Expand tokens to a dense row, using ``-1`` for absent coordinates."""
157
+ code = np.full(self.dimension, -1, dtype=np.int64)
158
+ covered = 0
159
+ for token in tokens:
160
+ value = int(token)
161
+ if not 0 <= value < self.vocab_size:
162
+ raise ValueError(f"token {value} is outside the vocabulary")
163
+ if covered & int(self.support[value]):
164
+ raise ValueError("token supports overlap; this is not a valid action")
165
+ covered |= int(self.support[value])
166
+ local = self.expansion[value]
167
+ code = np.where(local >= 0, local, code)
168
+ if active_dims is None:
169
+ expected = self.full_support
170
+ else:
171
+ dimensions = active_dimensions(active_dims, self.dimension)
172
+ expected = sum(1 << dimension for dimension in dimensions)
173
+ if covered != expected:
174
+ raise ValueError(
175
+ "token set does not exactly cover the requested active dimensions"
176
+ )
177
+ return code
runtime/tokenization/setbpe_spatial_training.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Spatial Set-BPE fitting implementations."
2
+
3
+ from __future__ import annotations
4
+
5
+ import heapq
6
+ from collections.abc import Sequence
7
+
8
+ import numpy as np
9
+
10
+ from ..backend.native_backend import require_native, resolve_backend
11
+ from .setbpe_spatial import SpatialVocabulary
12
+
13
+ __all__ = ["train_spatial"]
14
+
15
+
16
+ class _SparseSpatialTrainer:
17
+
18
+
19
+ def __init__(
20
+ self,
21
+ codes: np.ndarray,
22
+ weights: np.ndarray,
23
+ bins: Sequence[int],
24
+ ) -> None:
25
+ self.bins = tuple(int(value) for value in bins)
26
+ self.dimension = len(self.bins)
27
+ self.records = np.asarray(codes, dtype=np.int64)
28
+ self.weights = np.asarray(weights, dtype=np.int64)
29
+ if self.records.ndim != 2 or self.records.shape[1] != self.dimension:
30
+ raise ValueError("codes must have shape [U, D] matching the bin counts")
31
+ if self.weights.shape != (self.records.shape[0],):
32
+ raise ValueError("weights must have one entry per record")
33
+ counts = np.asarray(self.bins, dtype=np.int64)
34
+ if np.any(self.records < -1) or np.any(self.records >= counts[None, :]):
35
+ raise ValueError("codes must contain -1 or an in-range cell index")
36
+ if np.any(np.all(self.records < 0, axis=1)):
37
+ raise ValueError("every record must contain at least one active coordinate")
38
+ self.count = int(self.records.shape[0])
39
+ self.offsets = np.concatenate(
40
+ ([0], np.cumsum(np.asarray(self.bins, dtype=np.int64)))
41
+ )
42
+ self.atom_count = int(self.offsets[-1])
43
+
44
+ self.occurrence: list[np.ndarray] = []
45
+ self.support: list[int] = []
46
+ self.anchor: list[int] = []
47
+ self.frequency: list[int] = []
48
+ for index in range(self.dimension):
49
+ column = self.records[:, index]
50
+ for value in range(self.bins[index]):
51
+ members = np.flatnonzero(column == value).astype(np.int64)
52
+ self._append_token(members, 1 << index, index)
53
+ self._scratch = np.zeros(self.count, dtype=bool)
54
+ self.merges: list[tuple[int, int]] = []
55
+ self.statistics = {
56
+ "sparse_updates": 0,
57
+ "sparse_heap_rebuilds": 0,
58
+ "sparse_pair_types_peak": 0,
59
+ }
60
+
61
+ self.member = np.where(
62
+ self.records >= 0,
63
+ self.offsets[:-1][None, :] + self.records,
64
+ -1,
65
+ )
66
+ self._slot_pairs = tuple(
67
+ (first, second)
68
+ for first in range(self.dimension)
69
+ for second in range(first + 1, self.dimension)
70
+ )
71
+
72
+
73
+ self._sparse_counts: dict[int, int] | None = None
74
+ self._sparse_heap: list[tuple[int, int, int]] = []
75
+
76
+ def _append_token(self, members: np.ndarray, support: int, anchor: int) -> int:
77
+ token = len(self.occurrence)
78
+ self.occurrence.append(members)
79
+ self.support.append(support)
80
+ self.anchor.append(anchor)
81
+ self.frequency.append(int(self.weights[members].sum()) if members.size else 0)
82
+ return token
83
+
84
+ def _intersect(self, left: np.ndarray, right: np.ndarray) -> np.ndarray:
85
+ if left.size > right.size:
86
+ left, right = right, left
87
+ if left.size == 0:
88
+ return left
89
+ self._scratch[left] = True
90
+ result = right[self._scratch[right]]
91
+ self._scratch[left] = False
92
+ return result
93
+
94
+ @staticmethod
95
+ def _sparse_key(left: int, right: int) -> int:
96
+ low, high = (left, right) if left < right else (right, left)
97
+ return (int(low) << 32) | int(high)
98
+
99
+ @staticmethod
100
+ def _sparse_pair(key: int) -> tuple[int, int]:
101
+ return key >> 32, key & 0xFFFFFFFF
102
+
103
+ def build_sparse_index(self) -> None:
104
+ """Materialize exact counters only for currently observed pairs once."""
105
+ if self._sparse_counts is not None:
106
+ return
107
+ width = len(self.occurrence)
108
+ counts = np.zeros(width * width, dtype=np.int64)
109
+ for first, second in self._slot_pairs:
110
+ left = self.member[:, first]
111
+ right = self.member[:, second]
112
+ valid = (left >= 0) & (right >= 0)
113
+ if not valid.any():
114
+ continue
115
+ low = np.minimum(left[valid], right[valid])
116
+ high = np.maximum(left[valid], right[valid])
117
+ np.add.at(counts, low * width + high, self.weights[valid])
118
+ observed = np.flatnonzero(counts)
119
+ sparse: dict[int, int] = {}
120
+ heap: list[tuple[int, int, int]] = []
121
+ for packed in observed:
122
+ left, right = divmod(int(packed), width)
123
+ key = self._sparse_key(left, right)
124
+ value = int(counts[packed])
125
+ sparse[key] = value
126
+ heap.append((-value, left, right))
127
+ heapq.heapify(heap)
128
+ self._sparse_counts = sparse
129
+ self._sparse_heap = heap
130
+ self.statistics["sparse_pair_types_peak"] = len(sparse)
131
+
132
+ def sparse_best_pair(self) -> tuple[int, int, int] | None:
133
+ """Return the exact winner from the observed-pair max heap."""
134
+ if self._sparse_counts is None:
135
+ self.build_sparse_index()
136
+ assert self._sparse_counts is not None
137
+ while self._sparse_heap:
138
+ negative, left, right = heapq.heappop(self._sparse_heap)
139
+ value = self._sparse_counts.get(self._sparse_key(left, right), 0)
140
+ if value == -negative:
141
+ return left, right, value
142
+ return None
143
+
144
+ def _update_sparse_index(
145
+ self,
146
+ left: int,
147
+ right: int,
148
+ child: int,
149
+ rows: np.ndarray,
150
+ members: np.ndarray,
151
+ mass: int,
152
+ ) -> None:
153
+ if self._sparse_counts is None:
154
+ return
155
+ counts = self._sparse_counts
156
+ heap = self._sparse_heap
157
+ updates = 0
158
+
159
+ def change(key: int, delta: int) -> None:
160
+ nonlocal updates
161
+ new = counts.get(key, 0) + delta
162
+ if new < 0:
163
+ pair = self._sparse_pair(key)
164
+ raise RuntimeError(
165
+ f"sparse spatial pair counter became negative for {pair}"
166
+ )
167
+ if new:
168
+ counts[key] = new
169
+ pair = self._sparse_pair(key)
170
+ heapq.heappush(heap, (-new, pair[0], pair[1]))
171
+ else:
172
+ counts.pop(key, None)
173
+ updates += 1
174
+
175
+ change(self._sparse_key(left, right), -mass)
176
+ other_mass = np.zeros(len(self.occurrence), dtype=np.int64)
177
+ weights = self.weights[members]
178
+ for slot in range(rows.shape[1]):
179
+ values = rows[:, slot]
180
+ valid = (values >= 0) & (values != left) & (values != right)
181
+ if valid.any():
182
+ np.add.at(other_mass, values[valid], weights[valid])
183
+ for raw_other in np.flatnonzero(other_mass):
184
+ other = int(raw_other)
185
+ value = int(other_mass[other])
186
+ change(self._sparse_key(left, other), -value)
187
+ change(self._sparse_key(right, other), -value)
188
+ change(self._sparse_key(child, other), value)
189
+
190
+ self.statistics["sparse_updates"] += updates
191
+ self.statistics["sparse_pair_types_peak"] = max(
192
+ self.statistics["sparse_pair_types_peak"], len(counts)
193
+ )
194
+ if len(heap) > 2 * len(counts) + 65_536:
195
+ self._sparse_heap = [
196
+ (-value, *self._sparse_pair(key)) for key, value in counts.items()
197
+ ]
198
+ heapq.heapify(self._sparse_heap)
199
+ self.statistics["sparse_heap_rebuilds"] += 1
200
+
201
+ def merge(self, left: int, right: int) -> int:
202
+ """Apply one exact incremental merge update."""
203
+ members = self._intersect(self.occurrence[left], self.occurrence[right])
204
+ mass = int(self.weights[members].sum()) if members.size else 0
205
+ rows = self.member[members] if members.size else self.member[:0].copy()
206
+ self.occurrence[left] = np.setdiff1d(
207
+ self.occurrence[left], members, assume_unique=True
208
+ )
209
+ self.occurrence[right] = np.setdiff1d(
210
+ self.occurrence[right], members, assume_unique=True
211
+ )
212
+ self.frequency[left] -= mass
213
+ self.frequency[right] -= mass
214
+ token = self._append_token(
215
+ members,
216
+ self.support[left] | self.support[right],
217
+ min(self.anchor[left], self.anchor[right]),
218
+ )
219
+ self._update_sparse_index(left, right, token, rows, members, mass)
220
+
221
+
222
+ if members.size:
223
+ rows[rows == right] = -1
224
+ rows[rows == left] = token
225
+ self.member[members] = rows
226
+ self.merges.append((left, right))
227
+ return token
228
+
229
+
230
+ def _train_spatial_python(
231
+ codes: np.ndarray,
232
+ weights: np.ndarray,
233
+ bins: Sequence[int],
234
+ *,
235
+ vocab_size: int,
236
+ progress: bool = False,
237
+ ) -> tuple[SpatialVocabulary, dict[str, object]]:
238
+
239
+ trainer = _SparseSpatialTrainer(codes, weights, bins)
240
+ if vocab_size < trainer.atom_count:
241
+ raise ValueError(
242
+ f"vocab_size {vocab_size} is below the {trainer.atom_count} primitive atoms"
243
+ )
244
+ total_weight = int(trainer.weights.sum())
245
+ active_width = np.sum(trainer.records >= 0, axis=1, dtype=np.int64)
246
+ initial = int(np.dot(trainer.weights, active_width))
247
+ length = initial
248
+ trainer.build_sparse_index()
249
+ for step in range(vocab_size - trainer.atom_count):
250
+ winner = trainer.sparse_best_pair()
251
+ if winner is None:
252
+ break
253
+ left, right, frequency = winner
254
+ trainer.merge(left, right)
255
+ length -= frequency
256
+ if progress and step % 256 == 0:
257
+ print(
258
+ f" merge {step:>5} tokens/action {length / total_weight:.4f}",
259
+ flush=True,
260
+ )
261
+ vocabulary = SpatialVocabulary(
262
+ tuple(int(value) for value in bins), tuple(trainer.merges)
263
+ )
264
+ report = {
265
+ "records": int(trainer.count),
266
+ "weight": total_weight,
267
+ "atoms": trainer.atom_count,
268
+ "merges": len(trainer.merges),
269
+ "vocab_size": vocabulary.vocab_size,
270
+ "tokens_per_action_initial": initial / total_weight,
271
+ "tokens_per_action": length / total_weight,
272
+ "compression": initial / length,
273
+ "sparse_updates": trainer.statistics["sparse_updates"],
274
+ "sparse_heap_rebuilds": trainer.statistics["sparse_heap_rebuilds"],
275
+ "sparse_pair_types_peak": trainer.statistics["sparse_pair_types_peak"],
276
+ }
277
+ return vocabulary, report
278
+
279
+
280
+ def train_spatial(
281
+ codes: np.ndarray,
282
+ weights: np.ndarray,
283
+ bins: Sequence[int],
284
+ *,
285
+ vocab_size: int,
286
+ backend: str = "auto",
287
+ progress: bool = False,
288
+ **kwargs: object,
289
+ ) -> tuple[SpatialVocabulary, dict[str, object]]:
290
+ "Fit spatial Set-BPE, preferring the native kernel when it is built."
291
+ if resolve_backend(backend) == "python":
292
+
293
+
294
+ kwargs.pop("threads", None)
295
+ return _train_spatial_python(
296
+ codes, weights, bins, vocab_size=vocab_size, progress=progress, **kwargs
297
+ )
298
+ values = np.ascontiguousarray(codes, dtype=np.int32)
299
+ masses = np.ascontiguousarray(weights, dtype=np.int64)
300
+ merges, report = require_native().train_spatial(
301
+ values,
302
+ masses,
303
+ [int(count) for count in bins],
304
+ int(vocab_size),
305
+ int(kwargs.get("threads", 0)),
306
+ )
307
+ vocabulary = SpatialVocabulary(
308
+ tuple(int(count) for count in bins),
309
+ tuple((int(left), int(right)) for left, right in merges),
310
+ )
311
+ return vocabulary, dict(report)
runtime/tokenization/setbpe_temporal.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Runtime rooted-temporal Set-BPE vocabulary and lossless serialization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import heapq
6
+ from collections.abc import Sequence
7
+ from dataclasses import dataclass
8
+
9
+ import numpy as np
10
+
11
+ from ..common._validation import active_dimensions
12
+ from .setbpe_spatial import SpatialVocabulary
13
+
14
+ __all__ = ["TemporalVocabulary"]
15
+
16
+ Rule = tuple[int, int, int]
17
+ Pattern = tuple[tuple[int, int], ...]
18
+
19
+
20
+ def _active_support(active_dims: Sequence[int] | None, dimension: int) -> int:
21
+ dims = active_dimensions(active_dims, dimension)
22
+ return sum(1 << value for value in dims)
23
+
24
+
25
+ def _validate_episode(values: np.ndarray, spatial: SpatialVocabulary) -> int:
26
+ if values.ndim != 2 or values.shape[1] != spatial.dimension or len(values) < 1:
27
+ raise ValueError(
28
+ f"every episode must have non-empty shape [T, {spatial.dimension}], got {values.shape}"
29
+ )
30
+ counts = np.asarray(spatial.bins, dtype=np.int64)
31
+ if np.any(values < -1) or np.any(values >= counts[None, :]):
32
+ raise ValueError("episodes must contain -1 or an in-range cell index")
33
+ support = values >= 0
34
+ if np.any(np.sum(support, axis=1) == 0):
35
+ raise ValueError("every timestep needs at least one active coordinate")
36
+ if np.any(support != support[:1]):
37
+ raise ValueError("active dimensions must remain constant within an episode")
38
+ return sum(1 << int(value) for value in np.flatnonzero(support[0]))
39
+
40
+
41
+ @dataclass
42
+ class TemporalVocabulary:
43
+ """A spatial vocabulary plus rooted, forward-only temporal merge rules."""
44
+
45
+ spatial: SpatialVocabulary
46
+ lookahead: int
47
+ merges: tuple[Rule, ...]
48
+
49
+ def __post_init__(self) -> None:
50
+ if self.lookahead < 1:
51
+ raise ValueError("lookahead must be positive")
52
+ self.patterns: list[Pattern] = [
53
+ ((0, token),) for token in range(self.spatial.vocab_size)
54
+ ]
55
+ self.spans: list[int] = [1] * self.spatial.vocab_size
56
+ self.root: list[int] = list(range(self.spatial.vocab_size))
57
+ self.support_masks: list[tuple[int, ...]] = [
58
+ (int(self.spatial.support[token]),)
59
+ for token in range(self.spatial.vocab_size)
60
+ ]
61
+ self._rules: dict[Rule, tuple[int, int]] = {}
62
+ for rank, rule in enumerate(self.merges):
63
+ left, right, offset = (int(value) for value in rule)
64
+ child = self.spatial.vocab_size + rank
65
+ if left >= child or right >= child or left < 0 or right < 0:
66
+ raise ValueError(
67
+ "a temporal merge must reference already-created tokens"
68
+ )
69
+ pattern, masks = _merge_pattern(
70
+ self.patterns[left],
71
+ self.patterns[right],
72
+ offset,
73
+ self.lookahead,
74
+ self.spatial,
75
+ )
76
+ self.patterns.append(pattern)
77
+ self.spans.append(len(masks))
78
+ self.root.append(self.root[left])
79
+ self.support_masks.append(masks)
80
+ key = (left, right, offset)
81
+ if key in self._rules:
82
+ raise ValueError("duplicate temporal merge rule")
83
+ self._rules[key] = (child, rank)
84
+
85
+ @property
86
+ def vocab_size(self) -> int:
87
+ return self.spatial.vocab_size + len(self.merges)
88
+
89
+ def encode(self, codes: np.ndarray) -> list[int]:
90
+ """Encode a dense episode; ``-1`` slots are semantically absent."""
91
+ values = np.asarray(codes, dtype=np.int64)
92
+ _validate_episode(values, self.spatial)
93
+
94
+ at: list[dict[int, int]] = [dict() for _ in range(values.shape[0])]
95
+ tokens: list[int] = []
96
+ anchors: list[int] = []
97
+ active: list[bool] = []
98
+ for anchor, row in enumerate(values):
99
+ for token in self.spatial.encode(row):
100
+ instance = len(tokens)
101
+ tokens.append(token)
102
+ anchors.append(anchor)
103
+ active.append(True)
104
+ at[anchor][token] = instance
105
+
106
+ frontier: list[tuple[int, int, int, int, int, int]] = []
107
+
108
+ def offer(left_instance: int, right_instance: int) -> None:
109
+ left = tokens[left_instance]
110
+ right = tokens[right_instance]
111
+ offset = anchors[right_instance] - anchors[left_instance]
112
+ rule = self._rules.get((left, right, offset))
113
+ if rule is not None:
114
+ child, rank = rule
115
+ heapq.heappush(
116
+ frontier,
117
+ (
118
+ rank,
119
+ anchors[left_instance],
120
+ int(self.spatial.anchor[self.root[child]]),
121
+ left_instance,
122
+ right_instance,
123
+ child,
124
+ ),
125
+ )
126
+
127
+ for anchor, left_items in enumerate(at):
128
+ for offset in range(1, min(self.lookahead, len(at) - anchor)):
129
+ for left_instance in left_items.values():
130
+ for right_instance in at[anchor + offset].values():
131
+ offer(left_instance, right_instance)
132
+
133
+ while frontier:
134
+ _, anchor, _, left_instance, right_instance, child = heapq.heappop(frontier)
135
+ if not active[left_instance] or not active[right_instance]:
136
+ continue
137
+ if anchors[left_instance] != anchor:
138
+ continue
139
+ rule = (
140
+ tokens[left_instance],
141
+ tokens[right_instance],
142
+ anchors[right_instance] - anchor,
143
+ )
144
+ resolved = self._rules.get(rule)
145
+ if resolved is None or resolved[0] != child:
146
+ continue
147
+
148
+ active[left_instance] = False
149
+ active[right_instance] = False
150
+ del at[anchor][tokens[left_instance]]
151
+ del at[anchors[right_instance]][tokens[right_instance]]
152
+ new_instance = len(tokens)
153
+ tokens.append(child)
154
+ anchors.append(anchor)
155
+ active.append(True)
156
+ at[anchor][child] = new_instance
157
+
158
+ for offset in range(1, self.lookahead):
159
+ future = anchor + offset
160
+ if future < len(at):
161
+ for other in at[future].values():
162
+ offer(new_instance, other)
163
+ past = anchor - offset
164
+ if past >= 0:
165
+ for other in at[past].values():
166
+ offer(other, new_instance)
167
+
168
+ ordered: list[tuple[int, int, int]] = []
169
+ for anchor, instances in enumerate(at):
170
+ for token, instance in instances.items():
171
+ if active[instance]:
172
+ ordered.append(
173
+ (anchor, int(self.spatial.anchor[self.root[token]]), token)
174
+ )
175
+ ordered.sort()
176
+ return [token for _, _, token in ordered]
177
+
178
+ def decode(
179
+ self,
180
+ stream: Sequence[int],
181
+ *,
182
+ active_dims: Sequence[int] | None = None,
183
+ ) -> np.ndarray:
184
+ """Decode ids using the route's requested active-dimension support."""
185
+ if not stream:
186
+ return np.empty((0, self.spatial.dimension), dtype=np.int64)
187
+ full = _active_support(active_dims, self.spatial.dimension)
188
+ coverage: list[int] = [0]
189
+ spatial_tokens: list[list[int]] = [[]]
190
+ front = 0
191
+ for raw_token in stream:
192
+ token = int(raw_token)
193
+ if not 0 <= token < self.vocab_size:
194
+ raise ValueError(f"temporal token {token} is outside the vocabulary")
195
+ while front < len(coverage) and coverage[front] == full:
196
+ front += 1
197
+ if front == len(coverage):
198
+ coverage.append(0)
199
+ spatial_tokens.append([])
200
+
201
+ uncovered = full & ~coverage[front]
202
+ expected_anchor = int(uncovered & -uncovered).bit_length() - 1
203
+ actual_anchor = int(self.spatial.anchor[self.root[token]])
204
+ if actual_anchor != expected_anchor:
205
+ raise ValueError(
206
+ "temporal stream violates root order at the earliest incomplete step"
207
+ )
208
+
209
+ for offset, spatial_token in self.patterns[token]:
210
+ step = front + offset
211
+ while step >= len(coverage):
212
+ coverage.append(0)
213
+ spatial_tokens.append([])
214
+ support = int(self.spatial.support[spatial_token])
215
+ if coverage[step] & support:
216
+ raise ValueError(
217
+ "temporal stream contains overlapping spatial support"
218
+ )
219
+ coverage[step] |= support
220
+ spatial_tokens[step].append(spatial_token)
221
+
222
+ if any(mask != full for mask in coverage):
223
+ raise ValueError("temporal stream ended with an incomplete control step")
224
+ return np.stack(
225
+ [
226
+ self.spatial.decode(tokens, active_dims=active_dims)
227
+ for tokens in spatial_tokens
228
+ ]
229
+ )
230
+
231
+
232
+ def _merge_pattern(
233
+ left: Pattern,
234
+ right: Pattern,
235
+ offset: int,
236
+ lookahead: int,
237
+ spatial: SpatialVocabulary,
238
+ ) -> tuple[Pattern, tuple[int, ...]]:
239
+ if not 1 <= offset < lookahead:
240
+ raise ValueError("a temporal merge offset must be in [1, lookahead)")
241
+ shifted = tuple((time + offset, token) for time, token in right)
242
+ pattern = tuple(sorted((*left, *shifted)))
243
+ span = 1 + max(time for time, _ in pattern)
244
+ if span > lookahead:
245
+ raise ValueError("temporal merge exceeds the lookahead horizon")
246
+ masks = [0] * span
247
+ roots = 0
248
+ for time, token in pattern:
249
+ support = int(spatial.support[token])
250
+ if masks[time] & support:
251
+ raise ValueError("temporal merge joins overlapping spatial supports")
252
+ masks[time] |= support
253
+ roots += int(time == 0)
254
+ if roots != 1:
255
+ raise ValueError(
256
+ "a rooted temporal token must have exactly one time-zero spatial token"
257
+ )
258
+ return pattern, tuple(masks)
runtime/tokenization/setbpe_temporal_training.py ADDED
@@ -0,0 +1,649 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Temporal Set-BPE fitting implementations."
2
+
3
+ from __future__ import annotations
4
+
5
+ import heapq
6
+ from array import array
7
+ from collections import Counter
8
+ from collections.abc import Iterable, Iterator, Sequence
9
+
10
+ import numpy as np
11
+
12
+ from ..backend.native_backend import require_native, resolve_backend
13
+ from .setbpe_spatial import SpatialVocabulary
14
+ from .setbpe_temporal import (
15
+ Pattern,
16
+ Rule,
17
+ TemporalVocabulary,
18
+ _merge_pattern,
19
+ _validate_episode,
20
+ )
21
+
22
+ __all__ = ["train_temporal"]
23
+
24
+
25
+ class _SparseTemporalTrainer:
26
+
27
+
28
+ def __init__(
29
+ self,
30
+ episodes: Sequence[np.ndarray],
31
+ spatial: SpatialVocabulary,
32
+ lookahead: int,
33
+ max_vocab_size: int,
34
+ ) -> None:
35
+ if lookahead < 1:
36
+ raise ValueError("lookahead must be positive")
37
+ self.spatial = spatial
38
+ self.lookahead = lookahead
39
+ self.max_vocab_size = max_vocab_size
40
+ self.patterns: list[Pattern] = [
41
+ ((0, token),) for token in range(spatial.vocab_size)
42
+ ]
43
+ self.spans: list[int] = [1] * spatial.vocab_size
44
+ self.masks: list[tuple[int, ...]] = [
45
+ (int(spatial.support[token]),) for token in range(spatial.vocab_size)
46
+ ]
47
+ self._mask_table = np.zeros((max_vocab_size, lookahead), dtype=np.uint64)
48
+ self._mask_table[: spatial.vocab_size, 0] = spatial.support.astype(
49
+ np.uint64, copy=False
50
+ )
51
+ self.merges: list[Rule] = []
52
+ self.tokens = array("I")
53
+ self.anchors = array("I")
54
+ self.slots = array("I")
55
+ total_anchors = sum(len(episode) + lookahead for episode in episodes)
56
+ self.member = np.full((total_anchors, spatial.dimension), -1, dtype=np.int32)
57
+ self.occupied = np.zeros(total_anchors, dtype=np.uint64)
58
+ self._slot_mask = (1 << spatial.dimension) - 1
59
+ self.occurrences: list[dict[int, int]] = [
60
+ dict() for _ in range(spatial.vocab_size)
61
+ ]
62
+ self.episode_count = len(episodes)
63
+ self.action_count = sum(len(episode) for episode in episodes)
64
+ self.initial_count = 0
65
+ self.counts: dict[int, int] = {}
66
+ self.self_versions: dict[int, int] = {}
67
+ self.heap: list[tuple[int, int, int, int, int, bool]] = []
68
+ self.self_cache: dict[int, tuple[int, list[tuple[int, int]]]] = {}
69
+ self._legal_table = np.zeros(
70
+ (lookahead, max_vocab_size, max_vocab_size), dtype=np.uint8
71
+ )
72
+ for offset in range(1, lookahead):
73
+ self._legal_table[offset, : spatial.vocab_size, : spatial.vocab_size] = 2
74
+ self.statistics = Counter()
75
+ self._active_count = 0
76
+
77
+ self._load_episodes(episodes)
78
+ self.counts = self._initial_pair_counts()
79
+ self.heap = self._initial_heap()
80
+ self.statistics["initial_edge_types"] = len(self.counts)
81
+ self.statistics["initial_edges"] = sum(self.counts.values())
82
+
83
+ def _load_episodes(self, episodes: Sequence[np.ndarray]) -> None:
84
+
85
+
86
+ base = 0
87
+ encoding_cache: dict[bytes, tuple[int, ...]] = {}
88
+ for episode in episodes:
89
+ values = np.asarray(episode, dtype=np.int64)
90
+ _validate_episode(values, self.spatial)
91
+ needed = base + len(values) + self.lookahead
92
+ for time, row in enumerate(values):
93
+ cache_key = row.tobytes()
94
+ encoded = encoding_cache.get(cache_key)
95
+ if encoded is None:
96
+ encoded = tuple(self.spatial.encode(row))
97
+ encoding_cache[cache_key] = encoded
98
+ anchor = base + time
99
+ for token in encoded:
100
+ self._new_instance(token, anchor)
101
+ self.initial_count += 1
102
+ base = needed
103
+
104
+ def _initial_pair_counts(self) -> dict[int, int]:
105
+
106
+ base_vocab = self.spatial.vocab_size
107
+ dense_size = base_vocab * base_vocab * self.lookahead
108
+ if dense_size <= 1 << 25:
109
+ dense_counts = np.zeros(dense_size, dtype=np.int64)
110
+ for offset in range(1, self.lookahead):
111
+ left_rows = self.member[:-offset]
112
+ right_rows = self.member[offset:]
113
+ for left_slot in range(self.spatial.dimension):
114
+ left = left_rows[:, left_slot]
115
+ for right_slot in range(self.spatial.dimension):
116
+ right = right_rows[:, right_slot]
117
+ valid = (left >= 0) & (right >= 0)
118
+ if not valid.any():
119
+ continue
120
+ packed = (
121
+ left[valid].astype(np.int64) * base_vocab + right[valid]
122
+ ) * self.lookahead + offset
123
+ np.add.at(dense_counts, packed, 1)
124
+ initial_counts: dict[int, int] = {}
125
+ for raw_key in np.flatnonzero(dense_counts):
126
+ pair, offset = divmod(int(raw_key), self.lookahead)
127
+ left, right = divmod(pair, base_vocab)
128
+ packed_key = (
129
+ left * self.max_vocab_size + right
130
+ ) * self.lookahead + offset
131
+ initial_counts[packed_key] = int(dense_counts[raw_key])
132
+ return initial_counts
133
+ return self._initial_pair_counts_sparse()
134
+
135
+ def _initial_pair_counts_sparse(self) -> dict[int, int]:
136
+
137
+ fallback: Counter[int] = Counter()
138
+ for anchor, left_row in enumerate(self.member):
139
+ left_tokens = left_row[left_row >= 0]
140
+ for offset in range(1, self.lookahead):
141
+ target = anchor + offset
142
+ if target >= len(self.member):
143
+ break
144
+ right_tokens = self.member[target]
145
+ right_tokens = right_tokens[right_tokens >= 0]
146
+ for raw_left in left_tokens:
147
+ for raw_right in right_tokens:
148
+ packed_key = (
149
+ int(raw_left) * self.max_vocab_size + int(raw_right)
150
+ ) * self.lookahead + offset
151
+ fallback[packed_key] += 1
152
+ return dict(fallback)
153
+
154
+ def _initial_heap(self) -> list[tuple[int, int, int, int, int, bool]]:
155
+ heap: list[tuple[int, int, int, int, int, bool]] = []
156
+ for packed_key, count in self.counts.items():
157
+ pair, offset = divmod(packed_key, self.lookahead)
158
+ left, right = divmod(pair, self.max_vocab_size)
159
+ heap.append((-count, left, right, offset, 0, False))
160
+ heapq.heapify(heap)
161
+ return heap
162
+
163
+ def _new_instance(self, token: int, anchor: int) -> int:
164
+ while token >= len(self.occurrences):
165
+ self.occurrences.append({})
166
+ if anchor in self.occurrences[token]:
167
+ raise RuntimeError("a temporal type cannot occur twice at one anchor")
168
+ occupied = int(self.occupied[anchor])
169
+ available = self._slot_mask ^ occupied
170
+ if available == 0:
171
+ raise RuntimeError("temporal anchor exceeded the spatial token-width bound")
172
+ lowest = available & -available
173
+ slot = lowest.bit_length() - 1
174
+ self.occupied[anchor] = occupied | lowest
175
+ instance = len(self.tokens)
176
+ self.tokens.append(token)
177
+ self.anchors.append(anchor)
178
+ self.slots.append(slot)
179
+ self.member[anchor, slot] = token
180
+ self._active_count += 1
181
+ self.occurrences[token][anchor] = instance
182
+ return instance
183
+
184
+ def _initialize_legality(self, child: int) -> None:
185
+
186
+ count = child + 1
187
+ spans = np.asarray(self.spans[:count], dtype=np.int64)
188
+ masks = self._mask_table[:count]
189
+ child_masks = self._mask_table[child]
190
+ child_span = self.spans[child]
191
+ for offset in range(1, self.lookahead):
192
+ overlap = np.zeros(count, dtype=bool)
193
+ for relative in range(self.lookahead - offset):
194
+ overlap |= (child_masks[offset + relative] & masks[:, relative]) != 0
195
+ legal = (offset + spans <= self.lookahead) & ~overlap
196
+ self._legal_table[offset, child, :count] = np.where(legal, 2, 1)
197
+
198
+ overlap.fill(False)
199
+ for relative in range(min(child_span, self.lookahead - offset)):
200
+ overlap |= (masks[:, offset + relative] & child_masks[relative]) != 0
201
+ legal = (offset + child_span <= self.lookahead) & ~overlap
202
+ self._legal_table[offset, :count, child] = np.where(legal, 2, 1)
203
+
204
+ def _entry(
205
+ self, packed_key: int, priority: int, certified: bool
206
+ ) -> tuple[int, int, int, int, int, bool]:
207
+ pair, offset = divmod(packed_key, self.lookahead)
208
+ left, right = divmod(pair, self.max_vocab_size)
209
+ return (
210
+ -priority,
211
+ left,
212
+ right,
213
+ offset,
214
+ self.self_versions.get(packed_key, 0) if left == right else 0,
215
+ certified,
216
+ )
217
+
218
+ def _select(self, key: Rule) -> list[tuple[int, int]]:
219
+ left, right, offset = key
220
+ left_occurrences = self.occurrences[left]
221
+ right_occurrences = self.occurrences[right]
222
+ if left != right:
223
+ return [
224
+ (left_instance, right_occurrences[anchor + offset])
225
+ for anchor, left_instance in left_occurrences.items()
226
+ if anchor + offset in right_occurrences
227
+ ]
228
+ selected: list[tuple[int, int]] = []
229
+ consumed: set[int] = set()
230
+ for anchor in sorted(left_occurrences):
231
+ target = anchor + offset
232
+ if (
233
+ anchor in consumed
234
+ or target in consumed
235
+ or target not in right_occurrences
236
+ ):
237
+ continue
238
+ selected.append((left_occurrences[anchor], right_occurrences[target]))
239
+ consumed.add(anchor)
240
+ consumed.add(target)
241
+ return selected
242
+
243
+ def best(self) -> tuple[Rule, list[tuple[int, int]]] | None:
244
+ while self.heap:
245
+ negative, left, right, offset, version, certified = heapq.heappop(self.heap)
246
+ self.statistics["heap_pops"] += 1
247
+ key = (left, right, offset)
248
+ packed_key = (left * self.max_vocab_size + right) * self.lookahead + offset
249
+ current_count = self.counts.get(packed_key, 0)
250
+ if current_count <= 0:
251
+ continue
252
+ if left == right:
253
+ current_version = self.self_versions.get(packed_key, 0)
254
+ stale = current_version != version
255
+ else:
256
+ stale = current_count != -negative
257
+ if stale:
258
+ heapq.heappush(self.heap, self._entry(packed_key, current_count, False))
259
+ continue
260
+ if left == right and not certified:
261
+ matches = self._select(key)
262
+ self.self_cache[packed_key] = (version, matches)
263
+ heapq.heappush(self.heap, self._entry(packed_key, len(matches), True))
264
+ self.statistics["self_certifications"] += 1
265
+ continue
266
+ if certified:
267
+ cached = self.self_cache.get(packed_key)
268
+ if cached is None or cached[0] != version:
269
+ continue
270
+ matches = cached[1]
271
+ else:
272
+ matches = self._select(key)
273
+ gain = -negative
274
+ if len(matches) != gain:
275
+ raise RuntimeError(
276
+ "sparse temporal counter disagrees with exact matching"
277
+ )
278
+ return key, matches
279
+ return None
280
+
281
+ def _accumulate_incident(
282
+ self,
283
+ instances: Sequence[int] | set[int],
284
+ multiplier: int,
285
+ deltas: Counter[int],
286
+ ) -> None:
287
+
288
+ grouped: dict[int, list[int]] = {}
289
+ for instance in instances:
290
+ grouped.setdefault(self.tokens[instance], []).append(self.anchors[instance])
291
+ width = len(self.patterns)
292
+ total_anchors = self.member.shape[0]
293
+ vocab_width = self.max_vocab_size
294
+ lookahead = self.lookahead
295
+ for token, raw_anchors in grouped.items():
296
+ anchors = np.asarray(raw_anchors, dtype=np.int64)
297
+ for offset in range(1, self.lookahead):
298
+ future_anchors = anchors[anchors + offset < total_anchors] + offset
299
+ if future_anchors.size:
300
+ values = self.member[future_anchors].reshape(-1)
301
+ values = values[values >= 0]
302
+ if values.size:
303
+ counts = np.bincount(values, minlength=width)
304
+ neighbors = np.flatnonzero(counts)
305
+ neighbors = neighbors[
306
+ self._legal_table[offset, token, neighbors] == 2
307
+ ]
308
+ for raw_neighbor in neighbors:
309
+ neighbor = int(raw_neighbor)
310
+ packed_key = (
311
+ token * vocab_width + neighbor
312
+ ) * lookahead + offset
313
+ deltas[packed_key] += multiplier * int(counts[neighbor])
314
+ past_anchors = anchors[anchors >= offset] - offset
315
+ if past_anchors.size:
316
+ values = self.member[past_anchors].reshape(-1)
317
+ values = values[values >= 0]
318
+ if values.size:
319
+ counts = np.bincount(values, minlength=width)
320
+ neighbors = np.flatnonzero(counts)
321
+ neighbors = neighbors[
322
+ self._legal_table[offset, neighbors, token] == 2
323
+ ]
324
+ for raw_neighbor in neighbors:
325
+ neighbor = int(raw_neighbor)
326
+ packed_key = (
327
+ neighbor * vocab_width + token
328
+ ) * lookahead + offset
329
+ deltas[packed_key] += multiplier * int(counts[neighbor])
330
+
331
+ def _correct_internal_edges(
332
+ self,
333
+ instances: Sequence[int] | set[int],
334
+ correction: int,
335
+ deltas: Counter[int],
336
+ ) -> None:
337
+
338
+ by_anchor: dict[int, list[int]] = {}
339
+ for instance in instances:
340
+ by_anchor.setdefault(self.anchors[instance], []).append(
341
+ self.tokens[instance]
342
+ )
343
+ vocab_width = self.max_vocab_size
344
+ lookahead = self.lookahead
345
+ for anchor, left_tokens in by_anchor.items():
346
+ for offset in range(1, self.lookahead):
347
+ right_tokens = by_anchor.get(anchor + offset)
348
+ if right_tokens is None:
349
+ continue
350
+ for left in left_tokens:
351
+ for right in right_tokens:
352
+ if self._legal_table[offset, left, right] == 2:
353
+ packed_key = (
354
+ left * vocab_width + right
355
+ ) * lookahead + offset
356
+ deltas[packed_key] += correction
357
+
358
+ def _apply_deltas(self, deltas: Counter[int], selected: Rule) -> None:
359
+
360
+ counts = self.counts
361
+ self_versions = self.self_versions
362
+ heap = self.heap
363
+ updates = 0
364
+ vocab_width = self.max_vocab_size
365
+ lookahead = self.lookahead
366
+ selected_key = (selected[0] * vocab_width + selected[1]) * lookahead + selected[
367
+ 2
368
+ ]
369
+ for packed_key, delta in deltas.items():
370
+ previous = counts.get(packed_key, 0)
371
+ count = previous + delta
372
+ if count < 0:
373
+ raise RuntimeError("temporal edge counter became negative")
374
+
375
+
376
+ pair = packed_key // lookahead
377
+ is_self = pair % (vocab_width + 1) == 0
378
+ if is_self:
379
+ version = self_versions.get(packed_key, 0) + 1
380
+ self_versions[packed_key] = version
381
+ self.self_cache.pop(packed_key, None)
382
+ else:
383
+ version = 0
384
+ if count:
385
+ counts[packed_key] = count
386
+ if previous == 0 or packed_key == selected_key:
387
+ pair, offset = divmod(packed_key, lookahead)
388
+ left, right = divmod(pair, vocab_width)
389
+ heapq.heappush(heap, (-count, left, right, offset, version, False))
390
+ else:
391
+ counts.pop(packed_key, None)
392
+ updates += abs(delta)
393
+ self.statistics["counter_updates"] += updates
394
+
395
+ def commit(self, key: Rule, matches: Sequence[tuple[int, int]]) -> None:
396
+ left, right, offset = key
397
+ pattern, masks = _merge_pattern(
398
+ self.patterns[left],
399
+ self.patterns[right],
400
+ offset,
401
+ self.lookahead,
402
+ self.spatial,
403
+ )
404
+ child = len(self.patterns)
405
+ self.patterns.append(pattern)
406
+ self.spans.append(len(masks))
407
+ self.masks.append(masks)
408
+ self.occurrences.append({})
409
+ self.merges.append(key)
410
+ self._mask_table[child, : len(masks)] = masks
411
+ self._initialize_legality(child)
412
+
413
+ removed = {instance for pair in matches for instance in pair}
414
+ if len(removed) != 2 * len(matches):
415
+ raise RuntimeError("selected temporal matches overlap")
416
+ deltas: Counter[int] = Counter()
417
+ self._accumulate_incident(removed, -1, deltas)
418
+ self._correct_internal_edges(removed, 1, deltas)
419
+
420
+ for instance in removed:
421
+ token = self.tokens[instance]
422
+ anchor = self.anchors[instance]
423
+ self.member[anchor, self.slots[instance]] = -1
424
+ self._active_count -= 1
425
+ self.occupied[anchor] = int(self.occupied[anchor]) & ~(
426
+ 1 << self.slots[instance]
427
+ )
428
+ del self.occurrences[token][anchor]
429
+
430
+ children = [
431
+ self._new_instance(child, self.anchors[left_instance])
432
+ for left_instance, _ in matches
433
+ ]
434
+ self._accumulate_incident(children, 1, deltas)
435
+ self._correct_internal_edges(children, -1, deltas)
436
+ self._apply_deltas(deltas, key)
437
+
438
+ @property
439
+ def active_count(self) -> int:
440
+ return self._active_count
441
+
442
+
443
+ def _prepare_episodes(
444
+ episodes: Iterable[np.ndarray], spatial: SpatialVocabulary
445
+ ) -> Sequence[np.ndarray]:
446
+ if getattr(episodes, "_actioncodec2_validated", False):
447
+ if not isinstance(episodes, Sequence) or len(episodes) == 0:
448
+ raise ValueError("validated temporal corpus must be a non-empty sequence")
449
+ return episodes
450
+ prepared: list[np.ndarray] = []
451
+ for episode in episodes:
452
+ values = np.asarray(episode)
453
+ if values.dtype.kind not in {"i", "u"}:
454
+ raise ValueError("episodes must contain integer cell indices")
455
+ _validate_episode(values, spatial)
456
+ prepared.append(np.ascontiguousarray(values))
457
+ if not prepared or sum(len(episode) for episode in prepared) == 0:
458
+ raise ValueError("temporal training requires at least one non-empty episode")
459
+ return prepared
460
+
461
+
462
+ def _train_temporal_python(
463
+ episodes: Iterable[np.ndarray],
464
+ spatial: SpatialVocabulary,
465
+ *,
466
+ vocab_size: int,
467
+ lookahead: int,
468
+ progress: bool = False,
469
+ ) -> tuple[TemporalVocabulary, dict[str, object]]:
470
+
471
+ prepared = _prepare_episodes(episodes, spatial)
472
+ if vocab_size < spatial.vocab_size:
473
+ raise ValueError(
474
+ f"vocab_size {vocab_size} is below the {spatial.vocab_size} spatial tokens"
475
+ )
476
+ trainer = _SparseTemporalTrainer(prepared, spatial, lookahead, vocab_size)
477
+ length = trainer.initial_count
478
+ for step in range(vocab_size - spatial.vocab_size):
479
+ winner = trainer.best()
480
+ if winner is None:
481
+ break
482
+ key, matches = winner
483
+ if not matches:
484
+ break
485
+ trainer.commit(key, matches)
486
+ length -= len(matches)
487
+ if progress and step % 64 == 0:
488
+ print(
489
+ f" temporal merge {step:>5} tokens/action {length / trainer.action_count:.4f}",
490
+ flush=True,
491
+ )
492
+ if length != trainer.active_count:
493
+ raise RuntimeError("temporal token accounting disagrees with active tiling")
494
+ vocabulary = TemporalVocabulary(spatial, lookahead, tuple(trainer.merges))
495
+ report: dict[str, object] = {
496
+ "episodes": trainer.episode_count,
497
+ "actions": trainer.action_count,
498
+ "lookahead": lookahead,
499
+ "spatial_vocab_size": spatial.vocab_size,
500
+ "merges": len(trainer.merges),
501
+ "vocab_size": vocabulary.vocab_size,
502
+ "tokens_per_action_initial": trainer.initial_count / trainer.action_count,
503
+ "tokens_per_action": length / trainer.action_count,
504
+ "compression": trainer.initial_count / length,
505
+ "trainer": "sparse_exact",
506
+ **{key: int(value) for key, value in trainer.statistics.items()},
507
+ }
508
+ return vocabulary, report
509
+
510
+
511
+ def _flatten_instances_native(
512
+ episodes: Sequence[np.ndarray],
513
+ spatial: SpatialVocabulary,
514
+ lookahead: int,
515
+ threads: int,
516
+ ) -> tuple[Iterator[tuple[np.ndarray, np.ndarray]], int, int]:
517
+
518
+ if getattr(episodes, "_actioncodec2_validated", False):
519
+ return _flatten_cell_corpus(episodes, spatial, lookahead, threads)
520
+ actions = sum(len(episode) for episode in episodes)
521
+ total_anchors = actions + len(episodes) * lookahead
522
+ max_batch_actions = 1_000_000
523
+ merges = np.asarray(spatial.merges, dtype=np.int32).reshape(-1, 2)
524
+
525
+ def generate() -> Iterator[tuple[np.ndarray, np.ndarray]]:
526
+ pending: list[np.ndarray] = []
527
+ pending_anchors: list[np.ndarray] = []
528
+ pending_actions = 0
529
+ base = 0
530
+
531
+ def encode() -> tuple[np.ndarray, np.ndarray]:
532
+ codes = np.concatenate(pending, axis=0).astype(np.int32, copy=False)
533
+ row_anchors = np.concatenate(pending_anchors)
534
+ tokens, anchors = require_native().spatial_encode_instances(
535
+ np.ascontiguousarray(codes),
536
+ np.ascontiguousarray(row_anchors),
537
+ [int(count) for count in spatial.bins],
538
+ np.ascontiguousarray(merges),
539
+ int(threads),
540
+ )
541
+ return np.asarray(tokens, dtype=np.int32), np.asarray(
542
+ anchors, dtype=np.int64
543
+ )
544
+
545
+ for episode in episodes:
546
+ length = len(episode)
547
+ pending.append(episode)
548
+ pending_anchors.append(np.arange(base, base + length, dtype=np.int64))
549
+ pending_actions += length
550
+ base += length + lookahead
551
+ if pending_actions >= max_batch_actions:
552
+ yield encode()
553
+ pending.clear()
554
+ pending_anchors.clear()
555
+ pending_actions = 0
556
+ if pending:
557
+ yield encode()
558
+
559
+ return generate(), total_anchors, actions
560
+
561
+
562
+ def _flatten_cell_corpus(
563
+ episodes: Sequence[np.ndarray],
564
+ spatial: SpatialVocabulary,
565
+ lookahead: int,
566
+ threads: int,
567
+ ) -> tuple[Iterator[tuple[np.ndarray, np.ndarray]], int, int]:
568
+
569
+ actions = sum(len(codes) for codes in episodes.chunks if codes is not None)
570
+ total_anchors = actions + len(episodes) * lookahead
571
+ merges = np.asarray(spatial.merges, dtype=np.int32).reshape(-1, 2)
572
+ chunks = episodes.chunks
573
+ offsets_by_chunk = episodes.offsets
574
+
575
+ def generate() -> Iterator[tuple[np.ndarray, np.ndarray]]:
576
+ base = 0
577
+ for chunk_index, (codes, offsets) in enumerate(
578
+ zip(chunks, offsets_by_chunk, strict=True)
579
+ ):
580
+ if codes is None:
581
+ raise RuntimeError("cell corpus chunk has already been consumed")
582
+ lengths = np.diff(offsets)
583
+ row_anchors = (
584
+ base
585
+ + np.arange(len(codes), dtype=np.int64)
586
+ + np.repeat(
587
+ np.arange(len(lengths), dtype=np.int64) * lookahead, lengths
588
+ )
589
+ )
590
+ tokens, anchors = require_native().spatial_encode_instances(
591
+ np.ascontiguousarray(codes),
592
+ row_anchors,
593
+ [int(count) for count in spatial.bins],
594
+ np.ascontiguousarray(merges),
595
+ int(threads),
596
+ )
597
+ episodes.release_chunk(chunk_index)
598
+ yield (
599
+ np.asarray(tokens, dtype=np.int32),
600
+ np.asarray(anchors, dtype=np.int64),
601
+ )
602
+ base += len(codes) + len(lengths) * lookahead
603
+
604
+ return generate(), total_anchors, actions
605
+
606
+
607
+ def train_temporal(
608
+ episodes: Iterable[np.ndarray],
609
+ spatial: SpatialVocabulary,
610
+ *,
611
+ vocab_size: int,
612
+ lookahead: int,
613
+ backend: str = "auto",
614
+ progress: bool = False,
615
+ threads: int = 0,
616
+ ) -> tuple[TemporalVocabulary, dict[str, object]]:
617
+ "Fit rooted temporal Set-BPE on top of a frozen spatial vocabulary."
618
+ prepared = _prepare_episodes(episodes, spatial)
619
+ if resolve_backend(backend) == "python":
620
+ return _train_temporal_python(
621
+ prepared,
622
+ spatial,
623
+ vocab_size=vocab_size,
624
+ lookahead=lookahead,
625
+ progress=progress,
626
+ )
627
+ episode_count = len(prepared)
628
+ instance_chunks, total_anchors, actions = _flatten_instances_native(
629
+ prepared, spatial, lookahead, threads
630
+ )
631
+ merges, report = require_native().train_temporal(
632
+ instance_chunks,
633
+ int(total_anchors),
634
+ np.ascontiguousarray(spatial.support, dtype=np.uint64),
635
+ np.ascontiguousarray(spatial.anchor, dtype=np.int32),
636
+ int(spatial.dimension),
637
+ int(lookahead),
638
+ int(vocab_size),
639
+ int(actions),
640
+ int(threads),
641
+ )
642
+ vocabulary = TemporalVocabulary(
643
+ spatial,
644
+ int(lookahead),
645
+ tuple(tuple(int(value) for value in rule) for rule in merges),
646
+ )
647
+ payload = dict(report)
648
+ payload["episodes"] = episode_count
649
+ return vocabulary, payload
runtime/training/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Internal training components for ActionCodec2."""
runtime/training/fitting.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Replayable, bounded source staging for the public ActionCodec2.fit interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from contextlib import ExitStack
7
+ from pathlib import Path
8
+ from tempfile import TemporaryDirectory
9
+
10
+ import numpy as np
11
+ import torch
12
+
13
+ from ..physical.boundary import bootstrap_second_order_trajectory
14
+ from ..routing.routing import ActionCodec2Router
15
+ from ..frontend.tokenizer import _numpy
16
+ from .training import ProfileTrainer
17
+ from ..physical.trajectory import ActiveTrajectory
18
+
19
+
20
+ class TrajectorySpool:
21
+ "Stage canonical episodes in two sequential files, without retaining source data."
22
+
23
+ def __init__(self, directory):
24
+ self.directory = Path(directory)
25
+ self.counts = {}
26
+ self._stack = ExitStack()
27
+ self._writers = {}
28
+
29
+ def __enter__(self):
30
+ return self
31
+
32
+ def __exit__(self, *exc):
33
+ return self._stack.__exit__(*exc)
34
+
35
+ def append(self, profile, trajectory):
36
+ if profile not in self._writers:
37
+ self._writers[profile] = self._stack.enter_context(
38
+ (self.directory / f"{profile}.npy").open("wb")
39
+ )
40
+ stream = self._writers[profile]
41
+ for value in (
42
+ trajectory.values,
43
+ trajectory.active_dims,
44
+ []
45
+ if trajectory.previous_first_order is None
46
+ else trajectory.previous_first_order,
47
+ ):
48
+ np.save(stream, np.asarray(value), allow_pickle=False)
49
+ self.counts[profile] = self.counts.get(profile, 0) + 1
50
+
51
+ def trajectories(self, profile):
52
+ self._writers[profile].flush()
53
+ with (self.directory / f"{profile}.npy").open("rb") as stream:
54
+ for _ in range(self.counts[profile]):
55
+ values = np.load(stream, allow_pickle=False)
56
+ dims = tuple(int(d) for d in np.load(stream, allow_pickle=False))
57
+ previous = np.load(stream, allow_pickle=False)
58
+ yield ActiveTrajectory(
59
+ values,
60
+ dims,
61
+ previous_first_order=previous if previous.size else None,
62
+ )
63
+
64
+
65
+ class ActionCodec2Fitter:
66
+ "Route user layouts and fit replacement profiles without mutating the caller."
67
+
68
+ def __init__(self, codec, *, bootstrap_context=False):
69
+ if not isinstance(bootstrap_context, bool):
70
+ raise TypeError("bootstrap_context must be a boolean")
71
+ self.codec = codec
72
+ self.router = codec.action_tokenizer
73
+ self.bootstrap_context = bootstrap_context
74
+
75
+ def _route(self, record):
76
+ unknown = set(record) - {
77
+ "actions",
78
+ "action_space",
79
+ "fps",
80
+ "current_state",
81
+ "previous_first_order",
82
+ }
83
+ if unknown:
84
+ raise ValueError(f"unknown training record fields: {sorted(unknown)}")
85
+ if "actions" not in record:
86
+ raise ValueError("training record requires an 'actions' array")
87
+ preset = self.router.preset(
88
+ record.get("action_space", self.codec.action_space.name)
89
+ )
90
+ values = _numpy(torch.as_tensor(record["actions"]))
91
+ if values.ndim == 2:
92
+ values = values[None]
93
+ if values.ndim != 3 or values.shape[0] == 0 or values.shape[1] == 0:
94
+ raise ValueError(
95
+ "training actions must have nonempty shape (T,D) or (B,T,D)"
96
+ )
97
+ current = _numpy(record.get("current_state"))
98
+ if preset.input == "absolute" and current is None:
99
+ raise ValueError(
100
+ f"absolute action space {preset.name!r} requires current_state(B,D)"
101
+ )
102
+ if preset.input == "delta" and current is not None:
103
+ raise ValueError(f"delta action space {preset.name!r}: omit current_state")
104
+ previous = self.codec._numpy_boundary(record.get("previous_first_order"))
105
+ kwargs = dict(
106
+ preset=preset.name,
107
+ fps=self.codec._source_fps(record.get("fps"), preset),
108
+ current_state=current,
109
+ )
110
+ if not self.bootstrap_context:
111
+ return self.router.route_trajectories(
112
+ values, previous_first_order=previous, **kwargs
113
+ )
114
+ if previous is not None:
115
+ raise ValueError(
116
+ "bootstrap_context and previous_first_order are mutually exclusive"
117
+ )
118
+ canonical = preset.canonicalize(
119
+ values, source_hz=kwargs["fps"], current_state=current
120
+ )
121
+ routed = {}
122
+ for component in preset.components:
123
+ processor = self.router.profiles[component.space].processor
124
+ trajectories = canonical[component.name]
125
+ if processor.requires_previous_first_order:
126
+ trajectories = [
127
+ bootstrap_second_order_trajectory(t, processor.config)
128
+ for t in trajectories
129
+ ]
130
+ routed.setdefault(component.space, []).extend(trajectories)
131
+ return routed
132
+
133
+ def fit(self, episodes, *, work_dir=None, metadata=None, **options):
134
+ """Fit observed profiles from arrays or records, replaying staged episodes per profile."""
135
+ metadata = {k: v for k, v in (metadata or {}).items() if v is not None}
136
+ if isinstance(episodes, (np.ndarray, torch.Tensor, Mapping)):
137
+ episodes = (episodes,)
138
+ reports = {}
139
+ processors = {
140
+ name: runtime.processor for name, runtime in self.router.profiles.items()
141
+ }
142
+ records = 0
143
+ with TemporaryDirectory(prefix="actioncodec2-fit-", dir=work_dir) as directory:
144
+ with TrajectorySpool(directory) as spool:
145
+ for episode in episodes:
146
+ record = (
147
+ dict(episode)
148
+ if isinstance(episode, Mapping)
149
+ else {"actions": episode}
150
+ )
151
+ duplicates = set(record).intersection(metadata)
152
+ if duplicates:
153
+ raise ValueError(
154
+ f"training arguments provided twice: {sorted(duplicates)}"
155
+ )
156
+ record.update(metadata)
157
+ for profile, trajectories in self._route(record).items():
158
+ for trajectory in trajectories:
159
+ spool.append(profile, trajectory)
160
+ records += 1
161
+ if not spool.counts:
162
+ raise ValueError("fit requires at least one nonempty episode")
163
+ for name in spool.counts:
164
+ processors[name], reports[name] = ProfileTrainer(
165
+ processors[name].config
166
+ ).fit(spool.trajectories(name), **options)
167
+ router = ActionCodec2Router.from_processors(
168
+ processors,
169
+ token_budgets={
170
+ name: runtime.spec.token_budget
171
+ for name, runtime in self.router.profiles.items()
172
+ },
173
+ presets=list(self.router.action_spaces.values()),
174
+ )
175
+ return router, {
176
+ "records": records,
177
+ "bootstrap_context": self.bootstrap_context,
178
+ "profiles": reports,
179
+ }
runtime/training/training.py ADDED
@@ -0,0 +1,698 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "Whole-trajectory primitive construction and BPE fitting."
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import time
7
+ from bisect import bisect_right
8
+ from collections import defaultdict
9
+ from collections.abc import Iterable, Sequence
10
+
11
+ import numpy as np
12
+
13
+ from ..common._validation import broadcast_vector
14
+ from ..tokenization.bpe import BPESerializer, SetBPESerializer
15
+ from ..tokenization.bpe_training import train_bpe
16
+ from ..physical.configuration import ActionCodec2Config, ConfigError
17
+ from ..physical.physical_codec import PhysicalCodec
18
+ from ..physical.physical_processor import ActionCodec2Processor
19
+ from ..physical.primitives import PrimitiveStore
20
+ from ..tokenization.setbpe_spatial import aggregate_records
21
+ from ..tokenization.setbpe_spatial_training import train_spatial
22
+ from ..tokenization.setbpe_temporal_training import train_temporal
23
+ from ..physical.trajectory import (
24
+ TrajectoryLike,
25
+ prepare_trajectory,
26
+ trajectory_boundary,
27
+ )
28
+
29
+
30
+ def _zero_anchored_grid(
31
+ lower: np.float32, upper: np.float32, spacing: np.float32
32
+ ) -> np.ndarray:
33
+
34
+ below = int(np.ceil(-lower / spacing))
35
+ above = int(np.ceil(upper / spacing))
36
+ negative = np.linspace(lower, np.float32(0.0), below + 1, dtype=np.float32)[:-1]
37
+ positive = np.linspace(np.float32(0.0), upper, above + 1, dtype=np.float32)
38
+ return np.concatenate((negative, positive))
39
+
40
+
41
+ def _additive_axis(
42
+ lower: np.float32,
43
+ upper: np.float32,
44
+ distortion_limit: np.float32,
45
+ *,
46
+ zero_primitive: bool,
47
+ ) -> np.ndarray:
48
+
49
+ width = np.float32(upper - lower)
50
+ if width == 0.0:
51
+ return np.asarray([lower], dtype=np.float32)
52
+ spacing = np.float32(2.0) * distortion_limit
53
+ if zero_primitive and lower < 0.0 < upper:
54
+ return _zero_anchored_grid(lower, upper, spacing)
55
+ count = 1 + int(np.ceil(width / spacing))
56
+ return np.linspace(lower, upper, count, dtype=np.float32)
57
+
58
+
59
+ def build_primitives(config: ActionCodec2Config) -> PrimitiveStore:
60
+ "Derive primitive arrays and force an exact zero on every axis."
61
+ if config.train is None:
62
+ raise ConfigError("config.train is required to build primitives")
63
+ output: list[np.ndarray | None] = [None] * config.physical.input_dim
64
+ for component in config.physical.components.values():
65
+ width = len(component.dims)
66
+ if component.space == "additive":
67
+ domain = component.constraints[f"order_{component.primitive_order}"]
68
+ lower = broadcast_vector(
69
+ domain.minimum, width, dtype=np.dtype(np.float32), name="minimum"
70
+ )
71
+ upper = broadcast_vector(
72
+ domain.maximum, width, dtype=np.dtype(np.float32), name="maximum"
73
+ )
74
+ epsilon = broadcast_vector(
75
+ component.distortion_limit,
76
+ width,
77
+ dtype=np.dtype(np.float32),
78
+ name="distortion_limit",
79
+ )
80
+ if component.primitive_order == 2:
81
+
82
+ lower = lower - 2 * epsilon
83
+ upper = upper + 2 * epsilon
84
+ axes = [
85
+ _additive_axis(
86
+ lower[index],
87
+ upper[index],
88
+ epsilon[index],
89
+ zero_primitive=True,
90
+ )
91
+ for index in range(width)
92
+ ]
93
+ else:
94
+ domain = component.constraints[f"order_{component.primitive_order}"]
95
+ radius_dtype = (
96
+ np.float64
97
+ if component.representation == "rotation_vector"
98
+ else np.float32
99
+ )
100
+ lower = broadcast_vector(
101
+ domain.minimum, width, dtype=np.dtype(radius_dtype), name="minimum"
102
+ )
103
+ upper = broadcast_vector(
104
+ domain.maximum, width, dtype=np.dtype(radius_dtype), name="maximum"
105
+ )
106
+ corner = np.maximum(np.abs(lower), np.abs(upper))
107
+ source_radius = (
108
+ float(np.linalg.norm(corner))
109
+ if component.representation == "rotation_vector"
110
+ else float(np.sum(corner, dtype=np.float32))
111
+ )
112
+ source_radius = min(math.pi, source_radius)
113
+ epsilon = float(component.distortion_limit)
114
+ allowance = (3 if component.primitive_order == 2 else 1) * epsilon
115
+ cover_radius = float(min(math.pi, source_radius + allowance))
116
+ maximum_spacing = 2.0 * epsilon / math.sqrt(3.0)
117
+ count = int(math.ceil(2.0 * cover_radius / maximum_spacing)) + 1
118
+ if count % 2 == 0:
119
+ count += 1
120
+ axis = np.linspace(-cover_radius, cover_radius, count, dtype=np.float64)
121
+ if count % 2 == 1:
122
+ axis[count // 2] = 0.0
123
+ axes = [axis.copy() for _ in range(width)]
124
+ for dimension, axis in zip(component.dims, axes, strict=True):
125
+ output[dimension] = axis
126
+ if any(axis is None for axis in output):
127
+ raise RuntimeError(
128
+ "internal error: primitive construction left an unassigned dimension"
129
+ )
130
+ store = PrimitiveStore([axis for axis in output if axis is not None])
131
+ missing_zero = [
132
+ dimension
133
+ for dimension, axis in enumerate(store.values)
134
+ if not np.any(np.asarray(axis) == 0)
135
+ ]
136
+ if missing_zero:
137
+ raise ConfigError(
138
+ "every fitted primitive axis must contain exact zero; missing dimension(s) "
139
+ f"{missing_zero}. Ensure each configured domain contains zero."
140
+ )
141
+ return store
142
+
143
+
144
+ def _validate_component_support(
145
+ active_dims: tuple[int, ...], config: ActionCodec2Config, *, index: int
146
+ ) -> None:
147
+ active = set(active_dims)
148
+ for name, component in config.physical.components.items():
149
+ present = active.intersection(component.dims)
150
+ if component.space == "so3" and present and present != set(component.dims):
151
+ raise ValueError(
152
+ f"trajectory {index} activates only part of SO(3) component {name!r}"
153
+ )
154
+
155
+
156
+ def _length_buckets(
157
+ pending: Sequence[tuple[np.ndarray, tuple[int, ...], np.ndarray | None]],
158
+ *,
159
+ maximum_ratio: float = 1.25,
160
+ ) -> list[list[int]]:
161
+
162
+ ordered = sorted(range(len(pending)), key=lambda index: len(pending[index][0]))
163
+ buckets: list[list[int]] = []
164
+ current: list[int] = []
165
+ shortest = 0
166
+ for index in ordered:
167
+ length = len(pending[index][0])
168
+ if current and length > shortest * maximum_ratio:
169
+ buckets.append(current)
170
+ current = []
171
+ if not current:
172
+ shortest = length
173
+ current.append(index)
174
+ if current:
175
+ buckets.append(current)
176
+ return buckets
177
+
178
+
179
+ class _CellCorpus(Sequence[np.ndarray]):
180
+
181
+
182
+ _actioncodec2_validated = True
183
+
184
+ def __init__(self, chunks: list[np.ndarray], offsets: list[np.ndarray]) -> None:
185
+ self.chunks: list[np.ndarray | None] = list(chunks)
186
+ self.offsets = tuple(offsets)
187
+ counts = np.asarray([len(value) - 1 for value in offsets], dtype=np.int64)
188
+ self._episode_ends = np.cumsum(counts)
189
+
190
+ def __len__(self) -> int:
191
+ return int(self._episode_ends[-1]) if len(self._episode_ends) else 0
192
+
193
+ def __getitem__(self, index: int) -> np.ndarray:
194
+ if not isinstance(index, int):
195
+ raise TypeError("cell corpus indices must be integers")
196
+ if index < 0:
197
+ index += len(self)
198
+ if not 0 <= index < len(self):
199
+ raise IndexError(index)
200
+ chunk = bisect_right(self._episode_ends, index)
201
+ previous = 0 if chunk == 0 else int(self._episode_ends[chunk - 1])
202
+ local = index - previous
203
+ start, stop = self.offsets[chunk][local : local + 2]
204
+ values = self.chunks[chunk]
205
+ if values is None:
206
+ raise RuntimeError("cell corpus chunk has already been consumed")
207
+ return values[int(start) : int(stop)]
208
+
209
+ def __iter__(self):
210
+ for chunk, offsets in zip(self.chunks, self.offsets, strict=True):
211
+ if chunk is None:
212
+ raise RuntimeError("cell corpus chunk has already been consumed")
213
+ for start, stop in zip(offsets[:-1], offsets[1:], strict=True):
214
+ yield chunk[int(start) : int(stop)]
215
+
216
+ def release_chunk(self, index: int) -> None:
217
+ """Release a replayed chunk while retaining compact episode metadata."""
218
+ self.chunks[index] = None
219
+
220
+
221
+ def _quantize_pending_batch(
222
+ pending: Sequence[tuple[np.ndarray, tuple[int, ...], np.ndarray | None]],
223
+ physical: PhysicalCodec,
224
+ cell_dtype: type[np.signedinteger],
225
+ ) -> list[np.ndarray]:
226
+
227
+ result: list[np.ndarray | None] = [None] * len(pending)
228
+ dimension = physical.config.physical.input_dim
229
+ for indices in _length_buckets(pending):
230
+ length = max(len(pending[index][0]) for index in indices)
231
+ batch = np.zeros((len(indices), length, dimension), dtype=np.float32)
232
+ active_by_item: list[tuple[int, ...]] = []
233
+ for local, index in enumerate(indices):
234
+ trajectory, active_dims, _ = pending[index]
235
+ item = batch[local, : len(trajectory)]
236
+ item[:, active_dims] = trajectory
237
+ active_by_item.append(active_dims)
238
+ boundaries = [pending[index][2] for index in indices]
239
+ previous = (
240
+ None
241
+ if all(value is None for value in boundaries)
242
+ else np.stack(
243
+ [
244
+ np.zeros(dimension, dtype=np.float32) if value is None else value
245
+ for value in boundaries
246
+ ]
247
+ )
248
+ )
249
+ encoded = physical.encode_batch(batch, previous_first_order=previous).astype(
250
+ cell_dtype, copy=False
251
+ )
252
+ for local, index in enumerate(indices):
253
+ item_length = len(pending[index][0])
254
+ dense = np.full((item_length, dimension), -1, dtype=cell_dtype)
255
+ dense[:, active_by_item[local]] = encoded[local][
256
+ :item_length, active_by_item[local]
257
+ ]
258
+ result[index] = dense
259
+ if any(value is None for value in result):
260
+ raise RuntimeError("internal error: trajectory batch was not fully quantized")
261
+ return [value for value in result if value is not None]
262
+
263
+
264
+ def _quantize_trajectories(
265
+ trajectories: Iterable[TrajectoryLike],
266
+ physical: PhysicalCodec,
267
+ *,
268
+ batch_size: int,
269
+ max_cell_bytes: int | None,
270
+ ) -> tuple[_CellCorpus, int, int, dict[int, int]]:
271
+
272
+ if batch_size < 1:
273
+ raise ValueError("batch_size must be positive")
274
+ cell_chunks: list[np.ndarray] = []
275
+ offset_chunks: list[np.ndarray] = []
276
+ cell_parts: list[np.ndarray] = []
277
+ cell_lengths: list[int] = []
278
+ chunk_actions = 0
279
+ action_count = 0
280
+ active_value_count = 0
281
+ arity_counts: dict[int, int] = defaultdict(int)
282
+ source_index = 0
283
+ retained_cell_bytes = 0
284
+ cell_dtype = np.int16 if max(physical.bins) <= np.iinfo(np.int16).max else np.int32
285
+ iterator = iter(trajectories)
286
+
287
+ def flush_cells() -> None:
288
+ nonlocal chunk_actions, retained_cell_bytes
289
+ if not cell_parts:
290
+ return
291
+ chunk = np.concatenate(cell_parts, axis=0)
292
+ projected = retained_cell_bytes + chunk.nbytes
293
+ if max_cell_bytes is not None and projected > max_cell_bytes:
294
+ raise MemoryError(
295
+ "quantized ActionCodec2 corpus would retain at least "
296
+ f"{projected / 2**30:.2f} GiB, exceeding the configured "
297
+ f"{max_cell_bytes / 2**30:.2f} GiB limit; use stratified fitting, "
298
+ "lower coverage, or explicitly raise --max-cell-memory-gib"
299
+ )
300
+ cell_chunks.append(chunk)
301
+ retained_cell_bytes = projected
302
+ offset_chunks.append(
303
+ np.concatenate(([0], np.cumsum(cell_lengths, dtype=np.int64)))
304
+ )
305
+ cell_parts.clear()
306
+ cell_lengths.clear()
307
+ chunk_actions = 0
308
+
309
+ while True:
310
+ pending: list[tuple[np.ndarray, tuple[int, ...], np.ndarray | None]] = []
311
+ for _ in range(batch_size):
312
+ try:
313
+ raw = next(iterator)
314
+ except StopIteration:
315
+ break
316
+ trajectory, active_dims = prepare_trajectory(
317
+ raw,
318
+ index=source_index,
319
+ dimension=physical.config.physical.input_dim,
320
+ dtype=np.float32,
321
+ )
322
+ _validate_component_support(
323
+ active_dims, physical.config, index=source_index
324
+ )
325
+ source_index += 1
326
+ action_count += len(trajectory)
327
+ active_value_count += len(trajectory) * len(active_dims)
328
+ arity_counts[len(active_dims)] += 1
329
+ pending.append(
330
+ (
331
+ trajectory,
332
+ active_dims,
333
+ trajectory_boundary(raw, physical.config, active_dims),
334
+ )
335
+ )
336
+ if not pending:
337
+ break
338
+
339
+ for value in _quantize_pending_batch(pending, physical, cell_dtype):
340
+ cell_parts.append(value)
341
+ cell_lengths.append(len(value))
342
+ chunk_actions += len(value)
343
+ if chunk_actions >= 1_000_000:
344
+ flush_cells()
345
+ flush_cells()
346
+ cells = _CellCorpus(cell_chunks, offset_chunks)
347
+ if not cells:
348
+ raise ValueError("training requires at least one complete trajectory")
349
+ return cells, action_count, active_value_count, dict(sorted(arity_counts.items()))
350
+
351
+
352
+ def _aggregate_trajectory_records(
353
+ cells: Sequence[np.ndarray], bins: Sequence[int], action_count: int
354
+ ) -> tuple[np.ndarray, np.ndarray]:
355
+
356
+ del action_count
357
+ dimension = len(tuple(bins))
358
+ widths = tuple(int(count).bit_length() for count in bins)
359
+ if sum(widths) <= 64:
360
+ return _aggregate_packed_records(cells, bins, widths)
361
+ counts_by_row: dict[bytes, int] = defaultdict(int)
362
+ pending: list[np.ndarray] = []
363
+ pending_rows = 0
364
+ chunk_rows = 1_000_000
365
+
366
+ def flush() -> None:
367
+ nonlocal pending_rows
368
+ if not pending:
369
+ return
370
+ block = (
371
+ np.asarray(pending[0], dtype=np.int32)
372
+ if len(pending) == 1
373
+ else np.concatenate(pending, axis=0).astype(np.int32, copy=False)
374
+ )
375
+ unique, counts = aggregate_records(block, bins)
376
+ for row, count in zip(unique.astype(np.int32, copy=False), counts, strict=True):
377
+ counts_by_row[row.tobytes()] += int(count)
378
+ pending.clear()
379
+ pending_rows = 0
380
+
381
+ for trajectory in cells:
382
+ pending.append(np.asarray(trajectory, dtype=np.int32))
383
+ pending_rows += len(trajectory)
384
+ if pending_rows >= chunk_rows:
385
+ flush()
386
+ flush()
387
+ keys = sorted(counts_by_row)
388
+ records = np.frombuffer(b"".join(keys), dtype=np.int32).reshape(-1, dimension)
389
+ weights = np.fromiter(
390
+ (counts_by_row[key] for key in keys), dtype=np.int64, count=len(keys)
391
+ )
392
+ return records, weights
393
+
394
+
395
+ def _aggregate_packed_records(
396
+ cells: Sequence[np.ndarray], bins: Sequence[int], widths: Sequence[int]
397
+ ) -> tuple[np.ndarray, np.ndarray]:
398
+
399
+ counts_by_key: dict[int, int] = defaultdict(int)
400
+ pending: list[np.ndarray] = []
401
+ pending_rows = 0
402
+ chunk_rows = 1_000_000
403
+
404
+ def flush() -> None:
405
+ nonlocal pending_rows
406
+ if not pending:
407
+ return
408
+ block = pending[0] if len(pending) == 1 else np.concatenate(pending, axis=0)
409
+ keys = np.zeros(len(block), dtype=np.uint64)
410
+ shift = 0
411
+ for dimension, width in enumerate(widths):
412
+ encoded = np.asarray(block[:, dimension], dtype=np.int64) + 1
413
+ keys |= encoded.astype(np.uint64) << np.uint64(shift)
414
+ shift += width
415
+ unique, counts = np.unique(keys, return_counts=True)
416
+ for key, count in zip(unique, counts, strict=True):
417
+ counts_by_key[int(key)] += int(count)
418
+ pending.clear()
419
+ pending_rows = 0
420
+
421
+ for trajectory in cells:
422
+ pending.append(trajectory)
423
+ pending_rows += len(trajectory)
424
+ if pending_rows >= chunk_rows:
425
+ flush()
426
+ flush()
427
+
428
+ keys = np.fromiter(sorted(counts_by_key), dtype=np.uint64, count=len(counts_by_key))
429
+ records = np.empty((len(keys), len(tuple(bins))), dtype=np.int32)
430
+ shift = 0
431
+ for dimension, width in enumerate(widths):
432
+ mask = np.uint64((1 << width) - 1)
433
+ records[:, dimension] = ((keys >> np.uint64(shift)) & mask).astype(np.int32) - 1
434
+ shift += width
435
+ weights = np.fromiter(
436
+ (counts_by_key[int(key)] for key in keys), dtype=np.int64, count=len(keys)
437
+ )
438
+ return records, weights
439
+
440
+
441
+ def _fit_report(
442
+ config: ActionCodec2Config,
443
+ primitives: PrimitiveStore,
444
+ codec: ActionCodec2Processor,
445
+ *,
446
+ trajectory_count: int,
447
+ actions: int,
448
+ active_values: int,
449
+ arity_counts: dict[int, int],
450
+ cell_dtype: str,
451
+ unique_records: int,
452
+ spatial_report: dict[str, object],
453
+ temporal_report: dict[str, object],
454
+ timestamps: tuple[float, float, float, float, float, float],
455
+ ) -> dict[str, object]:
456
+ started, primitive_done, quantized, aggregated, spatial_done, finished = timestamps
457
+ scheme = config.set_bpe.scheme
458
+ stages: dict[str, object] = (
459
+ {"bpe": temporal_report}
460
+ if scheme == "bpe"
461
+ else {"spatial": spatial_report, "temporal": temporal_report}
462
+ )
463
+ return {
464
+ "trajectories": trajectory_count,
465
+ "actions": actions,
466
+ "active_values": active_values,
467
+ "trajectory_arities": {str(key): value for key, value in arity_counts.items()},
468
+ "bpe_boundary": "complete_component_trajectory",
469
+ "bpe_training_domain": "quantized_primitive_cells",
470
+ "physical_boundary": (
471
+ "explicit_previous_first_order"
472
+ if codec.requires_previous_first_order
473
+ else "zero_position_identity_orientation"
474
+ ),
475
+ "component_primitive_orders": {
476
+ name: component.primitive_order
477
+ for name, component in config.physical.components.items()
478
+ },
479
+ "allow_cross_component_merge": config.set_bpe.allow_cross_component_merge,
480
+ "lookahead": config.set_bpe.lookahead,
481
+ "physical_bins": list(primitives.bins),
482
+ "cell_dtype": cell_dtype,
483
+ "unique_spatial_records": unique_records,
484
+ "primitive_atoms": sum(primitives.bins),
485
+ "tokenization_scheme": scheme,
486
+ **stages,
487
+ "vocab_size": codec.vocab_size,
488
+ "tokens_per_action": float(temporal_report["tokens_per_action"]),
489
+ "seconds": {
490
+ "primitives": primitive_done - started,
491
+ "quantize": quantized - primitive_done,
492
+ "aggregate": aggregated - quantized,
493
+ "spatial_train": spatial_done - aggregated,
494
+ "spatial": spatial_done - quantized,
495
+ "temporal": finished - spatial_done,
496
+ "total": finished - started,
497
+ },
498
+ }
499
+
500
+
501
+ def _fit_set_bpe(
502
+ cells: Sequence[np.ndarray],
503
+ config: ActionCodec2Config,
504
+ primitives: PrimitiveStore,
505
+ actions: int,
506
+ *,
507
+ backend: str,
508
+ threads: int,
509
+ progress: bool,
510
+ ) -> tuple[SetBPESerializer, dict[str, object], dict[str, object], int, float, float]:
511
+ records, weights = _aggregate_trajectory_records(cells, primitives.bins, actions)
512
+ aggregated = time.perf_counter()
513
+ bpe = config.train.bpe if config.train is not None else None
514
+ assert bpe is not None
515
+ spatial, spatial_report = train_spatial(
516
+ records,
517
+ weights,
518
+ primitives.bins,
519
+ vocab_size=bpe.spatial_vocab_size,
520
+ backend=backend,
521
+ threads=threads,
522
+ progress=progress,
523
+ )
524
+ spatial_done = time.perf_counter()
525
+ vocabulary, temporal_report = train_temporal(
526
+ cells,
527
+ spatial,
528
+ vocab_size=bpe.vocab_size,
529
+ lookahead=config.set_bpe.lookahead,
530
+ backend=backend,
531
+ threads=threads,
532
+ progress=progress,
533
+ )
534
+ serializer = SetBPESerializer(
535
+ bins=primitives.bins,
536
+ lookahead=config.set_bpe.lookahead,
537
+ spatial_merges=spatial.merges,
538
+ temporal_merges=vocabulary.merges,
539
+ )
540
+ return (
541
+ serializer,
542
+ spatial_report,
543
+ temporal_report,
544
+ len(records),
545
+ aggregated,
546
+ spatial_done,
547
+ )
548
+
549
+
550
+ def _fit_bpe(
551
+ cells: Sequence[np.ndarray],
552
+ config: ActionCodec2Config,
553
+ primitives: PrimitiveStore,
554
+ *,
555
+ backend: str,
556
+ threads: int,
557
+ progress: bool,
558
+ ) -> tuple[BPESerializer, dict[str, object], dict[str, object], int, float, float]:
559
+
560
+ started = time.perf_counter()
561
+ bpe = config.train.bpe if config.train is not None else None
562
+ assert bpe is not None
563
+ vocabulary, report = train_bpe(
564
+ cells,
565
+ primitives.bins,
566
+ vocab_size=bpe.vocab_size,
567
+ max_token_cells=config.set_bpe.lookahead * config.physical.input_dim,
568
+ backend=backend,
569
+ threads=threads,
570
+ progress=progress,
571
+ )
572
+ serializer = BPESerializer(
573
+ bins=primitives.bins,
574
+ lookahead=config.set_bpe.lookahead,
575
+ merges=vocabulary.merges,
576
+ )
577
+ return serializer, {}, report, 0, started, started
578
+
579
+
580
+ def _validate_fit_options(
581
+ config: ActionCodec2Config, max_cell_bytes: int | None
582
+ ) -> None:
583
+ if config.train is None:
584
+ raise ConfigError("config.train is required for fitting")
585
+ if max_cell_bytes is not None and (
586
+ isinstance(max_cell_bytes, bool)
587
+ or not isinstance(max_cell_bytes, int)
588
+ or max_cell_bytes < 1
589
+ ):
590
+ raise ValueError("max_cell_bytes must be a positive integer or None")
591
+ if config.set_bpe.allow_cross_component_merge:
592
+ raise ConfigError(
593
+ "allow_cross_component_merge=true requires the future multi-component "
594
+ "corpus representation; v1 fitting supports the default isolated boundary only"
595
+ )
596
+ if config.set_bpe.scheme == "bpe" and config.train.bpe.spatial_vocab_size != (
597
+ config.train.bpe.vocab_size
598
+ ):
599
+ raise ConfigError(
600
+ "set_bpe.scheme=bpe has a single merge stage; set "
601
+ "train.bpe.spatial_vocab_size equal to train.bpe.vocab_size to make the "
602
+ "unused split explicit"
603
+ )
604
+
605
+
606
+ class ProfileTrainer:
607
+ "Internal whole-trajectory fitter for one physical profile."
608
+
609
+ def __init__(self, config: ActionCodec2Config) -> None:
610
+ self.config = config
611
+
612
+ def fit(
613
+ self,
614
+ trajectories: Iterable[TrajectoryLike],
615
+ *,
616
+ backend: str = "auto",
617
+ threads: int = 0,
618
+ batch_size: int = 256,
619
+ progress: bool = False,
620
+ max_cell_bytes: int | None = None,
621
+ ) -> tuple[ActionCodec2Processor, dict[str, object]]:
622
+ "Fit primitives and the configured tokenization scheme on whole trajectories."
623
+ config = self.config
624
+ _validate_fit_options(config, max_cell_bytes)
625
+ started = time.perf_counter()
626
+ primitives = build_primitives(config)
627
+ primitive_done = time.perf_counter()
628
+
629
+ atoms = sum(primitives.bins)
630
+ bpe = config.train.bpe
631
+ if bpe.spatial_vocab_size < atoms:
632
+ raise ConfigError(
633
+ f"train.bpe.spatial_vocab_size must be at least the {atoms} primitive atoms"
634
+ )
635
+
636
+ physical = PhysicalCodec(config, primitives)
637
+ cells, actions, active_values, arity_counts = _quantize_trajectories(
638
+ trajectories,
639
+ physical,
640
+ batch_size=batch_size,
641
+ max_cell_bytes=max_cell_bytes,
642
+ )
643
+ trajectory_count = len(cells)
644
+ first_cell_dtype = str(cells[0].dtype)
645
+ quantized = time.perf_counter()
646
+
647
+ if config.set_bpe.scheme == "bpe":
648
+ fitted = _fit_bpe(
649
+ cells,
650
+ config,
651
+ primitives,
652
+ backend=backend,
653
+ threads=threads,
654
+ progress=progress,
655
+ )
656
+ else:
657
+ fitted = _fit_set_bpe(
658
+ cells,
659
+ config,
660
+ primitives,
661
+ actions,
662
+ backend=backend,
663
+ threads=threads,
664
+ progress=progress,
665
+ )
666
+ (
667
+ serializer,
668
+ spatial_report,
669
+ temporal_report,
670
+ unique_records,
671
+ aggregated,
672
+ spatial_done,
673
+ ) = fitted
674
+ codec = ActionCodec2Processor(config, primitives, serializer)
675
+ finished = time.perf_counter()
676
+
677
+ report = _fit_report(
678
+ config,
679
+ primitives,
680
+ codec,
681
+ trajectory_count=trajectory_count,
682
+ actions=actions,
683
+ active_values=active_values,
684
+ arity_counts=arity_counts,
685
+ cell_dtype=first_cell_dtype,
686
+ unique_records=unique_records,
687
+ spatial_report=spatial_report,
688
+ temporal_report=temporal_report,
689
+ timestamps=(
690
+ started,
691
+ primitive_done,
692
+ quantized,
693
+ aggregated,
694
+ spatial_done,
695
+ finished,
696
+ ),
697
+ )
698
+ return codec, report
setup.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import setup
2
+ from pybind11.setup_helpers import Pybind11Extension, build_ext
3
+
4
+ native = "actioncodec2_kernels"
5
+ setup(
6
+ ext_modules=[Pybind11Extension(
7
+ "actioncodec2_kernels._actioncodec2_native",
8
+ sources=[f"{native}/{name}.cpp" for name in (
9
+ "module", "physical", "so3", "setbpe", "resample", "reconstruct", "grammar"
10
+ )],
11
+ include_dirs=[native],
12
+ depends=[f"{native}/{name}.hpp" for name in ("parallel", "quaternion")],
13
+ extra_compile_args=["-O3", "-fvisibility=hidden", "-ffp-contract=off"],
14
+ cxx_std=17,
15
+ )],
16
+ cmdclass={"build_ext": build_ext},
17
+ )
variants/extended-1st-10mm-16k/config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "model_type": "actioncodec2",
3
+ "processor_class": "ActionCodec2"
4
+ }
variants/extended-1st-10mm-16k/processor_config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "processor_class": "ActionCodec2",
3
+ "action_space": "single_eef_delta",
4
+ "fitted_profiles": [
5
+ "eef",
6
+ "joint"
7
+ ],
8
+ "auto_map": {
9
+ "AutoProcessor": "processing_actioncodec2.ActionCodec2"
10
+ }
11
+ }
variants/extended-1st-10mm-16k/profiles/eef/bpe.json ADDED
The diff for this file is too large to render. See raw diff
 
variants/extended-1st-10mm-16k/profiles/eef/config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "model_type": "actioncodec2_unified",
3
+ "processor_class": "ActionCodec2Processor"
4
+ }
variants/extended-1st-10mm-16k/profiles/eef/config.yaml ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model_type: actioncodec2_unified
2
+ config_version: 1
3
+ physical:
4
+ input_dim: 7
5
+ components:
6
+ translation_m:
7
+ dims:
8
+ start: 0
9
+ stop: 3
10
+ space: additive
11
+ input_order: 0
12
+ primitive_order: 1
13
+ input_transform:
14
+ scale: 1.0
15
+ clip: false
16
+ constraints:
17
+ order_0:
18
+ min: -2.0
19
+ max: 2.0
20
+ order_1:
21
+ min: -0.08
22
+ max: 0.08
23
+ distortion_limit: 0.01
24
+ primitive_selection: smooth
25
+ orientation_so3:
26
+ dims:
27
+ start: 3
28
+ stop: 6
29
+ space: so3
30
+ input_order: 0
31
+ primitive_order: 1
32
+ input_transform:
33
+ scale: 1.0
34
+ clip: false
35
+ constraints:
36
+ order_0:
37
+ min: -3.141592653589793
38
+ max: 3.141592653589793
39
+ order_1:
40
+ min: -0.25
41
+ max: 0.25
42
+ distortion_limit: 0.04
43
+ primitive_selection: smooth
44
+ representation: rotation_vector
45
+ multiplication: right
46
+ gripper_open_command:
47
+ dims:
48
+ - 6
49
+ space: additive
50
+ input_order: 0
51
+ primitive_order: 0
52
+ input_transform:
53
+ scale: 1.0
54
+ clip: true
55
+ constraints:
56
+ order_0:
57
+ min: 0.0
58
+ max: 1.0
59
+ distortion_limit: 0.5
60
+ primitive_selection: nearest
61
+ set_bpe:
62
+ lookahead: 8
63
+ allow_cross_component_merge: false
64
+ train:
65
+ primitives:
66
+ zero_primitive_components:
67
+ - translation_m
68
+ - orientation_so3
69
+ bpe:
70
+ spatial_vocab_size: 1024
71
+ vocab_size: 16384
72
+ quality_control:
73
+ hard_order_1_scale: 3.0
74
+ min_segment_length: 2
variants/extended-1st-10mm-16k/profiles/eef/primitives.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6d99d92d73fbfe2d96ff317f0eef2d0d2ba6420ac8049c47ae0cc387913854bf
3
+ size 2510
variants/extended-1st-10mm-16k/profiles/eef/processor_config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "processor_class": "ActionCodec2Processor"
3
+ }
variants/extended-1st-10mm-16k/profiles/joint/bpe.json ADDED
The diff for this file is too large to render. See raw diff
 
variants/extended-1st-10mm-16k/profiles/joint/config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "model_type": "actioncodec2_unified",
3
+ "processor_class": "ActionCodec2Processor"
4
+ }
variants/extended-1st-10mm-16k/profiles/joint/config.yaml ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model_type: actioncodec2_unified
2
+ config_version: 1
3
+ physical:
4
+ input_dim: 8
5
+ components:
6
+ generalized_joint_position:
7
+ dims:
8
+ start: 0
9
+ stop: 7
10
+ space: additive
11
+ input_order: 0
12
+ primitive_order: 1
13
+ input_transform:
14
+ scale: 1.0
15
+ clip: false
16
+ constraints:
17
+ order_0:
18
+ min: -6.283185307179586
19
+ max: 6.283185307179586
20
+ order_1:
21
+ min: -0.35
22
+ max: 0.35
23
+ distortion_limit:
24
+ - 0.008
25
+ - 0.008
26
+ - 0.012
27
+ - 0.015
28
+ - 0.03
29
+ - 0.04
30
+ - 0.04
31
+ primitive_selection: smooth
32
+ gripper_open_command:
33
+ dims:
34
+ - 7
35
+ space: additive
36
+ input_order: 0
37
+ primitive_order: 0
38
+ input_transform:
39
+ scale: 1.0
40
+ clip: true
41
+ constraints:
42
+ order_0:
43
+ min: 0.0
44
+ max: 1.0
45
+ distortion_limit: 0.5
46
+ primitive_selection: nearest
47
+ set_bpe:
48
+ lookahead: 8
49
+ allow_cross_component_merge: false
50
+ train:
51
+ primitives:
52
+ zero_primitive_components:
53
+ - generalized_joint_position
54
+ bpe:
55
+ spatial_vocab_size: 1024
56
+ vocab_size: 16384
57
+ quality_control:
58
+ hard_order_1_scale: 3.0
59
+ min_segment_length: 2
variants/extended-1st-10mm-16k/profiles/joint/primitives.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7424271014df93fbf66a292a1b04aa0f8b999c7c3cf06f965278149d0e41f3e7
3
+ size 2834
variants/extended-1st-10mm-16k/profiles/joint/processor_config.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "processor_class": "ActionCodec2Processor"
3
+ }
variants/extended-1st-10mm-16k/router_config.yaml ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model_type: actioncodec2_router
2
+ config_version: 3
3
+ profiles:
4
+ - name: joint
5
+ token_offset: 0
6
+ token_budget: 16384
7
+ artifact_fingerprint: 309e93d0fa330d19b4e597b5b82ef76cce820f536d6f23a31d103e746ab29e86
8
+ - name: eef
9
+ token_offset: 16384
10
+ token_budget: 16384
11
+ artifact_fingerprint: 10eec18e85e63394e59a20ca958f20af761b72575b5b8c9403bc230f4acaba28
12
+ presets:
13
+ - name: single_eef_abs
14
+ input_dim: 7
15
+ input: absolute
16
+ source_hz: 15.0
17
+ target_hz: 15.0
18
+ interpolation: cubic
19
+ components:
20
+ arm:
21
+ space: eef
22
+ position_dims:
23
+ - 0
24
+ - 1
25
+ - 2
26
+ rotation_dims:
27
+ - 3
28
+ - 4
29
+ - 5
30
+ rotation_format: rotation_vector
31
+ gripper_dim: 6
32
+ gripper_threshold: 0.8
33
+ gripper_open_is_high: true
34
+ - name: single_joint6_abs
35
+ input_dim: 7
36
+ input: absolute
37
+ source_hz: 15.0
38
+ target_hz: 15.0
39
+ interpolation: cubic
40
+ components:
41
+ arm:
42
+ space: joint
43
+ joint_dims:
44
+ - 0
45
+ - 1
46
+ - 2
47
+ - 3
48
+ - 4
49
+ - 5
50
+ gripper_dim: 6
51
+ gripper_threshold: 0.8
52
+ gripper_open_is_high: true
53
+ - name: single_joint7_abs
54
+ input_dim: 8
55
+ input: absolute
56
+ source_hz: 15.0
57
+ target_hz: 15.0
58
+ interpolation: cubic
59
+ components:
60
+ arm:
61
+ space: joint
62
+ joint_dims:
63
+ - 0
64
+ - 1
65
+ - 2
66
+ - 3
67
+ - 4
68
+ - 5
69
+ - 6
70
+ gripper_dim: 7
71
+ gripper_threshold: 0.8
72
+ gripper_open_is_high: true
73
+ - name: single_eef_delta
74
+ input_dim: 7
75
+ input: delta
76
+ source_hz: 15.0
77
+ target_hz: 15.0
78
+ interpolation: cubic
79
+ components:
80
+ arm:
81
+ space: eef
82
+ position_dims:
83
+ - 0
84
+ - 1
85
+ - 2
86
+ rotation_dims:
87
+ - 3
88
+ - 4
89
+ - 5
90
+ rotation_format: rotation_vector
91
+ gripper_dim: 6
92
+ gripper_threshold: 0.8
93
+ gripper_open_is_high: true
94
+ - name: single_joint6_delta
95
+ input_dim: 7
96
+ input: delta
97
+ source_hz: 15.0
98
+ target_hz: 15.0
99
+ interpolation: cubic
100
+ components:
101
+ arm:
102
+ space: joint
103
+ joint_dims:
104
+ - 0
105
+ - 1
106
+ - 2
107
+ - 3
108
+ - 4
109
+ - 5
110
+ gripper_dim: 6
111
+ gripper_threshold: 0.8
112
+ gripper_open_is_high: true
113
+ - name: single_joint7_delta
114
+ input_dim: 8
115
+ input: delta
116
+ source_hz: 15.0
117
+ target_hz: 15.0
118
+ interpolation: cubic
119
+ components:
120
+ arm:
121
+ space: joint
122
+ joint_dims:
123
+ - 0
124
+ - 1
125
+ - 2
126
+ - 3
127
+ - 4
128
+ - 5
129
+ - 6
130
+ gripper_dim: 7
131
+ gripper_threshold: 0.8
132
+ gripper_open_is_high: true
133
+ - name: dual_eef_abs
134
+ input_dim: 14
135
+ input: absolute
136
+ source_hz: 15.0
137
+ target_hz: 15.0
138
+ interpolation: cubic
139
+ components:
140
+ left_arm:
141
+ space: eef
142
+ position_dims:
143
+ - 0
144
+ - 1
145
+ - 2
146
+ rotation_dims:
147
+ - 3
148
+ - 4
149
+ - 5
150
+ rotation_format: rotation_vector
151
+ gripper_dim: 6
152
+ gripper_threshold: 0.8
153
+ gripper_open_is_high: true
154
+ right_arm:
155
+ space: eef
156
+ position_dims:
157
+ - 7
158
+ - 8
159
+ - 9
160
+ rotation_dims:
161
+ - 10
162
+ - 11
163
+ - 12
164
+ rotation_format: rotation_vector
165
+ gripper_dim: 13
166
+ gripper_threshold: 0.8
167
+ gripper_open_is_high: true
168
+ - name: dual_joint6_abs
169
+ input_dim: 14
170
+ input: absolute
171
+ source_hz: 15.0
172
+ target_hz: 15.0
173
+ interpolation: cubic
174
+ components:
175
+ left_arm:
176
+ space: joint
177
+ joint_dims:
178
+ - 0
179
+ - 1
180
+ - 2
181
+ - 3
182
+ - 4
183
+ - 5
184
+ gripper_dim: 6
185
+ gripper_threshold: 0.8
186
+ gripper_open_is_high: true
187
+ right_arm:
188
+ space: joint
189
+ joint_dims:
190
+ - 7
191
+ - 8
192
+ - 9
193
+ - 10
194
+ - 11
195
+ - 12
196
+ gripper_dim: 13
197
+ gripper_threshold: 0.8
198
+ gripper_open_is_high: true
199
+ - name: dual_joint7_abs
200
+ input_dim: 16
201
+ input: absolute
202
+ source_hz: 15.0
203
+ target_hz: 15.0
204
+ interpolation: cubic
205
+ components:
206
+ left_arm:
207
+ space: joint
208
+ joint_dims:
209
+ - 0
210
+ - 1
211
+ - 2
212
+ - 3
213
+ - 4
214
+ - 5
215
+ - 6
216
+ gripper_dim: 7
217
+ gripper_threshold: 0.8
218
+ gripper_open_is_high: true
219
+ right_arm:
220
+ space: joint
221
+ joint_dims:
222
+ - 8
223
+ - 9
224
+ - 10
225
+ - 11
226
+ - 12
227
+ - 13
228
+ - 14
229
+ gripper_dim: 15
230
+ gripper_threshold: 0.8
231
+ gripper_open_is_high: true
232
+ - name: dual_eef_delta
233
+ input_dim: 14
234
+ input: delta
235
+ source_hz: 15.0
236
+ target_hz: 15.0
237
+ interpolation: cubic
238
+ components:
239
+ left_arm:
240
+ space: eef
241
+ position_dims:
242
+ - 0
243
+ - 1
244
+ - 2
245
+ rotation_dims:
246
+ - 3
247
+ - 4
248
+ - 5
249
+ rotation_format: rotation_vector
250
+ gripper_dim: 6
251
+ gripper_threshold: 0.8
252
+ gripper_open_is_high: true
253
+ right_arm:
254
+ space: eef
255
+ position_dims:
256
+ - 7
257
+ - 8
258
+ - 9
259
+ rotation_dims:
260
+ - 10
261
+ - 11
262
+ - 12
263
+ rotation_format: rotation_vector
264
+ gripper_dim: 13
265
+ gripper_threshold: 0.8
266
+ gripper_open_is_high: true
267
+ - name: dual_joint6_delta
268
+ input_dim: 14
269
+ input: delta
270
+ source_hz: 15.0
271
+ target_hz: 15.0
272
+ interpolation: cubic
273
+ components:
274
+ left_arm:
275
+ space: joint
276
+ joint_dims:
277
+ - 0
278
+ - 1
279
+ - 2
280
+ - 3
281
+ - 4
282
+ - 5
283
+ gripper_dim: 6
284
+ gripper_threshold: 0.8
285
+ gripper_open_is_high: true
286
+ right_arm:
287
+ space: joint
288
+ joint_dims:
289
+ - 7
290
+ - 8
291
+ - 9
292
+ - 10
293
+ - 11
294
+ - 12
295
+ gripper_dim: 13
296
+ gripper_threshold: 0.8
297
+ gripper_open_is_high: true
298
+ - name: dual_joint7_delta
299
+ input_dim: 16
300
+ input: delta
301
+ source_hz: 15.0
302
+ target_hz: 15.0
303
+ interpolation: cubic
304
+ components:
305
+ left_arm:
306
+ space: joint
307
+ joint_dims:
308
+ - 0
309
+ - 1
310
+ - 2
311
+ - 3
312
+ - 4
313
+ - 5
314
+ - 6
315
+ gripper_dim: 7
316
+ gripper_threshold: 0.8
317
+ gripper_open_is_high: true
318
+ right_arm:
319
+ space: joint
320
+ joint_dims:
321
+ - 8
322
+ - 9
323
+ - 10
324
+ - 11
325
+ - 12
326
+ - 13
327
+ - 14
328
+ gripper_dim: 15
329
+ gripper_threshold: 0.8
330
+ gripper_open_is_high: true
331
+ - name: single_joint6_abs_continuous_gripper
332
+ input_dim: 6
333
+ input: absolute
334
+ source_hz: 15.0
335
+ target_hz: 15.0
336
+ interpolation: cubic
337
+ components:
338
+ arm:
339
+ space: joint
340
+ joint_dims:
341
+ - 0
342
+ - 1
343
+ - 2
344
+ - 3
345
+ - 4
346
+ - 5
variants/extended-1st-10mm-1k/config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "model_type": "actioncodec2",
3
+ "processor_class": "ActionCodec2"
4
+ }
variants/extended-1st-10mm-1k/processor_config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "processor_class": "ActionCodec2",
3
+ "action_space": "single_eef_delta",
4
+ "fitted_profiles": [
5
+ "eef",
6
+ "joint"
7
+ ],
8
+ "auto_map": {
9
+ "AutoProcessor": "processing_actioncodec2.ActionCodec2"
10
+ }
11
+ }
variants/extended-1st-10mm-1k/profiles/eef/bpe.json ADDED
@@ -0,0 +1,4222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "spatial_merges": [
3
+ [
4
+ 4,
5
+ 13
6
+ ],
7
+ [
8
+ 22,
9
+ 84
10
+ ],
11
+ [
12
+ 38,
13
+ 61
14
+ ],
15
+ [
16
+ 98,
17
+ 99
18
+ ],
19
+ [
20
+ 100,
21
+ 101
22
+ ],
23
+ [
24
+ 96,
25
+ 102
26
+ ],
27
+ [
28
+ 97,
29
+ 102
30
+ ],
31
+ [
32
+ 96,
33
+ 100
34
+ ],
35
+ [
36
+ 61,
37
+ 96
38
+ ],
39
+ [
40
+ 38,
41
+ 96
42
+ ],
43
+ [
44
+ 22,
45
+ 98
46
+ ],
47
+ [
48
+ 84,
49
+ 98
50
+ ],
51
+ [
52
+ 97,
53
+ 100
54
+ ],
55
+ [
56
+ 13,
57
+ 99
58
+ ],
59
+ [
60
+ 4,
61
+ 99
62
+ ],
63
+ [
64
+ 62,
65
+ 107
66
+ ],
67
+ [
68
+ 37,
69
+ 106
70
+ ],
71
+ [
72
+ 39,
73
+ 106
74
+ ],
75
+ [
76
+ 97,
77
+ 101
78
+ ],
79
+ [
80
+ 60,
81
+ 107
82
+ ],
83
+ [
84
+ 83,
85
+ 108
86
+ ],
87
+ [
88
+ 85,
89
+ 108
90
+ ],
91
+ [
92
+ 21,
93
+ 109
94
+ ],
95
+ [
96
+ 5,
97
+ 111
98
+ ],
99
+ [
100
+ 23,
101
+ 109
102
+ ],
103
+ [
104
+ 3,
105
+ 111
106
+ ],
107
+ [
108
+ 14,
109
+ 112
110
+ ],
111
+ [
112
+ 12,
113
+ 112
114
+ ],
115
+ [
116
+ 61,
117
+ 97
118
+ ],
119
+ [
120
+ 62,
121
+ 96
122
+ ],
123
+ [
124
+ 38,
125
+ 60
126
+ ],
127
+ [
128
+ 61,
129
+ 116
130
+ ],
131
+ [
132
+ 3,
133
+ 13
134
+ ],
135
+ [
136
+ 5,
137
+ 13
138
+ ],
139
+ [
140
+ 101,
141
+ 113
142
+ ],
143
+ [
144
+ 101,
145
+ 117
146
+ ],
147
+ [
148
+ 60,
149
+ 96
150
+ ],
151
+ [
152
+ 22,
153
+ 83
154
+ ],
155
+ [
156
+ 101,
157
+ 114
158
+ ],
159
+ [
160
+ 22,
161
+ 85
162
+ ],
163
+ [
164
+ 101,
165
+ 115
166
+ ],
167
+ [
168
+ 23,
169
+ 84
170
+ ],
171
+ [
172
+ 4,
173
+ 14
174
+ ],
175
+ [
176
+ 4,
177
+ 12
178
+ ],
179
+ [
180
+ 21,
181
+ 84
182
+ ],
183
+ [
184
+ 38,
185
+ 62
186
+ ],
187
+ [
188
+ 105,
189
+ 120
190
+ ],
191
+ [
192
+ 105,
193
+ 122
194
+ ],
195
+ [
196
+ 105,
197
+ 118
198
+ ],
199
+ [
200
+ 105,
201
+ 119
202
+ ],
203
+ [
204
+ 105,
205
+ 121
206
+ ],
207
+ [
208
+ 105,
209
+ 123
210
+ ],
211
+ [
212
+ 105,
213
+ 125
214
+ ],
215
+ [
216
+ 105,
217
+ 124
218
+ ],
219
+ [
220
+ 116,
221
+ 128
222
+ ],
223
+ [
224
+ 37,
225
+ 126
226
+ ],
227
+ [
228
+ 97,
229
+ 128
230
+ ],
231
+ [
232
+ 39,
233
+ 126
234
+ ],
235
+ [
236
+ 37,
237
+ 129
238
+ ],
239
+ [
240
+ 37,
241
+ 127
242
+ ],
243
+ [
244
+ 39,
245
+ 129
246
+ ],
247
+ [
248
+ 39,
249
+ 127
250
+ ],
251
+ [
252
+ 23,
253
+ 98
254
+ ],
255
+ [
256
+ 21,
257
+ 98
258
+ ],
259
+ [
260
+ 39,
261
+ 134
262
+ ],
263
+ [
264
+ 110,
265
+ 120
266
+ ],
267
+ [
268
+ 37,
269
+ 134
270
+ ],
271
+ [
272
+ 110,
273
+ 119
274
+ ],
275
+ [
276
+ 110,
277
+ 118
278
+ ],
279
+ [
280
+ 97,
281
+ 143
282
+ ],
283
+ [
284
+ 116,
285
+ 143
286
+ ],
287
+ [
288
+ 3,
289
+ 99
290
+ ],
291
+ [
292
+ 110,
293
+ 121
294
+ ],
295
+ [
296
+ 5,
297
+ 99
298
+ ],
299
+ [
300
+ 110,
301
+ 125
302
+ ],
303
+ [
304
+ 110,
305
+ 124
306
+ ],
307
+ [
308
+ 60,
309
+ 97
310
+ ],
311
+ [
312
+ 105,
313
+ 130
314
+ ],
315
+ [
316
+ 113,
317
+ 122
318
+ ],
319
+ [
320
+ 115,
321
+ 124
322
+ ],
323
+ [
324
+ 105,
325
+ 131
326
+ ],
327
+ [
328
+ 114,
329
+ 125
330
+ ],
331
+ [
332
+ 110,
333
+ 122
334
+ ],
335
+ [
336
+ 110,
337
+ 123
338
+ ],
339
+ [
340
+ 105,
341
+ 140
342
+ ],
343
+ [
344
+ 83,
345
+ 160
346
+ ],
347
+ [
348
+ 135,
349
+ 141
350
+ ],
351
+ [
352
+ 62,
353
+ 97
354
+ ],
355
+ [
356
+ 85,
357
+ 160
358
+ ],
359
+ [
360
+ 85,
361
+ 161
362
+ ],
363
+ [
364
+ 83,
365
+ 161
366
+ ],
367
+ [
368
+ 117,
369
+ 120
370
+ ],
371
+ [
372
+ 23,
373
+ 85
374
+ ],
375
+ [
376
+ 14,
377
+ 169
378
+ ],
379
+ [
380
+ 12,
381
+ 171
382
+ ],
383
+ [
384
+ 12,
385
+ 169
386
+ ],
387
+ [
388
+ 23,
389
+ 83
390
+ ],
391
+ [
392
+ 137,
393
+ 141
394
+ ],
395
+ [
396
+ 14,
397
+ 171
398
+ ],
399
+ [
400
+ 137,
401
+ 140
402
+ ],
403
+ [
404
+ 21,
405
+ 85
406
+ ],
407
+ [
408
+ 3,
409
+ 12
410
+ ],
411
+ [
412
+ 3,
413
+ 14
414
+ ],
415
+ [
416
+ 139,
417
+ 141
418
+ ],
419
+ [
420
+ 21,
421
+ 83
422
+ ],
423
+ [
424
+ 130,
425
+ 139
426
+ ],
427
+ [
428
+ 131,
429
+ 142
430
+ ],
431
+ [
432
+ 5,
433
+ 12
434
+ ],
435
+ [
436
+ 130,
437
+ 142
438
+ ],
439
+ [
440
+ 131,
441
+ 139
442
+ ],
443
+ [
444
+ 141,
445
+ 142
446
+ ],
447
+ [
448
+ 131,
449
+ 135
450
+ ],
451
+ [
452
+ 5,
453
+ 14
454
+ ],
455
+ [
456
+ 131,
457
+ 137
458
+ ],
459
+ [
460
+ 130,
461
+ 137
462
+ ],
463
+ [
464
+ 135,
465
+ 140
466
+ ],
467
+ [
468
+ 101,
469
+ 159
470
+ ],
471
+ [
472
+ 101,
473
+ 157
474
+ ],
475
+ [
476
+ 37,
477
+ 174
478
+ ],
479
+ [
480
+ 114,
481
+ 118
482
+ ],
483
+ [
484
+ 130,
485
+ 135
486
+ ],
487
+ [
488
+ 115,
489
+ 119
490
+ ],
491
+ [
492
+ 113,
493
+ 123
494
+ ],
495
+ [
496
+ 39,
497
+ 174
498
+ ],
499
+ [
500
+ 117,
501
+ 121
502
+ ],
503
+ [
504
+ 113,
505
+ 118
506
+ ],
507
+ [
508
+ 101,
509
+ 162
510
+ ],
511
+ [
512
+ 60,
513
+ 116
514
+ ],
515
+ [
516
+ 101,
517
+ 164
518
+ ],
519
+ [
520
+ 139,
521
+ 140
522
+ ],
523
+ [
524
+ 113,
525
+ 119
526
+ ],
527
+ [
528
+ 140,
529
+ 142
530
+ ],
531
+ [
532
+ 117,
533
+ 119
534
+ ],
535
+ [
536
+ 115,
537
+ 123
538
+ ],
539
+ [
540
+ 39,
541
+ 185
542
+ ],
543
+ [
544
+ 114,
545
+ 121
546
+ ],
547
+ [
548
+ 40,
549
+ 106
550
+ ],
551
+ [
552
+ 117,
553
+ 118
554
+ ],
555
+ [
556
+ 113,
557
+ 121
558
+ ],
559
+ [
560
+ 37,
561
+ 185
562
+ ],
563
+ [
564
+ 62,
565
+ 116
566
+ ],
567
+ [
568
+ 36,
569
+ 106
570
+ ],
571
+ [
572
+ 117,
573
+ 123
574
+ ],
575
+ [
576
+ 114,
577
+ 123
578
+ ],
579
+ [
580
+ 125,
581
+ 153
582
+ ],
583
+ [
584
+ 115,
585
+ 118
586
+ ],
587
+ [
588
+ 114,
589
+ 119
590
+ ],
591
+ [
592
+ 120,
593
+ 154
594
+ ],
595
+ [
596
+ 124,
597
+ 155
598
+ ],
599
+ [
600
+ 63,
601
+ 107
602
+ ],
603
+ [
604
+ 139,
605
+ 175
606
+ ],
607
+ [
608
+ 115,
609
+ 121
610
+ ],
611
+ [
612
+ 142,
613
+ 178
614
+ ],
615
+ [
616
+ 59,
617
+ 107
618
+ ],
619
+ [
620
+ 114,
621
+ 122
622
+ ],
623
+ [
624
+ 63,
625
+ 96
626
+ ],
627
+ [
628
+ 115,
629
+ 122
630
+ ],
631
+ [
632
+ 115,
633
+ 120
634
+ ],
635
+ [
636
+ 59,
637
+ 96
638
+ ],
639
+ [
640
+ 105,
641
+ 183
642
+ ],
643
+ [
644
+ 114,
645
+ 120
646
+ ],
647
+ [
648
+ 119,
649
+ 155
650
+ ],
651
+ [
652
+ 118,
653
+ 153
654
+ ],
655
+ [
656
+ 105,
657
+ 186
658
+ ],
659
+ [
660
+ 105,
661
+ 187
662
+ ],
663
+ [
664
+ 105,
665
+ 188
666
+ ],
667
+ [
668
+ 113,
669
+ 124
670
+ ],
671
+ [
672
+ 122,
673
+ 167
674
+ ],
675
+ [
676
+ 105,
677
+ 184
678
+ ],
679
+ [
680
+ 113,
681
+ 125
682
+ ],
683
+ [
684
+ 119,
685
+ 154
686
+ ],
687
+ [
688
+ 137,
689
+ 182
690
+ ],
691
+ [
692
+ 37,
693
+ 225
694
+ ],
695
+ [
696
+ 39,
697
+ 225
698
+ ],
699
+ [
700
+ 118,
701
+ 154
702
+ ],
703
+ [
704
+ 139,
705
+ 178
706
+ ],
707
+ [
708
+ 117,
709
+ 125
710
+ ],
711
+ [
712
+ 113,
713
+ 203
714
+ ],
715
+ [
716
+ 135,
717
+ 178
718
+ ],
719
+ [
720
+ 117,
721
+ 124
722
+ ],
723
+ [
724
+ 142,
725
+ 175
726
+ ],
727
+ [
728
+ 135,
729
+ 182
730
+ ],
731
+ [
732
+ 105,
733
+ 193
734
+ ],
735
+ [
736
+ 137,
737
+ 175
738
+ ],
739
+ [
740
+ 137,
741
+ 178
742
+ ],
743
+ [
744
+ 39,
745
+ 238
746
+ ],
747
+ [
748
+ 105,
749
+ 191
750
+ ],
751
+ [
752
+ 135,
753
+ 175
754
+ ],
755
+ [
756
+ 105,
757
+ 196
758
+ ],
759
+ [
760
+ 118,
761
+ 167
762
+ ],
763
+ [
764
+ 105,
765
+ 195
766
+ ],
767
+ [
768
+ 105,
769
+ 201
770
+ ],
771
+ [
772
+ 82,
773
+ 108
774
+ ],
775
+ [
776
+ 105,
777
+ 192
778
+ ],
779
+ [
780
+ 119,
781
+ 167
782
+ ],
783
+ [
784
+ 37,
785
+ 238
786
+ ],
787
+ [
788
+ 86,
789
+ 108
790
+ ],
791
+ [
792
+ 139,
793
+ 182
794
+ ],
795
+ [
796
+ 113,
797
+ 183
798
+ ],
799
+ [
800
+ 119,
801
+ 153
802
+ ],
803
+ [
804
+ 118,
805
+ 155
806
+ ],
807
+ [
808
+ 121,
809
+ 154
810
+ ],
811
+ [
812
+ 142,
813
+ 182
814
+ ],
815
+ [
816
+ 121,
817
+ 153
818
+ ],
819
+ [
820
+ 113,
821
+ 186
822
+ ],
823
+ [
824
+ 114,
825
+ 184
826
+ ],
827
+ [
828
+ 115,
829
+ 197
830
+ ],
831
+ [
832
+ 124,
833
+ 159
834
+ ],
835
+ [
836
+ 117,
837
+ 122
838
+ ],
839
+ [
840
+ 122,
841
+ 157
842
+ ],
843
+ [
844
+ 115,
845
+ 191
846
+ ],
847
+ [
848
+ 105,
849
+ 208
850
+ ],
851
+ [
852
+ 59,
853
+ 97
854
+ ],
855
+ [
856
+ 125,
857
+ 157
858
+ ],
859
+ [
860
+ 139,
861
+ 199
862
+ ],
863
+ [
864
+ 40,
865
+ 127
866
+ ],
867
+ [
868
+ 36,
869
+ 127
870
+ ],
871
+ [
872
+ 122,
873
+ 159
874
+ ],
875
+ [
876
+ 22,
877
+ 82
878
+ ],
879
+ [
880
+ 141,
881
+ 194
882
+ ],
883
+ [
884
+ 120,
885
+ 155
886
+ ],
887
+ [
888
+ 110,
889
+ 187
890
+ ],
891
+ [
892
+ 114,
893
+ 193
894
+ ],
895
+ [
896
+ 120,
897
+ 153
898
+ ],
899
+ [
900
+ 110,
901
+ 188
902
+ ],
903
+ [
904
+ 22,
905
+ 86
906
+ ],
907
+ [
908
+ 110,
909
+ 204
910
+ ],
911
+ [
912
+ 114,
913
+ 192
914
+ ],
915
+ [
916
+ 110,
917
+ 184
918
+ ],
919
+ [
920
+ 121,
921
+ 155
922
+ ],
923
+ [
924
+ 40,
925
+ 134
926
+ ],
927
+ [
928
+ 123,
929
+ 155
930
+ ],
931
+ [
932
+ 110,
933
+ 197
934
+ ],
935
+ [
936
+ 124,
937
+ 162
938
+ ],
939
+ [
940
+ 141,
941
+ 202
942
+ ],
943
+ [
944
+ 117,
945
+ 204
946
+ ],
947
+ [
948
+ 115,
949
+ 213
950
+ ],
951
+ [
952
+ 115,
953
+ 196
954
+ ],
955
+ [
956
+ 130,
957
+ 198
958
+ ],
959
+ [
960
+ 139,
961
+ 200
962
+ ],
963
+ [
964
+ 125,
965
+ 164
966
+ ],
967
+ [
968
+ 37,
969
+ 253
970
+ ],
971
+ [
972
+ 137,
973
+ 200
974
+ ],
975
+ [
976
+ 135,
977
+ 205
978
+ ],
979
+ [
980
+ 131,
981
+ 194
982
+ ],
983
+ [
984
+ 137,
985
+ 199
986
+ ],
987
+ [
988
+ 113,
989
+ 120
990
+ ],
991
+ [
992
+ 4,
993
+ 15
994
+ ],
995
+ [
996
+ 121,
997
+ 167
998
+ ],
999
+ [
1000
+ 114,
1001
+ 195
1002
+ ],
1003
+ [
1004
+ 117,
1005
+ 206
1006
+ ],
1007
+ [
1008
+ 37,
1009
+ 256
1010
+ ],
1011
+ [
1012
+ 135,
1013
+ 199
1014
+ ],
1015
+ [
1016
+ 120,
1017
+ 162
1018
+ ],
1019
+ [
1020
+ 140,
1021
+ 198
1022
+ ],
1023
+ [
1024
+ 123,
1025
+ 167
1026
+ ],
1027
+ [
1028
+ 113,
1029
+ 190
1030
+ ],
1031
+ [
1032
+ 117,
1033
+ 187
1034
+ ],
1035
+ [
1036
+ 130,
1037
+ 202
1038
+ ],
1039
+ [
1040
+ 135,
1041
+ 210
1042
+ ],
1043
+ [
1044
+ 113,
1045
+ 207
1046
+ ],
1047
+ [
1048
+ 117,
1049
+ 188
1050
+ ],
1051
+ [
1052
+ 137,
1053
+ 210
1054
+ ],
1055
+ [
1056
+ 36,
1057
+ 126
1058
+ ],
1059
+ [
1060
+ 36,
1061
+ 134
1062
+ ],
1063
+ [
1064
+ 110,
1065
+ 209
1066
+ ],
1067
+ [
1068
+ 142,
1069
+ 205
1070
+ ],
1071
+ [
1072
+ 135,
1073
+ 200
1074
+ ],
1075
+ [
1076
+ 40,
1077
+ 126
1078
+ ],
1079
+ [
1080
+ 39,
1081
+ 253
1082
+ ],
1083
+ [
1084
+ 110,
1085
+ 211
1086
+ ],
1087
+ [
1088
+ 114,
1089
+ 201
1090
+ ],
1091
+ [
1092
+ 142,
1093
+ 200
1094
+ ],
1095
+ [
1096
+ 124,
1097
+ 154
1098
+ ],
1099
+ [
1100
+ 125,
1101
+ 154
1102
+ ],
1103
+ [
1104
+ 120,
1105
+ 164
1106
+ ],
1107
+ [
1108
+ 139,
1109
+ 205
1110
+ ],
1111
+ [
1112
+ 118,
1113
+ 157
1114
+ ],
1115
+ [
1116
+ 123,
1117
+ 154
1118
+ ],
1119
+ [
1120
+ 115,
1121
+ 227
1122
+ ],
1123
+ [
1124
+ 123,
1125
+ 153
1126
+ ],
1127
+ [
1128
+ 119,
1129
+ 159
1130
+ ],
1131
+ [
1132
+ 2,
1133
+ 13
1134
+ ],
1135
+ [
1136
+ 130,
1137
+ 194
1138
+ ],
1139
+ [
1140
+ 39,
1141
+ 256
1142
+ ],
1143
+ [
1144
+ 137,
1145
+ 205
1146
+ ],
1147
+ [
1148
+ 142,
1149
+ 210
1150
+ ],
1151
+ [
1152
+ 131,
1153
+ 202
1154
+ ],
1155
+ [
1156
+ 119,
1157
+ 162
1158
+ ],
1159
+ [
1160
+ 140,
1161
+ 190
1162
+ ],
1163
+ [
1164
+ 6,
1165
+ 13
1166
+ ],
1167
+ [
1168
+ 122,
1169
+ 153
1170
+ ],
1171
+ [
1172
+ 131,
1173
+ 198
1174
+ ],
1175
+ [
1176
+ 4,
1177
+ 11
1178
+ ],
1179
+ [
1180
+ 122,
1181
+ 155
1182
+ ],
1183
+ [
1184
+ 118,
1185
+ 164
1186
+ ],
1187
+ [
1188
+ 63,
1189
+ 97
1190
+ ],
1191
+ [
1192
+ 131,
1193
+ 190
1194
+ ],
1195
+ [
1196
+ 123,
1197
+ 157
1198
+ ],
1199
+ [
1200
+ 110,
1201
+ 183
1202
+ ],
1203
+ [
1204
+ 110,
1205
+ 208
1206
+ ],
1207
+ [
1208
+ 123,
1209
+ 159
1210
+ ],
1211
+ [
1212
+ 110,
1213
+ 192
1214
+ ],
1215
+ [
1216
+ 141,
1217
+ 190
1218
+ ],
1219
+ [
1220
+ 123,
1221
+ 162
1222
+ ],
1223
+ [
1224
+ 142,
1225
+ 199
1226
+ ],
1227
+ [
1228
+ 24,
1229
+ 84
1230
+ ],
1231
+ [
1232
+ 121,
1233
+ 157
1234
+ ],
1235
+ [
1236
+ 115,
1237
+ 229
1238
+ ],
1239
+ [
1240
+ 110,
1241
+ 229
1242
+ ],
1243
+ [
1244
+ 110,
1245
+ 186
1246
+ ],
1247
+ [
1248
+ 121,
1249
+ 164
1250
+ ],
1251
+ [
1252
+ 125,
1253
+ 167
1254
+ ],
1255
+ [
1256
+ 113,
1257
+ 201
1258
+ ],
1259
+ [
1260
+ 140,
1261
+ 194
1262
+ ],
1263
+ [
1264
+ 124,
1265
+ 167
1266
+ ],
1267
+ [
1268
+ 118,
1269
+ 159
1270
+ ],
1271
+ [
1272
+ 139,
1273
+ 210
1274
+ ],
1275
+ [
1276
+ 110,
1277
+ 196
1278
+ ],
1279
+ [
1280
+ 113,
1281
+ 227
1282
+ ],
1283
+ [
1284
+ 110,
1285
+ 218
1286
+ ],
1287
+ [
1288
+ 15,
1289
+ 112
1290
+ ],
1291
+ [
1292
+ 110,
1293
+ 195
1294
+ ],
1295
+ [
1296
+ 123,
1297
+ 164
1298
+ ],
1299
+ [
1300
+ 110,
1301
+ 206
1302
+ ],
1303
+ [
1304
+ 110,
1305
+ 212
1306
+ ],
1307
+ [
1308
+ 119,
1309
+ 157
1310
+ ],
1311
+ [
1312
+ 110,
1313
+ 213
1314
+ ],
1315
+ [
1316
+ 141,
1317
+ 198
1318
+ ],
1319
+ [
1320
+ 11,
1321
+ 112
1322
+ ],
1323
+ [
1324
+ 130,
1325
+ 190
1326
+ ],
1327
+ [
1328
+ 119,
1329
+ 164
1330
+ ],
1331
+ [
1332
+ 121,
1333
+ 159
1334
+ ],
1335
+ [
1336
+ 118,
1337
+ 162
1338
+ ],
1339
+ [
1340
+ 121,
1341
+ 162
1342
+ ],
1343
+ [
1344
+ 110,
1345
+ 203
1346
+ ],
1347
+ [
1348
+ 114,
1349
+ 208
1350
+ ],
1351
+ [
1352
+ 155,
1353
+ 197
1354
+ ],
1355
+ [
1356
+ 110,
1357
+ 193
1358
+ ],
1359
+ [
1360
+ 153,
1361
+ 184
1362
+ ],
1363
+ [
1364
+ 2,
1365
+ 12
1366
+ ],
1367
+ [
1368
+ 20,
1369
+ 84
1370
+ ],
1371
+ [
1372
+ 140,
1373
+ 202
1374
+ ],
1375
+ [
1376
+ 110,
1377
+ 207
1378
+ ],
1379
+ [
1380
+ 157,
1381
+ 201
1382
+ ],
1383
+ [
1384
+ 110,
1385
+ 191
1386
+ ],
1387
+ [
1388
+ 120,
1389
+ 167
1390
+ ],
1391
+ [
1392
+ 159,
1393
+ 227
1394
+ ],
1395
+ [
1396
+ 117,
1397
+ 207
1398
+ ],
1399
+ [
1400
+ 38,
1401
+ 310
1402
+ ],
1403
+ [
1404
+ 124,
1405
+ 234
1406
+ ],
1407
+ [
1408
+ 24,
1409
+ 85
1410
+ ],
1411
+ [
1412
+ 114,
1413
+ 209
1414
+ ],
1415
+ [
1416
+ 114,
1417
+ 124
1418
+ ],
1419
+ [
1420
+ 114,
1421
+ 183
1422
+ ],
1423
+ [
1424
+ 113,
1425
+ 212
1426
+ ],
1427
+ [
1428
+ 117,
1429
+ 212
1430
+ ],
1431
+ [
1432
+ 115,
1433
+ 212
1434
+ ],
1435
+ [
1436
+ 115,
1437
+ 125
1438
+ ],
1439
+ [
1440
+ 110,
1441
+ 201
1442
+ ],
1443
+ [
1444
+ 125,
1445
+ 239
1446
+ ],
1447
+ [
1448
+ 2,
1449
+ 111
1450
+ ],
1451
+ [
1452
+ 110,
1453
+ 227
1454
+ ],
1455
+ [
1456
+ 113,
1457
+ 218
1458
+ ],
1459
+ [
1460
+ 115,
1461
+ 186
1462
+ ],
1463
+ [
1464
+ 113,
1465
+ 209
1466
+ ],
1467
+ [
1468
+ 119,
1469
+ 221
1470
+ ],
1471
+ [
1472
+ 114,
1473
+ 218
1474
+ ],
1475
+ [
1476
+ 122,
1477
+ 154
1478
+ ],
1479
+ [
1480
+ 117,
1481
+ 229
1482
+ ],
1483
+ [
1484
+ 117,
1485
+ 211
1486
+ ],
1487
+ [
1488
+ 115,
1489
+ 211
1490
+ ],
1491
+ [
1492
+ 117,
1493
+ 209
1494
+ ],
1495
+ [
1496
+ 117,
1497
+ 208
1498
+ ],
1499
+ [
1500
+ 6,
1501
+ 111
1502
+ ],
1503
+ [
1504
+ 115,
1505
+ 187
1506
+ ],
1507
+ [
1508
+ 114,
1509
+ 207
1510
+ ],
1511
+ [
1512
+ 117,
1513
+ 218
1514
+ ],
1515
+ [
1516
+ 114,
1517
+ 188
1518
+ ],
1519
+ [
1520
+ 101,
1521
+ 234
1522
+ ],
1523
+ [
1524
+ 125,
1525
+ 216
1526
+ ],
1527
+ [
1528
+ 154,
1529
+ 187
1530
+ ],
1531
+ [
1532
+ 101,
1533
+ 239
1534
+ ],
1535
+ [
1536
+ 118,
1537
+ 216
1538
+ ],
1539
+ [
1540
+ 114,
1541
+ 203
1542
+ ],
1543
+ [
1544
+ 113,
1545
+ 211
1546
+ ],
1547
+ [
1548
+ 23,
1549
+ 86
1550
+ ],
1551
+ [
1552
+ 115,
1553
+ 203
1554
+ ],
1555
+ [
1556
+ 118,
1557
+ 237
1558
+ ],
1559
+ [
1560
+ 120,
1561
+ 221
1562
+ ],
1563
+ [
1564
+ 120,
1565
+ 216
1566
+ ],
1567
+ [
1568
+ 24,
1569
+ 83
1570
+ ],
1571
+ [
1572
+ 124,
1573
+ 221
1574
+ ],
1575
+ [
1576
+ 36,
1577
+ 174
1578
+ ],
1579
+ [
1580
+ 154,
1581
+ 188
1582
+ ],
1583
+ [
1584
+ 119,
1585
+ 232
1586
+ ],
1587
+ [
1588
+ 115,
1589
+ 206
1590
+ ],
1591
+ [
1592
+ 115,
1593
+ 204
1594
+ ],
1595
+ [
1596
+ 20,
1597
+ 85
1598
+ ],
1599
+ [
1600
+ 162,
1601
+ 229
1602
+ ],
1603
+ [
1604
+ 157,
1605
+ 183
1606
+ ],
1607
+ [
1608
+ 114,
1609
+ 204
1610
+ ],
1611
+ [
1612
+ 86,
1613
+ 160
1614
+ ],
1615
+ [
1616
+ 153,
1617
+ 192
1618
+ ],
1619
+ [
1620
+ 124,
1621
+ 232
1622
+ ],
1623
+ [
1624
+ 24,
1625
+ 109
1626
+ ],
1627
+ [
1628
+ 38,
1629
+ 394
1630
+ ],
1631
+ [
1632
+ 159,
1633
+ 186
1634
+ ],
1635
+ [
1636
+ 115,
1637
+ 218
1638
+ ],
1639
+ [
1640
+ 40,
1641
+ 174
1642
+ ],
1643
+ [
1644
+ 122,
1645
+ 247
1646
+ ],
1647
+ [
1648
+ 114,
1649
+ 211
1650
+ ],
1651
+ [
1652
+ 113,
1653
+ 193
1654
+ ],
1655
+ [
1656
+ 113,
1657
+ 213
1658
+ ]
1659
+ ],
1660
+ "temporal_merges": [
1661
+ [
1662
+ 103,
1663
+ 103,
1664
+ 2
1665
+ ],
1666
+ [
1667
+ 104,
1668
+ 104,
1669
+ 1
1670
+ ],
1671
+ [
1672
+ 512,
1673
+ 512,
1674
+ 4
1675
+ ],
1676
+ [
1677
+ 513,
1678
+ 513,
1679
+ 2
1680
+ ],
1681
+ [
1682
+ 514,
1683
+ 514,
1684
+ 1
1685
+ ],
1686
+ [
1687
+ 512,
1688
+ 103,
1689
+ 4
1690
+ ],
1691
+ [
1692
+ 515,
1693
+ 515,
1694
+ 4
1695
+ ],
1696
+ [
1697
+ 103,
1698
+ 103,
1699
+ 4
1700
+ ],
1701
+ [
1702
+ 513,
1703
+ 104,
1704
+ 2
1705
+ ],
1706
+ [
1707
+ 104,
1708
+ 104,
1709
+ 2
1710
+ ],
1711
+ [
1712
+ 103,
1713
+ 512,
1714
+ 4
1715
+ ],
1716
+ [
1717
+ 103,
1718
+ 103,
1719
+ 6
1720
+ ],
1721
+ [
1722
+ 512,
1723
+ 103,
1724
+ 6
1725
+ ],
1726
+ [
1727
+ 515,
1728
+ 104,
1729
+ 4
1730
+ ],
1731
+ [
1732
+ 513,
1733
+ 104,
1734
+ 3
1735
+ ],
1736
+ [
1737
+ 515,
1738
+ 513,
1739
+ 4
1740
+ ],
1741
+ [
1742
+ 104,
1743
+ 104,
1744
+ 3
1745
+ ],
1746
+ [
1747
+ 103,
1748
+ 133,
1749
+ 2
1750
+ ],
1751
+ [
1752
+ 103,
1753
+ 132,
1754
+ 2
1755
+ ],
1756
+ [
1757
+ 103,
1758
+ 136,
1759
+ 2
1760
+ ],
1761
+ [
1762
+ 515,
1763
+ 520,
1764
+ 4
1765
+ ],
1766
+ [
1767
+ 103,
1768
+ 138,
1769
+ 2
1770
+ ],
1771
+ [
1772
+ 103,
1773
+ 144,
1774
+ 2
1775
+ ],
1776
+ [
1777
+ 104,
1778
+ 513,
1779
+ 2
1780
+ ],
1781
+ [
1782
+ 512,
1783
+ 132,
1784
+ 4
1785
+ ],
1786
+ [
1787
+ 512,
1788
+ 133,
1789
+ 4
1790
+ ],
1791
+ [
1792
+ 103,
1793
+ 145,
1794
+ 2
1795
+ ],
1796
+ [
1797
+ 103,
1798
+ 146,
1799
+ 2
1800
+ ],
1801
+ [
1802
+ 103,
1803
+ 147,
1804
+ 2
1805
+ ],
1806
+ [
1807
+ 514,
1808
+ 517,
1809
+ 1
1810
+ ],
1811
+ [
1812
+ 103,
1813
+ 148,
1814
+ 2
1815
+ ],
1816
+ [
1817
+ 512,
1818
+ 136,
1819
+ 4
1820
+ ],
1821
+ [
1822
+ 138,
1823
+ 138,
1824
+ 3
1825
+ ],
1826
+ [
1827
+ 512,
1828
+ 144,
1829
+ 4
1830
+ ],
1831
+ [
1832
+ 103,
1833
+ 149,
1834
+ 2
1835
+ ],
1836
+ [
1837
+ 514,
1838
+ 517,
1839
+ 3
1840
+ ],
1841
+ [
1842
+ 512,
1843
+ 145,
1844
+ 4
1845
+ ],
1846
+ [
1847
+ 150,
1848
+ 136,
1849
+ 1
1850
+ ],
1851
+ [
1852
+ 132,
1853
+ 132,
1854
+ 2
1855
+ ],
1856
+ [
1857
+ 512,
1858
+ 138,
1859
+ 4
1860
+ ],
1861
+ [
1862
+ 512,
1863
+ 147,
1864
+ 4
1865
+ ],
1866
+ [
1867
+ 512,
1868
+ 146,
1869
+ 4
1870
+ ],
1871
+ [
1872
+ 103,
1873
+ 151,
1874
+ 2
1875
+ ],
1876
+ [
1877
+ 133,
1878
+ 133,
1879
+ 3
1880
+ ],
1881
+ [
1882
+ 517,
1883
+ 514,
1884
+ 1
1885
+ ],
1886
+ [
1887
+ 103,
1888
+ 150,
1889
+ 2
1890
+ ],
1891
+ [
1892
+ 136,
1893
+ 136,
1894
+ 3
1895
+ ],
1896
+ [
1897
+ 512,
1898
+ 149,
1899
+ 4
1900
+ ],
1901
+ [
1902
+ 513,
1903
+ 513,
1904
+ 3
1905
+ ],
1906
+ [
1907
+ 512,
1908
+ 148,
1909
+ 4
1910
+ ],
1911
+ [
1912
+ 520,
1913
+ 104,
1914
+ 4
1915
+ ],
1916
+ [
1917
+ 145,
1918
+ 132,
1919
+ 1
1920
+ ],
1921
+ [
1922
+ 151,
1923
+ 138,
1924
+ 1
1925
+ ],
1926
+ [
1927
+ 104,
1928
+ 152,
1929
+ 1
1930
+ ],
1931
+ [
1932
+ 144,
1933
+ 133,
1934
+ 1
1935
+ ],
1936
+ [
1937
+ 132,
1938
+ 145,
1939
+ 1
1940
+ ],
1941
+ [
1942
+ 512,
1943
+ 150,
1944
+ 4
1945
+ ],
1946
+ [
1947
+ 104,
1948
+ 156,
1949
+ 1
1950
+ ],
1951
+ [
1952
+ 512,
1953
+ 151,
1954
+ 4
1955
+ ],
1956
+ [
1957
+ 146,
1958
+ 146,
1959
+ 2
1960
+ ],
1961
+ [
1962
+ 104,
1963
+ 158,
1964
+ 1
1965
+ ],
1966
+ [
1967
+ 147,
1968
+ 147,
1969
+ 2
1970
+ ],
1971
+ [
1972
+ 163,
1973
+ 152,
1974
+ 1
1975
+ ],
1976
+ [
1977
+ 517,
1978
+ 132,
1979
+ 6
1980
+ ],
1981
+ [
1982
+ 149,
1983
+ 149,
1984
+ 4
1985
+ ],
1986
+ [
1987
+ 136,
1988
+ 136,
1989
+ 4
1990
+ ],
1991
+ [
1992
+ 517,
1993
+ 133,
1994
+ 6
1995
+ ],
1996
+ [
1997
+ 138,
1998
+ 138,
1999
+ 4
2000
+ ],
2001
+ [
2002
+ 144,
2003
+ 133,
2004
+ 2
2005
+ ],
2006
+ [
2007
+ 172,
2008
+ 156,
2009
+ 1
2010
+ ],
2011
+ [
2012
+ 148,
2013
+ 148,
2014
+ 4
2015
+ ],
2016
+ [
2017
+ 513,
2018
+ 152,
2019
+ 2
2020
+ ],
2021
+ [
2022
+ 104,
2023
+ 165,
2024
+ 1
2025
+ ],
2026
+ [
2027
+ 104,
2028
+ 166,
2029
+ 1
2030
+ ],
2031
+ [
2032
+ 519,
2033
+ 144,
2034
+ 2
2035
+ ],
2036
+ [
2037
+ 173,
2038
+ 158,
2039
+ 1
2040
+ ],
2041
+ [
2042
+ 517,
2043
+ 147,
2044
+ 6
2045
+ ],
2046
+ [
2047
+ 517,
2048
+ 136,
2049
+ 6
2050
+ ],
2051
+ [
2052
+ 517,
2053
+ 138,
2054
+ 6
2055
+ ],
2056
+ [
2057
+ 517,
2058
+ 146,
2059
+ 6
2060
+ ],
2061
+ [
2062
+ 513,
2063
+ 156,
2064
+ 2
2065
+ ],
2066
+ [
2067
+ 104,
2068
+ 163,
2069
+ 1
2070
+ ],
2071
+ [
2072
+ 104,
2073
+ 168,
2074
+ 1
2075
+ ],
2076
+ [
2077
+ 103,
2078
+ 103,
2079
+ 1
2080
+ ],
2081
+ [
2082
+ 519,
2083
+ 132,
2084
+ 2
2085
+ ],
2086
+ [
2087
+ 517,
2088
+ 145,
2089
+ 6
2090
+ ],
2091
+ [
2092
+ 177,
2093
+ 177,
2094
+ 2
2095
+ ],
2096
+ [
2097
+ 133,
2098
+ 133,
2099
+ 2
2100
+ ],
2101
+ [
2102
+ 104,
2103
+ 170,
2104
+ 1
2105
+ ],
2106
+ [
2107
+ 513,
2108
+ 158,
2109
+ 2
2110
+ ],
2111
+ [
2112
+ 517,
2113
+ 144,
2114
+ 6
2115
+ ],
2116
+ [
2117
+ 519,
2118
+ 133,
2119
+ 2
2120
+ ],
2121
+ [
2122
+ 179,
2123
+ 179,
2124
+ 2
2125
+ ],
2126
+ [
2127
+ 145,
2128
+ 132,
2129
+ 2
2130
+ ],
2131
+ [
2132
+ 149,
2133
+ 149,
2134
+ 5
2135
+ ],
2136
+ [
2137
+ 513,
2138
+ 168,
2139
+ 2
2140
+ ],
2141
+ [
2142
+ 520,
2143
+ 152,
2144
+ 3
2145
+ ],
2146
+ [
2147
+ 515,
2148
+ 521,
2149
+ 4
2150
+ ],
2151
+ [
2152
+ 132,
2153
+ 132,
2154
+ 3
2155
+ ],
2156
+ [
2157
+ 517,
2158
+ 148,
2159
+ 6
2160
+ ],
2161
+ [
2162
+ 519,
2163
+ 146,
2164
+ 2
2165
+ ],
2166
+ [
2167
+ 519,
2168
+ 147,
2169
+ 2
2170
+ ],
2171
+ [
2172
+ 138,
2173
+ 151,
2174
+ 1
2175
+ ],
2176
+ [
2177
+ 136,
2178
+ 150,
2179
+ 1
2180
+ ],
2181
+ [
2182
+ 513,
2183
+ 163,
2184
+ 2
2185
+ ],
2186
+ [
2187
+ 520,
2188
+ 156,
2189
+ 3
2190
+ ],
2191
+ [
2192
+ 519,
2193
+ 145,
2194
+ 2
2195
+ ],
2196
+ [
2197
+ 517,
2198
+ 149,
2199
+ 6
2200
+ ],
2201
+ [
2202
+ 176,
2203
+ 176,
2204
+ 2
2205
+ ],
2206
+ [
2207
+ 513,
2208
+ 170,
2209
+ 2
2210
+ ],
2211
+ [
2212
+ 520,
2213
+ 158,
2214
+ 3
2215
+ ],
2216
+ [
2217
+ 133,
2218
+ 144,
2219
+ 1
2220
+ ],
2221
+ [
2222
+ 148,
2223
+ 148,
2224
+ 5
2225
+ ],
2226
+ [
2227
+ 152,
2228
+ 163,
2229
+ 1
2230
+ ],
2231
+ [
2232
+ 513,
2233
+ 165,
2234
+ 2
2235
+ ],
2236
+ [
2237
+ 180,
2238
+ 168,
2239
+ 1
2240
+ ],
2241
+ [
2242
+ 133,
2243
+ 144,
2244
+ 2
2245
+ ],
2246
+ [
2247
+ 513,
2248
+ 166,
2249
+ 2
2250
+ ],
2251
+ [
2252
+ 156,
2253
+ 156,
2254
+ 3
2255
+ ],
2256
+ [
2257
+ 520,
2258
+ 168,
2259
+ 3
2260
+ ],
2261
+ [
2262
+ 136,
2263
+ 136,
2264
+ 2
2265
+ ],
2266
+ [
2267
+ 520,
2268
+ 163,
2269
+ 3
2270
+ ],
2271
+ [
2272
+ 158,
2273
+ 158,
2274
+ 3
2275
+ ],
2276
+ [
2277
+ 138,
2278
+ 138,
2279
+ 2
2280
+ ],
2281
+ [
2282
+ 132,
2283
+ 145,
2284
+ 2
2285
+ ],
2286
+ [
2287
+ 157,
2288
+ 157,
2289
+ 2
2290
+ ],
2291
+ [
2292
+ 519,
2293
+ 136,
2294
+ 2
2295
+ ],
2296
+ [
2297
+ 519,
2298
+ 138,
2299
+ 2
2300
+ ],
2301
+ [
2302
+ 150,
2303
+ 136,
2304
+ 2
2305
+ ],
2306
+ [
2307
+ 166,
2308
+ 166,
2309
+ 2
2310
+ ],
2311
+ [
2312
+ 519,
2313
+ 148,
2314
+ 2
2315
+ ],
2316
+ [
2317
+ 520,
2318
+ 170,
2319
+ 3
2320
+ ],
2321
+ [
2322
+ 145,
2323
+ 145,
2324
+ 2
2325
+ ],
2326
+ [
2327
+ 165,
2328
+ 165,
2329
+ 2
2330
+ ],
2331
+ [
2332
+ 104,
2333
+ 172,
2334
+ 1
2335
+ ],
2336
+ [
2337
+ 146,
2338
+ 146,
2339
+ 3
2340
+ ],
2341
+ [
2342
+ 514,
2343
+ 144,
2344
+ 1
2345
+ ],
2346
+ [
2347
+ 164,
2348
+ 164,
2349
+ 2
2350
+ ],
2351
+ [
2352
+ 147,
2353
+ 147,
2354
+ 3
2355
+ ],
2356
+ [
2357
+ 151,
2358
+ 138,
2359
+ 2
2360
+ ],
2361
+ [
2362
+ 104,
2363
+ 173,
2364
+ 1
2365
+ ],
2366
+ [
2367
+ 132,
2368
+ 103,
2369
+ 2
2370
+ ],
2371
+ [
2372
+ 104,
2373
+ 181,
2374
+ 1
2375
+ ],
2376
+ [
2377
+ 519,
2378
+ 149,
2379
+ 2
2380
+ ],
2381
+ [
2382
+ 519,
2383
+ 151,
2384
+ 2
2385
+ ],
2386
+ [
2387
+ 517,
2388
+ 150,
2389
+ 6
2390
+ ],
2391
+ [
2392
+ 519,
2393
+ 150,
2394
+ 2
2395
+ ],
2396
+ [
2397
+ 521,
2398
+ 152,
2399
+ 1
2400
+ ],
2401
+ [
2402
+ 517,
2403
+ 151,
2404
+ 6
2405
+ ],
2406
+ [
2407
+ 144,
2408
+ 103,
2409
+ 2
2410
+ ],
2411
+ [
2412
+ 520,
2413
+ 165,
2414
+ 3
2415
+ ],
2416
+ [
2417
+ 133,
2418
+ 103,
2419
+ 2
2420
+ ],
2421
+ [
2422
+ 146,
2423
+ 103,
2424
+ 2
2425
+ ],
2426
+ [
2427
+ 136,
2428
+ 103,
2429
+ 2
2430
+ ],
2431
+ [
2432
+ 162,
2433
+ 162,
2434
+ 2
2435
+ ],
2436
+ [
2437
+ 147,
2438
+ 103,
2439
+ 2
2440
+ ],
2441
+ [
2442
+ 520,
2443
+ 166,
2444
+ 3
2445
+ ],
2446
+ [
2447
+ 138,
2448
+ 103,
2449
+ 2
2450
+ ],
2451
+ [
2452
+ 515,
2453
+ 156,
2454
+ 4
2455
+ ],
2456
+ [
2457
+ 149,
2458
+ 103,
2459
+ 2
2460
+ ],
2461
+ [
2462
+ 145,
2463
+ 103,
2464
+ 2
2465
+ ],
2466
+ [
2467
+ 148,
2468
+ 103,
2469
+ 2
2470
+ ],
2471
+ [
2472
+ 514,
2473
+ 144,
2474
+ 3
2475
+ ],
2476
+ [
2477
+ 104,
2478
+ 180,
2479
+ 1
2480
+ ],
2481
+ [
2482
+ 515,
2483
+ 152,
2484
+ 4
2485
+ ],
2486
+ [
2487
+ 117,
2488
+ 117,
2489
+ 2
2490
+ ],
2491
+ [
2492
+ 521,
2493
+ 152,
2494
+ 3
2495
+ ],
2496
+ [
2497
+ 159,
2498
+ 159,
2499
+ 2
2500
+ ],
2501
+ [
2502
+ 136,
2503
+ 150,
2504
+ 2
2505
+ ],
2506
+ [
2507
+ 513,
2508
+ 172,
2509
+ 2
2510
+ ],
2511
+ [
2512
+ 515,
2513
+ 158,
2514
+ 4
2515
+ ],
2516
+ [
2517
+ 114,
2518
+ 114,
2519
+ 2
2520
+ ],
2521
+ [
2522
+ 113,
2523
+ 113,
2524
+ 2
2525
+ ],
2526
+ [
2527
+ 514,
2528
+ 133,
2529
+ 1
2530
+ ],
2531
+ [
2532
+ 115,
2533
+ 115,
2534
+ 2
2535
+ ],
2536
+ [
2537
+ 513,
2538
+ 173,
2539
+ 2
2540
+ ],
2541
+ [
2542
+ 515,
2543
+ 163,
2544
+ 4
2545
+ ],
2546
+ [
2547
+ 138,
2548
+ 151,
2549
+ 2
2550
+ ],
2551
+ [
2552
+ 514,
2553
+ 512,
2554
+ 5
2555
+ ],
2556
+ [
2557
+ 155,
2558
+ 155,
2559
+ 2
2560
+ ],
2561
+ [
2562
+ 513,
2563
+ 181,
2564
+ 2
2565
+ ],
2566
+ [
2567
+ 156,
2568
+ 172,
2569
+ 1
2570
+ ],
2571
+ [
2572
+ 144,
2573
+ 144,
2574
+ 3
2575
+ ],
2576
+ [
2577
+ 151,
2578
+ 151,
2579
+ 3
2580
+ ],
2581
+ [
2582
+ 177,
2583
+ 177,
2584
+ 3
2585
+ ],
2586
+ [
2587
+ 158,
2588
+ 158,
2589
+ 4
2590
+ ],
2591
+ [
2592
+ 152,
2593
+ 152,
2594
+ 3
2595
+ ],
2596
+ [
2597
+ 132,
2598
+ 132,
2599
+ 4
2600
+ ],
2601
+ [
2602
+ 515,
2603
+ 170,
2604
+ 4
2605
+ ],
2606
+ [
2607
+ 179,
2608
+ 179,
2609
+ 3
2610
+ ],
2611
+ [
2612
+ 251,
2613
+ 251,
2614
+ 2
2615
+ ],
2616
+ [
2617
+ 520,
2618
+ 172,
2619
+ 3
2620
+ ],
2621
+ [
2622
+ 521,
2623
+ 156,
2624
+ 3
2625
+ ],
2626
+ [
2627
+ 515,
2628
+ 526,
2629
+ 4
2630
+ ],
2631
+ [
2632
+ 514,
2633
+ 144,
2634
+ 5
2635
+ ],
2636
+ [
2637
+ 513,
2638
+ 180,
2639
+ 2
2640
+ ],
2641
+ [
2642
+ 515,
2643
+ 168,
2644
+ 4
2645
+ ],
2646
+ [
2647
+ 520,
2648
+ 173,
2649
+ 3
2650
+ ],
2651
+ [
2652
+ 149,
2653
+ 149,
2654
+ 3
2655
+ ],
2656
+ [
2657
+ 514,
2658
+ 133,
2659
+ 3
2660
+ ],
2661
+ [
2662
+ 150,
2663
+ 103,
2664
+ 2
2665
+ ],
2666
+ [
2667
+ 168,
2668
+ 168,
2669
+ 3
2670
+ ],
2671
+ [
2672
+ 153,
2673
+ 153,
2674
+ 2
2675
+ ],
2676
+ [
2677
+ 520,
2678
+ 181,
2679
+ 3
2680
+ ],
2681
+ [
2682
+ 247,
2683
+ 247,
2684
+ 2
2685
+ ],
2686
+ [
2687
+ 148,
2688
+ 148,
2689
+ 3
2690
+ ],
2691
+ [
2692
+ 520,
2693
+ 180,
2694
+ 3
2695
+ ],
2696
+ [
2697
+ 521,
2698
+ 170,
2699
+ 3
2700
+ ],
2701
+ [
2702
+ 176,
2703
+ 176,
2704
+ 3
2705
+ ],
2706
+ [
2707
+ 158,
2708
+ 173,
2709
+ 1
2710
+ ],
2711
+ [
2712
+ 515,
2713
+ 165,
2714
+ 4
2715
+ ],
2716
+ [
2717
+ 146,
2718
+ 146,
2719
+ 4
2720
+ ],
2721
+ [
2722
+ 521,
2723
+ 168,
2724
+ 3
2725
+ ],
2726
+ [
2727
+ 133,
2728
+ 133,
2729
+ 4
2730
+ ],
2731
+ [
2732
+ 166,
2733
+ 166,
2734
+ 3
2735
+ ],
2736
+ [
2737
+ 154,
2738
+ 154,
2739
+ 2
2740
+ ],
2741
+ [
2742
+ 151,
2743
+ 103,
2744
+ 2
2745
+ ],
2746
+ [
2747
+ 165,
2748
+ 165,
2749
+ 3
2750
+ ],
2751
+ [
2752
+ 514,
2753
+ 132,
2754
+ 1
2755
+ ],
2756
+ [
2757
+ 515,
2758
+ 166,
2759
+ 4
2760
+ ],
2761
+ [
2762
+ 147,
2763
+ 147,
2764
+ 4
2765
+ ],
2766
+ [
2767
+ 521,
2768
+ 163,
2769
+ 3
2770
+ ],
2771
+ [
2772
+ 145,
2773
+ 145,
2774
+ 3
2775
+ ],
2776
+ [
2777
+ 150,
2778
+ 150,
2779
+ 3
2780
+ ],
2781
+ [
2782
+ 514,
2783
+ 133,
2784
+ 5
2785
+ ],
2786
+ [
2787
+ 512,
2788
+ 512,
2789
+ 1
2790
+ ],
2791
+ [
2792
+ 189,
2793
+ 189,
2794
+ 2
2795
+ ],
2796
+ [
2797
+ 512,
2798
+ 103,
2799
+ 1
2800
+ ],
2801
+ [
2802
+ 152,
2803
+ 163,
2804
+ 2
2805
+ ],
2806
+ [
2807
+ 521,
2808
+ 158,
2809
+ 3
2810
+ ],
2811
+ [
2812
+ 156,
2813
+ 156,
2814
+ 4
2815
+ ],
2816
+ [
2817
+ 105,
2818
+ 105,
2819
+ 2
2820
+ ],
2821
+ [
2822
+ 167,
2823
+ 167,
2824
+ 2
2825
+ ],
2826
+ [
2827
+ 234,
2828
+ 234,
2829
+ 2
2830
+ ],
2831
+ [
2832
+ 514,
2833
+ 512,
2834
+ 3
2835
+ ],
2836
+ [
2837
+ 522,
2838
+ 132,
2839
+ 2
2840
+ ],
2841
+ [
2842
+ 144,
2843
+ 144,
2844
+ 4
2845
+ ],
2846
+ [
2847
+ 216,
2848
+ 216,
2849
+ 2
2850
+ ],
2851
+ [
2852
+ 515,
2853
+ 172,
2854
+ 4
2855
+ ],
2856
+ [
2857
+ 517,
2858
+ 517,
2859
+ 1
2860
+ ],
2861
+ [
2862
+ 522,
2863
+ 136,
2864
+ 2
2865
+ ],
2866
+ [
2867
+ 170,
2868
+ 170,
2869
+ 5
2870
+ ],
2871
+ [
2872
+ 110,
2873
+ 110,
2874
+ 2
2875
+ ],
2876
+ [
2877
+ 515,
2878
+ 173,
2879
+ 4
2880
+ ],
2881
+ [
2882
+ 522,
2883
+ 147,
2884
+ 2
2885
+ ],
2886
+ [
2887
+ 103,
2888
+ 176,
2889
+ 2
2890
+ ],
2891
+ [
2892
+ 522,
2893
+ 146,
2894
+ 2
2895
+ ],
2896
+ [
2897
+ 522,
2898
+ 133,
2899
+ 2
2900
+ ],
2901
+ [
2902
+ 522,
2903
+ 138,
2904
+ 2
2905
+ ],
2906
+ [
2907
+ 514,
2908
+ 145,
2909
+ 5
2910
+ ],
2911
+ [
2912
+ 515,
2913
+ 180,
2914
+ 4
2915
+ ],
2916
+ [
2917
+ 239,
2918
+ 239,
2919
+ 2
2920
+ ],
2921
+ [
2922
+ 521,
2923
+ 165,
2924
+ 3
2925
+ ],
2926
+ [
2927
+ 514,
2928
+ 148,
2929
+ 1
2930
+ ],
2931
+ [
2932
+ 521,
2933
+ 166,
2934
+ 3
2935
+ ],
2936
+ [
2937
+ 163,
2938
+ 152,
2939
+ 2
2940
+ ],
2941
+ [
2942
+ 136,
2943
+ 136,
2944
+ 5
2945
+ ],
2946
+ [
2947
+ 168,
2948
+ 180,
2949
+ 1
2950
+ ],
2951
+ [
2952
+ 522,
2953
+ 144,
2954
+ 2
2955
+ ],
2956
+ [
2957
+ 517,
2958
+ 512,
2959
+ 1
2960
+ ],
2961
+ [
2962
+ 515,
2963
+ 181,
2964
+ 4
2965
+ ],
2966
+ [
2967
+ 132,
2968
+ 132,
2969
+ 5
2970
+ ],
2971
+ [
2972
+ 514,
2973
+ 149,
2974
+ 1
2975
+ ],
2976
+ [
2977
+ 151,
2978
+ 151,
2979
+ 4
2980
+ ],
2981
+ [
2982
+ 514,
2983
+ 146,
2984
+ 1
2985
+ ],
2986
+ [
2987
+ 514,
2988
+ 147,
2989
+ 1
2990
+ ],
2991
+ [
2992
+ 514,
2993
+ 136,
2994
+ 1
2995
+ ],
2996
+ [
2997
+ 156,
2998
+ 172,
2999
+ 2
3000
+ ],
3001
+ [
3002
+ 237,
3003
+ 237,
3004
+ 2
3005
+ ],
3006
+ [
3007
+ 138,
3008
+ 138,
3009
+ 5
3010
+ ],
3011
+ [
3012
+ 149,
3013
+ 149,
3014
+ 2
3015
+ ],
3016
+ [
3017
+ 514,
3018
+ 138,
3019
+ 1
3020
+ ],
3021
+ [
3022
+ 152,
3023
+ 152,
3024
+ 2
3025
+ ],
3026
+ [
3027
+ 158,
3028
+ 165,
3029
+ 1
3030
+ ],
3031
+ [
3032
+ 512,
3033
+ 176,
3034
+ 4
3035
+ ],
3036
+ [
3037
+ 514,
3038
+ 132,
3039
+ 5
3040
+ ],
3041
+ [
3042
+ 148,
3043
+ 148,
3044
+ 6
3045
+ ],
3046
+ [
3047
+ 104,
3048
+ 104,
3049
+ 4
3050
+ ],
3051
+ [
3052
+ 156,
3053
+ 166,
3054
+ 1
3055
+ ],
3056
+ [
3057
+ 514,
3058
+ 149,
3059
+ 5
3060
+ ],
3061
+ [
3062
+ 103,
3063
+ 189,
3064
+ 2
3065
+ ],
3066
+ [
3067
+ 150,
3068
+ 150,
3069
+ 4
3070
+ ],
3071
+ [
3072
+ 146,
3073
+ 146,
3074
+ 5
3075
+ ],
3076
+ [
3077
+ 525,
3078
+ 152,
3079
+ 5
3080
+ ],
3081
+ [
3082
+ 165,
3083
+ 158,
3084
+ 1
3085
+ ],
3086
+ [
3087
+ 177,
3088
+ 177,
3089
+ 4
3090
+ ],
3091
+ [
3092
+ 170,
3093
+ 170,
3094
+ 6
3095
+ ],
3096
+ [
3097
+ 147,
3098
+ 147,
3099
+ 5
3100
+ ],
3101
+ [
3102
+ 514,
3103
+ 148,
3104
+ 5
3105
+ ],
3106
+ [
3107
+ 522,
3108
+ 145,
3109
+ 2
3110
+ ],
3111
+ [
3112
+ 179,
3113
+ 136,
3114
+ 2
3115
+ ],
3116
+ [
3117
+ 525,
3118
+ 156,
3119
+ 5
3120
+ ],
3121
+ [
3122
+ 521,
3123
+ 173,
3124
+ 3
3125
+ ],
3126
+ [
3127
+ 166,
3128
+ 156,
3129
+ 1
3130
+ ],
3131
+ [
3132
+ 133,
3133
+ 133,
3134
+ 5
3135
+ ],
3136
+ [
3137
+ 221,
3138
+ 221,
3139
+ 2
3140
+ ],
3141
+ [
3142
+ 158,
3143
+ 168,
3144
+ 1
3145
+ ],
3146
+ [
3147
+ 148,
3148
+ 148,
3149
+ 2
3150
+ ],
3151
+ [
3152
+ 232,
3153
+ 232,
3154
+ 2
3155
+ ],
3156
+ [
3157
+ 514,
3158
+ 138,
3159
+ 5
3160
+ ],
3161
+ [
3162
+ 145,
3163
+ 145,
3164
+ 4
3165
+ ],
3166
+ [
3167
+ 117,
3168
+ 117,
3169
+ 3
3170
+ ],
3171
+ [
3172
+ 514,
3173
+ 136,
3174
+ 5
3175
+ ],
3176
+ [
3177
+ 525,
3178
+ 163,
3179
+ 5
3180
+ ],
3181
+ [
3182
+ 222,
3183
+ 222,
3184
+ 2
3185
+ ],
3186
+ [
3187
+ 521,
3188
+ 181,
3189
+ 3
3190
+ ],
3191
+ [
3192
+ 514,
3193
+ 512,
3194
+ 1
3195
+ ],
3196
+ [
3197
+ 152,
3198
+ 104,
3199
+ 1
3200
+ ],
3201
+ [
3202
+ 166,
3203
+ 168,
3204
+ 1
3205
+ ],
3206
+ [
3207
+ 165,
3208
+ 168,
3209
+ 1
3210
+ ],
3211
+ [
3212
+ 514,
3213
+ 147,
3214
+ 5
3215
+ ],
3216
+ [
3217
+ 104,
3218
+ 152,
3219
+ 2
3220
+ ],
3221
+ [
3222
+ 514,
3223
+ 146,
3224
+ 5
3225
+ ],
3226
+ [
3227
+ 149,
3228
+ 149,
3229
+ 6
3230
+ ],
3231
+ [
3232
+ 514,
3233
+ 151,
3234
+ 1
3235
+ ],
3236
+ [
3237
+ 521,
3238
+ 180,
3239
+ 3
3240
+ ],
3241
+ [
3242
+ 144,
3243
+ 144,
3244
+ 2
3245
+ ],
3246
+ [
3247
+ 525,
3248
+ 158,
3249
+ 5
3250
+ ],
3251
+ [
3252
+ 172,
3253
+ 156,
3254
+ 2
3255
+ ],
3256
+ [
3257
+ 514,
3258
+ 145,
3259
+ 1
3260
+ ],
3261
+ [
3262
+ 522,
3263
+ 148,
3264
+ 2
3265
+ ],
3266
+ [
3267
+ 514,
3268
+ 150,
3269
+ 1
3270
+ ],
3271
+ [
3272
+ 113,
3273
+ 113,
3274
+ 3
3275
+ ],
3276
+ [
3277
+ 103,
3278
+ 179,
3279
+ 2
3280
+ ],
3281
+ [
3282
+ 163,
3283
+ 104,
3284
+ 1
3285
+ ],
3286
+ [
3287
+ 114,
3288
+ 114,
3289
+ 3
3290
+ ],
3291
+ [
3292
+ 115,
3293
+ 115,
3294
+ 3
3295
+ ],
3296
+ [
3297
+ 157,
3298
+ 157,
3299
+ 3
3300
+ ],
3301
+ [
3302
+ 165,
3303
+ 152,
3304
+ 1
3305
+ ],
3306
+ [
3307
+ 155,
3308
+ 155,
3309
+ 3
3310
+ ],
3311
+ [
3312
+ 166,
3313
+ 152,
3314
+ 1
3315
+ ],
3316
+ [
3317
+ 144,
3318
+ 512,
3319
+ 2
3320
+ ],
3321
+ [
3322
+ 220,
3323
+ 220,
3324
+ 2
3325
+ ],
3326
+ [
3327
+ 290,
3328
+ 290,
3329
+ 2
3330
+ ],
3331
+ [
3332
+ 177,
3333
+ 138,
3334
+ 2
3335
+ ],
3336
+ [
3337
+ 156,
3338
+ 168,
3339
+ 1
3340
+ ],
3341
+ [
3342
+ 132,
3343
+ 512,
3344
+ 2
3345
+ ],
3346
+ [
3347
+ 133,
3348
+ 512,
3349
+ 2
3350
+ ],
3351
+ [
3352
+ 164,
3353
+ 164,
3354
+ 3
3355
+ ],
3356
+ [
3357
+ 525,
3358
+ 168,
3359
+ 5
3360
+ ],
3361
+ [
3362
+ 514,
3363
+ 150,
3364
+ 5
3365
+ ],
3366
+ [
3367
+ 525,
3368
+ 170,
3369
+ 5
3370
+ ],
3371
+ [
3372
+ 158,
3373
+ 152,
3374
+ 1
3375
+ ],
3376
+ [
3377
+ 514,
3378
+ 151,
3379
+ 5
3380
+ ],
3381
+ [
3382
+ 132,
3383
+ 176,
3384
+ 2
3385
+ ],
3386
+ [
3387
+ 521,
3388
+ 172,
3389
+ 3
3390
+ ],
3391
+ [
3392
+ 294,
3393
+ 294,
3394
+ 2
3395
+ ],
3396
+ [
3397
+ 147,
3398
+ 512,
3399
+ 2
3400
+ ],
3401
+ [
3402
+ 148,
3403
+ 144,
3404
+ 2
3405
+ ],
3406
+ [
3407
+ 522,
3408
+ 149,
3409
+ 2
3410
+ ],
3411
+ [
3412
+ 103,
3413
+ 136,
3414
+ 4
3415
+ ],
3416
+ [
3417
+ 173,
3418
+ 158,
3419
+ 2
3420
+ ],
3421
+ [
3422
+ 146,
3423
+ 512,
3424
+ 2
3425
+ ],
3426
+ [
3427
+ 165,
3428
+ 104,
3429
+ 1
3430
+ ],
3431
+ [
3432
+ 103,
3433
+ 132,
3434
+ 4
3435
+ ],
3436
+ [
3437
+ 103,
3438
+ 138,
3439
+ 4
3440
+ ],
3441
+ [
3442
+ 130,
3443
+ 130,
3444
+ 2
3445
+ ],
3446
+ [
3447
+ 144,
3448
+ 148,
3449
+ 2
3450
+ ],
3451
+ [
3452
+ 166,
3453
+ 104,
3454
+ 1
3455
+ ],
3456
+ [
3457
+ 156,
3458
+ 152,
3459
+ 1
3460
+ ],
3461
+ [
3462
+ 179,
3463
+ 179,
3464
+ 4
3465
+ ],
3466
+ [
3467
+ 519,
3468
+ 132,
3469
+ 6
3470
+ ],
3471
+ [
3472
+ 145,
3473
+ 145,
3474
+ 5
3475
+ ],
3476
+ [
3477
+ 189,
3478
+ 189,
3479
+ 3
3480
+ ],
3481
+ [
3482
+ 527,
3483
+ 152,
3484
+ 6
3485
+ ],
3486
+ [
3487
+ 153,
3488
+ 153,
3489
+ 3
3490
+ ],
3491
+ [
3492
+ 524,
3493
+ 132,
3494
+ 4
3495
+ ],
3496
+ [
3497
+ 519,
3498
+ 148,
3499
+ 6
3500
+ ],
3501
+ [
3502
+ 522,
3503
+ 151,
3504
+ 2
3505
+ ],
3506
+ [
3507
+ 522,
3508
+ 150,
3509
+ 2
3510
+ ],
3511
+ [
3512
+ 159,
3513
+ 159,
3514
+ 3
3515
+ ],
3516
+ [
3517
+ 514,
3518
+ 103,
3519
+ 5
3520
+ ],
3521
+ [
3522
+ 163,
3523
+ 170,
3524
+ 1
3525
+ ],
3526
+ [
3527
+ 519,
3528
+ 144,
3529
+ 6
3530
+ ],
3531
+ [
3532
+ 138,
3533
+ 512,
3534
+ 2
3535
+ ],
3536
+ [
3537
+ 162,
3538
+ 162,
3539
+ 3
3540
+ ],
3541
+ [
3542
+ 519,
3543
+ 136,
3544
+ 6
3545
+ ],
3546
+ [
3547
+ 156,
3548
+ 104,
3549
+ 1
3550
+ ],
3551
+ [
3552
+ 181,
3553
+ 181,
3554
+ 3
3555
+ ],
3556
+ [
3557
+ 103,
3558
+ 177,
3559
+ 2
3560
+ ],
3561
+ [
3562
+ 158,
3563
+ 104,
3564
+ 1
3565
+ ],
3566
+ [
3567
+ 151,
3568
+ 151,
3569
+ 5
3570
+ ],
3571
+ [
3572
+ 519,
3573
+ 133,
3574
+ 6
3575
+ ],
3576
+ [
3577
+ 148,
3578
+ 512,
3579
+ 2
3580
+ ],
3581
+ [
3582
+ 136,
3583
+ 512,
3584
+ 2
3585
+ ],
3586
+ [
3587
+ 103,
3588
+ 133,
3589
+ 4
3590
+ ],
3591
+ [
3592
+ 176,
3593
+ 132,
3594
+ 2
3595
+ ],
3596
+ [
3597
+ 168,
3598
+ 168,
3599
+ 2
3600
+ ],
3601
+ [
3602
+ 519,
3603
+ 149,
3604
+ 6
3605
+ ],
3606
+ [
3607
+ 519,
3608
+ 146,
3609
+ 6
3610
+ ],
3611
+ [
3612
+ 525,
3613
+ 172,
3614
+ 5
3615
+ ],
3616
+ [
3617
+ 313,
3618
+ 313,
3619
+ 2
3620
+ ],
3621
+ [
3622
+ 526,
3623
+ 163,
3624
+ 2
3625
+ ],
3626
+ [
3627
+ 521,
3628
+ 104,
3629
+ 4
3630
+ ],
3631
+ [
3632
+ 136,
3633
+ 136,
3634
+ 6
3635
+ ],
3636
+ [
3637
+ 170,
3638
+ 170,
3639
+ 4
3640
+ ],
3641
+ [
3642
+ 524,
3643
+ 133,
3644
+ 4
3645
+ ],
3646
+ [
3647
+ 519,
3648
+ 147,
3649
+ 6
3650
+ ],
3651
+ [
3652
+ 525,
3653
+ 165,
3654
+ 5
3655
+ ],
3656
+ [
3657
+ 525,
3658
+ 173,
3659
+ 5
3660
+ ],
3661
+ [
3662
+ 138,
3663
+ 138,
3664
+ 6
3665
+ ],
3666
+ [
3667
+ 166,
3668
+ 158,
3669
+ 1
3670
+ ],
3671
+ [
3672
+ 41,
3673
+ 41,
3674
+ 2
3675
+ ],
3676
+ [
3677
+ 527,
3678
+ 156,
3679
+ 6
3680
+ ],
3681
+ [
3682
+ 524,
3683
+ 147,
3684
+ 4
3685
+ ],
3686
+ [
3687
+ 240,
3688
+ 240,
3689
+ 2
3690
+ ],
3691
+ [
3692
+ 149,
3693
+ 512,
3694
+ 2
3695
+ ],
3696
+ [
3697
+ 154,
3698
+ 154,
3699
+ 3
3700
+ ],
3701
+ [
3702
+ 103,
3703
+ 146,
3704
+ 4
3705
+ ],
3706
+ [
3707
+ 525,
3708
+ 180,
3709
+ 5
3710
+ ],
3711
+ [
3712
+ 163,
3713
+ 513,
3714
+ 1
3715
+ ],
3716
+ [
3717
+ 519,
3718
+ 138,
3719
+ 6
3720
+ ],
3721
+ [
3722
+ 150,
3723
+ 150,
3724
+ 5
3725
+ ],
3726
+ [
3727
+ 524,
3728
+ 146,
3729
+ 4
3730
+ ],
3731
+ [
3732
+ 532,
3733
+ 152,
3734
+ 7
3735
+ ],
3736
+ [
3737
+ 189,
3738
+ 133,
3739
+ 2
3740
+ ],
3741
+ [
3742
+ 132,
3743
+ 146,
3744
+ 2
3745
+ ],
3746
+ [
3747
+ 524,
3748
+ 145,
3749
+ 4
3750
+ ],
3751
+ [
3752
+ 163,
3753
+ 152,
3754
+ 3
3755
+ ],
3756
+ [
3757
+ 525,
3758
+ 181,
3759
+ 5
3760
+ ],
3761
+ [
3762
+ 217,
3763
+ 217,
3764
+ 2
3765
+ ],
3766
+ [
3767
+ 156,
3768
+ 156,
3769
+ 2
3770
+ ],
3771
+ [
3772
+ 524,
3773
+ 144,
3774
+ 4
3775
+ ],
3776
+ [
3777
+ 146,
3778
+ 132,
3779
+ 2
3780
+ ],
3781
+ [
3782
+ 147,
3783
+ 133,
3784
+ 2
3785
+ ],
3786
+ [
3787
+ 514,
3788
+ 132,
3789
+ 3
3790
+ ],
3791
+ [
3792
+ 158,
3793
+ 173,
3794
+ 2
3795
+ ],
3796
+ [
3797
+ 145,
3798
+ 149,
3799
+ 2
3800
+ ],
3801
+ [
3802
+ 35,
3803
+ 35,
3804
+ 2
3805
+ ],
3806
+ [
3807
+ 105,
3808
+ 105,
3809
+ 3
3810
+ ],
3811
+ [
3812
+ 242,
3813
+ 242,
3814
+ 2
3815
+ ],
3816
+ [
3817
+ 527,
3818
+ 163,
3819
+ 6
3820
+ ],
3821
+ [
3822
+ 219,
3823
+ 219,
3824
+ 2
3825
+ ],
3826
+ [
3827
+ 103,
3828
+ 147,
3829
+ 4
3830
+ ],
3831
+ [
3832
+ 236,
3833
+ 236,
3834
+ 2
3835
+ ],
3836
+ [
3837
+ 514,
3838
+ 136,
3839
+ 3
3840
+ ],
3841
+ [
3842
+ 104,
3843
+ 167,
3844
+ 1
3845
+ ],
3846
+ [
3847
+ 314,
3848
+ 314,
3849
+ 2
3850
+ ],
3851
+ [
3852
+ 526,
3853
+ 168,
3854
+ 2
3855
+ ],
3856
+ [
3857
+ 132,
3858
+ 149,
3859
+ 2
3860
+ ],
3861
+ [
3862
+ 151,
3863
+ 151,
3864
+ 2
3865
+ ],
3866
+ [
3867
+ 527,
3868
+ 158,
3869
+ 6
3870
+ ],
3871
+ [
3872
+ 132,
3873
+ 147,
3874
+ 2
3875
+ ],
3876
+ [
3877
+ 148,
3878
+ 133,
3879
+ 2
3880
+ ],
3881
+ [
3882
+ 514,
3883
+ 138,
3884
+ 3
3885
+ ],
3886
+ [
3887
+ 527,
3888
+ 168,
3889
+ 6
3890
+ ],
3891
+ [
3892
+ 525,
3893
+ 166,
3894
+ 5
3895
+ ],
3896
+ [
3897
+ 176,
3898
+ 145,
3899
+ 2
3900
+ ],
3901
+ [
3902
+ 361,
3903
+ 361,
3904
+ 2
3905
+ ],
3906
+ [
3907
+ 519,
3908
+ 145,
3909
+ 6
3910
+ ],
3911
+ [
3912
+ 146,
3913
+ 136,
3914
+ 2
3915
+ ],
3916
+ [
3917
+ 132,
3918
+ 136,
3919
+ 2
3920
+ ],
3921
+ [
3922
+ 527,
3923
+ 170,
3924
+ 6
3925
+ ],
3926
+ [
3927
+ 180,
3928
+ 168,
3929
+ 2
3930
+ ],
3931
+ [
3932
+ 146,
3933
+ 133,
3934
+ 2
3935
+ ],
3936
+ [
3937
+ 165,
3938
+ 165,
3939
+ 4
3940
+ ],
3941
+ [
3942
+ 170,
3943
+ 104,
3944
+ 1
3945
+ ],
3946
+ [
3947
+ 82,
3948
+ 82,
3949
+ 2
3950
+ ],
3951
+ [
3952
+ 246,
3953
+ 246,
3954
+ 2
3955
+ ],
3956
+ [
3957
+ 132,
3958
+ 138,
3959
+ 2
3960
+ ],
3961
+ [
3962
+ 532,
3963
+ 156,
3964
+ 7
3965
+ ],
3966
+ [
3967
+ 147,
3968
+ 144,
3969
+ 2
3970
+ ],
3971
+ [
3972
+ 158,
3973
+ 158,
3974
+ 2
3975
+ ],
3976
+ [
3977
+ 514,
3978
+ 148,
3979
+ 3
3980
+ ],
3981
+ [
3982
+ 526,
3983
+ 170,
3984
+ 2
3985
+ ],
3986
+ [
3987
+ 345,
3988
+ 345,
3989
+ 2
3990
+ ],
3991
+ [
3992
+ 514,
3993
+ 149,
3994
+ 3
3995
+ ],
3996
+ [
3997
+ 147,
3998
+ 138,
3999
+ 2
4000
+ ],
4001
+ [
4002
+ 517,
4003
+ 512,
4004
+ 3
4005
+ ],
4006
+ [
4007
+ 163,
4008
+ 163,
4009
+ 5
4010
+ ],
4011
+ [
4012
+ 514,
4013
+ 150,
4014
+ 3
4015
+ ],
4016
+ [
4017
+ 223,
4018
+ 223,
4019
+ 2
4020
+ ],
4021
+ [
4022
+ 166,
4023
+ 166,
4024
+ 4
4025
+ ],
4026
+ [
4027
+ 523,
4028
+ 147,
4029
+ 4
4030
+ ],
4031
+ [
4032
+ 136,
4033
+ 146,
4034
+ 2
4035
+ ],
4036
+ [
4037
+ 526,
4038
+ 152,
4039
+ 2
4040
+ ],
4041
+ [
4042
+ 132,
4043
+ 148,
4044
+ 2
4045
+ ],
4046
+ [
4047
+ 165,
4048
+ 156,
4049
+ 1
4050
+ ],
4051
+ [
4052
+ 206,
4053
+ 206,
4054
+ 2
4055
+ ],
4056
+ [
4057
+ 524,
4058
+ 136,
4059
+ 4
4060
+ ],
4061
+ [
4062
+ 514,
4063
+ 145,
4064
+ 3
4065
+ ],
4066
+ [
4067
+ 366,
4068
+ 366,
4069
+ 2
4070
+ ],
4071
+ [
4072
+ 519,
4073
+ 151,
4074
+ 6
4075
+ ],
4076
+ [
4077
+ 136,
4078
+ 133,
4079
+ 2
4080
+ ],
4081
+ [
4082
+ 104,
4083
+ 168,
4084
+ 2
4085
+ ],
4086
+ [
4087
+ 222,
4088
+ 222,
4089
+ 3
4090
+ ],
4091
+ [
4092
+ 523,
4093
+ 132,
4094
+ 4
4095
+ ],
4096
+ [
4097
+ 524,
4098
+ 138,
4099
+ 4
4100
+ ],
4101
+ [
4102
+ 170,
4103
+ 152,
4104
+ 1
4105
+ ],
4106
+ [
4107
+ 40,
4108
+ 40,
4109
+ 2
4110
+ ],
4111
+ [
4112
+ 181,
4113
+ 181,
4114
+ 4
4115
+ ],
4116
+ [
4117
+ 173,
4118
+ 173,
4119
+ 3
4120
+ ],
4121
+ [
4122
+ 328,
4123
+ 328,
4124
+ 2
4125
+ ],
4126
+ [
4127
+ 145,
4128
+ 512,
4129
+ 2
4130
+ ],
4131
+ [
4132
+ 517,
4133
+ 103,
4134
+ 3
4135
+ ],
4136
+ [
4137
+ 514,
4138
+ 545,
4139
+ 1
4140
+ ],
4141
+ [
4142
+ 526,
4143
+ 156,
4144
+ 2
4145
+ ],
4146
+ [
4147
+ 150,
4148
+ 150,
4149
+ 2
4150
+ ],
4151
+ [
4152
+ 517,
4153
+ 176,
4154
+ 6
4155
+ ],
4156
+ [
4157
+ 514,
4158
+ 146,
4159
+ 3
4160
+ ],
4161
+ [
4162
+ 167,
4163
+ 167,
4164
+ 3
4165
+ ],
4166
+ [
4167
+ 146,
4168
+ 144,
4169
+ 2
4170
+ ],
4171
+ [
4172
+ 532,
4173
+ 168,
4174
+ 7
4175
+ ],
4176
+ [
4177
+ 138,
4178
+ 177,
4179
+ 2
4180
+ ],
4181
+ [
4182
+ 149,
4183
+ 149,
4184
+ 7
4185
+ ],
4186
+ [
4187
+ 103,
4188
+ 103,
4189
+ 3
4190
+ ],
4191
+ [
4192
+ 316,
4193
+ 316,
4194
+ 2
4195
+ ],
4196
+ [
4197
+ 519,
4198
+ 150,
4199
+ 6
4200
+ ],
4201
+ [
4202
+ 532,
4203
+ 163,
4204
+ 7
4205
+ ],
4206
+ [
4207
+ 532,
4208
+ 158,
4209
+ 7
4210
+ ],
4211
+ [
4212
+ 514,
4213
+ 151,
4214
+ 3
4215
+ ],
4216
+ [
4217
+ 138,
4218
+ 147,
4219
+ 2
4220
+ ]
4221
+ ]
4222
+ }
variants/extended-1st-10mm-1k/profiles/eef/config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "model_type": "actioncodec2_unified",
3
+ "processor_class": "ActionCodec2Processor"
4
+ }
variants/extended-1st-10mm-1k/profiles/eef/config.yaml ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model_type: actioncodec2_unified
2
+ config_version: 1
3
+ physical:
4
+ input_dim: 7
5
+ components:
6
+ translation_m:
7
+ dims:
8
+ start: 0
9
+ stop: 3
10
+ space: additive
11
+ input_order: 0
12
+ primitive_order: 1
13
+ input_transform:
14
+ scale: 1.0
15
+ clip: false
16
+ constraints:
17
+ order_0:
18
+ min: -2.0
19
+ max: 2.0
20
+ order_1:
21
+ min: -0.08
22
+ max: 0.08
23
+ distortion_limit: 0.01
24
+ primitive_selection: smooth
25
+ orientation_so3:
26
+ dims:
27
+ start: 3
28
+ stop: 6
29
+ space: so3
30
+ input_order: 0
31
+ primitive_order: 1
32
+ input_transform:
33
+ scale: 1.0
34
+ clip: false
35
+ constraints:
36
+ order_0:
37
+ min: -3.141592653589793
38
+ max: 3.141592653589793
39
+ order_1:
40
+ min: -0.25
41
+ max: 0.25
42
+ distortion_limit: 0.04
43
+ primitive_selection: smooth
44
+ representation: rotation_vector
45
+ multiplication: right
46
+ gripper_open_command:
47
+ dims:
48
+ - 6
49
+ space: additive
50
+ input_order: 0
51
+ primitive_order: 0
52
+ input_transform:
53
+ scale: 1.0
54
+ clip: true
55
+ constraints:
56
+ order_0:
57
+ min: 0.0
58
+ max: 1.0
59
+ distortion_limit: 0.5
60
+ primitive_selection: nearest
61
+ set_bpe:
62
+ lookahead: 8
63
+ allow_cross_component_merge: false
64
+ train:
65
+ primitives:
66
+ zero_primitive_components:
67
+ - translation_m
68
+ - orientation_so3
69
+ bpe:
70
+ spatial_vocab_size: 512
71
+ vocab_size: 1024
72
+ quality_control:
73
+ hard_order_1_scale: 3.0
74
+ min_segment_length: 2