nltools 0.6.0.dev0__tar.gz → 0.6.0.dev2__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 (162) hide show
  1. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/.gitignore +9 -4
  2. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/PKG-INFO +1 -1
  3. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/alignment/procrustes.py +69 -75
  4. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/alignment/srm.py +78 -48
  5. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/backends.py +31 -9
  6. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/corrections.py +71 -18
  7. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/decoding.py +4 -2
  8. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/bootstrap.py +55 -7
  9. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/correlation.py +13 -5
  10. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/intersubject.py +27 -27
  11. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/isc.py +359 -229
  12. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/one_sample.py +6 -2
  13. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/timeseries.py +6 -3
  14. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/two_sample.py +6 -2
  15. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/neighborhoods.py +7 -1
  16. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/outliers.py +52 -10
  17. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/regression.py +27 -8
  18. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/signal.py +97 -29
  19. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/similarity.py +10 -10
  20. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/cross_validation.py +12 -2
  21. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/__init__.py +6 -4
  22. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/adjacency/__init__.py +88 -4
  23. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/adjacency/io.py +7 -2
  24. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/adjacency/modeling.py +31 -13
  25. nltools-0.6.0.dev2/nltools/data/adjacency/plotting.py +395 -0
  26. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/adjacency/state.py +16 -6
  27. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/adjacency/stats.py +17 -8
  28. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/atlases/reporting.py +18 -15
  29. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/__init__.py +139 -56
  30. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/analysis.py +155 -89
  31. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/bootstrap.py +44 -16
  32. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/io.py +293 -97
  33. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/modeling.py +161 -64
  34. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/plotting.py +38 -4
  35. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/prediction.py +59 -43
  36. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/utils.py +162 -30
  37. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/validation.py +39 -0
  38. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/designmatrix/__init__.py +10 -0
  39. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/designmatrix/diagnostics.py +7 -3
  40. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/designmatrix/io.py +84 -41
  41. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/designmatrix/plotting.py +2 -4
  42. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/designmatrix/regressors.py +42 -5
  43. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/designmatrix/transforms.py +39 -34
  44. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/ownership.py +22 -0
  45. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/results.py +99 -6
  46. nltools-0.6.0.dev2/nltools/data/results_io.py +218 -0
  47. nltools-0.6.0.dev2/nltools/data/roc/__init__.py +604 -0
  48. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/simulator/__init__.py +74 -50
  49. nltools-0.6.0.dev2/nltools/data/simulator/haxby.py +283 -0
  50. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/validation.py +11 -10
  51. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/datasets.py +15 -5
  52. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/io/h5.py +125 -14
  53. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/mask.py +26 -32
  54. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/models/results.py +36 -1
  55. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/models/ridge.py +56 -26
  56. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/plotting/__init__.py +1 -2
  57. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/plotting/adjacency.py +10 -68
  58. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/plotting/brain.py +22 -23
  59. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/plotting/decomposition.py +4 -0
  60. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/plotting/prediction.py +3 -3
  61. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/templates/matching.py +8 -5
  62. nltools-0.6.0.dev2/nltools/tests/core/test_algorithms/test_corrections.py +127 -0
  63. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/test_decoding.py +13 -0
  64. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/test_intersubject.py +29 -0
  65. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/test_neighborhoods.py +15 -0
  66. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/test_outliers.py +60 -0
  67. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/test_procrustes.py +69 -305
  68. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/test_regression.py +47 -0
  69. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/test_signal.py +47 -0
  70. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/test_similarity.py +48 -0
  71. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_backends.py +28 -23
  72. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_bootstrap.py +73 -0
  73. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_cross_validation.py +24 -0
  74. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_correlation.py +29 -0
  75. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_cpu_parallelization.py +5 -41
  76. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_isc_group.py +53 -0
  77. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_isc_vocabulary.py +22 -2
  78. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_one_sample.py +12 -0
  79. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_timeseries.py +44 -0
  80. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_two_sample.py +12 -0
  81. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_isc.py +96 -37
  82. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_mask.py +82 -3
  83. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_srm.py +25 -1
  84. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/datasets/test_datasets.py +97 -4
  85. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/io_tests/test_h5.py +54 -0
  86. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/models/test_ridge.py +11 -5
  87. nltools-0.6.0.dev2/nltools/tests/plotting/test_adjacency.py +392 -0
  88. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/plotting/test_surface.py +49 -0
  89. nltools-0.6.0.dev2/nltools/tests/support/test_scripts.py +105 -0
  90. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/templates/test_brainspace.py +12 -0
  91. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/pyproject.toml +10 -9
  92. nltools-0.6.0.dev0/nltools/data/adjacency/plotting.py +0 -174
  93. nltools-0.6.0.dev0/nltools/data/roc/__init__.py +0 -398
  94. nltools-0.6.0.dev0/nltools/data/simulator/haxby.py +0 -124
  95. nltools-0.6.0.dev0/nltools/tests/core/test_algorithms/test_corrections.py +0 -68
  96. nltools-0.6.0.dev0/nltools/tests/plotting/test_adjacency.py +0 -184
  97. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/LICENSE +0 -0
  98. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/README.md +0 -0
  99. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/__init__.py +0 -0
  100. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/__init__.py +0 -0
  101. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/alignment/__init__.py +0 -0
  102. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/__init__.py +0 -0
  103. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/matrix.py +0 -0
  104. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/random.py +0 -0
  105. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/utils.py +0 -0
  106. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/inference/validation.py +0 -0
  107. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/algorithms/validation.py +0 -0
  108. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/adjacency/utils.py +0 -0
  109. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/atlases/__init__.py +0 -0
  110. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/atlases/labeling.py +0 -0
  111. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/atlases/loading.py +0 -0
  112. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/atlases/registry.py +0 -0
  113. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/viewer.js +0 -0
  114. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/braindata/viewer.py +0 -0
  115. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/combine.py +0 -0
  116. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/designmatrix/append.py +0 -0
  117. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/data/designmatrix/utils.py +0 -0
  118. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/io/__init__.py +0 -0
  119. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/io/events.py +0 -0
  120. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/models/__init__.py +0 -0
  121. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/models/glm.py +0 -0
  122. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/models/validation.py +0 -0
  123. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/resources/covariates_example.csv +0 -0
  124. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/resources/onsets_example.csv +0 -0
  125. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/templates/__init__.py +0 -0
  126. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/templates/config.py +0 -0
  127. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/templates/fetch.py +0 -0
  128. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/templates/paths.py +0 -0
  129. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/templates/registry.py +0 -0
  130. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/conftest.py +0 -0
  131. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/__init__.py +0 -0
  132. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/__init__.py +0 -0
  133. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_algorithms/conftest.py +0 -0
  134. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_gpu_policy.py +0 -0
  135. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_hyperalignment.py +0 -0
  136. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/__init__.py +0 -0
  137. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_api_conventions.py +0 -0
  138. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_matrix.py +0 -0
  139. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_progress_bar.py +0 -0
  140. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_tail_vocabulary.py +0 -0
  141. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_inference/test_utils.py +0 -0
  142. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/core/test_utils.py +0 -0
  143. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/datasets/__init__.py +0 -0
  144. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/io_tests/__init__.py +0 -0
  145. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/io_tests/test_file_reader.py +0 -0
  146. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/models/__init__.py +0 -0
  147. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/models/conftest.py +0 -0
  148. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/models/test_glm.py +0 -0
  149. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/models/test_glm_warnings.py +0 -0
  150. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/models/test_results.py +0 -0
  151. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/plotting/__init__.py +0 -0
  152. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/plotting/test_f123_prediction.py +0 -0
  153. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/pyodide/.gitignore +0 -0
  154. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/pyodide/test_runner.mjs +0 -0
  155. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/support/__init__.py +0 -0
  156. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/support/test_designation.py +0 -0
  157. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/templates/__init__.py +0 -0
  158. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/templates/test_fetch_pyodide.py +0 -0
  159. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/utils/__init__.py +0 -0
  160. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/tests/utils/test_utils.py +0 -0
  161. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/utils.py +0 -0
  162. {nltools-0.6.0.dev0 → nltools-0.6.0.dev2}/nltools/version.py +0 -0
@@ -78,10 +78,15 @@ scratch/
78
78
  # Zensical site output
79
79
  /site/
80
80
 
81
- # Tutorial pages: build artifacts generated from the marimo notebooks beside
82
- # them by scripts/marimo_to_zensical.py (`uv run poe docs-generate`). The
83
- # patterns match the notebooks (scripts/marimo_to_zensical.py TUTORIAL_GLOBS),
84
- # so a hand-written page in either directory is still tracked.
81
+ # A locally built wheel staged for the quickstart page's ```pyodide cells to
82
+ # install from, when testing a version that is not on PyPI yet.
83
+ docs/_static/wheels/
84
+
85
+ # Generated pages: build artifacts written from the marimo notebooks beside them
86
+ # by scripts/marimo_to_zensical.py (`uv run poe docs-generate`). The patterns
87
+ # match the notebooks (scripts/marimo_to_zensical.py NOTEBOOK_GLOBS), so a
88
+ # hand-written page in any of these directories is still tracked.
89
+ docs/quickstart.md
85
90
  docs/tutorials/basics/[0-9]*_*.md
86
91
  docs/tutorials/data-operations/[0-9]*_*.md
87
92
  docs/tutorials/analysis/[0-9]*_*.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nltools
3
- Version: 0.6.0.dev0
3
+ Version: 0.6.0.dev2
4
4
  Summary: A Python package to analyze neuroimaging data
5
5
  Project-URL: Homepage, https://nltools.org
6
6
  Author-email: "Luke J. Chang" <luke.j.chang@dartmouth.edu>, Eshin Jolly <eshin.jolly@gmail.com>
@@ -109,6 +109,23 @@ def _hyperalign(data, n_iter):
109
109
  return aligned, transformation_matrix, template, disparity, scale
110
110
 
111
111
 
112
+ def _procrustes_similarity(mat1, mat2):
113
+ """One minus scipy's Procrustes disparity, the only value inference needs.
114
+
115
+ Module level so `loky` can pickle it, and returning the scalar rather than
116
+ scipy's full tuple keeps the two transformed matrices out of the payload
117
+ every permutation ships back to the parent process.
118
+
119
+ Args:
120
+ mat1 (np.ndarray): Reference matrix, `(n_rows, n_cols)`.
121
+ mat2 (np.ndarray): Matrix fit to `mat1`, same shape.
122
+
123
+ Returns:
124
+ float: `1 - disparity`, higher meaning more similar.
125
+ """
126
+ return 1 - procrust(mat1, mat2)[2]
127
+
128
+
112
129
  def align(
113
130
  data,
114
131
  method="deterministic_srm",
@@ -134,7 +151,8 @@ def align(
134
151
  method (str): One of `'probabilistic_srm'`, `'deterministic_srm'`, or
135
152
  `'procrustes'`. Defaults to `'deterministic_srm'`.
136
153
  n_features (int | None): Number of features in the common space (SRM only).
137
- None uses the number of voxels. Must be None for `'procrustes'`.
154
+ None uses the number of voxels, and may not exceed any subject's
155
+ voxel count. Must be None for `'procrustes'`.
138
156
  axis (int): Axis to align on: 0 aligns timepoints (ISC computed per voxel),
139
157
  1 aligns voxels (ISC computed per timepoint). Defaults to 0.
140
158
  n_iter (int): Number of `_SRM`/`_DetSRM` iterations; ignored by
@@ -153,9 +171,14 @@ def align(
153
171
 
154
172
  Raises:
155
173
  ValueError: If `data` is not a same-typed list, `method` or `axis` is
156
- unknown, or `method='procrustes'` is combined with `axis=1` on
174
+ unknown, `method='procrustes'` is combined with `axis=1` on
157
175
  `BrainData` input — that transform spans images on both axes and has
158
- no voxel axis to be returned on.
176
+ no voxel axis to be returned on — or `method='procrustes'` is given
177
+ `BrainData` subjects with different voxel counts, whose zero-padded
178
+ results would not fit their own masks. Pass the subjects' `.data`
179
+ arrays to get the zero-padded result instead; it has no mask that
180
+ could describe it. An SRM `n_features` above any subject's voxel
181
+ count also raises.
159
182
 
160
183
  Examples:
161
184
  ```python
@@ -173,7 +196,7 @@ def align(
173
196
  ```
174
197
  """
175
198
 
176
- from nltools.data import BrainData, Adjacency
199
+ from nltools.data import BrainData
177
200
 
178
201
  if not isinstance(data, list):
179
202
  raise ValueError("Make sure you are inputting data is a list.")
@@ -185,6 +208,7 @@ def align(
185
208
  )
186
209
 
187
210
  if isinstance(data[0], BrainData):
211
+ from nltools.data.braindata.analysis import _brain_result
188
212
  from nltools.data.braindata.utils import _result_from_array
189
213
 
190
214
  data_type = "BrainData"
@@ -251,18 +275,22 @@ def align(
251
275
 
252
276
  if data_type == "BrainData":
253
277
  if method == "procrustes":
278
+ # `_hyperalign` zero-pads every subject's feature axis up to the
279
+ # widest subject, so a narrower subject's result is wider than its
280
+ # own mask. `_brain_result` refuses that rather than returning an
281
+ # object whose `to_nifti` fails later.
254
282
  out["transformed"] = [
255
- _result_from_array(source, values.T, rows="preserve")
283
+ _brain_result(source, values.T, "transformed", rows="preserve")
256
284
  for source, values in zip(sources, out["transformed"])
257
285
  ]
258
- out["common_model"] = _result_from_array(
259
- sources[0], out["common_model"], rows="clear"
286
+ out["common_model"] = _brain_result(
287
+ sources[0], out["common_model"], "common_model", rows="clear"
260
288
  )
261
289
  # `_hyperalign` already returns these in the
262
290
  # `transformed = original @ T` orientation, and they are square on
263
291
  # the voxel axis, so unlike the SRM matrices they are wrapped as-is.
264
292
  out["transformation_matrix"] = [
265
- _result_from_array(source, values, rows="clear")
293
+ _brain_result(source, values, "transformation_matrix", rows="clear")
266
294
  for source, values in zip(sources, out["transformation_matrix"])
267
295
  ]
268
296
  else:
@@ -281,67 +309,28 @@ def align(
281
309
  # BrainData: (timepoints, voxels)
282
310
  # numpy: (voxels, timepoints)
283
311
 
284
- a = Adjacency()
312
+ # For procrustes, transformed holds BrainData objects; extract .data.
313
+ # For SRM methods it already holds numpy arrays.
314
+ transformed_arrays = [
315
+ x.data if isinstance(x, BrainData) else x for x in out["transformed"]
316
+ ]
317
+
318
+ # Put every case in one orientation, (aligned units, observations), so the
319
+ # correlation below reads the same way whatever came in. BrainData results
320
+ # are (timepoints, voxels) and numpy results are (voxels, timepoints), so
321
+ # exactly one of the two needs a transpose for a given axis.
322
+ if (data_type == "BrainData") == (axis == 0):
323
+ units = [x.T for x in transformed_arrays]
324
+ else:
325
+ units = transformed_arrays
285
326
 
286
- if data_type == "BrainData":
287
- # BrainData transformed shape: (timepoints, voxels)
288
- # For procrustes, transformed contains BrainData objects; extract .data
289
- # For SRM methods, transformed contains numpy arrays after the .T
290
- transformed_arrays = [
291
- x.data if isinstance(x, BrainData) else x for x in out["transformed"]
292
- ]
293
- if axis == 0:
294
- # Aligned timepoints → ISC per voxel (correlation over time)
295
- n_isc = transformed_arrays[0].shape[1] # n_voxels
296
- for v in range(n_isc):
297
- # Extract timecourse for voxel v from each subject
298
- isc_data = np.array([x[:, v] for x in transformed_arrays])
299
- a = a.append(
300
- Adjacency(
301
- 1 - pairwise_distances(isc_data, metric="correlation"),
302
- matrix_type="similarity",
303
- )
304
- )
305
- else: # axis == 1
306
- # Aligned voxels → ISC per timepoint (spatial correlation)
307
- n_isc = transformed_arrays[0].shape[0] # n_timepoints
308
- for t in range(n_isc):
309
- # Extract spatial pattern at timepoint t from each subject
310
- isc_data = np.array([x[t, :] for x in transformed_arrays])
311
- a = a.append(
312
- Adjacency(
313
- 1 - pairwise_distances(isc_data, metric="correlation"),
314
- matrix_type="similarity",
315
- )
316
- )
317
- else: # numpy
318
- # numpy transformed shape: (voxels, timepoints)
319
- if axis == 0:
320
- # Aligned timepoints → ISC per voxel (correlation over time)
321
- n_isc = out["transformed"][0].shape[0] # n_voxels
322
- for v in range(n_isc):
323
- # Extract timecourse for voxel v from each subject
324
- isc_data = np.array([x[v, :] for x in out["transformed"]])
325
- a = a.append(
326
- Adjacency(
327
- 1 - pairwise_distances(isc_data, metric="correlation"),
328
- matrix_type="similarity",
329
- )
330
- )
331
- else: # axis == 1
332
- # Aligned voxels → ISC per timepoint (spatial correlation)
333
- n_isc = out["transformed"][0].shape[1] # n_timepoints
334
- for t in range(n_isc):
335
- # Extract spatial pattern at timepoint t from each subject
336
- isc_data = np.array([x[:, t] for x in out["transformed"]])
337
- a = a.append(
338
- Adjacency(
339
- 1 - pairwise_distances(isc_data, metric="correlation"),
340
- matrix_type="similarity",
341
- )
342
- )
343
-
344
- out["isc"] = dict(zip(np.arange(n_isc), a.mean(axis=1)))
327
+ upper_triangle = np.triu_indices(len(units), k=1)
328
+ out["isc"] = {}
329
+ for unit in range(units[0].shape[0]):
330
+ similarity = 1 - pairwise_distances(
331
+ np.array([x[unit] for x in units]), metric="correlation"
332
+ )
333
+ out["isc"][unit] = float(np.nanmean(similarity[upper_triangle]))
345
334
 
346
335
  return out
347
336
 
@@ -478,14 +467,12 @@ def procrustes_distance(
478
467
  # the SAME scale. Previously the observed disparity was compared against a
479
468
  # null of similarities, inverting the scales and yielding p ~ 1 for
480
469
  # near-identical matrices.
481
- _, _, disparity = procrust(mat1, mat2)
482
- observed_similarity = 1 - disparity
470
+ observed_similarity = _procrustes_similarity(mat1, mat2)
483
471
 
484
- null_disparities = Parallel(n_jobs=n_jobs)(
485
- delayed(procrust)(random_state.permutation(mat1), mat2)
472
+ null_similarity = Parallel(n_jobs=n_jobs)(
473
+ delayed(_procrustes_similarity)(random_state.permutation(mat1), mat2)
486
474
  for _ in range(n_permute)
487
475
  )
488
- null_similarity = [1 - x[2] for x in null_disparities]
489
476
 
490
477
  # Use _compute_pvalue from inference module (signature: obs_stat, null_dist, tail)
491
478
  stats = {"similarity": float(observed_similarity)}
@@ -521,7 +508,8 @@ def align_states(
521
508
  reordered data. Defaults to False.
522
509
  replace_zero_variance (bool): Replace zero-variance columns with uniform
523
510
  random numbers before computing distances; avoids NaNs with the
524
- correlation metric. Defaults to False.
511
+ correlation metric. Integer inputs are converted to float so the
512
+ replacement noise survives. Defaults to False.
525
513
 
526
514
  Returns:
527
515
  np.ndarray: If `return_index=False` (default), `target[:, remapping]` — the
@@ -541,12 +529,18 @@ def align_states(
541
529
  Prevents NaN values when correlation-based distance metrics encounter
542
530
  constant columns.
543
531
 
532
+ The array is converted to float first: writing U(0, 1) draws into an
533
+ integer array truncates every one of them to zero, leaving the constant
534
+ column constant and the correlation distance NaN.
535
+
544
536
  Args:
545
537
  data (np.ndarray): 2-D array whose columns are checked for zero variance.
546
538
 
547
539
  Returns:
548
- np.ndarray: Array with zero-variance columns replaced by U(0, 1) values.
540
+ np.ndarray: Float array with zero-variance columns replaced by
541
+ U(0, 1) values.
549
542
  """
543
+ data = np.asarray(data, dtype=float)
550
544
  if np.any(data.std(axis=0) == 0):
551
545
  for i in np.where(data.std(axis=0) == 0)[0]:
552
546
  data[:, i] = np.random.uniform(low=0, high=1, size=data.shape[0])
@@ -104,6 +104,62 @@ def _init_w_transforms(
104
104
  return w, voxels
105
105
 
106
106
 
107
+ def _check_n_features(X: list[np.ndarray], n_features: int) -> None:
108
+ """Reject a feature count no subject's voxel dimension can support.
109
+
110
+ `_init_w_transforms` takes a reduced QR, which silently returns
111
+ `min(voxels, n_features)` columns, so an oversized request would otherwise
112
+ produce a model of a dimension the caller never asked for.
113
+
114
+ Note:
115
+ "Voxels" is this module's name for the first axis throughout, and the
116
+ message follows it. `align(axis=1)` transposes before fitting, so on
117
+ that path the first axis is timepoints and the message still says
118
+ voxels.
119
+
120
+ Args:
121
+ X (list[np.ndarray]): One (voxels_i, samples) array per subject.
122
+ n_features (int): Requested number of shared features.
123
+
124
+ Raises:
125
+ ValueError: If `n_features` is not a positive integer, or exceeds any
126
+ subject's voxel count.
127
+ """
128
+ if not isinstance(n_features, (int, np.integer)) or n_features < 1:
129
+ raise ValueError(f"n_features must be a positive integer, got {n_features!r}.")
130
+ for subject, data in enumerate(X):
131
+ if data is None:
132
+ continue
133
+ if data.shape[0] < n_features:
134
+ raise ValueError(
135
+ f"subject {subject} has {data.shape[0]} voxels, too few to "
136
+ f"support {n_features} features. Lower n_features to at most "
137
+ "the smallest subject's voxel count."
138
+ )
139
+
140
+
141
+ def _update_transform_subject(Xi: np.ndarray, S: np.ndarray) -> np.ndarray:
142
+ """Update the mapping $W_i$ for one subject.
143
+
144
+ Solves the orthogonal Procrustes problem
145
+ $\\min ||X_i - W_i S||_F^2$ subject to $W_i^T W_i = I$: with the SVD
146
+ $U \\Sigma V^T = X_i S^T$, the optimum is $W_i = U V^T$.
147
+
148
+ Args:
149
+ Xi (np.ndarray): The subject's data $X_i$, shape (voxels, timepoints).
150
+ S (np.ndarray): The shared response, shape (n_features, timepoints).
151
+
152
+ Returns:
153
+ np.ndarray: The orthogonal transform $W_i$, shape (voxels, n_features).
154
+ """
155
+ # Compute cross-covariance: X_i S^T
156
+ A = Xi.dot(S.T)
157
+ # Solve the Procrustes problem via SVD
158
+ # Optimal orthogonal transform: W_i = U V^T where A = U Σ V^T
159
+ U, _, V = np.linalg.svd(A, full_matrices=False)
160
+ return U.dot(V)
161
+
162
+
107
163
  class _SRM(BaseEstimator, TransformerMixin):
108
164
  """Probabilistic Shared Response Model (SRM).
109
165
 
@@ -142,7 +198,7 @@ class _SRM(BaseEstimator, TransformerMixin):
142
198
  Examples:
143
199
  ```python
144
200
  import numpy as np
145
- from nltools.algorithms import SRM
201
+ from nltools.algorithms.alignment import _SRM
146
202
 
147
203
  data = [np.random.randn(100, 50) for _ in range(3)] # 3 subjects
148
204
 
@@ -173,6 +229,11 @@ class _SRM(BaseEstimator, TransformerMixin):
173
229
 
174
230
  Returns:
175
231
  _SRM: Fitted model (`self`).
232
+
233
+ Raises:
234
+ ValueError: If there are fewer than two subjects, the subjects
235
+ disagree on sample count, or `n_features` exceeds any subject's
236
+ voxel count.
176
237
  """
177
238
  logger.info("Starting Probabilistic SRM")
178
239
 
@@ -197,6 +258,10 @@ class _SRM(BaseEstimator, TransformerMixin):
197
258
  f"Different number of samples between subjects: {sample_counts}."
198
259
  )
199
260
 
261
+ # After the sample-count check, so input wrong on both axes reports the
262
+ # mismatched samples first.
263
+ _check_n_features(X, self.n_features)
264
+
200
265
  # Validate all data is finite
201
266
  for subject in range(number_subjects):
202
267
  if X[subject] is not None:
@@ -328,28 +393,6 @@ class _SRM(BaseEstimator, TransformerMixin):
328
393
 
329
394
  return loglikehood
330
395
 
331
- @staticmethod
332
- def _update_transform_subject(Xi, S):
333
- """Update the mapping $W_i$ for one subject.
334
-
335
- Solves the orthogonal Procrustes problem
336
- $\\min ||X_i - W_i S||_F^2$ subject to $W_i^T W_i = I$: with the SVD
337
- $U \\Sigma V^T = X_i S^T$, the optimum is $W_i = U V^T$.
338
-
339
- Args:
340
- Xi (np.ndarray): The subject's data $X_i$, shape (voxels, timepoints).
341
- S (np.ndarray): The shared response, shape (n_features, timepoints).
342
-
343
- Returns:
344
- np.ndarray: The orthogonal transform $W_i$, shape (voxels, n_features).
345
- """
346
- # Compute cross-covariance: X_i S^T
347
- A = Xi.dot(S.T)
348
- # Solve the Procrustes problem via SVD
349
- # Optimal orthogonal transform: W_i = U V^T where A = U Σ V^T
350
- U, _, V = np.linalg.svd(A, full_matrices=False)
351
- return U.dot(V)
352
-
353
396
  def transform_subject(self, X: np.ndarray) -> np.ndarray:
354
397
  """Transform a new subject using the existing model.
355
398
 
@@ -373,7 +416,7 @@ class _SRM(BaseEstimator, TransformerMixin):
373
416
  "The number of timepoints(TRs) does not match the one in the model."
374
417
  )
375
418
 
376
- w = self._update_transform_subject(X, self.s_)
419
+ w = _update_transform_subject(X, self.s_)
377
420
 
378
421
  return w
379
422
 
@@ -533,7 +576,7 @@ class _DetSRM(BaseEstimator, TransformerMixin):
533
576
  Examples:
534
577
  ```python
535
578
  import numpy as np
536
- from nltools.algorithms import DetSRM
579
+ from nltools.algorithms.alignment import _DetSRM
537
580
 
538
581
  data = [np.random.randn(100, 50) for _ in range(3)] # 3 subjects
539
582
 
@@ -563,6 +606,11 @@ class _DetSRM(BaseEstimator, TransformerMixin):
563
606
 
564
607
  Returns:
565
608
  _DetSRM: Fitted model (`self`).
609
+
610
+ Raises:
611
+ ValueError: If there are fewer than two subjects, the subjects
612
+ disagree on sample count, or `n_features` exceeds any subject's
613
+ voxel count.
566
614
  """
567
615
  logger.info("Starting Deterministic SRM")
568
616
 
@@ -587,6 +635,10 @@ class _DetSRM(BaseEstimator, TransformerMixin):
587
635
  if X[subject].shape[1] != number_trs:
588
636
  raise ValueError("Different number of samples between subjects.")
589
637
 
638
+ # After the sample-count check, so input wrong on both axes reports the
639
+ # mismatched samples first.
640
+ _check_n_features(X, self.n_features)
641
+
590
642
  # Run SRM
591
643
  self.w_, self.s_ = self._srm(X)
592
644
 
@@ -655,28 +707,6 @@ class _DetSRM(BaseEstimator, TransformerMixin):
655
707
 
656
708
  return s
657
709
 
658
- @staticmethod
659
- def _update_transform_subject(Xi, S):
660
- """Update the mapping $W_i$ for one subject.
661
-
662
- Solves the orthogonal Procrustes problem
663
- $\\min ||X_i - W_i S||_F^2$ subject to $W_i^T W_i = I$: with the SVD
664
- $U \\Sigma V^T = X_i S^T$, the optimum is $W_i = U V^T$.
665
-
666
- Args:
667
- Xi (np.ndarray): The subject's data $X_i$, shape (voxels, timepoints).
668
- S (np.ndarray): The shared response, shape (n_features, timepoints).
669
-
670
- Returns:
671
- np.ndarray: The orthogonal transform $W_i$, shape (voxels, n_features).
672
- """
673
- # Compute cross-covariance: X_i S^T
674
- A = Xi.dot(S.T)
675
- # Solve the Procrustes problem via SVD
676
- # Optimal orthogonal transform: W_i = U V^T where A = U Σ V^T
677
- U, _, V = np.linalg.svd(A, full_matrices=False)
678
- return U.dot(V)
679
-
680
710
  def transform_subject(self, X: np.ndarray) -> np.ndarray:
681
711
  """Transform a new subject using the existing model.
682
712
 
@@ -700,7 +730,7 @@ class _DetSRM(BaseEstimator, TransformerMixin):
700
730
  "The number of timepoints(TRs) does not match the one in the model."
701
731
  )
702
732
 
703
- w = self._update_transform_subject(X, self.s_)
733
+ w = _update_transform_subject(X, self.s_)
704
734
 
705
735
  return w
706
736
 
@@ -654,6 +654,10 @@ def _ridge_bootstrap_batch_size(
654
654
  #: every summary payload is converted to CPU float64 before it is retained.
655
655
  _BOOTSTRAP_OUTPUT_ITEMSIZE = 8
656
656
 
657
+ #: Bytes per resampling index. `_generate_bootstrap_indices` returns an int64
658
+ #: matrix, and every CPU worker's closure captures it.
659
+ _BOOTSTRAP_INDEX_ITEMSIZE = 8
660
+
657
661
  #: Output-sized arrays a bootstrap run always holds beyond its retained
658
662
  #: replicates: the two Welford accumulators (running mean and running sum of
659
663
  #: squared deviations) and the four `BootstrapResult` summary payloads.
@@ -734,15 +738,19 @@ def _bootstrap_output_bytes(
734
738
  *,
735
739
  confidence_level: float,
736
740
  return_samples: bool,
741
+ n_obs: int,
737
742
  n_workers: int = 1,
738
743
  ) -> int:
739
- """Bytes a bootstrap run must hold for its retained output.
744
+ """Bytes a bootstrap run must hold for its retained output and its indices.
740
745
 
741
746
  Charges eight bytes for every output-sized array a run holds at once: the
742
747
  two bounded tails, the replicates buffered before the next flush and the
743
748
  two temporaries that flush builds, one dispatch window of in-flight
744
749
  replicates, every replicate when `return_samples=True`, and the two Welford
745
- accumulators plus the four summary payloads.
750
+ accumulators plus the four summary payloads. On top of that it charges the
751
+ resampling index matrix twice: the run retains one `(n_samples, n_obs)`
752
+ int64 array, and building it holds the per-draw vectors alongside the
753
+ stacked result.
746
754
 
747
755
  Args:
748
756
  output_shape (tuple[int, ...]): Shape of one replicate's output.
@@ -750,6 +758,8 @@ def _bootstrap_output_bytes(
750
758
  confidence_level (float): Interval confidence level, which sets the
751
759
  retained tail size.
752
760
  return_samples (bool): Whether the complete distribution is retained.
761
+ n_obs (int): Observations resampled per replicate, which sizes the
762
+ index matrix.
753
763
  n_workers (int): Planned CPU worker count, which sets the dispatch
754
764
  window. Defaults to 1 (the GPU driver budgets its own batch through
755
765
  `_ridge_bootstrap_batch_size` instead).
@@ -770,7 +780,8 @@ def _bootstrap_output_bytes(
770
780
  + (int(n_samples) if return_samples else 0)
771
781
  + _BOOTSTRAP_FIXED_OUTPUT_ARRAYS
772
782
  )
773
- return output_size * arrays * _BOOTSTRAP_OUTPUT_ITEMSIZE
783
+ index_bytes = 2 * int(n_obs) * int(n_samples) * _BOOTSTRAP_INDEX_ITEMSIZE
784
+ return output_size * arrays * _BOOTSTRAP_OUTPUT_ITEMSIZE + index_bytes
774
785
 
775
786
 
776
787
  def _bootstrap_memory_preflight(
@@ -779,6 +790,7 @@ def _bootstrap_memory_preflight(
779
790
  *,
780
791
  confidence_level: float,
781
792
  return_samples: bool,
793
+ n_obs: int,
782
794
  n_workers: int = 1,
783
795
  memory_budget_gb: float | None = None,
784
796
  backend: "_Backend | None" = None,
@@ -795,6 +807,8 @@ def _bootstrap_memory_preflight(
795
807
  n_samples (int): Number of bootstrap replicates.
796
808
  confidence_level (float): Interval confidence level.
797
809
  return_samples (bool): Whether the complete distribution is retained.
810
+ n_obs (int): Observations resampled per replicate, which sizes the
811
+ index matrix.
798
812
  n_workers (int): Planned CPU worker count, which sets the dispatch
799
813
  window. Defaults to 1.
800
814
  memory_budget_gb (float | None): Explicit budget in GB, or None to
@@ -813,6 +827,7 @@ def _bootstrap_memory_preflight(
813
827
  n_samples,
814
828
  confidence_level=confidence_level,
815
829
  return_samples=return_samples,
830
+ n_obs=n_obs,
816
831
  n_workers=n_workers,
817
832
  )
818
833
  required_gb = required_bytes / 1e9
@@ -925,7 +940,9 @@ def _compute_oom_safe(fn, *arrays, min_chunk: int = 1):
925
940
  a numpy array whose axis 0 corresponds row-for-row to its inputs. On a
926
941
  device OOM the cache is emptied, the arrays are split in half along
927
942
  axis 0, and the halves are retried recursively; partial results are
928
- concatenated along axis 0.
943
+ concatenated along axis 0. Recovery happens after leaving the exception
944
+ handler, so the failed call's allocations are released before the smaller
945
+ retries ask for them.
929
946
 
930
947
  Because splitting reuses the *already generated* inputs rather than
931
948
  re-drawing them, recovery never changes which permutations a seeded
@@ -954,17 +971,22 @@ def _compute_oom_safe(fn, *arrays, min_chunk: int = 1):
954
971
  except Exception as exc:
955
972
  if not _is_oom_error(exc):
956
973
  raise
957
- _empty_device_cache()
958
974
  if n <= min_chunk:
975
+ _empty_device_cache()
959
976
  raise MemoryError(
960
977
  f"Device out of memory even for a single item (chunk of {n}). "
961
978
  "Reduce the problem size, lower max_gpu_memory_gb elsewhere on "
962
979
  "the device, or use device='cpu'."
963
980
  ) from exc
964
- mid = n // 2
965
- left = _compute_oom_safe(fn, *(a[:mid] for a in arrays), min_chunk=min_chunk)
966
- right = _compute_oom_safe(fn, *(a[mid:] for a in arrays), min_chunk=min_chunk)
967
- return np.concatenate([left, right], axis=0)
981
+
982
+ # Outside the handler, where Python's implicit `del exc` has dropped the
983
+ # traceback: the failed call's frame and the device allocations its
984
+ # locals held — are gone before the cache is emptied and the halves retried.
985
+ _empty_device_cache()
986
+ mid = n // 2
987
+ left = _compute_oom_safe(fn, *(a[:mid] for a in arrays), min_chunk=min_chunk)
988
+ right = _compute_oom_safe(fn, *(a[mid:] for a in arrays), min_chunk=min_chunk)
989
+ return np.concatenate([left, right], axis=0)
968
990
 
969
991
 
970
992
  # ----------------------------------------------------------------------