bioimage-cpp 0.5.0__tar.gz → 0.6.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 (307) hide show
  1. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/MIGRATION_GUIDE.md +92 -0
  2. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/PKG-INFO +1 -1
  3. bioimage_cpp-0.6.0/development/distance/PERFORMANCE_NOTES.md +103 -0
  4. bioimage_cpp-0.6.0/development/distance/_geodesic_reference.py +155 -0
  5. bioimage_cpp-0.6.0/development/distance/benchmark_geodesic.py +243 -0
  6. bioimage_cpp-0.6.0/development/distance/check_geodesic_distance.py +224 -0
  7. bioimage_cpp-0.6.0/examples/distance/complex-shape.tif +0 -0
  8. bioimage_cpp-0.6.0/examples/distance/geodesic_2d.py +21 -0
  9. bioimage_cpp-0.6.0/examples/distance/geodesic_3d.py +20 -0
  10. bioimage_cpp-0.6.0/examples/distance/geodesic_mesh.py +36 -0
  11. bioimage_cpp-0.6.0/examples/distance/shape.py +10 -0
  12. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/grid.hxx +20 -0
  13. bioimage_cpp-0.6.0/include/bioimage_cpp/distance/detail/fast_marching.hxx +264 -0
  14. bioimage_cpp-0.6.0/include/bioimage_cpp/distance/detail/mesh_fast_marching.hxx +246 -0
  15. bioimage_cpp-0.6.0/include/bioimage_cpp/distance/geodesic_mask.hxx +140 -0
  16. bioimage_cpp-0.6.0/include/bioimage_cpp/distance/geodesic_mesh.hxx +121 -0
  17. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/distance.cxx +281 -2
  18. bioimage_cpp-0.6.0/src/bioimage_cpp/_version.py +1 -0
  19. bioimage_cpp-0.6.0/src/bioimage_cpp/distance/__init__.py +25 -0
  20. bioimage_cpp-0.6.0/src/bioimage_cpp/distance/_geodesic.py +310 -0
  21. bioimage_cpp-0.6.0/tests/distance/test_geodesic_distance.py +438 -0
  22. bioimage_cpp-0.5.0/src/bioimage_cpp/_version.py +0 -1
  23. bioimage_cpp-0.5.0/src/bioimage_cpp/distance/__init__.py +0 -13
  24. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/.github/workflows/docs.yml +0 -0
  25. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/.github/workflows/tests.yml +0 -0
  26. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/.github/workflows/wheels.yml +0 -0
  27. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/.gitignore +0 -0
  28. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/AGENTS.md +0 -0
  29. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/CMakeLists.txt +0 -0
  30. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/LICENSE +0 -0
  31. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/README.md +0 -0
  32. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/affinities/check_compute_affinities.py +0 -0
  33. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/affinities/check_compute_embedding_distances.py +0 -0
  34. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/distance/benchmark.py +0 -0
  35. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/distance/check_non_maximum_distance_suppression.py +0 -0
  36. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/filters/PERFORMANCE_NOTES.md +0 -0
  37. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/filters/_bench_utils.py +0 -0
  38. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/filters/benchmark.py +0 -0
  39. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/filters/check_parity.py +0 -0
  40. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/flow/PERFORMANCE_NOTES.md +0 -0
  41. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/flow/_reference_impl.py +0 -0
  42. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/flow/benchmark.py +0 -0
  43. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/flow/check_flow_density.py +0 -0
  44. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/flow/create_test_data.py +0 -0
  45. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/_grid_affinity_compatibility.py +0 -0
  46. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/_rag_compatibility.py +0 -0
  47. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/PERFORMANCE_NOTES.md +0 -0
  48. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/_compatibility.py +0 -0
  49. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/check_edge_weighted.py +0 -0
  50. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/check_gasp.py +0 -0
  51. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/check_mala.py +0 -0
  52. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/check_node_and_edge_weighted.py +0 -0
  53. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/agglomeration/diagnose.py +0 -0
  54. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/benchmark_accumulate_labels.py +0 -0
  55. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/benchmark_bfs.py +0 -0
  56. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/check_grid_affinity_edges.py +0 -0
  57. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/check_grid_affinity_lifted_edges.py +0 -0
  58. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/check_rag_2d.py +0 -0
  59. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/check_rag_3d.py +0 -0
  60. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/PERFORMANCE_NOTES.md +0 -0
  61. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/_compatibility.py +0 -0
  62. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/check_fusion_move.py +0 -0
  63. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/check_greedy_additive.py +0 -0
  64. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/check_kernighan_lin.py +0 -0
  65. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/evaluate_solvers.py +0 -0
  66. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/lifted_multicut/repro_lifted_edges_segfault.py +0 -0
  67. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/multicut/PERFORMANCE_NOTES.md +0 -0
  68. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/multicut/_compatibility.py +0 -0
  69. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_chained.py +0 -0
  70. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_decomposer.py +0 -0
  71. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_fusion_move.py +0 -0
  72. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_greedy_additive.py +0 -0
  73. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_greedy_fixation.py +0 -0
  74. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/multicut/check_kernighan_lin.py +0 -0
  75. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/multicut/evaluate_solvers.py +0 -0
  76. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/mutex_watershed/check_mutex_clustering.py +0 -0
  77. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/graph/mutex_watershed/check_semantic_mutex_clustering.py +0 -0
  78. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/label_multiset/benchmark.py +0 -0
  79. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/local_maxima/benchmark.py +0 -0
  80. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/PERFORMANCE_NOTES.md +0 -0
  81. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/_label_equivalence.py +0 -0
  82. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/_mutex_watershed_equivalence.py +0 -0
  83. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/_semantic_mws_equivalence.py +0 -0
  84. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/_watershed_equivalence.py +0 -0
  85. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/_watershed_from_affinities_equivalence.py +0 -0
  86. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/benchmark_mutex_watershed.py +0 -0
  87. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_label_2d.py +0 -0
  88. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_label_3d.py +0 -0
  89. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_mutex_watershed_2d.py +0 -0
  90. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_mutex_watershed_3d.py +0 -0
  91. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_relabel_sequential.py +0 -0
  92. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_semantic_mutex_watershed_2d.py +0 -0
  93. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_semantic_mutex_watershed_3d.py +0 -0
  94. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_watershed_2d.py +0 -0
  95. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_watershed_3d.py +0 -0
  96. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_watershed_from_affinities_2d.py +0 -0
  97. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/segmentation/check_watershed_from_affinities_3d.py +0 -0
  98. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/transformation/PERFORMANCE_NOTES.md +0 -0
  99. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/transformation/check_affine.py +0 -0
  100. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/utils/_mesh_smoothing_reference.py +0 -0
  101. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/utils/benchmark_mesh_smoothing.py +0 -0
  102. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/development/utils/benchmark_union_find.py +0 -0
  103. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/examples/segmentation/_lifted_problem.py +0 -0
  104. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/examples/segmentation/lifted_multicut_from_affinities.py +0 -0
  105. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/examples/segmentation/multicut_from_affinities.py +0 -0
  106. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/examples/segmentation/mutex_watershed.py +0 -0
  107. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/examples/segmentation/serialize_grid_lifted_problem.py +0 -0
  108. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/examples/segmentation/serialize_lifted_problem.py +0 -0
  109. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/examples/segmentation/watershed.py +0 -0
  110. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/affinities/compute_affinities.hxx +0 -0
  111. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/affinities/compute_embedding_distances.hxx +0 -0
  112. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/array_view.hxx +0 -0
  113. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/blocking.hxx +0 -0
  114. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/edge_hash.hxx +0 -0
  115. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/indexed_heap.hxx +0 -0
  116. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/mutex_storage.hxx +0 -0
  117. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/profile.hxx +0 -0
  118. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/relabel.hxx +0 -0
  119. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/semantic_labels.hxx +0 -0
  120. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/detail/threading.hxx +0 -0
  121. {bioimage_cpp-0.5.0/include/bioimage_cpp → bioimage_cpp-0.6.0/include/bioimage_cpp/distance}/distance_transform.hxx +0 -0
  122. {bioimage_cpp-0.5.0/include/bioimage_cpp → bioimage_cpp-0.6.0/include/bioimage_cpp/distance}/non_maximum_distance_suppression.hxx +0 -0
  123. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/filters/convolve.hxx +0 -0
  124. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/filters/eigenvalues.hxx +0 -0
  125. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/filters/gaussian.hxx +0 -0
  126. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/filters/kernel.hxx +0 -0
  127. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/flow/flow_density.hxx +0 -0
  128. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/agglomerative_clustering.hxx +0 -0
  129. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/cluster_policy_base.hxx +0 -0
  130. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/detail.hxx +0 -0
  131. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/edge_weighted.hxx +0 -0
  132. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/gasp.hxx +0 -0
  133. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/mala.hxx +0 -0
  134. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration/node_and_edge_weighted.hxx +0 -0
  135. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/agglomeration.hxx +0 -0
  136. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/breadth_first_search.hxx +0 -0
  137. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/connected_components.hxx +0 -0
  138. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/detail/fusion_contract.hxx +0 -0
  139. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/edge_weighted_watershed.hxx +0 -0
  140. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/feature_accumulation.hxx +0 -0
  141. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/grid_edge_projection.hxx +0 -0
  142. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/grid_features.hxx +0 -0
  143. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/grid_graph.hxx +0 -0
  144. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/label_accumulation.hxx +0 -0
  145. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_from_affinities.hxx +0 -0
  146. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/detail.hxx +0 -0
  147. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/fusion_move.hxx +0 -0
  148. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/greedy_additive.hxx +0 -0
  149. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/kernighan_lin.hxx +0 -0
  150. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/lifted_from_node_labels.hxx +0 -0
  151. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut/objective.hxx +0 -0
  152. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/lifted_multicut.hxx +0 -0
  153. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/detail.hxx +0 -0
  154. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/fusion_move.hxx +0 -0
  155. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/greedy_additive.hxx +0 -0
  156. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/greedy_fixation.hxx +0 -0
  157. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/kernighan_lin.hxx +0 -0
  158. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut/objective.hxx +0 -0
  159. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/multicut.hxx +0 -0
  160. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/mutex_watershed/clustering.hxx +0 -0
  161. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/mutex_watershed/semantic.hxx +0 -0
  162. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/mutex_watershed.hxx +0 -0
  163. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/node_label_projection.hxx +0 -0
  164. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/proposal_generator.hxx +0 -0
  165. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/proposal_generators/greedy_additive_multicut.hxx +0 -0
  166. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/proposal_generators/watershed.hxx +0 -0
  167. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/rag_coordinates.hxx +0 -0
  168. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/region_adjacency_graph.hxx +0 -0
  169. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/graph/undirected_graph.hxx +0 -0
  170. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/downsample.hxx +0 -0
  171. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/from_labels.hxx +0 -0
  172. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/merger.hxx +0 -0
  173. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/multiset.hxx +0 -0
  174. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/label_multiset/read_subset.hxx +0 -0
  175. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/mesh_smoothing.hxx +0 -0
  176. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/overlap.hxx +0 -0
  177. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/run_length.hxx +0 -0
  178. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/connected_components.hxx +0 -0
  179. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/mutex_watershed.hxx +0 -0
  180. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/relabel_sequential.hxx +0 -0
  181. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/semantic_mutex_watershed.hxx +0 -0
  182. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/segmentation/watershed.hxx +0 -0
  183. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/take_dict.hxx +0 -0
  184. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/transformation/affine.hxx +0 -0
  185. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/transformation/coordinate.hxx +0 -0
  186. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/include/bioimage_cpp/util/union_find.hxx +0 -0
  187. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/pyproject.toml +0 -0
  188. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/affinities.cxx +0 -0
  189. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/affinities.hxx +0 -0
  190. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/blocking.cxx +0 -0
  191. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/blocking.hxx +0 -0
  192. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/distance.hxx +0 -0
  193. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/filters.cxx +0 -0
  194. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/filters.hxx +0 -0
  195. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/flow.cxx +0 -0
  196. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/flow.hxx +0 -0
  197. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/graph.cxx +0 -0
  198. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/graph.hxx +0 -0
  199. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/ground_truth.cxx +0 -0
  200. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/ground_truth.hxx +0 -0
  201. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/label_multiset.cxx +0 -0
  202. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/label_multiset.hxx +0 -0
  203. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/module.cxx +0 -0
  204. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/segmentation.cxx +0 -0
  205. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/segmentation.hxx +0 -0
  206. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/transformation.cxx +0 -0
  207. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/transformation.hxx +0 -0
  208. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/util.cxx +0 -0
  209. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/util.hxx +0 -0
  210. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/utils.cxx +0 -0
  211. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bindings/utils.hxx +0 -0
  212. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/__init__.py +0 -0
  213. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/_data.py +0 -0
  214. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/affinities/__init__.py +0 -0
  215. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/affinities/compute_affinities.py +0 -0
  216. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/affinities/compute_embedding_distances.py +0 -0
  217. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/distance/_distance.py +0 -0
  218. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/filters/__init__.py +0 -0
  219. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/filters/_filters.py +0 -0
  220. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/flow/__init__.py +0 -0
  221. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/flow/_flow.py +0 -0
  222. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/__init__.py +0 -0
  223. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/_external.py +0 -0
  224. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/_shared.py +0 -0
  225. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/agglomeration.py +0 -0
  226. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/features/__init__.py +0 -0
  227. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/lifted_multicut/__init__.py +0 -0
  228. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/multicut/__init__.py +0 -0
  229. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/graph/mutex_watershed.py +0 -0
  230. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/label_multiset/__init__.py +0 -0
  231. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/__init__.py +0 -0
  232. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/label.py +0 -0
  233. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/mutex_watershed.py +0 -0
  234. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/relabel_sequential.py +0 -0
  235. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/segmentation/watershed.py +0 -0
  236. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/transformation/__init__.py +0 -0
  237. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/transformation/_transformation.py +0 -0
  238. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/bioimage_cpp/utils.py +0 -0
  239. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/cpp/segmentation/mutex_watershed.cxx +0 -0
  240. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/cpp/segmentation/semantic_mutex_watershed.cxx +0 -0
  241. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/src/cpp/take_dict.cxx +0 -0
  242. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/affinities/test_compute_affinities.py +0 -0
  243. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/affinities/test_compute_embedding_distances.py +0 -0
  244. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/distance/test_distance_transform.py +0 -0
  245. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/distance/test_non_maximum_distance_suppression.py +0 -0
  246. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/__init__.py +0 -0
  247. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/_helpers.py +0 -0
  248. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/test_edge_weighted.py +0 -0
  249. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/test_gasp.py +0 -0
  250. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/test_mala.py +0 -0
  251. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/agglomeration/test_node_and_edge_weighted.py +0 -0
  252. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/__init__.py +0 -0
  253. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/_helpers.py +0 -0
  254. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/conftest.py +0 -0
  255. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_external_problem.py +0 -0
  256. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_fusion_move.py +0 -0
  257. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_greedy_additive.py +0 -0
  258. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_kernighan_lin.py +0 -0
  259. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_lifted_edges_from_node_labels.py +0 -0
  260. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/lifted_multicut/test_objective.py +0 -0
  261. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/__init__.py +0 -0
  262. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/_helpers.py +0 -0
  263. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/conftest.py +0 -0
  264. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_chain_decomposer.py +0 -0
  265. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_external_problem.py +0 -0
  266. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_fusion_move.py +0 -0
  267. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_greedy_additive.py +0 -0
  268. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_greedy_fixation.py +0 -0
  269. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_kernighan_lin.py +0 -0
  270. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_objective.py +0 -0
  271. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/multicut/test_proposal_generators.py +0 -0
  272. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_breadth_first_search.py +0 -0
  273. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_connected_components.py +0 -0
  274. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_edge_weighted_watershed.py +0 -0
  275. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_grid_affinity_multicut_integration.py +0 -0
  276. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_grid_features.py +0 -0
  277. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_grid_graph.py +0 -0
  278. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_mutex_watershed_graph.py +0 -0
  279. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_node_label_projection.py +0 -0
  280. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_accumulate_labels.py +0 -0
  281. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_coordinates.py +0 -0
  282. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_features.py +0 -0
  283. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_lifted_features.py +0 -0
  284. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_rag_multicut_integration.py +0 -0
  285. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_region_adjacency_graph.py +0 -0
  286. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_semantic_mutex_watershed_graph.py +0 -0
  287. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/graph/test_undirected_graph.py +0 -0
  288. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/label_multiset/__init__.py +0 -0
  289. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/label_multiset/test_against_nifty.py +0 -0
  290. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/label_multiset/test_downsample.py +0 -0
  291. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/label_multiset/test_merger.py +0 -0
  292. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/label_multiset/test_read_subset.py +0 -0
  293. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_label.py +0 -0
  294. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_mutex_watershed.py +0 -0
  295. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_relabel_sequential.py +0 -0
  296. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_semantic_mutex_watershed.py +0 -0
  297. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_watershed.py +0 -0
  298. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/segmentation/test_watershed_from_affinities.py +0 -0
  299. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/test_blocking.py +0 -0
  300. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/test_filters.py +0 -0
  301. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/test_flow.py +0 -0
  302. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/test_ground_truth_overlap.py +0 -0
  303. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/test_transformation.py +0 -0
  304. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/test_util_union_find.py +0 -0
  305. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/test_utils_compute_rle.py +0 -0
  306. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/test_utils_mesh_smoothing.py +0 -0
  307. {bioimage_cpp-0.5.0 → bioimage_cpp-0.6.0}/tests/test_utils_take_dict.py +0 -0
@@ -2188,6 +2188,98 @@ Important differences:
2188
2188
  pairwise distance matrix; threshold the distance map first to keep the
2189
2189
  candidate count modest.
2190
2190
 
2191
+ ## scikit-fmm
2192
+
2193
+ `scikit-fmm` computes geodesic distances on regular grids with the fast
2194
+ marching method. `bioimage-cpp` groups geodesic distances under `bic.distance`
2195
+ with two operations — a **distance field** to a set of sources, and the full
2196
+ **pairwise distance matrix** between a set of points — for two geometry types:
2197
+ regular-grid **masks** (the scikit-fmm equivalent) and triangle **meshes**
2198
+ (which scikit-fmm does not support; the reference for those is the exact MMP
2199
+ algorithm in `pygeodesic`).
2200
+
2201
+ Both are solved with a first-order fast marching method (Godunov/Eikonal on the
2202
+ grid, Kimmel–Sethian on triangles). Masks match scikit-fmm's `order=1` scheme;
2203
+ mesh distances are first-order approximations of the exact surface geodesics.
2204
+
2205
+ scikit-fmm:
2206
+
2207
+ ```python
2208
+ import numpy as np
2209
+ import skfmm
2210
+
2211
+ # Distance from a set of seed voxels, constrained to a mask (domain).
2212
+ phi = np.ones(mask.shape)
2213
+ phi[tuple(sources.T)] = -1 # zero contour marks the seeds
2214
+ phi = np.ma.MaskedArray(phi, ~mask) # obstacles / outside-domain
2215
+ field = np.abs(skfmm.distance(phi, dx=sampling))
2216
+ # Weighted (travel-time) variant:
2217
+ tt = skfmm.travel_time(phi, speed, dx=sampling)
2218
+ ```
2219
+
2220
+ bioimage-cpp:
2221
+
2222
+ ```python
2223
+ import bioimage_cpp as bic
2224
+
2225
+ # masks (regular grid); sources/points are (n, ndim) int64 voxel coordinates
2226
+ field = bic.distance.geodesic_distance_field(mask, sources, sampling=None,
2227
+ speed=None) # -> mask.shape, float64
2228
+ matrix = bic.distance.geodesic_distances(mask, points) # -> (N, N) float64
2229
+
2230
+ # optional per-axis gradient of the field (like vector_difference_transform)
2231
+ field, grad = bic.distance.geodesic_distance_field(mask, sources,
2232
+ return_gradient=True)
2233
+ # grad: (*mask.shape, ndim) float32, d(field)/d(axis), points away from source
2234
+ gradient = bic.distance.geodesic_gradient_field(mask, sources) # just the gradient
2235
+
2236
+ # surfaces (triangle mesh); sources/points are 1-D int64 vertex indices
2237
+ field = bic.distance.geodesic_distance_field_mesh(vertices, faces, sources,
2238
+ speed=None) # -> (n_vertices,) float64
2239
+ matrix = bic.distance.geodesic_distances_mesh(vertices, faces, points) # -> (N, N) float64
2240
+ ```
2241
+
2242
+ Name mapping:
2243
+
2244
+ | scikit-fmm / pygeodesic | bioimage-cpp name |
2245
+ | --- | --- |
2246
+ | `skfmm.distance` / `skfmm.travel_time` (from seed voxels, masked) | `geodesic_distance_field` |
2247
+ | gradient of the distance field (per-axis) | `geodesic_distance_field(..., return_gradient=True)` / `geodesic_gradient_field` |
2248
+ | pairwise via repeated `skfmm.distance` | `geodesic_distances` |
2249
+ | `pygeodesic … geodesicDistances(sources, None)` | `geodesic_distance_field_mesh` |
2250
+ | `pygeodesic … geodesicDistances` (pairwise) | `geodesic_distances_mesh` |
2251
+
2252
+ Important differences:
2253
+
2254
+ - Explicit, geometry-specific functions rather than a level-set encoding: the
2255
+ domain is passed directly as a `mask` (nonzero = inside) or as a
2256
+ `(vertices, faces)` mesh, and the sources are given as coordinates / vertex
2257
+ indices instead of being baked into a signed `phi`.
2258
+ - Outputs are `float64`. Voxels outside the mask and points/vertices
2259
+ unreachable from any source are `+inf`; pairwise matrices are symmetric with
2260
+ a zero diagonal.
2261
+ - `sampling` (per-axis voxel spacing, scalar or per-axis) maps to scikit-fmm's
2262
+ `dx` and applies to masks only — meshes carry real vertex coordinates.
2263
+ - `speed` is optional (`None` = unit-speed geodesic distance). When supplied it
2264
+ generalizes to a weighted travel time, matching `skfmm.travel_time`; for
2265
+ masks it has the mask's shape, for meshes it is per-vertex.
2266
+ - `number_of_threads` follows the `bic.distance` convention (`1` default,
2267
+ `0` = hardware concurrency); the pairwise solves parallelize over sources.
2268
+ - `geodesic_distance_field(..., return_gradient=True)` also returns the per-axis
2269
+ gradient `∇T` of the field (or use `geodesic_gradient_field` for the gradient
2270
+ alone), analogous to `vector_difference_transform`. It is `float32` with shape
2271
+ `(*mask.shape, ndim)` (channel-last, NumPy axis order); component `i` is
2272
+ `d(field)/d(axis_i)`, pointing **away** from the nearest source with
2273
+ `‖∇T‖ ≈ 1/speed`. Negate it to trace back toward the source — e.g. feed
2274
+ `-grad` (transposed to channel-first) to `bic.flow.compute_flow_density`. It
2275
+ is zero at sources, background, and unreachable voxels. Masks only.
2276
+ - Mesh geodesics are surface (2-manifold) distances from the Kimmel–Sethian
2277
+ triangle fast-marching method — a first-order approximation (a few % mean
2278
+ error vs the exact `pygeodesic` MMP algorithm, larger on very obtuse
2279
+ triangles, since obtuse-angle unfolding is not done yet). Like the mask
2280
+ solver it slightly overestimates. See `development/distance/` for the
2281
+ reference oracles and `benchmark_geodesic.py` for timings.
2282
+
2191
2283
  ## I/O and Build Dependencies
2192
2284
 
2193
2285
  `bioimage-cpp` intentionally does not replace nifty or affogato I/O helpers.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: bioimage-cpp
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Dependency-light C++ bioimage analysis algorithms with Python bindings
5
5
  Author: bioimage-cpp contributors
6
6
  License: MIT
@@ -0,0 +1,103 @@
1
+ # Geodesic FMM performance notes
2
+
3
+ Optimization of the two geodesic Fast Marching Method solvers, with the
4
+ before/after measurements that justify the changes. Both changes are pure
5
+ reorganizations of identical arithmetic — the solved fields are **bitwise
6
+ identical** to the pre-optimization solvers on every code path tested.
7
+
8
+ ## What changed
9
+
10
+ - **Lead A — mask/grid solver** (`distance/detail/fast_marching.hxx`): the FMM
11
+ march no longer calls `detail::valid_offset_target` (one integer division +
12
+ modulo *per axis, per call*, ~42 calls / popped voxel in 3D). Each popped voxel
13
+ is decoded **once** with the new `detail::coords_from_index` (divide-and-subtract,
14
+ no modulo); axis neighbours are reached with a single `p ± strides_[d]` add and an
15
+ integer bounds compare, and the neighbour's coordinates are handed to
16
+ `solve_eikonal` so it never re-decodes. The `offsets_` `vector<vector>` is gone.
17
+ - **Lead B — mesh solver** (`distance/detail/mesh_fast_marching.hxx`): when a
18
+ vertex `v` freezes, each incident face relaxes its other corners from *that face
19
+ alone* instead of calling `update_vertex`, which rescanned every incident face of
20
+ every touched vertex. `dist_[w]` is a running minimum that already reflects the
21
+ other faces' contributions (applied when their corners froze), so the single-face
22
+ update reaches the same value. `update_vertex` was removed. Operands are passed to
23
+ `triangle_update` in face-storage order so the acute update is bit-identical, not
24
+ just equal to within rounding.
25
+
26
+ ## Measurement setup
27
+
28
+ 8-core machine, `powersave` governor (frequency-scaling noise present; `perf`
29
+ unavailable, `perf_event_paranoid=4`). Mitigations: 1 warmup call + 20 timed
30
+ repeats, **min** used as the headline estimator (least-interference), spread
31
+ reported. Single-source **field** solve, `--threads 1` — the single-source solve is
32
+ the whole kernel (pairwise = N × field). Baseline and optimized were built
33
+ separately; the **`mesh/field` case is a null control for Lead A** (its code is
34
+ untouched between the baseline and A-only builds) and moved <1%, bounding machine
35
+ drift below the measured mask gains. Lead A's mask numbers also reproduced within
36
+ ~1% across two independent builds (A-only and A+B).
37
+
38
+ Reproduce (from `development/distance/`):
39
+
40
+ ```
41
+ python benchmark_geodesic.py --large --only field --threads 1 --repeats 20 --no-ref --json base.json
42
+ python benchmark_geodesic.py --xlarge --only field --threads 1 --repeats 20 --no-ref --json opt.json
43
+ python /path/to/compare.py base.json opt.json # or diff the JSON directly
44
+ ```
45
+
46
+ ## Wall-clock: baseline → optimized (single-thread field, min of 20)
47
+
48
+ | case | baseline min | optimized min | speedup |
49
+ |---|---|---|---|
50
+ | mask2d/field (1024²) | 294.0 ms | 208.9 ms | **1.41×** |
51
+ | mask2d/field (1536²) | 685.6 ms | 492.7 ms | **1.39×** |
52
+ | mask3d/field (64·128²) | 745.1 ms | 370.2 ms | **2.01×** |
53
+ | mask3d/field (128³) | 1811.6 ms | 912.4 ms | **1.99×** |
54
+ | mesh/field (V=20000) | 23.2 ms | 7.9 ms | **2.94×** |
55
+ | mesh/field (V=40000) | 49.1 ms | 17.5 ms | **2.81×** |
56
+
57
+ Lead A: ~2.0× in 3D (matches the div/mod arithmetic dominating 3 axes), ~1.4× in
58
+ 2D. Lead B: ~2.8–2.9× on the mesh. Both exceed the pre-work predictions (A: 1.5–2×
59
+ 3D; B: ~2×).
60
+
61
+ ## Mechanism confirmation (BIOIMAGE_PROFILE=ON, one field solve)
62
+
63
+ Coarse phases in `solve()`: `pop` (heap pop, untouched by either change) vs `relax`
64
+ (the neighbour/face loop where the removed work lived).
65
+
66
+ | solve | phase | baseline | optimized | change |
67
+ |---|---|---|---|---|
68
+ | mask 128³ | pop | 0.319 s | 0.316 s | unchanged |
69
+ | mask 128³ | relax | 1.700 s | 0.762 s | **2.23× smaller** |
70
+ | mesh 40k | pop | 0.0035 s | 0.0034 s | unchanged |
71
+ | mesh 40k | relax | 0.0500 s | 0.0163 s | **3.07× smaller** |
72
+
73
+ The heap phase is invariant; the targeted `relax` phase shrank by 2.2× (mask) and
74
+ 3.1× (mesh). `pop`'s *share* rose (15.8%→29.3% mask; 6.5%→17.1% mesh) only because
75
+ `relax` shrank around it — the gain came from exactly the phase each change targeted.
76
+
77
+ ## Versus reference (single-thread field, min of 10)
78
+
79
+ | case | bioimage-cpp | reference | speedup |
80
+ |---|---|---|---|
81
+ | mask2d/field (1024²) | 210.5 ms | scikit-fmm 331.8 ms | **1.58×** |
82
+ | mask3d/field (64·128²) | 368.9 ms | scikit-fmm 761.4 ms | **2.06×** |
83
+ | mesh/field (V=20000) | 8.4 ms | pygeodesic 1745 ms | 208× (apples-to-oranges: pygeodesic is *exact* MMP, ours is first-order) |
84
+
85
+ The mask solver went from parity with scikit-fmm's compiled C (pre-work ~1.0–1.1×)
86
+ to a clear lead (1.58× in 2D, 2.06× in 3D).
87
+
88
+ ## Correctness (all green on the shipping build)
89
+
90
+ - `pytest tests/distance/ -q` — 64 passed. The tight external cross-checks ran
91
+ (not skipped): `test_mask_matches_scikit_fmm` (residual < 1e-6),
92
+ `test_mesh_matches_pygeodesic` (mean rel < 0.06, p95 < 0.12).
93
+ - `python check_geodesic_distance.py` — all 6 cases OK, exit 0 (mask residuals
94
+ ~1e-12 vs scikit-fmm; mesh rel mean 0.027).
95
+ - **Bitwise equivalence**: 13 solver outputs (2D/3D field, anisotropic `sampling`,
96
+ `speed`, gradient, mask + mesh pairwise, mesh `speed`) are exactly equal
97
+ (`np.array_equal`) between the baseline and optimized builds. Determinism verified
98
+ (repeat solves identical). This is the dev-time check behind the "bitwise
99
+ identical" claim; the pytest suite guards correctness against the external
100
+ references on every run.
101
+
102
+ The profiler instrumentation (`BIOIMAGE_PROFILE_SCOPE` "pop"/"relax") is left in both
103
+ solvers; it is a no-op unless built with `-C cmake.define.BIOIMAGE_PROFILE=ON`.
@@ -0,0 +1,155 @@
1
+ """Reference geodesic-distance implementations for development cross-checks.
2
+
3
+ These are the oracles that ``bic.distance`` geodesic functions are validated
4
+ against. There is one backend per geometry, because scikit-fmm only supports
5
+ regular Cartesian grids:
6
+
7
+ - **masks** -> scikit-fmm (``skfmm``): fast marching / Eikonal solver.
8
+ - **meshes** -> pygeodesic: exact Mitchell-Mount-Papadimitriou (MMP) geodesics.
9
+
10
+ Not part of the pytest suite; requires ``scikit-fmm`` and/or ``pygeodesic``.
11
+ Importing this module is cheap (numpy only); each function imports its backend
12
+ lazily and raises a clear ``ImportError`` if the backend is missing.
13
+
14
+ Note on the scikit-fmm point-source idiom: to get the distance to a set of
15
+ seed voxels, we set ``phi = +1`` everywhere and ``phi = -1`` at the seeds and
16
+ take ``|skfmm.distance(phi)|``. The zero contour then sits roughly half a cell
17
+ away from each seed, so the reference is offset from a "distance is exactly 0 at
18
+ the seed" convention by ~0.5 * dx near the sources. Compare with a tolerance,
19
+ or exclude the immediate neighbourhood of the seeds, when checking against an
20
+ implementation that initialises the seeds to exactly 0.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import numpy as np
26
+
27
+
28
+ # --------------------------------------------------------------------------- #
29
+ # masks: scikit-fmm
30
+ # --------------------------------------------------------------------------- #
31
+
32
+
33
+ def _skfmm():
34
+ try:
35
+ import skfmm
36
+ except ImportError as error: # pragma: no cover - dev script
37
+ raise ImportError(
38
+ "scikit-fmm is required for the mask geodesic reference "
39
+ "(`pip install scikit-fmm`)"
40
+ ) from error
41
+ return skfmm
42
+
43
+
44
+ def _normalize_dx(sampling, ndim):
45
+ if sampling is None:
46
+ return 1.0
47
+ if np.isscalar(sampling):
48
+ return float(sampling)
49
+ dx = [float(s) for s in sampling]
50
+ if len(dx) != ndim:
51
+ raise ValueError(f"sampling must have length {ndim}, got {len(dx)}")
52
+ return dx
53
+
54
+
55
+ def reference_geodesic_field_mask(mask, sources, sampling=None, speed=None):
56
+ """Geodesic distance field within a mask from a set of source coordinates.
57
+
58
+ Mirrors ``bic.distance.geodesic_distance_field``: for every voxel, the
59
+ shortest-path distance to the nearest source, constrained to the nonzero
60
+ region of ``mask``. Voxels outside the mask (and voxels unreachable from any
61
+ source) are returned as ``+inf``.
62
+ """
63
+ skfmm = _skfmm()
64
+ mask = np.ascontiguousarray(mask) != 0
65
+ sources = np.atleast_2d(np.asarray(sources, dtype=np.int64))
66
+ dx = _normalize_dx(sampling, mask.ndim)
67
+
68
+ phi = np.ones(mask.shape, dtype=np.float64)
69
+ phi[tuple(sources.T)] = -1.0
70
+ phi = np.ma.MaskedArray(phi, mask=~mask)
71
+
72
+ # order=1 matches bioimage-cpp's first-order Godunov scheme (so agreement is
73
+ # limited only by the ~0.5-cell seed offset documented above, not by the
74
+ # stencil order).
75
+ if speed is None:
76
+ field = np.ma.abs(skfmm.distance(phi, dx=dx, order=1))
77
+ else:
78
+ speed = np.ascontiguousarray(speed, dtype=np.float64)
79
+ field = skfmm.travel_time(phi, speed, dx=dx, order=1)
80
+
81
+ out = np.full(mask.shape, np.inf, dtype=np.float64)
82
+ field_data = np.ma.getdata(field)
83
+ valid = ~np.ma.getmaskarray(field)
84
+ out[valid] = field_data[valid]
85
+ return out
86
+
87
+
88
+ def reference_geodesic_distances_mask(mask, points, sampling=None, speed=None):
89
+ """Full pairwise geodesic distance matrix between points within a mask.
90
+
91
+ Runs the field solve once per point and reads the field at the other
92
+ points. The result is symmetrized (per-source solves can differ by a tiny
93
+ numerical amount) with a zero diagonal.
94
+ """
95
+ points = np.atleast_2d(np.asarray(points, dtype=np.int64))
96
+ n = len(points)
97
+ out = np.full((n, n), np.inf, dtype=np.float64)
98
+ index = tuple(points.T)
99
+ for i in range(n):
100
+ field = reference_geodesic_field_mask(mask, points[i][None, :], sampling, speed)
101
+ out[i] = field[index]
102
+ out = 0.5 * (out + out.T)
103
+ np.fill_diagonal(out, 0.0)
104
+ return out
105
+
106
+
107
+ # --------------------------------------------------------------------------- #
108
+ # meshes: pygeodesic (exact MMP)
109
+ # --------------------------------------------------------------------------- #
110
+
111
+
112
+ def _pygeodesic():
113
+ try:
114
+ import pygeodesic.geodesic as geodesic
115
+ except ImportError as error: # pragma: no cover - dev script
116
+ raise ImportError(
117
+ "pygeodesic is required for the mesh geodesic reference "
118
+ "(`pip install pygeodesic`)"
119
+ ) from error
120
+ return geodesic
121
+
122
+
123
+ def _mesh_algorithm(vertices, faces):
124
+ geodesic = _pygeodesic()
125
+ vertices = np.ascontiguousarray(vertices, dtype=np.float64)
126
+ faces = np.ascontiguousarray(faces, dtype=np.int32)
127
+ return geodesic.PyGeodesicAlgorithmExact(vertices, faces)
128
+
129
+
130
+ def reference_geodesic_field_mesh(vertices, faces, sources):
131
+ """Geodesic distance field on a triangle mesh from a set of source vertices.
132
+
133
+ Mirrors ``bic.distance.geodesic_distance_field_mesh``. Returns a
134
+ ``(n_vertices,)`` array of surface geodesic distance to the nearest source.
135
+ """
136
+ algo = _mesh_algorithm(vertices, faces)
137
+ sources = np.atleast_1d(np.asarray(sources, dtype=np.int32))
138
+ distances, _ = algo.geodesicDistances(sources, None)
139
+ return np.asarray(distances, dtype=np.float64)
140
+
141
+
142
+ def reference_geodesic_distances_mesh(vertices, faces, points):
143
+ """Full pairwise geodesic distance matrix between mesh vertices."""
144
+ algo = _mesh_algorithm(vertices, faces)
145
+ points = np.atleast_1d(np.asarray(points, dtype=np.int32))
146
+ n = len(points)
147
+ out = np.full((n, n), np.inf, dtype=np.float64)
148
+ for i in range(n):
149
+ # Read the full single-source field and index the targets; passing an
150
+ # explicit target array to pygeodesic can overflow on some meshes.
151
+ distances, _ = algo.geodesicDistances(points[i : i + 1], None)
152
+ out[i] = np.asarray(distances, dtype=np.float64)[points]
153
+ out = 0.5 * (out + out.T)
154
+ np.fill_diagonal(out, 0.0)
155
+ return out
@@ -0,0 +1,243 @@
1
+ """Benchmark geodesic distances: bioimage-cpp vs scikit-fmm / pygeodesic.
2
+
3
+ Times the ``bic.distance`` geodesic entry points against their reference oracles
4
+ (scikit-fmm for masks, pygeodesic exact for meshes) on a range of mask grids and
5
+ sphere meshes, reporting per-call statistics and (optionally) the speedup versus
6
+ the reference. Reference columns are skipped when the backend is not installed or
7
+ when ``--no-ref`` is passed.
8
+
9
+ For each case it reports the min, median and spread (p75-p25) of the timed calls
10
+ plus ``ns/unit`` — nanoseconds per element processed (voxels for a field solve,
11
+ sources x voxels for a pairwise matrix), a size-independent throughput number that
12
+ is directly comparable across sizes and dimensionalities and is the right metric
13
+ for before/after optimization comparisons.
14
+
15
+ For rigorous before/after measurement use a high ``--repeats`` (>=15), pin
16
+ ``--threads 1`` on the field cases, run on a quiet machine, and dump ``--json`` for
17
+ each build so the two runs can be diffed. Interleave baseline/optimized builds to
18
+ cancel slow thermal/scheduler drift.
19
+
20
+ Not part of the pytest suite; requires scikit-fmm, pygeodesic and scipy (the last
21
+ only for the sphere-mesh construction; ``--no-ref`` still needs scipy for meshes).
22
+
23
+ Run::
24
+
25
+ python benchmark_geodesic.py --small
26
+ python benchmark_geodesic.py --large --threads 0
27
+ python benchmark_geodesic.py --xlarge --only field --threads 1 --repeats 20 \\
28
+ --no-ref --json /tmp/baseline.json
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import importlib.util
35
+ import json
36
+ import sys
37
+ from statistics import median
38
+ from time import perf_counter
39
+
40
+ import numpy as np
41
+
42
+ import bioimage_cpp as bic
43
+
44
+ from _geodesic_reference import (
45
+ reference_geodesic_distances_mask,
46
+ reference_geodesic_distances_mesh,
47
+ reference_geodesic_field_mask,
48
+ reference_geodesic_field_mesh,
49
+ )
50
+
51
+
52
+ def available(name: str) -> bool:
53
+ return importlib.util.find_spec(name) is not None
54
+
55
+
56
+ def time_call(fn, repeats: int, warmup: int = 1) -> list[float]:
57
+ """Return the list of per-call wall-clock times (seconds), after warmup."""
58
+ for _ in range(warmup):
59
+ fn()
60
+ timings = []
61
+ for _ in range(repeats):
62
+ start = perf_counter()
63
+ fn()
64
+ timings.append(perf_counter() - start)
65
+ return timings
66
+
67
+
68
+ def stats(timings: list[float]) -> dict:
69
+ ordered = sorted(timings)
70
+ n = len(ordered)
71
+ p25 = ordered[max(0, (n - 1) // 4)]
72
+ p75 = ordered[min(n - 1, (3 * (n - 1)) // 4)]
73
+ return {
74
+ "min": ordered[0],
75
+ "median": median(ordered),
76
+ "max": ordered[-1],
77
+ "spread": p75 - p25,
78
+ }
79
+
80
+
81
+ def make_sphere(n_points: int, radius: float = 5.0, seed: int = 0):
82
+ from scipy.spatial import ConvexHull
83
+
84
+ rng = np.random.default_rng(seed)
85
+ pts = rng.standard_normal((n_points, 3))
86
+ pts /= np.linalg.norm(pts, axis=1, keepdims=True)
87
+ pts *= radius
88
+ verts = np.ascontiguousarray(pts, dtype=np.float64)
89
+ faces = np.ascontiguousarray(ConvexHull(pts).simplices, dtype=np.int64)
90
+ return verts, faces
91
+
92
+
93
+ def scattered_points(n_axis_points: int, ndim: int, extent: int) -> np.ndarray:
94
+ coords = np.linspace(1, extent - 2, num=n_axis_points, dtype=np.int64)
95
+ grids = np.meshgrid(*([coords] * ndim), indexing="ij")
96
+ return np.stack([g.ravel() for g in grids], axis=1).astype(np.int64)
97
+
98
+
99
+ def build_cases(args):
100
+ """Return a list of (name, bic_fn, ref_fn, n_units) tuples for the size.
101
+
102
+ ``n_units`` is the number of elements processed by one call: the voxel/vertex
103
+ count for a field solve, or ``n_sources * count`` for a pairwise matrix. It is
104
+ the denominator for the ns/unit throughput metric.
105
+ """
106
+ if args.small:
107
+ mask2d_shape, mask3d_shape, mesh_n = (256, 256), (24, 48, 48), 1500
108
+ elif args.large:
109
+ mask2d_shape, mask3d_shape, mesh_n = (1024, 1024), (64, 128, 128), 20000
110
+ elif args.xlarge:
111
+ mask2d_shape, mask3d_shape, mesh_n = (1536, 1536), (128, 128, 128), 40000
112
+ else:
113
+ mask2d_shape, mask3d_shape, mesh_n = (512, 512), (32, 96, 96), 5000
114
+
115
+ def prod(shape):
116
+ n = 1
117
+ for s in shape:
118
+ n *= s
119
+ return n
120
+
121
+ cases = []
122
+
123
+ mask2d = np.ones(mask2d_shape, np.uint8)
124
+ src2d = np.array([[mask2d_shape[0] // 2, mask2d_shape[1] // 2]], np.int64)
125
+ pts2d = scattered_points(args.n_pairwise, 2, min(mask2d_shape))
126
+ cases.append((
127
+ f"mask2d/field {mask2d_shape}",
128
+ lambda: bic.distance.geodesic_distance_field(mask2d, src2d, number_of_threads=args.threads),
129
+ lambda: reference_geodesic_field_mask(mask2d, src2d),
130
+ prod(mask2d_shape),
131
+ ))
132
+ cases.append((
133
+ f"mask2d/pairwise N={len(pts2d)}",
134
+ lambda: bic.distance.geodesic_distances(mask2d, pts2d, number_of_threads=args.threads),
135
+ lambda: reference_geodesic_distances_mask(mask2d, pts2d),
136
+ len(pts2d) * prod(mask2d_shape),
137
+ ))
138
+
139
+ mask3d = np.ones(mask3d_shape, np.uint8)
140
+ src3d = np.array([[s // 2 for s in mask3d_shape]], np.int64)
141
+ pts3d = scattered_points(args.n_pairwise, 3, min(mask3d_shape))
142
+ cases.append((
143
+ f"mask3d/field {mask3d_shape}",
144
+ lambda: bic.distance.geodesic_distance_field(mask3d, src3d, number_of_threads=args.threads),
145
+ lambda: reference_geodesic_field_mask(mask3d, src3d),
146
+ prod(mask3d_shape),
147
+ ))
148
+ cases.append((
149
+ f"mask3d/pairwise N={len(pts3d)}",
150
+ lambda: bic.distance.geodesic_distances(mask3d, pts3d, number_of_threads=args.threads),
151
+ lambda: reference_geodesic_distances_mask(mask3d, pts3d),
152
+ len(pts3d) * prod(mask3d_shape),
153
+ ))
154
+
155
+ verts, faces = make_sphere(mesh_n, seed=args.seed)
156
+ src_v = np.array([0], np.int64)
157
+ pts_v = np.linspace(0, len(verts) - 1, num=args.n_pairwise, dtype=np.int64)
158
+ cases.append((
159
+ f"mesh/field V={len(verts)}",
160
+ lambda: bic.distance.geodesic_distance_field_mesh(verts, faces, src_v, number_of_threads=args.threads),
161
+ lambda: reference_geodesic_field_mesh(verts, faces, src_v),
162
+ len(verts),
163
+ ))
164
+ cases.append((
165
+ f"mesh/pairwise N={len(pts_v)}",
166
+ lambda: bic.distance.geodesic_distances_mesh(verts, faces, pts_v, number_of_threads=args.threads),
167
+ lambda: reference_geodesic_distances_mesh(verts, faces, pts_v),
168
+ len(pts_v) * len(verts),
169
+ ))
170
+
171
+ if args.only:
172
+ cases = [c for c in cases if args.only in c[0]]
173
+ return cases
174
+
175
+
176
+ def main():
177
+ parser = argparse.ArgumentParser(description=__doc__)
178
+ parser.add_argument("--repeats", type=int, default=7)
179
+ parser.add_argument("--small", action="store_true")
180
+ parser.add_argument("--large", action="store_true")
181
+ parser.add_argument("--xlarge", action="store_true")
182
+ parser.add_argument("--threads", type=int, default=1, help="0 = hardware concurrency")
183
+ parser.add_argument("--n-pairwise", type=int, default=8, dest="n_pairwise",
184
+ help="points per axis (mask) / total points (mesh) for pairwise")
185
+ parser.add_argument("--seed", type=int, default=0)
186
+ parser.add_argument("--no-ref", action="store_true", help="skip reference timings")
187
+ parser.add_argument("--only", type=str, default="",
188
+ help="substring filter on case name, e.g. 'field' or 'mask3d'")
189
+ parser.add_argument("--json", type=str, default="", help="dump results to this JSON path")
190
+ args = parser.parse_args()
191
+ n_sizes = sum(bool(x) for x in (args.small, args.large, args.xlarge))
192
+ if n_sizes > 1:
193
+ print("choose at most one of --small / --large / --xlarge", file=sys.stderr)
194
+ return 2
195
+
196
+ have_mask_ref = available("skfmm") and not args.no_ref
197
+ have_mesh_ref = available("pygeodesic") and not args.no_ref
198
+ if not args.no_ref:
199
+ if not available("skfmm"):
200
+ print("scikit-fmm not installed: mask reference column skipped", file=sys.stderr)
201
+ if not available("pygeodesic"):
202
+ print("pygeodesic not installed: mesh reference column skipped", file=sys.stderr)
203
+
204
+ cases = build_cases(args)
205
+
206
+ header = (f"{'case':>26} {'bic_min':>10} {'bic_med':>10} {'spread':>9} "
207
+ f"{'ns/unit':>9} {'ref_med':>10} {'speedup':>9}")
208
+ print(f"threads={args.threads} repeats={args.repeats}")
209
+ print(header)
210
+ print("-" * len(header))
211
+ results = []
212
+ for name, bic_fn, ref_fn, n_units in cases:
213
+ bic_t = time_call(bic_fn, args.repeats)
214
+ bs = stats(bic_t)
215
+ ns_per_unit = bs["median"] / n_units * 1e9
216
+ is_mesh = name.startswith("mesh")
217
+ has_ref = have_mesh_ref if is_mesh else have_mask_ref
218
+ entry = {"case": name, "n_units": n_units, "threads": args.threads,
219
+ "bic_s": bs, "ns_per_unit": ns_per_unit}
220
+ if has_ref:
221
+ ref_t = time_call(ref_fn, args.repeats)
222
+ rs = stats(ref_t)
223
+ speed = rs["median"] / bs["median"]
224
+ entry["ref_s"] = rs
225
+ entry["speedup"] = speed
226
+ ref_col, speed_col = f"{rs['median']*1e3:10.2f}", f"{speed:6.2f}x"
227
+ else:
228
+ ref_col, speed_col = f"{'n/a':>10}", f"{'n/a':>9}"
229
+ print(f"{name:>26} {bs['min']*1e3:10.2f} {bs['median']*1e3:10.2f} "
230
+ f"{bs['spread']*1e3:9.2f} {ns_per_unit:9.1f} {ref_col} {speed_col:>9}")
231
+ results.append(entry)
232
+
233
+ if args.json:
234
+ with open(args.json, "w") as fh:
235
+ json.dump({"threads": args.threads, "repeats": args.repeats,
236
+ "results": results}, fh, indent=2)
237
+ print(f"wrote {args.json}", file=sys.stderr)
238
+
239
+ return 0
240
+
241
+
242
+ if __name__ == "__main__":
243
+ sys.exit(main())