x4d-devkit 0.46.0rc2__tar.gz → 0.47.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 (135) hide show
  1. {x4d_devkit-0.46.0rc2/x4d_devkit.egg-info → x4d_devkit-0.47.0rc1}/PKG-INFO +1 -1
  2. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/pyproject.toml +1 -1
  3. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_clean_basemap.py +172 -0
  4. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_sdk_api.py +1 -1
  5. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/clean_basemap.py +104 -14
  6. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/reconstruction.py +9 -1
  7. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1/x4d_devkit.egg-info}/PKG-INFO +1 -1
  8. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/LICENSE +0 -0
  9. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/README.md +0 -0
  10. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/setup.cfg +0 -0
  11. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_annotation_contract.py +0 -0
  12. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_annotation_reference.py +0 -0
  13. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_assets_client.py +0 -0
  14. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_boundary.py +0 -0
  15. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_box.py +0 -0
  16. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_build_info.py +0 -0
  17. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_clean_basemap_client.py +0 -0
  18. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_cli_auth.py +0 -0
  19. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_client_json_contract.py +0 -0
  20. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_client_projects.py +0 -0
  21. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_clip_archives.py +0 -0
  22. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_contract_validation.py +0 -0
  23. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_coordinate_frames.py +0 -0
  24. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_current_state.py +0 -0
  25. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_current_state_identity_consumers_example.py +0 -0
  26. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_dataset_contract.py +0 -0
  27. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_dataset_identity.py +0 -0
  28. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_detection_eval.py +0 -0
  29. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_evaluate.py +0 -0
  30. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_export_bundle.py +0 -0
  31. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_image_review.py +0 -0
  32. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_inference_input.py +0 -0
  33. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_inference_schemas.py +0 -0
  34. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_inference_service.py +0 -0
  35. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_label_schemas.py +0 -0
  36. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_lidar_materialization.py +0 -0
  37. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_loader.py +0 -0
  38. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_matching.py +0 -0
  39. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_metrics.py +0 -0
  40. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_models.py +0 -0
  41. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_platform_pose.py +0 -0
  42. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_preannotation_client.py +0 -0
  43. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_reconstruction_client.py +0 -0
  44. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_release_gate.py +0 -0
  45. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_roadmark_preview.py +0 -0
  46. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_semantic_scene_authoring.py +0 -0
  47. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_semantic_scene_client.py +0 -0
  48. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_semantic_scene_contract.py +0 -0
  49. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_semantic_scene_independent.py +0 -0
  50. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_semantic_scene_native_clip.py +0 -0
  51. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_source_attributes.py +0 -0
  52. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_time_contract.py +0 -0
  53. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_token.py +0 -0
  54. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_tracking_eval.py +0 -0
  55. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_training_identity.py +0 -0
  56. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_transform.py +0 -0
  57. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_transform_chain.py +0 -0
  58. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_validation.py +0 -0
  59. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_vector_map.py +0 -0
  60. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_vector_map_geometry.py +0 -0
  61. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_vector_map_session.py +0 -0
  62. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_work_session.py +0 -0
  63. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/tests/test_work_session_validation_reuse.py +0 -0
  64. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/__init__.py +0 -0
  65. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/annotation_contract.py +0 -0
  66. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/annotation_reference.py +0 -0
  67. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/build_info.py +0 -0
  68. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/cli.py +0 -0
  69. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/__init__.py +0 -0
  70. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/_origin.py +0 -0
  71. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/annotation_reference.py +0 -0
  72. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/assets.py +0 -0
  73. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/bags.py +0 -0
  74. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/capabilities.py +0 -0
  75. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/checkpoints.py +0 -0
  76. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/clean_basemap.py +0 -0
  77. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/client.py +0 -0
  78. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/clip_work_sessions.py +0 -0
  79. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/clips.py +0 -0
  80. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/dataset_identity.py +0 -0
  81. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/image_review.py +0 -0
  82. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/label_schemas.py +0 -0
  83. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/preannotation.py +0 -0
  84. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/projects.py +0 -0
  85. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/semantic_scene.py +0 -0
  86. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  87. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/services.py +0 -0
  88. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/client/test_sets.py +0 -0
  89. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/consumer_identity.py +0 -0
  90. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/coordinate_contract.py +0 -0
  91. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/core/__init__.py +0 -0
  92. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/core/loader.py +0 -0
  93. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/core/models.py +0 -0
  94. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/core/token.py +0 -0
  95. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/core/transform.py +0 -0
  96. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/current_state.py +0 -0
  97. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/dataset_contract.py +0 -0
  98. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/dataset_identity.py +0 -0
  99. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/eval/__init__.py +0 -0
  100. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/eval/api.py +0 -0
  101. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/eval/box.py +0 -0
  102. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/eval/detection.py +0 -0
  103. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/eval/matching.py +0 -0
  104. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/eval/metrics.py +0 -0
  105. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/eval/tracking.py +0 -0
  106. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/export_bundle.py +0 -0
  107. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/image_review.py +0 -0
  108. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/inference/__init__.py +0 -0
  109. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/inference/input.py +0 -0
  110. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/inference/models.py +0 -0
  111. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/inference/schemas.py +0 -0
  112. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/inference/service.py +0 -0
  113. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/inference/tasks.py +0 -0
  114. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/json_contract.py +0 -0
  115. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/label_schema.py +0 -0
  116. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/lidar_materialization.py +0 -0
  117. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/materialize.py +0 -0
  118. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/roadmark_preview.py +0 -0
  119. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/semantic_scene_authoring.py +0 -0
  120. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/semantic_scene_contract.py +0 -0
  121. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/time_contract.py +0 -0
  122. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/training_identity.py +0 -0
  123. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/validation/__init__.py +0 -0
  124. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/validation/contract.py +0 -0
  125. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/validation/platform_pose.py +0 -0
  126. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/validation/report.py +0 -0
  127. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/validation/validator.py +0 -0
  128. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/vector_map.py +0 -0
  129. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/vector_map_geometry.py +0 -0
  130. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit/work_session.py +0 -0
  131. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit.egg-info/SOURCES.txt +0 -0
  132. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  133. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit.egg-info/entry_points.txt +0 -0
  134. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.0rc1}/x4d_devkit.egg-info/requires.txt +0 -0
  135. {x4d_devkit-0.46.0rc2 → x4d_devkit-0.47.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.46.0rc2
3
+ Version: 0.47.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.46.0rc2"
7
+ version = "0.47.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"
@@ -3,6 +3,7 @@ import copy
3
3
  import numpy as np
4
4
  import pytest
5
5
  import zstandard
6
+ import x4d_devkit.clean_basemap as clean_basemap
6
7
 
7
8
  from tests.semantic_scene_contract_fixtures import current_semantic_scene_component
8
9
  from x4d_devkit.clean_basemap import (
@@ -53,6 +54,7 @@ def fixture():
53
54
  raw_point_ordinals=[0, 1, 2],
54
55
  timestamps_ns=[100, 100, 100],
55
56
  decisions=decisions[:3],
57
+ intensities=[2.5, 19.0, 240.0],
56
58
  )
57
59
  profile = {
58
60
  "algorithm": {"id": "clean_static_basemap", "version": "0.6.0"},
@@ -101,6 +103,7 @@ def fixture():
101
103
  "bounds": bounds,
102
104
  "geometry": descriptor,
103
105
  "mapping": mapping.descriptor("mapping"),
106
+ "intensity_range": [2.5, 240.0],
104
107
  }
105
108
  ],
106
109
  "diagnostics": {
@@ -138,6 +141,9 @@ def validate(manifest, assets):
138
141
  manifest,
139
142
  read_asset=assets.__getitem__,
140
143
  observation_to_world=lambda sweep, ordinals, times: original[ordinals],
144
+ observation_intensity=lambda sweep, ordinals, times: np.asarray(
145
+ [2.5, 19.0, 240.0], dtype=np.float32
146
+ )[ordinals],
141
147
  )
142
148
 
143
149
 
@@ -154,6 +160,7 @@ def remap(manifest, assets, **overrides):
154
160
  "raw_point_ordinals": mapping["raw_point_ordinal"],
155
161
  "timestamps_ns": mapping["timestamp_ns"],
156
162
  "decisions": mapping["decision"],
163
+ "intensities": mapping["intensity"],
157
164
  }
158
165
  encoded = encode_clean_basemap_mapping(
159
166
  geometry_raw_sha256=chunk["geometry"]["raw_sha256"], **(arguments | overrides)
@@ -189,6 +196,167 @@ def test_complete_scene_and_exact_original_observation_roundtrip():
189
196
  parsed.data["profile"]["voxel_size_m"] = 1
190
197
 
191
198
 
199
+ def test_intensity_is_original_float32_and_does_not_change_geometry():
200
+ manifest, assets, _ = fixture()
201
+ before = copy.deepcopy(manifest["scene_source"])
202
+ assert MAPPING_DTYPE.itemsize == 24
203
+ remap(manifest, assets, intensities=[2.5, 20.0, 240.0])
204
+ assert manifest["scene_source"] == before
205
+ with pytest.raises(ValueError, match="intensity differs from its original"):
206
+ validate(manifest, assets)
207
+
208
+
209
+ @pytest.mark.parametrize(
210
+ "values",
211
+ [
212
+ [1, 2],
213
+ [True, False, True],
214
+ [1, float("nan"), 3],
215
+ [1, float("inf"), 3],
216
+ [1, 1e100, 3],
217
+ ],
218
+ )
219
+ def test_invalid_intensity_cannot_be_encoded(values):
220
+ manifest, assets, _ = fixture()
221
+ with pytest.raises(ValueError, match="intensit"):
222
+ remap(manifest, assets, intensities=values)
223
+
224
+
225
+ def test_intensity_range_cannot_hide_wrong_values():
226
+ manifest, assets, _ = fixture()
227
+ manifest["chunks"][0]["intensity_range"] = [0.0, 255.0]
228
+ manifest["basemap_revision"] = compute_clean_basemap_revision(manifest)
229
+ with pytest.raises(ValueError, match="intensity range"):
230
+ validate(manifest, assets)
231
+
232
+
233
+ def test_old_manifest_and_mapping_are_rejected():
234
+ manifest, assets, _ = fixture()
235
+ manifest["contract_version"] = "x4d-clean-static-basemap-v4"
236
+ manifest["basemap_revision"] = compute_clean_basemap_revision(manifest)
237
+ with pytest.raises(ValueError):
238
+ parse_clean_basemap_manifest(manifest)
239
+ chunk = manifest["chunks"][0]
240
+ descriptor = chunk["mapping"]
241
+ raw = bytearray(zstandard.ZstdDecompressor().decompress(assets[descriptor["path"]]))
242
+ raw[:8] = b"X4DCBM2\0"
243
+ payload = zstandard.ZstdCompressor().compress(raw)
244
+ descriptor = dict(
245
+ descriptor,
246
+ path=f"generated/reconstruction/clean_basemap/mapping/{sha256_bytes(payload)[7:]}.zst",
247
+ sha256=sha256_bytes(payload),
248
+ raw_sha256=sha256_bytes(bytes(raw)),
249
+ size_bytes=len(payload),
250
+ )
251
+ with pytest.raises(ValueError):
252
+ decode_clean_basemap_mapping(
253
+ payload,
254
+ descriptor=descriptor,
255
+ geometry_raw_sha256=chunk["geometry"]["raw_sha256"],
256
+ point_count=chunk["point_count"],
257
+ )
258
+
259
+
260
+ def test_decision_proof_reuse_is_within_one_validation_call(monkeypatch):
261
+ manifest, assets, _ = fixture()
262
+ decoded = []
263
+ original = clean_basemap.decode_clean_basemap_decisions
264
+
265
+ def count(*args, **kwargs):
266
+ result = original(*args, **kwargs)
267
+ assert not result.flags.writeable
268
+ decoded.append(result.nbytes)
269
+ return result
270
+
271
+ monkeypatch.setattr(clean_basemap, "decode_clean_basemap_decisions", count)
272
+ validate(manifest, assets)
273
+ assert decoded == [5]
274
+ validate(manifest, assets)
275
+ assert decoded == [5, 5]
276
+
277
+
278
+ @pytest.mark.parametrize("changed", [b"tampered", bytearray, memoryview])
279
+ def test_cached_decision_proof_still_checks_every_carrier_read(changed):
280
+ manifest, assets, _ = fixture()
281
+ original = original_points(manifest, assets)
282
+ path = manifest["sweep_sources"][0]["decisions"]["path"]
283
+ reads = 0
284
+
285
+ def read_asset(name):
286
+ nonlocal reads
287
+ body = assets[name]
288
+ if name == path:
289
+ reads += 1
290
+ if reads > 1:
291
+ return changed(body) if callable(changed) else changed
292
+ return body
293
+
294
+ with pytest.raises(ValueError, match="payload size/hash"):
295
+ validate_clean_basemap_resource(
296
+ manifest,
297
+ read_asset=read_asset,
298
+ observation_to_world=lambda sweep, ordinals, times: original[ordinals],
299
+ observation_intensity=lambda sweep, ordinals, times: np.asarray(
300
+ [2.5, 19.0, 240.0], dtype=np.float32
301
+ )[ordinals],
302
+ )
303
+
304
+
305
+ def add_second_sweep(manifest, assets):
306
+ source = copy.deepcopy(manifest["sweep_sources"][0])
307
+ source.update(sample_data_token="sweep-1", asset_sha256="sha256:" + "c" * 64)
308
+ carrier = encode_clean_basemap_decisions(
309
+ asset_sha256=source["asset_sha256"], decisions=[1, 0, 3, 18, 33]
310
+ )
311
+ source["decisions"] = carrier.descriptor("decisions")
312
+ assets[source["decisions"]["path"]] = carrier.payload
313
+ manifest["sweep_sources"].append(source)
314
+ manifest["diagnostics"]["source_point_count"] *= 2
315
+ manifest["diagnostics"]["decision_counts"] = {
316
+ key: value * 2
317
+ for key, value in manifest["diagnostics"]["decision_counts"].items()
318
+ }
319
+ manifest["basemap_revision"] = compute_clean_basemap_revision(manifest)
320
+ return source
321
+
322
+
323
+ @pytest.mark.parametrize("changed", ["source", "count", "descriptor"])
324
+ def test_decision_cache_never_reuses_a_different_binding(changed):
325
+ manifest, assets, _ = fixture()
326
+ second = add_second_sweep(manifest, assets)
327
+ second["decisions"] = copy.deepcopy(manifest["sweep_sources"][0]["decisions"])
328
+ if changed != "source":
329
+ second["asset_sha256"] = manifest["sweep_sources"][0]["asset_sha256"]
330
+ if changed == "count":
331
+ second["point_count"] += 1
332
+ manifest["diagnostics"]["source_point_count"] += 1
333
+ manifest["diagnostics"]["decision_counts"]["33"] += 1
334
+ if changed == "descriptor":
335
+ second["decisions"]["size_bytes"] += 1
336
+ manifest["basemap_revision"] = compute_clean_basemap_revision(manifest)
337
+ with pytest.raises(ValueError):
338
+ validate(manifest, assets)
339
+
340
+
341
+ @pytest.mark.parametrize("limit,expected_decodes", [(0, 3), (4, 3), (5, 3), (10, 2)])
342
+ def test_decision_proof_cache_is_bounded_and_evicted_entries_are_revalidated(
343
+ monkeypatch, limit, expected_decodes
344
+ ):
345
+ manifest, assets, _ = fixture()
346
+ add_second_sweep(manifest, assets)
347
+ original = clean_basemap.decode_clean_basemap_decisions
348
+ calls = []
349
+
350
+ def count(*args, **kwargs):
351
+ calls.append(kwargs["asset_sha256"])
352
+ return original(*args, **kwargs)
353
+
354
+ monkeypatch.setattr(clean_basemap, "_DECISION_CACHE_BYTES", limit)
355
+ monkeypatch.setattr(clean_basemap, "decode_clean_basemap_decisions", count)
356
+ validate(manifest, assets)
357
+ assert len(calls) == expected_decodes
358
+
359
+
192
360
  @pytest.mark.parametrize(
193
361
  "overrides,message",
194
362
  [
@@ -221,6 +389,9 @@ def test_original_observation_projection_is_required(kind):
221
389
  manifest,
222
390
  read_asset=assets.__getitem__,
223
391
  observation_to_world=lambda *args: result,
392
+ observation_intensity=lambda sweep, ordinals, times: np.asarray(
393
+ [2.5, 19.0, 240.0], dtype=np.float32
394
+ )[ordinals],
224
395
  )
225
396
 
226
397
 
@@ -289,6 +460,7 @@ def test_every_byte_in_last_record_obeys_exact_decision_domain(family):
289
460
  raw_point_ordinals=np.arange(count),
290
461
  timestamps_ns=np.full(count, 100),
291
462
  decisions=np.full(count, KEEP_GROUND),
463
+ intensities=np.arange(count, dtype=np.float32),
292
464
  )
293
465
  domain, decode = KEEP_CODES, decode_clean_basemap_mapping
294
466
  arguments = dict(geometry_raw_sha256=anchor, point_count=count)
@@ -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.46.0rc2"
14
+ assert x4d_devkit.__version__ == "0.47.0rc1"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -7,6 +7,7 @@ their native authorities; this module never guesses ownership from coordinates.
7
7
  from __future__ import annotations
8
8
 
9
9
  import copy
10
+ from collections import OrderedDict
10
11
  from dataclasses import dataclass
11
12
  import math
12
13
  import re
@@ -35,10 +36,11 @@ from x4d_devkit.semantic_scene_contract import (
35
36
  )
36
37
  from x4d_devkit.time_contract import parse_time_ns
37
38
 
38
- CLEAN_BASEMAP_CONTRACT_VERSION = "x4d-clean-static-basemap-v4"
39
+ CLEAN_BASEMAP_CONTRACT_VERSION = "x4d-clean-static-basemap-v5"
39
40
  CLEAN_BASEMAP_OPERATION = "reconstruction.prepare_clean_static_basemap"
40
- CLEAN_BASEMAP_MAPPING_FORMAT = "x4d-clean-static-basemap-mapping-v2"
41
+ CLEAN_BASEMAP_MAPPING_FORMAT = "x4d-clean-static-basemap-mapping-v3"
41
42
  CLEAN_BASEMAP_DECISION_FORMAT = "x4d-clean-static-basemap-decisions-v3"
43
+ _DECISION_CACHE_BYTES = 128 << 20
42
44
  CLEAN_BASEMAP_INPUT_FIELDS = frozenset(
43
45
  {"samples_revision", "sweeps_revision", "calibration_revision", "pose_revision"}
44
46
  )
@@ -53,7 +55,7 @@ DECISION_CODES = frozenset({0, 1, 3, 18, 32, 33, 34})
53
55
  KEEP_CODES = frozenset({0, 1, 3})
54
56
  _HASH = re.compile(r"^sha256:[0-9a-f]{64}$")
55
57
  _HEADER = struct.Struct("<8sI32s")
56
- _MAPPING_MAGIC = b"X4DCBM2\0"
58
+ _MAPPING_MAGIC = b"X4DCBM3\0"
57
59
  _DECISION_MAGIC = b"X4DCBD3\0"
58
60
  MAPPING_DTYPE = np.dtype(
59
61
  [
@@ -61,7 +63,8 @@ MAPPING_DTYPE = np.dtype(
61
63
  ("raw_point_ordinal", "<u4"),
62
64
  ("timestamp_ns", "<u8"),
63
65
  ("decision", "u1"),
64
- ("reserved", "u1", (7,)),
66
+ ("intensity", "<f4"),
67
+ ("reserved", "u1", (3,)),
65
68
  ]
66
69
  )
67
70
  _MAX_U32 = (1 << 32) - 1
@@ -106,6 +109,21 @@ def _bounds(value: Any, path: str) -> None:
106
109
  raise ValueError(f"{path} is inverted")
107
110
 
108
111
 
112
+ def _intensity_range(value: Any, path: str) -> None:
113
+ if (
114
+ not isinstance(value, list)
115
+ or len(value) != 2
116
+ or any(
117
+ isinstance(v, bool)
118
+ or not isinstance(v, (int, float))
119
+ or not math.isfinite(v)
120
+ for v in value
121
+ )
122
+ or value[0] > value[1]
123
+ ):
124
+ raise ValueError(f"{path} must contain finite ordered intensity bounds")
125
+
126
+
109
127
  def _freeze(value: Any) -> Any:
110
128
  if isinstance(value, Mapping):
111
129
  return MappingProxyType({k: _freeze(v) for k, v in value.items()})
@@ -347,7 +365,16 @@ def parse_clean_basemap_manifest(
347
365
  for i, chunk in enumerate(chunks):
348
366
  path = f"chunks[{i}]"
349
367
  _object(
350
- chunk, {"ordinal", "point_count", "bounds", "geometry", "mapping"}, path
368
+ chunk,
369
+ {
370
+ "ordinal",
371
+ "point_count",
372
+ "bounds",
373
+ "intensity_range",
374
+ "geometry",
375
+ "mapping",
376
+ },
377
+ path,
351
378
  )
352
379
  if chunk["ordinal"] != i or type(chunk["ordinal"]) is not int:
353
380
  raise ValueError("chunk ordinals must be contiguous in canonical order")
@@ -358,6 +385,7 @@ def parse_clean_basemap_manifest(
358
385
  f"{path}.point_count",
359
386
  )
360
387
  _bounds(chunk["bounds"], f"{path}.bounds")
388
+ _intensity_range(chunk["intensity_range"], f"{path}.intensity_range")
361
389
  bounds.append(chunk["bounds"])
362
390
  _descriptor(chunk["geometry"], "geometry", f"{path}.geometry")
363
391
  _descriptor(chunk["mapping"], "mapping", f"{path}.mapping")
@@ -465,6 +493,7 @@ def encode_clean_basemap_mapping(
465
493
  raw_point_ordinals: Any,
466
494
  timestamps_ns: Any,
467
495
  decisions: Any,
496
+ intensities: Any,
468
497
  ) -> CleanBasemapPayload:
469
498
  vectors = {
470
499
  "sweep_index": _vector(sweep_indices, _MAX_U32, "sweep_indices"),
@@ -485,6 +514,19 @@ def encode_clean_basemap_mapping(
485
514
  records = np.zeros(count, dtype=MAPPING_DTYPE)
486
515
  for key, vector in vectors.items():
487
516
  records[key] = vector
517
+ intensity = np.asarray(intensities)
518
+ if (
519
+ intensity.shape != (count,)
520
+ or intensity.dtype.kind not in "iuf"
521
+ or not np.isfinite(intensity).all()
522
+ ):
523
+ raise ValueError(
524
+ "intensities must contain one finite numeric value per geometry point"
525
+ )
526
+ with np.errstate(over="ignore", invalid="ignore"):
527
+ records["intensity"] = intensity
528
+ if not np.isfinite(records["intensity"]).all():
529
+ raise ValueError("intensities must be representable as finite float32")
488
530
  return _encode(_MAPPING_MAGIC, geometry_raw_sha256, records)
489
531
 
490
532
 
@@ -509,6 +551,7 @@ def decode_clean_basemap_mapping(
509
551
  np.any(records["reserved"] != 0)
510
552
  or np.any(~np.isin(records["decision"], tuple(KEEP_CODES)))
511
553
  or np.any(records["timestamp_ns"] > _MAX_I64)
554
+ or not np.isfinite(records["intensity"]).all()
512
555
  ):
513
556
  raise ValueError(
514
557
  "mapping records are noncanonical or contain removed observations"
@@ -602,6 +645,9 @@ def validate_clean_basemap_resource(
602
645
  observation_to_world: Callable[
603
646
  [Mapping[str, Any], np.ndarray, np.ndarray], np.ndarray
604
647
  ],
648
+ observation_intensity: Callable[
649
+ [Mapping[str, Any], np.ndarray, np.ndarray], np.ndarray
650
+ ],
605
651
  ) -> CleanBasemapManifest:
606
652
  """Verify exact original observations and byte-exact complete scene correspondence."""
607
653
  parsed = parse_clean_basemap_manifest(manifest)
@@ -611,13 +657,47 @@ def validate_clean_basemap_resource(
611
657
  for sweep in data["sweep_sources"]
612
658
  ]
613
659
  actual_counts = {str(code): 0 for code in DECISION_CODES}
614
- for sweep in data["sweep_sources"]:
660
+ decisions_cache = OrderedDict()
661
+ decisions_bytes = 0
662
+
663
+ def read_decisions(sweep):
664
+ nonlocal decisions_bytes
665
+ descriptor = sweep["decisions"]
666
+ payload = read_asset(descriptor["path"])
667
+ # A cached proof never authorizes changed or non-byte carrier data.
668
+ if (
669
+ type(payload) is not bytes
670
+ or len(payload) != descriptor["size_bytes"]
671
+ or sha256_bytes(payload) != descriptor["sha256"]
672
+ ):
673
+ raise ValueError(
674
+ "clean basemap payload size/hash does not match descriptor"
675
+ )
676
+ key = (
677
+ tuple(sorted(descriptor.items())),
678
+ sweep["asset_sha256"],
679
+ sweep["point_count"],
680
+ )
681
+ values = decisions_cache.pop(key, None)
682
+ if values is not None:
683
+ decisions_cache[key] = values
684
+ return values
615
685
  values = decode_clean_basemap_decisions(
616
- read_asset(sweep["decisions"]["path"]),
617
- descriptor=sweep["decisions"],
686
+ payload,
687
+ descriptor=descriptor,
618
688
  asset_sha256=sweep["asset_sha256"],
619
689
  point_count=sweep["point_count"],
620
690
  )
691
+ if values.nbytes <= _DECISION_CACHE_BYTES:
692
+ while decisions_bytes + values.nbytes > _DECISION_CACHE_BYTES:
693
+ _, evicted = decisions_cache.popitem(last=False)
694
+ decisions_bytes -= evicted.nbytes
695
+ decisions_cache[key] = values
696
+ decisions_bytes += values.nbytes
697
+ return values
698
+
699
+ for sweep in data["sweep_sources"]:
700
+ values = read_decisions(sweep)
621
701
  counts = np.bincount(values, minlength=256)
622
702
  for code in DECISION_CODES:
623
703
  actual_counts[str(code)] += int(counts[code])
@@ -645,6 +725,11 @@ def validate_clean_basemap_resource(
645
725
  geometry_raw_sha256=descriptor["raw_sha256"],
646
726
  point_count=chunk["point_count"],
647
727
  )
728
+ if [
729
+ float(mapping["intensity"].min()),
730
+ float(mapping["intensity"].max()),
731
+ ] != chunk["intensity_range"]:
732
+ raise ValueError("basemap intensity range differs from its actual points")
648
733
  if np.any(mapping["sweep_index"] >= len(data["sweep_sources"])):
649
734
  raise ValueError("mapping source index is outside the pinned source tables")
650
735
  for index in np.unique(mapping["sweep_index"]):
@@ -661,12 +746,7 @@ def validate_clean_basemap_resource(
661
746
  if np.any(times < low) or np.any(times > high):
662
747
  raise ValueError("mapping exact point time is outside its source sweep")
663
748
  _mark_unique(observation_seen[index], ordinals, "original observation")
664
- values = decode_clean_basemap_decisions(
665
- read_asset(sweep["decisions"]["path"]),
666
- descriptor=sweep["decisions"],
667
- asset_sha256=sweep["asset_sha256"],
668
- point_count=sweep["point_count"],
669
- )
749
+ values = read_decisions(sweep)
670
750
  if not np.array_equal(values[ordinals], mapping["decision"][selected]):
671
751
  raise ValueError(
672
752
  "mapping decision does not match its original observation"
@@ -684,6 +764,16 @@ def validate_clean_basemap_resource(
684
764
  raise ValueError(
685
765
  "basemap geometry differs from its original observations"
686
766
  )
767
+ intensity = np.asarray(observation_intensity(sweep, ordinals, times))
768
+ if (
769
+ intensity.shape != ordinals.shape
770
+ or intensity.dtype.kind not in "iuf"
771
+ or not np.isfinite(intensity).all()
772
+ or not np.array_equal(intensity, mapping["intensity"][selected])
773
+ ):
774
+ raise ValueError(
775
+ "basemap intensity differs from its original observations"
776
+ )
687
777
  return parsed
688
778
 
689
779
 
@@ -307,7 +307,7 @@ class ReconstructionAPI:
307
307
  "clean basemap chunk ordinal is outside the exact resource"
308
308
  )
309
309
  chunk = chunks[chunk_ordinal]
310
- return decode_clean_basemap_mapping(
310
+ mapping = decode_clean_basemap_mapping(
311
311
  self._client.assets.fetch(
312
312
  state.asset_urls[chunk["mapping"]["path"]]
313
313
  ).content,
@@ -315,6 +315,14 @@ class ReconstructionAPI:
315
315
  geometry_raw_sha256=chunk["geometry"]["raw_sha256"],
316
316
  point_count=chunk["point_count"],
317
317
  )
318
+ if [
319
+ float(mapping["intensity"].min()),
320
+ float(mapping["intensity"].max()),
321
+ ] != list(chunk["intensity_range"]):
322
+ raise ValueError(
323
+ "clean basemap intensity range differs from its descriptor"
324
+ )
325
+ return mapping
318
326
 
319
327
  def cancel_clean_basemap_task(
320
328
  self, *, project_id: int, task_id: str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.46.0rc2
3
+ Version: 0.47.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
File without changes
File without changes
File without changes