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.
- {brainstat-0.5.2/brainstat.egg-info → brainstat-0.7.0}/PKG-INFO +9 -7
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/__init__.py +1 -1
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/_utils.py +6 -2
- brainstat-0.7.0/brainstat/context/genetics.py +351 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/context/histology.py +23 -6
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/datasets/base.py +49 -2
- brainstat-0.7.0/brainstat/reporting/__init__.py +5 -0
- brainstat-0.7.0/brainstat/reporting/_slm.py +316 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/SLM.py +78 -11
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/_multiple_comparisons.py +0 -1
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/_t_test.py +4 -3
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_SLM.py +50 -0
- brainstat-0.7.0/brainstat/tests/test_download.py +65 -0
- brainstat-0.7.0/brainstat/tests/test_genetics.py +215 -0
- brainstat-0.7.0/brainstat/tests/test_reporting.py +136 -0
- {brainstat-0.5.2 → brainstat-0.7.0/brainstat.egg-info}/PKG-INFO +9 -7
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat.egg-info/SOURCES.txt +4 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat.egg-info/requires.txt +5 -3
- {brainstat-0.5.2 → brainstat-0.7.0}/setup.py +11 -6
- brainstat-0.5.2/brainstat/context/genetics.py +0 -301
- brainstat-0.5.2/brainstat/tests/test_genetics.py +0 -33
- {brainstat-0.5.2 → brainstat-0.7.0}/LICENSE +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/MANIFEST.in +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/README.rst +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/_typing.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/context/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/context/meta_analysis.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/context/resting.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/data_urls.json +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/logging.conf +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_civet164k.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_civet41k.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_fsaverage.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_fsaverage5.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/data/nn_interp_fslr32k.nii.gz +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/datasets/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/mesh/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/mesh/data.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/mesh/interpolate.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/mesh/utils.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/_linear_model.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/terms.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/stats/utils.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/conftest.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_f.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_fdr.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_linear_model.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_mesh_edges.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_mesh_smooth.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_peak_clus.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_random_field_theory.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_resels.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_slm.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_stat_threshold.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/datagen_t_test.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_datasets.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_f_test.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_fdr.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_histology.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_linear_model.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_mesh_edges.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_mesh_smooth.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_peak_clus.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_random_field_theory.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_resels.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_stat_threshold.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_t_test.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/test_terms.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tests/testutil.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tutorial/__init__.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat/tutorial/utils.py +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat.egg-info/dependency_links.txt +0 -0
- {brainstat-0.5.2 → brainstat-0.7.0}/brainstat.egg-info/top_level.txt +0 -0
- {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.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: A toolbox for statistical analysis of neuroimaging data
|
|
5
|
-
Home-page: https://github.com/MICA-
|
|
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-
|
|
11
|
-
Project-URL: Source, https://github.com/MICA-
|
|
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.
|
|
29
|
+
Requires-Dist: nilearn>=0.10.4
|
|
28
30
|
Requires-Dist: nimare
|
|
29
|
-
Requires-Dist: numpy
|
|
30
|
-
Requires-Dist: pandas
|
|
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.
|
|
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
|
|
183
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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", "
|
|
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]:
|