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,124 @@
|
|
|
1
|
+
"""A synthetic Haxby-like dataset, built the way `Simulator` builds data.
|
|
2
|
+
|
|
3
|
+
Despite living beside the fetchers in `nltools.datasets`, `load_haxby_example`
|
|
4
|
+
downloads nothing: it injects condition-specific signal into a tiny seeded
|
|
5
|
+
volume. That makes it a simulator, so its implementation sits here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from nltools.data.braindata import BrainData
|
|
9
|
+
from nltools.data.designmatrix import DesignMatrix
|
|
10
|
+
from nltools.io.events import events_to_dm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_HAXBY_CONDITIONS = (
|
|
14
|
+
"face",
|
|
15
|
+
"house",
|
|
16
|
+
"cat",
|
|
17
|
+
"bottle",
|
|
18
|
+
"scissors",
|
|
19
|
+
"shoe",
|
|
20
|
+
"chair",
|
|
21
|
+
"scrambledpix",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_haxby_example(n_runs=1, random_state=42):
|
|
26
|
+
"""Load a small synthetic Haxby-like dataset, entirely in-memory.
|
|
27
|
+
|
|
28
|
+
Returns paired lists of `BrainData` and `DesignMatrix`, one entry per
|
|
29
|
+
run, generated from a tiny synthetic volume (10 x 10 x 5 = 500 voxels)
|
|
30
|
+
with condition-specific signal injected into disjoint voxel clusters.
|
|
31
|
+
No network I/O, no disk I/O, no nilearn fetcher dependency. Runs in
|
|
32
|
+
well under a second.
|
|
33
|
+
|
|
34
|
+
Intended for tutorials, documentation examples, and tests where
|
|
35
|
+
downloading a real fMRI dataset is impractical. The
|
|
36
|
+
eight conditions match the real Haxby 2001 object-recognition experiment
|
|
37
|
+
(face, house, cat, bottle, scissors, shoe, chair, scrambledpix), arranged
|
|
38
|
+
in a randomized 9-TR block design with TR=2.5s.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
n_runs (int): Number of runs to generate. Default 1.
|
|
42
|
+
random_state (int | None): Seed for reproducible output. Default 42.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
tuple: `(list[BrainData], list[DesignMatrix])`, each of length `n_runs`.
|
|
46
|
+
The DesignMatrix columns are the eight condition names suffixed
|
|
47
|
+
with `_c0` (HRF-convolved boxcars).
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
```python
|
|
51
|
+
from nltools.datasets import load_haxby_example
|
|
52
|
+
|
|
53
|
+
brain_data, design_matrices = load_haxby_example()
|
|
54
|
+
data, dm = brain_data[0], design_matrices[0]
|
|
55
|
+
data.shape # → (72, 500)
|
|
56
|
+
"face_c0" in dm.columns # → True
|
|
57
|
+
```
|
|
58
|
+
"""
|
|
59
|
+
import numpy as np
|
|
60
|
+
import pandas as pd
|
|
61
|
+
import nibabel as nib
|
|
62
|
+
|
|
63
|
+
rng = np.random.default_rng(random_state)
|
|
64
|
+
|
|
65
|
+
TR = 2.5
|
|
66
|
+
block_tr = 9 # 22.5s blocks, same order of magnitude as real Haxby
|
|
67
|
+
n_conditions = len(_HAXBY_CONDITIONS)
|
|
68
|
+
n_timepoints = block_tr * n_conditions # 72 TRs per run
|
|
69
|
+
spatial_shape = (10, 10, 5)
|
|
70
|
+
n_voxels = int(np.prod(spatial_shape))
|
|
71
|
+
affine = np.diag([3.0, 3.0, 3.0, 1.0]).astype(np.float32)
|
|
72
|
+
mask_img = nib.Nifti1Image(np.ones(spatial_shape, dtype=np.float32), affine)
|
|
73
|
+
|
|
74
|
+
brain_data_list = []
|
|
75
|
+
design_matrix_list = []
|
|
76
|
+
|
|
77
|
+
for _ in range(n_runs):
|
|
78
|
+
order = list(rng.permutation(_HAXBY_CONDITIONS))
|
|
79
|
+
events_df = pd.DataFrame(
|
|
80
|
+
[
|
|
81
|
+
{
|
|
82
|
+
"onset": i * block_tr * TR,
|
|
83
|
+
"duration": block_tr * TR,
|
|
84
|
+
"trial_type": cond,
|
|
85
|
+
}
|
|
86
|
+
for i, cond in enumerate(order)
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
dm_data = events_to_dm(
|
|
90
|
+
events_df,
|
|
91
|
+
run_length=n_timepoints,
|
|
92
|
+
sampling_freq=1.0 / TR,
|
|
93
|
+
)
|
|
94
|
+
dm = DesignMatrix(dm_data, sampling_freq=1.0 / TR).convolve()
|
|
95
|
+
|
|
96
|
+
# Positive BOLD-like baseline intensity + voxelwise Gaussian noise,
|
|
97
|
+
# plus signal injected into a disjoint voxel cluster per condition
|
|
98
|
+
# so contrasts produce real spatial patterns. The positive baseline
|
|
99
|
+
# is required so percent-signal-change scaling inside GLM fits
|
|
100
|
+
# (which divides by the voxel mean) stays numerically sane.
|
|
101
|
+
baseline = 100.0
|
|
102
|
+
noise_sd = 1.0
|
|
103
|
+
signal_strength = 5.0 # BOLD units → clearly visible pattern in plots
|
|
104
|
+
data_flat = (
|
|
105
|
+
baseline + noise_sd * rng.standard_normal((n_voxels, n_timepoints))
|
|
106
|
+
).astype(np.float32)
|
|
107
|
+
voxels_per_cond = n_voxels // n_conditions
|
|
108
|
+
voxel_order = rng.permutation(n_voxels)
|
|
109
|
+
for c_idx, cond in enumerate(_HAXBY_CONDITIONS):
|
|
110
|
+
cond_col = f"{cond}_c0"
|
|
111
|
+
if cond_col not in dm.columns:
|
|
112
|
+
continue
|
|
113
|
+
regressor = np.asarray(dm[cond_col], dtype=np.float32)
|
|
114
|
+
cluster = voxel_order[
|
|
115
|
+
c_idx * voxels_per_cond : (c_idx + 1) * voxels_per_cond
|
|
116
|
+
]
|
|
117
|
+
data_flat[cluster, :] += signal_strength * regressor
|
|
118
|
+
|
|
119
|
+
data_4d = data_flat.reshape((*spatial_shape, n_timepoints))
|
|
120
|
+
img = nib.Nifti1Image(data_4d, affine)
|
|
121
|
+
brain_data_list.append(BrainData(img, mask=mask_img, verbose=0))
|
|
122
|
+
design_matrix_list.append(dm)
|
|
123
|
+
|
|
124
|
+
return brain_data_list, design_matrix_list
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Frame validation shared by the data classes.
|
|
2
|
+
|
|
3
|
+
`BrainData.X`/`.Y` and `Adjacency.Y` accept the same range of tabular inputs
|
|
4
|
+
and store the same polars frame, so the ingress check lives here rather than
|
|
5
|
+
inside either class.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import polars as pl
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _validate_frame(frame, data_shape=None, frame_type="DataFrame"):
|
|
15
|
+
"""Validate and process an X or Y frame for a data class.
|
|
16
|
+
|
|
17
|
+
Accepts pandas DataFrames for user convenience but always returns a
|
|
18
|
+
polars DataFrame. Internal data-class state is polars-only.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
frame (pl.DataFrame | pd.DataFrame | dict | np.ndarray | str | Path | None):
|
|
22
|
+
Input to validate: ``None``, a path to a CSV, a polars or pandas
|
|
23
|
+
DataFrame, a dict of columns, or a 1D/2D numpy array.
|
|
24
|
+
data_shape (tuple | None): Data shape to validate the row count against.
|
|
25
|
+
frame_type (str): Name of the frame for error messages (e.g. ``"X"``,
|
|
26
|
+
``"Y"``).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
pl.DataFrame: Validated frame as polars. Empty ``pl.DataFrame()`` when
|
|
30
|
+
``frame`` is ``None``.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
TypeError: If frame is not a supported type.
|
|
34
|
+
ValueError: If frame rows do not match ``data_shape[0]`` or CSV read fails.
|
|
35
|
+
"""
|
|
36
|
+
if frame is None:
|
|
37
|
+
return pl.DataFrame()
|
|
38
|
+
|
|
39
|
+
# Unwrap DesignMatrix to its underlying polars DataFrame — DM-specific
|
|
40
|
+
# metadata (sampling_freq, convolved, confounds) isn't preserved on
|
|
41
|
+
# BrainData, but users should be able to hand a DM in directly.
|
|
42
|
+
from nltools.data.designmatrix import DesignMatrix
|
|
43
|
+
|
|
44
|
+
if isinstance(frame, DesignMatrix):
|
|
45
|
+
frame = frame.data
|
|
46
|
+
|
|
47
|
+
if isinstance(frame, pl.DataFrame):
|
|
48
|
+
out = frame
|
|
49
|
+
elif isinstance(frame, (str, Path)):
|
|
50
|
+
try:
|
|
51
|
+
out = pl.read_csv(frame, has_header=False)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Could not read {frame_type} from file '{frame}'. "
|
|
55
|
+
f"Make sure the file exists and is a valid CSV. Error: {e}"
|
|
56
|
+
)
|
|
57
|
+
elif isinstance(frame, dict):
|
|
58
|
+
out = pl.DataFrame(frame)
|
|
59
|
+
elif isinstance(frame, np.ndarray):
|
|
60
|
+
arr = frame if frame.ndim == 2 else frame.reshape(-1, 1)
|
|
61
|
+
out = pl.DataFrame(arr)
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
import pandas as pd
|
|
65
|
+
except ImportError:
|
|
66
|
+
pd = None
|
|
67
|
+
if pd is not None and isinstance(frame, pd.DataFrame):
|
|
68
|
+
out = pl.DataFrame({str(c): frame[c].to_numpy() for c in frame.columns})
|
|
69
|
+
else:
|
|
70
|
+
raise TypeError(
|
|
71
|
+
f"{frame_type} must be a filepath (str/Path), numpy array, dict, or "
|
|
72
|
+
f"polars/pandas DataFrame. Received {type(frame).__name__}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not out.is_empty() and data_shape is not None:
|
|
76
|
+
if out.shape[0] != data_shape[0]:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"{frame_type} rows ({out.shape[0]}) do not match "
|
|
79
|
+
f"data rows ({data_shape[0]}). Each row in {frame_type} should "
|
|
80
|
+
f"correspond to an image in the data."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return out
|
nltools/datasets.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Dataset, resource, and atlas lookups.
|
|
2
|
+
|
|
3
|
+
Functions to fetch example datasets, bundled resources, and parcellations. The
|
|
4
|
+
curated example datasets (`fetch_pain`, `fetch_emotion_ratings`) are hosted on
|
|
5
|
+
the ``nltools/niftis`` Hugging Face dataset and resolve through the same
|
|
6
|
+
`fetch_resource` machinery as the MNI templates and atlases. Arbitrary
|
|
7
|
+
Neurovault collections are available via `fetch_neurovault_collection`, and
|
|
8
|
+
`list_atlases` / `load_atlas` / `label_coords` cover the parcellations.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"download_nifti",
|
|
13
|
+
"fetch_emotion_ratings",
|
|
14
|
+
"fetch_neurovault_collection",
|
|
15
|
+
"fetch_pain",
|
|
16
|
+
"fetch_resource",
|
|
17
|
+
"get_resource_path",
|
|
18
|
+
"label_coords",
|
|
19
|
+
"list_atlases",
|
|
20
|
+
"list_resources",
|
|
21
|
+
"load_atlas",
|
|
22
|
+
"load_haxby_example",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
import io
|
|
26
|
+
from contextlib import nullcontext, redirect_stdout
|
|
27
|
+
from os.path import dirname, join, sep as pathsep
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from nltools.data import BrainData
|
|
31
|
+
from nltools.data.atlases import label_coords, list_atlases, load_atlas
|
|
32
|
+
from nltools.data.simulator.haxby import load_haxby_example
|
|
33
|
+
from nltools.templates import fetch_resource, list_resources
|
|
34
|
+
|
|
35
|
+
# Core dependencies
|
|
36
|
+
from nilearn.datasets import fetch_neurovault_ids
|
|
37
|
+
|
|
38
|
+
# Optional dependencies
|
|
39
|
+
try:
|
|
40
|
+
import requests
|
|
41
|
+
except ImportError:
|
|
42
|
+
requests = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_resource_path():
|
|
46
|
+
"""Get the path to the nltools resource directory.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: Absolute path to `nltools/resources/`, with a trailing separator.
|
|
50
|
+
"""
|
|
51
|
+
return join(dirname(__file__), "resources") + pathsep
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Curated datasets hosted on the ``nltools/niftis`` HF dataset. Each directory
|
|
55
|
+
# holds a ``metadata.csv`` whose ``filename`` column is the image manifest.
|
|
56
|
+
_PAIN_DIR = "datasets/pain"
|
|
57
|
+
_EMOTION_DIR = "datasets/emotion_ratings"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def download_nifti(url, data_dir=None):
|
|
61
|
+
"""Download an image from a URL to a nifti file.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
url (str): URL of the image to download
|
|
65
|
+
data_dir (str, optional): Directory to save the file. If None, uses current directory.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
str: Path to the downloaded file
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ImportError: If requests is not available
|
|
72
|
+
ValueError: If URL is invalid
|
|
73
|
+
"""
|
|
74
|
+
if requests is None:
|
|
75
|
+
raise ImportError("requests package is required for downloading files")
|
|
76
|
+
|
|
77
|
+
if not url:
|
|
78
|
+
raise ValueError("URL cannot be empty")
|
|
79
|
+
if isinstance(url, Path):
|
|
80
|
+
url = str(url)
|
|
81
|
+
|
|
82
|
+
local_filename = url.split("/")[-1]
|
|
83
|
+
if data_dir is not None:
|
|
84
|
+
data_dir = Path(data_dir)
|
|
85
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
local_filename = data_dir / local_filename
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
with requests.get(url, stream=True, timeout=(10, 60)) as r:
|
|
90
|
+
r.raise_for_status()
|
|
91
|
+
|
|
92
|
+
with open(local_filename, "wb") as f:
|
|
93
|
+
for chunk in r.iter_content(chunk_size=1024):
|
|
94
|
+
if chunk: # filter out keep-alive new chunks
|
|
95
|
+
f.write(chunk)
|
|
96
|
+
except requests.RequestException as e:
|
|
97
|
+
raise ValueError(f"Failed to download {url}: {e}")
|
|
98
|
+
|
|
99
|
+
return str(local_filename)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def fetch_neurovault_collection(collection_id, data_dir=None, verbose=1):
|
|
103
|
+
"""Download images and metadata from a Neurovault collection.
|
|
104
|
+
|
|
105
|
+
This function uses the modern nilearn API to download collections from Neurovault.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
collection_id (int): Neurovault collection ID
|
|
109
|
+
data_dir (str, optional): Directory to store downloaded data.
|
|
110
|
+
If None, uses nilearn's default data directory.
|
|
111
|
+
verbose (int, optional): Verbosity level; `0` is silent, including the
|
|
112
|
+
data-directory line nilearn reports whatever it is asked for.
|
|
113
|
+
Default: 1
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
tuple[pl.DataFrame, list[str]]: `(metadata, files)` — the image metadata
|
|
117
|
+
table and the downloaded image paths.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If collection_id is invalid
|
|
121
|
+
RuntimeError: If download fails
|
|
122
|
+
"""
|
|
123
|
+
import polars as pl
|
|
124
|
+
|
|
125
|
+
if not isinstance(collection_id, int) or collection_id <= 0:
|
|
126
|
+
raise ValueError("collection_id must be a positive integer")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
# nilearn resolves its data directory with `get_dataset_dir("neurovault",
|
|
130
|
+
# data_dir)` without forwarding `verbose`, so it announces that
|
|
131
|
+
# directory's absolute path however quietly it was asked to work.
|
|
132
|
+
# `verbose=0` has to mean silence: anything that captures a session and
|
|
133
|
+
# publishes it — a notebook, the docs build — would otherwise carry the
|
|
134
|
+
# path of the machine that ran it.
|
|
135
|
+
quiet = redirect_stdout(io.StringIO()) if verbose == 0 else nullcontext()
|
|
136
|
+
with quiet:
|
|
137
|
+
nv_data = fetch_neurovault_ids(
|
|
138
|
+
collection_ids=[collection_id], data_dir=data_dir, verbose=verbose
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
files = nv_data["images"]
|
|
142
|
+
metadata = pl.DataFrame(nv_data["images_meta"])
|
|
143
|
+
|
|
144
|
+
return metadata, files
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
raise RuntimeError(f"Failed to download collection {collection_id}: {e}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def fetch_pain(verbose=0):
|
|
151
|
+
"""Download and load the pain dataset from the nltools HF dataset.
|
|
152
|
+
|
|
153
|
+
Loads the Chang et al. (2015) pain-perception study: 28 subjects x 3
|
|
154
|
+
stimulus-intensity conditions = 84 whole-brain contrast images, with a
|
|
155
|
+
curated metadata table (`SubjectID`, `PainLevel`, `PainIntensity`, `Age`,
|
|
156
|
+
`Sex`, provenance `neurovault_id` / `name`).
|
|
157
|
+
|
|
158
|
+
Data is hosted on the ``nltools/niftis`` Hugging Face dataset and cached
|
|
159
|
+
locally on first use, so this works with no extra setup.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
verbose (int, optional): Verbosity passed to `BrainData` while loading.
|
|
163
|
+
Default: 0
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
BrainData: `BrainData` with the 84 images; `X` holds the metadata table.
|
|
167
|
+
|
|
168
|
+
References:
|
|
169
|
+
Chang, L. J., Gianaros, P. J., Manuck, S. B., Krishnan, A., & Wager, T. D. (2015).
|
|
170
|
+
A sensitive and specific neural signature for picture-induced negative affect.
|
|
171
|
+
PLoS biology, 13(6), e1002180.
|
|
172
|
+
"""
|
|
173
|
+
import polars as pl
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
metadata = pl.read_csv(fetch_resource(f"{_PAIN_DIR}/metadata.csv"))
|
|
177
|
+
files = [fetch_resource(f"{_PAIN_DIR}/{fn}") for fn in metadata["filename"]]
|
|
178
|
+
return BrainData(data=files, X=metadata, verbose=verbose)
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
raise RuntimeError(f"Failed to fetch pain dataset: {e}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def fetch_emotion_ratings(verbose=0):
|
|
185
|
+
"""Download and load the emotion-rating dataset from the nltools HF dataset.
|
|
186
|
+
|
|
187
|
+
Loads the Chang et al. (2015) IAPS emotion-rating study: 679 whole-brain
|
|
188
|
+
contrast images across 150 subjects, each rating images 1-5, with a
|
|
189
|
+
built-in train/test holdout split. `X` carries the full portable Neurovault
|
|
190
|
+
metadata (key columns: `SubjectID`, `Rating`, `Holdout`, `AGE`, `SEX`).
|
|
191
|
+
|
|
192
|
+
Data is hosted on the ``nltools/niftis`` Hugging Face dataset and cached
|
|
193
|
+
locally on first use, so this works with no extra setup.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
verbose (int, optional): Verbosity passed to `BrainData` while loading.
|
|
197
|
+
Default: 0
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
BrainData: `BrainData` with the 679 images; `X` holds the metadata table.
|
|
201
|
+
|
|
202
|
+
References:
|
|
203
|
+
Chang, L. J., Gianaros, P. J., Manuck, S. B., Krishnan, A., & Wager, T. D. (2015).
|
|
204
|
+
A sensitive and specific neural signature for picture-induced negative affect.
|
|
205
|
+
PLoS biology, 13(6), e1002180.
|
|
206
|
+
"""
|
|
207
|
+
import polars as pl
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
metadata = pl.read_csv(fetch_resource(f"{_EMOTION_DIR}/metadata.csv"))
|
|
211
|
+
files = [
|
|
212
|
+
fetch_resource(f"{_EMOTION_DIR}/{fn}")
|
|
213
|
+
for fn in metadata["filename"].to_list()
|
|
214
|
+
]
|
|
215
|
+
return BrainData(data=files, X=metadata, verbose=verbose)
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
raise RuntimeError(f"Failed to fetch emotion ratings dataset: {e}")
|
nltools/io/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""nltools I/O utilities.
|
|
2
|
+
|
|
3
|
+
`events_to_dm` turns a BIDS events table into boxcar regressors. HDF5
|
|
4
|
+
serialization for the data classes lives in `nltools.io.h5`; users reach it
|
|
5
|
+
through `BrainData.write`/`Adjacency.write` and the constructors.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .events import events_to_dm
|
|
9
|
+
|
|
10
|
+
__all__ = ["events_to_dm"]
|
nltools/io/events.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Convert BIDS events tables into design-matrix regressors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import polars as pl
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def events_to_dm(
|
|
15
|
+
events: pl.DataFrame | pd.DataFrame,
|
|
16
|
+
*,
|
|
17
|
+
run_length: int,
|
|
18
|
+
sampling_freq: float,
|
|
19
|
+
) -> pl.DataFrame:
|
|
20
|
+
"""Convert a BIDS events table to boxcar regressors aligned to TRs.
|
|
21
|
+
|
|
22
|
+
Uses `nilearn.glm.first_level.make_first_level_design_matrix` with
|
|
23
|
+
`hrf_model=None` to sample events onto the TR grid without HRF
|
|
24
|
+
convolution — the caller is expected to call `DesignMatrix.convolve()`
|
|
25
|
+
explicitly when convolution is desired. Drops nilearn's auto-added
|
|
26
|
+
`constant` column; users add the intercept via `add_poly(0)`.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
events (pl.DataFrame | pd.DataFrame): Events table with BIDS columns
|
|
30
|
+
`onset`, `duration`, `trial_type` (required); `modulation` is
|
|
31
|
+
passed through if present.
|
|
32
|
+
run_length (int): Number of TRs the run contains.
|
|
33
|
+
sampling_freq (float): Sampling frequency in Hz (= 1/TR).
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
pl.DataFrame: One column per unique `trial_type`, values in
|
|
37
|
+
{0, modulation} indicating where each condition is active.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
```python
|
|
41
|
+
import polars as pl
|
|
42
|
+
from nltools.io import events_to_dm
|
|
43
|
+
|
|
44
|
+
events = pl.DataFrame(
|
|
45
|
+
{"onset": [0.0, 10.0], "duration": [5.0, 5.0], "trial_type": ["a", "b"]}
|
|
46
|
+
)
|
|
47
|
+
regressors = events_to_dm(events, run_length=20, sampling_freq=0.5)
|
|
48
|
+
```
|
|
49
|
+
"""
|
|
50
|
+
import pandas as pd
|
|
51
|
+
from nilearn.glm.first_level import make_first_level_design_matrix
|
|
52
|
+
|
|
53
|
+
if isinstance(events, pl.DataFrame):
|
|
54
|
+
events = pd.DataFrame(events.to_dict(as_series=False))
|
|
55
|
+
|
|
56
|
+
tr = 1.0 / sampling_freq
|
|
57
|
+
frame_times = np.arange(run_length) * tr
|
|
58
|
+
dm = make_first_level_design_matrix(
|
|
59
|
+
frame_times,
|
|
60
|
+
events=events,
|
|
61
|
+
hrf_model=None,
|
|
62
|
+
drift_model=None,
|
|
63
|
+
)
|
|
64
|
+
if "constant" in dm.columns:
|
|
65
|
+
dm = dm.drop(columns=["constant"])
|
|
66
|
+
# Avoid pyarrow dep on the pandas → polars hop.
|
|
67
|
+
return pl.DataFrame({str(c): dm[c].to_numpy() for c in dm.columns})
|