.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/01_decoding_pipeline/02_geodesic_decoder.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_01_decoding_pipeline_02_geodesic_decoder.py: ============================================================ Geodesic-regularised noise model for trial-wise decoding ============================================================ For Bayesian decoding from an encoding model, the noise covariance :math:`\Omega` between voxels matters as much as the tuning curves themselves. The simplest assumption is uncorrelated isotropic noise (diagonal :math:`\Omega`), but real fMRI noise has substantial spatially-structured correlations — voxels that sit near each other on the cortical sheet share more noise than distant ones. ``braincoder``'s :class:`braincoder.optimize.ResidualFitter` lets you parameterise this directly. With ``D=None`` (default) it fits a model where covariance is driven only by tuning similarity (:math:`\tau\tau^T`) plus an isotropic component. With ``D=`` it adds a distance-modulated component: .. math:: \Omega = \rho\,\bigl[\alpha \cdot (e^{-\beta D} \odot \tau\tau^T) + (1 - \alpha) \cdot \tau\tau^T \bigr] + (1 - \rho)\,\mathrm{diag}(\tau^2) + \sigma^2 W W^T where :math:`D` is voxel-to-voxel distance, and :math:`\alpha,\beta` are learned from data. Plug in **geodesic** distance on the cortical mesh (rather than Euclidean) and you respect the fact that two voxels sitting on opposite banks of a sulcus are close in 3D but functionally far apart. This example fits a numerosity tuning model in right parietal cortex (NPCr), then compares decoding with isotropic-ish vs. geodesic-regularised :math:`\Omega`. .. GENERATED FROM PYTHON SOURCE LINES 36-50 .. code-block:: Python import matplotlib.pyplot as plt import numpy as np import pandas as pd from braincoder.models import LogGaussianPRF from braincoder.optimize import ParameterFitter, ResidualFitter from braincoder.utils.cortex import geodesic_distance_matrix from braincoder.utils.data import load_pratcarrabin2025_npc from braincoder.utils.stats import ( fit_r2_mixture, get_rsq, ) .. GENERATED FROM PYTHON SOURCE LINES 51-57 Load the demo bundle and select responsive voxels ----------------------------------------------------------------- We re-run the same mixture-model selection from example 1 so the notebooks are independent. We then keep only voxels with :math:`P(\\text{signal} \\mid r^2) \\ge 0.8` and restrict to one stimulus range (``wide``) to keep the model simple. .. GENERATED FROM PYTHON SOURCE LINES 57-79 .. code-block:: Python bundle = load_pratcarrabin2025_npc() r2 = bundle['r2'] paradigm_all = bundle['paradigm'] data_all = bundle['data'] r2_wb = bundle['r2_wholebrain'].get_fdata() mask_wb = bundle['brain_mask'].get_fdata().astype(bool) fit = fit_r2_mixture(r2_wb[mask_wb]) # Noise μ + 2σ on the logit scale — see example 1 for the rationale. threshold = 1.0 / (1.0 + np.exp(-(fit['noise_mu'] + 2 * fit['noise_sigma']))) keep = r2.index[r2 > threshold] print(f'Kept {len(keep)}/{len(r2)} voxels (within-sample R² ≥ {threshold:.3f})') is_wide = paradigm_all['range'].values == 'wide' paradigm = paradigm_all.loc[is_wide].reset_index(drop=True) data = data_all.loc[is_wide, keep].reset_index(drop=True) data = data.astype(np.float32) # Per-voxel z-score within the kept set so the model only sees signal scale. data = (data - data.mean(axis=0)) / data.std(axis=0).replace(0, 1) print(f'Data: {data.shape} (trials × voxels)') .. rst-class:: sphx-glr-script-out .. code-block:: none Kept 155/852 voxels (within-sample R² ≥ 0.085) Data: (240, 155) (trials × voxels) .. GENERATED FROM PYTHON SOURCE LINES 80-84 Fit a Log-Gaussian PRF per voxel ----------------------------------------------------------------- The encoding model has 4 parameters per voxel — mu (preferred numerosity), sd (tuning width), amplitude, baseline. .. GENERATED FROM PYTHON SOURCE LINES 84-119 .. code-block:: Python stim = paradigm[['n']].astype(np.float32).values model = LogGaussianPRF(parameterisation='mu_sd_natural') n_train = int(0.75 * len(stim)) train_idx = np.arange(n_train) test_idx = np.arange(n_train, len(stim)) fitter = ParameterFitter(model, data.iloc[train_idx], paradigm[['n']].iloc[train_idx]) init = pd.DataFrame({ 'mu': np.full(data.shape[1], 25.0), 'sd': np.full(data.shape[1], 10.0), 'amplitude': np.full(data.shape[1], 1.0), 'baseline': np.full(data.shape[1], 0.0), }, index=data.columns, dtype=np.float32) # Coarse grid-init helps the optimiser find a decent starting mu. mu_grid = np.linspace(10, 40, 16, dtype=np.float32) sd_grid = np.array([5., 10., 20.], dtype=np.float32) amp_grid = np.array([0.5, 1.0, 2.0], dtype=np.float32) base_grid = np.array([0.0], dtype=np.float32) init = fitter.fit_grid(mu_grid, sd_grid, amp_grid, base_grid) pars = fitter.fit(max_n_iterations=600, init_pars=init, noise_model='gaussian', learning_rate=0.05, progressbar=False) train_pred = model.predict(paradigm=paradigm[['n']].iloc[train_idx], parameters=pars) train_pred.index = data.iloc[train_idx].index train_r2 = get_rsq(data.iloc[train_idx], train_pred) print(f'Train R² — median {np.nanmedian(train_r2):.3f}, ' f'p90 {np.nanpercentile(train_r2, 90):.3f}') .. rst-class:: sphx-glr-script-out .. code-block:: none Working with chunk size of 23894 0%| | 0/1 [00:00` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: 02_geodesic_decoder.py <02_geodesic_decoder.py>` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: 02_geodesic_decoder.zip <02_geodesic_decoder.zip>` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_