Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 62 additions & 7 deletions content/05_3dMEPFM.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import json
import os
from glob import glob

import nibabel as nb
import numpy as np
from nilearn.maskers import NiftiMasker

data_path = os.path.abspath('../DATA')
```
Expand Down Expand Up @@ -53,16 +54,70 @@ out_dir = os.path.join(data_path, "pySPFM")
```{code-cell} ipython3
:tags: [output_scroll]

from pySPFM import pySPFM
from pySPFM import SparseDeconvolution

pySPFM.pySPFM(
data_fn=data_files,
mask_fn=mask_file,
output_filename=os.path.join(out_dir, "out"),
# Create masker to convert 4D NIfTI data to 2D array
masker = NiftiMasker(mask_img=mask_file)

# Fit masker once on a representative image (first echo)
masker.fit(data_files[0])

# Load and mask each echo, then concatenate along the time axis
# For multi-echo data, each echo's data (shape: n_timepoints × n_voxels) are stacked sequentially
masked_data = []
for f in data_files:
echo_data = masker.transform(f) # Shape: (n_timepoints, n_voxels)
masked_data.append(echo_data)

X = np.vstack(masked_data) # Shape: (n_echoes * n_timepoints, n_voxels)

# Fit the sparse deconvolution model
model = SparseDeconvolution(
tr=2.47,
out_dir=out_dir,
te=echo_times,
criterion="bic",
n_jobs=-1, # Use all available CPU cores
)
model.fit(X)

# Get the deconvolved activity-inducing signals
# Note: coef_ has shape (n_timepoints, n_voxels) - the model recovers
# the underlying neural activity at the original temporal resolution
activity = model.coef_

# Transform back to NIfTI image and save
os.makedirs(out_dir, exist_ok=True)
activity_img = masker.inverse_transform(activity)
activity_img.to_filename(os.path.join(out_dir, "out_activity.nii.gz"))

# Also save the regularization parameter values
np.save(os.path.join(out_dir, "out_lambda.npy"), model.lambda_)

print(f"Activity shape: {activity.shape}")
print(f"Saved activity to: {os.path.join(out_dir, 'out_activity.nii.gz')}")
```

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

- `coef_`: The deconvolved activity-inducing signals (shape: n_timepoints × n_voxels)
- `lambda_`: The regularization parameter values
- `hrf_matrix_`: The HRF convolution matrix used
- `get_fitted_signal()`: Returns the fitted (reconstructed) signal; takes no arguments
- `get_residuals(X)`: Returns the residuals between the original data and fitted signal; requires the input data `X` as an argument

```{code-cell} ipython3
# Get the fitted signal and residuals
# Note: These have shape (n_echoes * n_timepoints, n_voxels) matching the input X
fitted_signal = model.get_fitted_signal()
residuals = model.get_residuals(X)

# Save the fitted signal and residuals as numpy arrays
# (shape doesn't match single-echo masker expectations for NIfTI output)
np.save(os.path.join(out_dir, "out_fitted.npy"), fitted_signal)
np.save(os.path.join(out_dir, "out_residuals.npy"), residuals)

print(f"Fitted signal shape: {fitted_signal.shape}")
print(f"Residuals shape: {residuals.shape}")
```

The pySPFM workflow writes out a number of files.
Expand Down