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.
Files changed (95) hide show
  1. nltools/__init__.py +55 -0
  2. nltools/algorithms/__init__.py +90 -0
  3. nltools/algorithms/alignment/__init__.py +21 -0
  4. nltools/algorithms/alignment/procrustes.py +565 -0
  5. nltools/algorithms/alignment/srm.py +758 -0
  6. nltools/algorithms/backends.py +1059 -0
  7. nltools/algorithms/corrections.py +177 -0
  8. nltools/algorithms/decoding.py +327 -0
  9. nltools/algorithms/inference/__init__.py +50 -0
  10. nltools/algorithms/inference/bootstrap.py +1386 -0
  11. nltools/algorithms/inference/correlation.py +373 -0
  12. nltools/algorithms/inference/intersubject.py +422 -0
  13. nltools/algorithms/inference/isc.py +1554 -0
  14. nltools/algorithms/inference/matrix.py +602 -0
  15. nltools/algorithms/inference/one_sample.py +288 -0
  16. nltools/algorithms/inference/random.py +122 -0
  17. nltools/algorithms/inference/timeseries.py +347 -0
  18. nltools/algorithms/inference/two_sample.py +212 -0
  19. nltools/algorithms/inference/utils.py +58 -0
  20. nltools/algorithms/inference/validation.py +282 -0
  21. nltools/algorithms/neighborhoods.py +207 -0
  22. nltools/algorithms/outliers.py +308 -0
  23. nltools/algorithms/regression.py +83 -0
  24. nltools/algorithms/signal.py +303 -0
  25. nltools/algorithms/similarity.py +234 -0
  26. nltools/algorithms/validation.py +151 -0
  27. nltools/cross_validation.py +72 -0
  28. nltools/data/__init__.py +30 -0
  29. nltools/data/adjacency/__init__.py +875 -0
  30. nltools/data/adjacency/io.py +111 -0
  31. nltools/data/adjacency/modeling.py +569 -0
  32. nltools/data/adjacency/plotting.py +174 -0
  33. nltools/data/adjacency/state.py +349 -0
  34. nltools/data/adjacency/stats.py +596 -0
  35. nltools/data/adjacency/utils.py +79 -0
  36. nltools/data/atlases/__init__.py +23 -0
  37. nltools/data/atlases/labeling.py +158 -0
  38. nltools/data/atlases/loading.py +76 -0
  39. nltools/data/atlases/registry.py +96 -0
  40. nltools/data/atlases/reporting.py +456 -0
  41. nltools/data/braindata/__init__.py +2170 -0
  42. nltools/data/braindata/analysis.py +1381 -0
  43. nltools/data/braindata/bootstrap.py +398 -0
  44. nltools/data/braindata/io.py +896 -0
  45. nltools/data/braindata/modeling.py +594 -0
  46. nltools/data/braindata/plotting.py +501 -0
  47. nltools/data/braindata/prediction.py +1250 -0
  48. nltools/data/braindata/utils.py +348 -0
  49. nltools/data/braindata/validation.py +197 -0
  50. nltools/data/braindata/viewer.js +266 -0
  51. nltools/data/braindata/viewer.py +770 -0
  52. nltools/data/combine.py +27 -0
  53. nltools/data/designmatrix/__init__.py +1032 -0
  54. nltools/data/designmatrix/append.py +518 -0
  55. nltools/data/designmatrix/diagnostics.py +248 -0
  56. nltools/data/designmatrix/io.py +356 -0
  57. nltools/data/designmatrix/plotting.py +291 -0
  58. nltools/data/designmatrix/regressors.py +463 -0
  59. nltools/data/designmatrix/transforms.py +200 -0
  60. nltools/data/designmatrix/utils.py +350 -0
  61. nltools/data/ownership.py +129 -0
  62. nltools/data/results.py +291 -0
  63. nltools/data/roc/__init__.py +398 -0
  64. nltools/data/simulator/__init__.py +927 -0
  65. nltools/data/simulator/haxby.py +124 -0
  66. nltools/data/validation.py +83 -0
  67. nltools/datasets.py +218 -0
  68. nltools/io/__init__.py +10 -0
  69. nltools/io/events.py +67 -0
  70. nltools/io/h5.py +246 -0
  71. nltools/mask.py +403 -0
  72. nltools/models/__init__.py +11 -0
  73. nltools/models/glm.py +543 -0
  74. nltools/models/results.py +49 -0
  75. nltools/models/ridge.py +1303 -0
  76. nltools/models/validation.py +26 -0
  77. nltools/plotting/__init__.py +32 -0
  78. nltools/plotting/adjacency.py +421 -0
  79. nltools/plotting/brain.py +669 -0
  80. nltools/plotting/decomposition.py +111 -0
  81. nltools/plotting/prediction.py +110 -0
  82. nltools/resources/covariates_example.csv +161 -0
  83. nltools/resources/onsets_example.csv +40 -0
  84. nltools/templates/__init__.py +51 -0
  85. nltools/templates/config.py +144 -0
  86. nltools/templates/fetch.py +260 -0
  87. nltools/templates/matching.py +183 -0
  88. nltools/templates/paths.py +106 -0
  89. nltools/templates/registry.py +25 -0
  90. nltools/utils.py +230 -0
  91. nltools/version.py +13 -0
  92. nltools-0.6.0.dev0.dist-info/METADATA +95 -0
  93. nltools-0.6.0.dev0.dist-info/RECORD +95 -0
  94. nltools-0.6.0.dev0.dist-info/WHEEL +4 -0
  95. 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())