FastICA on a single fUSI recording¶
This example shows how to use FastICA to decompose a fUSI recording into independent components.
PCA decomposes fUSI data into spatial maps and time courses by finding orthogonal axes that capture dominant covariance/correlation structure. The resulting components are uncorrelated but not necessarily independent: they may still share higher-order statistical structure.
ICA uses a stronger statistical objective. Instead of diagonalising covariance, it searches for components that are as statistically independent as possible, using higher-order structure beyond variance and correlation1. The interpretation of an ICA component depends on the orientation of the data:
- Temporal ICA (
mode="temporal"): the independent components are time courses and the spatial maps are their voxel-wise mixing weights. - Spatial ICA (
mode="spatial"): the independent components are spatial maps and the corresponding time courses are their time-wise mixing weights.
We start with temporal ICA because its orientation is directly comparable to the usual PCA decomposition, then contrast it with spatial ICA.
Historically, temporal ICA was less used in resting-state fMRI because acquisitions had relatively few time points compared with the number of voxels. Spatial ICA was then better conditioned and became the conventional choice. With fUSI (and accelerated fMRI), temporal sampling is richer, so both temporal and spatial ICA are practical and provide complementary information.
Load a fUSI recording¶
To demonstrate the use of ICA on fUSI data, we use the same spontaneous activity recording from the Nunez-Elizalde 2022 dataset as in the PCA example. See the Datasets user guide for more details on how to download this dataset using ConfUSIus.
from pathlib import Path
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import xarray as xr
import confusius as cf
from confusius.datasets import fetch_nunez_elizalde_2022
from confusius.decomposition import FastICA
from confusius.signal import standardize
# Adapt background color to the current Matplotlib style.
bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"])
# Keep notebook output compact for large DataArray displays.
xr.set_options(display_expand_data=False)
bids_root = fetch_nunez_elizalde_2022(
subjects="CR022",
sessions="20201011",
tasks="spontaneous",
acqs="slice03",
)
pwd_path = (
Path(bids_root)
/ "sub-CR022"
/ "ses-20201011"
/ "fusi"
/ "sub-CR022_ses-20201011_task-spontaneous_acq-slice03_pwd.nii.gz"
)
data = cf.load(pwd_path).compute()
data
- time: 751
- z: 1
- y: 114
- x: 80
- 4.464e+03 5.588e+03 5.064e+03 ... 2.977e+04 2.558e+04 2.353e+04
array([[[[ 4463.8 , 5588.404 , 5063.7925, ..., 6008.3438, 5016.807 , 5296.5776], [ 5369.187 , 5347.434 , 5393.3667, ..., 5593.255 , 5330.843 , 6088.181 ], [ 5835.804 , 5867.3096, 6175.863 , ..., 5321.1245, 5456.6743, 4612.9253], ..., [19816.035 , 20495.758 , 17393.748 , ..., 27059.963 , 26307.736 , 23214.475 ], [17802.514 , 17888.11 , 17680.232 , ..., 31637.012 , 29263.018 , 21627.123 ], [21509.184 , 18807.508 , 20343.31 , ..., 32594.61 , 30050.383 , 24083.986 ]]], [[[ 5407.5635, 5091.3843, 4347.751 , ..., 5399.663 , 6108.856 , 5394.4478], [ 4701.354 , 5039.4854, 5073.55 , ..., 5462.995 , 5485.5127, 5088.853 ], [ 6242.7456, 6078.8955, 5453.0264, ..., 5303.4873, ... [18455.709 , 19690.021 , 22065.379 , ..., 29279.365 , 24760.768 , 18865.059 ], [19500.25 , 20546.143 , 19018.531 , ..., 41545.703 , 27996.793 , 21179.195 ]]], [[[ 4493.446 , 5749.9062, 4908.0405, ..., 5116.4893, 5014.6816, 5606.2275], [ 4805.8306, 5201.5073, 6156.0605, ..., 6496.692 , 5214.4478, 6020.7847], [ 4523.9756, 5825.632 , 5338.9243, ..., 4959.6353, 5864.1226, 6266.524 ], ..., [18610.822 , 20753.242 , 18185.65 , ..., 24913.578 , 24254.578 , 22802.502 ], [17924.197 , 21776.996 , 19610.148 , ..., 29756.256 , 21754.053 , 21667.215 ], [22383.992 , 21130.988 , 21290.71 , ..., 29774.988 , 25576.12 , 23529.764 ]]]], shape=(751, 1, 114, 80), dtype=float32) - time(time)float6410.61 10.91 11.21 ... 235.4 235.7
- units :
- s
- volume_acquisition_reference :
- start
- volume_acquisition_duration :
- 0.3
array([ 10.608, 10.908, 11.208, ..., 235.095, 235.395, 235.695], shape=(751,))
- z(z)float641.0
- units :
- mm
- voxdim :
- 0.4000000059604645
array([1.])
- y(y)float642.73 2.778 2.827 ... 8.142 8.19
- units :
- mm
- voxdim :
- 0.04831999912858009
array([2.73008, 2.7784 , 2.82672, 2.87504, 2.92336, 2.97168, 3.02 , 3.06832, 3.11664, 3.16496, 3.21328, 3.2616 , 3.30992, 3.35824, 3.40656, 3.45488, 3.5032 , 3.55152, 3.59984, 3.64816, 3.69648, 3.7448 , 3.79312, 3.84144, 3.88976, 3.93808, 3.9864 , 4.03472, 4.08304, 4.13136, 4.17968, 4.228 , 4.27632, 4.32464, 4.37296, 4.42128, 4.4696 , 4.51792, 4.56624, 4.61456, 4.66288, 4.7112 , 4.75952, 4.80784, 4.85616, 4.90448, 4.9528 , 5.00112, 5.04944, 5.09776, 5.14608, 5.1944 , 5.24272, 5.29104, 5.33936, 5.38768, 5.436 , 5.48432, 5.53264, 5.58096, 5.62928, 5.6776 , 5.72592, 5.77424, 5.82256, 5.87088, 5.9192 , 5.96752, 6.01584, 6.06416, 6.11248, 6.1608 , 6.20912, 6.25744, 6.30576, 6.35408, 6.4024 , 6.45072, 6.49904, 6.54736, 6.59568, 6.644 , 6.69232, 6.74064, 6.78896, 6.83728, 6.8856 , 6.93392, 6.98224, 7.03056, 7.07888, 7.1272 , 7.17552, 7.22384, 7.27216, 7.32048, 7.3688 , 7.41712, 7.46544, 7.51376, 7.56208, 7.6104 , 7.65872, 7.70704, 7.75536, 7.80368, 7.852 , 7.90032, 7.94864, 7.99696, 8.04528, 8.0936 , 8.14192, 8.19024]) - x(x)float64-3.95 -3.85 -3.75 ... 3.85 3.95
- units :
- mm
- voxdim :
- 0.10000000149011612
array([-3.95, -3.85, -3.75, -3.65, -3.55, -3.45, -3.35, -3.25, -3.15, -3.05, -2.95, -2.85, -2.75, -2.65, -2.55, -2.45, -2.35, -2.25, -2.15, -2.05, -1.95, -1.85, -1.75, -1.65, -1.55, -1.45, -1.35, -1.25, -1.15, -1.05, -0.95, -0.85, -0.75, -0.65, -0.55, -0.45, -0.35, -0.25, -0.15, -0.05, 0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25, 1.35, 1.45, 1.55, 1.65, 1.75, 1.85, 1.95, 2.05, 2.15, 2.25, 2.35, 2.45, 2.55, 2.65, 2.75, 2.85, 2.95, 3.05, 3.15, 3.25, 3.35, 3.45, 3.55, 3.65, 3.75, 3.85, 3.95])
- qform_code :
- 1
- manufacturer :
- Verasonics
- manufacturers_model_name :
- Vantage 128
- software_version :
- Alan Urban Technology & Consulting (AUTC)
- probe_manufacturer :
- Vermon
- probe_type :
- linear
- probe_model :
- L22-XTech
- probe_central_frequency :
- 15000000.0
- probe_number_of_elements :
- 128
- probe_pitch :
- 0.1
- probe_focal_width :
- 0.4
- probe_focal_depth :
- 8.0
- power_doppler_integration_duration :
- 0.3
- power_doppler_integration_stride :
- 0.3
- clutter_filter_window_duration :
- 0.4
- clutter_filter_window_stride :
- 0.3
- clutter_filters :
- ['highpass:15Hz', 'svd:remove_first_15_components']
- task_name :
- spontaneous
- task_description :
- Spontaneous activity without explicit visual stimulation.
- depth :
- [0.0, 5.46016]
- transmit_frequency :
- 15625000.0
- compound_sampling_frequency :
- 500.0
- plane_wave_angles :
- [-10.0, -7.9, -5.8, -3.6999999999999993, -1.5999999999999996, 0.5000000000000018, 2.6000000000000014, 4.700000000000002, 6.8000000000000025, 8.900000000000002]
- probe_voltage :
- 25.0
- affines :
- {'physical_to_qform': array([[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]])}
Correct for brain motion¶
This recording contains some brain motion, which we can mitigate by performing a rigid
translation correction with
register_volumewise. This is the same
preprocessing step used in the PCA
example, and it helps
avoid components dominated by motion artefacts.
/home/runner/work/confusius/confusius/.venv/lib/python3.14/site-packages/rich/live.py:260: UserWarning: install
"ipywidgets" for Jupyter support
warnings.warn('install "ipywidgets" for Jupyter support')
Temporal ICA¶
Temporal ICA treats time courses as signals and voxels as instances. It is useful for separating components that are temporally independent but may overlap in space, and can help separate low-frequency physiological fluctuations.
Before fitting, we standardize the recording by centering and scaling each voxel's
time series to zero mean and unit variance with
standardize, for the same reasons as in the
PCA example.
With mode="temporal", FastICA operates on the
same (time, voxels) orientation as PCA. The
algorithm finds n_components time courses that are as statistically independent and
non-Gaussian as possible. The maps_ attribute
stores the corresponding spatial mixing weights (the voxel-space directions along
which each independent time course has its strongest influence).
ica_t = FastICA(
n_components=10,
mode="temporal",
random_state=42,
fun="cube",
max_iter=500,
)
signals_t = ica_t.fit_transform(data_std)
signals_t
- time: 751
- component: 10
- 1.705 0.4722 0.6067 0.5974 0.3297 ... 0.2544 1.129 0.08552 -0.1393
array([[ 1.7053471 , 0.47218508, 0.60667574, ..., 0.9067501 , 0.8413501 , -1.3073945 ], [ 3.3142476 , 1.4946027 , 0.289191 , ..., 0.22860953, 0.29316697, 0.7450524 ], [ 3.6038916 , 0.4376343 , 0.08941008, ..., -0.78943026, 0.82355165, 0.8383995 ], ..., [ 0.19005749, 0.06548941, 1.6409812 , ..., 0.31566784, 1.4230514 , 0.11540988], [ 0.18706222, 1.7543579 , 2.2873359 , ..., 1.463107 , 0.6754403 , 0.90918803], [ 0.6318028 , 0.74639934, 1.4053494 , ..., 1.1292399 , 0.08552428, -0.13928768]], shape=(751, 10), dtype=float32) - time(time)float6410.61 10.91 11.21 ... 235.4 235.7
- units :
- s
- volume_acquisition_reference :
- start
- volume_acquisition_duration :
- 0.3
array([ 10.608, 10.908, 11.208, ..., 235.095, 235.395, 235.695], shape=(751,))
- component(component)int640 1 2 3 4 5 6 7 8 9
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
- long_name :
- FastICA signals
Plotting the spatial mixing weights alongside the independent time courses gives a first sense of what each component captures.
n_show = 10
fig = plt.figure(figsize=(14, 20), constrained_layout=True)
fig.patch.set_facecolor(bg_color)
gs = fig.add_gridspec(n_show, 2, width_ratios=[1, 3])
axes_tc = [fig.add_subplot(gs[i, 1]) for i in range(n_show)]
for ax in axes_tc[1:]:
ax.sharex(axes_tc[0])
for i, comp in enumerate(range(n_show)):
component_map = ica_t.maps_.isel(component=[comp])
vmax = float(np.abs(component_map).max())
cf.plotting.plot_volume(
component_map,
axes=fig.add_subplot(gs[i, 0]),
slice_mode="component",
cmap="coolwarm",
vmin=-vmax,
vmax=vmax,
show_axes=False,
show_colorbar=False,
show_titles=False,
bg_color=bg_color,
)
signals_t.sel(component=comp).plot(ax=axes_tc[i], lw=1.1)
axes_tc[i].set_title(f"IC {comp + 1}")
axes_tc[i].set_ylabel("Signal")
axes_tc[i].set_xlabel("")
for ax in axes_tc[:-1]:
ax.tick_params(labelbottom=False)
axes_tc[-1].set_xlabel("Time (s)")
_ = fig.suptitle(
"Temporal ICA: mixing weights and time courses (first 10 components)", fontsize=21
)
Spatial ICA¶
Spatial ICA (mode="spatial", the default) treats spatial voxels as signals and time
points as instances by transposing data to (voxels, time) before fitting. It is the
conventional resting-state choice because the spatial dimension is usually much larger
than the temporal one, making decomposition better conditioned. In practice, spatial ICA
is effective at identifying spatially localized fluctuations and often better at
reducing whole-brain motion-related structure.
ica_s = FastICA(
n_components=10, mode="spatial", random_state=42, fun="cube", max_iter=500
)
signals_s = ica_s.fit_transform(data_std)
signals_s
- time: 751
- component: 10
- -520.3 3.505e+03 1.6e+03 -779.5 ... 1.028e+03 2.523e+03 -1.685e+03
array([[ -520.2989 , 3504.755 , 1599.7559 , ..., 669.60266, 1261.5713 , -1242.1854 ], [ 1308.4633 , 2354.9707 , 317.7082 , ..., -9.72171, 1024.7684 , 1084.4003 ], [ 343.88522, 2318.199 , 617.4685 , ..., 272.92017, 1235.4524 , 1638.7083 ], ..., [-1046.7009 , 2457.6367 , 3385.7192 , ..., 949.4414 , 1884.1799 , -1266.8386 ], [ 466.82806, 2348.8738 , 2849.441 , ..., 690.0863 , 2512.9348 , -1817.8994 ], [ -452.74954, 2462.4834 , 1238.6765 , ..., 1027.5919 , 2522.82 , -1685.204 ]], shape=(751, 10), dtype=float32) - time(time)float6410.61 10.91 11.21 ... 235.4 235.7
- units :
- s
- volume_acquisition_reference :
- start
- volume_acquisition_duration :
- 0.3
array([ 10.608, 10.908, 11.208, ..., 235.095, 235.395, 235.695], shape=(751,))
- component(component)int640 1 2 3 4 5 6 7 8 9
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
- long_name :
- FastICA signals
Spatial maps and time courses¶
maps_ is a (component, y, x) DataArray whose
rows are the independent spatial patterns themselves, in contrast to temporal ICA where
maps_ stores mixing weights. Comparing each map with its time course helps judge
whether a component reflects a plausible functional or vascular source, or an artefact
such as motion or physiological noise.
n_show = 10
fig = plt.figure(figsize=(14, 20), constrained_layout=True)
fig.patch.set_facecolor(bg_color)
gs = fig.add_gridspec(n_show, 2, width_ratios=[1, 3])
axes_tc = [fig.add_subplot(gs[i, 1]) for i in range(n_show)]
for ax in axes_tc[1:]:
ax.sharex(axes_tc[0])
for i, comp in enumerate(range(n_show)):
component_map = ica_s.maps_.isel(component=[comp])
vmax = float(np.abs(component_map).max())
cf.plotting.plot_volume(
component_map,
axes=fig.add_subplot(gs[i, 0]),
slice_mode="component",
cmap="coolwarm",
vmin=-vmax,
vmax=vmax,
show_axes=False,
show_colorbar=False,
show_titles=False,
bg_color=bg_color,
)
signals_s.sel(component=comp).plot(ax=axes_tc[i], lw=1.1)
axes_tc[i].set_title(f"IC {comp + 1}")
axes_tc[i].set_ylabel("Signal")
axes_tc[i].set_xlabel("")
for ax in axes_tc[:-1]:
ax.tick_params(labelbottom=False)
axes_tc[-1].set_xlabel("Time (s)")
_ = fig.suptitle("Spatial ICA: maps and time courses (first 10 components)", fontsize=21)
Total running time: 229.0 s
Launch in Binder Download .py Download .ipynb
-
Hyvärinen, A., and Oja, E. (2000). "Independent component analysis: algorithms and applications". Neural Networks, 13(4-5), 411-430. ↩



