x4d-devkit 0.40.0__tar.gz → 0.41.0rc1__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 (121) hide show
  1. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/PKG-INFO +1 -1
  2. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/pyproject.toml +1 -1
  3. x4d_devkit-0.41.0rc1/tests/test_annotation_reference.py +165 -0
  4. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_sdk_api.py +1 -1
  5. x4d_devkit-0.41.0rc1/x4d_devkit/annotation_reference.py +304 -0
  6. x4d_devkit-0.41.0rc1/x4d_devkit/client/annotation_reference.py +131 -0
  7. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/reconstruction.py +113 -16
  8. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/PKG-INFO +1 -1
  9. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/SOURCES.txt +3 -0
  10. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/LICENSE +0 -0
  11. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/README.md +0 -0
  12. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/setup.cfg +0 -0
  13. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_annotation_contract.py +0 -0
  14. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_assets_client.py +0 -0
  15. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_boundary.py +0 -0
  16. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_box.py +0 -0
  17. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_build_info.py +0 -0
  18. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_cli_auth.py +0 -0
  19. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_client_json_contract.py +0 -0
  20. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_client_projects.py +0 -0
  21. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_clip_archives.py +0 -0
  22. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_contract_validation.py +0 -0
  23. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_coordinate_frames.py +0 -0
  24. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_current_state.py +0 -0
  25. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_current_state_identity_consumers_example.py +0 -0
  26. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_dataset_contract.py +0 -0
  27. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_dataset_identity.py +0 -0
  28. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_detection_eval.py +0 -0
  29. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_evaluate.py +0 -0
  30. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_export_bundle.py +0 -0
  31. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_image_review.py +0 -0
  32. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_inference_input.py +0 -0
  33. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_inference_schemas.py +0 -0
  34. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_inference_service.py +0 -0
  35. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_label_schemas.py +0 -0
  36. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_lidar_materialization.py +0 -0
  37. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_loader.py +0 -0
  38. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_matching.py +0 -0
  39. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_metrics.py +0 -0
  40. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_models.py +0 -0
  41. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_platform_pose.py +0 -0
  42. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_preannotation_client.py +0 -0
  43. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_reconstruction_client.py +0 -0
  44. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_release_gate.py +0 -0
  45. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_semantic_scene_authoring.py +0 -0
  46. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_semantic_scene_client.py +0 -0
  47. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_semantic_scene_contract.py +0 -0
  48. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_semantic_scene_native_clip.py +0 -0
  49. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_time_contract.py +0 -0
  50. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_token.py +0 -0
  51. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_tracking_eval.py +0 -0
  52. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_training_identity.py +0 -0
  53. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_transform.py +0 -0
  54. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_transform_chain.py +0 -0
  55. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_validation.py +0 -0
  56. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/tests/test_work_session.py +0 -0
  57. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/__init__.py +0 -0
  58. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/annotation_contract.py +0 -0
  59. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/build_info.py +0 -0
  60. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/cli.py +0 -0
  61. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/__init__.py +0 -0
  62. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/_origin.py +0 -0
  63. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/assets.py +0 -0
  64. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/bags.py +0 -0
  65. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/capabilities.py +0 -0
  66. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/checkpoints.py +0 -0
  67. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/client.py +0 -0
  68. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/clip_work_sessions.py +0 -0
  69. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/clips.py +0 -0
  70. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/dataset_identity.py +0 -0
  71. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/image_review.py +0 -0
  72. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/label_schemas.py +0 -0
  73. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/preannotation.py +0 -0
  74. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/projects.py +0 -0
  75. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/semantic_scene.py +0 -0
  76. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  77. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/services.py +0 -0
  78. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/client/test_sets.py +0 -0
  79. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/consumer_identity.py +0 -0
  80. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/coordinate_contract.py +0 -0
  81. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/__init__.py +0 -0
  82. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/loader.py +0 -0
  83. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/models.py +0 -0
  84. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/token.py +0 -0
  85. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/core/transform.py +0 -0
  86. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/current_state.py +0 -0
  87. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/dataset_contract.py +0 -0
  88. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/dataset_identity.py +0 -0
  89. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/__init__.py +0 -0
  90. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/api.py +0 -0
  91. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/box.py +0 -0
  92. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/detection.py +0 -0
  93. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/matching.py +0 -0
  94. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/metrics.py +0 -0
  95. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/eval/tracking.py +0 -0
  96. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/export_bundle.py +0 -0
  97. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/image_review.py +0 -0
  98. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/__init__.py +0 -0
  99. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/input.py +0 -0
  100. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/models.py +0 -0
  101. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/schemas.py +0 -0
  102. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/service.py +0 -0
  103. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/inference/tasks.py +0 -0
  104. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/json_contract.py +0 -0
  105. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/label_schema.py +0 -0
  106. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/lidar_materialization.py +0 -0
  107. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/materialize.py +0 -0
  108. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/semantic_scene_authoring.py +0 -0
  109. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/semantic_scene_contract.py +0 -0
  110. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/time_contract.py +0 -0
  111. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/training_identity.py +0 -0
  112. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/__init__.py +0 -0
  113. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/contract.py +0 -0
  114. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/platform_pose.py +0 -0
  115. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/report.py +0 -0
  116. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/validation/validator.py +0 -0
  117. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit/work_session.py +0 -0
  118. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  119. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/entry_points.txt +0 -0
  120. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/x4d_devkit.egg-info/requires.txt +0 -0
  121. {x4d_devkit-0.40.0 → x4d_devkit-0.41.0rc1}/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.40.0
3
+ Version: 0.41.0rc1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x4d-devkit"
7
- version = "0.40.0"
7
+ version = "0.41.0rc1"
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"
@@ -0,0 +1,165 @@
1
+ import copy
2
+ import gzip
3
+ import hashlib
4
+
5
+ import httpx
6
+ import numpy as np
7
+ import pytest
8
+
9
+ from x4d_devkit import X4DClient
10
+ from x4d_devkit.annotation_reference import (
11
+ ANNOTATION_REFERENCE_OPERATION,
12
+ ANNOTATION_REFERENCE_SCHEMA,
13
+ GEOMETRY_IDENTITY_FIELDS,
14
+ decode_annotation_reference_chunk,
15
+ parse_annotation_reference_manifest,
16
+ )
17
+
18
+
19
+ def fixture():
20
+ xyz = np.array([[1.5, -2, 3.25], [4, 0, 5]], dtype="<f4")
21
+ payload = gzip.compress(xyz.tobytes(), mtime=0)
22
+ chunk = dict(
23
+ id="0",
24
+ uri="/api/v1/files/clips/c/generated/reconstruction/annotation_reference/a/0.bin.gz",
25
+ sha256="sha256:" + hashlib.sha256(payload).hexdigest(),
26
+ size_bytes=len(payload),
27
+ point_count=2,
28
+ bounds=[xyz.min(0).tolist(), xyz.max(0).tolist()],
29
+ )
30
+ overview = dict(
31
+ chunk, id="overview", uri=chunk["uri"].replace("0.bin", "overview.bin")
32
+ )
33
+ manifest = dict(
34
+ schema_version=ANNOTATION_REFERENCE_SCHEMA,
35
+ coordinate_frame="clip_world",
36
+ point_format="xyz-f32-le-gzip",
37
+ clip_id="c",
38
+ dependency_identity="sha256:" + "a" * 64,
39
+ input_identity={
40
+ name: "sha256:" + "b" * 64 for name in GEOMETRY_IDENTITY_FIELDS
41
+ },
42
+ profile=dict(
43
+ identity="annotation_reference@1",
44
+ algorithm="annotation_reference@1.0.0",
45
+ lidar_channels=["arbitrary-scanner"],
46
+ voxel_size_m=0.03,
47
+ overview_voxel_size_m=0.3,
48
+ min_range_m=2.0,
49
+ max_range_m=60.0,
50
+ point_deskew=False,
51
+ foreground_policy="retain_all",
52
+ ),
53
+ point_count=2,
54
+ bounds=chunk["bounds"],
55
+ chunks=[chunk],
56
+ overview=overview,
57
+ )
58
+ return manifest, payload, xyz.tobytes()
59
+
60
+
61
+ def parse(payload, **kwargs):
62
+ return parse_annotation_reference_manifest(
63
+ payload, api_url="http://platform", expected_clip_id="c", **kwargs
64
+ )
65
+
66
+
67
+ def test_exact_geometry_roundtrip_and_current_input_binding():
68
+ payload, compressed, points = fixture()
69
+ result = parse(payload, expected_input_identity=payload["input_identity"])
70
+ assert result.to_dict() == payload
71
+ assert decode_annotation_reference_chunk(result.chunks[0], compressed) == points
72
+ stale = dict(payload["input_identity"], pose_revision="sha256:" + "c" * 64)
73
+ with pytest.raises(ValueError, match="current geometry"):
74
+ parse(payload, expected_input_identity=stale)
75
+ with pytest.raises(ValueError, match="integrity"):
76
+ decode_annotation_reference_chunk(result.chunks[0], compressed[:-1] + b"x")
77
+
78
+
79
+ @pytest.mark.parametrize(
80
+ "edit",
81
+ [
82
+ lambda m: m.update(point_count=3),
83
+ lambda m: m["chunks"].append(copy.deepcopy(m["chunks"][0])),
84
+ lambda m: m["chunks"][0].update(uri="http://untrusted/api/v1/files/x"),
85
+ lambda m: m["chunks"][0].update(
86
+ uri=m["chunks"][0]["uri"].replace("clips/c/", "clips/other/")
87
+ ),
88
+ lambda m: m["chunks"][0].update(point_count=1_000_001),
89
+ lambda m: m["chunks"][0].update(bounds=[[0, -2, 3.25], [4, 0, 5]]),
90
+ lambda m: m["input_identity"].update(annotation_revision="sha256:" + "a" * 64),
91
+ lambda m: m["profile"].update(foreground_policy="remove_all_boxes"),
92
+ lambda m: m["profile"].update(point_deskew=True),
93
+ lambda m: m["profile"].update(lidar_channels=["x", "x"]),
94
+ lambda m: m.update(coordinate_frame="sensor"),
95
+ ],
96
+ )
97
+ def test_incompatible_partial_or_cross_clip_reference_is_rejected(edit):
98
+ payload, _, _ = fixture()
99
+ edit(payload)
100
+ with pytest.raises(ValueError):
101
+ parse(payload)
102
+
103
+
104
+ def test_valid_checksum_cannot_hide_extra_points_or_gzip_members():
105
+ payload, compressed, points = fixture()
106
+ for body in (gzip.compress(points + points), compressed + compressed):
107
+ payload["chunks"][0].update(
108
+ size_bytes=len(body), sha256="sha256:" + hashlib.sha256(body).hexdigest()
109
+ )
110
+ parsed = parse(payload)
111
+ with pytest.raises(ValueError, match="compression"):
112
+ decode_annotation_reference_chunk(parsed.chunks[0], body)
113
+
114
+
115
+ def test_client_routes_and_validates_task_and_clip_binding():
116
+ manifest, _, _ = fixture()
117
+ state = dict(
118
+ operation=ANNOTATION_REFERENCE_OPERATION,
119
+ project_id=7,
120
+ clip_id="c",
121
+ dependency_identity=manifest["dependency_identity"],
122
+ status="completed",
123
+ task_id="task",
124
+ progress=100.0,
125
+ stage="ready",
126
+ error=None,
127
+ current=True,
128
+ manifest=manifest,
129
+ )
130
+ calls = []
131
+
132
+ def handler(request):
133
+ calls.append(request)
134
+ return httpx.Response(200, json=state)
135
+
136
+ client = X4DClient(api_url="http://platform", token="test")
137
+ client._http.close()
138
+ client._http = httpx.Client(
139
+ base_url="http://platform", transport=httpx.MockTransport(handler)
140
+ )
141
+ try:
142
+ assert client.reconstruction.prepare_annotation_reference(
143
+ project_id=7, clip_id="c"
144
+ ).current
145
+ assert (
146
+ client.reconstruction.get_current_annotation_reference(
147
+ project_id=7, clip_id="c"
148
+ ).manifest.point_count
149
+ == 2
150
+ )
151
+ assert (
152
+ client.reconstruction.get_annotation_reference_task(
153
+ project_id=7, task_id="task"
154
+ ).task_id
155
+ == "task"
156
+ )
157
+ assert calls[0].method == "POST"
158
+ assert calls[1].url.path.endswith("/annotation-reference/current")
159
+ state["project_id"] = 8
160
+ with pytest.raises(ValueError, match="project"):
161
+ client.reconstruction.get_current_annotation_reference(
162
+ project_id=7, clip_id="c"
163
+ )
164
+ finally:
165
+ client.close()
@@ -11,7 +11,7 @@ from x4d_devkit.eval.matching import MetricData
11
11
 
12
12
 
13
13
  def test_package_version_is_current_release():
14
- assert x4d_devkit.__version__ == "0.40.0"
14
+ assert x4d_devkit.__version__ == "0.41.0rc1"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -0,0 +1,304 @@
1
+ """Pure point-cloud evidence bound to one Clip's exact geometry inputs.
2
+
3
+ This optional generated evidence is not a Dataset annotation component.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import copy
9
+ from dataclasses import dataclass
10
+ import hashlib
11
+ import math
12
+ import re
13
+ import zlib
14
+ from typing import Any
15
+ from urllib.parse import urlsplit
16
+
17
+ from x4d_devkit.client.assets import _validated_file_uri
18
+ from x4d_devkit.json_contract import ensure_i_json
19
+
20
+
21
+ ANNOTATION_REFERENCE_SCHEMA = "x4d-annotation-reference-v1"
22
+ ANNOTATION_REFERENCE_OPERATION = "reconstruction.prepare_annotation_reference"
23
+ GEOMETRY_IDENTITY_FIELDS = (
24
+ "samples_revision",
25
+ "sweeps_revision",
26
+ "calibration_revision",
27
+ "pose_revision",
28
+ )
29
+ _HASH = re.compile(r"^sha256:[0-9a-f]{64}$")
30
+
31
+
32
+ def _object(value, keys, path):
33
+ if not isinstance(value, dict) or set(value) != set(keys):
34
+ raise ValueError(f"{path} must contain exactly {sorted(keys)}")
35
+ return value
36
+
37
+
38
+ def _text(value, path):
39
+ if not isinstance(value, str) or not value or value.strip() != value:
40
+ raise ValueError(f"{path} must be canonical text")
41
+ return value
42
+
43
+
44
+ def _positive(value, path):
45
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
46
+ raise ValueError(f"{path} must be a positive integer")
47
+ return value
48
+
49
+
50
+ def _hash(value, path):
51
+ if not isinstance(value, str) or not _HASH.fullmatch(value):
52
+ raise ValueError(f"{path} must be a SHA-256 identity")
53
+ return value
54
+
55
+
56
+ def _bounds(value, path):
57
+ if not isinstance(value, list) or len(value) != 2:
58
+ raise ValueError(f"{path} must have lower and upper XYZ bounds")
59
+ for row in value:
60
+ if (
61
+ not isinstance(row, list)
62
+ or len(row) != 3
63
+ or any(
64
+ isinstance(v, bool)
65
+ or not isinstance(v, (int, float))
66
+ or not math.isfinite(v)
67
+ for v in row
68
+ )
69
+ ):
70
+ raise ValueError(f"{path} must contain finite XYZ coordinates")
71
+ if any(low > high for low, high in zip(*value)):
72
+ raise ValueError(f"{path} is inverted")
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class AnnotationReferenceChunk:
77
+ id: str
78
+ uri: str
79
+ sha256: str
80
+ size_bytes: int
81
+ point_count: int
82
+ bounds: tuple[tuple[float, ...], ...]
83
+
84
+ def to_dict(self):
85
+ return dict(
86
+ id=self.id,
87
+ uri=self.uri,
88
+ sha256=self.sha256,
89
+ size_bytes=self.size_bytes,
90
+ point_count=self.point_count,
91
+ bounds=[list(row) for row in self.bounds],
92
+ )
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class AnnotationReferenceManifest:
97
+ clip_id: str
98
+ dependency_identity: str
99
+ input_identity: dict[str, str]
100
+ profile: dict[str, Any]
101
+ point_count: int
102
+ bounds: tuple[tuple[float, ...], ...]
103
+ chunks: tuple[AnnotationReferenceChunk, ...]
104
+ overview: AnnotationReferenceChunk
105
+
106
+ def to_dict(self):
107
+ return dict(
108
+ schema_version=ANNOTATION_REFERENCE_SCHEMA,
109
+ coordinate_frame="clip_world",
110
+ point_format="xyz-f32-le-gzip",
111
+ clip_id=self.clip_id,
112
+ dependency_identity=self.dependency_identity,
113
+ input_identity=copy.deepcopy(self.input_identity),
114
+ profile=copy.deepcopy(self.profile),
115
+ point_count=self.point_count,
116
+ bounds=[list(row) for row in self.bounds],
117
+ chunks=[chunk.to_dict() for chunk in self.chunks],
118
+ overview=self.overview.to_dict(),
119
+ )
120
+
121
+
122
+ def parse_annotation_reference_manifest(
123
+ payload: Any,
124
+ *,
125
+ api_url: str,
126
+ expected_clip_id: str,
127
+ expected_dependency_identity: str | None = None,
128
+ expected_input_identity: dict[str, str] | None = None,
129
+ ) -> AnnotationReferenceManifest:
130
+ value = _object(
131
+ ensure_i_json(payload),
132
+ {
133
+ "schema_version",
134
+ "coordinate_frame",
135
+ "point_format",
136
+ "clip_id",
137
+ "dependency_identity",
138
+ "input_identity",
139
+ "profile",
140
+ "point_count",
141
+ "bounds",
142
+ "chunks",
143
+ "overview",
144
+ },
145
+ "annotation reference",
146
+ )
147
+ if (
148
+ value["schema_version"] != ANNOTATION_REFERENCE_SCHEMA
149
+ or value["coordinate_frame"] != "clip_world"
150
+ or value["point_format"] != "xyz-f32-le-gzip"
151
+ ):
152
+ raise ValueError("Unsupported annotation reference contract")
153
+ if value["clip_id"] != expected_clip_id:
154
+ raise ValueError("Annotation reference belongs to another Clip")
155
+ dependency = _hash(value["dependency_identity"], "dependency_identity")
156
+ if (
157
+ expected_dependency_identity is not None
158
+ and dependency != expected_dependency_identity
159
+ ):
160
+ raise ValueError("Annotation reference dependency changed")
161
+ identity = _object(
162
+ value["input_identity"], GEOMETRY_IDENTITY_FIELDS, "input_identity"
163
+ )
164
+ for name, item in identity.items():
165
+ _hash(item, name)
166
+ if expected_input_identity is not None and identity != expected_input_identity:
167
+ raise ValueError("Annotation reference does not match the current geometry")
168
+ profile = _object(
169
+ value["profile"],
170
+ {
171
+ "identity",
172
+ "algorithm",
173
+ "lidar_channels",
174
+ "voxel_size_m",
175
+ "overview_voxel_size_m",
176
+ "min_range_m",
177
+ "max_range_m",
178
+ "point_deskew",
179
+ "foreground_policy",
180
+ },
181
+ "profile",
182
+ )
183
+ _text(profile["identity"], "profile.identity")
184
+ _text(profile["algorithm"], "profile.algorithm")
185
+ channels = profile["lidar_channels"]
186
+ if not isinstance(channels, list) or not channels:
187
+ raise ValueError("Reference profile requires LiDAR channels")
188
+ for channel in channels:
189
+ _text(channel, "lidar channel")
190
+ if len(set(channels)) != len(channels):
191
+ raise ValueError("Reference profile has duplicate LiDAR channels")
192
+ for key in ("voxel_size_m", "overview_voxel_size_m", "min_range_m", "max_range_m"):
193
+ item = profile[key]
194
+ if (
195
+ isinstance(item, bool)
196
+ or not isinstance(item, (int, float))
197
+ or not math.isfinite(item)
198
+ or item < 0
199
+ ):
200
+ raise ValueError(f"Invalid reference {key}")
201
+ if (
202
+ profile["voxel_size_m"] <= 0
203
+ or profile["overview_voxel_size_m"] < profile["voxel_size_m"]
204
+ or profile["max_range_m"] <= profile["min_range_m"]
205
+ ):
206
+ raise ValueError("Invalid reference voxel or range ordering")
207
+ if (
208
+ profile["point_deskew"] is not False
209
+ or profile["foreground_policy"] != "retain_all"
210
+ ):
211
+ raise ValueError(
212
+ "Reference v1 requires declared rigid scans and retained foreground"
213
+ )
214
+ count = _positive(value["point_count"], "point_count")
215
+ _bounds(value["bounds"], "bounds")
216
+
217
+ def chunk(item):
218
+ item = _object(
219
+ item,
220
+ {"id", "uri", "sha256", "size_bytes", "point_count", "bounds"},
221
+ "chunk",
222
+ )
223
+ _text(item["id"], "chunk.id")
224
+ uri = _validated_file_uri(item["uri"], api_url=api_url)
225
+ prefix = f"/api/v1/files/clips/{expected_clip_id}/generated/reconstruction/annotation_reference/"
226
+ if not urlsplit(uri).path.startswith(prefix):
227
+ raise ValueError("Reference chunk is outside its Clip evidence namespace")
228
+ _hash(item["sha256"], "chunk.sha256")
229
+ _positive(item["size_bytes"], "chunk.size_bytes")
230
+ n = _positive(item["point_count"], "chunk.point_count")
231
+ if n > 1_000_000 or item["size_bytes"] > n * 12 + 65536:
232
+ raise ValueError("Reference chunk exceeds bounded transfer size")
233
+ _bounds(item["bounds"], "chunk.bounds")
234
+ if any(
235
+ item["bounds"][0][a] < value["bounds"][0][a]
236
+ or item["bounds"][1][a] > value["bounds"][1][a]
237
+ for a in range(3)
238
+ ):
239
+ raise ValueError("Chunk lies outside reference bounds")
240
+ return AnnotationReferenceChunk(
241
+ item["id"],
242
+ uri,
243
+ item["sha256"],
244
+ item["size_bytes"],
245
+ n,
246
+ tuple(tuple(map(float, row)) for row in item["bounds"]),
247
+ )
248
+
249
+ if not isinstance(value["chunks"], list) or not value["chunks"]:
250
+ raise ValueError("Reference must declare its complete fine chunks")
251
+ chunks = tuple(chunk(item) for item in value["chunks"])
252
+ overview = chunk(value["overview"])
253
+ all_chunks = (*chunks, overview)
254
+ if (
255
+ len({c.id for c in all_chunks}) != len(all_chunks)
256
+ or len({c.uri for c in all_chunks}) != len(all_chunks)
257
+ or sum(c.point_count for c in chunks) != count
258
+ or overview.point_count > count
259
+ ):
260
+ raise ValueError(
261
+ "Reference chunk identities or complete point count are inconsistent"
262
+ )
263
+ return AnnotationReferenceManifest(
264
+ expected_clip_id,
265
+ dependency,
266
+ copy.deepcopy(identity),
267
+ copy.deepcopy(profile),
268
+ count,
269
+ tuple(tuple(map(float, row)) for row in value["bounds"]),
270
+ chunks,
271
+ overview,
272
+ )
273
+
274
+
275
+ def decode_annotation_reference_chunk(
276
+ chunk: AnnotationReferenceChunk, payload: bytes
277
+ ) -> bytes:
278
+ """Verify compressed bytes and bound decompression before exposing XYZ."""
279
+ if (
280
+ len(payload) != chunk.size_bytes
281
+ or "sha256:" + hashlib.sha256(payload).hexdigest() != chunk.sha256
282
+ ):
283
+ raise ValueError("Annotation reference chunk integrity mismatch")
284
+ decoder = zlib.decompressobj(16 + zlib.MAX_WBITS)
285
+ points = decoder.decompress(payload, chunk.point_count * 12 + 1)
286
+ if (
287
+ len(points) != chunk.point_count * 12
288
+ or not decoder.eof
289
+ or decoder.unused_data
290
+ or decoder.unconsumed_tail
291
+ ):
292
+ raise ValueError(
293
+ "Annotation reference chunk point count or compression mismatch"
294
+ )
295
+ import numpy as np
296
+
297
+ xyz = np.frombuffer(points, dtype="<f4").reshape(-1, 3)
298
+ if (
299
+ not np.isfinite(xyz).all()
300
+ or not np.array_equal(xyz.min(axis=0), chunk.bounds[0])
301
+ or not np.array_equal(xyz.max(axis=0), chunk.bounds[1])
302
+ ):
303
+ raise ValueError("Annotation reference chunk geometry disagrees with manifest")
304
+ return points
@@ -0,0 +1,131 @@
1
+ """Typed reconstruction reference states without annotation write authority."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from x4d_devkit.annotation_reference import (
10
+ ANNOTATION_REFERENCE_OPERATION,
11
+ AnnotationReferenceManifest,
12
+ _hash,
13
+ _object,
14
+ _positive,
15
+ _text,
16
+ parse_annotation_reference_manifest,
17
+ )
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class AnnotationReferenceState:
22
+ project_id: int
23
+ clip_id: str
24
+ dependency_identity: str
25
+ status: str
26
+ task_id: str | None
27
+ progress: float
28
+ stage: str | None
29
+ error: dict[str, Any] | None
30
+ current: bool
31
+ manifest: AnnotationReferenceManifest | None
32
+
33
+ def to_dict(self):
34
+ return dict(
35
+ operation=ANNOTATION_REFERENCE_OPERATION,
36
+ project_id=self.project_id,
37
+ clip_id=self.clip_id,
38
+ dependency_identity=self.dependency_identity,
39
+ status=self.status,
40
+ task_id=self.task_id,
41
+ progress=self.progress,
42
+ stage=self.stage,
43
+ error=copy.deepcopy(self.error),
44
+ current=self.current,
45
+ manifest=self.manifest.to_dict() if self.manifest else None,
46
+ )
47
+
48
+
49
+ def parse_annotation_reference_state(
50
+ payload, *, api_url, project_id, clip_id=None, task_id=None
51
+ ):
52
+ value = _object(
53
+ payload,
54
+ {
55
+ "operation",
56
+ "project_id",
57
+ "clip_id",
58
+ "dependency_identity",
59
+ "status",
60
+ "task_id",
61
+ "progress",
62
+ "stage",
63
+ "error",
64
+ "current",
65
+ "manifest",
66
+ },
67
+ "reference state",
68
+ )
69
+ if (
70
+ value["operation"] != ANNOTATION_REFERENCE_OPERATION
71
+ or value["project_id"] != project_id
72
+ ):
73
+ raise ValueError("Reference response belongs to another operation or project")
74
+ _positive(value["project_id"], "project_id")
75
+ _text(value["clip_id"], "clip_id")
76
+ _hash(value["dependency_identity"], "dependency_identity")
77
+ if clip_id is not None and value["clip_id"] != clip_id:
78
+ raise ValueError("Reference response belongs to another Clip")
79
+ if task_id is not None and value["task_id"] != task_id:
80
+ raise ValueError("Reference response belongs to another task")
81
+ if value["status"] not in {
82
+ "unavailable",
83
+ "queued",
84
+ "accepted",
85
+ "running",
86
+ "completed",
87
+ "failed",
88
+ "cancelled",
89
+ }:
90
+ raise ValueError("Unsupported reference task status")
91
+ if value["task_id"] is not None:
92
+ _text(value["task_id"], "task_id")
93
+ if value["stage"] is not None:
94
+ _text(value["stage"], "stage")
95
+ if type(value["current"]) is not bool:
96
+ raise ValueError("Reference current must be boolean")
97
+ progress = value["progress"]
98
+ if (
99
+ isinstance(progress, bool)
100
+ or not isinstance(progress, (int, float))
101
+ or not 0 <= progress <= 100
102
+ ):
103
+ raise ValueError("Invalid reference progress")
104
+ if value["error"] is not None and not isinstance(value["error"], dict):
105
+ raise ValueError("Reference error must be structured")
106
+ manifest = (
107
+ None
108
+ if value["manifest"] is None
109
+ else parse_annotation_reference_manifest(
110
+ value["manifest"],
111
+ api_url=api_url,
112
+ expected_clip_id=value["clip_id"],
113
+ expected_dependency_identity=value["dependency_identity"],
114
+ )
115
+ )
116
+ if value["current"] != (manifest is not None) or (
117
+ manifest and value["status"] != "completed"
118
+ ):
119
+ raise ValueError("Reference ready state is inconsistent")
120
+ return AnnotationReferenceState(
121
+ project_id,
122
+ value["clip_id"],
123
+ value["dependency_identity"],
124
+ value["status"],
125
+ value["task_id"],
126
+ float(progress),
127
+ value["stage"],
128
+ copy.deepcopy(value["error"]),
129
+ value["current"],
130
+ manifest,
131
+ )