@@ -18,15 +18,29 @@ import os
1818from glob import glob
1919
2020import 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
2424data_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+
3044out_dir = os.path.join(data_path, "pySPFM")
3145```
3246
@@ -35,68 +49,73 @@ out_dir = os.path.join(data_path, "pySPFM")
3549
3650from 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.
4668masked_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
5476model = 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)
6081model.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)
6585activity = 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
6888os.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
7396np.save(os.path.join(out_dir, "out_lambda.npy"), model.lambda_)
7497
7598print(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
79103The ` 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
90112fitted_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}")
95114np.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 )
99117print(f"Residuals shape: {residuals.shape}")
118+ np.save(os.path.join(out_dir, "out_residuals.npy"), residuals)
100119```
101120
102121The pySPFM workflow writes out a number of files.
0 commit comments