neuromodes 0.1.0__py3-none-any.whl
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.
- neuromodes/__init__.py +11 -0
- neuromodes/basis.py +413 -0
- neuromodes/data/__init__.py +0 -0
- neuromodes/data/included_data.csv +18 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-L_fcgradient1.func.gii +6 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-L_medmask.label.gii +6 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-L_midthickness.surf.gii +196 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-L_myelinmap.func.gii +17 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-L_ndi.func.gii +63 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-L_odi.func.gii +63 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-L_sphere.surf.gii +109 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-L_thickness.func.gii +17 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-R_medmask.label.gii +6 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-R_midthickness.surf.gii +196 -0
- neuromodes/data/sp-human_tpl-fsLR_den-32k_hemi-R_sphere.surf.gii +109 -0
- neuromodes/data/sp-human_tpl-fsLR_den-4k_hemi-L_medmask.label.gii +6 -0
- neuromodes/data/sp-human_tpl-fsLR_den-4k_hemi-L_midthickness.surf.gii +110 -0
- neuromodes/data/sp-human_tpl-fsLR_den-4k_hemi-L_myelinmap.func.gii +6 -0
- neuromodes/data/sp-human_tpl-fsLR_den-4k_hemi-L_sphere.surf.gii +107 -0
- neuromodes/data/sp-human_tpl-fsLR_den-4k_hemi-R_medmask.label.gii +6 -0
- neuromodes/data/sp-human_tpl-fsLR_den-4k_hemi-R_midthickness.surf.gii +110 -0
- neuromodes/data/sp-human_tpl-fsLR_den-4k_hemi-R_myelinmap.func.gii +105 -0
- neuromodes/data/sp-human_tpl-fsLR_den-4k_hemi-R_sphere.surf.gii +92 -0
- neuromodes/data/sp-macaque_tpl-fsLR_den-32k_hemi-L_medmask.label.gii +6 -0
- neuromodes/data/sp-macaque_tpl-fsLR_den-32k_hemi-L_midthickness.surf.gii +194 -0
- neuromodes/data/sp-macaque_tpl-fsLR_den-32k_hemi-R_medmask.label.gii +6 -0
- neuromodes/data/sp-macaque_tpl-fsLR_den-32k_hemi-R_midthickness.surf.gii +194 -0
- neuromodes/data/sp-marmoset_tpl-fsLR_den-32k_hemi-L_medmask.label.gii +6 -0
- neuromodes/data/sp-marmoset_tpl-fsLR_den-32k_hemi-L_midthickness.surf.gii +287 -0
- neuromodes/data/sp-marmoset_tpl-fsLR_den-32k_hemi-R_medmask.label.gii +6 -0
- neuromodes/data/sp-marmoset_tpl-fsLR_den-32k_hemi-R_midthickness.surf.gii +287 -0
- neuromodes/eigen.py +808 -0
- neuromodes/io.py +242 -0
- neuromodes/mesh.py +153 -0
- neuromodes/network.py +93 -0
- neuromodes/nulls.py +543 -0
- neuromodes/stats.py +729 -0
- neuromodes/waves.py +784 -0
- neuromodes-0.1.0.dist-info/METADATA +123 -0
- neuromodes-0.1.0.dist-info/RECORD +43 -0
- neuromodes-0.1.0.dist-info/WHEEL +5 -0
- neuromodes-0.1.0.dist-info/licenses/LICENCE-CC-BY-NC-SA-4.0.md +437 -0
- neuromodes-0.1.0.dist-info/top_level.txt +1 -0
neuromodes/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
neuromodes
|
|
3
|
+
==========
|
|
4
|
+
Eigenmode-based brain mapping and modelling toolbox developed by the Neural Systems and Behaviour
|
|
5
|
+
Lab. Documentation is available at https://neuromodes.readthedocs.io/, and source code
|
|
6
|
+
at https://github.com/NSBLab/neuromodes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from neuromodes.eigen import EigenSolver
|
|
10
|
+
|
|
11
|
+
__all__ = ["EigenSolver"]
|
neuromodes/basis.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for expressing brain maps as linear combinations of orthogonal basis vectors, such as
|
|
3
|
+
geometric eigenmodes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from typing import TYPE_CHECKING, overload
|
|
8
|
+
from warnings import warn
|
|
9
|
+
import numpy as np
|
|
10
|
+
from neuromodes.eigen import EigenData
|
|
11
|
+
from neuromodes.stats import lstsqw, cdistw, _process_vertex_areas
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from typing import Any, TypeAlias, Literal
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from numpy.typing import NDArray
|
|
17
|
+
from scipy.sparse import csc_matrix
|
|
18
|
+
from scipy.spatial.distance import _MetricCallback, _MetricKind
|
|
19
|
+
from neuromodes.eigen import _CheckKind
|
|
20
|
+
|
|
21
|
+
_IntSequenceKind: TypeAlias = Sequence[int] | NDArray[np.integer]
|
|
22
|
+
_SeqSequenceKind: TypeAlias = Sequence[_IntSequenceKind] | NDArray[Any]
|
|
23
|
+
_DecompositionKind: TypeAlias = Literal['project', 'regress']
|
|
24
|
+
|
|
25
|
+
@overload
|
|
26
|
+
def decompose(
|
|
27
|
+
data: NDArray[np.floating],
|
|
28
|
+
emodes: NDArray[np.floating],
|
|
29
|
+
method: _DecompositionKind = ...,
|
|
30
|
+
*,
|
|
31
|
+
mass: csc_matrix | None = ...,
|
|
32
|
+
mode_counts: int | None = ...,
|
|
33
|
+
mode_ids: None = ...,
|
|
34
|
+
checks: _CheckKind | None = ...
|
|
35
|
+
) -> NDArray[np.floating]: ...
|
|
36
|
+
|
|
37
|
+
# 2. mode_counts is Sequence -> List of Arrays
|
|
38
|
+
@overload
|
|
39
|
+
def decompose(
|
|
40
|
+
data: NDArray[np.floating],
|
|
41
|
+
emodes: NDArray[np.floating],
|
|
42
|
+
method: _DecompositionKind = ...,
|
|
43
|
+
*,
|
|
44
|
+
mass: csc_matrix | None = ...,
|
|
45
|
+
mode_counts: _IntSequenceKind,
|
|
46
|
+
mode_ids: None = ...,
|
|
47
|
+
checks: _CheckKind | None = ...
|
|
48
|
+
) -> list[NDArray[np.floating]]: ...
|
|
49
|
+
|
|
50
|
+
# 3. mode_ids is Sequence -> List of Arrays
|
|
51
|
+
@overload
|
|
52
|
+
def decompose(
|
|
53
|
+
data: NDArray[np.floating],
|
|
54
|
+
emodes: NDArray[np.floating],
|
|
55
|
+
method: _DecompositionKind = ...,
|
|
56
|
+
*,
|
|
57
|
+
mass: csc_matrix | None = ...,
|
|
58
|
+
mode_counts: None = ...,
|
|
59
|
+
mode_ids: _SeqSequenceKind,
|
|
60
|
+
checks: _CheckKind | None = ...
|
|
61
|
+
) -> list[NDArray[np.floating]]: ...
|
|
62
|
+
|
|
63
|
+
def decompose(
|
|
64
|
+
data: NDArray[np.floating],
|
|
65
|
+
emodes: NDArray[np.floating],
|
|
66
|
+
method: _DecompositionKind = 'project',
|
|
67
|
+
*,
|
|
68
|
+
mass: csc_matrix | None = None,
|
|
69
|
+
mode_counts: _IntSequenceKind | int | None = None,
|
|
70
|
+
mode_ids: _SeqSequenceKind | None = None,
|
|
71
|
+
checks: _CheckKind | None = None,
|
|
72
|
+
) -> NDArray[np.floating] | list[NDArray[np.floating]]:
|
|
73
|
+
"""
|
|
74
|
+
Calculate the decomposition of the given data onto a basis set.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
data : array-like
|
|
79
|
+
The input data array of shape ``(n_verts, ...)``, where ``n_verts`` is the number of
|
|
80
|
+
vertices and additional axes represent maps to be decomposed independently.
|
|
81
|
+
emodes : array-like
|
|
82
|
+
The basis vectors array of shape ``(n_verts, n_modes)``, where ``n_modes`` is the number of
|
|
83
|
+
vectors.
|
|
84
|
+
method : str, optional
|
|
85
|
+
The method used for the decomposition, either ``'project'`` or ``'regress'``. Note that
|
|
86
|
+
``'project'`` is faster and more accurate, while ``'regress'`` should only be used when
|
|
87
|
+
``data`` contains missing values (NaNs; see Notes). The methods are otherwise equivalent.
|
|
88
|
+
Default is ``'project'``.
|
|
89
|
+
mass : array-like, optional
|
|
90
|
+
The mass matrix of shape ``(n_verts, n_verts)``. If vectors are orthonormal in Euclidean
|
|
91
|
+
space, leave as ``None``. See :func:`eigen.is_orthonormal_basis` for more details. Default
|
|
92
|
+
is ``None``.
|
|
93
|
+
mode_counts : array-like, optional
|
|
94
|
+
The sequence of vectors to be used for decomposition, of shape ``(n_recons,)``. For
|
|
95
|
+
example, ``mode_counts=np.array([10,20,30])`` will run three analyses: with the first 10
|
|
96
|
+
vectors, with the first 20 vectors, and with the first 30 vectors. Default is ``None``,
|
|
97
|
+
which uses ``n_modes``.
|
|
98
|
+
mode_ids : array-like, optional
|
|
99
|
+
The indices of the modes to be used for reconstruction, overriding ``mode_counts``. If
|
|
100
|
+
``None``, all modes are used. Default is ``None``.
|
|
101
|
+
checks : str or bool, optional
|
|
102
|
+
Whether to validate arguments prior to analysis. Default is ``None``.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
numpy.ndarray or list
|
|
107
|
+
The coefficients array of shape ``(n_modes, ...)`` or ``list of (n_modes, ...)``
|
|
108
|
+
arrays, obtained from the decomposition.
|
|
109
|
+
|
|
110
|
+
Raises
|
|
111
|
+
------
|
|
112
|
+
ValueError
|
|
113
|
+
If ``method`` is not ``'project'`` or ``'regress'``.
|
|
114
|
+
ValueError
|
|
115
|
+
If ``method`` is ``'project'`` and ``data`` contains NaNs.
|
|
116
|
+
|
|
117
|
+
Notes
|
|
118
|
+
-----
|
|
119
|
+
If ``data`` contains NaNs, these will be disregarded prior to decomposition (via
|
|
120
|
+
``method='regress'``) by removing corresponding entries from ``data``, ``emodes``, and ``mass``.
|
|
121
|
+
Note that extreme values may accumulate in the reconstructed data at these vertices, where data
|
|
122
|
+
provides no constraint. Consider instead interpolating missing data prior to decomposition.
|
|
123
|
+
"""
|
|
124
|
+
# Format / validate inputs
|
|
125
|
+
if method not in ['project', 'regress']:
|
|
126
|
+
raise ValueError(f"Invalid method '{method}'; must be 'project' or 'regress'.")
|
|
127
|
+
|
|
128
|
+
if checks is None:
|
|
129
|
+
checks = True if method == 'project' else 'maps'
|
|
130
|
+
if checks is not False:
|
|
131
|
+
ved = EigenData(emodes=emodes, mass=mass, data=data, checks=checks)
|
|
132
|
+
emodes, mass, data = ved.emodes, ved.mass, ved.data
|
|
133
|
+
|
|
134
|
+
mass = _process_vertex_areas(mass, data.shape[0])
|
|
135
|
+
|
|
136
|
+
mode_ids, squeeze_output = _process_mode_ids(mode_counts, mode_ids, emodes.shape[1])
|
|
137
|
+
n_modes = [len(x) for x in mode_ids]
|
|
138
|
+
|
|
139
|
+
# Manipulate input/output shapes
|
|
140
|
+
output_shapes = [(i,) + data.shape[1:] for i in n_modes]
|
|
141
|
+
coeffs = [np.empty(shape, dtype=data.dtype) for shape in output_shapes]
|
|
142
|
+
data_2d = data.reshape(data.shape[0], -1) # guaranteed 2d
|
|
143
|
+
|
|
144
|
+
# Handle NaNs with 'regress' by masking out afflicted vertices (separately for each NaN pattern)
|
|
145
|
+
data_isnan = np.isnan(data_2d)
|
|
146
|
+
if np.any(data_isnan):
|
|
147
|
+
if method == 'project':
|
|
148
|
+
raise ValueError("data contains NaNs; use method='regress' to mask out afflicted "
|
|
149
|
+
"vertices prior to decomposition, or consider interpolating missing "
|
|
150
|
+
"data.")
|
|
151
|
+
# method == 'regress'
|
|
152
|
+
if checks is True or checks == 'maps':
|
|
153
|
+
warn("NaN values detected in data; these will be disregarded during decomposition "
|
|
154
|
+
"by masking corresponding vertices from data, emodes, and mass. This may lead "
|
|
155
|
+
"to extreme values in affected areas of the reconstructed data. Consider "
|
|
156
|
+
"instead interpolating missing data prior to decomposition.")
|
|
157
|
+
masks, mask_indices = np.unique(~data_isnan, axis=1, return_inverse=True)
|
|
158
|
+
elif method == 'regress':
|
|
159
|
+
# Keep all vertices
|
|
160
|
+
masks = np.ones((data_2d.shape[0], 1), dtype=bool)
|
|
161
|
+
mask_indices = np.zeros(data_2d.shape[1], dtype=int)
|
|
162
|
+
|
|
163
|
+
# TODO : consider adding a method that does fitting/param estimation (like lstsqw) but using
|
|
164
|
+
# full (consistent) mass matrix (not just vertex areas). solvew?
|
|
165
|
+
if method == 'project':
|
|
166
|
+
# Find the unique mode IDs requested, and the inverse mapping back to mode_ids
|
|
167
|
+
unique_mids, inv = np.unique(np.concatenate(mode_ids), return_inverse=True)
|
|
168
|
+
inv = np.split(inv, np.cumsum([len(m) for m in mode_ids[:-1]])) # back in the same list pattern as mode_ids
|
|
169
|
+
|
|
170
|
+
# For each nan/inf pattern, get the coeffs for all the unique modes
|
|
171
|
+
coeffs_all = emodes[:, unique_mids].T @ (mass @ data_2d)
|
|
172
|
+
|
|
173
|
+
# Map the unique results back to the specific mode_ids requested
|
|
174
|
+
for j, idxs in enumerate(inv):
|
|
175
|
+
coeffs[j] = coeffs_all[idxs, :].reshape(output_shapes[j])
|
|
176
|
+
|
|
177
|
+
elif method == 'regress':
|
|
178
|
+
# Have to loop over each set of mode indices
|
|
179
|
+
for j in range(len(mode_ids)):
|
|
180
|
+
coeffs_current = np.empty((n_modes[j], data_2d.shape[1]), dtype=data.dtype)
|
|
181
|
+
# as well as each NaN pattern
|
|
182
|
+
for i, mask in enumerate(masks.T):
|
|
183
|
+
# Get indices of maps with this NaN/Inf pattern
|
|
184
|
+
# Remove verts with NaNs/Inf in this group from data and emodes
|
|
185
|
+
# Calculate coefficients for subset of data
|
|
186
|
+
map_indices = np.where(mask_indices == i)[0]
|
|
187
|
+
coeffs_current[:, map_indices] = lstsqw(
|
|
188
|
+
emodes[mask][:, mode_ids[j]],
|
|
189
|
+
data_2d[mask][:, map_indices],
|
|
190
|
+
mass=mass[mask][:, mask]
|
|
191
|
+
)[0]
|
|
192
|
+
coeffs[j] = coeffs_current.reshape(output_shapes[j])
|
|
193
|
+
|
|
194
|
+
return coeffs[0] if squeeze_output else coeffs # convert back to array if mode_counts was None/scalar
|
|
195
|
+
|
|
196
|
+
def reconstruct(
|
|
197
|
+
emodes: NDArray[np.floating],
|
|
198
|
+
data: NDArray[np.floating] | None = None,
|
|
199
|
+
coeffs: list[NDArray[np.floating]] | NDArray[np.floating] | None = None,
|
|
200
|
+
method: _DecompositionKind = 'project',
|
|
201
|
+
mass: csc_matrix | None = None,
|
|
202
|
+
mode_counts: _IntSequenceKind | int | None = None,
|
|
203
|
+
mode_ids: _SeqSequenceKind | None = None,
|
|
204
|
+
checks: _CheckKind | None = None
|
|
205
|
+
) -> NDArray[np.floating]:
|
|
206
|
+
"""
|
|
207
|
+
Calculate the reconstruction of the given independent data using the provided orthonormal
|
|
208
|
+
vectors (e.g., geometric eigenmodes).
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
emodes : array-like
|
|
213
|
+
The basis vectors array of shape ``(n_verts, n_modes)``, where ``n_modes`` is the number of
|
|
214
|
+
vectors.
|
|
215
|
+
data : array-like
|
|
216
|
+
The input data array of shape ``(n_verts, ...)``, where ``n_verts`` is the number of
|
|
217
|
+
vertices and additional axes represent maps to be decomposed independently. If ``None``,
|
|
218
|
+
``coeffs`` must be provided. Default is ``None``.
|
|
219
|
+
coeffs : array-like, optional
|
|
220
|
+
The modal coefficients array of shape ``(n_modes, ...)`` or ``list of (n_modes, ...)``
|
|
221
|
+
arrays, obtained from the decomposition. If ``None``, ``data`` must be provided. Default is
|
|
222
|
+
``None``.
|
|
223
|
+
method : str, optional
|
|
224
|
+
The method used for the decomposition, either ``'project'`` or ``'regress'``. Note that
|
|
225
|
+
``'project'`` is faster and more accurate, while ``'regress'`` should only be used when
|
|
226
|
+
``data`` contains missing values (NaNs; see Notes). The methods are otherwise equivalent.
|
|
227
|
+
Default is ``'project'``.
|
|
228
|
+
mass : array-like, optional
|
|
229
|
+
The mass matrix of shape ``(n_verts, n_verts)``. If vectors are orthonormal in Euclidean
|
|
230
|
+
space, leave as ``None``. See :func:`eigen.is_orthonormal_basis` for more details. Default
|
|
231
|
+
is ``None``.
|
|
232
|
+
mode_counts : array-like, optional
|
|
233
|
+
The sequence of vectors to be used for reconstruction, of shape ``(n_recons,)``. For
|
|
234
|
+
example, ``mode_counts=np.array([10,20,30])`` will run three analyses: with the first 10
|
|
235
|
+
vectors, with the first 20 vectors, and with the first 30 vectors. Default is ``None``,
|
|
236
|
+
which uses ``n_modes``.
|
|
237
|
+
mode_ids : array-like, optional
|
|
238
|
+
The indices of the modes to be used for reconstruction, overriding ``mode_counts``. If
|
|
239
|
+
``None``, all modes are used. Default is ``None``.
|
|
240
|
+
Default is ``None``.
|
|
241
|
+
checks : str or bool, optional
|
|
242
|
+
Whether to validate arguments prior to analysis. Default is ``True``.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
recon : numpy.ndarray
|
|
247
|
+
The reconstructed data array of shape ``(n_verts, ..., n_recons)``, where ``n_recons`` is
|
|
248
|
+
the number of different reconstructions ordered in ``mode_counts``. Each slice is the
|
|
249
|
+
independent reconstruction of each map. Note that if ``mode_counts`` includes any constant
|
|
250
|
+
vector (e.g., the first geometric eigenmode), the reconstructions will be constant for that
|
|
251
|
+
value of ``mode_counts`` (this may also result in warnings/nans for ``recon_error``).
|
|
252
|
+
|
|
253
|
+
Raises
|
|
254
|
+
------
|
|
255
|
+
ValueError
|
|
256
|
+
If neither or both of ``data`` and ``coeffs`` are provided.
|
|
257
|
+
|
|
258
|
+
Notes
|
|
259
|
+
-----
|
|
260
|
+
If ``data`` contains NaNs, these will be disregarded prior to decomposition (via
|
|
261
|
+
``method='regress'``) by removing corresponding entries from ``data``, ``emodes``, and ``mass``.
|
|
262
|
+
Note that extreme values may accumulate in the reconstructed data at these vertices, where data
|
|
263
|
+
provides no constraint. Consider instead interpolating missing data prior to decomposition.
|
|
264
|
+
"""
|
|
265
|
+
# Format / validate inputs
|
|
266
|
+
if checks is None:
|
|
267
|
+
if method == 'regress':
|
|
268
|
+
checks = 'maps'
|
|
269
|
+
else:
|
|
270
|
+
checks = True
|
|
271
|
+
if checks is not False:
|
|
272
|
+
ved = EigenData(emodes=emodes, mass=mass, data=data, checks=checks)
|
|
273
|
+
emodes, mass, data = ved.emodes, ved.mass, ved.data
|
|
274
|
+
|
|
275
|
+
# This should stay here as it needs to be run in both coefficients and data modes
|
|
276
|
+
mode_ids, squeeze_output = _process_mode_ids(mode_counts, mode_ids, emodes.shape[1])
|
|
277
|
+
|
|
278
|
+
# Validate that exactly one of coefficients/data is provided & decompose if only data is provided
|
|
279
|
+
if coeffs is not None and data is not None:
|
|
280
|
+
raise ValueError("Exactly one of 'coefficients' or 'data' must be provided.")
|
|
281
|
+
elif coeffs is not None:
|
|
282
|
+
if isinstance(coeffs, np.ndarray): # equivalent to `if squeeze_output`, but keeps pyright happy
|
|
283
|
+
coeffs = [coeffs]
|
|
284
|
+
elif data is not None: # coefficients will never be squeezed in this case (as mode_ids is passed)
|
|
285
|
+
coeffs = decompose(data, emodes, method=method, mass=mass, mode_ids=mode_ids, checks=False)
|
|
286
|
+
else: # neither provided (this order keeps pyright happy)
|
|
287
|
+
raise ValueError("Exactly one of 'coefficients' or 'data' must be provided.")
|
|
288
|
+
n_recons = len(coeffs)
|
|
289
|
+
|
|
290
|
+
# Main computation
|
|
291
|
+
recon_3d_shape = (emodes.shape[0], int(np.prod(coeffs[0].shape[1:])), n_recons)
|
|
292
|
+
recon_3d = np.empty(recon_3d_shape, dtype=coeffs[0].dtype)
|
|
293
|
+
for j, mids in enumerate(mode_ids):
|
|
294
|
+
recon_3d[:, :, j] = emodes[:, mids] @ coeffs[j].reshape(len(mids), -1) # convert to col vec if 1D
|
|
295
|
+
|
|
296
|
+
# Reshape outputs
|
|
297
|
+
if squeeze_output:
|
|
298
|
+
recon_nd_shape = (emodes.shape[0],) + coeffs[0].shape[1:]
|
|
299
|
+
else:
|
|
300
|
+
recon_nd_shape = (emodes.shape[0],) + coeffs[0].shape[1:] + (n_recons,)
|
|
301
|
+
recon_nd = recon_3d.reshape(recon_nd_shape)
|
|
302
|
+
|
|
303
|
+
return recon_nd
|
|
304
|
+
|
|
305
|
+
def recon_error(
|
|
306
|
+
data: NDArray[np.floating],
|
|
307
|
+
recon: NDArray[np.floating],
|
|
308
|
+
mass: csc_matrix | None = None,
|
|
309
|
+
metric: _MetricCallback | _MetricKind = 'correlation',
|
|
310
|
+
checks: _CheckKind = 'maps',
|
|
311
|
+
**cdist_kwargs
|
|
312
|
+
) -> NDArray[np.floating]:
|
|
313
|
+
"""
|
|
314
|
+
Calculate the reconstruction error between the given data and each reconstruction, using the
|
|
315
|
+
specified metric.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
data : array-like
|
|
320
|
+
The input data array of shape ``(n_verts, ...)``, where ``n_verts`` is the number of
|
|
321
|
+
vertices and additional axes represent maps that have been reconstructed.
|
|
322
|
+
recon : array-like
|
|
323
|
+
The reconstructed data array of shape ``(n_verts, ..., n_recons)``, where ``n_recons`` is
|
|
324
|
+
the number of different reconstructions ordered in ``mode_counts``. Each slice contains the
|
|
325
|
+
reconstruction(s) of the corresponding map in ``data``.
|
|
326
|
+
mass : array-like, optional
|
|
327
|
+
The mass matrix of shape ``(n_verts, n_verts)``. If vectors are orthonormal in Euclidean
|
|
328
|
+
space, leave as ``None``. See :func:`eigen.is_orthonormal_basis` for more details. Default
|
|
329
|
+
is ``None``.
|
|
330
|
+
metric : str or callable, optional
|
|
331
|
+
The distance metric to use for calculating reconstruction error. Can be any metric accepted
|
|
332
|
+
by ``scipy.spatial.distance.cdist``, or a custom metric function. Default is
|
|
333
|
+
``'correlation'``.
|
|
334
|
+
checks : str or bool, optional
|
|
335
|
+
Whether to validate arguments prior to analysis. Default is ``'maps'``.
|
|
336
|
+
**cdist_kwargs
|
|
337
|
+
Additional keyword arguments to be passed to ``scipy.spatial.distance.cdist`` when
|
|
338
|
+
calculating reconstruction error.
|
|
339
|
+
|
|
340
|
+
Returns
|
|
341
|
+
-------
|
|
342
|
+
recon_error : numpy.ndarray
|
|
343
|
+
The reconstruction error array of shape ``(..., n_recons)``, where ``n_recons`` is the
|
|
344
|
+
number of different reconstructions ordered in ``mode_counts``. Each slice contains the
|
|
345
|
+
error(s) of the corresponding map in ``data``.
|
|
346
|
+
|
|
347
|
+
Raises
|
|
348
|
+
------
|
|
349
|
+
ValueError
|
|
350
|
+
If ``data`` and ``recon`` have incompatible shapes.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
# Format / validate checks
|
|
354
|
+
if checks is not False:
|
|
355
|
+
ved = EigenData(mass=mass, data=(data, recon), checks=checks)
|
|
356
|
+
mass = ved.mass
|
|
357
|
+
data, recon = ved.data
|
|
358
|
+
|
|
359
|
+
# Get and check data/recon shapes
|
|
360
|
+
data_shape = data.shape
|
|
361
|
+
recon_shape = recon.shape
|
|
362
|
+
squeeze_output = len(data_shape) == len(recon_shape)
|
|
363
|
+
if squeeze_output:
|
|
364
|
+
if data_shape != recon_shape:
|
|
365
|
+
raise ValueError(f"data and recon must have the same shape; got {data_shape} and "
|
|
366
|
+
f"{recon_shape}.")
|
|
367
|
+
n_recons = 1
|
|
368
|
+
else:
|
|
369
|
+
if data_shape != recon_shape[:-1]:
|
|
370
|
+
raise ValueError("data and recon must have the same shape except for the last "
|
|
371
|
+
f"dimension; got {data_shape} and {recon_shape}.")
|
|
372
|
+
n_recons = recon_shape[-1]
|
|
373
|
+
|
|
374
|
+
# Main computation
|
|
375
|
+
data_2d = data.reshape(data.shape[0], -1)
|
|
376
|
+
recon_3d = recon.reshape(recon.shape[0], -1, n_recons)
|
|
377
|
+
|
|
378
|
+
error_2d_shape = (data_2d.shape[1],) + (n_recons,)
|
|
379
|
+
recon_error_2d = np.empty(error_2d_shape, dtype=data.dtype)
|
|
380
|
+
for i in range(data_2d.shape[1]):
|
|
381
|
+
recon_error_2d[i, :] = cdistw(data_2d[:, [i]], recon_3d[:, i, :],
|
|
382
|
+
mass=mass, metric=metric, **cdist_kwargs)
|
|
383
|
+
|
|
384
|
+
recon_error = recon_error_2d.reshape(recon.shape[1:])
|
|
385
|
+
return recon_error
|
|
386
|
+
|
|
387
|
+
def _process_mode_ids(
|
|
388
|
+
mode_counts: _IntSequenceKind | int | None,
|
|
389
|
+
mode_ids: _SeqSequenceKind | None,
|
|
390
|
+
n_modes: int
|
|
391
|
+
) -> tuple[_SeqSequenceKind, bool]:
|
|
392
|
+
"""
|
|
393
|
+
Process mode_counts and mode_ids into a consistent format for use in decompose/reconstruct.
|
|
394
|
+
"""
|
|
395
|
+
# mode_counts is just shorthand for mode_ids
|
|
396
|
+
# If mode_counts is provided, reformat into mode_ids
|
|
397
|
+
if mode_counts is not None and mode_ids is not None:
|
|
398
|
+
raise UserWarning("Both mode_counts and mode_ids provided; mode_counts will be ignored.")
|
|
399
|
+
|
|
400
|
+
if isinstance(mode_ids, (list, tuple, np.ndarray)):
|
|
401
|
+
output = mode_ids
|
|
402
|
+
elif mode_ids is not None:
|
|
403
|
+
raise ValueError("mode_ids must be a list or tuple of arrays of mode indices.")
|
|
404
|
+
elif mode_counts is None:
|
|
405
|
+
output = (np.arange(n_modes),)
|
|
406
|
+
elif isinstance(mode_counts, int):
|
|
407
|
+
output = (np.arange(mode_counts),)
|
|
408
|
+
else:
|
|
409
|
+
output = [np.arange(mc) for mc in mode_counts]
|
|
410
|
+
|
|
411
|
+
squeeze_output = (mode_ids is None) and (mode_counts is None or isinstance(mode_counts, int))
|
|
412
|
+
|
|
413
|
+
return output, squeeze_output
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
DATA_TYPE,SPECIES,TEMPLATE,DENSITY,HEMI,SOURCE,DOI,DESCRIPTION
|
|
2
|
+
midthickness,Homo sapiens,fsLR,4k,L,Glasser et al. (2011) J Neurosci,10.1523/JNEUROSCI.2180-11.2011,Cortical midthickness surface mesh and medial wall mask (Conte69). Note that the medial wall mask has been updated to correctly mask out all medial wall vertices (num. cortical vertices reduced from 3636 to 3619)
|
|
3
|
+
midthickness,Homo sapiens,fsLR,4k,R,Glasser et al. (2011) J Neurosci,10.1523/JNEUROSCI.2180-11.2011,Cortical midthickness surface mesh and medial wall mask (Conte69). Note that the medial wall mask has been updated to correctly mask out all medial wall vertices (num. cortical vertices reduced from 3636 to 3619)
|
|
4
|
+
midthickness,Homo sapiens,fsLR,32k,L,Glasser et al. (2011) J Neurosci,10.1523/JNEUROSCI.2180-11.2011,Cortical midthickness surface mesh and medial wall mask (Conte69)
|
|
5
|
+
midthickness,Homo sapiens,fsLR,32k,R,Glasser et al. (2011) J Neurosci,10.1523/JNEUROSCI.2180-11.2011,Cortical midthickness surface mesh and medial wall mask (Conte69)
|
|
6
|
+
midthickness,Macaca mulatta,fsLR,32k,L,Donahue et al. (2016) J Neurosci,10.1523/JNEUROSCI.0493-16.2016,Cortical midthickness surface mesh and medial wall mask (Yerkes19)
|
|
7
|
+
midthickness,Macaca mulatta,fsLR,32k,R,Donahue et al. (2016) J Neurosci,10.1523/JNEUROSCI.0493-16.2016,Cortical midthickness surface mesh and medial wall mask (Yerkes19)
|
|
8
|
+
midthickness,Callithrix jacchus,fsLR,32k,L,Hayashi et al. (2021) NeuroImage,10.1016/j.neuroimage.2021.117726,Cortical midthickness surface mesh and medial wall mask (Riken50)
|
|
9
|
+
midthickness,Callithrix jacchus,fsLR,32k,R,Hayashi et al. (2021) NeuroImage,10.1016/j.neuroimage.2021.117726,Cortical midthickness surface mesh and medial wall mask (Riken50)
|
|
10
|
+
sphere,Homo sapiens,fsLR,4k,L,Glasser et al. (2011) J Neurosci,10.1523/JNEUROSCI.2180-11.2011,Mesh inflated to sphere (Conte69)
|
|
11
|
+
sphere,Homo sapiens,fsLR,4k,R,Glasser et al. (2011) J Neurosci,10.1523/JNEUROSCI.2180-11.2011,Mesh inflated to sphere (Conte69)
|
|
12
|
+
sphere,Homo sapiens,fsLR,32k,L,Glasser et al. (2011) J Neurosci,10.1523/JNEUROSCI.2180-11.2011,Mesh inflated to sphere (Conte69)
|
|
13
|
+
sphere,Homo sapiens,fsLR,32k,R,Glasser et al. (2011) J Neurosci,10.1523/JNEUROSCI.2180-11.2011,Mesh inflated to sphere (Conte69)
|
|
14
|
+
fcgradient1,Homo sapiens,fsLR,32k,L,Margulies et al. (2016) PNAS,10.1073/pnas.1608282113,First functional connectivity gradient derived from resting-state fMRI
|
|
15
|
+
myelinmap,Homo sapiens,fsLR,32k,L,Glasser et al. (2016) Nature,10.1038/nature18933,Myelin map derived from T1w/T2w ratio
|
|
16
|
+
ndi,Homo sapiens,fsLR,32k,L,Fukutomi et al. (2018) NeuroImage,10.1016/j.neuroimage.2018.02.017,Neurite density index derived from NODDI
|
|
17
|
+
odi,Homo sapiens,fsLR,32k,L,Fukutomi et al. (2018) NeuroImage,10.1016/j.neuroimage.2018.02.017,Orientation dispersion index derived from NODDI
|
|
18
|
+
thickness,Homo sapiens,fsLR,32k,L,Glasser et al. (2016) Nature,10.1038/nature18933,Cortical thickness map
|