multimodars 0.3.1__tar.gz → 0.3.2__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 (116) hide show
  1. multimodars-0.3.2/.readthedocs.yaml +19 -0
  2. {multimodars-0.3.1 → multimodars-0.3.2}/CHANGELOG.md +21 -0
  3. {multimodars-0.3.1 → multimodars-0.3.2}/Cargo.lock +1 -1
  4. {multimodars-0.3.1 → multimodars-0.3.2}/Cargo.toml +1 -1
  5. {multimodars-0.3.1 → multimodars-0.3.2}/PKG-INFO +3 -3
  6. {multimodars-0.3.1 → multimodars-0.3.2}/README.md +2 -2
  7. {multimodars-0.3.1 → multimodars-0.3.2}/ROADMAP.MD +5 -4
  8. multimodars-0.3.2/benchmarks/BENCHMARK_PLAN.md +143 -0
  9. multimodars-0.3.2/benchmarks/benchmark_bruteforce_stepsize.py +196 -0
  10. multimodars-0.3.2/benchmarks/benchmark_cpu_scaling.py +258 -0
  11. multimodars-0.3.2/benchmarks/results/bruteforce_stepsize.png +0 -0
  12. multimodars-0.3.2/benchmarks/results/cpu_scaling.png +0 -0
  13. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/__init__.py +1 -1
  14. {multimodars-0.3.1 → multimodars-0.3.2}/paper/paper.md +6 -2
  15. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/processing/align_within.rs +2 -3
  16. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/processing/process_utils.rs +12 -20
  17. multimodars-0.3.1/.readthedocs.yaml +0 -36
  18. {multimodars-0.3.1 → multimodars-0.3.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {multimodars-0.3.1 → multimodars-0.3.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  20. {multimodars-0.3.1 → multimodars-0.3.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  21. {multimodars-0.3.1 → multimodars-0.3.2}/.github/workflows/CI.yml +0 -0
  22. {multimodars-0.3.1 → multimodars-0.3.2}/.github/workflows/draft-pdf.yml +0 -0
  23. {multimodars-0.3.1 → multimodars-0.3.2}/.gitignore +0 -0
  24. {multimodars-0.3.1 → multimodars-0.3.2}/.pre-commit-config.yaml +0 -0
  25. {multimodars-0.3.1 → multimodars-0.3.2}/CONTRIBUTING.md +0 -0
  26. {multimodars-0.3.1 → multimodars-0.3.2}/LICENSE +0 -0
  27. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/branch_diastolic_contours.csv +0 -0
  28. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/branch_systolic_contours.csv +0 -0
  29. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/calcium_diastolic_contours.csv +0 -0
  30. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/calcium_systolic_contours.csv +0 -0
  31. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/diastolic_contours.csv +0 -0
  32. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/diastolic_reference_points.csv +0 -0
  33. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/eem_diastolic_contours.csv +0 -0
  34. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/eem_systolic_contours.csv +0 -0
  35. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/systolic_contours.csv +0 -0
  36. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/idealized_geometry/systolic_reference_points.csv +0 -0
  37. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/branch_diastolic_contours.csv +0 -0
  38. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/branch_systolic_contours.csv +0 -0
  39. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/calcium_diastolic_contours.csv +0 -0
  40. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/calcium_systolic_contours.csv +0 -0
  41. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/diastolic_contours.csv +0 -0
  42. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/diastolic_reference_points.csv +0 -0
  43. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/eem_diastolic_contours.csv +0 -0
  44. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/eem_systolic_contours.csv +0 -0
  45. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/systolic_contours.csv +0 -0
  46. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_full/systolic_reference_points.csv +0 -0
  47. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_rest/combined_sorted_manual.csv +0 -0
  48. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_rest/diastolic_contours.csv +0 -0
  49. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_rest/diastolic_reference_points.csv +0 -0
  50. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_rest/systolic_contours.csv +0 -0
  51. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_rest/systolic_reference_points.csv +0 -0
  52. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_stress/combined_sorted_manual.csv +0 -0
  53. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_stress/diastolic_contours.csv +0 -0
  54. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_stress/diastolic_reference_points.csv +0 -0
  55. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_stress/systolic_contours.csv +0 -0
  56. {multimodars-0.3.1 → multimodars-0.3.2}/data/fixtures/ivus_stress/systolic_reference_points.csv +0 -0
  57. {multimodars-0.3.1 → multimodars-0.3.2}/media/coronary_artery_disease.jpg +0 -0
  58. {multimodars-0.3.1 → multimodars-0.3.2}/media/dynamic_lumen_changes.png +0 -0
  59. {multimodars-0.3.1 → multimodars-0.3.2}/media/multimoda-rs.jpg +0 -0
  60. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/_converters.py +0 -0
  61. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/_processing.py +0 -0
  62. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/ccta/__init__.py +0 -0
  63. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/ccta/debug_plots.py +0 -0
  64. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/ccta/fixing_functions.py +0 -0
  65. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/ccta/labeling.py +0 -0
  66. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/ccta/manipulating.py +0 -0
  67. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/io/__init__.py +0 -0
  68. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/io/read_geometrical.py +0 -0
  69. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/io/write_geometries.py +0 -0
  70. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/multimodars.pyi +0 -0
  71. {multimodars-0.3.1 → multimodars-0.3.2}/multimodars/py.typed +0 -0
  72. {multimodars-0.3.1 → multimodars-0.3.2}/paper/REVIEWER1.md +0 -0
  73. {multimodars-0.3.1 → multimodars-0.3.2}/paper/bibliography.bib +0 -0
  74. {multimodars-0.3.1 → multimodars-0.3.2}/paper/figures/Figure1.jpg +0 -0
  75. {multimodars-0.3.1 → multimodars-0.3.2}/paper/figures/Figure2.jpg +0 -0
  76. {multimodars-0.3.1 → multimodars-0.3.2}/paper/figures/Figure3.png +0 -0
  77. {multimodars-0.3.1 → multimodars-0.3.2}/paper/figures/Figure4.jpg +0 -0
  78. {multimodars-0.3.1 → multimodars-0.3.2}/pyproject.toml +0 -0
  79. {multimodars-0.3.1 → multimodars-0.3.2}/src/ccta/adjust_mesh/label_coronary.rs +0 -0
  80. {multimodars-0.3.1 → multimodars-0.3.2}/src/ccta/adjust_mesh/mod.rs +0 -0
  81. {multimodars-0.3.1 → multimodars-0.3.2}/src/ccta/adjust_mesh/scale_coronary.rs +0 -0
  82. {multimodars-0.3.1 → multimodars-0.3.2}/src/ccta/binding/ccta_py.rs +0 -0
  83. {multimodars-0.3.1 → multimodars-0.3.2}/src/ccta/binding/mod.rs +0 -0
  84. {multimodars-0.3.1 → multimodars-0.3.2}/src/ccta/mod.rs +0 -0
  85. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/binding/align.rs +0 -0
  86. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/binding/classes.rs +0 -0
  87. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/binding/entry.rs +0 -0
  88. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/binding/mod.rs +0 -0
  89. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/centerline_align/align_algorithms.rs +0 -0
  90. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/centerline_align/mod.rs +0 -0
  91. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/centerline_align/preprocessing.rs +0 -0
  92. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/io/geometry.rs +0 -0
  93. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/io/input.rs +0 -0
  94. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/io/integrity_check.rs +0 -0
  95. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/io/mod.rs +0 -0
  96. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/io/output.rs +0 -0
  97. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/mod.rs +0 -0
  98. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/processing/align_between.rs +0 -0
  99. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/processing/mod.rs +0 -0
  100. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/processing/postprocessing.rs +0 -0
  101. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/processing/preprocessing.rs +0 -0
  102. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/processing/wall.rs +0 -0
  103. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/to_object/interpolation.rs +0 -0
  104. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/to_object/mod.rs +0 -0
  105. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/to_object/texture.rs +0 -0
  106. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/to_object/write_mtl.rs +0 -0
  107. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/utils/general_utils.rs +0 -0
  108. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/utils/mod.rs +0 -0
  109. {multimodars-0.3.1 → multimodars-0.3.2}/src/intravascular/utils/test_utils.rs +0 -0
  110. {multimodars-0.3.1 → multimodars-0.3.2}/src/lib.rs +0 -0
  111. {multimodars-0.3.1 → multimodars-0.3.2}/tests/conftest.py +0 -0
  112. {multimodars-0.3.1 → multimodars-0.3.2}/tests/test_ccta.py +0 -0
  113. {multimodars-0.3.1 → multimodars-0.3.2}/tests/test_converters.py +0 -0
  114. {multimodars-0.3.1 → multimodars-0.3.2}/tests/test_core.py +0 -0
  115. {multimodars-0.3.1 → multimodars-0.3.2}/tests/test_wrappers.py +0 -0
  116. {multimodars-0.3.1 → multimodars-0.3.2}/tests/tests_todo.md +0 -0
@@ -0,0 +1,19 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-22.04
5
+ tools:
6
+ python: "3.12"
7
+ rust: "latest"
8
+ jobs:
9
+ post_create_environment:
10
+ - "$READTHEDOCS_VIRTUALENV_PATH/bin/pip install maturin sphinx sphinx-rtd-theme sphinx-autodoc-typehints myst-nb plotly furo"
11
+
12
+ sphinx:
13
+ configuration: docs/conf.py
14
+ fail_on_warning: true
15
+
16
+ python:
17
+ install:
18
+ - method: pip
19
+ path: .
@@ -3,6 +3,27 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project adheres to [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.3.2] - 2026-05-05
7
+
8
+ ### Added
9
+ - Benchmark suite (`benchmarks/`) with two scripts: `benchmark_bruteforce_stepsize.py`
10
+ (step-size scaling, `from_file_full`) and `benchmark_cpu_scaling.py` (core-count
11
+ scaling, `from_array_single`). Results saved to `benchmarks/results/`.
12
+ - Benchmark documentation page (`docs/benchmark.rst`) with result figures and table,
13
+ linked in the ReadTheDocs table of contents.
14
+
15
+ ### Changed
16
+ - `paper/paper.md` Performance section updated with quantified benchmark results and
17
+ corrected parallelisation description.
18
+
19
+ ### Performance
20
+ - Intra-pullback alignment (`search_range`) now parallelises over candidate rotation
21
+ angles via `par_iter` instead of the point-rotation loop. The inner cost-function
22
+ loop is now sequential. This provides enough rayon tasks per frame step to saturate
23
+ all cores: bruteforce scales 6.5× and optimised 4.2× from 2 to 16 cores (previously
24
+ ~1.4× for both). Combined algorithmic + parallelisation gain reaches **38.5×** vs.
25
+ brute-force at 2 cores.
26
+
6
27
  ## [0.3.1] - 2026-05-04
7
28
 
8
29
  ### Added
@@ -753,7 +753,7 @@ dependencies = [
753
753
 
754
754
  [[package]]
755
755
  name = "multimodars"
756
- version = "0.3.1"
756
+ version = "0.3.2"
757
757
  dependencies = [
758
758
  "anyhow",
759
759
  "approx",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "multimodars"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  edition = "2021"
5
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multimodars
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -79,7 +79,7 @@ For building from source or the full developer setup (tests, linting, docs), see
79
79
 
80
80
  ## Quick Example
81
81
 
82
- Download [examples.zip](https://github.com/yungselm/multimoda-rs/releases/latest/download/examples.zip) (SHA256: `d11ebc7607f43ab4571fb51c9ac9178caac57774cf5d97f4f068ace4eb070fee`) from the latest release and extract it to your working directory.
82
+ Checkout our **[Documentation](https://multimod-rs.readthedocs.io/en/latest)** for in-depth explanations of the functionality and preloaded notebooks including 3D plots. Otherwise clone the repo to follow along the different examples:
83
83
 
84
84
  ```python
85
85
  import multimodars as mm
@@ -133,7 +133,7 @@ aligned_pair, cl_resampled = mm.align_three_point(
133
133
 
134
134
  ## Documentation
135
135
 
136
- Full documentation — installation, step-by-step tutorials, interactive Jupyter notebooks, and API reference — is available at **[multimoda-rs.readthedocs.io](https://multimoda-rs.readthedocs.io)**.
136
+ Full documentation — installation, step-by-step tutorials, interactive (preloaded) Jupyter notebooks, and API reference — is available at **[multimoda-rs.readthedocs.io](https://multimoda-rs.readthedocs.io)**.
137
137
 
138
138
  ## License
139
139
 
@@ -55,7 +55,7 @@ For building from source or the full developer setup (tests, linting, docs), see
55
55
 
56
56
  ## Quick Example
57
57
 
58
- Download [examples.zip](https://github.com/yungselm/multimoda-rs/releases/latest/download/examples.zip) (SHA256: `d11ebc7607f43ab4571fb51c9ac9178caac57774cf5d97f4f068ace4eb070fee`) from the latest release and extract it to your working directory.
58
+ Checkout our **[Documentation](https://multimod-rs.readthedocs.io/en/latest)** for in-depth explanations of the functionality and preloaded notebooks including 3D plots. Otherwise clone the repo to follow along the different examples:
59
59
 
60
60
  ```python
61
61
  import multimodars as mm
@@ -109,7 +109,7 @@ aligned_pair, cl_resampled = mm.align_three_point(
109
109
 
110
110
  ## Documentation
111
111
 
112
- Full documentation — installation, step-by-step tutorials, interactive Jupyter notebooks, and API reference — is available at **[multimoda-rs.readthedocs.io](https://multimoda-rs.readthedocs.io)**.
112
+ Full documentation — installation, step-by-step tutorials, interactive (preloaded) Jupyter notebooks, and API reference — is available at **[multimoda-rs.readthedocs.io](https://multimoda-rs.readthedocs.io)**.
113
113
 
114
114
  ## License
115
115
 
@@ -1,5 +1,9 @@
1
1
  # Python package that has one purpose: Combining any intravascular image modality with CCTA meshes.
2
2
 
3
+ # Optimizations
4
+ - [] Optimize the ray intersection algorithm
5
+ - [] Better parallelization strategy for frames alignment by processing in batches (frames only have to be aligned relative to each other not sequentially)
6
+
3
7
  # Centerline alignment module
4
8
  (- [] Create Centerline directly from mesh) (v1.0.0)
5
9
 
@@ -16,9 +20,6 @@
16
20
  - [] helper function to automatically export only certain points on mesh (e.g. RCA or LCA)
17
21
  - [] function to combine anomalous section only with the distal part
18
22
 
19
- # Miscellaneous
20
- - [x] Improve print statements in the whole package
21
-
22
23
  # Prepublish checklist (JOSS review feedback — open until resolved):
23
24
 
24
25
  ## Dependencies / packaging
@@ -40,7 +41,7 @@
40
41
  so regressions are caught automatically.
41
42
 
42
43
  ## Performance claim
43
- - [] Provide reproducible code + sample data for the performance claim in the paper (lines 102-104):
44
+ - [x] Provide reproducible code + sample data for the performance claim in the paper (lines 102-104):
44
45
  "Empirical performance on a 16-core CPU: an OCT pullback with 280 frames and a rotation search range
45
46
  of ±3° (final accuracy 0.01°) saw alignment time reduced from 150 s to 18 s with the optimized
46
47
  multiscale search."
@@ -0,0 +1,143 @@
1
+ # Benchmark Plan: multimoda-rs Alignment Performance
2
+
3
+ ## Overview
4
+
5
+ Two benchmarks to characterise the alignment performance of `multimodars`.
6
+
7
+ | # | What | Function | Dataset |
8
+ |---|------|----------|---------|
9
+ | 1 | Bruteforce vs. optimised at varying step sizes | `from_file_full` | IVUS rest/stress |
10
+ | 2 | CPU-core scaling (parallelism) | `from_array_single` | OCT (280 frames) |
11
+
12
+ Results land in `benchmarks/results/`.
13
+
14
+ ---
15
+
16
+ ## Benchmark 1 — Bruteforce vs. Optimised: Step-Size Scaling
17
+
18
+ ### Goal
19
+
20
+ Quantify how bruteforce and optimised alignment scale as `step_rotation_deg` decreases
21
+ (i.e. as the rotation-search grid becomes finer).
22
+
23
+ ### Setup
24
+
25
+ | Parameter | Value |
26
+ |-----------|-------|
27
+ | Function | `from_file_full` |
28
+ | Dataset | `examples/data/ivus_rest` / `examples/data/ivus_stress` (IVUS) |
29
+ | `range_rotation_deg` | 90.0° (±90° → 180° total sweep) |
30
+ | `write_obj` | `False` — avoids disk-I/O noise |
31
+ | `smooth` | `False` |
32
+ | `postprocessing` | `False` |
33
+ | `interpolation_steps` | `0` |
34
+ | Repetitions per condition | 3 (median wall time reported) |
35
+
36
+ ### Independent variable
37
+
38
+ `step_rotation_deg` ∈ {5.0, 2.5, 1.0, 0.5, 0.1} degrees.
39
+
40
+ Corresponding number of rotation steps n = 180 / step (using the full ±90° range):
41
+
42
+ | step (°) | n\_steps |
43
+ |----------|---------|
44
+ | 5.0 | 36 |
45
+ | 2.5 | 72 |
46
+ | 1.0 | 180 |
47
+ | 0.5 | 360 |
48
+ | 0.1 | 1 800 |
49
+
50
+ ### Expected behaviour
51
+
52
+ - **Bruteforce**: exhaustively scores every rotation angle for every frame pair →
53
+ wall time grows linearly with n\_steps (O(n)).
54
+ - **Optimised**: coarse-to-fine hierarchical search → sub-linear in n\_steps,
55
+ asymptotically O(log n).
56
+
57
+ The plot axes are logarithmic so the two complexity classes appear as straight lines
58
+ with clearly different slopes.
59
+
60
+ ### Output
61
+
62
+ `benchmarks/results/bruteforce_stepsize.png` — log-log line plot:
63
+
64
+ - x-axis (bottom): number of rotation steps; (top): step size in degrees
65
+ - y-axis: median wall time (seconds)
66
+ - Line 1: bruteforce (red ○–) + O(n) reference dashed line
67
+ - Line 2: optimised (blue ■ --)
68
+ - Right y-axis: speedup factor (bruteforce / optimised)
69
+
70
+ ---
71
+
72
+ ## Benchmark 2 — CPU-Core Scaling
73
+
74
+ ### Goal
75
+
76
+ Measure how parallelism scales from 1 → 16 CPU cores for both alignment modes,
77
+ using a large OCT pullback (280 frames) to stress the frame-level parallelism
78
+ in the rayon thread pool.
79
+
80
+ ### Note on function choice
81
+
82
+ The OCT example data (`examples/data/oct_single/`) ships as
83
+ `oct_contours_raw.csv` + `oct_ref.csv`, which do not follow the AIVUS-CAA
84
+ file-naming convention expected by `from_file_single`. The benchmark therefore
85
+ uses `from_array_single` (as demonstrated in the docs notebook), which accepts
86
+ raw numpy arrays directly. The algorithm under test is identical.
87
+
88
+ ### Setup
89
+
90
+ | Parameter | Value |
91
+ |-----------|-------|
92
+ | Function | `from_array_single` |
93
+ | Dataset | `examples/data/oct_single/` (280 frames, OCT) |
94
+ | `step_rotation_deg` | 0.01° (from docs/notebooks) |
95
+ | `range_rotation_deg` | 6.0° (from docs/notebooks) |
96
+ | `image_center` | (5.0, 5.0) (from docs/notebooks) |
97
+ | `n_points` | 40 (from docs/notebooks) |
98
+ | `smooth` | `False` |
99
+ | `write_obj` | `False` |
100
+ | Repetitions per condition | 3 (median wall time reported) |
101
+
102
+ ### Independent variable
103
+
104
+ `RAYON_NUM_THREADS` ∈ {1, 2, 4, 8, 12, 16}
105
+
106
+ **Important**: rayon's global thread pool is initialised once per process from
107
+ the `RAYON_NUM_THREADS` environment variable. To guarantee a fresh pool at each
108
+ thread count, every (n\_cores × bruteforce × repeat) combination is executed in
109
+ a **separate subprocess** with the variable set before any import of `multimodars`.
110
+
111
+ ### Expected behaviour
112
+
113
+ - **Optimised**: near-linear speedup up to ~8 cores, then diminishing returns
114
+ (Amdahl's law — serial fraction of the pipeline limits gains beyond that).
115
+ - **Bruteforce**: also benefits from rayon frame-level parallelism, but expected
116
+ to saturate earlier because the per-step work is less cache-friendly.
117
+
118
+ ### Output
119
+
120
+ `benchmarks/results/cpu_scaling.png` — two-panel figure:
121
+
122
+ - Left panel: wall time (s) vs. CPU cores, two lines (bruteforce, optimised)
123
+ - Right panel: speedup relative to 1 core vs. CPU cores +
124
+ ideal linear-speedup reference; same two lines
125
+
126
+ ---
127
+
128
+ ## Running the Benchmarks
129
+
130
+ ```bash
131
+ # From project root
132
+ python benchmarks/benchmark_bruteforce_stepsize.py
133
+ python benchmarks/benchmark_cpu_scaling.py
134
+ ```
135
+
136
+ Estimated runtimes (rough, machine-dependent):
137
+
138
+ | Benchmark | Estimate |
139
+ |-----------|----------|
140
+ | Benchmark 1 (all step sizes × 2 modes × 3 reps) | 5–30 min |
141
+ | Benchmark 2 (6 core counts × 2 modes × 3 reps in subprocesses) | 20–60 min |
142
+
143
+ Results are saved as PNG files in `benchmarks/results/`.
@@ -0,0 +1,196 @@
1
+ """Benchmark 1: bruteforce vs. optimised alignment at varying step sizes.
2
+
3
+ Tests from_file_full on the bundled IVUS rest/stress example data with
4
+ step_rotation_deg in {5.0, 2.5, 1.0, 0.5, 0.1} degrees.
5
+
6
+ write_obj / smooth / postprocessing are all disabled so only the alignment
7
+ algorithm itself is timed.
8
+
9
+ Output: benchmarks/results/bruteforce_stepsize.png
10
+ """
11
+
12
+ import statistics
13
+ import time
14
+ from pathlib import Path
15
+
16
+ import matplotlib.pyplot as plt
17
+
18
+ import multimodars as mm
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Configuration
22
+ # ---------------------------------------------------------------------------
23
+
24
+ _ROOT = Path(__file__).parent.parent
25
+ DATA_ROOT = _ROOT / "examples" / "data"
26
+ INPUT_AB = str(DATA_ROOT / "ivus_rest")
27
+ INPUT_CD = str(DATA_ROOT / "ivus_stress")
28
+ RESULTS_DIR = Path(__file__).parent / "results"
29
+
30
+ STEP_SIZES: list[float] = [5.0, 2.5, 1.0, 0.5, 0.1, 0.05]
31
+ RANGE_DEG: float = 90.0
32
+ REPEATS: int = 3
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Helpers
37
+ # ---------------------------------------------------------------------------
38
+
39
+
40
+ def _n_steps(step: float) -> float:
41
+ return (RANGE_DEG * 2) / step
42
+
43
+
44
+ def _run_once(step: float, bruteforce: bool) -> float:
45
+ t0 = time.perf_counter()
46
+ mm.from_file_full(
47
+ input_path_ab=INPUT_AB,
48
+ input_path_cd=INPUT_CD,
49
+ step_rotation_deg=step,
50
+ range_rotation_deg=RANGE_DEG,
51
+ write_obj=False,
52
+ smooth=False,
53
+ postprocessing=False,
54
+ bruteforce=bruteforce,
55
+ interpolation_steps=0,
56
+ )
57
+ return time.perf_counter() - t0
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Benchmark
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ def run_benchmark() -> dict[str, list[float]]:
66
+ results: dict[str, list[float]] = {"bruteforce": [], "optimised": []}
67
+
68
+ for step in STEP_SIZES:
69
+ print(f"step={step:4.1f}° bruteforce=True ", end="", flush=True)
70
+ times_bf = [_run_once(step, bruteforce=True) for _ in range(REPEATS)]
71
+ med_bf = statistics.median(times_bf)
72
+ results["bruteforce"].append(med_bf)
73
+ print(f"{med_bf:.2f} s")
74
+
75
+ print(f"step={step:4.1f}° bruteforce=False ", end="", flush=True)
76
+ times_opt = [_run_once(step, bruteforce=False) for _ in range(REPEATS)]
77
+ med_opt = statistics.median(times_opt)
78
+ results["optimised"].append(med_opt)
79
+ speedup = med_bf / med_opt if med_opt > 0 else float("inf")
80
+ print(f"{med_opt:.2f} s (speedup {speedup:.1f}x)")
81
+
82
+ return results
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Plotting
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ def plot_results(results: dict[str, list[float]]) -> None:
91
+ RESULTS_DIR.mkdir(parents=True, exist_ok=True)
92
+
93
+ n_steps_list = [_n_steps(s) for s in STEP_SIZES]
94
+ bf_times = results["bruteforce"]
95
+ opt_times = results["optimised"]
96
+ speedups = [bf / opt for bf, opt in zip(bf_times, opt_times)]
97
+
98
+ fig, (ax_main, ax_speedup) = plt.subplots(
99
+ 1, 2, figsize=(14, 5), constrained_layout=True
100
+ )
101
+
102
+ # --- Left panel: wall time ---
103
+ ax_main.plot(
104
+ n_steps_list,
105
+ bf_times,
106
+ "o-",
107
+ color="tab:red",
108
+ linewidth=2,
109
+ markersize=7,
110
+ label="Bruteforce",
111
+ )
112
+ ax_main.plot(
113
+ n_steps_list,
114
+ opt_times,
115
+ "s--",
116
+ color="tab:blue",
117
+ linewidth=2,
118
+ markersize=7,
119
+ label="Optimised",
120
+ )
121
+
122
+ # O(n) reference anchored at the first bruteforce point
123
+ ref_n, ref_t = n_steps_list[0], bf_times[0]
124
+ linear_ref = [ref_t * (n / ref_n) for n in n_steps_list]
125
+ ax_main.plot(
126
+ n_steps_list,
127
+ linear_ref,
128
+ ":",
129
+ color="tab:red",
130
+ linewidth=1.5,
131
+ alpha=0.55,
132
+ label="O(n) reference",
133
+ )
134
+
135
+ ax_main.set_xscale("log")
136
+ ax_main.set_yscale("log")
137
+ ax_main.set_xlabel("Number of rotation steps (2 × range / step)", fontsize=12)
138
+ ax_main.set_ylabel("Median wall time (s)", fontsize=12)
139
+ ax_main.set_title(
140
+ "Bruteforce vs. Optimised — Step-Size Scaling\n"
141
+ "from_file_full · IVUS rest/stress · range = 90°",
142
+ fontsize=11,
143
+ )
144
+ ax_main.legend(fontsize=11)
145
+ ax_main.grid(True, which="both", alpha=0.3)
146
+
147
+ # Secondary x-axis with step-size labels
148
+ ax_top = ax_main.twiny()
149
+ ax_top.set_xscale("log")
150
+ ax_top.set_xlim(ax_main.get_xlim())
151
+ ax_top.set_xticks(n_steps_list)
152
+ ax_top.set_xticklabels([f"{s}°" for s in STEP_SIZES])
153
+ ax_top.set_xlabel("Step size (degrees)", fontsize=12)
154
+
155
+ # --- Right panel: speedup factor ---
156
+ ax_speedup.plot(
157
+ n_steps_list,
158
+ speedups,
159
+ "D-",
160
+ color="tab:green",
161
+ linewidth=2,
162
+ markersize=7,
163
+ )
164
+ ax_speedup.axhline(1.0, color="gray", linewidth=1, linestyle=":")
165
+ ax_speedup.set_xscale("log")
166
+ ax_speedup.set_xlabel("Number of rotation steps (2 × range / step)", fontsize=12)
167
+ ax_speedup.set_ylabel("Speedup (bruteforce / optimised)", fontsize=12)
168
+ ax_speedup.set_title(
169
+ "Speedup of Optimised over Bruteforce\n"
170
+ "from_file_full · IVUS rest/stress · range = 90°",
171
+ fontsize=11,
172
+ )
173
+ ax_speedup.grid(True, which="both", alpha=0.3)
174
+
175
+ ax_top2 = ax_speedup.twiny()
176
+ ax_top2.set_xscale("log")
177
+ ax_top2.set_xlim(ax_speedup.get_xlim())
178
+ ax_top2.set_xticks(n_steps_list)
179
+ ax_top2.set_xticklabels([f"{s}°" for s in STEP_SIZES])
180
+ ax_top2.set_xlabel("Step size (degrees)", fontsize=12)
181
+
182
+ out = RESULTS_DIR / "bruteforce_stepsize.png"
183
+ fig.savefig(out, dpi=150)
184
+ print(f"\nPlot saved → {out}")
185
+ plt.show()
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Entry point
190
+ # ---------------------------------------------------------------------------
191
+
192
+ if __name__ == "__main__":
193
+ print("=== Benchmark 1: Bruteforce vs. Optimised — Step-Size Scaling ===\n")
194
+ results = run_benchmark()
195
+ print("\nPlotting results …")
196
+ plot_results(results)