brainstat 0.4.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.
- {brainstat-0.4.2/brainstat.egg-info → brainstat-0.6.0}/PKG-INFO +38 -7
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/__init__.py +1 -1
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/_utils.py +28 -3
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/context/genetics.py +131 -47
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/context/histology.py +45 -7
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/datasets/base.py +163 -5
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/mesh/interpolate.py +1 -1
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/stats/SLM.py +62 -11
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/stats/_linear_model.py +3 -1
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/stats/_multiple_comparisons.py +2 -3
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/stats/_t_test.py +4 -3
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/stats/terms.py +16 -7
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/stats/utils.py +4 -2
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_SLM.py +81 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_datasets.py +1 -0
- brainstat-0.6.0/brainstat/tests/test_genetics.py +33 -0
- brainstat-0.6.0/brainstat/tests/test_histology.py +32 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_mesh_edges.py +1 -1
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_terms.py +26 -2
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tutorial/utils.py +1 -1
- {brainstat-0.4.2 → brainstat-0.6.0/brainstat.egg-info}/PKG-INFO +38 -7
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat.egg-info/SOURCES.txt +1 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat.egg-info/requires.txt +6 -5
- {brainstat-0.4.2 → brainstat-0.6.0}/setup.cfg +1 -1
- {brainstat-0.4.2 → brainstat-0.6.0}/setup.py +11 -9
- brainstat-0.4.2/brainstat/tests/test_histology.py +0 -21
- {brainstat-0.4.2 → brainstat-0.6.0}/LICENSE +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/MANIFEST.in +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/README.rst +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/_typing.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/context/__init__.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/context/meta_analysis.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/context/resting.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/data/data_urls.json +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/data/logging.conf +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/data/nn_interp_civet164k.nii.gz +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/data/nn_interp_civet41k.nii.gz +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/data/nn_interp_fsaverage.nii.gz +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/data/nn_interp_fsaverage5.nii.gz +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/data/nn_interp_fslr32k.nii.gz +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/datasets/__init__.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/mesh/__init__.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/mesh/data.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/mesh/utils.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/stats/__init__.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/__init__.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/conftest.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_f.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_fdr.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_linear_model.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_mesh_edges.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_mesh_smooth.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_peak_clus.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_random_field_theory.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_resels.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_slm.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_stat_threshold.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/datagen_t_test.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_f_test.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_fdr.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_linear_model.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_mesh_smooth.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_peak_clus.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_random_field_theory.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_resels.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_stat_threshold.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/test_t_test.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tests/testutil.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat/tutorial/__init__.py +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat.egg-info/dependency_links.txt +0 -0
- {brainstat-0.4.2 → brainstat-0.6.0}/brainstat.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: brainstat
|
|
3
|
-
Version: 0.
|
|
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
|
|
@@ -13,13 +13,44 @@ Classifier: Intended Audience :: Science/Research
|
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Topic :: Software Development :: Build Tools
|
|
15
15
|
Classifier: Topic :: Scientific/Engineering
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.
|
|
17
|
-
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
-
Requires-Python: >=3.7.*
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Requires-Python: >=3.10
|
|
20
18
|
Description-Content-Type: text/x-rst
|
|
21
|
-
Provides-Extra: dev
|
|
22
19
|
License-File: LICENSE
|
|
20
|
+
Requires-Dist: abagen>=0.1
|
|
21
|
+
Requires-Dist: brainspace>=0.1.2
|
|
22
|
+
Requires-Dist: h5py
|
|
23
|
+
Requires-Dist: matplotlib>=3.4
|
|
24
|
+
Requires-Dist: netneurotools<0.3.0
|
|
25
|
+
Requires-Dist: neurosynth
|
|
26
|
+
Requires-Dist: nibabel
|
|
27
|
+
Requires-Dist: nilearn>=0.7.0
|
|
28
|
+
Requires-Dist: nimare
|
|
29
|
+
Requires-Dist: numpy<2,>=1.21
|
|
30
|
+
Requires-Dist: pandas<2.1,>=1.3
|
|
31
|
+
Requires-Dist: scikit_learn>=1.0
|
|
32
|
+
Requires-Dist: scipy>=1.7
|
|
33
|
+
Requires-Dist: templateflow
|
|
34
|
+
Requires-Dist: trimesh
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: gitpython; extra == "dev"
|
|
37
|
+
Requires-Dist: hcp-utils; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy; extra == "dev"
|
|
39
|
+
Requires-Dist: plotly; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest; extra == "dev"
|
|
41
|
+
Dynamic: author
|
|
42
|
+
Dynamic: author-email
|
|
43
|
+
Dynamic: classifier
|
|
44
|
+
Dynamic: description
|
|
45
|
+
Dynamic: description-content-type
|
|
46
|
+
Dynamic: home-page
|
|
47
|
+
Dynamic: license
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
Dynamic: project-url
|
|
50
|
+
Dynamic: provides-extra
|
|
51
|
+
Dynamic: requires-dist
|
|
52
|
+
Dynamic: requires-python
|
|
53
|
+
Dynamic: summary
|
|
23
54
|
|
|
24
55
|
BrainStat : A toolbox for statistical analysis of neuroimaging data
|
|
25
56
|
===================================================================
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Neuroimaging statistics toolbox."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.6.0"
|
|
@@ -150,7 +150,7 @@ def deprecated(message: str) -> Callable:
|
|
|
150
150
|
def _download_file(
|
|
151
151
|
url: str, output_file: Path, overwrite: bool = False, verbose=True
|
|
152
152
|
) -> None:
|
|
153
|
-
"""Downloads a file.
|
|
153
|
+
"""Downloads a file with retry logic for network failures.
|
|
154
154
|
|
|
155
155
|
Parameters
|
|
156
156
|
----------
|
|
@@ -163,14 +163,39 @@ def _download_file(
|
|
|
163
163
|
verbose : bool
|
|
164
164
|
If true, print a download message, defaults to True.
|
|
165
165
|
"""
|
|
166
|
+
import time
|
|
167
|
+
from http.client import RemoteDisconnected
|
|
168
|
+
from urllib.error import URLError
|
|
166
169
|
|
|
167
170
|
if output_file.exists() and not overwrite:
|
|
168
171
|
return
|
|
169
172
|
|
|
170
173
|
if verbose:
|
|
171
174
|
logger.info("Downloading " + str(output_file) + " from " + url + ".")
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
|
|
176
|
+
# Retry logic for intermittent network failures
|
|
177
|
+
max_retries = 3
|
|
178
|
+
retry_delay = 2 # seconds
|
|
179
|
+
|
|
180
|
+
for attempt in range(max_retries):
|
|
181
|
+
try:
|
|
182
|
+
with urllib.request.urlopen(url, timeout=30) as response, open(output_file, "wb") as out_file:
|
|
183
|
+
shutil.copyfileobj(response, out_file)
|
|
184
|
+
return # Success, exit function
|
|
185
|
+
except (RemoteDisconnected, URLError, TimeoutError) as e:
|
|
186
|
+
if attempt < max_retries - 1:
|
|
187
|
+
logger.warning(
|
|
188
|
+
f"Download attempt {attempt + 1}/{max_retries} failed: {e}. "
|
|
189
|
+
f"Retrying in {retry_delay} seconds..."
|
|
190
|
+
)
|
|
191
|
+
time.sleep(retry_delay)
|
|
192
|
+
retry_delay *= 2 # Exponential backoff
|
|
193
|
+
else:
|
|
194
|
+
logger.error(f"Download failed after {max_retries} attempts.")
|
|
195
|
+
raise # Re-raise the exception after all retries fail
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
174
199
|
|
|
175
200
|
|
|
176
201
|
if __name__ == "__main__":
|
|
@@ -2,13 +2,59 @@
|
|
|
2
2
|
import tempfile
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Optional, Sequence, Union
|
|
5
|
-
|
|
5
|
+
import os
|
|
6
6
|
import nibabel as nib
|
|
7
7
|
import numpy as np
|
|
8
8
|
import pandas as pd
|
|
9
|
+
|
|
10
|
+
# Monkeypatch for abagen compatibility with pandas 2.0+
|
|
11
|
+
if not hasattr(pd.DataFrame, 'append'):
|
|
12
|
+
def _append(self, other, ignore_index=False, verify_integrity=False, sort=False):
|
|
13
|
+
from pandas import concat
|
|
14
|
+
if isinstance(other, (list, tuple)):
|
|
15
|
+
to_concat = [self] + list(other)
|
|
16
|
+
else:
|
|
17
|
+
to_concat = [self, other]
|
|
18
|
+
return concat(to_concat, ignore_index=ignore_index, verify_integrity=verify_integrity, sort=sort)
|
|
19
|
+
pd.DataFrame.append = _append
|
|
20
|
+
|
|
21
|
+
# Monkeypatch for abagen compatibility with pandas 2.0+ (set_axis inplace)
|
|
22
|
+
_original_set_axis = pd.DataFrame.set_axis
|
|
23
|
+
def _set_axis_patched(self, labels, *args, **kwargs):
|
|
24
|
+
if 'inplace' in kwargs:
|
|
25
|
+
del kwargs['inplace']
|
|
26
|
+
return _original_set_axis(self, labels, *args, **kwargs)
|
|
27
|
+
pd.DataFrame.set_axis = _set_axis_patched
|
|
28
|
+
|
|
29
|
+
import abagen.images
|
|
30
|
+
|
|
31
|
+
def _labeltable_to_df_patched(labels):
|
|
32
|
+
"""
|
|
33
|
+
Patched version of abagen.utils.labeltable_to_df to handle missing 0 index
|
|
34
|
+
and use pd.concat instead of append.
|
|
35
|
+
"""
|
|
36
|
+
info = pd.DataFrame(columns=['id', 'label', 'hemisphere', 'structure'])
|
|
37
|
+
for table, hemi in zip(labels, ('L', 'R')):
|
|
38
|
+
if len(table) == 0:
|
|
39
|
+
continue
|
|
40
|
+
ids, label = zip(*table.items())
|
|
41
|
+
new_df = pd.DataFrame(dict(id=ids, label=label, hemisphere=hemi, structure='cortex'))
|
|
42
|
+
info = pd.concat([info, new_df], ignore_index=True)
|
|
43
|
+
|
|
44
|
+
# Use errors='ignore' to handle missing 0
|
|
45
|
+
info = info.set_index('id').drop([0], axis=0, errors='ignore').sort_index()
|
|
46
|
+
|
|
47
|
+
if len(info) != 0:
|
|
48
|
+
return info
|
|
49
|
+
|
|
50
|
+
abagen.images.labeltable_to_df = _labeltable_to_df_patched
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
import collections
|
|
9
54
|
from abagen import check_atlas, get_expression_data
|
|
10
55
|
from brainspace.mesh.mesh_io import read_surface, write_surface
|
|
11
56
|
from sklearn.model_selection import ParameterGrid
|
|
57
|
+
from nibabel.gifti import GiftiImage, GiftiDataArray
|
|
12
58
|
|
|
13
59
|
from brainstat._utils import data_directories, logger
|
|
14
60
|
from brainstat.datasets.base import (
|
|
@@ -23,7 +69,7 @@ def surface_genetic_expression(
|
|
|
23
69
|
surfaces: Optional[Union[str, Sequence[str]]] = None,
|
|
24
70
|
space: Optional[str] = None,
|
|
25
71
|
*,
|
|
26
|
-
atlas_info: str = None,
|
|
72
|
+
atlas_info: Optional[str] = None,
|
|
27
73
|
ibf_threshold: float = 0.5,
|
|
28
74
|
probe_selection: str = "diff_stability",
|
|
29
75
|
donor_probes: str = "aggregate",
|
|
@@ -89,52 +135,90 @@ def surface_genetic_expression(
|
|
|
89
135
|
elif surfaces is None:
|
|
90
136
|
surfaces = []
|
|
91
137
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if
|
|
95
|
-
#
|
|
138
|
+
temp_files = []
|
|
139
|
+
try:
|
|
140
|
+
if isinstance(labels, np.ndarray):
|
|
141
|
+
# Assuming 'labels' is a 1D NumPy array of length 20484
|
|
142
|
+
num_vertices = len(labels) # Should be 20484
|
|
143
|
+
half_size = num_vertices // 2 # Half of 20484, which is 10242
|
|
144
|
+
|
|
145
|
+
# Split the array into two halves
|
|
146
|
+
labels_left = labels[:half_size] # First half for the left hemisphere
|
|
147
|
+
labels_right = labels[half_size:] # Second half for the right hemisphere
|
|
148
|
+
|
|
149
|
+
# Create GiftiDataArrays for each hemisphere
|
|
150
|
+
data_array_left = GiftiDataArray(data=labels_left)
|
|
151
|
+
data_array_right = GiftiDataArray(data=labels_right)
|
|
152
|
+
|
|
153
|
+
# Create separate GiftiImages for each hemisphere
|
|
154
|
+
labels_left_gii = GiftiImage(darrays=[data_array_left])
|
|
155
|
+
labels_right_gii = GiftiImage(darrays=[data_array_right])
|
|
156
|
+
|
|
157
|
+
# Save to temporary files for abagen compatibility
|
|
158
|
+
labels_files = []
|
|
159
|
+
for img in [labels_left_gii, labels_right_gii]:
|
|
160
|
+
f = tempfile.NamedTemporaryFile(suffix=".gii", delete=False)
|
|
161
|
+
f.close()
|
|
162
|
+
nib.save(img, f.name)
|
|
163
|
+
labels_files.append(f.name)
|
|
164
|
+
temp_files.append(f.name)
|
|
165
|
+
labels = tuple(labels_files)
|
|
166
|
+
|
|
167
|
+
surfaces_gii = []
|
|
168
|
+
for surface in surfaces:
|
|
169
|
+
if not isinstance(surface, str) and not isinstance(surface, Path):
|
|
170
|
+
# Cast surface data to float32 to comply with GIFTI standard
|
|
171
|
+
# GIFTI only supports uint8, int32, and float32 datatypes
|
|
172
|
+
if hasattr(surface, 'Points') and surface.Points.dtype != np.float32:
|
|
173
|
+
surface = surface.copy()
|
|
174
|
+
surface.Points = surface.Points.astype(np.float32)
|
|
175
|
+
|
|
176
|
+
f = tempfile.NamedTemporaryFile(suffix=".gii", delete=False)
|
|
177
|
+
f.close()
|
|
178
|
+
write_surface(surface, f.name, otype="gii")
|
|
179
|
+
surfaces_gii.append(f.name)
|
|
180
|
+
temp_files.append(f.name)
|
|
181
|
+
else:
|
|
182
|
+
surfaces_gii.append(surface)
|
|
183
|
+
|
|
184
|
+
# Use abagen to grab expression data.
|
|
185
|
+
logger.info(
|
|
186
|
+
"If you use BrainStat's genetics functionality, please cite abagen (https://abagen.readthedocs.io/en/stable/citing.html)."
|
|
187
|
+
)
|
|
188
|
+
atlas = check_atlas(labels, geometry=surfaces_gii, space=space)
|
|
189
|
+
expression = get_expression_data(
|
|
190
|
+
atlas,
|
|
191
|
+
atlas_info=atlas_info,
|
|
192
|
+
ibf_threshold=ibf_threshold,
|
|
193
|
+
probe_selection=probe_selection,
|
|
194
|
+
donor_probes=donor_probes,
|
|
195
|
+
lr_mirror=lr_mirror,
|
|
196
|
+
missing=missing,
|
|
197
|
+
tolerance=tolerance,
|
|
198
|
+
sample_norm=sample_norm,
|
|
199
|
+
gene_norm=gene_norm,
|
|
200
|
+
norm_matched=norm_matched,
|
|
201
|
+
norm_structures=norm_structures,
|
|
202
|
+
region_agg=region_agg,
|
|
203
|
+
agg_metric=agg_metric,
|
|
204
|
+
corrected_mni=corrected_mni,
|
|
205
|
+
reannotated=reannotated,
|
|
206
|
+
return_counts=return_counts,
|
|
207
|
+
return_donors=return_donors,
|
|
208
|
+
return_report=return_report,
|
|
209
|
+
donors=donors,
|
|
210
|
+
data_dir=data_dir,
|
|
211
|
+
verbose=verbose,
|
|
212
|
+
n_proc=n_proc,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return expression
|
|
216
|
+
finally:
|
|
217
|
+
for f in temp_files:
|
|
96
218
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
surfaces_gii.append(nib.load(name))
|
|
101
|
-
finally:
|
|
102
|
-
Path(name).unlink()
|
|
103
|
-
else:
|
|
104
|
-
surfaces_gii.append(nib.load(surface))
|
|
105
|
-
|
|
106
|
-
# Use abagen to grab expression data.
|
|
107
|
-
logger.info(
|
|
108
|
-
"If you use BrainStat's genetics functionality, please cite abagen (https://abagen.readthedocs.io/en/stable/citing.html)."
|
|
109
|
-
)
|
|
110
|
-
atlas = check_atlas(labels, geometry=surfaces_gii, space=space)
|
|
111
|
-
expression = get_expression_data(
|
|
112
|
-
atlas,
|
|
113
|
-
atlas_info=atlas_info,
|
|
114
|
-
ibf_threshold=ibf_threshold,
|
|
115
|
-
probe_selection=probe_selection,
|
|
116
|
-
donor_probes=donor_probes,
|
|
117
|
-
lr_mirror=lr_mirror,
|
|
118
|
-
missing=missing,
|
|
119
|
-
tolerance=tolerance,
|
|
120
|
-
sample_norm=sample_norm,
|
|
121
|
-
gene_norm=gene_norm,
|
|
122
|
-
norm_matched=norm_matched,
|
|
123
|
-
norm_structures=norm_structures,
|
|
124
|
-
region_agg=region_agg,
|
|
125
|
-
agg_metric=agg_metric,
|
|
126
|
-
corrected_mni=corrected_mni,
|
|
127
|
-
reannotated=reannotated,
|
|
128
|
-
return_counts=return_counts,
|
|
129
|
-
return_donors=return_donors,
|
|
130
|
-
return_report=return_report,
|
|
131
|
-
donors=donors,
|
|
132
|
-
data_dir=data_dir,
|
|
133
|
-
verbose=verbose,
|
|
134
|
-
n_proc=n_proc,
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
return expression
|
|
219
|
+
Path(f).unlink()
|
|
220
|
+
except FileNotFoundError:
|
|
221
|
+
pass
|
|
138
222
|
|
|
139
223
|
|
|
140
224
|
def __create_precomputed(
|
|
@@ -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
|
-------
|
|
@@ -156,13 +166,32 @@ def read_histology_profile(
|
|
|
156
166
|
)
|
|
157
167
|
|
|
158
168
|
with h5py.File(histology_file, "r") as h5_file:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
if template == "fslr32k":
|
|
170
|
+
# Known issue #369: The fslr32k data file on the server actually contains
|
|
171
|
+
# fs_LR_64k resolution data (64984 vertices) instead of fs_LR_32k (32492 vertices).
|
|
172
|
+
# This is a data hosting issue. Users expecting 32k resolution data should be aware
|
|
173
|
+
# that they will receive 64k resolution data until this is fixed on the server.
|
|
174
|
+
logger.warning(
|
|
175
|
+
"Known issue: The fslr32k histology profile data currently contains "
|
|
176
|
+
"fs_LR_64k resolution (64984 vertices) instead of fs_LR_32k (32492 vertices). "
|
|
177
|
+
"See https://github.com/MICA-MNI/BrainStat/issues/369 for details."
|
|
178
|
+
)
|
|
179
|
+
profiles = h5_file.get("fs_LR_64k")[...]
|
|
164
180
|
else:
|
|
165
|
-
|
|
181
|
+
profiles = h5_file.get(template)[...]
|
|
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
|
|
166
195
|
|
|
167
196
|
|
|
168
197
|
def download_histology_profiles(
|
|
@@ -191,7 +220,16 @@ def download_histology_profiles(
|
|
|
191
220
|
|
|
192
221
|
data_dir = Path(data_dir) if data_dir else data_directories["BIGBRAIN_DATA_DIR"]
|
|
193
222
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
194
|
-
|
|
223
|
+
if template == "fslr32k":
|
|
224
|
+
output_file = data_dir / "histology_fslr32k.h5"
|
|
225
|
+
# Known issue #369: warn users about data resolution mismatch
|
|
226
|
+
logger.warning(
|
|
227
|
+
"Note: The fslr32k histology profile data currently contains "
|
|
228
|
+
"fs_LR_64k resolution (64984 vertices) instead of fs_LR_32k. "
|
|
229
|
+
"See https://github.com/MICA-MNI/BrainStat/issues/369 for details."
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
output_file = data_dir / ("histology_" + template + ".h5")
|
|
195
233
|
|
|
196
234
|
url = read_data_fetcher_json()["bigbrain_profiles"][template]["url"]
|
|
197
235
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
""" Load external datasets. """
|
|
2
|
+
import json
|
|
2
3
|
import tempfile
|
|
3
4
|
import zipfile
|
|
5
|
+
from collections import namedtuple
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import List, Optional, Tuple, Union
|
|
6
8
|
|
|
@@ -11,9 +13,19 @@ from brainspace.mesh.mesh_io import read_surface
|
|
|
11
13
|
from brainspace.mesh.mesh_operations import combine_surfaces
|
|
12
14
|
from brainspace.vtk_interface.wrappers.data_object import BSPolyData
|
|
13
15
|
from netneurotools import datasets as nnt_datasets
|
|
14
|
-
from netneurotools.civet import read_civet
|
|
15
16
|
from nibabel import load as nib_load
|
|
16
17
|
from nibabel.freesurfer.io import read_annot, read_geometry
|
|
18
|
+
from sklearn.utils import Bunch
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from netneurotools.datasets.utils import _get_data_dir, _get_dataset_info
|
|
22
|
+
except ImportError:
|
|
23
|
+
from netneurotools.datasets.datasets_utils import _get_data_dir, _get_dataset_info
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from nilearn.datasets._utils import fetch_files as _fetch_files
|
|
27
|
+
except ImportError:
|
|
28
|
+
from nilearn.datasets.utils import fetch_files as _fetch_files
|
|
17
29
|
|
|
18
30
|
from brainstat._utils import (
|
|
19
31
|
_download_file,
|
|
@@ -24,6 +36,65 @@ from brainstat._utils import (
|
|
|
24
36
|
from brainstat.mesh.interpolate import _surf2surf
|
|
25
37
|
|
|
26
38
|
|
|
39
|
+
def read_civet(fname: Union[str, Path]) -> Tuple[np.ndarray, np.ndarray]:
|
|
40
|
+
"""
|
|
41
|
+
Reads a CIVET surface file (MNI OBJ format).
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
fname : str or Path
|
|
46
|
+
Path to the CIVET surface file.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
vertices : numpy.ndarray
|
|
51
|
+
(N, 3) array of vertex coordinates.
|
|
52
|
+
faces : numpy.ndarray
|
|
53
|
+
(M, 3) array of face indices.
|
|
54
|
+
"""
|
|
55
|
+
fname = str(fname)
|
|
56
|
+
with open(fname, "rb") as f:
|
|
57
|
+
first_char = f.read(1)
|
|
58
|
+
|
|
59
|
+
if first_char == b"P":
|
|
60
|
+
with open(fname, "r") as f:
|
|
61
|
+
content = f.read().split()
|
|
62
|
+
|
|
63
|
+
n_vertices = int(content[6])
|
|
64
|
+
|
|
65
|
+
idx = 7
|
|
66
|
+
# Vertices
|
|
67
|
+
vertices = np.array(content[idx : idx + n_vertices * 3], dtype=float).reshape(
|
|
68
|
+
-1, 3
|
|
69
|
+
)
|
|
70
|
+
idx += n_vertices * 3
|
|
71
|
+
|
|
72
|
+
# Normals
|
|
73
|
+
idx += n_vertices * 3
|
|
74
|
+
|
|
75
|
+
n_triangles = int(content[idx])
|
|
76
|
+
idx += 1
|
|
77
|
+
|
|
78
|
+
color_flag = int(content[idx])
|
|
79
|
+
idx += 1
|
|
80
|
+
|
|
81
|
+
if color_flag == 0:
|
|
82
|
+
idx += 4
|
|
83
|
+
else:
|
|
84
|
+
idx += n_vertices * 4
|
|
85
|
+
|
|
86
|
+
# Check remaining items to decide if we need to skip
|
|
87
|
+
remaining = len(content) - idx
|
|
88
|
+
if remaining == n_triangles * 3 + n_triangles:
|
|
89
|
+
idx += n_triangles
|
|
90
|
+
|
|
91
|
+
triangles = np.array(content[idx:], dtype=int).reshape(-1, 3)
|
|
92
|
+
|
|
93
|
+
return vertices, triangles
|
|
94
|
+
else:
|
|
95
|
+
raise NotImplementedError("Binary CIVET files are not supported yet.")
|
|
96
|
+
|
|
97
|
+
|
|
27
98
|
def fetch_parcellation(
|
|
28
99
|
template: str,
|
|
29
100
|
atlas: str,
|
|
@@ -40,12 +111,15 @@ def fetch_parcellation(
|
|
|
40
111
|
The surface template. Valid values are "fsaverage", "fsaverage5",
|
|
41
112
|
"fsaverage6", "fslr32k", "civet41k", "civet164k", by default "fsaverage5".
|
|
42
113
|
atlas : str
|
|
43
|
-
Name of the atlas. Valid names are "cammoun", "
|
|
114
|
+
Name of the atlas. Valid names are "cammoun", "destrieux", "glasser",
|
|
115
|
+
"schaefer", "yeo".
|
|
44
116
|
n_regions : int
|
|
45
117
|
Number of regions of the requested atlas. Valid values for the cammoun
|
|
46
118
|
atlas are 33, 60, 125, 250, 500. Valid values for the glasser atlas are
|
|
47
119
|
360. Valid values for the "schaefer" atlas are 100, 200, 300, 400, 500,
|
|
48
|
-
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``.
|
|
49
123
|
join : bool, optional
|
|
50
124
|
If true, returns parcellation as a single array, if false, returns an
|
|
51
125
|
array per hemisphere, by default True.
|
|
@@ -83,6 +157,8 @@ def fetch_parcellation(
|
|
|
83
157
|
parcellations = _fetch_glasser_parcellation(template, data_dir)
|
|
84
158
|
elif atlas == "yeo":
|
|
85
159
|
parcellations = _fetch_yeo_parcellation(template, n_regions, data_dir)
|
|
160
|
+
elif atlas == "destrieux":
|
|
161
|
+
parcellations = _fetch_destrieux_parcellation(template, data_dir)
|
|
86
162
|
else:
|
|
87
163
|
raise ValueError(f"Invalid atlas: {atlas}")
|
|
88
164
|
|
|
@@ -147,7 +223,7 @@ def fetch_template_surface(
|
|
|
147
223
|
surfaces_fs = [read_geometry(file) for file in surface_files]
|
|
148
224
|
surfaces = [build_polydata(surface[0], surface[1]) for surface in surfaces_fs]
|
|
149
225
|
elif template == "fslr32k":
|
|
150
|
-
surfaces = [read_surface(file) for file in surface_files]
|
|
226
|
+
surfaces = [read_surface(str(file)) for file in surface_files]
|
|
151
227
|
else:
|
|
152
228
|
surfaces_obj = [read_civet(file) for file in surface_files]
|
|
153
229
|
surfaces = [build_polydata(surface[0], surface[1]) for surface in surfaces_obj]
|
|
@@ -384,7 +460,7 @@ def _fetch_template_surface_files(
|
|
|
384
460
|
|
|
385
461
|
if template == "fslr32k":
|
|
386
462
|
layer = layer if layer else "midthickness"
|
|
387
|
-
bunch =
|
|
463
|
+
bunch = _fetch_conte69_fixed(data_dir=str(data_dir))
|
|
388
464
|
elif template == "civet41k" or template == "civet164k":
|
|
389
465
|
layer = layer if layer else "mid"
|
|
390
466
|
if layer == "sphere":
|
|
@@ -466,6 +542,48 @@ def _fetch_glasser_parcellation(template: str, data_dir: Path) -> List[np.ndarra
|
|
|
466
542
|
return parcellations
|
|
467
543
|
|
|
468
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
|
+
|
|
469
587
|
def _fetch_yeo_parcellation(
|
|
470
588
|
template: str, n_regions: int, data_dir: Path
|
|
471
589
|
) -> List[np.ndarray]:
|
|
@@ -515,3 +633,43 @@ def _fetch_civet_spheres(template: str, data_dir: Path) -> Tuple[str, str]:
|
|
|
515
633
|
|
|
516
634
|
# Return two filenames to conform to other left/right hemisphere functions.
|
|
517
635
|
return (str(filename), str(filename))
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
SURFACE = namedtuple('Surface', ('lh', 'rh'))
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _fetch_conte69_fixed(data_dir=None, url=None, resume=True, verbose=1):
|
|
642
|
+
"""
|
|
643
|
+
Download files for Van Essen et al., 2012 Conte69 template.
|
|
644
|
+
Fixed version to handle encoding on Windows.
|
|
645
|
+
"""
|
|
646
|
+
dataset_name = 'tpl-conte69'
|
|
647
|
+
keys = ['midthickness', 'inflated', 'vinflated']
|
|
648
|
+
|
|
649
|
+
data_dir = _get_data_dir(data_dir=data_dir)
|
|
650
|
+
info = _get_dataset_info(dataset_name)
|
|
651
|
+
if url is None:
|
|
652
|
+
url = info['url']
|
|
653
|
+
|
|
654
|
+
opts = {
|
|
655
|
+
'uncompress': True,
|
|
656
|
+
'md5sum': info['md5'],
|
|
657
|
+
'move': '{}.tar.gz'.format(dataset_name)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
filenames = [
|
|
661
|
+
'tpl-conte69/tpl-conte69_space-MNI305_variant-fsLR32k_{}.{}.surf.gii'
|
|
662
|
+
.format(res, hemi) for res in keys for hemi in ['L', 'R']
|
|
663
|
+
] + ['tpl-conte69/template_description.json']
|
|
664
|
+
|
|
665
|
+
data = _fetch_files(data_dir, files=[(f, url, opts) for f in filenames],
|
|
666
|
+
resume=resume, verbose=verbose)
|
|
667
|
+
|
|
668
|
+
# Fix: Specify encoding='utf-8'
|
|
669
|
+
with open(data[-1], 'r', encoding='utf-8') as src:
|
|
670
|
+
data[-1] = json.load(src)
|
|
671
|
+
|
|
672
|
+
# bundle hemispheres together
|
|
673
|
+
data = [SURFACE(*data[:-1][i:i + 2]) for i in range(0, 6, 2)] + [data[-1]]
|
|
674
|
+
|
|
675
|
+
return Bunch(**dict(zip(keys + ['info'], data)))
|
|
@@ -13,7 +13,7 @@ from brainspace.mesh.mesh_elements import get_cells, get_points
|
|
|
13
13
|
from brainspace.mesh.mesh_io import read_surface
|
|
14
14
|
from brainspace.vtk_interface.wrappers.data_object import BSPolyData
|
|
15
15
|
from nilearn.datasets import load_mni152_brain_mask
|
|
16
|
-
from scipy.interpolate
|
|
16
|
+
from scipy.interpolate import LinearNDInterpolator, NearestNDInterpolator
|
|
17
17
|
from scipy.spatial import cKDTree
|
|
18
18
|
|
|
19
19
|
from brainstat._typing import ArrayLike
|