x4d-devkit 0.8.0__tar.gz → 0.11.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {x4d_devkit-0.8.0/x4d_devkit.egg-info → x4d_devkit-0.11.1}/PKG-INFO +1 -1
  2. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/pyproject.toml +1 -1
  3. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_coordinate_frames.py +1 -1
  4. x4d_devkit-0.11.1/tests/test_v06_schema.py +414 -0
  5. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/converters/nuscenes.py +89 -28
  6. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/loader.py +115 -19
  7. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/models.py +38 -13
  8. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/transform.py +112 -0
  9. x4d_devkit-0.11.1/x4d_devkit/validation/validator.py +463 -0
  10. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1/x4d_devkit.egg-info}/PKG-INFO +1 -1
  11. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/SOURCES.txt +1 -0
  12. x4d_devkit-0.8.0/x4d_devkit/validation/validator.py +0 -273
  13. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/LICENSE +0 -0
  14. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/README.md +0 -0
  15. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/setup.cfg +0 -0
  16. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_box.py +0 -0
  17. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_detection_eval.py +0 -0
  18. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_evaluate.py +0 -0
  19. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_fusion.py +0 -0
  20. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_loader.py +0 -0
  21. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_matching.py +0 -0
  22. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_metrics.py +0 -0
  23. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_models.py +0 -0
  24. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_nuscenes_converter.py +0 -0
  25. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_token.py +0 -0
  26. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_transform.py +0 -0
  27. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_transform_chain.py +0 -0
  28. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_v06_api_additions.py +0 -0
  29. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/tests/test_validation.py +0 -0
  30. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/__init__.py +0 -0
  31. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/cli.py +0 -0
  32. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/__init__.py +0 -0
  33. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/checkpoints.py +0 -0
  34. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/client.py +0 -0
  35. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/clips.py +0 -0
  36. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/projects.py +0 -0
  37. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/client/test_sets.py +0 -0
  38. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/converters/__init__.py +0 -0
  39. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/__init__.py +0 -0
  40. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/core/token.py +0 -0
  41. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/__init__.py +0 -0
  42. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/api.py +0 -0
  43. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/box.py +0 -0
  44. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/detection.py +0 -0
  45. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/matching.py +0 -0
  46. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/eval/metrics.py +0 -0
  47. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/fusion.py +0 -0
  48. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/validation/__init__.py +0 -0
  49. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit/validation/report.py +0 -0
  50. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  51. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/entry_points.txt +0 -0
  52. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/requires.txt +0 -0
  53. {x4d_devkit-0.8.0 → x4d_devkit-0.11.1}/x4d_devkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.8.0
3
+ Version: 0.11.1
4
4
  Summary: X-4D dataset format SDK — load, validate, evaluate, and convert autonomous driving datasets
5
5
  Author: windzu
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x4d-devkit"
7
- version = "0.8.0"
7
+ version = "0.11.1"
8
8
  description = "X-4D dataset format SDK — load, validate, evaluate, and convert autonomous driving datasets"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -331,7 +331,7 @@ class TestAnnotationsForSampleFrames:
331
331
  assert not np.allclose(anns_world[0].translation, anns_ego[0].translation, atol=0.1)
332
332
 
333
333
  def test_sensor_frame_raises(self, loader):
334
- with pytest.raises(ValueError, match="sensor frame"):
334
+ with pytest.raises(ValueError, match="ambiguous"):
335
335
  loader.annotations_for_sample("s-0", frame="sensor")
336
336
 
337
337
  def test_invalid_frame_raises(self, loader):
@@ -0,0 +1,414 @@
1
+ """Tests for v0.6 schema additions: annotation_frame_id + per-sensor frame_id.
2
+
3
+ Covers:
4
+ - SensorInfo.frame_id round-trip
5
+ - ClipMeta.annotation_frame_id round-trip
6
+ - ClipLoader.annotation_frame_id property (explicit + back-compat default)
7
+ - ClipLoader.frame_id_for_channel
8
+ - annotations_for_sample(frame=<arbitrary frame_id>)
9
+ - TransformChain.get_ego_from_frame_id
10
+ - validate_clip schema 0.6 strict checks
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import math
17
+ from pathlib import Path
18
+
19
+ import numpy as np
20
+ import pytest
21
+
22
+ from x4d_devkit import ClipLoader, validate_clip
23
+ from x4d_devkit.core.models import ClipMeta, PointFormat, SensorInfo
24
+
25
+
26
+ def _q_from_yaw(yaw: float) -> dict[str, float]:
27
+ return {"qx": 0.0, "qy": 0.0, "qz": math.sin(yaw / 2), "qw": math.cos(yaw / 2)}
28
+
29
+
30
+ def _write_clip(
31
+ clip_dir: Path,
32
+ *,
33
+ schema_version: str,
34
+ annotation_frame_id: str | None,
35
+ sensors_meta: dict,
36
+ calibrated: list[dict],
37
+ samples: list[dict] | None = None,
38
+ sample_data: list[dict] | None = None,
39
+ annotations: list[dict] | None = None,
40
+ ) -> None:
41
+ clip_dir.mkdir(parents=True, exist_ok=True)
42
+ meta = {
43
+ "clip_id": "test-v06",
44
+ "schema_version": schema_version,
45
+ "session_id": "s",
46
+ "vehicle_id": "v",
47
+ "capture_time_utc": "2026-05-06T00:00:00Z",
48
+ "duration_s": 0.0,
49
+ "keyframe_count": len(samples or []),
50
+ "sweep_count": 0,
51
+ "sensors": sensors_meta,
52
+ }
53
+ if annotation_frame_id is not None:
54
+ meta["annotation_frame_id"] = annotation_frame_id
55
+ (clip_dir / "meta.json").write_text(json.dumps(meta))
56
+ (clip_dir / "calibrated_sensor.json").write_text(json.dumps(calibrated))
57
+ (clip_dir / "ego_pose.json").write_text(json.dumps([{
58
+ "ego_pose_token": "ep-0",
59
+ "timestamp_us": 0,
60
+ "translation": {"x": 0, "y": 0, "z": 0},
61
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
62
+ }]))
63
+ (clip_dir / "sample.json").write_text(json.dumps(samples or [{
64
+ "sample_token": "s-0", "timestamp_us": 0, "is_keyframe": True,
65
+ "prev": "", "next": "",
66
+ }]))
67
+ (clip_dir / "sample_data.json").write_text(json.dumps(sample_data or [{
68
+ "sample_data_token": "sd-0",
69
+ "sample_token": "s-0",
70
+ "channel": calibrated[0]["channel"],
71
+ "timestamp_us": 0,
72
+ "file_path": "samples/x/0.bin",
73
+ "ego_pose_token": "ep-0",
74
+ "calibrated_sensor_token": calibrated[0]["calibrated_sensor_token"],
75
+ "is_keyframe": True,
76
+ "width": None, "height": None,
77
+ "prev": "", "next": "",
78
+ }]))
79
+ (clip_dir / "annotation.json").write_text(json.dumps(annotations or []))
80
+ (clip_dir / "instance.json").write_text(json.dumps([]))
81
+
82
+
83
+ # --- Model round-trip ---
84
+
85
+
86
+ def test_sensor_info_frame_id_round_trip():
87
+ info = SensorInfo.from_dict({"modality": "lidar", "frame_id": "LIDAR_TOP"})
88
+ assert info.frame_id == "LIDAR_TOP"
89
+ assert info.to_dict()["frame_id"] == "LIDAR_TOP"
90
+
91
+
92
+ def test_sensor_info_no_frame_id_omits_key_in_to_dict():
93
+ info = SensorInfo.from_dict({"modality": "camera"})
94
+ assert info.frame_id is None
95
+ assert "frame_id" not in info.to_dict()
96
+
97
+
98
+ def test_clip_meta_annotation_frame_id_default_none():
99
+ meta = ClipMeta(
100
+ clip_id="x", schema_version="0.5", session_id="s", vehicle_id="v",
101
+ capture_time_utc="t", duration_s=0.0, num_keyframes=0, num_sweeps=0,
102
+ sensors={},
103
+ )
104
+ assert meta.annotation_frame_id is None
105
+
106
+
107
+ # --- Loader: annotation_frame_id property ---
108
+
109
+
110
+ def test_loader_annotation_frame_id_explicit(tmp_path):
111
+ _write_clip(
112
+ tmp_path,
113
+ schema_version="0.6",
114
+ annotation_frame_id="LIDAR_TOP",
115
+ sensors_meta={"LIDAR_TOP": {"modality": "lidar", "frame_id": "LIDAR_TOP",
116
+ "point_format": {"fields": ["x", "y", "z"],
117
+ "types": ["float32"] * 3,
118
+ "bytes_per_point": 12}}},
119
+ calibrated=[{
120
+ "calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
121
+ "translation": {"x": 0, "y": 0, "z": 1.5},
122
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
123
+ "intrinsics": None, "distortion": None,
124
+ }],
125
+ )
126
+ loader = ClipLoader(tmp_path)
127
+ assert loader.annotation_frame_id == "LIDAR_TOP"
128
+
129
+
130
+ def test_loader_annotation_frame_id_back_compat_default(tmp_path):
131
+ """Pre-v0.6 clip with no annotation_frame_id falls back to base_link."""
132
+ _write_clip(
133
+ tmp_path,
134
+ schema_version="0.5",
135
+ annotation_frame_id=None,
136
+ sensors_meta={"lidar": {"modality": "lidar"}},
137
+ calibrated=[{
138
+ "calibrated_sensor_token": "cs-0", "channel": "lidar",
139
+ "translation": {"x": 0, "y": 0, "z": 0},
140
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
141
+ "intrinsics": None, "distortion": None,
142
+ }],
143
+ )
144
+ loader = ClipLoader(tmp_path)
145
+ assert loader.annotation_frame_id == "base_link"
146
+
147
+
148
+ def test_loader_frame_id_for_channel_explicit(tmp_path):
149
+ _write_clip(
150
+ tmp_path,
151
+ schema_version="0.6",
152
+ annotation_frame_id="base_link",
153
+ sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
154
+ calibrated=[{
155
+ "calibrated_sensor_token": "cs-0", "channel": "lidar",
156
+ "translation": {"x": 0, "y": 0, "z": 0},
157
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
158
+ "intrinsics": None, "distortion": None,
159
+ }],
160
+ )
161
+ loader = ClipLoader(tmp_path)
162
+ assert loader.frame_id_for_channel("lidar") == "base_link"
163
+
164
+
165
+ def test_loader_frame_id_for_channel_fallback_to_channel_name(tmp_path):
166
+ """Pre-v0.6 clip falls back to channel ≡ frame_id convention."""
167
+ _write_clip(
168
+ tmp_path,
169
+ schema_version="0.5",
170
+ annotation_frame_id=None,
171
+ sensors_meta={"LIDAR_TOP": {"modality": "lidar"}},
172
+ calibrated=[{
173
+ "calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
174
+ "translation": {"x": 0, "y": 0, "z": 0},
175
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
176
+ "intrinsics": None, "distortion": None,
177
+ }],
178
+ )
179
+ loader = ClipLoader(tmp_path)
180
+ assert loader.frame_id_for_channel("LIDAR_TOP") == "LIDAR_TOP"
181
+
182
+
183
+ # --- Loader: annotations_for_sample with arbitrary frame_id ---
184
+
185
+
186
+ def test_annotations_for_sample_in_storage_frame_no_op(tmp_path):
187
+ """Requesting the storage frame returns annotations unchanged."""
188
+ ann = {
189
+ "annotation_token": "a-0", "sample_token": "s-0", "instance_token": "i-0",
190
+ "category": "car",
191
+ "bbox_3d": {
192
+ "translation": {"x": 5.0, "y": 0.0, "z": 0.0},
193
+ "size": {"length": 4.0, "width": 2.0, "height": 1.5},
194
+ "rotation": _q_from_yaw(0.0),
195
+ },
196
+ "num_lidar_pts": 100, "visibility": "4", "velocity": None,
197
+ "attributes": [], "prev": "", "next": "", "score": None,
198
+ }
199
+ _write_clip(
200
+ tmp_path,
201
+ schema_version="0.6",
202
+ annotation_frame_id="LIDAR_TOP",
203
+ sensors_meta={"LIDAR_TOP": {"modality": "lidar", "frame_id": "LIDAR_TOP",
204
+ "point_format": {"fields": ["x", "y", "z"],
205
+ "types": ["float32"] * 3,
206
+ "bytes_per_point": 12}}},
207
+ calibrated=[{
208
+ "calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
209
+ "translation": {"x": 0, "y": 0, "z": 1.5},
210
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
211
+ "intrinsics": None, "distortion": None,
212
+ }],
213
+ annotations=[ann],
214
+ )
215
+ loader = ClipLoader(tmp_path)
216
+ [a] = loader.annotations_for_sample("s-0", frame="LIDAR_TOP")
217
+ np.testing.assert_allclose(a.translation, [5.0, 0.0, 0.0])
218
+
219
+
220
+ def test_annotations_for_sample_lidar_top_to_base_link(tmp_path):
221
+ """Annotation in LIDAR_TOP with z-offset 1.5 → base_link adds 1.5 to z."""
222
+ ann = {
223
+ "annotation_token": "a-0", "sample_token": "s-0", "instance_token": "i-0",
224
+ "category": "car",
225
+ "bbox_3d": {
226
+ "translation": {"x": 5.0, "y": 0.0, "z": 0.0},
227
+ "size": {"length": 4.0, "width": 2.0, "height": 1.5},
228
+ "rotation": _q_from_yaw(0.0),
229
+ },
230
+ "num_lidar_pts": 100, "visibility": "4", "velocity": None,
231
+ "attributes": [], "prev": "", "next": "", "score": None,
232
+ }
233
+ _write_clip(
234
+ tmp_path,
235
+ schema_version="0.6",
236
+ annotation_frame_id="LIDAR_TOP",
237
+ sensors_meta={"LIDAR_TOP": {"modality": "lidar", "frame_id": "LIDAR_TOP",
238
+ "point_format": {"fields": ["x", "y", "z"],
239
+ "types": ["float32"] * 3,
240
+ "bytes_per_point": 12}}},
241
+ calibrated=[{
242
+ "calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
243
+ "translation": {"x": 0, "y": 0, "z": 1.5},
244
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
245
+ "intrinsics": None, "distortion": None,
246
+ }],
247
+ annotations=[ann],
248
+ )
249
+ loader = ClipLoader(tmp_path)
250
+ [a] = loader.annotations_for_sample("s-0", frame="base_link")
251
+ np.testing.assert_allclose(a.translation, [5.0, 0.0, 1.5])
252
+
253
+
254
+ def test_annotations_for_sample_unknown_frame_id_raises(tmp_path):
255
+ _write_clip(
256
+ tmp_path,
257
+ schema_version="0.6",
258
+ annotation_frame_id="base_link",
259
+ sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
260
+ calibrated=[{
261
+ "calibrated_sensor_token": "cs-0", "channel": "lidar",
262
+ "translation": {"x": 0, "y": 0, "z": 0},
263
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
264
+ "intrinsics": None, "distortion": None,
265
+ }],
266
+ annotations=[{
267
+ "annotation_token": "a-0", "sample_token": "s-0", "instance_token": "i-0",
268
+ "category": "car",
269
+ "bbox_3d": {"translation": {"x": 0, "y": 0, "z": 0},
270
+ "size": {"length": 1, "width": 1, "height": 1},
271
+ "rotation": _q_from_yaw(0.0)},
272
+ "num_lidar_pts": 0, "visibility": "", "velocity": None,
273
+ "attributes": [], "prev": "", "next": "", "score": None,
274
+ }],
275
+ )
276
+ loader = ClipLoader(tmp_path)
277
+ with pytest.raises(ValueError, match="Invalid frame"):
278
+ loader.annotations_for_sample("s-0", frame="never_exists_frame")
279
+
280
+
281
+ # --- TransformChain.get_ego_from_frame_id ---
282
+
283
+
284
+ def test_transform_chain_ego_from_frame_id_base_link_is_identity(tmp_path):
285
+ _write_clip(
286
+ tmp_path,
287
+ schema_version="0.6",
288
+ annotation_frame_id="base_link",
289
+ sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
290
+ calibrated=[{
291
+ "calibrated_sensor_token": "cs-0", "channel": "lidar",
292
+ "translation": {"x": 0, "y": 0, "z": 0},
293
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
294
+ "intrinsics": None, "distortion": None,
295
+ }],
296
+ )
297
+ loader = ClipLoader(tmp_path)
298
+ from x4d_devkit.core.transform import TransformChain
299
+ chain = TransformChain(loader)
300
+ T = chain.get_ego_from_frame_id("base_link")
301
+ np.testing.assert_allclose(T.translation, [0, 0, 0])
302
+ np.testing.assert_allclose(T.rotation, np.eye(3))
303
+
304
+
305
+ def test_transform_chain_ego_from_frame_id_resolves_via_sensor_frame_id(tmp_path):
306
+ """When a sensor declares frame_id != channel, lookup uses frame_id."""
307
+ _write_clip(
308
+ tmp_path,
309
+ schema_version="0.6",
310
+ annotation_frame_id="base_link",
311
+ sensors_meta={"recon_pcd": {"modality": "lidar", "frame_id": "camera-front"}},
312
+ calibrated=[{
313
+ "calibrated_sensor_token": "cs-0", "channel": "recon_pcd",
314
+ "translation": {"x": 1.0, "y": 0, "z": 0},
315
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
316
+ "intrinsics": None, "distortion": None,
317
+ }],
318
+ )
319
+ loader = ClipLoader(tmp_path)
320
+ from x4d_devkit.core.transform import TransformChain
321
+ chain = TransformChain(loader)
322
+ T = chain.get_ego_from_frame_id("camera-front")
323
+ np.testing.assert_allclose(T.translation, [1.0, 0, 0])
324
+
325
+
326
+ # --- Validator: schema 0.6 strict checks ---
327
+
328
+
329
+ def test_validate_v06_missing_annotation_frame_id_errors(tmp_path):
330
+ _write_clip(
331
+ tmp_path,
332
+ schema_version="0.6",
333
+ annotation_frame_id=None, # missing on purpose
334
+ sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
335
+ calibrated=[{
336
+ "calibrated_sensor_token": "cs-0", "channel": "lidar",
337
+ "translation": {"x": 0, "y": 0, "z": 0},
338
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
339
+ "intrinsics": None, "distortion": None,
340
+ }],
341
+ )
342
+ report = validate_clip(tmp_path)
343
+ assert any(i.check == "annotation_frame_id" for i in report.errors)
344
+
345
+
346
+ def test_validate_v06_missing_sensor_frame_id_errors(tmp_path):
347
+ _write_clip(
348
+ tmp_path,
349
+ schema_version="0.6",
350
+ annotation_frame_id="base_link",
351
+ sensors_meta={"lidar": {"modality": "lidar"}}, # no frame_id
352
+ calibrated=[{
353
+ "calibrated_sensor_token": "cs-0", "channel": "lidar",
354
+ "translation": {"x": 0, "y": 0, "z": 0},
355
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
356
+ "intrinsics": None, "distortion": None,
357
+ }],
358
+ )
359
+ report = validate_clip(tmp_path)
360
+ assert any(i.check == "sensor_frame_id" for i in report.errors)
361
+
362
+
363
+ def test_validate_v06_unresolvable_annotation_frame_errors(tmp_path):
364
+ _write_clip(
365
+ tmp_path,
366
+ schema_version="0.6",
367
+ annotation_frame_id="non_existent_frame",
368
+ sensors_meta={"lidar": {"modality": "lidar", "frame_id": "base_link"}},
369
+ calibrated=[{
370
+ "calibrated_sensor_token": "cs-0", "channel": "lidar",
371
+ "translation": {"x": 0, "y": 0, "z": 0},
372
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
373
+ "intrinsics": None, "distortion": None,
374
+ }],
375
+ )
376
+ report = validate_clip(tmp_path)
377
+ msgs = [i.message for i in report.errors if i.check == "annotation_frame_id"]
378
+ assert any("does not resolve" in m for m in msgs)
379
+
380
+
381
+ def test_validate_v06_passes_with_lidar_top_annotation_frame(tmp_path):
382
+ """Scenario 1 (nuScenes-style): annotation_frame_id == sensor's own frame_id."""
383
+ ann = {
384
+ "annotation_token": "a-0", "sample_token": "s-0", "instance_token": "i-0",
385
+ "category": "car",
386
+ "bbox_3d": {"translation": {"x": 5, "y": 0, "z": 0},
387
+ "size": {"length": 4, "width": 2, "height": 1.5},
388
+ "rotation": _q_from_yaw(0.0)},
389
+ "num_lidar_pts": 50, "visibility": "4", "velocity": None,
390
+ "attributes": [], "prev": "", "next": "", "score": None,
391
+ }
392
+ _write_clip(
393
+ tmp_path,
394
+ schema_version="0.6",
395
+ annotation_frame_id="LIDAR_TOP",
396
+ sensors_meta={"LIDAR_TOP": {"modality": "lidar", "frame_id": "LIDAR_TOP",
397
+ "point_format": {"fields": ["x", "y", "z", "intensity", "t"],
398
+ "types": ["float32"] * 5,
399
+ "bytes_per_point": 20}}},
400
+ calibrated=[{
401
+ "calibrated_sensor_token": "cs-0", "channel": "LIDAR_TOP",
402
+ "translation": {"x": 0, "y": 0, "z": 1.5},
403
+ "rotation": {"qx": 0, "qy": 0, "qz": 0, "qw": 1},
404
+ "intrinsics": None, "distortion": None,
405
+ }],
406
+ annotations=[ann],
407
+ )
408
+ pcd_path = tmp_path / "samples/x/0.bin"
409
+ pcd_path.parent.mkdir(parents=True, exist_ok=True)
410
+ np.zeros(5, dtype=np.float32).tofile(pcd_path)
411
+
412
+ report = validate_clip(tmp_path)
413
+ schema_errors = [i for i in report.errors if i.check in ("annotation_frame_id", "sensor_frame_id")]
414
+ assert schema_errors == [], f"unexpected schema errors: {schema_errors}"