nltools 0.6.0.dev0__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.
- nltools/__init__.py +55 -0
- nltools/algorithms/__init__.py +90 -0
- nltools/algorithms/alignment/__init__.py +21 -0
- nltools/algorithms/alignment/procrustes.py +565 -0
- nltools/algorithms/alignment/srm.py +758 -0
- nltools/algorithms/backends.py +1059 -0
- nltools/algorithms/corrections.py +177 -0
- nltools/algorithms/decoding.py +327 -0
- nltools/algorithms/inference/__init__.py +50 -0
- nltools/algorithms/inference/bootstrap.py +1386 -0
- nltools/algorithms/inference/correlation.py +373 -0
- nltools/algorithms/inference/intersubject.py +422 -0
- nltools/algorithms/inference/isc.py +1554 -0
- nltools/algorithms/inference/matrix.py +602 -0
- nltools/algorithms/inference/one_sample.py +288 -0
- nltools/algorithms/inference/random.py +122 -0
- nltools/algorithms/inference/timeseries.py +347 -0
- nltools/algorithms/inference/two_sample.py +212 -0
- nltools/algorithms/inference/utils.py +58 -0
- nltools/algorithms/inference/validation.py +282 -0
- nltools/algorithms/neighborhoods.py +207 -0
- nltools/algorithms/outliers.py +308 -0
- nltools/algorithms/regression.py +83 -0
- nltools/algorithms/signal.py +303 -0
- nltools/algorithms/similarity.py +234 -0
- nltools/algorithms/validation.py +151 -0
- nltools/cross_validation.py +72 -0
- nltools/data/__init__.py +30 -0
- nltools/data/adjacency/__init__.py +875 -0
- nltools/data/adjacency/io.py +111 -0
- nltools/data/adjacency/modeling.py +569 -0
- nltools/data/adjacency/plotting.py +174 -0
- nltools/data/adjacency/state.py +349 -0
- nltools/data/adjacency/stats.py +596 -0
- nltools/data/adjacency/utils.py +79 -0
- nltools/data/atlases/__init__.py +23 -0
- nltools/data/atlases/labeling.py +158 -0
- nltools/data/atlases/loading.py +76 -0
- nltools/data/atlases/registry.py +96 -0
- nltools/data/atlases/reporting.py +456 -0
- nltools/data/braindata/__init__.py +2170 -0
- nltools/data/braindata/analysis.py +1381 -0
- nltools/data/braindata/bootstrap.py +398 -0
- nltools/data/braindata/io.py +896 -0
- nltools/data/braindata/modeling.py +594 -0
- nltools/data/braindata/plotting.py +501 -0
- nltools/data/braindata/prediction.py +1250 -0
- nltools/data/braindata/utils.py +348 -0
- nltools/data/braindata/validation.py +197 -0
- nltools/data/braindata/viewer.js +266 -0
- nltools/data/braindata/viewer.py +770 -0
- nltools/data/combine.py +27 -0
- nltools/data/designmatrix/__init__.py +1032 -0
- nltools/data/designmatrix/append.py +518 -0
- nltools/data/designmatrix/diagnostics.py +248 -0
- nltools/data/designmatrix/io.py +356 -0
- nltools/data/designmatrix/plotting.py +291 -0
- nltools/data/designmatrix/regressors.py +463 -0
- nltools/data/designmatrix/transforms.py +200 -0
- nltools/data/designmatrix/utils.py +350 -0
- nltools/data/ownership.py +129 -0
- nltools/data/results.py +291 -0
- nltools/data/roc/__init__.py +398 -0
- nltools/data/simulator/__init__.py +927 -0
- nltools/data/simulator/haxby.py +124 -0
- nltools/data/validation.py +83 -0
- nltools/datasets.py +218 -0
- nltools/io/__init__.py +10 -0
- nltools/io/events.py +67 -0
- nltools/io/h5.py +246 -0
- nltools/mask.py +403 -0
- nltools/models/__init__.py +11 -0
- nltools/models/glm.py +543 -0
- nltools/models/results.py +49 -0
- nltools/models/ridge.py +1303 -0
- nltools/models/validation.py +26 -0
- nltools/plotting/__init__.py +32 -0
- nltools/plotting/adjacency.py +421 -0
- nltools/plotting/brain.py +669 -0
- nltools/plotting/decomposition.py +111 -0
- nltools/plotting/prediction.py +110 -0
- nltools/resources/covariates_example.csv +161 -0
- nltools/resources/onsets_example.csv +40 -0
- nltools/templates/__init__.py +51 -0
- nltools/templates/config.py +144 -0
- nltools/templates/fetch.py +260 -0
- nltools/templates/matching.py +183 -0
- nltools/templates/paths.py +106 -0
- nltools/templates/registry.py +25 -0
- nltools/utils.py +230 -0
- nltools/version.py +13 -0
- nltools-0.6.0.dev0.dist-info/METADATA +95 -0
- nltools-0.6.0.dev0.dist-info/RECORD +95 -0
- nltools-0.6.0.dev0.dist-info/WHEEL +4 -0
- nltools-0.6.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Coordinate-level atlas labeling.
|
|
2
|
+
|
|
3
|
+
Adapted from [atlasreader](https://github.com/miykael/atlasreader)
|
|
4
|
+
(BSD-3-Clause); please cite it when using these tools.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
Notter, M. P., Gale, D., Herholz, P., Markello, R., Notter-Bielser, M.-L., &
|
|
8
|
+
Whittingstall, K. (2019). AtlasReader: A Python package to generate
|
|
9
|
+
coordinate tables, region labels, and informative figures from statistical
|
|
10
|
+
MRI images. *Journal of Open Source Software*, 4(34), 1257.
|
|
11
|
+
https://doi.org/10.21105/joss.01257
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import polars as pl
|
|
18
|
+
from numpy.typing import ArrayLike
|
|
19
|
+
|
|
20
|
+
from .loading import _Atlas, load_atlas
|
|
21
|
+
|
|
22
|
+
CoordsLike = ArrayLike | Sequence[Sequence[float]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _as_xyz_array(coords: CoordsLike) -> np.ndarray:
|
|
26
|
+
"""Coerce coords to an ``(N, 3)`` float64 array."""
|
|
27
|
+
arr = np.asarray(coords, dtype=float)
|
|
28
|
+
if arr.ndim == 1:
|
|
29
|
+
arr = arr[None, :]
|
|
30
|
+
if arr.ndim != 2 or arr.shape[1] != 3:
|
|
31
|
+
raise ValueError(f"coords must have shape (N, 3); got {arr.shape}")
|
|
32
|
+
return arr
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _xyz_to_ijk(coords_xyz: np.ndarray, affine: np.ndarray) -> np.ndarray:
|
|
36
|
+
"""Map MNI mm → integer voxel ijk via the inverse affine.
|
|
37
|
+
|
|
38
|
+
Solves the affine, rounds to the nearest voxel, and casts to int.
|
|
39
|
+
"""
|
|
40
|
+
homog = np.hstack([coords_xyz, np.ones((coords_xyz.shape[0], 1))])
|
|
41
|
+
ijk = np.linalg.solve(affine, homog.T)[:3].T
|
|
42
|
+
return np.round(ijk).astype(int)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _clip_to_box(ijk: np.ndarray, shape: tuple[int, ...]) -> np.ndarray:
|
|
46
|
+
"""Clamp out-of-bounds voxel indices to the origin (background)."""
|
|
47
|
+
box = np.array(shape[:3])
|
|
48
|
+
out = np.any((ijk < 0) | (ijk >= box), axis=1)
|
|
49
|
+
ijk = ijk.copy()
|
|
50
|
+
ijk[out] = 0
|
|
51
|
+
return ijk
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _label_lookup(atlas: _Atlas) -> dict[int, str]:
|
|
55
|
+
"""Build an ``{integer index → region name}`` dict from atlas labels."""
|
|
56
|
+
return dict(
|
|
57
|
+
zip(
|
|
58
|
+
atlas.labels["index"].to_list(),
|
|
59
|
+
atlas.labels["name"].to_list(),
|
|
60
|
+
strict=True,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _label_deterministic(atlas: _Atlas, ijk: np.ndarray) -> list[str]:
|
|
66
|
+
"""Look up region names for voxels in a deterministic atlas."""
|
|
67
|
+
data = atlas.image.get_fdata()
|
|
68
|
+
lut = _label_lookup(atlas)
|
|
69
|
+
out: list[str] = []
|
|
70
|
+
for v in ijk:
|
|
71
|
+
idx = int(data[v[0], v[1], v[2]])
|
|
72
|
+
out.append(lut.get(idx, "no_label"))
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _format_prob_entry(pct: float, name: str) -> str:
|
|
77
|
+
"""Format a single ``"pct%% name"`` entry for probabilistic output."""
|
|
78
|
+
return f"{pct:.1f}% {name}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _label_probabilistic(
|
|
82
|
+
atlas: _Atlas, ijk: np.ndarray, prob_threshold: float
|
|
83
|
+
) -> list[str]:
|
|
84
|
+
"""Format ``"X% Foo; Y% Bar"`` strings per voxel for a probabilistic atlas.
|
|
85
|
+
|
|
86
|
+
Regions with probability below ``prob_threshold`` are dropped; if no
|
|
87
|
+
region survives, the voxel gets ``"no_label"``.
|
|
88
|
+
"""
|
|
89
|
+
data = atlas.image.get_fdata()
|
|
90
|
+
lut = _label_lookup(atlas)
|
|
91
|
+
out: list[str] = []
|
|
92
|
+
for v in ijk:
|
|
93
|
+
probs = np.asarray(data[v[0], v[1], v[2]], dtype=float)
|
|
94
|
+
keep = np.where(probs >= prob_threshold)[0]
|
|
95
|
+
if keep.size == 0:
|
|
96
|
+
out.append("no_label")
|
|
97
|
+
continue
|
|
98
|
+
order = keep[np.argsort(probs[keep])[::-1]]
|
|
99
|
+
out.append(
|
|
100
|
+
"; ".join(
|
|
101
|
+
_format_prob_entry(float(probs[i]), lut.get(int(i), "no_label"))
|
|
102
|
+
for i in order
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
return out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _labels_for_atlas(
|
|
109
|
+
atlas: _Atlas, ijk: np.ndarray, prob_threshold: float
|
|
110
|
+
) -> list[str]:
|
|
111
|
+
"""Dispatch to the deterministic or probabilistic labeling path."""
|
|
112
|
+
if atlas.kind == "probabilistic":
|
|
113
|
+
return _label_probabilistic(atlas, ijk, prob_threshold)
|
|
114
|
+
return _label_deterministic(atlas, ijk)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def label_coords(
|
|
118
|
+
coords: CoordsLike,
|
|
119
|
+
*,
|
|
120
|
+
atlas: str | Sequence[str] = "harvard_oxford",
|
|
121
|
+
prob_threshold: float = 5.0,
|
|
122
|
+
) -> pl.DataFrame:
|
|
123
|
+
"""Look up anatomical labels for a set of MNI mm coordinates.
|
|
124
|
+
|
|
125
|
+
For each coordinate, returns the atlas region(s) it falls in. Works
|
|
126
|
+
for both deterministic atlases (single label per coord) and
|
|
127
|
+
probabilistic atlases (formatted `"42.0% Foo; 18.0% Bar"` strings,
|
|
128
|
+
sorted by descending probability).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
coords (array-like): `(N, 3)` MNI mm coordinates `(x, y, z)`. A single
|
|
132
|
+
coordinate like `(-42, -22, 56)` is also accepted.
|
|
133
|
+
atlas (str | Sequence[str]): Atlas name or list of names from
|
|
134
|
+
`list_atlases`. One column is added to the output per atlas. Default
|
|
135
|
+
`'harvard_oxford'`.
|
|
136
|
+
prob_threshold (float): For probabilistic atlases only — drop regions
|
|
137
|
+
with probability (in percent units) below this threshold. Default 5.0.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
pl.DataFrame: Frame with columns `x`, `y`, `z` plus one column per atlas.
|
|
141
|
+
All atlas columns are `Utf8`.
|
|
142
|
+
"""
|
|
143
|
+
xyz = _as_xyz_array(coords)
|
|
144
|
+
atlas_names = [atlas] if isinstance(atlas, str) else list(atlas)
|
|
145
|
+
|
|
146
|
+
columns: dict[str, list] = {
|
|
147
|
+
"x": xyz[:, 0].tolist(),
|
|
148
|
+
"y": xyz[:, 1].tolist(),
|
|
149
|
+
"z": xyz[:, 2].tolist(),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for name in atlas_names:
|
|
153
|
+
a = load_atlas(name)
|
|
154
|
+
ijk = _xyz_to_ijk(xyz, a.image.affine)
|
|
155
|
+
ijk = _clip_to_box(ijk, a.image.shape)
|
|
156
|
+
columns[name] = _labels_for_atlas(a, ijk, prob_threshold)
|
|
157
|
+
|
|
158
|
+
return pl.DataFrame(columns)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Lazy loading of atlas NIfTI + label CSV files from the HF dataset."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import nibabel as nb
|
|
7
|
+
import polars as pl
|
|
8
|
+
|
|
9
|
+
from nltools.templates.fetch import fetch_resource
|
|
10
|
+
|
|
11
|
+
from .registry import ATLASES, AtlasKind
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class _Atlas:
|
|
16
|
+
"""A loaded atlas — image, labels, and metadata.
|
|
17
|
+
|
|
18
|
+
Constructed by `load_atlas`; users normally don't instantiate
|
|
19
|
+
directly.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
name (str): Registry key (e.g. `'harvard_oxford'`).
|
|
23
|
+
image (nibabel.Nifti1Image): NIfTI volume. 3-D for deterministic atlases,
|
|
24
|
+
4-D for probabilistic ones (last axis indexes regions).
|
|
25
|
+
labels (pl.DataFrame): Two-column `index, name` table. For deterministic
|
|
26
|
+
atlases `index` is the integer voxel value; for probabilistic atlases
|
|
27
|
+
`index` is the region index along the 4th dim of `image`.
|
|
28
|
+
kind (AtlasKind): `'deterministic'` or `'probabilistic'`.
|
|
29
|
+
citation (str): Short citation for the original atlas.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
name: str
|
|
33
|
+
image: nb.Nifti1Image
|
|
34
|
+
labels: pl.DataFrame
|
|
35
|
+
kind: AtlasKind
|
|
36
|
+
citation: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@functools.cache
|
|
40
|
+
def load_atlas(name: str) -> _Atlas:
|
|
41
|
+
"""Lazy-load an atlas by registry name.
|
|
42
|
+
|
|
43
|
+
The first call fetches the NIfTI + label CSV from
|
|
44
|
+
`huggingface.co/datasets/nltools/niftis` (cached locally afterwards).
|
|
45
|
+
Subsequent calls in the same process are memoized.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
name (str): Atlas key from `list_atlases`.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
_Atlas: The atlas with image, labels, and metadata loaded.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If `name` isn't a registered atlas.
|
|
55
|
+
|
|
56
|
+
Note:
|
|
57
|
+
An atlas is in MNI space, and nltools resamples it onto your data by
|
|
58
|
+
header affine alone. That is a grid change, not a spatial
|
|
59
|
+
normalization, so parcel boundaries are approximate unless your data
|
|
60
|
+
are already normalized to the same space.
|
|
61
|
+
"""
|
|
62
|
+
if name not in ATLASES:
|
|
63
|
+
known = ", ".join(sorted(ATLASES))
|
|
64
|
+
raise ValueError(f"unknown atlas {name!r}; choose from: {known}")
|
|
65
|
+
|
|
66
|
+
meta = ATLASES[name]
|
|
67
|
+
img_path = fetch_resource(f"atlases/atlas_{name}.nii.gz")
|
|
68
|
+
csv_path = fetch_resource(f"atlases/labels_{name}.csv")
|
|
69
|
+
|
|
70
|
+
return _Atlas(
|
|
71
|
+
name=name,
|
|
72
|
+
image=nb.load(img_path),
|
|
73
|
+
labels=pl.read_csv(csv_path),
|
|
74
|
+
kind=meta.kind,
|
|
75
|
+
citation=meta.citation,
|
|
76
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Static registry of atlases hosted at `nltools/niftis/atlases`.
|
|
2
|
+
|
|
3
|
+
Each entry describes an atlas's kind (deterministic vs probabilistic) and
|
|
4
|
+
the citation users should cite when they use it. The actual NIfTI + label
|
|
5
|
+
files are fetched lazily by `load_atlas` via `fetch_resource`.
|
|
6
|
+
|
|
7
|
+
Atlases were sourced from atlasreader (BSD-3-Clause) and are subject to
|
|
8
|
+
their original upstream licenses — see `LICENSES.md` in the HF dataset.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
AtlasKind = Literal["deterministic", "probabilistic"]
|
|
15
|
+
"""Kind of atlas: `'deterministic'` (3-D integer labels) or `'probabilistic'` (4-D, last axis indexes regions)."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class _AtlasMetadata:
|
|
20
|
+
"""Static description of a registered atlas.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
kind (AtlasKind): `'deterministic'` (3-D integer-labeled) or
|
|
24
|
+
`'probabilistic'` (4-D, last axis indexes regions).
|
|
25
|
+
citation (str): Short citation string for the original atlas.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
kind: AtlasKind
|
|
29
|
+
citation: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
ATLASES: dict[str, _AtlasMetadata] = {
|
|
33
|
+
"aal": _AtlasMetadata(
|
|
34
|
+
kind="deterministic",
|
|
35
|
+
citation="Tzourio-Mazoyer et al. 2002, NeuroImage",
|
|
36
|
+
),
|
|
37
|
+
"aicha": _AtlasMetadata(
|
|
38
|
+
kind="deterministic",
|
|
39
|
+
citation="Joliot et al. 2015, J Neurosci Methods",
|
|
40
|
+
),
|
|
41
|
+
"desikan_killiany": _AtlasMetadata(
|
|
42
|
+
kind="deterministic",
|
|
43
|
+
citation="Desikan et al. 2006, NeuroImage (FreeSurfer license)",
|
|
44
|
+
),
|
|
45
|
+
"destrieux": _AtlasMetadata(
|
|
46
|
+
kind="deterministic",
|
|
47
|
+
citation="Destrieux et al. 2010, NeuroImage (FreeSurfer license)",
|
|
48
|
+
),
|
|
49
|
+
"harvard_oxford": _AtlasMetadata(
|
|
50
|
+
kind="probabilistic",
|
|
51
|
+
citation="Desikan et al. 2006, NeuroImage / FSL Harvard-Oxford",
|
|
52
|
+
),
|
|
53
|
+
"juelich": _AtlasMetadata(
|
|
54
|
+
kind="probabilistic",
|
|
55
|
+
citation="Eickhoff et al. 2005, NeuroImage",
|
|
56
|
+
),
|
|
57
|
+
"marsatlas": _AtlasMetadata(
|
|
58
|
+
kind="deterministic",
|
|
59
|
+
citation="Auzias et al. 2016, Hum Brain Mapp",
|
|
60
|
+
),
|
|
61
|
+
"neuromorphometrics": _AtlasMetadata(
|
|
62
|
+
kind="deterministic",
|
|
63
|
+
citation="MICCAI 2012 Multi-Atlas Labeling Challenge",
|
|
64
|
+
),
|
|
65
|
+
"schaefer_200": _AtlasMetadata(
|
|
66
|
+
kind="deterministic",
|
|
67
|
+
citation="Schaefer et al. 2018, Cereb Cortex (200-parcel, 7-network)",
|
|
68
|
+
),
|
|
69
|
+
"talairach_ba": _AtlasMetadata(
|
|
70
|
+
kind="deterministic",
|
|
71
|
+
citation="Talairach & Tournoux 1988 (Brodmann areas)",
|
|
72
|
+
),
|
|
73
|
+
"talairach_gyrus": _AtlasMetadata(
|
|
74
|
+
kind="deterministic",
|
|
75
|
+
citation="Talairach & Tournoux 1988 (gyri)",
|
|
76
|
+
),
|
|
77
|
+
}
|
|
78
|
+
"""Registered atlases keyed by name; each value is an `_AtlasMetadata` (kind + citation)."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
DEFAULT_ATLASES: tuple[str, ...] = ("harvard_oxford", "aal", "schaefer_200")
|
|
82
|
+
"""Default atlas trio for `BrainData.cluster_report` and `label_coords`.
|
|
83
|
+
|
|
84
|
+
Picked to give one probabilistic (`harvard_oxford`), one anatomical
|
|
85
|
+
deterministic (`aal`), and one functional deterministic (`schaefer_200`) atlas
|
|
86
|
+
at once.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def list_atlases() -> list[str]:
|
|
91
|
+
"""Return the sorted list of registered atlas names.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
list[str]: Sorted list of atlas names usable with `load_atlas`.
|
|
95
|
+
"""
|
|
96
|
+
return sorted(ATLASES.keys())
|