cloudanalyzer 0.2.0__tar.gz → 0.3.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.
Files changed (198) hide show
  1. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/PKG-INFO +1 -1
  2. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/__init__.py +1 -1
  3. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/ground_evaluate.py +30 -8
  4. cloudanalyzer-0.3.0/ca/core/map_evaluate.py +302 -0
  5. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/height_band.py +5 -5
  6. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/voxel_confusion.py +5 -4
  7. cloudanalyzer-0.3.0/ca/experiments/map_evaluate/common.py +99 -0
  8. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/map_evaluate/evaluate.py +19 -11
  9. cloudanalyzer-0.3.0/ca/experiments/map_evaluate/nn_thresholds.py +16 -0
  10. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/geometry.py +279 -73
  11. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/split.py +19 -7
  12. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/PKG-INFO +1 -1
  13. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/SOURCES.txt +5 -0
  14. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer_cli/main.py +37 -4
  15. cloudanalyzer-0.3.0/tests/test_geometry_splat_ellipsoid.py +275 -0
  16. cloudanalyzer-0.3.0/tests/test_map_evaluate_core.py +81 -0
  17. cloudanalyzer-0.3.0/tests/test_phase17_vectorize.py +210 -0
  18. cloudanalyzer-0.3.0/tests/test_phase18_vectorize.py +150 -0
  19. cloudanalyzer-0.2.0/ca/experiments/map_evaluate/common.py +0 -201
  20. cloudanalyzer-0.2.0/ca/experiments/map_evaluate/nn_thresholds.py +0 -135
  21. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/LICENSE +0 -0
  22. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/README.md +0 -0
  23. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/align.py +0 -0
  24. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/baseline_history.py +0 -0
  25. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/batch.py +0 -0
  26. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/benchmark.py +0 -0
  27. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/bundle.py +0 -0
  28. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/compare.py +0 -0
  29. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/convert.py +0 -0
  30. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/__init__.py +0 -0
  31. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/check_baseline_evolution.py +0 -0
  32. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/check_scaffolding.py +0 -0
  33. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/check_triage.py +0 -0
  34. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/checks.py +0 -0
  35. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/web_progressive_loading.py +0 -0
  36. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/web_sampling.py +0 -0
  37. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/core/web_trajectory_sampling.py +0 -0
  38. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/crop.py +0 -0
  39. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/density_map.py +0 -0
  40. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/detection.py +0 -0
  41. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/diff.py +0 -0
  42. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/downsample.py +0 -0
  43. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/evaluate.py +0 -0
  44. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/__init__.py +0 -0
  45. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/__init__.py +0 -0
  46. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/common.py +0 -0
  47. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/evaluate.py +0 -0
  48. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/pareto_promote.py +0 -0
  49. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/stability_window.py +0 -0
  50. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_baseline_evolution/threshold_guard.py +0 -0
  51. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/__init__.py +0 -0
  52. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/common.py +0 -0
  53. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/evaluate.py +0 -0
  54. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/literal_profiles.py +0 -0
  55. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/object_sections.py +0 -0
  56. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_scaffolding/pipeline_overlays.py +0 -0
  57. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/__init__.py +0 -0
  58. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/common.py +0 -0
  59. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/evaluate.py +0 -0
  60. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/pareto_frontier.py +0 -0
  61. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/severity_weighted.py +0 -0
  62. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/check_triage/signature_cluster.py +0 -0
  63. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/__init__.py +0 -0
  64. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/common.py +0 -0
  65. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/evaluate.py +0 -0
  66. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/ground_evaluate/nearest_neighbor.py +0 -0
  67. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/map_evaluate/__init__.py +0 -0
  68. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/map_evaluate/voxel_entropy.py +0 -0
  69. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/process_docs.py +0 -0
  70. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/__init__.py +0 -0
  71. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/common.py +0 -0
  72. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/distance_shells.py +0 -0
  73. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/evaluate.py +0 -0
  74. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/grid_tiles.py +0 -0
  75. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_progressive_loading/spatial_shuffle.py +0 -0
  76. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/__init__.py +0 -0
  77. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/common.py +0 -0
  78. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/evaluate.py +0 -0
  79. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/functional_voxel.py +0 -0
  80. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/object_random.py +0 -0
  81. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_sampling/pipeline_hybrid.py +0 -0
  82. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/__init__.py +0 -0
  83. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/common.py +0 -0
  84. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/distance_accumulator.py +0 -0
  85. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/evaluate.py +0 -0
  86. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/turn_aware.py +0 -0
  87. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/experiments/web_trajectory_sampling/uniform_stride.py +0 -0
  88. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/filter.py +0 -0
  89. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/ground_evaluate.py +0 -0
  90. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/history.py +0 -0
  91. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/info.py +0 -0
  92. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/io.py +0 -0
  93. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/kitti.py +0 -0
  94. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/log.py +0 -0
  95. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/loop_closure_report.py +0 -0
  96. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/merge.py +0 -0
  97. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/metrics.py +0 -0
  98. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/mme.py +0 -0
  99. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/normals.py +0 -0
  100. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/object_eval.py +0 -0
  101. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/pareto.py +0 -0
  102. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/pipeline.py +0 -0
  103. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/plot.py +0 -0
  104. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/point_summary.py +0 -0
  105. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/posegraph.py +0 -0
  106. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/pr_comment.py +0 -0
  107. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/registration.py +0 -0
  108. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/report.py +0 -0
  109. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/run_evaluate.py +0 -0
  110. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/sample.py +0 -0
  111. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/scan_match_debug.py +0 -0
  112. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/slam_debug.py +0 -0
  113. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/stats.py +0 -0
  114. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/tracking.py +0 -0
  115. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/trajectory.py +0 -0
  116. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/view.py +0 -0
  117. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/visualization.py +0 -0
  118. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/ca/web.py +0 -0
  119. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/dependency_links.txt +0 -0
  120. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/entry_points.txt +0 -0
  121. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/requires.txt +0 -0
  122. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer.egg-info/top_level.txt +0 -0
  123. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/cloudanalyzer_cli/__init__.py +0 -0
  124. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/pyproject.toml +0 -0
  125. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/setup.cfg +0 -0
  126. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/setup.py +0 -0
  127. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_align.py +0 -0
  128. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_baseline_history.py +0 -0
  129. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_batch.py +0 -0
  130. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_benchmark.py +0 -0
  131. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_bundle.py +0 -0
  132. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_check_baseline_evolution_process.py +0 -0
  133. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_check_scaffolding_process.py +0 -0
  134. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_check_suite.py +0 -0
  135. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_check_triage_process.py +0 -0
  136. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_cli.py +0 -0
  137. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_compare.py +0 -0
  138. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_convert.py +0 -0
  139. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_crop.py +0 -0
  140. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_density_map.py +0 -0
  141. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_detection_evaluate.py +0 -0
  142. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_detection_tracking_report.py +0 -0
  143. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_diff.py +0 -0
  144. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_downsample.py +0 -0
  145. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_evaluate.py +0 -0
  146. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_filter.py +0 -0
  147. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_geometry.py +0 -0
  148. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_ground_evaluate.py +0 -0
  149. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_ground_evaluate_process.py +0 -0
  150. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_history.py +0 -0
  151. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_info.py +0 -0
  152. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_io.py +0 -0
  153. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_kitti.py +0 -0
  154. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_logging.py +0 -0
  155. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_loop_closure_report.py +0 -0
  156. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_map_evaluate.py +0 -0
  157. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_merge.py +0 -0
  158. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_metrics.py +0 -0
  159. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_mme.py +0 -0
  160. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_normals.py +0 -0
  161. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_object_eval_iou.py +0 -0
  162. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_output_json.py +0 -0
  163. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_pareto.py +0 -0
  164. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_phase16_large_scale.py +0 -0
  165. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_pipeline.py +0 -0
  166. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_plot.py +0 -0
  167. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_posegraph.py +0 -0
  168. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_pr_comment.py +0 -0
  169. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_process_docs.py +0 -0
  170. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_public_benchmark_pack.py +0 -0
  171. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_public_object_eval_examples.py +0 -0
  172. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_registration.py +0 -0
  173. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_report.py +0 -0
  174. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_run_batch.py +0 -0
  175. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_run_evaluate.py +0 -0
  176. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_sample.py +0 -0
  177. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_scan_match_debug.py +0 -0
  178. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_slam_debug.py +0 -0
  179. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_split.py +0 -0
  180. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_stats.py +0 -0
  181. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_threshold.py +0 -0
  182. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_tracking_evaluate.py +0 -0
  183. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_traj_batch.py +0 -0
  184. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_trajectory.py +0 -0
  185. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_visualization.py +0 -0
  186. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web.py +0 -0
  187. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_progressive_loading_core.py +0 -0
  188. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_progressive_loading_evaluate.py +0 -0
  189. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_progressive_loading_process.py +0 -0
  190. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_progressive_loading_strategies.py +0 -0
  191. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_sampling_core.py +0 -0
  192. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_sampling_evaluate.py +0 -0
  193. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_sampling_process.py +0 -0
  194. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_sampling_strategies.py +0 -0
  195. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_trajectory_sampling_core.py +0 -0
  196. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_trajectory_sampling_evaluate.py +0 -0
  197. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_trajectory_sampling_process.py +0 -0
  198. {cloudanalyzer-0.2.0 → cloudanalyzer-0.3.0}/tests/test_web_trajectory_sampling_strategies.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudanalyzer
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CLI-first QA toolkit for point cloud, trajectory, and 3D perception outputs
5
5
  Author: rsasaki0109
6
6
  License-Expression: MIT
@@ -1,3 +1,3 @@
1
1
  """CloudAnalyzer - AI-friendly CLI tool for point cloud analysis."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -47,12 +47,34 @@ class GroundEvaluateStrategy(Protocol):
47
47
  """Evaluate ground segmentation quality."""
48
48
 
49
49
 
50
- def _voxel_keys(points: np.ndarray, voxel_size: float) -> set[tuple[int, int, int]]:
51
- """Compute voxel grid keys for an Nx3 point array."""
50
+ def _voxel_keys(points: np.ndarray, voxel_size: float) -> np.ndarray:
51
+ """Compute unique voxel grid keys for an Nx3 point array.
52
+
53
+ Returns an (M, 3) int64 array of unique (i, j, k) voxel indices, sorted
54
+ lexicographically. The original implementation built a ``set`` of Python
55
+ tuples via a per-row generator, which dominated runtime on city-scale
56
+ inputs (millions of points × 4 calls per evaluation).
57
+ """
52
58
  if points.shape[0] == 0:
53
- return set()
59
+ return np.empty((0, 3), dtype=np.int64)
54
60
  indices = np.floor(points / voxel_size).astype(np.int64)
55
- return {(int(row[0]), int(row[1]), int(row[2])) for row in indices}
61
+ return np.unique(indices, axis=0)
62
+
63
+
64
+ def _voxel_intersection_size(a: np.ndarray, b: np.ndarray) -> int:
65
+ """Count voxel keys present in both ``a`` and ``b``.
66
+
67
+ Both inputs are expected to be the unique (M, 3) int64 output of
68
+ :func:`_voxel_keys`. Packs each row into a single ``np.void`` of the row's
69
+ byte length so ``np.intersect1d`` can run in C.
70
+ """
71
+ if a.size == 0 or b.size == 0:
72
+ return 0
73
+ item_size = a.dtype.itemsize * a.shape[1]
74
+ void_dtype = np.dtype((np.void, item_size))
75
+ a_view = np.ascontiguousarray(a).view(void_dtype).ravel()
76
+ b_view = np.ascontiguousarray(b).view(void_dtype).ravel()
77
+ return int(np.intersect1d(a_view, b_view, assume_unique=True).size)
56
78
 
57
79
 
58
80
  def confusion_metrics(tp: int, fp: int, fn: int, tn: int) -> dict[str, float]:
@@ -83,10 +105,10 @@ class VoxelConfusionGroundEvaluateStrategy:
83
105
  ref_ground_voxels = _voxel_keys(request.reference_ground, request.voxel_size)
84
106
  ref_nonground_voxels = _voxel_keys(request.reference_nonground, request.voxel_size)
85
107
 
86
- tp = len(est_ground_voxels & ref_ground_voxels)
87
- fp = len(est_ground_voxels & ref_nonground_voxels)
88
- fn = len(est_nonground_voxels & ref_ground_voxels)
89
- tn = len(est_nonground_voxels & ref_nonground_voxels)
108
+ tp = _voxel_intersection_size(est_ground_voxels, ref_ground_voxels)
109
+ fp = _voxel_intersection_size(est_ground_voxels, ref_nonground_voxels)
110
+ fn = _voxel_intersection_size(est_nonground_voxels, ref_ground_voxels)
111
+ tn = _voxel_intersection_size(est_nonground_voxels, ref_nonground_voxels)
90
112
  metrics = confusion_metrics(tp, fp, fn, tn)
91
113
 
92
114
  return GroundEvaluateResult(
@@ -0,0 +1,302 @@
1
+ """Stable, minimal interface for point-cloud map evaluation.
2
+
3
+ This slice started as ``ca/experiments/map_evaluate/`` with two
4
+ non-comparable strategies on separate metric-family lanes:
5
+
6
+ - ``nn_thresholds`` — reference-based (GT-aware) MapEval-style accuracy and
7
+ completeness@τ via per-point nearest-neighbor distances (scipy cKDTree).
8
+ - ``voxel_entropy`` — reference-free self-consistency proxy via voxelized
9
+ neighborhood occupancy entropy.
10
+
11
+ Because the two strategies do not compete head-to-head, "promoting to core"
12
+ means lifting the **request/result contract plus the adopted reference-based
13
+ strategy** here, leaving ``voxel_entropy`` in ``ca/experiments/map_evaluate``
14
+ as the orthogonal reference-free option. Callers that just want GT-based
15
+ metrics can now depend on ``ca.core.map_evaluate`` directly and the CLI
16
+ no longer reaches into ``ca.experiments``.
17
+
18
+ The reference-free lane remains experimental until we settle on a single
19
+ GT-free metric — anyone needing it for now imports from
20
+ ``ca.experiments.map_evaluate.voxel_entropy`` explicitly.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Any, Protocol
28
+
29
+ import numpy as np
30
+
31
+
32
+ # ---------------------------------------------------------------- contract
33
+
34
+
35
+ @dataclass(slots=True)
36
+ class MapEvaluateRequest:
37
+ """Compare an estimated point cloud map to a reference (optional).
38
+
39
+ Strategies with ``reference_required=True`` raise on ``reference_points=None``;
40
+ reference-free strategies treat the same field as unused.
41
+ """
42
+
43
+ estimated_points: np.ndarray # (N, 3)
44
+ reference_points: np.ndarray | None = None # (M, 3) when present
45
+ # MapEval-inspired: allow using an external coarse alignment.
46
+ initial_transform_4x4: np.ndarray | None = None # shape (4, 4)
47
+ # "none" = use estimated_points as-is, "initial" = pre-apply initial_transform_4x4.
48
+ align_mode: str = "none"
49
+ downsample_voxel_size: float = 0.0
50
+ # Optional output dir for strategy-specific artifacts (colored PLYs, etc.)
51
+ artifact_dir: str | None = None
52
+ # MapEval "accuracy_level" thresholds for inlier ratios.
53
+ thresholds_m: tuple[float, ...] = (0.2, 0.1, 0.08, 0.05, 0.01)
54
+ # A coarse voxel size useful for structure-aware proxies.
55
+ structure_voxel_size: float = 0.5
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class MapEvaluateResult:
60
+ """Common output for all strategies.
61
+
62
+ Classification fields (``metric_family``, ``reference_required``, ``mode``,
63
+ ``sampling_policy``) describe *how* the metrics were computed so consumers
64
+ (CI gates, reports, batch aggregators) can keep reference-based and
65
+ reference-free metrics in separate lanes without parsing metric names.
66
+ """
67
+
68
+ strategy: str
69
+ design: str
70
+ metrics: dict[str, float]
71
+ artifacts: dict[str, Any]
72
+ metric_family: str = "unspecified"
73
+ reference_required: bool = False
74
+ mode: str = "exact"
75
+ sampling_policy: dict[str, Any] = field(default_factory=dict)
76
+
77
+
78
+ class MapEvaluateStrategy(Protocol):
79
+ """Protocol kept in core after promoting the reference-based strategy."""
80
+
81
+ name: str
82
+ design: str
83
+
84
+ def evaluate(self, request: MapEvaluateRequest) -> MapEvaluateResult:
85
+ """Evaluate a map and return classified metrics."""
86
+
87
+
88
+ # ---------------------------------------------------------------- helpers
89
+
90
+
91
+ def _require_xyz(points: np.ndarray, name: str) -> np.ndarray:
92
+ arr = np.asarray(points, dtype=np.float64)
93
+ if arr.ndim != 2 or arr.shape[1] != 3:
94
+ raise ValueError(f"{name} must be shape (N, 3); got {arr.shape}")
95
+ return arr
96
+
97
+
98
+ def _require_transform_4x4(matrix: np.ndarray, name: str) -> np.ndarray:
99
+ mat = np.asarray(matrix, dtype=np.float64)
100
+ if mat.shape != (4, 4):
101
+ raise ValueError(f"{name} must be shape (4, 4); got {mat.shape}")
102
+ return mat
103
+
104
+
105
+ def apply_transform(points: np.ndarray, transform_4x4: np.ndarray) -> np.ndarray:
106
+ pts = _require_xyz(points, "points")
107
+ t = _require_transform_4x4(transform_4x4, "transform_4x4")
108
+ if pts.shape[0] == 0:
109
+ return pts
110
+ hom = np.concatenate([pts, np.ones((pts.shape[0], 1), dtype=np.float64)], axis=1)
111
+ out = (hom @ t.T)[:, :3]
112
+ return np.asarray(out, dtype=np.float64)
113
+
114
+
115
+ def aligned_estimated_points(request: MapEvaluateRequest) -> np.ndarray:
116
+ est = _require_xyz(request.estimated_points, "estimated_points")
117
+ mode = (request.align_mode or "none").strip().lower()
118
+ if mode == "none":
119
+ return est
120
+ if mode == "initial":
121
+ if request.initial_transform_4x4 is None:
122
+ raise ValueError("align_mode='initial' requires initial_transform_4x4.")
123
+ return apply_transform(est, request.initial_transform_4x4)
124
+ raise ValueError(f"Unsupported align_mode: {request.align_mode!r}. Use 'none' or 'initial'.")
125
+
126
+
127
+ def ensure_artifact_dir(request: MapEvaluateRequest, subdir: str) -> Path | None:
128
+ """Return an ensured artifact directory path or None if disabled."""
129
+ if request.artifact_dir is None:
130
+ return None
131
+ root = Path(request.artifact_dir)
132
+ out = root / subdir
133
+ out.mkdir(parents=True, exist_ok=True)
134
+ return out
135
+
136
+
137
+ def voxel_downsample(points: np.ndarray, voxel_size: float) -> np.ndarray:
138
+ """Deterministic voxel downsample by centroid per voxel."""
139
+ pts = _require_xyz(points, "points")
140
+ if pts.shape[0] == 0 or voxel_size <= 0:
141
+ return pts
142
+ keys = np.floor(pts / voxel_size).astype(np.int64)
143
+ order = np.lexsort((keys[:, 2], keys[:, 1], keys[:, 0]))
144
+ keys_sorted = keys[order]
145
+ pts_sorted = pts[order]
146
+ unique, start_idx = np.unique(keys_sorted, axis=0, return_index=True)
147
+ centroids: list[np.ndarray] = []
148
+ for i, s in enumerate(start_idx):
149
+ e = start_idx[i + 1] if i + 1 < len(start_idx) else pts_sorted.shape[0]
150
+ centroids.append(pts_sorted[s:e].mean(axis=0))
151
+ out = np.vstack(centroids) if centroids else np.zeros((0, 3), dtype=np.float64)
152
+ _ = unique # kept to mirror the index ordering for documentation
153
+ return np.asarray(out)
154
+
155
+
156
+ # ------------------------------------------------------- adopted strategy
157
+
158
+
159
+ def _min_distances_kdtree(a: np.ndarray, b: np.ndarray) -> np.ndarray:
160
+ """Per-point min Euclidean distance from ``a`` to ``b`` via scipy cKDTree."""
161
+ if a.shape[0] == 0:
162
+ return np.zeros((0,), dtype=np.float64)
163
+ if b.shape[0] == 0:
164
+ return np.full((a.shape[0],), np.inf, dtype=np.float64)
165
+
166
+ from scipy.spatial import cKDTree
167
+
168
+ tree = cKDTree(np.asarray(b, dtype=np.float64))
169
+ distances, _ = tree.query(np.asarray(a, dtype=np.float64), k=1)
170
+ return np.asarray(distances, dtype=np.float64)
171
+
172
+
173
+ def _error_colors(dist_m: np.ndarray, vmax_m: float) -> np.ndarray:
174
+ """Green→red ramp for distance visualization."""
175
+ vmax = float(max(vmax_m, 1e-12))
176
+ x = np.clip(dist_m / vmax, 0.0, 1.0)
177
+ r = x
178
+ g = 1.0 - x
179
+ b = np.zeros_like(x)
180
+ return np.column_stack([r, g, b]).astype(np.float64)
181
+
182
+
183
+ def _write_colored_ply(points: np.ndarray, colors: np.ndarray, path: str) -> None:
184
+ import open3d as o3d
185
+
186
+ pcd = o3d.geometry.PointCloud()
187
+ pcd.points = o3d.utility.Vector3dVector(np.asarray(points, dtype=np.float64))
188
+ pcd.colors = o3d.utility.Vector3dVector(np.asarray(colors, dtype=np.float64))
189
+ o3d.io.write_point_cloud(path, pcd, write_ascii=False)
190
+
191
+
192
+ @dataclass(slots=True)
193
+ class NNThresholdMapEvaluateStrategy:
194
+ """MapEval-style accuracy@τ / completeness@τ via per-point NN distances.
195
+
196
+ Promoted from ``ca/experiments/map_evaluate/nn_thresholds.py`` as the
197
+ adopted reference-based map evaluation strategy. The reference-free
198
+ ``voxel_entropy`` lane stays under ``ca.experiments`` because the two
199
+ strategies do not compete on the same metric family.
200
+ """
201
+
202
+ name: str = "nn_thresholds"
203
+ design: str = "functional"
204
+
205
+ def evaluate(self, request: MapEvaluateRequest) -> MapEvaluateResult:
206
+ est_aligned = aligned_estimated_points(request)
207
+ est = voxel_downsample(_require_xyz(est_aligned, "estimated_points"), request.downsample_voxel_size)
208
+ if request.reference_points is None:
209
+ raise ValueError("nn_thresholds requires reference_points (GT).")
210
+ ref = voxel_downsample(_require_xyz(request.reference_points, "reference_points"), request.downsample_voxel_size)
211
+
212
+ est_to_ref = _min_distances_kdtree(est, ref)
213
+ ref_to_est = _min_distances_kdtree(ref, est)
214
+
215
+ thresholds = tuple(float(x) for x in request.thresholds_m)
216
+ metrics: dict[str, float] = {
217
+ "n_est": float(est.shape[0]),
218
+ "n_ref": float(ref.shape[0]),
219
+ "mean_est_to_ref_m": float(np.mean(est_to_ref)) if est_to_ref.size else float("inf"),
220
+ "mean_ref_to_est_m": float(np.mean(ref_to_est)) if ref_to_est.size else float("inf"),
221
+ "chamfer_m": (
222
+ float(np.mean(est_to_ref)) + float(np.mean(ref_to_est))
223
+ if (est_to_ref.size and ref_to_est.size)
224
+ else float("inf")
225
+ ),
226
+ }
227
+
228
+ for t in thresholds:
229
+ metrics[f"accuracy@{t:.3f}m"] = float(np.mean(est_to_ref <= t)) if est_to_ref.size else 0.0
230
+ metrics[f"completeness@{t:.3f}m"] = float(np.mean(ref_to_est <= t)) if ref_to_est.size else 0.0
231
+
232
+ # One scalar summary (F-score) using the first threshold.
233
+ t0 = thresholds[0] if thresholds else 0.2
234
+ acc = metrics.get(f"accuracy@{t0:.3f}m", 0.0)
235
+ com = metrics.get(f"completeness@{t0:.3f}m", 0.0)
236
+ fscore = (2 * acc * com / (acc + com)) if (acc + com) > 0 else 0.0
237
+ metrics[f"fscore@{t0:.3f}m"] = float(fscore)
238
+
239
+ artifacts: dict[str, object] = {
240
+ "thresholds_m": thresholds,
241
+ "downsample_voxel_size": float(request.downsample_voxel_size),
242
+ "align_mode": request.align_mode,
243
+ }
244
+
245
+ # Optional: MapEval-style raw/inlier error visualization PLYs.
246
+ out_dir = ensure_artifact_dir(request, "map_evaluate/nn_thresholds")
247
+ if out_dir is not None and est.shape[0] > 0:
248
+ vmax = thresholds[0] if thresholds else 0.2
249
+ raw_colors = _error_colors(est_to_ref, vmax_m=vmax)
250
+ raw_path = str(out_dir / "estimated_error_raw.ply")
251
+ _write_colored_ply(est, raw_colors, raw_path)
252
+
253
+ inlier_mask = est_to_ref <= vmax
254
+ inlier_points = est[inlier_mask]
255
+ inlier_colors = raw_colors[inlier_mask]
256
+ inlier_path = str(out_dir / f"estimated_error_inlier_{vmax:.3f}m.ply")
257
+ _write_colored_ply(inlier_points, inlier_colors, inlier_path)
258
+
259
+ artifacts["estimated_error_raw_ply"] = raw_path
260
+ artifacts[f"estimated_error_inlier_{vmax:.3f}m_ply"] = inlier_path
261
+
262
+ downsample_voxel = float(request.downsample_voxel_size)
263
+ mode = "voxelized" if downsample_voxel > 0 else "exact"
264
+ sampling_policy: dict[str, object] = {
265
+ "downsample_voxel_size_m": downsample_voxel,
266
+ "thresholds_m": list(thresholds),
267
+ "align_mode": request.align_mode,
268
+ "nn_backend": "scipy_ckdtree",
269
+ }
270
+
271
+ return MapEvaluateResult(
272
+ strategy=self.name,
273
+ design=self.design,
274
+ metrics=metrics,
275
+ artifacts=artifacts,
276
+ metric_family="reference_based_nn_thresholds",
277
+ reference_required=True,
278
+ mode=mode,
279
+ sampling_policy=sampling_policy,
280
+ )
281
+
282
+
283
+ def evaluate_map(
284
+ request: MapEvaluateRequest,
285
+ strategy: MapEvaluateStrategy | None = None,
286
+ ) -> MapEvaluateResult:
287
+ """Evaluate a map using the stabilized strategy (defaults to NNThreshold)."""
288
+ eval_strategy = strategy or NNThresholdMapEvaluateStrategy()
289
+ return eval_strategy.evaluate(request)
290
+
291
+
292
+ __all__ = [
293
+ "MapEvaluateRequest",
294
+ "MapEvaluateResult",
295
+ "MapEvaluateStrategy",
296
+ "NNThresholdMapEvaluateStrategy",
297
+ "aligned_estimated_points",
298
+ "apply_transform",
299
+ "ensure_artifact_dir",
300
+ "evaluate_map",
301
+ "voxel_downsample",
302
+ ]
@@ -33,7 +33,7 @@ def _per_band_confusion(
33
33
  voxel_size: float,
34
34
  ) -> list[dict]:
35
35
  """Compute confusion matrix per height band using voxel matching."""
36
- from ca.core.ground_evaluate import _voxel_keys
36
+ from ca.core.ground_evaluate import _voxel_intersection_size, _voxel_keys
37
37
 
38
38
  num_bands = len(band_edges) - 1
39
39
  bands: list[dict] = []
@@ -51,10 +51,10 @@ def _per_band_confusion(
51
51
  rg = _voxel_keys(_filter(reference_ground), voxel_size)
52
52
  rn = _voxel_keys(_filter(reference_nonground), voxel_size)
53
53
 
54
- tp = len(eg & rg)
55
- fp = len(eg & rn)
56
- fn = len(en & rg)
57
- tn = len(en & rn)
54
+ tp = _voxel_intersection_size(eg, rg)
55
+ fp = _voxel_intersection_size(eg, rn)
56
+ fn = _voxel_intersection_size(en, rg)
57
+ tn = _voxel_intersection_size(en, rn)
58
58
  metrics = confusion_metrics(tp, fp, fn, tn)
59
59
  bands.append({
60
60
  "band": band_idx,
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from ca.core.ground_evaluate import (
6
6
  GroundEvaluateRequest,
7
7
  GroundEvaluateResult,
8
+ _voxel_intersection_size,
8
9
  _voxel_keys,
9
10
  confusion_metrics,
10
11
  )
@@ -22,10 +23,10 @@ class VoxelConfusionExperimentalStrategy:
22
23
  ref_ground_voxels = _voxel_keys(request.reference_ground, request.voxel_size)
23
24
  ref_nonground_voxels = _voxel_keys(request.reference_nonground, request.voxel_size)
24
25
 
25
- tp = len(est_ground_voxels & ref_ground_voxels)
26
- fp = len(est_ground_voxels & ref_nonground_voxels)
27
- fn = len(est_nonground_voxels & ref_ground_voxels)
28
- tn = len(est_nonground_voxels & ref_nonground_voxels)
26
+ tp = _voxel_intersection_size(est_ground_voxels, ref_ground_voxels)
27
+ fp = _voxel_intersection_size(est_ground_voxels, ref_nonground_voxels)
28
+ fn = _voxel_intersection_size(est_nonground_voxels, ref_ground_voxels)
29
+ tn = _voxel_intersection_size(est_nonground_voxels, ref_nonground_voxels)
29
30
  metrics = confusion_metrics(tp, fp, fn, tn)
30
31
 
31
32
  return GroundEvaluateResult(
@@ -0,0 +1,99 @@
1
+ """Re-exports for backward compatibility after the core promotion.
2
+
3
+ The map_evaluate request/result contract and helpers are now owned by
4
+ ``ca.core.map_evaluate``. The experiment slice keeps this module as a thin
5
+ re-export so the remaining experimental strategies
6
+ (``voxel_entropy.VoxelEntropyMapEvaluateStrategy``) and any external callers
7
+ still work via the old import path.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from ca.core.map_evaluate import (
13
+ MapEvaluateRequest,
14
+ MapEvaluateResult,
15
+ _require_xyz,
16
+ _require_transform_4x4,
17
+ aligned_estimated_points,
18
+ apply_transform,
19
+ ensure_artifact_dir,
20
+ voxel_downsample,
21
+ )
22
+
23
+ import numpy as np
24
+ from dataclasses import dataclass
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class MapEvaluateDatasetCase:
29
+ """Small fixture: human-readable name + the request to score."""
30
+
31
+ name: str
32
+ description: str
33
+ request: MapEvaluateRequest
34
+
35
+
36
+ def build_default_datasets() -> list[MapEvaluateDatasetCase]:
37
+ """Small deterministic point-cloud scenarios for quick strategy comparison."""
38
+ rng = np.random.default_rng(7)
39
+
40
+ ref = rng.uniform([-5, -5, -1], [5, 5, 1], size=(400, 3))
41
+
42
+ est_drift = ref + np.array([0.15, -0.05, 0.0]) + rng.normal(0, 0.02, size=ref.shape)
43
+
44
+ mask = ref[:, 0] > -1.0
45
+ est_incomplete = ref[mask] + rng.normal(0, 0.02, size=(mask.sum(), 3))
46
+
47
+ scan_a = rng.uniform([-6, -6, -1], [6, 6, 1], size=(250, 3))
48
+ scan_b = scan_a + rng.normal(0, 0.03, size=scan_a.shape)
49
+ est_self = np.vstack([scan_a, scan_b])
50
+
51
+ return [
52
+ MapEvaluateDatasetCase(
53
+ name="gt_drift",
54
+ description="Estimated map has small rigid drift relative to reference.",
55
+ request=MapEvaluateRequest(
56
+ estimated_points=est_drift,
57
+ reference_points=ref,
58
+ downsample_voxel_size=0.0,
59
+ thresholds_m=(0.2, 0.1, 0.08, 0.05, 0.01),
60
+ structure_voxel_size=0.5,
61
+ ),
62
+ ),
63
+ MapEvaluateDatasetCase(
64
+ name="gt_incomplete",
65
+ description="Estimated map misses a region; completeness should drop.",
66
+ request=MapEvaluateRequest(
67
+ estimated_points=est_incomplete,
68
+ reference_points=ref,
69
+ downsample_voxel_size=0.0,
70
+ thresholds_m=(0.2, 0.1, 0.08, 0.05, 0.01),
71
+ structure_voxel_size=0.5,
72
+ ),
73
+ ),
74
+ MapEvaluateDatasetCase(
75
+ name="no_gt_self_consistency",
76
+ description="No GT; fused map from two noisy overlapping scans.",
77
+ request=MapEvaluateRequest(
78
+ estimated_points=est_self,
79
+ reference_points=None,
80
+ downsample_voxel_size=0.0,
81
+ thresholds_m=(0.2, 0.1, 0.08, 0.05, 0.01),
82
+ structure_voxel_size=0.5,
83
+ ),
84
+ ),
85
+ ]
86
+
87
+
88
+ __all__ = [
89
+ "MapEvaluateDatasetCase",
90
+ "MapEvaluateRequest",
91
+ "MapEvaluateResult",
92
+ "_require_xyz",
93
+ "_require_transform_4x4",
94
+ "aligned_estimated_points",
95
+ "apply_transform",
96
+ "build_default_datasets",
97
+ "ensure_artifact_dir",
98
+ "voxel_downsample",
99
+ ]
@@ -79,9 +79,14 @@ def run_map_evaluate_experiment(
79
79
  ],
80
80
  "results": rows,
81
81
  "decision": {
82
- "selected_experiment": None,
83
- "stabilized_core_strategy": None,
84
- "reason": "Not yet promoted to core; keep iterating on metrics and IO formats.",
82
+ "selected_experiment": "nn_thresholds",
83
+ "stabilized_core_strategy": "nn_thresholds",
84
+ "reason": (
85
+ "Promoted to ca.core.map_evaluate as the reference-based "
86
+ "(GT-aware) MapEval-style nearest-neighbor threshold strategy. "
87
+ "voxel_entropy remains experimental as the orthogonal "
88
+ "reference-free lane until a single GT-free metric is settled."
89
+ ),
85
90
  },
86
91
  }
87
92
 
@@ -136,16 +141,18 @@ def render_decision_section(report: dict) -> str:
136
141
  "",
137
142
  "### Adopted",
138
143
  "",
139
- "No strategy is adopted yet (still experimental).",
144
+ "- `nn_thresholds` (reference-based, GT-aware MapEval-style accuracy/completeness@τ).",
145
+ " Promoted to `ca/core/map_evaluate.py` as `NNThresholdMapEvaluateStrategy`.",
140
146
  "",
141
147
  "### Not Adopted",
142
148
  "",
143
- "- All strategies remain experimental until we settle IO formats and performance constraints.",
149
+ "- `voxel_entropy` (reference-free self-consistency proxy) stays under `ca/experiments`",
150
+ " as the orthogonal GT-free lane until a single reference-free metric is settled.",
144
151
  "",
145
- "### Trigger To Promote",
152
+ "### Trigger To Promote `voxel_entropy`",
146
153
  "",
147
- "- A stable request/result contract is needed by the CLI or library callers.",
148
- "- We have at least two strategies with clear trade-offs and representative datasets.",
154
+ "- Pick one GT-free metric (entropy / structure / MME) as the canonical lane.",
155
+ "- Define a stable failure-mode contract (when does a GT-free score block CI?).",
149
156
  ]
150
157
  )
151
158
 
@@ -155,10 +162,11 @@ def render_interface_section(report: dict) -> str:
155
162
  [
156
163
  "## map_evaluate",
157
164
  "",
158
- "### Current Minimal Interface (experimental)",
165
+ "### Current Minimal Interface",
159
166
  "",
160
- "Not promoted to `ca/core` yet. Current request/result shapes live in "
161
- "`cloudanalyzer/ca/experiments/map_evaluate/common.py`.",
167
+ "Promoted to `ca/core/map_evaluate.py`. The request/result contract,",
168
+ "shared helpers, and adopted `NNThresholdMapEvaluateStrategy` all live there.",
169
+ "`ca/experiments/map_evaluate/{common,nn_thresholds}.py` re-export from core for back-compat.",
162
170
  "",
163
171
  "Result objects carry classification fields so reference-based and reference-free metrics stay in separate lanes:",
164
172
  "",
@@ -0,0 +1,16 @@
1
+ """Backward-compatible re-export of the now-promoted nn_thresholds strategy.
2
+
3
+ The MapEval-style nearest-neighbor threshold strategy has been promoted to
4
+ ``ca.core.map_evaluate`` as ``NNThresholdMapEvaluateStrategy``. This module
5
+ stays as a thin re-export so any existing imports (and the experimental
6
+ ``map_evaluate`` registry) keep working.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from ca.core.map_evaluate import (
12
+ NNThresholdMapEvaluateStrategy,
13
+ _min_distances_kdtree,
14
+ )
15
+
16
+ __all__ = ["NNThresholdMapEvaluateStrategy", "_min_distances_kdtree"]