brainstat 0.3.6__tar.gz → 0.5.2__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 (72) hide show
  1. {brainstat-0.3.6/brainstat.egg-info → brainstat-0.5.2}/PKG-INFO +42 -13
  2. {brainstat-0.3.6 → brainstat-0.5.2}/README.rst +3 -2
  3. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/__init__.py +1 -1
  4. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/_utils.py +30 -5
  5. brainstat-0.5.2/brainstat/context/genetics.py +301 -0
  6. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/context/histology.py +23 -2
  7. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/context/meta_analysis.py +97 -4
  8. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/data/data_urls.json +2 -2
  9. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/datasets/base.py +115 -4
  10. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/mesh/interpolate.py +1 -1
  11. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/mesh/utils.py +0 -2
  12. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/stats/SLM.py +74 -0
  13. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/stats/_linear_model.py +3 -1
  14. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/stats/_multiple_comparisons.py +4 -4
  15. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/stats/_t_test.py +1 -1
  16. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/stats/terms.py +16 -7
  17. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/stats/utils.py +4 -2
  18. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_SLM.py +31 -0
  19. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_datasets.py +1 -0
  20. brainstat-0.5.2/brainstat/tests/test_genetics.py +33 -0
  21. brainstat-0.5.2/brainstat/tests/test_histology.py +32 -0
  22. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_mesh_edges.py +1 -1
  23. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_terms.py +26 -2
  24. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tutorial/utils.py +1 -1
  25. {brainstat-0.3.6 → brainstat-0.5.2/brainstat.egg-info}/PKG-INFO +42 -13
  26. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat.egg-info/SOURCES.txt +1 -0
  27. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat.egg-info/requires.txt +6 -5
  28. {brainstat-0.3.6 → brainstat-0.5.2}/setup.cfg +1 -1
  29. {brainstat-0.3.6 → brainstat-0.5.2}/setup.py +9 -10
  30. brainstat-0.3.6/brainstat/context/genetics.py +0 -210
  31. brainstat-0.3.6/brainstat/tests/test_histology.py +0 -21
  32. {brainstat-0.3.6 → brainstat-0.5.2}/LICENSE +0 -0
  33. {brainstat-0.3.6 → brainstat-0.5.2}/MANIFEST.in +0 -0
  34. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/_typing.py +0 -0
  35. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/context/__init__.py +0 -0
  36. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/context/resting.py +0 -0
  37. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/data/logging.conf +0 -0
  38. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/data/nn_interp_civet164k.nii.gz +0 -0
  39. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/data/nn_interp_civet41k.nii.gz +0 -0
  40. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/data/nn_interp_fsaverage.nii.gz +0 -0
  41. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/data/nn_interp_fsaverage5.nii.gz +0 -0
  42. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/data/nn_interp_fslr32k.nii.gz +0 -0
  43. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/datasets/__init__.py +0 -0
  44. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/mesh/__init__.py +0 -0
  45. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/mesh/data.py +0 -0
  46. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/stats/__init__.py +0 -0
  47. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/__init__.py +0 -0
  48. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/conftest.py +0 -0
  49. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_f.py +0 -0
  50. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_fdr.py +0 -0
  51. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_linear_model.py +0 -0
  52. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_mesh_edges.py +0 -0
  53. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_mesh_smooth.py +0 -0
  54. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_peak_clus.py +0 -0
  55. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_random_field_theory.py +0 -0
  56. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_resels.py +0 -0
  57. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_slm.py +0 -0
  58. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_stat_threshold.py +0 -0
  59. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/datagen_t_test.py +0 -0
  60. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_f_test.py +0 -0
  61. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_fdr.py +0 -0
  62. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_linear_model.py +0 -0
  63. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_mesh_smooth.py +0 -0
  64. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_peak_clus.py +0 -0
  65. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_random_field_theory.py +0 -0
  66. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_resels.py +0 -0
  67. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_stat_threshold.py +0 -0
  68. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/test_t_test.py +0 -0
  69. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tests/testutil.py +0 -0
  70. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat/tutorial/__init__.py +0 -0
  71. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat.egg-info/dependency_links.txt +0 -0
  72. {brainstat-0.3.6 → brainstat-0.5.2}/brainstat.egg-info/top_level.txt +0 -0
@@ -1,26 +1,56 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: brainstat
3
- Version: 0.3.6
3
+ Version: 0.5.2
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
7
- Author-email: reinder.vosdewael@gmail.com, sheymaba@gmail.com
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
10
  Project-URL: Bug Reports, https://github.com/MICA-LAB/BrainStat/issues
11
11
  Project-URL: Source, https://github.com/MICA-LAB/BrainStat/
12
- Platform: UNKNOWN
13
12
  Classifier: Intended Audience :: Science/Research
14
13
  Classifier: Intended Audience :: Developers
15
14
  Classifier: Topic :: Software Development :: Build Tools
16
15
  Classifier: Topic :: Scientific/Engineering
17
- Classifier: Programming Language :: Python :: 3.7
18
- Classifier: Programming Language :: Python :: 3.8
19
- Classifier: Programming Language :: Python :: 3.9
20
- Requires-Python: >=3.7.*
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Requires-Python: >=3.10
21
18
  Description-Content-Type: text/x-rst
22
- Provides-Extra: dev
23
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>=1.21
30
+ Requires-Dist: pandas>=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
24
54
 
25
55
  BrainStat : A toolbox for statistical analysis of neuroimaging data
26
56
  ===================================================================
@@ -60,11 +90,12 @@ Developers
60
90
  ----------
61
91
  .. |seyma_s| unicode:: U+015E .. CEDILLA S
62
92
 
63
- - Reinder Vos de Wael - MICA Lab, Montreal Neurological Institute
93
+ - Sara Lariviere - MICA Lab, Montreal Neurological Institute
64
94
  - |seyma_s|\ eyma Bayrak - Max Planck Institute for Human Cognitive and Brain Sciences
95
+ - Reinder Vos de Wael - MICA Lab, Montreal Neurological Institute
65
96
  - Oualid Benkarim - MICA Lab, Montreal Neurological Institute
66
- - Sara Lariviere - MICA Lab, Montreal Neurological Institute
67
97
  - Raul Cruces - MICA Lab, Montreal Neurological Institute
98
+ - Jessica Royer - MICA Lab, Montreal Neurological Institute
68
99
  - Peer Herholz - Montreal Neurological Institute
69
100
  - Seok-Jun Hong - Sungkyunkwan University
70
101
  - Sofie Valk - Max Planck Institute for Human Cognitive and Brain Sciences
@@ -84,5 +115,3 @@ Support
84
115
  If you have problems installing the software or questions about usage and
85
116
  documentation, or something else related to BrainStat, you can post to the
86
117
  `Issues <https://github.com/MICA-MNI/BrainStat/issues>`_ section of our repository.
87
-
88
-
@@ -36,11 +36,12 @@ Developers
36
36
  ----------
37
37
  .. |seyma_s| unicode:: U+015E .. CEDILLA S
38
38
 
39
- - Reinder Vos de Wael - MICA Lab, Montreal Neurological Institute
39
+ - Sara Lariviere - MICA Lab, Montreal Neurological Institute
40
40
  - |seyma_s|\ eyma Bayrak - Max Planck Institute for Human Cognitive and Brain Sciences
41
+ - Reinder Vos de Wael - MICA Lab, Montreal Neurological Institute
41
42
  - Oualid Benkarim - MICA Lab, Montreal Neurological Institute
42
- - Sara Lariviere - MICA Lab, Montreal Neurological Institute
43
43
  - Raul Cruces - MICA Lab, Montreal Neurological Institute
44
+ - Jessica Royer - MICA Lab, Montreal Neurological Institute
44
45
  - Peer Herholz - Montreal Neurological Institute
45
46
  - Seok-Jun Hong - Sungkyunkwan University
46
47
  - Sofie Valk - Max Planck Institute for Human Cognitive and Brain Sciences
@@ -1,2 +1,2 @@
1
1
  """Neuroimaging statistics toolbox."""
2
- __version__ = "0.3.6"
2
+ __version__ = "0.5.2"
@@ -56,8 +56,8 @@ def generate_data_fetcher_json() -> None:
56
56
  "glasser": {
57
57
  "fsaverage": {
58
58
  "url": (
59
- "https://box.bic.mni.mcgill.ca/s/y2NMHXr47WOCtpp/download",
60
- "https://box.bic.mni.mcgill.ca/s/Y0Fmd2tIF69Mqpt/download",
59
+ "https://box.bic.mni.mcgill.ca/s/j4nfMA4D7jSx3QZ/download",
60
+ "https://box.bic.mni.mcgill.ca/s/qZTplkH4A4exOnF/download",
61
61
  ),
62
62
  },
63
63
  "fsaverage5": {
@@ -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
- with urllib.request.urlopen(url) as response, open(output_file, "wb") as out_file:
173
- shutil.copyfileobj(response, out_file)
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__":
@@ -0,0 +1,301 @@
1
+ """Genetic decoding using abagen."""
2
+ import tempfile
3
+ from pathlib import Path
4
+ from typing import Optional, Sequence, Union
5
+ import os
6
+ import nibabel as nib
7
+ import numpy as np
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
54
+ from abagen import check_atlas, get_expression_data
55
+ from brainspace.mesh.mesh_io import read_surface, write_surface
56
+ from sklearn.model_selection import ParameterGrid
57
+ from nibabel.gifti import GiftiImage, GiftiDataArray
58
+
59
+ from brainstat._utils import data_directories, logger
60
+ from brainstat.datasets.base import (
61
+ _fetch_template_surface_files,
62
+ _valid_parcellations,
63
+ fetch_parcellation,
64
+ )
65
+
66
+
67
+ def surface_genetic_expression(
68
+ labels: Union[Sequence[str], np.ndarray],
69
+ surfaces: Optional[Union[str, Sequence[str]]] = None,
70
+ space: Optional[str] = None,
71
+ *,
72
+ atlas_info: Optional[str] = None,
73
+ ibf_threshold: float = 0.5,
74
+ probe_selection: str = "diff_stability",
75
+ donor_probes: str = "aggregate",
76
+ lr_mirror: Optional[bool] = None,
77
+ missing: Optional[str] = None,
78
+ tolerance: float = 2,
79
+ sample_norm: str = "srs",
80
+ gene_norm: str = "srs",
81
+ norm_matched: bool = True,
82
+ norm_structures: bool = False,
83
+ region_agg: str = "donors",
84
+ agg_metric: str = "mean",
85
+ corrected_mni: bool = True,
86
+ reannotated: bool = True,
87
+ return_counts: bool = False,
88
+ return_donors: bool = False,
89
+ return_report: bool = False,
90
+ donors: str = "all",
91
+ data_dir: Optional[str] = None,
92
+ verbose: float = 0,
93
+ n_proc: int = 1
94
+ ) -> pd.DataFrame:
95
+ """Computes genetic expression of surface parcels.
96
+
97
+ Parameters
98
+ ----------
99
+ labels : list-of-str or numpy.ndarray
100
+ List of paths to label files for the parcellation, or numpy array
101
+ containing the pre-loaded labels
102
+ surfaces : list-of-image, optional
103
+ List of paths to surface files or preloaded surfaces. If not specified
104
+ assumes that `labels` are on the `fsaverage5` surface. Default: None
105
+ space : {'fsaverage', 'fslr'}
106
+ What template space `surfaces` are aligned to. If not specified assumes
107
+ that `labels` are on the `fsaverage5` surface. Default: None
108
+
109
+ For details of the remaining parameters please consult the
110
+ abagen.get_expression_data() documentation. All its parameters bar "atlas"
111
+ are valid input parameters.
112
+
113
+ Returns
114
+ -------
115
+ pandas.DataFrame
116
+ Dataframe containing the expression of each gene within each region.
117
+
118
+ Examples
119
+ --------
120
+ >>> from brainstat.context.genetics import surface_genetic_expression
121
+ >>> from nilearn import datasets
122
+ >>> import numpy as np
123
+
124
+ >>> destrieux = datasets.fetch_atlas_surf_destrieux()
125
+ >>> labels = np.hstack((destrieux['map_left'], destrieux['map_right']))
126
+ >>> fsaverage = datasets.fetch_surf_fsaverage()
127
+ >>> surfaces = (fsaverage['pial_left'], fsaverage['pial_right'])
128
+ >>> expression = surface_genetic_expression(labels, surfaces,
129
+ ... space='fsaverage')
130
+ """
131
+
132
+ # Deal with the input parameters.
133
+ if isinstance(surfaces, str):
134
+ surfaces = [surfaces]
135
+ elif surfaces is None:
136
+ surfaces = []
137
+
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:
218
+ try:
219
+ Path(f).unlink()
220
+ except FileNotFoundError:
221
+ pass
222
+
223
+
224
+ def __create_precomputed(
225
+ data_dir: Optional[Union[str, Path]] = None,
226
+ output_dir: Optional[Union[str, Path]] = None,
227
+ ) -> None:
228
+ """Creates precomputed matrices of genetic expression for standard atlases. These are
229
+ used for the MATLAB implementation."""
230
+
231
+ if output_dir is None:
232
+ output_dir = data_dir = (
233
+ Path(data_dir) if data_dir else data_directories["BRAINSTAT_DATA_DIR"]
234
+ )
235
+
236
+ # Get all parcellations in a format compatible with ParameterGrid.
237
+ parcellations = _valid_parcellations()
238
+ for key in parcellations:
239
+ parcellations[key].update({"atlas": [key]})
240
+ if key == "schaefer":
241
+ parcellations[key].update({"seven_networks": (True, False)})
242
+ else:
243
+ parcellations[key].update({"seven_networks": (True,)})
244
+
245
+ # We only really need to compute this for one template surface.
246
+ # We'll use fsaverage5 for all.
247
+ parcellations[key].update({"surfaces": ["fsaverage5"]})
248
+
249
+ param_grid = ParameterGrid(list(parcellations.values()))
250
+
251
+ # Compute expression for all parcellations.
252
+ for params in param_grid:
253
+ if params["atlas"] == "schaefer":
254
+ network_tag = "7Networks" if params["seven_networks"] else "17Networks"
255
+ else:
256
+ network_tag = ""
257
+
258
+ filename_components = filter(
259
+ None,
260
+ (
261
+ "expression",
262
+ params["atlas"],
263
+ str(params["n_regions"]),
264
+ network_tag,
265
+ ),
266
+ )
267
+
268
+ filename = "_".join(filename_components) + ".csv.gz"
269
+ output_file = Path(output_dir) / filename
270
+ if output_file.exists():
271
+ print('Skipping "{}"'.format(output_file))
272
+ continue
273
+ else:
274
+ print('Computing "{}"'.format(output_file))
275
+
276
+ surface_files = _fetch_template_surface_files(
277
+ params["surfaces"], data_dir=data_dir # type: ignore
278
+ )
279
+ space = "fslr" if params["surfaces"] == "fslr32k" else "fsaverage"
280
+
281
+ labels = fetch_parcellation(
282
+ params["surfaces"],
283
+ params["atlas"],
284
+ params["n_regions"],
285
+ seven_networks=params["seven_networks"],
286
+ data_dir=data_dir,
287
+ )
288
+
289
+ surf_lh = tempfile.NamedTemporaryFile(suffix=".surf.gii")
290
+ surf_rh = tempfile.NamedTemporaryFile(suffix=".surf.gii")
291
+ for i, surf in enumerate((surf_lh, surf_rh)):
292
+ __freesurfer_to_surfgii(surface_files[i], surf.name)
293
+ surface_paths = [surf_lh.name, surf_rh.name]
294
+
295
+ expression = surface_genetic_expression(labels, surface_paths, space=space) # type: ignore
296
+ expression.to_csv(output_file) # type: ignore
297
+
298
+
299
+ def __freesurfer_to_surfgii(freesurfer_file: str, gifti_file: str) -> None:
300
+ surf = read_surface(freesurfer_file, itype="fs")
301
+ write_surface(surf, gifti_file, otype="gii")
@@ -156,7 +156,19 @@ def read_histology_profile(
156
156
  )
157
157
 
158
158
  with h5py.File(histology_file, "r") as h5_file:
159
- profiles = h5_file.get(template)[...]
159
+ if template == "fslr32k":
160
+ # Known issue #369: The fslr32k data file on the server actually contains
161
+ # fs_LR_64k resolution data (64984 vertices) instead of fs_LR_32k (32492 vertices).
162
+ # This is a data hosting issue. Users expecting 32k resolution data should be aware
163
+ # that they will receive 64k resolution data until this is fixed on the server.
164
+ logger.warning(
165
+ "Known issue: The fslr32k histology profile data currently contains "
166
+ "fs_LR_64k resolution (64984 vertices) instead of fs_LR_32k (32492 vertices). "
167
+ "See https://github.com/MICA-MNI/BrainStat/issues/369 for details."
168
+ )
169
+ profiles = h5_file.get("fs_LR_64k")[...]
170
+ else:
171
+ profiles = h5_file.get(template)[...]
160
172
  if civet_template:
161
173
  fsaverage_surface = fetch_template_surface("fsaverage")
162
174
  civet_surface = fetch_template_surface(civet_template)
@@ -191,7 +203,16 @@ def download_histology_profiles(
191
203
 
192
204
  data_dir = Path(data_dir) if data_dir else data_directories["BIGBRAIN_DATA_DIR"]
193
205
  data_dir.mkdir(parents=True, exist_ok=True)
194
- output_file = data_dir / ("histology_" + template + ".h5")
206
+ if template == "fslr32k":
207
+ output_file = data_dir / "histology_fslr32k.h5"
208
+ # Known issue #369: warn users about data resolution mismatch
209
+ logger.warning(
210
+ "Note: The fslr32k histology profile data currently contains "
211
+ "fs_LR_64k resolution (64984 vertices) instead of fs_LR_32k. "
212
+ "See https://github.com/MICA-MNI/BrainStat/issues/369 for details."
213
+ )
214
+ else:
215
+ output_file = data_dir / ("histology_" + template + ".h5")
195
216
 
196
217
  url = read_data_fetcher_json()["bigbrain_profiles"][template]["url"]
197
218
 
@@ -11,7 +11,9 @@ import numpy as np
11
11
  import pandas as pd
12
12
  import templateflow.api as tflow
13
13
  from brainspace.vtk_interface.wrappers.data_object import BSPolyData
14
- from scipy.stats.stats import pearsonr
14
+ from scipy.stats.stats import pearsonr, spearmanr
15
+ from math import pi
16
+ import matplotlib.pyplot as plt
15
17
 
16
18
  from brainstat._utils import (
17
19
  data_directories,
@@ -25,6 +27,7 @@ from brainstat.mesh.interpolate import _surf2vol, multi_surface_to_volume
25
27
  def meta_analytic_decoder(
26
28
  template: str,
27
29
  stat_labels: np.ndarray,
30
+ corrtype: Optional[str] = "pearson",
28
31
  data_dir: Optional[Union[str, Path]] = None,
29
32
  ):
30
33
  """Meta-analytic decoding of surface maps using NeuroSynth or NeuroQuery.
@@ -36,6 +39,8 @@ def meta_analytic_decoder(
36
39
  stat_labels : str, numpy.ndarray, sequence of str or numpy.ndarray
37
40
  Path to a label file for the surfaces, numpy array containing the
38
41
  labels, or a list containing multiple of the aforementioned.
42
+ corrtype : str, optional
43
+ Correlation type {'pearson', 'spearman'}. Default is 'pearson'.
39
44
  data_dir : str, optional
40
45
  The directory of the dataset. If none exists, a new dataset will
41
46
  be downloaded and saved to this path. If None, the directory defaults to
@@ -74,9 +79,14 @@ def meta_analytic_decoder(
74
79
  | np.isnan(stat_vector)
75
80
  | np.isinf(stat_vector)
76
81
  )
77
- correlations[i], _ = pearsonr(stat_vector[keep], feature_data[keep])
78
-
79
- df = pd.DataFrame(correlations, index=feature_names, columns=["Pearson's r"])
82
+ if corrtype == "pearson":
83
+ correlations[i], _ = pearsonr(stat_vector[keep], feature_data[keep])
84
+ colname = "Pearson's r"
85
+ elif corrtype == "spearman":
86
+ correlations[i], _ = spearmanr(stat_vector[keep], feature_data[keep])
87
+ colname = "Spearman's r"
88
+
89
+ df = pd.DataFrame(correlations, index=feature_names, columns=[colname])
80
90
  return df.sort_values(by="Pearson's r", ascending=False)
81
91
 
82
92
 
@@ -224,3 +234,86 @@ def _fetch_precomputed_neurosynth(data_dir: Path) -> Generator[Path, None, None]
224
234
  (Path(name)).unlink()
225
235
 
226
236
  return data_dir.glob("Neurosynth_TFIDF__*z_desc-consistency.nii.gz")
237
+
238
+
239
+ def radar_plot(data=None, title="", axis_range=None, label=None, color=(0, 0, 0)):
240
+ """Visualize data in radar plot (author: @saratheriver)
241
+ Parameters
242
+ ----------
243
+ data : ndarray, shape = (n_val,)
244
+ Data.
245
+ title : string, optional
246
+ Title of spider plot. Default is empty.
247
+ axis_range : tuple, optional
248
+ Range of spider plot axes. Default is (min, max).
249
+ label : list, optional
250
+ List of axis labels. Length = same as data.shape[0]. Default is empty.
251
+ color : tuple, optional
252
+ Color of line. Default is (0, 0, 0).
253
+
254
+ Returns
255
+ -------
256
+ class_mean : ndarray, shape = (data.shape[0],)
257
+ Values for each branch.
258
+ figure
259
+ Spider plot.
260
+ """
261
+ # Data check
262
+ if data is None:
263
+ print("Need data to plot")
264
+
265
+ # Create dataframe
266
+ class_mean = pd.DataFrame(data=data.T, columns=label)
267
+
268
+ # Number of variable
269
+ categories = list(class_mean)
270
+ N = len(categories)
271
+
272
+ # We are going to plot the first line of the data frame.
273
+ # But we need to repeat the first value to close the circular graph:
274
+ values = class_mean.loc[0].values.flatten().tolist()
275
+ values += values[:1]
276
+
277
+ # What will be the angle of each axis in the plot? (we divide the plot / number of variable)
278
+ angles = [n / float(N) * 2 * pi for n in range(N)]
279
+ angles += angles[:1]
280
+
281
+ # Initialise the spider plot
282
+ plt.figure(figsize=(8, 4))
283
+ ax = plt.subplot(111, polar=True)
284
+ ax.spines["polar"].set_visible(False)
285
+
286
+ # Draw one axe per variable + add labels labels yet
287
+ if label is None:
288
+ label = list(class_mean)
289
+
290
+ plt.xticks(angles[:-1], label, color="black", size=12)
291
+ ax.xaxis.get_majorticklabels()[0].set_horizontalalignment("left")
292
+ ax.xaxis.get_majorticklabels()[2].set_verticalalignment("bottom")
293
+ ax.xaxis.get_majorticklabels()[3].set_verticalalignment("top")
294
+
295
+ # Draw ylabels
296
+ ax.set_rlabel_position(0)
297
+ if axis_range is None:
298
+ axis_range = (np.min(values), np.max(values))
299
+
300
+ inc = (axis_range[1] - axis_range[0]) / 4
301
+ newinc = [
302
+ axis_range[0] + inc,
303
+ axis_range[0] + (inc * 2),
304
+ axis_range[0] + (inc * 3),
305
+ axis_range[0],
306
+ ]
307
+ plt.yticks(
308
+ newinc, [str("{:.2f}".format(elem)) for elem in newinc], color="grey", size=10
309
+ )
310
+ plt.ylim(axis_range)
311
+
312
+ # add title
313
+ if title:
314
+ plt.title(title)
315
+
316
+ # Plot data
317
+ ax.plot(angles, values, linewidth=3, linestyle="solid", color=color)
318
+
319
+ return class_mean
@@ -53,8 +53,8 @@
53
53
  "glasser": {
54
54
  "fsaverage": {
55
55
  "url": [
56
- "https://box.bic.mni.mcgill.ca/s/y2NMHXr47WOCtpp/download",
57
- "https://box.bic.mni.mcgill.ca/s/Y0Fmd2tIF69Mqpt/download"
56
+ "https://box.bic.mni.mcgill.ca/s/j4nfMA4D7jSx3QZ/download",
57
+ "https://box.bic.mni.mcgill.ca/s/qZTplkH4A4exOnF/download"
58
58
  ]
59
59
  },
60
60
  "fsaverage5": {