cloudanalyzer 0.3.0__tar.gz → 0.4.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 (205) hide show
  1. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/PKG-INFO +5 -1
  2. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/__init__.py +1 -1
  3. cloudanalyzer-0.4.0/ca/core/slam_run.py +329 -0
  4. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/process_docs.py +10 -0
  5. cloudanalyzer-0.4.0/ca/experiments/slam_run/__init__.py +46 -0
  6. cloudanalyzer-0.4.0/ca/experiments/slam_run/common.py +244 -0
  7. cloudanalyzer-0.4.0/ca/experiments/slam_run/evaluate.py +242 -0
  8. cloudanalyzer-0.4.0/ca/experiments/slam_run/identity_passthrough.py +90 -0
  9. cloudanalyzer-0.4.0/ca/experiments/slam_run/kiss_icp_driver.py +126 -0
  10. cloudanalyzer-0.4.0/ca/experiments/slam_run/kiss_slam_driver.py +175 -0
  11. cloudanalyzer-0.4.0/ca/experiments/slam_run/small_gicp_driver.py +179 -0
  12. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/PKG-INFO +5 -1
  13. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/SOURCES.txt +9 -0
  14. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/requires.txt +5 -0
  15. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer_cli/main.py +210 -1
  16. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/pyproject.toml +5 -0
  17. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_process_docs.py +3 -0
  18. cloudanalyzer-0.4.0/tests/test_slam_run.py +638 -0
  19. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/LICENSE +0 -0
  20. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/README.md +0 -0
  21. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/align.py +0 -0
  22. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/baseline_history.py +0 -0
  23. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/batch.py +0 -0
  24. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/benchmark.py +0 -0
  25. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/bundle.py +0 -0
  26. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/compare.py +0 -0
  27. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/convert.py +0 -0
  28. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/__init__.py +0 -0
  29. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/check_baseline_evolution.py +0 -0
  30. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/check_scaffolding.py +0 -0
  31. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/check_triage.py +0 -0
  32. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/checks.py +0 -0
  33. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/ground_evaluate.py +0 -0
  34. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/map_evaluate.py +0 -0
  35. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/web_progressive_loading.py +0 -0
  36. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/web_sampling.py +0 -0
  37. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/core/web_trajectory_sampling.py +0 -0
  38. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/crop.py +0 -0
  39. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/density_map.py +0 -0
  40. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/detection.py +0 -0
  41. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/diff.py +0 -0
  42. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/downsample.py +0 -0
  43. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/evaluate.py +0 -0
  44. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/__init__.py +0 -0
  45. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/__init__.py +0 -0
  46. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/common.py +0 -0
  47. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/evaluate.py +0 -0
  48. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/pareto_promote.py +0 -0
  49. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/stability_window.py +0 -0
  50. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_baseline_evolution/threshold_guard.py +0 -0
  51. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/__init__.py +0 -0
  52. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/common.py +0 -0
  53. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/evaluate.py +0 -0
  54. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/literal_profiles.py +0 -0
  55. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/object_sections.py +0 -0
  56. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_scaffolding/pipeline_overlays.py +0 -0
  57. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/__init__.py +0 -0
  58. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/common.py +0 -0
  59. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/evaluate.py +0 -0
  60. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/pareto_frontier.py +0 -0
  61. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/severity_weighted.py +0 -0
  62. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/check_triage/signature_cluster.py +0 -0
  63. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/__init__.py +0 -0
  64. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/common.py +0 -0
  65. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/evaluate.py +0 -0
  66. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/height_band.py +0 -0
  67. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/nearest_neighbor.py +0 -0
  68. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/ground_evaluate/voxel_confusion.py +0 -0
  69. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/__init__.py +0 -0
  70. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/common.py +0 -0
  71. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/evaluate.py +0 -0
  72. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/nn_thresholds.py +0 -0
  73. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/map_evaluate/voxel_entropy.py +0 -0
  74. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/__init__.py +0 -0
  75. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/common.py +0 -0
  76. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/distance_shells.py +0 -0
  77. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/evaluate.py +0 -0
  78. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/grid_tiles.py +0 -0
  79. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_progressive_loading/spatial_shuffle.py +0 -0
  80. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/__init__.py +0 -0
  81. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/common.py +0 -0
  82. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/evaluate.py +0 -0
  83. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/functional_voxel.py +0 -0
  84. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/object_random.py +0 -0
  85. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_sampling/pipeline_hybrid.py +0 -0
  86. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/__init__.py +0 -0
  87. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/common.py +0 -0
  88. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/distance_accumulator.py +0 -0
  89. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/evaluate.py +0 -0
  90. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/turn_aware.py +0 -0
  91. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/experiments/web_trajectory_sampling/uniform_stride.py +0 -0
  92. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/filter.py +0 -0
  93. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/geometry.py +0 -0
  94. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/ground_evaluate.py +0 -0
  95. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/history.py +0 -0
  96. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/info.py +0 -0
  97. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/io.py +0 -0
  98. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/kitti.py +0 -0
  99. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/log.py +0 -0
  100. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/loop_closure_report.py +0 -0
  101. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/merge.py +0 -0
  102. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/metrics.py +0 -0
  103. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/mme.py +0 -0
  104. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/normals.py +0 -0
  105. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/object_eval.py +0 -0
  106. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/pareto.py +0 -0
  107. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/pipeline.py +0 -0
  108. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/plot.py +0 -0
  109. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/point_summary.py +0 -0
  110. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/posegraph.py +0 -0
  111. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/pr_comment.py +0 -0
  112. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/registration.py +0 -0
  113. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/report.py +0 -0
  114. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/run_evaluate.py +0 -0
  115. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/sample.py +0 -0
  116. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/scan_match_debug.py +0 -0
  117. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/slam_debug.py +0 -0
  118. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/split.py +0 -0
  119. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/stats.py +0 -0
  120. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/tracking.py +0 -0
  121. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/trajectory.py +0 -0
  122. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/view.py +0 -0
  123. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/visualization.py +0 -0
  124. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/ca/web.py +0 -0
  125. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/dependency_links.txt +0 -0
  126. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/entry_points.txt +0 -0
  127. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer.egg-info/top_level.txt +0 -0
  128. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/cloudanalyzer_cli/__init__.py +0 -0
  129. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/setup.cfg +0 -0
  130. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/setup.py +0 -0
  131. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_align.py +0 -0
  132. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_baseline_history.py +0 -0
  133. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_batch.py +0 -0
  134. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_benchmark.py +0 -0
  135. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_bundle.py +0 -0
  136. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_check_baseline_evolution_process.py +0 -0
  137. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_check_scaffolding_process.py +0 -0
  138. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_check_suite.py +0 -0
  139. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_check_triage_process.py +0 -0
  140. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_cli.py +0 -0
  141. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_compare.py +0 -0
  142. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_convert.py +0 -0
  143. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_crop.py +0 -0
  144. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_density_map.py +0 -0
  145. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_detection_evaluate.py +0 -0
  146. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_detection_tracking_report.py +0 -0
  147. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_diff.py +0 -0
  148. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_downsample.py +0 -0
  149. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_evaluate.py +0 -0
  150. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_filter.py +0 -0
  151. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_geometry.py +0 -0
  152. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_geometry_splat_ellipsoid.py +0 -0
  153. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_ground_evaluate.py +0 -0
  154. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_ground_evaluate_process.py +0 -0
  155. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_history.py +0 -0
  156. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_info.py +0 -0
  157. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_io.py +0 -0
  158. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_kitti.py +0 -0
  159. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_logging.py +0 -0
  160. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_loop_closure_report.py +0 -0
  161. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_map_evaluate.py +0 -0
  162. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_map_evaluate_core.py +0 -0
  163. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_merge.py +0 -0
  164. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_metrics.py +0 -0
  165. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_mme.py +0 -0
  166. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_normals.py +0 -0
  167. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_object_eval_iou.py +0 -0
  168. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_output_json.py +0 -0
  169. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_pareto.py +0 -0
  170. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_phase16_large_scale.py +0 -0
  171. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_phase17_vectorize.py +0 -0
  172. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_phase18_vectorize.py +0 -0
  173. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_pipeline.py +0 -0
  174. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_plot.py +0 -0
  175. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_posegraph.py +0 -0
  176. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_pr_comment.py +0 -0
  177. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_public_benchmark_pack.py +0 -0
  178. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_public_object_eval_examples.py +0 -0
  179. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_registration.py +0 -0
  180. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_report.py +0 -0
  181. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_run_batch.py +0 -0
  182. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_run_evaluate.py +0 -0
  183. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_sample.py +0 -0
  184. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_scan_match_debug.py +0 -0
  185. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_slam_debug.py +0 -0
  186. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_split.py +0 -0
  187. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_stats.py +0 -0
  188. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_threshold.py +0 -0
  189. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_tracking_evaluate.py +0 -0
  190. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_traj_batch.py +0 -0
  191. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_trajectory.py +0 -0
  192. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_visualization.py +0 -0
  193. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web.py +0 -0
  194. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_progressive_loading_core.py +0 -0
  195. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_progressive_loading_evaluate.py +0 -0
  196. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_progressive_loading_process.py +0 -0
  197. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_progressive_loading_strategies.py +0 -0
  198. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_sampling_core.py +0 -0
  199. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_sampling_evaluate.py +0 -0
  200. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_sampling_process.py +0 -0
  201. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_sampling_strategies.py +0 -0
  202. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_trajectory_sampling_core.py +0 -0
  203. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_trajectory_sampling_evaluate.py +0 -0
  204. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.0}/tests/test_web_trajectory_sampling_process.py +0 -0
  205. {cloudanalyzer-0.3.0 → cloudanalyzer-0.4.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.3.0
3
+ Version: 0.4.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
@@ -36,6 +36,10 @@ Requires-Dist: mypy; extra == "dev"
36
36
  Requires-Dist: pkginfo>=1.12.0; extra == "dev"
37
37
  Requires-Dist: pytest; extra == "dev"
38
38
  Requires-Dist: twine>=6.0.0; extra == "dev"
39
+ Provides-Extra: slam
40
+ Requires-Dist: kiss-icp>=1.2.0; extra == "slam"
41
+ Requires-Dist: kiss-slam>=0.0.2; extra == "slam"
42
+ Requires-Dist: small_gicp>=1.0.0; extra == "slam"
39
43
  Dynamic: license-file
40
44
 
41
45
  # CloudAnalyzer
@@ -1,3 +1,3 @@
1
1
  """CloudAnalyzer - AI-friendly CLI tool for point cloud analysis."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.0"
@@ -0,0 +1,329 @@
1
+ """Stable contract for ``ca slam-run``: drive a LiDAR-odometry pipeline
2
+ end-to-end on a sequence of input scans and produce the artifacts the rest of
3
+ CloudAnalyzer's evaluation stack expects (estimated map PLY + TUM trajectory).
4
+
5
+ The slice has two implementations under ``ca.experiments.slam_run``:
6
+
7
+ - ``KissICPSlamDriver`` (adopted, also re-exported here): wraps the
8
+ ``kiss-icp`` package — a small, well-known scan-to-map LiDAR-odometry
9
+ registration pipeline. Picks up KITTI / Newer College sequences with
10
+ ``deskew`` enabled.
11
+ - ``IdentityPassthroughSlamDriver``: a zero-motion sentinel that emits identity
12
+ poses and concatenates input frames as the "map". It is intentionally
13
+ bad — its only job is to prove the harness is wired and to provide a floor
14
+ for the slice's evaluator.
15
+
16
+ The contract:
17
+
18
+ - :class:`SlamRunRequest` carries the inputs you actually have (frame paths,
19
+ optional per-frame timestamps, driver knobs).
20
+ - :class:`SlamRunResult` carries everything needed to feed the existing
21
+ ``ca run-evaluate`` pipeline plus enough metadata for ``ca check`` /
22
+ ``ca report-pr-comment``.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from dataclasses import dataclass, field
28
+ from pathlib import Path
29
+ from typing import Any, Protocol, runtime_checkable
30
+
31
+ import numpy as np
32
+
33
+
34
+ @dataclass(slots=True)
35
+ class SlamRunRequest:
36
+ """Inputs to a single SLAM driver run."""
37
+
38
+ frame_paths: tuple[Path, ...]
39
+ """Per-frame scan paths in temporal order (``.pcd`` / ``.ply`` / ``.bin``)."""
40
+
41
+ timestamps_s: tuple[float, ...] | None = None
42
+ """Optional per-frame timestamps in seconds (length must equal frame_paths).
43
+ Used for the TUM trajectory output and for drivers that consume timing.
44
+ If omitted, drivers fall back to ``index * frame_period_s``."""
45
+
46
+ frame_period_s: float = 0.1
47
+ """Fallback timestamp spacing used when ``timestamps_s`` is None."""
48
+
49
+ max_range_m: float | None = None
50
+ """Drop scan points farther than this from the sensor before registration.
51
+ ``None`` keeps every point."""
52
+
53
+ voxel_size_m: float | None = None
54
+ """Driver-side voxel grid size for the local map. ``None`` uses the driver's
55
+ own default."""
56
+
57
+ deskew: bool = True
58
+ """Whether the driver should motion-deskew input frames (KISS-ICP default)."""
59
+
60
+ max_frames: int | None = None
61
+ """Optional cap on the number of frames consumed. ``None`` runs everything."""
62
+
63
+
64
+ @dataclass(slots=True)
65
+ class SlamRunResult:
66
+ """Outputs of a single SLAM driver run."""
67
+
68
+ driver: str
69
+ """Identifier of the driver that produced this result."""
70
+
71
+ poses: np.ndarray
72
+ """``(N, 4, 4)`` array of sensor-to-world poses, one per processed frame."""
73
+
74
+ timestamps_s: np.ndarray
75
+ """``(N,)`` array of pose timestamps in seconds."""
76
+
77
+ map_points: np.ndarray
78
+ """``(M, 3)`` accumulated map point cloud in the world frame."""
79
+
80
+ runtime_s: float
81
+ """Total wall-clock seconds spent inside the driver (frame iteration only)."""
82
+
83
+ frames_processed: int
84
+ """How many frames the driver consumed before producing the result."""
85
+
86
+ metadata: dict[str, Any] = field(default_factory=dict)
87
+ """Driver-specific knobs / config snapshot. Echoed into the summary JSON
88
+ so a downstream baseline can prove two runs used the same settings."""
89
+
90
+
91
+ @runtime_checkable
92
+ class SlamRunDriver(Protocol):
93
+ """A SLAM driver consumes a request and returns the artifacts contract."""
94
+
95
+ name: str
96
+
97
+ def run(self, request: SlamRunRequest) -> SlamRunResult: # pragma: no cover - Protocol
98
+ ...
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # Shared helpers used by every driver (loading frames, writing artifacts).
103
+ # ---------------------------------------------------------------------------
104
+
105
+
106
+ def load_frame(path: Path) -> np.ndarray:
107
+ """Load one LiDAR scan as an ``(N, 3)`` float64 ndarray.
108
+
109
+ Supports:
110
+
111
+ - ``.bin``: KITTI Velodyne format (float32 quadruples, XYZI; intensity dropped).
112
+ - ``.pcd`` / ``.ply``: read through Open3D.
113
+ - ``.csv``: x,y,z columns (optionally with a header), parsed by ``ca.io``.
114
+
115
+ Empty scans are returned as ``(0, 3)`` so drivers can skip them without
116
+ raising.
117
+ """
118
+
119
+ suffix = path.suffix.lower()
120
+ if suffix == ".bin":
121
+ raw = np.fromfile(str(path), dtype=np.float32)
122
+ if raw.size == 0:
123
+ return np.empty((0, 3), dtype=np.float64)
124
+ if raw.size % 4 != 0:
125
+ raise ValueError(
126
+ f"KITTI .bin frame {path} has size {raw.size} not divisible by 4"
127
+ )
128
+ return raw.reshape(-1, 4)[:, :3].astype(np.float64, copy=False)
129
+ if suffix in {".pcd", ".ply", ".csv"}:
130
+ from ca.io import load_point_cloud
131
+
132
+ pcd = load_point_cloud(str(path))
133
+ pts = np.asarray(pcd.points, dtype=np.float64)
134
+ if pts.size == 0:
135
+ return np.empty((0, 3), dtype=np.float64)
136
+ return pts
137
+ raise ValueError(
138
+ f"Unsupported frame format '{suffix}'. Expected one of .bin .pcd .ply .csv"
139
+ )
140
+
141
+
142
+ def discover_frame_paths(input_path: Path) -> list[Path]:
143
+ """Resolve ``--input`` to an ordered list of frame paths.
144
+
145
+ Accepts either:
146
+
147
+ - A directory: pick up ``*.bin``, ``*.pcd``, ``*.ply`` (in that priority
148
+ order, picking whichever extension dominates) and sort lexicographically.
149
+ KITTI Velodyne dumps use lex-sortable zero-padded filenames so this
150
+ matches frame order.
151
+ - A list file (``*.txt``): one path per line. Relative paths are resolved
152
+ against the list file's parent directory.
153
+ """
154
+
155
+ if input_path.is_file() and input_path.suffix.lower() == ".txt":
156
+ out: list[Path] = []
157
+ base = input_path.parent
158
+ for raw in input_path.read_text(encoding="utf-8").splitlines():
159
+ stripped = raw.strip()
160
+ if not stripped or stripped.startswith("#"):
161
+ continue
162
+ candidate = Path(stripped)
163
+ if not candidate.is_absolute():
164
+ candidate = base / candidate
165
+ out.append(candidate)
166
+ if not out:
167
+ raise ValueError(f"Frame list {input_path} is empty")
168
+ return out
169
+
170
+ if not input_path.is_dir():
171
+ raise ValueError(
172
+ f"--input must be a directory of scans or a .txt list; got {input_path}"
173
+ )
174
+
175
+ for ext in (".bin", ".pcd", ".ply"):
176
+ hits = sorted(input_path.glob(f"*{ext}"))
177
+ if hits:
178
+ return hits
179
+ raise ValueError(
180
+ f"No .bin/.pcd/.ply scans found under {input_path}. "
181
+ "Pass a directory containing LiDAR sweeps or a frames-list.txt."
182
+ )
183
+
184
+
185
+ def write_tum_trajectory(
186
+ path: Path, poses: np.ndarray, timestamps_s: np.ndarray
187
+ ) -> None:
188
+ """Dump poses to TUM format (``timestamp tx ty tz qx qy qz qw``).
189
+
190
+ Matches the format ``ca traj-evaluate`` already consumes.
191
+ """
192
+
193
+ if poses.ndim != 3 or poses.shape[1:] != (4, 4):
194
+ raise ValueError(f"poses must be (N, 4, 4); got {poses.shape}")
195
+ if timestamps_s.shape != (poses.shape[0],):
196
+ raise ValueError(
197
+ f"timestamps_s {timestamps_s.shape} must match poses {poses.shape[0]}"
198
+ )
199
+ path.parent.mkdir(parents=True, exist_ok=True)
200
+ quats = _rotation_matrices_to_quaternions(poses[:, :3, :3])
201
+ with path.open("w", encoding="utf-8") as f:
202
+ f.write("# timestamp tx ty tz qx qy qz qw\n")
203
+ for t, pose, q in zip(timestamps_s, poses, quats):
204
+ tx, ty, tz = pose[:3, 3]
205
+ qx, qy, qz, qw = q
206
+ f.write(
207
+ f"{t:.6f} {tx:.6f} {ty:.6f} {tz:.6f} "
208
+ f"{qx:.6f} {qy:.6f} {qz:.6f} {qw:.6f}\n"
209
+ )
210
+
211
+
212
+ def write_map_ply(path: Path, map_points: np.ndarray) -> None:
213
+ """Save the accumulated SLAM map as an Open3D-readable PLY."""
214
+
215
+ if map_points.ndim != 2 or map_points.shape[1] != 3:
216
+ raise ValueError(f"map_points must be (M, 3); got {map_points.shape}")
217
+ path.parent.mkdir(parents=True, exist_ok=True)
218
+ import open3d as o3d
219
+
220
+ pcd = o3d.geometry.PointCloud()
221
+ pcd.points = o3d.utility.Vector3dVector(
222
+ np.ascontiguousarray(map_points, dtype=np.float64)
223
+ )
224
+ o3d.io.write_point_cloud(str(path), pcd, write_ascii=False)
225
+
226
+
227
+ def _rotation_matrices_to_quaternions(rotmats: np.ndarray) -> np.ndarray:
228
+ """Vectorized rotation-matrix → ``(qx, qy, qz, qw)`` quaternion.
229
+
230
+ Uses the standard Shepperd-style branchless trace selection so it works
231
+ cleanly even when most rotations are near-identity (the common case for
232
+ indoor LiDAR sweeps where motion is small per frame).
233
+ """
234
+
235
+ if rotmats.ndim != 3 or rotmats.shape[1:] != (3, 3):
236
+ raise ValueError(f"rotmats must be (N, 3, 3); got {rotmats.shape}")
237
+
238
+ n = rotmats.shape[0]
239
+ out = np.zeros((n, 4), dtype=np.float64)
240
+
241
+ m00 = rotmats[:, 0, 0]
242
+ m11 = rotmats[:, 1, 1]
243
+ m22 = rotmats[:, 2, 2]
244
+ trace = m00 + m11 + m22
245
+
246
+ # Branch on the largest diagonal entry to avoid numerical blowups.
247
+ case_w = trace > 0.0
248
+ case_x = (~case_w) & (m00 >= m11) & (m00 >= m22)
249
+ case_y = (~case_w) & (~case_x) & (m11 >= m22)
250
+ case_z = (~case_w) & (~case_x) & (~case_y)
251
+
252
+ if np.any(case_w):
253
+ idx = np.where(case_w)[0]
254
+ s = np.sqrt(trace[idx] + 1.0) * 2.0
255
+ qw = 0.25 * s
256
+ qx = (rotmats[idx, 2, 1] - rotmats[idx, 1, 2]) / s
257
+ qy = (rotmats[idx, 0, 2] - rotmats[idx, 2, 0]) / s
258
+ qz = (rotmats[idx, 1, 0] - rotmats[idx, 0, 1]) / s
259
+ out[idx, 0] = qx
260
+ out[idx, 1] = qy
261
+ out[idx, 2] = qz
262
+ out[idx, 3] = qw
263
+
264
+ if np.any(case_x):
265
+ idx = np.where(case_x)[0]
266
+ s = np.sqrt(1.0 + m00[idx] - m11[idx] - m22[idx]) * 2.0
267
+ out[idx, 0] = 0.25 * s
268
+ out[idx, 1] = (rotmats[idx, 0, 1] + rotmats[idx, 1, 0]) / s
269
+ out[idx, 2] = (rotmats[idx, 0, 2] + rotmats[idx, 2, 0]) / s
270
+ out[idx, 3] = (rotmats[idx, 2, 1] - rotmats[idx, 1, 2]) / s
271
+
272
+ if np.any(case_y):
273
+ idx = np.where(case_y)[0]
274
+ s = np.sqrt(1.0 + m11[idx] - m00[idx] - m22[idx]) * 2.0
275
+ out[idx, 0] = (rotmats[idx, 0, 1] + rotmats[idx, 1, 0]) / s
276
+ out[idx, 1] = 0.25 * s
277
+ out[idx, 2] = (rotmats[idx, 1, 2] + rotmats[idx, 2, 1]) / s
278
+ out[idx, 3] = (rotmats[idx, 0, 2] - rotmats[idx, 2, 0]) / s
279
+
280
+ if np.any(case_z):
281
+ idx = np.where(case_z)[0]
282
+ s = np.sqrt(1.0 + m22[idx] - m00[idx] - m11[idx]) * 2.0
283
+ out[idx, 0] = (rotmats[idx, 0, 2] + rotmats[idx, 2, 0]) / s
284
+ out[idx, 1] = (rotmats[idx, 1, 2] + rotmats[idx, 2, 1]) / s
285
+ out[idx, 2] = 0.25 * s
286
+ out[idx, 3] = (rotmats[idx, 1, 0] - rotmats[idx, 0, 1]) / s
287
+
288
+ return out
289
+
290
+
291
+ # ---------------------------------------------------------------------------
292
+ # Adopted driver re-export. Concrete impl lives under ca.experiments.slam_run
293
+ # so the slice's evaluator can still benchmark it against the sentinel; the
294
+ # CLI imports from here so ``ca.core`` stays the only stable surface.
295
+ # ---------------------------------------------------------------------------
296
+
297
+
298
+ def _import_kiss_icp_driver() -> type[SlamRunDriver]:
299
+ from ca.experiments.slam_run.kiss_icp_driver import KissICPSlamDriver
300
+
301
+ return KissICPSlamDriver
302
+
303
+
304
+ def make_default_driver() -> SlamRunDriver:
305
+ """Return the currently adopted SLAM driver instance (KISS-ICP)."""
306
+
307
+ cls = _import_kiss_icp_driver()
308
+ return cls()
309
+
310
+
311
+ def run_slam(request: SlamRunRequest, driver: SlamRunDriver | None = None) -> SlamRunResult:
312
+ """Convenience entry point used by the CLI; delegates to the supplied
313
+ driver or to the adopted default."""
314
+
315
+ drv = driver or make_default_driver()
316
+ return drv.run(request)
317
+
318
+
319
+ __all__ = [
320
+ "SlamRunRequest",
321
+ "SlamRunResult",
322
+ "SlamRunDriver",
323
+ "load_frame",
324
+ "discover_frame_paths",
325
+ "write_tum_trajectory",
326
+ "write_map_ply",
327
+ "make_default_driver",
328
+ "run_slam",
329
+ ]
@@ -54,6 +54,12 @@ from ca.experiments.map_evaluate.evaluate import (
54
54
  render_interface_section as render_map_evaluate_interface_section,
55
55
  run_map_evaluate_experiment,
56
56
  )
57
+ from ca.experiments.slam_run.evaluate import (
58
+ render_decision_section as render_slam_run_decision_section,
59
+ render_experiment_section as render_slam_run_experiment_section,
60
+ render_interface_section as render_slam_run_interface_section,
61
+ run_slam_run_experiment,
62
+ )
57
63
 
58
64
 
59
65
  def build_experiment_reports(repetitions: int = 3) -> list[dict]:
@@ -68,6 +74,7 @@ def build_experiment_reports(repetitions: int = 3) -> list[dict]:
68
74
  run_check_baseline_evolution_experiment(repetitions=repetitions),
69
75
  run_ground_evaluate_experiment(repetitions=repetitions),
70
76
  run_map_evaluate_experiment(repetitions=repetitions),
77
+ run_slam_run_experiment(repetitions=1),
71
78
  ]
72
79
 
73
80
 
@@ -83,6 +90,7 @@ def render_experiments_markdown(reports: list[dict]) -> str:
83
90
  render_check_baseline_evolution_experiment_section(reports[5]),
84
91
  render_ground_evaluate_experiment_section(reports[6]),
85
92
  render_map_evaluate_experiment_section(reports[7]),
93
+ render_slam_run_experiment_section(reports[8]),
86
94
  ]
87
95
  return (
88
96
  "# Experiments\n\n"
@@ -104,6 +112,7 @@ def render_decisions_markdown(reports: list[dict]) -> str:
104
112
  render_check_baseline_evolution_decision_section(reports[5]),
105
113
  render_ground_evaluate_decision_section(reports[6]),
106
114
  render_map_evaluate_decision_section(reports[7]),
115
+ render_slam_run_decision_section(reports[8]),
107
116
  ]
108
117
  return "# Decisions\n\n" + "\n\n".join(sections) + "\n"
109
118
 
@@ -120,6 +129,7 @@ def render_interfaces_markdown(reports: list[dict]) -> str:
120
129
  render_check_baseline_evolution_interface_section(reports[5]),
121
130
  render_ground_evaluate_interface_section(reports[6]),
122
131
  render_map_evaluate_interface_section(reports[7]),
132
+ render_slam_run_interface_section(reports[8]),
123
133
  ]
124
134
  return (
125
135
  "# Interfaces\n\n"
@@ -0,0 +1,46 @@
1
+ """Experimental SLAM drivers compared by the slam_run slice evaluator.
2
+
3
+ Four drivers participate:
4
+
5
+ - :class:`KissICPSlamDriver` — the adopted real driver, wrapping the
6
+ ``kiss-icp`` package. Also re-exported from ``ca.core.slam_run`` so the
7
+ CLI keeps depending only on core. Scan-to-map with constant-velocity
8
+ prediction and adaptive max-correspondence-distance.
9
+ - :class:`KissSLAMSlamDriver` — wraps ``kiss-slam`` (KISS-ICP odometry +
10
+ pose-graph optimization + MapClosures loop closure). Same scan-to-map
11
+ inner loop as KISS-ICP with a PGO pass on top.
12
+ - :class:`SmallGICPSlamDriver` — wraps ``small_gicp`` (PyPI MIT).
13
+ Scan-to-map VGICP using ``small_gicp.GaussianVoxelMap`` as the
14
+ registration target; map output is scan-stitched + voxel-downsampled
15
+ (the voxel map stores quantized centers and can't clear the Chamfer
16
+ threshold against a dense reference on its own).
17
+ - :class:`IdentityPassthroughSlamDriver` — sentinel that returns identity
18
+ poses and concatenates input frames as the "map". It is intentionally
19
+ bad; its only job is to prove the harness runs and to set the floor for
20
+ ``evaluate.py``.
21
+ """
22
+
23
+ from ca.experiments.slam_run.identity_passthrough import IdentityPassthroughSlamDriver
24
+ from ca.experiments.slam_run.kiss_icp_driver import KissICPSlamDriver
25
+ from ca.experiments.slam_run.kiss_slam_driver import KissSLAMSlamDriver
26
+ from ca.experiments.slam_run.small_gicp_driver import SmallGICPSlamDriver
27
+
28
+
29
+ def get_slam_run_drivers() -> list:
30
+ """Return the concrete drivers compared by the slice evaluator."""
31
+
32
+ return [
33
+ KissICPSlamDriver(),
34
+ KissSLAMSlamDriver(),
35
+ SmallGICPSlamDriver(),
36
+ IdentityPassthroughSlamDriver(),
37
+ ]
38
+
39
+
40
+ __all__ = [
41
+ "IdentityPassthroughSlamDriver",
42
+ "KissICPSlamDriver",
43
+ "KissSLAMSlamDriver",
44
+ "SmallGICPSlamDriver",
45
+ "get_slam_run_drivers",
46
+ ]
@@ -0,0 +1,244 @@
1
+ """Shared datasets for slam_run driver experiments.
2
+
3
+ Each dataset case synthesizes a small set of LiDAR-style frames in a temp
4
+ directory, defines the ground-truth sensor poses, and packages them into a
5
+ :class:`SlamRunRequest`. The slice evaluator runs every concrete driver
6
+ against every case and compares against the GT trajectory.
7
+
8
+ The synthetic frames are tiny on purpose — the slice's job is to verify
9
+ that the harness wires through, not to benchmark large maps. Real-scale
10
+ benchmarking happens via ``ca slam-run`` + ``ca run-evaluate`` on the
11
+ KITTI-mini / Newer-College-mini benchmark suites.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import tempfile
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Callable
20
+
21
+ import numpy as np
22
+
23
+ from ca.core.slam_run import SlamRunRequest
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class SlamRunDatasetCase:
28
+ """One comparable SLAM run for the slice's driver bake-off."""
29
+
30
+ name: str
31
+ description: str
32
+ build_request: Callable[[Path], SlamRunRequest]
33
+ """Closure that materializes synthetic frames under the given temp dir
34
+ and returns the request pointing at them."""
35
+
36
+ gt_poses: np.ndarray
37
+ """``(N, 4, 4)`` ground-truth sensor-to-world poses for ATE comparison."""
38
+
39
+ expected_kiss_icp_max_ate_m: float
40
+ """Pass condition for KISS-ICP on this case."""
41
+
42
+ keep_files: tuple[str, ...] = field(default_factory=tuple)
43
+ """Filenames the case promises to leave under the temp dir (helps tests
44
+ assert on driver outputs without re-running the synthesis)."""
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Synthetic frame builders
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ def _box_scene(seed: int, n_points: int = 1500) -> np.ndarray:
53
+ """Build a deterministic synthetic 'room' scene in the world frame.
54
+
55
+ Returns ``(n_points, 3)`` of points lying on the floor and four walls of
56
+ a 20 × 20 m room — enough structure for ICP to actually have something
57
+ to register against.
58
+ """
59
+
60
+ rng = np.random.default_rng(seed)
61
+ # Floor (z = 0)
62
+ floor = rng.uniform([-10, -10, -0.02], [10, 10, 0.02], size=(n_points // 2, 3))
63
+ # Four walls
64
+ rem = n_points - floor.shape[0]
65
+ per_wall = rem // 4
66
+ walls = []
67
+ for axis_pair in [(0, 1), (0, -1), (1, 1), (1, -1)]:
68
+ axis, sign = axis_pair
69
+ fixed = sign * 10.0
70
+ sweeping = rng.uniform(-10, 10, size=(per_wall, 1))
71
+ heights = rng.uniform(0.0, 3.0, size=(per_wall, 1))
72
+ if axis == 0:
73
+ pts = np.hstack(
74
+ [np.full((per_wall, 1), fixed), sweeping, heights]
75
+ )
76
+ else:
77
+ pts = np.hstack(
78
+ [sweeping, np.full((per_wall, 1), fixed), heights]
79
+ )
80
+ pts += rng.normal(0, 0.02, size=pts.shape)
81
+ walls.append(pts)
82
+ return np.vstack([floor, *walls]).astype(np.float64)
83
+
84
+
85
+ def _pose(tx: float, ty: float, tz: float, yaw_rad: float = 0.0) -> np.ndarray:
86
+ """Build a 4×4 SE(3) pose with rotation about z (yaw) only."""
87
+ c, s = np.cos(yaw_rad), np.sin(yaw_rad)
88
+ pose = np.eye(4, dtype=np.float64)
89
+ pose[0, 0] = c
90
+ pose[0, 1] = -s
91
+ pose[1, 0] = s
92
+ pose[1, 1] = c
93
+ pose[0, 3] = tx
94
+ pose[1, 3] = ty
95
+ pose[2, 3] = tz
96
+ return pose
97
+
98
+
99
+ def _write_pcd(path: Path, points: np.ndarray) -> None:
100
+ """Minimal ASCII PCD writer suitable for Open3D's reader."""
101
+ path.parent.mkdir(parents=True, exist_ok=True)
102
+ n = points.shape[0]
103
+ header = (
104
+ "# .PCD v.7 - Point Cloud Data file format\n"
105
+ "VERSION .7\n"
106
+ "FIELDS x y z\n"
107
+ "SIZE 4 4 4\n"
108
+ "TYPE F F F\n"
109
+ "COUNT 1 1 1\n"
110
+ f"WIDTH {n}\n"
111
+ "HEIGHT 1\n"
112
+ "VIEWPOINT 0 0 0 1 0 0 0\n"
113
+ f"POINTS {n}\n"
114
+ "DATA ascii\n"
115
+ )
116
+ with path.open("w", encoding="utf-8") as f:
117
+ f.write(header)
118
+ for x, y, z in points:
119
+ f.write(f"{x:.6f} {y:.6f} {z:.6f}\n")
120
+
121
+
122
+ def _straight_line_dataset() -> SlamRunDatasetCase:
123
+ """6 sensor poses sliding forward along +x by 0.5 m / frame in a static room."""
124
+ n_frames = 6
125
+ scene = _box_scene(seed=11, n_points=1200)
126
+ gt_poses = np.stack(
127
+ [_pose(i * 0.5, 0.0, 0.0, yaw_rad=0.0) for i in range(n_frames)], axis=0
128
+ )
129
+
130
+ def build(tmp: Path) -> SlamRunRequest:
131
+ frame_dir = tmp / "straight"
132
+ frame_dir.mkdir(parents=True, exist_ok=True)
133
+ paths = []
134
+ for i, gt in enumerate(gt_poses):
135
+ sensor_to_world = gt
136
+ world_to_sensor = np.linalg.inv(sensor_to_world)
137
+ ones = np.ones((scene.shape[0], 1))
138
+ world_h = np.hstack([scene, ones])
139
+ sensor_pts = (world_to_sensor @ world_h.T).T[:, :3]
140
+ p = frame_dir / f"frame_{i:04d}.pcd"
141
+ _write_pcd(p, sensor_pts)
142
+ paths.append(p)
143
+ return SlamRunRequest(
144
+ frame_paths=tuple(paths),
145
+ timestamps_s=tuple(float(i) * 0.1 for i in range(n_frames)),
146
+ max_range_m=50.0,
147
+ voxel_size_m=0.5,
148
+ deskew=False,
149
+ max_frames=None,
150
+ )
151
+
152
+ return SlamRunDatasetCase(
153
+ name="straight_line_6frames",
154
+ description=(
155
+ "6 sensor poses sliding +0.5 m/frame along +x in a static 20×20 m room. "
156
+ "Identity-passthrough collapses every pose to origin; KISS-ICP should "
157
+ "recover the slide within a few cm."
158
+ ),
159
+ build_request=build,
160
+ gt_poses=gt_poses,
161
+ expected_kiss_icp_max_ate_m=0.20,
162
+ keep_files=tuple(f"frame_{i:04d}.pcd" for i in range(n_frames)),
163
+ )
164
+
165
+
166
+ def _short_turn_dataset() -> SlamRunDatasetCase:
167
+ """4 poses tracing a quarter-turn: translate + yaw by 22.5° each step."""
168
+ n_frames = 4
169
+ scene = _box_scene(seed=23, n_points=1500)
170
+ gt_poses = np.stack(
171
+ [
172
+ _pose(0.3 * i, 0.1 * i, 0.0, yaw_rad=0.392 * i) # ~22.5° per step
173
+ for i in range(n_frames)
174
+ ],
175
+ axis=0,
176
+ )
177
+
178
+ def build(tmp: Path) -> SlamRunRequest:
179
+ frame_dir = tmp / "turn"
180
+ frame_dir.mkdir(parents=True, exist_ok=True)
181
+ paths = []
182
+ for i, gt in enumerate(gt_poses):
183
+ world_to_sensor = np.linalg.inv(gt)
184
+ ones = np.ones((scene.shape[0], 1))
185
+ world_h = np.hstack([scene, ones])
186
+ sensor_pts = (world_to_sensor @ world_h.T).T[:, :3]
187
+ p = frame_dir / f"frame_{i:04d}.pcd"
188
+ _write_pcd(p, sensor_pts)
189
+ paths.append(p)
190
+ return SlamRunRequest(
191
+ frame_paths=tuple(paths),
192
+ timestamps_s=tuple(float(i) * 0.1 for i in range(n_frames)),
193
+ max_range_m=50.0,
194
+ voxel_size_m=0.5,
195
+ deskew=False,
196
+ max_frames=None,
197
+ )
198
+
199
+ return SlamRunDatasetCase(
200
+ name="short_turn_4frames",
201
+ description=(
202
+ "4 sensor poses tracing a short curve (~22.5° yaw + small translation "
203
+ "per step). Tests that the harness handles non-trivial rotation."
204
+ ),
205
+ build_request=build,
206
+ gt_poses=gt_poses,
207
+ expected_kiss_icp_max_ate_m=0.30,
208
+ keep_files=tuple(f"frame_{i:04d}.pcd" for i in range(n_frames)),
209
+ )
210
+
211
+
212
+ def build_default_datasets() -> list[SlamRunDatasetCase]:
213
+ """Return the deterministic dataset suite the slice evaluator runs."""
214
+
215
+ return [_straight_line_dataset(), _short_turn_dataset()]
216
+
217
+
218
+ def absolute_trajectory_error_m(
219
+ estimated_poses: np.ndarray, gt_poses: np.ndarray
220
+ ) -> float:
221
+ """RMS Euclidean distance between the translation components of two
222
+ pose sequences."""
223
+
224
+ if estimated_poses.shape[0] == 0 or gt_poses.shape[0] == 0:
225
+ return float("inf")
226
+ n = min(estimated_poses.shape[0], gt_poses.shape[0])
227
+ est_t = estimated_poses[:n, :3, 3]
228
+ gt_t = gt_poses[:n, :3, 3]
229
+ diffs = np.linalg.norm(est_t - gt_t, axis=1)
230
+ return float(np.sqrt(np.mean(diffs**2)))
231
+
232
+
233
+ def make_temp_dir() -> tempfile.TemporaryDirectory:
234
+ """Helper used by the evaluator / tests to allocate a scratch dir."""
235
+
236
+ return tempfile.TemporaryDirectory(prefix="ca_slam_run_")
237
+
238
+
239
+ __all__ = [
240
+ "SlamRunDatasetCase",
241
+ "build_default_datasets",
242
+ "absolute_trajectory_error_m",
243
+ "make_temp_dir",
244
+ ]