brainstat 0.5.2__tar.gz → 0.7.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 (76) hide show
  1. {brainstat-0.5.2/brainstat.egg-info → brainstat-0.7.0}/PKG-INFO +9 -7
  2. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/__init__.py +1 -1
  3. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/_utils.py +6 -2
  4. brainstat-0.7.0/brainstat/context/genetics.py +351 -0
  5. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/context/histology.py +23 -6
  6. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/datasets/base.py +49 -2
  7. brainstat-0.7.0/brainstat/reporting/__init__.py +5 -0
  8. brainstat-0.7.0/brainstat/reporting/_slm.py +316 -0
  9. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/SLM.py +78 -11
  10. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/_multiple_comparisons.py +0 -1
  11. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/_t_test.py +4 -3
  12. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_SLM.py +50 -0
  13. brainstat-0.7.0/brainstat/tests/test_download.py +65 -0
  14. brainstat-0.7.0/brainstat/tests/test_genetics.py +215 -0
  15. brainstat-0.7.0/brainstat/tests/test_reporting.py +136 -0
  16. {brainstat-0.5.2 → brainstat-0.7.0/brainstat.egg-info}/PKG-INFO +9 -7
  17. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat.egg-info/SOURCES.txt +4 -0
  18. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat.egg-info/requires.txt +5 -3
  19. {brainstat-0.5.2 → brainstat-0.7.0}/setup.py +11 -6
  20. brainstat-0.5.2/brainstat/context/genetics.py +0 -301
  21. brainstat-0.5.2/brainstat/tests/test_genetics.py +0 -33
  22. {brainstat-0.5.2 → brainstat-0.7.0}/LICENSE +0 -0
  23. {brainstat-0.5.2 → brainstat-0.7.0}/MANIFEST.in +0 -0
  24. {brainstat-0.5.2 → brainstat-0.7.0}/README.rst +0 -0
  25. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/_typing.py +0 -0
  26. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/context/__init__.py +0 -0
  27. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/context/meta_analysis.py +0 -0
  28. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/context/resting.py +0 -0
  29. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/data_urls.json +0 -0
  30. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/logging.conf +0 -0
  31. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_civet164k.nii.gz +0 -0
  32. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_civet41k.nii.gz +0 -0
  33. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_fsaverage.nii.gz +0 -0
  34. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_fsaverage5.nii.gz +0 -0
  35. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_fslr32k.nii.gz +0 -0
  36. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/datasets/__init__.py +0 -0
  37. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/mesh/__init__.py +0 -0
  38. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/mesh/data.py +0 -0
  39. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/mesh/interpolate.py +0 -0
  40. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/mesh/utils.py +0 -0
  41. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/__init__.py +0 -0
  42. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/_linear_model.py +0 -0
  43. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/terms.py +0 -0
  44. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/utils.py +0 -0
  45. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/__init__.py +0 -0
  46. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/conftest.py +0 -0
  47. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_f.py +0 -0
  48. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_fdr.py +0 -0
  49. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_linear_model.py +0 -0
  50. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_mesh_edges.py +0 -0
  51. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_mesh_smooth.py +0 -0
  52. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_peak_clus.py +0 -0
  53. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_random_field_theory.py +0 -0
  54. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_resels.py +0 -0
  55. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_slm.py +0 -0
  56. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_stat_threshold.py +0 -0
  57. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_t_test.py +0 -0
  58. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_datasets.py +0 -0
  59. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_f_test.py +0 -0
  60. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_fdr.py +0 -0
  61. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_histology.py +0 -0
  62. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_linear_model.py +0 -0
  63. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_mesh_edges.py +0 -0
  64. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_mesh_smooth.py +0 -0
  65. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_peak_clus.py +0 -0
  66. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_random_field_theory.py +0 -0
  67. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_resels.py +0 -0
  68. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_stat_threshold.py +0 -0
  69. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_t_test.py +0 -0
  70. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_terms.py +0 -0
  71. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/testutil.py +0 -0
  72. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tutorial/__init__.py +0 -0
  73. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tutorial/utils.py +0 -0
  74. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat.egg-info/dependency_links.txt +0 -0
  75. {brainstat-0.5.2 → brainstat-0.7.0}/brainstat.egg-info/top_level.txt +0 -0
  76. {brainstat-0.5.2 → brainstat-0.7.0}/setup.cfg +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainstat
3
- Version: 0.5.2
3
+ Version: 0.7.0
4
4
  Summary: A toolbox for statistical analysis of neuroimaging data
5
- Home-page: https://github.com/MICA-LAB/BrainStat
5
+ Home-page: https://github.com/MICA-MNI/BrainStat
6
6
  Author: MNI-MICA Lab and MPI-CNG Lab
7
7
  Author-email: saratheriver@gmail.com, sheymaba@gmail.com
8
8
  License: BSD 3-Clause License
9
9
  Project-URL: Documentation, https://brainstat.readthedocs.io
10
- Project-URL: Bug Reports, https://github.com/MICA-LAB/BrainStat/issues
11
- Project-URL: Source, https://github.com/MICA-LAB/BrainStat/
10
+ Project-URL: Bug Reports, https://github.com/MICA-MNI/BrainStat/issues
11
+ Project-URL: Source, https://github.com/MICA-MNI/BrainStat/
12
12
  Classifier: Intended Audience :: Science/Research
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Topic :: Software Development :: Build Tools
@@ -18,16 +18,18 @@ Requires-Python: >=3.10
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: LICENSE
20
20
  Requires-Dist: abagen>=0.1
21
+ Requires-Dist: setuptools<81
22
+ Requires-Dist: statsmodels
21
23
  Requires-Dist: brainspace>=0.1.2
22
24
  Requires-Dist: h5py
23
25
  Requires-Dist: matplotlib>=3.4
24
26
  Requires-Dist: netneurotools<0.3.0
25
27
  Requires-Dist: neurosynth
26
28
  Requires-Dist: nibabel
27
- Requires-Dist: nilearn>=0.7.0
29
+ Requires-Dist: nilearn>=0.10.4
28
30
  Requires-Dist: nimare
29
- Requires-Dist: numpy>=1.21
30
- Requires-Dist: pandas>=1.3
31
+ Requires-Dist: numpy<2,>=1.21
32
+ Requires-Dist: pandas<2.1,>=1.3
31
33
  Requires-Dist: scikit_learn>=1.0
32
34
  Requires-Dist: scipy>=1.7
33
35
  Requires-Dist: templateflow
@@ -1,2 +1,2 @@
1
1
  """Neuroimaging statistics toolbox."""
2
- __version__ = "0.5.2"
2
+ __version__ = "0.7.0"
@@ -6,6 +6,7 @@ import shutil
6
6
  import urllib.request
7
7
  import warnings
8
8
  from pathlib import Path
9
+ from tempfile import TemporaryDirectory
9
10
  from typing import Callable
10
11
 
11
12
  import brainstat
@@ -179,8 +180,11 @@ def _download_file(
179
180
 
180
181
  for attempt in range(max_retries):
181
182
  try:
182
- with urllib.request.urlopen(url, timeout=30) as response, open(output_file, "wb") as out_file:
183
- shutil.copyfileobj(response, out_file)
183
+ with TemporaryDirectory(dir=output_file.parent) as tmpdir:
184
+ temporary_file = Path(tmpdir) / output_file.name
185
+ with urllib.request.urlopen(url, timeout=30) as response, open(temporary_file, "wb") as out_file:
186
+ shutil.copyfileobj(response, out_file)
187
+ temporary_file.replace(output_file)
184
188
  return # Success, exit function
185
189
  except (RemoteDisconnected, URLError, TimeoutError) as e:
186
190
  if attempt < max_retries - 1:
@@ -0,0 +1,351 @@
1
+ """Genetic decoding using abagen."""
2
+
3
+ import tempfile
4
+ from contextlib import contextmanager
5
+ from pathlib import Path
6
+ from threading import RLock
7
+ from typing import Optional, Sequence, Union
8
+
9
+ import abagen.images
10
+ import nibabel as nib
11
+ import numpy as np
12
+ import pandas as pd
13
+ from abagen import check_atlas, get_expression_data
14
+ from brainspace.mesh.mesh_io import read_surface, write_surface
15
+ from nibabel.gifti import GiftiDataArray, GiftiImage
16
+ from sklearn.model_selection import ParameterGrid
17
+
18
+ from brainstat._utils import data_directories, logger
19
+ from brainstat.datasets.base import (
20
+ _fetch_template_surface_files,
21
+ _valid_parcellations,
22
+ fetch_parcellation,
23
+ )
24
+
25
+ _COMPAT_LOCK = RLock()
26
+
27
+
28
+ def _labeltable_to_df(labels):
29
+ rows = [
30
+ dict(id=key, label=value, hemisphere=hemi, structure="cortex")
31
+ for table, hemi in zip(labels, ("L", "R"))
32
+ for key, value in table.items()
33
+ if key != 0
34
+ ]
35
+ if rows:
36
+ return pd.DataFrame(rows).set_index("id").sort_index()
37
+ return None
38
+
39
+
40
+ @contextmanager
41
+ def _abagen_compat():
42
+ """Scope legacy abagen adapters to a call and always restore originals.
43
+
44
+ The lock serializes BrainStat genetics calls. The pandas adapters are
45
+ process-wide while the call runs; callers using abagen/pandas directly in
46
+ other threads should avoid running them concurrently with this workflow.
47
+ Dependency bounds remain in place until upstream supports newer pandas.
48
+ """
49
+ with _COMPAT_LOCK:
50
+ original_append = getattr(pd.DataFrame, "append", None)
51
+ original_set_axis = pd.DataFrame.set_axis
52
+ original_labels = abagen.images.labeltable_to_df
53
+
54
+ def append(
55
+ frame, other, ignore_index=False, verify_integrity=False, sort=False
56
+ ):
57
+ if isinstance(other, pd.Series):
58
+ other = other.to_frame().T
59
+ objects = [frame] + (
60
+ list(other) if isinstance(other, (list, tuple)) else [other]
61
+ )
62
+ return pd.concat(
63
+ objects,
64
+ ignore_index=ignore_index,
65
+ verify_integrity=verify_integrity,
66
+ sort=sort,
67
+ )
68
+
69
+ def set_axis(frame, labels, *args, **kwargs):
70
+ inplace = kwargs.pop("inplace", False)
71
+ if inplace:
72
+ raise ValueError(
73
+ "abagen compatibility supports set_axis(inplace=False) only"
74
+ )
75
+ return original_set_axis(frame, labels, *args, **kwargs)
76
+
77
+ try:
78
+ if original_append is None:
79
+ pd.DataFrame.append = append
80
+ pd.DataFrame.set_axis = set_axis
81
+ abagen.images.labeltable_to_df = _labeltable_to_df
82
+ yield
83
+ finally:
84
+ if original_append is None:
85
+ del pd.DataFrame.append
86
+ else:
87
+ pd.DataFrame.append = original_append
88
+ pd.DataFrame.set_axis = original_set_axis
89
+ abagen.images.labeltable_to_df = original_labels
90
+
91
+
92
+ def _surface_size(surface):
93
+ if isinstance(surface, (str, Path)):
94
+ surface = read_surface(str(surface))
95
+ return surface.n_points
96
+
97
+
98
+ def surface_genetic_expression(
99
+ labels: Union[Sequence[str], np.ndarray],
100
+ surfaces: Optional[Union[str, Sequence[str]]] = None,
101
+ space: Optional[str] = None,
102
+ *,
103
+ atlas_info: Optional[str] = None,
104
+ ibf_threshold: float = 0.5,
105
+ probe_selection: str = "diff_stability",
106
+ donor_probes: str = "aggregate",
107
+ lr_mirror: Optional[bool] = None,
108
+ missing: Optional[str] = None,
109
+ tolerance: float = 2,
110
+ sample_norm: str = "srs",
111
+ gene_norm: str = "srs",
112
+ norm_matched: bool = True,
113
+ norm_structures: bool = False,
114
+ region_agg: str = "donors",
115
+ agg_metric: str = "mean",
116
+ corrected_mni: bool = True,
117
+ reannotated: bool = True,
118
+ return_counts: bool = False,
119
+ return_donors: bool = False,
120
+ return_report: bool = False,
121
+ donors: str = "all",
122
+ data_dir: Optional[str] = None,
123
+ verbose: float = 0,
124
+ n_proc: int = 1,
125
+ ) -> Union[pd.DataFrame, dict, tuple]:
126
+ """Computes genetic expression of surface parcels.
127
+
128
+ Parameters
129
+ ----------
130
+ labels : list-of-str or numpy.ndarray
131
+ List of paths to label files for the parcellation, or numpy array
132
+ containing the pre-loaded labels
133
+ surfaces : list-of-image, optional
134
+ List of paths to surface files or preloaded surfaces. If not specified
135
+ assumes that `labels` are on the `fsaverage5` surface. Default: None
136
+ space : {'fsaverage', 'fslr'}
137
+ What template space `surfaces` are aligned to. If not specified assumes
138
+ that `labels` are on the `fsaverage5` surface. Default: None
139
+
140
+ For details of the remaining parameters please consult the
141
+ abagen.get_expression_data() documentation. All its parameters bar "atlas"
142
+ are valid input parameters.
143
+
144
+ Returns
145
+ -------
146
+ pandas.DataFrame, dict or tuple
147
+ Expression data returned unchanged from abagen. The return_counts,
148
+ return_donors and return_report options can change the return type.
149
+
150
+ Examples
151
+ --------
152
+ >>> from brainstat.context.genetics import surface_genetic_expression
153
+ >>> from nilearn import datasets
154
+ >>> import numpy as np
155
+
156
+ >>> destrieux = datasets.fetch_atlas_surf_destrieux()
157
+ >>> labels = np.hstack((destrieux['map_left'], destrieux['map_right']))
158
+ >>> fsaverage = datasets.fetch_surf_fsaverage()
159
+ >>> surfaces = (fsaverage['pial_left'], fsaverage['pial_right'])
160
+ >>> expression = surface_genetic_expression(labels, surfaces,
161
+ ... space='fsaverage')
162
+ """
163
+
164
+ if isinstance(surfaces, (str, Path)):
165
+ surfaces = [surfaces]
166
+ elif surfaces is not None:
167
+ surfaces = list(surfaces)
168
+
169
+ if surfaces is not None and len(surfaces) != 2:
170
+ raise ValueError("Provide both left and right hemisphere surfaces.")
171
+
172
+ if isinstance(labels, np.ndarray):
173
+ if (
174
+ labels.ndim != 1
175
+ or labels.size == 0
176
+ or not np.issubdtype(labels.dtype, np.number)
177
+ or np.iscomplexobj(labels)
178
+ or not np.all(np.isfinite(labels))
179
+ or np.any(labels < 0)
180
+ or np.any(labels > np.iinfo(np.int32).max)
181
+ or not np.all(labels == np.floor(labels))
182
+ ):
183
+ raise ValueError(
184
+ "labels must be a nonempty 1D array of nonnegative int32 label IDs."
185
+ )
186
+ if surfaces is None:
187
+ if labels.size != 20484:
188
+ raise ValueError(
189
+ "Without surfaces, labels must match fsaverage5 (20484 vertices)."
190
+ )
191
+ surfaces = list(
192
+ _fetch_template_surface_files(
193
+ "fsaverage5",
194
+ data_dir=(
195
+ Path(data_dir)
196
+ if data_dir
197
+ else data_directories["SURFACE_DATA_DIR"]
198
+ ),
199
+ )
200
+ )
201
+ if space is None:
202
+ space = "fsaverage"
203
+ sizes = [_surface_size(surface) for surface in surfaces]
204
+ if sum(sizes) != labels.size:
205
+ raise ValueError(
206
+ "Label count does not match the supplied surface geometry."
207
+ )
208
+
209
+ # A directory owns every temporary file, including files whose save fails.
210
+ with tempfile.TemporaryDirectory(prefix="brainstat-genetics-") as tmpdir:
211
+ if isinstance(labels, np.ndarray):
212
+ arrays = np.split(labels.astype(np.int32), [sizes[0]])
213
+ label_files = []
214
+ for index, array in enumerate(arrays):
215
+ path = str(Path(tmpdir) / f"labels-{index}.label.gii")
216
+ image = GiftiImage(
217
+ darrays=[GiftiDataArray(data=array, intent="NIFTI_INTENT_LABEL")]
218
+ )
219
+ nib.save(image, path)
220
+ label_files.append(path)
221
+ labels = tuple(label_files)
222
+ else:
223
+ labels = tuple(str(path) for path in labels)
224
+
225
+ surfaces_gii = None
226
+ if surfaces is not None:
227
+ surfaces_gii = []
228
+ for index, surface in enumerate(surfaces):
229
+ if isinstance(surface, (str, Path)):
230
+ surfaces_gii.append(str(surface))
231
+ else:
232
+ # Cast a copy: converting the surface must not alter caller geometry.
233
+ surface = surface.copy()
234
+ surface.Points = surface.Points.astype(np.float32)
235
+ path = str(Path(tmpdir) / f"surface-{index}.surf.gii")
236
+ write_surface(surface, path, otype="gii")
237
+ surfaces_gii.append(path)
238
+
239
+ # Use abagen to grab expression data.
240
+ logger.info(
241
+ "If you use BrainStat's genetics functionality, please cite abagen (https://abagen.readthedocs.io/en/stable/citing.html)."
242
+ )
243
+ with _abagen_compat():
244
+ atlas = check_atlas(labels, geometry=surfaces_gii, space=space)
245
+ expression = get_expression_data(
246
+ atlas,
247
+ atlas_info=atlas_info,
248
+ ibf_threshold=ibf_threshold,
249
+ probe_selection=probe_selection,
250
+ donor_probes=donor_probes,
251
+ lr_mirror=lr_mirror,
252
+ missing=missing,
253
+ tolerance=tolerance,
254
+ sample_norm=sample_norm,
255
+ gene_norm=gene_norm,
256
+ norm_matched=norm_matched,
257
+ norm_structures=norm_structures,
258
+ region_agg=region_agg,
259
+ agg_metric=agg_metric,
260
+ corrected_mni=corrected_mni,
261
+ reannotated=reannotated,
262
+ return_counts=return_counts,
263
+ return_donors=return_donors,
264
+ return_report=return_report,
265
+ donors=donors,
266
+ data_dir=data_dir,
267
+ verbose=verbose,
268
+ n_proc=n_proc,
269
+ )
270
+
271
+ return expression
272
+
273
+
274
+ def __create_precomputed(
275
+ data_dir: Optional[Union[str, Path]] = None,
276
+ output_dir: Optional[Union[str, Path]] = None,
277
+ ) -> None:
278
+ """Creates precomputed matrices of genetic expression for standard atlases. These are
279
+ used for the MATLAB implementation."""
280
+
281
+ if output_dir is None:
282
+ output_dir = data_dir = (
283
+ Path(data_dir) if data_dir else data_directories["BRAINSTAT_DATA_DIR"]
284
+ )
285
+
286
+ # Get all parcellations in a format compatible with ParameterGrid.
287
+ parcellations = _valid_parcellations()
288
+ for key in parcellations:
289
+ parcellations[key].update({"atlas": [key]})
290
+ if key == "schaefer":
291
+ parcellations[key].update({"seven_networks": (True, False)})
292
+ else:
293
+ parcellations[key].update({"seven_networks": (True,)})
294
+
295
+ # We only really need to compute this for one template surface.
296
+ # We'll use fsaverage5 for all.
297
+ parcellations[key].update({"surfaces": ["fsaverage5"]})
298
+
299
+ param_grid = ParameterGrid(list(parcellations.values()))
300
+
301
+ # Compute expression for all parcellations.
302
+ for params in param_grid:
303
+ if params["atlas"] == "schaefer":
304
+ network_tag = "7Networks" if params["seven_networks"] else "17Networks"
305
+ else:
306
+ network_tag = ""
307
+
308
+ filename_components = filter(
309
+ None,
310
+ (
311
+ "expression",
312
+ params["atlas"],
313
+ str(params["n_regions"]),
314
+ network_tag,
315
+ ),
316
+ )
317
+
318
+ filename = "_".join(filename_components) + ".csv.gz"
319
+ output_file = Path(output_dir) / filename
320
+ if output_file.exists():
321
+ print('Skipping "{}"'.format(output_file))
322
+ continue
323
+ else:
324
+ print('Computing "{}"'.format(output_file))
325
+
326
+ surface_files = _fetch_template_surface_files(
327
+ params["surfaces"], data_dir=data_dir # type: ignore
328
+ )
329
+ space = "fslr" if params["surfaces"] == "fslr32k" else "fsaverage"
330
+
331
+ labels = fetch_parcellation(
332
+ params["surfaces"],
333
+ params["atlas"],
334
+ params["n_regions"],
335
+ seven_networks=params["seven_networks"],
336
+ data_dir=data_dir,
337
+ )
338
+
339
+ surf_lh = tempfile.NamedTemporaryFile(suffix=".surf.gii")
340
+ surf_rh = tempfile.NamedTemporaryFile(suffix=".surf.gii")
341
+ for i, surf in enumerate((surf_lh, surf_rh)):
342
+ __freesurfer_to_surfgii(surface_files[i], surf.name)
343
+ surface_paths = [surf_lh.name, surf_rh.name]
344
+
345
+ expression = surface_genetic_expression(labels, surface_paths, space=space) # type: ignore
346
+ expression.to_csv(output_file) # type: ignore
347
+
348
+
349
+ def __freesurfer_to_surfgii(freesurfer_file: str, gifti_file: str) -> None:
350
+ surf = read_surface(freesurfer_file, itype="fs")
351
+ write_surface(surf, gifti_file, otype="gii")
@@ -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]:
@@ -0,0 +1,5 @@
1
+ """Reporting helpers for BrainStat results."""
2
+
3
+ from ._slm import make_slm_report
4
+
5
+ __all__ = ["make_slm_report"]