odx 0.3.2__tar.gz → 0.3.3__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 (111) hide show
  1. {odx-0.3.2 → odx-0.3.3}/.github/workflows/release.yml +2 -2
  2. {odx-0.3.2 → odx-0.3.3}/Cargo.lock +2 -2
  3. {odx-0.3.2 → odx-0.3.3}/Cargo.toml +1 -1
  4. {odx-0.3.2 → odx-0.3.3}/PKG-INFO +1 -1
  5. {odx-0.3.2 → odx-0.3.3}/src/peak_finder.rs +122 -22
  6. {odx-0.3.2 → odx-0.3.3}/.github/workflows/ci.yml +0 -0
  7. {odx-0.3.2 → odx-0.3.3}/.gitignore +0 -0
  8. {odx-0.3.2 → odx-0.3.3}/LICENSE-APACHE +0 -0
  9. {odx-0.3.2 → odx-0.3.3}/LICENSE-MIT +0 -0
  10. {odx-0.3.2 → odx-0.3.3}/LICENSE-MRTRIX +0 -0
  11. {odx-0.3.2 → odx-0.3.3}/LICENSE-NIBABEL +0 -0
  12. {odx-0.3.2 → odx-0.3.3}/README.md +0 -0
  13. {odx-0.3.2 → odx-0.3.3}/SPECIFICATION.md +0 -0
  14. {odx-0.3.2 → odx-0.3.3}/TODO.md +0 -0
  15. {odx-0.3.2 → odx-0.3.3}/assets/dsistudio_odf8_faces.txt +0 -0
  16. {odx-0.3.2 → odx-0.3.3}/assets/dsistudio_odf8_vertices.txt +0 -0
  17. {odx-0.3.2 → odx-0.3.3}/benches/dsistudio_io.rs +0 -0
  18. {odx-0.3.2 → odx-0.3.3}/benches/sphere_lookup.rs +0 -0
  19. {odx-0.3.2 → odx-0.3.3}/docs/cli.md +0 -0
  20. {odx-0.3.2 → odx-0.3.3}/docs/compare.md +0 -0
  21. {odx-0.3.2 → odx-0.3.3}/docs/dsistudio_mrtrix_conversion_workflows.md +0 -0
  22. {odx-0.3.2 → odx-0.3.3}/docs/fixel_qc.md +0 -0
  23. {odx-0.3.2 → odx-0.3.3}/docs/template.md +0 -0
  24. {odx-0.3.2 → odx-0.3.3}/examples/afd_validate.rs +0 -0
  25. {odx-0.3.2 → odx-0.3.3}/examples/check_rotation.rs +0 -0
  26. {odx-0.3.2 → odx-0.3.3}/examples/convert_dsistudio.rs +0 -0
  27. {odx-0.3.2 → odx-0.3.3}/examples/convert_mrtrix_to_fz.rs +0 -0
  28. {odx-0.3.2 → odx-0.3.3}/examples/inspect_h5.rs +0 -0
  29. {odx-0.3.2 → odx-0.3.3}/odx/__init__.py +0 -0
  30. {odx-0.3.2 → odx-0.3.3}/odx/adapters/__init__.py +0 -0
  31. {odx-0.3.2 → odx-0.3.3}/odx/adapters/dipy.py +0 -0
  32. {odx-0.3.2 → odx-0.3.3}/odx/adapters/nibabel.py +0 -0
  33. {odx-0.3.2 → odx-0.3.3}/odx/spheres.py +0 -0
  34. {odx-0.3.2 → odx-0.3.3}/pyproject.toml +0 -0
  35. {odx-0.3.2 → odx-0.3.3}/python/.cargo/config.toml +0 -0
  36. {odx-0.3.2 → odx-0.3.3}/python/Cargo.toml +0 -0
  37. {odx-0.3.2 → odx-0.3.3}/python/README.md +0 -0
  38. {odx-0.3.2 → odx-0.3.3}/python/src/lib.rs +0 -0
  39. {odx-0.3.2 → odx-0.3.3}/python/tests/test_basic.py +0 -0
  40. {odx-0.3.2 → odx-0.3.3}/python/tests/test_coherence.py +0 -0
  41. {odx-0.3.2 → odx-0.3.3}/python/tests/test_dipy_adapter.py +0 -0
  42. {odx-0.3.2 → odx-0.3.3}/python/tests/test_foreign_io.py +0 -0
  43. {odx-0.3.2 → odx-0.3.3}/python/tests/test_graddev.py +0 -0
  44. {odx-0.3.2 → odx-0.3.3}/python/tests/test_nibabel_adapter.py +0 -0
  45. {odx-0.3.2 → odx-0.3.3}/python/tests/test_peak_finder.py +0 -0
  46. {odx-0.3.2 → odx-0.3.3}/setting_up_cs_and_benchmarking.md +0 -0
  47. {odx-0.3.2 → odx-0.3.3}/src/bin/odx.rs +0 -0
  48. {odx-0.3.2 → odx-0.3.3}/src/cli_support.rs +0 -0
  49. {odx-0.3.2 → odx-0.3.3}/src/combine.rs +0 -0
  50. {odx-0.3.2 → odx-0.3.3}/src/compare.rs +0 -0
  51. {odx-0.3.2 → odx-0.3.3}/src/data_array.rs +0 -0
  52. {odx-0.3.2 → odx-0.3.3}/src/densify.rs +0 -0
  53. {odx-0.3.2 → odx-0.3.3}/src/descoteaux_sh.rs +0 -0
  54. {odx-0.3.2 → odx-0.3.3}/src/dtype.rs +0 -0
  55. {odx-0.3.2 → odx-0.3.3}/src/error.rs +0 -0
  56. {odx-0.3.2 → odx-0.3.3}/src/fixel_match.rs +0 -0
  57. {odx-0.3.2 → odx-0.3.3}/src/fmls.rs +0 -0
  58. {odx-0.3.2 → odx-0.3.3}/src/formats/dsistudio.rs +0 -0
  59. {odx-0.3.2 → odx-0.3.3}/src/formats/dsistudio_odf8.rs +0 -0
  60. {odx-0.3.2 → odx-0.3.3}/src/formats/mat4.rs +0 -0
  61. {odx-0.3.2 → odx-0.3.3}/src/formats/mif.rs +0 -0
  62. {odx-0.3.2 → odx-0.3.3}/src/formats/mod.rs +0 -0
  63. {odx-0.3.2 → odx-0.3.3}/src/formats/mrtrix.rs +0 -0
  64. {odx-0.3.2 → odx-0.3.3}/src/formats/pam.rs +0 -0
  65. {odx-0.3.2 → odx-0.3.3}/src/formats/pam_repulsion724_hemisphere_f32.bin +0 -0
  66. {odx-0.3.2 → odx-0.3.3}/src/formats/pyafq_aodf.rs +0 -0
  67. {odx-0.3.2 → odx-0.3.3}/src/formats/tortoise_mapmri.rs +0 -0
  68. {odx-0.3.2 → odx-0.3.3}/src/graddev.rs +0 -0
  69. {odx-0.3.2 → odx-0.3.3}/src/header.rs +0 -0
  70. {odx-0.3.2 → odx-0.3.3}/src/icosphere.rs +0 -0
  71. {odx-0.3.2 → odx-0.3.3}/src/interop.rs +0 -0
  72. {odx-0.3.2 → odx-0.3.3}/src/io/archive_edit.rs +0 -0
  73. {odx-0.3.2 → odx-0.3.3}/src/io/directory.rs +0 -0
  74. {odx-0.3.2 → odx-0.3.3}/src/io/filename.rs +0 -0
  75. {odx-0.3.2 → odx-0.3.3}/src/io/mod.rs +0 -0
  76. {odx-0.3.2 → odx-0.3.3}/src/io/zip.rs +0 -0
  77. {odx-0.3.2 → odx-0.3.3}/src/lib.rs +0 -0
  78. {odx-0.3.2 → odx-0.3.3}/src/mmap_backing.rs +0 -0
  79. {odx-0.3.2 → odx-0.3.3}/src/mrtrix_sh.rs +0 -0
  80. {odx-0.3.2 → odx-0.3.3}/src/nifti_canon.rs +0 -0
  81. {odx-0.3.2 → odx-0.3.3}/src/nifti_export.rs +0 -0
  82. {odx-0.3.2 → odx-0.3.3}/src/odx_file.rs +0 -0
  83. {odx-0.3.2 → odx-0.3.3}/src/qc.rs +0 -0
  84. {odx-0.3.2 → odx-0.3.3}/src/reference_affine.rs +0 -0
  85. {odx-0.3.2 → odx-0.3.3}/src/sh_basis_evaluator.rs +0 -0
  86. {odx-0.3.2 → odx-0.3.3}/src/sh_basis_evaluator.rs.orig +0 -0
  87. {odx-0.3.2 → odx-0.3.3}/src/sphere_lookup.rs +0 -0
  88. {odx-0.3.2 → odx-0.3.3}/src/stream.rs +0 -0
  89. {odx-0.3.2 → odx-0.3.3}/src/template.rs +0 -0
  90. {odx-0.3.2 → odx-0.3.3}/src/transform/mod.rs +0 -0
  91. {odx-0.3.2 → odx-0.3.3}/src/transform/resample.rs +0 -0
  92. {odx-0.3.2 → odx-0.3.3}/src/transform/sh_apsf.rs +0 -0
  93. {odx-0.3.2 → odx-0.3.3}/src/transform/source_volume.rs +0 -0
  94. {odx-0.3.2 → odx-0.3.3}/src/transform/upsample.rs +0 -0
  95. {odx-0.3.2 → odx-0.3.3}/src/typed_view.rs +0 -0
  96. {odx-0.3.2 → odx-0.3.3}/src/validate.rs +0 -0
  97. {odx-0.3.2 → odx-0.3.3}/tests/archive_mutation.rs +0 -0
  98. {odx-0.3.2 → odx-0.3.3}/tests/cli.rs +0 -0
  99. {odx-0.3.2 → odx-0.3.3}/tests/common/mod.rs +0 -0
  100. {odx-0.3.2 → odx-0.3.3}/tests/fibgz_round_trip.rs +0 -0
  101. {odx-0.3.2 → odx-0.3.3}/tests/graddev.rs +0 -0
  102. {odx-0.3.2 → odx-0.3.3}/tests/interop.rs +0 -0
  103. {odx-0.3.2 → odx-0.3.3}/tests/load_options.rs +0 -0
  104. {odx-0.3.2 → odx-0.3.3}/tests/mif_reader.rs +0 -0
  105. {odx-0.3.2 → odx-0.3.3}/tests/mrtrix_io.rs +0 -0
  106. {odx-0.3.2 → odx-0.3.3}/tests/pam_io.rs +0 -0
  107. {odx-0.3.2 → odx-0.3.3}/tests/qc.rs +0 -0
  108. {odx-0.3.2 → odx-0.3.3}/tests/round_trip.rs +0 -0
  109. {odx-0.3.2 → odx-0.3.3}/tests/transform.rs +0 -0
  110. {odx-0.3.2 → odx-0.3.3}/tests/transform_mrtrix_compat.rs +0 -0
  111. {odx-0.3.2 → odx-0.3.3}/tests/upsample.rs +0 -0
@@ -220,7 +220,7 @@ jobs:
220
220
  # ─── macOS wheels (x86_64 + Apple Silicon) ─────────────────────────────────
221
221
  #
222
222
  # Runner mapping:
223
- # - aarch64 → macos-14 (Apple Silicon, free)
223
+ # - aarch64 → macos-15 (Apple Silicon; macos-14 runners are retired)
224
224
  # - x86_64 → macos-15-intel (Intel x86_64 on macOS 15; GitHub's
225
225
  # recommended migration path for x86_64
226
226
  # builds, supported until August 2027).
@@ -231,7 +231,7 @@ jobs:
231
231
  # macos-15-intel is the long tail.
232
232
  macos:
233
233
  name: macOS ${{ matrix.target }}
234
- runs-on: ${{ matrix.target == 'aarch64' && 'macos-14' || 'macos-15-intel' }}
234
+ runs-on: ${{ matrix.target == 'aarch64' && 'macos-15' || 'macos-15-intel' }}
235
235
  needs: [verify-version]
236
236
  strategy:
237
237
  fail-fast: false
@@ -1042,7 +1042,7 @@ dependencies = [
1042
1042
 
1043
1043
  [[package]]
1044
1044
  name = "odx-py"
1045
- version = "0.3.2"
1045
+ version = "0.3.3"
1046
1046
  dependencies = [
1047
1047
  "bytemuck",
1048
1048
  "ndarray",
@@ -1054,7 +1054,7 @@ dependencies = [
1054
1054
 
1055
1055
  [[package]]
1056
1056
  name = "odx-rs"
1057
- version = "0.3.2"
1057
+ version = "0.3.3"
1058
1058
  dependencies = [
1059
1059
  "assert_cmd",
1060
1060
  "bytemuck",
@@ -11,7 +11,7 @@ members = [".", "python"]
11
11
  # release workflow has a verify-version step that fails if the tag and
12
12
  # this value disagree.
13
13
  [workspace.package]
14
- version = "0.3.2"
14
+ version = "0.3.3"
15
15
  edition = "2021"
16
16
  # Bulk of the crate is dual MIT OR Apache-2.0 (the typical Rust ecosystem
17
17
  # license). Two files (src/peak_finder.rs, src/mrtrix_sh.rs) port code from
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odx
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: License :: OSI Approved :: Apache Software License
@@ -93,6 +93,30 @@ impl SpherePeakFinder {
93
93
  ///
94
94
  /// Returns peaks sorted by amplitude descending, capped at `npeaks`.
95
95
  pub fn find_peaks(&self, odf: &[f32]) -> Vec<(f32, [f32; 3])> {
96
+ self.find_peaks_capped(odf, self.config.npeaks)
97
+ }
98
+
99
+ /// Greedy separation filter over `(amplitude, direction)` candidates sorted by
100
+ /// amplitude, descending (antipodal symmetry: `|dot|`), keeping at most `cap`.
101
+ fn separate(&self, candidates: impl IntoIterator<Item = (f32, [f32; 3])>, cap: usize)
102
+ -> Vec<(f32, [f32; 3])> {
103
+ let mut accepted: Vec<(f32, [f32; 3])> = Vec::new();
104
+ 'candidate: for (value, dir) in candidates {
105
+ for &(_, acc_dir) in &accepted {
106
+ let dot = dir[0] * acc_dir[0] + dir[1] * acc_dir[1] + dir[2] * acc_dir[2];
107
+ if dot.abs() > self.cos_min_sep {
108
+ continue 'candidate;
109
+ }
110
+ }
111
+ accepted.push((value, dir));
112
+ if accepted.len() >= cap {
113
+ break;
114
+ }
115
+ }
116
+ accepted
117
+ }
118
+
119
+ fn find_peaks_capped(&self, odf: &[f32], cap: usize) -> Vec<(f32, [f32; 3])> {
96
120
  assert_eq!(
97
121
  odf.len(),
98
122
  self.vertices.len(),
@@ -126,22 +150,7 @@ impl SpherePeakFinder {
126
150
  .collect();
127
151
 
128
152
  // Step 3: greedy separation-angle filter (antipodal symmetry: use |dot|).
129
- let mut accepted: Vec<(f32, [f32; 3])> = Vec::new();
130
- 'candidate: for (value, idx) in candidates {
131
- let dir = self.vertices[idx];
132
- for &(_, acc_dir) in &accepted {
133
- let dot = dir[0] * acc_dir[0] + dir[1] * acc_dir[1] + dir[2] * acc_dir[2];
134
- if dot.abs() > self.cos_min_sep {
135
- continue 'candidate;
136
- }
137
- }
138
- accepted.push((value, dir));
139
- if accepted.len() >= self.config.npeaks {
140
- break;
141
- }
142
- }
143
-
144
- accepted
153
+ self.separate(candidates.into_iter().map(|(v, idx)| (v, self.vertices[idx])), cap)
145
154
  }
146
155
 
147
156
  /// Refine a vertex-quantized seed direction to sub-vertex precision by
@@ -244,17 +253,20 @@ impl SpherePeakFinder {
244
253
  /// Like [`find_peaks`], but refines each accepted peak to sub-vertex
245
254
  /// precision using the original SH coefficients.
246
255
  ///
247
- /// Local-maxima detection, threshold filtering, and separation-angle
248
- /// pruning all run on the discrete `odf` (so the set of accepted peaks
249
- /// is identical to [`find_peaks`]). Each accepted seed is then nudged
250
- /// off-vertex by [`Self::refine_with_sh`].
256
+ /// Local maxima, the relative threshold and the separation angle are found
257
+ /// on the discrete `odf`, without the `npeaks` cap; each seed is then nudged
258
+ /// off-vertex by [`Self::refine_with_sh`], and the separation filter and the
259
+ /// cap run again on the refined peaks. The second pass matters: the search
260
+ /// sphere is a hemisphere whose mesh is not joined across its boundary, so
261
+ /// a lobe near the boundary can seed two maxima more than the separation
262
+ /// angle apart, and refinement takes both onto the same axis.
251
263
  pub fn find_peaks_with_sh(
252
264
  &self,
253
265
  odf: &[f32],
254
266
  sh: &[f32],
255
267
  basis: ShBasisKind,
256
268
  ) -> Vec<(f32, [f32; 3])> {
257
- let seeds = self.find_peaks(odf);
269
+ let seeds = self.find_peaks_capped(odf, usize::MAX);
258
270
  let mut out = Vec::with_capacity(seeds.len());
259
271
  for (_seed_amp, seed_dir) in seeds {
260
272
  let (dir, amp) = self.refine_with_sh(seed_dir, sh, basis);
@@ -269,7 +281,8 @@ impl SpherePeakFinder {
269
281
  out.push((amp, dir));
270
282
  }
271
283
  }
272
- out
284
+ out.sort_by(|a, b| b.0.total_cmp(&a.0));
285
+ self.separate(out, self.config.npeaks)
273
286
  }
274
287
 
275
288
  /// Batch version of [`find_peaks`].
@@ -674,6 +687,93 @@ mod tests {
674
687
  }
675
688
  }
676
689
 
690
+ #[test]
691
+ fn find_peaks_with_sh_keeps_one_peak_per_lobe_near_the_hemisphere_edge() {
692
+ // The search sphere is a hemisphere whose mesh is not joined across its
693
+ // boundary, so one lobe near the boundary shows up as a discrete local
694
+ // maximum on both sides. The two seeds can sit more than the separation
695
+ // angle apart, and refinement then takes both to the same axis (often
696
+ // with opposite signs). A single fibre must still give one peak.
697
+ use crate::mrtrix_sh::{sh2amp_cart, RowSamplePlan};
698
+ let finder = SpherePeakFinder::for_dsistudio_odf8(PeakFinderConfig {
699
+ npeaks: 5,
700
+ relative_peak_threshold: 0.5,
701
+ min_separation_angle_deg: 25.0,
702
+ });
703
+ let lmax = 8;
704
+ let basis = ShBasisKind::MrtrixTournier { lmax };
705
+ let mut worst = 0usize;
706
+ let mut n_bad = 0usize;
707
+ for el_deg in [-8.0_f32, -4.0, -1.0, 0.0, 1.0, 4.0, 8.0] {
708
+ for k in 0..36 {
709
+ let az = (k as f32 * 10.0 + 3.0).to_radians();
710
+ let el = el_deg.to_radians();
711
+ let dir = [el.cos() * az.cos(), el.cos() * az.sin(), el.sin()];
712
+ // Rotate so the fibre lies near every part of the boundary,
713
+ // whatever axis the hemisphere is cut along.
714
+ for d in [dir, [dir[2], dir[0], dir[1]], [dir[1], dir[2], dir[0]]] {
715
+ let sh: Vec<f32> = sh2amp_cart(&[d], lmax).iter().copied().collect();
716
+ let plan = RowSamplePlan::for_sh_rows_nonnegative(finder.vertices(), sh.len())
717
+ .unwrap();
718
+ let odf = plan.apply_row(&sh);
719
+ let peaks = finder.find_peaks_with_sh(&odf, &sh, basis);
720
+ if peaks.len() != 1 {
721
+ n_bad += 1;
722
+ worst = worst.max(peaks.len());
723
+ }
724
+ let errs: Vec<f32> = peaks.iter().map(|(_, p)| angle_between_deg(*p, d)).collect();
725
+ if peaks.len() != 1 {
726
+ eprintln!("d={d:?} n={} errs={errs:?} pairwise={:?}", peaks.len(),
727
+ peaks.iter().skip(1).map(|(_, p)| angle_between_deg(*p, peaks[0].1)).collect::<Vec<_>>());
728
+ }
729
+ assert!(errs.iter().all(|e| *e < 5.0), "peak off the fibre: {errs:?}");
730
+ }
731
+ }
732
+ }
733
+ assert_eq!(n_bad, 0, "{n_bad} single fibres gave more than one peak (up to {worst})");
734
+ }
735
+
736
+ #[test]
737
+ fn find_peaks_with_sh_never_returns_two_peaks_on_one_axis() {
738
+ // WM fODF rows from CONSH (cs_dmri 0.3.0, lmax 8) where the exported peaks
739
+ // held the same axis twice: two seeds more than 25 deg apart refined onto
740
+ // one lobe, often with opposite signs.
741
+ const ROWS: &[&str] = &[
742
+ "0.020001 3.70544e-05 0.000196457 -0.000274297 -0.000669049 -0.000114257 0.000340161 -0.000152604 0.00012367 -1.47892e-08 -0.000129584 4.10546e-05 0.00027226 -0.000124367 5.74178e-05 3.87874e-05 9.05643e-05 -1.58243e-05 -1.25684e-05 -7.64095e-05 -5.5034e-05 6.98173e-05 4.2683e-05 -6.40127e-05 -6.85215e-05 -1.74923e-05 3.44863e-06 2.48358e-05 -1.9128e-05 -9.33313e-07 -1.09802e-05 5.76294e-06 -5.86146e-06 -1.27285e-05 -6.2177e-06 5.6455e-06 -1.8438e-05 -2.31331e-05 -1.5045e-06 -2.2925e-05 -7.64343e-06 1.73684e-05 -1.12061e-05 1.50676e-05 -1.14419e-05",
743
+ "0.00509206 -0.00394427 -0.0026448 -0.00204412 0.00408692 0.0036147 -0.00174032 -0.00232267 -0.000869546 0.000881936 -0.000435028 -0.00195087 -0.000935052 -4.33417e-05 0.000525665 -0.000460918 -0.00029834 4.3253e-05 0.000240121 0.000428127 0.000159857 0.000146224 0.000199289 0.000139787 -0.000484234 -0.000750837 -0.000163946 0.000216163 -0.000107683 -7.20465e-05 7.32629e-05 9.57164e-05 -8.02023e-07 1.50129e-05 4.11548e-05 -3.06238e-05 6.22351e-05 -2.61229e-05 -2.96008e-05 0.000119038 5.45087e-05 -8.04789e-05 -5.56021e-05 2.83407e-05 3.90552e-05",
744
+ "0.0166295 0.00653633 -0.00569082 -0.00193573 -0.00942998 0.0124597 0.00272468 -0.00243812 0.00178219 -0.000953129 -0.000677992 0.0004924 -0.00144635 3.00494e-06 0.00318468 -8.45897e-05 0.000567782 -0.000520075 0.000466809 -0.000436867 0.000103431 -0.000347938 -0.000236118 -0.000412912 0.000456039 -0.000427854 0.000722666 0.000201158 -0.000315116 0.000484258 -0.00022278 9.19441e-05 -0.000417422 0.000285048 7.92924e-06 3.89435e-06 -0.0001305 0.000129701 -1.90785e-05 -0.000125447 0.000222657 -0.000193764 5.43387e-05 8.78768e-05 -2.45091e-05",
745
+ "0.0204613 -0.00066584 0.00197433 0.00222784 -0.00180702 0.00102427 -0.000411757 -1.2118e-05 0.000273079 7.08231e-05 0.000471344 -0.000657725 0.000382965 -0.00020176 -0.000729273 3.91606e-05 -0.000202432 -0.000244344 0.000108551 1.98104e-05 0.000170449 -3.65766e-05 -6.70476e-05 6.62493e-05 0.000162324 9.92119e-05 -5.86277e-05 0.000157406 -3.26614e-05 -4.73648e-05 1.84313e-05 -2.99623e-06 -7.01198e-05 1.99084e-05 -7.38982e-06 2.77363e-05 2.28139e-05 -2.69119e-05 -4.31075e-05 7.03224e-06 -7.8422e-06 1.1047e-05 3.96872e-05 -8.43552e-06 -5.75591e-05",
746
+ "0.00713807 0.00339079 -0.00103044 -0.0061386 -0.000826552 0.0082003 0.00190633 -0.00198514 -0.00140997 0.00109988 0.00331778 0.000812522 -0.00393889 0.000462763 0.0019602 -0.000102241 -0.000762335 -0.000470507 0.00120132 0.000369221 -0.000609059 -0.00113804 -0.000404316 0.00139879 -0.000316085 -0.000765427 0.000707538 -7.7881e-05 -0.000148948 -5.6335e-05 -1.18645e-05 0.000239571 5.7593e-05 -0.000329128 -2.99322e-05 0.000165437 0.000189011 8.75542e-05 -0.000278514 0.000104215 0.000190993 -0.000222191 -6.21878e-05 0.000128678 -3.36133e-05",
747
+ "0.00911124 0.0042636 -0.00250162 -0.00292044 -0.00642482 0.00798826 0.00157445 -0.00195749 0.00115058 -0.000117568 2.75139e-05 0.000873749 -0.000624711 -0.00126921 0.00167151 -0.000551438 0.000200251 0.000173153 -0.00054223 0.00035316 0.000187881 -0.000489381 0.000230142 0.000361385 0.000153043 -0.000342866 -8.28056e-05 0.000193832 -0.000325103 0.000195456 -3.17966e-05 -3.89624e-05 9.31491e-05 -8.7443e-05 -5.91938e-05 6.76002e-05 5.93242e-05 1.96581e-05 -4.39012e-05 6.68431e-05 -6.04483e-05 -2.98802e-06 8.03673e-05 -0.00014283 0.000149106",
748
+ "0.0207078 -0.00912826 -0.000280418 -0.00345379 0.00851847 0.00280469 -0.000571926 -0.00113182 -3.22305e-05 -0.000560326 -0.000178424 -0.000903974 -0.00274634 0.000206762 0.00127298 -0.00059195 -0.000280693 0.000474782 7.13967e-05 -0.00011802 0.000244228 -5.81581e-05 6.46518e-05 0.000119734 6.28979e-06 -0.000311702 0.000148417 -0.000156499 7.89762e-05 0.000181594 -0.000104104 -0.00038818 0.000126554 1.05881e-06 -3.73684e-05 -5.1545e-05 3.69212e-05 6.98023e-05 -0.000193472 -1.69508e-05 0.000354341 7.27828e-05 -5.16985e-05 -0.000145303 7.49501e-05",
749
+ "0.00858252 -0.00204651 0.000317345 0.00182916 -0.00456516 0.00489997 -0.000985689 0.000126664 -0.000217271 -3.43666e-06 0.00039176 -9.36268e-05 0.000310303 -0.000137112 0.000698595 -1.80399e-05 -0.000300732 4.15087e-05 6.39551e-05 -0.000152642 0.000250385 -4.19572e-05 9.95349e-05 -8.17123e-06 9.71228e-05 -0.000101752 0.000169359 -3.17502e-05 7.00833e-05 -5.8298e-05 -4.24459e-05 5.37188e-06 1.8318e-06 2.58761e-05 1.25461e-06 0.000125873 3.5538e-05 4.7104e-05 -4.9865e-05 2.20606e-05 3.27844e-05 -5.69153e-05 9.91125e-05 -5.7064e-05 1.86506e-05",
750
+ "0.0271781 0.00204935 0.00128115 -0.000257789 0.000200468 -0.00067315 0.000330585 -9.49055e-05 -0.000119205 -0.000696796 -0.000278246 -0.000551226 0.000259925 -0.000254365 0.000494597 -2.04616e-05 -0.000126276 8.02324e-05 0.000104759 -9.09473e-05 6.75586e-05 0.000148725 2.83542e-05 -0.000143331 8.69828e-05 0.000104617 -0.000211406 1.36491e-05 3.60502e-05 -2.75081e-05 3.7928e-05 -9.66078e-06 -6.78423e-06 4.45139e-05 -3.0848e-05 5.87303e-05 -1.25989e-05 -2.52647e-05 -1.97211e-05 -3.77388e-05 4.78251e-06 6.28862e-05 4.76508e-05 -1.58487e-05 6.55682e-05",
751
+ "0.0106931 -0.00175415 0.00586164 -0.00190705 -0.00390398 -0.00210425 -0.000223556 -0.000121788 -0.00057407 -0.000161456 0.000923586 0.000336871 0.000645904 -0.000428887 -0.000440656 -1.48008e-05 -0.00021295 -0.000148304 0.000154047 0.000109541 0.000475584 -0.000665469 0.000110946 -0.000441458 -0.000318587 1.52103e-05 0.000135829 -0.000103869 2.67965e-05 5.74638e-05 -3.30177e-06 -8.46844e-05 -9.90469e-05 -0.000132889 8.73883e-05 0.000103447 0.000255929 -3.81133e-05 -1.91169e-05 -0.000209072 0.00014204 2.77721e-05 7.39294e-05 0.000119412 1.07416e-05",
752
+ ];
753
+ use crate::mrtrix_sh::RowSamplePlan;
754
+ let finder = SpherePeakFinder::for_dsistudio_odf8(PeakFinderConfig {
755
+ npeaks: 5,
756
+ relative_peak_threshold: 0.5,
757
+ min_separation_angle_deg: 25.0,
758
+ });
759
+ let basis = ShBasisKind::MrtrixTournier { lmax: 8 };
760
+ let plan = RowSamplePlan::for_sh_rows_nonnegative(finder.vertices(), 45).unwrap();
761
+ let mut n_dup = 0;
762
+ for row in ROWS {
763
+ let sh: Vec<f32> = row.split_whitespace().map(|x| x.parse().unwrap()).collect();
764
+ let odf = plan.apply_row(&sh);
765
+ let peaks = finder.find_peaks_with_sh(&odf, &sh, basis);
766
+ for i in 0..peaks.len() {
767
+ for j in i + 1..peaks.len() {
768
+ if angle_between_deg(peaks[i].1, peaks[j].1) < 25.0 {
769
+ n_dup += 1;
770
+ }
771
+ }
772
+ }
773
+ }
774
+ assert_eq!(n_dup, 0, "{n_dup} peak pairs closer than the separation angle");
775
+ }
776
+
677
777
  #[test]
678
778
  fn refine_with_sh_on_flat_odf_returns_finite() {
679
779
  // Pure isotropic SH (lmax=0): no real peak, but refinement must
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes