brainstat 0.5.2__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 (70) hide show
  1. {brainstat-0.5.2/brainstat.egg-info → brainstat-0.6.0}/PKG-INFO +3 -3
  2. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/__init__.py +1 -1
  3. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/histology.py +23 -6
  4. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/datasets/base.py +49 -2
  5. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/SLM.py +62 -11
  6. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/_multiple_comparisons.py +0 -1
  7. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/_t_test.py +4 -3
  8. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_SLM.py +50 -0
  9. {brainstat-0.5.2 → brainstat-0.6.0/brainstat.egg-info}/PKG-INFO +3 -3
  10. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat.egg-info/requires.txt +2 -2
  11. {brainstat-0.5.2 → brainstat-0.6.0}/setup.py +5 -2
  12. {brainstat-0.5.2 → brainstat-0.6.0}/LICENSE +0 -0
  13. {brainstat-0.5.2 → brainstat-0.6.0}/MANIFEST.in +0 -0
  14. {brainstat-0.5.2 → brainstat-0.6.0}/README.rst +0 -0
  15. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/_typing.py +0 -0
  16. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/_utils.py +0 -0
  17. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/__init__.py +0 -0
  18. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/genetics.py +0 -0
  19. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/meta_analysis.py +0 -0
  20. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/context/resting.py +0 -0
  21. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/data_urls.json +0 -0
  22. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/logging.conf +0 -0
  23. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_civet164k.nii.gz +0 -0
  24. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_civet41k.nii.gz +0 -0
  25. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_fsaverage.nii.gz +0 -0
  26. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_fsaverage5.nii.gz +0 -0
  27. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/data/nn_interp_fslr32k.nii.gz +0 -0
  28. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/datasets/__init__.py +0 -0
  29. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/mesh/__init__.py +0 -0
  30. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/mesh/data.py +0 -0
  31. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/mesh/interpolate.py +0 -0
  32. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/mesh/utils.py +0 -0
  33. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/__init__.py +0 -0
  34. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/_linear_model.py +0 -0
  35. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/terms.py +0 -0
  36. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/stats/utils.py +0 -0
  37. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/__init__.py +0 -0
  38. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/conftest.py +0 -0
  39. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_f.py +0 -0
  40. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_fdr.py +0 -0
  41. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_linear_model.py +0 -0
  42. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_mesh_edges.py +0 -0
  43. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_mesh_smooth.py +0 -0
  44. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_peak_clus.py +0 -0
  45. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_random_field_theory.py +0 -0
  46. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_resels.py +0 -0
  47. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_slm.py +0 -0
  48. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_stat_threshold.py +0 -0
  49. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/datagen_t_test.py +0 -0
  50. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_datasets.py +0 -0
  51. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_f_test.py +0 -0
  52. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_fdr.py +0 -0
  53. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_genetics.py +0 -0
  54. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_histology.py +0 -0
  55. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_linear_model.py +0 -0
  56. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_mesh_edges.py +0 -0
  57. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_mesh_smooth.py +0 -0
  58. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_peak_clus.py +0 -0
  59. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_random_field_theory.py +0 -0
  60. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_resels.py +0 -0
  61. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_stat_threshold.py +0 -0
  62. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_t_test.py +0 -0
  63. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/test_terms.py +0 -0
  64. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tests/testutil.py +0 -0
  65. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tutorial/__init__.py +0 -0
  66. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat/tutorial/utils.py +0 -0
  67. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat.egg-info/SOURCES.txt +0 -0
  68. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat.egg-info/dependency_links.txt +0 -0
  69. {brainstat-0.5.2 → brainstat-0.6.0}/brainstat.egg-info/top_level.txt +0 -0
  70. {brainstat-0.5.2 → brainstat-0.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainstat
3
- Version: 0.5.2
3
+ Version: 0.6.0
4
4
  Summary: A toolbox for statistical analysis of neuroimaging data
5
5
  Home-page: https://github.com/MICA-LAB/BrainStat
6
6
  Author: MNI-MICA Lab and MPI-CNG Lab
@@ -26,8 +26,8 @@ Requires-Dist: neurosynth
26
26
  Requires-Dist: nibabel
27
27
  Requires-Dist: nilearn>=0.7.0
28
28
  Requires-Dist: nimare
29
- Requires-Dist: numpy>=1.21
30
- Requires-Dist: pandas>=1.3
29
+ Requires-Dist: numpy<2,>=1.21
30
+ Requires-Dist: pandas<2.1,>=1.3
31
31
  Requires-Dist: scikit_learn>=1.0
32
32
  Requires-Dist: scipy>=1.7
33
33
  Requires-Dist: templateflow
@@ -1,2 +1,2 @@
1
1
  """Neuroimaging statistics toolbox."""
2
- __version__ = "0.5.2"
2
+ __version__ = "0.6.0"
@@ -114,6 +114,7 @@ def read_histology_profile(
114
114
  data_dir: Optional[Union[str, Path]] = None,
115
115
  template: str = "fsaverage",
116
116
  overwrite: bool = False,
117
+ invert: bool = True,
117
118
  ) -> np.ndarray:
118
119
  """Reads BigBrain histology profiles.
119
120
 
@@ -127,6 +128,15 @@ def read_histology_profile(
127
128
  default 'fsaverage'.
128
129
  overwrite : bool, optional
129
130
  If true, existing data will be overwrriten, by default False.
131
+ invert : bool, optional
132
+ If True (default), invert the 8-bit intensity values so that higher
133
+ numbers correspond to darker, more cell-dense regions, matching the
134
+ convention used in Paquola et al. 2021 (eLife) and most downstream
135
+ BigBrain analyses. The raw BigBrainWarp data uses the opposite
136
+ convention (0 = black). Set to False to return the raw values. MPC
137
+ and gradient computations (correlation-based) are invariant to this
138
+ flip; only direct inspection of profile intensities is affected.
139
+ See https://github.com/MICA-MNI/BrainStat/issues/274 for context.
130
140
 
131
141
  Returns
132
142
  -------
@@ -169,12 +179,19 @@ def read_histology_profile(
169
179
  profiles = h5_file.get("fs_LR_64k")[...]
170
180
  else:
171
181
  profiles = h5_file.get(template)[...]
172
- if civet_template:
173
- fsaverage_surface = fetch_template_surface("fsaverage")
174
- civet_surface = fetch_template_surface(civet_template)
175
- return _surf2surf(fsaverage_surface, civet_surface, profiles.T).T
176
- else:
177
- return profiles
182
+
183
+ if invert:
184
+ # Map from BigBrainWarp convention (low = dark/dense) to the
185
+ # literature convention (high = dark/dense). The data is unsigned
186
+ # 8-bit, so the inversion is `255 - x`.
187
+ max_value = np.iinfo(profiles.dtype).max if np.issubdtype(profiles.dtype, np.integer) else 255
188
+ profiles = max_value - profiles
189
+
190
+ if civet_template:
191
+ fsaverage_surface = fetch_template_surface("fsaverage")
192
+ civet_surface = fetch_template_surface(civet_template)
193
+ return _surf2surf(fsaverage_surface, civet_surface, profiles.T).T
194
+ return profiles
178
195
 
179
196
 
180
197
  def download_histology_profiles(
@@ -111,12 +111,15 @@ def fetch_parcellation(
111
111
  The surface template. Valid values are "fsaverage", "fsaverage5",
112
112
  "fsaverage6", "fslr32k", "civet41k", "civet164k", by default "fsaverage5".
113
113
  atlas : str
114
- Name of the atlas. Valid names are "cammoun", "glasser", "schaefer", "yeo".
114
+ Name of the atlas. Valid names are "cammoun", "destrieux", "glasser",
115
+ "schaefer", "yeo".
115
116
  n_regions : int
116
117
  Number of regions of the requested atlas. Valid values for the cammoun
117
118
  atlas are 33, 60, 125, 250, 500. Valid values for the glasser atlas are
118
119
  360. Valid values for the "schaefer" atlas are 100, 200, 300, 400, 500,
119
- 600, 800, 1000. Valid values for "yeo" are 7 and 17.
120
+ 600, 800, 1000. Valid values for "yeo" are 7 and 17. The
121
+ "destrieux" atlas (a2009s, FreeSurfer's 75 cortical labels per
122
+ hemisphere) ignores ``n_regions``.
120
123
  join : bool, optional
121
124
  If true, returns parcellation as a single array, if false, returns an
122
125
  array per hemisphere, by default True.
@@ -154,6 +157,8 @@ def fetch_parcellation(
154
157
  parcellations = _fetch_glasser_parcellation(template, data_dir)
155
158
  elif atlas == "yeo":
156
159
  parcellations = _fetch_yeo_parcellation(template, n_regions, data_dir)
160
+ elif atlas == "destrieux":
161
+ parcellations = _fetch_destrieux_parcellation(template, data_dir)
157
162
  else:
158
163
  raise ValueError(f"Invalid atlas: {atlas}")
159
164
 
@@ -537,6 +542,48 @@ def _fetch_glasser_parcellation(template: str, data_dir: Path) -> List[np.ndarra
537
542
  return parcellations
538
543
 
539
544
 
545
+ def _fetch_destrieux_parcellation(
546
+ template: str, data_dir: Path
547
+ ) -> List[np.ndarray]:
548
+ """Fetches the Destrieux 2009 (FreeSurfer ``aparc.a2009s``) parcellation.
549
+
550
+ Uses :func:`nilearn.datasets.fetch_atlas_surf_destrieux`, which provides
551
+ the parcellation on the ``fsaverage5`` surface. For other ``fsaverage*``
552
+ templates we resample with nearest-neighbour interpolation; ``fslr32k``
553
+ is not supported because the destrieux atlas is FreeSurfer-specific.
554
+ """
555
+ from nilearn.datasets import fetch_atlas_surf_destrieux
556
+
557
+ if template == "fslr32k":
558
+ raise ValueError(
559
+ "The destrieux atlas is FreeSurfer-specific; fslr32k is not "
560
+ "supported. Use a fsaverage* template instead."
561
+ )
562
+
563
+ bunch = fetch_atlas_surf_destrieux(data_dir=str(data_dir))
564
+ # nilearn returns int32 arrays for fsaverage5 (10242 vertices/hemi).
565
+ parcellations = [np.asarray(bunch["map_left"]), np.asarray(bunch["map_right"])]
566
+
567
+ if template != "fsaverage5":
568
+ # Nearest-neighbour resample fsaverage5 -> requested fsaverage*.
569
+ from brainstat.mesh.interpolate import _surf2surf
570
+
571
+ src_left, src_right = fetch_template_surface(
572
+ "fsaverage5", layer="white", join=False
573
+ )
574
+ dst_left, dst_right = fetch_template_surface(
575
+ template, layer="white", join=False
576
+ )
577
+ parcellations[0] = _surf2surf(
578
+ src_left, dst_left, parcellations[0], interpolation="nearest"
579
+ ).astype(parcellations[0].dtype)
580
+ parcellations[1] = _surf2surf(
581
+ src_right, dst_right, parcellations[1], interpolation="nearest"
582
+ ).astype(parcellations[1].dtype)
583
+
584
+ return parcellations
585
+
586
+
540
587
  def _fetch_yeo_parcellation(
541
588
  template: str, n_regions: int, data_dir: Path
542
589
  ) -> List[np.ndarray]:
@@ -1,7 +1,6 @@
1
- # type: ignore
2
1
  """ Standard Linear regression models. """
3
2
  import warnings
4
- from cmath import sqrt
3
+ from math import sqrt
5
4
  from pathlib import Path
6
5
  from pprint import pformat
7
6
  from typing import Optional, Sequence, Tuple, Union
@@ -17,8 +16,11 @@ from nibabel.nifti1 import Nifti1Image
17
16
 
18
17
  from brainstat._typing import ArrayLike
19
18
  from brainstat.datasets import fetch_parcellation, fetch_template_surface
20
- from brainstat.datasets.base import fetch_template_surface, fetch_yeo_networks_metadata
19
+ from brainstat.datasets.base import fetch_yeo_networks_metadata
21
20
  from brainstat.mesh.utils import _mask_edges, mesh_edges
21
+ from brainstat.stats._linear_model import _linear_model
22
+ from brainstat.stats._multiple_comparisons import _fdr, _random_field_theory
23
+ from brainstat.stats._t_test import _t_test
22
24
  from brainstat.stats.terms import FixedEffect, MixedEffect
23
25
  from brainstat.stats.utils import apply_mask, undo_mask
24
26
 
@@ -26,10 +28,13 @@ from brainstat.stats.utils import apply_mask, undo_mask
26
28
  class SLM:
27
29
  """Core Class for running BrainStat linear models"""
28
30
 
29
- # Import class methods
30
- from ._linear_model import _linear_model
31
- from ._multiple_comparisons import _fdr, _random_field_theory
32
- from ._t_test import _t_test
31
+ # Class methods are imported at module level (above) and assigned here
32
+ # rather than via in-class `from ... import ...`, which trips
33
+ # python/mypy#10521 and previously forced a file-level `# type: ignore`.
34
+ _linear_model = _linear_model
35
+ _fdr = _fdr
36
+ _random_field_theory = _random_field_theory
37
+ _t_test = _t_test
33
38
 
34
39
  def __init__(
35
40
  self,
@@ -89,7 +94,7 @@ class SLM:
89
94
  """
90
95
  # Input arguments.
91
96
  self.model = model
92
- self.contrast = np.array(contrast)
97
+ self.contrast = self._coerce_contrast(contrast)
93
98
 
94
99
  if isinstance(surf, str):
95
100
  self.surf_name = surf
@@ -98,7 +103,7 @@ class SLM:
98
103
  self.surf_name = None
99
104
  self.surf = surf
100
105
 
101
- self.mask = mask
106
+ self.mask = self._coerce_mask(mask)
102
107
  self.correction = [correction] if isinstance(correction, str) else correction
103
108
  self.niter = 1
104
109
  self.thetalim = thetalim
@@ -110,12 +115,54 @@ class SLM:
110
115
  # Error check
111
116
  if self.surf is None:
112
117
  if self.correction is not None and "rft" in self.correction:
113
- raise ValueError("Random Field Theory corrections require a surface.")
118
+ raise ValueError(
119
+ "Random Field Theory corrections require a surface. For "
120
+ "volumetric data, drop 'rft' from `correction` and use "
121
+ "`correction='fdr'` (cluster-level RFT/TFCE on volumes is "
122
+ "not supported by BrainStat — see issue #342)."
123
+ )
114
124
 
115
125
  # We have to initialize fit parameters for our unit tests here.
116
126
  # TODO: remove this requirement.
117
127
  self._reset_fit_parameters()
118
128
 
129
+ @staticmethod
130
+ def _coerce_contrast(contrast: ArrayLike) -> np.ndarray:
131
+ """Convert ``contrast`` to a numeric ndarray.
132
+
133
+ Pandas Series and categorical/object inputs are common foot-guns —
134
+ np.dot then raises a cryptic "can't multiply sequence by non-int"
135
+ error inside _t_test. Cast to float here and surface a clear
136
+ TypeError if that fails (see issue #342).
137
+ """
138
+ if isinstance(contrast, FixedEffect):
139
+ return contrast # type: ignore[return-value]
140
+ arr = np.asarray(contrast)
141
+ if arr.dtype.kind in "biufc":
142
+ return arr
143
+ try:
144
+ return arr.astype(float)
145
+ except (ValueError, TypeError) as exc:
146
+ raise TypeError(
147
+ f"`contrast` must be numeric; got dtype {arr.dtype}. If you "
148
+ "passed a pandas Series with categorical/object dtype, cast "
149
+ "it to float first (e.g. df['col'].astype(float))."
150
+ ) from exc
151
+
152
+ @staticmethod
153
+ def _coerce_mask(mask: Optional[ArrayLike]) -> Optional[np.ndarray]:
154
+ """Convert ``mask`` to a 1-D boolean ndarray.
155
+
156
+ Volumetric workflows commonly produce 0/1 *integer* masks (e.g.
157
+ from `nib.load(...).get_fdata().astype(int)`). The downstream
158
+ boolean-indexing path then degenerates into fancy indexing, which
159
+ triggers shape-mismatch errors in `_fdr` (see issue #342). Cast to
160
+ bool here so callers don't have to remember.
161
+ """
162
+ if mask is None:
163
+ return None
164
+ return np.asarray(mask).astype(bool)
165
+
119
166
  def fit(self, Y: np.ndarray) -> None:
120
167
  """Fits the SLM model
121
168
 
@@ -322,7 +369,11 @@ class SLM:
322
369
  )
323
370
  yeo_names, _ = fetch_yeo_networks_metadata(7)
324
371
  yeo_names.insert(0, "Undefined")
325
- yeo7_index = yeo7[self.P["peak"]["vertid"][i]]
372
+ # vertid is stored as 1-based (inherited from SurfStat /
373
+ # MATLAB conventions in peak_clus); subtract 1 before
374
+ # indexing the 0-based yeo7 array (see #347).
375
+ vertid = self.P["peak"]["vertid"][i]
376
+ yeo7_index = yeo7[vertid - 1] if vertid is not None else None
326
377
  if "yeo7" not in self.P["peak"]:
327
378
  self.P["peak"]["yeo7"] = []
328
379
  self.P["peak"]["yeo7"].append(np.take(yeo_names, yeo7_index))
@@ -1,4 +1,3 @@
1
- # type: ignore
2
1
  """Multiple comparison corrections."""
3
2
  import copy
4
3
  import math
@@ -1,5 +1,4 @@
1
1
  import math
2
- import sys
3
2
  import warnings
4
3
 
5
4
  import numpy as np
@@ -39,7 +38,7 @@ def _t_test(self) -> None:
39
38
  if np.square(np.dot(null_space(self.X).T, c)).sum() / np.square(
40
39
  c
41
40
  ).sum() > np.spacing(1):
42
- sys.exit("Contrast is not estimable :-(")
41
+ raise ValueError("Contrast is not estimable.")
43
42
 
44
43
  else:
45
44
  c = np.dot(pinvX, self.contrast)
@@ -173,6 +172,8 @@ def _t_test(self) -> None:
173
172
  M[M_ef] = ef_duplicate[:, i]
174
173
  M[M_sse] = sse_matrix.flatten()
175
174
 
176
- self.t[i] = float(np.sqrt(-np.linalg.det(M) / det_sse / vf))
175
+ # Use .item() rather than float(): numpy >=2.0 raises
176
+ # TypeError when float() is applied to size-1 ndarrays.
177
+ self.t[i] = np.sqrt(-np.linalg.det(M) / det_sse / vf).item()
177
178
 
178
179
  self.t = np.atleast_2d(self.t)
@@ -180,3 +180,53 @@ def test_volumetric_input():
180
180
 
181
181
  slm = SLM(model, contrast, surf=mask_image)
182
182
  slm.fit(data)
183
+
184
+
185
+ def test_volumetric_contrast_coercion():
186
+ """Pandas Series / categorical contrasts must be coerced rather than
187
+ crashing inside _t_test (issue #342)."""
188
+ model = FixedEffect(np.ones(3))
189
+ slm = SLM(model, pd.Series([1.0, 0.5, -0.5]))
190
+ assert slm.contrast.dtype == np.float64
191
+
192
+ with pytest.raises(TypeError, match="must be numeric"):
193
+ SLM(model, pd.Series(["a", "b", "c"]))
194
+
195
+
196
+ def test_volumetric_int_mask_is_coerced_to_bool():
197
+ """Volumetric workflows commonly hand SLM a 0/1 *int* mask
198
+ (issue #342). It should be coerced rather than degenerating into
199
+ fancy indexing inside _fdr."""
200
+ model = FixedEffect(np.ones(3))
201
+ int_mask = np.array([1, 0, 1, 1, 0, 1], dtype=int)
202
+ slm = SLM(model, np.ones(3), mask=int_mask)
203
+ assert slm.mask.dtype == np.bool_
204
+ assert slm.mask.sum() == 4
205
+
206
+
207
+ def test_volumetric_rft_without_surface_explains_alternatives():
208
+ """RFT on a volume is unsupported; the error should point users at FDR
209
+ rather than just refusing (issue #342)."""
210
+ model = FixedEffect(np.ones(3))
211
+ with pytest.raises(ValueError, match="fdr"):
212
+ SLM(model, np.ones(3), correction="rft")
213
+
214
+
215
+ def test_volumetric_fdr_with_int_mask_runs_end_to_end():
216
+ """The full int-mask + FDR + non-trivial contrast path that the issue
217
+ reporters originally hit should now run without error (issue #342)."""
218
+ n_subjects, n_voxels = 8, 64
219
+ rng = np.random.default_rng(0)
220
+ data = rng.standard_normal((n_subjects, n_voxels))
221
+ # Half the voxels in the mask, expressed as ints (the foot-gun shape).
222
+ int_mask = np.zeros(n_voxels, dtype=int)
223
+ int_mask[: n_voxels // 2] = 1
224
+ model = FixedEffect(rng.standard_normal(n_subjects))
225
+ contrast = pd.Series(rng.standard_normal(n_subjects)) # pandas, on purpose
226
+ slm = SLM(model, contrast, mask=int_mask, correction="fdr")
227
+ slm.fit(data)
228
+ assert slm.t.shape[-1] == n_voxels
229
+ # Inside the mask the t-values must be finite; outside they're masked
230
+ # out and undo_mask fills them with NaN.
231
+ assert np.all(np.isfinite(slm.t[..., int_mask.astype(bool)]))
232
+ assert slm.Q.shape[-1] == n_voxels
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainstat
3
- Version: 0.5.2
3
+ Version: 0.6.0
4
4
  Summary: A toolbox for statistical analysis of neuroimaging data
5
5
  Home-page: https://github.com/MICA-LAB/BrainStat
6
6
  Author: MNI-MICA Lab and MPI-CNG Lab
@@ -26,8 +26,8 @@ Requires-Dist: neurosynth
26
26
  Requires-Dist: nibabel
27
27
  Requires-Dist: nilearn>=0.7.0
28
28
  Requires-Dist: nimare
29
- Requires-Dist: numpy>=1.21
30
- Requires-Dist: pandas>=1.3
29
+ Requires-Dist: numpy<2,>=1.21
30
+ Requires-Dist: pandas<2.1,>=1.3
31
31
  Requires-Dist: scikit_learn>=1.0
32
32
  Requires-Dist: scipy>=1.7
33
33
  Requires-Dist: templateflow
@@ -7,8 +7,8 @@ neurosynth
7
7
  nibabel
8
8
  nilearn>=0.7.0
9
9
  nimare
10
- numpy>=1.21
11
- pandas>=1.3
10
+ numpy<2,>=1.21
11
+ pandas<2.1,>=1.3
12
12
  scikit_learn>=1.0
13
13
  scipy>=1.7
14
14
  templateflow
@@ -44,8 +44,11 @@ setuptools.setup(
44
44
  "nibabel",
45
45
  "nilearn>=0.7.0",
46
46
  "nimare",
47
- "numpy>=1.21",
48
- "pandas>=1.3",
47
+ # numpy<2 is required so the pinned pandas<2.1 (whose 2.0.x C
48
+ # extensions were built against the numpy 1.x ABI) loads cleanly.
49
+ # See requirements.txt for the abagen-driven rationale.
50
+ "numpy>=1.21,<2",
51
+ "pandas>=1.3,<2.1",
49
52
  "scikit_learn>=1.0",
50
53
  "scipy>=1.7",
51
54
  "templateflow",
File without changes
File without changes
File without changes
File without changes
File without changes