x4d-devkit 0.16.0__tar.gz → 0.17.0__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.
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/PKG-INFO +7 -6
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/README.md +5 -5
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/pyproject.toml +2 -2
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_boundary.py +2 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_box.py +17 -10
- x4d_devkit-0.17.0/tests/test_contract_validation.py +413 -0
- x4d_devkit-0.17.0/tests/test_coordinate_frames.py +81 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_current_state.py +34 -6
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_current_state_identity_consumers_example.py +1 -1
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_dataset_identity.py +84 -85
- x4d_devkit-0.17.0/tests/test_detection_eval.py +23 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_evaluate.py +4 -4
- x4d_devkit-0.17.0/tests/test_label_schemas.py +453 -0
- x4d_devkit-0.17.0/tests/test_loader.py +115 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_matching.py +1 -1
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_metrics.py +5 -5
- x4d_devkit-0.17.0/tests/test_models.py +167 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_nuscenes_converter.py +160 -44
- x4d_devkit-0.17.0/tests/test_sdk_api.py +47 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_training_identity.py +32 -19
- x4d_devkit-0.17.0/tests/test_training_manifest.py +455 -0
- x4d_devkit-0.17.0/tests/test_transform_chain.py +51 -0
- x4d_devkit-0.17.0/tests/test_validation.py +92 -0
- x4d_devkit-0.17.0/tests/test_work_session.py +409 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/__init__.py +4 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/cli.py +2 -28
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/clip_work_sessions.py +4 -8
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/label_schemas.py +11 -6
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/converters/nuscenes.py +232 -145
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/core/__init__.py +6 -1
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/core/loader.py +141 -154
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/core/models.py +170 -51
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/core/transform.py +35 -90
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/current_state.py +83 -25
- x4d_devkit-0.17.0/x4d_devkit/dataset_contract.py +408 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/dataset_identity.py +57 -14
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/eval/api.py +2 -2
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/eval/box.py +23 -14
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/eval/detection.py +0 -5
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/eval/metrics.py +6 -4
- x4d_devkit-0.17.0/x4d_devkit/label_schema.py +2093 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/manifest.py +251 -223
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/materialize.py +9 -9
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/training_identity.py +79 -18
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/validation/__init__.py +2 -1
- x4d_devkit-0.17.0/x4d_devkit/validation/contract.py +1544 -0
- x4d_devkit-0.17.0/x4d_devkit/validation/validator.py +817 -0
- x4d_devkit-0.17.0/x4d_devkit/work_session.py +568 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit.egg-info/PKG-INFO +7 -6
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit.egg-info/SOURCES.txt +2 -3
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit.egg-info/requires.txt +1 -0
- x4d_devkit-0.16.0/tests/test_contract_validation.py +0 -537
- x4d_devkit-0.16.0/tests/test_coordinate_frames.py +0 -501
- x4d_devkit-0.16.0/tests/test_detection_eval.py +0 -195
- x4d_devkit-0.16.0/tests/test_label_schemas.py +0 -565
- x4d_devkit-0.16.0/tests/test_loader.py +0 -254
- x4d_devkit-0.16.0/tests/test_models.py +0 -232
- x4d_devkit-0.16.0/tests/test_training_manifest.py +0 -824
- x4d_devkit-0.16.0/tests/test_transform_chain.py +0 -88
- x4d_devkit-0.16.0/tests/test_v06_api_additions.py +0 -161
- x4d_devkit-0.16.0/tests/test_v06_schema.py +0 -414
- x4d_devkit-0.16.0/tests/test_v10_schema.py +0 -379
- x4d_devkit-0.16.0/tests/test_validation.py +0 -421
- x4d_devkit-0.16.0/tests/test_work_session.py +0 -601
- x4d_devkit-0.16.0/x4d_devkit/label_schema.py +0 -1451
- x4d_devkit-0.16.0/x4d_devkit/validation/contract.py +0 -954
- x4d_devkit-0.16.0/x4d_devkit/validation/validator.py +0 -587
- x4d_devkit-0.16.0/x4d_devkit/work_session.py +0 -388
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/LICENSE +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/setup.cfg +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_inference_builders.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_token.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/tests/test_transform.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/converters/__init__.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/inference/builders.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.16.0 → x4d_devkit-0.17.0}/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.
|
|
3
|
+
Version: 0.17.0
|
|
4
4
|
Summary: Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation
|
|
5
5
|
Author: windzu
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -21,6 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: numpy>=1.24
|
|
23
23
|
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: rfc8785==0.1.4
|
|
24
25
|
Provides-Extra: converters
|
|
25
26
|
Requires-Dist: nuscenes-devkit>=1.1.0; extra == "converters"
|
|
26
27
|
Provides-Extra: inference
|
|
@@ -294,12 +295,12 @@ label_schema.projects.<project>.contract # binding + full registry record
|
|
|
294
295
|
label_schema.projects.<project>.class_catalog # training/observed class catalog
|
|
295
296
|
```
|
|
296
297
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
`
|
|
298
|
+
Consumers must parse `contract` with
|
|
299
|
+
`parse_project_label_schema_snapshot()`. `class_catalog` is
|
|
300
|
+
non-authoritative observation metadata; there are no per-project
|
|
301
|
+
`classes`, `source`, or `schema_version` aliases.
|
|
301
302
|
|
|
302
|
-
This strict parser/view contract is introduced in x4d-devkit `0.
|
|
303
|
+
This strict parser/view contract is introduced in x4d-devkit `0.17.0`.
|
|
303
304
|
Consumers tracking `main` should still record `get_build_info()["source"]` in
|
|
304
305
|
training identity so an untagged development run remains reproducible by exact
|
|
305
306
|
commit. Deployments that copy a source tree without its Git directory must set
|
|
@@ -260,12 +260,12 @@ label_schema.projects.<project>.contract # binding + full registry record
|
|
|
260
260
|
label_schema.projects.<project>.class_catalog # training/observed class catalog
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
`
|
|
263
|
+
Consumers must parse `contract` with
|
|
264
|
+
`parse_project_label_schema_snapshot()`. `class_catalog` is
|
|
265
|
+
non-authoritative observation metadata; there are no per-project
|
|
266
|
+
`classes`, `source`, or `schema_version` aliases.
|
|
267
267
|
|
|
268
|
-
This strict parser/view contract is introduced in x4d-devkit `0.
|
|
268
|
+
This strict parser/view contract is introduced in x4d-devkit `0.17.0`.
|
|
269
269
|
Consumers tracking `main` should still record `get_build_info()["source"]` in
|
|
270
270
|
training identity so an untagged development run remains reproducible by exact
|
|
271
271
|
commit. Deployments that copy a source tree without its Git directory must set
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "x4d-devkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.17.0"
|
|
8
8
|
description = "Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Programming Language :: Python :: 3.12",
|
|
25
25
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
26
|
]
|
|
27
|
-
dependencies = ["numpy>=1.24", "httpx>=0.27"]
|
|
27
|
+
dependencies = ["numpy>=1.24", "httpx>=0.27", "rfc8785==0.1.4"]
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
converters = ["nuscenes-devkit>=1.1.0"]
|
|
@@ -65,6 +65,7 @@ def test_top_level_exports_only_stable_sdk_surface():
|
|
|
65
65
|
"Annotation",
|
|
66
66
|
"AssetRef",
|
|
67
67
|
"CalibratedSensor",
|
|
68
|
+
"ClipContractValidationError",
|
|
68
69
|
"ClipLoader",
|
|
69
70
|
"ClipMeta",
|
|
70
71
|
"ClipWorkSessionManifest",
|
|
@@ -87,6 +88,7 @@ def test_top_level_exports_only_stable_sdk_surface():
|
|
|
87
88
|
"get_build_info",
|
|
88
89
|
"get_training_coordinate_contract",
|
|
89
90
|
"materialize_work_session_clip",
|
|
91
|
+
"parse_clip_work_session",
|
|
90
92
|
"validate_clip",
|
|
91
93
|
}
|
|
92
94
|
for name in (
|
|
@@ -21,7 +21,7 @@ class TestBoxConstruction:
|
|
|
21
21
|
assert b.category == "car"
|
|
22
22
|
assert b.score is None
|
|
23
23
|
assert b.velocity is None
|
|
24
|
-
assert b.attributes ==
|
|
24
|
+
assert b.attributes == {}
|
|
25
25
|
assert b.id is None
|
|
26
26
|
assert b.geometry_type == "normal"
|
|
27
27
|
|
|
@@ -33,12 +33,12 @@ class TestBoxConstruction:
|
|
|
33
33
|
category="car",
|
|
34
34
|
score=0.85,
|
|
35
35
|
velocity=[1.0, 0.5, 0.0],
|
|
36
|
-
attributes=
|
|
36
|
+
attributes={"vehicle.moving": True},
|
|
37
37
|
)
|
|
38
38
|
assert b.score == 0.85
|
|
39
39
|
assert b.velocity.dtype == np.float64
|
|
40
40
|
assert b.velocity.tolist() == [1.0, 0.5, 0.0]
|
|
41
|
-
assert b.attributes ==
|
|
41
|
+
assert b.attributes == {"vehicle.moving": True}
|
|
42
42
|
|
|
43
43
|
def test_identity_and_geometry_type(self):
|
|
44
44
|
b = Box(
|
|
@@ -135,14 +135,14 @@ class TestFromXyzlwhyawVxvy:
|
|
|
135
135
|
b = Box.from_xyzlwhyaw_vxvy(
|
|
136
136
|
[0, 0, 0, 4, 2, 1.5, 0, 0, 0], category="car",
|
|
137
137
|
)
|
|
138
|
-
assert b.attributes ==
|
|
138
|
+
assert b.attributes == {}
|
|
139
139
|
|
|
140
140
|
def test_attributes_passthrough(self):
|
|
141
141
|
b = Box.from_xyzlwhyaw_vxvy(
|
|
142
142
|
[0, 0, 0, 4, 2, 1.5, 0, 0, 0], category="car",
|
|
143
|
-
attributes=
|
|
143
|
+
attributes={"vehicle.moving": True},
|
|
144
144
|
)
|
|
145
|
-
assert b.attributes ==
|
|
145
|
+
assert b.attributes == {"vehicle.moving": True}
|
|
146
146
|
|
|
147
147
|
def test_identity_and_geometry_type_passthrough(self):
|
|
148
148
|
b = Box.from_xyzlwhyaw_vxvy(
|
|
@@ -176,14 +176,21 @@ class TestFromXyzlwhyaw:
|
|
|
176
176
|
|
|
177
177
|
class TestGeometryTypeFromAttributes:
|
|
178
178
|
def test_normal_when_geometry_attribute_absent(self):
|
|
179
|
-
assert geometry_type_from_attributes(
|
|
179
|
+
assert geometry_type_from_attributes({"vehicle.moving": True}) == "normal"
|
|
180
180
|
|
|
181
181
|
def test_front_only(self):
|
|
182
|
-
assert geometry_type_from_attributes(
|
|
182
|
+
assert geometry_type_from_attributes({"geometry.front_only": True}) == "front_only"
|
|
183
183
|
|
|
184
184
|
def test_rear_only(self):
|
|
185
|
-
assert geometry_type_from_attributes(
|
|
185
|
+
assert geometry_type_from_attributes({"geometry.rear_only": True}) == "rear_only"
|
|
186
186
|
|
|
187
187
|
def test_rejects_mutually_exclusive_geometry_attributes(self):
|
|
188
188
|
with pytest.raises(ValueError, match="mutually exclusive"):
|
|
189
|
-
geometry_type_from_attributes(
|
|
189
|
+
geometry_type_from_attributes({
|
|
190
|
+
"geometry.front_only": True,
|
|
191
|
+
"geometry.rear_only": True,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
def test_rejects_false_presence_flag(self):
|
|
195
|
+
with pytest.raises(ValueError, match="literal true"):
|
|
196
|
+
geometry_type_from_attributes({"geometry.front_only": False})
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from tests.schema_contract_fixtures import schema_identity, schema_record
|
|
9
|
+
from x4d_devkit.label_schema import parse_label_schema_record
|
|
10
|
+
from x4d_devkit.validation.contract import (
|
|
11
|
+
ClipValidationContext,
|
|
12
|
+
validate_annotation_payload,
|
|
13
|
+
validate_clip_contract,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _schema():
|
|
18
|
+
return parse_label_schema_record(schema_record())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _meta() -> dict:
|
|
22
|
+
return {
|
|
23
|
+
"clip_id": "clip-a",
|
|
24
|
+
"schema_version": "0.11",
|
|
25
|
+
"session_id": "session-a",
|
|
26
|
+
"vehicle_id": "vehicle-a",
|
|
27
|
+
"capture_time_utc": "2026-07-10T00:00:00Z",
|
|
28
|
+
"duration_s": 0.0,
|
|
29
|
+
"keyframe_count": 1,
|
|
30
|
+
"sweep_count": 0,
|
|
31
|
+
"annotation_source_channel": "lidar",
|
|
32
|
+
"ego_pose_frame_id": "base_link",
|
|
33
|
+
"sensors": {
|
|
34
|
+
"lidar": {
|
|
35
|
+
"modality": "lidar",
|
|
36
|
+
"frame_id": "base_link",
|
|
37
|
+
"point_format": {
|
|
38
|
+
"fields": ["x", "y", "z", "intensity"],
|
|
39
|
+
"types": ["float32", "float32", "float32", "float32"],
|
|
40
|
+
"bytes_per_point": 16,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"label_schema": schema_identity(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _sample() -> dict:
|
|
49
|
+
return {
|
|
50
|
+
"sample_token": "s0",
|
|
51
|
+
"clip_id": "clip-a",
|
|
52
|
+
"timestamp_us": 1,
|
|
53
|
+
"is_keyframe": True,
|
|
54
|
+
"prev": None,
|
|
55
|
+
"next": None,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _sample_data() -> dict:
|
|
60
|
+
return {
|
|
61
|
+
"sample_data_token": "sd0",
|
|
62
|
+
"sample_token": "s0",
|
|
63
|
+
"ego_pose_token": "ep0",
|
|
64
|
+
"calibrated_sensor_token": "cs0",
|
|
65
|
+
"channel": "lidar",
|
|
66
|
+
"file_path": "samples/lidar/1.bin",
|
|
67
|
+
"timestamp_us": 1,
|
|
68
|
+
"is_keyframe": True,
|
|
69
|
+
"width": None,
|
|
70
|
+
"height": None,
|
|
71
|
+
"prev": None,
|
|
72
|
+
"next": None,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _annotation(
|
|
77
|
+
*,
|
|
78
|
+
token: str = "a0",
|
|
79
|
+
sample_token: str = "s0",
|
|
80
|
+
instance_token: str = "i0",
|
|
81
|
+
category: str = "car",
|
|
82
|
+
attributes: dict | None = None,
|
|
83
|
+
prev: str | None = None,
|
|
84
|
+
next: str | None = None,
|
|
85
|
+
) -> dict:
|
|
86
|
+
return {
|
|
87
|
+
"annotation_token": token,
|
|
88
|
+
"sample_token": sample_token,
|
|
89
|
+
"instance_token": instance_token,
|
|
90
|
+
"category": category,
|
|
91
|
+
"bbox_3d": {
|
|
92
|
+
"translation": {"x": 1.0, "y": 2.0, "z": 0.5},
|
|
93
|
+
"size": {"length": 4.0, "width": 2.0, "height": 1.5},
|
|
94
|
+
"rotation": {"qx": 0.0, "qy": 0.0, "qz": 0.0, "qw": 1.0},
|
|
95
|
+
},
|
|
96
|
+
"num_lidar_pts": 5,
|
|
97
|
+
"velocity": None,
|
|
98
|
+
"attributes": attributes if attributes is not None else {"visibility": "1"},
|
|
99
|
+
"prev": prev,
|
|
100
|
+
"next": next,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _instance(
|
|
105
|
+
*,
|
|
106
|
+
token: str = "i0",
|
|
107
|
+
category: str = "car",
|
|
108
|
+
count: int = 1,
|
|
109
|
+
first: str = "a0",
|
|
110
|
+
last: str = "a0",
|
|
111
|
+
) -> dict:
|
|
112
|
+
return {
|
|
113
|
+
"instance_token": token,
|
|
114
|
+
"category": category,
|
|
115
|
+
"num_annotations": count,
|
|
116
|
+
"first_annotation_token": first,
|
|
117
|
+
"last_annotation_token": last,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _context(
|
|
122
|
+
*,
|
|
123
|
+
annotations: list[dict] | None = None,
|
|
124
|
+
instances: list[dict] | None = None,
|
|
125
|
+
meta: dict | None = None,
|
|
126
|
+
sample_data: list[dict] | None = None,
|
|
127
|
+
) -> ClipValidationContext:
|
|
128
|
+
anns = [_annotation()] if annotations is None else annotations
|
|
129
|
+
insts = [_instance()] if instances is None else instances
|
|
130
|
+
return ClipValidationContext(
|
|
131
|
+
clip_id="clip-a",
|
|
132
|
+
clip_schema_version="0.11",
|
|
133
|
+
label_schema=_schema(),
|
|
134
|
+
samples=[_sample()],
|
|
135
|
+
sample_data=[_sample_data()] if sample_data is None else sample_data,
|
|
136
|
+
annotations=anns,
|
|
137
|
+
instances=insts,
|
|
138
|
+
meta=_meta() if meta is None else meta,
|
|
139
|
+
annotation_frame_id="base_link",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _codes(report) -> set[str]:
|
|
144
|
+
return {issue.code for issue in report.issues}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_valid_current_clip_contract_passes_and_serializes():
|
|
148
|
+
report = validate_clip_contract(_context())
|
|
149
|
+
assert report.ok
|
|
150
|
+
assert report.to_dict()["issue_count"] == 0
|
|
151
|
+
json.dumps(report.to_dict())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@pytest.mark.parametrize(
|
|
155
|
+
("mutation", "code"),
|
|
156
|
+
[
|
|
157
|
+
(lambda ann: ann.update(category="unknown"), "unknown_category"),
|
|
158
|
+
(lambda ann: ann.update(attributes=["vehicle.moving"]), "invalid_attributes_shape"),
|
|
159
|
+
(lambda ann: ann.update(attributes={"visibility": "1", "unknown": True}), "unknown_attribute"),
|
|
160
|
+
(lambda ann: ann.update(attributes={"visibility": "1", "truck.loaded": True}), "invalid_attribute_for_category"),
|
|
161
|
+
(lambda ann: ann.update(attributes={"visibility": "1", "vehicle.moving": False}), "invalid_attribute_value"),
|
|
162
|
+
(lambda ann: ann.update(attributes={}), "missing_required_attribute"),
|
|
163
|
+
(lambda ann: ann["bbox_3d"]["size"].update(length=-1.0), "invalid_bbox_size"),
|
|
164
|
+
(lambda ann: ann["bbox_3d"]["translation"].update(x=float("nan")), "non_finite_bbox_value"),
|
|
165
|
+
(lambda ann: ann["bbox_3d"]["rotation"].update(qw=2.0), "invalid_bbox_rotation"),
|
|
166
|
+
(lambda ann: ann.update(num_lidar_pts=True), "invalid_num_lidar_pts"),
|
|
167
|
+
(lambda ann: ann.update(velocity={"vx": 1.0, "vy": 0.0}), "invalid_velocity"),
|
|
168
|
+
(lambda ann: ann.update(sample_token="missing"), "sample_token_not_in_clip"),
|
|
169
|
+
(lambda ann: ann.update(instance_token=""), "missing_instance_token"),
|
|
170
|
+
(lambda ann: ann.update(prev=""), "invalid_annotation_shape"),
|
|
171
|
+
(lambda ann: ann.update(score=0.9), "invalid_annotation_shape"),
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
def test_annotation_contract_reports_strict_issue_codes(mutation, code):
|
|
175
|
+
annotation = _annotation()
|
|
176
|
+
mutation(annotation)
|
|
177
|
+
report = validate_annotation_payload(
|
|
178
|
+
_context(),
|
|
179
|
+
annotations=[annotation],
|
|
180
|
+
instances=[_instance()],
|
|
181
|
+
)
|
|
182
|
+
assert code in _codes(report)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pytest.mark.parametrize(
|
|
186
|
+
"mutation",
|
|
187
|
+
[
|
|
188
|
+
lambda ann: ann["bbox_3d"]["translation"].update(extra=0.0),
|
|
189
|
+
lambda ann: ann["bbox_3d"]["size"].pop("height"),
|
|
190
|
+
lambda ann: ann["bbox_3d"]["rotation"].update(qx="0"),
|
|
191
|
+
lambda ann: ann.update(velocity={"vx": 0.0, "vy": 0.0, "vz": 0.0, "extra": 0.0}),
|
|
192
|
+
],
|
|
193
|
+
)
|
|
194
|
+
def test_nested_geometry_and_velocity_require_exact_numeric_objects(mutation):
|
|
195
|
+
annotation = _annotation()
|
|
196
|
+
mutation(annotation)
|
|
197
|
+
assert not validate_annotation_payload(
|
|
198
|
+
_context(), annotations=[annotation], instances=[_instance()]
|
|
199
|
+
).ok
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_multiple_enum_requires_sorted_unique_nonempty_values():
|
|
203
|
+
annotation = _annotation(
|
|
204
|
+
attributes={"visibility": "1", "vehicle.state_tags": ["turning", "stopped"]}
|
|
205
|
+
)
|
|
206
|
+
assert "invalid_attribute_value" in _codes(
|
|
207
|
+
validate_annotation_payload(_context(), annotations=[annotation], instances=[_instance()])
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_boolean_mutual_exclusion_uses_literal_true_presence():
|
|
212
|
+
annotation = _annotation(
|
|
213
|
+
category="truck",
|
|
214
|
+
attributes={"visibility": "1", "truck.empty": True, "truck.loaded": True},
|
|
215
|
+
)
|
|
216
|
+
instance = _instance(category="truck")
|
|
217
|
+
assert "mutually_exclusive_attributes" in _codes(
|
|
218
|
+
validate_annotation_payload(_context(), annotations=[annotation], instances=[instance])
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_instance_shape_tokens_counts_and_chain_are_exact():
|
|
223
|
+
first = _annotation(token="a0", next="a1")
|
|
224
|
+
second = _annotation(token="a1", sample_token="s1", prev="a0")
|
|
225
|
+
valid_instance = _instance(count=2, first="a0", last="a1")
|
|
226
|
+
context = _context()
|
|
227
|
+
context.samples[0]["next"] = "s1"
|
|
228
|
+
context.samples.append({
|
|
229
|
+
"sample_token": "s1",
|
|
230
|
+
"clip_id": "clip-a",
|
|
231
|
+
"timestamp_us": 2,
|
|
232
|
+
"is_keyframe": True,
|
|
233
|
+
"prev": "s0",
|
|
234
|
+
"next": None,
|
|
235
|
+
})
|
|
236
|
+
second_sample_data = _sample_data()
|
|
237
|
+
context.sample_data[0]["next"] = "sd1"
|
|
238
|
+
second_sample_data.update(
|
|
239
|
+
sample_data_token="sd1",
|
|
240
|
+
sample_token="s1",
|
|
241
|
+
timestamp_us=2,
|
|
242
|
+
file_path="samples/lidar/2.bin",
|
|
243
|
+
prev="sd0",
|
|
244
|
+
)
|
|
245
|
+
context.sample_data.append(second_sample_data)
|
|
246
|
+
assert validate_annotation_payload(
|
|
247
|
+
context, annotations=[first, second], instances=[valid_instance]
|
|
248
|
+
).ok
|
|
249
|
+
|
|
250
|
+
broken = copy.deepcopy(valid_instance)
|
|
251
|
+
broken["extra"] = True
|
|
252
|
+
broken["last_annotation_token"] = "a0"
|
|
253
|
+
report = validate_annotation_payload(
|
|
254
|
+
context, annotations=[first, second], instances=[broken]
|
|
255
|
+
)
|
|
256
|
+
assert {"invalid_instance_shape", "broken_instance_chain"}.issubset(_codes(report))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_instance_links_must_be_reciprocal_and_stay_in_one_instance():
|
|
260
|
+
first = _annotation(token="a0", next="a1")
|
|
261
|
+
second = _annotation(token="a1", instance_token="i1", prev=None)
|
|
262
|
+
report = validate_annotation_payload(
|
|
263
|
+
_context(),
|
|
264
|
+
annotations=[first, second],
|
|
265
|
+
instances=[_instance(), _instance(token="i1", first="a1", last="a1")],
|
|
266
|
+
)
|
|
267
|
+
assert "broken_instance_chain" in _codes(report)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_draft_frame_scope_skips_final_clip_closure_only():
|
|
271
|
+
report = validate_annotation_payload(
|
|
272
|
+
_context(),
|
|
273
|
+
annotations=[_annotation()],
|
|
274
|
+
instances=[],
|
|
275
|
+
scope="frame",
|
|
276
|
+
phase="draft",
|
|
277
|
+
)
|
|
278
|
+
assert report.ok
|
|
279
|
+
|
|
280
|
+
bad = _annotation(category="unknown")
|
|
281
|
+
report = validate_annotation_payload(
|
|
282
|
+
_context(), annotations=[bad], instances=[], scope="frame", phase="draft"
|
|
283
|
+
)
|
|
284
|
+
assert "unknown_category" in _codes(report)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_final_sensor_coverage_requires_every_declared_channel():
|
|
288
|
+
meta = _meta()
|
|
289
|
+
meta["sensors"]["cam_front"] = {"modality": "camera", "frame_id": "cam_front"}
|
|
290
|
+
report = validate_clip_contract(_context(meta=meta))
|
|
291
|
+
assert "incomplete_sensor_coverage" in _codes(report)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_context_rejects_non_list_payloads_and_invalid_meta():
|
|
295
|
+
with pytest.raises(TypeError):
|
|
296
|
+
validate_annotation_payload(_context(), annotations=(), instances=[])
|
|
297
|
+
with pytest.raises(TypeError):
|
|
298
|
+
ClipValidationContext(
|
|
299
|
+
clip_id="clip-a",
|
|
300
|
+
clip_schema_version="0.11",
|
|
301
|
+
label_schema=_schema(),
|
|
302
|
+
samples=(),
|
|
303
|
+
meta=_meta(),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
meta = _meta()
|
|
307
|
+
meta["topic_config_id"] = 1
|
|
308
|
+
assert "invalid_clip_meta" in _codes(validate_clip_contract(_context(meta=meta)))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_context_identity_and_derived_frame_must_match():
|
|
312
|
+
meta = _meta()
|
|
313
|
+
meta["label_schema"]["fingerprint"] = "sha256:" + "0" * 64
|
|
314
|
+
report = validate_clip_contract(_context(meta=meta))
|
|
315
|
+
assert "label_schema_fingerprint_mismatch" in _codes(report)
|
|
316
|
+
|
|
317
|
+
context = _context()
|
|
318
|
+
object.__setattr__(context, "annotation_frame_id", "other")
|
|
319
|
+
assert "annotation_frame_mismatch" in _codes(validate_clip_contract(context))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@pytest.mark.parametrize("status", ["draft", "deprecated"])
|
|
323
|
+
def test_shared_contract_requires_active_label_schema_in_every_phase(status):
|
|
324
|
+
context = _context()
|
|
325
|
+
object.__setattr__(context, "label_schema", parse_label_schema_record(schema_record(status=status)))
|
|
326
|
+
for phase in ("draft", "final"):
|
|
327
|
+
assert "inactive_label_schema" in _codes(
|
|
328
|
+
validate_clip_contract(context, phase=phase)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_instance_declared_chain_must_cover_disconnected_cycle():
|
|
333
|
+
context = _context()
|
|
334
|
+
samples = []
|
|
335
|
+
sample_data = []
|
|
336
|
+
annotations = []
|
|
337
|
+
for index in range(4):
|
|
338
|
+
samples.append({
|
|
339
|
+
"sample_token": f"s{index}",
|
|
340
|
+
"clip_id": "clip-a",
|
|
341
|
+
"timestamp_us": index + 1,
|
|
342
|
+
"is_keyframe": True,
|
|
343
|
+
"prev": f"s{index - 1}" if index else None,
|
|
344
|
+
"next": f"s{index + 1}" if index < 3 else None,
|
|
345
|
+
})
|
|
346
|
+
row = _sample_data()
|
|
347
|
+
row.update(
|
|
348
|
+
sample_data_token=f"sd{index}",
|
|
349
|
+
sample_token=f"s{index}",
|
|
350
|
+
timestamp_us=index + 1,
|
|
351
|
+
file_path=f"samples/lidar/{index + 1}.bin",
|
|
352
|
+
prev=f"sd{index - 1}" if index else None,
|
|
353
|
+
next=f"sd{index + 1}" if index < 3 else None,
|
|
354
|
+
)
|
|
355
|
+
sample_data.append(row)
|
|
356
|
+
annotations.append(
|
|
357
|
+
_annotation(
|
|
358
|
+
token=f"a{index}",
|
|
359
|
+
sample_token=f"s{index}",
|
|
360
|
+
prev=(None if index == 0 else "a0" if index == 1 else f"a{5 - index}"),
|
|
361
|
+
next=("a1" if index == 0 else None if index == 1 else f"a{5 - index}"),
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
object.__setattr__(context, "samples", samples)
|
|
365
|
+
object.__setattr__(context, "sample_data", sample_data)
|
|
366
|
+
report = validate_annotation_payload(
|
|
367
|
+
context,
|
|
368
|
+
annotations=annotations,
|
|
369
|
+
instances=[_instance(count=4, first="a0", last="a1")],
|
|
370
|
+
)
|
|
371
|
+
assert "broken_instance_chain" in _codes(report)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_shared_contract_rejects_nonreciprocal_sample_data_chain():
|
|
375
|
+
context = _context()
|
|
376
|
+
context.samples[0]["next"] = "s1"
|
|
377
|
+
context.samples.append({
|
|
378
|
+
"sample_token": "s1",
|
|
379
|
+
"clip_id": "clip-a",
|
|
380
|
+
"timestamp_us": 2,
|
|
381
|
+
"is_keyframe": True,
|
|
382
|
+
"prev": "s0",
|
|
383
|
+
"next": None,
|
|
384
|
+
})
|
|
385
|
+
context.sample_data[0]["next"] = "sd1"
|
|
386
|
+
second = _sample_data()
|
|
387
|
+
second.update(
|
|
388
|
+
sample_data_token="sd1",
|
|
389
|
+
sample_token="s1",
|
|
390
|
+
timestamp_us=2,
|
|
391
|
+
file_path="samples/lidar/2.bin",
|
|
392
|
+
prev=None,
|
|
393
|
+
)
|
|
394
|
+
context.sample_data.append(second)
|
|
395
|
+
assert "sample_data_chain" in _codes(validate_clip_contract(context))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_sample_chain_rejects_disconnected_cycle():
|
|
399
|
+
context = _context()
|
|
400
|
+
object.__setattr__(context, "samples", [
|
|
401
|
+
{"sample_token": "s0", "clip_id": "clip-a", "timestamp_us": 1, "is_keyframe": True, "prev": None, "next": "s1"},
|
|
402
|
+
{"sample_token": "s1", "clip_id": "clip-a", "timestamp_us": 2, "is_keyframe": True, "prev": "s0", "next": None},
|
|
403
|
+
{"sample_token": "s2", "clip_id": "clip-a", "timestamp_us": 3, "is_keyframe": True, "prev": "s3", "next": "s3"},
|
|
404
|
+
{"sample_token": "s3", "clip_id": "clip-a", "timestamp_us": 4, "is_keyframe": True, "prev": "s2", "next": "s2"},
|
|
405
|
+
])
|
|
406
|
+
assert "broken_sample_chain" in _codes(validate_clip_contract(context))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_invalid_scope_and_phase_are_rejected():
|
|
410
|
+
with pytest.raises(ValueError):
|
|
411
|
+
validate_annotation_payload(_context(), annotations=[], instances=[], scope="dataset")
|
|
412
|
+
with pytest.raises(ValueError):
|
|
413
|
+
validate_annotation_payload(_context(), annotations=[], instances=[], phase="publish")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from tests.current_clip_fixtures import write_current_clip
|
|
7
|
+
from x4d_devkit.core.loader import CLIP_WORLD_FRAME_ID, ClipLoader
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture()
|
|
11
|
+
def loader(tmp_path):
|
|
12
|
+
clip = write_current_clip(
|
|
13
|
+
tmp_path / "coordinate-clip",
|
|
14
|
+
sample_count=2,
|
|
15
|
+
include_camera=True,
|
|
16
|
+
include_fused=True,
|
|
17
|
+
)
|
|
18
|
+
return ClipLoader(clip)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_current_frame_ids_are_explicit_and_annotation_frame_is_derived(loader):
|
|
22
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
23
|
+
assert loader.ego_pose_frame_id == "base_link"
|
|
24
|
+
assert loader.annotation_source_channel == "fused"
|
|
25
|
+
assert loader.annotation_frame_id == "base_link"
|
|
26
|
+
assert loader.sensor_frame_id(sd) == "lidar_top"
|
|
27
|
+
assert CLIP_WORLD_FRAME_ID == "clip_world"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_point_cloud_defaults_to_sensor_frame_and_preserves_payload(loader):
|
|
31
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
32
|
+
raw = loader.load_point_cloud(sd)
|
|
33
|
+
explicit = loader.load_point_cloud(sd, frame="lidar_top")
|
|
34
|
+
np.testing.assert_array_equal(raw, explicit)
|
|
35
|
+
np.testing.assert_allclose(raw[0], [1.0, 0.0, 0.0, 0.5])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_point_cloud_transforms_to_ego_and_clip_world(loader):
|
|
39
|
+
first, second = loader.sample_data_for_channel("lidar_top")
|
|
40
|
+
first_ego = loader.load_point_cloud(first, frame="base_link")
|
|
41
|
+
first_world = loader.load_point_cloud(first, frame=CLIP_WORLD_FRAME_ID)
|
|
42
|
+
second_world = loader.load_point_cloud(second, frame=CLIP_WORLD_FRAME_ID)
|
|
43
|
+
np.testing.assert_allclose(first_ego[0, :3], [1.0, 0.0, 1.0])
|
|
44
|
+
np.testing.assert_allclose(first_world[0, :3], [1.0, 0.0, 1.0])
|
|
45
|
+
np.testing.assert_allclose(second_world[0, :3], [11.0, 0.0, 1.0])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_annotations_transform_from_native_frame_to_world(loader):
|
|
49
|
+
first = loader.annotations_for_sample("sample-0")
|
|
50
|
+
second = loader.annotations_for_sample("sample-1")
|
|
51
|
+
first_world = loader.annotations_for_sample("sample-0", frame=CLIP_WORLD_FRAME_ID)
|
|
52
|
+
second_world = loader.annotations_for_sample("sample-1", frame=CLIP_WORLD_FRAME_ID)
|
|
53
|
+
assert first_world[0] is not first[0]
|
|
54
|
+
np.testing.assert_allclose(first_world[0].translation, [5.0, 3.0, 0.0])
|
|
55
|
+
np.testing.assert_allclose(second_world[0].translation, [10.0, 3.0, 0.0])
|
|
56
|
+
np.testing.assert_allclose(second[0].velocity, second_world[0].velocity)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_static_and_dynamic_transform_round_trip(loader):
|
|
60
|
+
sd = loader.sample_data_for_channel("lidar_top")[1]
|
|
61
|
+
sensor_to_world = loader.get_transform("lidar_top", CLIP_WORLD_FRAME_ID, sd=sd)
|
|
62
|
+
world_to_sensor = loader.get_transform(CLIP_WORLD_FRAME_ID, "lidar_top", sd=sd)
|
|
63
|
+
points = np.array([[1.0, 2.0, 3.0]])
|
|
64
|
+
np.testing.assert_allclose(world_to_sensor.apply(sensor_to_world.apply(points)), points)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize("alias", ["sensor", "ego", "world"])
|
|
68
|
+
def test_legacy_magic_frame_aliases_are_rejected(loader, alias):
|
|
69
|
+
sd = loader.sample_data_for_channel("lidar_top")[0]
|
|
70
|
+
with pytest.raises(ValueError, match="legacy magic aliases"):
|
|
71
|
+
loader.load_point_cloud(sd, frame=alias)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_unknown_real_frame_is_rejected(loader):
|
|
75
|
+
with pytest.raises(KeyError):
|
|
76
|
+
loader.get_transform("lidar_top", "unknown_frame")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_clip_world_requires_sample_data_for_dynamic_transform(loader):
|
|
80
|
+
with pytest.raises(ValueError, match="pass sd"):
|
|
81
|
+
loader.get_transform("lidar_top", CLIP_WORLD_FRAME_ID)
|