Skip to content

Commit 3851dc0

Browse files
eurunuelaclaude
andcommitted
Switch pySPFM notebook to Schaefer atlas and address review comments
- Replace voxel-wise NiftiMasker with NiftiLabelsMasker + Schaefer 1000-parcel atlas, reducing input matrix from ~218k voxels to 1000 parcels (feasible on a laptop without OOM) - Fix data path: ../DATA → ../data (case-sensitive on Linux/CI) - Remove n_jobs=-1 (pySPFM v2.0 removed that parameter) - Remove unused mask_file and confounds_file variables - Remove unused hrf_matrix assignment - Save parcel-wise activity as out_activity.npy instead of a large 4D NIfTI; add a compact mean-abs summary image (out_activity_mean.nii.gz) for lightweight voxel-space visualization Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cd753a8 commit 3851dc0

1 file changed

Lines changed: 52 additions & 33 deletions

File tree

content/05_3dMEPFM.md

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,29 @@ import os
1818
from glob import glob
1919
2020
import numpy as np
21-
from book_utils import load_pafin
22-
from nilearn.maskers import NiftiMasker
21+
from nilearn.datasets import fetch_atlas_schaefer_2018
22+
from nilearn.maskers import NiftiLabelsMasker
2323
2424
data_path = os.path.abspath('../data')
2525
```
2626

2727
```{code-cell} ipython3
28-
raise ValueError("SKIP")
29-
data = load_pafin(data_path)
28+
func_dir = os.path.join(data_path, "ds006185/sub-24053/ses-1/func/")
29+
data_files = sorted(
30+
glob(
31+
os.path.join(
32+
func_dir,
33+
"sub-24053_ses-1_task-rat_rec-nordic_dir-PA_run-01_echo-*_part-mag_desc-preproc_bold.nii.gz",
34+
),
35+
),
36+
)
37+
echo_times = []
38+
for f in data_files:
39+
json_file = f.replace('.nii.gz', '.json')
40+
with open(json_file, 'r') as fo:
41+
metadata = json.load(fo)
42+
echo_times.append(metadata['EchoTime'] * 1000)
43+
3044
out_dir = os.path.join(data_path, "pySPFM")
3145
```
3246

@@ -35,68 +49,73 @@ out_dir = os.path.join(data_path, "pySPFM")
3549
3650
from pySPFM import SparseDeconvolution
3751
38-
# Create masker to convert 4D NIfTI data to 2D array
39-
masker = NiftiMasker(mask_img=data['mask'])
40-
41-
# Fit masker once on a representative image (first echo)
42-
masker.fit(data['echo_files'][0])
52+
# Fetch the Schaefer 1000-parcel atlas (MNI152NLin2009cAsym, 2 mm).
53+
# Running pySPFM parcel-wise rather than voxel-wise reduces the problem
54+
# from ~218k voxels to 1000 ROIs, making it feasible on a laptop.
55+
atlas = fetch_atlas_schaefer_2018(n_rois=1000)
56+
57+
# NiftiLabelsMasker averages all voxels within each parcel.
58+
# resampling_target='data' resamples the atlas to the fMRI's native grid.
59+
masker = NiftiLabelsMasker(
60+
labels_img=atlas.maps,
61+
resampling_target='data',
62+
standardize=False,
63+
)
64+
masker.fit(data_files[0])
4365
44-
# Load and mask each echo, then concatenate along the time axis
45-
# For multi-echo data, each echo's data (shape: n_timepoints × n_voxels) are stacked sequentially
66+
# Load each echo and extract parcel-averaged timeseries, then concatenate
67+
# along the time axis to form the multi-echo input matrix.
4668
masked_data = []
47-
for f in data['echo_files']:
48-
echo_data = masker.transform(f) # Shape: (n_timepoints, n_voxels)
69+
for f in data_files:
70+
echo_data = masker.transform(f) # Shape: (n_timepoints, n_parcels)
4971
masked_data.append(echo_data)
5072
51-
X = np.vstack(masked_data) # Shape: (n_echoes * n_timepoints, n_voxels)
73+
X = np.vstack(masked_data) # Shape: (n_echoes * n_timepoints, n_parcels)
5274
5375
# Fit the sparse deconvolution model
5476
model = SparseDeconvolution(
5577
tr=2.47,
56-
te=data['echo_times'],
78+
te=echo_times,
5779
criterion="bic",
58-
n_jobs=-1, # Use all available CPU cores
5980
)
6081
model.fit(X)
6182
6283
# Get the deconvolved activity-inducing signals
63-
# Note: coef_ has shape (n_timepoints, n_voxels) - the model recovers
64-
# the underlying neural activity at the original temporal resolution
84+
# coef_ shape: (n_timepoints, n_parcels)
6585
activity = model.coef_
6686
67-
# Transform back to NIfTI image and save
87+
# Save parcel-wise activity to avoid reconstructing a large 4D voxel-wise NIfTI
6888
os.makedirs(out_dir, exist_ok=True)
69-
activity_img = masker.inverse_transform(activity)
70-
activity_img.to_filename(os.path.join(out_dir, "out_activity.nii.gz"))
89+
np.save(os.path.join(out_dir, "out_activity.npy"), activity)
90+
91+
# Compact summary: mean absolute activity per parcel → inverse_transform to a single-volume NIfTI
92+
activity_mean = np.abs(activity).mean(axis=0, keepdims=True)
93+
activity_mean_img = masker.inverse_transform(activity_mean)
94+
activity_mean_img.to_filename(os.path.join(out_dir, "out_activity_mean.nii.gz"))
7195
72-
# Also save the regularization parameter values
7396
np.save(os.path.join(out_dir, "out_lambda.npy"), model.lambda_)
7497
7598
print(f"Activity shape: {activity.shape}")
76-
print(f"Saved activity to: {os.path.join(out_dir, 'out_activity.nii.gz')}")
99+
print(f"Saved parcel-wise activity to: {os.path.join(out_dir, 'out_activity.npy')}")
100+
print(f"Saved mean activity image to: {os.path.join(out_dir, 'out_activity_mean.nii.gz')}")
77101
```
78102

79103
The `SparseDeconvolution` model provides several useful attributes and methods after fitting:
80104

81-
- `coef_`: The deconvolved activity-inducing signals (shape: n_timepoints × n_voxels)
82-
- `lambda_`: The regularization parameter values
105+
- `coef_`: The deconvolved activity-inducing signals (shape: n_timepoints × n_parcels)
106+
- `lambda_`: The regularization parameter values (one per parcel)
83107
- `hrf_matrix_`: The HRF convolution matrix used
84108
- `get_fitted_signal()`: Returns the fitted (reconstructed) signal; takes no arguments
85109
- `get_residuals(X)`: Returns the residuals between the original data and fitted signal; requires the input data `X` as an argument
86110

87111
```{code-cell} ipython3
88-
# Get the fitted signal and residuals
89-
# Note: These have shape (n_echoes * n_timepoints, n_voxels) matching the input X
90112
fitted_signal = model.get_fitted_signal()
91-
residuals = model.get_residuals(X)
92-
93-
# Save the fitted signal and residuals as numpy arrays
94-
# (shape doesn't match single-echo masker expectations for NIfTI output)
113+
print(f"Fitted signal shape: {fitted_signal.shape}")
95114
np.save(os.path.join(out_dir, "out_fitted.npy"), fitted_signal)
96-
np.save(os.path.join(out_dir, "out_residuals.npy"), residuals)
97115
98-
print(f"Fitted signal shape: {fitted_signal.shape}")
116+
residuals = model.get_residuals(X)
99117
print(f"Residuals shape: {residuals.shape}")
118+
np.save(os.path.join(out_dir, "out_residuals.npy"), residuals)
100119
```
101120

102121
The pySPFM workflow writes out a number of files.

0 commit comments

Comments
 (0)