# ConfUSIus > Python package for analysis and visualization of functional ultrasound imaging data. This file describes ConfUSIus version 0.9.0.dev0. ConfUSIus is a Python library built on xarray for loading, registering, extracting signals from, and visualizing functional ultrasound imaging (fUSI) data. Every spatial array is a VoxelData `xarray.DataArray` with dims `(..., time, pose, k, j, i)` and a voxel-to-world affine index. Install with `pip install confusius`. The package is in beta and the API may change between releases. # User guide # Getting Started Welcome to ConfUSIus! This guide introduces the package and helps you understand how it fits into a typical fUSI data analysis workflow. ## What is fUSI? Functional ultrasound imaging (fUSI) is a neuroimaging technique that measures brain activity by detecting changes in cerebral blood volume[^/dev/user-guide/getting-started/#mace2011] [^/dev/user-guide/getting-started/#deffieux2021] [^/dev/user-guide/getting-started/#montaldo2022]. fUSI offers: - **High spatiotemporal resolution**: Sub-millimeter spatial resolution (\~100 μm in-plane and \~500 μm out-of-plane with 15 MHz linear probes) with temporal resolution on the order of 0.5–10 Hz, depending on the acquisition protocol. - **Wide field of view**: Ability to image large cortical and subcortical regions simultaneously. - **Awake preclinical imaging**: Compatible with awake, behaving animals and various experimental paradigms. - **Multimodal integration**: Can be combined with electrophysiology, optogenetics, and other techniques for comprehensive studies of brain function. For more information on fUSI, check out this review article: [Functional Ultrasound Neuroimaging]() by Montaldo *et al.* (2022). fUSI data typically consists of large 3D+t datasets acquired from fUSI systems (for example, [Iconeus](), [AUTC](), EchoFrame, or in-house setups based on ultrasound research platforms such as [Verasonics]()). ## What is ConfUSIus? ConfUSIus is a Python package for handling, processing, and analyzing functional ultrasound imaging (fUSI) data. It provides tools for the entire fUSI analysis pipeline, from converting raw beamformed IQ data to processing hemodynamic signals and performing statistical analyses. It also ships a [napari]() plugin for interactive data exploration. > [!TIP] > > **Prefer not to script?** > > The [ConfUSIus napari plugin]() is a good starting point if you want to explore a dataset before writing any analysis code. It covers data loading and exploration, time series inspection, and quality control—all without scripting. ## Key Features ConfUSIus provides a comprehensive toolkit designed specifically for fUSI data: - Convert AUTC and EchoFrame beamformed IQ data to Zarr for efficient processing. - Load Zarr, NIfTI, and Iconeus SCAN files as labeled VoxelData arrays. - Load and save NIfTI files with automatic fUSI-BIDS JSON sidecar handling. - Save DataArrays to Zarr for chunked, large-scale processing. - SVD-based clutter filtering to separate blood from tissue signals. - Compute power Doppler, B-mode, and axial velocity images from filtered IQ data. - A general-purpose block-processing primitive lets you apply any custom function to IQ blocks efficiently using Dask sliding windows. - Temporal metric: DVARS for detecting outlier volumes. - Spatial metrics: coefficient of variation (CV) for mapping high-variance regions, and tSNR for compatibility with fMRI-style workflows (with caveats for power Doppler). - Carpet plots (grayplots) to spot global disturbances, motion spikes, and scanner drift. - Frame-by-frame motion correction with rigid or affine transforms; framewise displacement and motion parameter extraction for downstream scrubbing. - Volume registration for inter-session alignment or atlas co-registration (rigid, affine, or deformable B-spline transforms). - **Napari plugin**: Graphical interface for data loading and exploration, time series inspection, and quality control—no scripting required. - **Programmatic napari**: Interactive 3D+t exploration from scripts or notebooks. - **Matplotlib**: Static figure generation for publications and reports. - Extract voxel-wise or region-averaged signals using binary masks or label maps. - Clean time series in one step: confound regression (including CompCor), Butterworth filtering (low-, high-, or band-pass), detrending, and standardization. - Censor or interpolate motion-contaminated samples identified during QC. - Load any BrainGlobe-compatible atlas (Allen CCFv3, Waxholm, and more). - Generate region masks and label maps for targeted signal extraction. - Resample atlases to match your acquisition grid; overlay region contours on fUSI images. - Seed-based correlation maps and region-to-region functional connectivity matrices. - General linear models (GLMs) for task-based and event-related designs. - Interoperability with Nilearn, scikit-learn, statsmodels, and other scientific Python libraries. ## Typical Workflow ConfUSIus supports the complete fUSI analysis pipeline, from raw data acquisition to statistical analysis and visualization: ``` flowchart TB subgraph acquire["Data Acquisition"] A[fUSI system] end acquire --> convert subgraph convert["Format Conversion"] B["Source formats"] C[Xarray] B --> C end convert --> process subgraph process["Process Beamformed IQs"] D[Beamformed IQs] E[Power Doppler, velocity, custom measures] D --> E end process --> qc process --> visualization process --> registration subgraph qc["Quality Control"] qcmeasures[Global signal, DVARS, axial tissue velocity] end subgraph visualization["Visualization"] plotting[napari, Matplotlib] end subgraph registration["Registration"] motioncorr[Motion correction, template alignment] end qc --> signal visualization --> signal registration --> signal subgraph signal["Signal Processing"] extract[Signals extraction] denoising[Denoising] extract --> denoising end signal --> analyze analyze["Analysis & Visualization"] ``` ### Workflow Stages 1. **Data Acquisition**: Acquire beamformed IQ data from your ultrasound system (AUTC, EchoFrame, Iconeus, or custom setups). 2. **Format Conversion**: Convert proprietary formats to Xarray-compatible formats (typically Zarr for large datasets, or NIfTI for BIDS compliance). 3. **Process Beamformed IQs**: Apply clutter filtering and extract hemodynamic signals such as power Doppler, velocity, or custom measures from beamformed IQ data. 4. **Quality Control, Visualization & Registration**: These steps can be performed in parallel as needed: - Monitor data quality using metrics like global signal, DVARS, or axial tissue velocity. - Explore data interactively with napari or create static figures with Matplotlib. - Perform motion correction or align acquisitions to a template. 5. **Signal Processing**: Extract signals from specific brain regions and apply denoising and filtering. 6. **Analysis**: Perform statistical analyses or integrate with other Python scientific libraries. > [!TIP] > > **Entering ConfUSIus at any stage** > > You don't need to start from beamformed IQ data. If you already have processed acquisitions (e.g., power Doppler in NIfTI format), you can enter the pipeline at any stage—quality control, registration, signal processing, or analysis. ## When to Use ConfUSIus ConfUSIus is designed for anyone working with fUSI data: - **Comprehensive fUSI toolkit**: ConfUSIus covers the entire analysis pipeline in a single, actively maintained open-source package—from IQ processing and quality control to atlas integration, signal extraction, and connectivity analysis. - **Accessible at every skill level**: Non-programmers can use the [napari plugin]() for interactive data loading, time series inspection, and quality control. Python users can build fully custom pipelines using the scripting API. - **Python ecosystem integration**: Works seamlessly with NumPy, Xarray, Dask, Nilearn, scikit-learn, and other scientific Python libraries. - **Standardized workflows**: Follow fUSI-BIDS conventions for reproducible, shareable research. - **Scalable to large datasets**: Process large beamformed IQ datasets out-of-core using Dask and Zarr without running out of memory. ## Before You Begin ConfUSIus is built on several key technologies: - **[Xarray]()**: Labeled multi-dimensional arrays with metadata. - **[Zarr]()**: Chunked, compressed array storage for large datasets. - **[Dask]()**: Parallel computing and out-of-core processing. If you're new to these tools, check out the [Working with Xarray]() section to get familiar with the core concepts. You may also want to review the [Input/Output Guide]() to understand how ConfUSIus handles data formats. ## Next Steps Ready to get started? 1. **[Install ConfUSIus]()**: Set up your environment and install the package. 2. **[I/O Guide]()**: Learn how to load and convert your fUSI data. 3. **[Graphical Interface]()**: Explore your data interactively with the napari plugin—no scripting required. 4. **[Examples]()**: See complete workflows for common tasks. 5. **[API Reference]()**: Explore detailed function documentation. Still have questions? Check the [GitHub issues]() or open a new one! You can also join the [fUSI community Discord]() to ask questions and connect with other researchers using fUSI. ## Getting Help We host weekly drop-in hours on the [fUSI community Discord]() usually every **Thursday from 3pm to 4pm UTC**. Join us in the `drop-in-hours` voice channel to: - Get help with ConfUSIus usage and troubleshooting. - Discuss new features and provide feedback. - Connect with other fUSI researchers. - Report bugs in an informal setting. You can also use the `help-desk` text channel anytime for questions. We post on [Bluesky]() in advance to confirm each week's session. ## Useful Python Resources for fUSI While ConfUSIus offers many features to handle fUSI data, it obviously does not do everything. Below are some useful resources to read and packages to use alongside ConfUSIus. - [Xarray](): ConfUSIus relies on Xarray for storing volumetric fUSI data. Learning how to use Xarray efficiently will get you a long way. - [Dask](): Dask is a Python package for distributed parallel computing. Xarray supports Dask arrays as backend, and ConfUSIus relies on them notably for processing large beamformed IQ datasets. Xarray's user guide on [parallel computing with Dask]() might also be a good read. - [Zarr](): Zarr is a chunked, compressed array storage for large N-dimensional datasets. ConfUSIus relies on Zarr for storing beamformed IQ data, and large derived measures. Zarr's user guide on [optimizing performance]() is a useful resource if you intend to work with very large beamformed IQ datasets. - [Brain Imaging Data Structure](): ConfUSIus uses BIDS conventions when reading and writing JSON sidecars alongside NIfTI files. Even if you don't use NIfTI as your storage format, following BIDS conventions can help you organize and share your datasets later. You may also want to take a look at the [fUSI-BIDS specification draft]() and at [PyBIDS](). - [Nilearn](): Nilearn is a versatile package for analysis of brain volumes and surfaces. While it is mostly geared toward fMRI, many fUSI users have found it useful for analyzing fUSI data. ConfUSIus itself borrows some of Nilearn's features. - [SimpleITK](): SimpleITK offers a simple wrapper API over ITK, the widely used image segmentation and image registration library. ConfUSIus builds on SimpleITK for image registration. - [BrainGlobe](): BrainGlobe is a suite of Python-based software tools for computational neuroanatomy. Of particular interest is the [BrainGlobe Atlas API](), which provides a common interface to download and process brain atlases from multiple sources. ConfUSIus builds on the BrainGlobe Atlas API for atlas integration. - [Matplotlib](): The obvious choice for creating static figures in Python. - [PyVista](): While ConfUSIus uses napari for 3D rendering, PyVista also offers a simple API to make custom 3D visualizations. [^/dev/user-guide/getting-started/#mace2011]: Macé, Emilie, et al. “Functional Ultrasound Imaging of the Brain.” Nature Methods, vol. 8, no. 8, Aug. 2011, pp. 662–64. DOI.org (Crossref), [https://doi.org/10.1038/nmeth.1641](). [^/dev/user-guide/getting-started/#deffieux2021]: Deffieux, Thomas, et al. “Functional Ultrasound Imaging: A New Imaging Modality for Neuroscience.” Neuroscience, vol. 474, Oct. 2021, pp. 110–21. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroscience.2021.03.005](). [^/dev/user-guide/getting-started/#montaldo2022]: Montaldo, Gabriel, et al. “Functional Ultrasound Neuroimaging.” Annual Review of Neuroscience, vol. 45, no. 1, July 2022, pp. 491–513. DOI.org (Crossref), [https://doi.org/10.1146/annurev-neuro-111020-100706](). # Installation ConfUSIus is compatible with Python 3.13 and above. We recommend using [uv]() for fast and efficient package management, but you can also install ConfUSIus using the Python package manager of your choice. ## Install with uv (recommended) ```bash # Create a new project. uv init my-project cd my-project # Add confusius. uv add confusius ``` ## Development Installation Please find all development installation instructions on the [Community page](). ## Verify Installation To verify that ConfUSIus has been installed correctly, you may run the following code snippet in a Python environment: ```pycon >>> import confusius as cf >>> print(cf.__version__) '0.9.0.dev0' ``` If the installation was successful, this code will print the installed version of ConfUSIus without any errors. If you encounter an error, please open an issue on the [GitHub repository](). # Input/Output ## Overview ConfUSIus is designed to handle large-scale fUSI datasets efficiently. This guide explains how to load supported formats directly in ConfUSIus, choose appropriate storage formats for your workflow, and convert beamformed IQ data when needed. ## Working with Xarray ConfUSIus uses [Xarray]() as its core data structure for representing multi-dimensional fUSI data. Xarray provides several advantages over raw NumPy arrays: - **Named dimensions**: Access data using meaningful names (e.g., `i`, `j`, `k`, `pose`, `time`) instead of remembering axis indices. - **Coordinates**: Associate time and world coordinates with the native dimensions (e.g., `x`, `y`, and `z` in millimeters). - **Metadata storage**: Keep acquisition parameters, units, and other metadata alongside your data. - **Unified API**: Use the same operations regardless of the underlying storage format. Every loader in this guide returns **VoxelData arrays**: DataArrays with `(k, j, i)` voxel dimensions backed by a `VoxelToWorldIndex`, which derives world coordinates `(z, y, x)` from voxel-to-world affine transformations. See [The VoxelData Model]() for details on the coordinate model. ### Xarray-Compatible Formats Xarray can read and write data from multiple storage formats, including: - **[Zarr]()**: Chunked, compressed, cloud-native format (recommended for large datasets). - **[HDF5]()**: Hierarchical format widely used in research. - **[netCDF]()**: Self-describing format common in scientific computing (e.g., basis of the [MINC]() 1.0 format). Additionally, ConfUSIus provides utilities to read and write [**NIfTI**]() files (the standard neuroimaging format for BIDS) as VoxelData arrays, automatically reading and writing matching fUSI-BIDS JSON sidecars when present. ### Recommended Formats for fUSI ConfUSIus works with all Xarray-compatible formats, but two formats are particularly well-suited for fUSI workflows: [NIfTI]() is the standard format for: - **fUSI-BIDS compliance**: Required for sharing datasets following the BIDS specification. - **Neuroimaging pipelines**: Compatible with tools like [Nilearn](), [ANTsPy](), and [FSL](), which have been used in some fUSI studies. - **Regular recordings**: Power Doppler, velocity, and other processed signals. Use NIfTI when you need to share data, ensure BIDS compliance, or integrate with existing neuroimaging analysis tools. [Zarr]() excels at handling massive datasets through: - **Out-of-core processing**: Work with datasets larger than memory by loading only needed chunks. - **Compression**: Reduce storage footprint without sacrificing access speed. - **Concurrent chunk access**: Multiple workers can read or write different chunks efficiently. - **Cloud compatibility**: Store and access data on remote object storage (S3, GCS, etc.). Use Zarr for beamformed IQ data, large-scale analyses, and cloud-based processing workflows. ConfUSIus converts beamformed IQ data to Zarr by default for optimal performance with large-scale datasets. ## fUSI Data Types fUSI workflows involve two main categories of data: Complex-valued signals resulting from ultrasound beamforming of RF signals. These signals are typically further processed to extract derived signals such as power Doppler or velocity. Beamformed IQ datasets are typically very large (10s to 100s of GB per acquisition session), and stored in system-specific layouts depending on the acquisition system (e.g., Iconeus, AUTC, EchoFrame). Processed data such as power Doppler, velocity, or other signals derived from beamformed IQ data. These datasets are generally much smaller than the original IQ data (often 10-100x smaller) and are frequently stored in standardized formats like NIfTI for interoperability and BIDS compliance. > [!TIP] > > **Zarr for large recordings** > > Large-scale fUSI recordings (e.g., using matrix probes) can also benefit from storage in Zarr for efficient processing. ## Loading Data The universal `confusius.load` function allows loading VoxelData arrays from multiple formats, including NIfTI, Zarr, EchoFrame DAT, and Iconeus SCAN files. It automatically detects the file format and dispatches to the appropriate loader (`load_nifti`, `load_echoframe_dat`, `load_scan`, etc.). All ConfUSIus loaders return **lazy** DataArrays backed by Dask—data stays on disk until an operation requires it. > [!TIP] > > **Load into memory when it fits** > > Lazy loading is essential for datasets larger than available RAM, but it introduces Dask scheduling overhead on every operation and can make things difficult if your data chunking layout doesn't fit your operation. If your data fits comfortably in memory (leaving enough headroom for intermediate results), load it eagerly with `.compute()` for better performance: > > ```python > da = cf.load("sub-01_task-awake_pwd.nii.gz").compute() > ``` > > For gzip-compressed NIfTI files (`.nii.gz`), repeated lazy partial reads such as per-frame loops may decompress the file again for each independent Dask computation. Use `.compute()` for ordinary eager processing, or `.persist()` before many downstream Dask operations that should stay chunked and parallel: > > ```python > da = cf.load("sub-01_task-awake_pwd.nii.gz").persist() > ``` > > For larger repeated random-access workloads, prefer Zarr or uncompressed `.nii`. ### Loading Zarr Files Xarray treats Zarr stores as multi-variable datasets. ConfUSIus' `load` returns the first variable as a DataArray by default. If the Zarr store contains multiple variables, you can specify which one to load with the `variable` argument. ```pycon >>> import confusius as cf >>> >>> # Load beamformed IQ data (returns the first variable as a DataArray by default). >>> iq_data = cf.load("sub-01_task-awake_iq.zarr") >>> >>> # Or specify the variable name if there are multiple variables in the Zarr store. >>> iq_data = cf.load("sub-01_task-awake_iq.zarr", variable="iq") >>> >>> iq_data Size: 57GB dask.array Coordinates: * time (time) float64 9MB 5.551 5.553 5.555 ... 2.355e+03 2.355e+03 * k (k) float64 8B 0.0 * j (j) float64 944B 0.0 1.0 2.0 3.0 ... 114.0 115.0 116.0 117.0 * i (i) float64 416B 0.0 1.0 2.0 3.0 ... 48.0 49.0 50.0 51.0 * z (k, j, i) float64 ... 0.0 0.0 0.0 ... * y (k, j, i) float64 ... 4.656 4.705 4.753 ... * x (k, j, i) float64 ... -2.671 -2.57 -2.469 ... Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: transmit_frequency: 15625000.0 probe_number_of_elements: 128 probe_pitch: 0.0001 beamforming_sound_velocity: 1510.0 plane_wave_angles: [-10.0, -9.310344696044922, -8.620689392089... compound_sampling_frequency: 500.0 pulse_repetition_frequency: 15000.0 beamforming_method: Fourier ``` > [!NOTE] > > **Loading a full Dataset** > > `confusius.load` always returns a single DataArray. To load all variables in a Zarr store as a Dataset, use `xarray.open_zarr` directly: > > ```python > import xarray as xr > > ds = xr.open_zarr("sub-01_task-awake_iq.zarr") > ``` ### Loading EchoFrame DAT Files For one-time processing, load EchoFrame DAT files directly with `confusius.load`: ```python import confusius as cf # Uses ScanParameters.mat next to the DAT file by default. iq = cf.load("path/to/data.dat") # Or pass the sidecar path explicitly. iq = cf.load("path/to/data.dat", meta_path="path/to/metadata.mat") ``` When you expect to process the same recording repeatedly, [convert it to Zarr](<#converting-beamformed-iq-data-to-zarr>) instead for better performance. ### Loading Iconeus SCAN Files Iconeus SCAN files come in two flavors, depending on your version of IcoScan: IcoScan versions before 2.0 output SCAN files using the HDF5-based SCAN v1 format, while versions 2.0 and after use the newer binary SCAN v2 format. `confusius.load` automatically detects the format and dispatches to the appropriate loader. #### SCAN v1 (HDF5 format) ```pycon >>> import confusius as cf >>> da = cf.load("sub-01_task-awake_pwd.source.scan") >>> da Size: 606MB dask.array Coordinates: time (time, pose) float64 16kB 0.4 2.2 1.0 ... 1.199e+03 1.199e+03 * pose (pose) int64 32B 0 1 2 3 * k (k) int64 32B 0 1 2 3 * j (j) int64 736B 0 1 2 3 4 5 6 7 8 9 ... 83 84 85 86 87 88 89 90 91 * i (i) int64 824B 0 1 2 3 4 5 6 7 8 ... 94 95 96 97 98 99 100 101 102 * z (pose, k, j, i) float64 1MB -0.19 -0.3 -0.41 ... -11.3 -11.41 * y (pose, k, j, i) float64 1MB 20.3 20.3 20.3 ... 29.27 29.27 29.27 * x (pose, k, j, i) float64 1MB -3.851 -3.851 -3.851 ... 4.024 4.024 Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: affines: {} device_serial_number: XXXXXXXX software_version: IcoScan version 1.0.0 iconeus_scan_mode: 4Dscan iconeus_subject: Mouse01 iconeus_session: Session01 iconeus_scan: sub-01_task-awake_pwd iconeus_project: Project01 iconeus_datetime: 2021-10-22T14:14:12+00:00 ``` Provenance metadata from the file is stored in `da.attrs`, including `iconeus_scan_mode`, `iconeus_subject`, `iconeus_session`, `iconeus_scan`, `iconeus_project`, `iconeus_datetime`, `software_version`, and `device_serial_number`. `iconeus_datetime` uses ISO 8601 format for both SCAN v1 and SCAN v2. SCAN v1 stores the date without a time zone, so ConfUSIus treats it as UTC. Note that for recordings consisting of multiple probe poses (e.g., `3Dscan`, `4Dscan`), the `time` coordinate is **pose-dependent**, giving the acquisition time of each volume for each pose. This is useful for volume-timing corrections and for aligning multi-pose acquisitions with external events. Additionally, each pose has its own voxel-to-world affine transformation, meaning the `z`/`y`/`x` world coordinates are **pose-dependent**. The `pose` coordinate indexes the pose dimension, and the `z`/`y`/`x` coordinates are derived from the corresponding pose's affine. See [The VoxelData Model]() for details on how world coordinates are derived from voxel indices and affines. > [!WARNING] > > **SCAN files and parallel processing** > > SCAN v1 files are HDF5 files, and h5py datasets **cannot be pickled**. This means lazy SCAN DataArrays cannot be passed to functions that use parallel workers (e.g., `register_volumewise` with `n_jobs != 1`). Call `.compute()` to load the data into memory before running any parallel operation: > > ```python > import confusius as cf > > fusi = cf.load("recording.scan").compute() # materialize first > fusi = cf.registration.register_volumewise(fusi) > ``` > > Alternatively, use `n_jobs=1` for serial processing (slower but works with lazy SCAN data). #### SCAN v2 (binary format) SCAN v2 files preserve the recording's data, acquisition timing, and VoxelData geometry. Metadata recovered into `da.attrs` include the `world_to_lab` affine, provenance (`iconeus_*` attrs), acquisition datetime, and BIDS-corresponding acquisition settings (`probe_model`, `probe_center_frequency`, `probe_aperture`, `transmit_frequency`, `pulse_repetition_frequency`, `plane_wave_angles`, `svd_low_cutoff`, …). For multi-pose recordings, see the [multi-pose user guide](). ```pycon >>> import confusius as cf >>> da = cf.load("sub-01_task-awake_pwd.source.scan") >>> da Size: 84MB dask.array Coordinates: * time (time) float64 304B 0.4 0.8 1.2 1.6 2.0 ... 318.8, 319.2, 319.6, 320.0 * k (k) int64 8B 0 * j (j) int64 736B 0 1 2 3 4 5 6 7 8 9 ... 83 84 85 86 87 88 89 90 91 * i (i) int64 1kB 0 1 2 3 4 5 6 7 8 ... 120 121 122 123 124 125 126 127 * z (k, j, i) float64 94kB -4.385 -4.275 -4.165 ... 9.365 9.475 9.585 * y (k, j, i) float64 94kB 1.0 1.0 1.0 1.0 ... 9.969 9.969 9.969 9.969 * x (k, j, i) float64 94kB 0.7 0.7 0.7 0.7 0.7 ... 0.7 0.7 0.7 0.7 0.7 Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: (12/26) affines: {} iconeus_scan_format: v2 iconeus_scan_mode: 2Dscan iconeus_datetime: 2026-07-14T05:00:00+00:00 device_serial_number: XXXXXXX iconeus_hardware: Hardware_1_0-0123 ... ... probe_pitch: 0.11 probe_focal_depth: 8.0 imaging_depth: (1.0, 10.0) transmit_frequency: 15.625 pulse_repetition_frequency: 5500.0 plane_wave_angles: [-10.0, -8.0, -6.0, -4.0, -2.0, 0.0, 2... ``` #### Loading a BPS File Iconeus' BPS files are HDF5 containers produced by Iconeus' Brain Positioning System. They contain an affine matrix that maps Iconeus brain coordinates `(x_brain, y_brain, z_brain, 1)` to Iconeus lab coordinates `(x_lab, y_lab, z_lab, 1)` in meters. The Iconeus lab frame is a fixed scanner space defined by Iconeus. ConfUSIus re-expresses this space as **ConfUSIus-ordered** lab space `(z_lab, y_lab, x_lab)` in millimeters: that is the world space used when loading SCAN files. The affine matrix in the BPS file can be loaded with `confusius.io.load_bps`. It is generally a good idea to store it in the `.attrs["affines"]` attribute of the DataArray: ```python import confusius as cf import numpy as np da = cf.load("sub-01_task-awake_pwd.source.scan") brain_to_world = cf.io.load_bps("sub-01_task-awake_pwd.bps") da.attrs["affines"]["world_to_brain"] = np.linalg.inv(brain_to_world) ``` Passing the BPS file path using the `bps_path` argument when loading a SCAN file offers a convenient shortcut: ```python import confusius as cf da = cf.load( "sub-01_task-awake_pwd.source.scan", bps_path="sub-01_task-awake_pwd.bps", ) ``` Compose it with brain-side affines (e.g. brain-to-CCFv3 from a brain atlas) to register fUSI data into atlas space directly. For multi-pose files (`3Dscan`, `4Dscan`) the affine has shape `(npose, 4, 4)` and is indexed by pose, the same way `world_to_lab` is. #### Converting SCAN Data to NIfTI Since `load` returns a VoxelData array, you can save it directly to NIfTI using `save` or the Xarray accessor. For **2Dscan** data, save it directly: ```python import confusius as cf da = cf.load("sub-01_task-awake_pwd.scan") cf.save(da, "sub-01_task-awake_pwd.nii.gz") # Or equivalently: da.fusi.save("sub-01_task-awake_pwd.nii.gz") ``` For **3Dscan** and **4Dscan** data, consolidate the poses into a single volume before saving, or save each pose separately if you want to retain the multi-pose structure. See the [Multi-Pose Data guide]() for details on working with multi-pose acquisitions. ```python import confusius as cf anat = cf.load("sub-01_acq-anat_pwd.scan") volume = cf.multipose.consolidate_poses(anat) cf.save(volume, "sub-01_acq-anat_pwd.nii.gz") ``` ```python import confusius as cf anat = cf.load("sub-01_acq-anat_pwd.scan") for pose in anat.pose: pose_da = anat.sel(pose=pose) cf.save(pose_da, f"sub-01_acq-anat_pose-{pose.values}.nii.gz") ``` ### Loading NIfTI Files When loading NIfTI files, ConfUSIus automatically loads a [fUSI-BIDS]() metadata. For files inside a BIDS folder tree, matching sidecars are inherited from the dataset root (the folder containing `dataset_description.json`) down to the recording folder, with more specific sidecars overriding shared values. Metadata fields are interpreted using the fUSI-BIDS naming conventions and converted back to the usual ConfUSIus attribute names on the loaded DataArray. Timing metadata in the sidecar takes precedence over the NIfTI header when both are available. ```pycon >>> import confusius as cf >>> >>> # Load with automatic fUSI-BIDS sidecar metadata. >>> da = cf.load("sub-01_task-awake_pwd.nii.gz") >>> da Size: 51MB dask.array Coordinates: * time (time) float64 10kB 10.42 10.72 11.02 11.32 ... 393.6 393.9 394.2 * k (k) int64 8B 0 * j (j) int64 1kB 0 1 2 3 4 5 6 7 8 ... 117 118 119 120 121 122 123 124 * i (i) int64 640B 0 1 2 3 4 5 6 7 8 9 ... 71 72 73 74 75 76 77 78 79 * z (k, j, i) float64 80kB 0.3 0.3 0.3 0.3 0.3 ... 0.3 0.3 0.3 0.3 0.3 * y (k, j, i) float64 80kB 2.996 2.996 2.996 ... 8.988 8.988 8.988 * x (k, j, i) float64 80kB -3.95 -3.85 -3.75 -3.65 ... 3.75 3.85 3.95 Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: (12/24) qform_code: 1 manufacturer: Verasonics manufacturers_model_name: Vantage 128 software_version: Alan Urban Technology & Consulting (... probe_manufacturer: Vermon probe_type: linear ... ... task_description: Visual stimulation using a flickerin... depth: [0.0, 5.991680000000001] transmit_frequency: 15625000.0 compound_sampling_frequency: 500.0 plane_wave_angles: [-10.0, -7.9, -5.8, -3.6999999999999... transmit_voltage: 25.0 ``` ### Loading Other Formats For unsupported formats, such as lab-specific MAT-files, load the array with the appropriate Python tool and use `create_voxeldata` to attach dimensions, coordinates, and metadata: ```python import confusius as cf # Replace this with scipy.io.loadmat, h5py, mat73, or your lab's loader. raw_power = load_my_mat_file("path/to/power_doppler.mat") # source array, (i, j, time) power = cf.create_voxeldata( raw_power, dims=("i", "j", "time"), # missing k is added as a singleton voxel dimension dt=1 / 2.5, # 2.5 Hz frame rate spacing=(0.4, 0.05, 0.1), # world spacing in z/y/x order, in mm. attrs={"description": "Power Doppler from my system"}, ) ``` See the [Create a VoxelData array from a MAT file]() example for a complete walkthrough, from a real lab-specific MAT file to motion correction and a task GLM. ## Converting Beamformed IQ Data to Zarr Beamformed IQ exports are often stored as large binary files together with acquisition metadata. The file structure from AUTC and EchoFrame systems is documented, allowing ConfUSIus to provide built-in conversion utilities that reorganize these datasets into Zarr for more efficient processing. > [!NOTE] > > **Why doesn't ConfUSIus support Iconeus RAW files?** > > ConfUSIus cannot currently read Iconeus beamformed IQ files because the Iconeus RAW format is not documented publicly. If you need to process RAW files with ConfUSIus, please contact Iconeus to request an export tool or format documentation. This format consists of a series of binary `.dat` files (often split into parts), where each file contains multiple acquisition blocks. To convert a folder of AUTC DAT files to Zarr, use the `convert_autc_dats_to_zarr` function. ConfUSIus does not provide direct lazy loading for AUTC DAT acquisitions because they are split across multiple binary files, which makes practical Dask parallel processing difficult. ```python from confusius.io import convert_autc_dats_to_zarr convert_autc_dats_to_zarr( dats_root="path/to/data_folder", output_path="sub-01_task-awake_iq.zarr", # Optional: specify block start times, transmit frequency, axis coordinates, and # other metadata via keyword arguments (see API for details). block_times=block_times, compound_sampling_frequency=500.0, transmit_frequency=15.625e6, beamforming_sound_velocity=1510.0, ) ``` This will create a Zarr group containing: - `iq`: Beamformed IQ data with native dimensions `(time, k, j, i)`. - `time`, `k`, `j`, `i`: Native time and voxel-space dimension coordinates. - `voxel_to_world` and `voxel_to_world_units`: Attributes on `iq` used to restore the `VoxelToWorldIndex` and derive `z`/`y`/`x` world coordinates. - Metadata attributes (e.g., `transmit_frequency`, `plane_wave_angles`) as provided via keyword arguments. This format consists of a binary `.dat` file containing the beamformed data and a `.mat` file containing metadata (sequence parameters). To convert EchoFrame data to Zarr, use `convert_echoframe_dat_to_zarr`. Zarr is useful for long-term storage or repeated processing of the same recording. ```python from confusius.io import convert_echoframe_dat_to_zarr convert_echoframe_dat_to_zarr( dat_path="path/to/data.dat", meta_path="path/to/metadata.mat", output_path="sub-01_task-awake_iq.zarr", # Optional: specify block start times. Other metadata (e.g., transmit frequency, # axis coordinates) will be automatically extracted from the metadata file. block_times=block_times, ) ``` This will create a Zarr group containing: - `iq`: Beamformed IQ data with native dimensions `(time, k, j, i)`. - `time`, `k`, `j`, `i`: Native time and voxel-space dimension coordinates. - `voxel_to_world` and `voxel_to_world_units`: Attributes on `iq` used to restore the `VoxelToWorldIndex` and derive `z`/`y`/`x` world coordinates. - Metadata attributes (e.g., `transmit_frequency`, `plane_wave_angles`) as extracted from the metadata file. ### Other Systems For beamformed IQ data from a system other than AUTC or EchoFrame, load the complex array with the tool appropriate for your file format, then wrap it as a VoxelData array with `create_voxeldata`. Put IQ-specific metadata such as `transmit_frequency` and `beamforming_sound_velocity` in `attrs`: ```python import confusius as cf from confusius.validation import validate_voxeldata raw_iq = load_my_iq_file("path/to/iq.mat") # complex array, (time, k, j, i) iq = cf.create_voxeldata( raw_iq, dims=("time", "k", "j", "i"), dt=1 / 500, spacing=(0.4, 0.05, 0.1), # world spacing in z/y/x order, in mm. volume_acquisition_duration=1 / 500, attrs={ "transmit_frequency": 15.625e6, "beamforming_sound_velocity": 1540.0, }, ) validate_voxeldata(iq, require_velocity_attrs=True) ``` See [Processing Beamformed IQ Data]() for the required dimensions, metadata fields, and processing assumptions. ## Saving Data You can save VoxelData arrays to NIfTI and Zarr using the universal `confusius.save` function or the Xarray accessor: ```python import confusius as cf # Save to NIfTI with automatic fUSI-BIDS JSON sidecar creation. cf.save(data_array, "output.nii.gz") # Save to Zarr. cf.save(data_array, "output.zarr") ``` ```python import confusius # Save to NIfTI with automatic fUSI-BIDS JSON sidecar creation. data_array.fusi.save("output.nii.gz") # Save to Zarr. data_array.fusi.save("output.zarr") ``` When saving to NIfTI, a [fUSI-BIDS]() JSON sidecar file will be automatically created in fUSI-BIDS style. Spatial coordinates and units are encoded in the NIfTI header itself; the sidecar stores converted metadata fields, custom attributes, and timing metadata such as `RepetitionTime`, `DelayAfterTrigger`, or `VolumeTiming`. When possible, `RepetitionTime` is inferred directly from the `time` coordinate so the sidecar stays consistent with the data being saved. Multi-pose data cannot be written directly to a single NIfTI file; save it to Zarr, consolidate poses first, or save one pose per file instead. If `data_array.attrs["affines"]` contains named world-to-reference affines, you can choose which ones are written into the NIfTI header: ```python import confusius as cf cf.save( data_array, "output.nii.gz", qform="world_to_scanner", sform="world_to_template", ) ``` When `qform` and/or `sform` are omitted, `save` first looks for `"world_to_qform"` and `"world_to_sform"` in `attrs["affines"]`. If those keys are not present, the NIfTI header falls back to the DataArray's coordinate-defining `voxel_to_world` geometry. Any named affine from `attrs["affines"]` that is actually written into the NIfTI `qform` or `sform` header is omitted from the `ConfUSIusAffines` JSON sidecar field so it is not stored twice. ## Format Conversion Reference Quick reference for converting between formats: | From | To | Function | | --- | --- | --- | | AUTC DATs | Zarr | `confusius.io.convert_autc_dats_to_zarr` | | EchoFrame DAT | VoxelData array | `confusius.io.load_echoframe_dat` | | EchoFrame DAT | Zarr | `confusius.io.convert_echoframe_dat_to_zarr` | | Iconeus SCAN | VoxelData array | `confusius.load` | | NIfTI | VoxelData array | `confusius.load` | | Zarr | VoxelData array or Dataset | `confusius.load` / `xarray.open_zarr` (Dataset) | | VoxelData array | NIfTI | `confusius.save` / `.fusi.save` | | VoxelData array | Zarr | `confusius.save` / `.fusi.save` / `.to_zarr` | # The VoxelData Model ## VoxelData Arrays **VoxelData** is ConfUSIus's canonical [DataArray]() model for any spatially referenced voxel array—beamformed IQ and fUSI recordings, atlas volumes, decomposition component maps, displacement fields, and anything else gridded in space. A **VoxelData array** is a DataArray that satisfies the following requirements: 1. Dimensions `(..., time, pose, k, j, i)` in that order, where `...` maybe be any number of extra dimensions (e.g. decomposition components, channels, etc.) and `time` and `pose` may be absent. 2. A [**`VoxelToWorldIndex`**]() attached to the world coordinates `(z, y, x)`, which it derives either from `(pose, k, j, i)` through one voxel-to-world affine transformation per pose. 3. Attributes `units`, `volume_acquisition_reference`, and `volume_acquisition_duration` on `time`. ### Dimension Ordering: `(..., time, pose, k, j, i)` VoxelData arrays have the following dimensions, in the order shown: | Dimension | Coordinate dtype | Optional | Typical axis meaning | Typical size | | --- | --- | --- | --- | --- | | `...` | any | Yes | Components, regions, ... | Any | | `time` | float | Yes | Acquisition time | Thousands | | `pose` | int | Yes | Probe poses | Tens | | `k` | int | No | Elevation (stacking direction) | One to tens | | `j` | int | No | Axial / depth | Tens to hundreds | | `i` | int | No | Lateral | Tens to hundreds | > [!NOTE] > > **Why `(..., time, k, j, i)` instead of `(i, j, k, time, ...)`?** > > Users familiar with neuroimaging are typically accustomed to spatiotemporal conventions like `(i, j, k, time, ...)`. These conventions come from languages like MATLAB and formats like NIfTI where the first axis `i` varies fastest in storage. This is the opposite of NumPy's (and most of Python's) default memory layout, where the last axis varies fastest. ConfUSIus therefore uses `(..., time, pose, k, j, i)` to map NIfTI data naturally onto the memory layout used throughout the Python scientific ecosystem, often without copying or rearranging the data. > > Thankfully, Xarray makes dimension ordering transparent in practice: you can always refer to dimensions by name and in any order (e.g. `data.mean("time")`, `data.sel(x=4.54, y=-2.48, z=0.0)`) rather than by axis index, so you won't have to remember the order of the dimensions. Moreover, `ensure_voxeldata` will automatically reorder dimensions to the canonical order if they are not already, so you can always pass a DataArray to ConfUSIus functions without worrying about its dimension order. This ordering pays off beyond the NumPy memory layout above: - **Contiguous volumes:** the last axes are contiguous in memory, so `data[t]` is one contiguous block—the natural unit for IQ processing, motion correction, and other volume-wise operations. - **`(samples, features)` for free:** `data.stack(space=["k", "j", "i"])` reshapes to `(time, space)` without copy, matching the [scikit-learn]()/ [statsmodels]() convention for statistical analysis. - **Atlas-aligned:** `(k, j, i)` follows the same orientation as [BrainGlobe]() atlases (e.g. Allen CCFv3). - **Visualization-ready:** plotting `(time, k, j, i)` directly yields a correctly oriented `(j, i)` slice with `time`/`k` sliders, since many Python tools (e.g., napari) expect the last two axes as the display axes. ### Metadata The `units`, `volume_acquisition_reference`, and `volume_acquisition_duration` attributes from requirement 3 above have the following meanings: | Attribute | Meaning | | --- | --- | | `units` | Physical unit of the `time` coordinate values (`"s"` is typical). | | `volume_acquisition_reference` | Which point of the acquisition window each `time` value marks: `"start"`, `"center"`, or `"end"`. | | `volume_acquisition_duration` | Duration to acquire the whole `(k, j, i)` grid, in the same units as `time`. | Additionally, the `VoxelToWorldIndex` attached to `(z, y, x)` carries its own `units` attribute, which is the physical unit of the world coordinates (millimeters is typical). It is accessed through the `.fusi.affine.units` property and set with `.fusi.affine.set_units(...)`. > [!WARNING] > > **Don't set attributes on world coordinates directly** > > World coordinates `(z, y, x)` are always derived fresh from the `VoxelToWorldIndex`; any operation that touches the index (`.isel`, `.assign_coords`, etc.) regenerates them, including their `.attrs`, from the index's own state. Mutating `data.coords["z"].attrs[...]` directly is unsupported: the mutation is silently discarded the next time the coordinate is rebuilt. The index does write `units` onto each world coordinate's `.attrs` too, purely so external packages that read it from there still see it — but some xarray operations that combine several arrays (e.g. `xr.where`, which only keeps attrs from its `x` argument) can drop that copy from the result without touching the index's own `units`. Always trust `.fusi.affine.units`, never `.coords["z"].attrs["units"]`, when correctness matters. ### Creating and Validating VoxelData Arrays To build a VoxelData array from raw arrays (NumPy, CuPy, etc.), use `create_voxeldata`. If you then modified a VoxelData array and want to check that it still satisfies the VoxelData model, use `validate_voxeldata`. All ConfUSIus functions that expect a VoxelData array call `ensure_voxeldata` on their input, which canonicalizes the DataArray first—reordering dimensions to `(...extra, time, pose, k, j, i)`, restoring a voxel dimension collapsed to a scalar coordinate by a prior `.isel`, and filling in missing `time` metadata with sensible defaults—before validating. ## Temporal Conventions The `time` coordinate marks when each volume was acquired, in the units named by its `units` attribute (seconds, typically). Because a volume is acquired over a nonzero window rather than instantaneously, `time` alone doesn't say which point of that window the value refers to—`volume_acquisition_reference` disambiguates this: `"start"`, `"center"`, or `"end"` of the acquisition window. `volume_acquisition_duration` gives the window's length, in the same units as `time`. Together, these three attributes let downstream code (e.g. GLM HRF convolution) reconstruct the exact acquisition window of every volume rather than treating `time` as an instantaneous sample. The rest of this section covers how these definitions adapt for [multi-pose data](), where poses making up one volume are swept through sequentially rather than acquired simultaneously. ### Unconsolidated Multi-Pose Data While a VoxelData array still carries a `pose` dimension, `time` must be pose-dependent with shape `(time, pose)` instead of the plain 1D coordinate above. `time` then describes each pose's acquisition timestamp. In this shape, `volume_acquisition_duration` and `volume_acquisition_reference` describe **one pose's own `(k, j, i)` acquisition window**, not the time it takes to sweep through every pose. Resolving a single real timestamp requires selecting a scalar `pose` first, which reduces `time` back to the plain 1D case. ### Consolidated Multi-Pose Data `consolidate_poses` merges `pose` into the sweep voxel dimension the poses were stepped along (`i`, `j`, or `k`, detected automatically), so the result has no `pose` dimension left. Two things happen to timing: - `time` becomes an ordinary whole-array 1D coordinate again, but `volume_acquisition_duration`/`volume_acquisition_reference` are recomputed to describe the full sweep across every pose—from the earliest pose's onset to the latest pose's offset—rather than one pose's window. - Each slice's own real timestamp survives separately as a new `slice_time` coordinate, with dims `(time, )` (or a single dim when `time` is scalar or absent), inheriting the pre-consolidation `time`'s `volume_acquisition_duration`/ `volume_acquisition_reference`. `slice_time` requires the same three attributes as `time` and `ensure_voxeldata` fills in missing ones the same way, defaulting `volume_acquisition_duration` from the median consecutive gap between slices along ``. `validate_voxeldata` checks that every slice's `slice_time` falls within its own volume's acquisition window, as defined by `time` and its `volume_acquisition_duration`/`volume_acquisition_reference`. This doesn't forbid overlap between consecutive volumes: `volume_acquisition_duration` may exceed the spacing between `time` values, e.g sliding-window beamformed IQ processing acquisition). It only constrains a slice against its own volume's window. ### Slice Timing Correction `correct_slice_timings` uses `slice_time` (or an unconsolidated pose-dependent `time`) to resample each slice's or pose's time series onto the shared, whole-array `time` reference—working on both unconsolidated and consolidated data. See the [Multi-Pose Imaging guide]() for a full example. ## Spatial Conventions To localize a VoxelData array in physical space, ConfUSIus works with four kinds of coordinate systems: - **Array space**: the dense, zero-based array position along each spatial dimension. - **Voxel space**: the `(i, j, k)` coordinate labels attached to the underlying array storage, coinciding with array space only when voxel labels themselves start at `0` and increase by `1`. - **World space**: derived from voxel space through the DataArray's voxel-to-world affine transformation(s) and exposed as the coordinates `(x, y, z)`. - **Reference spaces**: any coordinate system (atlas, scanner, etc.) linked to the world space through affine transforms stored in `.attrs["affines"]`. For most recordings, one voxel-to-world affine defines one world grid for the whole DataArray. Multi-pose acquisitions are the main exception: they carry one affine per `pose`, so `(x, y, z)` become pose-dependent coordinates and a scalar `pose` selection is required before selecting by world coordinate. Each space feeds the next: array position gets a voxel label, a voxel label gets a world coordinate, and a world coordinate can reach any number of reference spaces: ``` --- config: layout: elk --- flowchart LR A["Array space"] V["Voxel space"] P["World space"] W1["Scanner space"] ellipsis{{"..."}} W2["Atlas space"] A -->|"integer labels"| V V -->|"VoxelToWorldIndex"| P P -->|".attrs[affines]"| W1 P -->|".attrs[affines]"| W2 P --> ellipsis ellipsis@{ shape: text } ``` ### Array Space The array space defines the dense, zero-based position along each spatial dimension: position `0` is always the array's first stored element, position `dim_size - 1` its last, and every position in between is contiguous. In Xarray, `.isel` indexes by array position. If you're used to NumPy, think of array space as the "axis index" of each dimension, for example `data[0, 0, 0]` is the first voxel in array space, `data[-1, -1, -1]` the last. ### Voxel Space The Voxel space is defined by the DataArray's `(i, j, k)` coordinate labels, indexed by label with `.sel`. Labels coincide with array space for a freshly built DataArray, but the two diverge once a DataArray is cropped or strided from a larger one: ```pycon >>> cropped = data.isel(i=slice(3, 6)) >>> cropped.coords["i"].values array([3, 4, 5]) >>> cropped.isel(i=0).coords["i"].item() # Array position 0's label. 3 >>> cropped.sel(i=3).coords["i"].item() # The voxel labeled 3. 3 ``` Use `reindex_voxels` to rebase voxel labels back to dense array-space positions—see [Rebasing voxel coordinates to dense positions]() in Working with Xarray. ### World Space The world space is defined by the DataArray's voxel-to-world affine transformation (or transformations, for multi-pose data) contained in the `VoxelToWorldIndex` and exposed as coordinates `(x, y, z)`. For ordinary single-pose data these coordinates are arrays with shape `(k, j, i)`. For multi-pose data they are pose-dependent with shape `(pose, k, j, i)`, so selecting in world space requires a scalar `pose` first. The unit shared by `x`/`y`/`z` is exposed as `data.fusi.affine.units` and set with `data.fusi.affine.set_units(...)`; millimeters are the usual default for fUSI recordings. > [!WARNING] > > **Units are not enforced** > > ConfUSIus does not check or convert between units across its APIs—`units` is metadata only. We plan to make the data model more unit-aware in the future. World space is not tied to any one physical space—it's whatever space the DataArray's voxel-to-world affine currently encodes, and that changes over the course of a pipeline. A freshly loaded recording is typically expressed in scanner space: the space of the first acquired probe pose, with origin at the probe surface and axes along lateral, depth, and elevation. Once the data is resampled or registered, world space becomes whatever space that operation targeted instead: an atlas template (e.g. Allen CCFv3), another recording's grid, or any other space you choose. World coordinates are set when attaching a `VoxelToWorldIndex` to the DataArray. Different loaders derive them in different ways: - **EchoFrame**: Lateral and axial coordinates are read from the acquisition metadata file. - **AUTC**: Spacing must be supplied explicitly to `convert_autc_dats_to_zarr`—AUTC files carry no spacing metadata of their own. Origin is optional and defaults to a probe-centered, surface-referenced position when omitted. - **Iconeus SCAN**: Coordinates are derived from the `voxelsToProbe` affine embedded in the SCAN file. The axial axis is flipped so that it is always positive and increases with depth. - **NIfTI**: Coordinates are derived from the "best" affine transformation found in the file header, or from whichever one `load_nifti`'s `coordinate_affine` argument selects explicitly. > [!TIP] > > **The "best" NIfTI affine** > > NIfTI files can store two affine transforms in their header: `qform` and a `sform`, each with an associated integer code indicating whether the affine is valid (`code > 0`) and which space it points to. > > - `qform` cannot contain shears and is typically used to encode transforms from voxel space to scanner space. > - `sform` can be any arbitrary affine transformation and is typically used to encode transforms from voxel space to "standard" reference spaces, such as a recording's world space or an atlas space. > > By default, ConfUSIus follows the [same logic as NiBabel](): if `sform_code > 0` the `sform` is used to define the world coordinates; otherwise, if `qform_code > 0` the `qform` is used. If both codes are zero a warning is emitted and coordinates fall back to a diagonal affine built from the NIfTI `pixdim` field. Pass `coordinate_affine="sform"`/`"qform"` to force one explicitly. Hand-constructed DataArrays get whatever voxel-to-world affine the user provides via `create_voxeldata`. #### Voxel-to-World Affine The voxel-to-world affine is a `(4, 4)` homogeneous matrix (or a `(pose, 4, 4)` stack for multi-pose data) mapping `(k, j, i)` voxel-space coordinates to `(z, y, x)` world-space coordinates: ```text [z, y, x, 1] = voxel_to_world @ [k_label, j_label, i_label, 1] ``` The voxel-to-world affine can be read with `.fusi.affine.voxel_to_world`, or replaced outright with `.fusi.affine.set_voxel_to_world`. That raw mapping is rarely what you want directly, though: most tools that consume world-space geometry think in [array space](<#array-space>), not in whatever labels the `(k, j, i)` coordinates happen to carry after upstream cropping or striding. If you specifically need the affine mapping array space to world space (that packages like NiBabel or the NIfTI format expect) call `reindex_voxels` first: it rebases the voxel space to the array space and updates `voxel_to_world` to match, so the affine you then read off directly maps dense array positions instead of labels. #### Origin, Spacing, Direction `.fusi.origin`, `.fusi.spacing`, and `.fusi.direction` (see [Global Helpers]() in Working with Xarray) describe the same voxel-to-world affine, but anchored to array space rather than voxel labels. They are the typical parameters used to describe a world-space grid in neuroimaging tools like ITK: - **origin**: world position of the array space's origin. Keyed by world axis (`z`/`y`/`x`). - **spacing**: world distance covered by one array-space step. Keyed by voxel axis (`k`/`j`/`i`). - **direction**: `(3, 3)` matrix of unit world-space direction vectors, one per array-space axis: columns are voxel axes (`k`/`j`/`i`), rows are world axes (`z`/`y`/`x`). Direction flips sign on an axis whose labels run descending (e.g. after `.isel(dim=slice(None, None, -1))`). For example, if `k`'s coordinates are `[0, 2, 4]` (every other voxel was kept, e.g. by `.isel(k=slice(None, None, 2))`) and the voxel-to-world affine's scale along `k` is `0.1`, then `data.fusi.spacing["k"]` is `0.2`, not `0.1`: consecutive array positions are two *sampled voxels* apart. Together, `origin`/`spacing`/`direction` reconstruct world coordinates from array position rather than from labels: ```text world = origin + direction @ (spacing * array_position) ``` ### Reference Spaces ConfUSIus stores affine transformations relating the DataArray's current world space to any number of other named spaces in `.attrs["affines"]`, a dictionary keyed by affine name. Reference spaces can be an atlas space, a scanner or lab space, another recording's world space, or a space the data has already moved away from (e.g. the raw probe-relative space a recording started in). A space counts as a "reference space" rather than *the* world space only because reaching it currently takes an affine. `.fusi.affine.apply` can turn any one of them into the world space itself (see [Switching World Spaces](<#switching-world-spaces>)). Each reference space is stored in `.attrs["affines"]` as a homogeneous affine matrix in `(z, y, x)` convention that maps a world-space point to the corresponding point in the reference space. Most are plain `(4, 4)` matrices; for multi-pose data they may also be stacked `(pose, 4, 4)` affines with one entry per pose. Several loaders populate `.attrs["affines"]` automatically: - **NIfTI**: The world space is whichever affine (`sform` or `qform`) was selected; its own entry (`"world_to_sform"` or `"world_to_qform"`) is the identity. When the other one is also valid, it is stored under the other key, so the world space can be switched between the two. `save_nifti` can write any named affine in `.attrs["affines"]` back to the header via its `qform=`/`sform=` arguments, defaulting to `"world_to_qform"`/`"world_to_sform"` when not specified. - **Iconeus SCAN**: `load_scan` stores a `"world_to_lab"` affine mapping ConfUSIus world coordinates to the Iconeus lab coordinate system. For multi-pose acquisitions (`3Dscan`, `4Dscan`), one affine per pose is stored, with shape `(pose, 4, 4)`. Registration transforms are handled separately from `.attrs["affines"]`. `register_volume` returns the estimated transform explicitly but does not store automatically in `.attrs["affines"]`. > [!NOTE] > > **Why store affines as world → reference instead of voxel → reference?** > > A voxel → reference affine breaks the moment the voxel space is reindexed—via `reindex_voxels` or when saving to NIfTI. A world → reference affine doesn't have this problem: it operates on world coordinate values, which stay physically correct through reindexing. `save_nifti` reconstructs the voxel → reference NIfTI affine it needs by composing the stored world → reference affine with the DataArray's current voxel → world affine at save time. ## Switching World Spaces To switch a DataArray's world space to one of its reference spaces, apply the affine that relates them: `.fusi.affine.apply` takes a `(4, 4)` affine (or a key into `.attrs["affines"]`) and re-expresses the DataArray's world coordinates in that space, which becomes the new world space. For multi-pose data, applying a single affine broadcasts it over every pose, while a stacked affine is applied pose-by-pose. Take the `sform`/`qform` example from [Reference Spaces](<#reference-spaces>). A NIfTI file with `sform_code > 0` anchors its world space to the `sform` space, and the relationship to the `qform` space (a 90° rotation in the `(z, y)` plane, here) is carried on the `world_to_qform` affine attribute: ```pycon >>> da.coords["z"].values.flatten() array([-1., -1., -1., -1., 0., 0., 0., 0., 1., 1., 1., 1.]) >>> da.coords["y"].values.flatten() array([0.5, 1.5, 2.5, 3.5, 0.5, 1.5, 2.5, 3.5, 0.5, 1.5, 2.5, 3.5]) >>> da.attrs["affines"]["world_to_qform"] array([[ 0., -1., 0., 0.], [ 1., 0., 0., 0.], [ 0., 0., 1., 0.], [ 0., 0., 0., 1.]]) ``` Applying `world_to_qform` absorbs the rotation into the DataArray's voxel-to-world affine, and the derived `z`/`y` coordinates change accordingly. The `qform` space becomes the new world space, so `"world_to_qform"` is now the identity. The `sform` entry, identity before, now holds the inverse rotation, so the `sform` space stays recoverable. ```pycon >>> da_q = da.fusi.affine.apply("world_to_qform") >>> da_q.coords["z"].values.flatten() array([-0.5, -1.5, -2.5, -3.5, -0.5, -1.5, -2.5, -3.5, -0.5, -1.5, -2.5, -3.5]) >>> da_q.coords["y"].values.flatten() array([-1., -1., -1., -1., 0., 0., 0., 0., 1., 1., 1., 1.]) >>> da_q.attrs["affines"] {'world_to_sform': array([[ 0., 1., 0., 0.], [-1., 0., 0., 0.], [ 0., 0., 1., 0.], [ 0., 0., 0., 1.]]), 'world_to_qform': array([[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]])} ``` See [Affine Transforms]() in Working with Xarray for the rest of the `.fusi.affine` API (`voxel_to_world`, `set_voxel_to_world`, `to`). # Working with Xarray ## Why Xarray? A typical fUSI recording is a 4D array indexed by time and native voxel dimensions (`i`, `j`, `k`). Some experimental paradigms acquire data at multiple probe poses, adding a `pose` dimension and making the recording 5D. World coordinates corresponding to the voxel dimensions are (`x`, `y`, `z`). Storing this as a plain NumPy array means losing all of that structure: axes become anonymous integers, world coordinates must be tracked separately, and keeping them in sync with the data after slicing or averaging is error-prone. A custom wrapper class would address the labeling and coordinate tracking, but at the cost of needing a complex reimplementation of many array operations and losing access to the broader scientific Python ecosystem. [Xarray]() solves this by wrapping arrays with named dimensions and world coordinates, while [maintaining compatibility]() with the Python scientific ecosystem. With Xarray, operations become self-documenting: ```python # NumPy: what does axis 0 mean here? mean_volume = pwd.mean(axis=0) # Xarray: unambiguous. mean_volume = pwd.mean("time") ``` Coordinates also carry metadata through transformations, so voxel sizes, timestamps, and acquisition parameters travel with the data rather than being stored separately. ```python # Select a depth range by world coordinate. shallow = pwd.sel(z=slice(1, 2.5)) # Coordinates are updated automatically, no manual bookkeeping needed. shallow.y ``` ConfUSIus leverages Xarray to provide a robust data model for fUSI recordings while staying interoperable with other scientific Python libraries. Thus, ConfUSIus can add fUSI-specific conventions and functionality without trapping the data in a ConfUSIus-specific object. ## DataArrays and Datasets Xarray has two core data structures: **DataArrays** and **Datasets**. A **DataArray** is a single array with its own dimensions, coordinates, and attributes. You get a DataArray whenever you load a single fUSI recording, for example a power Doppler NIfTI file from the Nunez-Elizalde 2022 dataset via `confusius.load`. Such a DataArray follows **VoxelData**, ConfUSIus's canonical model for any spatially referenced voxel array—see [The VoxelData Model]() for the full definition: ```pycon >>> import confusius as cf >>> >>> pwd = cf.load("sub-CR022_ses-20201011_task-spontaneous_acq-slice03_pwd.nii.gz") >>> pwd Size: 27MB dask.array Coordinates: * time (time) float64 6kB 10.61 10.91 11.21 ... 235.1 235.4 235.7 * k (k) float64 8B 0.0 * j (j) float64 912B 0.0 1.0 2.0 ... 112.0 113.0 * i (i) float64 640B 0.0 1.0 2.0 ... 78.0 79.0 * z (k, j, i) float64 73kB 1.0 1.0 1.0 ... 1.0 1.0 * y (k, j, i) float64 73kB 2.73 2.73 2.73 ... 8.19 8.19 * x (k, j, i) float64 73kB -3.95 -3.85 -3.75 ... 3.85 3.95 Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: (12/24) qform_code: 1 manufacturer: Verasonics manufacturers_model_name: Vantage 128 software_version: Alan Urban Technology & Consulting (AUTC) probe_manufacturer: Vermon probe_type: linear ... ... 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.7, -1.6, 0.5, 2.6, 4.7, 6.8, 8.9] transmit_voltage: 25.0 ``` Reading the output from top to bottom, a DataArray has four components: - **Dimensions** `(time, k, j, i)`: native voxel axes in the order they appear in the underlying array. `time` is the temporal axis; `i`, `j`, and `k` index the voxel grid. > **Why `(time, k, j, i)` instead of `(i, j, k, time)`?** > > Neuroimaging formats such as NIfTI conventionally order axes `(x, y, z, time, ...)`, with the first axis varying fastest in storage. NumPy uses the opposite convention: its default memory layout has the last axis varying fastest. ConfUSIus therefore uses `(..., time, k, j, i)`, which maps NIfTI data naturally onto the memory layout used throughout the Python scientific ecosystem, often without copying or rearranging the data. In practice, this ordering is usually transparent because Xarray operations refer to dimensions by name rather than by position. See [The VoxelData Model]() for the full explanation. - **Data**: the underlying array. ConfUSIus loaders return [Dask]()-backed data, meaning values are not loaded into memory until you explicitly request them (e.g., by calling `.compute()` or accessing `.values`). - **Coordinates**: timestamps and world positions, typically in seconds and millimeters. The world coordinates `x`, `y`, and `z` are derived from the voxel-to-world geometry (via the `VoxelToWorldIndex` shown under `Indexes`) and enable slicing to work in physical units rather than array indices. - **Attributes**: acquisition metadata as a flat key-value dictionary. Attributes are preserved through most ConfUSIus operations, and some are required for certain functions (for example, `transmit_frequency` is needed for velocity calculations). A **Dataset** is a dictionary-like container of multiple DataArrrays that share some dimensions and coordinates. It shows up when a source naturally groups several co-registered variables together, rather than one array per file. An atlas is a good example: loading one with `fetch_brainglobe_atlas` gives you a Dataset with `template`, `annotation`, and `hemispheres` variables sharing the same voxel grid and world coordinates: ```pycon >>> atlas = cf.datasets.fetch_brainglobe_atlas("allen_mouse_100um") >>> atlas Size: 40MB Dimensions: (k: 132, j: 80, i: 114) Coordinates: * k (k) float64 1kB 0.0 1.0 2.0 ... 130.0 131.0 * j (j) float64 640B 0.0 1.0 2.0 ... 78.0 79.0 * i (i) float64 912B 0.0 1.0 2.0 ... 112.0 113.0 * z (k, j, i) float64 10MB 0.0 0.0 0.0 ... 13.1 13.1 * y (k, j, i) float64 10MB 0.0 0.0 0.0 ... 7.9 7.9 * x (k, j, i) float64 10MB 0.0 0.1 0.2 ... 11.2 11.3 Data variables: template (k, j, i) float32 5MB 0.0 0.0 0.0 ... 1.0 1.0 annotation (k, j, i) int32 5MB 0 0 0 0 0 ... 0 0 0 0 0 hemispheres (k, j, i) int8 1MB 2 2 2 2 2 ... 1 1 1 1 1 Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: name: allen_mouse citation: Wang et al 2020, https://doi.org/10.1016/j.cell.2020.04.007 species: Mus musculus orientation: asr ``` ConfUSIus mostly operates on DataArray objects. Datasets are a convenient way to group several DataArrays together when they naturally belong together, as with the atlas above—pull out a single variable (e.g. `atlas["template"]`) to get a DataArray back. ## Basic Operations > [!NOTE] > > **New to Xarray?** > > If you are not yet familiar with Xarray, the [Xarray quick overview]() is the best place to start. Understanding indexing, selection, and broadcasting will make working with ConfUSIus much easier. A DataArray behaves like a NumPy array in most respects. Arithmetic and broadcasting work as usual, and thanks to NumPy's [array protocol](), NumPy functions can be called directly on a DataArray and return a DataArray back: ```python pwd_sqrt = np.sqrt(pwd) # np.sqrt dispatches to Xarray, returns a DataArray. ``` The main difference is that reductions and indexing use dimension names instead of axis positions: ```python mean_volume = pwd.mean("time") # reduction by name, not axis=0. ``` Indexing comes in two flavors: `.isel` indexes by integer position, like plain NumPy indexing, while `.sel` indexes by coordinate value: ```python first_50_volumes = pwd.isel(time=slice(0, 50)) # first 50 volumes, by position. shallow = pwd.sel(y=slice(0, 2.5)) # depth 0-2.5 mm, by coordinate. ``` > [!WARNING] > > **Multi-pose caveat** > > For recordings with a `pose` dimension, world coordinates become pose-dependent. Select a single pose before slicing in world space: > > ```python > pose0 = pwd.sel(pose=0).sel(y=slice(0, 2.5)) # OK > shallow = pose0.sel(y=slice(0, 2.5)) > ``` > > Likewise, pose-dependent `time` coordinates must be reduced to a single pose before label-based time selection. `.sel(time=slice(...))` is not supported for pose-dependent time, use `.isel(time=slice(...))` instead or select a single pose first. Scalar indexing (e.g. `.isel(k=0)`) drops the indexed dimension but keeps its coordinate as a scalar: ```python slice_movie = pwd.isel(k=0) slice_movie.dims # ('time', 'j', 'i') slice_movie.coords["z"] # scalar world coordinate: z = 0.0 mm, with the original coordinate metadata ``` Most geometry-sensitive ConfUSIus functions automatically restore such scalar-indexed spatial coordinates as singleton dimensions before validating the data. For example, `slice_movie` is treated as `(time, k, j, i)` with `k=1` when passed to registration or resampling APIs. Dimension-generic operations such as smoothing preserve the indexed shape. ## Creating VoxelData arrays from Raw Arrays Use `create_voxeldata` when you already have a NumPy, Dask, or array-like object and want to create a VoxelData array. This is useful when you have raw data from a custom acquisition system or a non-standard file format. The function will attach VoxelData dimensions, coordinates, and metadata. Dimensions can be supplied in any order; the result is canonicalized to native `(..., time, pose, k, j, i)` order: ```python import confusius as cf recording = cf.create_voxeldata( raw_power, # shape: (i, j, k, time) dims=("i", "j", "k", "time"), dt=0.6, # seconds spacing=(0.4, 0.05, 0.1), # world spacing in z/y/x order, in mm. attrs={"description": "Power Doppler from my system"}, ) ``` Single-slice recordings can be provided by omitting the relevant voxel dimension in the `dims` argument. ConfUSIus will automatically add the missing singleton dimension and its corresponding world coordinate. Note that you must still provide the world spacing for the missing dimension in `spacing`, since a fUSI slice still has a physical thickness in the missing dimension. ```python single_slice = cf.create_voxeldata( raw_power, # shape: (i, j, time) dims=("i", "j", "time"), dt=0.6, spacing=(0.4, 0.05, 0.1), # world spacing in z/y/x order, in mm. ) ``` Acquisition metadata that describes the whole recording belongs in the DataArray `attrs`. Coordinate metadata such as `units` is added automatically. ### Multi-Pose Data For multi-pose data, `create_voxeldata` also accepts a `pose` dimension, a 2D `(time, pose)` time coordinate, a tuple `t0` time origin, and a stacked `(npose, 4, 4)` `voxel_to_world` affine: ```python multipose = cf.create_voxeldata( raw_power, dims=("i", "j", "k", "pose", "time"), dt=2.4, t0=[0.0, 0.6, 1.2, 1.8], voxel_to_world=pose_affines, ) ``` See the [Multi-Pose Data guide]() for a full example. ### Beamformed IQ Data For beamformed IQ data, use `create_voxeldata` the same way and put IQ-specific metadata in `attrs`: ```python iq = cf.create_voxeldata( raw_iq, # shape: (time, j, i) dims=("time", "j", "i"), dt=1 / 500, spacing=(0.4, 0.05, 0.1), # world spacing in z/y/x order, in mm. attrs={ "transmit_frequency": 15.625e6, "beamforming_sound_velocity": 1540.0, }, ) ``` ## The `.fusi` Accessor Most of the functions you have seen so far (`cf.load`, `create_voxeldata`, etc.) are module-level, imported explicitly from `confusius`. An **accessor** is Xarray's mechanism for attaching a custom namespace directly to a DataArray or Dataset instead, so related functionality is reachable straight off the data—for example, `pwd.fusi.scale.db()`. This also keeps the boundary between Xarray's own API and library-specific functionality explicit; see [Xarray's guide to extending Xarray]() for the general mechanism. ConfUSIus registers two such accessors: `.fusi` on DataArrays, and `.atlas` on Datasets. `.atlas` is useful on atlas Datasets such as the one returned by `fetch_brainglobe_atlas`, and is covered separately in the [Atlases guide](). The rest of this section covers `.fusi`. Importing ConfUSIus registers the accessors automatically: ```python import xarray as xr import confusius as cf # Registers the .fusi and .atlas accessors. ``` The `.fusi` accessor is organized into focused sub-accessors, plus a set of global helper properties: | Accessor | Description | | --- | --- | | `.fusi.save` | Save data to file (NIfTI or Zarr), dispatching by extension. | | `.fusi.iq` | Process beamformed IQ into power Doppler or axial velocity volumes. | | `.fusi.scale` | Scaling transformations: decibel, log, and power scaling. | | `.fusi.affine` | Inspect and apply voxel-to-world and world-to-reference affine transforms. | | `.fusi.register` | Motion correction via volumewise image registration. | | `.fusi.extract` | Extract and reconstruct signals using spatial masks. | | `.fusi.plot` | Visualization with napari and carpet plots. | | `.fusi.connectivity` | Seed-based functional connectivity maps. | The sub-accessors offer the same functions as the module-level API, but with an intuitive syntax that allows quick operations directly on DataArray objects. They are designed to be used for easy exploration and quick analyses, while the module-level functions are available for more complex workflows where you might prefer explicit function calls for readability. ### Global Helpers Currently, three global helpers are available. Together they decompose the DataArray's voxel-to-world affine into origin, spacing, and direction—see [The voxel-to-world affine]() in The VoxelData Model for how the three combine: - `.fusi.spacing`, which returns the step size along each dimension as a dictionary: ```pycon >>> pwd.fusi.spacing {'time': 0.6, 'k': 0.4, 'j': 0.049, 'i': 0.091} ``` This is particularly useful for sanity-checking voxel sizes or sampling periods before passing data to functions that require regular spacing (e.g., temporal filters, affine registration). Spatial dimensions (`k`/`j`/`i`) always have their spacing derived from the voxel-to-world affine, even for a singleton dimension. A singleton `time` dimension falls back to the `volume_acquisition_duration` attribute when present. Otherwise, `None` is returned with a warning when spacing cannot be determined: non-uniform coordinates, a single coordinate point, or no coordinate at all. - `.fusi.origin`, which returns the coordinate values at the origin along each dimension as a dictionary: ```pycon >>> pwd.fusi.origin {'time': 0.299, 'z': 0.0, 'y': 5.664, 'x': -3.583} ``` This is typically used for computing the affine transformation corresponding to the world coordinates of the DataArray, for example when saving to NIfTI. For multi-pose data, select a scalar pose first. - `.fusi.direction`, which returns the world-space direction matrix for the spatial dimensions: ```pycon >>> pwd.fusi.direction array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) ``` This is the identity for axis-aligned data (the common case). For oblique data, the matrix has shape `(3, 3)`: columns are unit world-space directions for each voxel axis (`k`/`j`/`i`), rows correspond to world axes (`z`/`y`/`x`). A voxel coordinate that runs descending (e.g. after slicing with a negative step) flips the sign of its column, since the matrix tracks array position rather than the coordinate's own direction. For multi-pose data, select a scalar pose first. ### IQ Processing The `.fusi.iq` accessor lets you access the `process_iq_to_power_doppler` and `process_iq_to_axial_velocity` functions directly on a DataArray containing beamformed IQ data. Refer to the [Beamformed IQ guide]() for background IQ processing. ```python import dask import xarray as xr import confusius # Registers the .fusi accessor. iq = cf.load("iq.zarr") # Power Doppler with SVD clutter filtering (default). pwd = iq.fusi.iq.process_to_power_doppler( clutter_window_width=200, doppler_window_width=100, low_cutoff=40, ) # Axial velocity (in m/s). velocity = iq.fusi.iq.process_to_axial_velocity( clutter_window_width=200, velocity_window_width=100, ) (pwd, velocity) = dask.compute(pwd, velocity) # Compute both in a single pass. ``` ### Scaling The `.fusi.scale` accessor provides common scaling transformations: decibel, natural log, and power scaling. ```python import numpy as np pwd_db = pwd.fusi.scale.db() # Default factor=10 for power quantities. iq_db = np.abs(iq).fusi.scale.db(factor=20) # Use factor=20 for amplitude quantities. pwd_log = pwd.fusi.scale.log() pwd_sqrt = pwd.fusi.scale.power(exponent=0.5) ``` Because the accessor returns a DataArray, it chains naturally with standard Xarray operations: ```python pwd_db = pwd.where(pwd > 0).fusi.scale.db() ``` ### Registration The `.fusi.register` accessor provides easy access to the `register_volumewise` function for motion correction. ```python registered = pwd.fusi.register.volumewise(reference_time=0) ``` Pass `fixed` to register every frame to any spatial-only volume on the same grid instead of a frame index, for example the mean of a few low-motion frames: ```python fixed = pwd.isel(time=slice(0, 10)).mean("time") registered = pwd.fusi.register.volumewise(fixed=fixed) ``` Registration operates on a single spatial grid. For multi-pose data, select one pose first or consolidate poses before registering. By default, rigid registration allows translation and rotation. Pass `transform="translation"` for translation-only correction. For rigid registration, set the first three `optimizer_weights` values to `0` to freeze rotation. ### Affine Transforms The `.fusi.affine` accessor inspects and applies the voxel-to-world and world-to-reference affines described in [The VoxelData Model](). The voxel-to-world affine returned by `voxel_to_world` below is the same one [Global Helpers](<#global-helpers>) decomposes into `origin`/`spacing`/`direction`—use whichever form suits the task: the `(4, 4)` matrix for composing with other affines, the decomposed form for sanity-checking or constructing SimpleITK/ITK images. #### Reading and applying affines Read the DataArray's `voxel_to_world` affine, or apply a world-space affine to its coordinates with `apply`—either a `(4, 4)` array directly, or a string key naming a reference frame already stored in `attrs["affines"]`: ```python voxel_to_world = pwd.fusi.affine.voxel_to_world # (4, 4) array. # By key: re-express world coordinates in the "world_to_qform" reference frame; the # full affine (including any rotation) is absorbed into the DataArray's # VoxelToWorldIndex. registered_to_qform = pwd.fusi.affine.apply("world_to_qform") # By array: apply an arbitrary (4, 4) world-space affine directly. shifted = pwd.fusi.affine.apply(my_affine) ``` `bounding_box` gives the extent of the data in world space, enclosing the full extent of every voxel (half a voxel beyond the outermost voxel centers). It is computed from the affine alone (exact for oblique geometry, and one bounding box per pose when the geometry is pose-dependent): ```python bbox = pwd.fusi.affine.bounding_box # Dims (bound, component), e.g. ("max", "x"). ``` To replace a DataArray's voxel-to-world geometry outright (e.g. after computing a new affine by hand), use `set_voxel_to_world`: ```python pwd = pwd.fusi.affine.set_voxel_to_world(new_voxel_to_world) ``` #### Relating two DataArrays To compute the affine relating two DataArrays' world spaces through a named affine they both carry in `attrs["affines"]` (e.g. a shared `"world_to_lab"` key from two poses of the same acquisition), use `to`: ```python moving_to_fixed = moving.fusi.affine.to(fixed, via="world_to_lab") ``` #### Rebasing voxel coordinates to dense positions Cropping or striding a DataArray doesn't renumber its `k`/`j`/`i` coordinates—slicing `i=slice(3, 6)` keeps the labels `[3, 4, 5]`, not `[0, 1, 2]`: ```pycon >>> cropped = pwd.isel(i=slice(3, 6)) >>> cropped.coords["i"].values array([3, 4, 5]) >>> cropped.coords["x"].isel(k=0, j=0).values array([3., 4., 5.]) ``` That's correct as long as you keep indexing by *label*. But tools that assume dense, zero-based voxel indices (ITK, nilearn, ...) read array *position* 0 as voxel 0—which would silently place this cropped array at the wrong spot in world space. `reindex_voxels` relabels `k`/`j`/`i` back to `0, 1, ..., dim - 1` and adjusts the affine to compensate, so the world coordinates stay exactly the same: ```pycon >>> dense = cropped.fusi.affine.reindex_voxels() >>> dense.coords["i"].values array([0, 1, 2]) >>> dense.coords["x"].isel(k=0, j=0).values # unchanged array([3., 4., 5.]) ``` The reverse problem shows up when two DataArrays occupy the exact same world grid but carry different voxel labels—for example one was cropped from a larger array and the other was freshly built with dense labels. Since `.sel()`, arithmetic, and `xarray.align` all match by coordinate *label*, two such arrays won't align automatically despite describing the same physical space. `reindex_voxels_like` first verifies the two DataArrays already occupy the same world grid, then relabels one DataArray's voxel coordinates and affine to match the other's: ```python aligned = data.fusi.affine.reindex_voxels_like(reference) ``` ### Signal Extraction The `.fusi.extract` accessor provides access to signal extraction and reconstruction functions, making it easy to pass fUSI data to scikit-learn, pandas, or other tools that expect a 2D matrix of shape `(samples, features)`. #### Mask-based extraction `extract_with_mask` flattens all voxels selected by a boolean (or single-label integer) mask into a `space` dimension: ```python mask = cf.load("brain_mask.zarr") # signals has dims (time, space). signals = registered.fusi.extract.with_mask(mask) ``` For a quick round-trip, [`.unstack("space")`]() reconstructs the spatial dimensions within the bounding box of the mask. To reconstruct the full spatial volume, use `.fusi.extract.unmask()` with the original mask: ```python # reconstructed is a VoxelData array. reconstructed = signals.fusi.extract.unmask(mask) ``` #### Label-based extraction `extract_with_labels` aggregates signals by brain region using an integer label map. It accepts two label formats: - **Flat label map** `(k, j, i)`: each unique non-zero integer identifies a distinct, non-overlapping region (e.g., from an atlas annotation volume). - **Stacked mask format** `(mask, k, j, i)`: one layer per region, with values in `{0, region_id}`. Regions may overlap. This is the format returned by `get_masks`. ```python # Using a flat label map (e.g., atlas annotations). label_map = cf.load("atlas_labels.zarr") # region_signals has dims (time, region). region_signals = registered.fusi.extract.with_labels(label_map) # Use a different aggregation (default is "mean"). region_signals = registered.fusi.extract.with_labels(label_map, reduction="sum") ``` ### Functional Connectivity The `.fusi.connectivity` accessor fits seed-based correlation maps, wrapping `SeedBasedMaps`. Provide either a seed mask (voxels averaged into a seed signal) or a pre-computed seed signal directly: ```python seed_masks = cf.load("seed_masks.zarr") mapper = registered.fusi.connectivity.seed_map(seed_masks=seed_masks) # spatial correlation maps, one per seed, and the seed signals used. correlation_maps = mapper.maps_ seed_signals = mapper.seed_signals_ ``` ### Visualization The `.fusi.plot` accessor provides easy access to visualization functions for quick data inspection and quality control. For example, to display data in [napari](): ```python viewer, layer = registered.fusi.plot.napari(gamma=0.5) ``` Or to show standardized time series in a carpet plot, useful for quality control: ```python fig, ax = registered.fusi.plot.carpet(mask=mask) ``` ### Saving The `.fusi.save` accessor allows saving a DataArray to NIfTI or Zarr. For NIfTI, an accompanying fUSI-BIDS JSON sidecar is always written alongside, storing converted metadata fields, custom attributes, and timing fields derived from the `time` coordinate when available. Multi-pose data must be consolidated or split by pose first, since a single NIfTI file can only store one spatial affine: ```python # Creates sub-01_task-awake_pwd.nii.gz and sub-01_task-awake_pwd.json registered.fusi.save("sub-01_task-awake_pwd.nii.gz") ``` ## Complete Workflow Example The following example shows a typical fUSI analysis from raw IQ to saved results: ```python import confusius as cf from confusius.decomposition import PCA # 1. Load beamformed IQ data and corresponding brain mask. iq = cf.load("iq.zarr") brain_mask = cf.load("brain_mask.zarr") # 2. Process IQ into power Doppler. pwd = iq.fusi.iq.process_to_power_doppler( clutter_window_width=200, doppler_window_width=100, clutter_mask=brain_mask, low_cutoff=40, ) # 3. Inspect in napari. viewer, layer = pwd.fusi.plot.napari(gamma=0.5) # 4. Motion correction. registered = pwd.fusi.register.volumewise() # 5. Quick quality check with a carpet plot. fig, ax = registered.fusi.plot.carpet(mask=brain_mask) # 6. Save registered power Doppler to NIfTI with a fUSI-BIDS JSON sidecar. registered.fusi.save("sub-01_task-awake_pwd.nii.gz") # 7. Extract global signal using the (boolean) brain mask. global_signal = registered.fusi.extract.with_mask(brain_mask).mean("space") # 8. Denoise and standardize power Doppler signals. pwd_denoised = cf.signal.clean( registered, low_cutoff=0.01, confounds=global_signal, standardize_method="zscore" ) # 9. Decompose brain signals with PCA. pca = PCA(n_components=5, random_state=0) component_signals = pca.fit_transform(pwd_denoised) spatial_maps = pca.maps_ ``` ## API Reference For full parameter documentation, see the [Xarray Integration API reference](). # Multi-Pose Data ## What is Multi-Pose Imaging? In **multi-pose fUSI**, a probe is physically stepped to a series of positions along one spatial axis. At each position (a **pose**), one or more volumes are acquired. Stacking the poses together extends the field of view beyond what a single probe position can cover. The probe at each pose can image a 2D plane or a 3D volume, depending on the probe type: - **Linear probes** (e.g., standard linear probes): each pose yields a single 2D image (one elevation slice). Stepping across *N* poses and stacking gives a 3D volume of *N* elevation slices. - **2D probes** (e.g., matrix, RCA, or stacked linear probes): each pose already yields a 3D volume. Stepping across *N* poses concatenates these volumes into a larger 3D volume. Multiple fUSI systems support this approach, including Iconeus, EchoFrame, and AUTC. ConfUSIus represents multi-pose data with a `pose` dimension and per-pose affine transformations that record the world position of each pose. > [!WARNING] > > **Rotational sweeps are not yet supported** > > `consolidate_poses` requires a **purely translational** sweep, where the probe is shifted along one axis without rotating. Rotational sweeps (so-called tomographic acquisitions) are not yet supported and will raise a `ValueError`. ## The Multi-Pose Data Model A multi-pose VoxelData array follows the same dimension order as any other VoxelData array, `(..., time, pose, k, j, i)`, with `pose` sitting between `time` and the native voxel dimensions `k`/`j`/`i`. This reflects the fact that probe poses are typically acquired sequentially during a single recording. `pose` is not just an extra axis: it changes what the `z`/`y`/`x` world coordinates and the `time` coordinate mean, because both are per-pose rather than per-array. - **Geometry is per-pose.** Each pose has its own voxel-to-world affine, so the world coordinates `z`/`y`/`x` are pose-dependent with shape `(pose, k, j, i)`. Resolving a voxel to a single world position requires selecting a scalar `pose` first, e.g. `data.isel(pose=0).sel(z=1.2)`. See [The VoxelData Model]() for the full explanation of pose-dependent voxel-to-world geometry. - **Timing can be per-pose.** When poses are acquired sequentially rather than simultaneously, each pose is captured at a slightly different time. The `time` coordinate then holds each pose's own acquisition timestamp directly, shaped `(time, pose)` instead of the usual 1D `(time,)`—mirroring how `z`/`y`/`x` require a scalar `pose` selection before they resolve to a single value: ```python pwd.time # (time, pose) in seconds. pwd.isel(pose=0).time # (time,) in seconds, for pose 0 only. ``` This matters for slice timing correction (see [Slice Timing Correction](<#slice-timing-correction>) below), which accounts for the fact that different poses were not acquired simultaneously. Per-volume quantities such as confounds and sample masks are timed by the whole volume, not by one pose: their 1D `time` coordinate is the whole-volume time, as a consolidated array would have. These two rules—pose-dependent world coordinates, pose-dependent timing—drive everything else in this guide: how multi-pose data is loaded and built, what you can and cannot do with it directly, and what consolidation resolves. ## Loading and Constructing Multi-Pose Data ### Iconeus SCAN Files ConfUSIus reads two Iconeus SCAN formats: SCAN v1 (HDF5) and SCAN v2 (binary). The two formats keep multi-pose recordings in different forms: - SCAN v1 keeps one `pose` entry for each probe position. - SCAN v2 keeps the recording as one volume that IcoScan already consolidated. #### SCAN v1 (HDF5) When you load a multi-pose SCAN v1 file (`3Dscan` or `4Dscan` acquisition mode), the VoxelData array has a `pose` dimension. The example that follows shows a mouse recording with an **IcoPrime-4D MultiArray probe**. This probe has four linear arrays along the elevation axis, which give 4 elevation slices per pose. A motor moved the probe across a series of positions with a regular spacing: ```pycon >>> from confusius.io import load_scan >>> pwd = load_scan("sub-01_task-awake_pwd.scan") >>> pwd Size: 568MB dask.array Coordinates: time (time, pose) float64 13kB 0.4 2.05 0.95 1.5 ... 899.7 898.6 899.1 * pose (pose) int64 32B 0 1 2 3 * k (k) int64 32B 0 1 2 3 * j (j) int64 736B 0 1 2 3 ... 89 90 91 * i (i) int64 944B 0 1 2 3 ... 116 117 z (pose, k, j, i) float64 1MB ... y (pose, k, j, i) float64 1MB ... x (pose, k, j, i) float64 1MB ... Attributes: affines: {} device_serial_number: ASAO0000 software_version: IcoScan v.1.9.0 iconeus_scan_mode: 4Dscan iconeus_subject: sub-01 iconeus_session: task-awake iconeus_scan: sub-01_task-awake_pwd ... ``` The motor moved the probe across 4 positions. Each position gives 4 elevation slices. After [consolidation](<#pose-consolidation>), the array has 16 slices. #### SCAN v2 (binary) IcoScan writes multi-pose SCAN v2 recordings that are already consolidated, if possible. Thus, `load_scan` gives an array without a `pose` dimension. The `k` dimension holds all the slices of the volume. The example that follows shows a SCAN v2 recording with an IcoPrime-4D MultiArray probe: ```pycon >>> from confusius.io import load_scan >>> pwd = load_scan("sub-02_task-awake_pwd.scan") >>> pwd Size: 1GB dask.array Coordinates: * time (time) float64 8kB 2.2 4.6 7.0 ... 2.395e+03 2.397e+03 2.4e+03 * k (k) int64 128B 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 slice_time (time, k) float64 128kB 0.4 2.2 1.0 ... 2.399e+03 2.399e+03 * j (j) int64 736B 0 1 2 3 4 5 6 7 8 ... 83 84 85 86 87 88 89 90 91 * i (i) int64 816B 0 1 2 3 4 5 6 7 8 ... 94 95 96 97 98 99 100 101 * z (k, j, i) float64 1MB ... * y (k, j, i) float64 1MB ... * x (k, j, i) float64 1MB ... Attributes: probe_model: IcoPrime 4D MultiArray iconeus_scan_format: v2 iconeus_scan_mode: 4Dscan ... ``` The 16 slices come from 4 linear arrays (2.1 mm apart) and 4 motor steps (0.525 mm apart). The `time` coordinate gives the end time of each volume, with a period of 2.4 s. The `slice_time` coordinate gives the end time of each slice, with dims `(time, k)`. The 4 arrays acquire at the same time. Thus, slices that are 4 positions apart have the same timestamp. In the first volume, the slices end at 0.4, 2.2, 1.0, and 1.6 s, and this pattern occurs 4 times. [Slice Timing Correction](<#slice-timing-correction>) can be used to resample the slices onto a shared reference time. ### Reloading from Zarr Saving unconsolidated multi-pose data to **Zarr** preserves the `pose` dimension and the per-pose affine stack (see [Saving](<#saving>) below). `cf.load` round-trips this transparently: reloading a Zarr store written by `cf.save` rebuilds the pose-dependent `VoxelToWorldIndex` from `attrs["voxel_to_world"]`, so a reloaded array is indistinguishable from the one that was saved. ```python import confusius as cf pwd = cf.load("sub-01_task-awake_pwd_multipose.zarr") # dims: (time, pose, k, j, i) ``` ### Other Systems For fUSI systems without a dedicated loader, multi-pose data must be assembled manually, in one of two ways: - **From a raw array**, using `create_voxeldata` with a `pose` dimension, an `(npose, 4, 4)` `voxel_to_world` affine stack, and, if poses were acquired sequentially, a 1D `t0` array giving each pose's own time origin: ```python import confusius as cf multipose = cf.create_voxeldata( raw_power, # shape: (i, j, k, pose, time) dims=("i", "j", "k", "pose", "time"), dt=0.6, # seconds, shared across poses. t0=[0.0, 0.15, 0.3, 0.45], # per-pose time origin, in seconds. voxel_to_world=pose_affines, # shape: (4, 4, 4) ) ``` - **From independently loaded single-pose arrays**, using `stack_poses` to combine them into one pose-dependent array. ```python from confusius.multipose import stack_poses poses = [cf.load(f"sub-01_pose-{i:02d}_pwd.nii.gz") for i in range(4)] multipose = stack_poses(poses) # dims: (time, pose, k, j, i) ``` If the individual poses were acquired sequentially rather than simultaneously and carry different `time` values, `stack_poses` automatically produces a pose-dependent `(time, pose)` coordinate instead of a shared 1D `time`. ## Working with Multi-Pose Arrays Most Xarray operations that don't touch world coordinates or timing work on multi-pose arrays exactly as on any other VoxelData array—reductions, arithmetic, `.isel`, mask extraction, and so on all treat `pose` as an ordinary dimension. Operations that resolve *positions* or *timestamps*, however, need a scalar `pose` first, per [The Multi-Pose Data Model](<#the-multi-pose-data-model>) above: ```python pose0 = pwd.sel(pose=0) shallow = pose0.sel(z=slice(0, 2.5)) # world-coordinate selection now works. subset = pose0.sel(time=slice(10, 60)) # time is now 1D, .sel(time=...) works too. ``` A number of ConfUSIus functions go further and require multi-pose data to be reduced to a single spatial grid entirely, since they operate on one voxel-to-world affine: | Operation | Requires | | --- | --- | | Registration ([registration guide]()) | Single pose, or [consolidated](<#pose-consolidation>) data | | Saving to NIfTI (single affine per file) | Single pose, or consolidated data—see [Saving](<#saving>) | ## Slice Timing Correction Because poses in a sequential acquisition are not captured simultaneously, comparing voxels across poses at the same nominal `time` index compares samples taken at slightly different real times. `correct_slice_timings` resamples each pose's time series onto a shared, per-volume reference time, removing this offset. It accepts either unconsolidated data with a pose-dependent `(time, pose)` `time` coordinate, or already-[consolidated](<#pose-consolidation>) data with a `slice_time` coordinate: ```python from confusius.multipose import correct_slice_timings corrected = correct_slice_timings(pwd) # time coordinate becomes a plain 1D index. ``` ## Pose Consolidation `consolidate_poses` merges the `pose` dimension and the sweep spatial dimension into a single axis with physically meaningful coordinates, producing a VoxelData array with a single, non-pose-dependent voxel-to-world affine. `consolidate_poses` performs the following steps: 1. Detect the swept voxel dimension (`sweep_dim`) from the per-pose affines: match the pose-translation direction against each voxel dimension's world-space direction, and take the best-aligned one. 2. Read the per-pose affines to compute the world position of every `(pose, sweep_dim)` voxel. 3. Find the precise sweep direction via SVD of all these voxel positions, and project each voxel onto that axis. 4. Check that the resulting positions form a regular grid. 5. Reindex the data in ascending position order, replacing `pose` and `sweep_dim` with a single consolidated coordinate in world space. ```pycon >>> import confusius as cf >>> pwd = cf.load("sub-01_task-awake_pwd.scan") >>> volume = cf.multipose.consolidate_poses(pwd) >>> volume Size: 568MB array([...]) Coordinates: * time (time) float64 3kB 2.05 4.25 6.45 8.65 ... 895.2 897.5 899.7 * k (k) int64 128B 0 1 2 3 ... 13 14 15 slice_time (time, k) float64 52kB 0.4 2.05 0.95 1.5 ... 899.7 898.6 899.1 * j (j) int64 736B 0 1 2 3 ... 89 90 91 * i (i) int64 944B 0 1 2 3 ... 116 117 z (k, j, i) float64 1MB ... y (k, j, i) float64 1MB ... x (k, j, i) float64 1MB ... Attributes: affines: {} device_serial_number: ASAO0000 software_version: IcoScan v.1.9.0 iconeus_scan_mode: 4Dscan iconeus_subject: sub-01 iconeus_session: task-awake iconeus_scan: sub-01_task-awake_pwd ... ``` 4 poses × 4 slices = 16 consolidated z positions, spanning −4.95 to 2.93 mm in world space. The pose-dependent `time` coordinate is reduced to a single whole-volume `time` and a `slice_time` coordinate with dims `(time, k)`: each slice retains the timestamp of the pose it came from. After consolidation, the per-pose affine stack is reduced to a single `(4, 4)` matrix representing the consolidated volume's orientation in world space. > [!WARNING] > > **Regularity requirement** > > `consolidate_poses` will raise a `ValueError` if the consolidated positions are not regularly spaced within a relative tolerance of 1% (default `rtol=0.01`). This check ensures uniform voxel spacing, which is required for registration and NIfTI export. Non-uniform spacing typically indicates motor positioning errors. ## Saving ### After Consolidation Once consolidated, a multi-pose array is still a VoxelData array and can be saved to any format: ```python import confusius as cf anat = cf.load("sub-01_acq-anat_pwd.scan") volume = cf.multipose.consolidate_poses(anat) # Save to NIfTI (creates .nii.gz and a matching fUSI-BIDS JSON sidecar). volume.fusi.save("sub-01_acq-anat_pwd.nii.gz") # Or to Zarr. volume.to_zarr("sub-01_acq-anat_pwd.zarr") ``` ### Without Consolidation Non-consolidated data can be saved to **Zarr** directly, preserving the `pose` dimension and all per-pose affines: ```python data.to_zarr("sub-01_acq-anat_pwd_multipose.zarr") ``` Saving non-consolidated data to **NIfTI** is not straightforward because NIfTI stores a single affine per file. If you need NIfTI output before consolidating (e.g., for per-pose slice timing correction), save each pose as a separate file: ```python for i, pose in enumerate(anat.pose.values): # The pose entity is defined in the fUSI-BIDS specification. anat.sel(pose=pose).fusi.save(f"sub-01_acq-anat_pose-{i:02d}_pwd.nii.gz") ``` # Processing Beamformed IQ Data ## Introduction After [loading your beamformed IQ data](), the next step in most fUSI workflows is processing it to extract hemodynamic signals. This guide explains how to use ConfUSIus to transform beamformed IQ data into measures like B-mode, power Doppler, and axial velocity. ## Why Process IQ Data? Beamformed IQ data contains signals from all moving and stationary structures in the imaging field: - **Tissue signals**: Strong echoes from slow-moving tissue (\~40-60 dB stronger than blood). - **Blood signals**: Weak echoes from fast-moving red blood cells. Beamformed IQ signals can be processed to extract different types of information depending on your application: - **Structural imaging** (B-mode): Use amplitude information to visualize tissue anatomy. - **Blood volume** (power Doppler): Separate blood from tissue to quantify cerebral blood volume changes. - **Blood flow velocimetry** (color Doppler): Estimate blood flow speed and direction. - **Tissue motion**: Track tissue displacement (e.g., cardiac pulsatility, respiration). This guide covers the **IQ processing functions in ConfUSIus**, which focus primarily on extracting blood volume information for fUSI applications. The typical workflow involves: 1. **Filtering** to isolate signals of interest (e.g., blood). 2. **Computing derived measures** from the filtered IQ signals (e.g., power Doppler, velocity). For fUSI specifically, this means using clutter filtering to separate blood from tissue, then computing measures like power Doppler or axial velocity. The result is a series of volumes showing changes in cerebral blood volume or flow. ## Understanding Your IQ Data Before processing, it's important to understand what beamformed IQ data represents and how it's structured. ### Quick Primer on Beamformed IQ Signals Most fUSI users work with beamformed IQ signals rather than raw radio-frequency (RF) signals. Indeed, ultrasound systems emit pulses at high frequencies (typically 1–30 MHz). The received echoes oscillate at these same high frequencies. However, for imaging we only care about: - **Amplitude**: How strong is the reflection (related to tissue properties)? - **Phase**: How much has the phase shifted (related to motion via Doppler effect)? IQ demodulation extracts this amplitude and phase information while discarding the high-frequency carrier, allowing for a significant reduction in sampling rate and thus drastically reducing data volume. A beamformed IQ sample can be represented as \\\[ \\text{IQ}(i, j, k, t) = A(i, j, k, t) \\, e^{j\\phi(i, j, k, t)}, \\\] where \\(A\\) is amplitude, \\(\\phi\\) is phase, \\((i, j, k)\\) are spatial coordinates, and \\(t\\) is the **slow-time** index. > [!NOTE] > > **Slow time vs. fast time** > > - **Fast time**: RF signals sampled at MHz rates. > - **Slow time**: Beamformed IQ signals sampled once per pulse, at kHz rates. For single plane-wave imaging, the sampling rate of the beamformed IQ signals equals the pulse repetition frequency (PRF), typically kHz rates. With **compound imaging** (common in fUSI), multiple plane waves are emitted at different angles and their echoes are combined to form a single compound volume[^/dev/user-guide/beamformed-iq/#montaldo2009]. In this case, the effective sampling rate, the **compound frequency**, is the PRF divided by the number of angles, typically hundreds of hertz. ### Expected Data Structure Beamformed IQ data in ConfUSIus is a complex-valued, [VoxelData model]() DataArray, with `time` as the slow-time dimension. This dimension order keeps IQ processing's sliding-window operations efficient. Beyond the VoxelData requirements, axial velocity estimation also requires the `transmit_frequency` and `beamforming_sound_velocity` DataArray attributes. `create_voxeldata` accepts both as optional arguments, and `process_iq_to_axial_velocity` checks for their presence internally via `validate_voxeldata`. Here is what a valid IQ DataArray looks like once loaded: ```pycon >>> import confusius as cf >>> >>> iq = cf.load("sub-01_task-rest_iq.zarr") >>> iq Size: 2GB dask.array, shape=(50000, 1, 118, 52), dtype=complex64, chunksize=(1000, 1, 118, 52), chunktype=numpy.ndarray> Coordinates: * time (time) float64 400kB 0.0 0.002 0.004 ... 99.99 100.0 100.0 * k (k) int64 8B 0 * j (j) int64 944B 0 1 2 3 4 5 6 7 ... 110 111 112 113 114 115 116 117 * i (i) int64 416B 0 1 2 3 4 5 6 7 8 9 ... 43 44 45 46 47 48 49 50 51 * z (k, j, i) float64 49kB dask.array * y (k, j, i) float64 49kB dask.array * x (k, j, i) float64 49kB dask.array Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: compound_sampling_frequency: 500.0 pulse_repetition_frequency: 15000.0 transmit_frequency: 15625000.0 beamforming_sound_velocity: 1540.0 ``` If your acquisition system isn't directly supported by ConfUSIus, see [Other Systems]() for how to wrap a custom complex array as an IQ DataArray. > [!NOTE] > > **Finding metadata values** > > If you're unsure about the correct values: > > - `volume_acquisition_duration`: This is usually equal to the number of plane wave angles divided by the pulse repetition frequency. > - `transmit_frequency`: Found in your probe specifications or acquisition settings. Generally around 5-10 MHz for clinical probes, and 12-20 MHz for high-frequency probes used in small animal imaging. > - `beamforming_sound_velocity`: Typically 1540 m/s for brain tissues, but may vary with temperature and tissue type. ## How ConfUSIus Processes IQ Data ConfUSIus offers a flexible framework for processing IQ data using sliding windows. At its core is the `process_iq_blocks` function, which allows you to run any user-defined function on sliding windows of IQ signals along the temporal dimension. This base function is then wrapped by specialized processing functions for common fUSI applications. ### Generic IQ Processing The `process_iq_blocks` function is the foundation of IQ processing in ConfUSIus. It provides a generic interface for applying any user-defined function to "blocks" of IQ data using sliding temporal windows. You will generally not call this function directly, but it is worth knowing it exists if you ever need to implement a custom processing workflow (for example, when implementing new clutter filters). ``` --- config: layout: elk --- flowchart LR IQ -->|Window 1| F1{{"User Function"}} IQ --> ellipsis{{"..."}} IQ -->|Window n| F2{{"User Function"}} F1 -->|"Output Volume(s)"| output F2 -->|"Output Volume(s)"| output IQ@{ shape: processes, label: "Input IQ Volumes" } output@{ shape: processes, label: "Stacked Output Volumes" } ellipsis@{ shape: text} ``` > [!NOTE] > > Contrary to most ConfUSIus functions, `process_iq_blocks` is a low-level function that expects a [Dask array]() instead of an Xarray DataArray. This is because it operates on IQ data directly and returns processed data without metadata and coordinate handling. The wrapper functions for power Doppler and axial velocity handle the Xarray integration and metadata management for you. ### Convenience Wrappers The specialized functions `process_iq_to_power_doppler` and `process_iq_to_axial_velocity` wrap `process_iq_blocks` to implement a **nested sliding window** approach. `process_iq_to_bmode` is similar but uses a single window level since no clutter filtering is involved: 1. **Outer windows (clutter filtering)**: A first sliding window is used for clutter filtering. Within each window, the clutter filter is applied to separate blood from tissue signals. 2. **Inner windows (derived measure computation)**: Inside each clutter-filtered window, a second sliding window runs to compute the desired measure (power Doppler or axial velocity). Multiple output volumes can be extracted from each clutter window. ``` --- config: layout: elk --- flowchart LR IQ -->|"Outer Window 1"| CF1{{"Clutter Filter"}} IQ --> ellipsis{{"..."}} IQ -->|"Outer Window n"| CFn{{"Clutter Filter"}} CF1 -->|"Inner Window 1"| C1{{"Compute"}} CF1 --> e1{{"..."}} CF1 -->|"Inner Window m"| C2{{"Compute"}} CFn -->|"Inner Window 1"| C3{{"Compute"}} CFn --> e2{{"..."}} CFn -->|"Inner Window m"| C4{{"Compute"}} C1 -->|"Output Volume"| output C2 -->|"Output Volume"| output C3 -->|"Output Volume"| output C4 -->|"Output Volume"| output IQ@{ shape: processes, label: "Input IQ Volumes" } output@{ shape: processes, label: "Stacked Output Volumes" } ellipsis@{ shape: text} e1@{ shape: text} e2@{ shape: text} ``` This nested approach allows independent control over: - **Clutter filtering temporal resolution**: Controlled by `clutter_window_width` and `clutter_window_stride`. - **Measure computation temporal resolution**: Controlled by `doppler_window_width` / `velocity_window_width` and their corresponding strides. ### Memory and Performance Optimization Beamformed IQ datasets are typically very large (tens to hundreds of GB). ConfUSIus relies on the Dask backend for Xarray to enable out-of-core processing, allowing you to work with datasets larger than available RAM. Dask uses **lazy evaluation**, meaning data stays on disk until you explicitly compute results. This allows you to build complex processing pipelines that only execute when needed. For optimal performance, **always start a Dask distributed `Client`** before running computations. Even for local processing (single machine), the distributed scheduler provides significantly better performance than the default scheduler, along with task monitoring and a web-based dashboard to track progress: ```python from dask.distributed import Client import xarray as xr import confusius # Start a local Dask cluster. client = Client() # The client prints a dashboard link for monitoring progress. # Example: http://127.0.0.1:8787/status # Load IQ data (metadata only, no data in memory yet). iq = cf.load("large_iq_data.zarr") # Build computation graph (still no execution). pwd = iq.fusi.iq.process_to_power_doppler(low_cutoff=40) # Execute and save results (data processed in chunks, written to disk). pwd.to_zarr("power_doppler.zarr") ``` > [!TIP] > > **Customizing the cluster** > > You can customize cluster resources based on your system: > > ```python > # Limit workers and memory. > client = Client( > n_workers=4, # Number of worker processes. > threads_per_worker=2, # Threads per worker. > memory_limit="8GB", # Memory limit per worker. > ) > ``` > > See the [Dask documentation]() for more configuration options. > [!TIP] > > **Computing multiple measures in a single pass** > > Since `process_to_power_doppler`, `process_to_axial_velocity`, and `process_to_bmode` all return lazy Dask-backed arrays, you can compute them **simultaneously** using [`dask.compute`](): > > ```python > import dask > import confusius > > pwd = iq.fusi.iq.process_to_power_doppler(...) > velocity = iq.fusi.iq.process_to_axial_velocity(...) > bmode = iq.fusi.iq.process_to_bmode(...) > > # Compute all three with a single pass over the IQ data. > pwd, velocity, bmode = dask.compute(pwd, velocity, bmode) > ``` > > Calling `.compute()` on each separately would scan through the IQ file multiple times. `dask.compute` merges the task graphs and reads the data only once, which can significantly cut processing time for large datasets. ## Clutter Filtering Clutter filtering is an essential step in fUSI processing to separate weak blood signals from strong tissue signals. Indeed, blood signals from red blood cells are typically 40-60 dB (100-1000×) weaker than tissue signals, making effective clutter rejection essential for detecting hemodynamic changes. ### How Clutter Filtering Works Clutter filters exploit the different spatiotemporal characteristics of tissue and blood signals: - **Tissue signals**: Spatially coherent across large regions, with low temporal frequencies from slow tissue motion. - **Blood signals**: Spatially localized to vascular structures, with higher temporal frequencies from faster blood flow. The challenge is that tissue motion (especially in awake subjects) can create Doppler frequencies that overlap with blood flow signals, making simple frequency-based separation insufficient. ### Available Filter Methods ConfUSIus provides several clutter filtering methods, each with different trade-offs: Singular Value Decomposition (SVD) filters exploit both spatial and temporal information to separate tissue from blood[^/dev/user-guide/beamformed-iq/#demene2015]. The SVD decomposes IQ signals into orthogonal components (modes) sorted by energy. Tissue motion, being spatially coherent and energetic, concentrates in the highest-energy modes. Blood flow, being spatially incoherent and weak, distributes across lower-energy modes. By removing the top singular vectors, tissue clutter is suppressed while preserving blood signals. **Static filter:** - **`svd_indices`**: Removes a fixed number of singular vectors across all windows. Simple and widely used, but assumes constant motion levels throughout the acquisition. **Adaptive filters:**[^/dev/user-guide/beamformed-iq/#baranger2018] - **`svd_energy`**: Adaptively determines the threshold based on individual singular vector energies. Adjusts to varying motion levels, but its interpretation can be less intuitive. - **`svd_cumulative_energy`**: Adaptively determines the threshold based on cumulative energy distribution. More robust to motion variations, but removes global energy variations across windows (similar to global signal regression). > [!NOTE] > > **Static vs. adaptive SVD filters** > > **Static SVD filters** (`svd_indices`) are the standard method used in the vast majority of fUSI studies[^/dev/user-guide/beamformed-iq/#ferrier2020] [^/dev/user-guide/beamformed-iq/#rabut2020] [^/dev/user-guide/beamformed-iq/#brunner2021] [^/dev/user-guide/beamformed-iq/#nunez-elizalde2022]. They are well-tested and effective for typical imaging conditions. > > However, static filters assume constant tissue energy levels throughout the acquisition. This assumption breaks down in paradigms with strong, variable tissue motion, such as awake preclinical imaging. In these cases, **adaptive clutter filters** have shown promise by adjusting thresholds to each window's characteristics[^/dev/user-guide/beamformed-iq/#lemeurdiebolt2025] [^/dev/user-guide/beamformed-iq/#bertolo2023]. > > **Important caveats for adaptive filters:** > > - Much less tested than static filters in the fUSI literature. > - Their impact on filtered signals can complicate interpretation. > - `svd_energy` may over-filter signals leading to drops in power Doppler intensity during motion periods. > - `svd_cumulative_energy` removes global energy variations (like global signal regression), which is a strong denoising step only appropriate when studying local effects. > > **Recommendation:** > > - Use **`svd_indices`** (static) as the default choice for most fUSI applications. > - Consider **adaptive filters** only when imaging conditions involve high, variable motion levels (e.g., awake subjects) and you are studying local hemodynamic effects rather than global changes. Temporal high-pass Butterworth filter that removes low-frequency components. Uses only temporal frequency information, not spatial characteristics. > [!WARNING] > > **Limited use for fUSI** > > Butterworth filters alone are **inadequate for fUSI** because they cannot separate tissue and blood when Doppler frequencies overlap (common during motion), and they remove slow blood flows in capillaries and small vessels. > > May be used as a pre-processing step before SVD filtering[^/dev/user-guide/beamformed-iq/#brunner2020] [^/dev/user-guide/beamformed-iq/#nunez-elizalde2022] [^/dev/user-guide/beamformed-iq/#lemeurdiebolt2025] or for specialized applications like tissue motion tracking. ### Filter Parameters The interpretation of cutoff parameters depends on the filter method: | Filter Method | `low_cutoff` | `high_cutoff` | | --- | --- | --- | | `svd_indices` | Remove components with index \< `low_cutoff` (high-energy tissue, typically 40-100). | Remove components with index ≥ `high_cutoff` (low-energy noise). | | `svd_energy` | Remove components with energy \< `low_cutoff` (low-energy noise). | Remove components with energy \> `high_cutoff` (high-energy tissue). | | `svd_cumulative_energy` | Remove components with cumulative energy \< `low_cutoff` (low-energy noise). | Remove components with cumulative energy \> `high_cutoff` (high-energy tissue). | | `butterworth` | Remove frequencies below this value (Hz). | Remove frequencies above this value (Hz). | ### Computing the Cumulative Energy Threshold Setting `high_cutoff` for `svd_cumulative_energy` is non-trivial because cumulative energy values depend on the amount of tissue motion and change from window to window. `compute_svd_cumulative_energy_threshold` automates this: it iterates over sliding temporal windows, computes the cumulative eigenvalue spectrum for each window (accumulating from low-energy to high-energy components), and returns the minimum cumulative energy at a given singular-value index across all windows. Using the minimum ensures that the chosen cutoff does not over-filter any window. ```python import confusius as cf iq = cf.load("recording.zarr")["iq"] brain_mask = cf.load("brain_mask.zarr")["mask"] # Compute the minimum cumulative energy at singular value index 40 across all windows. threshold = cf.iq.compute_svd_cumulative_energy_threshold( iq, singular_value_index=40, clutter_mask=brain_mask, window_width=200, ).compute() # Apply the cumulative energy filter using the computed threshold. pwd = iq.fusi.iq.process_to_power_doppler( filter_method="svd_cumulative_energy", clutter_mask=brain_mask, high_cutoff=threshold, clutter_window_width=200, ) ``` ### Clutter Masks for SVD Filters For SVD-based filters, you can provide a spatial mask identifying tissue regions. This improves filter performance by computing tissue subspaces only from masked voxels[^/dev/user-guide/beamformed-iq/#lemeurdiebolt2025]: ```python import xarray as xr # Load a brain mask (e.g., from an anatomical atlas or segmentation). brain_mask = cf.load("brain_mask.zarr") # Use the mask for clutter filtering. pwd = iq.fusi.iq.process_to_power_doppler( filter_method="svd_indices", clutter_mask=brain_mask, low_cutoff=50, ) ``` ## Computing Derived Measures ConfUSIus can compute several derived measures from beamformed IQ data. In fUSI, the primary measures are **B-mode** (tissue anatomy), **power Doppler** (cerebral blood volume changes), and **axial velocity** (blood flow speed and direction). > [!NOTE] > > **When clutter filtering is not needed** > > While clutter filtering is essential for isolating blood signals in fUSI, it can be skipped for applications focused on tissue motion rather than blood flow. > > - **Tissue velocity estimation**: Computing axial velocity without clutter filtering captures tissue motion, useful for extracting physiological signals like cardiac pulsatility and respiration[^/dev/user-guide/beamformed-iq/#zucker2025]. > - **Motion tracking**: Brain tissue motion can also be used to estimate animal movement during awake imaging[^/dev/user-guide/beamformed-iq/#lemeurdiebolt2025]. > > To disable clutter filtering, omit both the `low_cutoff` and `high_cutoff` parameters. ### Power Doppler Power Doppler allows estimation of cerebral blood volume by integrating the Doppler power spectrum over time. It is the most common fUSI readout and is ideally suited for mapping functional brain activity. #### Computing Power Doppler To compute power Doppler signals from beamformed IQ data, you may use either the `confusius.iq.process_iq_to_power_doppler` function or the corresponding Xarray accessor method. ```python from dask.distributed import Client import xarray as xr import confusius as cf # Start a local Dask cluster for optimal performance. client = Client() # Load IQ data. iq = cf.load("sub-01_task-rest_iq.zarr") # Process to power Doppler using SVD filtering. pwd = cf.iq.process_iq_to_power_doppler( iq, clutter_window_width=200, # Use 200 volumes per clutter filter window. doppler_window_width=100, # Integrate power in nested windows of 100 volumes. filter_method="svd_indices", # Use the static SVD filter. low_cutoff=50, # Remove 50 strongest singular vectors. ) ``` ```python from dask.distributed import Client import xarray as xr import confusius # Start a local Dask cluster for optimal performance. client = Client() # Load IQ data. iq = cf.load("sub-01_task-rest_iq.zarr") # Process to power Doppler using SVD filtering. pwd = iq.fusi.iq.process_to_power_doppler( clutter_window_width=200, # Use 200 volumes per clutter filter window. doppler_window_width=100, # Integrate power over 100 volumes. filter_method="svd_indices", # Use static SVD filter. low_cutoff=50, # Remove 50 strongest singular vectors. ) ``` The resulting power Doppler DataArray will be backed by a Dask array (even if the input wasn't Dask-backed) and rely on lazy evaluation. ```pycon >>> pwd Size: 123kB dask.array Coordinates: * time (time) float64 160B 0.0 0.2 0.4 0.6 0.8 1.0 ... 3.0 3.2 3.4 3.6 3.8 * k (k) int64 8B 0 * j (j) int64 256B 0 1 2 3 4 5 6 7 8 9 ... 23 24 25 26 27 28 29 30 31 * i (i) int64 192B 0 1 2 3 4 5 6 7 8 9 ... 15 16 17 18 19 20 21 22 23 * z (k, j, i) float64 6kB 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 * y (k, j, i) float64 6kB 4.656 4.656 4.656 4.656 ... 7.756 7.756 7.756 * x (k, j, i) float64 6kB -2.671 -2.571 -2.471 ... -0.571 -0.471 -0.371 Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: compound_sampling_frequency: 500.0 pulse_repetition_frequency: 15000.0 transmit_frequency: 15625000.0 beamforming_sound_velocity: 1540.0 units: a.u. long_name: Power Doppler intensity cmap: gray clutter_filters: Index-based SVD [50, +inf[ clutter_filter_window_duration: 0.4 clutter_filter_window_stride: 0.3999999999999999 power_doppler_integration_duration: 0.19999999999999996 ``` To actually compute the power Doppler values and load them into memory, you must call `.compute()`: ```pycon >>> pwd = pwd.compute() >>> pwd Size: 123kB array([[[[1.07897822, 1.233959 , 1.26681306, ..., 1.10582708, 1.15312583, 1.01522477], [1.03980397, 1.1777878 , 1.34812695, ..., 1.14753257, 1.18028614, 0.99100816], ... ... ``` > [!NOTE] > > **Processing parameters** > > For detailed documentation of all parameters (window widths, strides, filter methods, cutoffs, etc.), see the API reference for `process_iq_to_power_doppler`. ### Axial Velocity Axial velocity estimation quantifies the speed and direction of blood flow along the ultrasound beam axis using autocorrelation-based methods. #### Computing Axial Velocity To compute axial velocity from beamformed IQ data, you may use either the `confusius.iq.process_iq_to_axial_velocity` function or the corresponding Xarray accessor method. ```python from dask.distributed import Client import xarray as xr import confusius as cf # Start a local Dask cluster for optimal performance. client = Client() # Load IQ data. iq = cf.load("sub-01_task-rest_iq.zarr") # Process to axial velocity using SVD filtering. velocity = cf.iq.process_iq_to_axial_velocity( iq, clutter_window_width=50, velocity_window_width=50, filter_method="svd_indices", low_cutoff=50, lag=1, # Autocorrelation lag (volumes). ) ``` ```python from dask.distributed import Client import xarray as xr import confusius # Start a local Dask cluster for optimal performance. client = Client() # Load IQ data. iq = cf.load("sub-01_task-rest_iq.zarr") # Process to axial velocity using SVD filtering. velocity = iq.fusi.iq.process_to_axial_velocity( clutter_window_width=50, velocity_window_width=50, filter_method="svd_indices", low_cutoff=50, lag=1, # Autocorrelation lag (volumes). ) ``` The resulting axial velocity DataArray will be backed by a Dask array (even if the input wasn't Dask-backed) and rely on lazy evaluation. ```pycon >>> velocity Size: 123kB dask.array Coordinates: * time (time) float64 160B 0.0 0.2 0.4 0.6 0.8 1.0 ... 3.0 3.2 3.4 3.6 3.8 * k (k) int64 8B 0 * j (j) int64 256B 0 1 2 3 4 5 6 7 8 9 ... 23 24 25 26 27 28 29 30 31 * i (i) int64 192B 0 1 2 3 4 5 6 7 8 9 ... 15 16 17 18 19 20 21 22 23 * z (k, j, i) float64 6kB 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 * y (k, j, i) float64 6kB 4.656 4.656 4.656 4.656 ... 7.756 7.756 7.756 * x (k, j, i) float64 6kB -2.671 -2.571 -2.471 ... -0.571 -0.471 -0.371 Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: compound_sampling_frequency: 500.0 pulse_repetition_frequency: 15000.0 transmit_frequency: 15625000.0 beamforming_sound_velocity: 1540.0 units: m/s long_name: Axial velocity cmap: coolwarm clutter_filters: Index-based SVD [50, +inf[ clutter_filter_window_duration: 0.4 clutter_filter_window_stride: 0.3999999999999999 axial_velocity_lag: 1 axial_velocity_spatial_kernel: 3 axial_velocity_integration_duration: 0.19999999999999996 ``` > [!NOTE] > > **Processing parameters** > > For detailed documentation of all parameters (lag, estimation method, spatial kernel, etc.), see the API reference for `process_iq_to_axial_velocity`. #### Interpreting Velocity Velocity values are returned in **meters per second** and correspond to the speed of scatterers along the ultrasound beam. However, the **sign convention is scanner- dependent**: it depends on choices such as the analytic-signal convention, the IQ demodulation, and other parameters. Before interpreting the sign, verify it on your scanner with a known flow direction (e.g., large arteries flowing from the circle of Willis toward the probe) or a phantom, or another acquisition where the motion direction is unambiguous. > [!WARNING] > > **Velocity aliasing** > > Very high velocities may exceed the Nyquist limit and cause aliasing (wrapping). If you observe sudden sign reversals in high-motion regions, reduce the `lag` parameter or increase the pulse repetition frequency during acquisition. ### B-mode B-mode imaging visualizes tissue anatomy by averaging the IQ signal magnitude over time. Unlike power Doppler, no clutter filtering is applied and the mean magnitude (not squared magnitude) is computed within each temporal window. #### Computing B-mode To compute B-mode volumes from beamformed IQ data, you may use either the `confusius.iq.process_iq_to_bmode` function or the corresponding Xarray accessor method. ```python from dask.distributed import Client import xarray as xr import confusius as cf # Start a local Dask cluster for optimal performance. client = Client() # Load IQ data. iq = cf.load("sub-01_task-rest_iq.zarr") # Process to B-mode using a sliding window of 200 volumes. bmode = cf.iq.process_iq_to_bmode(iq, bmode_window_width=200) ``` ```python from dask.distributed import Client import xarray as xr import confusius # Start a local Dask cluster for optimal performance. client = Client() # Load IQ data. iq = cf.load("sub-01_task-rest_iq.zarr") # Process to B-mode using a sliding window of 200 volumes. bmode = iq.fusi.iq.process_to_bmode(bmode_window_width=200) ``` > [!NOTE] > > **Processing parameters** > > For detailed documentation of all parameters, see the API reference for `process_iq_to_bmode`. ## Next Steps Now that you've processed your beamformed IQ data into power Doppler or velocity volumes, you're ready to: 1. **[Visualization]()**: explore your data interactively with napari and generate static figures with Matplotlib. 2. **[Quality Control]()**: assess data quality and identify artifacts *(coming soon)* 3. **[Registration]()**: correct for motion and align to anatomical templates *(coming soon)* 4. **[Signal Processing]()**: extract regional signals and apply denoising *(coming soon)* [^/dev/user-guide/beamformed-iq/#montaldo2009]: Montaldo, G., et al. "Coherent Plane-Wave Compounding for Very High Frame Rate Ultrasonography and Transient Elastography." IEEE Transactions on Ultrasonics, Ferroelectrics and Frequency Control, vol. 56, no. 3, Mar. 2009, pp. 489–506. DOI.org (Crossref), [https://doi.org/10.1109/TUFFC.2009.1067](). [^/dev/user-guide/beamformed-iq/#demene2015]: Demené, Charlie, et al. "Spatiotemporal Clutter Filtering of Ultrafast Ultrasound Data Highly Increases Doppler and fUltrasound Sensitivity." IEEE Transactions on Medical Imaging, vol. 34, no. 11, Nov. 2015, pp. 2271–85. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2015.2428634](). [^/dev/user-guide/beamformed-iq/#baranger2018]: Baranger, J., et al. "Adaptive Spatiotemporal SVD Clutter Filtering for Ultrafast Doppler Imaging Using Similarity of Spatial Singular Vectors." IEEE Transactions on Medical Imaging, vol. 37, no. 7, 2018, pp. 1574–86. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2018.2789499](). [^/dev/user-guide/beamformed-iq/#lemeurdiebolt2025]: Le Meur-Diebolt, Samuel, et al. "Robust Functional Ultrasound Imaging in the Awake and Behaving Brain: A Systematic Framework for Motion Artifact Removal." bioRxiv, 17 June 2025. DOI.org (Crossref), [https://doi.org/10.1101/2025.06.16.659882](). [^/dev/user-guide/beamformed-iq/#bertolo2023]: Bertolo, Adrien, et al. "High Sensitivity Mapping of Brain-Wide Functional Networks in Awake Mice Using Simultaneous Multi-Slice fUS Imaging." Imaging Neuroscience, vol. 1, Nov. 2023, p. imag\_a\_00030. DOI.org (Crossref), [https://doi.org/10.1162/imag\_a\_00030](). [^/dev/user-guide/beamformed-iq/#brunner2020]: Brunner, Clément, et al. "A Platform for Brain-Wide Volumetric Functional Ultrasound Imaging and Analysis of Circuit Dynamics in Awake Mice." Neuron, vol. 108, no. 5, Dec. 2020, pp. 861–875.e7. DOI.org (Crossref), [https://doi.org/10.1016/j.neuron.2020.09.020](). [^/dev/user-guide/beamformed-iq/#zucker2025]: Zucker, Nicolas, et al. “Physio-fUS: A Tissue-Motion Based Method for Heart and Breathing Rate Assessment in Neurofunctional Ultrasound Imaging.” eBioMedicine, vol. 112, Feb. 2025, p. 105581. DOI.org (Crossref), [https://doi.org/10.1016/j.ebiom.2025.105581](). [^/dev/user-guide/beamformed-iq/#ferrier2020]: Ferrier, Jeremy, et al. “Functional Imaging Evidence for Task-Induced Deactivation and Disconnection of a Major Default Mode Network Hub in the Mouse Brain.” Proceedings of the National Academy of Sciences, vol. 117, no. 26, June 2020, pp. 15270–80. DOI.org (Crossref), [https://doi.org/10.1073/pnas.1920475117](). [^/dev/user-guide/beamformed-iq/#rabut2020]: Rabut, Claire, et al. “Pharmaco-fUS: Quantification of Pharmacologically-Induced Dynamic Changes in Brain Perfusion and Connectivity by Functional Ultrasound Imaging in Awake Mice.” NeuroImage, vol. 222, Nov. 2020, p. 117231. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2020.117231](). [^/dev/user-guide/beamformed-iq/#brunner2021]: Brunner, Clément, et al. “Whole-Brain Functional Ultrasound Imaging in Awake Head-Fixed Mice.” Nature Protocols, vol. 16, no. 7, July 2021, pp. 3547–71. DOI.org (Crossref), [https://doi.org/10.1038/s41596-021-00548-8](). [^/dev/user-guide/beamformed-iq/#nunez-elizalde2022]: Nunez-Elizalde, Anwar O., et al. “Neural Correlates of Blood Flow Measured by Ultrasound.” Neuron, vol. 110, no. 10, May 2022, pp. 1631-1640.e4. DOI.org (Crossref), [https://doi.org/10.1016/j.neuron.2022.02.012](). # Visualization ConfUSIus provides tools for both **interactive exploration** and **static figure generation**: | Tool | Backend | Best for | | --- | --- | --- | | `plot_napari` / `.fusi.plot.napari()` | napari | Interactive exploration of 3D+t datasets | | `draw_napari_labels` / `.fusi.plot.draw_napari_labels()` + `extract_labels_from_layer` / `.fusi.plot.extract_labels_from_layer()` | napari | Interactive manual ROI drawing | | `plot_volume` / `.fusi.plot.volume()` | Matplotlib | Static slice grids | | `plot_contours` / `.fusi.plot.contours()` | Matplotlib | Contour-only grids (masks or atlas outlines) | | `plot_composite` / `.fusi.plot.composite()` | Matplotlib | Composite plots of two volumes | | `plot_stat_map` / `.fusi.plot.stat_map()` | Matplotlib | Statistical overlays on an anatomical background | | `plot_carpet` / `.fusi.plot.carpet()` | Matplotlib | Voxel time-series raster (quality control) | | `plot_matrix` | Matplotlib | Any square 2D matrix (e.g. correlation, connectivity, or statistical matrices) | All functions accept DataArrays and use world coordinates for axis scaling automatically. The [Xarray accessor]() variants (`.fusi.plot.*`) offer a more concise syntax; both call the same underlying functions. > [!NOTE] > > **Example dataset setup (Nunez-Elizalde *et al.*, 2022)** > > The figures on this page are generated using the Nunez-Elizalde *et al.* (2022) dataset[^/dev/user-guide/visualization/#nunez2022] obtained with `fetch_nunez_elizalde_2022`. The code below shows how to load the power Doppler, angiography, and Allen atlas segmentation for one acquisition and build a napari colormap from the Allen structure tree. You can run this code in a Jupyter notebook to follow along and generate the same figures as you read through the guide. > > ```python > import csv > > import confusius as cf > import numpy as np > from confusius.datasets import fetch_nunez_elizalde_2022 > from napari.utils.colormaps import DirectLabelColormap > > > def _nunez_allen_label_colormap(structure_tree_csv, atlas_labels): > """Build a napari labels colormap from structure-tree colors.""" > with open(structure_tree_csv, newline="", encoding="utf-8") as f: > rows = list(csv.DictReader(f)) > > labels = {int(v) for v in np.unique(atlas_labels.values) if int(v) > 0} > key = max( > ("graph_order", "sphinx_id", "id"), > key=lambda k: len({int(float(r[k])) for r in rows if r.get(k)} & labels), > ) > > rgb = {} > for r in rows: > if not r.get(key) or not r.get("color_hex_triplet"): > continue > lab = int(float(r[key])) > if lab not in labels: > continue > h = r["color_hex_triplet"].lstrip("#") > rgb[lab] = tuple(int(h[i : i + 2], 16) / 255 for i in (0, 2, 4)) > > return DirectLabelColormap( > color_dict={ > 0: np.zeros(4), > None: np.zeros(4), > **{k: np.array([*v, 0.7]) for k, v in rgb.items()}, > } > ) > > > bids_root = fetch_nunez_elizalde_2022( > subjects=["CR022"], > sessions=["20201011", "20201007"], > tasks=["spontaneous"], > acqs=["slice03"], > ) > > pwd = cf.load( > bids_root > / "sub-CR022/ses-20201011/fusi" > / "sub-CR022_ses-20201011_task-spontaneous_acq-slice03_pwd.nii.gz" > ) > mean_vol = pwd.mean("time").compute() > angio = cf.load( > bids_root / "sub-CR022/ses-20201011/susi" / "sub-CR022_ses-20201011_pwd.nii.gz" > ).compute() > angio_2 = cf.load( > bids_root / "sub-CR022/ses-20201007/susi" / "sub-CR022_ses-20201007_pwd.nii.gz" > ).compute() > atlas_labels = cf.load( > bids_root > / "derivatives/allenccf_align/sub-CR022/ses-20201011/fusi" > / "sub-CR022_ses-20201011_space-fusi_desc-allenccf_dseg.nii.gz" > ) > atlas_labels = atlas_labels.sel(z=mean_vol["z"], method="nearest").assign_coords( > z=mean_vol["z"] > ) > > label_colormap = _nunez_allen_label_colormap( > bids_root / "derivatives/allenccf_align/structure_tree_safe_2017.csv", > atlas_labels, > ) > ``` ## Interactive Exploration with napari ConfUSIus relies on [napari]() for interactive visualization of 3D+t fUSI data. Napari handles large datasets efficiently through lazy loading, allowing you to explore even beamformed IQ data without running out of memory. You may find it helpful to read through the [tour of the napari viewer]() to familiarize yourself with the controls and features. ### Basic Usage ```python viewer, layer = pwd.fusi.plot.napari() ``` ```python import confusius as cf viewer, layer = cf.plotting.plot_napari(pwd) ``` This opens a napari viewer with a scale bar, colorbar, and correct world scaling across axes. The viewer is fully interactive: you can zoom, pan, and scroll through time and elevation slices with the sliders or mouse wheel. > [!TIP] > > **Using napari's annotation tools and plugins** > > Napari's annotation tools let you [draw regions of interest]() and [place markers]() on your fUSI volumes. ConfUSIus also provides `draw_napari_labels` to open a viewer with an empty Labels layer ready for painting (see the [Manual ROI Drawing](<#manual-roi-drawing>) section below). These annotations can be saved and loaded for later analysis. Additionally, the [napari Hub]() hosts hundreds of plugins that extend functionality—from segmentation algorithms to integration with other imaging modalities like microscopy or MRI. ### Napari Parameters Both `plot_napari` and `data.fusi.plot.napari()` default to `layer_type="auto"`: integer and boolean data open as labels layers, and other data as image layers. Pass `layer_type="image"` explicitly for integer-valued intensity data, or `layer_type="labels"` to explicitly select a labels layer. The shorthand `data.fusi.plot()` uses the same default. By default napari auto-scales contrast to the data range. For power Doppler, working in decibel scale with explicit limits is often more informative: ```python # dB-scaled power Doppler with fixed contrast window. viewer, layer = cf.plotting.plot_napari( pwd.fusi.scale.db(), contrast_limits=(-20, 0), colormap="hot", ) ``` `contrast_limits`, `colormap`, and any other keyword arguments are forwarded directly to [`napari.imshow`](). ### Overlaying an Atlas as a Labels Layer Napari's **labels** layer renders integer-labeled masks as filled or contoured regions, ideal for visualizing an atlas alongside your power Doppler data. With the Nunez-Elizalde dataset, you can use the precomputed Allen segmentation in `derivatives/allenccf_align`: ```python # Load power Doppler mean volume and open viewer. viewer, layer = cf.plotting.plot_napari( mean_vol.fusi.scale.db(), contrast_limits=(-20, 0), ) # Add pre-registered Allen labels as a labels layer. viewer, labels_layer = cf.plotting.plot_napari( atlas_labels, viewer=viewer, layer_type="labels", colormap=label_colormap, name="Allen atlas", opacity=0.6, ) ``` > [!TIP] > > **Adding Multiple Image Layers** > > The `viewer` argument works for any `plot_napari` call, letting you load two image datasets into the same viewer for direct comparison; for example, before and after motion correction: > > ```python > viewer, layer = pwd_before.fusi.plot.napari(name="Before correction") > viewer, layer = pwd_after.fusi.plot.napari(viewer=viewer, name="After correction") > ``` ### Slicing Across Different Spatial Dimensions By default, napari shows the last two spatial dimensions as a 2D plane and the remaining dimensions (e.g., `time`, `z`) as sliders. If you prefer a different default slicing axis—for example slicing along `y` (depth) instead of `z` (elevation)—use `dim_order` to reorder the spatial axes: ```python # Swap z and y: depth becomes the slider axis. viewer, layer = pwd_3d.fusi.plot.napari(dim_order=("y", "z", "x")) ``` ### 3D Rendering For volumetric datasets, napari can render the data in full 3D using volume rendering (accessible by clicking the second icon in the bottom-left controls). In the 3D view you can drag to orbit, scroll to zoom, and use the napari controls to adjust the rendering. ## Manual ROI Drawing `draw_napari_labels` opens a napari viewer with your data as a background image and an empty **Labels** layer on top. You can then paint integer labels directly in the viewer using napari's brush tool—each distinct integer becomes a separate region of interest (ROI). ```python import xarray as xr import confusius as cf pwd = cf.load("sub-01_task-awake_pwd.zarr") mean_vol = pwd.mean("time").compute() # Open viewer with an empty Labels layer ready for painting. viewer, labels_layer = cf.plotting.draw_napari_labels( mean_vol.fusi.scale.db(), contrast_limits=(-20, 0), colormap="gray", ) ``` The Labels layer is aligned to the same world coordinate frame as the image layer, so the spatial scale and origin are consistent regardless of voxel size or data origin. Once you have finished painting, use `extract_labels_from_layer` to convert the Labels layer into a stacked integer DataArray compatible with `extract_with_labels`, `plot_contours`, and `VolumePlotter.add_contours`: ```python from confusius.plotting import extract_labels_from_layer # Convert the painted layer to a stacked DataArray. # label_map has dims ("mask", "k", "j", "i"), one layer per painted label. label_map = extract_labels_from_layer(labels_layer, mean_vol) # Each label's color as painted in napari is stored in attrs["rgb_lookup"] # and will be reused automatically by plot_contours and add_contours. # Extract region-averaged signals. region_signals = pwd.fusi.extract.with_labels(label_map) # region_signals has dims (time, region). # Overlay contours on a volume plot. plotter = mean_vol.fusi.scale.db().fusi.plot.volume(slice_mode="z", cmap="gray") plotter.add_contours(label_map) ``` For oblique or sheared data, painting takes place on an axis-aligned display grid. Extraction maps the painted labels back onto `mean_vol`'s original voxel grid using nearest-neighbor interpolation, with zero background outside the display grid. Label IDs are preserved, but boundaries can change and thin regions can disappear. Prefer drawing in an existing axis-aligned acquisition/scanner frame when available. A known transform can be applied afterward to both the image and extracted labels using `.fusi.affine.apply` without resampling. ## Static Volume Plots `plot_volume` generates a static Matplotlib grid of 2D slices—one panel per coordinate along the chosen slicing dimension. It is the standard tool in ConfUSIus for generating static figures of 3D volumes, functional activation maps, or 3D angiography data. ### Basic Usage ```python import xarray as xr import confusius as cf pwd = cf.load("sub-01_task-awake_pwd.zarr") # Planar data defaults to its singleton world dimension; full 3D data defaults to z. plotter = pwd.fusi.plot.volume() ``` ```python import xarray as xr import confusius as cf pwd = cf.load("sub-01_task-awake_pwd.zarr") # Planar data defaults to its singleton world dimension; full 3D data defaults to z. plotter = cf.plotting.plot_volume(pwd) ``` The function returns a `VolumePlotter` object that manages the underlying Matplotlib `Figure` and `Axes` and supports overlay operations (see [Overlaying Contours](<#overlaying-contours>)). When the data has multiple slices along the sliced dimension, `plot_volume` lays them out automatically in an approximately square grid. Pass `slice_mode` explicitly to choose a different world slicing direction: ```python angio = cf.load("sub-01_acq-angio_pwd.zarr") plotter = angio.fusi.plot.volume(slice_mode="z", show_colorbar=False) ``` ### Selecting Slices and Colormap By default all coordinates along `slice_mode` are shown. Use `slice_coords` to pick specific ones and `cmap`/`vmin`/`vmax` to control the colormap and contrast. The grid layout can be controlled using `nrows` and `ncols`, or by specifying axes directly with `axes` (see the [API reference]() for details). ```python import numpy as np z = angio["z"].values margin = max(1, int(round(0.12 * (len(z) - 1)))) slice_coords = tuple(np.linspace(float(z[margin]), float(z[-margin - 1]), 3)) plotter = angio.fusi.scale.db().fusi.plot.volume( nrows=1, slice_mode="z", slice_coords=slice_coords, cmap="inferno", vmin=-20, vmax=0, show_colorbar=False, ) ``` ### Slicing Over a Non-Spatial Dimension `slice_mode` can be a world axis (`z`/`y`/`x`) or any extra non-voxel dim, e.g. `time`, `pose`, or a `region` dim such as the one a seed-based connectivity map is stacked along (see [below](<#statistical-maps>) and the [seed-based connectivity example]()). Native voxel dims (`k`/`j`/`i`) are not valid slice modes. Each requested coordinate along that dim becomes its own panel, the same as slicing over `z`. Display is always in world space, labeled in mm. For already axis-aligned data this is a cheap dim rename onto a regular world grid — the same display `z`/`y`/`x` slicing produces. For oblique data, each panel is resampled onto the world-axis-aligned grid, keeping its own native per-axis resolution, so cells always render rectangular. `transpose=True` swaps which of the two remaining spatial dims is drawn on rows versus columns. ### Thresholding For functional activation maps or data where you want to suppress noise, `threshold` sets a cutoff value. Subthreshold voxels are rendered transparently: ```python # Hide values where |data| < 3.0 (noise floor suppression). plotter = stat_map.fusi.plot.volume( slice_mode="z", threshold=3.0, threshold_mode="lower", cmap="coolwarm", vmin=-6, vmax=6, ) ``` `threshold_mode="upper"` masks values *above* the threshold instead—useful for removing saturation artifacts or thresholding decibel-scaled data. ### Saving and Closing ```python plotter = pwd.fusi.plot.volume(slice_mode="z", cmap="hot") plotter.savefig("sub-01_task-awake_pwd.png", dpi=150) plotter.close() ``` Pass any keyword argument accepted by `matplotlib.figure.Figure.savefig` (e.g., `bbox_inches="tight"`, `transparent=True`). ## Statistical Maps `plot_stat_map` is the convenience helper for the common statistical-overlay pattern: draw an anatomical/background volume first, then overlay a statistical map with a colormap and value range picked automatically from the statistic's sign. ### Basic Usage ```python plotter = t_map.fusi.plot.stat_map(bg_volume=pwd, slice_mode="z", threshold=3.0) ``` ```python plotter = cf.plotting.plot_stat_map(t_map, bg_volume=pwd, slice_mode="z", threshold=3.0) ``` `bg_volume` is optional. Omit it to plot the statistical map on its own. When `alpha` is not provided, the statistical layer is fully opaque wherever it has a value, and the background only shows through where the statistical map has been masked out (typically with `threshold`). If you want the background to remain visible throughout, pass `alpha < 1`. ### Automatic Range and Colormap Selection By default, `vmin` and `vmax` are taken from the full `stat_map`, and `auto_range=True` picks a suitable range + default colormap automatically: - Both positive and negative values: diverging, symmetric `[-m, m]` range (`m = max(|vmin|, |vmax|)` over the bounds you pass, or the largest magnitude in `stat_map` when you pass neither), with `cmap="coolwarm"` by default. - Only non-negative values (for example R² or F-statistics): sequential `[0, vmax]` range, with `cmap="viridis"` by default. - Only non-positive values: sequential `[vmin, 0]` range, with `cmap="viridis_r"` by default. ### Customizing the Overlay and Background Pass `vmin`/`vmax` explicitly to define a custom range, `alpha` or `threshold` to blend the overlay with the background, and `bg_kwargs` to style the background volume itself: ```python plotter = t_map.fusi.plot.stat_map( bg_volume=pwd.fusi.scale.db(), slice_mode="z", threshold=3.0, vmax=6.0, cbar_label="t-statistic", bg_kwargs={"cmap": "gray", "vmin": -20, "vmax": 0}, ) ``` ```python plotter = cf.plotting.plot_stat_map( t_map, bg_volume=pwd.fusi.scale.db(), slice_mode="z", threshold=3.0, vmax=6.0, cbar_label="t-statistic", bg_kwargs={"cmap": "gray", "vmin": -20, "vmax": 0}, ) ``` Pass an explicit `cmap` to override the automatic choice, `auto_range=False` to use `vmin`/`vmax` directly with no zero-anchoring, or a `matplotlib.colors.Normalize` instance as `norm` (for example `TwoSlopeNorm`, `BoundaryNorm`, or `LogNorm`) when you need a custom mapping. When `norm` is provided, it overrides `vmin`, `vmax`, and `auto_range` entirely. To overlay a statistical map onto an existing plot (for example one built with `plot_volume`), call `VolumePlotter.add_stat_map` on the returned plotter; it applies the same automatic colormap and range logic as `plot_stat_map`. For full manual control over the colormap and range, use `VolumePlotter.add_volume` instead. This figure is generated in the [atlas-based seed connectivity maps example](), which walks through registering a recording to an Allen-space template, resampling the Allen Mouse Brain Atlas onto the recording's native grid, and computing voxel-wise seed-based correlation maps with `SeedBasedMaps`. `SeedBasedMaps` stacks one map per seed along a `region` dim, so the figure is produced with `slice_mode="region"` — one panel per seed, faceted over a non-spatial dim as described [above](<#slicing-over-a-non-spatial-dimension>). ## Overlaying Contours Atlas outlines or region of interest (ROI) boundaries can be drawn on top of a volume plot to provide anatomical context. ConfUSIus represents masks as **integer-labeled DataArrays** where 0 is background and each positive integer identifies a distinct region. ### Contours on top of a Volume The most common use case is to draw atlas outlines on top of a fUSI volume. `plot_volume` returns a `VolumePlotter` that remembers the coordinate-to-axis mapping; calling `add_contours` on it draws outlines on the matching panels. Masks produced by `get_masks` carry Allen colors in their `attrs["rgb_lookup"]`, so no explicit color argument is needed: > [!NOTE] > > **Registering your data to an atlas** > > This example assumes your fUSI data has already been registered to the Allen Mouse Brain atlas. See the [Atlases]() guide for loading and working with brain atlases, and the [register a recording to an Allen fUSI template]() example for how to obtain the `transform` used in `atlas.atlas.resample_like` below. ```python from confusius.datasets import fetch_brainglobe_atlas # Load atlas and resample to fUSI space (see the registration example linked above). atlas = fetch_brainglobe_atlas("allen_mouse_100um") atlas_fusi = atlas.atlas.resample_like(mean_vol, transform) # Step 1: display an average power Doppler volume. plotter = cf.plotting.plot_volume(mean_vol.fusi.scale.db(), show_colorbar=False) # Step 2: overlay contours. Allen colors are read from # atlas_fusi.annotation.attrs["rgb_lookup"] automatically. plotter.add_contours(atlas_fusi.annotation) ``` This figure is generated in the [register a recording to an Allen fUSI template example](), which walks through the full registration and atlas-resampling workflow. Coordinate matching is done along the plotter's `slice_mode`'s own coordinate value — world position for `z`/`y`/`x`, voxel index for `k`/`j`/`i`, or the facet's own coordinate (e.g. a region label) for a non-spatial `slice_mode` — matching contour panels with those of the previously plotted volume. Slices present in the mask but absent from the volume are skipped with a warning. ### Contours-only Grid `plot_contours` produces a contour grid without any background image—useful for quickly inspecting mask or atlas coverage across slices, or for drawing contours onto a set of pre-existing `Axes` without the coordinate-matching of `VolumePlotter`: ```python # Contours on a black background (default). plotter = cf.plotting.plot_contours(atlas_fusi.annotation, slice_mode="z") # Specific colors per region. plotter = cf.plotting.plot_contours( atlas_fusi.annotation, slice_mode="z", colors={1: "cyan", 2: "magenta"}, ) ``` The `.fusi.plot.contours()` accessor provides the same function with a shorter syntax: ```python plotter = atlas_fusi.annotation.fusi.plot.contours(slice_mode="z") ``` ## Composite Plots `plot_composite` overlays two volumes as a **red/cyan composite**: the first volume drives the red channel, the second drives the cyan channel (green + blue). This is the same encoding used by the live registration progress preview and is a quick way to inspect how well two volumes are aligned: two sessions of the same subject, a moving image against a reference, or a registered output against its target. ### Basic Usage ```python # Every third elevation slice keeps the figure compact while still showing the # anatomical depth range. composite_slices = list(np.asarray(angio["z"].values, dtype=float)[::3][:-1]) # Two angiography volumes from different sessions of the same subject. plotter = angio.fusi.plot.composite( angio_2, slice_coords=composite_slices, normalize_strategy="per_slice" ) ``` ```python # Every third elevation slice keeps the figure compact while still showing the # anatomical depth range. composite_slices = list(np.asarray(angio["z"].values, dtype=float)[::3][:-1]) # Two angiography volumes from different sessions of the same subject. plotter = cf.plotting.plot_composite( angio, angio_2, slice_coords=composite_slices, normalize_strategy="per_slice" ) ``` By default `angio_2` is resampled onto `angio`'s grid (`resample=True`), so the two volumes do not need to share the same coordinates. Additionally, the function returns a `VolumePlotter` so you can use `add_contours` to layer atlas outlines over the composite. ### Normalization Strategies `normalize_strategy` controls how voxel intensities are mapped into the `[0, 1]` range that drives each channel: | Strategy | Per-array range | When to use | | --- | --- | --- | | `"per_volume"` (default) | one range per volume | Each input is shown at its own contrast; preserves within-volume relative brightness. | | `"per_slice"` | one range per slice | Maximizes contrast per slice; improves within-slice comparability. | | `"shared"` | one range across volumes | Preserves absolute intensity differences between the two inputs. | ### Skipping Resampling If the two volumes already share the same grid (if one was previously registered onto the other) pass `resample=False` to skip the resample step: ```python plotter = fixed.fusi.plot.composite(registered_moving, resample=False) ``` When `resample=False`, the two arrays must share dimensions, shape, and coordinates within tolerance. Coordinates are checked with `numpy.allclose`-style tolerances (`rtol=1e-5`, `atol=1e-8` by default); once the check passes, `data2`'s coordinates are replaced with `data1`'s so the two volumes share an exact coordinate frame downstream. Widen `rtol` / `atol` to accept acquisitions on offset grids that you know are equivalent. ## Carpet Plots A **carpet plot** (also called a grayplot or Power plot[^/dev/user-guide/visualization/#power2017]) displays every voxel's time-series as a row in a 2D raster image with time on the x-axis. Z-scored by default, it makes motion artifacts, global signal transients, and outlier volumes immediately visible as vertical stripes or abrupt intensity changes. Carpet plots are primarily used for quality control—for a deeper discussion of QC metrics, see the [Quality Control]() guide. ### Basic Usage ```python import xarray as xr import confusius as cf brain_mask = cf.load("brain_mask.zarr") fig, ax = pwd.fusi.plot.carpet(mask=brain_mask) ``` ```python import xarray as xr import confusius as cf brain_mask = cf.load("brain_mask.zarr") fig, ax = cf.plotting.plot_carpet(pwd, mask=brain_mask) ``` Without a `mask`, all non-zero voxels are included. With a mask, only voxels where `mask == True` (or `mask > 0` for integer-labeled masks) are shown. See the API reference of `plot_carpet` for more options. ## Matrix Plots `plot_matrix` plots any square 2D matrix: a region-by-region correlation or connectivity matrix, a confusion matrix, or any other `(n, n)` array. It supports optional lower/upper triangle masking and grid lines. A `groups` parameter annotates contiguous blocks of rows/columns with colored strips, handy for marking anatomical groupings (e.g. cortex, thalamus) without cluttering the plot with per-region colors. `triangle` can also overlay two matrices on the same axes—for example, an estimate on the lower triangle and its significance mask on the upper—by calling `plot_matrix` twice with complementary `triangle` values. ```python import confusius as cf fig, ax = cf.plotting.plot_matrix( connectivity, labels=region_order, groups=group_labels, group_colors=group_colors, grid="gray", cbar_label="correlation", ) ``` This figure is generated in the [atlas-based region correlation matrix example](), which walks through registering a recording to an Allen-space template, resampling the Allen Mouse Brain Atlas onto the recording's native grid, and extracting region signals—including `group_colors`, sourced directly from the atlas so each area is colored with its official Allen color. ## Next Steps Now that you can visualize your data, you're ready for: 1. **[Registration]()**: Correct for motion and align acquisitions to an anatomical template. 2. **[Quality Control]()**: Assess data quality and identify artifacts. 3. **[Signal Processing]()**: Extract regional signals and apply denoising. ## API Reference For full parameter documentation, see the [Plotting API reference]() and the [Xarray Integration API reference](). [^/dev/user-guide/visualization/#power2017]: Power, Jonathan D. "A Simple but Useful Way to Assess fMRI Scan Qualities." NeuroImage, vol. 154, July 2017, pp. 150–58. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2016.08.009](). [^/dev/user-guide/visualization/#nunez2022]: Nunez-Elizalde, A. O., et al. "A Neurophysiological fUSI-BIDS dataset from awake, behaving mice." figshare dataset, 2022. DOI.org (Datacite), [https://doi.org/10.6084/m9.figshare.19316228](); the fUSI-BIDS re-export is hosted in the [ConfUSIus dataset collection](). # Quality Control Quality control (QC) in fUSI aims to identify problematic time points, assess spatial signal quality across the brain, and flag acquisitions that may need to be discarded or corrected before downstream analysis. ConfUSIus provides three QC metrics, divided into two categories: | Metric | Function | Type | Best for | | --- | --- | --- | --- | | DVARS | `compute_dvars` | Temporal | Identifying outlier volumes | | Coefficient of variation | `compute_cv` | Spatial | Mapping temporal variability | | Temporal SNR | `compute_tsnr` | Spatial | — see note below | > [!NOTE] > > **Example dataset setup (Nunez-Elizalde *et al.*, 2022)** > > The figures on this page are generated using the Nunez-Elizalde *et al.* (2022) dataset[^/dev/user-guide/quality-control/#nunez2022] obtained with `fetch_nunez_elizalde_2022`. The code below shows how to load the power Doppler and Allen atlas segmentation for one acquisition. You can run this code in a Jupyter notebook to follow along and generate the same figures as you read through the guide. > > ```python > import confusius as cf > from confusius.datasets import fetch_nunez_elizalde_2022 > > bids_root = fetch_nunez_elizalde_2022( > subjects=["CR022"], > sessions=["20201011"], > tasks=["spontaneous"], > acqs=["slice03"], > ) > > pwd = cf.load( > bids_root > / "sub-CR022/ses-20201011/fusi" > / "sub-CR022_ses-20201011_task-spontaneous_acq-slice03_pwd.nii.gz" > ) > atlas_labels = cf.load( > bids_root > / "derivatives/allenccf_align/sub-CR022/ses-20201011/fusi" > / "sub-CR022_ses-20201011_space-fusi_desc-allenccf_dseg.nii.gz" > ) > atlas_labels = atlas_labels.sel(z=pwd["z"], method="nearest").assign_coords(z=pwd["z"]) > brain_mask = atlas_labels > 0 > signals = pwd.fusi.extract.with_mask(brain_mask) > ``` ## DVARS DVARS (temporal **D**erivative of **VAR**iance**S**) measures how much the signal intensity changes between consecutive time points, averaged across all brain voxels[^/dev/user-guide/quality-control/#power2012]. A spike in DVARS typically indicates a motion event, a scanner artifact, or another transient disturbance that corrupted one or more volumes. By default, ConfUSIus computes the **standardized** DVARS[^/dev/user-guide/quality-control/#nichols2017], which accounts for the temporal autocorrelation structure of the data and is therefore more comparable across acquisitions. Standardized DVARS is approximately 1 for a stationary recording; values significantly above 1 indicate outlier volumes. ### Usage DVARS is computed from any `(time, ...)` DataArray—spatial dimensions are flattened internally. Using brain-masked signals is recommended to exclude background voxels that inflate the variance estimate: ```python from confusius.qc import compute_dvars # Compute standardized DVARS (default). dvars = compute_dvars(signals) # dvars has dims (time,). ``` ### Visualizing and Thresholding DVARS is a time series and is best examined as a line plot. Flagged frames can be marked for quick visual inspection: ```python import matplotlib.pyplot as plt threshold = 2.5 flagged = dvars > threshold flagged_dvars = dvars.where(flagged, drop=True) fig, ax = plt.subplots(figsize=(10, 3)) dvars.plot(ax=ax, linewidth=0.8, label="Standardized DVARS") if flagged_dvars.size > 0: ax.plot( flagged_dvars["time"].values, flagged_dvars.values, marker="o", linestyle="", color="red", ms=3, label="Flagged frames", ) ax.axhline(threshold, color="red", linestyle="--", label=f"Threshold ({threshold})") ax.legend() ``` ```python print(f"{flagged.sum().item()} outlier frames detected out of {len(dvars)} total.") ``` Simply dropping flagged frames introduces discontinuities in the time series and can bias downstream connectivity estimates. The recommended approach is to pass the outlier mask to the scrubbing step in the signal cleaning pipeline—see [Signal Processing]() for details. ## Carpet Plot A carpet plot (also known as a grayplot or Power plot) displays the voxel intensity time series as a 2D raster image—time on the *x*-axis, voxels on the *y*-axis—making it easy to spot global signal disturbances, motion spikes, and scanner drift that DVARS alone may not fully characterize. ```python from confusius.plotting import plot_carpet fig, ax = plot_carpet(pwd, mask=brain_mask) ``` Vertical streaks across voxels correspond to time points where many voxels change simultaneously. Horizontal banding can reveal spatially structured noise such as pulsatility or drift confined to specific voxel groups. ## Coefficient of Variation The coefficient of variation (CV) is a voxel-wise spatial metric defined as the ratio of temporal standard deviation to temporal mean: \\\[\\text{CV} = \\frac{\\sigma\_t}{\\bar{x}}\\\] CV quantifies how variable the signal is relative to its mean. In fUSI: - **High CV** typically indicates voxels affected by motion, pulsatility artifacts, or other sources of noise. - **Low CV** indicates temporally stable voxels with little variation relative to their mean signal level. > [!WARNING] > > **CV is not a signal-to-noise ratio** > > Low CV does not imply good signal quality. Regions with **no signal**—gel layers, shadow zones behind the skull—also exhibit low CV. CV is therefore best used as a **detector of high-variance regions** (artifacts, motion) rather than as a positive indicator of signal quality. Compare CV against the mean power Doppler image to distinguish reliable vascular signal from noisy background. ### Usage ```python from confusius.qc import compute_cv # Compute CV on the full spatial DataArray directly. cv = compute_cv(pwd) # cv has dims (k, j, i) — the time dimension is reduced. ``` ### Visualizing Comparing CV to the mean power Doppler image helps interpret the map: motion artifacts appear as regions of elevated CV, while vascular structure appears with low CV. ```python mean_pwd = pwd.mean("time").fusi.scale.db() plotter = mean_pwd.fusi.plot.volume(slice_mode="z", vmin=-20, vmax=0) plotter = cv.fusi.plot.volume(slice_mode="z", vmin=0, vmax=1) ``` ## Temporal SNR > [!WARNING] > > **tSNR is misleading for power Doppler: use CV instead** > > The temporal signal-to-noise ratio is well-established in fMRI, where higher tSNR reliably indicates a better signal. **This interpretation does not hold for fUSI power Doppler data.** > > Power Doppler signals are non-negative and scale with cerebral blood volume. As a consequence, the temporal mean and temporal standard deviation are intrinsically positively correlated across voxels: regions with more blood have both a higher mean *and* a higher standard deviation. The ratio mean/std is therefore roughly constant across the vasculature. Paradoxically, regions with **very low vascular signal**—noise regions, gel layers, shadow zones behind the skull—can exhibit disproportionately high tSNR, because their mean remains relatively large compared to their near-zero temporal standard deviation. This behavior is the opposite of what a signal-quality metric should indicate[^/dev/user-guide/quality-control/#lemeurdiebolt2025]. > > **Recommendation:** use [CV]() and average power Doppler images instead of tSNR for assessing spatial signal quality in fUSI data. CV is the mathematical inverse of tSNR but correctly highlights regions with high temporal variability, regardless of their absolute signal level. The tSNR function is provided for completeness and for compatibility with workflows that report it: \\\[\\text{tSNR} = \\frac{\\bar{x}}{\\sigma\_t} = \\frac{1}{\\text{CV}}\\\] ```python from confusius.qc import compute_tsnr tsnr = compute_tsnr(pwd) ``` Comparing tSNR to the mean power Doppler image makes the paradox concrete: low-signal regions—gel layers, shadow zones—appear bright in tSNR because their mean signal, though small in absolute terms, remains large relative to their near-zero temporal variability. ## Next Steps After quality control, you're ready to move on to: 1. **[Registration]()**: Correct for motion and align acquisitions to an anatomical template. 2. **[Signal Processing]()**: Extract regional signals and apply denoising. ## API Reference For full parameter documentation, see the [QC API reference](). [^/dev/user-guide/quality-control/#power2012]: Power, Jonathan D., et al. "Spurious but Systematic Correlations in Functional Connectivity MRI Networks Arise from Subject Motion." NeuroImage, vol. 59, no. 3, Feb. 2012, pp. 2142–54. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2011.10.018](). [^/dev/user-guide/quality-control/#nichols2017]: Nichols, Thomas E. "Notes on Creating a Standardized Version of DVARS." arXiv, 2017. DOI.org (Datacite), [https://doi.org/10.48550/ARXIV.1704.01469](). [^/dev/user-guide/quality-control/#lemeurdiebolt2025]: Le Meur-Diebolt, Samuel, et al. "Robust Functional Ultrasound Imaging in the Awake and Behaving Brain: A Systematic Framework for Motion Artifact Removal." bioRxiv, 17 June 2025. DOI.org (Crossref), [https://doi.org/10.1101/2025.06.16.659882](). [^/dev/user-guide/quality-control/#nunez2022]: Nunez-Elizalde, A. O., et al. "A Neurophysiological fUSI-BIDS dataset from awake, behaving mice." figshare dataset, 2022. DOI.org (Datacite), [https://doi.org/10.6084/m9.figshare.19316228](); the fUSI-BIDS re-export is hosted in the [ConfUSIus dataset collection](). # Atlases A brain atlas ties every voxel of a template volume to a named region. In ConfUSIus an atlas is a plain `xarray.Dataset` on a common [VoxelData grid]() (`k`/`j`/`i` voxel dims, derived world `z`/`y`/`x` coordinates), with three data variables (`template`, the anatomical template volume; `annotation`, integer region labels; and `hemispheres`, 1 = left, 2 = right), plus an `.atlas` accessor that carries all atlas-aware operations. The structure hierarchy rides along in `Dataset.attrs["structures"]`, so a single object fully describes the atlas and its region tree. ## BrainGlobe Atlases ConfUSIus does not ship its own atlases. It builds on the [BrainGlobe Atlas API](), which packages dozens of community atlases behind one interface, and `fetch_brainglobe_atlas` fetches any of them by name: ```python import confusius as cf atlas = cf.datasets.fetch_brainglobe_atlas("allen_mouse_100um") ``` The first call downloads the atlas through BrainGlobe and caches it in BrainGlobe's own cache (`~/.brainglobe`, shared with other BrainGlobe tools); later calls read from the cache. Any name from the [BrainGlobe atlas list]() works, covering mouse, rat, human, zebrafish, and more, at several resolutions. The returned object is the Dataset described above: ```pycon >>> atlas Size: 40MB Dimensions: (k: 132, j: 80, i: 114) Coordinates: * k (k) int64 1kB 0 1 2 3 4 5 6 7 ... 125 126 127 128 129 130 131 * j (j) int64 640B 0 1 2 3 4 5 6 7 8 ... 71 72 73 74 75 76 77 78 79 * i (i) int64 912B 0 1 2 3 4 5 6 7 ... 107 108 109 110 111 112 113 * z (k, j, i) float64 10MB 0.0 0.0 0.0 0.0 ... 13.1 13.1 13.1 13.1 * y (k, j, i) float64 10MB 0.0 0.0 0.0 0.0 0.0 ... 7.9 7.9 7.9 7.9 * x (k, j, i) float64 10MB 0.0 0.1 0.2 0.3 ... 11.0 11.1 11.2 11.3 Data variables: template (k, j, i) float32 5MB 0.0 0.0 0.0 0.0 0.0 ... 1.0 1.0 1.0 1.0 annotation (k, j, i) int32 5MB 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 hemispheres (k, j, i) int8 1MB 2 2 2 2 2 2 2 2 2 2 ... 1 1 1 1 1 1 1 1 1 1 Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: name: allen_mouse citation: Wang et al 2020, https://doi.org/10.1016/j.cell.2020.04.007 species: Mus musculus orientation: asr structures: root (997)\n├── VS (73)\n│ ├── AQ (140)\n│ ├── V3 (12... world_to_base: [[1. 0. 0. 0.]\n [0. 1. 0. 0.]\n [0. 0. 1. 0.]\n [0. 0. 0... ``` Coordinates are in millimeters, so an atlas plots and registers against fUSI recordings directly. > [!TIP] > > **Building your own atlas** > > Because ConfUSIus leans on the BrainGlobe atlas format, a custom atlas (for example a coarse parcellation for functional connectivity) is best authored as a BrainGlobe atlas and then loaded with the same `fetch_brainglobe_atlas` call. BrainGlobe's [Adding a new atlas]() guide and the `atlas_scripts` in the [brainglobe-atlasapi]() repository walk through packaging a template stack, an annotation stack, and a structure hierarchy into a distributable atlas. ## Exploring the Structure Hierarchy Regions form a tree: `root` contains gray and white matter, which contain areas, which contain layers. `show_tree` prints the whole hierarchy, and its `nid` argument restricts the printout to one branch, which is handy since the full Allen tree has well over a thousand nodes: ```pycon >>> # Print just the hippocampus (HIP, id 1080) subtree. >>> atlas.atlas.show_tree(nid=1080) HIP (1080) ├── CA (375) │ ├── CA1 (382) │ ├── CA2 (423) │ └── CA3 (463) ├── DG (726) │ ├── DG-mo (10703) │ ├── DG-po (10704) │ └── DG-sg (632) ├── FC (982) └── IG (19) ``` Indexing `structures` by acronym (or integer id) returns the BrainGlobe record for a region, including its full path from the root: ```pycon >>> atlas.structures["HIP"] {'acronym': 'HIP', 'id': 1080, 'name': 'Hippocampal region', 'structure_id_path': [997, 8, 567, 688, 695, 1089, 1080], 'rgb_triplet': [126, 208, 75], 'mesh_filename': PosixPath('.../allen_mouse_100um_v1.2/meshes/1080.obj'), 'mesh': None} ``` `ancestors` returns the same path as tree nodes, from the root down to (but excluding) the region: ```pycon >>> [node.tag for node in atlas.atlas.ancestors("HIP")] ['root (997)', 'grey (8)', 'CH (567)', 'CTX (688)', 'CTXpl (695)', 'HPF (1089)'] ``` ## Looking Up and Searching Regions `lookup` flattens the hierarchy into a `pandas.DataFrame` indexed by region id, with the acronym, full name, and RGB color of every structure: ```pycon >>> atlas.atlas.lookup.head() acronym name rgb_triplet id 997 root root [255, 255, 255] 8 grey Basic cell groups and regions [191, 218, 227] 567 CH Cerebrum [176, 240, 255] 688 CTX Cerebral cortex [176, 255, 184] 695 CTXpl Cortical plate [112, 255, 112] ``` Region acronyms are terse, so `search` finds structures by substring or regex. By default it matches both the acronym and the name (case-insensitive): ```pycon >>> atlas.atlas.search("visual").head() acronym name rgb_triplet id 669 VIS Visual areas [8, 133, 140] 402 VISal Anterolateral visual area [8, 133, 140] 1074 VISal1 Anterolateral visual area, layer 1 [8, 133, 140] 905 VISal2/3 Anterolateral visual area, layer 2/3 [8, 133, 140] 1114 VISal4 Anterolateral visual area, layer 4 [8, 133, 140] ``` Pass `field="acronym"` (or `"name"`) to match one column exactly with a regex, useful for pinning down a single area without its layer subdivisions: ```pycon >>> atlas.atlas.search("VISp", field="acronym") acronym name rgb_triplet id 385 VISp Primary visual area [8, 133, 140] ``` `search`, `get_masks`, and `get_meshes` are also exposed as free functions in `confusius.atlas` that take the Dataset as their first argument, so `cf.atlas.search_atlas(atlas, "visual")` is equivalent to `atlas.atlas.search("visual")`. Both forms validate the Dataset as an atlas before running. ## Plotting Annotations over the Template The template and annotation volumes plot with the ordinary `plot_volume` tools. Overlaying the region boundaries on a template slice with `add_contours` gives the familiar atlas view; contour colors come from each region's atlas color automatically: ```python plotter = cf.plotting.plot_volume( atlas.atlas.template.sel(z=slice(6, 6)), show_colorbar=False ) plotter.add_contours(atlas.atlas.annotation.sel(z=slice(6, 6))) ``` ## Region Surface Meshes Many BrainGlobe atlases bundle a triangular surface mesh per region. `get_meshes` takes one or more regions and returns a `(vertices, faces)` pair per region, keyed by acronym, in the atlas's world space (millimeters) and ready to hand to any 3D viewer: ```pycon >>> atlas.atlas.get_meshes(["VISp", "AUDp"]).keys() dict_keys(['VISp', 'AUDp']) >>> vertices, faces = atlas.atlas.get_meshes("root")["root"] ``` napari's `add_surface` takes exactly that pair: ```python import napari napari.Viewer(ndisplay=3).add_surface(atlas.atlas.get_meshes("root")["root"]) ``` Pass `sides="left"` or `sides="right"` to clip to one hemisphere, or a list of sides to choose one per region; single-hemisphere meshes come back under `_L`/`_R`-suffixed keys. Because meshes come back in the atlas's current world space, they stay aligned with the volumes after a resample (see below). All region meshes are downloaded together the first time an atlas is fetched, so they are available offline afterwards. ## Masks for Regional Analysis `get_masks` turns regions into integer voxel masks stacked along a `mask` dimension, automatically including every descendant in the hierarchy. Pass one region or many, and optionally restrict each to a hemisphere: ```pycon >>> atlas.atlas.get_masks(["VISp", "AUDp", "MOp"]) Size: 14MB 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 Coordinates: * mask (mask) norm: >>> left_hip = atlas.atlas.get_masks("HIP", sides="left") >>> left_hip.mask Size: 20B 'HIP_L' Coordinates: * mask (mask) ) and [Atlas-based seed connectivity maps]() for more. ## Aligning an Atlas to a Recording An atlas is only useful once it shares a grid with your data. After registering a recording to the atlas template (see [Registration]()), `resample_like` warps the template, annotation, hemisphere map, and region meshes onto the recording's grid in one call, using an affine, B-spline, or displacement-field transform from `register_volume`: ```python resampled = atlas.atlas.resample_like(recording, transform) ``` Resampling can be costly, so it is worth doing once and caching the result. `save_atlas` / `load_atlas` write the whole atlas (arrays, structure hierarchy, and region meshes) to a Zarr store and read it back ready to use. The [Register a recording to an Allen fUSI template]() example walks through the full register → resample → save → reload workflow. ## API Reference For full parameter documentation, see the [atlas API reference]() and the [I/O API reference]() for `save_atlas`/`load_atlas`. # Datasets The `confusius.datasets` module provides fetchers for publicly available atlases, templates, and fUSI datasets distributed in [fUSI-BIDS]() format. Each fetcher downloads the dataset on first call, caches it locally for offline reuse, and returns either the path to the root directory, or a more specific object (e.g., a DataArray for templates or an atlas Dataset for atlases\]). > [!TIP] > > **Try before you buy** > > Fetchers generally accept filters (subjects, sessions, tasks, derivatives, etc.) so you can download a small subset first and decide later whether you want the full dataset. Cached files with matching SHA-256 hashes are reused. The ConfUSIus dataset collection is hosted on Amazon S3, with hosting sponsored by **AWS Open Data**. Downloads are anonymous HTTPS requests: no AWS account, credentials, or CLI is required. The [collection repository]() documents provenance, conversion recipes, and per-dataset licenses. Hosting does not change attribution or licensing requirements. ## Quick Start The fastest way to get started is to fetch a single subject from the Nunez-Elizalde 2022 dataset[^/dev/user-guide/datasets/#nunez2022] and load one of its NIfTI files: ```pycon >>> import confusius as cf >>> from confusius.datasets import fetch_nunez_elizalde_2022 >>> >>> # Download data for one mouse, one task, one brain slice (~30 MB). >>> root = fetch_nunez_elizalde_2022( ... subjects=["CR020"], ... sessions=["20191122"], ... tasks=["spontaneous"], ... acqs=["slice03"], ... ) >>> >>> # Load a power Doppler acquisition from the returned BIDS tree. >>> pwd = cf.load( ... root ... / "sub-CR020" ... / "ses-20191122" ... / "fusi" ... / "sub-CR020_ses-20191122_task-spontaneous_acq-slice03_pwd.nii.gz" ... ) >>> pwd Size: 30MB dask.array Coordinates: * time (time) float64 6kB 10.65 10.95 11.25 11.55 ... 234.8 235.1 235.4 * k (k) int64 8B 0 * j (j) int64 1kB 0 1 2 3 4 5 6 7 8 ... 117 118 119 120 121 122 123 124 * i (i) int64 640B 0 1 2 3 4 5 6 7 8 9 ... 71 72 73 74 75 76 77 78 79 * z (k, j, i) float64 80kB 0.9 0.9 0.9 0.9 0.9 ... 0.9 0.9 0.9 0.9 0.9 * y (k, j, i) float64 80kB 2.996 2.996 2.996 ... 8.988 8.988 8.988 * x (k, j, i) float64 80kB -3.95 -3.85 -3.75 -3.65 ... 3.75 3.85 3.95 Indexes: ┌ z VoxelToWorldIndex │ y └ x Attributes: (12/24) qform_code: 1 manufacturer: Verasonics manufacturers_model_name: Vantage 128 software_version: Alan Urban Technology & Consulting (... probe_manufacturer: Vermon probe_type: linear ... ... task_description: Spontaneous activity without explici... depth: [0.0, 5.991680000000001] transmit_frequency: 15625000.0 compound_sampling_frequency: 500.0 plane_wave_angles: [-10.0, -7.9, -5.8, -3.6999999999999... transmit_voltage: 25.0 ``` ## Datasets Storage ConfUSIus resolves the cache directory using the following priority chain: 1. The `data_dir` argument passed to the fetcher. 2. The `CONFUSIUS_DATA` environment variable. 3. The platform cache directory (e.g. `~/.cache/confusius` on Linux, `~/Library/Caches/confusius` on macOS, `%LOCALAPPDATA%\confusius\Cache` on Windows). You can inspect the resolved directory at any time with `get_datasets_dir`: ```pycon >>> from confusius.datasets import get_datasets_dir >>> get_datasets_dir() PosixPath('/home/alice/.cache/confusius') ``` Each collection fetcher caches releases under `//` (e.g. `nunez-elizalde-2022-bids/1.0.0/`). Its returned path is the versioned BIDS root, so paths constructed relative to that return value continue to work. Earlier releases are preserved when downloading a newer version. ## Listing Available Datasets Use `list_datasets` to print a table of available fetchers and their full download sizes: ```pycon >>> from confusius.datasets import list_datasets >>> >>> list_datasets() Available Datasets ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┓ ┃ Fetch function ┃ Size ┃ On disk ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━┩ │ fetch_nunez_elizalde_2022 │ 6.983 GB │ ✗ │ │ fetch_pereira_2025 │ 29.96 GB │ ✗ │ │ fetch_cybis_pereira_2026 │ 12.88 GB │ ✗ │ │ fetch_pepe_mariani_2026 │ 37.56 GB │ ✗ │ │ fetch_landemard_2026 │ 42.04 GB │ ✗ │ │ fetch_khallaf_2026 │ 24.02 GB │ ✗ │ │ fetch_template_brunner_grillet_2021 │ 16.34 MB │ ✗ │ │ fetch_template_pepe_mariani_2026 │ 5.508 MB │ ✗ │ └─────────────────────────────────────┴──────────┴─────────┘ ``` The sizes shown are for the **full** dataset. Filtered fetches are typically a small fraction of this (see the examples below). The same table is available from the command line: ```bash confusius datasets --list ``` ## Available fUSI-BIDS Datasets Simultaneous neural activity and cerebral blood volume recordings in awake mice, from Nunez-Elizalde et al. (2022)[^/dev/user-guide/datasets/#nunez2022], converted to fUSI-BIDS format and hosted in the S3 collection. Total size: **\~7 GB**. Use `fetch_nunez_elizalde_2022` to download the dataset. Four filters narrow the download: | Filter | BIDS entity | Example | | --- | --- | --- | | `subjects` | `sub-` | `["CR017", "CR020"]` | | `sessions` | `ses-` | `["20191122"]` | | `tasks` | `task-` | `["spontaneous"]` | | `acqs` | `acq-` | `["slice03"]` | Angiography files are always included regardless of the `tasks`/`acqs` filters, since they are useful as anatomical references. ```python from confusius.datasets import fetch_nunez_elizalde_2022 # Two subjects, one task, one slice. bids_root = fetch_nunez_elizalde_2022( subjects=["CR017", "CR020"], tasks=["spontaneous"], acqs=["slice03"], ) ``` Functional ultrasound imaging data from a rat model of neuroinflammation, from Pereira et al. (2025)[^/dev/user-guide/datasets/#pereira2025], re-exported to fUSI-BIDS format and hosted on the S3 collection. Total size: **\~30 GB**. Use `fetch_pereira_2025` to download the dataset. Three filters narrow the download: | Filter | BIDS entity / scope | Example | | --- | --- | --- | | `subjects` | `sub-` | `"r11582"` | | `sessions` | `ses-` | `"awakebaseline"` | | `tasks` | `task-` | `"rest"`, `"stim"` | ```python from confusius.datasets import fetch_pereira_2025 bids_root = fetch_pereira_2025( subjects="r11582", sessions="awakebaseline", tasks="rest", ) ``` Functional ultrasound imaging data from freely-moving rats investigating vascular coding of speed in the spatial navigation system, from Cybis Pereira et al. (2026)[^/dev/user-guide/datasets/#cybis2026], hosted in the S3 collection. Total size: **\~13 GB**. Use `fetch_cybis_pereira_2026` to download the dataset. Four filters narrow the download: | Filter | BIDS entity / scope | Example | | --- | --- | --- | | `datasets` | dataset name | `"glm-speed"`, `["rawdata", "dlc-videos"]` | | `subjects` | `sub-` | `["rat75", "rat73"]` | | `sessions` | `ses-` | `["20220523"]` | | `acqs` | `acq-` | `["slice32"]` | Files that lack a session or acquisition entity (e.g. subject-level statmaps in `glm-speed`, or `decode-speed` aggregates) are kept regardless of the `sessions` / `acqs` filters, since they aggregate across those entities. The `datasets` filter accepts: | Name | Contents | | --- | --- | | `rawdata` | Raw fUSI acquisitions per subject | | `glm-speed` | GLM outputs for linear speed | | `glm-angular-speed` | GLM outputs for angular speed | | `decode-speed` | Within-animal speed decoding results | | `interanimal-decode-speed` | Inter-animal speed decoding results | | `dlc-videos` | DeepLabCut behavior tracking videos | Derivatives are small relative to `rawdata`, so fetching a single derivative across all subjects is often the right first step: ```python from confusius.datasets import fetch_cybis_pereira_2026 # All subjects, GLM-speed derivative only. bids_root = fetch_cybis_pereira_2026(datasets="glm-speed") # Or raw data for a single subject, session, and acquisition slice. bids_root = fetch_cybis_pereira_2026( datasets="rawdata", subjects="rat73", sessions="20220523", acqs="slice32", ) ``` Transcranial mouse resting-state fUSI recordings and derivatives, from Pepe, Mariani et al. (2026)[^/dev/user-guide/datasets/#pepe_mariani2026], re-exported to fUSI-BIDS format and hosted in the S3 collection. Total size: **\~38 GB**. Use `fetch_pepe_mariani_2026` to download the dataset. Five filters narrow the download: | Filter | BIDS entity / scope | Example | | --- | --- | --- | | `datasets` | dataset name | `"registered"`, `"preprocessed"` | | `subjects` | `sub-` | `"m01"` | | `sessions` | `ses-` | `"rest"` | | `acqs` | `acq-` | `"coronal"` | | `datatypes` | datatype directory | `"fusi"`, `"susi"` | The `datasets` filter accepts `"rawdata"`, `"registered"`, `"preprocessed"`, and `"Params"`. ```python from confusius.datasets import fetch_pepe_mariani_2026 # Registered fUSI derivative only. bids_root = fetch_pepe_mariani_2026(datasets="registered", datatypes="fusi") ``` Functional ultrasound imaging recordings from awake, head-fixed, freely-running mice, from Landemard et al. (2026)[^/dev/user-guide/datasets/#landemard2026], re-exported to fUSI-BIDS format and hosted on the S3 collection. Total size: **\~42 GB**. Use `fetch_landemard_2026` to download the dataset. Four filters narrow the download: | Filter | BIDS entity / scope | Example | | --- | --- | --- | | `datasets` | dataset name | `"atlas_mapping"`, `["rawdata", "processed_data"]` | | `subjects` | `sub-` | `["ALD001", "ALD019"]` | | `acqs` | `acq-` | `["ref04", "ref11"]` | | `datatypes` | datatype directory | `"fusi"`, `["fusi", "susi"]` | The Landemard dataset has no session layer: every recording sits directly under `sub-*/fusi/` or `sub-*/susi/`. Files that lack an `acq-` entity (e.g. `sub-ALD001_scans.tsv`, `sub-ALD001/susi/sub-ALD001_pwd.nii.gz`) are always included regardless of the `acqs` filter. The `datasets` filter accepts: | Name | Contents | | --- | --- | | `rawdata` | Raw fUSI and angiography acquisitions per subject | | `atlas_mapping` | Per-subject Allen atlas alignment and region/session matches | | `processed_data` | Dataset-level compact representations, PSTHs, and ephys→fUSI filters | ```python from confusius.datasets import fetch_landemard_2026 # All subjects, atlas_mapping derivative only. bids_root = fetch_landemard_2026(datasets="atlas_mapping") # Or raw fUSI data for a single subject, with a single acquisition. bids_root = fetch_landemard_2026( datasets="rawdata", subjects="ALD001", acqs="ref04", datatypes="fusi", ) ``` Functional ultrasound imaging data from naked mole-rats exposed to olfactory stimulation, from Khallaf et al. (2026)[^/dev/user-guide/datasets/#khallaf2026], organised following BIDS and the proposed fUSI extension BEP-040. The original data is published on [Edmond (10.17617/3.7QCU1F)](); ConfUSIus downloads individual files from the S3 collection rather than the source archive. Use `fetch_khallaf_2026` to download the dataset. Six filters narrow the download: | Filter | BIDS entity / scope | Example | | --- | --- | --- | | `datasets` | dataset name | `"glm"`, `["rawdata", "bootstrapping"]` | | `subjects` | `sub-` | `"5622"`, `["5622", "6036"]` | | `sessions` | `ses-` | `"IPM"`, `["Air", "Etoh"]` | | `runs` | `run-` | `"1"`, `["1", "2"]` | | `reconstruction` | `rec-` | `"raw"`, `"resampled"`, `"both"` | | `sourcedata` | `sourcedata/` tree | `True` | Files that lack a subject, session, or run entity (e.g. `participants.tsv`, or group-level GLM maps) are kept regardless of those filters, since they aggregate across entities. `subjects` and `runs` also accept integers (`subjects=5622`). The `datasets` filter accepts: | Name | Contents | | --- | --- | | `rawdata` | Raw fUSI acquisitions per subject | | `glm` | GLM outputs, per-subject and group-level | | `bootstrapping` | Bootstrapping derivative | The `reconstruction` filter applies only to rawdata `fusi/` volumes: `"raw"` keeps the unregistered volumes (no `rec-` entity), `"resampled"` keeps the registered `rec-resampled` ones, and `"both"` (the default) keeps each. The raw Iconeus acquisitions under `sourcedata/` (\~7.8 GB) are excluded by default. Set `sourcedata=True` to include the whole tree; it is never entity-filtered. ```python from confusius.datasets import fetch_khallaf_2026 # All subjects, GLM derivative only. bids_root = fetch_khallaf_2026(datasets="glm") # Or the resampled raw data for a single subject, session, and run. bids_root = fetch_khallaf_2026( datasets="rawdata", subjects="5622", sessions="IPM", runs="1", reconstruction="resampled", ) ``` ## Working with fUSI-BIDS Datasets fUSI-BIDS dataset fetchers return a `pathlib.Path` to the dataset's root directory. You may want to use the [PyBIDS]() package for querying files, or simply use `pathlib` for quick exploration: ```python # List every power Doppler NIfTI for one subject. for nii in sorted((bids_root / "sub-CR020").rglob("*_pwd.nii.gz")): print(nii.relative_to(bids_root)) ``` See the [I/O guide]() for loading NIfTI, Zarr, and Iconeus SCAN files into VoxelData arrays. ### Selecting a New Release Collection fetchers resolve the published version from `last_versions.conf` and read that release's `manifest.json`, which records each file's size and SHA-256 hash. A local `s3_index.json` remembers the selected release. Subsequent calls reuse that release without querying the catalog. Existing files are trusted without rereading or hashing their contents, so cached fetches work offline and avoid scanning the whole collection. Missing files are downloaded and SHA-256 verified. To repair a corrupted or manually modified file, delete it and fetch again. Pass `refresh=True` to select the latest published release: ```python # Select the latest release without overwriting a previously cached version. bids_root = fetch_nunez_elizalde_2022(subjects=["CR020"], refresh=True) ``` Versions are immutable and use separate cache directories. Refreshing an unchanged release does not reread or download files whose recorded SHA-256 matches the remote manifest. If an upstream hash changes or the cached hash is unknown, the file is revalidated and downloaded when necessary. The same release-selection behavior applies to template fetchers. ## Available Templates A vascular mouse fUSI template from Brunner, Grillet et al. (2021)[^/dev/user-guide/datasets/#brunner2021], sourced from the Macé and Montaldo Zenodo dataset[^/dev/user-guide/datasets/#brunner_dataset] and distributed as a NIfTI with its JSON sidecar in the S3 collection under **CC BY 4.0**. Total size: **\~16.3 MB**. Use `fetch_template_brunner_grillet_2021` to download and load the template directly. Its axes are reordered onto the Allen 50 µm grid, whose affine is explicitly assigned without changing vascular voxel values: ```python from confusius.datasets import ( fetch_brainglobe_atlas, fetch_template_brunner_grillet_2021, ) template = fetch_template_brunner_grillet_2021() atlas = fetch_brainglobe_atlas("allen_mouse_50um") ``` A mouse fUSI template derived from Pepe, Mariani et al. (2026)[^/dev/user-guide/datasets/#pepe_mariani2026] and distributed as a NIfTI with its JSON sidecar in the S3 collection. Total size: **\~5.5 MB**. Use `fetch_template_pepe_mariani_2026` to download and load the template directly. Its native voxel grid and scanner-space `qform` are retained for registration. The `sform` encodes the full Allen atlas alignment, including shear. Use `world_to_sform` to resample the atlas onto the template's native grid: ```python from confusius.datasets import fetch_brainglobe_atlas, fetch_template_pepe_mariani_2026 template = fetch_template_pepe_mariani_2026() atlas = fetch_brainglobe_atlas("allen_mouse_100um") resampled_atlas = atlas.atlas.resample_like( template, template.attrs["affines"]["world_to_sform"], ) ``` ## Available Brain Atlases `fetch_brainglobe_atlas` fetches any [BrainGlobe]() atlas by name and returns it as an atlas `xarray.Dataset`. ConfUSIus provides the `.atlas` accessor for common atlas operations like resampling, masking, and region lookups: ```python from confusius.datasets import fetch_brainglobe_atlas atlas = fetch_brainglobe_atlas("allen_mouse_100um") masks = atlas.atlas.get_masks("VISp") ``` Unlike the fUSI-BIDS datasets above, BrainGlobe atlases are cached in BrainGlobe's own atlas directory (`~/.brainglobe`, shared with other BrainGlobe tools), so they do not appear in `list_datasets`. Use `data_dir` to override the cache location. ## API Reference See the `confusius.datasets` API reference for the full list of parameters and return types. [^/dev/user-guide/datasets/#nunez2022]: Nunez-Elizalde, A.O. et al. (2022). Neural correlates of blood flow measured by ultrasound. *Neuron*, 110(10), 1631–1640. [https://doi.org/10.1016/j.neuron.2022.02.012]() [^/dev/user-guide/datasets/#cybis2026]: Cybis Pereira, F. et al. (2026). A vascular code for speed in the spatial navigation system. *Cell Reports*, 45(1). [https://doi.org/10.1016/j.celrep.2025.116791]() [^/dev/user-guide/datasets/#landemard2026]: Landemard, A., Krumin, M., Harris, K. D., & Carandini, M. (2026). Brainwide blood volume reflects opposing neural populations. *Nature*. [https://doi.org/10.1038/s41586-026-10350-9]() [^/dev/user-guide/datasets/#pereira2025]: Pereira, M. et al. (2025). Induction of haemodynamic travelling waves by glial-related vasomotion in a rat model of neuroinflammation: Implications for functional neuroimaging. *eBioMedicine*, 116, 105777. [https://doi.org/10.1016/j.ebiom.2025.105777]() [^/dev/user-guide/datasets/#khallaf2026]: Khallaf, M. A. et al. (2026). A queen odour mediates reproductive suppression in a eusocial mammal. *Nature*. [https://doi.org/10.1038/s41586-026-10772-5]() [^/dev/user-guide/datasets/#brunner2021]: Brunner, C., Grillet, M. et al. (2021). Whole-brain functional ultrasound imaging in awake head-fixed mice. *Nature Protocols*, 16, 3547–3571. [https://doi.org/10.1038/s41596-021-00548-8]() [^/dev/user-guide/datasets/#brunner_dataset]: Macé, E. and Montaldo, G. (2020). Whole-brain visually evoked activity maps in head-fixed awake mice (June 2021 metadata update). *Zenodo*. [https://doi.org/10.5281/zenodo.4905862]() [^/dev/user-guide/datasets/#pepe_mariani2026]: Pepe, C. et al. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (fUSI). [https://doi.org/10.64898/2026.02.05.704055]() # Registration > [!NOTE] > > **Coming soon** > > This page is currently under construction. The `registration` module provides tools for motion correction and spatial alignment: > > **Motion correction:** > > - `register_volumewise`: Register each frame in a 4D series to a reference volume. > - `extract_motion_parameters`: Extract rigid-body motion parameters from transformation matrices. > - `compute_framewise_displacement`: Compute frame-to-frame displacement from motion parameters. > - `create_motion_dataframe`: Create a DataFrame with motion parameters for visualization. > > **Volume registration:** > > - `register_volume`: Register a 3D volume to a reference using rigid, affine, or deformable transforms. > - `resample_volume`: Resample a volume to a target affine and shape. > - `resample_like`: Resample a volume to match the affine and shape of a reference. > > **Affine utilities:** > > - `compose_affine`: Compose multiple affine transformations into one. > - `decompose_affine`: Decompose an affine matrix into translation, rotation, and scaling components. > > Please refer to the [API Reference]() for more information. # Temporal Resampling > [!NOTE] > > **Coming soon** > > This page is currently under construction. The timing module provides: > > - `get_time_coord_to_seconds_factor`: Get the conversion factor from time coordinate units to seconds. > - `convert_time_units`: Convert time values between units. > - `get_representative_time_step`: Get a representative time step from non-uniform time coordinates. > - `convert_time_reference`: Convert between volume timing references (start, center, end). > - `resample_time`: Resample fUSI time series to new time coordinates. > - `resample_to_uniform_time`: Resample fUSI time series onto a uniform time grid, fixing jitter, dropped frames, or non-uniform sampling. > > Please refer to the [API Reference]() for more information. # Spatial Processing > [!NOTE] > > **Coming soon** > > This page is currently under construction. The spatial processing module provides: > > - `smooth_volume`: Smooth a DataArray spatially using a Gaussian kernel, with FWHM specified in physical units. > > Please refer to the [API Reference]() for more information. # Signal Processing > [!NOTE] > > **Coming soon** > > This page is currently under construction. The signal processing modules provide: > > **Extraction:** > > - `extract_with_mask`: Flatten spatial dimensions into a voxel array using a boolean mask. > - `extract_with_labels`: Extract region-averaged signals using an integer label map. > - `unmask`: Reconstruct a full spatial volume from a flat voxel array. > > **Preprocessing:** > > - `censor_samples` / `interpolate_samples`: Mark or interpolate corrupted samples (e.g., motion outliers). > - `regress_confounds`: Remove nuisance signals via least-squares regression. > - `compute_compcor_confounds`: Compute CompCor confounds from high-variance voxels. > - `filter_butterworth`: Apply low-pass, high-pass, or band-pass Butterworth filters. > - `standardize`: Scale signals to unit variance or percent signal change. > - `detrend`: Remove linear or polynomial trends from time series. > - `clean`: Apply full preprocessing pipeline in one step. > > Please refer to the [API Reference]() for more information. # Decomposition > [!NOTE] > > **Coming soon** > > This page is currently under construction. The decomposition module currently provides: > > - `PCA`: Principal component analysis for `(time, ...)` `xarray.DataArray` inputs while preserving metadata through `transform` and `inverse_transform`. > - `NMF`: Non-negative matrix factorization for `(time, ...)` `xarray.DataArray` inputs while preserving metadata through `transform` and `inverse_transform`. > - `FastICA`: Fast independent component analysis for `(time, ...)` `xarray.DataArray` inputs while preserving metadata through `transform` and `inverse_transform`. > > Please refer to the [API Reference]() for more information. # Decoding > [!NOTE] > > **Coming soon** > > This guide is being written. In the meantime, see the API reference for `SearchLight`, and the [searchlight example](). # Functional Connectivity > [!NOTE] > > **Coming soon** > > This page is currently under construction. The `connectivity` module provides tools for computing functional connectivity from preprocessed fUSI time series. > > **Seed-based analysis:** > > - `SeedBasedMaps`: Compute seed-based Pearson correlation maps from fUSI time series. > > **Connectivity matrices:** > > - `ConnectivityMatrix`: Compute functional connectivity matrices from fUSI time series. > > Please refer to the [API Reference]() for more information. # General Linear Model > [!NOTE] > > **Coming soon** > > This page is currently under construction. The `glm` module provides a general linear model implementation for stimulus-evoked and task-based fUSI analysis. > > **First-level analysis:** > > - `FirstLevelModel`: Estimate a first-level GLM from fUSI data. > - `make_first_level_design_matrix`: Create a design matrix from stimulus onset times and timing parameters. > > **Second-level analysis:** > > - `SecondLevelModel`: Estimate a second-level (group) GLM. > - `make_second_level_design_matrix`: Create a design matrix for group analysis. > > **Contrasts:** > > - `Contrast`: Define and compute contrasts for statistical inference. > > **HRF models:** > > - `gamma_difference_hrf`: Base double-gamma HRF model with customizable parameters. > - `glover_hrf`: Glover canonical HRF. > - `spm_hrf`: SPM canonical HRF. > - `gamma_hrf`: Single-gamma HRF without undershoot (suitable for fUSI). > - `inverse_gamma_hrf`: Inverse gamma HRF model. > - `verhoef2025_hrf`: Human fUSI HRF preset (Verhoef et al., 2025). > - `claron2021_hrf`: Rodent spinal cord fUSI HRF preset (Claron et al., 2021). > > Please refer to the [API Reference]() for more information. # Statistical Inference > [!NOTE] > > **Coming soon** > > This page is currently under construction. The `stats` module provides shared statistical inference utilities for ConfUSIus analyses. > > **Multiple-comparison correction:** > > - `adjust_pvalues`: Adjust a p-value map for multiple comparisons. > > **Statistical-map thresholding:** > > - `apply_statistical_threshold`: Threshold a z-scaled statistical map with multiple-comparison correction and an optional cluster-extent threshold. > > Please refer to the [API Reference]() for more information. # Graphical interface # ConfUSIus Graphical Interface ConfUSIus ships a [napari]() plugin that provides an interactive graphical interface for fUSI data exploration, signals inspection, and quality control—no scripting required. > [!NOTE] > > **New to napari?** > > This guide only covers ConfUSIus-specific features. If you are new to napari, the official [napari tutorials]() are a great starting point for learning the viewer, layer system, and annotation tools. napari also has a rich [plugin hub]() with plugins for loading videos, multimodal recordings, and much more—all of which can be used alongside ConfUSIus. ## Launching the Plugin The `confusius` command is available in any Python environment where ConfUSIus is installed—see the [Installation guide]() for details. Alternatively, [`uvx`]() can fetch and run ConfUSIus without a prior install step. There are two ways to start the plugin: Open napari, then go to **Plugins \> ConfUSIus**. The ConfUSIus sidebar docks on the right side of the viewer automatically. Run the `confusius` command. This opens napari with the ConfUSIus sidebar ready to use: ```bash confusius ``` With `uvx`, no prior installation is needed: ```bash uvx -p 3.13 confusius ``` The ConfUSIus sidebar contains six collapsible panels—[Data I/O](), [Video](), [Signals](), [Events](), [QC](), and [Registration]()—that can each be expanded or collapsed independently. If you want a quick walkthrough inside napari itself, click **Take a Tour** in the upper-right corner of the sidebar header. > [!TIP] > > **Running napari programmatically** > > If you prefer to open napari from a Python script or Jupyter notebook, see the [Visualization]() guide, which covers `plot_napari` and related functions. ## Opening Data > [!TIP] > > **Try the built-in samples** > > To follow along without preparing your own data first, open **File \> Open Sample**. Sample recordings from ConfUSIus' dataset fetchers are available in this menu. ### Recommended: Data Panel and CLI The recommended way to open files is through the [Data Panel]() or by passing one or multiple paths to the `confusius` command on launch: ```bash confusius path/to/data.zarr path/to/other/data.zarr ``` Both routes use `confusius.load()` under the hood, which produces a fully-labeled DataArray with named dimensions, physical coordinates, and all file metadata preserved. The DataArray is attached to the layer and used automatically by the Signals and QC panels. Integer-dtype files (e.g. atlas annotations, ROI masks) are added as a `Labels` layer with per-label colors instead of a continuous colormap; all other dtypes are added as an `Image` layer. By default the full array is loaded into memory for responsive time scrubbing. For files that don't fit in RAM, lazy loading keeps the array Dask-backed—the layer appears instantly and slices are read from disk on demand. Enable it via the **Load lazily** checkbox in the Data Panel, or with the `--lazy` flag on the CLI: ```bash confusius --lazy path/to/data.zarr ``` ### Alternative: napari file readers ConfUSIus also registers native napari file readers for NIfTI, Iconeus SCAN, and Zarr files. These let you open files by dragging them onto the canvas or using **File \> Open** (Ctrl\+O) without switching to the Data Panel. Like the Data Panel and CLI, integer-dtype files are added as a `Labels` layer. > [!WARNING] > > **Axis labels are not propagated to the viewer** > > Due to a current napari limitation, files opened through the native readers have axis labels stored on the layer but not propagated to `viewer.dims.axis_labels`. This means dimension names will not appear in the napari status bar. Use the Data Panel or the `confusius` command instead to get correct axis labels everywhere. ## Saving Data ### Recommended: Data I/O Panel The [Data I/O Panel]() save section lets you export any layer to NIfTI or Zarr. Select the layer, optionally pick a template layer to borrow physical coordinates from, enter an output path, and click **Save**. > [!WARNING] > > **Multi-pose data** > > Saving multi-pose data from the GUI is not yet supported. Use the Python API to consolidate poses first or select a single pose before saving. ### Alternative: napari file writers ConfUSIus also registers native napari writers for NIfTI and Zarr files. These let you save any image or labels layer via **File → Save Selected Layer** (Ctrl\+S) without switching to the Data I/O Panel. Note that the native writer only supports saving the raw array data, so physical coordinates and metadata will not be preserved if they're not already present on the layer. ## Next Steps - [Using the Plugin](): a walkthrough of each panel, including loading and saving. - [Visualization](): programmatic napari and Matplotlib plotting from Python. - [Quality Control](): background and interpretation of the QC metrics computed by the plugin. - [napari tutorials](): official guides covering the viewer, layer system, and annotation tools. - [napari hub](): community plugins for loading additional data types and extending the viewer. # Using the Plugin The ConfUSIus sidebar contains six collapsible panels. Each panel operates independently and can be expanded or collapsed by clicking its header. For an in-app introduction, click **Take a Tour** in the sidebar header. - [**Data I/O**](<#data-io-panel>) — load and save fUSI files (NIfTI, Zarr, SCAN). - [**Video**](<#video-panel>) — load videos side-by-side, temporally synced with the fUSI acquisition. - [**Signals**](<#signals-panel>) — plot voxel, point, or label-region signals in a bottom dock. - [**Events**](<#events-panel>) — annotate periods of time (BIDS events) and shade them on the signal plot. - [**QC**](<#qc-panel>) — compute DVARS, carpet, CV, tSNR for a selected layer. - [**Registration**](<#registration-panel>) — run between-scan or within-scan registration, inspect progress, and save/apply transforms. ## Data I/O Panel The Data I/O Panel handles both loading and saving fUSI files without leaving the viewer. ### Loading data Click **Browse** to pick a file—it loads immediately on selection. Or paste a path directly in the text field and press Enter. Enable **Load lazily** beforehand to keep the array Dask-backed for large files. A progress bar animates during loading, and any error is reported in the napari notification bar. > [!NOTE] > > **Time overlay** > > When a loaded scan contains a time dimension, the current time coordinate is displayed as a text overlay in the bottom-left corner of the canvas. The value and units are read from the scan's coordinate metadata and update automatically as you scrub the time slider. > > When multiple scans are open, the overlay reflects the currently selected layer. If zero or more than one layer is selected, it keeps following the previously selected one. ### Saving data 1. Select the layer to save from the **Save layer** dropdown. 2. Optionally select a layer in the **Coordinates from** dropdown to borrow its physical coordinates and attributes. This is useful when saving a labels layer drawn on top of a fUSI image: selecting the image layer as the template preserves the full physical coordinate system. If the labels layer has fewer dimensions than the template (e.g. a 3D labels layer against a 4D image), the trailing spatial dimensions are used automatically. 3. Type an output path or click **Browse**. The format is inferred from the extension: `.nii` / `.nii.gz` for NIfTI and `.zarr` for Zarr. 4. Click **Save**. A notification confirms success. > [!WARNING] > > **Multi-pose data** > > Saving multi-pose data from the plugin is not yet supported. Use the Python API to consolidate poses first or select a single pose before saving. Three save modes are applied automatically depending on what is available: | Mode | When applied | | --- | --- | | **Direct** | The layer was loaded via ConfUSIus (DataArray in metadata). Saved verbatim, all coordinates and attributes preserved. | | **Template** | A template layer is selected. Coordinates are borrowed from the template DataArray. | | **Reconstruct** | No template and no DataArray in metadata (e.g. a freshly drawn labels layer). Coordinates are reconstructed from the napari layer state (`scale`, `translate`, `axis_labels`). | ## Video Panel The Video Panel loads one or more videos (`.mp4`, `.mov`, `.avi`) and overlays them beside a fUSI scan in a synchronized grid. Each video becomes its own napari Image layer whose time axis is locked to the reference scan, so scrubbing the time slider plays every video in lockstep with the fUSI recording. ### Loading a video 1. Pick the fUSI image layer to synchronize against in the **Reference layer** dropdown. The reference must have ConfUSIus coordinate metadata, so load it through the [Data I/O Panel](<#data-io-panel>) or the [`confusius` CLI]() command. 2. Insert a path or click **Browse** to pick a video file. 3. Click **Add video**. The video appears as a new Image layer, grid mode is enabled with a single-row layout, and the viewer shows the reference scan and the video side by side. Repeat to add more videos (each will get its own cell). All videos share the reference layer's axis labels, time index, and dimensionality; their spatial scale is chosen so the video height matches the fUSI height, with isotropic pixels and the frame centered on the scan. > [!TIP] > > **Launch with a video from the CLI** > > Pass both a data file and `--video` to open them together: > > ```bash > confusius path/to/scan.nii.gz --video path/to/camera.mp4 > ``` ### Playback | Option | Description | | --- | --- | | **Frame step** | Show every *N*-th frame of the video. Higher values skip frames for lighter playback of long or high-frame-rate recordings. The effective frame rate becomes `fps / N`. Changes apply to every loaded video. | > [!TIP] > > **Napari playback performance** > > Napari handles animations quite smoothly up to around 30 to 50 FPS (even higher depending on user hardware and operating system). Use **Frame step** to reduce the effective frame rate if playback is choppy or buffering. The time scale of each video layer is `frame_step / fps` seconds, so the napari time slider and the time overlay continue to report time in seconds regardless of the chosen step. > [!NOTE] > > **Time axis is kept out of the displayed dims** > > The panel installs a guard that prevents napari from ever placing the time axis in the 2D display. If you reorder dimensions such that time would become a display dimension, the order is silently corrected. ## Signals Panel The Signals Panel plots signals extracted from image layers along any non-spatial dimension (time, lag, feature, etc.). The plot appears in a bottom dock that is created the first time you click **Show Signal Plot**. ### Choosing a data source Pick one of three source modes in the **Source** group: ****Mouse**** Hold Shift and move the mouse over the canvas. The plot updates live with the single-voxel signal at the cursor position. ****Points**** Select a Points layer from the dropdown (or click **\+** to create one). Each point is plotted as a separate line colored by its face color. Add or remove points in napari and the plot updates automatically. ****Labels**** Select a Labels layer from the dropdown (or click **\+** to create one). The mean signal is extracted for each distinct non-zero label and plotted as a separate line, colored by the label's color in the napari colormap. This is useful for quickly comparing region-averaged signals after painting ROIs with napari's brush tool. In Points and Labels modes, the **Reference** dropdown selects which image layer to extract signals from. It defaults to **All image layers**, which plots each layer as a separate line (distinguished by line style). ### Axis parameters | Option | Description | | --- | --- | | ***x*-axis** | Choose which non-spatial dimension to plot on the horizontal axis. Defaults to `time` when available, otherwise the first non-spatial dimension. | | ***y* min / *y* max** | Manual limits for the vertical axis (disabled while autoscale is on). | | **Autoscale *y*-axis** | When enabled, the vertical axis rescales to fit the data automatically. Disabling it captures the current limits so you can fine-tune them. | ### Display options | Option | Description | | --- | --- | | **Show grid** | Show or hide the background grid. | | **Show x-axis cursor** | Draw a vertical line on the plot that follows the napari dimension slider for the selected *x*-axis dimension. | | **Z-score signal** | Normalize each signal to zero mean and unit variance before plotting. The *y*-axis label changes from "Intensity" to "Z-score". | > [!TIP] > > **Click to navigate** > > Left-click anywhere on the signal plot to jump the napari viewer to the corresponding time slice. Clicks are ignored while a zoom or pan tool is active in the plot toolbar. ### Managing signals Click **Manage Signals** to open a floating dialog where you can customize both live signals (from the current source mode) and imported signals: - **Rename**: Double-click a signal's name to edit it. - **Recolor**: Click the color swatch to pick a new color. Changes are synced back to the napari layer (point face color or label colormap). - **Show / Hide**: Toggle individual signal visibility with the checkbox. ### Importing and exporting signals ****Import**** In the Manage Signals dialog, click **Import** to load one or more signal files at once. Standard CSV / TSV files must contain a column whose header matches the current *x*-axis dimension name (e.g. `time`) plus one or more numeric value columns. BIDS physio files ending in `_physio.tsv.gz` are also supported: ConfUSIus reads their JSON sidecar, uses the declared column names, and synthesizes `time` from `SamplingFrequency` and `StartTime` when needed. Each value column becomes a separate signal overlaid on the plot. ****Export**** Click the **Export** button in the plot toolbar to export all currently plotted signals—both live and imported—to a CSV or TSV file. ## Events Panel The Events Panel allows annotating *periods of time*—not individual frames—following the [BIDS events]() convention (`onset`, `duration`, and an optional `trial_type`). Annotated events shade the [signal plot](<#signals-panel>) and are named in the time overlay while they are active. ### Annotating events 1. Type a name for the event in the **Event** field (defaults to `event`). 2. Press **Start (S)** to mark the onset at the current time step, scrub the time slider forward, then press **End (E)** to mark the offset. **Cancel (Esc)** discards an in-progress annotation. The shortcuts are active whenever the panel is open. 3. The event appears in the table with its trial type, onset, end, and duration. The End time must not be before the Start time. Ending at the Start time creates an instantaneous (zero-duration) event, as allowed by BIDS; it is drawn as a vertical line instead of a shaded band. ### Loading and saving Use **Load** to import a BIDS events `.tsv` file (`onset` and `duration` are required; a missing `trial_type` defaults to `event`) and **Save** to write the current events back out as a BIDS events `.tsv`. > [!TIP] > > **Straight into analysis** > > An events `.tsv` saved here is a standard BIDS events table, so it can be read back with load\_events and fed directly to either make\_first\_level\_design\_matrix or the `fit` method of a FirstLevelModel: > > ```python > from confusius.bids import load_events > from confusius.glm import FirstLevelModel > > events = load_events("sub-01_task-rest_events.tsv") > model = FirstLevelModel(hrf_model="glover").fit(fusi_scan, events=events) > ``` ### Display options | Option | Description | | --- | --- | | **Shade events on signal plot** | Draw each event as a colored band spanning its interval on the bottom-dock signal plot, colored by trial type. | | **Show active events in time overlay** | Append each active event to the time overlay as `name [onset, end)`. An event is active while the current frame's acquisition window overlaps it. The window comes from the layer's `volume_acquisition_reference`/`volume_acquisition_duration` metadata, using the frame spacing as the duration when only the reference is present, and the frame's timestamp alone when neither is; an event too short to be sampled by any frame is shown on the next frame instead. | ## QC Panel The QC Panel computes quality control metrics for a selected image layer. Select a layer from the **Layer** dropdown, check the metrics you want, and click **Compute**. Temporal metrics are rendered as plots in the bottom dock (the same dock used by the Signals Panel, in separate tabs). Computed plots are cached and survive dock closure: closing and reopening the bottom dock restores the last computed result. ****DVARS**** Plots the standardized temporal derivative of variance over time. A vertical cursor follows the napari time slider. See [DVARS]() for interpretation. ****Carpet plot**** Displays the full voxel time series as a 2D raster (time × voxels). See [Carpet Plot]() for interpretation. > [!TIP] > > **Click to navigate** > > Left-click anywhere on a temporal metric plot (DVARS or carpet) to jump the napari viewer to the corresponding time slice. Clicks are ignored while a zoom or pan tool is active in the plot toolbar. Spatial map metrics are added as new image layers in the napari layer list. ****CV**** Coefficient of variation map. ****tSNR**** Temporal signal-to-noise ratio map. > [!WARNING] > > **Prefer CV over tSNR for fUSI power Doppler data** > > tSNR is misleading for power Doppler: low-signal regions such as gel layers and shadow zones behind the skull can appear bright. CV correctly highlights regions with high temporal variability. See the [Quality Control guide]() for a full explanation. ## Registration Panel The Registration Panel runs the ConfUSIus registration workflows directly from napari. Use **Between scans** for registering different recordings, or **Within-scan** for volume-wise motion correction within a single recording. The panel supports modifying registration parameters, live preview, and saving/loading/applying computed transforms. > [!WARNING] > > **Multi-pose data** > > Registration of multi-pose data from the plugin is not yet supported. Use the Python API to consolidate poses first or select a single pose before registering. ### Between scans Use **Between scans** when you want to register one layer onto another, for example two recordings from different animals or a functional recording onto an angiography. 1. Select the **Moving layer** and **Fixed layer**. 2. Choose a transform model. 3. Optionally choose a **Scale** for to compress the intensity dynamics. 4. Optionally choose an **Initialization** transform if layers are very misaligned. 5. Click **Run registration**. Available transform models are: - `translation` for x/y/z-only shifts, - `rigid` for translations and rotations, - `affine` for translations, rotations, scaling, and shear, - `bspline` for non-linear local deformations. For `bspline`, a staged workflow usually works best: first run `rigid` or `affine`, then run `bspline` and select the previous transform in **Initialization**. This lets the B-spline model refine a good global alignment instead of trying to solve both large-scale and local deformation at once. #### Main parameters | Parameter | What it does | When it is useful | | --- | --- | --- | | **Transform** | Chooses the motion model being optimized. | Start with `translation` or `rigid` for simple alignment; use `affine` for global scale/shear differences; use `bspline` only after a good global initialization. | | **Mesh size** | Sets the B-spline control-grid density. | Increase it only when `bspline` needs to capture finer local mismatches; too fine a grid can lead to unrealistic warping. | | **Metric** | Chooses the similarity criterion (`correlation` or `mattes_mi`). | `correlation` is a good default for power Doppler data; `mattes_mi` is more robust when intensity distributions differ. | | **Fixed / Moving intensity scaling** | Applies optional intensity scaling (`decibel`, `square root`, or `none`) to the fixed and moving layers, only for the optimizer; result layers keep their original intensities. | Useful for power Doppler data where large vessels are typically overbright compared to finer structures. Set them differently when the two layers are already on different scales (e.g. a dB atlas and linear power Doppler). | | **Initialization** | Sets the starting transform before optimization. | Use `center_geometry` or `center_moments` for coarse setup; reuse a saved/manual affine transform when you already have a good approximate alignment. | | **Learning rate** | Sets the optimizer step size. | Leave **Auto** enabled to let SimpleITK estimate it each iteration, or untick it to use a fixed value (default `0.01`). | | **Iterations** | Maximum number of optimizer steps. | Increase it when alignment is still improving near the end of a run. | #### Advanced parameters | Parameter | What it does | When it is useful | | --- | --- | --- | | **Histogram bins** | Number of bins used by `mattes_mi`. | Tune only when using mutual information; more bins can capture finer intensity structure but may be noisier. | | **Convergence min** | Minimum optimizer improvement required to keep iterating. | Lower it when you want stricter convergence. | | **Convergence window** | Number of recent iterations used to test convergence. | Increase it to make convergence detection less sensitive to noise. | | **Multi-resolution** | Runs registration from coarse to fine scales. | Usually helpful for difficult B-spline alignments or large initial offsets. | | **Shrink factors** | Downsampling factors for each resolution level. | Use larger coarse levels when the initial mismatch is large. | | **Smoothing sigmas** | Gaussian smoothing at each resolution level. | Helps emphasize global structure before fine alignment. | | **Resample interp.** | Interpolation used for the registered output and previews. | `linear` is the usual default; `bspline` can give smoother resampled images. | | **Fill value** | Value written outside the moving field of view after resampling. | Useful for controlling the appearance of padded background. | | **ITK Threads** | Number of SimpleITK threads used for between-scan registration. | Reduce it when you are running several heavy jobs at once; `-1` uses all CPUs. | | **Optimizer weights** | Applies per-parameter optimizer weights. | Use it to freeze or down-weight specific affine parameters; not available for B-spline transforms. | The animation below uses between-session angiography volumes from the same animal across different days. ConfUSIus keeps the original layers untouched, adds dedicated preview layers for inspection, and stores the final registered result as a new layer when the run completes. ### Within-scan Use **Within-scan** for motion correction inside a single time series. 1. Switch **Mode** to **Within-scan**. 2. Select the time-series **Moving layer**. 3. Pick the registration target: either **Reference time**, a time index of the moving layer, or **Fixed layer**, another layer on the same grid without a time dimension (for example the mean of a few low-motion frames). 4. Pick a transform model (`translation`, `rigid`, or `affine`). 5. Click **Run registration**. #### Main parameters | Parameter | What it does | When it is useful | | --- | --- | --- | | **Reference time** | Registers every frame to the moving layer's frame at this time index. | Pick a representative, sharp frame with little motion. | | **Fixed layer** | Registers every frame to another layer on the same grid instead. The layer must have no time dimension. | Use a more stable target than any single frame, for example the mean of a few low-motion frames. | | **Transform** | Chooses the volume-wise motion model. | `rigid` is the safest starting point; `affine` is available when motion is more complex. | | **Metric** | Chooses the volume-to-reference similarity criterion. | `correlation` is usually a good default for within-recording motion correction. | | **Fixed / Moving intensity scaling** | Applies optional intensity scaling (`decibel`, `square root`, or `none`) to the fixed layer and to every frame, only for the optimizer. The fixed scaling is only active with **Fixed layer** selected; the reference frame uses the moving scaling. | Useful when an intensity transform makes anatomy more stable across time for the optimizer, or when the fixed layer is on a different intensity scale than the recording. | | **Initialization** | Sets the initial volume-wise centering transform. | Most runs can use no initialization. | | **Learning rate** | Sets the optimizer step size for each frame. | Within-scan uses a fixed value here; the default is `0.01`. Reduce it if updates look unstable; increase it if frames are already close and convergence is too slow. | | **Iterations** | Maximum optimizer steps per frame. | Increase it for harder motion or more flexible transforms. | #### Advanced parameters | Parameter | What it does | When it is useful | | --- | --- | --- | | **Histogram bins** | Number of bins used by `mattes_mi`. | Only relevant when using mutual information. | | **Convergence min** | Minimum optimizer improvement required to keep iterating. | Lower it when you want stricter volume-wise convergence. | | **Convergence window** | Number of recent iterations used to test convergence. | Increase it when convergence decisions look too jittery. | | **Multi-resolution** | Runs each frame registration from coarse to fine scales. | Helpful when motion is large or frames are noisy. | | **Shrink factors** | Downsampling factors for each resolution level. | Useful for coarse-to-fine motion correction. | | **Smoothing sigmas** | Gaussian smoothing at each resolution level. | Helps stabilize coarse registration before fine refinement. | | **Resample interp.** | Interpolation used for the motion-corrected output. | Controls output smoothness. | | **Fill value** | Value used outside the field of view after resampling. | Mostly useful for controlling output background appearance. | | **Diagnostics** | Stores full volume-wise optimizer traces. | Enable it only when you want detailed debugging or later inspection. | | **Parallel jobs** | Number of workers used for volume-wise registration. | Increase it to speed up long runs; reduce it if your machine is already busy. `-1` uses all available CPUs. | This workflow returns a `Motion corrected` layer and updates the progress bar as frames finish. The animation below uses a short open-field recording chunk and shows the result filling in progressively. # API reference # `confusius.atlas` ## atlas Brain atlases and region-aware utilities. An atlas is an `xarray.Dataset` with a registered `.atlas` accessor. Fetch one from BrainGlobe with `fetch_brainglobe_atlas`, save/load it with `save_atlas` / `load_atlas`, and operate on it through `ds.atlas.*` (see `AtlasAccessor`). The core operations are also exposed as standalone functions that take the Dataset as their first argument — `get_atlas_meshes`, `get_atlas_masks`, and `search_atlas` — each of which validates its input as an atlas first. Classes: - **`AtlasAccessor`** – Brain-atlas operations on an atlas `xarray.Dataset`. Functions: - **`get_atlas_masks`** – Return integer region masks stacked along a `mask` dimension. - **`get_atlas_meshes`** – Return one surface mesh per requested region, keyed by acronym. - **`search_atlas`** – Search an atlas Dataset's structures by name or acronym. ### AtlasAccessor Brain-atlas operations on an atlas `xarray.Dataset`. Registered as the `.atlas` namespace on any Dataset produced by `fetch_brainglobe_atlas` or `load_atlas`. `Dataset.attrs["structures"]` holds the BrainGlobe `StructuresDict` directly, so structural queries keep working for as long as that attribute rides along (xarray drops `attrs` on many ops by default; use `xarray.set_options(keep_attrs=True)` in pipelines). Parameters: - #### **`ds`** (`Dataset`) – Atlas Dataset with `template`, `annotation`, and `hemispheres` data variables as VoxelData arrays on a common `k`/`j`/`i` grid, and the atlas metadata in `attrs`. Methods: - **`ancestors`** – Return the ancestor nodes of `region`, from root down (exclusive). - **`get_masks`** – Return integer region masks stacked along a `mask` dimension. - **`get_meshes`** – Return one surface mesh per requested region, keyed by acronym. - **`resample`** – Resample the atlas onto an explicit output grid. - **`resample_like`** – Resample the atlas onto `reference`'s VoxelData grid. - **`search`** – Search structures by name or acronym. - **`show_tree`** – Print the structure hierarchy tree. Attributes: - **`annotation`** (`DataArray`) – Region annotations VoxelData array. - **`cmap`** (`ListedColormap`) – `ListedColormap` derived from `annotation.attrs["rgb_lookup"]`. - **`hemispheres`** (`DataArray`) – Hemisphere map VoxelData array (1 = left, 2 = right). - **`lookup`** (`DataFrame`) – DataFrame with columns `acronym`, `name`, `rgb_triplet`. - **`norm`** (`BoundaryNorm`) – `BoundaryNorm` derived from `annotation.attrs["rgb_lookup"]`. - **`structures`** (`StructuresDict`) – BrainGlobe structure dictionary held in `Dataset.attrs["structures"]`. - **`template`** (`DataArray`) – Anatomical template VoxelData array. #### annotation ```python annotation: DataArray ``` Region annotations VoxelData array. `attrs["rgb_lookup"]` carries a `{id: [r, g, b]}` dict used for colormap construction. Returns: - `DataArray` – The region annotation VoxelData array with integer labels. #### cmap ```python cmap: ListedColormap ``` `ListedColormap` derived from `annotation.attrs["rgb_lookup"]`. Returns: - `ListedColormap` – The colormap to use for atlas rendering. #### hemispheres ```python hemispheres: DataArray ``` Hemisphere map VoxelData array (1 = left, 2 = right). Returns: - `DataArray` – The hemisphere map VoxelData array. #### lookup ```python lookup: DataFrame ``` DataFrame with columns `acronym`, `name`, `rgb_triplet`. The DataFrame is indexed by structure index. Returns: - `DataFrame` – The structure lookup DataFrame. Cached on first access. #### norm ```python norm: BoundaryNorm ``` `BoundaryNorm` derived from `annotation.attrs["rgb_lookup"]`. Returns: - `BoundaryNorm` – The norm to use for atlas rendering. #### structures ```python structures: StructuresDict ``` BrainGlobe structure dictionary held in `Dataset.attrs["structures"]`. Returns: - `StructuresDict` – The structure dictionary with its hierarchy tree. Raises: - `KeyError` – If `Dataset.attrs` has no `structures` entry (e.g. after an xarray op that dropped `attrs`; wrap the pipeline in `xarray.set_options(keep_attrs=True)`). #### template ```python template: DataArray ``` Anatomical template VoxelData array. Returns: - `DataArray` – The anatomical template VoxelData array. #### ancestors ```python ancestors(region: int | str) -> list[Node] ``` Return the ancestor nodes of `region`, from root down (exclusive). Parameters: - ##### **`region`** (`int or str`) – Structure index or acronym. Returns: - `list[Node]` – Ancestor nodes ordered from root toward `region`, not including `region` itself. #### get\_masks ```python get_masks( regions: int | str | Sequence[int | str], sides: Literal["left", "right", "both"] | Sequence[Literal["left", "right", "both"]] = "both", ) -> DataArray ``` Return integer region masks stacked along a `mask` dimension. Each layer along `mask` has values in `{0, region_id}`; voxels belonging to the requested region (including all descendants in the hierarchy) carry the region's index, all others are zero. Parameters: - ##### **`regions`** (`int or str or sequence of int or str`) – One or more regions, each given as a structure index or acronym. - ##### **`sides`** (`(left, right, both)`, default: `"left"` ) – Hemisphere filter. Pass a scalar to apply the same side to all regions, or a sequence of the same length as `regions` for per-region control. Returns: - `DataArray` – Integer VoxelData array with dims `["mask", *annotation.dims]`. The `mask` coordinate holds the region acronym for each layer, suffixed with `_L`/`_R` when the corresponding `side` is `"left"`/`"right"` (left/right requests for the same region would otherwise share an acronym). Raises: - `TypeError` – If `ds` is not a well-formed atlas Dataset. - `KeyError` – If any requested region acronym or index is not found in the atlas. - `ValueError` – If `sides` is a sequence whose length does not match `regions`, or if any element of `sides` is not `"left"`, `"right"`, or `"both"`. Examples: ```pycon >>> ds.atlas.get_masks("VISp") >>> ds.atlas.get_masks("VISp", sides="left") >>> ds.atlas.get_masks(["VISp", "AUDp", "MOp"]) >>> ds.atlas.get_masks(["VISp", "AUDp"], sides=["left", "both"]) >>> ds.atlas.get_masks(["VISp", "VISp"], sides=["left", "right"]).coords["mask"].values array(['VISp_L', 'VISp_R'], dtype=object) ``` #### get\_meshes ```python get_meshes( regions: int | str | Sequence[int | str], sides: Literal["left", "right", "both"] | Sequence[Literal["left", "right", "both"]] = "both", *, clip: bool = True, ) -> dict[str, tuple[NDArray[float64], NDArray[int32]]] ``` Return one surface mesh per requested region, keyed by acronym. Reads each region's mesh, transforms its vertices from micron space to the DataArrays' current world space (millimetres), then optionally drops out-of-grid vertices and clips to one hemisphere. The mesh comes from the structure's `mesh_filename`: for a freshly fetched atlas this points into the BrainGlobe cache; for an atlas loaded with `load_atlas` it points at the mesh bundled inside the store. Parameters: - ##### **`regions`** (`int or str or sequence of int or str`) – One or more regions, each given as a structure index or acronym. - ##### **`sides`** (`(left, right, both)`, default: `"left"` ) – Hemisphere filter. Pass a scalar to apply the same side to all regions, or a sequence of the same length as `regions` for per-region control. `"both"` keeps the full mesh. `"left"` and `"right"` keep only vertices whose nearest `hemispheres` voxel carries that side's label (`hemispheres.attrs["left"]` / `["right"]`), sampled in the current world space. Faces are kept only when all three of their vertices survive, so the cut face is not closed. Sampling the hemisphere map makes this orientation-agnostic and correct after an arbitrary resample. - ##### **`clip`** (`bool`, default: `True` ) – Whether to clip each mesh to the current template grid. Unclipped meshes are still transformed to the current world space but may extend past the grid's bounding box. Returns: - `dict[str, tuple[(N, 3) numpy.ndarray, (M, 3) numpy.ndarray]]` – One `(vertices, faces)` pair per requested region, keyed by the region's acronym, suffixed with `_L`/`_R` when the corresponding `side` is `"left"`/`"right"` (left/right requests for the same region would otherwise share a key). `vertices` holds coordinates in the current world space (millimetres); after a nonlinear resample, vertices warped outside the template grid are dropped. `faces` holds zero-indexed triangle indices (int32). Raises: - `TypeError` – If `ds` is not a well-formed atlas Dataset. - `KeyError` – If any requested region is not found in the atlas. - `ValueError` – If `sides` is a sequence whose length does not match `regions`, if any element of `sides` is not `"left"`, `"right"`, or `"both"`, or if a region has no mesh file. - `RuntimeError` – If a region's mesh cannot be downloaded or read. Examples: ```pycon >>> vertices, faces = ds.atlas.get_meshes("VISp")["VISp"] >>> ds.atlas.get_meshes(["VISp", "AUDp"], sides=["left", "both"]).keys() dict_keys(['VISp_L', 'AUDp']) ``` #### resample ```python resample( transform: WorldToBaseTransform, *, output_sizes: Mapping[Hashable, SupportsIndex], output_spacing: Mapping[ Hashable, SupportsFloat | SupportsIndex ], output_origin: Mapping[ Hashable, SupportsFloat | SupportsIndex ], output_direction: ArrayLike, interpolation: Literal[ "linear", "nearest", "bspline" ] = "linear", sitk_threads: int = -1, ) -> Dataset ``` Resample the atlas onto an explicit output grid. The atlas template volume is resampled with `interpolation`; `annotation` and `hemispheres` are always resampled with nearest-neighbor interpolation to preserve integer labels. The returned Dataset stores the composed pull transform from its new world space back to the atlas base space in `attrs["world_to_base"]`. Parameters: - ##### **`transform`** (`(4, 4) numpy.ndarray or xarray.DataArray`) – Pull transform mapping output world coordinates to current atlas world coordinates. - ##### **`output_sizes`** (`mapping of str to int`) – Number of voxels along output axes, read by `k`/`j`/`i` keys. - ##### **`output_spacing`** (`mapping of str to float`) – World distance between output positions, read by `k`/`j`/`i` keys. - ##### **`output_origin`** (`mapping of str to float`) – World location of output position `(0, 0, 0)`, read by `z`/`y`/`x` keys. - ##### **`output_direction`** (`(3, 3) numpy.ndarray`) – Unit world-space direction columns for native `k`/`j`/`i` output axes. - ##### **`interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation used for the atlas template volume. - ##### **`sitk_threads`** (`int`, default: `-1` ) – Number of SimpleITK threads. Returns: - `Dataset` – Resampled atlas Dataset on the requested grid. Meshes returned by `get_meshes` are transformed through the composed `world_to_base` attribute. #### resample\_like ```python resample_like( reference: DataArray, transform: WorldToBaseTransform, *, interpolation: Literal[ "linear", "nearest", "bspline" ] = "linear", sitk_threads: int = -1, ) -> Dataset ``` Resample the atlas onto `reference`'s VoxelData grid. Parameters: - ##### **`reference`** (`DataArray`) – VoxelData array defining the target grid. Only its `k`/`j`/`i` grid (`sizes`/`spacing`/`origin`/`direction`) is used, so a `time` dimension, if present, is ignored. Must not have a `pose` or extra non-spatial dimension. - ##### **`transform`** (`(4, 4) numpy.ndarray or xarray.DataArray`) – Pull transform mapping reference world coordinates to atlas world coordinates. - ##### **`interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation used for the atlas template volume. - ##### **`sitk_threads`** (`int`, default: `-1` ) – Number of SimpleITK threads. Returns: - `Dataset` – Resampled atlas Dataset with exactly `reference`'s voxel labels and voxel-to-world affine. Meshes returned by `get_meshes` are transformed through the composed `world_to_base` attribute. Raises: - `ValueError` – If `reference` has a `pose` or extra non-spatial dimension, or is not a VoxelData array. #### search ```python search( pattern: str, field: Literal["all", "acronym", "name"] = "all", ) -> DataFrame ``` Search structures by name or acronym. Parameters: - ##### **`pattern`** (`str`) – Substring or regex pattern. - ##### **`field`** (`(all, acronym, name)`, default: `"all"` ) – Which column to search. - `"all"`: case-insensitive regex search on both `acronym` and `name`. - `"acronym"` / `"name"`: case-insensitive full regex match on that column only. Returns: - `DataFrame` – Filtered view of `lookup` matching the search criteria. Examples: ```pycon >>> ds.atlas.search("visual cortex") >>> ds.atlas.search("VISp", field="acronym") ``` #### show\_tree ```python show_tree(**kwargs: object) -> None ``` Print the structure hierarchy tree. Parameters: - ##### **`**kwargs`** (`object`, default: `{}` ) – Additional keyword arguments forwarded to `treelib.Tree.show`. Returns: - `None` – The tree is printed to standard output. ### get\_atlas\_masks ```python get_atlas_masks( ds: Dataset, regions: int | str | Sequence[int | str], sides: Literal["left", "right", "both"] | Sequence[Literal["left", "right", "both"]] = "both", ) -> DataArray ``` Return integer region masks stacked along a `mask` dimension. Each layer along `mask` has values in `{0, region_id}`; voxels belonging to the requested region (including all descendants in the hierarchy) carry the region's index, all others are zero. Parameters: - #### **`ds`** (`Dataset`) – Atlas Dataset; validated as an atlas before use. - #### **`regions`** (`int or str or sequence of int or str`) – One or more regions, each given as a structure index or acronym. - #### **`sides`** (`(left, right, both)`, default: `"left"` ) – Hemisphere filter. Pass a scalar to apply the same side to all regions, or a sequence of the same length as `regions` for per-region control. Returns: - `DataArray` – Integer VoxelData array with dims `["mask", *annotation.dims]`. The `mask` coordinate holds the region acronym for each layer, suffixed with `_L`/`_R` when the corresponding `side` is `"left"`/`"right"` (left/right requests for the same region would otherwise share an acronym). Raises: - `TypeError` – If `ds` is not a well-formed atlas Dataset. - `KeyError` – If any requested region acronym or index is not found in the atlas. - `ValueError` – If `ds` is not a well-formed atlas, if `sides` is a sequence whose length does not match `regions`, or if any element of `sides` is not `"left"`, `"right"`, or `"both"`. Examples: ```pycon >>> import confusius as cf >>> cf.atlas.get_atlas_masks(ds, "VISp") >>> cf.atlas.get_atlas_masks(ds, ["VISp", "AUDp"], sides=["left", "both"]) ``` ### get\_atlas\_meshes ```python get_atlas_meshes( ds: Dataset, regions: int | str | Sequence[int | str], sides: Literal["left", "right", "both"] | Sequence[Literal["left", "right", "both"]] = "both", *, clip: bool = True, ) -> dict[str, tuple[NDArray[float64], NDArray[int32]]] ``` Return one surface mesh per requested region, keyed by acronym. Reads each region's mesh, transforms its vertices from micron space to the atlas's world space (millimetres), then optionally drops out-of-grid vertices and clips to one hemisphere. The mesh comes from the structure's `mesh_filename`: for a freshly fetched atlas this points into the BrainGlobe cache; for an atlas loaded with `load_atlas` it points at the mesh bundled inside the store. Parameters: - #### **`ds`** (`Dataset`) – Atlas Dataset; validated as an atlas before use. - #### **`regions`** (`int or str or sequence of int or str`) – One or more regions, each given as a structure index or acronym. - #### **`sides`** (`(left, right, both)`, default: `"left"` ) – Hemisphere filter. Pass a scalar to apply the same side to all regions, or a sequence of the same length as `regions` for per-region control. `"both"` keeps the full mesh. `"left"` and `"right"` keep only vertices whose nearest `hemispheres` voxel carries that side's label (`hemispheres.attrs["left"]` / `["right"]`), sampled in the atlas's world space. Faces are kept only when all three of their vertices survive, so the cut face is not closed. Sampling the hemisphere map makes this orientation-agnostic and correct after an arbitrary resample. - #### **`clip`** (`bool`, default: `True` ) – Whether to clip each mesh to the template grid. Unclipped meshes are still transformed to the atlas's world space but may extend past the grid's bounding box. Returns: - `dict[str, tuple[(N, 3) numpy.ndarray, (M, 3) numpy.ndarray]]` – One `(vertices, faces)` pair per requested region, keyed by the region's acronym, suffixed with `_L`/`_R` when the corresponding `side` is `"left"`/`"right"` (left/right requests for the same region would otherwise share a key). `vertices` holds coordinates in the atlas's world space (millimetres); after a nonlinear resample, vertices warped outside the template grid are dropped. `faces` holds zero-indexed triangle indices (int32). Raises: - `TypeError` – If `ds` is not a well-formed atlas Dataset. - `KeyError` – If any requested region is not found in the atlas. - `ValueError` – If `ds` is not a well-formed atlas, if `sides` is a sequence whose length does not match `regions`, if any element of `sides` is not `"left"`, `"right"`, or `"both"`, or if a region has no mesh file. - `RuntimeError` – If a region's mesh cannot be downloaded or read. Examples: ```pycon >>> import confusius as cf >>> vertices, faces = cf.atlas.get_atlas_meshes(ds, "root")["root"] >>> cf.atlas.get_atlas_meshes(ds, ["VISp", "AUDp"], sides=["left", "both"]).keys() dict_keys(['VISp_L', 'AUDp']) ``` ### search\_atlas ```python search_atlas( ds: Dataset, pattern: str, field: Literal["all", "acronym", "name"] = "all", ) -> DataFrame ``` Search an atlas Dataset's structures by name or acronym. Parameters: - #### **`ds`** (`Dataset`) – Atlas Dataset to search; validated as an atlas before use. - #### **`pattern`** (`str`) – Substring or regex pattern. - #### **`field`** (`(all, acronym, name)`, default: `"all"` ) – Which column to search. - `"all"`: case-insensitive regex search on both `acronym` and `name`. - `"acronym"` / `"name"`: case-insensitive full regex match on that column only. Returns: - `DataFrame` – Filtered view of the atlas structure lookup table matching the search criteria. Raises: - `TypeError` – If `ds` is not a well-formed atlas Dataset. - `ValueError` – If `ds` is not a well-formed atlas Dataset. Examples: ```pycon >>> import confusius as cf >>> cf.atlas.search_atlas(ds, "visual cortex") >>> cf.atlas.search_atlas(ds, "VISp", field="acronym") ``` # fUSI-BIDS Integration ## bids FUSI-BIDS utilities for ConfUSIus. This module provides utilities for working with fUSI-BIDS metadata, including conversion between ConfUSIus and fUSI-BIDS naming conventions, validation of metadata, and coordinate handling. Examples: ```pycon >>> from confusius import bids >>> >>> # Convert ConfUSIus attributes to BIDS format >>> attrs = {"repetition_time": 1.5, "task_name": "rest", "transmit_frequency": 15e6} >>> bids_attrs = bids.to_bids(attrs) >>> bids_attrs {"RepetitionTime": 1.5, "TaskName": "rest", "TransmitFrequency": 15000000.0} >>> >>> # Convert BIDS metadata back to ConfUSIus format >>> attrs_back = bids.from_bids(bids_attrs) >>> attrs_back {"repetition_time": 1.5, "task_name": "rest", "transmit_frequency": 15000000.0} >>> >>> # Validate fUSI-BIDS metadata >>> metadata = bids.validate_metadata({ ... "TaskName": "rest", ... "RepetitionTime": 1.5, ... }) ``` Modules: - **`coordinates`** – Coordinate handling for fUSI-BIDS timing metadata. - **`events`** – Read and write BIDS events files (`.tsv`). - **`mapping`** – FUSI-BIDS field mapping between ConfUSIus and BIDS naming conventions. - **`physio`** – Read BIDS physio files into a signals table. - **`validation`** – Pydantic models for fUSI-BIDS metadata validation. Classes: - **`FUSIBIDSMetadata`** – FUSI-BIDS metadata model for sidecar JSON validation. Functions: - **`create_bids_slice_timing_from_coordinate`** – Extract fUSI-BIDS `SliceTiming` metadata from a `slice_time` coordinate. - **`create_slice_time_coordinate_from_bids`** – Create a `slice_time` coordinate from fUSI-BIDS `SliceTiming` metadata. - **`from_bids`** – Convert fUSI-BIDS metadata to ConfUSIus format. - **`load_events`** – Load a BIDS events file into an events table. - **`load_physio`** – Load a BIDS physio table into a DataFrame. - **`save_events`** – Save an events table to a BIDS events file. - **`to_bids`** – Convert ConfUSIus attributes to fUSI-BIDS format. - **`validate_metadata`** – Validate fUSI-BIDS metadata dictionary. Attributes: - **`DEFAULT_TRIAL_TYPE`** – Trial type assigned to events whose `trial_type` is missing or empty. - **`EXPLICIT_BIDS_FIELD_MAPPINGS`** (`Final[dict[str, str]]`) – Explicit mappings for standard BIDS fields with non-automatic names. - **`FUSI_BIDS_FIELDS`** (`frozenset[str]`) – Set of all fUSI-BIDS field names derived from the Pydantic model. ### DEFAULT\_TRIAL\_TYPE ```python DEFAULT_TRIAL_TYPE = 'event' ``` Trial type assigned to events whose `trial_type` is missing or empty. ### EXPLICIT\_BIDS\_FIELD\_MAPPINGS ```python EXPLICIT_BIDS_FIELD_MAPPINGS: Final[dict[str, str]] = { "volume_acquisition_duration": "FrameAcquisitionDuration" } ``` Explicit mappings for standard BIDS fields with non-automatic names. Maps ConfUSIus internal names to fUSI-BIDS field names. ### FUSI\_BIDS\_FIELDS ```python FUSI_BIDS_FIELDS: frozenset[str] = frozenset( FUSIBIDSMetadata.model_fields.keys() ) ``` Set of all fUSI-BIDS field names derived from the Pydantic model. This is the single source of truth for field names. ### FUSIBIDSMetadata Bases: `BaseModel` FUSI-BIDS metadata model for sidecar JSON validation. This model represents the fUSI-BIDS sidecar metadata as defined in the fUSI-BIDS extension proposal (BEP) v0.0.14. > [!NOTE] > > **Notes** > > At least one of `RepetitionTime` or `VolumeTiming` must be provided for valid fUSI-BIDS timing information. Examples: ```pycon >>> metadata = FUSIBIDSMetadata( ... TaskName="rest", ... RepetitionTime=1.5, ... Manufacturer="Verasonics", ... ) ``` Methods: - **`convert_numpy_array`** – Convert numpy arrays to lists for JSON serialization. - **`copy`** – Returns a copy of the model. - **`model_computed_fields`** – A mapping of computed field names to their respective `ComputedFieldInfo` instances. - **`model_construct`** – Creates a new instance of the `Model` class with validated data. - **`model_copy`** – > **Usage Documentation** - **`model_dump`** – > **Usage Documentation** - **`model_dump_json`** – > **Usage Documentation** - **`model_fields`** – A mapping of field names to their respective `FieldInfo` instances. - **`model_json_schema`** – Generates a JSON schema for a model class. - **`model_parametrized_name`** – Compute the class name for parametrizations of generic classes. - **`model_post_init`** – Override this method to perform additional initialization after `__init__` and `model_construct`. - **`model_rebuild`** – Try to rebuild the pydantic-core schema for the model. - **`model_validate`** – Validate a pydantic model instance. - **`model_validate_json`** – > **Usage Documentation** - **`model_validate_strings`** – Validate the given object with string data against the Pydantic model. - **`validate_slice_timing_consistency`** – Validate that `SliceTiming` and `SliceEncodingDirection` are consistent. - **`validate_timing`** – Validate timing field constraints per fUSI-BIDS spec. - **`validate_volume_timing`** – Validate that volume onset times are finite and strictly increasing. Attributes: - **`model_extra`** (`dict[str, Any] | None`) – Get extra fields set during validation. - **`model_fields_set`** (`set[str]`) – Returns the set of fields that have been explicitly set on this model instance. #### model\_extra ```python model_extra: dict[str, Any] | None ``` Get extra fields set during validation. Returns: A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`. #### model\_fields\_set ```python model_fields_set: set[str] ``` Returns the set of fields that have been explicitly set on this model instance. Returns: A set of strings representing the fields that have been set, i.e. that were not filled from defaults. #### convert\_numpy\_array ```python convert_numpy_array(v: Any) -> Any ``` Convert numpy arrays to lists for JSON serialization. Parameters: - ##### **`v`** (`Any`) – Value to convert. If a numpy array, converted to list. Returns: - `Any` – List if input was numpy array, otherwise input unchanged. #### copy ```python copy( *, include: AbstractSetIntStr | MappingIntStrAny | None = None, exclude: AbstractSetIntStr | MappingIntStrAny | None = None, update: Dict[str, Any] | None = None, deep: bool = False, ) -> Self ``` Returns a copy of the model. > [!WARNING] > > **Deprecated** > > This method is now deprecated; use `model_copy` instead. If you need `include` or `exclude`, use: ```python data = self.model_dump(include=include, exclude=exclude, round_trip=True) data = {**data, **(update or {})} copied = self.model_validate(data) ``` Args: include: Optional set or mapping specifying which fields to include in the copied model. exclude: Optional set or mapping specifying which fields to exclude in the copied model. update: Optional dictionary of field-value pairs to override field values in the copied model. deep: If True, the values of fields that are Pydantic models will be deep-copied. Returns: A copy of the model with included, excluded and updated fields as specified. #### model\_computed\_fields ```python model_computed_fields() -> dict[str, ComputedFieldInfo] ``` A mapping of computed field names to their respective `ComputedFieldInfo` instances. > [!WARNING] > > Accessing this attribute from a model instance is deprecated, and will not work in Pydantic V3. Instead, you should access this attribute from the model class. #### model\_construct ```python model_construct( _fields_set: set[str] | None = None, **values: Any ) -> Self ``` Creates a new instance of the `Model` class with validated data. Creates a new model setting `__dict__` and `__pydantic_fields_set__` from trusted or pre-validated data. Default values are respected, but no other validation is performed. > [!NOTE] > > `model_construct()` generally respects the `model_config.extra` setting on the provided model. That is, if `model_config.extra == 'allow'`, then all extra passed values are added to the model instance's `__dict__` and `__pydantic_extra__` fields. If `model_config.extra == 'ignore'` (the default), then all extra passed values are ignored. Because no validation is performed with a call to `model_construct()`, having `model_config.extra == 'forbid'` does not result in an error if extra values are passed, but they will be ignored. Args: \_fields\_set: A set of field names that were originally explicitly set during instantiation. If provided, this is directly used for the `model_fields_set` attribute. Otherwise, the field names from the `values` argument will be used. values: Trusted or pre-validated data dictionary. Returns: A new instance of the `Model` class with validated data. #### model\_copy ```python model_copy( *, update: Mapping[str, Any] | None = None, deep: bool = False, ) -> Self ``` > [!NOTE] > > **Usage Documentation** > > [`model_copy`]() Returns a copy of the model. > [!NOTE] > > The underlying instance's `__dict__` attribute is copied. This might have unexpected side effects if you store anything in it, on top of the model fields (e.g. the value of cached properties). Args: update: Values to change/add in the new model. Note: the data is not validated before creating the new model. You should trust this data. deep: Set to `True` to make a deep copy of the model. Returns: New model instance. #### model\_dump ```python model_dump( *, mode: Literal["json", "python"] | str = "python", include: IncEx | None = None, exclude: IncEx | None = None, context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, polymorphic_serialization: bool | None = None, ) -> dict[str, Any] ``` > [!NOTE] > > **Usage Documentation** > > [`model_dump`]() Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. Args: mode: The mode in which `to_python` should run. If mode is 'json', the output will only contain JSON serializable types. If mode is 'python', the output may contain non-JSON-serializable Python objects. include: A set of fields to include in the output. exclude: A set of fields to exclude from the output. context: Additional context to pass to the serializer. by\_alias: Whether to use the field's alias in the dictionary key if defined. exclude\_unset: Whether to exclude fields that have not been explicitly set. exclude\_defaults: Whether to exclude fields that are set to their default value. exclude\_none: Whether to exclude fields that have a value of `None`. exclude\_computed\_fields: Whether to exclude computed fields. While this can be useful for round-tripping, it is usually recommended to use the dedicated `round_trip` parameter instead. round\_trip: If True, dumped values should be valid as input for non-idempotent types such as Json\[T\]. warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, "error" raises a `PydanticSerializationError`. fallback: A function to call when an unknown value is encountered. If not provided, a `PydanticSerializationError` error is raised. serialize\_as\_any: Whether to serialize fields with duck-typing serialization behavior. polymorphic\_serialization: Whether to use model and dataclass polymorphic serialization for this call. Returns: A dictionary representation of the model. #### model\_dump\_json ```python model_dump_json( *, indent: int | None = None, ensure_ascii: bool = False, include: IncEx | None = None, exclude: IncEx | None = None, context: Any | None = None, by_alias: bool | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, exclude_computed_fields: bool = False, round_trip: bool = False, warnings: bool | Literal["none", "warn", "error"] = True, fallback: Callable[[Any], Any] | None = None, serialize_as_any: bool = False, polymorphic_serialization: bool | None = None, ) -> str ``` > [!NOTE] > > **Usage Documentation** > > [`model_dump_json`]() Generates a JSON representation of the model using Pydantic's `to_json` method. Args: indent: Indentation to use in the JSON output. If None is passed, the output will be compact. ensure\_ascii: If `True`, the output is guaranteed to have all incoming non-ASCII characters escaped. If `False` (the default), these characters will be output as-is. include: Field(s) to include in the JSON output. exclude: Field(s) to exclude from the JSON output. context: Additional context to pass to the serializer. by\_alias: Whether to serialize using field aliases. exclude\_unset: Whether to exclude fields that have not been explicitly set. exclude\_defaults: Whether to exclude fields that are set to their default value. exclude\_none: Whether to exclude fields that have a value of `None`. exclude\_computed\_fields: Whether to exclude computed fields. While this can be useful for round-tripping, it is usually recommended to use the dedicated `round_trip` parameter instead. round\_trip: If True, dumped values should be valid as input for non-idempotent types such as Json\[T\]. warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, "error" raises a `PydanticSerializationError`. fallback: A function to call when an unknown value is encountered. If not provided, a `PydanticSerializationError` error is raised. serialize\_as\_any: Whether to serialize fields with duck-typing serialization behavior. polymorphic\_serialization: Whether to use model and dataclass polymorphic serialization for this call. Returns: A JSON string representation of the model. #### model\_fields ```python model_fields() -> dict[str, FieldInfo] ``` A mapping of field names to their respective `FieldInfo` instances. > [!WARNING] > > Accessing this attribute from a model instance is deprecated, and will not work in Pydantic V3. Instead, you should access this attribute from the model class. #### model\_json\_schema ```python model_json_schema( by_alias: bool = True, ref_template: str = DEFAULT_REF_TEMPLATE, schema_generator: type[ GenerateJsonSchema ] = GenerateJsonSchema, mode: JsonSchemaMode = "validation", *, union_format: Literal[ "any_of", "primitive_type_array" ] = "any_of", ) -> dict[str, Any] ``` Generates a JSON schema for a model class. Args: by\_alias: Whether to use attribute aliases or not. ref\_template: The reference template. union\_format: The format to use when combining schemas from unions together. Can be one of: ```text - `'any_of'`: Use the [`anyOf`](https://json-schema.org/understanding-json-schema/reference/combining#anyOf) keyword to combine schemas (the default). - `'primitive_type_array'`: Use the [`type`](https://json-schema.org/understanding-json-schema/reference/type) keyword as an array of strings, containing each type of the combination. If any of the schemas is not a primitive type (`string`, `boolean`, `null`, `integer` or `number`) or contains constraints/metadata, falls back to `any_of`. schema_generator: To override the logic used to generate the JSON schema, as a subclass of `GenerateJsonSchema` with your desired modifications mode: The mode in which to generate the schema. ``` Returns: The JSON schema for the given model class. #### model\_parametrized\_name ```python model_parametrized_name( params: tuple[type[Any], ...], ) -> str ``` Compute the class name for parametrizations of generic classes. This method can be overridden to achieve a custom naming scheme for generic BaseModels. Args: params: Tuple of types of the class. Given a generic class `Model` with 2 type variables and a concrete model `Model[str, int]`, the value `(str, int)` would be passed to `params`. Returns: String representing the new class where `params` are passed to `cls` as type variables. Raises: TypeError: Raised when trying to generate concrete names for non-generic models. #### model\_post\_init ```python model_post_init(context: Any) -> None ``` Override this method to perform additional initialization after `__init__` and `model_construct`. This is useful if you want to do some validation that requires the entire model to be initialized. #### model\_rebuild ```python model_rebuild( *, force: bool = False, raise_errors: bool = True, _parent_namespace_depth: int = 2, _types_namespace: MappingNamespace | None = None, ) -> bool | None ``` Try to rebuild the pydantic-core schema for the model. This may be necessary when one of the annotations is a ForwardRef which could not be resolved during the initial attempt to build the schema, and automatic rebuilding fails. Args: force: Whether to force the rebuilding of the model schema, defaults to `False`. raise\_errors: Whether to raise errors, defaults to `True`. \_parent\_namespace\_depth: The depth level of the parent namespace, defaults to 2. \_types\_namespace: The types namespace, defaults to `None`. Returns: Returns `None` if the schema is already "complete" and rebuilding was not required. If rebuilding *was* required, returns `True` if rebuilding was successful, otherwise `False`. #### model\_validate ```python model_validate( obj: Any, *, strict: bool | None = None, extra: ExtraValues | None = None, from_attributes: bool | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None, ) -> Self ``` Validate a pydantic model instance. Args: obj: The object to validate. strict: Whether to enforce types strictly. extra: Whether to ignore, allow, or forbid extra data during model validation. See the `extra` configuration value for details. from\_attributes: Whether to extract data from object attributes. context: Additional context to pass to the validator. by\_alias: Whether to use the field's alias when validating against the provided input data. by\_name: Whether to use the field's name when validating against the provided input data. Raises: ValidationError: If the object could not be validated. Returns: The validated model instance. #### model\_validate\_json ```python model_validate_json( json_data: str | bytes | bytearray, *, strict: bool | None = None, extra: ExtraValues | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None, ) -> Self ``` > [!NOTE] > > **Usage Documentation** > > [JSON Parsing]() Validate the given JSON data against the Pydantic model. Args: json\_data: The JSON data to validate. strict: Whether to enforce types strictly. extra: Whether to ignore, allow, or forbid extra data during model validation. See the `extra` configuration value for details. context: Extra variables to pass to the validator. by\_alias: Whether to use the field's alias when validating against the provided input data. by\_name: Whether to use the field's name when validating against the provided input data. Returns: The validated Pydantic model. Raises: ValidationError: If `json_data` is not a JSON string or the object could not be validated. #### model\_validate\_strings ```python model_validate_strings( obj: Any, *, strict: bool | None = None, extra: ExtraValues | None = None, context: Any | None = None, by_alias: bool | None = None, by_name: bool | None = None, ) -> Self ``` Validate the given object with string data against the Pydantic model. Args: obj: The object containing string data to validate. strict: Whether to enforce types strictly. extra: Whether to ignore, allow, or forbid extra data during model validation. See the `extra` configuration value for details. context: Extra variables to pass to the validator. by\_alias: Whether to use the field's alias when validating against the provided input data. by\_name: Whether to use the field's name when validating against the provided input data. Returns: The validated Pydantic model. #### validate\_slice\_timing\_consistency ```python validate_slice_timing_consistency() -> FUSIBIDSMetadata ``` Validate that `SliceTiming` and `SliceEncodingDirection` are consistent. Returns: - `FUSIBIDSMetadata` – Self, if validation passes. #### validate\_timing ```python validate_timing() -> FUSIBIDSMetadata ``` Validate timing field constraints per fUSI-BIDS spec. Enforces: - `RepetitionTime` and `VolumeTiming` are mutually exclusive. - `FrameAcquisitionDuration` is REQUIRED when `VolumeTiming` is used AND `SliceTiming` is not set. - `FrameAcquisitionDuration` is mutually exclusive with `RepetitionTime`. Returns: - `FUSIBIDSMetadata` – Self, if validation passes. #### validate\_volume\_timing ```python validate_volume_timing(values: Any) -> Any ``` Validate that volume onset times are finite and strictly increasing. Parameters: - ##### **`values`** (`Any`) – Volume onset times in seconds, or None. Returns: - `Any` – Validated onset times, unchanged. Raises: - `ValueError` – If onset times are empty, negative, non-finite, or not increasing. ### create\_bids\_slice\_timing\_from\_coordinate ```python create_bids_slice_timing_from_coordinate( slice_time_coord: DataArray, volume_times: ArrayLike ) -> tuple[ndarray, str] ``` Extract fUSI-BIDS `SliceTiming` metadata from a `slice_time` coordinate. Parameters: - #### **`slice_time_coord`** (`(time, slice_encoding_direction) xarray.DataArray`) – Absolute slice time coordinate. - #### **`volume_times`** (`(n_time,) array-like`) – Absolute onset time of each volume, in the same units as `slice_time_coord`. Returns: - **`slice_timing`** ( `ndarray` ) – The slice acquisition times for one volume. - **`slice_encoding_direction`** ( `{i, j, k}` ) – The direction of slice acquisition. Raises: - `ValueError` – If the coordinate is not 2D, has an invalid spatial dimension, or if the slice timing varies across volumes after converting to onset-relative offsets. Examples: ```pycon >>> import numpy as np >>> volume_times = np.array([0.0, 1.0]) >>> slice_timing = np.array([0.0, 0.1, 0.2, 0.3]) >>> coord = create_slice_time_coordinate_from_bids( ... volume_times, slice_timing, slice_encoding_direction="k" ... ) >>> slice_timing, direction = create_bids_slice_timing_from_coordinate( ... coord, volume_times ... ) >>> slice_timing array([0. , 0.1, 0.2, 0.3]) >>> direction 'k' ``` ### create\_slice\_time\_coordinate\_from\_bids ```python create_slice_time_coordinate_from_bids( volume_times: ArrayLike, slice_timing: ArrayLike, slice_encoding_direction: Literal[ "i", "j", "k", "i-", "j-", "k-" ], units: str = "s", ) -> DataArray ``` Create a `slice_time` coordinate from fUSI-BIDS `SliceTiming` metadata. The `slice_time` coordinate stores the absolute acquisition time of each slice for each volume. Parameters: - #### **`volume_times`** (`(n_time,) array-like`) – Absolute onset time of each volume, in the same units as `slice_timing`. - #### **`slice_timing`** (`(n_slices,) array-like`) – Array of slice acquisition times relative to the onset of each volume. - #### **`slice_encoding_direction`** (`(i, j, k, 'i-', 'j-', 'k-')`, default: `"i"` ) – Direction of slice acquisition. A trailing `-` indicates that `slice_timing` is defined in reverse order (the first entry corresponds to the slice with the largest index). The values are reversed internally so the stored coordinate is always aligned with natural slice index order. - #### **`units`** (`str`, default: `'s'` ) – Units of the slice timing values. Returns: - `(n_time, n_slices) xarray.DataArray` – Absolute slice time coordinates. Examples: ```pycon >>> import numpy as np >>> slice_timing = np.array([0.0, 0.1, 0.2, 0.3]) # 4 slices >>> volume_times = np.array([0.0, 1.0]) >>> coord = create_slice_time_coordinate_from_bids( ... volume_times, slice_timing, slice_encoding_direction="k" ... ) >>> coord.dims ('time', 'k') ``` ```pycon >>> coord.shape (2, 4) ``` ### from\_bids ```python from_bids( bids_attrs: Mapping[str, object], ) -> dict[str, object] ``` Convert fUSI-BIDS metadata to ConfUSIus format. Known fUSI-BIDS fields are converted to snake\_case. ConfUSIus-prefixed attributes are converted back to internal names. Unknown fields are preserved as-is to ensure round-trip safety. Values and units are unchanged. Parameters: - #### **`bids_attrs`** (`Mapping[str, object]`) – Dictionary with fUSI-BIDS (PascalCase) field names. Returns: - `dict[str, object]` – Dictionary with ConfUSIus (snake\_case) attribute names. Examples: ```pycon >>> bids_attrs = {"RepetitionTime": 1.5, "TaskName": "rest", "CustomField": 123} >>> from_bids(bids_attrs) {'repetition_time': 1.5, 'task_name': 'rest', 'CustomField': 123} ``` ### load\_events ```python load_events(path: str | Path) -> DataFrame ``` Load a BIDS events file into an events table. Parameters: - #### **`path`** (`str or Path`) – Path to a tab-separated BIDS events file. Returns: - `DataFrame` – Events table with columns ordered `onset` (float), `duration` (float), `trial_type` (str), then any extra columns from the file (preserved verbatim). A missing `trial_type` column or blank/`n/a` cell defaults to `DEFAULT_TRIAL_TYPE`. Raises: - `ValueError` – If the `onset` or `duration` column is missing, or if any `onset` or `duration` value is missing or non-numeric. ### load\_physio ```python load_physio(path: str | Path) -> DataFrame ``` Load a BIDS physio table into a DataFrame. Parameters: - #### **`path`** (`str or Path`) – Path to a BIDS physio TSV file, typically ending in `_physio.tsv` or `_physio.tsv.gz`. Returns: - `DataFrame` – Physio table with sidecar-defined column names. When the sidecar does not include a `time` column, one is inserted first using `SamplingFrequency` and `StartTime`. The sidecar JSON fields are copied to `DataFrame.attrs`. Raises: - `FileNotFoundError` – If the matching JSON sidecar does not exist. - `ValueError` – If the sidecar JSON is invalid, does not define a non-empty `Columns` list, if the TSV width does not match `Columns`, or if timing metadata is needed to synthesize `time` but missing or invalid. ### save\_events ```python save_events(path: str | Path, events: DataFrame) -> None ``` Save an events table to a BIDS events file. Rows are sorted by onset, as recommended by BIDS, and columns are ordered `onset`, `duration`, `trial_type` (when present), then any extra columns. Missing values are written as `"n/a"`. Parameters: - #### **`path`** (`str or Path`) – Output path for the tab-separated events file. - #### **`events`** (`DataFrame`) – Events table containing at least `onset` and `duration` columns. An empty table writes a header-only file. Returns: - `None` – This function writes to disk and returns nothing. Raises: - `TypeError` – If `events` is not a `pandas.DataFrame`. - `ValueError` – If `events` is missing the `onset` or `duration` column. ### to\_bids ```python to_bids(attrs: Mapping[str, object]) -> dict[str, object] ``` Convert ConfUSIus attributes to fUSI-BIDS format. Only converts known fUSI-BIDS fields. Internal ConfUSIus attributes are prefixed with "ConfUSIus". Values and units are unchanged; unknown fields are preserved as-is. Parameters: - #### **`attrs`** (`Mapping[str, object]`) – Dictionary with ConfUSIus (snake\_case) attribute names. Returns: - `dict[str, object]` – Dictionary with fUSI-BIDS (PascalCase) field names. Examples: ```pycon >>> attrs = {"repetition_time": 1.5, "task_name": "rest", "custom_field": 123} >>> to_bids(attrs) {'RepetitionTime': 1.5, 'TaskName': 'rest', 'custom_field': 123} ``` ### validate\_metadata ```python validate_metadata( metadata: Mapping[str, Any], ) -> FUSIBIDSMetadata ``` Validate fUSI-BIDS metadata dictionary. Parameters: - #### **`metadata`** (`Mapping[str, Any]`) – Dictionary containing fUSI-BIDS metadata fields. Returns: - `FUSIBIDSMetadata` – Validated metadata model. Raises: - `ValidationError` – If the metadata fails validation. Examples: ```pycon >>> metadata = validate_metadata({ ... "TaskName": "rest", ... "RepetitionTime": 1.5, ... }) >>> metadata.TaskName 'rest' ``` # `confusius.connectivity` ## connectivity Functional connectivity analysis for fUSI data. Modules: - **`matrix`** – Connectivity matrix estimation from time series data. - **`seed`** – Seed-based functional connectivity maps for fUSI data. Classes: - **`ConnectivityMatrix`** – Functional connectivity matrices from fUSI region time series. - **`SeedBasedMaps`** – Seed-based functional connectivity maps from fUSI data. Functions: - **`covariance_to_correlation`** – Return the correlation matrix for a given covariance matrix. - **`precision_to_partial_correlation`** – Return the partial correlation matrix for a given precision matrix. - **`symmetric_matrix_to_vector`** – Return the flattened lower triangular part of a symmetric matrix. - **`vector_to_symmetric_matrix`** – Return the symmetric matrix given its flattened lower triangular part. ### ConnectivityMatrix Bases: `BaseEstimator` Functional connectivity matrices from fUSI region time series. Computes pairwise connectivity matrices between brain regions from time series DataArrays using one of several estimators: covariance, correlation, partial correlation, precision, or tangent-space projection. Supports both single-subject and group-level analysis. Parameters: - #### **`cov_estimator`** (`sklearn covariance estimator`, default: `None` ) – Estimator used to compute covariance matrices. Defaults to `LedoitWolf(store_precision=False)`, which applies a small shrinkage towards zero compared to the maximum-likelihood estimate. - #### **`kind`** (`(covariance, correlation, 'partial correlation', tangent, precision)`, default: `"covariance"` ) – Type of connectivity matrix to compute. - `"covariance"`: raw covariance matrix. - `"correlation"`: Pearson correlation matrix. - `"partial correlation"`: partial correlation matrix, controlling for all other variables. - `"precision"`: inverse of the covariance matrix. - `"tangent"`: symmetric displacement in the tangent space at the group geometric mean. Requires at least two subjects in `fit_transform`. - #### **`vectorize`** (`bool`, default: `False` ) – Whether connectivity matrices should be flattened to 1D vectors containing only the lower triangular elements. - #### **`discard_diagonal`** (`bool`, default: `False` ) – Whether diagonal elements should be excluded from the vectorized output. Only used when `vectorize` is `True`. Attributes: - **`cov_estimator_`** (`sklearn covariance estimator`) – A copy of `cov_estimator` with the same parameters, used during fitting. - **`mean_`** (`(n_features, n_features) numpy.ndarray`) – Mean connectivity matrix across subjects. For `"tangent"` kind, this is the geometric mean of the covariance matrices. For other kinds, it is the arithmetic mean. - **`whitening_`** (`(n_features, n_features) numpy.ndarray or None`) – Inverse square-root of the geometric mean covariance. Only set for `"tangent"` kind; `None` otherwise. - **`n_features_in_`** (`int`) – Number of features seen during `fit`. - **`features_dim_in_`** (`str`) – Name of the features dimension in the input DataArrays. > [!NOTE] > > **Notes** > > Adapted from Nilearn's `ConnectivityMeasure` (BSD-3-Clause License; see `NOTICE` for attribution). **References** 1. Varoquaux, G., Baronnet, F., Kleinschmidt, A., Fillard, P., Thirion, B. (2010). Detection of Brain Functional-Connectivity Difference in Post-stroke Patients Using Group-Level Covariance Modeling. In: Jiang, T., Navab, N., Pluim, J.P.W., Viergever, M.A. (eds) Medical Image Computing and Computer-Assisted Intervention – MICCAI 2010. MICCAI 2010. Lecture Notes in Computer Science, vol 6361. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-642-15705-9\_25 Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> from confusius.connectivity import ConnectivityMatrix >>> >>> rng = np.random.default_rng(0) >>> # Five subjects, each with 100 time points and 10 brain regions. >>> signals = [ ... xr.DataArray( ... rng.standard_normal((100, 10)), ... dims=["time", "regions"], ... ) ... for _ in range(5) ... ] >>> >>> measure = ConnectivityMatrix(kind="correlation") >>> connectivities = measure.fit_transform(signals) >>> connectivities.shape (5, 10, 10) >>> >>> # Vectorized output. >>> measure_vec = ConnectivityMatrix(kind="correlation", vectorize=True) >>> vecs = measure_vec.fit_transform(signals) >>> vecs.shape (5, 55) ``` Methods: - **`fit`** – Fit the covariance estimator and compute group-level statistics. - **`fit_transform`** – Fit and transform in one step, computing covariances only once. - **`get_metadata_routing`** – Get metadata routing of this object. - **`get_params`** – Get parameters for this estimator. - **`inverse_transform`** – Reconstruct connectivity matrices from vectorized or tangent-space forms. - **`set_params`** – Set the parameters of this estimator. - **`transform`** – Compute connectivity matrices for new subjects. #### fit ```python fit( X: DataArray | list[DataArray], y: None = None ) -> ConnectivityMatrix ``` Fit the covariance estimator and compute group-level statistics. Parameters: - ##### **`X`** (`DataArray or list[DataArray]`) – Time series for each subject. Each DataArray must have a `time` dimension and exactly one additional dimension (the features/regions dimension). The number of timepoints may differ across subjects, but the features dimension must have the same name and size. - ##### **`y`** (`None`, default: `None` ) – Ignored. Present for sklearn API compatibility. Returns: - `ConnectivityMatrix` – Fitted estimator. Raises: - `TypeError` – If `X` is not a DataArray or list of DataArrays. - `ValueError` – If any subject is missing the `time` dimension, has an incorrect number of dimensions, has inconsistent feature sizes, or if `kind` is not one of the allowed values. > [!NOTE] > > **Notes** > > Dask-backed DataArrays are computed in memory during `fit` when covariance matrices are estimated. This class is inherently eager: covariance estimation requires the full time series. #### fit\_transform ```python fit_transform( X: DataArray | list[DataArray], y: None = None ) -> NDArray ``` Fit and transform in one step, computing covariances only once. Parameters: - ##### **`X`** (`DataArray or list[DataArray]`) – Time series for each subject. Each DataArray must have a `time` dimension and exactly one additional features dimension. The number of timepoints may differ across subjects, but the features dimension must be consistent. - ##### **`y`** (`None`, default: `None` ) – Ignored. Present for sklearn API compatibility. Returns: - `(n_subjects, n_features, n_features) numpy.ndarray or (n_subjects, n_features * (n_features + 1) / 2) numpy.ndarray` – Connectivity matrices, or their vectorized lower triangular parts when `vectorize` is `True`. Raises: - `TypeError` – If `X` is not a DataArray or list of DataArrays. - `ValueError` – If subjects have inconsistent features dimensions, if `kind` is not valid, or if `kind="tangent"` is used with a single subject (tangent space returns deviations from a group mean, which is trivially zero for a single subject). #### get\_metadata\_routing ```python get_metadata_routing() ``` Get metadata routing of this object. Please check :ref:`User Guide ` on how the routing mechanism works. Returns: - **`routing`** ( `MetadataRequest` ) – A :class:`~sklearn.utils.metadata_routing.MetadataRequest` encapsulating routing information. #### get\_params ```python get_params(deep=True) ``` Get parameters for this estimator. Parameters: - ##### **`deep`** (`bool`, default: `True` ) – If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns: - **`params`** ( `dict` ) – Parameter names mapped to their values. #### inverse\_transform ```python inverse_transform( connectivities: NDArray, diagonal: NDArray | None = None ) -> NDArray ``` Reconstruct connectivity matrices from vectorized or tangent-space forms. Parameters: - ##### **`connectivities`** (`(n_subjects, n_features, n_features) numpy.ndarray or (n_subjects, n_features * (n_features + 1) / 2) numpy.ndarray or (n_subjects, (n_features - 1) * n_features / 2) numpy.ndarray`) – Connectivity matrices or their vectorized forms. When `kind="tangent"`, these are tangent space displacements that are mapped back to covariance matrices. - ##### **`diagonal`** (`(ndarray, shape(n_subjects, n_features))`, default: `None` ) – Diagonal values to restore when `discard_diagonal` was `True`. Required for `"covariance"` and `"precision"` kinds when the diagonal was discarded; for `"correlation"` and `"partial correlation"`, a diagonal of ones is assumed automatically. Returns: - `(ndarray, shape(n_subjects, n_features, n_features))` – Reconstructed connectivity matrices. For `"tangent"` kind, these are the original covariance matrices. Raises: - `NotFittedError` – If the estimator has not been fitted yet. - `ValueError` – If the diagonal was discarded for an ambiguous kind (`"covariance"` or `"precision"`) and no `diagonal` is provided. #### set\_params ```python set_params(**params) ``` Set the parameters of this estimator. The method works on simple estimators as well as on nested objects (such as :class:`~sklearn.pipeline.Pipeline`). The latter have parameters of the form `__` so that it's possible to update each component of a nested object. Parameters: - ##### **`**params`** (`dict`, default: `{}` ) – Estimator parameters. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. #### transform ```python transform(X: DataArray | list[DataArray]) -> NDArray ``` Compute connectivity matrices for new subjects. Parameters: - ##### **`X`** (`DataArray or list[DataArray]`) – Time series for each subject. The features dimension name and size must match the values seen during `fit`. Returns: - `(n_subjects, n_features, n_features) numpy.ndarray or` – ```text (n_subjects, n_features * (n_features + 1) / 2) numpy.ndarray ``` Connectivity matrices, or their vectorized lower triangular parts when `vectorize` is `True`. Raises: - `NotFittedError` – If the estimator has not been fitted yet. - `ValueError` – If any subject has a features dimension that does not match `features_dim_in_` or `n_features_in_`. ### SeedBasedMaps Bases: `BaseEstimator` Seed-based functional connectivity maps from fUSI data. Computes voxel-wise Pearson correlation maps between one or more seed region signals and every voxel in a VoxelData array. Two ways to supply the seed signal are supported: - **Mask-based** (`seed_masks`): integer label maps are passed and the seed signals are extracted from the (optionally cleaned) data via `extract_with_labels`. Signal cleaning via `clean` is applied to the full data array *before* seed extraction so that both the seed signal and the per-voxel signals are preprocessed consistently. - **Signal-based** (`seed_signals`): pre-computed `(time, region)` seed signals are provided directly. In this case extraction is skipped entirely and the supplied signals are correlated against the (optionally cleaned) data. This is useful when the seed signal has been computed externally or originates from a different modality. Exactly one of `seed_masks` or `seed_signals` must be provided. Parameters: - #### **`seed_masks`** (`DataArray`, default: `None` ) – Integer label maps defining the seed region(s). Two formats are accepted (same as `extract_with_labels`): - **Flat label map**: spatial dims only, e.g. `(k, j, i)`. Background voxels are `0`; each unique non-zero integer is a separate seed region. - **Stacked mask format**: leading `mask` dim followed by spatial dims, e.g. `(mask, k, j, i)`. Each layer has values in `{0, region_id}` and regions may overlap. A boolean mask can be used by converting it first: `mask.astype(int)`. Mutually exclusive with `seed_signals`. - #### **`seed_signals`** (`DataArray`, default: `None` ) – Pre-computed seed signals with a `time` dimension and an optional `region` dimension. When provided, seed extraction from the data is skipped and these signals are used directly to compute Pearson correlations. `clean_kwargs` is still applied to the *data* array before computing correlations, but the seed signals themselves are used as-is. Mutually exclusive with `seed_masks`. - #### **`labels_reduction`** (`('mean', 'sum', 'median', 'min', 'max', 'var', 'std')`, default: `"mean"` ) – Aggregation function applied across voxels within each seed region when extracting seed signals from `seed_masks`. Ignored when `seed_signals` is provided. - #### **`clean_kwargs`** (`dict`, default: `None` ) – Keyword arguments forwarded to `clean`. Cleaning is applied before computing correlations. If `mask` is provided, cleaning is applied to the masked `(time, space)` signals for efficiency. If not provided, no cleaning is applied. > **Chunking along time** > > Any operation in `clean_kwargs` that involves detrending or filtering requires the `time` dimension to be un-chunked. Rechunk your data before calling `fit`: `data.chunk({'time': -1})`. - #### **`mask`** (`DataArray`, default: `None` ) – Boolean spatial mask selecting which voxels are included in the correlation computation. When provided, voxel signals are first extracted with `extract_with_mask`, so cleaning and correlation are computed only on masked voxels. Output maps retain the full spatial geometry, with voxels outside the mask set to `0`. Attributes: - **`seed_signals_`** (`(time, region) xarray.DataArray`) – Extracted (and cleaned) seed region signals when `seed_masks` is used, or the supplied signals (possibly transposed to `(time, region)` order) when `seed_signals` is used. Set after `fit`. - **`maps_`** (`(region, ...) xarray.DataArray`) – Voxel-wise Pearson r maps, one per seed region, set after `fit`. `region` is the leading dimension; the remaining dimensions match the spatial dimensions of the data passed to `fit`. If a single region is present the `region` dimension is squeezed out. `attrs["cmap"]` is set to `"coolwarm"`, `attrs["norm"]` to `Normalize(vmin=-1, vmax=1)`, and `attrs["long_name"]` to `"Pearson r"` so that plotting functions pick up sensible defaults automatically. Examples: Mask-based usage: two seed regions from a flat integer label map. ```pycon >>> import numpy as np >>> import xarray as xr >>> from confusius.connectivity import SeedBasedMaps >>> from confusius.xarray import create_voxeldata >>> >>> rng = np.random.default_rng(0) >>> data = create_voxeldata( ... rng.standard_normal((200, 1, 10, 20)), ... dims=("time", "k", "j", "i"), ... dt=0.1, ... spacing=(1.0, 1.0, 1.0), ... ) >>> >>> labels = create_voxeldata( ... np.zeros((1, 10, 20), dtype=int), ... dims=("k", "j", "i"), ... spacing=(1.0, 1.0, 1.0), ... ) >>> labels[:, :3, :] = 1 # Region 1: first 3 j-slices. >>> labels[:, 3:6, :] = 2 # Region 2: next 3 j-slices. >>> >>> mapper = SeedBasedMaps(seed_masks=labels) >>> mapper.fit(data) SeedBasedMaps(seed_masks=...) >>> mapper.maps_.dims ('region', 'k', 'j', 'i') >>> mapper.maps_.coords["region"].values array([1, 2]) >>> >>> # Single seed from a boolean mask converted to integer. >>> mask = create_voxeldata( ... np.zeros((1, 10, 20), dtype=bool), ... dims=("k", "j", "i"), ... spacing=(1.0, 1.0, 1.0), ... ) >>> mask[:, :3, :] = True >>> mapper_single = SeedBasedMaps(seed_masks=mask.astype(int)) >>> mapper_single.fit(data) SeedBasedMaps(seed_masks=...) >>> mapper_single.maps_.dims # region dim is squeezed for a single seed ('k', 'j', 'i') ``` Signal-based usage: provide seed signals directly. ```pycon >>> seed_signal = xr.DataArray( ... rng.standard_normal(200), ... dims=["time"], ... coords={"time": np.arange(200) * 0.1}, ... ) >>> mapper_sig = SeedBasedMaps(seed_signals=seed_signal) >>> mapper_sig.fit(data) SeedBasedMaps(seed_signals=...) >>> mapper_sig.maps_.dims # single signal, region dim squeezed ('k', 'j', 'i') ``` Methods: - **`fit`** – Compute the seed-based correlation maps. - **`get_metadata_routing`** – Get metadata routing of this object. - **`get_params`** – Get parameters for this estimator. - **`set_params`** – Set the parameters of this estimator. #### fit ```python fit(X: DataArray, y: None = None) -> SeedBasedMaps ``` Compute the seed-based correlation maps. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array to estimate seed-based maps from. Must have a `time` dimension. The spatial dimensions must be compatible with `seed_masks` when using mask-based seeding. > **Chunking along time** > > The `time` dimension must NOT be chunked when `clean_kwargs` includes detrending or filtering steps. Rechunk first: `X.chunk({'time': -1})`. - ##### **`y`** (`None`, default: `None` ) – Ignored. Present for sklearn API compatibility. Returns: - `SeedBasedMaps` – Fitted estimator. Raises: - `ValueError` – If neither or both of `seed_masks` and `seed_signals` are provided, if `X` has no `time` dimension, fewer than 2 timepoints, or if the `time` dimension is chunked when required. When `seed_signals` is provided: also raised if it has unexpected dimensions, a `time` size that differs from `X`, or `time` coordinates that do not match `X`. - `TypeError` – If `seed_masks` is not an integer-dtype DataArray. #### get\_metadata\_routing ```python get_metadata_routing() ``` Get metadata routing of this object. Please check :ref:`User Guide ` on how the routing mechanism works. Returns: - **`routing`** ( `MetadataRequest` ) – A :class:`~sklearn.utils.metadata_routing.MetadataRequest` encapsulating routing information. #### get\_params ```python get_params(deep=True) ``` Get parameters for this estimator. Parameters: - ##### **`deep`** (`bool`, default: `True` ) – If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns: - **`params`** ( `dict` ) – Parameter names mapped to their values. #### set\_params ```python set_params(**params) ``` Set the parameters of this estimator. The method works on simple estimators as well as on nested objects (such as :class:`~sklearn.pipeline.Pipeline`). The latter have parameters of the form `__` so that it's possible to update each component of a nested object. Parameters: - ##### **`**params`** (`dict`, default: `{}` ) – Estimator parameters. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. ### covariance\_to\_correlation ```python covariance_to_correlation(covariance: NDArray) -> NDArray ``` Return the correlation matrix for a given covariance matrix. Parameters: - #### **`covariance`** (`(ndarray, shape(n_features, n_features))`) – Input covariance matrix. Returns: - `(ndarray, shape(n_features, n_features))` – Correlation matrix. Diagonal elements are exactly `1`. > [!NOTE] > > **Notes** > > Adapted from [`nilearn.connectome.connectivity_matrices`]() (BSD-3-Clause License; see `NOTICE` for attribution). ### precision\_to\_partial\_correlation ```python precision_to_partial_correlation( precision: NDArray, ) -> NDArray ``` Return the partial correlation matrix for a given precision matrix. Parameters: - #### **`precision`** (`(ndarray, shape(n_features, n_features))`) – Input precision matrix (inverse of the covariance matrix). Returns: - `(ndarray, shape(n_features, n_features))` – Partial correlation matrix. Diagonal elements are exactly `1`. > [!NOTE] > > **Notes** > > Adapted from [`nilearn.connectome.connectivity_matrices`]() (BSD-3-Clause License; see `NOTICE` for attribution). ### symmetric\_matrix\_to\_vector ```python symmetric_matrix_to_vector( symmetric: NDArray, discard_diagonal: bool = False ) -> NDArray ``` Return the flattened lower triangular part of a symmetric matrix. Diagonal elements are divided by `sqrt(2)` when `discard_diagonal` is `False` so that the Frobenius norm is preserved under the vectorization. Acts on the last two dimensions if the input is not 2D. Parameters: - #### **`symmetric`** (`(..., n_features, n_features) numpy.ndarray`) – Input symmetric matrix or batch of symmetric matrices. - #### **`discard_diagonal`** (`bool`, default: `False` ) – Whether diagonal elements should be omitted from the output. Returns: - `ndarray` – Flattened lower triangular part. Shape is `(..., n_features * (n_features + 1) / 2)` when `discard_diagonal` is `False` and `(..., (n_features - 1) * n_features / 2)` otherwise. > [!NOTE] > > **Notes** > > Adapted from [`nilearn.connectome.connectivity_matrices`]() (BSD-3-Clause License; see `NOTICE` for attribution). ### vector\_to\_symmetric\_matrix ```python vector_to_symmetric_matrix( vec: NDArray, diagonal: NDArray | None = None ) -> NDArray ``` Return the symmetric matrix given its flattened lower triangular part. This is the inverse of `symmetric_matrix_to_vector`. Diagonal elements are multiplied by `sqrt(2)` to invert the norm-preserving scaling applied during vectorization. Acts on the last dimension of the input if it is not 1D. Parameters: - #### **`vec`** (`(..., n * (n + 1) / 2) numpy.ndarray or (..., (n - 1) * n / 2) numpy.ndarray`) – Vectorized lower triangular part. The diagonal may be included in `vec` or supplied separately via `diagonal`. - #### **`diagonal`** (`(ndarray, shape(..., n))`, default: `None` ) – Diagonal values to insert. When provided, `vec` is assumed to contain only the off-diagonal elements and `diagonal` supplies the main diagonal. Returns: - `(ndarray, shape(..., n, n))` – Reconstructed symmetric matrix. Raises: - `ValueError` – If `vec` has a length that does not correspond to a valid triangular number, or if `diagonal` has an incompatible shape. > [!NOTE] > > **Notes** > > Adapted from [`nilearn.connectome.connectivity_matrices`]() (BSD-3-Clause License; see `NOTICE` for attribution). # `confusius.datasets` ## datasets Datasets for confusius examples and tutorials. Functions: - **`fetch_brainglobe_atlas`** – Fetch a BrainGlobe brain atlas by name and return it as an atlas Dataset. - **`fetch_cybis_pereira_2026`** – Fetch the Cybis Pereira 2026 fUSI-BIDS dataset. - **`fetch_khallaf_2026`** – Fetch the Khallaf et al. (2026) naked mole-rat fUSI dataset. - **`fetch_landemard_2026`** – Fetch the Landemard 2026 fUSI-BIDS dataset. - **`fetch_nunez_elizalde_2022`** – Fetch the Nunez-Elizalde 2022 fUSI-BIDS dataset. - **`fetch_pepe_mariani_2026`** – Fetch the Pepe Mariani 2026 fUSI-BIDS dataset. - **`fetch_pereira_2025`** – Fetch the Pereira 2025 fUSI-BIDS dataset. - **`fetch_template_brunner_grillet_2021`** – Fetch the Brunner, Grillet et al. (2021) mouse vascular fUSI template. - **`fetch_template_pepe_mariani_2026`** – Fetch the Pepe, Mariani et al. (2026) mouse fUSI template. - **`get_datasets_dir`** – Return the confusius data directory. - **`list_datasets`** – Print a table of available datasets. ### fetch\_brainglobe\_atlas ```python fetch_brainglobe_atlas( atlas_name: AtlasName, *, data_dir: str | Path | None = None, check_latest: bool = False, ) -> Dataset ``` Fetch a BrainGlobe brain atlas by name and return it as an atlas Dataset. Downloads the named atlas via the [BrainGlobe Atlas API]() on first call, caching it in BrainGlobe's own atlas cache (shared with other BrainGlobe tools), then builds a self-describing atlas `xarray.Dataset`. The first fetch of an atlas also downloads every available region surface mesh, in one batched call rather than BrainGlobe's one-at-a-time lazy download, so `get_meshes` works offline afterwards for those regions. Regions with no upstream mesh are recorded in `.confusius-unavailable-meshes.json` in the versioned BrainGlobe mesh directory; later fetches skip them silently without network access. Delete this record to recheck availability if upstream meshes change without a version bump. If the BrainGlobe bucket is unreachable, uncached meshes fall back to that lazy download on first use. Parameters: - #### **`atlas_name`** (`AtlasName`) – BrainGlobe atlas name, e.g. `"allen_mouse_25um"`. See the [BrainGlobe atlas list](). - #### **`data_dir`** (`str or Path`, default: `None` ) – Directory in which BrainGlobe caches the atlas. If not provided, BrainGlobe uses its own default cache (`~/.brainglobe`), shared with other BrainGlobe tools. - #### **`check_latest`** (`bool`, default: `False` ) – Whether to check online for a newer atlas version. Left off by default so cached atlases load without a network round-trip. Returns: - `Dataset` – Atlas Dataset with data variables `template`, `annotation`, and `hemispheres` on a common voxel-to-world `(k, j, i)` grid with world `z`/`y`/`x` coordinates in millimetres, and the `.atlas` accessor for structure queries, masks, and meshes. Examples: ```pycon >>> atlas = fetch_brainglobe_atlas("allen_mouse_100um") >>> masks = atlas.atlas.get_masks("VISp") ``` ### fetch\_cybis\_pereira\_2026 ```python fetch_cybis_pereira_2026( data_dir: str | Path | None = None, datasets: str | list[str] | None = None, subjects: str | list[str] | None = None, sessions: str | list[str] | None = None, acqs: str | list[str] | None = None, datatypes: str | list[str] | None = None, refresh: bool = False, progress_callback: Callable[[int, int, str], None] | None = None, print_citation: bool = True, ) -> Path ``` Fetch the Cybis Pereira 2026 fUSI-BIDS dataset. Downloads functional ultrasound imaging data from freely-moving rats investigating vascular coding of speed in the spatial navigation system, converted to fUSI-BIDS format from Cybis Pereira et al. (2026). Files are downloaded on first call and cached locally. Subsequent calls with the same `data_dir` return immediately for already-cached files. Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Directory in which to cache the dataset. Defaults to the platform cache directory (e.g. `~/.cache/confusius` on Linux, `~/Library/Caches/confusius` on macOS, `%LOCALAPPDATA%\confusius\Cache` on Windows), overridable via the `CONFUSIUS_DATA` environment variable. - #### **`datasets`** (`str or list[str]`, default: `None` ) – Datasets to download. Use `"rawdata"` for the raw fUSI data and derivative names for processed outputs: `"glm-speed"`, `"glm-angular-speed"`, `"decode-speed"`, `"interanimal-decode-speed"`, `"dlc-videos"`. Accepts a single string or a list. If not provided, all datasets are downloaded. - #### **`subjects`** (`str or list[str]`, default: `None` ) – Subject IDs to download (without "sub-" prefix), e.g. `"rat73"` or `["rat75", "rat73"]`. If not provided, all subjects are downloaded. - #### **`sessions`** (`str or list[str]`, default: `None` ) – Session IDs to download (without "ses-" prefix), e.g. `"20220523"` or `["20220523", "20220524"]`. If not provided, all sessions are downloaded. Files with no session entity (e.g. subject-level derivatives) are always included. - #### **`acqs`** (`str or list[str]`, default: `None` ) – Acquisition labels to download (without "acq-" prefix), e.g. `"slice32"` or `["slice32", "slice42"]`. If not provided, all acquisitions are downloaded. Files with no acquisition entity are always included. - #### **`datatypes`** (`str or list[str]`, default: `None` ) – BIDS datatype directories to download, e.g. `"fusi"`, `"susi"`, `["fusi", "susi"]`. Valid values are `"fusi"`, `"susi"` and `"motion"`. If not provided, all datatypes are downloaded. Files that do not sit under a datatype directory (e.g. session-level `scans.tsv`, subject-level derivative aggregates) are always included. - #### **`refresh`** (`bool`, default: `False` ) – Whether to resolve the latest published S3 release. Otherwise the cached release is reused offline. Releases are cached in separate version directories. - #### **`progress_callback`** (`Callable[[int, int, str], None]`, default: `None` ) – Callback receiving cumulative downloaded bytes, total bytes to download, and a user-facing description. Intended for GUI progress bars. - #### **`print_citation`** (`bool`, default: `True` ) – Whether to print the citation for the dataset. Returns: - `Path` – Path to the BIDS root directory of the cached dataset. Raises: - `ValueError` – If an unknown dataset name is passed in `datasets`, or an unknown datatype is passed in `datatypes`. **References** 1. Cybis Pereira, F. et al. (2026). A vascular code for speed in the spatial navigation system. *Cell Reports*, 45(1). [https://doi.org/10.1016/j.celrep.2025.116791]() 2. [ConfUSIus dataset collection](). 3. Dataset license (CC BY 4.0): [https://creativecommons.org/licenses/by/4.0/]() ### fetch\_khallaf\_2026 ```python fetch_khallaf_2026( data_dir: str | Path | None = None, datasets: str | list[str] | None = None, subjects: str | int | list[int] | list[str] | None = None, sessions: str | list[str] | None = None, runs: str | int | list[int] | list[str] | None = None, reconstruction: str = "both", sourcedata: bool = False, refresh: bool = False, print_citation: bool = True, ) -> Path ``` Fetch the Khallaf et al. (2026) naked mole-rat fUSI dataset. Downloads functional ultrasound imaging data from naked mole-rats exposed to olfactory stimulation, organised following BIDS and the proposed fUSI extension BEP-040. Data is distributed through the ConfUSIus dataset collection; selected files are downloaded individually and verified against SHA-256 hashes. Files are downloaded on first call and cached locally. Subsequent calls with the same `data_dir` return immediately for already-cached files. Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Directory in which to cache the dataset. Defaults to the platform cache directory (e.g. `~/.cache/confusius` on Linux, `~/Library/Caches/confusius` on macOS, `%LOCALAPPDATA%\confusius\Cache` on Windows), overridable via the `CONFUSIUS_DATA` environment variable. - #### **`datasets`** (`str or list[str]`, default: `None` ) – Dataset buckets to download: `"rawdata"` for the raw fUSI data, `"glm"` and `"bootstrapping"` for the processed derivatives. Accepts a single string or a list. If not provided, all of these are downloaded. Does not control `"sourcedata"` (see the `"sourcedata"` parameter). - #### **`subjects`** (`str or int or list[str | int]`, default: `None` ) – Subject IDs to download (without `sub-` prefix), e.g. `"5622"` or `["5622", "6036"]`. If not provided, all subjects are downloaded. Integer inputs are coerced to strings. Files with no subject entity are always included. - #### **`sessions`** (`str or list[str]`, default: `None` ) – Session IDs to download (without `ses-` prefix), e.g. `"IPM"` or `["Air", "Etoh"]`. If not provided, all sessions are downloaded. Files with no session entity are always included. - #### **`runs`** (`str or list[str]`, default: `None` ) – Run indices to download (without `run-` prefix), e.g. `"1"` or `["1", "2"]`. If not provided, all runs are downloaded. Integer inputs are coerced to strings. Files with no run entity are always included. - #### **`reconstruction`** (`str`, default: `"both"` ) – Which reconstruction of the rawdata `fusi/` volumes to download: `"raw"` for the unregistered volumes (no `rec-` entity), `"resampled"` for the registered `rec-resampled` volumes, or `"both"`. Only affects rawdata; derivatives are unaffected. - #### **`sourcedata`** (`bool`, default: `False` ) – Whether to download the raw Iconeus acquisition files under `sourcedata/`. When `True`, the entire `sourcedata/` directory (\~7.8 GB) is downloaded with no subject/session filtering applied. - #### **`refresh`** (`bool`, default: `False` ) – Whether to resolve the latest published S3 release. Otherwise the cached release is reused offline. Releases are cached in separate version directories. - #### **`print_citation`** (`bool`, default: `True` ) – Whether to print the citation for the dataset. Returns: - `Path` – Path to the BIDS root directory of the cached dataset. Raises: - `ValueError` – If an unknown dataset name is passed in `datasets`, or an unknown value is passed in `reconstruction`. **References** 1. Khallaf, M. A., Hart, D. W., Luo, W., Murad, F., Cybis Pereira, F., Mendez-Aranda, D., Hagenah, N., Rossi, A., Bégay, V., Okrouhlík, J., Krautwurst, D., Ngalameno, M. K., Ganswindt, A., Barker, A. J., Šumbera, R., Knaden, M., Pezet, S., Woehler, A., Hansson, B. S., Bennett, N. C., & Lewin, G. R. (2026). A queen odour mediates reproductive suppression in a eusocial mammal. *Nature*. [https://doi.org/10.1038/s41586-026-10772-5]() 2. fUSI dataset on Edmond (Max Planck Society): [https://doi.org/10.17617/3.7QCU1F]() 3. Dataset license (CC0 1.0): [https://creativecommons.org/publicdomain/zero/1.0/]() ### fetch\_landemard\_2026 ```python fetch_landemard_2026( data_dir: str | Path | None = None, datasets: str | list[str] | None = None, subjects: str | list[str] | None = None, acqs: str | list[str] | None = None, datatypes: str | list[str] | None = None, refresh: bool = False, print_citation: bool = True, ) -> Path ``` Fetch the Landemard 2026 fUSI-BIDS dataset. Downloads functional ultrasound imaging recordings from awake, head-fixed, freely-running mice, re-exported to fUSI-BIDS format from Landemard et al. (2026). Files are downloaded on first call and cached locally. Subsequent calls with the same `data_dir` return immediately for already-cached files. Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Directory in which to cache the dataset. Defaults to the platform cache directory (e.g. `~/.cache/confusius` on Linux, `~/Library/Caches/confusius` on macOS, `%LOCALAPPDATA%\confusius\Cache` on Windows), overridable via the `CONFUSIUS_DATA` environment variable. - #### **`datasets`** (`str or list[str]`, default: `None` ) – Datasets to download. Use `"rawdata"` for the raw fUSI/angiography data and derivative names for processed outputs: `"atlas_mapping"`, `"processed_data"`. Accepts a single string or a list. If not provided, all datasets are downloaded. - #### **`subjects`** (`str or list[str]`, default: `None` ) – Subject IDs to download (without "sub-" prefix), e.g. `"ALD001"` or `["ALD001", "ALD019"]`. If not provided, all subjects are downloaded. - #### **`acqs`** (`str or list[str]`, default: `None` ) – Acquisition labels to download (without "acq-" prefix), e.g. `"ref04"` or `["ref04", "ref11"]`. If not provided, all acquisitions are downloaded. Files with no `acq-` entity (e.g. `sub-ALD001_scans.tsv`, `sub-ALD001/susi/sub-ALD001_pwd.nii.gz`) are always included. The `run-` entity is not exposed as a filter. - #### **`datatypes`** (`str or list[str]`, default: `None` ) – BIDS datatype directories to download, e.g. `"fusi"`, `"susi"`, `["fusi", "susi"]`. Valid values are `"fusi"` and `"susi"`. If not provided, all datatypes are downloaded. Files that do not sit under a datatype directory (e.g. subject-level `scans.tsv`) are always included. - #### **`refresh`** (`bool`, default: `False` ) – Whether to resolve the latest published S3 release. Otherwise the cached release is reused offline. Releases are cached in separate version directories. - #### **`print_citation`** (`bool`, default: `True` ) – Whether to print the citation for the dataset. Returns: - `Path` – Path to the BIDS root directory of the cached dataset. Raises: - `ValueError` – If an unknown dataset name is passed in `datasets`, or an unknown datatype is passed in `datatypes`. **References** 1. Landemard, A., Krumin, M., Harris, K. D., & Carandini, M. (2026). Brainwide blood volume reflects opposing neural populations. *Nature*. [https://doi.org/10.1038/s41586-026-10350-9]() 2. [ConfUSIus dataset collection](). 3. Dataset license (CC BY-NC 4.0): [https://creativecommons.org/licenses/by-nc/4.0/]() ### fetch\_nunez\_elizalde\_2022 ```python fetch_nunez_elizalde_2022( data_dir: str | Path | None = None, datasets: str | list[str] | None = None, subjects: str | list[str] | None = None, sessions: str | list[str] | None = None, tasks: str | list[str] | None = None, acqs: str | list[str] | None = None, datatypes: str | list[str] | None = None, refresh: bool = False, progress_callback: Callable[[int, int, str], None] | None = None, print_citation: bool = True, ) -> Path ``` Fetch the Nunez-Elizalde 2022 fUSI-BIDS dataset. Downloads simultaneous neural activity and cerebral blood volume recordings in awake mice, converted to fUSI-BIDS format from Nunez-Elizalde et al. (2022). Files are downloaded on first call and cached locally. Subsequent calls with the same `data_dir` return immediately for already-cached files. Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Directory in which to cache the dataset. Defaults to the platform cache directory (e.g. `~/.cache/confusius` on Linux, `~/Library/Caches/confusius` on macOS, `%LOCALAPPDATA%\confusius\Cache` on Windows), overridable via the `CONFUSIUS_DATA` environment variable. - #### **`datasets`** (`str or list[str]`, default: `None` ) – Datasets to download. Use `"rawdata"` for the raw subject data and `"allenccf_align"` for the atlas-alignment derivatives. Accepts a single string or a list. If not provided, all datasets are downloaded. - #### **`subjects`** (`str or list[str]`, default: `None` ) – Subject IDs to download (without "sub-" prefix), e.g. "CR020" or `["CR020"]`. Accepts a single string or a list. If not provided, all subjects are downloaded. - #### **`sessions`** (`str or list[str]`, default: `None` ) – Session IDs to download (without "ses-" prefix), e.g. "20191122" or `["20191122"]`. Accepts a single string or a list. If not provided, all sessions are downloaded. Files with no session entity are always included. - #### **`tasks`** (`str or list[str]`, default: `None` ) – Task names to download, e.g. "kalatsky" or `["kalatsky", "spontaneous"]`. Accepts a single string or a list. If not provided, all tasks are downloaded. Only applies to `fusi/` files. - #### **`acqs`** (`str or list[str]`, default: `None` ) – Acquisition labels to download (without `acq-`), e.g. "slice03" or `["slice03"]`. Accepts a single string or a list. If not provided, all acquisitions are downloaded. Only applies to `fusi/` files. - #### **`datatypes`** (`str or list[str]`, default: `None` ) – BIDS datatype directories to download, e.g. `"fusi"`, `"susi"`, `["fusi", "susi"]`. Valid values are `"fusi"` and `"susi"`. If not provided, all datatypes are downloaded. Files that do not sit under a datatype directory (e.g. subject-level metadata) are always included. - #### **`refresh`** (`bool`, default: `False` ) – Whether to resolve the latest published S3 release. Otherwise the cached release is reused offline. Releases are cached in separate version directories. - #### **`progress_callback`** (`Callable[[int, int, str], None]`, default: `None` ) – Callback receiving cumulative downloaded bytes, total bytes to download, and a user-facing description. Intended for GUI progress bars. - #### **`print_citation`** (`bool`, default: `True` ) – Whether to print the citation for the dataset. Returns: - `Path` – Path to the BIDS root directory of the cached dataset. Raises: - `ValueError` – If an unknown dataset name is passed in `datasets`, or an unknown datatype is passed in `datatypes`. **References** 1. Nunez-Elizalde, A.O. et al. (2022). Neural correlates of blood flow measured by ultrasound. *Neuron*, 110(10), 1631–1640. [https://doi.org/10.1016/j.neuron.2022.02.012]() 2. [ConfUSIus dataset collection](). 3. Dataset license (CC BY 4.0): [https://creativecommons.org/licenses/by/4.0/]() ### fetch\_pepe\_mariani\_2026 ```python fetch_pepe_mariani_2026( data_dir: str | Path | None = None, datasets: str | list[str] | None = None, subjects: str | list[str] | None = None, sessions: str | list[str] | None = None, acqs: str | list[str] | None = None, datatypes: str | list[str] | None = None, refresh: bool = False, print_citation: bool = True, ) -> Path ``` Fetch the Pepe Mariani 2026 fUSI-BIDS dataset. Downloads transcranial mouse resting-state fUSI recordings and derivatives, re-exported to fUSI-BIDS format from Pepe et al. (2026). Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Directory in which to cache the dataset. Defaults to the platform cache directory, overridable via the `CONFUSIUS_DATA` environment variable. - #### **`datasets`** (`str or list[str]`, default: `None` ) – Datasets to download. Use `"rawdata"` for raw fUSI/angiography data and derivative names for processed outputs: `"registered"`, `"preprocessed"`, `"Params"`. Accepts a single string or a list. If not provided, all datasets are downloaded. - #### **`subjects`** (`str or list[str]`, default: `None` ) – Subject IDs to download (without "sub-" prefix). If not provided, all subjects are downloaded. - #### **`sessions`** (`str or list[str]`, default: `None` ) – Session IDs to download (without "ses-" prefix). If not provided, all sessions are downloaded. Files with no session entity are always included. - #### **`acqs`** (`str or list[str]`, default: `None` ) – Acquisition labels to download (without "acq-" prefix). If not provided, all acquisitions are downloaded. Files with no acquisition entity are always included. - #### **`datatypes`** (`str or list[str]`, default: `None` ) – BIDS datatype directories to download, e.g. `"fusi"` or `"susi"`. If not provided, all datatypes are downloaded. Files that do not sit under a datatype directory are always included. - #### **`refresh`** (`bool`, default: `False` ) – Whether to resolve the latest published S3 release. Otherwise the cached release is reused offline. Releases are cached in separate version directories. - #### **`print_citation`** (`bool`, default: `True` ) – Whether to print the citation for the dataset. Returns: - `Path` – Path to the BIDS root directory of the cached dataset. Raises: - `ValueError` – If an unknown dataset name is passed in `datasets`, or an unknown datatype is passed in `datatypes`. **References** 1. Pepe, C. et al. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (Fusi). [https://doi.org/10.64898/2026.02.05.704055]() 2. [ConfUSIus dataset collection](). 3. Dataset license (CC BY 4.0): [https://creativecommons.org/licenses/by/4.0/]() ### fetch\_pereira\_2025 ```python fetch_pereira_2025( data_dir: str | Path | None = None, subjects: str | list[str] | None = None, sessions: str | list[str] | None = None, tasks: str | list[str] | None = None, refresh: bool = False, print_citation: bool = True, ) -> Path ``` Fetch the Pereira 2025 fUSI-BIDS dataset. Downloads fUSI recordings from a rat model of neuroinflammation, re-exported to fUSI-BIDS format from Pereira et al. (2025). Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Directory in which to cache the dataset. Defaults to the platform cache directory, overridable via the `CONFUSIUS_DATA` environment variable. - #### **`subjects`** (`str or list[str]`, default: `None` ) – Subject IDs to download (without "sub-" prefix), e.g. `"r11582"`. If not provided, all subjects are downloaded. - #### **`sessions`** (`str or list[str]`, default: `None` ) – Session IDs to download (without "ses-" prefix), e.g. `"awakebaseline"`. If not provided, all sessions are downloaded. Files with no session entity are always included. - #### **`tasks`** (`str or list[str]`, default: `None` ) – Task labels to download (without "task-" prefix), e.g. `"rest"` or `"stim"`. If not provided, all tasks are downloaded. Files with no task entity are always included. - #### **`refresh`** (`bool`, default: `False` ) – Whether to resolve the latest published S3 release. Otherwise the cached release is reused offline. Releases are cached in separate version directories. - #### **`print_citation`** (`bool`, default: `True` ) – Whether to print the citation for the dataset. Returns: - `Path` – Path to the BIDS root directory of the cached dataset. **References** 1. Pereira, M. et al. (2025). Induction of haemodynamic travelling waves by glial-related vasomotion in a rat model of neuroinflammation: Implications for functional neuroimaging. *eBioMedicine*, 116, 105777. [https://doi.org/10.1016/j.ebiom.2025.105777]() 2. [ConfUSIus dataset collection](). 3. Dataset license (CC BY 4.0): [https://creativecommons.org/licenses/by/4.0/]() ### fetch\_template\_brunner\_grillet\_2021 ```python fetch_template_brunner_grillet_2021( data_dir: str | Path | None = None, refresh: bool = False, print_citation: bool = True, ) -> DataArray ``` Fetch the Brunner, Grillet et al. (2021) mouse vascular fUSI template. Downloads the template from the ConfUSIus dataset collection, caches it locally, and returns the loaded NIfTI as a VoxelData array. Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Directory in which to cache the template. Defaults to the platform cache directory (e.g. `~/.cache/confusius` on Linux, `~/Library/Caches/confusius` on macOS, `%LOCALAPPDATA%\confusius\Cache` on Windows), overridable via the `CONFUSIUS_DATA` environment variable. - #### **`refresh`** (`bool`, default: `False` ) – Whether to resolve the latest published release. Otherwise the cached release is reused offline. Releases are cached in separate version directories. - #### **`print_citation`** (`bool`, default: `True` ) – Whether to print the citation for the template. Returns: - `DataArray` – Vascular template aligned to Allen CCF space. **References** 1. Brunner, C., Grillet, M. et al. (2021). Whole-brain functional ultrasound imaging in awake head-fixed mice. Nature Protocols, 16, 3547–3571. [https://doi.org/10.1038/s41596-021-00548-8]() 2. Macé, E. and Montaldo, G. (2020). Whole-brain visually evoked activity maps in head-fixed awake mice (June 2021 metadata update). Zenodo. [https://doi.org/10.5281/zenodo.4905862]() 3. [ConfUSIus dataset collection](). 4. Template license (CC BY 4.0): [https://creativecommons.org/licenses/by/4.0/]() ### fetch\_template\_pepe\_mariani\_2026 ```python fetch_template_pepe_mariani_2026( data_dir: str | Path | None = None, refresh: bool = False, print_citation: bool = True, ) -> DataArray ``` Fetch the Pepe, Mariani et al. (2026) mouse fUSI template. Downloads the template from the ConfUSIus dataset collection, caches it locally, and returns the loaded NIfTI as a VoxelData array. Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Directory in which to cache the template. Defaults to the platform cache directory (e.g. `~/.cache/confusius` on Linux, `~/Library/Caches/confusius` on macOS, `%LOCALAPPDATA%\confusius\Cache` on Windows), overridable via the `CONFUSIUS_DATA` environment variable. - #### **`refresh`** (`bool`, default: `False` ) – Whether to resolve the latest published release. Otherwise the cached release is reused offline. Releases are cached in separate version directories. - #### **`print_citation`** (`bool`, default: `True` ) – Whether to print the citation for the template. Returns: - `DataArray` – Native-resolution template in scanner space, with `world_to_sform` mapping its world coordinates into Allen Mouse Brain atlas space. **References** 1. Pepe, C. et al. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (fUSI). [https://doi.org/10.64898/2026.02.05.704055]() 2. [ConfUSIus dataset collection](). 3. Template license (CC BY 4.0): [https://creativecommons.org/licenses/by/4.0/]() ### get\_datasets\_dir ```python get_datasets_dir( data_dir: str | Path | None = None, ) -> Path ``` Return the confusius data directory. Priority order: 1. The `data_dir` argument. 2. The `CONFUSIUS_DATA` environment variable. 3. The platform cache directory (e.g. `~/.cache/confusius` on Linux). Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Custom data directory. If not provided, falls back to the environment variable or the platform cache. Returns: - `Path` – Resolved data directory (created if it does not exist). ### list\_datasets ```python list_datasets(data_dir: str | Path | None = None) -> None ``` Print a table of available datasets. Parameters: - #### **`data_dir`** (`str or Path`, default: `None` ) – Cache directory to check for existing datasets. Defaults to the directory resolved by `get_datasets_dir`. # `confusius.decoding` ## decoding Pattern decoding for VoxelData arrays. This package provides decoders in the spirit of [`nilearn.decoding`](): - `SearchLight`: per-voxel local decoding, also known as "spotlight" decoding. Portions of this package are inspired by `nilearn.decoding`, which is licensed under the BSD-3-Clause License. See `NOTICE` for details. Modules: - **`searchlight`** – Searchlight decoding for VoxelData arrays. Classes: - **`SearchLight`** – Searchlight decoder for fUSI data. ### SearchLight Bases: `BaseEstimator` Searchlight decoder for fUSI data. For every voxel of `process_mask`, `fit` gathers the `mask` voxels lying within `radius`, cross-validates `estimator` on that neighborhood, and stores the mean score. This estimator wraps scikit-learn while keeping xarray metadata: - Input data must be a VoxelData array with dims `(time, k, j, i)`—no `pose` or extra dimensions. - The `time` dimension is the sample axis. It need not be temporally ordered. For trial-averaged data, rename the trial dimension with `.rename(trial="time")`. - `scores_` is returned in the spatial geometry of `process_mask`. Parameters: - #### **`estimator`** (`BaseEstimator`) – Estimator or `Pipeline`. - #### **`mask`** (`DataArray`, default: `None` ) – Boolean spatial mask selecting the voxels that may act as *features*. If not provided, every voxel of the input data is used as a feature voxel. Must be a VoxelData array on `X`'s grid, because `radius` is measured in world coordinates. - #### **`radius`** (`float`, default: `1.0` ) – Neighborhood radius, in the units of the data's spatial coordinates. Check `X.coords[dim].attrs.get("units")` if unsure. Radii are measured in world coordinates rather than voxel indices, so anisotropic voxels behave correctly. - #### **`process_mask`** (`DataArray`, default: `None` ) – Boolean mask selecting the voxels that act as neighborhood *centers*. Must be a subset of `mask`, and a VoxelData array on `X`'s grid. If not provided, a score is computed at every `mask` voxel. Use it to restrict the searchlight to a region of interest while still drawing features from the surrounding tissue. - #### **`cv`** (`int or BaseCrossValidator`, default: `5` ) – Cross-validation strategy. An integer builds a `KFold` for regressors, whose folds are contiguous blocks of time, or a `StratifiedKFold` for classifiers, which keeps folds class-balanced but interleaves time. Both use `shuffle=False`. Any scikit-learn splitter is accepted. - #### **`scoring`** (`str or callable`, default: `None` ) – Scorer passed to `cross_val_score`. If not provided, the estimator's own `score` is used, which is accuracy for classifiers and the coefficient of determination for regressors. - #### **`n_jobs`** (`int`, default: `1` ) – Number of joblib workers. Centers are dispatched in batches, not one task each. - #### **`show_progress`** (`bool`, default: `True` ) – Whether to display a progress bar during fit. Attributes: - **`scores_`** (`(k, j, i) xarray.DataArray`) – Mean cross-validation score at each `process_mask` center, in the spatial geometry of `process_mask`. Voxels outside `process_mask` are `numpy.nan`. The input's attributes are carried over, including any `affines`, so the map stays in the same world space. `long_name` is set to `"Searchlight CV score"` and `units` to the metric implied by `scoring`: the scorer string when one is given, otherwise `"accuracy"` for a classifier or `"R²"` for a regressor. A callable scorer leaves `units` unset. Warns: - `UserWarning` – If `radius` is small enough that the median neighborhood holds a single voxel. The run still produces a valid map, but it has silently become a univariate analysis rather than a multivariate one. Also if `process_mask` selects no voxels at all, which yields an entirely NaN map. > [!NOTE] > > **Notes** > > Consecutive fUSI volumes are strongly autocorrelated. Passing a splitter with `shuffle=True` places near-duplicate neighboring volumes in both the training and test sets, which inflates scores. This is why an integer `cv` builds `KFold` folds with `shuffle=False` for regressors. Classifiers instead get `StratifiedKFold`, which balances classes across folds but interleaves time rather than keeping contiguous blocks, so on its own it does not prevent this leakage. For data with a run or block structure, or to keep classification folds contiguous, prefer `LeaveOneGroupOut` with `groups`. **References** 1. Kriegeskorte, N., Goebel, R., and Bandettini, P. (2006). "Information-based functional brain mapping". PNAS, 103(10), 3863-3868. Examples: ```pycon >>> import numpy as np >>> from sklearn.linear_model import Ridge >>> from confusius.decoding import SearchLight >>> from confusius.xarray import create_voxeldata >>> >>> rng = np.random.default_rng(0) >>> data = create_voxeldata( ... rng.standard_normal((40, 1, 5, 5)), ... dims=("time", "k", "j", "i"), ... dt=0.5, ... spacing=(1.0, 0.2, 0.2), ... ) >>> speed = rng.standard_normal(40) >>> >>> searchlight = SearchLight( ... estimator=Ridge(), radius=0.25, cv=3, show_progress=False ... ) >>> searchlight.fit(data, speed).scores_.dims ('k', 'j', 'i') ``` Methods: - **`fit`** – Run searchlight decoding. - **`get_metadata_routing`** – Get metadata routing of this object. - **`get_params`** – Get parameters for this estimator. - **`score`** – Refuse to produce a single score. - **`set_params`** – Set the parameters of this estimator. #### fit ```python fit( X: DataArray, y: ArrayLike | DataArray, groups: ArrayLike | None = None, ) -> SearchLight ``` Run searchlight decoding. Parameters: - ##### **`X`** (`(time, k, j, i) xarray.DataArray`) – VoxelData array. The `time` dimension is the sample axis. - ##### **`y`** (`(n_samples,) array-like or xarray.DataArray`) – Targets aligned with `X`'s `time` axis. - ##### **`groups`** (`(n_samples,) array-like`, default: `None` ) – Group labels forwarded to the cross-validator. Required by `LeaveOneGroupOut`. Returns: - `SearchLight` – The fitted estimator. Raises: - `ValueError` – If `X` is h5py-backed or is not a VoxelData array with exactly dims `(time, k, j, i)`; if `radius` is negative; if `mask` or `process_mask` is not a VoxelData array; if `process_mask` is not a subset of `mask`; if `y` or `groups` do not align with `X`; or if the masked data contains non-finite values. Warns: - `UserWarning` – If the median neighborhood holds a single voxel, either because `radius` is below the voxel spacing or because `mask` is very sparse. The run still produces a valid map, but it has silently become a univariate analysis rather than a multivariate one. Also if `process_mask` selects no voxels at all, which yields an entirely NaN map. #### get\_metadata\_routing ```python get_metadata_routing() ``` Get metadata routing of this object. Please check :ref:`User Guide ` on how the routing mechanism works. Returns: - **`routing`** ( `MetadataRequest` ) – A :class:`~sklearn.utils.metadata_routing.MetadataRequest` encapsulating routing information. #### get\_params ```python get_params(deep=True) ``` Get parameters for this estimator. Parameters: - ##### **`deep`** (`bool`, default: `True` ) – If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns: - **`params`** ( `dict` ) – Parameter names mapped to their values. #### score ```python score(X: DataArray, y: ArrayLike) -> float ``` Refuse to produce a single score. Parameters: - ##### **`X`** (`DataArray`) – Ignored. SearchLight does not refit a single model. - ##### **`y`** (`array - like`) – Ignored. SearchLight does not refit a single model. Returns: - `float` – Never returns. Raises: - `NotImplementedError` – Always. A searchlight produces one score per voxel, not one per model, so there is no single number to report. Read `scores_` instead. #### set\_params ```python set_params(**params) ``` Set the parameters of this estimator. The method works on simple estimators as well as on nested objects (such as :class:`~sklearn.pipeline.Pipeline`). The latter have parameters of the form `__` so that it's possible to update each component of a nested object. Parameters: - ##### **`**params`** (`dict`, default: `{}` ) – Estimator parameters. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. # `confusius.decomposition` ## decomposition Decomposition techniques for fUSI data. Modules: - **`fastica`** – FastICA decomposition for VoxelData arrays. - **`nmf`** – Non-negative matrix factorization for VoxelData arrays. - **`pca`** – Principal component decomposition for VoxelData arrays. Classes: - **`FastICA`** – Fast independent component analysis (ICA) for VoxelData arrays. - **`NMF`** – Non-negative matrix factorization (NMF) for VoxelData arrays. - **`PCA`** – Principal component analysis (PCA) for VoxelData arrays. ### FastICA Bases: `_BaseFUSIDecomposer` Fast independent component analysis (ICA) for VoxelData arrays. The FastICA algorithm is based on Hyvarinen *et al.* (2000). This estimator wraps `sklearn.decomposition.FastICA` while keeping xarray metadata through `transform` and `inverse_transform`: - Input data are expected as `(time, ...)` where `...` are spatial dimensions. - `transform` returns a `(time, component)` DataArray of IC time courses. - `inverse_transform` reconstructs to `(time, ...)` using the fitted spatial coordinates. Two ICA modes are supported, selected via the `mode` parameter. **Spatial ICA** (`mode="spatial"`, default, matches FSL MELODIC): Maximises independence across the *spatial* dimension. The independent components are spatial maps; time courses are derived by projecting the data onto those maps. This is the appropriate choice when different brain regions are assumed to have independently fluctuating activity, which is the standard prior for network discovery. It is also better conditioned for fUSI data where the number of voxels greatly exceeds the number of time points. **Temporal ICA** (`mode="temporal"`): Maximises independence across the *time* dimension. The independent components are temporal signals; the spatial maps are the corresponding mixing weights. Suitable when temporal independence is the relevant prior. Parameters: - #### **`n_components`** (`int`, default: `None` ) – Number of components to use. If not provided, all are used. - #### **`mode`** (`(spatial, temporal)`, default: `"spatial"` ) – Whether to find spatially or temporally independent components: - `"spatial"`: fit on `(space, time)`. The independent components are spatial maps; the projected time courses are their temporal mixing weights. Matches FSL MELODIC's default for single-subject data. - `"temporal"`: fit on `(time, space)`. The independent components are time courses; the spatial maps are their voxel-wise mixing weights. - #### **`algorithm`** (`(parallel, deflation)`, default: `"parallel"` ) – Specify which algorithm to use for FastICA. - #### **`whiten`** (`(unit - variance, arbitrary - variance)`, default: `"unit-variance"` ) – Specify the whitening strategy to use. - If `"arbitrary-variance"`, a whitening with arbitrary variance is used. - If `"unit-variance"`, the whitening matrix is rescaled to ensure that each recovered source has unit variance. - If `False`, the data are already considered whitened, and no whitening is performed. - #### **`fun`** (`(logcosh, exp, cube)`, default: `"logcosh"` ) – The functional form of the `G` function used in the approximation to neg-entropy. Can be either `"logcosh"`, `"exp"`, or `"cube"`. Default is `"cube"` to align with FSL MELODIC (`pow3`). You can also provide your own function. It should return a tuple containing the value of the function and its derivative at the point. The derivative should be averaged along its last dimension. - #### **`fun_args`** (`dict[str, Any]`, default: `None` ) – Arguments to send to the functional form. If empty or `None` and `fun="logcosh"`, `fun_args` defaults to `{"alpha": 1.0}`. - #### **`max_iter`** (`int`, default: `500` ) – Maximum number of iterations during fit. Default matches FSL MELODIC (`--maxit=500`). - #### **`tol`** (`float`, default: `1e-4` ) – A positive scalar giving the tolerance at which the un-mixing matrix is considered to have converged. - #### **`w_init`** (`(n_components, n_components) numpy.ndarray`, default: `None` ) – Initial un-mixing array. If not provided, values drawn from a normal distribution are used. - #### **`whiten_solver`** (`(svd, eigh)`, default: `"svd"` ) – The solver to use for whitening. - `"svd"` is more stable numerically if the problem is degenerate, and often faster when `n_samples <= n_features`. - `"eigh"` is generally more memory efficient when `n_samples >= n_features`, and can be faster when `n_samples >= 50 * n_features`. - #### **`random_state`** (`int`, default: `None` ) – Used to initialize `w_init` when not provided, with a normal distribution. Pass an int for reproducible results across multiple function calls. - #### **`mask`** (`DataArray`, default: `None` ) – Boolean VoxelData mask selecting which voxels to include during fitting and projection. Must match the non-`time` dimensions of the input data in the same order. Attributes: - **`maps_`** (`(n_components, ...) xarray.DataArray`) – Spatial maps reshaped to the original spatial geometry. - In `"spatial"` mode: the independent component sources themselves, that is, the spatially independent patterns that FastICA found. These are the ICs in the strict sense. - In `"temporal"` mode: the unmixing directions in voxel space, that is, the spatial weights applied to each volume to extract IC time courses. The actual temporal ICs are returned by `transform`. - **`mean_`** (`(...) xarray.DataArray`) – Per-voxel empirical mean from the training set. - **`whitening_`** (`(n_components, ...) xarray.DataArray`) – Pre-whitening matrix reshaped to the original spatial geometry. Only present in `"temporal"` mode when `whiten` is not `False`. - **`n_components_`** (`int`) – Number of components estimated during fit. - **`n_iter_`** (`int`) – If `algorithm="deflation"`, the maximum number of iterations run across all components. Otherwise, the number of iterations taken to converge. - **`n_features_in_`** (`int`) – Number of features seen during fit. **References** 1. Hyvarinen, A., and Oja, E. (2000). "Independent component analysis: Algorithms and applications". Neural Networks, 13(4-5), 411-430. Examples: Spatial ICA (default, matches FSL MELODIC): ```pycon >>> import numpy as np >>> from confusius.decomposition import FastICA >>> from confusius.xarray import create_voxeldata >>> >>> rng = np.random.default_rng(0) >>> data = create_voxeldata( ... rng.standard_normal((200, 5, 10, 20)), ... dims=("time", "k", "j", "i"), ... dt=0.1, ... spacing=(1.0, 1.0, 1.0), ... ) >>> >>> ica = FastICA(n_components=5, random_state=0) >>> signals = ica.fit_transform(data) >>> signals.dims ('time', 'component') >>> reconstructed = ica.inverse_transform(signals) >>> reconstructed.dims ('time', 'k', 'j', 'i') ``` Methods: - **`fit`** – Fit FastICA on a VoxelData array. - **`fit_transform`** – Fit on `X` and return transformed signals. - **`get_metadata_routing`** – Get metadata routing of this object. - **`get_params`** – Get parameters for this estimator. - **`inverse_transform`** – Reconstruct data from component signals. - **`set_output`** – Set output container. - **`set_params`** – Set the parameters of this estimator. - **`transform`** – Project data into component space. #### fit ```python fit(X: DataArray, y: None = None) -> FastICA ``` Fit FastICA on a VoxelData array. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array. - ##### **`y`** (`None`, default: `None` ) – Ignored. Present for scikit-learn API compatibility. Returns: - `FastICA` – Fitted estimator. Raises: - `ValueError` – If input has no `time` dimension, fewer than 2 timepoints, or no spatial dimensions. - `ValueError` – If `mode` is not `"spatial"` or `"temporal"`. #### fit\_transform ```python fit_transform( X: DataArray, y: None = None, **fit_params: object ) -> DataArray ``` Fit on `X` and return transformed signals. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array. - ##### **`y`** (`None`, default: `None` ) – Ignored. Present for scikit-learn API compatibility. - ##### **`**fit_params`** (`object`, default: `{}` ) – Additional fit parameters. Unsupported for this estimator. Returns: - `(time, component) xarray.DataArray` – Decomposed signals in component space. Raises: - `TypeError` – If additional fit parameters are provided. #### get\_metadata\_routing ```python get_metadata_routing() ``` Get metadata routing of this object. Please check :ref:`User Guide ` on how the routing mechanism works. Returns: - **`routing`** ( `MetadataRequest` ) – A :class:`~sklearn.utils.metadata_routing.MetadataRequest` encapsulating routing information. #### get\_params ```python get_params(deep=True) ``` Get parameters for this estimator. Parameters: - ##### **`deep`** (`bool`, default: `True` ) – If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns: - **`params`** ( `dict` ) – Parameter names mapped to their values. #### inverse\_transform ```python inverse_transform( X: DataArray | NDArray[floating], ) -> DataArray ``` Reconstruct data from component signals. Parameters: - ##### **`X`** (`(time, component) xarray.DataArray or (time, component) numpy.ndarray`) – Signals in component space. Returns: - `(time, ...) xarray.DataArray` – Reconstructed data in the fitted spatial geometry. Raises: - `ValueError` – If `X` has invalid shape or component count. - `TypeError` – If `X` is neither `xarray.DataArray` nor `numpy.ndarray`. #### set\_output ```python set_output(*, transform=None) ``` Set output container. Refer to the :ref:`user guide ` for more details and :ref:`sphx_glr_auto_examples_miscellaneous_plot_set_output.py` for an example on how to use the API. Parameters: - ##### **`transform`** (`(default, pandas, polars)`, default: `"default"` ) – Configure output of `transform` and `fit_transform`. - `"default"`: Default output format of a transformer - `"pandas"`: DataFrame output - `"polars"`: Polars output - `None`: Transform configuration is unchanged .. versionadded:: 1.4 `"polars"` option was added. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. #### set\_params ```python set_params(**params) ``` Set the parameters of this estimator. The method works on simple estimators as well as on nested objects (such as :class:`~sklearn.pipeline.Pipeline`). The latter have parameters of the form `__` so that it's possible to update each component of a nested object. Parameters: - ##### **`**params`** (`dict`, default: `{}` ) – Estimator parameters. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. #### transform ```python transform(X: DataArray) -> DataArray ``` Project data into component space. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array with the same spatial dimensions and sizes as the data used during fit. Returns: - `(time, component) xarray.DataArray` – Decomposed signals in component space. ### NMF Bases: `_BaseFUSIDecomposer` Non-negative matrix factorization (NMF) for VoxelData arrays. Find two non-negative matrices, i.e. matrices with all non-negative elements, (`W`, `H`) whose product approximates the non-negative matrix `X`. This factorization can be used for example for dimensionality reduction, source separation, or topic extraction. The objective function is: \\\[ \\begin{aligned} L(W, H) &= 0.5 \* ||X - WH||\_{\\text{loss}}^2 \\\\ &+ \\alpha\_W \\, l\_{1,\\text{ratio}} \\, n\_{\\text{features}} \\, ||\\mathrm{vec}(W)||\_1 \\\\ &+ \\alpha\_H \\, l\_{1,\\text{ratio}} \\, n\_{\\text{samples}} \\, ||\\mathrm{vec}(H)||\_1 \\\\ &+ 0.5 \\, \\alpha\_W \\, (1 - l\_{1,\\text{ratio}}) \\, n\_{\\text{features}} \\, ||W||\_{F}^2 \\\\ &+ 0.5 \\, \\alpha\_H \\, (1 - l\_{1,\\text{ratio}}) \\, n\_{\\text{samples}} \\, ||H||\_{F}^2, \\end{aligned} \\\] where \\(||A||\_{F}^2 = \\sum\_{i,j} A\_{ij}^2\\) is the Frobenius norm and \\(||vec(A)||\_1 = \\sum\_{i,j} abs(A\_{ij})\\) is the elementwise L1 norm. The generic norm `||X - WH||_{\text{loss}}` may represent the Frobenius norm or another supported beta-divergence loss. The regularization terms are scaled by `n_features` for `W` and by `n_samples` for `H` to keep their impact balanced with respect to one another and to the data fit term. This estimator wraps `sklearn.decomposition.NMF` while keeping xarray metadata through `transform` and `inverse_transform`: - Input data are expected as `(time, ...)` where `...` are spatial dimensions. - `transform` returns a `(time, component)` DataArray. - `inverse_transform` reconstructs to `(time, ...)` using the fitted spatial coordinates. Parameters: - #### **`n_components`** (`int or {auto} or None`, default: `"auto"` ) – Number of components. If `None`, all features are kept. If `n_components="auto"`, the number of components is automatically inferred from `W` or `H` shapes. - #### **`init`** (`(random, nndsvd, nndsvda, nndsvdar)`, default: `"random"` ) – Method used to initialize the procedure. - `None`: `"nndsvda"` if `n_components <= min(n_samples, n_features)`, otherwise `"random"`. - `"random"`: non-negative random matrices, scaled with `sqrt(X.mean() / n_components)`. - `"nndsvd"`: non-negative double singular value decomposition (NNDSVD) initialization, better for sparseness. - `"nndsvda"`: NNDSVD with zeros filled with the average of `X`, better when sparsity is not desired. - `"nndsvdar"`: NNDSVD with zeros filled with small random values, generally faster but less accurate than `"nndsvda"` when sparsity is not desired. - #### **`solver`** (`(cd, mu)`, default: `"cd"` ) – Numerical solver to use: - `"cd"`: coordinate descent solver. - `"mu"`: multiplicative update solver. - #### **`beta_loss`** (`float or {"frobenius", "kullback-leibler", "itakura-saito"}, \`, default: `'frobenius'` ) – ```text default: "frobenius" ``` Beta divergence to be minimized, measuring the distance between `X` and the dot product `WH`. Note that values different from `"frobenius"` (or `2`) and `"kullback-leibler"` (or `1`) lead to significantly slower fits. Note that for `beta_loss <= 0` (or `"itakura-saito"`), the input matrix `X` cannot contain zeros. Used only in the `"mu"` solver. - #### **`tol`** (`float`, default: `1e-4` ) – Tolerance of the stopping condition. - #### **`max_iter`** (`int`, default: `200` ) – Maximum number of iterations before timing out. - #### **`random_state`** (`int or RandomState`, default: `None` ) – Used for initialisation (when `init == "nndsvdar"` or `"random"`) and in coordinate descent. Pass an int for reproducible results across multiple function calls. - #### **`alpha_W`** (`float`, default: `0.0` ) – Constant that multiplies the regularization terms of `W`. Set it to zero to have no regularization on `W`. - #### **`alpha_H`** (`float or same`, default: `"same"` ) – Constant that multiplies the regularization terms of `H`. Set it to zero to have no regularization on `H`. If `"same"`, it takes the same value as `alpha_W`. - #### **`l1_ratio`** (`float`, default: `0.0` ) – The regularization mixing parameter, with `0 <= l1_ratio <= 1`. - For `l1_ratio = 0`, the penalty is an elementwise L2 penalty (aka Frobenius Norm). - For `l1_ratio = 1`, it is an elementwise L1 penalty. - For `0 < l1_ratio < 1`, the penalty is a combination of L1 and L2. - #### **`verbose`** (`int`, default: `0` ) – Whether to be verbose. - #### **`shuffle`** (`bool`, default: `False` ) – Whether to randomize the order of coordinates in the coordinate descent solver. - #### **`mode`** (`(temporal, spatial)`, default: `"temporal"` ) – Whether to fit NMF along temporal or spatial orientation: - `"temporal"`: fit on `(time, space)`. The transformed data `W` are non-negative temporal time courses and the components matrix `H` reshapes to spatial maps. - `"spatial"`: fit on `(space, time)`. In the underlying sklearn fit, `W` lives on voxels and `H` lives on timepoints because the data matrix is transposed. This wrapper still exposes `maps_` as spatial maps and `transform` as `(time, component)` signals. - #### **`mask`** (`DataArray`, default: `None` ) – Boolean VoxelData mask selecting which voxels to include during fitting and projection. Must match the non-`time` dimensions of the input data in the same order. Attributes: - **`maps_`** (`(n_components, ...) xarray.DataArray`) – Non-negative component maps reshaped to the original spatial geometry. - In `"temporal"` mode: the components matrix `H`, interpreted as spatial maps. - In `"spatial"` mode: spatial projection maps derived from the fitted NMF model on transposed data. - **`n_components_`** (`int`) – The number of components. It is the same as the `n_components` parameter if it was given. Otherwise, it is inferred from the fitted factorization. - **`reconstruction_err_`** (`float`) – Frobenius norm of the matrix difference, or beta-divergence, between the training data `X` and the reconstructed data `WH` from the fitted model. - **`n_iter_`** (`int`) – Actual number of iterations. - **`n_features_in_`** (`int`) – Number of features seen during fit. > [!NOTE] > > **Notes** > > In sklearn's notation, the transformed data are named `W` and the components matrix is named `H`. In much of the NMF literature the convention is often the opposite because the data matrix is transposed. > > In `"spatial"` mode, the estimator is still fitted with `sklearn.decomposition.NMF`, but on the transposed data matrix `(space, time)`. The wrapper then computes `(time, component)` signals by projecting the original `(time, voxel)` matrix onto the fitted spatial maps, so that `transform` and `inverse_transform` keep the same public API in both modes. **References** 1. Cichocki, A., and Anh-Huy, P. H. A. N. (2009). "Fast local algorithms for large scale nonnegative matrix and tensor factorizations". IEICE Transactions on Fundamentals of Electronics, Communications and Computer Sciences, 92(3), 708-721. 2. Fevotte, C., and Idier, J. (2011). "Algorithms for nonnegative matrix factorization with the beta-divergence". Neural Computation, 23(9). Examples: ```pycon >>> import numpy as np >>> from confusius.decomposition import NMF >>> from confusius.xarray import create_voxeldata >>> >>> rng = np.random.default_rng(0) >>> k = 5 >>> n_t, n_z, n_y, n_x = 200, 4, 6, 8 >>> temporal = rng.random((n_t, k)) >>> spatial = rng.random((k, n_z * n_y * n_x)) >>> data = create_voxeldata( ... (10.0 * (temporal @ spatial) + 1.0).reshape(n_t, n_z, n_y, n_x), ... dims=("time", "k", "j", "i"), ... dt=0.1, ... spacing=(1.0, 1.0, 1.0), ... ) >>> >>> nmf = NMF(n_components=k, init="nndsvda", random_state=0) >>> signals = nmf.fit_transform(data) >>> signals.dims ('time', 'component') >>> reconstructed = nmf.inverse_transform(signals) >>> reconstructed.dims ('time', 'k', 'j', 'i') ``` Methods: - **`fit`** – Fit NMF on a VoxelData array. - **`fit_transform`** – Fit on `X` and return transformed signals. - **`get_metadata_routing`** – Get metadata routing of this object. - **`get_params`** – Get parameters for this estimator. - **`inverse_transform`** – Reconstruct data from component signals. - **`set_output`** – Set output container. - **`set_params`** – Set the parameters of this estimator. - **`transform`** – Project data into component space. #### fit ```python fit(X: DataArray, y: None = None) -> NMF ``` Fit NMF on a VoxelData array. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array. Must be non-negative. - ##### **`y`** (`None`, default: `None` ) – Ignored. Present for scikit-learn API compatibility. Returns: - `NMF` – Fitted estimator. Raises: - `ValueError` – If input has no `time` dimension, fewer than 2 timepoints, no spatial dimensions, contains negative values, or `mode` is not `"temporal"` or `"spatial"`. #### fit\_transform ```python fit_transform( X: DataArray, y: None = None, **fit_params: object ) -> DataArray ``` Fit on `X` and return transformed signals. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array. - ##### **`y`** (`None`, default: `None` ) – Ignored. Present for scikit-learn API compatibility. - ##### **`**fit_params`** (`object`, default: `{}` ) – Additional fit parameters. Unsupported for this estimator. Returns: - `(time, component) xarray.DataArray` – Decomposed signals in component space. Raises: - `TypeError` – If additional fit parameters are provided. #### get\_metadata\_routing ```python get_metadata_routing() ``` Get metadata routing of this object. Please check :ref:`User Guide ` on how the routing mechanism works. Returns: - **`routing`** ( `MetadataRequest` ) – A :class:`~sklearn.utils.metadata_routing.MetadataRequest` encapsulating routing information. #### get\_params ```python get_params(deep=True) ``` Get parameters for this estimator. Parameters: - ##### **`deep`** (`bool`, default: `True` ) – If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns: - **`params`** ( `dict` ) – Parameter names mapped to their values. #### inverse\_transform ```python inverse_transform( X: DataArray | NDArray[floating], ) -> DataArray ``` Reconstruct data from component signals. Parameters: - ##### **`X`** (`(time, component) xarray.DataArray or (time, component) numpy.ndarray`) – Signals in component space. Returns: - `(time, ...) xarray.DataArray` – Reconstructed data in the fitted spatial geometry. Raises: - `ValueError` – If `X` has invalid shape or component count. - `TypeError` – If `X` is neither `xarray.DataArray` nor `numpy.ndarray`. #### set\_output ```python set_output(*, transform=None) ``` Set output container. Refer to the :ref:`user guide ` for more details and :ref:`sphx_glr_auto_examples_miscellaneous_plot_set_output.py` for an example on how to use the API. Parameters: - ##### **`transform`** (`(default, pandas, polars)`, default: `"default"` ) – Configure output of `transform` and `fit_transform`. - `"default"`: Default output format of a transformer - `"pandas"`: DataFrame output - `"polars"`: Polars output - `None`: Transform configuration is unchanged .. versionadded:: 1.4 `"polars"` option was added. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. #### set\_params ```python set_params(**params) ``` Set the parameters of this estimator. The method works on simple estimators as well as on nested objects (such as :class:`~sklearn.pipeline.Pipeline`). The latter have parameters of the form `__` so that it's possible to update each component of a nested object. Parameters: - ##### **`**params`** (`dict`, default: `{}` ) – Estimator parameters. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. #### transform ```python transform(X: DataArray) -> DataArray ``` Project data into component space. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array with the same spatial dimensions and sizes as the data used during fit. Returns: - `(time, component) xarray.DataArray` – Decomposed signals in component space. ### PCA Bases: `_BaseFUSIDecomposer` Principal component analysis (PCA) for VoxelData arrays. Linear dimensionality reduction using singular value decomposition (SVD) of the data to project it to a lower dimensional space. The input data is centered but not scaled for each feature before applying the SVD. It uses the LAPACK implementation of the full SVD or a randomized truncated SVD by the method of Halko *et al.* (2009), depending on the shape of the input data and the number of components to extract. This estimator wraps `sklearn.decomposition.PCA` while keeping xarray metadata through `transform` and `inverse_transform`: - Input data are expected as `(time, ...)` where `...` are spatial dimensions. - `transform` returns a `(time, component)` DataArray. - `inverse_transform` reconstructs to `(time, ...)` using the fitted spatial coordinates. Parameters: - #### **`n_components`** (`(int, float or mle)`, default: `None` ) – Number of components to keep: - If `n_components` is not provided all components are kept: `n_components == min(n_samples, n_features)` - If `n_components == "mle"` and `svd_solver == "full"`, Minka's MLE is used to guess the dimension. Use of `n_components == "mle"` will interpret `svd_solver == "auto"` as `svd_solver == "full"`. - If `0 < n_components < 1` and `svd_solver == "full"`, the number of components will be selected such that the amount of variance that needs to be explained is greater than the percentage specified by `n_components`. - If `svd_solver == "arpack"`, the number of components must be strictly less than the minimum of `n_features` and `n_samples`. Hence, the `None` case results in: `n_components == min(n_samples, n_features) - 1`. - #### **`whiten`** (`bool`, default: `False` ) – Whether to multiply the loading vectors by the square root of `n_samples` and then divide them by the singular values to ensure uncorrelated outputs with unit component-wise variances. Whitening will remove some information from the transformed signal (the relative variance scales of the components) but can sometimes improve the predictive accuracy of the downstream estimators by making their data respect some hard-wired assumptions. - #### **`svd_solver`** (`(auto, full, covariance_eigh, arpack, randomized)`, default: `"auto"` ) – - `"auto"`: The solver is selected by a default policy based on data shape and `n_components`: if the input data has fewer than 1000 features and more than 10 times as many samples, then the "covariance\_eigh" solver is used. Otherwise, if the input data is larger than 500x500 and the number of components to extract is lower than 80% of the smallest dimension of the data, then the more efficient "randomized" method is selected. Otherwise the exact "full" SVD is computed and optionally truncated afterwards. - `"full"`: Run an exact full SVD calling the standard LAPACK solver via `scipy.linalg.svd` and select the components by postprocessing. - `"covariance_eigh"`: Precompute the covariance matrix (on centered data), run a classical eigenvalue decomposition on the covariance matrix typically using LAPACK and select the components by postprocessing. This solver is very efficient for `n_samples >> n_features` and small `n_features`. It is, however, not tractable otherwise for large `n_features` (large memory footprint required to materialize the covariance matrix). Also note that compared to the "full" solver, this solver effectively doubles the condition number and is therefore less numerically stable (e.g. on input data with a large range of singular values). - `"arpack"`: Run SVD truncated to `n_components` calling ARPACK solver via `scipy.sparse.linalg.svds`. It requires strictly `0 < n_components < min(X.shape)`. - `"randomized"`: Run randomized SVD by the method of Halko et al. - #### **`tol`** (`float`, default: `0.0` ) – Tolerance for singular values computed by `svd_solver == "arpack"`. Must be of range `[0.0, infinity)`. - #### **`iterated_power`** (`int or auto`, default: `"auto"` ) – Number of iterations for the power method computed by `svd_solver == "randomized"`. Must be of range `[0, infinity)`. - #### **`n_oversamples`** (`int`, default: `10` ) – This parameter is only relevant when `svd_solver="randomized"`. It corresponds to the additional number of random vectors to sample the range of `X` so as to ensure proper conditioning. See `randomized_svd` for more details. - #### **`power_iteration_normalizer`** (`(auto, QR, LU, none)`, default: `"auto"` ) – Power iteration normalizer for randomized SVD solver. Not used by ARPACK. See `randomized_svd` for more details. - #### **`random_state`** (`int`, default: `None` ) – Used when the `"arpack"` or `"randomized"` solvers are used. Pass an int for reproducible results across multiple function calls. - #### **`mode`** (`(temporal, spatial)`, default: `"temporal"` ) – Whether to fit PCA along temporal or spatial orientation: - `"temporal"`: fit on `(time, space)`. The principal axes are spatial maps; the projected components are temporal time courses that capture dominant variance across voxels. - `"spatial"`: fit on `(space, time)`. The principal axes are time courses; the projected components are spatial maps that capture dominant variance across time. - #### **`mask`** (`DataArray`, default: `None` ) – Boolean VoxelData mask selecting which voxels to include during fitting and projection. Must match the non-`time` dimensions of the input data in the same order. Attributes: - **`maps_`** (`(n_components, ...) xarray.DataArray`) – Principal directions in feature space (loadings), representing the axes of maximum variance. Equivalently, the right singular vectors of the centered input data, parallel to its eigenvectors. Sorted by decreasing `explained_variance_` and reshaped to the original spatial geometry. - **`explained_variance_`** (`(n_components,) xarray.DataArray`) – The amount of variance explained by each of the selected components. The variance estimation uses `n_samples - 1` degrees of freedom. Equal to `n_components` largest eigenvalues of the covariance matrix of `X`. - **`explained_variance_ratio_`** (`(n_components,) xarray.DataArray`) – Percentage of variance explained by each of the selected components. If `n_components` is not set then all components are stored and the sum of the ratios is equal to 1.0. - **`singular_values_`** (`(n_components,) xarray.DataArray`) – The singular values corresponding to each of the selected components. The singular values are equal to the 2-norms of the `n_components` variables in the lower-dimensional space. - **`mean_`** (`(...) xarray.DataArray`) – Per-feature empirical mean, estimated from the training set. Equal to `X.mean(axis=0)`. - **`n_components_`** (`int`) – The estimated number of components. When `n_components` is set to "mle" or a number between 0 and 1 (with `svd_solver == "full"`) this number is estimated from input data. Otherwise it equals the parameter n\_components, or the lesser value of `n_features` and `n_samples` if `n_components` is `None`. - **`n_samples_`** (`int`) – Number of samples in the training data. - **`noise_variance_`** (`float`) – The estimated noise covariance following the Probabilistic PCA model from Tipping and Bishop 1999. See "Pattern Recognition and Machine Learning" by C. Bishop, 12.2.1 p. 574 or [http://www.miketipping.com/papers/met-mppca.pdf](). It is required to compute the estimated data covariance and score samples. Equal to the average of `min(n_features, n_samples) - n_components` smallest eigenvalues of the covariance matrix of `X`. - **`n_features_in_`** (`int`) – Number of features seen during fit. **References** 1. Halko, N., Martinsson, P. G., and Tropp, J. A. (2011). "Finding structure with randomness: Probabilistic algorithms for constructing approximate matrix decompositions". SIAM Review, 53(2), 217-288. Examples: ```pycon >>> import numpy as np >>> from confusius.decomposition import PCA >>> from confusius.xarray import create_voxeldata >>> >>> rng = np.random.default_rng(0) >>> data = create_voxeldata( ... rng.standard_normal((200, 5, 10, 20)), ... dims=("time", "k", "j", "i"), ... dt=0.1, ... spacing=(1.0, 1.0, 1.0), ... ) >>> >>> pca = PCA(n_components=5, random_state=0) >>> signals = pca.fit_transform(data) >>> signals.dims ('time', 'component') >>> reconstructed = pca.inverse_transform(signals) >>> reconstructed.dims ('time', 'k', 'j', 'i') ``` Methods: - **`fit`** – Fit PCA on a VoxelData array. - **`fit_transform`** – Fit on `X` and return transformed signals. - **`get_metadata_routing`** – Get metadata routing of this object. - **`get_params`** – Get parameters for this estimator. - **`inverse_transform`** – Reconstruct data from component signals. - **`set_output`** – Set output container. - **`set_params`** – Set the parameters of this estimator. - **`transform`** – Project data into component space. #### fit ```python fit(X: DataArray, y: None = None) -> PCA ``` Fit PCA on a VoxelData array. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array. - ##### **`y`** (`None`, default: `None` ) – Ignored. Present for scikit-learn API compatibility. Returns: - `PCA` – Fitted estimator. Raises: - `ValueError` – If input has no `time` dimension, fewer than 2 timepoints, or no spatial dimensions. - `ValueError` – If `mode` is not `"temporal"` or `"spatial"`. #### fit\_transform ```python fit_transform( X: DataArray, y: None = None, **fit_params: object ) -> DataArray ``` Fit on `X` and return transformed signals. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array. - ##### **`y`** (`None`, default: `None` ) – Ignored. Present for scikit-learn API compatibility. - ##### **`**fit_params`** (`object`, default: `{}` ) – Additional fit parameters. Unsupported for this estimator. Returns: - `(time, component) xarray.DataArray` – Decomposed signals in component space. Raises: - `TypeError` – If additional fit parameters are provided. #### get\_metadata\_routing ```python get_metadata_routing() ``` Get metadata routing of this object. Please check :ref:`User Guide ` on how the routing mechanism works. Returns: - **`routing`** ( `MetadataRequest` ) – A :class:`~sklearn.utils.metadata_routing.MetadataRequest` encapsulating routing information. #### get\_params ```python get_params(deep=True) ``` Get parameters for this estimator. Parameters: - ##### **`deep`** (`bool`, default: `True` ) – If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns: - **`params`** ( `dict` ) – Parameter names mapped to their values. #### inverse\_transform ```python inverse_transform( X: DataArray | NDArray[floating], ) -> DataArray ``` Reconstruct data from component signals. Parameters: - ##### **`X`** (`(time, component) xarray.DataArray or (time, component) numpy.ndarray`) – Signals in component space. Returns: - `(time, ...) xarray.DataArray` – Reconstructed data in the fitted spatial geometry. Raises: - `ValueError` – If `X` has invalid shape or component count. - `TypeError` – If `X` is neither `xarray.DataArray` nor `numpy.ndarray`. #### set\_output ```python set_output(*, transform=None) ``` Set output container. Refer to the :ref:`user guide ` for more details and :ref:`sphx_glr_auto_examples_miscellaneous_plot_set_output.py` for an example on how to use the API. Parameters: - ##### **`transform`** (`(default, pandas, polars)`, default: `"default"` ) – Configure output of `transform` and `fit_transform`. - `"default"`: Default output format of a transformer - `"pandas"`: DataFrame output - `"polars"`: Polars output - `None`: Transform configuration is unchanged .. versionadded:: 1.4 `"polars"` option was added. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. #### set\_params ```python set_params(**params) ``` Set the parameters of this estimator. The method works on simple estimators as well as on nested objects (such as :class:`~sklearn.pipeline.Pipeline`). The latter have parameters of the form `__` so that it's possible to update each component of a nested object. Parameters: - ##### **`**params`** (`dict`, default: `{}` ) – Estimator parameters. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. #### transform ```python transform(X: DataArray) -> DataArray ``` Project data into component space. Parameters: - ##### **`X`** (`(time, ...) xarray.DataArray`) – VoxelData array with the same spatial dimensions and sizes as the data used during fit. Returns: - `(time, component) xarray.DataArray` – Decomposed signals in component space. # `confusius.extract` ## extract Signal extraction from fUSI data. Modules: - **`labels`** – Extraction of region-aggregated signals using integer label maps. - **`mask`** – Extraction of signals using boolean masks. - **`reconstruction`** – Reconstruction of VoxelData arrays from N-D signals using masks. Functions: - **`extract_with_labels`** – Extract region-aggregated signals from a VoxelData array. - **`extract_with_mask`** – Extract signals from a VoxelData array using a binary mask. - **`unmask`** – Reconstruct a VoxelData array from N-D signals using a mask. ### extract\_with\_labels ```python extract_with_labels( data: DataArray, labels: DataArray, reduction: Literal[ "mean", "sum", "median", "min", "max", "var", "std" ] = "mean", ) -> DataArray ``` Extract region-aggregated signals from a VoxelData array. For each unique non-zero label in `labels`, applies `reduction` across all voxels belonging to that region. The native voxel dimensions (`k`/`j`/`i`) are collapsed into a single `region` dimension. Parameters: - #### **`data`** (`DataArray`) – VoxelData array with native voxel dims `k`/`j`/`i` and a `VoxelToWorldIndex`, plus any number of non-spatial dimensions (e.g., `time`, `pose`). See `ensure_voxeldata`. - #### **`labels`** (`DataArray`) – Integer label map sharing `data`'s voxel grid, in one of two formats: - **Flat label map**: Spatial dims only, e.g. `(k, j, i)`. Background voxels labeled `0`; each unique non-zero integer identifies a distinct, non-overlapping region. The `region` coordinate of the output holds the integer label values. - **Stacked mask format**: Has a leading `mask` dimension followed by spatial dims, e.g. `(mask, k, j, i)`. Each layer has exactly one non-zero value identifying its own voxels, and regions may overlap; the non-zero value itself is not used to identify the layer, so it may repeat across layers (e.g. the same region id for left/right hemisphere layers). The `region` coordinate of the output holds the `mask` coordinate values (e.g., region label). - #### **`reduction`** (`(mean, sum, median, min, max, var, std)`, default: `"mean"` ) – Aggregation function applied across voxels in each region. Returns: - `DataArray` – Array with spatial dimensions replaced by a `region` dimension. All non-spatial dimensions are preserved. For example (flat label map): - `(time, k, j, i)` → `(time, region)` - `(time, pose, k, j, i)` → `(time, pose, region)` - `(k, j, i)` → `(region,)` Raises: - `ValueError` – If `labels` or `data` isn't a VoxelData array, if `labels`'s voxel grid doesn't match `data`'s, if `reduction` is not a valid option, or if `labels` contains no non-zero values. - `TypeError` – If `labels` is not integer dtype. > [!NOTE] > > **Notes** > > Uses [flox]() for efficient, lazy groupby reductions on Dask-backed arrays. Data can be chunked along any dimension without restriction. Examples: ```pycon >>> import numpy as np >>> from confusius.extract import extract_with_labels >>> from confusius.xarray import create_voxeldata >>> >>> # 3D+t data: (time, k, j, i) >>> data = create_voxeldata( ... np.random.randn(100, 10, 20, 30), ... dims=("time", "k", "j", "i"), ... dt=0.5, ... spacing=(1.0, 1.0, 1.0), ... ) >>> labels = create_voxeldata( ... np.zeros((10, 20, 30), dtype=int), ... dims=("k", "j", "i"), ... spacing=(1.0, 1.0, 1.0), ... ) >>> labels[0, :, :] = 1 # Region 1: first k-slice. >>> labels[1, :, :] = 2 # Region 2: second k-slice. >>> signals = extract_with_labels(data, labels) >>> signals.dims ('time', 'region') >>> signals.coords["region"].values array([1, 2]) >>> >>> # Stacked mask format from the atlas accessor's get_masks. Left/right hemisphere >>> # layers share a region id, but each is disambiguated by its `mask` coord. >>> mask = atlas_fusi.atlas.get_masks(["VISp", "VISp"], sides=["left", "right"]) >>> signals = extract_with_labels(data, mask) >>> signals.coords["region"].values array(['VISp_L', 'VISp_R'], dtype=object) ``` ### extract\_with\_mask ```python extract_with_mask( data: DataArray, mask: DataArray ) -> DataArray ``` Extract signals from a VoxelData array using a binary mask. This function flattens `mask`'s native voxel dimensions (`k`/`j`/`i`) into a single `space` dimension, while preserving all other dimensions of `data` (e.g., `time`, `pose`). Parameters: - #### **`data`** (`DataArray`) – VoxelData array with native voxel dims `k`/`j`/`i` and a `VoxelToWorldIndex`, plus any number of non-spatial dimensions (e.g., `time`, `pose`). See `ensure_voxeldata`. - #### **`mask`** (`DataArray`) – Mask defining which voxels to extract, sharing `data`'s voxel grid. Must have boolean dtype, or binary numeric dtype (0 = background, at most one non-zero value = foreground). The latter format is produced by `get_masks`, and also covers masks written by tools without a boolean dtype (e.g. FSL/NiBabel NIfTI masks with values in `{0, 1}` or `{0.0, 1.0}`). Returns: - `DataArray` – Array with `k`/`j`/`i` flattened into a `space` dimension. All non-spatial dimensions are preserved. The `space` dimension has a MultiIndex storing spatial coordinates. - `(time, k, j, i)` → `(time, space)` - `(time, pose, k, j, i)` → `(time, pose, space)` - `(k, j, i)` → `(space,)` For simple round-trip reconstruction, use `.unstack("space")` which re-creates the original DataArray using the smallest bounding box containing the masked voxels. For full mask shape reconstruction, use `confusius.extract.unmask`. Raises: - `ValueError` – If `mask` or `data` isn't a VoxelData array, or if `mask`'s voxel grid doesn't match `data`'s. - `TypeError` – If `mask` is not boolean dtype (or a binary numeric dtype). Examples: ```pycon >>> import numpy as np >>> from confusius.extract import extract_with_mask >>> from confusius.xarray import create_voxeldata >>> >>> # 3D+t data: (time, k, j, i) >>> data = create_voxeldata( ... np.random.randn(100, 10, 20, 30), ... dims=("time", "k", "j", "i"), ... dt=0.5, ... spacing=(1.0, 1.0, 1.0), ... ) >>> mask = create_voxeldata( ... np.random.rand(10, 20, 30) > 0.5, ... dims=("k", "j", "i"), ... spacing=(1.0, 1.0, 1.0), ... ) >>> signals = extract_with_mask(data, mask) >>> signals.dims ('time', 'space') >>> >>> # 3D+t data with extra dim: (time, pose, k, j, i) >>> pose_data = create_voxeldata( ... np.random.randn(100, 5, 10, 20, 30), ... dims=("time", "pose", "k", "j", "i"), ... dt=0.5, ... spacing=(1.0, 1.0, 1.0), ... ) >>> pose_signals = extract_with_mask(pose_data, mask) >>> pose_signals.dims ('time', 'pose', 'space') ``` ### unmask ```python unmask( signals: ndarray | DataArray, mask: DataArray, new_dims: list[str] | None = None, new_dims_coords: dict[str, ndarray] | None = None, attrs: dict | None = None, fill_value: float = 0.0, ) -> DataArray ``` Reconstruct a VoxelData array from N-D signals using a mask. Parameters: - #### **`signals`** (`ndarray or DataArray`) – Array with shape `(..., space)` where `...` can be any number of dimensions. The last dimension must correspond to masked voxels. - If `signals` is a DataArray, it must have a `space` dimension as the last dimension. All other dimensions and their coordinates are preserved. - If `signals` is a Numpy array, you can specify names and coordinates for the leading dimensions using `new_dims` and `new_dims_coords`. If not provided, dimensions are named `["dim_0", "dim_1", ...]` with integer coordinates. - #### **`mask`** (`DataArray`) – VoxelData mask used for the original extraction. Provides spatial dimensions, coordinates, and `VoxelToWorldIndex` for reconstruction. Must be either boolean dtype, or binary numeric dtype (0 = background, at most one non-zero value = foreground). Spatial dimensions and coordinates must match the original data. - #### **`new_dims`** (`list of str`, default: `None` ) – Names for leading dimensions when `signals` is a Numpy array. Must match the number of leading dimensions `(ndim - 1)`. If not provided, uses `["dim_0", "dim_1", ...]`. Ignored if `signals` is a DataArray. - #### **`new_dims_coords`** (`dict[str, ndarray]`, default: `None` ) – Coordinates for leading dimensions when `signals` is a Numpy array. Keys must match dimension names in `new_dims`. If not provided, uses integer indices for all dimensions. Ignored if `signals` is a DataArray. - #### **`attrs`** (`dict`, default: `None` ) – Attributes to attach to the output DataArray. - #### **`fill_value`** (`float`, default: `0.0` ) – Value to fill in non-masked voxels. Returns: - `DataArray` – Reconstructed VoxelData array with shape `(..., *mask.dims)`, where spatial dimensions, coordinates, and the derived world `z`/`y`/`x` coordinates come from the mask. Raises: - `ValueError` – If `mask` isn't a valid VoxelData array, if `signals` shape doesn't match `mask`, or if `new_dims`/`new_dims_coords` are inconsistent with `signals` shape. - `TypeError` – If `mask` is not boolean dtype (or a binary numeric dtype). Examples: ```pycon >>> import numpy as np >>> from confusius.extract import extract_with_mask, unmask >>> from confusius.xarray import create_voxeldata >>> from sklearn.cluster import KMeans >>> >>> data = create_voxeldata( ... np.random.rand(10, 4, 5, 6), ... dims=("time", "k", "j", "i"), ... dt=0.5, ... spacing=(1.0, 1.0, 1.0), ... ) >>> mask = create_voxeldata( ... np.random.rand(4, 5, 6) > 0.5, dims=("k", "j", "i"), spacing=(1.0, 1.0, 1.0) ... ) >>> >>> # Extract signals into a (time, space) matrix, then cluster voxel time-courses. >>> # Clustering needs the flat voxel axis as samples, so this can't be done >>> # directly on the gridded (k, j, i) array. >>> signals = extract_with_mask(data, mask) >>> labels = KMeans(n_clusters=3, n_init="auto").fit_predict(signals.values.T) >>> >>> # Unmask - reconstruct the cluster labels as a spatial map, no extra dims >>> cluster_map = unmask(labels, mask, fill_value=-1) >>> cluster_map.dims ('k', 'j', 'i') >>> >>> # Unmask - two extra dims, with custom coords >>> pose_data = np.random.randn(5, 3, signals.sizes["space"]) # (component, pose, space) >>> spatial_pose = unmask( ... pose_data, ... mask, ... new_dims=["component", "pose"], ... new_dims_coords={"component": [1, 2, 3, 4, 5], "pose": [0, 1, 2]}, ... ) >>> spatial_pose.dims ('component', 'pose', 'k', 'j', 'i') ``` # `confusius.glm` ## glm GLM analysis for fUSI data. This package provides General Linear Model tools for voxel-wise statistical analysis of functional ultrasound imaging data. The code is adapted from [Nilearn's GLM module]() under the BSD-3-Clause license. Modules: - **`first_level`** – First-level GLM for single-subject fUSI analysis. - **`second_level`** – Second-level GLM for group-level fUSI analysis. Classes: - **`Contrast`** – Passive container for contrast results. - **`FirstLevelModel`** – First-level GLM estimator for voxel-wise fUSI analysis. - **`RegressionResults`** – Container for regression model results. - **`SecondLevelModel`** – Second-level GLM estimator for group-level fUSI analysis. Functions: - **`claron2021_hrf`** – Return the Claron et al. (2021) inverse-gamma fUSI HRF preset. - **`gamma_difference_hrf`** – Return an HRF modeled as the difference of two gamma functions. - **`gamma_hrf`** – Return a positive-only gamma HRF on an oversampled time grid. - **`glover_hrf`** – Return the Glover canonical HRF on an oversampled time grid. - **`inverse_gamma_hrf`** – Return an HRF modeled as an inverse gamma distribution. - **`make_first_level_design_matrix`** – Create a first-level design matrix from events and confounds. - **`make_second_level_design_matrix`** – Build a second-level design matrix. - **`spm_hrf`** – Return the SPM canonical HRF on an oversampled time grid. - **`verhoef2025_hrf`** – Return the Verhoef et al. (2025) single-gamma human fUSI HRF preset. ### Contrast Passive container for contrast results. Holds a contrast's effect/variance estimates and the precomputed test statistics. To construct one from raw `(effect, variance, dof)`, use `from_estimate`; that is where the t/F statistic, p-value, and z-score are computed. Supports addition for fixed-effects combination across runs, and scalar multiplication/division for rescaling. Attributes: - **`effect`** (`(n_voxels,) numpy.ndarray or (dim, n_voxels) numpy.ndarray`) – Contrast effect estimates. - **`variance`** (`(n_voxels,) numpy.ndarray`) – Contrast variance estimates. - **`dim`** (`int`) – Contrast dimension (1 for *t*, \>1 for *F*). - **`dof`** (`float`) – Degrees of freedom of the residuals. - **`stat_type`** (`{'t', 'F'}`) – Type of contrast statistic. - **`baseline`** (`float`) – Null-hypothesis value used to compute the statistic. - **`statistic`** (`(n_voxels,) numpy.ndarray`) – Test statistic (*t* or *F*). - **`pvalue`** (`(n_voxels,) numpy.ndarray`) – One-sided *p*-values (upper tail). - **`one_minus_pvalue`** (`(n_voxels,) numpy.ndarray`) – `1 - pvalue`, computed via the CDF for numerical stability in the lower tail. - **`zscore`** (`(n_voxels,) numpy.ndarray`) – *z*-scores derived from `pvalue`/`one_minus_pvalue`. - **`tiny`** (`float`) – Small value used to guard against division by zero. - **`dofmax`** (`float`) – Cap on degrees of freedom used when computing p-values. Methods: - **`from_estimate`** – Build a `Contrast` from raw estimates. - **`from_results`** – Build a `Contrast` from regression results. #### from\_estimate ```python from_estimate( effect: NDArray[floating], variance: NDArray[floating], *, dim: int | None = None, dof: float = 10000000000.0, stat_type: Literal["t", "F"] = "t", baseline: float = 0.0, tiny: float = 1e-50, dofmax: float = 10000000000.0, ) -> Contrast ``` Build a `Contrast` from raw estimates. Computes the test statistic, *p*-values, and *z*-scores for the supplied `baseline` and packs everything into a `Contrast` result object. Parameters: - ##### **`effect`** (`(n_voxels,) or (dim, n_voxels) numpy.ndarray`) – Contrast effect estimates. - ##### **`variance`** (`(n_voxels,) numpy.ndarray`) – Contrast variance estimates. - ##### **`dim`** (`int`, default: `None` ) – Contrast dimension. If not provided, inferred from `effect`. - ##### **`dof`** (`float`, default: `1e10` ) – Degrees of freedom of the residuals. - ##### **`stat_type`** (`('t', 'F')`, default: `"t"` ) – Type of contrast statistic. - ##### **`baseline`** (`float`, default: `0.0` ) – Null-hypothesis value tested against. The statistic is computed as `(effect - baseline) / sqrt(variance)` (*t*) or `sum((effect - baseline)**2) / dim / variance` (*F*). To test a different null, call `from_estimate` again with a new `baseline`. - ##### **`tiny`** (`float`, default: `1e-50` ) – Small value to guard against division by zero. - ##### **`dofmax`** (`float`, default: `1e10` ) – Maximum degrees of freedom used when computing p-values. Returns: - `Contrast` – Contrast result with `statistic`, `pvalue`, `one_minus_pvalue`, and `zscore` precomputed. Raises: - `ValueError` – If `effect` is not 1D or 2D, `variance` is not 1D, or `stat_type` is not `"t"` or `"F"`. #### from\_results ```python from_results( results: RegressionResults, contrast_vec: NDArray[floating], *, stat_type: Literal["t", "F"] | None = None, baseline: float = 0.0, ) -> Contrast ``` Build a `Contrast` from regression results. Dispatches to a *t* or *F* contrast based on the shape of `contrast_vec` (or `stat_type` when provided), then packages the per-voxel effect, variance, and degrees of freedom into a `Contrast`. For *F*-contrasts, the whitened-effect representation is used so that the standard `sum(effect²) / dim / variance` formula in `from_estimate` recovers the proper quadratic-form *F* statistic — see `compute_f_contrast` for the whitening derivation. Parameters: - ##### **`results`** (`RegressionResults`) – Fitted GLM results. - ##### **`contrast_vec`** (`(n_columns,) or (q, n_columns) numpy.ndarray`) – Numeric contrast vector (1D → *t*) or matrix (2D → *F*). - ##### **`stat_type`** (`('t', 'F')`, default: `"t"` ) – Force the contrast type. By default, inferred from the shape of `contrast_vec`. - ##### **`baseline`** (`float`, default: `0.0` ) – Null-hypothesis value tested against. Returns: - `Contrast` – Result with `statistic`, `pvalue`, `one_minus_pvalue`, and `zscore` precomputed. ### FirstLevelModel Bases: `BaseEstimator` First-level GLM estimator for voxel-wise fUSI analysis. Fits a General Linear Model to VoxelData arrays and computes statistical contrasts. Supports multiple runs (fixed-effects combination) and autoregressive noise modelling. This implementation is adapted from `nilearn.glm.first_level.FirstLevelModel`. Parameters: - #### **`hrf_model`** (`('glover', 'spm', 'verhoef2025', 'claron2021', 'fir')`, default: `"glover"` ) – Hemodynamic response function model. A callable matching the `HRFModel` protocol (a function taking `dt` and `oversampling` and returning a 1D array) is invoked to produce a custom HRF kernel. If not provided, skips HRF convolution and uses the raw block regressors, matching `make_first_level_design_matrix`. - #### **`drift_model`** (`('cosine', 'polynomial')`, default: `"cosine"` ) – Drift model for low-frequency confounds. - #### **`low_cutoff`** (`float`, default: `0.01` ) – Low cutoff frequency in hertz (used with `drift_model="cosine"`). - #### **`drift_order`** (`int`, default: `1` ) – Polynomial order when `drift_model="polynomial"`. - #### **`fir_delays`** (`list of int`, default: `None` ) – FIR delays in volumes (required when `hrf_model="fir"`). - #### **`noise_model`** (`'ols'`, default: `"ols"` ) – Noise model. `"arN"` estimates AR(N) coefficients from OLS residuals and refits with a whitened model. - #### **`minimize_memory`** (`bool`, default: `True` ) – Whether to keep only the statistics needed for contrast computation (per-run `RegressionResults` are discarded after extracting the contrast-relevant fields). - #### **`oversampling`** (`int`, default: `50` ) – Oversampling factor for HRF convolution. - #### **`min_onset`** (`float`, default: `-24.0` ) – Minimum onset time in seconds for event regressors. - #### **`uniformity_tolerance`** (`float`, default: `1e-2` ) – Maximum allowed per-interval relative deviation from the median consecutive interval in the run `time` coordinate. Increase this value to tolerate slight timestamp jitter. - #### **`smoothing_fwhm`** (`float or dict[str, float]`, default: `None` ) – Full width at half maximum of the Gaussian smoothing kernel, in the world units of the spatial coordinates, applied to each run before model fitting. A scalar smooths all non-`time` dimensions (caution if the data has a `pose` dimension); a dict maps dimension names to per-dimension FWHM. Smoothing is delegated to `smooth_volume`, which requires each smoothed dimension to have uniform coordinate spacing. > **Warning** > > If the data has any other dimension besides `time`, a scalar `smoothing_fwhm` will smooth these dimensions as well. > **Attention** > > The dict may also include the `"time"` key to smooth along the time axis as well (this is intended behavior). If not provided, no smoothing is applied. - #### **`mask`** (`DataArray`, default: `None` ) – Boolean spatial mask selecting voxels to include in model fitting. Must match the spatial dimensions and coordinates of each run. - #### **`show_progress`** (`bool`, default: `False` ) – Whether to display a progress bar counting the runs fitted so far. Attributes: - **`design_matrices_`** (`list of pandas.DataFrame`) – Design matrix for each run (set after `fit`). - **`results_`** (`list of RegressionResults`) – Per-run regression results (set after `fit`). Examples: ```pycon >>> import numpy as np, pandas as pd, xarray as xr >>> data = xr.DataArray( ... np.random.randn(200, 2, 3, 4), ... dims=["time", "z", "y", "x"], ... coords={"time": np.arange(200) * 0.5}, ... ) >>> events = pd.DataFrame( ... {"trial_type": ["A", "B"] * 5, ... "onset": np.arange(10) * 10.0, ... "duration": [2.0] * 10} ... ) >>> model = FirstLevelModel(noise_model="ols") >>> model.fit(data, events=events) >>> z_map = model.compute_contrast("A - B") ``` Methods: - **`compute_contrast`** – Compute a contrast and return a statistical map. - **`fit`** – Fit the GLM to fUSI data. - **`get_metadata_routing`** – Get metadata routing of this object. - **`get_params`** – Get parameters for this estimator. - **`set_params`** – Set the parameters of this estimator. #### compute\_contrast ```python compute_contrast( contrast_def: str | NDArray[floating], stat_type: Literal["t", "F"] | None = None, output_type: Literal[ "zscore", "statistic", "pvalue", "effect", "variance", ] = "zscore", baseline: float = 0.0, ) -> DataArray ``` Compute a contrast and return a statistical map. Parameters: - ##### **`contrast_def`** (`str or ndarray`) – Contrast definition. A string is parsed as an expression over design-matrix column names (e.g. `"A - B"`). A 1D array specifies a *t*-contrast vector; a 2D array specifies an *F*-contrast matrix. - ##### **`stat_type`** (`('t', 'F')`, default: `"t"` ) – Force the contrast type. By default inferred from the shape of the contrast vector (1D → *t*, 2D → *F*). - ##### **`output_type`** (`('zscore', 'statistic', 'pvalue', 'effect', 'variance')`, default: `"zscore"` ) – Which statistical map to return. - ##### **`baseline`** (`float`, default: `0.0` ) – Null-hypothesis value tested against. The statistic is `(effect - baseline) / sqrt(variance)` for *t*-contrasts and `sum((effect - baseline)**2) / dim / variance` for *F*-contrasts. Returns: - `DataArray` – Statistical map with the spatial dimensions of the input data. Raises: - `ValueError` – If the model has not been fitted or the contrast definition is invalid. #### fit ```python fit( run_data: DataArray | list[DataArray], events: DataFrame | list[DataFrame] | None = None, confounds: DataFrame | NDArray[floating] | DataArray | list[DataFrame | NDArray[floating] | DataArray | None] | None = None, design_matrices: DataFrame | list[DataFrame] | None = None, ) -> FirstLevelModel ``` Fit the GLM to fUSI data. Either `events` or `design_matrices` must be provided. When `design_matrices` is given, `events`, `confounds`, and the design-related constructor parameters (`hrf_model`, etc.) are ignored. Parameters: - ##### **`run_data`** (`xarray.DataArray or list of xarray.DataArray`) – A single-run VoxelData array, or a list of per-run VoxelData arrays for multi-run data. Each array must have a `time` dimension; all other dimensions are treated as spatial (e.g. `(time, k, j, i)` or `(time, pose, k, j, i)`). - ##### **`events`** (`pandas.DataFrame or list of pandas.DataFrame`, default: `None` ) – Events table(s) with `onset`, `duration`, and `trial_type` columns. Onsets are in the same world time units as the `time` coordinate of `run_data`. - ##### **`confounds`** (`pandas.DataFrame, numpy.ndarray, xarray.DataArray, or list, optional`, default: `None` ) – Confound regressors per run, `(time, n_confounds)`. A DataFrame must have a `time` column and a DataArray a `time` dimension; their times are validated against `run_data`. A NumPy array is assumed ordered like `run_data` along `time`. A single table is applied to every run, so its times must match every run; pass a list to give each run its own table. - ##### **`design_matrices`** (`pandas.DataFrame or list of pandas.DataFrame`, default: `None` ) – Pre-built design matrices. Overrides `events` / `confounds`. Returns: - `FirstLevelModel` – Fitted estimator (`self`). Raises: - `ValueError` – If neither `events` nor `design_matrices` is provided, if list lengths are inconsistent across arguments, or if runs have different spatial shapes or mismatched spatial coordinates. #### get\_metadata\_routing ```python get_metadata_routing() ``` Get metadata routing of this object. Please check :ref:`User Guide ` on how the routing mechanism works. Returns: - **`routing`** ( `MetadataRequest` ) – A :class:`~sklearn.utils.metadata_routing.MetadataRequest` encapsulating routing information. #### get\_params ```python get_params(deep=True) ``` Get parameters for this estimator. Parameters: - ##### **`deep`** (`bool`, default: `True` ) – If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns: - **`params`** ( `dict` ) – Parameter names mapped to their values. #### set\_params ```python set_params(**params) ``` Set the parameters of this estimator. The method works on simple estimators as well as on nested objects (such as :class:`~sklearn.pipeline.Pipeline`). The latter have parameters of the form `__` so that it's possible to update each component of a nested object. Parameters: - ##### **`**params`** (`dict`, default: `{}` ) – Estimator parameters. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. ### RegressionResults Container for regression model results. Stores fitted parameters, residuals, and provides methods for computing contrasts and statistical tests. This implementation is adapted from \[`nilearn.glm.regression.RegressionResults`\]\[ nilearn.glm.regression.RegressionResults\] and \[`nilearn.glm.model.LikelihoodModelResults`\]\[ nilearn.glm.model.LikelihoodModelResults\]. Parameters: - #### **`theta`** (`(n_regressors, n_voxels) numpy.ndarray`) – Parameter estimates (beta coefficients). - #### **`Y`** (`(n_timepoints, n_voxels) numpy.ndarray`) – Original data. - #### **`model`** (`OLSModel or ARModel`) – Fitted model instance. - #### **`whitened_Y`** (`(n_timepoints, n_voxels) numpy.ndarray`) – Whitened data. - #### **`whitened_residuals`** (`(n_timepoints, n_voxels) numpy.ndarray`) – Whitened residuals. - #### **`dispersion`** (`(n_voxels,) numpy.ndarray`) – Residual variance estimate per voxel. - #### **`cov`** (`(n_regressors, n_regressors) numpy.ndarray`) – Normalized covariance matrix. Attributes: - **`theta`** (`(n_regressors, n_voxels) numpy.ndarray`) – Parameter estimates. - **`cov`** (`(n_regressors, n_regressors) numpy.ndarray`) – Covariance matrix. - **`dispersion`** (`(n_voxels,) numpy.ndarray`) – Variance estimates. - **`df_total`** (`int`) – Total degrees of freedom. - **`df_model`** (`int`) – Model degrees of freedom. - **`df_residuals`** (`int`) – Residual degrees of freedom. Methods: - **`compute_f_contrast`** – Compute *F*-statistic for a multi-dimensional contrast. - **`compute_t_contrast`** – Compute *t*-statistic for a contrast. #### mse ```python mse: NDArray[floating] ``` Mean squared error: `SSE / df_residuals`. Returns: - `(n_voxels,) numpy.ndarray` – Mean squared error per voxel. #### predicted ```python predicted: NDArray[floating] ``` Fitted values: `X @ theta`. Returns: - `(n_timepoints, n_voxels) numpy.ndarray` – Fitted values. Raises: - `RuntimeError` – If the model reference was dropped via `minimize_memory=True`. #### residuals ```python residuals: NDArray[floating] ``` Raw residuals: `Y - X @ theta`. Returns: - `(n_timepoints, n_voxels) numpy.ndarray` – Raw residuals. Raises: - `RuntimeError` – If the heavy fields were stripped via `minimize_memory=True`. #### sse ```python sse: NDArray[float64] ``` Sum of squared errors (RSS). Returns: - `(n_voxels,) numpy.ndarray` – Sum of squared whitened residuals per voxel. Raises: - `RuntimeError` – If `whitened_residuals` was dropped via `minimize_memory=True`. #### compute\_f\_contrast ```python compute_f_contrast( contrast: NDArray[floating], ) -> dict[str, NDArray[floating]] ``` Compute *F*-statistic for a multi-dimensional contrast. Parameters: - ##### **`contrast`** (`(q, n_regressors) numpy.ndarray`) – Contrast matrix with `q` rows (`q > 1` for F-test). Returns: - `dict` – Dictionary with keys: - `"effect"`: Raw contrast effect `c·θ`, shape `(q, n_voxels)`. - `"whitened_effect"`: Effect whitened by `cholesky(invcov)`, shape `(q, n_voxels)`. Designed so that `sum(whitened_effect**2, axis=0) / q / dispersion == F`, which is what `Contrast` consumes downstream. - `"covariance"`: Per-voxel contrast covariance with dispersion, shape `(n_voxels, q, q)`. - `"dispersion"`: Per-voxel residual variance, shape `(n_voxels,)`. - `"F"`: *F*-statistic `(n_voxels,)`. - `"df_num"`: Numerator degrees of freedom (`q`). - `"df_den"`: Denominator degrees of freedom. #### compute\_t\_contrast ```python compute_t_contrast( contrast: NDArray[floating], ) -> dict[str, NDArray[floating]] ``` Compute *t*-statistic for a contrast. Parameters: - ##### **`contrast`** (`(n_regressors,) or (1, n_regressors) numpy.ndarray`) – Contrast vector defining the linear combination of parameters. Returns: - `dict` – Dictionary with keys: - `"effect"`: Contrast effect size (scalar or `(n_voxels,)`). - `"sd"`: Standard error of contrast (scalar or `(n_voxels,)`). - `"t"`: *t*-statistic (scalar or `(n_voxels,)`). - `"df_den"`: Denominator degrees of freedom. ### SecondLevelModel Bases: `BaseEstimator` Second-level GLM estimator for group-level fUSI analysis. Fits a voxel-wise OLS model to a collection of first-level contrast maps (one per subject or session). This is equivalent to a mass-univariate one-sample *t*-test by default, but any linear group-level model can be specified via a design matrix. `second_level_input` accepts either a list of fitted `FirstLevelModel` objects or a list of spatial `xarray.DataArray` maps (e.g. output of `compute_contrast` with `output_type="effect"`). When `FirstLevelModel` objects are passed, `first_level_contrast` must be provided so the effect map can be extracted automatically from each model. This implementation is adapted from \[`nilearn.glm.second_level.SecondLevelModel`\]\[ nilearn.glm.second\_level.SecondLevelModel\]. Attributes: - **`design_matrix_`** (`DataFrame`) – Design matrix used for fitting (set after `fit`). - **`results_`** (`RegressionResults`) – Regression results (set after `fit`). Examples: ```pycon >>> import numpy as np, xarray as xr >>> maps = [ ... xr.DataArray(np.random.randn(2, 3, 4), dims=["z", "y", "x"]) ... for _ in range(10) ... ] >>> model = SecondLevelModel() >>> model.fit(maps) >>> z_map = model.compute_contrast("intercept") ``` Methods: - **`compute_contrast`** – Compute a group-level contrast and return a statistical map. - **`fit`** – Fit the group-level GLM. - **`get_metadata_routing`** – Get metadata routing of this object. - **`get_params`** – Get parameters for this estimator. - **`set_params`** – Set the parameters of this estimator. #### compute\_contrast ```python compute_contrast( second_level_contrast: str | NDArray[floating] = "intercept", stat_type: Literal["t", "F"] | None = None, output_type: Literal[ "zscore", "statistic", "pvalue", "effect", "variance", ] = "zscore", baseline: float = 0.0, ) -> DataArray ``` Compute a group-level contrast and return a statistical map. Parameters: - ##### **`second_level_contrast`** (`str or ndarray`, default: `"intercept"` ) – Contrast definition. A string is parsed as an expression over design-matrix column names (e.g. `"group_A - group_B"`). A 1D array specifies a *t*-contrast vector; a 2D array specifies an *F*-contrast matrix. Defaults to `"intercept"` for a one-sample *t*-test. - ##### **`stat_type`** (`('t', 'F')`, default: `"t"` ) – Force the contrast type. By default inferred from the shape of the contrast (1D → *t*, 2D → *F*). - ##### **`output_type`** (`('zscore', 'statistic', 'pvalue', 'effect', 'variance')`, default: `"zscore"` ) – Which statistical map to return. - ##### **`baseline`** (`float`, default: `0.0` ) – Null-hypothesis value tested against. The statistic is `(effect - baseline) / sqrt(variance)` for *t*-contrasts and `sum((effect - baseline)**2) / dim / variance` for *F*-contrasts. Returns: - `DataArray` – Statistical map with the spatial dimensions of the input maps. Raises: - `ValueError` – If the model has not been fitted or the contrast definition is invalid. #### fit ```python fit( second_level_input: list[DataArray] | list[FirstLevelModel], first_level_contrast: str | NDArray[floating] | None = None, confounds: DataFrame | None = None, design_matrix: DataFrame | None = None, ) -> SecondLevelModel ``` Fit the group-level GLM. Accepts either a list of fitted `FirstLevelModel` objects or a list of spatial contrast maps (`xarray.DataArray` with no `time` dimension). All maps must share the same spatial dimensions and shape. Parameters: - ##### **`second_level_input`** (`list of FirstLevelModel or list of xarray.DataArray`) – Input data. When a list of `FirstLevelModel` is provided, `first_level_contrast` must be given and effect maps are extracted automatically via `compute_contrast`. When a list of `xarray.DataArray` is provided, maps are used directly. - ##### **`first_level_contrast`** (`str or ndarray`, default: `None` ) – Contrast passed to each `FirstLevelModel.compute_contrast` call when `second_level_input` is a list of `FirstLevelModel`. Ignored otherwise. - ##### **`confounds`** (`(n_subjects, n_confounds) pandas.DataFrame`, default: `None` ) – Subject-level confound regressors in the same order as `second_level_input`. Ignored when `design_matrix` is provided. - ##### **`design_matrix`** (`(n_subjects, n_regressors) pandas.DataFrame`, default: `None` ) – Pre-built group-level design matrix. When provided, `confounds` is ignored. Returns: - `SecondLevelModel` – Fitted estimator (`self`). Raises: - `ValueError` – If `second_level_input` is empty, if `FirstLevelModel` objects are passed without `first_level_contrast`, if a map is not valid VoxelData, if maps have inconsistent spatial shapes or mismatched spatial coordinates, or if `design_matrix` row count does not match the number of inputs. - `TypeError` – If `second_level_input` is not a list of `FirstLevelModel` or `xarray.DataArray`. #### get\_metadata\_routing ```python get_metadata_routing() ``` Get metadata routing of this object. Please check :ref:`User Guide ` on how the routing mechanism works. Returns: - **`routing`** ( `MetadataRequest` ) – A :class:`~sklearn.utils.metadata_routing.MetadataRequest` encapsulating routing information. #### get\_params ```python get_params(deep=True) ``` Get parameters for this estimator. Parameters: - ##### **`deep`** (`bool`, default: `True` ) – If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns: - **`params`** ( `dict` ) – Parameter names mapped to their values. #### set\_params ```python set_params(**params) ``` Set the parameters of this estimator. The method works on simple estimators as well as on nested objects (such as :class:`~sklearn.pipeline.Pipeline`). The latter have parameters of the form `__` so that it's possible to update each component of a nested object. Parameters: - ##### **`**params`** (`dict`, default: `{}` ) – Estimator parameters. Returns: - **`self`** ( `estimator instance` ) – Estimator instance. ### claron2021\_hrf ```python claron2021_hrf( dt: float, oversampling: int = 50, time_length: float = 32.0, alpha: float = 2.5, beta: float = 12.7, onset: float = 0.0, ) -> NDArray[floating] ``` Return the Claron et al. (2021) inverse-gamma fUSI HRF preset. HRF model proposed for functional ultrasound imaging of the rodent spinal cord in Claron *et al.* (2021). Parameters: - #### **`dt`** (`float`) – Sampling interval of the original data in seconds. - #### **`oversampling`** (`int`, default: `50` ) – Oversampling factor for the HRF time grid. - #### **`time_length`** (`float`, default: `32.0` ) – Total length of the HRF in seconds. - #### **`alpha`** (`float`, default: `2.5` ) – Shape parameter of the inverse gamma distribution. - #### **`beta`** (`float`, default: `12.7` ) – Scale parameter of the inverse gamma distribution. - #### **`onset`** (`float`, default: `0.0` ) – Onset of the HRF in seconds. Returns: - `(n_samples,) numpy.ndarray` – HRF values on the oversampled time grid, normalized to sum to 1. **References** 1. Claron, J., Hingot, V., Rivals, I., Rahal, L., Couture, O., Deffieux, T., Tanter, M., & Pezet, S. (2021). Large-scale functional ultrasound imaging of the spinal cord reveals in-depth spatiotemporal responses of spinal nociceptive circuits in both normal and inflammatory states. Pain, 162(4), 1047-1059. https://doi.org/10.1097/j.pain.0000000000002078 ### gamma\_difference\_hrf ```python gamma_difference_hrf( dt: float, oversampling: int = 50, time_length: float = 32.0, onset: float = 0.0, delay: float = 6.0, undershoot: float = 16.0, dispersion: float = 1.0, undershoot_dispersion: float = 1.0, ratio: float = 1.0 / 6.0, ) -> NDArray[floating] ``` Return an HRF modeled as the difference of two gamma functions. The general parameterization underlying `glover_hrf` and `spm_hrf`; expose the parameters directly to build custom double-gamma HRFs. Parameters: - #### **`dt`** (`float`) – Sampling interval in seconds. - #### **`oversampling`** (`int`, default: `50` ) – Temporal oversampling factor relative to the acquisition grid. - #### **`time_length`** (`float`, default: `32.0` ) – Duration of the HRF in seconds. - #### **`onset`** (`float`, default: `0.0` ) – Onset of the HRF in seconds. - #### **`delay`** (`float`, default: `6.0` ) – Peak delay of the first gamma in seconds. - #### **`undershoot`** (`float`, default: `16.0` ) – Peak delay of the undershoot gamma in seconds. - #### **`dispersion`** (`float`, default: `1.0` ) – Dispersion of the peak gamma. - #### **`undershoot_dispersion`** (`float`, default: `1.0` ) – Dispersion of the undershoot gamma. - #### **`ratio`** (`float`, default: `1.0/6.0` ) – Ratio of undershoot to peak amplitude. Returns: - `(n_timepoints,) numpy.ndarray` – Normalized HRF sampled on an oversampled time grid. ### gamma\_hrf ```python gamma_hrf( dt: float, oversampling: int = 50, time_length: float = 32.0, peak_delay: float = 5.0, dispersion: float = 1.0, onset: float = 0.0, ) -> NDArray[floating] ``` Return a positive-only gamma HRF on an oversampled time grid. HRF model for functional ultrasound signals that show a single positive lobe without a BOLD-like post-stimulus undershoot. The gamma distribution is parameterized so that its mode occurs `peak_delay` seconds after onset and its scale is `dispersion` seconds. Parameters: - #### **`dt`** (`float`) – Sampling interval of the original data in seconds. - #### **`oversampling`** (`int`, default: `50` ) – Oversampling factor for the HRF time grid. - #### **`time_length`** (`float`, default: `32.0` ) – Total length of the HRF in seconds. - #### **`peak_delay`** (`float`, default: `5.0` ) – Time in seconds at which the gamma HRF peaks. - #### **`dispersion`** (`float`, default: `1.0` ) – Scale parameter of the gamma distribution in seconds. - #### **`onset`** (`float`, default: `0.0` ) – Onset of the HRF in seconds. Returns: - `(n_samples,) numpy.ndarray` – HRF values on the oversampled time grid, normalized to sum to 1. ### glover\_hrf ```python glover_hrf( dt: float, oversampling: int = 50, time_length: float = 32.0, onset: float = 0.0, ) -> NDArray[floating] ``` Return the Glover canonical HRF on an oversampled time grid. Parameters: - #### **`dt`** (`float`) – Sampling interval of the original data in seconds. - #### **`oversampling`** (`int`, default: `50` ) – Oversampling factor for the HRF time grid. - #### **`time_length`** (`float`, default: `32.0` ) – Total length of the HRF in seconds. - #### **`onset`** (`float`, default: `0.0` ) – Onset of the HRF in seconds. Returns: - `(n_samples,) numpy.ndarray` – HRF values on the oversampled time grid, normalized to sum to 1. ### inverse\_gamma\_hrf ```python inverse_gamma_hrf( dt: float, oversampling: int = 50, time_length: float = 32.0, alpha: float = 2.5, beta: float = 12.7, onset: float = 0.0, ) -> NDArray[floating] ``` Return an HRF modeled as an inverse gamma distribution. Continuous-time inverse-gamma HRF whose sampled kernel is normalized to unit mass for GLM convolution. This corresponds to the shape `beta**alpha / gamma(alpha) * t**(-(alpha + 1)) * exp(-beta / t)` for `t > 0`, omitting any overall amplitude prefactor. Parameters: - #### **`dt`** (`float`) – Sampling interval of the original data in seconds. - #### **`oversampling`** (`int`, default: `50` ) – Oversampling factor for the HRF time grid. - #### **`time_length`** (`float`, default: `32.0` ) – Total length of the HRF in seconds. - #### **`alpha`** (`float`, default: `2.5` ) – Shape parameter of the inverse gamma distribution. - #### **`beta`** (`float`, default: `12.7` ) – Scale parameter of the inverse gamma distribution. - #### **`onset`** (`float`, default: `0.0` ) – Onset of the HRF in seconds. Returns: - `(n_samples,) numpy.ndarray` – HRF values on the oversampled time grid, normalized to sum to 1. ### make\_first\_level\_design\_matrix ```python make_first_level_design_matrix( volume_times: NDArray[floating], events: DataFrame | None = None, hrf_model: HrfModelSpec = None, drift_model: DriftModelSpec = "cosine", low_cutoff: float = 0.01, drift_order: int = 1, fir_delays: list[int] | None = None, confounds: NDArray[floating] | DataFrame | DataArray | None = None, confound_names: list[str] | None = None, oversampling: int = 50, min_onset: float = -24.0, uniformity_tolerance: float = 0.01, ) -> DataFrame ``` Create a first-level design matrix from events and confounds. Parameters: - #### **`volume_times`** (`(n_volumes,) numpy.ndarray`) – Acquisition time of each volume in seconds. - #### **`events`** (`DataFrame`, default: `None` ) – Events table with `onset`, `duration`, and `trial_type` columns. - #### **`hrf_model`** (`('glover', 'spm', 'verhoef2025', 'claron2021', 'fir')`, default: `"glover"` ) – Hemodynamic response function model. A callable matching the `HRFModel` protocol (i.e., a function taking `dt` and `oversampling` and returning a 1D array) is invoked to produce a custom HRF kernel. If not provided, no convolution is performed and the design matrix will contain boxcar regressors sampled at the acquisition times. - #### **`drift_model`** (`('cosine', 'polynomial')`, default: `"cosine"` ) – Drift model for low-frequency confounds. - #### **`low_cutoff`** (`float`, default: `0.01` ) – Low cutoff frequency in hertz (used with `drift_model="cosine"`). - #### **`drift_order`** (`int`, default: `1` ) – Polynomial order when `drift_model="polynomial"`. - #### **`fir_delays`** (`list of int`, default: `None` ) – FIR delays in volumes (required when `hrf_model="fir"`). - #### **`confounds`** (`(n_volumes, n_confounds) numpy.ndarray, pandas.DataFrame, or xarray.DataArray`, default: `None` ) – Confound regressors. A DataFrame must have a `time` column and a DataArray a `time` dimension; their times must match `volume_times`. A NumPy array is assumed ordered like `volume_times`; unlike the signal-processing functions, no warning is issued, since `volume_times` is itself a plain array with nothing to verify against. - #### **`confound_names`** (`list of str`, default: `None` ) – Names for confound regressors. Cannot be combined with `confounds` that already carry names (DataFrame columns or non-numeric DataArray coordinates). If not provided, `confound_` names are used. - #### **`oversampling`** (`int`, default: `50` ) – Oversampling factor for HRF convolution. - #### **`min_onset`** (`float`, default: `-24.0` ) – Minimum onset time in seconds for event regressors. - #### **`uniformity_tolerance`** (`float`, default: `1e-2` ) – Maximum allowed per-interval relative deviation from the median consecutive interval in `volume_times`. Raise a `ValueError` if any interval exceeds this threshold. Increase this value to tolerate slight timestamp jitter (e.g. from acquisition clocks). Returns: - `(n_volumes, n_columns) pandas.DataFrame` – Design matrix indexed by `volume_times`. Columns are, in order: one regressor per condition in `events` (or per FIR delay when `hrf_model="fir"`), then any confounds, then the drift regressors and the constant column. ### make\_second\_level\_design\_matrix ```python make_second_level_design_matrix( n_subjects: int, confounds: DataFrame | None = None ) -> DataFrame ``` Build a second-level design matrix. Creates a design matrix for group-level analysis. By default, includes only an intercept column (one-sample *t*-test / group mean). If confounds are provided, they are prepended as additional regressors. Parameters: - #### **`n_subjects`** (`int`) – Number of subjects (rows in the design matrix). - #### **`confounds`** (`(n_subjects, n_confounds) pandas.DataFrame`, default: `None` ) – Subject-level confound regressors. Columns are added before the intercept. Returns: - `(n_subjects, n_confounds + 1) pandas.DataFrame` – Design matrix. Raises: - `ValueError` – If `confounds` has a number of rows different from `n_subjects`. Examples: ```pycon >>> import pandas as pd >>> dm = make_second_level_design_matrix(5) >>> list(dm.columns) ['intercept'] >>> dm.shape (5, 1) ``` ```pycon >>> confounds = pd.DataFrame({"age": [25, 30, 35, 40, 45]}) >>> dm = make_second_level_design_matrix(5, confounds=confounds) >>> list(dm.columns) ['age', 'intercept'] ``` ### spm\_hrf ```python spm_hrf( dt: float, oversampling: int = 50, time_length: float = 32.0, onset: float = 0.0, ) -> NDArray[floating] ``` Return the SPM canonical HRF on an oversampled time grid. Parameters: - #### **`dt`** (`float`) – Sampling interval of the original data in seconds. - #### **`oversampling`** (`int`, default: `50` ) – Oversampling factor for the HRF time grid. - #### **`time_length`** (`float`, default: `32.0` ) – Total length of the HRF in seconds. - #### **`onset`** (`float`, default: `0.0` ) – Onset of the HRF in seconds. Returns: - `(n_samples,) numpy.ndarray` – HRF values on the oversampled time grid, normalized to sum to 1. ### verhoef2025\_hrf ```python verhoef2025_hrf( dt: float, oversampling: int = 50, time_length: float = 32.0, peak_delay: float = 5.0, dispersion: float = 1.0, onset: float = 0.0, ) -> NDArray[floating] ``` Return the Verhoef et al. (2025) single-gamma human fUSI HRF preset. HRF model proposed for human 4D functional ultrasound imaging in Verhoef *et al.* (2025). The preset follows the reported single-gamma HRF, excluding any post-stimulus undershoot. Parameters: - #### **`dt`** (`float`) – Sampling interval of the original data in seconds. - #### **`oversampling`** (`int`, default: `50` ) – Oversampling factor for the HRF time grid. - #### **`time_length`** (`float`, default: `32.0` ) – Total length of the HRF in seconds. - #### **`peak_delay`** (`float`, default: `5.0` ) – Time in seconds at which the HRF peaks. - #### **`dispersion`** (`float`, default: `1.0` ) – Scale parameter of the gamma distribution in seconds. - #### **`onset`** (`float`, default: `0.0` ) – Onset of the HRF in seconds. Returns: - `(n_samples,) numpy.ndarray` – HRF values on the oversampled time grid, normalized to sum to 1. **References** 1. Verhoef, L., Soloukey, S., Springeling, G., Flikweert, A. J., Lippe, B., de Jong, A. J., Radeljic-Jakic, N., Baas, M., Voorneveld, J., Vincent, A. J. P. E., & Kruizinga, P. (2025). Miniaturized Four-Dimensional Functional Ultrasound for Mapping Human Brain Activity. medRxiv. https://doi.org/10.1101/2025.08.19.25332261 # API Reference Complete reference documentation for the ConfUSIus API. Please refer to the [user guide]() for more detailed explanations and usage examples. ## Core Modules - --- Loading and saving fUSI data in various formats, including those from AUTC and EchoFrame systems, as well as general-purpose formats like Zarr and NIfTI. - --- fUSI-BIDS metadata validation, conversion utilities, and coordinate handling for BIDS-compliant fUSI data. - --- Validation of fUSI data compatibility with expected formats in ConfUSIus. - --- Multi-pose consolidation tools for combining fUSI data across acquisition poses. - --- Processing of beamformed IQ signals, clutter filtering, and computation of derived measures such as power Doppler and axial velocity. - --- Rich visualization utilities for fUSI data including static plots and interactive napari viewers. - --- Quality control metrics for assessing fUSI data quality. - --- Motion correction and spatial alignment tools for aligning fUSI data across time points or with anatomical references. - --- Brain atlas integration for anatomical labeling and region-of-interest extraction. - --- Timing utilities for resampling fUSI time series onto uniform time grids. - --- Spatial processing tools for fUSI volumetric data, including Gaussian smoothing. - --- Extract and reconstruct signals from fUSI data using spatial masks. - --- Signal processing tools for fUSI time series including denoising and motion artifact correction. - --- Matrix decomposition algorithms for fUSI time series (e.g. PCA, NMF, ICA). - --- Multivariate pattern decoding for fUSI data, including searchlight mapping. - --- Functional connectivity analysis for fUSI data. - --- Voxel-wise GLM for first- and second-level analysis of fUSI data, including design matrix construction and contrast computation. - --- Multiple-comparison correction and statistical-map thresholding utilities for p-value maps and z-maps. - --- Xarray integration providing DataArray accessors for fUSI data. - --- Fetchers for publicly available fUSI datasets with automatic caching and offline support. # `confusius.io` ## io Loading and saving utilities for fUSI data. Modules: - **`atlas`** – Zarr save/load for atlas Datasets. - **`autc`** – Utilities for loading and converting AUTC DAT files. - **`echoframe`** – Utilities for loading and converting EchoFrame DAT files. - **`loadsave`** – Generic file loading and saving dispatcher. - **`nifti`** – Utilities for loading and saving NIfTI files. - **`scan`** – Utilities for loading Iconeus SCAN files. - **`utils`** – Miscellaneous I/O utility functions. Classes: - **`AUTCDAT`** – Beamformed IQ data stored in AUTC DAT format. - **`AUTCDATsLoader`** – Load a beamformed IQ acquisition from a set of AUTC DAT files. - **`EchoFrameMetadata`** – Metadata extracted from an EchoFrame MAT file. Functions: - **`check_path`** – Resolve full path and check its type. - **`convert_autc_dats_to_zarr`** – Convert AUTC DAT files to Zarr format compatible with Xarray. - **`convert_echoframe_dat_to_zarr`** – Convert an EchoFrame DAT file to Zarr format compatible with Xarray. - **`load`** – Load a VoxelData array from file, dispatching by extension. - **`load_atlas`** – Load an atlas Dataset from a Zarr store. - **`load_bps`** – Load a BPS file and return an affine from Iconeus' brain space to ConfUSIus lab space. - **`load_echoframe_dat`** – Load an EchoFrame DAT file as a lazy, ConfUSIus-ordered DataArray. - **`load_echoframe_metadata`** – Load acquisition metadata from an EchoFrame MAT file. - **`load_nifti`** – Load a NIfTI file as a lazy VoxelData array. - **`load_scan`** – Load an Iconeus SCAN file as a lazy VoxelData array. - **`save`** – Save a VoxelData array to file, dispatching by extension. - **`save_atlas`** – Save an atlas Dataset to a Zarr store, bundling its region meshes. - **`save_nifti`** – Save a VoxelData array to NIfTI format. ### AUTCDAT Beamformed IQ data stored in AUTC DAT format. Parameters: - #### **`path`** (`str or Path`) – Path to the AUTC DAT file. - #### **`n_header_items`** (`int`, default: `10` ) – Number of header items to read from each block. - #### **`header_dtype`** (`dtype_like`, default: `numpy.int32` ) – Data type used for header fields. - #### **`data_dtype`** (`dtype_like`, default: `numpy.complex64` ) – Data type used for data fields. Attributes: - **`path`** (`Path`) – Resolved path to the AUTC DAT file. - **`shape`** (`tuple[int, int, int, int]`) – Shape of the data in the AUTC DAT file in the format `(blocks, x, z, frames)`. - **`n_blocks`** (`int`) – Number of frame blocks in the AUTC DAT file. - **`n_frames_per_block`** (`int`) – Number of frames per block in the AUTC DAT file. - **`acquisition_block_indices`** (`ndarray`) – Array of acquisition block indices contained in the AUTC DAT file. #### acquisition\_block\_indices ```python acquisition_block_indices: NDArray ``` Acquisition-wide frame indices contained in this AUTC DAT file. Returns: - `ndarray` – Array of acquisition block indices. #### n\_blocks ```python n_blocks: int ``` Number of frame blocks in this AUTC DAT file. Returns: - `int` – Number of blocks. #### n\_frames\_per\_block ```python n_frames_per_block: int ``` Number of frames per block in this AUTC DAT file. Returns: - `int` – Number of frames per block. #### shape ```python shape: tuple[int, int, int, int] ``` Shape of the data in this AUTC DAT file. Returns: - `tuple[int, int, int, int]` – Tuple of the form `(blocks, x, z, frames)`. ### AUTCDATsLoader Load a beamformed IQ acquisition from a set of AUTC DAT files. Parameters: - #### **`root`** (`str or Path`) – Path to the root directory containing the AUTC DAT files. - #### **`pattern`** (`str`, default: `"bf*part*.dat"` ) – Glob pattern used to search for AUTC DAT files inside `root`. - #### **`show_progress`** (`bool`, default: `True` ) – Whether to show progress during file indexing. If `False`, no progress bars are displayed. - #### **`progress`** (`Progress`, default: `None` ) – External `rich.progress.Progress` instance to add tasks to. If provided and `show_progress` is `True`, a task will be added to this `rich.progress.Progress` instance instead of creating a new progress bar with `rich.progress.track`. - #### **`track_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments to pass to `rich.progress.track` if using internal progress tracking (only used if `show_progress` is `True` and `progress` is `None`). Attributes: - **`root`** (`Path`) – Resolved path to the root directory. - **`pattern`** (`str`) – Glob pattern used for file searching. - **`dats`** (`dict[Path, AUTCDAT]`) – Dictionary mapping AUTC DAT file paths to `AUTCDAT` instances. - **`block_index_to_file`** (`dict[int, Path]`) – Mapping from block indices to their corresponding file paths. #### blocks ```python blocks: NDArray ``` List of all blocks available in the acquisition. Returns: - `ndarray` – Sorted array of all available block indices. #### dtype ```python dtype: dtype ``` Data type of the arrays in the AUTC DAT files. Returns: - `dtype` – Data type of the stored arrays. #### shape ```python shape: tuple[int, int, int, int] ``` Shape of the complete dataset across all AUTC DAT files. Returns: - `tuple[int, int, int, int]` – Shape in the format `(n_blocks, n_x, n_z, n_frames)`. ### EchoFrameMetadata Bases: `TypedDict` Metadata extracted from an EchoFrame MAT file. Uses fUSI-BIDS compliant field names for consistency with the BIDS specification. Attributes: - **`lateral_coords`** (`ndarray`) – Lateral coordinates in millimeters, cropped if the sequence uses ROI cropping. - **`axial_coords`** (`ndarray`) – Axial (depth) coordinates in millimeters, cropped if the sequence uses ROI cropping. - **`transmit_frequency`** (`float`) – Central frequency of the ultrasound probe in hertz. - **`probe_number_of_elements`** (`int`) – Number of probe transducer elements. - **`probe_pitch`** (`float`) – Inter-element pitch of the probe in millimeters. - **`beamforming_sound_velocity`** (`float`) – Speed of sound in meters per second used during beamforming. - **`plane_wave_angles`** (`ndarray`) – Angles at which tilted plane waves are emitted in degrees. - **`compound_sampling_frequency`** (`float`) – Sampling frequency of the compounded frames in hertz. - **`pulse_repetition_frequency`** (`float`) – Single plane wave pulse repetition frequency in hertz. - **`beamforming_method`** (`str`) – Beamforming method used (e.g. `"DAS"`). - **`n_volumes_per_block`** (`int`) – Number of volumes per acquisition block. ### check\_path ```python check_path( path: Path | str | Any, label: str = "path", type: Literal["file", "dir"] | None = None, ) -> Path ``` Resolve full path and check its type. Parameters: - #### **`path`** (`str or Path`) – Path to check validity. - #### **`label`** (`str`, default: `"path"` ) – Name of the variable passed to `check_path`, used in the error message. - #### **`type`** (`(file, dir)`, default: `"file"` ) – Type of path to check for. If `"file"`, checks that the path is a file. If `"dir"`, checks that the path is a directory. If not provided, no type check is performed, meaning the path may not exist. Returns: - `Path` – If successful, the path resolved to its full path. Raises: - `TypeError` – If `path` cannot be cast to pathlib.Path. - `ValueError` – If type is not `None` and the path is not of the correct type. ### convert\_autc\_dats\_to\_zarr ```python convert_autc_dats_to_zarr( dats_root: str | Path, output_path: str | Path, spacing: Sequence[float], origin: Sequence[float] | None = None, dats_pattern: str = "bf*part*.dat", frames_per_chunk: int | None = None, chunks_per_shard: int | None = None, batch_size: int = 100, overwrite: bool = False, zarr_kwargs: dict[str, Any] | None = None, transmit_frequency: float | None = None, probe_n_elements: int | None = None, probe_pitch: float | None = None, beamforming_sound_velocity: float | None = None, plane_wave_angles: ArrayLike | None = None, compound_sampling_frequency: float | None = None, pulse_repetition_frequency: float | None = None, beamforming_method: str | None = None, skip_first_blocks: int = 0, skip_last_blocks: int = 0, block_times: ArrayLike | None = None, show_progress: bool = True, progress: Optional[Progress] = None, track_kwargs: dict[str, Any] | None = None, ) -> Group ``` Convert AUTC DAT files to Zarr format compatible with Xarray. Beamformed IQ data is converted to a Zarr group with an `iq` array of shape `(time, k, j, i)` chunked along the first dimension. Voxel-to-world geometry is stored as `attrs["voxel_to_world"]` on the `iq` array rather than dense `z`/`y`/`x` coordinate arrays, matching `confusius.io.save`'s Zarr convention. Coordinates are stored as separate Zarr arrays following Xarray conventions, allowing the data to be opened directly with `xarray.open_zarr()`. Parameters: - #### **`dats_root`** (`str or Path`) – Path to the directory containing AUTC DAT files. - #### **`output_path`** (`str or Path`) – Path where the Zarr group will be saved. - #### **`spacing`** (`sequence[float]`) – World voxel spacing in `(k, j, i)` order (elevation, axial/depth, lateral), in millimeters. AUTC's own file data carries no physical calibration for any axis (see the Notes section for the elevation placeholder), so this must always be supplied explicitly. - #### **`origin`** (`sequence[float]`, default: `None` ) – World position of voxel index 0 in `(k, j, i)` order, in millimeters in the probe-relative coordinate system (origin at the center of the probe face). If not provided, defaults to the same probe-centered/surface-referenced convention as `create_voxeldata`: elevation and lateral centered on the probe, depth referenced to the probe surface. - #### **`dats_pattern`** (`str`, default: `"bf*part*.dat"` ) – Glob pattern used to search for AUTC DAT files inside `dats_root`. - #### **`frames_per_chunk`** (`int`, default: `None` ) – Number of frames to include in each Zarr chunk. If not provided, defaults to the number of frames per block in the AUTC DAT files. - #### **`chunks_per_shard`** (`int`, default: `None` ) – Number of chunks to bundle into each shard. If provided, enables Zarr sharding to reduce the number of files on disk. If not provided, sharding is disabled. - #### **`batch_size`** (`int`, default: `100` ) – Number of blocks to process in each batch. - #### **`overwrite`** (`bool`, default: `False` ) – Whether to overwrite existing Zarr group at the output path. - #### **`zarr_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments to pass to `zarr.create_array` for the main data array. - #### **`transmit_frequency`** (`float`, default: `None` ) – Central frequency of the ultrasound probe in hertz. - #### **`probe_n_elements`** (`int`, default: `None` ) – Number of probe transducers. - #### **`probe_pitch`** (`float`, default: `None` ) – Inter-element pitch of the probe in millimeters. - #### **`beamforming_sound_velocity`** (`float`, default: `None` ) – Speed of sound in meters per second. - #### **`plane_wave_angles`** (`array_like`, default: `None` ) – Angles at which tilted plane waves are emitted in degrees. - #### **`compound_sampling_frequency`** (`float`, default: `None` ) – Sampling frequency of the compounded frames in hertz. - #### **`pulse_repetition_frequency`** (`float`, default: `None` ) – Pulse repetition frequency in hertz. - #### **`beamforming_method`** (`str`, default: `None` ) – Beamforming method used (e.g. `"DAS"`). - #### **`skip_first_blocks`** (`int`, default: `0` ) – Number of blocks to skip from the beginning of the acquisition. This is useful when the first blocks are known to be corrupted or unusable. - #### **`skip_last_blocks`** (`int`, default: `0` ) – Number of blocks to skip from the end of the acquisition. This is useful when the last blocks are known to be corrupted or unusable. - #### **`block_times`** (`(n_blocks_after_skip,) array_like`, default: `None` ) – Start time of each IQ block in seconds, for the retained blocks only. If provided, individual volume times will be computed and stored as a time coordinate. Requires `compound_sampling_frequency` to be provided. If not provided, time coordinate will be computed based on `compound_sampling_frequency` or set to frame indices. - #### **`show_progress`** (`bool`, default: `True` ) – Whether to show progress during conversion. If `False`, no progress bars are displayed. - #### **`progress`** (`Progress`, default: `None` ) – External `rich.progress.Progress` instance to add tasks to. If provided and `show_progress` is `True`, a task will be added to this `rich.progress.Progress` instance instead of creating a new progress bar with `rich.progress.track`. - #### **`track_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments to pass to `rich.progress.track` if using internal progress tracking (only used if `show_progress` is `True` and `progress` is `None`). Returns: - `Group` – The created Zarr group containing the `iq` data array and coordinate arrays. Can be opened directly with `xarray.open_zarr()`. Raises: - `ValueError` – If `spacing` or `origin` does not have length 3, or if `block_times` is provided without `compound_sampling_frequency`. > [!NOTE] > > **Notes** > > The output Zarr group follows Xarray conventions and can be opened with:: > > ```text > import xarray as xr > ds = xr.open_zarr("output.zarr") > iq = ds["iq"] > ``` > > `iq` above carries plain `k`/`j`/`i` voxel dims, not VoxelData -- `xarray.open_zarr` doesn't know how to rebuild a `VoxelToWorldIndex` from `attrs["voxel_to_world"]`. Load with `confusius.io.load` instead to get the world coordinates back: > > ```text > import confusius as cf > iq = cf.io.load("output.zarr") > ``` > > Metadata attributes (e.g., `transmit_frequency`, `beamforming_sound_velocity`) are stored on the `iq` DataArray (accessible via `iq.attrs`), consistent with how reduction functions return DataArrays with attributes. > > The group contains: > > - `iq`: The main data array with dimensions `(time, k, j, i)`. Voxel-to-world geometry is stored as `attrs["voxel_to_world"]`/`attrs["voxel_to_world_units"]` rather than dense `z`/`y`/`x` coordinate arrays. > - `time`: Time coordinate array. > - `k`: Elevation voxel coordinate array (always `[0]` for 2D data). > - `j`: Axial (depth) voxel coordinate array. > - `i`: Lateral voxel coordinate array. > > Spatial coordinates (`z`, `y`, `x`) follow the ConfUSIus probe-relative coordinate system: world distances in millimeters along each voxel axis, with the origin at the center of the probe face. Unlike EchoFrame data (where coordinates are embedded in the metadata file), AUTC's own file data carries no physical calibration for any axis, so `spacing` must always be supplied by the caller -- there is no meaningful default. > > Correctly computing elevation (`spacing[0]`) spacing requires the elevation beam width, which itself needs the elevation aperture and elevation focus; that computation isn't implemented here, so pass `spacing[0]` explicitly if that information is known, or an intentional placeholder (e.g. `0.4`) otherwise. ### convert\_echoframe\_dat\_to\_zarr ```python convert_echoframe_dat_to_zarr( dat_path: str | Path, meta_path: str | Path, output_path: str | Path, dat_dtype: DTypeLike = complex64, header_dtype: DTypeLike = uint64, n_header_items: int = 5, volumes_per_chunk: int | None = None, chunks_per_shard: int | None = None, batch_size: int = 100, overwrite: bool = False, zarr_kwargs: dict[str, Any] | None = None, skip_first_blocks: int = 0, skip_last_blocks: int = 0, block_times: ArrayLike | None = None, show_progress: bool = True, progress: Progress | None = None, track_kwargs: dict[str, Any] | None = None, ) -> Group ``` Convert an EchoFrame DAT file to Zarr format compatible with Xarray. Beamformed IQ data is converted to a Zarr group with an `iq` array of shape `(time, k, j, i)` chunked along the first dimension. Voxel-to-world geometry is stored as `attrs["voxel_to_world"]` on the `iq` array rather than dense `z`/`y`/`x` coordinate arrays, matching `confusius.io.save`'s Zarr convention. Coordinates are stored as separate Zarr arrays following Xarray conventions, allowing the data to be opened directly with `xarray.open_zarr()`. Parameters: - #### **`dat_path`** (`str or Path`) – Path to the EchoFrame DAT file containing beamformed IQ data. - #### **`meta_path`** (`str or Path`) – Path to the EchoFrame sequence parameter file (MAT v7.3 / HDF5 format). - #### **`output_path`** (`str or Path`) – Path where the Zarr group will be saved. - #### **`dat_dtype`** (`dtype_like`, default: `numpy.complex64` ) – Data type of the beamformed IQ data in the DAT file. - #### **`header_dtype`** (`dtype_like`, default: `numpy.uint64` ) – Data type of the DAT file header. - #### **`n_header_items`** (`int`, default: `5` ) – Number of items in the DAT file header. - #### **`volumes_per_chunk`** (`int`, default: `None` ) – Number of volumes to include in each Zarr chunk. If not provided, defaults to the number of volumes per block from the raw file. - #### **`chunks_per_shard`** (`int`, default: `None` ) – Number of chunks to bundle into each shard. If provided, enables Zarr v3 sharding to reduce the number of files on disk. If not provided, sharding is disabled. - #### **`batch_size`** (`int`, default: `100` ) – Number of blocks to process in each batch. - #### **`overwrite`** (`bool`, default: `False` ) – Whether to overwrite existing Zarr group at the output path. - #### **`zarr_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments to pass to `zarr.create_array` for the main data array. - #### **`skip_first_blocks`** (`int`, default: `0` ) – Number of blocks to skip from the beginning of the acquisition. This is useful when the first blocks are known to be corrupted or unusable. - #### **`skip_last_blocks`** (`int`, default: `0` ) – Number of blocks to skip from the end of the acquisition. This is useful when the last blocks are known to be corrupted or unusable. - #### **`block_times`** (`(n_blocks_after_skip,) array_like`, default: `None` ) – Start time of each IQ block in seconds, for the retained blocks only. If provided, individual volume times will be computed and stored as a time coordinate. Requires `compound_sampling_frequency` to be provided. If not provided, time coordinate will be computed based on `compound_sampling_frequency` or set to frame indices. - #### **`show_progress`** (`bool`, default: `True` ) – Whether to show progress during conversion. If `False`, no progress bars are displayed. - #### **`progress`** (`Progress`, default: `None` ) – External `rich.progress.Progress` instance to add tasks to. If provided and `show_progress` is `True`, a task will be added to this `rich.progress.Progress` instance instead of creating a new progress bar with `rich.progress.track`. - #### **`track_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments to pass to `rich.progress.track` if using internal progress tracking (only used if `show_progress` is `True` and `progress` is `None`). Returns: - `Group` – The created Zarr group containing the `iq` data array and coordinate arrays. Can be opened directly with `xarray.open_zarr()`. > [!NOTE] > > **Notes** > > The output Zarr group follows Xarray conventions and can be opened with:: > > ```text > import xarray as xr > ds = xr.open_zarr("output.zarr") > iq = ds["iq"] > ``` > > `iq` above carries plain `k`/`j`/`i` voxel dims, not a VoxelData array -- `xarray.open_zarr` doesn't know how to rebuild a `VoxelToWorldIndex` from `attrs["voxel_to_world"]`. Load with `confusius.io.load` instead to get the world coordinates back: > > ```text > import confusius as cf > iq = cf.io.load("output.zarr") > ``` > > Metadata attributes (e.g., `transmit_frequency`, `beamforming_sound_velocity`) are stored on the `iq` DataArray (accessible via `iq.attrs`), consistent with how reduction functions return DataArrays with attributes. > > The group contains: > > - `iq`: The main data array with dimensions `(time, k, j, i)`. Voxel-to-world geometry is stored as `attrs["voxel_to_world"]`/`attrs["voxel_to_world_units"]` rather than dense `z`/`y`/`x` coordinate arrays. > - `time`: Time coordinate array. > - `k`: Elevation voxel coordinate array (always `[0]` for 2D data). > - `j`: Axial (depth) voxel coordinate array. > - `i`: Lateral voxel coordinate array. ### load ```python load( path: str | Path, variable: str | None = None, **kwargs: Any, ) -> DataArray ``` Load a VoxelData array from file, dispatching by extension. Supported formats: - **NIfTI** (`.nii`, `.nii.gz`): loaded via `load_nifti`. - **SCAN** (`.scan`): loaded via `load_scan`. - **EchoFrame DAT** (`.dat`): loaded via `load_echoframe_dat`. If no metadata path is provided, `load_echoframe_dat` looks for `ScanParameters.mat` next to the DAT file. - **Zarr** (`.zarr`): opened via `xarray.open_zarr` and a single variable is extracted. Must be a store previously written by `save` (identified by `attrs["voxel_to_world"]`); for an arbitrary/foreign Zarr store, use `xarray.open_zarr` directly and build a VoxelData array yourself (e.g. via `create_voxeldata`). If `attrs["rgb_lookup"]` is present but `attrs["cmap"]`/`attrs["norm"]` are missing (as happens after a save/load round-trip, since matplotlib colormap/norm objects are not JSON-serializable and are dropped on save), the colormap and normalization are rebuilt from the RGB lookup table so atlas-derived masks and annotations keep their canonical colors after reload. A Zarr-saved VoxelData array stores `attrs["voxel_to_world"]` instead of dense `z`/`y`/`x` coordinate arrays (see `save`); this rebuilds the world coordinates and `VoxelToWorldIndex` from that affine. Parameters: - #### **`path`** (`str or Path`) – Path to the file to load. - #### **`variable`** (`str`, default: `None` ) – Zarr only. Name of the variable to extract as a DataArray. If not provided, the first variable in the dataset is returned. - #### **`**kwargs`** (`Any`, default: `{}` ) – Additional keyword arguments forwarded to the underlying loader. Returns: - `DataArray` – Loaded VoxelData array. Raises: - `ValueError` – If the file extension is not supported, or the Zarr store at `path` wasn't written by `save` (no `attrs["voxel_to_world"]`). > [!NOTE] > > **Notes** > > Gzip-compressed NIfTI files (`.nii.gz`) do not support true random access. Repeated lazy partial reads, such as per-frame loops, may decompress the file again for each independent Dask computation. If the file fits in memory, use `.compute()` for eager processing, or `.persist()` before many downstream Dask operations. For larger repeated random-access workloads, prefer Zarr or uncompressed `.nii`. ### load\_atlas ```python load_atlas(path: str | Path, **kwargs: Any) -> Dataset ``` Load an atlas Dataset from a Zarr store. The serialized structures are rebuilt into a `StructuresDict` in `attrs["structures"]`. The `cmap`/`norm` colormap objects dropped on save are rebuilt into `annotation.attrs` from `rgb_lookup`, and each ROI's `mesh_filename` is re-pointed at the meshes bundled under the store's `meshes/` subdirectory, so `get_meshes` works on the loaded atlas without the BrainGlobe cache. See the warning on `save_atlas` for meshes that were not bundled. Parameters: - #### **`path`** (`str or Path`) – Path to the Zarr store; must end with the `.zarr` suffix. - #### **`**kwargs`** (`Any`, default: `{}` ) – Additional keyword arguments forwarded to `xarray.open_zarr`. Returns: - `Dataset` – The loaded atlas Dataset, with `attrs["structures"]` rebuilt into a `StructuresDict`, `attrs["world_to_base"]` restored to a numpy affine or displacement-field DataArray, `cmap`/`norm` restored on `annotation.attrs`, and `mesh_filename` paths pointing at the bundled meshes. Raises: - `ValueError` – If `path` does not end with the `.zarr` suffix. ### load\_bps ```python load_bps(bps_path: str | Path) -> NDArray[float64] ``` Load a BPS file and return an affine from Iconeus' brain space to ConfUSIus lab space. BPS files are HDF5 sidecars produced by Iconeus' brain positioning system. They store a `BrainToLab` affine that maps Iconeus brain coordinates `(x_brain, y_brain, z_brain, 1)` to Iconeus lab coordinates `(x_lab, y_lab, z_lab, 1)` in meters. The Iconeus lab frame is a fixed scanner frame; `probeToLab` carries any rotation of the probe within it. To compose this affine with the rest of the ConfUSIus pipeline we re-express the lab side as **ConfUSIus-ordered** lab space `(z_lab, y_lab, x_lab)` in millimeters. The brain side is left in its original axis order (the brain coordinate units are not declared by the BPS format and are therefore not converted). The change of basis from ConfUSIus-ordered millimeter lab coordinates to Iconeus-ordered meter lab coordinates is ```text confusius_lab_to_iconeus_lab = mm_to_m @ WORLD_TO_PROBE_PERMUTATION ``` `WORLD_TO_PROBE_PERMUTATION` permutes the axes from ConfUSIus order `(z, y, x)` to probe / Iconeus-lab order `(x, y, z)`, and `mm_to_m = diag(1e-3, 1e-3, 1e-3, 1)` rescales the translation column. The returned affine is then ```text brain_to_lab = inv(confusius_lab_to_iconeus_lab) @ BrainToLab ``` Parameters: - #### **`bps_path`** (`str or Path`) – Path to the BPS file (`.bps`). Returns: - `(4, 4) numpy.ndarray` – Affine mapping Iconeus brain coordinates to ConfUSIus-ordered Iconeus lab coordinates `(z_lab, y_lab, x_lab, 1)` in millimeters. ### load\_echoframe\_dat ```python load_echoframe_dat( dat_path: str | Path, meta_path: str | Path | None = None, dat_dtype: DTypeLike = complex64, header_dtype: DTypeLike = uint64, n_header_items: int = 5, ) -> DataArray ``` Load an EchoFrame DAT file as a lazy, ConfUSIus-ordered DataArray. Beamformed IQ data is loaded as a VoxelData array with voxel dimensions `(time, k, j, i)` and a voxel-to-world index deriving world coordinates `z`, `y`, `x`. Coordinates and acquisition metadata (e.g. `transmit_frequency`, `beamforming_sound_velocity`) are attached from the EchoFrame sequence parameter file. The `time` coordinate is computed from frame indices and the compound sampling frequency; callers with acquisition timestamps for each block should override it (e.g. via `data.assign_coords(time=...)`). Each chunk reopens a read-only memory map on its worker; in-place processing requires a copy. Parameters: - #### **`dat_path`** (`str or Path`) – Path to the EchoFrame DAT file containing beamformed IQ data. - #### **`meta_path`** (`str or Path`, default: `None` ) – Path to the EchoFrame sequence parameter file (MAT v7.3 / HDF5 format). If not provided, looks for `ScanParameters.mat` next to `dat_path`, matching the filename EchoFrame's storage writer uses by convention. - #### **`dat_dtype`** (`dtype_like`, default: `numpy.complex64` ) – Data type of the beamformed IQ data in the DAT file. - #### **`header_dtype`** (`dtype_like`, default: `numpy.uint64` ) – Data type of the DAT file header. - #### **`n_header_items`** (`int`, default: `5` ) – Number of items in the DAT file header. Returns: - `DataArray` – Lazy VoxelData array with dimensions `(time, k, j, i)`, where `k` is a singleton elevation dimension. Data is wrapped in a Dask array, chunked so that each chunk corresponds to one acquisition block's volumes; individual blocks remain accessible via `data.isel(time=slice(i * n, (i + 1) * n))`, where `n` is `data.attrs["n_volumes_per_block"]`. Raises: - `ValueError` – If `meta_path` is not provided and no `ScanParameters.mat` file exists next to `dat_path`. > [!NOTE] > > **Notes** > > Acquisition metadata is stored in `da.attrs`, including `n_volumes_per_block` (number of volumes per acquisition block). ### load\_echoframe\_metadata ```python load_echoframe_metadata( meta_path: str | Path, ) -> EchoFrameMetadata ``` Load acquisition metadata from an EchoFrame MAT file. Parameters: - #### **`meta_path`** (`str or Path`) – Path to the EchoFrame sequence parameter file (MAT v7.3 / HDF5 format). Returns: - `EchoFrameMATMetadata` – Dictionary containing the extracted metadata fields. Raises: - `FileNotFoundError` – If `meta_path` does not exist or is not a file. ### load\_nifti ```python load_nifti( path: str | Path, chunks: int | tuple[int, ...] | str | None = "auto", *, coordinate_affine: Literal[ "auto", "sform", "qform" ] = "auto", ) -> DataArray ``` Load a NIfTI file as a lazy VoxelData array. Loads NIfTI files using nibabel's proxy arrays for memory-efficient access, wrapping the data in Dask arrays for chunked, parallel processing. The data is transposed to ConfUSIus conventions with voxel-space dimensions `(time, k, j, i)` and derived world coordinates `z`, `y`, `x`. A BIDS-style JSON sidecar file is loaded automatically when present. For files inside a BIDS dataset, matching sidecars are inherited from the dataset root to the recording folder before conversion to ConfUSIus attributes. Parameters: - #### **`path`** (`str or Path`) – Path to the NIfTI file (`.nii` or `.nii.gz`). - #### **`chunks`** (`int or tuple[int, ...] or str or None`, default: `"auto"` ) – How to chunk the array. Must be one of the following forms. - A blocksize like `1000`. - A blockshape like `(1000, 1000)`. - Explicit sizes of all blocks along all dimensions like `((1000, 1000, 500), (400, 400))`. - A size in bytes, like `"100 MiB"` which will choose a uniform block-like shape. - The word `"auto"` to let Dask choose chunk sizes based on heuristics. See `dask.array.normalize_chunks` for more details on how chunk sizes are determined. - `-1` or `None` as a blocksize indicate the size of the corresponding dimension. - #### **`coordinate_affine`** (`(auto, sform, qform)`, default: `"auto"` ) – Header affine to use as the primary coordinate-defining geometry. - `"auto"` prefers sform when both sform and qform are valid, and falls back to qform when only qform is valid. - `"sform"` forces the sform to define the in-memory coordinate geometry. - `"qform"` forces the qform to define the in-memory coordinate geometry. The non-selected valid header affine is still preserved in `data_array.attrs["affines"]` as a world-to-world transform. Returns: - `DataArray` – Lazy VoxelData array with voxel-space dimensions in ConfUSIus order (`k`, `j`, `i` plus optional `time`) and world coordinates `z`, `y`, `x`. Data is wrapped in a Dask array for out-of-core computation. > [!NOTE] > > **Notes** > > In memory, the coordinate-defining geometry uses native voxel dimensions `k`, `j`, `i` and derived world coordinates `z`, `y`, `x`. `coordinate_affine` controls which NIfTI header affine defines that voxel-to-world mapping. > > Gzip-compressed `.nii.gz` files do not support true random access. Repeated lazy partial reads, such as per-frame loops, may decompress the file again for each independent Dask computation. If the file fits in memory, use `.compute()` for ordinary eager processing, or `.persist()` before many downstream Dask operations that should stay chunked and parallel. For larger repeated random-access workloads, prefer Zarr or uncompressed `.nii`. > > World-to-world affines are stored in `da.attrs["affines"]`, a dict keyed by affine name. Each value is a 4×4 affine in ConfUSIus `(z, y, x)` convention that maps **world coordinates** (as stored in `da.coords`) to world-space coordinates. Apply as `da.attrs["affines"]["world_to_sform"] @ np.array([pz, py, px, 1.0])` to get `[wz, wy, wx, 1]`, where `pz`, `py`, `px` come from `da.coords["z"]`, `da.coords["y"]`, `da.coords["x"]` respectively. > > Unlike the NIfTI affine (which maps voxel *indices* to world space), the `world_to_*` affines are invariant to any slicing or downsampling because they operate on world positions, not grid indices. > > With `coordinate_affine="auto"`, affine selection follows NIfTI conventions: > > - If `sform_code > 0`: sform is used as the primary affine; an identity `"world_to_sform"` entry is written. When `qform_code > 0` as well, a `"world_to_qform"` entry is also stored as secondary. > - Else, if only `qform_code > 0`: qform is used as the primary affine; only an identity `"world_to_qform"` entry is written. > > The primary form's identity entry is what keeps that form recoverable after `apply` re-anchors the world frame elsewhere: it is re-expressed along with the other entries, so `save_nifti` can still write the original header form. - If both codes are zero: a warning is emitted, coordinates are built from `pixdim` only (origin 0, step = voxel size), and no `"affines"` entry is stored in `da.attrs`. > > Choosing `coordinate_affine="sform"` or `"qform"` *forces* that header affine: unlike `"auto"`, there is no fallback to the other form or to `pixdim` when the requested affine is not valid -- `load_nifti` raises `ValueError` instead. > > Processing-window durations and strides in JSON (including ConfUSIus-prefixed B-mode and axial-velocity durations) are converted from milliseconds to the loaded time coordinate's units. Without a time coordinate or declared time units, seconds are assumed. > > The raw integer form codes are stored as `da.attrs["qform_code"]` and `da.attrs["sform_code"]` (only when \> 0) so that a save/load roundtrip can reproduce the original NIfTI header codes. Examples: ```pycon >>> import confusius as cf >>> da = cf.io.load_nifti("brain.nii.gz") >>> print(da.dims) ("time", "k", "j", "i") >>> da = cf.io.load_nifti("brain.nii.gz", coordinate_affine="qform") ``` ### load\_scan ```python load_scan( path: str | Path, bps_path: str | Path | None = None, chunks: int | tuple[int, ...] | str | None = "auto", ) -> DataArray ``` Load an Iconeus SCAN file as a lazy VoxelData array. SCAN files (`.scan`) come in two on-disk formats, both handled here: - **v1**: an HDF5 container produced by IcoScan/NeuroScan, holding power Doppler data and spatial/temporal metadata for 2D, 3D, or 3D+t fUSI volumes. The returned DataArray wraps an open `h5py` handle via a Dask array; keep it in scope (or call `.compute()`) before the handle is garbage-collected. - **v2**: a flat binary file (variable-length header + little-endian `float64` payload). Each Dask chunk opens its own read-only memory map and, when a `bps_path` is given, the returned DataArray includes `world_to_brain`. `load_scan` sniffs the format automatically and dispatches accordingly. Parameters: - #### **`path`** (`str or Path`) – Path to the SCAN file (`.scan`). - #### **`bps_path`** (`str or Path`, default: `None` ) – Path to the corresponding BPS file (`.bps`). If provided, a `world_to_brain` affine is added to `da.attrs["affines"]`. - #### **`chunks`** (`int or tuple[int, ...] or str or None`, default: `"auto"` ) – Dask chunk specification. Accepted forms: - A blocksize like `1000`. - A blockshape like `(1000, 1000)`. - Explicit sizes of all blocks like `((1000, 1000, 500), (400, 400))`. - A size in bytes like `"100 MiB"`. - `"auto"` to let Dask choose based on heuristics. - `-1` or `None` for the full dimension size (no chunking). Returns: - `DataArray` – Lazy VoxelData array with dimensions and coordinates: - v1 `2Dscan` → `(time, k, j, i)`. - v1 `3Dscan` → `(pose, k, j, i)`. - v1 `4Dscan`/`4DscanCustom` → `(time, pose, k, j, i)`. - v2 single-pose → `(time, k, j, i)`. - v2 multi-pose → `(time, pose, k, j, i)`. - v2 dim6 payloads prepend a `dim6` dimension. World coordinates `z`, `y`, `x` are in millimeters. The `time` coordinate is in seconds. For v1/v2 multi-pose time series, `time` is pose-dependent (`(time, pose)`-shaped), holding each pose's own acquisition timestamps directly. Raises: - `ValueError` – If `path` does not exist or is not a file, if the file is neither an HDF5-based SCAN (v1) nor a binary SCAN v2 file, if a v2 file's `probe_to_lab` affine cannot be built, or if a v1 `acquisitionMode` is not one of `"2Dscan"`, `"3Dscan"`, `"4Dscan"`, or `"4DscanCustom"`. > [!NOTE] > > **Notes** > > **v2.** The binary layout follows PyIconeus: header fields, measured times, one 6DOF probe pose per `pose`, acquisition/provenance metadata, then a Fortran-ordered `(size_x, size_y, size_z, time, pose, dim6)` payload. The loader exposes this as VoxelData order `(..., time, pose, k, j, i)` and keeps dim6 as an extra leading dimension when present. Mapped chunks are read-only; in-place chunk-processing functions must copy them first. > > Acquisition settings that correspond to fUSI-BIDS fields are also surfaced as attributes, in native header units: `probe_model`, `probe_center_frequency` (MHz), `probe_pitch` (mm), `probe_aperture` (mm), `probe_focal_depth` (mm), `imaging_depth` (mm start/end), `transmit_frequency` (MHz), `pulse_repetition_frequency` (Hz), `plane_wave_angles` (deg), `svd_low_cutoff`, and `power_doppler_integration_duration` (s). > > ConfUSIus world coordinates `(z, y, x)` for SCAN data are **ConfUSIus-ordered Iconeus lab coordinates** (mm): a fixed scanner frame shared by every pose, used as the canonical world frame because it is the one frame that is physically meaningful across poses. For multi-pose files, `da`'s voxel-to-world geometry is itself pose-dependent (a `(npose, 4, 4)` affine stack, one affine per `da.coords["pose"]` label); world selection therefore requires reducing `pose` to a scalar first, e.g. `da.isel(pose=0).sel(z=..., y=..., x=...)` or `da.isel(pose=0)`. > > If `bps_path` is provided, a `world_to_brain` affine is stored in `da.attrs["affines"]["world_to_brain"]` that maps ConfUSIus world coordinates (already lab space, as above) to Iconeus' brain coordinates. Apply as `da.attrs["affines"]["world_to_brain"] @ np.array([z, y, x, 1.0])`. > > Provenance attributes are stored in `da.attrs`: BIDS-compatible fields (`device_serial_number`, `software_version`) and Iconeus-specific fields (`iconeus_scan_mode`, `iconeus_subject`, `iconeus_session`, `iconeus_scan`, `iconeus_project`, `iconeus_datetime`). `iconeus_datetime` is the acquisition date and time in ISO 8601 format, e.g. `"2021-10-22T14:14:12+00:00"`. SCAN v1 stores it without a time zone, so ConfUSIus treats it as UTC. SCAN v2 files also provide `iconeus_hardware`. ### save ```python save( data_array: DataArray, path: str | Path, **kwargs: Any ) -> None ``` Save a VoxelData array to file, dispatching by extension. Supported formats: - **NIfTI** (`.nii`, `.nii.gz`): saved via `save_nifti`. - **Zarr** (`.zarr`): saved via `xarray.DataArray.to_zarr`. Voxel-to-world geometry is stored as `attrs["voxel_to_world"]` rather than dense `z`/`y`/`x` coordinate arrays, since those are cheaply derived from the affine on load and, for oblique geometry, would otherwise duplicate a full dense array per axis. Parameters: - #### **`data_array`** (`DataArray`) – VoxelData array to save. - #### **`path`** (`str or Path`) – Output path. The extension determines the format. - #### **`**kwargs`** (`Any`, default: `{}` ) – Additional keyword arguments forwarded to the underlying saver. Raises: - `ValueError` – If the file extension is not supported. ### save\_atlas ```python save_atlas( ds: Dataset, path: str | Path, **kwargs: Any ) -> None ``` Save an atlas Dataset to a Zarr store, bundling its region meshes. The in-memory `attrs["structures"]` `StructuresDict` is serialized to a flat JSON list for storage. Meshes already downloaded into the BrainGlobe cache are copied into a `meshes/` subdirectory of the store (as plain sibling files) and their `mesh_filename` is stored as a basename, so a loaded atlas can render them without the BrainGlobe cache. The non-serializable `cmap`/`norm` matplotlib objects in `annotation.attrs` are stripped before writing (the caller's in-memory Dataset is left untouched); `load_atlas` rebuilds the `StructuresDict`, the `cmap`/`norm` from `rgb_lookup`, and re-points the bundled mesh paths on load. > [!WARNING] > > **Undownloaded meshes are not bundled** > > A region whose mesh was never accessed before `save_atlas` keeps its partial BrainGlobe-cache path instead of being bundled. `get_meshes` on the loaded atlas can still trigger BrainGlobe's lazy download from that path. Parameters: - #### **`ds`** (`Dataset`) – Atlas Dataset to save. - #### **`path`** (`str or Path`) – Output Zarr store path; must end with the `.zarr` suffix. Must be a filesystem path (the meshes are written as sibling files); zarr store objects are not supported. - #### **`**kwargs`** (`Any`, default: `{}` ) – Additional keyword arguments forwarded to `xarray.Dataset.to_zarr`. Returns: - `None` – The Dataset is written to `path`. Raises: - `ValueError` – If `path` does not end with the `.zarr` suffix, or if `ds` is not a well-formed atlas Dataset (see `validate_atlas`). - `TypeError` – If `ds` is not a well-formed atlas Dataset (see `validate_atlas`). ### save\_nifti ```python save_nifti( data_array: DataArray, path: str | Path, nifti_version: NiftiVersion = 1, *, qform: str | None = None, sform: str | None = None, qform_code: int | None = None, sform_code: int | None = None, ) -> None ``` Save a VoxelData array to NIfTI format. Saves the DataArray to a NIfTI file and always writes a BIDS-style JSON sidecar alongside it. The data is transposed to NIfTI convention `(x, y, z, time)` before saving. Parameters: - #### **`data_array`** (`DataArray`) – VoxelData array to save. - #### **`path`** (`str or Path`) – Output path for the NIfTI file, with `.nii` or `.nii.gz` extension. If `.nii.gz` is used, the file will be saved in compressed format. - #### **`nifti_version`** (`(1, 2)`, default: `1` ) – NIfTI format version to use. Version 2 is a simple extension to support larger files and arrays with dimension sizes greater than 32,767. - #### **`qform`** (`str`, default: `None` ) – Key in `data_array.attrs["affines"]` to write into the NIfTI qform. When not provided, `"world_to_qform"` is used if present; otherwise qform falls back to the coordinate-defining voxel geometry (`voxel_to_world`) directly. If the coordinate-defining affine contains shear, qform writing is disabled because the NIfTI qform cannot represent shear. In that case, the geometry is written to sform instead. - #### **`sform`** (`str`, default: `None` ) – Key in `data_array.attrs["affines"]` to write into the NIfTI sform. When not provided, `"world_to_sform"` is used if present; otherwise sform also falls back to `voxel_to_world` directly (like qform), unless disabled via `sform_code=0` or `attrs["sform_code"] = 0`. - #### **`qform_code`** (`int`, default: `None` ) – NIfTI qform code to write. When provided, takes precedence over `data_array.attrs["qform_code"]`. When not provided, the value from `attrs["qform_code"]` is used if present; otherwise defaults to `2` (`NIFTI_XFORM_ALIGNED_ANAT`). - #### **`sform_code`** (`int`, default: `None` ) – NIfTI sform code to write. When provided, takes precedence over `data_array.attrs["sform_code"]`. When not provided, the value from `attrs["sform_code"]` is used if present; otherwise defaults to `2` (`NIFTI_XFORM_ALIGNED_ANAT`). > [!NOTE] > > **Notes** > > Time coordinates are automatically converted to seconds for BIDS compliance. If the time coordinate has a "units" attribute, values are converted from "ms" or "us" to "s". If no units are specified, seconds are assumed. Known time-valued processing metadata stored in `data_array.attrs` uses the time coordinate's units internally and is converted to milliseconds in JSON, including ConfUSIus-prefixed B-mode and axial-velocity integration durations. Missing time units default to seconds. > > A warning is issued if spatial dimensions `(x, y, z)` have inconsistent units, as NIfTI only supports a single spatial unit in the `xyzt_units` header field. Examples: ```pycon >>> import confusius as cf >>> import numpy as np >>> da = cf.xarray.create_voxeldata( ... np.random.rand(10, 1, 32, 64), ... dims=("time", "k", "j", "i"), ... dt=0.5, ... spacing=(0.4, 0.1, 0.1), ... ) >>> cf.io.save_nifti(da, "output.nii.gz") >>> da.attrs["affines"] = {"world_to_template": np.eye(4)} >>> cf.io.save_nifti(da, "output.nii.gz", sform="world_to_template") ``` # `confusius.iq` ## iq Processing beamformed IQ data. Modules: - **`clutter_filters`** – Utilities for clutter filtering of beamformed IQ data. - **`process`** – Utilities for processing beamformed IQ data. Functions: - **`clutter_filter_butterworth`** – Filter IQ data using a Butterworth digital filter. - **`clutter_filter_sosfiltfilt`** – Filter IQ data using second-order sections (SOS) digital filter. - **`clutter_filter_svd_from_cumulative_energy`** – Filter IQ data using SVD clutter filtering based on cumulative component energies. - **`clutter_filter_svd_from_energy`** – Filter IQ data using SVD clutter filtering based on component energies. - **`clutter_filter_svd_from_indices`** – Filter IQ data using SVD clutter filtering based on component indices. - **`compute_axial_velocity_volume`** – Compute axial velocity volumes from beamformed IQ data. - **`compute_bmode_volume`** – Compute a B-mode volume from beamformed IQ data. - **`compute_power_doppler_volume`** – Compute power Doppler volumes from beamformed IQ data. - **`compute_processed_volume_timings`** – Compute timings from processing input IQ volumes with nested sliding windows. - **`compute_svd_cumulative_energy_threshold`** – Compute the high cutoff threshold for the cumulative-energy SVD clutter filter. - **`process_iq_block_with_clutter_filter`** – Process a block of IQ data into new volumes using clutter filtering. - **`process_iq_blocks`** – Process blocks of IQ data using sliding windows. - **`process_iq_to_axial_velocity`** – Process blocks of beamformed IQ into axial velocity volumes using sliding windows. - **`process_iq_to_bmode`** – Process blocks of beamformed IQ into B-mode volumes using sliding windows. - **`process_iq_to_power_doppler`** – Process blocks of beamformed IQ into power Doppler volumes using sliding windows. ### clutter\_filter\_butterworth ```python clutter_filter_butterworth( block: NDArray, fs: float, low_cutoff: float | None = None, high_cutoff: float | None = None, order: int = 4, ) -> NDArray ``` Filter IQ data using a Butterworth digital filter. Applies a Butterworth infinite impulse response (IIR) filter using forward-backward filtering (`scipy.signal.sosfiltfilt`) to eliminate phase distortion. Supports low-pass, high-pass, and band-pass filtering based on the cutoff frequency parameters. Parameters: - #### **`block`** (`(time, k, j, i) numpy.ndarray`) – Complex beamformed IQ data, where `time` is the temporal dimension and `(k, j, i)` are spatial dimensions. - #### **`fs`** (`float`) – Sampling frequency in hertz. - #### **`low_cutoff`** (`float`, default: `None` ) – Low cutoff frequency in hertz, in range `(0, fs/2)`. If provided, applies high-pass filtering above this frequency. - #### **`high_cutoff`** (`float`, default: `None` ) – High cutoff frequency in hertz, in range `(0, fs/2)`. If provided, applies low-pass filtering below this frequency. - #### **`order`** (`int`, default: `4` ) – Filter order. Due to forward-backward filtering, the effective order is doubled. Returns: - `(time, k, j, i) numpy.ndarray` – Filtered IQ data. Raises: - `ValueError` – If `block` is not 4D, if cutoff frequencies are invalid, or if both `low_cutoff` and `high_cutoff` are `None`. ### clutter\_filter\_sosfiltfilt ```python clutter_filter_sosfiltfilt( block: NDArray, sos: NDArray ) -> NDArray ``` Filter IQ data using second-order sections (SOS) digital filter. Applies a digital filter defined by second-order sections using forward-backward filtering to eliminate phase distortion. This is a general-purpose filtering function that accepts pre-computed SOS coefficients from any SciPy filter design. Parameters: - #### **`block`** (`(time, k, j, i) numpy.ndarray`) – Complex beamformed IQ data, where `time` is the temporal dimension and `(k, j, i)` are spatial dimensions. - #### **`sos`** (`(sections, 6) numpy.ndarray`) – Second-order sections filter coefficients, typically obtained from SciPy functions like `scipy.signal.butter`, `scipy.signal.cheby1`, etc. Returns: - `(time, k, j, i) numpy.ndarray` – Filtered IQ data. Raises: - `ValueError` – If `block` is not 4D. > [!NOTE] > > **Notes** > > Forward-backward filtering (`scipy.signal.sosfiltfilt`) ensures zero phase delay by filtering the signal twice: once forward and once backward. ### clutter\_filter\_svd\_from\_cumulative\_energy ```python clutter_filter_svd_from_cumulative_energy( block: NDArray, mask: NDArray | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, ) -> NDArray ``` Filter IQ data using SVD clutter filtering based on cumulative component energies. This function performs singular value decomposition (SVD) on masked IQ signals and removes clutter by regressing out singular vectors whose cumulative energies fall outside the specified range. This approach allows filtering based on the total energy contribution of components, useful for retaining a specific percentage of total signal energy. > [!WARNING] > > `block` will be cast to double-precision floating point to avoid numerical instabilities. Parameters: - #### **`block`** (`(time, k, j, i) numpy.ndarray`) – Complex beamformed IQ data, where `time` is the temporal dimension and `(k, j, i)` are spatial dimensions. - #### **`mask`** (`(k, j, i) numpy.ndarray`, default: `None` ) – Boolean mask. SVD is computed only from masked voxels. If not provided, all voxels are used. - #### **`low_cutoff`** (`int or float`, default: `None` ) – Lower bound for cumulative energy to retain (inclusive). Components with cumulative energy lower than `low_cutoff` are treated as clutter and removed. `low_cutoff` must be positive. If not provided, defaults to `0.0` (no low-energy removal). - #### **`high_cutoff`** (`int or float`, default: `None` ) – Upper bound for cumulative energy to retain (inclusive). Components with cumulative energy greater than `high_cutoff` are treated as clutter and removed. If not provided, defaults to `numpy.inf` (no high-energy removal). Returns: - `(time, k, j, i) numpy.ndarray` – Filtered IQ data. Raises: - `ValueError` – If `block` is not 4D, or if cutoff values are invalid. > [!NOTE] > > **Notes** > > Cumulative energy is computed as the running sum of singular values squared. This allows filtering based on the total energy contribution rather than individual component energies. > > For efficiency, this function computes the eigendecomposition of the temporal Gram matrix rather than full SVD of the data matrix, avoiding computation of the large spatial covariance matrix. **References** 1. Demene, Charlie, et al. "Spatiotemporal Clutter Filtering of Ultrafast Ultrasound Data Highly Increases Doppler and fUltrasound Sensitivity." IEEE Transactions on Medical Imaging, vol. 34, no. 11, Nov. 2015, pp. 2271–85. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2015.2428634](). 2. Baranger, Jerome, et al. "Adaptive Spatiotemporal SVD Clutter Filtering for Ultrafast Doppler Imaging Using Similarity of Spatial Singular Vectors." IEEE Transactions on Medical Imaging, vol. 37, no. 7, July 2018, pp. 1574–86. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2018.2789499](). 3. Le Meur-Diebolt, Samuel, et al. "Robust Functional Ultrasound Imaging in the Awake and Behaving Brain: A Systematic Framework for Motion Artifact Removal." 17 June 2025. Neuroscience, [https://doi.org/10.1101/2025.06.16.659882](). ### clutter\_filter\_svd\_from\_energy ```python clutter_filter_svd_from_energy( block: NDArray, mask: NDArray | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, ) -> NDArray ``` Filter IQ data using SVD clutter filtering based on component energies. This function performs singular value decomposition (SVD) on masked IQ signals and removes clutter by regressing out singular vectors whose energies fall outside the specified range. This is an adaptive filtering approach that identifies clutter based on signal energy rather than component indices. > [!WARNING] > > `block` will be cast to double-precision floating point to avoid numerical instabilities. Parameters: - #### **`block`** (`(time, k, j, i) numpy.ndarray`) – Complex beamformed IQ data, where `time` is the temporal dimension and `(k, j, i)` are spatial dimensions. - #### **`mask`** (`(k, j, i) numpy.ndarray`, default: `None` ) – Boolean mask. SVD is computed only from masked voxels. If not provided, all voxels are used. - #### **`low_cutoff`** (`int or float`, default: `None` ) – Lower bound for singular vector energy to retain (inclusive). Vectors with energy less than `low_cutoff` are treated as clutter and removed. `low_cutoff` must be positive. If not provided, defaults to `0.0` (no low-energy removal). - #### **`high_cutoff`** (`int or float`, default: `None` ) – Upper bound for singular vector energy to retain (inclusive). Vectors with energy greater than `high_cutoff` are treated as clutter and removed. If not provided, defaults to `numpy.inf` (no high-energy removal). Returns: - `(time, k, j, i) numpy.ndarray` – Filtered IQ data. Raises: - `ValueError` – If `block` is not 4D, or if cutoff values are invalid. > [!NOTE] > > **Notes** > > For efficiency, this function computes the eigendecomposition of the temporal Gram matrix rather than full SVD of the data matrix, avoiding computation of the large spatial covariance matrix. **References** 1. Demene, Charlie, et al. "Spatiotemporal Clutter Filtering of Ultrafast Ultrasound Data Highly Increases Doppler and fUltrasound Sensitivity." IEEE Transactions on Medical Imaging, vol. 34, no. 11, Nov. 2015, pp. 2271–85. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2015.2428634](). 2. Baranger, Jerome, et al. "Adaptive Spatiotemporal SVD Clutter Filtering for Ultrafast Doppler Imaging Using Similarity of Spatial Singular Vectors." IEEE Transactions on Medical Imaging, vol. 37, no. 7, July 2018, pp. 1574–86. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2018.2789499](). 3. Le Meur-Diebolt, Samuel, et al. "Robust Functional Ultrasound Imaging in the Awake and Behaving Brain: A Systematic Framework for Motion Artifact Removal." 17 June 2025. Neuroscience, [https://doi.org/10.1101/2025.06.16.659882](). ### clutter\_filter\_svd\_from\_indices ```python clutter_filter_svd_from_indices( block: NDArray, mask: NDArray | None = None, low_cutoff: int | None = None, high_cutoff: int | None = None, ) -> NDArray ``` Filter IQ data using SVD clutter filtering based on component indices. This function performs singular value decomposition (SVD) on masked IQ signals and removes clutter by regressing out singular vectors outside the provided index range. Singular vectors are ordered by decreasing energy, so lower indices correspond to higher-energy components (typically tissue clutter). > [!WARNING] > > `block` will be cast to double-precision floating point to avoid numerical instabilities. Parameters: - #### **`block`** (`(time, k, j, i) numpy.ndarray`) – Complex beamformed IQ data, where `time` is the temporal dimension and `(k, j, i)` are spatial dimensions. - #### **`mask`** (`(z, y x) numpy.ndarray`, default: `None` ) – Boolean mask. SVD is computed only from masked voxels. If not provided, all voxels are used. - #### **`low_cutoff`** (`int`, default: `None` ) – Lower bound for singular vector indices to retain (inclusive), counted from the highest-energy component (index 0). Vectors with indices less than `low_cutoff` are treated as clutter and removed. If not provided, defaults to `0` (no high-energy removal). - #### **`high_cutoff`** (`int`, default: `None` ) – Upper bound for singular vector indices to retain (exclusive), counted from the highest-energy component (index 0). Vectors with indices greater than or equal to `high_cutoff` are treated as clutter and removed. `high_cutoff` must be at most `min(time, mask.sum())`. If not provided, defaults to the maximum number of components (no low-energy removal). Returns: - `(time, k, j, i) numpy.ndarray` – Filtered IQ data. Raises: - `ValueError` – If `block` is not 4D, or if cutoff values are invalid. > [!NOTE] > > **Notes** > > For efficiency, this function computes the eigendecomposition of the temporal Gram matrix rather than full SVD of the data matrix, avoiding computation of the large spatial covariance matrix. **References** 1. Demene, Charlie, et al. "Spatiotemporal Clutter Filtering of Ultrafast Ultrasound Data Highly Increases Doppler and fUltrasound Sensitivity." IEEE Transactions on Medical Imaging, vol. 34, no. 11, Nov. 2015, pp. 2271–85. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2015.2428634](). 2. Baranger, Jerome, et al. "Adaptive Spatiotemporal SVD Clutter Filtering for Ultrafast Doppler Imaging Using Similarity of Spatial Singular Vectors." IEEE Transactions on Medical Imaging, vol. 37, no. 7, July 2018, pp. 1574–86. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2018.2789499](). 3. Le Meur-Diebolt, Samuel, et al. "Robust Functional Ultrasound Imaging in the Awake and Behaving Brain: A Systematic Framework for Motion Artifact Removal." 17 June 2025. Neuroscience, [https://doi.org/10.1101/2025.06.16.659882](). ### compute\_axial\_velocity\_volume ```python compute_axial_velocity_volume( block: NDArray, fs: float, filter_method: Literal[ "svd_indices", "svd_energy", "svd_cumulative_energy", "butterworth", ] = "svd_indices", clutter_mask: NDArray | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, butterworth_order: int = 4, velocity_window_width: int | None = None, velocity_window_stride: int | None = None, lag: int = 1, spatial_kernel: int | tuple[int, int, int] | list[int] = 3, transmit_frequency: float = 15625000.0, beamforming_sound_velocity: float = 1540, ) -> NDArray ``` Compute axial velocity volumes from beamformed IQ data. This function computes axial blood flow velocity volumes by first applying clutter filtering to remove tissue signals, then estimating velocity using the Kasai autocorrelation method within sliding temporal windows. Axial velocity imaging measures blood flow velocity along the ultrasound beam direction. Parameters: - #### **`block`** (`(time, k, j, i) numpy.ndarray`) – Complex beamformed IQ data, where `time` is the temporal dimension and `(k, j, i)` are spatial dimensions. - #### **`fs`** (`float`) – Volume sampling frequency in hertz. - #### **`filter_method`** (`(svd_indices, svd_energy, svd_cumulative_energy, butterworth)`, default: `"svd_indices"` ) – Clutter filtering method to apply before velocity computation. - `"svd_indices"`: Static SVD filter using singular vector indices - `"svd_energy"`: Adaptive SVD filter using singular vector energies - `"svd_cumulative_energy"`: Adaptive SVD filter using cumulative energies - `"butterworth"`: Butterworth frequency-domain filter - #### **`clutter_mask`** (`(k, j, i) numpy.ndarray`, default: `None` ) – Boolean mask to define clutter regions. Only used by SVD-based clutter filters to compute clutter vectors from masked voxels. If not provided, all voxels are used. - #### **`low_cutoff`** (`int or float`, default: `None` ) – Low cutoff of the clutter filter. See `filter_method` for details. If not provided, the lower bound of the range is used. - #### **`high_cutoff`** (`int or float`, default: `None` ) – High cutoff of the clutter filter. See `filter_method` for details. If not provided, the upper bound of the range is used. - #### **`butterworth_order`** (`int`, default: `4` ) – Order of Butterworth filter. Effective order is doubled due to forward-backward filtering. - #### **`velocity_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for velocity estimation, in volumes. If not provided, uses all available volumes. - #### **`velocity_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window, in volumes. If not provided, equals `velocity_window_width`. - #### **`lag`** (`int`, default: `1` ) – Temporal lag in volumes for autocorrelation computation. Must be positive. - #### **`spatial_kernel`** (`int or tuple[int, int, int] or list[int]`, default: `3` ) – Size of the median filter kernel applied spatially to denoise. A scalar uses the same kernel size on all spatial axes; a length-3 sequence specifies `(k, j, i)` sizes directly. Values must be positive. Any even sizes are rounded up to the next odd size. If all sizes are `1`, no spatial filtering is applied. - #### **`transmit_frequency`** (`float`, default: `15.625e6` ) – Ultrasound transmit frequency in hertz. - #### **`beamforming_sound_velocity`** (`float`, default: `1540` ) – Speed of sound assumed during beamforming, in meters per second. Returns: - `(windows, z, y, x) numpy.ndarray` – Axial velocity volumes, where `windows` is the number of temporal sliding windows and `(k, j, i)` are spatial dimensions. Velocity values are in meters per second. > [!NOTE] > > **Notes** > > The Kasai estimator computes velocity from the phase shift of the autocorrelation function between IQ volumes separated by `lag` volumes. ### compute\_bmode\_volume ```python compute_bmode_volume(block: NDArray, **_: Any) -> NDArray ``` Compute a B-mode volume from beamformed IQ data. This function computes a B-mode volume by averaging the magnitude of the IQ data across the temporal dimension. Unlike power Doppler, no clutter filtering is applied and the magnitude (not squared magnitude) is averaged. Parameters: - #### **`block`** (`(time, k, j, i) numpy.ndarray`) – Complex beamformed IQ data, where `time` is the temporal dimension and `(k, j, i)` are spatial dimensions. - #### **`**_`** (`Any`, default: `{}` ) – Additional unused keyword arguments (absorbed and ignored). Returns: - `(1, z, y, x) numpy.ndarray` – B-mode volume, computed as the mean magnitude of the IQ data across time. ### compute\_power\_doppler\_volume ```python compute_power_doppler_volume( block: NDArray, filter_method: Literal[ "svd_indices", "svd_energy", "svd_cumulative_energy", "butterworth", ] = "svd_indices", clutter_mask: NDArray | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, fs: float | None = None, butterworth_order: int = 4, doppler_window_width: int | None = None, doppler_window_stride: int | None = None, ) -> NDArray ``` Compute power Doppler volumes from beamformed IQ data. This function computes power Doppler volumes by first applying clutter filtering to remove tissue clutter, then averaging the squared magnitude of the filtered IQ data within sliding temporal windows. Parameters: - #### **`block`** (`(time, k, j, i) numpy.ndarray`) – Complex beamformed IQ data, where `time` is the temporal dimension and `(k, j, i)` are spatial dimensions. - #### **`filter_method`** (`(svd_indices, svd_energy, svd_cumulative_energy, butterworth)`, default: `"svd_indices"` ) – Clutter filtering method to apply before power Doppler computation. - `"svd_indices"`: Static SVD filter using singular vector indices. - `"svd_energy"`: Adaptive SVD filter using singular vector energies. - `"svd_cumulative_energy"`: Adaptive SVD filter using cumulative energies. - `"butterworth"`: Butterworth frequency-domain filter. - #### **`clutter_mask`** (`(k, j, i) numpy.ndarray`, default: `None` ) – Boolean mask to define clutter regions. Only used by SVD-based clutter filters to compute clutter vectors from masked voxels. If not provided, all voxels are used. - #### **`low_cutoff`** (`int or float`, default: `None` ) – Low cutoff of the clutter filter. See `filter_method` for details. If not provided, the lower bound of the range is used. - #### **`high_cutoff`** (`int or float`, default: `None` ) – High cutoff of the clutter filter. See `filter_method` for details. If not provided, the upper bound of the range is used. - #### **`fs`** (`float`, default: `None` ) – When using the Butterworth clutter filter, sampling frequency in hertz. - #### **`butterworth_order`** (`int`, default: `4` ) – Order of Butterworth filter. Effective order is doubled due to forward-backward filtering. - #### **`doppler_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for power Doppler integration, in volumes. If not provided, uses all available volumes. - #### **`doppler_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window, in volumes. If not provided, equals `doppler_window_width`. Returns: - `(windows, z, y, x) numpy.ndarray` – Power Doppler volumes, where `windows` is the number of temporal sliding windows and `(k, j, i)` are spatial dimensions. ### compute\_processed\_volume\_timings ```python compute_processed_volume_timings( iq: DataArray, clutter_window_width: int, clutter_window_stride: int, inner_window_width: int, inner_window_stride: int, processed_time_reference: VolumeAcquisitionReference | None = None, ) -> tuple[NDArray[floating], NDArray[floating]] ``` Compute timings from processing input IQ volumes with nested sliding windows. The IQ processing functions `process_iq_to_power_doppler` and `process_iq_to_axial_velocity` use nested sliding windows: 1. **Outer windows** (clutter filtering): Defined by `clutter_window_width` and `clutter_window_stride`. 2. **Inner windows** (Doppler/velocity): Within each outer window, defined by `inner_window_width` and `inner_window_stride`. This function computes timings and durations for the output volumes corresponding to the inner windows, based on the input IQ timing metadata and the windowing parameters. Parameters: - #### **`iq`** (`DataArray`) – Input IQ data. Timing information is read from `iq.coords["time"]` and its attributes. - #### **`clutter_window_width`** (`int`) – Width of the outer (clutter filtering) window, in volumes. - #### **`clutter_window_stride`** (`int`) – Stride of the outer window, in volumes. - #### **`inner_window_width`** (`int`) – Width of the inner (Doppler/velocity) window, in volumes. - #### **`inner_window_stride`** (`int`) – Stride of the inner window, in volumes. - #### **`processed_time_reference`** (`(start, center, end)`, default: `"start"` ) – Which point of the output window bin to use as the output timings: - `"start"`: Start of the first volume's bin (onset). - `"center"`: Midpoint of the full window bin. - `"end"`: End of the last volume's bin. If not provided, the input `volume_acquisition_reference` is reused. Returns: - **`output_timings`** ( `ndarray` ) – Timings for each output volume, in the same units as `iq.coords["time"]`. - **`output_durations`** ( `ndarray` ) – Duration of each output window, in the same units as `iq.coords["time"]`. Raises: - `ValueError` – If `processed_time_reference` is not one of `"start"`, `"center"`, or `"end"`. Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> from confusius.iq import compute_processed_volume_timings >>> from confusius.xarray import create_voxeldata >>> # 100 volumes at 10 Hz, volume_duration = 0.1 s >>> iq = create_voxeldata( ... np.ones((100, 1, 1, 1), dtype=np.complex128), ... dims=("time", "k", "j", "i"), ... time=xr.DataArray( ... np.arange(100) * 0.1, ... dims=("time",), ... attrs={ ... "units": "s", ... "volume_acquisition_duration": 0.1, ... "volume_acquisition_reference": "start", ... }, ... ), ... spacing=(1.0, 1.0, 1.0), ... origin=(0.0, 0.0, 0.0), ... ) >>> output_timings, output_durations = compute_processed_volume_timings( ... iq, ... clutter_window_width=50, ... clutter_window_stride=50, ... inner_window_width=25, ... inner_window_stride=25, ... processed_time_reference="center", ... ) >>> output_timings array([1.25, 3.75, 6.25, 8.75]) >>> output_durations array([2.5, 2.5, 2.5, 2.5]) ``` ### compute\_svd\_cumulative\_energy\_threshold ```python compute_svd_cumulative_energy_threshold( iq: DataArray, singular_value_index: int, clutter_mask: DataArray | None = None, window_width: int | None = None, window_stride: int | None = None, ) -> Array ``` Compute the high cutoff threshold for the cumulative-energy SVD clutter filter. Iterates over sliding temporal windows of an IQ acquisition and computes the cumulative eigenvalue spectrum for each window. The threshold is defined as the minimum cumulative energy at a given singular-value index across all windows, ensuring that the chosen cutoff does not over-filter any window. The result is returned as a lazy scalar `dask.array.Array`. Call `.compute` on it to obtain the concrete `float` value when needed. Parameters: - #### **`iq`** (`(time, k, j, i) xarray.DataArray`) – Complex beamformed IQ data, where `time` is the temporal dimension and `(k, j, i)` are native voxel spatial dimensions. - #### **`singular_value_index`** (`int`) – Number of high-energy components to remove. Must satisfy `1 <= singular_value_index <= window_width - 1`. - #### **`clutter_mask`** (`(k, j, i) xarray.DataArray`, default: `None` ) – Boolean spatial mask. Eigendecomposition is computed only from masked voxels. If not provided, all voxels are used. - #### **`window_width`** (`int`, default: `None` ) – Width of the sliding temporal window, in volumes. If not provided, defaults to the total number of volumes. - #### **`window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window, in volumes. If not provided, defaults to `window_width`. Returns: - `Array` – Lazy scalar array containing the minimum cumulative energy at `singular_value_index` across all sliding windows. Call `.compute()` to materialise the value. It can be passed directly as `high_cutoff` to `clutter_filter_svd_from_cumulative_energy` after computing. Raises: - `ValueError` – If `singular_value_index` is less than 1 or greater than `window_width - 1`. > [!NOTE] > > **Notes** > > For efficiency, this function computes the eigendecomposition of the temporal Gram matrix rather than full SVD of the data matrix, avoiding computation of the large spatial covariance matrix. > > Eigenvalues are sorted in ascending order, so the cumulative sum accumulates from the lowest-energy (noise/blood) component to the highest-energy (tissue/clutter) component. The threshold is set to the cumulative energy just *below* the top `singular_value_index` components, so that when used as `high_cutoff` in `clutter_filter_svd_from_cumulative_energy`, exactly those `singular_value_index` high-energy (tissue/clutter) components have cumulative energy that exceeds the threshold and are removed. **References** 1. Demene, Charlie, et al. "Spatiotemporal Clutter Filtering of Ultrafast Ultrasound Data Highly Increases Doppler and fUltrasound Sensitivity." IEEE Transactions on Medical Imaging, vol. 34, no. 11, Nov. 2015, pp. 2271–85. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2015.2428634](). 2. Baranger, Jerome, et al. "Adaptive Spatiotemporal SVD Clutter Filtering for Ultrafast Doppler Imaging Using Similarity of Spatial Singular Vectors." IEEE Transactions on Medical Imaging, vol. 37, no. 7, July 2018, pp. 1574–86. DOI.org (Crossref), [https://doi.org/10.1109/TMI.2018.2789499](). 3. Le Meur-Diebolt, Samuel, et al. "Robust Functional Ultrasound Imaging in the Awake and Behaving Brain: A Systematic Framework for Motion Artifact Removal." 17 June 2025. Neuroscience, [https://doi.org/10.1101/2025.06.16.659882](). ### process\_iq\_block\_with\_clutter\_filter ```python process_iq_block_with_clutter_filter( block: NDArray, process_block_func: Callable[ Concatenate[NDArray, ...], NDArray ], clutter_mask: NDArray | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, window_width: int | None = None, window_stride: int | None = None, filter_method: Literal[ "svd_indices", "svd_energy", "svd_cumulative_energy", "butterworth", ] = "svd_indices", fs: float | None = None, butterworth_order: int = 4, **kwargs: Any, ) -> NDArray ``` Process a block of IQ data into new volumes using clutter filtering. Four clutter filters are available (see `filter_method`). After clutter filtering, reduction is performed using a sliding window across volumes and a user-provided processing function (`process_block_func`). Parameters: - #### **`block`** (`(time, k, j, i) numpy.ndarray`) – Array of complex IQ data. - #### **`process_block_func`** (`callable`) – Function used to process a set of IQ volumes into a single volume. `process_block_func` must accept a `(time, k, j, i)` array of complex IQ data as first argument and return a `(k, j, i)` array corresponding to the processed volume. `process_block_func` may accept additional arguments provided via `kwargs`, but must ignore any extra keyword arguments. Note that `fs` will be passed to `process_block_func`. - #### **`clutter_mask`** (`(k, j, i) numpy.ndarray`, default: `None` ) – Boolean mask to define clutter regions. Only used by SVD-based clutter filters to compute clutter vectors from masked voxels. If not provided, all voxels are used. - #### **`low_cutoff`** (`int or float`, default: `None` ) – Low cutoff of the clutter filter. See `filter_method` for details. If not provided, the lower bound of the range is used. - #### **`high_cutoff`** (`int or float`, default: `None` ) – High cutoff of the clutter filter. See `filter_method` for details. If not provided, the upper bound of the range is used. - #### **`window_width`** (`int`, default: `None` ) – Width of the sliding window. If not provided, the window width will be set to the number of IQ volumes. - #### **`window_stride`** (`int`, default: `None` ) – Stride of the sliding window. If not provided, `window_stride` will be set to `window_width`. - #### **`filter_method`** (`(svd_indices, svd_energy, svd_cumulative_energy, butterworth)`, default: `"svd_indices"` ) – `"svd_indices"`: By default, `filter_method` is `"svd_indices"` to use a static SVD clutter filter based on singular vector indices. Assuming singular vectors in decreasing singular value order, singular vectors with indices outside `[low_cutoff, high_cutoff[` are regressed out. `"svd_energy"`: The `"svd_energy"` clutter filter is an adaptive SVD clutter filter based on singular vector energy. Singular vectors with energy outside `[low_cutoff, high_cutoff]` are regressed out. `"svd_cumulative_energy"`: The `"svd_cumulative_energy"` clutter filter is an adaptive SVD clutter filter based on singular vector cumulative energy. Singular vectors with cumulative energy outside `[low_cutoff, high_cutoff]` are regressed out. `"butterworth"`: The `"butterworth"` clutter filter uses a Butterworth low-pass, high-pass, or band-pass filter. - #### **`fs`** (`float`, default: `None` ) – When using the Butterworth clutter filter, the sampling frequency, in hertz. - #### **`butterworth_order`** (`int`, default: `4` ) – When using the Butterworth clutter filter, the order of the filter. - #### **`**kwargs`** (`Any`, default: `{}` ) – Additional keyword arguments passed to `process_block_func`. Returns: - `(windows, z, y, x) numpy.ndarray` – The computed volumes, with `windows` the number of sliding windows used. Raises: - `ValueError` – If `filter_method` is `"butterworth"` and `fs` is not provided, if `filter_method` is `"svd_indices"` and `low_cutoff`/`high_cutoff` are not both integers or `None`, or if `filter_method` is not one of `"svd_indices"`, `"svd_energy"`, `"svd_cumulative_energy"`, or `"butterworth"`. ### process\_iq\_blocks ```python process_iq_blocks( iq: Array, process_func: Callable[ Concatenate[NDArray, ...], NDArray ], window_width: int | None = None, window_stride: int | None = None, drop_axis: int | tuple[int, ...] | None = None, new_axis: int | tuple[int, ...] | None = None, **kwargs: Any, ) -> Array ``` Process blocks of IQ data using sliding windows. This function applies a processing operation to IQ data using Dask. It uses `dask.array.map_blocks` for non-overlapping windows. When temporal overlap is required, it batches multiple sliding windows into each task, applies explicit overlap between batches, and runs `process_func` on each window within the batch. > [!WARNING] > > Depending on the window width and stride, some input volumes may be dropped if they do not fit into a complete window. Parameters: - #### **`iq`** (`(time, k, j, i) dask.array.Array`) – Dask array of complex IQ data. - #### **`process_func`** (`callable`) – Function to apply to each temporal window. It must accept a `(window_volumes, z, y, x)` array as first argument and return a `(output_volumes, ...)` array. - #### **`window_width`** (`int`, default: `None` ) – Width of the sliding temporal window, in volumes. If not provided, uses the chunk size along the first dimension. - #### **`window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window, in volumes. Must be less than or equal to `window_width`. If not provided, equals `window_width`. - #### **`drop_axis`** (`int or tuple[int, ...]`, default: `None` ) – Axes dropped by `process_func`. - #### **`new_axis`** (`int or tuple[int, ...]`, default: `None` ) – New axes added by `process_func`. - #### **`**kwargs`** (`Any`, default: `{}` ) – Additional keyword arguments passed to `process_func`. Returns: - `Array` – Processed array. Raises: - `ValueError` – If `window_stride` is greater than `window_width`. ### process\_iq\_to\_axial\_velocity ```python process_iq_to_axial_velocity( iq: DataArray, clutter_window_width: int | None = None, clutter_window_stride: int | None = None, filter_method: Literal[ "svd_indices", "svd_energy", "svd_cumulative_energy", "butterworth", ] = "svd_indices", clutter_mask: DataArray | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, butterworth_order: int = 4, velocity_window_width: int | None = None, velocity_window_stride: int | None = None, lag: int = 1, spatial_kernel: int | tuple[int, int, int] | list[int] = 3, ) -> DataArray ``` Process blocks of beamformed IQ into axial velocity volumes using sliding windows. This function computes axial velocity volumes from blocks of beamformed IQ data using nested sliding windows. A first sliding window is used for clutter filtering. Inside each clutter-filtered window, axial velocity volumes are computed using a second sliding window. See the notes section for an illustration of the nested sliding window approach. Parameters: - #### **`iq`** (`DataArray`) – VoxelData array containing complex beamformed IQ data with dimensions `(time, k, j, i)`, where `time` is the temporal dimension and `(k, j, i)` are native voxel spatial dimensions. The DataArray must have the following attributes: - `transmit_frequency`: Ultrasound transmit frequency in hertz. - `beamforming_sound_velocity`: Speed of sound assumed during beamforming in meters per second. - #### **`clutter_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for clutter filtering, in volumes. If not provided, uses the chunk size of the IQ data along the temporal dimension. - #### **`clutter_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window for clutter filtering, in volumes. If not provided, equals `clutter_window_width`. - #### **`filter_method`** (`(svd_indices, svd_energy, svd_cumulative_energy, butterworth)`, default: `"svd_indices"` ) – Clutter filtering method to apply before velocity computation. - `"svd_indices"`: Static SVD filter using singular vector indices. - `"svd_energy"`: Adaptive SVD filter using singular vector energies. - `"svd_cumulative_energy"`: Adaptive SVD filter using cumulative energies. - `"butterworth"`: Butterworth frequency-domain filter. - #### **`clutter_mask`** (`(k, j, i) xarray.DataArray`, default: `None` ) – Boolean mask to define clutter regions. Only used by SVD-based clutter filters to compute clutter vectors from masked voxels. If not provided, all voxels are used. The mask spatial dimensions and coordinates must match the IQ data. - #### **`low_cutoff`** (`int or float`, default: `None` ) – Low cutoff of the clutter filter. See `filter_method` for details. If not provided, the lower bound of the range is used. - #### **`high_cutoff`** (`int or float`, default: `None` ) – High cutoff of the clutter filter. See `filter_method` for details. If not provided, the upper bound of the range is used. - #### **`butterworth_order`** (`int`, default: `4` ) – Order of Butterworth filter. Effective order is doubled due to forward-backward filtering. - #### **`velocity_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for velocity estimation, in volumes. If not provided, equals `clutter_window_width`. - #### **`velocity_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window for velocity estimation, in volumes. If not provided, equals `velocity_window_width`. - #### **`lag`** (`int`, default: `1` ) – Temporal lag in volumes for autocorrelation computation. Must be positive. - #### **`spatial_kernel`** (`int or tuple[int, int, int] or list[int]`, default: `3` ) – Size of the median filter kernel applied spatially to denoise. A scalar uses the same kernel size on all spatial axes; a length-3 sequence specifies `(k, j, i)` sizes directly. Values must be positive. Any even sizes are rounded up to the next odd size. If all sizes are `1`, no spatial filtering is applied. Returns: - `(clutter_windows * velocity_windows, k, j, i) xarray.DataArray` – Axial velocity volumes as a VoxelData array with updated time coordinates and `iq`'s voxel-to-world geometry carried over, where `clutter_windows` is the number of clutter filter sliding windows and `velocity_windows` is the number of velocity sliding windows per clutter window. Velocity values are in meters per second. > [!NOTE] > > **Notes** > > The nested sliding window approach can be visualized as follows:: > > ```text > Input IQ volumes (temporal dimension): > [0][1][2][3][4][5][6][7][8][9][10][11] > > ┌─────────────────────────────────────────────────────────────────────┐ > │ OUTER WINDOWS: Clutter filtering (width=6, stride=3) │ > └─────────────────────────────────────────────────────────────────────┘ > > Window 1: [0][1][2][3][4][5][6][7][8][9][10][11] > [════════════════] > │ > ├─ Clutter filter applied > │ > └─ INNER WINDOWS: Axial velocity (width=3, stride=2) > ┌───────────────────┐ > │[0][1][2] ──> AV output 1 > │ [2][3][4] ──> AV output 2 > │ [4][5] ──> (incomplete, dropped) > └───────────────────┘ > > Window 2: [0][1][2][3][4][5][6][7][8][9][10][11] > [════════════════] > │ > ├─ Clutter filter applied > │ > └─ INNER WINDOWS: Axial velocity (width=3, stride=2) > ┌───────────────────┐ > │[3][4][5] ──> AV output 3 > │ [5][6][7] ──> AV output 4 > │ [7][8] ──> (incomplete, dropped) > └───────────────────┘ > > Window 3: [0][1][2][3][4][5][6][7][8][9][10][11] > [══════════════════] > │ > └─ (...) > ``` ### process\_iq\_to\_bmode ```python process_iq_to_bmode( iq: DataArray, bmode_window_width: int | None = None, bmode_window_stride: int | None = None, ) -> DataArray ``` Process blocks of beamformed IQ into B-mode volumes using sliding windows. This function computes B-mode volumes from beamformed IQ data using a single sliding temporal window. Unlike power Doppler, no clutter filtering is applied; the mean magnitude (not squared magnitude) of the IQ data within each window is computed. Parameters: - #### **`iq`** (`DataArray`) – VoxelData array containing complex beamformed IQ data with dimensions `(time, k, j, i)`, where `time` is the temporal dimension and `(k, j, i)` are native voxel spatial dimensions. - #### **`bmode_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for B-mode integration, in volumes. If not provided, uses the chunk size of the IQ data along the temporal dimension. - #### **`bmode_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window, in volumes. If not provided, equals `bmode_window_width`. Returns: - `(windows, k, j, i) xarray.DataArray` – B-mode volumes as a VoxelData array with updated time coordinates and `iq`'s voxel-to-world geometry carried over, where `windows` is the number of sliding windows. ### process\_iq\_to\_power\_doppler ```python process_iq_to_power_doppler( iq: DataArray, clutter_window_width: int | None = None, clutter_window_stride: int | None = None, filter_method: Literal[ "svd_indices", "svd_energy", "svd_cumulative_energy", "butterworth", ] = "svd_indices", clutter_mask: DataArray | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, butterworth_order: int = 4, doppler_window_width: int | None = None, doppler_window_stride: int | None = None, ) -> DataArray ``` Process blocks of beamformed IQ into power Doppler volumes using sliding windows. This function computes power Doppler volumes from blocks of beamformed IQ data using nested sliding windows. A first sliding window is used for clutter filtering. Inside each clutter-filtered window, power Doppler volumes are computed using a second sliding window. See the notes section for an illustration of the nested sliding window approach. Parameters: - #### **`iq`** (`DataArray`) – VoxelData array containing complex beamformed IQ data with dimensions `(time, k, j, i)`, where `time` is the temporal dimension and `(k, j, i)` are native voxel spatial dimensions. - #### **`clutter_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for clutter filtering, in volumes. If not provided, uses the chunk size of the IQ data along the temporal dimension. - #### **`clutter_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window for clutter filtering, in volumes. If not provided, equals `clutter_window_width`. - #### **`filter_method`** (`(svd_indices, svd_energy, svd_cumulative_energy, butterworth)`, default: `"svd_indices"` ) – Clutter filtering method to apply before power Doppler computation. - `"svd_indices"`: Static SVD filter using singular vector indices. - `"svd_energy"`: Adaptive SVD filter using singular vector energies. - `"svd_cumulative_energy"`: Adaptive SVD filter using cumulative energies. - `"butterworth"`: Butterworth frequency-domain filter. - #### **`clutter_mask`** (`(k, j, i) xarray.DataArray`, default: `None` ) – Boolean mask to define clutter regions. Only used by SVD-based clutter filters to compute clutter vectors from masked voxels. If not provided, all voxels are used. The mask spatial dimensions and coordinates must match the IQ data. - #### **`low_cutoff`** (`int or float`, default: `None` ) – Low cutoff of the clutter filter. See `filter_method` for details. If not provided, the lower bound of the range is used. - #### **`high_cutoff`** (`int or float`, default: `None` ) – High cutoff of the clutter filter. See `filter_method` for details. If not provided, the upper bound of the range is used. - #### **`butterworth_order`** (`int`, default: `4` ) – Order of Butterworth filter. Effective order is doubled due to forward-backward filtering. - #### **`doppler_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for power Doppler integration, in volumes. If not provided, equals `clutter_window_width`. - #### **`doppler_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window for power Doppler integration, in volumes. If not provided, equals `doppler_window_width`. Returns: - `(clutter_windows * doppler_windows, k, j, i) xarray.DataArray` – Power Doppler volumes as a VoxelData array with updated time coordinates and `iq`'s voxel-to-world geometry carried over, where `clutter_windows` is the number of clutter filter sliding windows and `doppler_windows` is the number of power Doppler sliding windows per clutter window. > [!NOTE] > > **Notes** > > The nested sliding window approach can be visualized as follows:: > > ```text > Input IQ volumes (temporal dimension): > [0][1][2][3][4][5][6][7][8][9][10][11] > > ┌─────────────────────────────────────────────────────────────────────┐ > │ OUTER WINDOWS: Clutter Filtering (width=6, stride=3) │ > └─────────────────────────────────────────────────────────────────────┘ > > Window 1: [0][1][2][3][4][5][6][7][8][9][10][11] > [════════════════] > │ > ├─ Clutter filter applied > │ > └─ INNER WINDOWS: Power Doppler (width=3, stride=2) > ┌───────────────────┐ > │[0][1][2] ──> PD output 1 > │ [2][3][4] ──> PD output 2 > │ [4][5] ──> (incomplete, dropped) > └───────────────────┘ > > Window 2: [0][1][2][3][4][5][6][7][8][9][10][11] > [════════════════] > │ > ├─ Clutter filter applied > │ > └─ INNER WINDOWS: Power Doppler (width=3, stride=2) > ┌───────────────────┐ > │[3][4][5] ──> PD output 3 > │ [5][6][7] ──> PD output 4 > │ [7][8] ──> (incomplete, dropped) > └───────────────────┘ > > Window 3: [0][1][2][3][4][5][6][7][8][9][10][11] > [══════════════════] > │ > └─ (...) > ``` # `confusius.multipose` ## multipose Multi-pose data processing utilities. This module provides functions for processing multi-pose fUSI data, including assembling independently loaded poses into one array, consolidating multiple poses into a single volume, slice timing correction, and other multi-pose specific operations. Modules: - **`consolidate`** – Multi-pose volume consolidation. - **`slice_timing`** – Slice timing correction for multi-pose fUSI data. - **`stack`** – Assembling single-pose VoxelData arrays into one pose-dependent DataArray. - **`timing`** – Timing helpers shared across the multipose module and other pose-aware consumers. Functions: - **`consolidate_poses`** – Merge the `pose` dimension into the swept voxel dimension, ordered by position. - **`correct_slice_timings`** – Resample each sweep position to the volume's reference time. - **`stack_poses`** – Stack independently loaded single-pose VoxelData arrays into one pose-dependent array. ### consolidate\_poses ```python consolidate_poses( da: DataArray, rtol: float = 0.01, atol: float | None = None, ) -> DataArray ``` Merge the `pose` dimension into the swept voxel dimension, ordered by position. Per-`(pose, sweep_dim)` world positions are read directly from `da`'s own world coordinates, which requires `da`'s primary voxel-to-world geometry to itself be pose-dependent (a `(npose, 4, 4)` affine stack, one affine per pose). This is the case for SCAN data, where lab space (a fixed scanner frame shared by every pose) is the canonical world frame — see `load_scan` — and for `stack_poses` output. If `da`'s primary geometry is instead driven by a *secondary*, named affine in `da.attrs["affines"]` (e.g. a stack of NIfTI DataArrays with their `world_to_qform` affines stacked, no pose-dependent primary geometry of their own), rebase onto it first with `.fusi.affine.apply`, e.g. `da.fusi.affine.apply("world_to_qform")`, before calling this function. The swept voxel dimension is detected from `da`'s own geometry: the per-pose translation's dominant direction (via SVD) is matched against each voxel dimension's world-space direction, and the best-aligned dimension is used. The primary sweep direction is then found via singular value decomposition of all positions along that dimension. Each voxel is projected onto that axis, the positions are checked for regularity, then the data is reindexed in ascending order along the consolidated sweep axis. This function is primarily intended for consolidating multi-pose fUSI volumes acquired with an Iconeus system using a purely translational probe sweep. In that workflow, each pose corresponds to one probe position along the elevation axis (`k`/world `z`), and the VoxelData array is produced by `load_scan`: ```python scan_3d = load_scan("recording.scan") # dims: (pose, k, j, i) volume = consolidate_poses(scan_3d) # dims: (k, j, i) scan_4d = load_scan("recording_4d.scan") # dims: (time, pose, k, j, i) volume = consolidate_poses(scan_4d) # dims: (time, k, j, i) ``` Parameters: - #### **`da`** (`DataArray`) – VoxelData array with a `pose` dimension and pose-dependent primary voxel-to-world geometry. Typically produced by `load_scan` for `3Dscan` or `4Dscan` files, or by `stack_poses`. - #### **`rtol`** (`float`, default: `0.01` ) – Relative tolerance for the regularity check (fraction of mean spacing). Combined with `atol` as `abs(spacing - mean_spacing) <= atol + rtol * abs(mean_spacing)` (`numpy.isclose` convention), so `rtol` alone dominates at large step sizes. - #### **`atol`** (`float`, default: `None` ) – Absolute tolerance for the regularity check, in `da`'s world units, combined with `rtol` as described above. If not provided, defaults to 5 um converted to `da`'s units. Returns: - `DataArray` – VoxelData array with `pose` merged into the swept voxel dimension, sorted by world position. Every voxel keeps its world position: the output voxel-to-world affine carries over the input's non-swept columns (including any rotation), uses one regular step along the detected sweep axis for the swept column, and is anchored at the first sorted voxel. For inputs whose `time` coordinate is itself pose-dependent (`(time, pose)`-shaped -- see stack\_poses), a consolidated `slice_time` with dims `("time", )` is included: each slice inherits the timestamp of the pose it came from. Raises: - `ValueError` – If `da` has no `pose` dimension, if `da`'s primary geometry is not pose-dependent, if the rotation block of the affine is not constant across poses (non-translation sweep), if the swept voxel dimension cannot be detected (fewer than two poses, identical pose positions, or a degenerate voxel axis), if the consolidated positions are not regularly spaced within `atol`/`rtol` -- which also rejects a sweep that steps along more than one voxel axis at once, since no single voxel dimension can span it -- or if `atol` is not provided and `da`'s world units aren't one of `"mm"`, `"cm"`, `"m"`, `"um"`/`"µm"`, `"nm"`. Warns: - `UserWarning` – If the sweep is not purely 1D (secondary/primary singular value ratio \> 0.01). ### correct\_slice\_timings ```python correct_slice_timings( da: DataArray, method: Literal[ "linear", "nearest", "nearest-up", "zero", "slinear", "quadratic", "cubic", "previous", "next", ] = "linear", fill_value: float | tuple[float, float] | Literal["extrapolate", "nan"] = "extrapolate", ) -> DataArray ``` Resample each sweep position to the volume's reference time. In multi-pose fUSI acquisitions, each sweep position is acquired at a different time within the volume period. This function resamples each position's time series so that all positions appear to have been acquired at the time stored in the `time` coordinate. This function works on both: - Consolidated data: dims `(time, , ...)` with a `slice_time` coordinate with dims `(time, )`, typically produced by `consolidate_poses`. - Unconsolidated data: dims `(time, pose, ...)` with a pose-dependent `(time, pose)`-shaped `time` coordinate (see stack\_poses), holding each pose's own real timestamp directly. The result's `time` becomes a genuine 1D coordinate, computed the same way `consolidate_poses` derives its whole-array `time` from per-pose timestamps -- after correction, every pose really is simultaneous, so there is no more reason for `time` to stay pose-dependent. The sweep dimension is inferred from the second dim of whichever timing coordinate is present. If the input is Dask-backed, the function stays lazy: computation is deferred until `.compute()` is called. The time dimension must not be chunked; spatial dimensions may be freely chunked. Parameters: - #### **`da`** (`DataArray`) – VoxelData array with a `slice_time` coordinate, or a pose-dependent `(time, pose)`-shaped `time` coordinate, with dims `(time, )`. - #### **`method`** (`(linear, nearest, nearest - up, zero, slinear, quadratic, cubic, previous, next)`, default: `"linear"` ) – Interpolation method passed to `scipy.interpolate.interp1d`: - `"linear"`: linear interpolation. - `"nearest"`: nearest-neighbour interpolation; rounds down at half-integers. - `"nearest-up"`: nearest-neighbour interpolation; rounds up at half-integers. - `"zero"`: zeroth-order spline (step function). - `"slinear"`: first-order spline. - `"quadratic"`: second-order spline. - `"cubic"`: third-order spline. - `"previous"`: use previous point's value. - `"next"`: use next point's value. - #### **`fill_value`** (`float or tuple[float, float] or {extrapolate, nan}`, default: `"extrapolate"` ) – How to handle target times that fall outside the range of a position's acquisition times. `"extrapolate"` allows linear extrapolation. `"nan"` inserts NaNs out of bounds. Use a float for a constant fill value, or a tuple `(left, right)` for different values on each side. Returns: - `DataArray` – New VoxelData array with the same dims as the input, resampled so every sweep position appears simultaneous. For already-consolidated input, `time` is unchanged and `slice_time` is dropped (avoiding accidental double-correction). For pose-dependent input, `time` becomes a genuine 1D coordinate (see stack\_poses for what it was before correction). Raises: - `ValueError` – If `da` has no `time` dimension or only one time point, if `da` has neither a `slice_time` coordinate nor a pose-dependent `time` coordinate, if the timing coordinate does not have dims `(time, )`, or if the `time` dimension is chunked. Warns: - `UserWarning` – If a spline method fails due to too few points and falls back to `"linear"`. ### stack\_poses ```python stack_poses( poses: Sequence[DataArray], pose: Sequence[Hashable] | None = None, ) -> DataArray ``` Stack independently loaded single-pose VoxelData arrays into one pose-dependent array. Unlike a direct `xr.concat(..., dim="pose")` call on single-pose inputs, this function preserves each input's voxel-to-world affine in a pose-dependent `VoxelToWorldIndex`. The output's world coordinates (`z`/`y`/`x`) therefore reflect each probe position, with pose labels and affines kept in input order. Parameters: - #### **`poses`** (`sequence[DataArray]`) – VoxelData arrays to stack, one per pose, in pose order. Each must have no existing `pose` dimension (see ensure\_voxeldata's `allow_pose` parameter). Voxel dimensions, shape, voxel-space (`k`/`j`/`i`) coordinate values, and any non-core dimensions must otherwise agree across poses, exactly as required to merge non-concatenated variables in any `xr.concat` call. - #### **`pose`** (`sequence[hashable]`, default: `None` ) – Pose coordinate labels, one per entry of `poses`. If not provided, defaults to `0, 1, ..., len(poses) - 1`. Returns: - `DataArray` – Stacked DataArray with a new `pose` dimension and pose-dependent voxel-to-world geometry (an `(npose, 4, 4)` affine stack, one affine per input). If a `time` dimension is present and every pose shares identical `time` values, `time` stays an ordinary 1D dimension coordinate. If per-pose `time` values differ (poses acquired sequentially rather than simultaneously), `time` instead becomes a genuine `(time, pose)`-shaped coordinate holding each pose's own real timestamp directly -- there is no single answer for "the" time of a `(pose, k, j, i)` voxel any more than there is a single answer for its `z`/`y`/`x` position, so `time` requires a scalar `pose` selection first, exactly like world coordinates already do. A 2D `time` is not itself an index (xarray dimension coordinates must be 1D), so `.sel(time=...)` is unavailable until a pose is selected; after that, `.set_xindex("time")` promotes the resulting 1D `time` back into a real, selectable index. Raises: - `ValueError` – If `poses` is empty, if `pose` does not have one label per entry of `poses`, if any pose already has a `pose` dimension, or if poses have mismatched `time` lengths. # `confusius.plotting` ## plotting Plotting module for fUSI data. Modules: - **`image`** – Image visualization utilities for fUSI data. - **`matrix`** – 2D matrix visualization utilities. - **`motion`** – Motion diagnostics plotting utilities. - **`napari`** – Napari-based visualization utilities for fUSI data. Classes: - **`VolumePlotter`** – Manager for volume slice plots with coordinate-based overlay support. Functions: - **`draw_napari_labels`** – Open a napari viewer to interactively paint integer labels over fUSI data. - **`extract_labels_from_layer`** – Convert a napari Labels layer to an integer label map DataArray. - **`plot_carpet`** – Plot voxel intensities across time as a raster image. - **`plot_composite`** – Plot a red/cyan composite of two volumes as a grid of 2D slice panels. - **`plot_contours`** – Plot mask contours as a grid of 2D slice panels. - **`plot_contrast_matrix`** – Plot a GLM contrast as an array of weights aligned with the design regressors. - **`plot_design_matrix`** – Plot a first-level GLM design matrix as a heatmap. - **`plot_matrix`** – Plot a 2D matrix, e.g. a connectivity or correlation matrix. - **`plot_motion_diagnostics`** – Plot motion diagnostics from `create_motion_dataframe`. - **`plot_napari`** – Display fUSI data using the napari viewer. - **`plot_stat_map`** – Plot a statistical map, optionally over a background volume. - **`plot_volume`** – Plot 2D slices of a volume using matplotlib. ### VolumePlotter Manager for volume slice plots with coordinate-based overlay support. This class maintains the state of a figure with multiple axes, each representing a slice through a volume at a specific coordinate. It enables overlaying multiple volumes on the same axes by matching coordinates. Parameters: - #### **`slice_mode`** (`str`, default: `'z'` ) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension to slice. Native voxel dimensions (`"k"`, `"j"`, `"i"`) are not valid slice modes. - #### **`figure`** (`Figure`, default: `None` ) – The figure containing the axes. If not provided, a new figure will be created on the first call to `add_volume`. - #### **`axes`** (`ndarray[Axes] or Axes`, default: `None` ) – Existing axes to draw into: either a single `matplotlib.axes.Axes` or an array of them. If not provided, axes will be created on the first call to `add_volume`. - #### **`bg_color`** (`str`, default: `"black"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - #### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - #### **`yincrease`** (`bool`, default: `False` ) – Whether the y-axis increases upward. When `False`, y coordinates decrease upward. - #### **`xincrease`** (`bool`, default: `True` ) – Whether the x-axis increases to the right. - #### **`resample_interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation method used whenever oblique voxel-to-world data is resampled onto the axis-aligned world grid for display -- for the whole array upfront when `slice_mode` is spatial (`"z"`/`"y"`/`"x"`) or any other non-`pose` dim (e.g. `"region"`); per-panel, after slicing, when `slice_mode="pose"` (the only dimension that can vary the underlying voxel-to-world geometry). Applies to `add_volume`; mask data passed to `add_contours` always resamples with `"nearest"` regardless of this setting, since blending distinct integer labels together is never meaningful. - #### **`resample_fill_value`** (`float`, default: `None` ) – Value assigned to voxels outside the source data's field of view after resampling oblique data. If not provided, defaults to the source data's own minimum value (see `resample_volume`). - #### **`transpose`** (`bool`, default: `False` ) – Whether to swap the row/column display dims of each slice panel. Attributes: - **`slice_mode`** (`str`) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension being sliced. - **`figure`** (`Figure or None`) – The figure. `None` until the first call to `add_volume` when no figure is provided at construction time. - **`axes`** (`ndarray or None`) – Array of `matplotlib.axes.Axes`. `None` until the first call to `add_volume` when no axes are provided at construction time. Methods: - **`add_composite`** – Plot a red/cyan composite of two volumes on the axes. - **`add_contours`** – Add mask contours to existing axes. - **`add_stat_map`** – Overlay a statistical map on the axes. - **`add_volume`** – Plot or overlay a volume on the axes. - **`close`** – Close the figure and release resources. - **`savefig`** – Save the figure to a file. - **`show`** – Display the figure. #### add\_composite ```python add_composite( data1: DataArray, data2: DataArray, *, resample: bool = True, resample_kwargs: dict[str, Any] | None = None, rtol: float = 1e-05, atol: float = 1e-08, normalize_strategy: Literal[ "per_volume", "per_slice", "shared" ] = "per_volume", slice_coords: Sequence[Hashable] | None = None, match_coordinates: bool = False, alpha: float | NDArray[floating] | None = None, show_titles: bool = True, show_axis_labels: bool = True, show_axis_ticks: bool = True, show_axes: bool = True, fontsize: float | None = None, nrows: int | None = None, ncols: int | None = None, dpi: int | None = None, ) -> VolumePlotter ``` Plot a red/cyan composite of two volumes on the axes. Each slice is rendered as an RGB image where `data1` drives the red channel and `data2` drives the green and blue channels (cyan), making overlap visible as desaturated grey. This is the same visual encoding used by the live registration progress preview. Parameters: - ##### **`data1`** (`DataArray`) – First volume, plotted in red. 3D volume data. Unitary dimensions (except `slice_mode`) are squeezed before processing. Complex-valued inputs are converted to magnitude (`abs(data)`) with a warning. - ##### **`data2`** (`DataArray`) – Second volume, plotted in cyan. Must have the same dimensionality as `data1` after squeezing; when `resample=True` it is resampled onto `data1`'s grid before plotting, so its native shape and coordinates may differ. - ##### **`resample`** (`bool`, default: `True` ) – Whether to resample `data2` onto `data1`'s grid using an identity transform before blending. When `False`, the two arrays must already share the same dimensions and shape, and their coordinates must match within `rtol`/`atol`; once validated, `data2`'s coordinates are replaced with `data1`'s so the two volumes share an exact coordinate frame downstream. - ##### **`resample_kwargs`** (`dict`, default: `None` ) – Extra keyword arguments forwarded to `resample_like` when `resample=True`. Ignored when `resample=False`. - ##### **`rtol`** (`float`, default: `1e-5` ) – Relative tolerance used to validate that `data1` and `data2` share coordinates when `resample=False`. Widen to accept acquisitions on slightly offset grids known to be equivalent. Ignored when `resample=True`. - ##### **`atol`** (`float`, default: `1e-8` ) – Absolute tolerance used to validate that `data1` and `data2` share coordinates when `resample=False`. Ignored when `resample=True`. - ##### **`normalize_strategy`** (`(per_volume, per_slice, shared)`, default: `"per_volume"` ) – Intensity normalisation strategy. - `"per_volume"`: rescale each input to `[0, 1]` independently over its full volume. Preserves slice-to-slice contrast within each array but loses the absolute-intensity relationship between `data1` and `data2`. - `"per_slice"`: rescale each 2D slice independently. Maximises contrast on dim slices at the cost of cross-slice comparability. - `"shared"`: rescale both volumes together using a single shared `[min(data1.min(), data2.min()), max(data1.max(), data2.max())]` range. Preserves the absolute-intensity relationship between the two inputs, useful when comparing data acquired at the same dynamic range. - ##### **`slice_coords`** (`Sequence[Hashable]`, default: `None` ) – Coordinate values along `slice_mode` at which to extract slices. Numeric coordinates are matched by nearest-neighbour lookup; non-numeric coordinates (e.g. region labels) require an exact match. If not provided, all coordinate values from `data1` are used. - ##### **`match_coordinates`** (`bool`, default: `False` ) – If True, match slice coordinates to the stored coordinate mapping of an existing figure (for use as an overlay). If False, plot sequentially on a fresh grid of axes — the natural mode for a standalone composite plot. - ##### **`alpha`** (`float or ndarray`, default: `None` ) – Opacity of the composite image, either a single value or a per-voxel array matching the shape of the displayed slices. If not provided, the image is fully opaque. - ##### **`show_titles`** (`bool`, default: `True` ) – Whether to display subplot titles showing the slice coordinate. - ##### **`show_axis_labels`** (`bool`, default: `True` ) – Whether to display axis labels (with units when available). - ##### **`show_axis_ticks`** (`bool`, default: `True` ) – Whether to display axis tick labels. - ##### **`show_axes`** (`bool`, default: `True` ) – Whether to show axis decorations. When `False`, overrides `show_axis_labels` and `show_axis_ticks`. - ##### **`fontsize`** (`float`, default: `None` ) – Base font size for all text elements. Subplot titles use `fontsize` directly; axis labels use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - ##### **`nrows`** (`int`, default: `None` ) – Number of rows in the subplot grid when creating a new figure. If not provided, computed automatically. - ##### **`ncols`** (`int`, default: `None` ) – Number of columns in the subplot grid when creating a new figure. If not provided, computed automatically. - ##### **`dpi`** (`int`, default: `None` ) – Figure resolution in dots per inch. Ignored when using an existing figure. Returns: - `VolumePlotter` – Returns self for method chaining. Raises: - `ValueError` – If either input has a `time` dimension, is not 2D or 3D, lacks `slice_mode` as a dimension, or (when `resample=False`) the two arrays do not share dims, shape, and coordinates within `rtol`/`atol`. > [!NOTE] > > **Notes** > > The composite is rendered with `pcolormesh` using its RGB-`C` codepath, so panels line up with overlays drawn by `add_volume` / `add_contours`. Hover tooltips, colormaps, colorbars, and intensity thresholds are not supported on composite axes — use `add_volume` for those. #### add\_contours ```python add_contours( mask: DataArray, *, colors: dict[int | str, str] | str | None = None, linewidths: float = 1.5, linestyles: str = "solid", match_coordinates: bool = True, slice_coords: list[Hashable] | None = None, fontsize: float | None = None, roi_labels: dict[int, str] | None = None, **kwargs, ) -> VolumePlotter ``` Add mask contours to existing axes. Parameters: - ##### **`mask`** (`DataArray`) – Integer label map in one of two formats: - **Flat label map**: Spatial dims only, e.g. `(k, j, i)`. Background voxels labeled `0`; each unique non-zero integer identifies a distinct, non-overlapping region. The `region` coordinate of the output holds the integer label values. - **Stacked mask format**: Has a leading `mask` dimension followed by spatial dims, e.g. `(mask, k, j, i)`. Each layer has values in `{0, region_id}` and regions may overlap. The `region` coordinate of the output holds the `mask` coordinate values (e.g., region label). Drawn on the same display geometry as the plotter's own data -- world space, resampled onto the axis-aligned grid as needed (see `VolumePlotter`). - ##### **`colors`** (`dict[int | str, str] or str`, default: `None` ) – Color specification for contour lines. - `dict`: maps each label (integer index) or region acronym (string) to a color string. - `str`: applies one color to all regions. - `None`: colors are derived from `attrs["cmap"]` and `attrs["norm"]` when present, otherwise from the `tab10`/`tab20` colormap. - ##### **`linewidths`** (`float`, default: `1.5` ) – Width of contour lines in points. - ##### **`linestyles`** (`str`, default: `"solid"` ) – Line style for contour lines (e.g. `"solid"`, `"dashed"`). - ##### **`match_coordinates`** (`bool`, default: `True` ) – If `True`, overlay contours on axes whose slice coordinate matches the mask. If `False`, plot sequentially on all axes. - ##### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Coordinate values along the plotter's `slice_mode` at which to draw contours. Numeric coordinates are matched by nearest-neighbour lookup; non-numeric coordinates (e.g. region labels) require an exact match. If not provided, all coordinate values along `slice_mode` are used. - ##### **`fontsize`** (`float`, default: `None` ) – Base font size for text elements when a standalone contour figure is created (`match_coordinates=False`). Subplot titles use `fontsize` directly; axis labels use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - ##### **`roi_labels`** (`dict[int, str]`, default: `None` ) – Mapping from integer label to display name. When provided (or when `mask.attrs["roi_labels"]` is populated), hovering the cursor over a voxel shows `= ()` in the matplotlib status bar. The cursor samples the underlying label map directly, so hovering inside a closed contour is sufficient — there is no need to be on the line. - ##### **`**kwargs`** – Additional keyword arguments passed to `matplotlib.axes.Axes.plot`. Returns: - `VolumePlotter` – Returns `self` for method chaining. Raises: - `ValueError` – If the plotter's `slice_mode` is not a dimension of `mask`. - `ValueError` – If `mask` is not 3D or 4D with a leading `mask` dimension. #### add\_stat\_map ```python add_stat_map( stat_map: DataArray, *, slice_coords: Sequence[Hashable] | None = None, match_coordinates: bool = True, cmap: str | Colormap | None = None, norm: Normalize | None = None, vmin: float | None = None, vmax: float | None = None, auto_range: bool = True, threshold: float | None = None, threshold_mode: Literal["lower", "upper"] = "lower", alpha: float | DataArray | None = None, show_colorbar: bool = True, cbar_label: str | None = None, cbar_kwargs: dict[str, Any] | None = None, show_titles: bool = True, show_axis_labels: bool = True, show_axis_ticks: bool = True, show_axes: bool = True, fontsize: float | None = None, nrows: int | None = None, ncols: int | None = None, dpi: int | None = None, ) -> VolumePlotter ``` Overlay a statistical map on the axes. Thin wrapper around `add_volume` that additionally picks the colormap and range automatically based on the sign of `stat_map`, as done by `plot_stat_map`. Use this method instead of `plot_stat_map` to overlay a statistical map onto an existing plot (e.g. one built with `add_composite`). Parameters: - ##### **`stat_map`** (`DataArray`) – Statistical map to plot. 3D volume data. Unitary dimensions (except `slice_mode`) are squeezed before processing. - ##### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Specific coordinates to plot. Numeric coordinates are matched by nearest-neighbour lookup; non-numeric coordinates (e.g. region labels) require an exact match. If not provided, uses all coordinates from `stat_map`. - ##### **`match_coordinates`** (`bool`, default: `True` ) – Whether to match slice coordinates to the stored coordinate mapping (for overlays) instead of plotting sequentially on all axes (which requires an exact axis count match). - ##### **`cmap`** (`str or Colormap`, default: `None` ) – Colormap for `stat_map`. If not provided, the default depends on `auto_range` and the sign of `stat_map` (see below); an explicit `cmap` is always used as-is regardless of `auto_range`. - ##### **`norm`** (`Normalize`, default: `None` ) – Normalization instance (e.g. `TwoSlopeNorm`, `BoundaryNorm`, `LogNorm`) for cases `vmin`/`vmax`/`auto_range` can't express. When provided, `vmin`, `vmax`, and `auto_range`'s range computation are bypassed entirely; `cmap` still follows the usual rules above. - ##### **`vmin`** (`float`, default: `None` ) – Lower bound of the colormap. If not provided, defaults to the minimum value of `stat_map`, computed over the full array rather than just the displayed slices. Ignored when `norm` is provided, when `auto_range=True` and `stat_map` has only non-negative values, or when `auto_range=True`, `stat_map` spans both signs and `vmax` is given on its own (see `auto_range`). - ##### **`vmax`** (`float`, default: `None` ) – Upper bound of the colormap. If not provided, defaults to the maximum value of `stat_map`, computed over the full array rather than just the displayed slices. Ignored when `norm` is provided, when `auto_range=True` and `stat_map` has only non-positive values, or when `auto_range=True`, `stat_map` spans both signs and `vmin` is given on its own (see `auto_range`). - ##### **`auto_range`** (`bool`, default: `True` ) – Whether to pick the colormap range and default colormap automatically based on the sign of `stat_map`: - Both positive and negative values: diverging, symmetric `[-m, m]` range where `m = max(|vmin|, |vmax|)` over the bounds actually provided, falling back to the largest magnitude in `stat_map` when neither is given, with `cmap` defaulting to `"coolwarm"` — the right choice for diverging statistics where the sign is meaningful (e.g. t-statistics, correlation coefficients, PCA/ICA component maps). - Only non-negative values: sequential `[0, vmax]` range, with `cmap` defaulting to `"viridis"` — the right choice for non-diverging statistics where only magnitude matters (e.g. R², F-statistics). - Only non-positive values: sequential `[vmin, 0]` range, with `cmap` defaulting to `"viridis_r"` (reversed, so that values near zero map to the same end of the colormap in both the non-negative and non-positive cases). Set to `False` to use the resolved `vmin`/`vmax` directly with no zero-anchoring (`cmap` then defaults to `"coolwarm"` regardless of sign). - ##### **`threshold`** (`float`, default: `None` ) – Threshold applied to `|stat_map|`. See `threshold_mode` for the masking direction. If not provided, no thresholding is applied. - ##### **`threshold_mode`** (`(lower, upper)`, default: `"lower"` ) – Controls how `threshold` is applied: - `"lower"`: set pixels where `|stat_map| < threshold` to NaN. - `"upper"`: set pixels where `|stat_map| > threshold` to NaN. - ##### **`alpha`** (`float or DataArray`, default: `None` ) – Opacity of the `stat_map` overlay: a single scalar value, or a 3D DataArray sharing `stat_map`'s dims, shape, and coordinates (for independent per-slice, per-voxel opacity, e.g. to fade out low-magnitude voxels instead of masking them out with `threshold`). A per-voxel opacity must be a DataArray, not a bare array, so it can be validated and aligned against `stat_map`. If not provided, the colormap's own alpha channel is respected. - ##### **`show_colorbar`** (`bool`, default: `True` ) – Whether to add a colorbar for `stat_map`. - ##### **`cbar_label`** (`str`, default: `None` ) – Label for the colorbar. - ##### **`cbar_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments forwarded to `matplotlib.figure.Figure.colorbar` (e.g. `shrink`, `fraction`, `pad`, `aspect`). Useful to shrink the colorbar when it spans a multi-panel grid, since the defaults are sized for a single axes. - ##### **`show_titles`** (`bool`, default: `True` ) – Whether to display subplot titles. - ##### **`show_axis_labels`** (`bool`, default: `True` ) – Whether to display axis labels. - ##### **`show_axis_ticks`** (`bool`, default: `True` ) – Whether to display axis tick labels. - ##### **`show_axes`** (`bool`, default: `True` ) – Whether to show all axis decorations (spines, ticks, labels). When `False`, overrides `show_axis_labels` and `show_axis_ticks`. - ##### **`fontsize`** (`float`, default: `None` ) – Base font size for all text elements. Subplot titles use `fontsize` directly; axis labels and the colorbar label use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - ##### **`nrows`** (`int`, default: `None` ) – Number of rows in the subplot grid when creating a new figure. If not provided, computed automatically. - ##### **`ncols`** (`int`, default: `None` ) – Number of columns in the subplot grid when creating a new figure. If not provided, computed automatically. - ##### **`dpi`** (`int`, default: `None` ) – Figure resolution in dots per inch. Ignored when using an existing figure. Returns: - `VolumePlotter` – Returns self for method chaining. Raises: - `ValueError` – If no matching coordinates are found or axis count doesn't match. - `ValueError` – If `alpha` is a DataArray and its dims, shape, or coordinates do not match `stat_map`. - `TypeError` – If `alpha` is neither a scalar nor a DataArray. Examples: ```pycon >>> import xarray as xr >>> from confusius.plotting import plot_volume >>> anatomical = xr.open_zarr("output.zarr")["power_doppler"] >>> t_map = xr.open_zarr("output.zarr")["t_stat"] >>> plotter = plot_volume(anatomical, show_colorbar=False) >>> plotter = plotter.add_stat_map(t_map, threshold=3.0) ``` #### add\_volume ```python add_volume( data: DataArray, *, slice_coords: Sequence[Hashable] | None = None, match_coordinates: bool = True, cmap: str | Colormap | None = None, norm: Normalize | None = None, vmin: float | None = None, vmax: float | None = None, threshold: float | None = None, threshold_mode: Literal["lower", "upper"] = "lower", alpha: float | DataArray | None = None, show_colorbar: bool = True, cbar_label: str | None = None, cbar_kwargs: dict[str, Any] | None = None, roi_labels: dict[int, str] | None = None, show_titles: bool = True, show_axis_labels: bool = True, show_axis_ticks: bool = True, show_axes: bool = True, fontsize: float | None = None, nrows: int | None = None, ncols: int | None = None, dpi: int | None = None, ) -> VolumePlotter ``` Plot or overlay a volume on the axes. Parameters: - ##### **`data`** (`DataArray`) – 3D volume data. Unitary dimensions (except `slice_mode`) are squeezed before processing. Complex-valued inputs are converted to magnitude (`abs(data)`) with a warning. - ##### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Specific coordinates to plot. Numeric coordinates are matched by nearest-neighbour lookup; non-numeric coordinates (e.g. region labels) require an exact match. If not provided, uses all coordinates from data. - ##### **`match_coordinates`** (`bool`, default: `True` ) – Whether to match slice coordinates to the stored coordinate mapping (for overlays) instead of plotting sequentially on all axes (which requires an exact axis count match). - ##### **`cmap`** (`str or Colormap`, default: `None` ) – Colormap. When not provided, falls back to `data.attrs["cmap"]` if present, otherwise `"gray"`. - ##### **`norm`** (`Normalize`, default: `None` ) – Normalization instance (e.g. `BoundaryNorm` for integer label maps). When not provided, falls back to `data.attrs["norm"]` if present. When a norm is active, `vmin` and `vmax` are ignored. - ##### **`vmin`** (`float`, default: `None` ) – Lower bound of the colormap. Defaults to the 2nd percentile. Ignored when `norm` is provided explicitly (that is, not just inherited from data attributes). - ##### **`vmax`** (`float`, default: `None` ) – Upper bound of the colormap. Defaults to the 98th percentile. Ignored when `norm` is provided explicitly (that is, not just inherited from data attributes). - ##### **`threshold`** (`float`, default: `None` ) – Threshold value for masking. - ##### **`threshold_mode`** (`(lower, upper)`, default: `"lower"` ) – Whether to mask values below or above threshold. - ##### **`alpha`** (`float or DataArray`, default: `None` ) – Opacity of the image: a single scalar value, or a DataArray sharing `data`'s dims, shape, and coordinates (for independent per-slice, per-voxel opacity — e.g. fading out low-confidence voxels). A per-voxel opacity must be a DataArray, not a bare array, so it can be validated and aligned against `data`. If not provided, the colormap's own alpha channel is respected. - ##### **`show_colorbar`** (`bool`, default: `True` ) – Whether to add a colorbar. - ##### **`cbar_label`** (`str`, default: `None` ) – Label for the colorbar. - ##### **`cbar_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments forwarded to `matplotlib.figure.Figure.colorbar` (e.g. `shrink`, `fraction`, `pad`, `aspect`). Useful to shrink the colorbar when it spans a multi-panel grid, since the defaults are sized for a single axes. - ##### **`roi_labels`** (`dict[int, str]`, default: `None` ) – Mapping from integer label to display name. When provided (or when `data.attrs["roi_labels"]` is populated), hovering the cursor over a voxel shows `= ()` in the matplotlib status bar. - ##### **`show_titles`** (`bool`, default: `True` ) – Whether to display subplot titles. - ##### **`show_axis_labels`** (`bool`, default: `True` ) – Whether to display axis labels. - ##### **`show_axis_ticks`** (`bool`, default: `True` ) – Whether to display axis tick labels. - ##### **`show_axes`** (`bool`, default: `True` ) – Whether to show all axis decorations (spines, ticks, labels). When `False`, overrides `show_axis_labels` and `show_axis_ticks`. - ##### **`fontsize`** (`float`, default: `None` ) – Base font size for all text elements. Subplot titles use `fontsize` directly; axis labels and the colorbar label use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - ##### **`nrows`** (`int`, default: `None` ) – Number of rows in the subplot grid when creating a new figure. If not provided, computed automatically. - ##### **`ncols`** (`int`, default: `None` ) – Number of columns in the subplot grid when creating a new figure. If not provided, computed automatically. - ##### **`dpi`** (`int`, default: `None` ) – Figure resolution in dots per inch. Ignored when using an existing figure. Returns: - `VolumePlotter` – Returns self for method chaining. Raises: - `ValueError` – If no matching coordinates are found or axis count doesn't match. - `ValueError` – If `alpha` is a DataArray and its dims, shape, or coordinates do not match `data`. - `TypeError` – If `alpha` is neither a scalar nor a DataArray. #### close ```python close() -> None ``` Close the figure and release resources. #### savefig ```python savefig(fname: str, **kwargs) -> None ``` Save the figure to a file. Parameters: - ##### **`fname`** (`str`) – Path to save the figure. Extension determines format (e.g., `.png`, `.pdf`). - ##### **`**kwargs`** – Additional arguments passed to `matplotlib.figure.Figure.savefig`. Raises: - `RuntimeError` – If called before any data has been plotted. #### show ```python show() -> None ``` Display the figure. Raises: - `RuntimeError` – If called before any data has been plotted. ### draw\_napari\_labels ```python draw_napari_labels( data: DataArray, labels_layer_name: str = "labels", viewer: Viewer | None = None, **kwargs, ) -> tuple[Viewer, Labels] ``` Open a napari viewer to interactively paint integer labels over fUSI data. Displays the data as an image layer and adds an empty Labels layer on top. The user can paint integer labels directly on the image using napari's brush tool. After painting, call `extract_labels_from_layer` with the returned Labels layer and the original data to obtain an integer label map as a DataArray with the same spatial coordinates. Parameters: - #### **`data`** (`DataArray`) – Input data array to display as the background image. Typically a time-averaged power Doppler frame, e.g. `data.mean("time")`. - #### **`labels_layer_name`** (`str`, default: `"labels"` ) – Name assigned to the Labels layer added to the viewer. - #### **`viewer`** (`Viewer`, default: `None` ) – Existing napari viewer to add layers to. If not provided, a new viewer is created via `plot_napari`. - #### **`**kwargs`** – Additional keyword arguments forwarded to `plot_napari` for the image layer (e.g. `colormap`, `contrast_limits`). Returns: - **`viewer`** ( `Viewer` ) – The napari viewer instance with the image and Labels layers. - **`labels_layer`** ( `Labels` ) – The empty Labels layer initialised to zeros. After the user paints labels in the viewer, pass this layer to `extract_labels_from_layer` to obtain an integer label map. > [!NOTE] > > **Notes** > > The Labels layer is initialised with the same `scale` and `translate` parameters as the image layer so that the napari canvas shows a consistent world coordinate frame regardless of voxel spacing or data origin. > > Oblique or sheared input is interpolated onto an axis-aligned grid for display; extraction then uses nearest-neighbor interpolation to return painted labels to the original grid. Thin regions and boundaries can change in this round trip. Prefer drawing in an existing axis-aligned acquisition/scanner frame when available. A known world-space transform can be applied with `.fusi.affine.apply` without resampling voxels; labels must be transformed consistently when changing frames. Examples: ```pycon >>> import confusius as cf >>> pwd = cf.load("power_doppler.nii.gz") >>> # Display the time-averaged image and add an interactive Labels layer. >>> viewer, labels_layer = draw_napari_labels(pwd.mean("time")) >>> # … paint labels in the viewer … >>> # Convert painted labels to an integer label map DataArray. >>> label_map = extract_labels_from_layer(labels_layer, pwd.mean("time")) ``` ### extract\_labels\_from\_layer ```python extract_labels_from_layer( labels_layer: Labels, data: DataArray ) -> DataArray ``` Convert a napari Labels layer to an integer label map DataArray. Reads the integer array painted in `labels_layer` and wraps it in a DataArray whose spatial dimensions and coordinates match those of `data`. The result is compatible with `extract_with_labels`, `plot_contours`, and `VolumePlotter.add_contours`. Parameters: - #### **`labels_layer`** (`Labels`) – A Labels layer populated by the user (e.g. via `draw_napari_labels`). Integer values identify distinct regions; zero is the background and is excluded from downstream analyses. - #### **`data`** (`DataArray`) – Reference data array. Its spatial dimensions and coordinates define the shape and labelling of the output. A time dimension, if present, is ignored: the label map is purely spatial. Returns: - `DataArray` – Stacked integer VoxelData array where the `mask` coordinate holds each unique non-zero label integer. Each layer has values `m` where the user painted label `m` and `0` elsewhere. This format is directly compatible with `extract_with_labels`, `plot_contours`, and `VolumePlotter.add_contours`, and can be sliced by label (e.g. `label_map.sel(mask=2)`) for per-label display. The `attrs` dict carries: - `"long_name"`: "Drawn label map" - `"labels_layer_name"`: name of the source napari layer. - `"rgb_lookup"`: `dict[int, list[int]]` mapping each non-zero label to its `[r, g, b]` color (0–255) as painted in napari. Raises: - `ValueError` – If the layer shape does not match its voxel grid, or no non-zero labels remain on the reference grid. > [!NOTE] > > **Notes** > > Painted labels are read on the display grid stored in the layer's `xarray` metadata, then resampled onto `data`'s grid with nearest-neighbor interpolation and zero background when the grids differ. Without that metadata, the layer is assumed to use `data`'s grid. Nearest-neighbor interpolation preserves label IDs but can change boundaries or remove thin regions. Prefer drawing on an existing axis-aligned grid to avoid this interpolation round trip. > > Per-label colors are read from `labels_layer.get_color(label)`, which works for both the default cyclic colormap and any `DirectLabelColormap` set on the layer. Examples: ```pycon >>> import xarray as xr >>> import confusius # Register accessor. >>> pwd = xr.open_zarr("output.zarr")["power_doppler"].compute() >>> viewer, labels_layer = draw_napari_labels(pwd.mean("time")) >>> # … paint labels in the viewer … >>> label_map = extract_labels_from_layer(labels_layer, pwd.mean("time")) >>> label_map.dims ('mask', 'k', 'j', 'i') >>> # Slice a single label for display alongside a seed map. >>> label_map.sel(mask=2) >>> # Use the label map for region-based analysis. >>> from confusius.extract import extract_with_labels >>> signals = extract_with_labels(pwd, label_map) ``` ### plot\_carpet ```python plot_carpet( data: DataArray, mask: DataArray | None = None, detrend_order: int | None = None, standardize: bool = True, cmap: str | Colormap = "gray", vmin: float | None = None, vmax: float | None = None, decimation_threshold: int | None = 800, figsize: tuple[float, float] = (10, 5), title: str | None = None, fontsize: float | None = None, bg_color: str = "white", fg_color: str | None = None, ax: Axes | None = None, ) -> tuple[Figure | SubFigure, Axes] ``` Plot voxel intensities across time as a raster image. A carpet plot (also known as "grayplot" or "Power plot") displays voxel intensities as a 2D raster image with time on the x-axis and voxels on the y-axis. Each row represents one voxel's signals, typically standardized to z-scores. Parameters: - #### **`data`** (`DataArray`) – Input data array with a `time` dimension: a VoxelData array, or an already-extracted signals array (e.g. `extract_with_labels` output). - #### **`mask`** (`DataArray`, default: `None` ) – Boolean mask with the same non-`time` dimensions and coordinates as `data`. True values indicate elements to include. If not provided, all non-zero elements from the data are included. - #### **`detrend_order`** (`int`, default: `None` ) – Polynomial order for detrending: - `0`: Remove mean (constant detrending). - `1`: Remove linear trend using least squares regression. - `2+`: Remove polynomial trend of specified order. If not provided, no detrending is applied. - #### **`standardize`** (`bool`, default: `True` ) – Whether to standardize each voxel's signals to z-scores. - #### **`cmap`** (`str`, default: `"gray"` ) – Matplotlib colormap name. - #### **`vmin`** (`float`, default: `None` ) – Minimum value for colormap. If not provided, uses `mean - 2*std`. - #### **`vmax`** (`float`, default: `None` ) – Maximum value for colormap. If not provided, uses `mean + 2*std`. - #### **`decimation_threshold`** (`int or None`, default: `800` ) – If the number of timepoints exceeds this value, data is downsampled along the time axis to improve plotting performance. Set to `None` to disable downsampling. - #### **`figsize`** (`tuple[float, float]`, default: `(10, 5)` ) – Figure size in inches `(width, height)`. - #### **`title`** (`str`, default: `None` ) – Plot title. - #### **`fontsize`** (`float`, default: `None` ) – Base font size for text elements. Title uses `fontsize` directly; axis labels and colorbar label use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - #### **`bg_color`** (`str`, default: `"white"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - #### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - #### **`ax`** (`Axes`, default: `None` ) – Axes to plot on. If not provided, creates new figure and axes. Returns: - **`figure`** ( `Figure or SubFigure` ) – Figure object containing the carpet plot. - **`axes`** ( `Axes` ) – Axes object with the carpet plot. > [!NOTE] > > **Notes** > > Complex-valued data is converted to magnitude before processing. > > This function was inspired by Nilearn's `nilearn.plotting.plot_carpet`. **References** 1. Power, Jonathan D. “A Simple but Useful Way to Assess fMRI Scan Qualities.” NeuroImage, vol. 154, July 2017, pp. 150–58. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2016.08.009](). Examples: ```pycon >>> import xarray as xr >>> from confusius.plotting import plot_carpet >>> data = xr.open_zarr("output.zarr")["iq"] >>> fig, ax = plot_carpet(data) ``` ```pycon >>> # With linear detrending >>> fig, ax = plot_carpet(data, detrend_order=1) ``` ```pycon >>> # With mask >>> import numpy as np >>> mask = xr.DataArray( ... np.abs(data.isel(time=0)) > threshold, ... dims=["z", "y", "x"], ... ) >>> fig, ax = plot_carpet(data, mask=mask) ``` ### plot\_composite ```python plot_composite( data1: DataArray, data2: DataArray, *, resample: bool = True, resample_kwargs: dict[str, Any] | None = None, rtol: float = 1e-05, atol: float = 1e-08, normalize_strategy: Literal[ "per_volume", "per_slice", "shared" ] = "per_volume", slice_coords: Sequence[Hashable] | None = None, slice_mode: str | None = None, transpose: bool = False, alpha: float | NDArray[floating] | None = None, show_titles: bool = True, show_axis_labels: bool = True, show_axis_ticks: bool = True, show_axes: bool = True, fontsize: float | None = None, yincrease: bool = False, xincrease: bool = True, bg_color: str = "black", fg_color: str | None = None, figure: Figure | None = None, axes: NDArray[Any] | Axes | None = None, nrows: int | None = None, ncols: int | None = None, dpi: int | None = None, resample_interpolation: Literal[ "linear", "nearest", "bspline" ] = "linear", resample_fill_value: float | None = None, ) -> VolumePlotter ``` Plot a red/cyan composite of two volumes as a grid of 2D slice panels. Each slice is rendered as an RGB image where `data1` drives the red channel and `data2` drives the green and blue channels (cyan), making overlap visible as desaturated grey. This is the same visual encoding used by the live registration progress preview. If `slice_mode` is not provided and `data1` is planar, the singleton world dimension is used; otherwise the default is `"z"`. Parameters: - #### **`data1`** (`DataArray`) – First volume, plotted in red. 3D volume data. Unitary dimensions (except `slice_mode`) are squeezed before processing. Complex-valued inputs are converted to magnitude (`abs(data)`) with a warning. - #### **`data2`** (`DataArray`) – Second volume, plotted in cyan. Must have the same dimensionality as `data1` after squeezing; when `resample=True` it is resampled onto `data1`'s grid before plotting, so its native shape and coordinates may differ. - #### **`resample`** (`bool`, default: `True` ) – Whether to resample `data2` onto `data1`'s grid using an identity transform before blending. When `False`, the two arrays must already share dimensions and shape, and their coordinates must match within `rtol`/`atol`; once validated, `data2`'s coordinates are replaced with `data1`'s so the two volumes share an exact coordinate frame downstream. - #### **`resample_kwargs`** (`dict`, default: `None` ) – Extra keyword arguments forwarded to `resample_like` when `resample=True`. Ignored when `resample=False`. - #### **`rtol`** (`float`, default: `1e-5` ) – Relative tolerance used to validate that `data1` and `data2` share coordinates when `resample=False`. Widen to accept acquisitions on slightly offset grids known to be equivalent. Ignored when `resample=True`. - #### **`atol`** (`float`, default: `1e-8` ) – Absolute tolerance used to validate that `data1` and `data2` share coordinates when `resample=False`. Ignored when `resample=True`. - #### **`normalize_strategy`** (`(per_volume, per_slice, shared)`, default: `"per_volume"` ) – Intensity normalisation strategy. - `"per_volume"`: rescale each input to `[0, 1]` independently over its full volume. Preserves slice-to-slice contrast within each array but loses the absolute-intensity relationship between `data1` and `data2`. - `"per_slice"`: rescale each 2D slice independently. Maximises contrast on dim slices at the cost of cross-slice comparability. - `"shared"`: rescale both volumes together using a single shared `[min(data1.min(), data2.min()), max(data1.max(), data2.max())]` range. Preserves the absolute-intensity relationship between the two inputs. - #### **`slice_coords`** (`Sequence[Hashable]`, default: `None` ) – Coordinate values along `slice_mode` at which to extract slices. Numeric coordinates are matched by nearest-neighbour lookup; non-numeric coordinates (e.g. region labels) require an exact match. If not provided, from `data1` are used. - #### **`slice_mode`** (`str`, default: `None` ) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension to slice. Native voxel dimensions (`"k"`, `"j"`, `"i"`) are not valid slice modes. If not provided, planar `data1` is sliced along its singleton world dimension and full 3D data is sliced along `"z"`. After slicing, each panel must be 2D. - #### **`transpose`** (`bool`, default: `False` ) – Whether to swap the row/column display dims of each slice panel. - #### **`alpha`** (`float or ndarray`, default: `None` ) – Opacity of the composite image, either a single value or a per-voxel array matching the shape of the displayed slices. If not provided, the image is fully opaque. - #### **`show_titles`** (`bool`, default: `True` ) – Whether to display subplot titles showing the slice coordinate. - #### **`show_axis_labels`** (`bool`, default: `True` ) – Whether to display axis labels (with units when available). - #### **`show_axis_ticks`** (`bool`, default: `True` ) – Whether to display axis tick labels. - #### **`show_axes`** (`bool`, default: `True` ) – Whether to show all axis decorations. When `False`, overrides `show_axis_labels` and `show_axis_ticks`. - #### **`fontsize`** (`float`, default: `None` ) – Base font size for all text elements. Subplot titles use `fontsize` directly; axis labels use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - #### **`yincrease`** (`bool`, default: `False` ) – Whether the y-axis increases upward (`True`) or downward (`False`). - #### **`xincrease`** (`bool`, default: `True` ) – Whether the x-axis increases to the right (`True`) or left (`False`). - #### **`bg_color`** (`str`, default: `"black"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - #### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - #### **`figure`** (`Figure`, default: `None` ) – Existing figure to draw into. If not provided, a new figure is created. - #### **`axes`** (`ndarray or Axes`, default: `None` ) – Existing axes to draw into: either a single `matplotlib.axes.Axes` or a 2D array of them. Must contain exactly as many elements as there are slices. A single `Axes` is wrapped automatically. If not provided, new axes are created inside `figure`. - #### **`nrows`** (`int`, default: `None` ) – Number of rows in the subplot grid. If not provided, computed automatically. - #### **`ncols`** (`int`, default: `None` ) – Number of columns in the subplot grid. If not provided, computed automatically. - #### **`dpi`** (`int`, default: `None` ) – Figure resolution in dots per inch. Ignored when `figure` is provided. - #### **`resample_interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation method used when resampling oblique (non-axis-aligned) voxel-to-world `data1`/`data2` onto an axis-aligned world grid for display. Distinct from `resample_kwargs`, which controls resampling `data2` onto `data1`'s grid for compositing. - #### **`resample_fill_value`** (`float`, default: `None` ) – Value assigned to voxels outside `data1`/`data2`'s field of view after display resampling. If not provided, defaults to each array's own minimum value. Returns: - `VolumePlotter` – Object managing the figure, axes, and coordinate mapping for overlays. Raises: - `ValueError` – If either input has a `time` dimension, is not 2D or 3D, lacks `slice_mode` as a dimension, or (when `resample=False`) the two arrays do not share dims, shape, and coordinates within `rtol`/`atol`. > [!NOTE] > > **Notes** > > Rendering uses `pcolormesh` with an RGB `C` array, so panels share their cell geometry with `plot_volume` / `plot_contours` and overlay correctly. Colormaps, colorbars, intensity thresholds, and hover tooltips are not supported on composite axes — use `plot_volume` for those. > > The returned `VolumePlotter` stores the coordinate-to-axis mapping, so you can overlay further volumes or contours with `VolumePlotter.add_volume` or `VolumePlotter.add_contours`. Examples: ```pycon >>> import xarray as xr >>> from confusius.plotting import plot_composite >>> fixed = xr.open_zarr("fixed.zarr")["power_doppler"] >>> moving = xr.open_zarr("moving.zarr")["power_doppler"] >>> plotter = plot_composite(fixed, moving, slice_mode="z") ``` ```pycon >>> # Skip resampling when the two volumes are already aligned. >>> plotter = plot_composite(fixed, registered_moving, resample=False) ``` ```pycon >>> # Maximise contrast on dim slices. >>> plotter = plot_composite(fixed, moving, normalize_strategy="per_slice") ``` ### plot\_contours ```python plot_contours( mask: DataArray, *, colors: dict[int | str, str] | str | None = None, linewidths: float = 1.5, linestyles: str = "solid", slice_mode: str = "z", slice_coords: list[Hashable] | None = None, transpose: bool = False, fontsize: float | None = None, yincrease: bool = False, xincrease: bool = True, bg_color: str = "black", fg_color: str | None = None, figure: Figure | None = None, axes: NDArray[Any] | Axes | None = None, roi_labels: dict[int, str] | None = None, **kwargs, ) -> VolumePlotter ``` Plot mask contours as a grid of 2D slice panels. Displays contour lines for each labeled region in `mask` across a grid of subplots. Each panel shows the contours for one slice along `slice_mode`, drawn in world coordinates when available. Parameters: - #### **`mask`** (`DataArray`) – Integer label map in one of two formats: - **Flat label map**: Spatial dims only, e.g. `(k, j, i)`. Background voxels labeled `0`; each unique non-zero integer identifies a distinct, non-overlapping region. The `regions` coordinate of the output holds the integer label values. - **Stacked mask format**: Has a leading `masks` dimension followed by spatial dims, e.g. `(masks, k, j, i)`. Each layer has values in `{0, region_id}` and regions may overlap. The `regions` coordinate of the output holds the `masks` coordinate values (e.g., region label). - #### **`colors`** (`dict[int | str, str] or str`, default: `None` ) – Color specification for contour lines. A `dict` maps each label (integer index or region acronym string) to a color; a `str` applies one color to all regions. If not provided, colors are derived from `attrs["cmap"]` and `attrs["norm"]` when present, otherwise from the `tab10`/`tab20` colormap. - #### **`linewidths`** (`float`, default: `1.5` ) – Width of contour lines in points. - #### **`linestyles`** (`str`, default: `"solid"` ) – Line style for contour lines (e.g. `"solid"`, `"dashed"`). - #### **`slice_mode`** (`str`, default: `"z"` ) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension to slice. Native voxel dimensions (`"k"`, `"j"`, `"i"`) are not valid slice modes. After slicing, each panel must be 2D. - #### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Coordinate values along `slice_mode` at which to extract slices. Numeric coordinates are matched by nearest-neighbour lookup; non-numeric coordinates (e.g. region labels) require an exact match. If not provided, all coordinate values along `slice_mode` are used. - #### **`transpose`** (`bool`, default: `False` ) – Whether to swap the row/column display dims of each slice panel. - #### **`fontsize`** (`float`, default: `None` ) – Base font size for text elements. Subplot titles use `fontsize` directly; axis labels use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - #### **`yincrease`** (`bool`, default: `False` ) – Whether the y-axis increases upward (`True`) or downward (`False`). - #### **`xincrease`** (`bool`, default: `True` ) – Whether the x-axis increases to the right (`True`) or left (`False`). - #### **`bg_color`** (`str`, default: `"black"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - #### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - #### **`figure`** (`Figure`, default: `None` ) – Existing figure to draw into. If not provided, a new figure is created. - #### **`axes`** (`ndarray or Axes`, default: `None` ) – Existing axes to draw into: either a single `matplotlib.axes.Axes` or a 2D array of them. A single `Axes` is wrapped automatically. If not provided, new axes are created inside `figure`. - #### **`roi_labels`** (`dict[int, str]`, default: `None` ) – Mapping from integer label to display name. When provided (or when `mask.attrs["roi_labels"]` is populated), hovering the cursor over a voxel shows `= ()` in the matplotlib status bar. - #### **`**kwargs`** – Additional keyword arguments passed to `matplotlib.axes.Axes.plot`. Returns: - `VolumePlotter` – Object managing the figure, axes, and coordinate mapping for overlays. Raises: - `ValueError` – If `slice_mode` is not a dimension of `mask`. - `ValueError` – If `mask` is not 3D. > [!NOTE] > > **Notes** > > Contours are computed with `skimage.measure.find_contours` on a binary mask for each label, then mapped to world coordinates via linear interpolation between coordinate centers. Each panel has `aspect="equal"` so that 1 unit in x matches 1 unit in y. > > The returned `VolumePlotter` stores the coordinate-to-axis mapping, so you can overlay a volume afterwards with `VolumePlotter.add_volume`. Examples: ```pycon >>> import xarray as xr >>> from confusius.plotting import plot_contours >>> mask = xr.open_zarr("output.zarr")["roi_mask"] >>> plotter = plot_contours(mask, slice_mode="z") ``` ```pycon >>> # Custom colors per label. >>> plotter = plot_contours(mask, slice_mode="z", colors={1: "red", 2: "cyan"}) ``` ```pycon >>> # Overlay contours on an existing volume plot. >>> from confusius.plotting import plot_volume >>> volume = xr.open_zarr("output.zarr")["power_doppler"] >>> plotter = plot_volume(volume, slice_mode="z") >>> plotter.add_contours(mask, colors="yellow") ``` ### plot\_contrast\_matrix ```python plot_contrast_matrix( contrast_def: str | NDArray[Any], design_matrix: DataFrame | NDArray[Any], *, cmap: str | Colormap | None = "gray", title: str | None = None, bg_color: str = "white", fg_color: str | None = None, fontsize: float | None = None, show_colorbar: bool = False, row_height: float = 0.5, column_width: float = 0.6, ax: Axes | None = None, ) -> tuple[Figure | SubFigure, Axes] ``` Plot a GLM contrast as an array of weights aligned with the design regressors. Visualizes the same `contrast_def` you would pass to `compute_contrast`, laid out under `design_matrix`'s regressor columns. Each contrast is drawn as one row of a heatmap on a symmetric range `[-m, m]` (`m = max(|weights|)`), so positive and negative weights read symmetrically. Parameters: - #### **`contrast_def`** (`str or ndarray`) – Contrast definition. A string is parsed as an expression over the design-matrix column names (e.g. `"stim_A - stim_B"`); a 1D `(n_columns,)` array is a *t*-contrast (one row) and a 2D `(q, n_columns)` array an *F*-contrast (`q` rows). Arrays narrower than the design are zero-padded on the right, mirroring `compute_contrast`. - #### **`design_matrix`** (`(n_volumes, n_regressors) pandas.DataFrame or numpy.ndarray`) – Design matrix the contrast applies to, used for its regressor count and column labels. When a DataFrame is passed, its column names label the regressors. A bare array has no column names, so a string `contrast_def` cannot be resolved against it and raises. - #### **`cmap`** (`str or Colormap`, default: `"gray"` ) – Colormap for the weights. - #### **`title`** (`str`, default: `None` ) – Plot title. - #### **`bg_color`** (`str`, default: `"white"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - #### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - #### **`fontsize`** (`float`, default: `None` ) – Base font size for text elements. Title uses `fontsize` directly; the y-axis label uses `0.9 * fontsize` and the tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - #### **`show_colorbar`** (`bool`, default: `False` ) – Whether to add a colorbar for the weights to the figure. - #### **`row_height`** (`float`, default: `0.5` ) – Figure height contributed per contrast row, in inches, used only when `ax` is not provided. The height is `1.5 + row_height * n_contrasts` (the 1.5 in base leaves room for the rotated regressor labels and a colorbar taller than the strip), floored at 2 inches. - #### **`column_width`** (`float`, default: `0.6` ) – Figure width contributed per regressor, in inches, used only when `ax` is not provided. The width is `column_width * n_regressors`, floored at 4 inches. - #### **`ax`** (`Axes`, default: `None` ) – Axes to plot on. If not provided, creates a new figure and axes sized from `row_height` and `column_width`. If provided, the Axes is cleared before plotting. Returns: - **`figure`** ( `Figure or SubFigure` ) – Figure object containing the contrast plot. - **`axes`** ( `Axes` ) – Axes object with the contrast plot. Raises: - `ValueError` – If `contrast_def` is a string but `design_matrix` is a bare array with no column names to resolve it against. ### plot\_design\_matrix ```python plot_design_matrix( design_matrix: DataFrame | NDArray[Any], *, cmap: str | Colormap | None = None, title: str | None = None, index_yaxis: bool = False, yaxis_label: str | None = None, bg_color: str = "white", fg_color: str | None = None, fontsize: float | None = None, height: float = 5.0, column_width: float = 0.6, ax: Axes | None = None, ) -> tuple[Figure | SubFigure, Axes] ``` Plot a first-level GLM design matrix as a heatmap. Renders each regressor (column) against acquisition volumes (row). Values are shown on a norm centered at zero, so positive and negative regressor weights read symmetrically. When `ax` is not provided, the figure width scales with the number of regressors so wide designs stay legible. Parameters: - #### **`design_matrix`** (`(n_volumes, n_regressors) pandas.DataFrame or numpy.ndarray`) – Design matrix to plot, e.g. an entry of the `design_matrices_` attribute of a fitted `FirstLevelModel`. Volumes run along the rows (top to bottom) and regressors along the columns. When a DataFrame is passed, its column names label the regressors and its index (the per-volume acquisition times, in seconds) labels the y-axis (see `index_yaxis`). - #### **`cmap`** (`str or Colormap`, default: `None` ) – Colormap for the design matrix. If not provided, the active Matplotlib default is used. - #### **`title`** (`str`, default: `None` ) – Plot title. - #### **`index_yaxis`** (`bool`, default: `False` ) – Whether to label the y-axis with the design matrix's index (the per-volume acquisition times, in seconds), titled "Time (s)". When False, the y-axis instead shows the integer volume index titled "Volume". A bare array, or a DataFrame with a trivial `pandas.RangeIndex`, has no meaningful index and always uses the volume index regardless of this flag. - #### **`yaxis_label`** (`str`, default: `None` ) – Override for the y-axis label. If not provided, the label is "Time (s)" when `index_yaxis` selects the acquisition-time index and "Volume" otherwise. - #### **`bg_color`** (`str`, default: `"white"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - #### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - #### **`fontsize`** (`float`, default: `None` ) – Base font size for text elements. Title uses `fontsize` directly; the y-axis label uses `0.9 * fontsize` and the regressor tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - #### **`height`** (`float`, default: `5.0` ) – Figure height in inches, used only when `ax` is not provided. - #### **`column_width`** (`float`, default: `0.6` ) – Figure width contributed per regressor, in inches, used only when `ax` is not provided. The width is `column_width * n_regressors`, floored at 4 inches. - #### **`ax`** (`Axes`, default: `None` ) – Axes to plot on. If not provided, creates a new figure and axes sized from `height` and `column_width`. If provided, the Axes is cleared before plotting. Returns: - **`figure`** ( `Figure or SubFigure` ) – Figure object containing the design matrix plot. - **`axes`** ( `Axes` ) – Axes object with the design matrix plot. ### plot\_matrix ```python plot_matrix( matrix: NDArray[Any] | DataArray, labels: Sequence[str] | Literal[False] | None = None, groups: Sequence[Hashable] | None = None, group_colors: Mapping[Hashable, str] | None = None, show_group_labels: bool = True, triangle: Literal[ "full", "lower", "diag_lower", "diag_upper", "upper" ] = "full", grid: str | bool = False, grid_linewidth: float | None = None, cmap: str | Colormap | None = None, vmin: float | None = None, vmax: float | None = None, auto_range: bool = True, show_colorbar: bool = True, cbar_label: str | None = None, title: str | None = None, fontsize: float | None = None, bg_color: str = "white", fg_color: str | None = None, figsize: tuple[float, float] = (7, 6), ax: Axes | None = None, ) -> tuple[Figure | SubFigure, Axes] ``` Plot a 2D matrix, e.g. a connectivity or correlation matrix. Parameters: - #### **`matrix`** (`(n, n) numpy.ndarray or xarray.DataArray`) – Square matrix to plot. - #### **`labels`** (`sequence[str] or False`, default: `None` ) – Label for each row/column, in matrix order. If not provided and `matrix` is a `xarray.DataArray` with a coordinate on its first dimension, that coordinate's values are used. Pass `False` to force no labels even when such a coordinate exists. - #### **`groups`** (`sequence of hashable`, default: `None` ) – Group value for each row/column, in matrix order, e.g. `["cortex"] * 5 + ["thalamus"] * 3`. Rows/columns are assumed to already be sorted by group: contiguous runs of equal values are drawn as colored rectangles alongside the matrix. If not provided, no group annotation is drawn. - #### **`group_colors`** (`Mapping[Hashable, str]`, default: `None` ) – Color for each unique value in `groups`. If not provided, colors are assigned automatically from a qualitative colormap. - #### **`show_group_labels`** (`bool`, default: `True` ) – Whether to annotate each group's colored rectangle with its value. - #### **`triangle`** (`(full, lower, diag_lower, diag_upper, upper)`, default: `"full"` ) – Which part of the matrix to display: - `"lower"`: only the part strictly below the diagonal. - `"diag_lower"`: the part below the diagonal, including it. - `"diag_upper"`: the part above the diagonal, including it. - `"upper"`: only the part strictly above the diagonal. - `"full"`: the entire matrix. `"lower"`/`"diag_lower"` and `"upper"`/`"diag_upper"` are useful for overlaying two matrices on the same axes (e.g. an estimate on one side and its significance mask on the other): call `plot_matrix` twice, passing the first call's `ax` to the second along with `show_colorbar=False`. Pass the same `labels` to both calls: since the second call reuses `ax` directly (rather than a new axes sharing it), it otherwise clears the labels the first call set. - #### **`grid`** (`str or bool`, default: `False` ) – Grid lines separating cells. Pass a color string to draw them in that color, `True` to use the resolved foreground color (see `fg_color`), or `False` to disable the grid. - #### **`grid_linewidth`** (`float`, default: `None` ) – Width of the grid lines. If not provided, uses the active Matplotlib default. Ignored when `grid` is `False`. - #### **`cmap`** (`str or Colormap`, default: `None` ) – Colormap for `matrix`. If not provided, the default depends on `auto_range` and the sign of `matrix` (see below); an explicitly provided `cmap` is always used as-is regardless of `auto_range`. - #### **`vmin`** (`float`, default: `None` ) – Lower bound of the colormap. If not provided, defaults to the minimum value in `matrix`. Ignored when `auto_range=True` and `matrix` has only non-negative values, or when `auto_range=True`, `matrix` spans both signs and `vmax` is given on its own (see below). - #### **`vmax`** (`float`, default: `None` ) – Upper bound of the colormap. If not provided, defaults to the maximum value in `matrix`. Ignored when `auto_range=True` and `matrix` has only non-positive values, or when `auto_range=True`, `matrix` spans both signs and `vmin` is given on its own (see below). - #### **`auto_range`** (`bool`, default: `True` ) – Whether to pick the colormap range and default colormap automatically based on the sign of `matrix`: - Both positive and negative values: diverging, symmetric `[-m, m]` range where `m = max(|vmin|, |vmax|)` over the bounds actually provided, falling back to the largest magnitude in `matrix` when neither is given, with `cmap` defaulting to `"coolwarm"`. - Only non-negative values: sequential `[0, vmax]` range, with `cmap` defaulting to `"viridis"`. - Only non-positive values: sequential `[vmin, 0]` range, with `cmap` defaulting to `"viridis_r"` (reversed, so that values near zero map to the same end of the colormap in both the non-negative and non-positive cases). Set to `False` to use the resolved `vmin`/`vmax` directly with no zero-anchoring (`cmap` then defaults to `"coolwarm"` regardless of sign). - #### **`show_colorbar`** (`bool`, default: `True` ) – Whether to add a colorbar for `matrix` to the figure. - #### **`cbar_label`** (`str`, default: `None` ) – Label for the colorbar. - #### **`title`** (`str`, default: `None` ) – Plot title. - #### **`fontsize`** (`float`, default: `None` ) – Base font size for text elements. Title uses `fontsize` directly; the colorbar label and group labels use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - #### **`bg_color`** (`str`, default: `"white"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - #### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - #### **`figsize`** (`tuple[float, float]`, default: `(7, 6)` ) – Figure size in inches `(width, height)`, used only when `ax` is not provided. - #### **`ax`** (`Axes`, default: `None` ) – Axes to plot on. If not provided, creates new figure and axes. Returns: - **`figure`** ( `Figure or SubFigure` ) – Figure object containing the matrix plot. - **`axes`** ( `Axes` ) – Axes object with the matrix plot. Examples: ```pycon >>> import numpy as np >>> from confusius.plotting import plot_matrix >>> rng = np.random.default_rng(0) >>> corr = np.corrcoef(rng.standard_normal((10, 20))) >>> fig, ax = plot_matrix(corr, labels=[f"region_{i}" for i in range(10)]) ``` ```pycon >>> # Annotate groups of regions with colored rectangles. >>> groups = ["cortex"] * 4 + ["thalamus"] * 3 + ["hippocampus"] * 3 >>> fig, ax = plot_matrix(corr, groups=groups) ``` ```pycon >>> # Overlay two matrices, e.g. an estimate and its significance mask. >>> pvalues = rng.uniform(size=(10, 10)) >>> labels = [f"region_{i}" for i in range(10)] >>> fig, ax = plot_matrix(corr, labels=labels, triangle="diag_lower") >>> fig, ax = plot_matrix( ... pvalues, labels=labels, triangle="upper", ax=ax, show_colorbar=False ... ) ``` ### plot\_motion\_diagnostics ```python plot_motion_diagnostics( motion_df: DataFrame, *, figsize: tuple[float, float] | None = None, ) -> tuple[Figure, ndarray] ``` Plot motion diagnostics from `create_motion_dataframe`. Parameters: - #### **`motion_df`** (`DataFrame`) – Motion summary table, typically `result.attrs["motion_params"]` from `register_volumewise`. The function plots whichever standard columns are present. - #### **`figsize`** (`tuple of float`, default: `None` ) – Figure size passed to Matplotlib. If not provided, a height is chosen from the number of panels. Returns: - **`fig`** ( `Figure` ) – Figure containing the motion plots. - **`axes`** ( `ndarray` ) – Array of Matplotlib axes, one per panel. Raises: - `ValueError` – If `motion_df` contains none of the supported motion-diagnostics columns. ### plot\_napari ```python plot_napari( data: DataArray, show_colorbar: bool = True, show_scale_bar: bool = True, dim_order: tuple[str, ...] | None = None, viewer: Viewer | None = None, layer_type: Literal["image", "labels", "auto"] = "auto", resample_interpolation: Literal[ "linear", "nearest", "bspline" ] | None = None, resample_fill_value: float | None = None, **layer_kwargs, ) -> tuple[Viewer, Image | Labels] ``` Display fUSI data using the napari viewer. Parameters: - #### **`data`** (`DataArray`) – Input data array to visualize. Must carry a `VoxelToWorldIndex` (native voxel dimensions `(..., time, pose, k, j, i)` deriving world `z/y/x` coordinates). Use `dim_order` to specify a different displayed spatial ordering. Can be image data or label/mask data (e.g., ROIs, segmentations). - #### **`show_colorbar`** (`bool`, default: `True` ) – Whether to show the colorbar. Only applies to image layers. - #### **`show_scale_bar`** (`bool`, default: `True` ) – Whether to show the scale bar. - #### **`dim_order`** (`tuple[str, ...]`, default: `None` ) – Dimension ordering for the spatial axes (last three dimensions). If not provided, singleton spatial dimensions (e.g. the elevation axis of a single-slice acquisition) are placed first so the canvas always shows the two axes that actually vary; otherwise the dimensions' native ordering in `data` is used. - #### **`viewer`** (`Viewer`, default: `None` ) – Existing napari viewer to add the layer to. If not provided, a new viewer is created. - #### **`layer_type`** (`(image, labels, auto)`, default: `"image"` ) – Type of layer to create. Use "image" for fUSI data and "labels" for ROI masks, segmentations, or other label data. "auto" picks "labels" for integer or boolean data and "image" otherwise. - #### **`resample_interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation method used when resampling oblique (non-axis-aligned) voxel-to-world `data` onto an axis-aligned world grid for display. If not provided, defaults to `"nearest"` for labels layers (including those inferred with `layer_type="auto"`; blending distinct integer labels together is never meaningful) and `"linear"` for image layers. - #### **`resample_fill_value`** (`float`, default: `None` ) – Value assigned to voxels outside `data`'s field of view after resampling oblique data. If not provided, defaults to `data`'s own minimum value. - #### **`**layer_kwargs`** – Additional keyword arguments passed to the layer creation method (`napari.imshow` for images or `viewer.add_labels` for labels). For image layers, if `data.attrs` contains `"cmap"` and `"colormap"` is not in `layer_kwargs`, the attribute is used as the colormap. For labels layers, if `data.attrs` contains `"cmap"` and `"norm"` (as set by atlas functions) and `"colormap"` is not in `layer_kwargs`, a per-label color dict is built automatically from those attributes. Returns: - **`viewer`** ( `Viewer` ) – The napari viewer instance with the layer added. - **`layer`** ( `Image or Labels` ) – The layer added to the viewer. > [!NOTE] > > **Notes** > > Complex-valued data is converted to magnitude (`abs(data)`) before display. > > Napari's axis labels always show world `z`/`y`/`x` names, even though the displayed layer keeps `data`'s native voxel dims and index. Oblique or sheared data is resampled to an axis-aligned world grid first, because this display path does not yet pass `data.fusi.affine`'s rotation/shear through as a napari layer `affine`. > > If all displayed dimensions have coordinates, their spacing is used as the scale parameter for napari to ensure correct world scaling. If any displayed dimension is missing coordinates, no scaling is applied for that dimension. The spacing is computed as the median difference between consecutive coordinate values. > > When spatial coordinates carry a `units` attribute (e.g. `"m"`), the unit list is forwarded to napari as the `units` layer parameter, which populates the status bar with world coordinates and sets the scale bar unit if units are consistent across displayed axes. > > For unitary voxel dimensions (e.g., a single-slice elevation axis in 2D+t data), the spacing cannot be inferred from consecutive coordinate differences. In that case, `.fusi.spacing` derives it from the voxel-to-world affine column norm instead. For unitary non-voxel dimensions with no affine to fall back on, unit spacing is assumed and a warning is emitted. > > The first coordinate value of each displayed dimension is used as the `translate` parameter so that the image is positioned at its correct world origin. For dimensions without coordinates, a translate of `0.0` is used. This ensures that multiple datasets with different fields of view overlay correctly when added to the same viewer. Examples: ```pycon >>> import confusius as cf >>> from confusius.plotting import plot_napari >>> data = cf.load("output.nii.gz") >>> viewer, layer = plot_napari(data) ``` ```pycon >>> # Custom contrast limits >>> viewer, layer = plot_napari(data, contrast_limits=(0, 100)) ``` ```pycon >>> # Different dimension ordering (e.g., depth, elevation, lateral) >>> viewer, layer = plot_napari(data, dim_order=("y", "z", "x")) ``` ```pycon >>> # Add a second dataset as a new layer in an existing viewer >>> viewer, layer = plot_napari(data1) >>> viewer, layer = plot_napari(data2, viewer=viewer) ``` ```pycon >>> # Display ROI labels (e.g., segmentation mask) >>> roi_mask = cf.load("roi_mask.nii.gz") >>> viewer, layer = plot_napari(roi_mask, layer_type="labels") ``` ```pycon >>> # Overlay labels on existing image >>> viewer, layer = plot_napari(data) >>> viewer, layer = plot_napari(roi_mask, viewer=viewer, layer_type="labels") ``` ### plot\_stat\_map ```python plot_stat_map( stat_map: DataArray, *, bg_volume: DataArray | None = None, slice_coords: list[Hashable] | None = None, slice_mode: str = "z", transpose: bool = False, bg_kwargs: dict[str, Any] | None = None, cmap: str | Colormap | None = None, norm: Normalize | None = None, vmin: float | None = None, vmax: float | None = None, auto_range: bool = True, alpha: float | DataArray | None = None, threshold: float | None = None, threshold_mode: Literal["lower", "upper"] = "lower", show_colorbar: bool = True, cbar_label: str | None = None, cbar_kwargs: dict[str, Any] | None = None, show_titles: bool = True, show_axis_labels: bool = True, show_axis_ticks: bool = True, show_axes: bool = True, fontsize: float | None = None, yincrease: bool = False, xincrease: bool = True, bg_color: str = "black", fg_color: str | None = None, figure: Figure | None = None, axes: NDArray[Any] | Axes | None = None, nrows: int | None = None, ncols: int | None = None, dpi: int | None = None, resample_interpolation: Literal[ "linear", "nearest", "bspline" ] = "linear", resample_fill_value: float | None = None, ) -> VolumePlotter ``` Plot a statistical map, optionally over a background volume. Performs the recurring pattern of `plot_volume` to show a background anatomical volume + `VolumePlotter.add_stat_map` to overlay a statistical map, with the colormap and range picked automatically based on whether the statistic is diverging (has both positive and negative values) or one-signed. Parameters: - #### **`stat_map`** (`DataArray`) – Statistical map to plot. 3D volume data. Unitary dimensions (except `slice_mode`) are squeezed before processing. - #### **`bg_volume`** (`DataArray`, default: `None` ) – Background anatomical volume, plotted underneath `stat_map`. When `alpha` is not provided, `stat_map` fully covers `bg_volume` wherever it has a value; `bg_volume` only shows through where `stat_map` is masked out by `threshold`. Lower `alpha` to blend the two layers instead. Must share `slice_mode` and, after squeezing, the same display dimensions as `stat_map`. If not provided, `stat_map` is plotted on its own. - #### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Coordinate values along `slice_mode` at which to extract slices. Numeric coordinates are matched by nearest-neighbour lookup; non-numeric coordinates (e.g. region labels) require an exact match. If not provided, all coordinate values from `bg_volume` (or `stat_map` when `bg_volume` is not provided) along `slice_mode` are used. - #### **`slice_mode`** (`str`, default: `"z"` ) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension to slice. Native voxel dimensions (`"k"`, `"j"`, `"i"`) are not valid slice modes. After slicing, each panel must be 2D. - #### **`transpose`** (`bool`, default: `False` ) – Whether to swap the row/column display dims of each slice panel. - #### **`bg_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments forwarded to `plot_volume` for the background layer (e.g. `cmap`, `vmin`, `vmax`, `norm`, `alpha`, `roi_labels`). Ignored when `bg_volume` is not provided. Layout and text styling (`slice_coords`, `slice_mode`, `show_titles`, `fontsize`, etc.) are controlled by this function's own parameters instead, so that both layers share consistent styling. - #### **`cmap`** (`str or Colormap`, default: `None` ) – Colormap for `stat_map`. If not provided, the default depends on `auto_range` and the sign of `stat_map` (see below); an explicit `cmap` is always used as-is regardless of `auto_range`. - #### **`norm`** (`Normalize`, default: `None` ) – Normalization instance (e.g. `TwoSlopeNorm`, `BoundaryNorm`, `LogNorm`) for cases `vmin`/`vmax`/`auto_range` can't express. When provided, `vmin`, `vmax`, and `auto_range`'s range computation are bypassed entirely; `cmap` still follows the usual rules above. - #### **`vmin`** (`float`, default: `None` ) – Lower bound of the colormap. If not provided, defaults to the minimum value of `stat_map`, computed over the full array rather than just the displayed slices. Ignored when `norm` is provided, when `auto_range=True` and `stat_map` has only non-negative values, or when `auto_range=True`, `stat_map` spans both signs and `vmax` is given on its own (see `auto_range`). - #### **`vmax`** (`float`, default: `None` ) – Upper bound of the colormap. If not provided, defaults to the maximum value of `stat_map`, computed over the full array rather than just the displayed slices. Ignored when `norm` is provided, when `auto_range=True` and `stat_map` has only non-positive values, or when `auto_range=True`, `stat_map` spans both signs and `vmin` is given on its own (see `auto_range`). - #### **`auto_range`** (`bool`, default: `True` ) – Whether to pick the colormap range and default colormap automatically based on the sign of `stat_map`: - Both positive and negative values: diverging, symmetric `[-m, m]` range where `m = max(|vmin|, |vmax|)` over the bounds actually provided, falling back to the largest magnitude in `stat_map` when neither is given, with `cmap` defaulting to `"coolwarm"` — the right choice for diverging statistics where the sign is meaningful (e.g. t-statistics, correlation coefficients, PCA/ICA component maps). - Only non-negative values: sequential `[0, vmax]` range, with `cmap` defaulting to `"viridis"` — the right choice for non-diverging statistics where only magnitude matters (e.g. R², F-statistics). - Only non-positive values: sequential `[vmin, 0]` range, with `cmap` defaulting to `"viridis_r"` (reversed, so that values near zero map to the same end of the colormap in both the non-negative and non-positive cases). Set to `False` to use the resolved `vmin`/`vmax` directly with no zero-anchoring (`cmap` then defaults to `"coolwarm"` regardless of sign). - #### **`alpha`** (`float or DataArray`, default: `None` ) – Opacity of the `stat_map` overlay: a single scalar value, or a 3D DataArray sharing `stat_map`'s dims, shape, and coordinates (for independent per-slice, per-voxel opacity, e.g. to fade out low-magnitude voxels instead of masking them out with `threshold`). A per-voxel opacity must be a DataArray, not a bare array, so it can be validated and aligned against `stat_map`; note it is validated against `stat_map`, not `bg_volume`. If not provided, the colormap's own alpha channel is respected. - #### **`threshold`** (`float`, default: `None` ) – Threshold applied to `|stat_map|`. See `threshold_mode` for the masking direction. If not provided, no thresholding is applied. - #### **`threshold_mode`** (`(lower, upper)`, default: `"lower"` ) – Controls how `threshold` is applied: - `"lower"`: set pixels where `|stat_map| < threshold` to NaN. - `"upper"`: set pixels where `|stat_map| > threshold` to NaN. - #### **`show_colorbar`** (`bool`, default: `True` ) – Whether to add a shared colorbar for `stat_map` to the figure. - #### **`cbar_label`** (`str`, default: `None` ) – Label for the colorbar. - #### **`cbar_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments forwarded to `matplotlib.figure.Figure.colorbar` (e.g. `shrink`, `fraction`, `pad`, `aspect`). Useful to shrink the colorbar when it spans a multi-panel grid, since the defaults are sized for a single axes. - #### **`show_titles`** (`bool`, default: `True` ) – Whether to display subplot titles showing the slice coordinate. - #### **`show_axis_labels`** (`bool`, default: `True` ) – Whether to display axis labels (with units when available). - #### **`show_axis_ticks`** (`bool`, default: `True` ) – Whether to display axis tick labels. - #### **`show_axes`** (`bool`, default: `True` ) – Whether to show all axis decorations (spines, ticks, labels). When `False`, overrides `show_axis_labels` and `show_axis_ticks`. - #### **`fontsize`** (`float`, default: `None` ) – Base font size for all text elements. Subplot titles use `fontsize` directly; axis labels and the colorbar label use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - #### **`yincrease`** (`bool`, default: `False` ) – Whether the y-axis increases upward (`True`) or downward (`False`). - #### **`xincrease`** (`bool`, default: `True` ) – Whether the x-axis increases to the right (`True`) or left (`False`). - #### **`bg_color`** (`str`, default: `"black"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - #### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - #### **`figure`** (`Figure`, default: `None` ) – Existing figure to draw into. If not provided, a new figure is created. - #### **`axes`** (`ndarray or Axes`, default: `None` ) – Existing axes to draw into: either a single `matplotlib.axes.Axes` or a 2D array of them. Must contain exactly as many elements as there are slices. A single `Axes` is wrapped automatically and limits the plot to one slice. If not provided, new axes are created inside `figure`. - #### **`nrows`** (`int`, default: `None` ) – Number of rows in the subplot grid. If not provided, computed automatically. - #### **`ncols`** (`int`, default: `None` ) – Number of columns in the subplot grid. If not provided, computed automatically. - #### **`dpi`** (`int`, default: `None` ) – Figure resolution in dots per inch. Ignored when `figure` is provided. - #### **`resample_interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation method used when resampling oblique (non-axis-aligned) voxel-to-world `stat_map`/`bg_volume` onto an axis-aligned world grid for display. Applied to both, since they share one `VolumePlotter`. - #### **`resample_fill_value`** (`float`, default: `None` ) – Value assigned to voxels outside `stat_map`/`bg_volume`'s field of view after display resampling. If not provided, defaults to each array's own minimum value. Returns: - `VolumePlotter` – Object managing the figure, axes, and coordinate mapping for overlays. Raises: - `ValueError` – If `slice_mode` is not a dimension of `stat_map` or `bg_volume`. - `ValueError` – If `stat_map` or `bg_volume` is not 3D after squeezing unitary dimensions. > [!NOTE] > > **Notes** > > When `bg_volume` is provided, this is equivalent to calling `plot_volume(bg_volume, show_colorbar=False, ...)` followed by `plotter.add_stat_map(stat_map, ...)`. Use `VolumePlotter.add_stat_map` directly to overlay a statistical map onto an existing plot, or `VolumePlotter.add_volume` for full manual control over the colormap and range. Examples: ```pycon >>> import xarray as xr >>> from confusius.plotting import plot_stat_map >>> anatomical = xr.open_zarr("output.zarr")["power_doppler"] >>> t_map = xr.open_zarr("output.zarr")["t_stat"] >>> plotter = plot_stat_map(t_map, bg_volume=anatomical, slice_mode="z") ``` ```pycon >>> # Suppress subthreshold voxels and cap the colormap range explicitly. >>> plotter = plot_stat_map( ... t_map, ... bg_volume=anatomical, ... threshold=3.0, ... vmax=6.0, ... cbar_label="t-statistic", ... ) ``` ```pycon >>> # Blend the overlay with the background instead of fully covering it. >>> plotter = plot_stat_map(t_map, bg_volume=anatomical, alpha=0.6) ``` ```pycon >>> # Non-diverging statistic (e.g. R²): sequential range and colormap picked >>> # automatically since r2_map has only non-negative values. >>> r2_map = xr.open_zarr("output.zarr")["r2"] >>> plotter = plot_stat_map(r2_map, bg_volume=anatomical) ``` ```pycon >>> # No background: plot the statistical map on its own. >>> plotter = plot_stat_map(t_map, slice_mode="z") ``` ### plot\_volume ```python plot_volume( data: DataArray, *, slice_coords: list[Hashable] | None = None, slice_mode: str | None = None, transpose: bool = False, cmap: str | Colormap | None = None, norm: Normalize | None = None, vmin: float | None = None, vmax: float | None = None, threshold: float | None = None, threshold_mode: Literal["lower", "upper"] = "lower", alpha: float | DataArray | None = None, show_colorbar: bool = True, cbar_label: str | None = None, cbar_kwargs: dict[str, Any] | None = None, roi_labels: dict[int, str] | None = None, show_titles: bool = True, show_axis_labels: bool = True, show_axis_ticks: bool = True, show_axes: bool = True, fontsize: float | None = None, yincrease: bool = False, xincrease: bool = True, bg_color: str = "black", fg_color: str | None = None, figure: Figure | None = None, axes: NDArray[Any] | Axes | None = None, nrows: int | None = None, ncols: int | None = None, dpi: int | None = None, resample_interpolation: Literal[ "linear", "nearest", "bspline" ] = "linear", resample_fill_value: float | None = None, ) -> VolumePlotter ``` Plot 2D slices of a volume using matplotlib. Displays a series of 2D slices extracted along `slice_mode` as a grid of subplots. Each slice is rendered using world coordinates for axis ticks when available. If `slice_mode` is not provided and `data` is planar, the singleton world dimension is used; otherwise the default is `"z"`. Parameters: - #### **`data`** (`DataArray`) – Input data array. Unitary non-world dimensions are squeezed before processing; singleton world display axes are preserved. After that, data must be 3D. Complex-valued data is converted to magnitude before display. - #### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Coordinate values along `slice_mode` at which to extract slices. Numeric coordinates are matched by nearest-neighbour lookup; non-numeric coordinates (e.g. region labels) require an exact match. If not provided, all coordinate values along `slice_mode` are used. - #### **`slice_mode`** (`str`, default: `None` ) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension to slice. Native voxel dimensions (`"k"`, `"j"`, `"i"`) are not valid slice modes. If not provided, planar data is sliced along its singleton world dimension and full 3D data is sliced along `"z"`. After slicing, each panel must be 2D. - #### **`transpose`** (`bool`, default: `False` ) – Whether to swap the row/column display dims of each slice panel. - #### **`cmap`** (`str or Colormap`, default: `None` ) – Colormap. When not provided, falls back to `data.attrs["cmap"]` if present, otherwise `"gray"`. - #### **`norm`** (`Normalize`, default: `None` ) – Normalization instance (e.g. `BoundaryNorm` for integer label maps such as atlas annotations). When not provided, falls back to `data.attrs["norm"]` if present. When a norm is active, `vmin` and `vmax` are ignored. - #### **`vmin`** (`float`, default: `None` ) – Lower bound of the colormap. Defaults to the 2nd percentile. Ignored when a norm is active. - #### **`vmax`** (`float`, default: `None` ) – Upper bound of the colormap. Defaults to the 98th percentile. Ignored when a norm is active. - #### **`threshold`** (`float`, default: `None` ) – Threshold applied to `|data|`. See `threshold_mode` for the masking direction. If not provided, no thresholding is applied. - #### **`threshold_mode`** (`(lower, upper)`, default: `"lower"` ) – Controls how `threshold` is applied: - `"lower"`: set pixels where `|data| < threshold` to NaN. - `"upper"`: set pixels where `|data| > threshold` to NaN. - #### **`alpha`** (`float or DataArray`, default: `None` ) – Opacity of the image: a single scalar value, or a 3D DataArray sharing `data`'s dims, shape, and coordinates (for independent per-slice, per-voxel opacity). A per-voxel opacity must be a DataArray, not a bare array, so it can be validated and aligned against `data`. If not provided, the colormap's own alpha channel is respected. - #### **`show_colorbar`** (`bool`, default: `True` ) – Whether to add a shared colorbar to the figure. - #### **`cbar_label`** (`str`, default: `None` ) – Label for the colorbar. - #### **`cbar_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments forwarded to `matplotlib.figure.Figure.colorbar` (e.g. `shrink`, `fraction`, `pad`, `aspect`). Useful to shrink the colorbar when it spans a multi-panel grid, since the defaults are sized for a single axes. - #### **`roi_labels`** (`dict[int, str]`, default: `None` ) – Mapping from integer label to display name. When provided (or when `data.attrs["roi_labels"]` is populated), hovering the cursor over a voxel shows `= ()` in the matplotlib status bar. - #### **`show_titles`** (`bool`, default: `True` ) – Whether to display subplot titles showing the slice coordinate. - #### **`show_axis_labels`** (`bool`, default: `True` ) – Whether to display axis labels (with units when available). - #### **`show_axis_ticks`** (`bool`, default: `True` ) – Whether to display axis tick labels. - #### **`show_axes`** (`bool`, default: `True` ) – Whether to show all axis decorations (spines, ticks, labels). When `False`, overrides `show_axis_labels` and `show_axis_ticks`. - #### **`fontsize`** (`float`, default: `None` ) – Base font size for all text elements. Subplot titles use `fontsize` directly; axis labels and the colorbar label use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - #### **`yincrease`** (`bool`, default: `False` ) – Whether the y-axis increases upward (`True`) or downward (`False`). - #### **`xincrease`** (`bool`, default: `True` ) – Whether the x-axis increases to the right (`True`) or left (`False`). - #### **`bg_color`** (`str`, default: `"black"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - #### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - #### **`figure`** (`Figure`, default: `None` ) – Existing figure to draw into. If not provided, a new figure is created. - #### **`axes`** (`ndarray or Axes`, default: `None` ) – Existing axes to draw into: either a single `matplotlib.axes.Axes` or a 2D array of them. Must contain exactly as many elements as there are slices. A single `Axes` is wrapped automatically and limits the plot to one slice. If not provided, new axes are created inside `figure`. - #### **`nrows`** (`int`, default: `None` ) – Number of rows in the subplot grid. If not provided, computed automatically. - #### **`ncols`** (`int`, default: `None` ) – Number of columns in the subplot grid. If not provided, computed automatically. - #### **`dpi`** (`int`, default: `None` ) – Figure resolution in dots per inch. Ignored when `figure` is provided. - #### **`roi_labels`** (`dict[int, str]`, default: `None` ) – Mapping from integer label to display name. When provided (or when `data.attrs["roi_labels"]` is populated), hovering the cursor over a voxel shows `= ()` in the matplotlib status bar. - #### **`resample_interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation method used when resampling oblique (non-axis-aligned) voxel-to-world `data` onto an axis-aligned world grid for display. - #### **`resample_fill_value`** (`float`, default: `None` ) – Value assigned to voxels outside `data`'s field of view after resampling oblique data. If not provided, defaults to `data`'s own minimum value. Returns: - `VolumePlotter` – Object managing the figure, axes, and coordinate mapping for overlays. Raises: - `ValueError` – If `slice_mode` is not a dimension of `data`. - `ValueError` – If `data` is not 3D after squeezing unitary dimensions. - `ValueError` – If `axes` is provided but does not contain enough elements for all slices. > [!NOTE] > > **Notes** > > Rendering is done with `pcolormesh`, which accepts coordinate arrays directly and therefore handles non-uniform coordinate spacing correctly. Because each panel is drawn in world coordinate space, multiple calls with different `axes` elements will overlay correctly as long as the displayed dimensions are the same. > > The two dimensions that remain after slicing define the panel axes: the first remaining dimension maps to the vertical axis and the second to the horizontal axis. Coordinates are used directly as axis tick values; each axis has `aspect="equal"` so that 1 unit in x matches 1 unit in y. > > NaN and Inf values (including those introduced by `threshold`) are rendered transparently via a masked array. > > When the figure is created internally, `layout="constrained"` is used so that subplot titles, axis labels, tick labels, and the colorbar are spaced automatically without overlapping. When an external `figure` or `axes` is provided, layout management is left to the caller. Examples: ```pycon >>> import xarray as xr >>> from confusius.plotting import plot_volume >>> data = xr.open_zarr("output.zarr")["power_doppler"] >>> plotter = plot_volume(data, slice_mode="z") ``` ```pycon >>> # Select specific z slices. >>> plotter = plot_volume(data, slice_coords=[0.0, 1.5, 3.0], slice_mode="z") ``` ```pycon >>> # Threshold noise and label the colorbar. >>> plotter = plot_volume( ... data, ... slice_mode="z", ... threshold=0.5, ... threshold_mode="lower", ... cbar_label="Power (dB)", ... ) ``` # `confusius.qc` ## qc Quality control metrics for functional ultrasound imaging. Modules: - **`dvars`** – DVARS computation for quality control. - **`tsnr`** – Temporal SNR and coefficient of variation computation for quality control. Functions: - **`compute_cv`** – Compute the coefficient of variation (CV). - **`compute_dvars`** – Compute the DVARS metric. - **`compute_tsnr`** – Compute the temporal signal-to-noise ratio (tSNR). ### compute\_cv ```python compute_cv(signals: DataArray) -> DataArray ``` Compute the coefficient of variation (CV). CV is the ratio of the temporal standard deviation to the temporal mean, computed voxel-wise. It is the inverse of tSNR and provides a normalized measure of temporal variability that is independent of signal magnitude. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Signals to compute CV from. Must have a `time` dimension. Additional dimensions represent spatial locations (e.g., `space`, `k`/`j`/`i`). Returns: - `DataArray` – Spatial map of CV values with the `time` dimension reduced. Voxels with zero temporal mean yield `inf` (or `NaN` if the standard deviation is also zero). The returned array has `long_name="Coefficient of variation"`, `units="a.u."`, and `cmap="viridis"` in its `attrs`. Raises: - `ValueError` – If `signals` has no `time` dimension or if the `time` dimension has only 1 timepoint. > [!NOTE] > > **Notes** > > The coefficient of variation is defined as: > > \\\[\\text{CV} = \\frac{\\sigma\_x}{\\bar{x}}\\\] > > where \\(\\sigma\_x\\) is the temporal standard deviation (computed with `ddof=0`) and \\(\\bar{x}\\) is the temporal mean. This is the inverse of the temporal signal-to-noise ratio (see `compute_tsnr`). Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> from confusius.qc import compute_cv >>> rng = np.random.default_rng(42) >>> data = rng.standard_normal((100, 50)) + 10.0 >>> signals = xr.DataArray( ... data, ... dims=["time", "space"], ... coords={"time": np.arange(100) * 0.1}, ... ) >>> cv = compute_cv(signals) >>> cv.shape (50,) ``` ### compute\_dvars ```python compute_dvars( signals: DataArray, standardize: bool = True, normalization_factor: float | None = 1000, remove_zero_variance: bool = True, variance_tolerance: float = 0.0, ) -> DataArray ``` Compute the DVARS metric. DVARS (D referring to temporal derivative, VARS to RMS variance) measures how much the intensity at one time point changes relative to the previous time point. The standardized version accounts for the expected variance given the temporal autocorrelation structure of the data. This function was adapted from `nipype.algorithms.confounds.compute_dvars`. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Signals to compute DVARS from. Must have a `time` dimension. Any additional dimensions (e.g., `pose`, `k`, `j`, `i`) are flattened to a single `space` dimension before computation. - #### **`standardize`** (`bool`, default: `True` ) – Whether to computed the standardized DVARS. When `True`, the DVARS values are standardized by the expected standard deviation given the AR(1) structure of the data. When `False`, return non-standardized DVARS (raw RMS of temporal differences). - #### **`normalization_factor`** (`float or None`, default: `1000` ) – Normalization factor applied to the median signal intensity. If `None`, no normalization is performed. Note that standardized DVARS is invariant to intensity normalization. - #### **`remove_zero_variance`** (`bool`, default: `True` ) – Whether to exclude signals with near-zero variance. Zero-variance signals can artificially reduce the median during intensity normalization and should typically be removed. - #### **`variance_tolerance`** (`float`, default: `0.0` ) – Tolerance for identifying zero-variance signals. Signals with robust standard deviation (`IQR/1.349`) less than or equal to this value are considered to have zero variance. Returns: - `(time,) xarray.DataArray` – DVARS values with time coordinates matching input. When `standardized` is `True`, returns standardized DVARS. The first element is set to the minimum DVARS value (following FSL convention). > [!NOTE] > > **Notes** > > **Laziness**: This function cannot preserve Dask laziness because it requires: (1) robust standard deviation via IQR (scipy function, not Dask-compatible), and (2) AR(1) coefficient estimation requiring full time series. The computation is executed immediately and returned as a DataArray with computed values. > > The standardized DVARS metric was introduced by Nichols (2013) to provide a normalized measure of DVARS that accounts for the temporal autocorrelation in fMRI data. > > The computation involves: > > 1. Robust standard deviation estimation using IQR (following FSL's "lower" interpolation method). > 2. Optional removal of zero-variance signals. > 3. Optional intensity normalization by median scaling. > 4. AR(1) coefficient estimation via Yule-Walker equations (standardized only). > 5. Computation of expected variance for temporal differences given AR(1) structure (standardized only). > 6. Standardization by dividing observed DVARS by expected DVARS (standardized only). **References** 1. Smyser, Christopher D., et al. "Functional Connectivity MRI in Infants: Exploration of the Functional Organization of the Developing Brain." NeuroImage, vol. 56, no. 3, June 2011, pp. 1437–52. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2011.02.073](). 2. Power, Jonathan D., et al. "Spurious but Systematic Correlations in Functional Connectivity MRI Networks Arise from Subject Motion." NeuroImage, vol. 59, no. 3, Feb. 2012, pp. 2142–54. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2011.10.018](). 3. Nichols, Thomas E. "Notes on Creating a Standardized Version of DVARS." arXiv, 2017. DOI.org (Datacite), [https://doi.org/10.48550/ARXIV.1704.01469](). 4. Afyouni, Soroosh, and Thomas E. Nichols. "Insight and Inference for DVARS." NeuroImage, vol. 172, May 2018, pp. 291–312. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2017.12.098](). Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> from confusius.qc import compute_dvars >>> # Generate synthetic 2D signals: 100 time points, 50 voxels >>> rng = np.random.default_rng(42) >>> data = rng.standard_normal((100, 50)) >>> # Add AR(1) structure >>> for t in range(1, 100): ... data[t] = 0.5 * data[t - 1] + 0.5 * data[t] >>> signals = xr.DataArray( ... data, ... dims=["time", "space"], ... coords={"time": np.arange(100) * 0.1}, ... ) >>> # Compute standardized DVARS (default) >>> dvars_stdz = compute_dvars(signals) >>> dvars_stdz.shape (100,) >>> # Compute non-standardized DVARS >>> dvars_nstd = compute_dvars(signals, standardize=False) >>> dvars_nstd.shape (100,) ``` ### compute\_tsnr ```python compute_tsnr(signals: DataArray) -> DataArray ``` Compute the temporal signal-to-noise ratio (tSNR). tSNR is defined as the ratio of the temporal mean to the temporal standard deviation, computed voxel-wise. It provides a spatial map of signal stability over time: higher tSNR indicates a more stable signal relative to its mean. > [!WARNING] > > **Limited usefulness of tSNR for power Doppler** > > The intrinsic positive correlation between temporal average and standard deviation of power Doppler signals creates a paradoxical situation where areas with low signal (such as "shadow zones" due to skull attenuation, or the ultrasound gel) exhibit high tSNR values, while the vasculature itself shows low tSNR. This relationship fundamentally differs from other neuroimaging modalities like fMRI, where higher tSNR generally indicates better signal quality. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Signals to compute tSNR from. Must have a `time` dimension. Additional dimensions represent spatial locations (e.g., `space`, `k`/`j`/`i`). Returns: - `DataArray` – Spatial map of tSNR values with the `time` dimension reduced. Voxels with zero temporal standard deviation (constant signals) yield `inf`. The returned array has `long_name="Temporal signal-to-noise ratio"`, `units="a.u."`, and `cmap="viridis"` in its `attrs`. Raises: - `ValueError` – If `signals` has no `time` dimension or if the `time` dimension has only 1 timepoint. > [!NOTE] > > **Notes** > > The tSNR is defined as: > > \\\[\\text{tSNR} = \\frac{\\bar{x}}{\\sigma\_x}\\\] > > where \\(\\bar{x}\\) is the temporal mean and \\(\\sigma\_x\\) is the temporal standard deviation (computed with `ddof=0`). This is the inverse of the coefficient of variation (see `compute_cv`). **References** 1. Murphy, Kevin, et al. “How Long to Scan? The Relationship between fMRI Temporal Signal to Noise Ratio and Necessary Scan Duration.” NeuroImage, vol. 34, no. 2, Jan. 2007, pp. 565–74. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2006.09.032](). 2. Welvaert, Marijke, and Yves Rosseel. “On the Definition of Signal-To-Noise Ratio and Contrast-To-Noise Ratio for fMRI Data.” PLOS ONE, vol. 8, no. 11, Nov. 2013, p. e77089. PLoS Journals, [https://doi.org/10.1371/journal.pone.0077089](). 3. Le Meur-Diebolt, Samuel, et al. “Robust Functional Ultrasound Imaging in the Awake and Behaving Brain: A Systematic Framework for Motion Artifact Removal.” 17 June 2025. Neuroscience, [https://doi.org/10.1101/2025.06.16.659882](). Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> from confusius.qc import compute_tsnr >>> rng = np.random.default_rng(42) >>> data = rng.standard_normal((100, 50)) + 10.0 >>> signals = xr.DataArray( ... data, ... dims=["time", "space"], ... coords={"time": np.arange(100) * 0.1}, ... ) >>> tsnr = compute_tsnr(signals) >>> tsnr.shape (50,) ``` # `confusius.registration` ## registration Registration module for fUSI data. Modules: - **`affines`** – Affine matrix decomposition and composition utilities. - **`bspline`** – B-spline transform helpers for fUSI registration. - **`diagnostics`** – Diagnostics returned alongside a registration result. - **`motion`** – Motion parameter estimation and framewise displacement computation. - **`progress`** – Registration progress visualization. - **`resampling`** – Volume resampling utilities for fUSI data. - **`volume`** – Volume-to-volume registration for fUSI data. - **`volumewise`** – Volumewise registration for fUSI data. - **`volumewise_progress`** – Progress reporting protocol for `register_volumewise`. Classes: - **`MatplotlibRegistrationProgressPlotter`** – Plot registration progress in real time. - **`RegistrationDiagnostics`** – Per-call diagnostics for a registration optimization. - **`RegistrationProgress`** – Duck-typed contract for an iteration progress reporter. - **`VolumewiseProgressReporter`** – Duck-typed contract for `register_volumewise` progress reporting. Functions: - **`compose_affine`** – Compose translations, rotations, zooms, and shears into an affine matrix. - **`compute_framewise_displacement`** – Compute framewise displacement from affine transforms. - **`create_motion_dataframe`** – Create a DataFrame with motion parameters and framewise displacement. - **`decompose_affine`** – Decompose a 4x4 homogeneous affine into translation, rotation, zoom, shear. - **`extract_motion_parameters`** – Extract motion parameters from affine matrices. - **`initialize_single_slice_rigid_transform`** – Build a rigid fixed-to-moving initializer for single-slice voxel-to-world volumes. - **`invert_displacement_field`** – Invert a dense displacement field DataArray with SimpleITK. - **`register_volume`** – Register a single 3D volume to a fixed reference. - **`register_volumewise`** – Register all volumes to a reference time point or a reference volume. - **`resample_like`** – Resample a volume onto the grid of a reference DataArray. - **`resample_volume`** – Resample a volume onto an explicit output grid using a pre-computed transform. - **`sample_displacement_field`** – Sample a registration transform onto an explicit output grid. - **`sample_displacement_field_like`** – Sample a registration transform onto the grid of a reference DataArray. ### MatplotlibRegistrationProgressPlotter Plot registration progress in real time. Displays an optimizer metric curve, a composite fixed/moving overlay, or both, updated at every iteration. Works in both a Jupyter notebook and an interactive matplotlib backend (e.g. Qt). Parameters: - #### **`registration_method`** (`ImageRegistrationMethod`) – The registration method whose progress to monitor. - #### **`fixed_img`** (`Image`) – The fixed (reference) image, used to resample the composite view. - #### **`moving_img`** (`Image`) – The moving image, used to resample the composite view. - #### **`plot_metric`** (`bool`, default: `True` ) – Whether to display the optimizer metric over iterations. - #### **`plot_composite`** (`bool`, default: `True` ) – Whether to display a blended fixed/moving composite at each iteration. Requires an additional `sitk.Resample` call per iteration. - #### **`resample_kwargs`** (`dict`, default: `None` ) – Extra keyword arguments for the internal resample call at each iteration. Supported keys are `interpolation`, `fill_value`, and `sitk_threads`. - #### **`max_composite_slices`** (`int or None`, default: `9` ) – Maximum number of slices to draw in the composite mosaic for 3D volumes, evenly spaced along the volume's slice axis. Keeps the mosaic grid readable and each iteration's render cheap for volumes with many slices. Use `None` to plot every slice. Ignored for 2D volumes. Methods: - **`close`** – Finalize the plot when registration ends. - **`update`** – Update the plot with the current iteration's data. Attributes: - **`figure`** (`Figure`) – The matplotlib figure used for plotting. - **`metric_values`** (`list[float]`) – Optimizer metric value recorded at each iteration. #### figure ```python figure: Figure ``` The matplotlib figure used for plotting. Returns: - `Figure` – The figure instance owned by this monitor. #### metric\_values ```python metric_values: list[float] ``` Optimizer metric value recorded at each iteration. Returns: - `list of float` – Copy of the internal metric value buffer. #### close ```python close() -> None ``` Finalize the plot when registration ends. Called at `sitkEndEvent`. #### update ```python update() -> None ``` Update the plot with the current iteration's data. Called at every `sitkIterationEvent`. ### RegistrationDiagnostics Per-call diagnostics for a registration optimization. Returned as the third element of `register_volume`. Useful for plotting metric convergence curves, comparing runs, and detecting frames that failed to converge. Attributes: - **`metric`** (`{'correlation', 'mattes_mi'}`) – Similarity metric used during optimization, echoed from `register_volume`'s `metric` argument. Determines the sign and scale of `metric_values` (SimpleITK always minimizes, so both supported metrics are recorded as negative values). - **`metric_values`** (`(n_iterations,) numpy.ndarray`) – Similarity metric value recorded at each optimizer iteration, in chronological order. - **`final_metric_value`** (`float`) – Last value of the metric recorded by the optimizer. Equal to `metric_values[-1]` when at least one iteration ran. - **`n_iterations`** (`int`) – Number of iterations actually performed by the optimizer. May be smaller than `register_volume`'s `number_of_iterations` if the optimizer converged early. - **`stop_condition`** (`str`) – Human-readable description of the optimizer stop condition. When the registration completes normally, this is SimpleITK's `GetOptimizerStopConditionDescription`. When the registration is aborted cooperatively, this is a short abort message. - **`status`** (`{'completed', 'aborted'}`) – Whether the registration ran to completion or returned an intermediate result after cooperative cancellation. ### RegistrationProgress Bases: `Protocol` Duck-typed contract for an iteration progress reporter. Implementations are called from the registration thread (SimpleITK's iteration/end callbacks). They must be safe to call from a non-GUI thread; any GUI side effects must be marshalled via Qt signals or similar. Methods: - **`close`** – Called once at the registration end event. - **`update`** – Called at every optimizer iteration event. #### close ```python close() -> None ``` Called once at the registration end event. #### update ```python update() -> None ``` Called at every optimizer iteration event. ### VolumewiseProgressReporter Bases: `Protocol` Duck-typed contract for `register_volumewise` progress reporting. Implementations may be called from worker threads when volumewise registration runs in parallel. Any GUI updates must therefore be marshalled via thread-safe mechanisms such as Qt signals. Methods: - **`close`** – Report that the full volumewise run has ended. - **`frame_completed`** – Report that one frame finished and provide its registered output. #### close ```python close() -> None ``` Report that the full volumewise run has ended. #### frame\_completed ```python frame_completed( frame_index: int, registered_frame: DataArray, diagnostics: RegistrationDiagnostics, ) -> None ``` Report that one frame finished and provide its registered output. Parameters: - ##### **`frame_index`** (`int`) – Index of the completed frame. - ##### **`registered_frame`** (`DataArray`) – Registered frame output. - ##### **`diagnostics`** (`RegistrationDiagnostics`) – Diagnostics collected for the completed frame. ### compose\_affine ```python compose_affine( T: NDArray[floating], R: NDArray[floating], Z: NDArray[floating], S: NDArray[floating] | None = None, ) -> NDArray[floating] ``` Compose translations, rotations, zooms, and shears into an affine matrix. Parameters: - #### **`T`** (`(N,) numpy.ndarray`) – Translation vector, where `N` is usually 3 (3D case). - #### **`R`** (`(N, N) numpy.ndarray`) – Rotation matrix, where `N` is usually 3 (3D case). - #### **`Z`** (`(N,) numpy.ndarray`) – Zoom (scale) vector, where `N` is usually 3 (3D case). - #### **`S`** (`(P,) numpy.ndarray`, default: `None` ) – Shear vector filling the upper triangle above the diagonal of the shear matrix. `P` is the `(N-2)`-th triangular number (3 for the 3D case). If not provided, no shear is applied. Returns: - `(N+1, N+1) numpy.ndarray` – Homogeneous affine transformation matrix. > [!NOTE] > > **Notes** > > Adapted from `transforms3d.affines.compose` by Matthew Brett *et al*. (BSD-2-Clause License). See the `NOTICE` and `LICENSE-BSD-2-Clause` files for details. Source: https://github.com/matthew-brett/transforms3d ### compute\_framewise\_displacement ```python compute_framewise_displacement( affines: Sequence[NDArray[floating]], reference: DataArray, mask: NDArray[bool_] | None = None, ) -> dict[str, NDArray[floating]] ``` Compute framewise displacement from affine transforms. Framewise displacement measures how much voxels move between consecutive frames after registration. For each voxel, we compute the Euclidean distance between its position at frame t and frame t+1 after applying the affine transforms. Parameters: - #### **`affines`** (`list[ndarray]`) – List of affine matrices, one per frame. - #### **`reference`** (`DataArray`) – Spatial DataArray defining the world grid (spacing and origin derived from its coordinates). - #### **`mask`** (`ndarray`, default: `None` ) – Boolean mask indicating which voxels to include. If not provided, uses all voxels. Returns: - `dict` – Dictionary with keys: - `"mean_fd"`: Mean framewise displacement per frame. - `"max_fd"`: Maximum framewise displacement per frame. - `"rms_fd"`: RMS framewise displacement per frame. Raises: - `ValueError` – If `reference` contains a `time` dimension, fails VoxelData validation, or if `affines` is empty or contains an array that is not a `(4, 4)` homogeneous 3D affine. - `TypeError` – If any affine entry is not a `numpy.ndarray`. **References** 1. Power, J. D., Barnes, K. A., Snyder, A. Z., Schlaggar, B. L. & Petersen, S. E. Spurious but systematic correlations in functional connectivity MRI networks arise from subject motion. Neuroimage 59, 2142-2154 ### create\_motion\_dataframe ```python create_motion_dataframe( affines: Sequence[NDArray[floating]], reference: DataArray, mask: NDArray[bool_] | None = None, time_coords: NDArray[floating] | None = None, ) -> DataFrame ``` Create a DataFrame with motion parameters and framewise displacement. Parameters: - #### **`affines`** (`list[ndarray]`) – List of affine matrices from registration. - #### **`reference`** (`DataArray`) – Spatial-only VoxelData array defining the world grid for framewise displacement computation. - #### **`mask`** (`ndarray`, default: `None` ) – Boolean mask for FD computation. - #### **`time_coords`** (`ndarray`, default: `None` ) – Time coordinates for each frame. Returns: - `DataFrame` – DataFrame with the following columns: - `rot_x, rot_y, rot_z`: Rotation angles around the DataArray's `x`, `y`, and `z` axes, in radians. - `trans_x, trans_y, trans_z`: Translations along the DataArray's `x`, `y`, and `z` axes (mm), even when one spatial axis is singleton. - `mean_fd`: Mean framewise displacement (mm). - `max_fd`: Maximum framewise displacement (mm). - `rms_fd`: RMS framewise displacement (mm). Motion columns are reported in named-axis order (`x`, `y`, `z`), even when the input DataArray stores its spatial dimensions in a different order such as `(z, y, x)`. Raises: - `ValueError` – If `reference` contains a `time` dimension, fails VoxelData validation, or if `affines` is empty or contains an array that is not a `(4, 4)` homogeneous 3D affine. - `TypeError` – If any affine entry is not a `numpy.ndarray`. ### decompose\_affine ```python decompose_affine( A44: NDArray[floating], ) -> tuple[ NDArray[floating], NDArray[floating], NDArray[floating], NDArray[floating], ] ``` Decompose a 4x4 homogeneous affine into translation, rotation, zoom, shear. Decomposes `A44` into `T, R, Z, S` such that:: ```text Smat = np.array([[1, S[0], S[1]], [0, 1, S[2]], [0, 0, 1]]) RZS = R @ np.diag(Z) @ Smat A44[:3, :3] = RZS A44[:3, 3] = T ``` Parameters: - #### **`A44`** (`(4, 4) numpy.ndarray`) – Homogeneous affine matrix. Returns: - **`T`** ( `(3,) numpy.ndarray` ) – Translation vector. - **`R`** ( `(3, 3) numpy.ndarray` ) – Rotation matrix. - **`Z`** ( `(3,) numpy.ndarray` ) – Zoom (scale) vector. May have one negative zoom to avoid a negative determinant in `R`. - **`S`** ( `(3,) numpy.ndarray` ) – Shear vector `[sxy, sxz, syz]` filling the upper triangle of the shear matrix. Zero for pure rotation/zoom affines. > [!NOTE] > > **Notes** > > Adapted from `transforms3d.affines.decompose44` by Matthew Brett et al. (BSD-2-Clause License). See the `NOTICE` and `LICENSE-BSD-2-Clause` files for details. Source: https://github.com/matthew-brett/transforms3d > > See also: Spencer W. Thomas, "Decomposing a matrix into simple transformations", pp 320-323 in *Graphics Gems II*, James Arvo (ed.), Academic Press, 1991. ### extract\_motion\_parameters ```python extract_motion_parameters( affines: Sequence[NDArray[floating]], ) -> NDArray[floating] ``` Extract motion parameters from affine matrices. Decomposes each `(4, 4)` homogeneous 3D affine into translation and rotation parameters, extracting `[rotation_0, rotation_1, rotation_2, translation_0, translation_1, translation_2]`. Parameters: - #### **`affines`** (`list[ndarray]`) – List of `(4, 4)` affine matrices from registration. Returns: - `(n_frames, 6) numpy.ndarray` – Motion parameters array in raw transform-component order `(r0, r1, r2, t0, t1, t2)`. Raises: - `ValueError` – If `affines` is empty or contains an array that is not a `(4, 4)` homogeneous 3D affine. - `TypeError` – If any affine entry is not a `numpy.ndarray`. ### initialize\_single\_slice\_rigid\_transform ```python initialize_single_slice_rigid_transform( fixed: DataArray, moving: DataArray ) -> ndarray ``` Build a rigid fixed-to-moving initializer for single-slice voxel-to-world volumes. Parameters: - #### **`fixed`** (`DataArray`) – Fixed single-slice voxel-to-world volume. - #### **`moving`** (`DataArray`) – Moving single-slice voxel-to-world volume. Returns: - `(4, 4) numpy.ndarray` – Rigid transform in world space mapping fixed coordinates into moving coordinates. Raises: - `ValueError` – If either input is not a VoxelData array with a time-free, pose-free, 3D voxel-to-world grid (see `confusius.validation.ensure_voxeldata`). ### invert\_displacement\_field ```python invert_displacement_field( field: DataArray, *, max_iterations: int = 20, max_error_tolerance: float = 0.1, sitk_threads: int = -1, ) -> DataArray ``` Invert a dense displacement field DataArray with SimpleITK. Uses `InvertDisplacementFieldImageFilter`, a fixed-point iterative solver, on the same grid as `field`. The inverse maps world points in the opposite direction of `field`: if `field` is a fixed-to-moving pull transform, the returned field is (approximately) the corresponding moving-to-fixed pull transform. Parameters: - #### **`field`** (`DataArray`) – Dense displacement field DataArray as produced by `sample_displacement_field` or `sample_displacement_field_like`. - #### **`max_iterations`** (`int`, default: `20` ) – Maximum number of fixed-point iterations. - #### **`max_error_tolerance`** (`float`, default: `0.1` ) – Maximum error tolerance (mm) at which the solver is considered converged. - #### **`sitk_threads`** (`int`, default: `-1` ) – Number of threads SimpleITK may use internally. Negative values resolve to `max(1, os.cpu_count() + 1 + sitk_threads)`, so `-1` means all CPUs, `-2` means all minus one, and so on. Returns: - `DataArray` – Inverted displacement field DataArray on the same grid as `field`. Raises: - `ValueError` – If `field` does not look like a valid displacement field DataArray. ### register\_volume ```python register_volume( moving: DataArray, fixed: DataArray, *, fixed_mask: DataArray | None = ..., moving_mask: DataArray | None = ..., transform_type: Literal[ "translation", "rigid", "affine" ], metric: Literal["correlation", "mattes_mi"] = ..., fixed_intensity_scaling: Literal["none", "db", "sqrt"] | float = ..., moving_intensity_scaling: Literal["none", "db", "sqrt"] | float = ..., number_of_histogram_bins: int = ..., metric_sampling_percentage: float | None = ..., metric_sampling_seed: int | None = ..., learning_rate: float | Literal["auto"] = ..., number_of_iterations: int = ..., convergence_minimum_value: float = ..., convergence_window_size: int = ..., initialization: Literal[ "center_geometry", "center_moments" ] | NDArray[floating] | None = ..., optimizer_weights: list[float] | None = ..., mesh_size: tuple[int, int, int] = ..., use_multi_resolution: bool = ..., shrink_factors: Sequence[int] = ..., smoothing_sigmas: Sequence[int] = ..., resample: bool = ..., resample_interpolation: Literal[ "linear", "bspline" ] = ..., fill_value: float | None = ..., sitk_threads: int = ..., show_progress: bool = ..., plot_metric: bool = ..., plot_composite: bool = ..., max_composite_slices: int | None = ..., progress_plotter: Callable[..., RegistrationProgress] | None = None, abort_event: Event | None = ..., ) -> tuple[ DataArray, NDArray[floating], RegistrationDiagnostics ] ``` ```python register_volume( moving: DataArray, fixed: DataArray, *, fixed_mask: DataArray | None = ..., moving_mask: DataArray | None = ..., transform_type: Literal["bspline"], metric: Literal["correlation", "mattes_mi"] = ..., fixed_intensity_scaling: Literal["none", "db", "sqrt"] | float = ..., moving_intensity_scaling: Literal["none", "db", "sqrt"] | float = ..., number_of_histogram_bins: int = ..., metric_sampling_percentage: float | None = ..., metric_sampling_seed: int | None = ..., learning_rate: float | Literal["auto"] = ..., number_of_iterations: int = ..., convergence_minimum_value: float = ..., convergence_window_size: int = ..., initialization: Literal[ "center_geometry", "center_moments" ] | NDArray[floating] | None = ..., optimizer_weights: list[float] | None = ..., mesh_size: tuple[int, int, int] = ..., use_multi_resolution: bool = ..., shrink_factors: Sequence[int] = ..., smoothing_sigmas: Sequence[int] = ..., resample: bool = ..., resample_interpolation: Literal[ "linear", "bspline" ] = ..., fill_value: float | None = ..., sitk_threads: int = ..., show_progress: bool = ..., plot_metric: bool = ..., plot_composite: bool = ..., max_composite_slices: int | None = ..., progress_plotter: Callable[..., RegistrationProgress] | None = None, abort_event: Event | None = ..., ) -> tuple[DataArray, DataArray, RegistrationDiagnostics] ``` ```python register_volume( moving: DataArray, fixed: DataArray, *, fixed_mask: DataArray | None = ..., moving_mask: DataArray | None = ..., metric: Literal["correlation", "mattes_mi"] = ..., fixed_intensity_scaling: Literal["none", "db", "sqrt"] | float = ..., moving_intensity_scaling: Literal["none", "db", "sqrt"] | float = ..., number_of_histogram_bins: int = ..., metric_sampling_percentage: float | None = ..., metric_sampling_seed: int | None = ..., learning_rate: float | Literal["auto"] = ..., number_of_iterations: int = ..., convergence_minimum_value: float = ..., convergence_window_size: int = ..., initialization: Literal[ "center_geometry", "center_moments" ] | NDArray[floating] | None = ..., optimizer_weights: list[float] | None = ..., mesh_size: tuple[int, int, int] = ..., use_multi_resolution: bool = ..., shrink_factors: Sequence[int] = ..., smoothing_sigmas: Sequence[int] = ..., resample: bool = ..., resample_interpolation: Literal[ "linear", "bspline" ] = ..., fill_value: float | None = ..., sitk_threads: int = ..., show_progress: bool = ..., plot_metric: bool = ..., plot_composite: bool = ..., max_composite_slices: int | None = ..., progress_plotter: Callable[..., RegistrationProgress] | None = None, abort_event: Event | None = ..., ) -> tuple[ DataArray, NDArray[floating], RegistrationDiagnostics ] ``` ```python register_volume( moving: DataArray, fixed: DataArray, *, fixed_mask: DataArray | None = None, moving_mask: DataArray | None = None, transform_type: Literal[ "translation", "rigid", "affine", "bspline" ] = "rigid", metric: Literal[ "correlation", "mattes_mi" ] = "correlation", fixed_intensity_scaling: Literal["none", "db", "sqrt"] | float = "none", moving_intensity_scaling: Literal["none", "db", "sqrt"] | float = "none", number_of_histogram_bins: int = 50, metric_sampling_percentage: float | None = None, metric_sampling_seed: int | None = None, learning_rate: float | Literal["auto"] = "auto", number_of_iterations: int = 100, convergence_minimum_value: float = 1e-06, convergence_window_size: int = 10, initialization: Literal[ "center_geometry", "center_moments" ] | NDArray[floating] | None = "center_geometry", optimizer_weights: list[float] | None = None, mesh_size: tuple[int, int, int] = (10, 10, 10), use_multi_resolution: bool = False, shrink_factors: Sequence[int] = (6, 2, 1), smoothing_sigmas: Sequence[int] = (6, 2, 1), resample: bool = True, resample_interpolation: Literal[ "linear", "bspline" ] = "linear", fill_value: float | None = None, sitk_threads: int = -1, show_progress: bool = False, plot_metric: bool = True, plot_composite: bool = True, max_composite_slices: int | None = 9, progress_plotter: Callable[..., RegistrationProgress] | None = None, abort_event: Event | None = None, ) -> tuple[ DataArray, NDArray[floating] | DataArray, RegistrationDiagnostics, ] ``` Register a single 3D volume to a fixed reference. Voxel spacing and origin are automatically extracted from the DataArray coordinates. Both inputs must be spatial-only (no `time` dimension). Single-slice recordings are supported as 3D volumes with a singleton `k` axis. Parameters: - #### **`moving`** (`DataArray`) – Volume to register to `fixed`. Must be a 3D volume with dimensions `k`, `j`, `i` (single-slice recordings use a singleton `k` axis). - #### **`fixed`** (`DataArray`) – Reference volume. Must be a 3D volume with dimensions `k`, `j`, `i`. Need not have the same shape as `moving`. When spatial coordinate `units` metadata is present on both `moving` and `fixed`, it must match. - #### **`fixed_mask`** (`DataArray`, default: `None` ) – Mask for the fixed image. Must have boolean dtype and match the shape and coordinates of `fixed`. When provided, only voxels where the mask is True are used for computing the similarity metric. This is useful when the fixed image contains NaN values or regions that should be excluded from registration. - #### **`moving_mask`** (`DataArray`, default: `None` ) – Mask for the moving image. Must have boolean dtype and match the shape and coordinates of `moving`. When provided, only voxels where the mask is True are used for computing the similarity metric. This is useful when the moving image contains NaN values or regions that should be excluded from registration. - #### **`transform_type`** (`(translation, rigid, affine, bspline)`, default: `"translation"` ) – Transform model to use during registration. `"translation"` allows only shifts. `"rigid"` adds rotation. `"affine"` adds scaling and shearing. `"bspline"` fits a non-linear deformable transform (see `mesh_size`). - #### **`metric`** (`(correlation, mattes_mi)`, default: `"correlation"` ) – Similarity metric. `"correlation"` (normalized cross-correlation) is appropriate for same-modality registration. `"mattes_mi"` (Mattes mutual information) is better suited for multi-modal registration or when the intensity relationship between images is non-linear. - #### **`fixed_intensity_scaling`** (`(none, db, sqrt)`, default: `"none"` ) – Intensity transform applied to `fixed` only for the registration optimizer. Floats apply power scaling with that exponent; `"sqrt"` is an alias for `0.5`. - #### **`moving_intensity_scaling`** (`(none, db, sqrt)`, default: `"none"` ) – Intensity transform applied to `moving` only for the registration optimizer, with the same options as `fixed_intensity_scaling`. Returned/resampled data keeps the original input intensities. - #### **`number_of_histogram_bins`** (`int`, default: `50` ) – Number of histogram bins used by Mattes mutual information. Only relevant when using `"mattes_mi"` metric. - #### **`metric_sampling_percentage`** (`float`, default: `None` ) – Percentage of voxels randomly sampled when computing the metric, in `(0, 1]`. If not provided, all voxels are used without random sampling. - #### **`metric_sampling_seed`** (`int`, default: `None` ) – Seed for random metric sampling. Only used when `metric_sampling_percentage < 1`. If not provided, SimpleITK's default `sitkWallClock` seed is used. - #### **`learning_rate`** (`float or auto`, default: `"auto"` ) – Optimizer step size in normalized units. `"auto"` re-estimates the rate at every iteration. A float uses that value directly; if registration diverges or fails to converge, reduce it. - #### **`number_of_iterations`** (`int`, default: `100` ) – Maximum number of optimizer iterations. - #### **`convergence_minimum_value`** (`float`, default: `1e-6` ) – Value used for convergence checking in conjunction with the energy profile of the similarity metric that is estimated in the given window size. - #### **`convergence_window_size`** (`int`, default: `10` ) – Number of values of the similarity metric which are used to estimate the energy profile of the similarity metric. - #### **`initialization`** (`(center_geometry, center_moments)`, default: `"center_geometry"` ) – Initial transform mapping `fixed` to `moving` coordinates, applied before optimization: - `"center_geometry"`: aligns image centers. - `"center_moments"`: aligns centers of mass. - `(N+1, N+1)` homogeneous affine matrix: uses a precomputed affine transform. - `None`: uses the identity transform. For `transform_type="bspline"`, centering modes are ignored but affine initialization is supported. - #### **`optimizer_weights`** (`list of float`, default: `None` ) – Per-parameter weights applied on top of the auto-estimated world shift scales. `None` uses identity weights (all ones). A list is passed directly to SimpleITK's `SetOptimizerWeights`; its length must match the number of transform parameters (6 for rigid, 12 for affine). The weight for each parameter is multiplied into the effective step size: `0` freezes a parameter entirely, values in `(0, 1)` slow it down, and `1` leaves it unchanged. For the 3D Euler transform the parameter order is `[angleX, angleY, angleZ, tx, ty, tz]`; to disable rotations around x and y set weights to `[0, 0, 1, 1, 1, 1]`. - #### **`mesh_size`** (`tuple of int`, default: `(10, 10, 10)` ) – Number of B-spline mesh nodes along each spatial dimension. Only used when `transform_type="bspline"`. - #### **`use_multi_resolution`** (`bool`, default: `False` ) – Whether to use a multi-resolution pyramid during registration. When `True`, registration proceeds from a coarse downsampled version of the images to the full resolution, which improves convergence for large displacements and reduces the risk of local minima. - #### **`shrink_factors`** (`sequence of int`, default: `(6, 2, 1)` ) – Downsampling factor at each pyramid level, from coarsest to finest. Must have the same length as `smoothing_sigmas`. Only used when `use_multi_resolution=True`. - #### **`smoothing_sigmas`** (`sequence of int`, default: `(6, 2, 1)` ) – Gaussian smoothing sigma (in voxels) applied at each pyramid level, from coarsest to finest. Must have the same length as `shrink_factors`. Only used when `use_multi_resolution=True`. - #### **`resample`** (`bool`, default: `True` ) – Whether to resample the moving volume onto the fixed grid after estimating the transform. When `True`, the output is resampled onto the fixed grid and its coordinates match `fixed`. When `False`, only the transform is computed and the moving volume is returned unchanged with its original coordinates. - #### **`resample_interpolation`** (`(linear, bspline)`, default: `"linear"` ) – Interpolator used when resampling the moving volume onto the fixed grid. `"linear"` is fast and appropriate for most cases. `"bspline"` (3rd-order B-spline) produces smoother results and reduces ringing, useful for atlas registration. Only used when `resample=True`. - #### **`fill_value`** (`float`, default: `None` ) – Fill value for voxels that fall outside the moving image's field of view after resampling. Applied to both the final registered output (when `resample=True`) and the progress composite overlay (when `show_progress=True` and `plot_composite=True`). If not provided, defaults to the minimum value of `moving`, which renders out-of-FOV regions as background regardless of intensity scale (important for dB data where 0 is maximum intensity). Must be representable in `moving`'s dtype: to fill an integer volume with NaN, cast it to float first. - #### **`sitk_threads`** (`int`, default: `-1` ) – Number of threads SimpleITK may use internally. Negative values resolve to `max(1, os.cpu_count() + 1 + sitk_threads)`, so `-1` means all CPUs, `-2` means all minus one, and so on. You may want to set this to a lower value or `1` when running multiple registrations in parallel (e.g. with joblib) to avoid over-subscribing the CPU. - #### **`show_progress`** (`bool`, default: `False` ) – Whether to display a live progress plot during registration. The plot is shown in a Jupyter notebook or in an interactive matplotlib window depending on the active backend. - #### **`plot_metric`** (`bool`, default: `True` ) – Whether to include the optimizer metric curve in the progress plot. Ignored when `show_progress=False`. - #### **`plot_composite`** (`bool`, default: `True` ) – Whether to include a fixed/moving composite overlay in the progress plot. Requires resampling the moving image at every iteration. Ignored when `show_progress=False`. - #### **`max_composite_slices`** (`int or None`, default: `9` ) – Maximum number of slices to draw in the composite overlay for 3D volumes, evenly spaced along the volume's `k` axis. Keeps the mosaic grid readable and each iteration's render cheap for volumes with many slices. Use `None` to plot every slice. Ignored when `show_progress=False`, `plot_composite=False`, or the volume is 2D. - #### **`progress_plotter`** (`callable`, default: `None` ) – Factory that builds the progress reporter, called inside `register_volume` as `progress_plotter(registration_method, fixed_img, moving_img, *, plot_metric, plot_composite, resample_kwargs, max_composite_slices)`. Here `resample_kwargs` carries `interpolation`, `fill_value`, and `sitk_threads`. The returned object must implement the `RegistrationProgress` protocol (`update()` / `close()`). If not provided, the default `MatplotlibRegistrationProgressPlotter` is used. Ignored when `show_progress=False`. Custom factories are expected to be safe to call from a non-GUI thread; GUI side effects must be marshalled via thread-safe primitives such as Qt signals. - #### **`abort_event`** (`Event`, default: `None` ) – Cooperative cancellation flag. If set before or during optimisation, the registration stops at the next SimpleITK iteration boundary and returns the current intermediate result with `diagnostics.status="aborted"`. Returns: - **`registered`** ( `DataArray` ) – When `resample=True`, the moving volume resampled onto the fixed grid with coordinates matching `fixed` and world-space affines inherited from `fixed`. When `resample=False`, the original moving volume with its original coordinates and attributes. - **`transform`** ( `(4, 4) numpy.ndarray or xarray.DataArray or None` ) – Estimated registration transform. For linear transforms (`"translation"`, `"rigid"`, `"affine"`), returns a homogeneous `(4, 4)` affine matrix in world space. Follows SimpleITK's pull/inverse convention: the matrix maps fixed-space coordinates to moving-space coordinates. For `transform_type="bspline"`, returns an `xarray.DataArray` containing the B-spline control-point grid, not a dense deformation field. The first dimension is `component` with length 3, followed by spatial dimensions in ConfUSIus order (`("k", "j", "i")`). The coordinate values along each spatial axis are the world positions of the control points. Attributes include `type = "bspline_transform"`, the spline `order`, and the control-grid `direction` matrix. When an affine `initialization` was also supplied, the DataArray also includes `attrs["affines"]["bspline_initialization"]` so that the full composite transform (pre-affine + B-spline) can be reconstructed for later resampling. - **`diagnostics`** ( `RegistrationDiagnostics` ) – Per-iteration metric values, final metric value, iteration count, and the optimizer stop condition. Useful for plotting convergence curves, comparing runs, and detecting registrations that did not converge. Raises: - `ValueError` – If either input contains a `time` dimension or does not contain the spatial dimensions `k`, `j`, and `i`. - `ValueError` – If `moving` or `fixed` contains NaN values. - `ValueError` – If `transform_type`, `metric`, `fixed_intensity_scaling`, `moving_intensity_scaling`, `initialization`, or `resample_interpolation` is not a recognised value. - `ValueError` – If `learning_rate` is not a positive finite float or `"auto"`. - `ValueError` – If `number_of_iterations`, `convergence_window_size`, or `number_of_histogram_bins` is not a positive integer. - `ValueError` – If `metric_sampling_percentage` is outside `(0, 1]`, or if `metric_sampling_seed` is negative or provided without random sampling. - `ValueError` – If `shrink_factors` and `smoothing_sigmas` have different lengths. - `ValueError` – If an affine `initialization` is provided and its shape does not match the image dimensionality. - `TypeError` – If `fixed_mask` or `moving_mask` is not a boolean DataArray. - `ValueError` – If `fixed_mask` shape does not match `fixed` or `moving_mask` shape does not match `moving`. ### register\_volumewise ```python register_volumewise( data: DataArray, *, reference_time: int | None = None, fixed: DataArray | None = None, n_jobs: int = -1, transform: Literal[ "translation", "rigid", "affine" ] = "rigid", metric: Literal[ "correlation", "mattes_mi" ] = "correlation", fixed_intensity_scaling: Literal["none", "db", "sqrt"] | float | None = None, moving_intensity_scaling: Literal["none", "db", "sqrt"] | float = "none", number_of_histogram_bins: int = 50, learning_rate: float | Literal["auto"] = 0.01, number_of_iterations: int = 100, convergence_minimum_value: float = 1e-06, convergence_window_size: int = 10, initialization: Literal[ "center_geometry", "center_moments" ] | None = "center_geometry", optimizer_weights: list[float] | None = None, use_multi_resolution: bool = False, shrink_factors: Sequence[int] = (6, 2, 1), smoothing_sigmas: Sequence[int] = (6, 2, 1), resample_interpolation: Literal[ "linear", "bspline" ] = "linear", fill_value: float | None = None, show_progress: bool = True, progress_reporter: VolumewiseProgressReporter | None = None, abort_event: Event | None = None, keep_diagnostics: bool = False, ) -> DataArray ``` Register all volumes to a reference time point or a reference volume. Parameters: - #### **`data`** (`DataArray`) – VoxelData array with a `time` dimension to register. - #### **`reference_time`** (`int`, default: `None` ) – Index of the time point of `data` to use as registration target. If not provided, and `fixed` is not provided either, the first time point (`0`) is used. Cannot be combined with `fixed`. - #### **`fixed`** (`DataArray`, default: `None` ) – Spatial-only VoxelData array to register every frame to, for example the mean of a few low-motion frames of `data`. Must share `data`'s voxel grid (same `k`/`j`/`i` coordinates and voxel-to-world affine) and have no `time` dimension. Its own `fixed_intensity_scaling` applies to it. If not provided, the frame at `reference_time` is used. Cannot be combined with `reference_time`. - #### **`n_jobs`** (`int`, default: `-1` ) – Number of parallel jobs. Negative values resolve to `max(1, os.cpu_count() + 1 + n_jobs)`, so `-1` means all CPUs, `-2` means all minus one, and so on. Use `1` for serial processing. - #### **`transform`** (`(translation, rigid, affine)`, default: `"translation"` ) – Transform model to use during registration. `"translation"` allows only shifts. `"rigid"` adds rotation. `"affine"` adds scaling and shearing. B-spline is not available for motion correction. - #### **`metric`** (`(correlation, mattes_mi)`, default: `"correlation"` ) – Similarity metric. `"correlation"` (normalized cross-correlation) is appropriate for same-modality registration. `"mattes_mi"` (Mattes mutual information) is better suited for multi-modal registration or when the intensity relationship between images is non-linear. - #### **`fixed_intensity_scaling`** (`(none, db, sqrt)`, default: `"none"` ) – Intensity transform applied to `fixed`, only for the registration optimizer. Useful when `fixed` comes from another source than `data` and does not share its intensity range. Only allowed together with `fixed`; the reference frame selected by `reference_time` always uses `moving_intensity_scaling`. If not provided, `fixed` is scaled with `moving_intensity_scaling` too. - #### **`moving_intensity_scaling`** (`(none, db, sqrt)`, default: `"none"` ) – Intensity transform applied to every frame of `data`, only for the registration optimizer. Floats apply power scaling with that exponent; `"sqrt"` is an alias for `0.5`. Returned/resampled data keeps the original input intensities. - #### **`number_of_histogram_bins`** (`int`, default: `50` ) – Number of histogram bins used by Mattes mutual information. Only relevant when `metric="mattes_mi"`. - #### **`learning_rate`** (`float or auto`, default: `0.01` ) – Optimizer step size in normalised units (after `SetOptimizerScalesFromPhysicalShift`). `"auto"` re-estimates the rate at every iteration. A float uses that value directly; increase it for large inter-volume shifts, or reduce it if registration creates motion in otherwise stable data. - #### **`number_of_iterations`** (`int`, default: `100` ) – Maximum number of optimizer iterations. - #### **`convergence_minimum_value`** (`float`, default: `1e-6` ) – Convergence threshold. Optimization stops early when the estimated energy profile falls below this value. - #### **`convergence_window_size`** (`int`, default: `10` ) – Number of recent metric values used to estimate the energy profile for convergence checking. - #### **`initialization`** (`(center_geometry, center_moments)`, default: `"center_geometry"` ) – Initial transform mapping `fixed` to `moving` coordinates, applied before optimization: - `"center_geometry"`: aligns image centers. - `"center_moments"`: aligns centers of mass. - `None`: uses the identity transform. - #### **`optimizer_weights`** (`list of float`, default: `None` ) – Per-parameter weights applied on top of the auto-estimated world shift scales. If not provided, identity weights are used. A list is passed directly to SimpleITK's `SetOptimizerWeights`; its length must match the number of transform parameters (3 for 2D rigid, 6 for 3D rigid, 6 for 2D affine, 12 for 3D affine). The weight for each parameter is multiplied into the effective step size: `0` freezes a parameter entirely, values in `(0, 1)` slow it down, and `1` leaves it unchanged. For the 3D Euler transform the parameter order is `[angleX, angleY, angleZ, tx, ty, tz]`; to disable rotations around x and y set weights to `[0, 0, 1, 1, 1, 1]`. - #### **`use_multi_resolution`** (`bool`, default: `False` ) – Whether to use a multi-resolution pyramid during registration. When `True`, registration proceeds from a coarse downsampled version of the images to the full resolution, which improves convergence for large displacements and reduces the risk of local minima. - #### **`shrink_factors`** (`sequence of int`, default: `(6, 2, 1)` ) – Downsampling factor at each pyramid level, from coarsest to finest. Must have the same length as `smoothing_sigmas`. Only used when `use_multi_resolution=True`. - #### **`smoothing_sigmas`** (`sequence of int`, default: `(6, 2, 1)` ) – Gaussian smoothing sigma (in voxels) applied at each pyramid level, from coarsest to finest. Must have the same length as `shrink_factors`. Only used when `use_multi_resolution=True`. - #### **`resample_interpolation`** (`(linear, bspline)`, default: `"linear"` ) – Interpolator used when resampling each volume onto the reference grid. `"linear"` is fast and appropriate for motion correction. `"bspline"` (3rd-order B-spline) produces smoother results at the cost of speed. - #### **`fill_value`** (`float`, default: `None` ) – Fill value for voxels outside each moving volume's field of view after resampling. If not provided, defaults to that volume's minimum value. - #### **`show_progress`** (`bool`, default: `True` ) – Whether to display a progress bar while registering volumes. - #### **`progress_reporter`** (`VolumewiseProgressReporter`, default: `None` ) – Thread-safe reporter notified whenever one frame completes. Useful for GUI progress bars or progressively filling an output layer while frames finish. - #### **`abort_event`** (`Event`, default: `None` ) – Cooperative cancellation flag shared across frames. If set before or during execution, in-flight frame registrations stop at the next optimiser iteration boundary and this function returns the partial dataset collected so far. Frames that were not started are left blank (filled with the data minimum), and per-frame `motion_params` rows are marked via the diagnostics status. - #### **`keep_diagnostics`** (`bool`, default: `False` ) – Whether to keep the full per-frame `RegistrationDiagnostics` list on the returned DataArray under `attrs["registration_diagnostics"]`. Disabled by default because each diagnostics object carries the full optimizer metric trace, which adds up over long recordings. The cheap per-frame summaries (`final_metric_value`, `n_iterations`) are always added to `motion_params` regardless of this flag. Returns: - `DataArray` – Registered data with the same coordinates as input, input attributes, and added motion metadata in `attrs["reference_time"]` (the index of the frame used as target, or `None` when `fixed` was given) and `attrs["motion_params"]`. `motion_params` always carries per-frame `final_metric_value`, `n_iterations`, and `status` columns. When `keep_diagnostics=True`, `attrs["registration_diagnostics"]` also carries a list of `RegistrationDiagnostics` (one entry per frame) with the full per-iteration metric trace. Raises: - `ValueError` – If both `reference_time` and `fixed` are given, if `fixed_intensity_scaling` is given without `fixed`, or if `fixed` has a `time` dimension, is not a VoxelData array, or does not share `data`'s voxel grid. - `TypeError` – If `n_jobs != 1` and `data` is backed by an h5py dataset. See Notes. > [!NOTE] > > **Notes** > > SCAN files are HDF5 files loaded lazily via h5py. h5py datasets cannot be pickled, so they cannot be passed to joblib workers for parallel processing. Materialize the data before calling this function: > > ```python > import confusius as cf > > fusi = cf.load("recording.scan").compute() # load into memory first > fusi = cf.registration.register_volumewise(fusi) > ``` > > Alternatively, use `n_jobs=1` for serial processing (slower but works with lazy SCAN data). ### resample\_like ```python resample_like( moving: DataArray, reference: DataArray, transform: NDArray[floating] | DataArray, interpolation: Literal[ "auto", "linear", "nearest", "bspline" ] = "auto", fill_value: float | None = None, default_value: float | None = None, sitk_threads: int = -1, ) -> DataArray ``` Resample a volume onto the grid of a reference DataArray. Convenience wrapper around `resample_volume` that extracts the position-anchored output grid (`sizes`, `spacing`, `origin`, `direction`) from `reference`'s coordinates via the `fusi` accessor, so the grid is correct even if `reference` has been cropped or strided from a larger DataArray. Parameters: - #### **`moving`** (`DataArray`) – VoxelData array to resample. May be spatial-only, or have any number of extra non-spatial dimensions (`time` and/or others; `pose` is not supported) (single-slice recordings use a singleton `k` axis). The same transform is applied to every slice along the extra dimensions. - #### **`reference`** (`DataArray`) – VoxelData array defining the output grid. Only its `k`/`j`/`i` grid (`sizes`/`spacing`/`origin`/`direction`) is used, so `time` or other extra dimensions, if present, are ignored. When spatial coordinate `units` metadata is present on both `moving` and `reference`, they must match. - #### **`transform`** (`(4, 4) numpy.ndarray or xarray.DataArray`) – Registration transform, as returned by `register_volume`. Maps points from the reference world space to moving world space (pull/inverse convention). - **Affine** (`numpy.ndarray`): homogeneous matrix whose translation entries are expressed in the same physical units as `moving` and `reference`. - **B-spline** (`xarray.DataArray`): control-point DataArray. - **Displacement field** (`xarray.DataArray`): dense field with `attrs["type"] == "displacement_field_transform"`. When `transform` is a DataArray and spatial coordinate `units` metadata is present on both it and `reference`, those units must also match. - #### **`interpolation`** (`(auto, linear, nearest, bspline)`, default: `"auto"` ) – Interpolation method used during resampling. `"auto"` picks `"nearest"` when `moving` has an integer dtype (e.g. atlas region labels), and `"linear"` otherwise. - #### **`fill_value`** (`float`, default: `None` ) – Value assigned to voxels that fall outside the moving image's field of view after resampling. If not provided, defaults to `float(moving.min())`, which renders out-of-FOV voxels as background regardless of intensity scale (important for dB data where 0 is maximum intensity). - #### **`default_value`** (`float`, default: `None` ) – Alias for `fill_value` kept for compatibility with older branch code. If both values are provided, `fill_value` takes precedence. - #### **`sitk_threads`** (`int`, default: `-1` ) – Number of threads SimpleITK may use internally. Negative values resolve to `max(1, os.cpu_count() + 1 + sitk_threads)`, so `-1` means all CPUs, `-2` means all minus one, and so on. You may want to set this to a lower value or `1` when running multiple registrations in parallel (e.g. with joblib) to avoid over-subscribing the CPU. Returns: - `DataArray` – Resampled volume on the grid of `reference`, with `reference`'s `k`/`j`/`i` voxel labels and affine, `moving`'s non-spatial attributes, and world-space affines inherited from `reference`. Any extra non-spatial dimensions on `moving` (including `time`) are carried over unchanged (`moving`'s, not `reference`'s). Raises: - `ValueError` – If `reference` does not contain the spatial dimensions `k`, `j`, and `i`. ### resample\_volume ```python resample_volume( moving: DataArray, transform: NDArray[floating] | DataArray, *, output_sizes: Mapping[Hashable, SupportsIndex], output_spacing: Mapping[ Hashable, SupportsFloat | SupportsIndex ], output_origin: Mapping[ Hashable, SupportsFloat | SupportsIndex ], output_direction: ArrayLike, interpolation: Literal[ "auto", "linear", "nearest", "bspline" ] = "auto", fill_value: float | None = None, sitk_threads: int = -1, ) -> DataArray ``` Resample a volume onto an explicit output grid using a pre-computed transform. Low-level resampling primitive. For the common case of resampling onto the grid of another DataArray, use `resample_like` instead. The output grid is specified as position-anchored `sizes`/`spacing`/`origin`/ `direction` metadata (matching what a SimpleITK image expects), not as a single affine. A DataArray's `voxel_to_world` affine is defined in terms of its voxel coordinate values, which stay unchanged across cropping or striding; it does not generally describe where the array's position `(0, ..., 0)` sits, or the world distance between consecutive positions, once the array has been cropped or strided. Use `reference.fusi.spacing`/`reference.fusi.origin` (as `resample_like` does) to derive a position-anchored grid from an existing DataArray. Parameters: - #### **`moving`** (`DataArray`) – VoxelData array to resample. May be spatial-only, or have any number of extra non-spatial dimensions (`time` and/or others; `pose` is not supported) (single-slice recordings use a singleton `k` axis). The same transform is applied to every slice along the extra dimensions. - #### **`transform`** (`(4, 4) numpy.ndarray or xarray.DataArray`) – Registration transform, as returned by `register_volume`. - **Affine** (`numpy.ndarray`): homogeneous `(4, 4)` matrix mapping output (fixed) world coordinates to moving world coordinates (pull/inverse convention). - **B-spline** (`xarray.DataArray`): control-point DataArray with `attrs["type"] == "bspline_transform"` as returned by `register_volume(transform="bspline")`. - **Displacement field** (`xarray.DataArray`): dense field with `attrs["type"] == "displacement_field_transform"`, as returned by `sample_displacement_field` or `invert_displacement_field`. - #### **`output_sizes`** (`mapping of str to int`) – Number of voxels along each output voxel axis, read by `k`/`j`/`i` keys. `reference.sizes` can be passed directly. - #### **`output_spacing`** (`mapping of str to float`) – World distance between consecutive voxel positions along each output voxel axis, read by `k`/`j`/`i` keys. `reference.fusi.spacing` can be passed directly. - #### **`output_origin`** (`mapping of str to float`) – World location of output voxel position `(0, 0, 0)`, read by `z`/`y`/`x` keys. `reference.fusi.origin` can be passed directly. - #### **`output_direction`** (`ArrayLike`) – `(3, 3)` matrix whose columns are the unit world-space direction of each output voxel axis, in native `k/j/i` column order. `reference.fusi.direction` can be passed directly. - #### **`interpolation`** (`(auto, linear, nearest, bspline)`, default: `"auto"` ) – Interpolation method used during resampling. `"auto"` picks `"nearest"` when `moving` has an integer dtype (e.g. atlas region labels), and `"linear"` otherwise. - #### **`fill_value`** (`float`, default: `None` ) – Value assigned to voxels that fall outside the moving image's field of view after resampling. If not provided, defaults to `float(moving.min())`, which renders out-of-FOV voxels as background regardless of intensity scale (important for dB data where 0 is maximum intensity). Must be representable in `moving`'s dtype: to fill an integer volume with NaN, cast it to float first. - #### **`sitk_threads`** (`int`, default: `-1` ) – Number of threads SimpleITK may use internally. Negative values resolve to `max(1, os.cpu_count() + 1 + sitk_threads)`, so `-1` means all CPUs, `-2` means all minus one, and so on. You may want to set this to a lower value or `1` when running multiple registrations in parallel (e.g. with joblib) to avoid over-subscribing the CPU. Returns: - `DataArray` – VoxelData array resampled onto the requested grid, with `moving`'s attributes and any extra non-spatial dimensions. Raises: - `ValueError` – If `transform` is a numpy array whose shape does not match the spatial dimensionality, if `output_direction` has the wrong shape, or if `output_sizes`, `output_spacing`, or `output_origin` is missing a required key. ### sample\_displacement\_field ```python sample_displacement_field( transform: DataArray, *, shape: Sequence[int], spacing: Sequence[float], origin: Sequence[float], dims: Sequence[str], direction: ArrayLike | None = None, sitk_threads: int = -1, ) -> DataArray ``` Sample a registration transform onto an explicit output grid. Low-level sampling primitive. For the common case of sampling onto the grid of another DataArray, use `sample_displacement_field_like` instead. Parameters: - #### **`transform`** (`DataArray`) – Registration transform DataArray to sample. Currently this accepts B-spline control-point DataArrays produced by `register_volume`. - #### **`shape`** (`sequence of int`) – Number of voxels along each output axis, in DataArray dimension order. - #### **`spacing`** (`sequence of float`) – Voxel spacing along each output axis, in DataArray dimension order. - #### **`origin`** (`sequence of float`) – World origin (first voxel centre) along each output axis, in DataArray dimension order. - #### **`dims`** (`sequence of str`) – Dimension names of the output displacement field. - #### **`direction`** (`array - like`, default: `None` ) – Spatial direction matrix for the output grid, in DataArray spatial-dimension order. If not provided, the output grid is treated as axis-aligned. - #### **`sitk_threads`** (`int`, default: `-1` ) – Number of threads SimpleITK may use internally. Negative values resolve to `max(1, os.cpu_count() + 1 + sitk_threads)`, so `-1` means all CPUs, `-2` means all minus one, and so on. Returns: - `DataArray` – Dense displacement field DataArray with `attrs["type"] == "displacement_field_transform"`, one displacement vector per voxel of the requested grid. ### sample\_displacement\_field\_like ```python sample_displacement_field_like( transform: DataArray, reference: DataArray, *, sitk_threads: int = -1, ) -> DataArray ``` Sample a registration transform onto the grid of a reference DataArray. Convenience wrapper around `sample_displacement_field` that extracts the output grid from `reference`'s coordinates. Parameters: - #### **`transform`** (`DataArray`) – Registration transform DataArray to sample. Currently this accepts the B-spline control-point DataArrays produced by `register_volume`. - #### **`reference`** (`DataArray`) – DataArray defining the output grid. Must be a 3D spatial volume (no time dimension) with dimensions `k`, `j`, `i`. When spatial coordinate `units` metadata is present on both `transform` and `reference`, they must match. - #### **`sitk_threads`** (`int`, default: `-1` ) – Number of threads SimpleITK may use internally. Negative values resolve to `max(1, os.cpu_count() + 1 + sitk_threads)`, so `-1` means all CPUs, `-2` means all minus one, and so on. Returns: - `DataArray` – Dense displacement field DataArray sampled on `reference`'s grid. Raises: - `ValueError` – If `reference` contains a `time` dimension or does not contain the spatial dimensions `k`, `j`, and `i`. # `confusius.signal` ## signal Signal processing module for fUSI time series. Modules: - **`censor`** – Sample censoring and interpolation for motion scrubbing. - **`confounds`** – Confound regression functions for signal preprocessing. - **`core`** – Signal cleaning pipeline for fUSI time series. - **`detrending`** – Detrending functions for signal preprocessing. - **`filters`** – Filtering functions for signal preprocessing. - **`standardization`** – Standardization functions for signal preprocessing. Functions: - **`censor_samples`** – Remove censored samples from signals. - **`clean`** – Clean signals with detrending, filtering, confound regression, and scrubbing. - **`compute_compcor_confounds`** – Extract noise components using the CompCor method. - **`detrend`** – Remove trends from signals across time. - **`filter_butterworth`** – Apply a low-pass, high-pass, or band-pass Butterworth digital filter to signals. - **`filter_cosine`** – Apply cosine high-pass filtering along `time`. - **`interpolate_samples`** – Interpolate censored samples from signals. - **`regress_confounds`** – Remove confounds from signals via linear regression. - **`standardize`** – Standardize signals across time. ### censor\_samples ```python censor_samples( signals: DataArray, sample_mask: DataArray | ndarray ) -> DataArray ``` Remove censored samples from signals. This function removes timepoints marked as censored (bad) from the signals. After censoring, the time series becomes irregular (non-uniform time steps). Be cautious with subsequent time-domain analyses that assume uniform sampling. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Array to censor. Must have a `time` dimension. Can be any shape, e.g., extracted signals `(time, space)` or VoxelData array `(time, k, j, i)`. - #### **`sample_mask`** (`(time,) xarray.DataArray or numpy.ndarray`) – Boolean sample mask indicating which timepoints to keep (`True`) vs. remove (`False`). A DataArray must have a `time` dimension whose coordinates match those of `signals` within the default coordinate-comparison tolerance (`rtol=1e-5`, `atol=1e-8`); a NumPy array, or a DataArray without `time` coordinates, is assumed ordered like `signals` and takes its `time` coordinates, with a warning since alignment cannot be verified (see `ensure_time_aligned`). Returns: - `DataArray` – Signals with censored timepoints removed. Shape is `(n_kept, ...)` where `n_kept` is the number of `True` values in `sample_mask`. Time coordinates are subsetted to kept samples. Raises: - `ValueError` – If `signals` doesn't have a `time` dimension or `time` coordinates. - `TypeError` – If `sample_mask` is neither a DataArray nor a NumPy array. - `ValueError` – If `sample_mask` has wrong dtype, length, missing time dimension, or mismatched coordinates. - `ValueError` – If all samples are censored (cannot interpolate). Warns: - `UserWarning` – If all samples are kept (no censoring performed). - `UserWarning` – If `sample_mask` has no `time` coordinates, since alignment cannot be verified. Examples: Remove high-motion volumes: ```pycon >>> import xarray as xr >>> import numpy as np >>> from confusius.signal import censor_samples >>> # Create signals. >>> signals = xr.DataArray( ... np.random.randn(100, 50), ... dims=["time", "space"], ... coords={"time": np.arange(100) / 500} ... ) >>> # Mark frames to keep (False = remove). >>> mask_values = np.ones(100, dtype=bool) >>> mask_values[[10, 25, 60]] = False # Remove these frames. >>> sample_mask = xr.DataArray( ... mask_values, dims=["time"], coords={"time": signals.coords["time"]} ... ) >>> # Remove censored samples. >>> censored = censor_samples(signals, sample_mask) >>> censored.sizes["time"] # 97 timepoints (3 removed). 97 ``` Complete pre-scrubbing workflow: ```pycon >>> from confusius.signal import interpolate_samples, filter_butterworth >>> # 1. Interpolate censored samples. >>> interpolated = interpolate_samples(signals, sample_mask) >>> # 2. Apply temporal filter. >>> filtered = filter_butterworth(interpolated, high_cutoff=0.1) >>> # 3. Remove censored samples. >>> cleaned = censor_samples(filtered, sample_mask) ``` ### clean ```python clean( signals: DataArray, *, detrend_order: int | None = None, standardize_method: Literal["zscore", "psc"] | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, filter_method: Literal[ "butterworth", "cosine" ] = "butterworth", filter_kwargs: dict | None = None, confounds: DataArray | ndarray | DataFrame | None = None, standardize_confounds: bool = True, ensure_finite: bool = False, sample_mask: DataArray | ndarray | None = None, interpolate_method: InterpOptions = "linear", interpolate_kwargs: dict | None = None, ) -> DataArray ``` Clean signals with detrending, filtering, confound regression, and scrubbing. Cleaning steps are applied in the following order, according to recommendations by Lindquist *et al.* (2019): 1. Interpolate censored samples (pre-scrubbing). 2. Detrend. 3. Temporal filtering (Butterworth or cosine high-pass). 4. Censor samples. 5. Regress confounds. 6. Standardize. This function is inspired by `nilearn.signal.clean`. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Signals to clean. Must have a `time` dimension. Can be any shape, e.g., extracted signals `(time, space)` or VoxelData array `(time, k, j, i)`. > **Chunking along time is not supported** > > The `time` dimension must NOT be chunked, except when using standardization or scrubbing only. Chunk only spatial dimensions: `data.chunk({'time': -1})`. - #### **`detrend_order`** (`int`, default: `None` ) – Polynomial order for detrending: - `0`: Remove mean (constant detrending). - `1`: Remove linear trend using least squares regression (default). - `2+`: Remove polynomial trend of specified order. If not provided, no detrending is applied. - #### **`standardize_method`** (`(zscore, psc)`, default: `"zscore"` ) – Standardization method. If not provided, no standardization is applied. - #### **`low_cutoff`** (`float`, default: `None` ) – Low cutoff frequency in Hz. Frequencies below this are attenuated (acts as high-pass filter). If not provided, no high-pass filtering is applied. - #### **`high_cutoff`** (`float`, default: `None` ) – High cutoff frequency in Hz. Frequencies above this are attenuated (acts as low-pass filter). If not provided, no low-pass filtering is applied. - #### **`filter_method`** (`(butterworth, cosine)`, default: `"butterworth"` ) – Filtering method used when `low_cutoff` or `high_cutoff` is provided. Butterworth filtering supports low-pass, high-pass, and band-pass filtering. Cosine filtering supports high-pass filtering only. - #### **`filter_kwargs`** (`dict`, default: `None` ) – Extra keyword arguments passed to the selected filtering function. This means `filter_butterworth` when `filter_method="butterworth"` and `filter_cosine` when `filter_method="cosine"`. - #### **`confounds`** (`(time, n_confounds) xarray.DataArray, numpy.ndarray, or pandas.DataFrame`, default: `None` ) – Confound regressors to remove. Can have shape `(time,)` for a single confound. When provided, confounds are detrended and filtered along with signals before regression. A DataArray must have a `time` dimension; a DataFrame must have a `time` column, its other numeric columns being the confounds; a NumPy array must have time along its first axis. `time` coordinates must match those of `signals` within the default coordinate-comparison tolerance (`rtol=1e-5`, `atol=1e-8`); an input without `time` coordinates is assumed ordered like `signals` and takes its `time` coordinates, with a warning since alignment cannot be verified (see `ensure_time_aligned`). If not provided, no confound regression is applied. - #### **`standardize_confounds`** (`bool`, default: `True` ) – Whether to z-score confounds before regression. If `False`, confounds are divided by their maximum absolute value for numerical stability without centering. When `filter_method="cosine"`, confounds are not z-scored because the cosine drift matrix includes a constant column. - #### **`ensure_finite`** (`bool`, default: `False` ) – Whether to repair non-finite values (`NaN`, `Inf`) in `signals` and `confounds` before cleaning by interpolating each series along `time` and filling boundary gaps from the nearest finite sample. If `False`, non-finite values are left unchanged. - #### **`sample_mask`** (`(time,) xarray.DataArray or numpy.ndarray`, default: `None` ) – Boolean sample mask indicating which timepoints to keep (`True`) vs. remove (`False`). A DataArray must have a `time` dimension whose coordinates match those of `signals` within the default coordinate-comparison tolerance (`rtol=1e-5`, `atol=1e-8`); a NumPy array, or a DataArray without `time` coordinates, is assumed ordered like `signals` and takes its `time` coordinates, with a warning since alignment cannot be verified (see `ensure_time_aligned`). If not provided, no scrubbing is applied. - #### **`interpolate_method`** (`(linear, nearest, zero, slinear, quadratic, cubic, quintic, polynomial, pchip, barycentric, krogh, akima, makima)`, default: `"linear"` ) – Interpolation method passed to `confusius.signal.interpolate_samples` when using pre-scrubbing interpolation. Ignored if `sample_mask` is not provided or if no detrending or filtering is applied. Common options: - `"nearest"`: Nearest-neighbor interpolation (fastest, least smooth). - `"linear"`: Linear interpolation (faster, less smooth). - `"cubic"`: Cubic spline interpolation (slower, smooth). See `xarray.DataArray.interp` for all available methods. - #### **`interpolate_kwargs`** (`dict`, default: `None` ) – Extra keyword arguments forwarded to `confusius.signal.interpolate_samples` during pre-scrubbing interpolation. Ignored if `sample_mask` is not provided or if no detrending or filtering is applied. Cannot contain `method`, which must be passed via `interpolate_method`. Returns: - `DataArray` – Cleaned signals. Warns: - `UserWarning` – If `confounds` or `sample_mask` has no `time` coordinates, since alignment with `signals` cannot be verified. - `UserWarning` – If `confounds` is given and `signals` has a `pose` dimension, since the same confounds are regressed from every pose. > [!NOTE] > > **Notes** > > Signals with pose-dependent `(time, pose)` `time` coordinates are cleaned pose by pose along `time`. `confounds` and `sample_mask` are per-volume quantities and align with the whole-volume time (see `ensure_time_aligned`), so the same confounds and mask apply to every pose. **References** 1. Lindquist, Martin A., et al. “Modular Preprocessing Pipelines Can Reintroduce Artifacts into fMRI Data.” Human Brain Mapping, vol. 40, no. 8, June 2019, pp. 2358–76. DOI.org (Crossref), [https://doi.org/10.1002/hbm.24528](). ### compute\_compcor\_confounds ```python compute_compcor_confounds( signals: DataArray, noise_mask: DataArray | None = None, variance_threshold: float | None = None, n_components: int = 5, detrend: bool = False, skipna: bool = False, ) -> DataArray ``` Extract noise components using the CompCor method. CompCor (Component-based Noise Correction) extracts principal components from noise regions (aCompCor) or high-variance voxels (tCompCor) to use as confound regressors \[^1\]. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Signals from which to extract components. Must have a `time` dimension. For extracted signals, shape is typically `(time, space)`. For a full VoxelData array, shape is typically `(time, k, j, i)`. > **Chunking along time is not supported** > > The `time` dimension must NOT be chunked. Chunk only spatial dimensions: `data.chunk({'time': -1})`. - #### **`noise_mask`** (`DataArray`, default: `None` ) – Binary mask indicating voxels to consider. Must have the same spatial dimensions and coordinates as `signals` (excluding time). Can be combined with `variance_threshold` for hybrid selection. - #### **`variance_threshold`** (`float`, default: `None` ) – Variance percentile threshold (0-1) for selecting high-variance voxels. For example, 0.02 selects the top 2% highest-variance voxels from the voxels selected by `noise_mask` (if provided) or all voxels. Can be combined with `noise_mask` for hybrid selection. - #### **`n_components`** (`int`, default: `5` ) – Number of principal components to extract. - #### **`detrend`** (`bool`, default: `False` ) – Whether to linearly detrend the selected voxels before SVD. Can improve component quality by removing slow drifts. - #### **`skipna`** (`bool`, default: `False` ) – Whether to skip NaN values when computing variance quantiles for tCompCor. If `False`, uses fast quantile calculation. If `True`, uses slower NaN-aware quantile calculation. Set to `True` only if your data contains NaN values. Returns: - `(time, component) xarray.DataArray` – Extracted CompCor components. Each column (component) is a principal component that can be used as a confound regressor. The DataArray includes: - `time` dimension with coordinates matching the input signals. - `component` dimension (0 to `n_components - 1`). - `explained_variance_ratio` coordinate on `component` dimension, containing the proportion of total variance explained by each component. Raises: - `ValueError` – If `signals` does not have a `time` dimension. - `ValueError` – If mask doesn't have the right dtype or its dimensions/coordinates don't match signal spatial dimensions. - `ValueError` – If both `noise_mask` and `variance_threshold` are `None` (must specify at least one). - `ValueError` – If `variance_threshold` is not in range `(0, 1)`. - `ValueError` – If `n_components` is not positive. - `ValueError` – If no voxels are selected (empty mask or threshold too high). > [!NOTE] > > **Notes** > > - **aCompCor**: Specify only `noise_mask` to extract components from anatomically-defined noise regions (e.g., white matter, CSF). Useful when anatomical segmentation is available. > - **tCompCor**: Specify only `variance_threshold` to extract components from high-variance voxels. Useful when no anatomical masks are available. > - **Hybrid**: Specify both `noise_mask` and `variance_threshold` to extract components from high-variance voxels within a specific anatomical region (e.g., high-variance white matter voxels). **References** 1. Behzadi, Yashar, et al. “A Component Based Noise Correction Method (CompCor) for BOLD and Perfusion Based fMRI.” NeuroImage, vol. 37, no. 1, Aug. 2007, pp. 90–101. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2007.04.042](). Examples: Extract aCompCor components from white matter: ```pycon >>> import xarray as xr >>> import numpy as np >>> from confusius.signal import compute_compcor_confounds, regress_confounds >>> signals = xr.DataArray( ... np.random.randn(100, 50), ... dims=["time", "space"], ... coords={"time": np.arange(100) * 0.1} ... ) >>> wm_mask = xr.DataArray( ... np.zeros(50, dtype=bool), ... dims=["space"] ... ) >>> wm_mask.values[:10] = True >>> a_compcor = compute_compcor_confounds( ... signals, ... noise_mask=wm_mask, ... n_components=5, ... detrend=True ... ) >>> a_compcor.shape (100, 5) ``` Extract tCompCor from high-variance voxels: ```pycon >>> t_compcor = compute_compcor_confounds( ... signals, ... variance_threshold=0.2, ... n_components=5, ... detrend=True ... ) ``` Hybrid mode - high-variance WM voxels only: ```pycon >>> hybrid_compcor = compute_compcor_confounds( ... signals, ... noise_mask=wm_mask, ... variance_threshold=0.5, ... n_components=5 ... ) ``` Combine different CompCor variants for cleaning: ```pycon >>> all_compcor = xr.concat([a_compcor, t_compcor, hybrid_compcor], dim="component") >>> cleaned = regress_confounds(signals, all_compcor) ``` ### detrend ```python detrend(signals: DataArray, order: int = 1) -> DataArray ``` Remove trends from signals across time. This function operates along the `time` dimension and works with arrays of any shape, making it flexible for both extracted signals and full fUSI data. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Array to detrend. Must have a `time` dimension. Can be any shape, e.g., extracted signals `(time, space)` or VoxelData array `(time, k, j, i)`. > **Chunking along time is not supported** > > The `time` dimension must NOT be chunked. Chunk only spatial dimensions: `data.chunk({'time': -1})`. - #### **`order`** (`int`, default: `1` ) – Polynomial order for detrending: - `0`: Remove mean (constant detrending). - `1`: Remove linear trend using least squares regression (default). - `2+`: Remove polynomial trend of specified order. Returns: - `DataArray` – Detrended signals with same shape and coordinates as input. Raises: - `ValueError` – If `signals` does not have a `time` dimension. If `order` is negative. Warns: - `UserWarning` – If `signals` have only one timepoint (detrending is skipped). > [!NOTE] > > **Notes** > > - For `order=0` and `order=1`, uses `scipy.signal.detrend`. > - For `order >= 2`, fits a polynomial of the specified order and subtracts it. Examples: Remove mean (constant detrending): ```pycon >>> import xarray as xr >>> import numpy as np >>> signals = xr.DataArray( ... np.random.randn(100, 50) + 10, # Noise with offset. ... dims=["time", "space"] ... ) >>> detrended = detrend(signals, order=0) # Remove mean. ``` Remove linear trend (default): ```pycon >>> time = np.arange(100) >>> drift = time[:, None] * 0.5 # Linear drift. >>> noise = np.random.randn(100, 50) >>> signals = xr.DataArray(drift + noise, dims=["time", "space"]) >>> detrended = detrend(signals) # order=1 by default. ``` Remove quadratic trend: ```pycon >>> quadratic_drift = (time[:, None] ** 2) * 0.01 >>> signals_quad = xr.DataArray(quadratic_drift + noise, dims=["time", "space"]) >>> detrended_quad = detrend(signals_quad, order=2) ``` Works on 3D+t data: ```pycon >>> imaging_3dt = xr.DataArray( ... np.random.randn(100, 10, 20, 30), ... dims=["time", "z", "y", "x"] ... ) >>> detrended_3dt = detrend(imaging_3dt, order=1) ``` ### filter\_butterworth ```python filter_butterworth( signals: DataArray, low_cutoff: float | None = None, high_cutoff: float | None = None, order: int = 5, padtype: str = "odd", padlen: int | None = None, uniformity_tolerance: float = 0.01, ) -> DataArray ``` Apply a low-pass, high-pass, or band-pass Butterworth digital filter to signals. This function filters along the `time` dimension and works with arrays of any shape, making it flexible for both extracted signals and full fUSI data. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Array to filter. Must have a `time` dimension. Can be any shape, e.g., extracted signals `(time, space)` or VoxelData array `(time, k, j, i)`. > **Chunking along time is not supported** > > The `time` dimension must NOT be chunked. Chunk only spatial dimensions: `data.chunk({'time': -1})`. - #### **`low_cutoff`** (`float`, default: `None` ) – Low cutoff frequency in hertz. Frequencies below this are attenuated (acts as high-pass filter). If not provided, no high-pass filtering is applied. - #### **`high_cutoff`** (`float`, default: `None` ) – High cutoff frequency in hertz. Frequencies above this are attenuated (acts as low-pass filter). If not provided, no low-pass filtering is applied. - #### **`order`** (`int`, default: `5` ) – Filter order. Higher orders give steeper roll-off but may be less stable. - #### **`padtype`** (`(odd, even, constant, None)`, default: `"odd"` ) – Type of padding to use. See `scipy.signal.sosfiltfilt` for details. `"odd"` pads with odd-extension (reduces edge effects), `"even"` pads with even-extension, `"constant"` pads with zeros. If not provided, no padding is applied. - #### **`padlen`** (`int`, default: `None` ) – Number of elements to pad at each end. If not provided, uses scipy's default: `3 * (2 * n_sections + 1 - compensation)` where `n_sections = ceil(order/2)`. - #### **`uniformity_tolerance`** (`float`, default: `1e-2` ) – Maximum allowed relative range of consecutive time intervals, defined as `(max_interval - min_interval) / median_interval`. Raise a `ValueError` if the time coordinate exceeds this threshold. Increase this value to tolerate slight timestamp jitter (e.g. from acquisition clocks or dropped volumes). Returns: - `DataArray` – Filtered signals with same shape and coordinates as input. Raises: - `ValueError` – If `signals` does not have a `time` dimension or `time` coordinates, if time coordinates are not uniformly sampled within the specified tolerance, or if insufficient timepoints for the filter order (needs more than `3 * (2 * ceil(order / 2) + 1)`). - `ValueError` – If both `low_cutoff` and `high_cutoff` are `None` (no filtering), or if cutoff frequencies are invalid (negative, above Nyquist, or if high\_cutoff \<= low\_cutoff for band-pass). - `ValueError` – If `order` is not positive. > [!NOTE] > > **Notes** > > - Uses `scipy.signal.butter` with second-order sections (SOS) for numerical stability. > - Uses `scipy.signal.sosfiltfilt` for zero-phase filtering (forward-backward). > - Edge effects are handled automatically using padding. Examples: Low-pass filter to remove high-frequency noise: ```pycon >>> import xarray as xr >>> import numpy as np >>> from confusius.signal import filter_butterworth >>> # Create signals with time coordinates (500 Hz sampling). >>> signals = xr.DataArray( ... np.random.randn(1000, 50), ... dims=["time", "space"], ... coords={"time": np.arange(1000) / 500} ... ) >>> # Keep frequencies below 0.1 Hz (sampling rate computed from time coords). >>> filtered = filter_butterworth(signals, high_cutoff=0.1) ``` High-pass filter to remove slow drift: ```pycon >>> # Keep frequencies above 0.01 Hz (remove lower frequencies). >>> filtered = filter_butterworth(signals, low_cutoff=0.01) ``` Band-pass filter: ```pycon >>> # Keep only frequencies between 0.01 and 0.1 Hz. >>> filtered = filter_butterworth( ... signals, low_cutoff=0.01, high_cutoff=0.1, order=5 ... ) ``` ### filter\_cosine ```python filter_cosine( signals: DataArray, low_cutoff: float, uniformity_tolerance: float = 0.01, ) -> DataArray ``` Apply cosine high-pass filtering along `time`. This removes low-frequency drift by regressing each time series onto the same discrete-cosine basis used by the GLM `drift_model="cosine"` design matrix. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Array to filter. Must have a `time` dimension. Can be any shape, e.g., extracted signals `(time, space)` or VoxelData array `(time, k, j, i)`. > **Chunking along time is not supported** > > The `time` dimension must NOT be chunked. Chunk only spatial dimensions: `data.chunk({'time': -1})`. - #### **`low_cutoff`** (`float`) – High-pass cutoff frequency in hertz. Frequencies below this are removed by projection onto the cosine drift basis. - #### **`uniformity_tolerance`** (`float`, default: `1e-2` ) – Maximum allowed relative range of consecutive time intervals, defined as `(max_interval - min_interval) / median_interval`. Raise a `ValueError` if the time coordinate exceeds this threshold. Increase this value to tolerate slight timestamp jitter (e.g. from acquisition clocks or dropped volumes). Returns: - `DataArray` – Filtered signals with same shape and coordinates as input. Raises: - `ValueError` – If `signals` does not have a `time` dimension or `time` coordinates, if time coordinates are not uniformly sampled within the specified tolerance, or if `low_cutoff` is not positive. ### interpolate\_samples ```python interpolate_samples( signals: DataArray, sample_mask: DataArray | ndarray, method: InterpOptions = "linear", **kwargs, ) -> DataArray ``` Interpolate censored samples from signals. This function interpolates values at censored (bad) timepoints using samples marked as good. The typical use case is to fill in high-motion volumes before temporal filtering, then remove them afterward with `censor_samples`. This allows retaining regular time sampling during filtering. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Array to interpolate. Must have a `time` dimension and `time` coordinates. Can be any shape, e.g., extracted signals `(time, space)` or VoxelData array `(time, k, j, i)`. - #### **`sample_mask`** (`(time,) xarray.DataArray or numpy.ndarray`) – Boolean sample mask indicating which timepoints to keep (`True`) vs. interpolate (`False`). A DataArray must have a `time` dimension whose coordinates match those of `signals` within the default coordinate-comparison tolerance (`rtol=1e-5`, `atol=1e-8`); a NumPy array, or a DataArray without `time` coordinates, is assumed ordered like `signals` and takes its `time` coordinates, with a warning since alignment cannot be verified (see `ensure_time_aligned`). - #### **`method`** (`(linear, nearest, zero, slinear, quadratic, cubic, quintic, polynomial, pchip, barycentric, krogh, akima, makima)`, default: `"linear"` ) – Interpolation method passed to `xarray.DataArray.interp`. Common options: - `"nearest"`: Nearest-neighbor interpolation (fastest, least smooth). - `"linear"`: Linear interpolation (faster, less smooth). - `"cubic"`: Cubic spline interpolation (slower, smooth). See `xarray.DataArray.interp` for all available methods. - #### **`**kwargs`** – Additional keyword arguments passed to `xarray.DataArray.interp`. Returns: - `DataArray` – Signals with interpolated values at censored positions. Same shape and coordinates as input. Raises: - `ValueError` – If `signals` doesn't have a `time` dimension or `time` coordinates. - `TypeError` – If `sample_mask` is neither a DataArray nor a NumPy array. - `ValueError` – If `sample_mask` has wrong dtype, length, missing time dimension, or mismatched coordinates. - `ValueError` – If all samples are censored (cannot interpolate). Warns: - `UserWarning` – If all samples are marked as good (no interpolation needed). - `UserWarning` – If `sample_mask` has no `time` coordinates, since alignment cannot be verified. > [!NOTE] > > **Notes** > > - Kept samples (`sample_mask=True`) are unchanged; only censored samples (`sample_mask=False`) are replaced with interpolated values. > - Uses `xarray.DataArray.interp` which handles coordinates and Dask arrays automatically. > - Signals with pose-dependent `(time, pose)` `time` coordinates are interpolated pose by pose, each on its own timestamps, but using the same `sample_mask`. Examples: Interpolate high-motion volumes before filtering: ```pycon >>> import xarray as xr >>> import numpy as np >>> from confusius.signal import interpolate_samples, filter_butterworth, censor_samples >>> # Create signals with time coordinates. >>> signals = xr.DataArray( ... np.random.randn(100, 50), ... dims=["time", "space"], ... coords={"time": np.arange(100) / 500} # 500 Hz. ... ) >>> # Mark high-motion frames (e.g., frames 10, 25, 60 are bad). >>> motion_outliers = np.array([10, 25, 60]) >>> mask_values = np.ones(100, dtype=bool) >>> mask_values[motion_outliers] = False # False = censor. >>> sample_mask = xr.DataArray( ... mask_values, dims=["time"], coords={"time": signals.coords["time"]} ... ) ``` Pre-scrubbing workflow (recommended): ```pycon >>> # 1. Interpolate censored samples. >>> interpolated = interpolate_samples(signals, sample_mask, method="cubic") >>> # 2. Apply temporal filter to complete signal. >>> filtered = filter_butterworth(interpolated, high_cutoff=0.1) >>> # 3. Remove censored samples after filtering. >>> cleaned = censor_samples(filtered, sample_mask) ``` Control boundary behavior: ```pycon >>> # Extrapolate at boundaries instead of leaving them as NaN. >>> interpolated = interpolate_samples(signals, sample_mask, fill_value="extrapolate") >>> # Or explicitly keep NaN outside the kept range. >>> interpolated_nan = interpolate_samples(signals, sample_mask, fill_value=np.nan) ``` ### regress\_confounds ```python regress_confounds( signals: DataArray, confounds: DataArray | ndarray | DataFrame, standardize_confounds: bool = True, ) -> DataArray ``` Remove confounds from signals via linear regression. This function performs confound regression by projecting the signals onto the orthogonal complement of the confound space. This removes variance in the signals that can be explained by the confounds. This function was adapted from `nilearn.signal.clean`. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Signals to clean. Must have a `time` dimension. Can be any shape, e.g., extracted signals `(time, space)` or VoxelData array `(time, k, j, i)`. > **Chunking along time is not supported** > > The `time` dimension must NOT be chunked. Chunk only spatial dimensions: `data.chunk({'time': -1})`. - #### **`confounds`** (`(time, n_confounds) xarray.DataArray, numpy.ndarray, or pandas.DataFrame`) – Confound regressors to remove. Can have shape `(time,)` for a single confound. A DataArray must have a `time` dimension; a DataFrame must have a `time` column, its other numeric columns being the confounds; a NumPy array must have time along its first axis. `time` coordinates must match those of `signals` within the default coordinate-comparison tolerance (`rtol=1e-5`, `atol=1e-8`); an input without `time` coordinates is assumed ordered like `signals` and takes its `time` coordinates, with a warning since alignment cannot be verified (see `ensure_time_aligned`). - #### **`standardize_confounds`** (`bool`, default: `True` ) – Whether to z-score confounds before regression. If `False`, confounds are divided by their maximum absolute value for numerical stability without centering. Returns: - `DataArray` – Residuals after confound regression, same shape and coordinates as input signals. Raises: - `ValueError` – If `signals` does not have a `time` dimension, or if `confounds` have mismatched time dimension or invalid shape. - `TypeError` – If `confounds` is not a DataArray, NumPy array, or DataFrame. Warns: - `UserWarning` – If `confounds` has no `time` coordinates, since alignment cannot be verified. - `UserWarning` – If `signals` has a `pose` dimension, since the same confounds are regressed from every pose. > [!NOTE] > > **Notes** > > - Uses QR decomposition with column pivoting for numerical stability. > - Handles rank-deficient confound matrices (e.g., collinear confounds) by removing redundant columns. > - Based on the projection method from Friston et al. (1994). **References** 1. Friston, K. J., Holmes, A. P., Worsley, K. J., Poline, J. P., Frith, C. D., & Frackowiak, R. S. (1994). Statistical parametric maps in functional imaging: a general linear approach. Human brain mapping, 2(4), 189-210. Examples: Remove motion parameters from extracted signals: ```pycon >>> import xarray as xr >>> import numpy as np >>> from confusius.signal import regress_confounds >>> # Create signals (100 timepoints, 50 voxels) >>> signals = xr.DataArray( ... np.random.randn(100, 50), ... dims=["time", "space"], ... coords={"time": np.arange(100) * 0.1} ... ) >>> # Create motion confounds (6 motion parameters) >>> motion_params = xr.DataArray( ... np.random.randn(100, 6), ... dims=["time", "confound"], ... coords={"time": signals.coords["time"]}, ... ) >>> # Remove motion effects >>> cleaned = regress_confounds(signals, motion_params) ``` Works on 3D+t imaging data: ```pycon >>> imaging_data = xr.DataArray( ... np.random.randn(100, 10, 20, 30), ... dims=["time", "z", "y", "x"] ... ) >>> cleaned_imaging = regress_confounds( ... imaging_data, motion_params ... ) ``` ### standardize ```python standardize( signals: DataArray, method: Literal["zscore", "psc"] = "zscore", ) -> DataArray ``` Standardize signals across time. This function operates along the `time` dimension and works with arrays of any shape, making it flexible for both extracted signals and full fUSI data. Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Array to standardize. Must have a `time` dimension. Can be any shape, e.g., extracted signals `(time, space)` or VoxelData array `(time, k, j, i)`. - #### **`method`** (`(zscore, psc)`, default: `"zscore"` ) – Standardization method: - `"zscore"`: `(x - mean) / std` using sample standard deviation with Bessel's correction. Elements with zero or near-zero variance will be set to `numpy.nan`. - `"psc"`: Percent signal change: `(x - mean) / |mean| * 100`. Elements with zero or near-zero mean will be set to `numpy.nan`. Returns: - `DataArray` – Standardized array with same shape and coordinates as input. Raises: - `ValueError` – If `signals` does not have a `time` dimension. - `ValueError` – If `method` is not `"zscore"` or `"psc"`. Warns: - `UserWarning` – If `signals` have only one timepoint (standardization is skipped). Examples: Standardize extracted signals: ```pycon >>> import xarray as xr >>> import numpy as np >>> signals = xr.DataArray( ... np.random.randn(100, 50), ... dims=["time", "space"] ... ) >>> standardized = standardize(signals, method="zscore") >>> standardized.mean(dim="time").values # Should be close to 0. >>> standardized.std(dim="time", ddof=1).values # Should be close to 1. ``` Standardize 3D+t imaging data directly: ```pycon >>> imaging_data = xr.DataArray( ... np.random.randn(100, 10, 20, 30), ... dims=["time", "z", "y", "x"] ... ) >>> standardized_3dt = standardize(imaging_data, method="psc") >>> # Each voxel is independently standardized across time. ``` # `confusius.spatial` ## spatial Spatial processing module for fUSI volumetric data. Modules: - **`smooth`** – Spatial smoothing functions for fUSI volumetric data. Functions: - **`smooth_volume`** – Smooth a DataArray spatially using a Gaussian kernel. ### smooth\_volume ```python smooth_volume( data: DataArray, fwhm: float | dict[str, float], ensure_finite: bool = False, ) -> DataArray ``` Smooth a DataArray spatially using a Gaussian kernel. FWHM values are specified in physical units and converted to voxel-space sigma values using the coordinate spacing of each dimension. Smoothing is only applied to dimensions with uniform coordinate spacing. Parameters: - #### **`data`** (`DataArray`) – VoxelData array to smooth. Can have any number of dimensions, including a `time` dimension. > **Chunking along smoothed dimensions is not supported** > > Dimensions selected for smoothing must NOT be chunked. Chunk only along other dimensions, e.g. `data.chunk({"time": 10})` when smoothing spatial axes. - #### **`fwhm`** (`float or dict[str, float]`) – Full width at half maximum of the Gaussian kernel in world unit. A scalar applies the same FWHM to all dimensions except `"time"`. A dict maps dimension names to per-dimension FWHM values, e.g. `{"k": 0.5, "j": 0.2, "i": 0.2}`; only the listed dimensions are smoothed. Dimensions of length 1 are left untouched. - #### **`ensure_finite`** (`bool`, default: `False` ) – Whether to replace non-finite values (`NaN`, `Inf`) with zero before filtering. This prevents `NaN`s from propagating to neighbouring voxels through the Gaussian kernel. Returns: - `DataArray` – Smoothed data with the same shape, dimensions, coordinates, and attributes as the input. Raises: - `TypeError` – If `data` is not an `xarray.DataArray`. - `ValueError` – If `fwhm` is a dict and any key is not present in the DataArray, or if any smoothed dimension has non-uniform or undefined coordinate spacing, or is chunked (for Dask-backed arrays). > [!NOTE] > > **Notes** > > The Gaussian sigma in voxel units is computed as: > > \\\[ \\sigma\_{\\text{vox}} = \\frac{\\text{FWHM}}{ 2 \\sqrt{2 \\ln 2} \\cdot \\Delta} \\\] > > where \\(\\Delta\\) is the coordinate spacing for that dimension, in the same physical units as `fwhm`. > > Smoothing is applied via `scipy.ndimage.gaussian_filter` with zero sigma for non-smoothed dimensions, making the operation separable and efficient. Examples: Smooth isotropically with a 0.3 (coordinate-unit) FWHM kernel: ```pycon >>> import xarray as xr >>> import numpy as np >>> import confusius # noqa: F401 >>> from confusius.xarray import create_voxeldata >>> data = create_voxeldata( ... np.random.randn(5, 10, 1, 20), ... dims=("time", "k", "j", "i"), ... dt=0.2, ... spacing=(0.1, 0.1, 0.1), ... ) >>> smoothed = smooth_volume(data, fwhm=0.3) ``` Smooth with anisotropic kernels: ```pycon >>> smoothed = smooth_volume(data, fwhm={"k": 0.5, "j": 0.2, "i": 0.2}) ``` Smooth only selected dimensions: ```pycon >>> smoothed = smooth_volume(data, fwhm={"k": 0.3, "i": 0.3}) ``` Suppress NaN propagation when some voxels are masked: ```pycon >>> smoothed = smooth_volume(data, fwhm=0.3, ensure_finite=True) ``` # `confusius.stats` ## stats Statistical inference utilities for fUSI analysis. This package provides shared statistical utilities that apply across multiple analysis workflows, such as statistical-map thresholding with multiple-comparison correction. Modules: - **`thresholding`** – Statistical thresholding and multiple-comparison correction utilities. Functions: - **`adjust_pvalues`** – Adjust a p-value map for multiple comparisons. - **`apply_statistical_threshold`** – Threshold a statistical map with multiple-comparison correction. ### adjust\_pvalues ```python adjust_pvalues( pvalue_map: DataArray, mask: DataArray | None = None, *, method: CorrectionMethod = "fdr_bh", skipzero: bool = False, skipna: bool = False, ) -> DataArray ``` Adjust a p-value map for multiple comparisons. The selected voxels are adjusted according to `method`; untested voxels are set to `1.0` in the output so they are never significant under any `alpha <= 1`. Parameters: - #### **`pvalue_map`** (`DataArray`) – P-value map with spatial dimensions. Tested voxels must lie in `[0, 1]`. - #### **`mask`** (`DataArray`, default: `None` ) – Boolean (or binary numeric) mask of the tested voxels. If not provided, the tested voxels are derived from `pvalue_map` according to `skipzero` and `skipna`. - #### **`method`** (`CorrectionMethod`, default: `"fdr_bh"` ) – Multiple-comparison correction method. Available methods: - `"uncorrected"`: no correction. - `"bonferroni"`: one-step correction. - `"sidak"`: one-step correction. - `"holm-sidak"`: step down method using Sidak adjustments. - `"holm"`: step-down method using Bonferroni adjustments. - `"simes-hochberg"`: step-up method (independent). - `"hommel"`: closed method based on Simes tests (non-negative). - `"fdr_bh"`: Benjamini/Hochberg (non-negative). - `"fdr_by"`: Benjamini/Yekutieli (negative). - #### **`skipzero`** (`bool`, default: `False` ) – Whether to exclude voxels equal to zero from the tested voxels when `mask` is not provided. - #### **`skipna`** (`bool`, default: `False` ) – Whether to exclude non-finite voxels from the tested voxels when `mask` is not provided. Set to True for maps containing NaNs. Returns: - `DataArray` – Copy of `pvalue_map` containing adjusted p-values at tested voxels and `1.0` at untested voxels. Raises: - `ValueError` – If `method` is not one of the allowed values, any tested voxel lies outside `[0, 1]`, or no voxel is tested. ### apply\_statistical\_threshold ```python apply_statistical_threshold( stat_map: DataArray, mask: DataArray | None = None, *, alpha: float = 0.05, method: CorrectionMethod | None = "fdr_bh", threshold: float | None = None, cluster_threshold: int = 0, two_sided: bool = True, skipzero: bool = False, skipna: bool = False, ) -> tuple[DataArray, float] ``` Threshold a statistical map with multiple-comparison correction. Applies a voxel-level statistical threshold followed by an optional cluster-extent threshold. The input is assumed to be z-scaled, such as a map returned by `compute_contrast`. The z-scores are converted to p-values, adjusted for multiple comparisons with `adjust_pvalues`, and voxels whose adjusted p-value is at or below `alpha` are kept. Set `method` to `None` to instead apply the explicit `threshold` z-score cutoff. If a mask is not provided, `skipzero` and `skipna` can be used to exclude zero and non-finite voxels from the tested voxels. Parameters: - #### **`stat_map`** (`DataArray`) – Z-scaled statistical map with spatial dimensions. Voxels that do not survive the thresholds are set to zero. - #### **`mask`** (`DataArray`, default: `None` ) – Boolean (or binary numeric) mask of the tested voxels. If not provided, the tested voxels are derived from `stat_map` according to `skipzero` and `skipna`. - #### **`alpha`** (`float`, default: `0.05` ) – Significance level. A voxel is kept when its `method`-adjusted p-value is at or below `alpha`. Ignored when `method` is not provided. - #### **`method`** (`CorrectionMethod`, default: `'fdr_bh'` ) – Multiple-comparison correction method, by default `"fdr_bh"`. If not provided, `threshold` is applied directly with no statistical control. Available methods: - `"uncorrected"`: no correction. - `"bonferroni"`: one-step correction. - `"sidak"`: one-step correction. - `"holm-sidak"`: step down method using Sidak adjustments. - `"holm"`: step-down method using Bonferroni adjustments. - `"simes-hochberg"`: step-up method (independent). - `"hommel"`: closed method based on Simes tests (non-negative). - `"fdr_bh"`: Benjamini/Hochberg (non-negative). - `"fdr_by"`: Benjamini/Yekutieli (negative). - #### **`threshold`** (`float`, default: `None` ) – Explicit z-score threshold, used only when `method` is not provided. If not provided in that case, defaults to 3.0. Ignored (with a warning) when `method` is set. - #### **`cluster_threshold`** (`int`, default: `0` ) – Minimum cluster size in voxels. Connected clusters smaller than this are removed after the voxel-level threshold. Disabled when 0. - #### **`two_sided`** (`bool`, default: `True` ) – Whether to test both tails. When True, the magnitude of the map is tested; otherwise only positive z-scores can survive. - #### **`skipzero`** (`bool`, default: `False` ) – Whether to exclude voxels equal to zero from the tested voxels when `mask` is not provided. Useful because maps from `compute_contrast` fill non-brain voxels with zero. - #### **`skipna`** (`bool`, default: `False` ) – Whether to exclude non-finite voxels from the tested voxels when `mask` is not provided. Set to True for maps containing NaNs. Returns: - **`thresholded_map`** ( `DataArray` ) – Copy of `stat_map` with sub-threshold and untested voxels set to zero. - **`threshold`** ( `float` ) – The z-score threshold that was applied. `numpy.inf` when a data-adaptive method rejects every voxel. Raises: - `ValueError` – If `method` is not one of the allowed values, `cluster_threshold` is negative, `alpha` is not in `[0, 1]`, or no voxel is tested. # `confusius.timing` ## timing Timing utilities for ConfUSIus. Functions: - **`convert_time_reference`** – Convert timings from one volume acquisition reference to another. - **`convert_time_units`** – Convert time values between units. - **`ensure_slice_time_acquisition_attrs`** – Fill in default metadata attrs on `data`'s `slice_time` coordinate. - **`ensure_time_acquisition_attrs`** – Fill in default metadata attrs on `data`'s `time` coordinate. - **`get_representative_time_step`** – Return a representative time step for the time coordinate. - **`get_time_coord_to_seconds_factor`** – Return the factor that converts a time coordinate to seconds. - **`resample_time`** – Resample data to new time coordinates. - **`resample_to_uniform_time`** – Resample data to a uniform time grid. Attributes: - **`TIMING_REFERENCE_FACTORS`** (`dict[VolumeAcquisitionReference, float]`) – Fractional position of each timing reference within an acquisition window. - **`VolumeAcquisitionReference`** – Where within an acquisition window a timestamp is anchored. ### TIMING\_REFERENCE\_FACTORS ```python TIMING_REFERENCE_FACTORS: dict[ VolumeAcquisitionReference, float ] = {"start": 0.0, "center": 0.5, "end": 1.0} ``` Fractional position of each timing reference within an acquisition window. ### VolumeAcquisitionReference ```python VolumeAcquisitionReference = Literal[ "start", "center", "end" ] ``` Where within an acquisition window a timestamp is anchored. ### convert\_time\_reference ```python convert_time_reference( time: ArrayLike, volume_duration: float | ArrayLike, from_reference: VolumeAcquisitionReference, to_reference: VolumeAcquisitionReference, ) -> NDArray[floating] ``` Convert timings from one volume acquisition reference to another. Parameters: - #### **`time`** (`array_like`) – Input timings in any world time unit. - #### **`volume_duration`** (`float or array_like`) – Duration of one volume in the same units as `time`. May be a scalar or one duration per input timing. - #### **`from_reference`** (`(start, center, end)`, default: `"start"` ) – Reference point used by the input timings. - #### **`to_reference`** (`(start, center, end)`, default: `"start"` ) – Reference point to convert the timings to. Returns: - `ndarray` – Converted timings in the same units as `time`. ### convert\_time\_units ```python convert_time_units( values: ArrayLike, from_unit: str | None, to_unit: str | None = "s", *, raise_on_missing: bool = False, raise_on_unknown: bool = False, ) -> NDArray[floating] ``` Convert time values between units. Parameters: - #### **`values`** (`array_like`) – Time values to convert. - #### **`from_unit`** (`str or None`) – Current unit of the values. - #### **`to_unit`** (`str or None`, default: `"s"` ) – Target unit of the values. - #### **`raise_on_missing`** (`bool`, default: `False` ) – Whether to raise when either unit is missing. - #### **`raise_on_unknown`** (`bool`, default: `False` ) – Whether to raise when either unit is not recognized. Returns: - `ndarray` – Time values converted to `to_unit`. ### ensure\_slice\_time\_acquisition\_attrs ```python ensure_slice_time_acquisition_attrs( data: DataArray, ) -> DataArray ``` Fill in default metadata attrs on `data`'s `slice_time` coordinate. Every VoxelData array with a `slice_time` coordinate carries `volume_acquisition_reference`, `volume_acquisition_duration`, and `units` on it. Data built without going through consolidate\_poses (e.g. a `slice_time` coordinate assembled by hand) may still be missing them; this fills in `"start"` and `"s"`, respectively. `volume_acquisition_duration` is inferred from the median consecutive difference along `slice_time`'s sweep dimension (its last dimension). Simultaneous slices (e.g. a stacked-linear-probe pose) are expected and not treated as an error. Parameters: - #### **`data`** (`DataArray`) – VoxelData array to fill in. Returns: - `DataArray` – `data` unchanged when it has no `slice_time` coordinate or that coordinate already carries all three attrs. Otherwise a copy with `volume_acquisition_reference` defaulted to `"start"`, `units` defaulted to `"s"`, and `volume_acquisition_duration` defaulted from the median sweep-axis step, when it can be inferred. Warns: - `UserWarning` – If any attr was missing and defaulted, naming which ones and what they were defaulted to. ### ensure\_time\_acquisition\_attrs ```python ensure_time_acquisition_attrs(data: DataArray) -> DataArray ``` Fill in default metadata attrs on `data`'s `time` coordinate. Every VoxelData array with a `time` dimension carries `volume_acquisition_reference`, `volume_acquisition_duration`, and `units` on its `time` coordinate. Data built without going through create\_voxeldata or an I/O loader (e.g. a `time` coordinate assembled by hand) may still be missing them; this fills in `"start"` and `"s"`, respectively. `volume_acquisition_duration` is inferred from the representative `time` step. Parameters: - #### **`data`** (`DataArray`) – VoxelData array to fill in. Returns: - `DataArray` – `data` unchanged when it has no `time` dimension or its time coordinate already carries all three attrs. Otherwise a copy with `volume_acquisition_reference` defaulted to `"start"`, `units` defaulted to `"s"`, and `volume_acquisition_duration` defaulted from the representative `time` step, when it can be inferred. Warns: - `UserWarning` – If any attr was missing and defaulted, naming which ones and what they were defaulted to. ### get\_representative\_time\_step ```python get_representative_time_step( data: DataArray, *, unit: str | None = None, uniformity_tolerance: float = 0.01, ) -> tuple[float | None, bool] ``` Return a representative time step for the time coordinate. Parameters: - #### **`data`** (`DataArray`) – DataArray containing the time coordinate. - #### **`unit`** (`str`, default: `None` ) – Unit in which to evaluate the representative step. If not provided, use the native units of the time coordinate. - #### **`uniformity_tolerance`** (`float`, default: `1e-2` ) – Maximum allowed per-interval relative deviation from the median consecutive difference for the time coordinate to be considered uniform. Returns: - **`step`** ( `float or None` ) – Representative time step in `unit` when provided, otherwise in the native units of the time coordinate. Returns `None` if fewer than two time points are available. - **`approximate`** ( `bool` ) – Whether the returned step is a median approximation derived from non-uniform sampling. ### get\_time\_coord\_to\_seconds\_factor ```python get_time_coord_to_seconds_factor(data: DataArray) -> float ``` Return the factor that converts a time coordinate to seconds. Parameters: - #### **`data`** (`DataArray`) – DataArray containing a `time` coordinate. Returns: - `float` – Multiplicative factor that converts values expressed in the time coordinate's units to seconds. Returns `1.0` when units are missing or unknown. Raises: - `ValueError` – If `data` does not have a `time` coordinate. Warns: - `UserWarning` – If the time coordinate has missing or unknown units. In that case, seconds are assumed. ### resample\_time ```python resample_time( data: DataArray, new_time: ArrayLike, *, method: Literal[ "linear", "nearest", "nearest-up", "zero", "slinear", "quadratic", "cubic", "previous", "next", ] = "linear", fill_value: float | tuple[float, float] | Literal["extrapolate", "nan"] = "extrapolate", ) -> DataArray ``` Resample data to new time coordinates. Parameters: - #### **`data`** (`DataArray`) – DataArray with a `time` coordinate. - #### **`new_time`** (`array_like`) – New time coordinates to resample to. - #### **`method`** (`(linear, nearest, nearest - up, zero, slinear, quadratic, cubic, previous, next)`, default: `"linear"` ) – Interpolation method passed to `scipy.interpolate.interp1d`: - `"linear"`: linear interpolation. - `"nearest"`: nearest-neighbour interpolation; rounds down at half-integers. - `"nearest-up"`: nearest-neighbour interpolation; rounds up at half-integers. - `"zero"`: zeroth-order spline (step function). - `"slinear"`: first-order spline. - `"quadratic"`: second-order spline. - `"cubic"`: third-order spline. - `"previous"`: use previous point's value. - `"next"`: use next point's value. - #### **`fill_value`** (`float or tuple[float, float] or {extrapolate, nan}`, default: `"extrapolate"` ) – How to handle target times that fall outside the range of the input time coordinates. `"extrapolate"` allows linear extrapolation. Use a float for a constant fill value, or a tuple `(left, right)` for different values on each side. Returns: - `DataArray` – DataArray resampled to `new_time` coordinates. Raises: - `ValueError` – If `data` does not have a `time` dimension, has only 1 timepoint, or its `time` coordinate is not strictly increasing. ### resample\_to\_uniform\_time ```python resample_to_uniform_time( data: DataArray, *, start: float | None = None, stop: float | None = None, step: float | None = None, method: Literal[ "linear", "nearest", "nearest-up", "zero", "slinear", "quadratic", "cubic", "previous", "next", ] = "linear", fill_value: float | tuple[float, float] | Literal["extrapolate", "nan"] = "extrapolate", ) -> DataArray ``` Resample data to a uniform time grid. Parameters: - #### **`data`** (`DataArray`) – DataArray with a `time` coordinate. - #### **`start`** (`float`, default: `None` ) – Start of the new time grid. If not provided, use the first time point. - #### **`stop`** (`float`, default: `None` ) – Stop of the new time grid. If not provided, use the last time point. - #### **`step`** (`float`, default: `None` ) – Time step for the uniform grid. If not provided, derive it from the input coordinate by estimating a representative interval from consecutive time differences. For non-uniform input, the median interval is used and a warning is emitted. The generated grid always starts at `start` and includes `stop` only when it falls on the `start + n * step` lattice. - #### **`method`** (`(linear, nearest, nearest - up, zero, slinear, quadratic, cubic, previous, next)`, default: `"linear"` ) – Interpolation method passed to `scipy.interpolate.interp1d`: - `"linear"`: linear interpolation. - `"nearest"`: nearest-neighbour interpolation; rounds down at half-integers. - `"nearest-up"`: nearest-neighbour interpolation; rounds up at half-integers. - `"zero"`: zeroth-order spline (step function). - `"slinear"`: first-order spline. - `"quadratic"`: second-order spline. - `"cubic"`: third-order spline. - `"previous"`: use previous point's value. - `"next"`: use next point's value. - #### **`fill_value`** (`float or tuple[float, float] or {extrapolate, nan}`, default: `"extrapolate"` ) – How to handle target times that fall outside the range of the input time coordinates. `"extrapolate"` allows linear extrapolation. Use a float for a constant fill value, or a tuple `(left, right)` for different values on each side. Returns: - `DataArray` – DataArray resampled to a uniform time grid. Raises: - `ValueError` – If `data` does not have a `time` dimension or has only 1 timepoint. - `ValueError` – If `start` is greater than or equal to `stop`, if `step` is not positive, or if no valid representative step can be derived. Warns: - `UserWarning` – If the original time coordinate is not uniform and `step` is not provided. In that case the median step is used. # `confusius.validation` ## validation Data validation utilities for confusius. Modules: - **`atlas`** – Atlas Dataset validation utilities. - **`coordinates`** – Coordinate validation utilities. - **`mask`** – Mask validation utilities. - **`registration`** – Validation helpers for registration transform DataArrays. - **`time_series`** – Time series validation utilities. - **`units`** – Unit validation utilities. - **`voxeldata`** – Validation helpers for the ConfUSIus VoxelData model and fUSI recordings. Functions: - **`canonicalize_voxeldata`** – Restore a scalar-indexed VoxelData dimension and its geometry. - **`ensure_labels`** – Canonicalize `labels` and `data`, then validate labels share data's grid. - **`ensure_mask`** – Canonicalize `mask` and `data`, then validate that `mask` shares data's grid. - **`ensure_time_aligned`** – Return `value` as a `(time, ...)` DataArray aligned with `signals`. - **`ensure_voxeldata`** – Return a canonical, validated VoxelData array. - **`validate_atlas`** – Validate that a Dataset is a well-formed atlas. - **`validate_bspline`** – Raise ValueError if `da` is not a valid B-spline transform DataArray. - **`validate_displacement_field`** – Raise ValueError if `da` is not a valid displacement field DataArray. - **`validate_labels`** – Validate that a label map shares data's VoxelData grid. - **`validate_mask`** – Validate that a mask shares data's VoxelData grid. - **`validate_matching_coordinates`** – Validate that selected coordinates match between two DataArrays. - **`validate_matching_spatial_units`** – Raise `ValueError` if world-space units disagree across DataArrays. - **`validate_time_series`** – Validate time series for time series processing operations. - **`validate_voxeldata`** – Validate a DataArray against the VoxelData model without modifying it. ### canonicalize\_voxeldata ```python canonicalize_voxeldata(data: DataArray) -> DataArray ``` Restore a scalar-indexed VoxelData dimension and its geometry. Scalar indexing such as `data.isel(j=0)` removes `j` from the array dimensions but retains it as a scalar coordinate. This function restores every missing native voxel dimension (`k`, `j`, `i`) as a length-one dimension, then orders all dimensions as `(...extra_dims, time, pose, k, j, i)`. It preserves coordinate values and their order. When scalar indexing fixed a dimension in a `VoxelToWorldIndex`, its geometry is rebuilt from the unchanged voxel-to-world affine, preserving the world-space units. Missing time acquisition metadata also defaults where possible. This function does not otherwise validate the VoxelData model; use ensure\_voxeldata to canonicalize and validate. Parameters: - #### **`data`** (`DataArray`) – DataArray to canonicalize. Returns: - `DataArray` – Canonicalized DataArray with all native voxel dimensions present. Raises: - `TypeError` – If `data` is not an `xarray.DataArray`. - `ValueError` – If a native voxel dimension is absent and has no scalar coordinate from which to restore it, or its same-named coordinate is not scalar. Warns: - `UserWarning` – If missing `time` or `slice_time` acquisition metadata is defaulted. ### ensure\_labels ```python ensure_labels( labels: DataArray, data: DataArray, labels_name: str = "labels", ) -> DataArray ``` Canonicalize `labels` and `data`, then validate labels share data's grid. Both `labels` and `data` are canonicalized via `ensure_voxeldata` (restoring any scalar-reduced voxel dims) before `validate_labels` checks them. Parameters: - #### **`labels`** (`DataArray`) – Label map to validate. Must have integer dtype. Accepts two formats: - **Flat label map**: Spatial dims only, e.g. `(k, j, i)`. Background voxels labeled `0`; each unique non-zero integer identifies a distinct, non-overlapping region. The `regions` coordinate of the output holds the integer label values. - **Stacked mask format**: Has a leading `mask` dimension followed by spatial dims, e.g. `(mask, k, j, i)`. Each layer has values in `{0, region_id}` and regions may overlap. The `region` coordinate of the output holds the `mask` coordinate values (e.g., region label). - #### **`data`** (`DataArray`) – VoxelData array to validate labels against. - #### **`labels_name`** (`str`, default: `"labels"` ) – Name of the labels parameter (used in error messages). Returns: - `DataArray` – The canonicalized `labels`. Raises: - `TypeError` – If `labels` is not an integer dtype DataArray. - `ValueError` – If `labels` or `data` isn't a VoxelData array, or if `labels`'s voxel grid doesn't match `data`'s. ### ensure\_mask ```python ensure_mask( mask: DataArray, data: DataArray, mask_name: str = "mask", require_exact_dims: bool = False, coerce_bool: bool = True, ) -> DataArray ``` Canonicalize `mask` and `data`, then validate that `mask` shares data's grid. Both `mask` and `data` are canonicalized via `ensure_voxeldata` (restoring any scalar-reduced voxel dims) before `validate_mask` checks them. Parameters: - #### **`mask`** (`DataArray`) – Mask to validate. Must have boolean dtype, or binary numeric dtype (0 = background, at most one non-zero value = foreground). The latter format is produced by `get_masks`, and also covers masks written by tools without a boolean dtype (e.g. FSL/NiBabel NIfTI masks with values in `{0, 1}` or `{0.0, 1.0}`). - #### **`data`** (`DataArray`) – VoxelData array to validate mask against. - #### **`mask_name`** (`str`, default: `"mask"` ) – Name of the mask parameter (used in error messages). - #### **`require_exact_dims`** (`bool`, default: `False` ) – Whether `mask.dims` must match all non-`time` dimensions of `data` in the same order. - #### **`coerce_bool`** (`bool`, default: `True` ) – Whether to coerce the returned `mask` to boolean dtype. Binary numeric masks (`{0, x}`) become `{False, True}` so callers can index with the result without the non-zero value being misread as a positional index. When False, `mask` is returned with its original dtype unchanged. Returns: - `DataArray` – The canonicalized `mask`, coerced to boolean dtype when `coerce_bool` is `True` (the default), otherwise returned with its original dtype. Raises: - `TypeError` – If `mask` is not a boolean or binary numeric DataArray. - `ValueError` – If `mask` or `data` isn't a VoxelData array, if `mask`'s voxel grid doesn't match `data`'s, or if `require_exact_dims` is set and `mask`'s dimensions don't match `data`'s. ### ensure\_time\_aligned ```python ensure_time_aligned( signals: DataArray, value: DataArray | ndarray | DataFrame, name: str, *, ndim: Literal[1, 2], allow_dataframe: bool = True, ) -> DataArray ``` Return `value` as a `(time, ...)` DataArray aligned with `signals`. `value` is validated against `signals` and returned as a `(time,)` DataArray (`ndim=1`) or a `(time, name)` DataArray (`ndim=2`, a 1D `value` becoming one column) with `time` as its first dimension: - A DataArray must have a `time` dimension. - A DataFrame must have a `time` column and at least one other, numeric column; the other columns become a `name` dimension named after them. - A NumPy array is wrapped with dims `(time, name)`. `value` must have as many timepoints as `signals`. When both carry `time` coordinates these must match within the default coordinate-comparison tolerance (`rtol=1e-5`, `atol=1e-8`). When only `signals` does, `value` is assumed to be ordered like `signals` along `time` and takes its `time` coordinates, with a warning since alignment cannot be verified. `signals` with a pose-dependent `(time, pose)` `time` coordinate are represented by their consolidated time (see `consolidate_poses`). Parameters: - #### **`signals`** (`(time, ...) xarray.DataArray`) – Signals defining the `time` grid. - #### **`value`** (`(time, ...) xarray.DataArray, numpy.ndarray, or pandas.DataFrame`) – Array to align. - #### **`name`** (`str`) – Name of `value` used in error and warning messages. If `value` is a DataFrame, its columns become the `name` dimension. - #### **`ndim`** (`(1, 2)`, default: `1` ) – Number of dimensions of the result: `1` for a single series such as a sample mask, `2` for a set of regressors such as confounds. - #### **`allow_dataframe`** (`bool`, default: `True` ) – Whether to accept a DataFrame `value`. Returns: - `(time,) or (time, confound) xarray.DataArray` – `value` with `time` as its first dimension and `ndim` dimensions. Raises: - `TypeError` – If `value` is not one of the accepted types. - `ValueError` – If `value` has no `time` dimension or column, has more than `ndim` dimensions, or does not have as many timepoints as `signals`; if a DataFrame `value` has duplicate or non-numeric columns or no column besides `time`; or if `time` coordinates do not match those of `signals`. Warns: - `UserWarning` – If `value` has no `time` coordinates while `signals` does, since alignment cannot be verified. - `UserWarning` – If the per-pose timing metadata of pose-dependent `signals` are insufficient to infer their whole-volume time, in which case the first pose's timestamps are used. ### ensure\_voxeldata ```python ensure_voxeldata( data: DataArray, **validate_kwargs: Any ) -> DataArray ``` Return a canonical, validated VoxelData array. This is the normal entry point for spatial inputs: it restores scalar-indexed native voxel dimensions and geometry, and orders dimensions as `(...extra_dims, time, pose, k, j, i)` with canonicalize\_voxeldata, then checks the resulting DataArray against the VoxelData model. Use validate\_voxeldata when the input must already follow that model. Parameters: - #### **`data`** (`DataArray`) – DataArray to canonicalize and validate. - #### **`**validate_kwargs`** (`Any`, default: `{}` ) – Keyword arguments forwarded to validate\_voxeldata. Returns: - `DataArray` – Canonicalized VoxelData array that satisfies the requested validation checks. Raises: - `TypeError` – If `data` is not an `xarray.DataArray`. - `ValueError` – If canonicalization or validation fails. ### validate\_atlas ```python validate_atlas( ds: Dataset, *, require_mesh_use: bool = False ) -> None ``` Validate that a Dataset is a well-formed atlas. Companion to `validate_voxeldata`. Checks that `ds` matches the atlas schema produced by `fetch_brainglobe_atlas` and consumed by the `.atlas` accessor: 1. **Type**: `ds` is an `xarray.Dataset`. 2. **Data variables**: `template`, `annotation`, and `hemispheres` are all present as data variables (a `hemispheres` stored as a coordinate is reported as missing). 3. **Grid**: the three variables share identical dimensions, those dimensions are a subset of `(k, j, i)` (a resampled single slice has a singleton `k`), and each variable carries a `VoxelToWorldIndex` deriving its world `z`/`y`/`x` coordinates. 4. **Data types**: `template` is floating-point; `annotation` and `hemispheres` are integer-valued. 5. **Attributes**: `attrs["structures"]` is present and is a brainglobe `StructuresDict`. The descriptive metadata the builder adds (`name`, `citation`, `species`, `orientation`) is not required. 6. **Affines**: where two data variables both define an affine of the same name (in `attrs["affines"]`), the matrices must be equal — a mismatch means the variables are not on a common world frame. 7. **Mesh use** (only when `require_mesh_use` is set): `attrs["world_to_base"]` — the pull mesh transform get\_meshes needs — is present, and at least one structure references a mesh file that exists on disk. Parameters: - #### **`ds`** (`Dataset`) – Dataset to validate as an atlas. - #### **`require_mesh_use`** (`bool`, default: `False` ) – Whether to also require the machinery `get_meshes` needs: the `world_to_base` transform attribute and at least one existing region mesh file. Raises: - `TypeError` – If `ds` is not an `xarray.Dataset`, or if `template` is not floating-point or `annotation`/`hemispheres` are not integer-valued. - `ValueError` – If any required data variable or attribute is missing, if the variables do not share dimensions that are a subset of `(k, j, i)`, if a variable lacks a `VoxelToWorldIndex`, if `attrs["structures"]` is not a brainglobe `StructuresDict`, or if `require_mesh_use` is set and `world_to_base` or usable region meshes are absent. Examples: ```pycon >>> from confusius.datasets import fetch_brainglobe_atlas >>> atlas = fetch_brainglobe_atlas("allen_mouse_100um") >>> validate_atlas(atlas) ``` ### validate\_bspline ```python validate_bspline(da: DataArray) -> None ``` Raise ValueError if `da` is not a valid B-spline transform DataArray. Parameters: - #### **`da`** (`DataArray`) – DataArray to validate. Raises: - `ValueError` – If `da.attrs["transform_type"] != "bspline_transform"`, required attrs are missing, or `da` is not a VoxelData array. ### validate\_displacement\_field ```python validate_displacement_field(da: DataArray) -> None ``` Raise ValueError if `da` is not a valid displacement field DataArray. Parameters: - #### **`da`** (`DataArray`) – DataArray to validate. Raises: - `ValueError` – If `da.attrs["type"] != "displacement_field_transform"`, `da` does not have `"component"` as its first dimension, or `da` is not a VoxelData array. ### validate\_labels ```python validate_labels( labels: DataArray, data: DataArray, labels_name: str = "labels", ) -> None ``` Validate that a label map shares data's VoxelData grid. `labels` and `data` must already be canonical VoxelData arrays (see `validate_voxeldata`) -- this does not canonicalize either. For a `labels`/`data` pair that may not already be canonical (e.g. a scalar-reduced voxel dim), use `ensure_labels` instead. Parameters: - #### **`labels`** (`DataArray`) – Label map to validate. Must have integer dtype. Accepts two formats: - **Flat label map**: Spatial dims only, e.g. `(k, j, i)`. Background voxels labeled `0`; each unique non-zero integer identifies a distinct, non-overlapping region. The `regions` coordinate of the output holds the integer label values. - **Stacked mask format**: Has a leading `mask` dimension followed by spatial dims, e.g. `(mask, k, j, i)`. Each layer has values in `{0, region_id}` and regions may overlap. The `region` coordinate of the output holds the `mask` coordinate values (e.g., region label). - #### **`data`** (`DataArray`) – VoxelData array to validate labels against. - #### **`labels_name`** (`str`, default: `"labels"` ) – Name of the labels parameter (used in error messages). Raises: - `TypeError` – If `labels` is not an integer dtype DataArray. - `ValueError` – If `labels` or `data` isn't a valid VoxelData array, or if `labels`'s voxel grid doesn't match `data`'s. ### validate\_mask ```python validate_mask( mask: DataArray, data: DataArray, mask_name: str = "mask", require_exact_dims: bool = False, ) -> None ``` Validate that a mask shares data's VoxelData grid. `mask` and `data` must already be canonical VoxelData arrays (see `validate_voxeldata`) -- this does not canonicalize either. For a `mask`/`data` pair that may not already be canonical (e.g. a scalar-reduced voxel dim), use `ensure_mask` instead. Parameters: - #### **`mask`** (`DataArray`) – Mask to validate. Must have boolean dtype, or binary numeric dtype (0 = background, at most one non-zero value = foreground). The latter format is produced by `get_masks`, and also covers masks written by tools without a boolean dtype (e.g. FSL/NiBabel NIfTI masks with values in `{0, 1}` or `{0.0, 1.0}`). - #### **`data`** (`DataArray`) – VoxelData array to validate mask against. - #### **`mask_name`** (`str`, default: `"mask"` ) – Name of the mask parameter (used in error messages). - #### **`require_exact_dims`** (`bool`, default: `False` ) – Whether `mask.dims` must match all non-`time` dimensions of `data` in the same order. Raises: - `TypeError` – If `mask` is not a boolean or binary numeric DataArray. - `ValueError` – If `mask` or `data` isn't a valid VoxelData array, if `mask`'s voxel grid doesn't match `data`'s, or if `require_exact_dims` is set and `mask`'s dimensions don't match `data`'s. ### validate\_matching\_coordinates ```python validate_matching_coordinates( left: DataArray, right: DataArray, coord_names: Hashable | Iterable[Hashable] | None = None, *, left_name: str = "left array", right_name: str = "right array", rtol: float = 1e-05, atol: float = 1e-08, ) -> None ``` Validate that selected coordinates match between two DataArrays. Comparison is performed on coordinate values rather than the full coordinate `DataArray`, so unrelated attached coordinates do not cause false mismatches. Numeric coordinates are compared with tolerance to accommodate harmless floating-point drift (for example after serialization and reload). Non-numeric coordinates are compared exactly. Parameters: - #### **`left`** (`DataArray`) – First array to compare. - #### **`right`** (`DataArray`) – Second array to compare. - #### **`coord_names`** (`Hashable or Iterable[Hashable]`, default: `None` ) – Coordinate names to compare. If not provided, all shared dimension coordinates are checked. - #### **`left_name`** (`str`, default: `"left array"` ) – Label used for `left` in error messages. Override with a context-specific name (e.g. `"run 0"`, `"map 0"`) for more actionable errors. - #### **`right_name`** (`str`, default: `"right array"` ) – Label used for `right` in error messages. - #### **`rtol`** (`float`, default: `1e-5` ) – Relative tolerance used for numeric coordinate comparison. - #### **`atol`** (`float`, default: `1e-8` ) – Absolute tolerance used for numeric coordinate comparison. Raises: - `ValueError` – If a requested coordinate is missing or if coordinates do not match. ### validate\_matching\_spatial\_units ```python validate_matching_spatial_units( arrays: Sequence[tuple[str, DataArray]], ) -> None ``` Raise `ValueError` if world-space units disagree across DataArrays. Parameters: - #### **`arrays`** (`sequence of tuple[str, xarray.DataArray]`) – Named DataArrays to compare. Each must carry voxel-to-world geometry. Raises: - `ValueError` – If any input lacks voxel-to-world geometry, or if any two inputs disagree on their `.fusi.affine.units`. ### validate\_time\_series ```python validate_time_series( time_series: DataArray, operation_name: str, require_unchunked_time: bool = True, require_sorted_time: bool = False, require_uniform_time: Literal[False] = False, uniformity_tolerance: float = 0.01, ) -> tuple[int, None] ``` ```python validate_time_series( time_series: DataArray, operation_name: str, require_unchunked_time: bool = True, require_sorted_time: bool = False, require_uniform_time: Literal[True] = True, uniformity_tolerance: float = 0.01, ) -> tuple[int, float] ``` ```python validate_time_series( time_series: DataArray, operation_name: str, require_unchunked_time: bool = True, require_sorted_time: bool = False, require_uniform_time: bool = False, uniformity_tolerance: float = 0.01, ) -> tuple[int, float | None] ``` Validate time series for time series processing operations. Performs common validation checks: 1. Time series have a `time` dimension. 2. Time dimension has more than 1 timepoint. 3. Time dimension is not chunked for Dask arrays (optional). 4. Time coordinate is strictly increasing (optional). 5. Time coordinate is uniformly sampled (optional). Parameters: - #### **`time_series`** (`DataArray`) – Input time series to validate. Must have a `time` dimension. - #### **`operation_name`** (`str`) – Name of the operation (used in error/warning messages). - #### **`require_unchunked_time`** (`bool`, default: `True` ) – Whether to require the time dimension to occupy one Dask chunk. Set to `False` for operations that can process chunked time (e.g., `confusius.signal.standardize`). - #### **`require_sorted_time`** (`bool`, default: `False` ) – Whether to require strictly increasing `time` coordinates. - #### **`require_uniform_time`** (`bool`, default: `False` ) – Whether to require uniformly sampled `time` coordinates and return their spacing. - #### **`uniformity_tolerance`** (`float`, default: `1e-2` ) – Maximum allowed relative range of consecutive time intervals, defined as `(max_interval - min_interval) / median_interval`. Raise a `ValueError` if the time coordinate exceeds this threshold. Returns: - **`time_axis`** ( `int` ) – Axis number for the `time` dimension. - **`time_spacing`** ( `float or None` ) – Time spacing when `require_uniform_time=True`, otherwise `None`. Raises: - `ValueError` – If `time_series` has no `time` dimension, if the `time` dimension has only 1 timepoint, if the `time` dimension is chunked in a Dask array (when `require_unchunked_time=True`), if `require_sorted_time=True` and the `time` coordinate is not strictly increasing, or if `require_uniform_time=True` and the `time` coordinate is not uniformly sampled. ### validate\_voxeldata ```python validate_voxeldata( data: DataArray, *, require_time: bool = False, require_unchunked_time: bool = False, require_uniform_time: bool = False, uniformity_tolerance: float = 0.01, allow_pose: bool = True, allow_extra_dims: bool = True, require_regular_spacing: bool = False, regular_spacing_tolerance: float = 0.01, regular_spacing_dims: RegularSpacingDims = "space", require_velocity_attrs: bool = False, require_dtype: Any | None = None, ) -> None ``` Validate a DataArray against the VoxelData model without modifying it. This requires non-empty native voxel dimensions (`k`, `j`, `i`), their coordinates, and a matching `VoxelToWorldIndex` (which carries the world-space `units` shared by `z`/`y`/`x`, exposed via `.fusi.affine.units`), and dimensions ordered as `(...extra_dims, time, pose, k, j, i)`. It also validates every dimension coordinate. When `time` is present, its acquisition metadata and `units` are required. The optional flags add requirements needed by a particular consumer. Use ensure\_voxeldata when scalar indexing may have removed a voxel dimension and the input should be canonicalized first. Parameters: - #### **`data`** (`DataArray`) – DataArray to validate. - #### **`require_time`** (`bool`, default: `False` ) – Whether to require `time` with more than one coordinate value. - #### **`require_unchunked_time`** (`bool`, default: `False` ) – Whether to require `time` with more than one coordinate value in a single Dask chunk. - #### **`require_uniform_time`** (`bool`, default: `False` ) – Whether to require `time` with more than one uniformly spaced coordinate value. - #### **`uniformity_tolerance`** (`float`, default: `1e-2` ) – Maximum relative variation allowed between consecutive time intervals. - #### **`allow_pose`** (`bool`, default: `True` ) – Whether to allow a `pose` dimension. - #### **`allow_extra_dims`** (`bool`, default: `True` ) – Whether to allow dimensions outside `time`, `pose`, `k`, `j`, and `i`. - #### **`require_regular_spacing`** (`bool`, default: `False` ) – Whether to require regular spacing for selected numeric dimension coordinates. - #### **`regular_spacing_tolerance`** (`float`, default: `1e-2` ) – Relative tolerance used to assess coordinate regularity. - #### **`regular_spacing_dims`** (`('space', 'core', 'all')`, default: `"space"` ) – Dimensions to check for regular spacing. `"space"` checks `k`, `j`, and `i`; `"core"` checks present core dimensions; `"all"` checks every dimension. - #### **`require_velocity_attrs`** (`bool`, default: `False` ) – Whether to require positive, finite `transmit_frequency` and `beamforming_sound_velocity` DataArray attributes. - #### **`require_dtype`** (`Any`, default: `None` ) – Required data dtype or dtype class, passed to `numpy.issubdtype`. Raises: - `TypeError` – If `data` is not an `xarray.DataArray` or its dtype does not satisfy `require_dtype`. - `ValueError` – If VoxelData geometry, dimensions, coordinates, timing, spacing, or metadata validation fails. # `confusius.xarray` ## xarray Xarray extensions for fUSI data analysis. Modules: - **`accessors`** – Xarray accessor for fUSI-specific operations. - **`affine`** – Xarray accessor for affine transform operations. - **`connectivity`** – Xarray accessor for connectivity analysis. - **`create`** – Constructor helpers for building VoxelData arrays. - **`extract`** – Xarray accessor for signal extraction. - **`iq`** – Xarray accessor for IQ processing. - **`plotting`** – Xarray accessor for plotting. - **`registration`** – Xarray accessor for registration. - **`scale`** – Xarray accessor for scaling operations. Classes: - **`FUSIAccessor`** – Xarray accessor for fUSI-specific operations. - **`FUSIAffineAccessor`** – Accessor for affine transform operations on VoxelData arrays. - **`FUSIConnectivityAccessor`** – Xarray accessor for seed-based functional connectivity analysis. - **`FUSIExtractAccessor`** – Xarray accessor for signal extraction operations. - **`FUSIIQAccessor`** – Accessor for IQ processing operations on fUSI data. - **`FUSIPlotAccessor`** – Accessor for plotting fUSI data. - **`FUSIRegistrationAccessor`** – Accessor for registration operations on fUSI data. - **`FUSIScaleAccessor`** – Accessor for scaling operations on fUSI data. Functions: - **`apply_affine`** – Apply a world-space affine to a DataArray's world coordinates. - **`create_voxeldata`** – Build a VoxelData array from a raw array. - **`db_scale`** – Convert data to decibel scale relative to maximum value. - **`get_bounding_box`** – Return the world-space bounding box of a VoxelData array. - **`get_relative_affine`** – Return the affine mapping `da`'s world space into `other`'s. - **`log_scale`** – Apply natural logarithm to data. - **`power_scale`** – Apply power scaling to data. - **`reindex_voxels`** – Rebase voxel coordinates to dense positions without moving world coordinates. - **`reindex_voxels_like`** – Rebase voxel coordinates onto `reference`'s voxel labels. ### FUSIAccessor Xarray accessor for fUSI-specific operations. Provides convenient methods for functional ultrasound imaging data analysis. Parameters: - #### **`xarray_obj`** (`DataArray`) – The `DataArray` to wrap. Examples: ```pycon >>> import xarray as xr >>> import numpy as np >>> from confusius import xarray as cxr # Registers the accessor >>> data = xr.DataArray([1, 10, 100, 1000]) >>> data.fusi.scale.db(factor=20) array([-60., -40., -20., 0.]) ``` Methods: - **`save`** – Save the DataArray to file, dispatching by extension. Attributes: - **`affine`** (`FUSIAffineAccessor`) – Access affine transform operations. - **`connectivity`** (`FUSIConnectivityAccessor`) – Access connectivity analysis operations. - **`direction`** – World-space direction matrix for the present spatial geometry. - **`extract`** (`FUSIExtractAccessor`) – Access signal extraction operations. - **`iq`** (`FUSIIQAccessor`) – Access IQ processing operations. - **`origin`** (`dict[str, float]`) – World origin metadata for the DataArray. - **`plot`** (`FUSIPlotAccessor`) – Access plotting operations. - **`register`** (`FUSIRegistrationAccessor`) – Access registration operations. - **`scale`** (`FUSIScaleAccessor`) – Access scaling operations. - **`spacing`** (`dict[str, float | None]`) – Coordinate spacing for all dimensions. #### affine ```python affine: FUSIAffineAccessor ``` Access affine transform operations. Returns: - `FUSIAffineAccessor` – Accessor for computing relative transforms between scans and for applying axis-aligned affines to spatial coordinates. Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> import confusius # noqa: F401 >>> eye = np.eye(4) >>> a = xr.DataArray(np.zeros((2, 2)), attrs={"affines": {"to_world": eye}}) >>> b = xr.DataArray(np.zeros((2, 2)), attrs={"affines": {"to_world": eye}}) >>> np.allclose(a.fusi.affine.to(b, via="to_world"), np.eye(4)) True ``` #### connectivity ```python connectivity: FUSIConnectivityAccessor ``` Access connectivity analysis operations. Returns: - `FUSIConnectivityAccessor` – Accessor for seed-based functional connectivity maps. Examples: ```pycon >>> import xarray as xr >>> import numpy as np >>> import confusius # noqa: F401 >>> data = xr.open_zarr("recording.zarr")["power_doppler"] >>> seed_masks = xr.open_zarr("seed_masks.zarr")["masks"] >>> mapper = data.fusi.connectivity.seed_map(seed_masks=seed_masks) ``` #### direction ```python direction ``` World-space direction matrix for the present spatial geometry. Columns are unit world-space direction vectors for each voxel-space axis (`k`/`j`/`i`, in that order); rows correspond to world axes (`z`/`y`/`x`, in that order). Direction is expressed in dense array-position terms: a voxel coordinate that runs descending (e.g. after `.isel(dim=slice(None, None, -1))`) flips the sign of its column, since array position always counts up from 0 regardless of the coordinate's own direction. Returns: - `(3, 3) numpy.ndarray` – Identity for axis-aligned data. For oblique data, the columns are the unit world-space directions of the voxel axes. Raises: - `ValueError` – If `self` does not carry a voxel-to-world index. #### extract ```python extract: FUSIExtractAccessor ``` Access signal extraction operations. Returns: - `FUSIExtractAccessor` – Accessor for extracting signals from fUSI data and reconstructing fUSI data from processed signals. Examples: ```pycon >>> import xarray as xr >>> data = xr.open_zarr("output.zarr")["power_doppler"] >>> mask = xr.open_zarr("brain_mask.zarr")["mask"] >>> signals = data.fusi.extract.with_mask(mask) >>> # ... process signals ... >>> restored = signals.fusi.extract.unmask(mask) ``` #### iq ```python iq: FUSIIQAccessor ``` Access IQ processing operations. Returns: - `FUSIIQAccessor` – Accessor for IQ processing methods. Examples: ```pycon >>> import xarray as xr >>> ds = xr.open_zarr("output.zarr") >>> iq = ds["iq"] >>> pwd = iq.fusi.iq.process_to_power_doppler() >>> velocity = iq.fusi.iq.process_to_axial_velocity() ``` #### origin ```python origin: dict[str, float] ``` World origin metadata for the DataArray. Non-spatial dimensions use their first coordinate value. Spatial origin is returned in world coordinate order as the world location of the first sampled voxel under the DataArray's voxel-to-world affine. Returns: - `dict[str, float]` – Origin metadata for the DataArray. Raises: - `ValueError` – If `self` does not carry a voxel-to-world index. Examples: ```pycon >>> import xarray as xr >>> import numpy as np >>> import confusius # noqa: F401 >>> data = xr.DataArray( ... np.zeros((3, 10, 20)), ... dims=["k", "j", "i"], ... coords={"k": np.arange(3), "j": np.arange(10), "i": np.arange(20)}, ... ) >>> voxel_to_world = np.array( ... [[0.2, 0.0, 0.0, 1.0], [0.0, 0.1, 0.0, 2.0], [0.0, 0.0, 0.05, 3.0], [0.0, 0.0, 0.0, 1.0]] ... ) >>> data = data.fusi.affine.set_voxel_to_world(voxel_to_world) >>> data.fusi.origin {'z': 1.0, 'y': 2.0, 'x': 3.0} ``` #### plot ```python plot: FUSIPlotAccessor ``` Access plotting operations. Returns: - `FUSIPlotAccessor` – Accessor for plotting methods. Examples: ```pycon >>> import xarray as xr >>> data = xr.open_zarr("output.zarr")["iq"] >>> viewer, layer = data.fusi.plot.napari() ``` #### register ```python register: FUSIRegistrationAccessor ``` Access registration operations. Returns: - `FUSIRegistrationAccessor` – Accessor for registration methods. Examples: ```pycon >>> import xarray as xr >>> data = xr.open_zarr("output.zarr")["power_doppler"] >>> registered = data.fusi.register.volumewise(reference_time=0) ``` #### scale ```python scale: FUSIScaleAccessor ``` Access scaling operations. Returns: - `FUSIScaleAccessor` – Accessor for scaling transformations. Examples: ```pycon >>> data = xr.DataArray([1, 10, 100, 1000]) >>> data.fusi.scale.db(factor=10) array([-30., -20., -10., 0.]) ``` #### spacing ```python spacing: dict[str, float | None] ``` Coordinate spacing for all dimensions. Spacing is reported in DataArray dimension order. For native voxel dimensions `k/j/i`, each voxel-space dimension receives its world step length derived from the voxel-to-world affine column norm and the 1D voxel-coordinate step. A coordinate is considered uniform if every interval is within 1% of the median interval (per-interval `|diff - median| <= 0.01 * |median|`). `time` has no such fallback: `volume_acquisition_duration` is the time to acquire one volume, not the step between volumes, so a singleton or non-uniform `time` reports `None` here like any other dimension without a defined step. Returns: - `dict[str, float | None]` – Spacing per dimension. Returns `None` for dimensions with non-uniform or undefined spacing, with a warning. Raises: - `ValueError` – If `self` does not carry a voxel-to-world index. Examples: ```pycon >>> import xarray as xr >>> import numpy as np >>> import confusius # noqa: F401 >>> data = xr.DataArray( ... np.zeros((3, 10, 20)), ... dims=["k", "j", "i"], ... coords={"k": np.arange(3), "j": np.arange(10), "i": np.arange(20)}, ... ) >>> data = data.fusi.affine.set_voxel_to_world( ... np.diag([0.2, 0.1, 0.05, 1.0]) ... ) >>> data.fusi.spacing {'k': 0.2, 'j': 0.1, 'i': 0.05} ``` #### save ```python save(path: str | Path, **kwargs: Any) -> None ``` Save the DataArray to file, dispatching by extension. Supported formats: - **NIfTI** (`.nii`, `.nii.gz`): saved via `save_nifti`. - **Zarr** (`.zarr`): saved via `xarray.DataArray.to_zarr`. Parameters: - ##### **`path`** (`str or Path`) – Output path. The extension determines the format. - ##### **`**kwargs`** (`Any`, default: `{}` ) – Additional keyword arguments forwarded to the underlying saver. Examples: ```pycon >>> import xarray as xr >>> import numpy as np >>> import confusius # noqa: F401 >>> data = xr.DataArray( ... np.zeros((10, 32, 1, 64)), dims=["time", "k", "j", "i"] ... ) >>> data.fusi.save("recording.nii.gz") ``` ### FUSIAffineAccessor Accessor for affine transform operations on VoxelData arrays. Provides methods to compute relative transforms between scans and to apply axis-aligned affines to a scan's spatial coordinates. Parameters: - #### **`xarray_obj`** (`DataArray`) – The `DataArray` to wrap. Methods: - **`apply`** – Apply a world-space affine to a DataArray's world coordinates. - **`reindex_voxels`** – Rebase voxel coordinates to dense positions without moving world coordinates. - **`reindex_voxels_like`** – Rebase voxel coordinates onto `reference`'s voxel labels. - **`set_units`** – Replace the world-space unit, rebuilding the VoxelToWorldIndex. - **`set_voxel_to_world`** – Replace voxel-to-world geometry. - **`to`** – Return the affine mapping `self`'s world space into `other`'s. Attributes: - **`bounding_box`** (`DataArray`) – World-space bounding box of the wrapped VoxelData array. - **`units`** (`str`) – World-space unit shared by every world coordinate (e.g. `"mm"`). - **`voxel_to_world`** (`NDArray[float64]`) – Affine mapping native voxel coordinates to world coordinates. #### bounding\_box ```python bounding_box: DataArray ``` World-space bounding box of the wrapped VoxelData array. Encloses the full extent of every voxel (half a voxel beyond the outermost voxel centers), see get\_bounding\_box for details. Returns: - `(2, 3) xarray.DataArray` – Bounding box with dims `(bound, component)`, where `bound` is `["min", "max"]` and `component` is `["z", "y", "x"]`, and a `units` attr from the voxel-to-world index. Pose-dependent geometry adds a leading `pose` dim carrying the input's `pose` coordinate, one bounding box per pose. Raises: - `ValueError` – If `self` is not a valid VoxelData array, or if a voxel dim has irregular coordinates. Examples: ```pycon >>> import numpy as np >>> import confusius # noqa: F401 >>> from confusius.xarray import create_voxeldata >>> data = create_voxeldata( ... np.zeros((2, 3, 4)), dims=("k", "j", "i"), spacing=(1.0, 1.0, 1.0) ... ) >>> data.fusi.affine.bounding_box.sel(bound="max", component="x").item() 2.0 ``` #### units ```python units: str ``` World-space unit shared by every world coordinate (e.g. `"mm"`). Returns: - `str` – Physical unit shared by `z`/`y`/`x`, since they're derived jointly from one affine. #### voxel\_to\_world ```python voxel_to_world: NDArray[float64] ``` Affine mapping native voxel coordinates to world coordinates. Returns: - `ndarray` – Homogeneous voxel-to-world affine. #### apply ```python apply( affine: NDArray[float64] | str, inplace: bool = False ) -> DataArray ``` Apply a world-space affine to a DataArray's world coordinates. The transform is composed into the DataArray's `VoxelToWorldIndex`, derived world coordinates are regenerated, and existing `attrs["affines"]` entries are re-expressed against the new world frame. Parameters: - ##### **`affine`** (`(4, 4) numpy.ndarray or str`) – Homogeneous world-space affine matrix to apply. If a string, it is looked up as a key in `self.attrs["affines"]`. - ##### **`inplace`** (`bool`, default: `False` ) – Whether to modify the DataArray in-place. Returns: - `DataArray` – The DataArray with updated spatial coordinates and `attrs["affines"]`. No entry is removed: when `affine` is a string, that key is kept as the identity, since the world frame now is that named space. Raises: - `ValueError` – If `self` lacks voxel-to-world geometry, if `affine` shape does not match the DataArray's voxel-to-world affine, or if `affine` is a string and `self` has no `"affines"` entry in `attrs`. - `KeyError` – If `affine` is a string not present in `self.attrs["affines"]`. Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> import confusius # noqa: F401 >>> data = xr.DataArray( ... np.zeros((3, 4)), ... dims=["j", "i"], ... coords={"j": np.arange(3), "i": np.arange(4)}, ... ) >>> data = data.fusi.affine.set_voxel_to_world(np.eye(3)) >>> shift = np.eye(3) >>> shift[:2, 2] = [10.0, 5.0] >>> result = data.fusi.affine.apply(shift) >>> float(result.fusi.affine.voxel_to_world[0, 2]) 10.0 ``` #### reindex\_voxels ```python reindex_voxels(*, inplace: bool = False) -> DataArray ``` Rebase voxel coordinates to dense positions without moving world coordinates. See reindex\_voxels for details. Parameters: - ##### **`inplace`** (`bool`, default: `False` ) – Whether to modify the wrapped DataArray in-place. Returns: - `DataArray` – DataArray with voxel coordinates rebased to `0, 1, ..., dim - 1` and an updated `voxel_to_world` affine. World coordinates are unchanged. Raises: - `ValueError` – If `self` lacks voxel-to-world geometry, or if world spacing is undefined for any voxel dimension. Examples: ```pycon >>> import numpy as np >>> import confusius # noqa: F401 >>> from confusius.xarray import create_voxeldata >>> base = create_voxeldata( ... np.zeros((5, 5)), dims=("j", "i"), voxel_to_world=np.eye(4) ... ) >>> data = base.isel(j=slice(2, 5), i=slice(1, 5)) >>> reindexed = data.fusi.affine.reindex_voxels() >>> reindexed.coords["j"].values array([0, 1, 2]) >>> float(reindexed.coords["y"].isel(j=0, i=0, k=0)) 2.0 ``` #### reindex\_voxels\_like ```python reindex_voxels_like( reference: DataArray, *, atol: float = 1e-06, inplace: bool = False, ) -> DataArray ``` Rebase voxel coordinates onto `reference`'s voxel labels. See reindex\_voxels\_like for details. Parameters: - ##### **`reference`** (`DataArray`) – DataArray whose voxel labels and affine `self` should adopt. - ##### **`atol`** (`float`, default: `1e-6` ) – Absolute tolerance, in `reference`'s physical units, for the world-coordinate alignment check between `self` and `reference`. - ##### **`inplace`** (`bool`, default: `False` ) – Whether to modify the wrapped DataArray in-place. Returns: - `DataArray` – `self` with voxel coordinates and `voxel_to_world` replaced by `reference`'s. World coordinates are unchanged. Raises: - `ValueError` – If `self` or `reference` lacks voxel-to-world geometry, if their voxel dimensions or shapes differ, or if their world coordinates do not match within `atol`. #### set\_units ```python set_units( units: str, *, inplace: bool = False ) -> DataArray ``` Replace the world-space unit, rebuilding the VoxelToWorldIndex. Parameters: - ##### **`units`** (`str`) – New physical unit shared by every world coordinate. - ##### **`inplace`** (`bool`, default: `False` ) – Whether to modify the wrapped DataArray in-place. Returns: - `DataArray` – DataArray with rebuilt VoxelToWorldIndex-backed coordinates. #### set\_voxel\_to\_world ```python set_voxel_to_world( voxel_to_world: ArrayLike, *, units: str | None = None, inplace: bool = False, ) -> DataArray ``` Replace voxel-to-world geometry. Parameters: - ##### **`voxel_to_world`** (`ArrayLike`) – Homogeneous affine mapping native voxel coordinates to world coordinates. - ##### **`units`** (`str`, default: `None` ) – New physical unit shared by every world coordinate. If not provided, the existing unit is kept, or `"mm"` if there is none yet (e.g. when attaching geometry for the first time onto a plain, not-yet-indexed DataArray). - ##### **`inplace`** (`bool`, default: `False` ) – Whether to modify the wrapped DataArray in-place. Returns: - `DataArray` – DataArray with rebuilt VoxelToWorldIndex-backed coordinates. #### to ```python to(other: DataArray, via: str) -> NDArray[float64] ``` Return the affine mapping `self`'s world space into `other`'s. Computes `inv(other.attrs["affines"][via]) @ self.attrs["affines"][via]`, giving the transform from `self`'s world frame to `other`'s. Parameters: - ##### **`other`** (`DataArray`) – The scan whose world space is the target. - ##### **`via`** (`str`) – Key into `attrs["affines"]` naming the shared intermediate coordinate space (e.g. `"world_to_lab"`). Returns: - `(ndarray, shape(4, 4))` – Homogeneous affine matrix mapping `self`'s world coordinates to `other`'s world coordinates. Raises: - `KeyError` – If `via` is not present in either scan's `attrs["affines"]`. - `ValueError` – If either scan has no `"affines"` entry in `attrs`. Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> import confusius # noqa: F401 >>> eye = np.eye(4) >>> a = xr.DataArray(np.zeros((2, 2)), attrs={"affines": {"to_world": eye}}) >>> b = xr.DataArray(np.zeros((2, 2)), attrs={"affines": {"to_world": eye}}) >>> np.allclose(a.fusi.affine.to(b, via="to_world"), np.eye(4)) True ``` ### FUSIConnectivityAccessor Xarray accessor for seed-based functional connectivity analysis. Parameters: - #### **`xarray_obj`** (`DataArray`) – The DataArray to wrap. Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> import confusius # noqa: F401 >>> >>> data = xr.open_zarr("recording.zarr")["power_doppler"] >>> seed_masks = xr.open_zarr("seed_masks.zarr")["masks"] >>> mapper = data.fusi.connectivity.seed_map(seed_masks=seed_masks) ``` Methods: - **`seed_map`** – Fit a seed-based correlation map. #### seed\_map ```python seed_map( *, seed_masks: DataArray | None = None, seed_signals: DataArray | None = None, labels_reduction: Literal[ "mean", "sum", "median", "min", "max", "var", "std" ] = "mean", clean_kwargs: dict | None = None, ) -> SeedBasedMaps ``` Fit a seed-based correlation map. Convenience wrapper around `SeedBasedMaps` that constructs the estimator, calls `SeedBasedMaps.fit` on the wrapped DataArray, and returns the fitted estimator. Parameters: - ##### **`seed_masks`** (`DataArray`, default: `None` ) – Integer label map defining the seed region(s). See `SeedBasedMaps` for accepted formats. Mutually exclusive with `seed_signals`. - ##### **`seed_signals`** (`DataArray`, default: `None` ) – Pre-computed `(time, ...)` seed signals used directly for correlation. When provided, seed extraction from the data is skipped. Mutually exclusive with `seed_masks`. - ##### **`labels_reduction`** (`(mean, sum, median, min, max, var, std)`, default: `"mean"` ) – Aggregation function applied across voxels within each seed region. Ignored when `seed_signals` is provided. - ##### **`clean_kwargs`** (`dict`, default: `None` ) – Keyword arguments forwarded to `clean`. If not provided, no cleaning is applied. Returns: - `SeedBasedMaps` – Fitted estimator. Access `maps_` and `seed_signals_` on the returned object. Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> import confusius # noqa: F401 >>> >>> data = xr.open_zarr("recording.zarr")["power_doppler"] >>> seed_masks = xr.open_zarr("seed_masks.zarr")["masks"] >>> mapper = data.fusi.connectivity.seed_map(seed_masks=seed_masks) ``` ### FUSIExtractAccessor Xarray accessor for signal extraction operations. Provides convenient methods for extracting signals from VoxelData arrays by flattening spatial dimensions, and reconstructing VoxelData arrays from processed signals. Parameters: - #### **`xarray_obj`** (`DataArray`) – DataArray to wrap. Extraction methods expect a VoxelData array; `unmask` expects an already-extracted signals array. Examples: ```pycon >>> import numpy as np >>> from confusius.xarray import create_voxeldata >>> >>> # 3D+t data: (time, k, j, i), native voxel dims with world z/y/x derived >>> # from the attached VoxelToWorldIndex. >>> data = create_voxeldata( ... np.random.randn(100, 10, 20, 30), ... dims=("time", "k", "j", "i"), ... dt=1.0, ... spacing=(1.0, 1.0, 1.0), ... ) >>> mask = create_voxeldata( ... np.random.rand(10, 20, 30) > 0.5, ... dims=("k", "j", "i"), ... spacing=(1.0, 1.0, 1.0), ... ) >>> >>> # Extract signals >>> signals = data.fusi.extract.with_mask(mask) >>> signals.dims ('time', 'space') >>> >>> # Reconstruct full spatial volume from signals >>> reconstructed = signals.fusi.extract.unmask(mask) >>> reconstructed.dims ('time', 'k', 'j', 'i') ``` Methods: - **`unmask`** – Reconstruct N-D volume from masked signals. - **`with_labels`** – Extract region-aggregated signals using an integer label map. - **`with_mask`** – Extract signals using a boolean or binary numeric mask. #### unmask ```python unmask( mask: DataArray, fill_value: float = 0.0 ) -> DataArray ``` Reconstruct N-D volume from masked signals. Reconstructs the full spatial volume from a DataArray of signals, which must have a `space` dimension. This is a convenience wrapper around `confusius.extract.unmask()`. Parameters: - ##### **`mask`** (`DataArray`) – Boolean VoxelData mask array used for the original extraction. Provides native voxel dimensions and VoxelData geometry for reconstruction. - ##### **`fill_value`** (`float`, default: `0.0` ) – Value to fill in non-masked voxels. Returns: - `DataArray` – Reconstructed VoxelData array with shape `(..., k, j, i)` where native voxel dimensions and VoxelData geometry come from the mask. Examples: ```pycon >>> signals = data.fusi.extract.with_mask(mask) >>> signals.dims ('time', 'space') >>> reconstructed = signals.fusi.extract.unmask(mask) >>> reconstructed.dims ('time', 'k', 'j', 'i') ``` #### with\_labels ```python with_labels( labels: DataArray, reduction: Literal[ "mean", "sum", "median", "min", "max", "var", "std" ] = "mean", ) -> DataArray ``` Extract region-aggregated signals using an integer label map. Parameters: - ##### **`labels`** (`DataArray`) – Integer label map in one of two formats: - **Flat label map**: Spatial dims only, e.g. `(k, j, i)`. Background voxels labeled `0`; each unique non-zero integer identifies a distinct, non-overlapping region. The `regions` coordinate of the output holds the integer label values. - **Stacked mask format**: Has a leading `mask` dimension followed by spatial dims, e.g. `(mask, k, j, i)`. Each layer has values in `{0, region_id}` and regions may overlap. The `region` coordinate of the output holds the `mask` coordinate values (e.g., region label). - ##### **`reduction`** (`(mean, sum, median, min, max, var, std)`, default: `"mean"` ) – Aggregation function applied across voxels in each region: - `"mean"`: arithmetic mean. - `"sum"`: sum of values. - `"median"`: median value. - `"min"`: minimum value. - `"max"`: maximum value. - `"var"`: variance. - `"std"`: standard deviation. Returns: - `DataArray` – Array with spatial dimensions replaced by a `region` dimension. The `region` dimension has integer coordinates corresponding to each unique non-zero label in `labels`. All non-spatial dimensions are preserved. For example: - `(time, k, j, i)` → `(time, region)` - `(time, pose, k, j, i)` → `(time, pose, region)` - `(k, j, i)` → `(region,)` Raises: - `ValueError` – If `labels` dimensions don't match `data`'s spatial dimensions, if coordinates don't match, or if `reduction` is not a valid option. - `TypeError` – If `labels` is not integer dtype. Examples: ```pycon >>> signals = data.fusi.extract.with_labels(labels) >>> signals.dims ("time", "region") >>> signals.coords["region"].values array([1, 2, 3]) >>> >>> # Sum per region instead of mean. >>> sums = data.fusi.extract.with_labels(labels, reduction="sum") ``` #### with\_mask ```python with_mask(mask: DataArray) -> DataArray ``` Extract signals using a boolean or binary numeric mask. Parameters: - ##### **`mask`** (`DataArray`) – Mask defining which voxels to extract. Its dimensions define the spatial dimensions that will be flattened. Must have boolean dtype, or binary numeric dtype (0 = background, at most one non-zero value = foreground). The latter format is produced by `get_masks`. Coordinates must match data. Returns: - `DataArray` – Array with spatial dimensions flattened into a `space` dimension. All non-spatial dimensions are preserved. The `space` dimension has a MultiIndex storing spatial coordinates. For simple round-trip reconstruction, use `.unstack("space")` which re-creates the original DataArray using the smallest bounding box. For full mask shape reconstruction, use `.fusi.extract.unmask()`. Raises: - `ValueError` – If `mask` dimensions don't match `data`'s spatial dimensions. - `TypeError` – If `mask` is not boolean dtype. Examples: ```pycon >>> signals = data.fusi.extract.with_mask(mask) >>> signals.dims ("time", "space") >>> >>> # Quick bounding box reconstruction >>> bbox = signals.unstack("space") >>> >>> # Full mask shape reconstruction >>> full = signals.fusi.extract.unmask(mask) ``` ### FUSIIQAccessor Accessor for IQ processing operations on fUSI data. This accessor provides methods to process beamformed IQ data into derived quantities such as power Doppler and axial velocity. Parameters: - #### **`xarray_obj`** (`DataArray`) – The DataArray to wrap. Must contain complex beamformed IQ data with dimensions `(time, k, j, i)`. Examples: ```pycon >>> import xarray as xr >>> ds = xr.open_zarr("iq_data.zarr") >>> iq = ds["iq"] >>> pwd = iq.fusi.iq.process_to_power_doppler(low_cutoff=40) ``` Methods: - **`process_to_axial_velocity`** – Process beamformed IQ into axial velocity volumes. - **`process_to_bmode`** – Process beamformed IQ into B-mode volumes. - **`process_to_power_doppler`** – Process beamformed IQ into power Doppler volumes. #### process\_to\_axial\_velocity ```python process_to_axial_velocity( clutter_window_width: int | None = None, clutter_window_stride: int | None = None, filter_method: Literal[ "svd_indices", "svd_energy", "svd_cumulative_energy", "butterworth", ] = "svd_indices", clutter_mask: DataArray | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, butterworth_order: int = 4, velocity_window_width: int | None = None, velocity_window_stride: int | None = None, lag: int = 1, spatial_kernel: int | tuple[int, int, int] | list[int] = 3, ) -> DataArray ``` Process beamformed IQ into axial velocity volumes. This method computes axial velocity volumes from beamformed IQ data using nested sliding windows. A first sliding window is used for clutter filtering. Inside each clutter-filtered window, axial velocity volumes are computed using a second sliding window. Parameters: - ##### **`clutter_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for clutter filtering, in volumes. If not provided, uses the chunk size of the IQ data along the temporal dimension. - ##### **`clutter_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window for clutter filtering, in volumes. If not provided, equals `clutter_window_width`. - ##### **`filter_method`** (`(svd_indices, svd_energy, svd_cumulative_energy, butterworth)`, default: `"svd_indices"` ) – Clutter filtering method to apply before velocity computation. - `"svd_indices"`: Static SVD filter using singular vector indices. - `"svd_energy"`: Adaptive SVD filter using singular vector energies. - `"svd_cumulative_energy"`: Adaptive SVD filter using cumulative energies. - `"butterworth"`: Butterworth frequency-domain filter. - ##### **`clutter_mask`** (`(k, j, i) xarray.DataArray`, default: `None` ) – Boolean mask to define clutter regions. Only used by SVD-based clutter filters to compute clutter vectors from masked voxels. If not provided, all voxels are used. The mask spatial dimensions and coordinates must match the IQ data. - ##### **`low_cutoff`** (`int or float`, default: `None` ) – Low cutoff for clutter filtering. Interpretation depends on `filter_method`. If not provided, uses method-specific defaults. - ##### **`high_cutoff`** (`int or float`, default: `None` ) – High cutoff for clutter filtering. Interpretation depends on `filter_method`. If not provided, uses method-specific defaults. - ##### **`butterworth_order`** (`int`, default: `4` ) – Order of Butterworth filter. Effective order is doubled due to forward-backward filtering. - ##### **`velocity_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for velocity estimation, in volumes. If not provided, equals `clutter_window_width`. - ##### **`velocity_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window for velocity estimation, in volumes. If not provided, equals `velocity_window_width`. - ##### **`lag`** (`int`, default: `1` ) – Temporal lag in volumes for autocorrelation computation. Must be positive. - ##### **`spatial_kernel`** (`int or tuple[int, int, int] or list[int]`, default: `3` ) – Size of the median filter kernel applied spatially to denoise. A scalar uses the same kernel size on all spatial axes; a length-3 sequence specifies `(k, j, i)` sizes directly. Values must be positive. Any even sizes are rounded up to the next odd size. If all sizes are `1`, no spatial filtering is applied. Returns: - `(clutter_windows * velocity_windows, k, j, i) xarray.DataArray` – Axial velocity volumes with updated time coordinates, where `clutter_windows` is the number of clutter filter sliding windows and `velocity_windows` is the number of velocity sliding windows per clutter window. Velocity values are in meters per second. Examples: ```pycon >>> import xarray as xr >>> ds = xr.open_zarr("iq_data.zarr") >>> iq = ds["iq"] >>> velocity = iq.fusi.iq.process_to_axial_velocity( ... clutter_window_width=50, ... velocity_window_width=25, ... ) ``` #### process\_to\_bmode ```python process_to_bmode( bmode_window_width: int | None = None, bmode_window_stride: int | None = None, ) -> DataArray ``` Process beamformed IQ into B-mode volumes. This method computes B-mode volumes from beamformed IQ data using a single sliding temporal window. Unlike power Doppler, no clutter filtering is applied; the mean magnitude (not squared magnitude) of the IQ data within each window is computed. Parameters: - ##### **`bmode_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for B-mode integration, in volumes. If not provided, uses the chunk size of the IQ data along the temporal dimension. - ##### **`bmode_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window, in volumes. If not provided, equals `bmode_window_width`. Returns: - `(windows, k, j, i) xarray.DataArray` – B-mode volumes with updated time coordinates, where `windows` is the number of sliding windows. Examples: ```pycon >>> import xarray as xr >>> ds = xr.open_zarr("iq_data.zarr") >>> iq = ds["iq"] >>> bmode = iq.fusi.iq.process_to_bmode(bmode_window_width=50) ``` #### process\_to\_power\_doppler ```python process_to_power_doppler( clutter_window_width: int | None = None, clutter_window_stride: int | None = None, filter_method: Literal[ "svd_indices", "svd_energy", "svd_cumulative_energy", "butterworth", ] = "svd_indices", clutter_mask: DataArray | None = None, low_cutoff: float | None = None, high_cutoff: float | None = None, butterworth_order: int = 4, doppler_window_width: int | None = None, doppler_window_stride: int | None = None, ) -> DataArray ``` Process beamformed IQ into power Doppler volumes. This method computes power Doppler volumes from beamformed IQ data using nested sliding windows. A first sliding window is used for clutter filtering. Inside each clutter-filtered window, power Doppler volumes are computed using a second sliding window. Parameters: - ##### **`clutter_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for clutter filtering, in volumes. If not provided, uses the chunk size of the IQ data along the temporal dimension. - ##### **`clutter_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window for clutter filtering, in volumes. If not provided, equals `clutter_window_width`. - ##### **`filter_method`** (`(svd_indices, svd_energy, svd_cumulative_energy, butterworth)`, default: `"svd_indices"` ) – Clutter filtering method to apply before power Doppler computation. - `"svd_indices"`: Static SVD filter using singular vector indices. - `"svd_energy"`: Adaptive SVD filter using singular vector energies. - `"svd_cumulative_energy"`: Adaptive SVD filter using cumulative energies. - `"butterworth"`: Butterworth frequency-domain filter. - ##### **`clutter_mask`** (`(k, j, i) xarray.DataArray`, default: `None` ) – Boolean mask to define clutter regions. Only used by SVD-based clutter filters to compute clutter vectors from masked voxels. If not provided, all voxels are used. The mask spatial dimensions and coordinates must match the IQ data. - ##### **`low_cutoff`** (`int or float`, default: `None` ) – Low cutoff for clutter filtering. Interpretation depends on `filter_method`. If not provided, uses method-specific defaults. - ##### **`high_cutoff`** (`int or float`, default: `None` ) – High cutoff for clutter filtering. Interpretation depends on `filter_method`. If not provided, uses method-specific defaults. - ##### **`butterworth_order`** (`int`, default: `4` ) – Order of Butterworth filter. Effective order is doubled due to forward-backward filtering. - ##### **`doppler_window_width`** (`int`, default: `None` ) – Width of the sliding temporal window for power Doppler integration, in volumes. If not provided, equals `clutter_window_width`. - ##### **`doppler_window_stride`** (`int`, default: `None` ) – Stride of the sliding temporal window for power Doppler integration, in volumes. If not provided, equals `doppler_window_width`. Returns: - `(clutter_windows * doppler_windows, k, j, i) xarray.DataArray` – Power Doppler volumes with updated time coordinates, where `clutter_windows` is the number of clutter filter sliding windows and `doppler_windows` is the number of power Doppler sliding windows per clutter window. Examples: ```pycon >>> import xarray as xr >>> ds = xr.open_zarr("iq_data.zarr") >>> iq = ds["iq"] >>> pwd = iq.fusi.iq.process_to_power_doppler( ... clutter_window_width=50, ... doppler_window_width=25, ... low_cutoff=40, ... ) ``` ### FUSIPlotAccessor Accessor for plotting fUSI data. This accessor provides convenient plotting methods for functional ultrasound imaging data, with specialized support for napari visualization. Parameters: - #### **`xarray_obj`** (`DataArray`) – The VoxelData array to wrap. Examples: ```pycon >>> import xarray as xr >>> data = xr.open_zarr("output.zarr")["iq"] >>> viewer, layer = data.fusi.plot.napari() ``` Methods: - **`carpet`** – Plot voxel intensities across time as a raster image. - **`composite`** – Plot a red/cyan composite of this volume against `other`. - **`contours`** – Plot mask contours as a grid of 2D slice panels. - **`draw_napari_labels`** – Open napari to interactively paint integer labels over fUSI data. - **`extract_labels_from_layer`** – Convert a napari Labels layer to an integer label map DataArray. - **`napari`** – Display data in napari viewer. - **`stat_map`** – Plot this statistical map, optionally over `bg_volume`. - **`volume`** – Plot 2D slices of a volume as a matplotlib subplot grid. #### carpet ```python carpet( mask: DataArray | None = None, detrend_order: int | None = None, standardize: bool = True, cmap: str | Colormap = "gray", vmin: float | None = None, vmax: float | None = None, decimation_threshold: int | None = 800, figsize: tuple[float, float] = (10, 5), title: str | None = None, fontsize: float | None = None, bg_color: str = "white", fg_color: str | None = None, ax: Axes | None = None, ) -> tuple[Figure | SubFigure, Axes] ``` Plot voxel intensities across time as a raster image. A carpet plot (also known as "grayplot" or "Power plot") displays voxel intensities as a 2D raster image with time on the x-axis and voxels on the y-axis. Each row represents one voxel's time series, typically standardized to z-scores. Parameters: - ##### **`mask`** (`DataArray`, default: `None` ) – Boolean mask with same spatial dimensions and coordinates as `data`. `True` values indicate voxels to include. If not provided, all non-zero voxels from the data are included. - ##### **`detrend_order`** (`int`, default: `None` ) – Polynomial order for detrending: - `0`: Remove mean (constant detrending). - `1`: Remove linear trend using least squares regression (default). - `2+`: Remove polynomial trend of specified order. If not provided, no detrending is applied. - ##### **`standardize`** (`bool`, default: `True` ) – Whether to standardize each voxel's time series to z-scores. - ##### **`cmap`** (`str`, default: `"gray"` ) – Matplotlib colormap name. - ##### **`vmin`** (`float`, default: `None` ) – Minimum value for colormap. If not provided, uses `mean - 2*std`. - ##### **`vmax`** (`float`, default: `None` ) – Maximum value for colormap. If not provided, uses `mean + 2*std`. - ##### **`decimation_threshold`** (`int or None`, default: `800` ) – If the number of timepoints exceeds this value, data is downsampled along the time axis to improve plotting performance. Set to `None` to disable downsampling. - ##### **`figsize`** (`tuple[float, float]`, default: `(10, 5)` ) – Figure size in inches `(width, height)`. - ##### **`title`** (`str`, default: `None` ) – Plot title. - ##### **`fontsize`** (`float`, default: `None` ) – Base font size for text elements. Title uses `fontsize` directly; axis labels and colorbar label use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - ##### **`bg_color`** (`str`, default: `"white"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - ##### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - ##### **`ax`** (`Axes`, default: `None` ) – Axes to plot on. If not provided, creates new figure and axes. Returns: - **`figure`** ( `Figure or SubFigure` ) – Figure object containing the carpet plot. - **`axes`** ( `Axes` ) – Axes object with the carpet plot. > [!NOTE] > > **Notes** > > Complex-valued data is converted to magnitude before processing. > > This function was inspired by Nilearn's `nilearn.plotting.plot_carpet`. **References** 1. Power, Jonathan D. “A Simple but Useful Way to Assess fMRI Scan Qualities.” NeuroImage, vol. 154, July 2017, pp. 150–58. DOI.org (Crossref), [https://doi.org/10.1016/j.neuroimage.2016.08.009](). Examples: ```pycon >>> import xarray as xr >>> data = xr.open_zarr("output.zarr")["iq"] >>> fig, ax = data.fusi.plot.carpet() ``` ```pycon >>> # With linear detrending. >>> fig, ax = data.fusi.plot.carpet(detrend_order=1) ``` ```pycon >>> # With mask. >>> mask = np.abs(data.isel(time=0)) > threshold >>> fig, ax = data.fusi.plot.carpet(mask=mask) ``` #### composite ```python composite( other: DataArray, resample: bool = True, resample_kwargs: dict[str, Any] | None = None, rtol: float = 1e-05, atol: float = 1e-08, normalize_strategy: Literal[ "per_volume", "per_slice", "shared" ] = "per_volume", slice_coords: list[Hashable] | None = None, slice_mode: str = "z", transpose: bool = False, alpha: float | NDArray[floating] | None = None, show_titles: bool = True, show_axis_labels: bool = True, show_axis_ticks: bool = True, show_axes: bool = True, fontsize: float | None = None, yincrease: bool = False, xincrease: bool = True, bg_color: str = "black", fg_color: str | None = None, figure: Figure | None = None, axes: NDArray[Any] | None = None, nrows: int | None = None, ncols: int | None = None, dpi: int | None = None, resample_interpolation: Literal[ "linear", "nearest", "bspline" ] = "linear", resample_fill_value: float | None = None, ) -> VolumePlotter ``` Plot a red/cyan composite of this volume against `other`. Self drives the red channel; `other` drives the cyan channel. See `confusius.plotting.plot_composite` for full details. Parameters: - ##### **`other`** (`DataArray`) – Second volume, plotted in cyan. When `resample=True`, resampled onto this DataArray's grid before blending. - ##### **`resample`** (`bool`, default: `True` ) – Whether to resample `other` onto this DataArray's grid using an identity transform before blending. When `False`, the two arrays must already share dims and shape, and their coordinates must match within `rtol`/`atol`; once validated, `other`'s coordinates are replaced with this DataArray's so the two volumes share an exact coordinate frame downstream. - ##### **`resample_kwargs`** (`dict`, default: `None` ) – Extra keyword arguments forwarded to `resample_like` when `resample=True`. Ignored when `resample=False`. - ##### **`rtol`** (`float`, default: `1e-5` ) – Relative tolerance used to validate that this DataArray and `other` share coordinates when `resample=False`. Widen to accept acquisitions on slightly offset grids known to be equivalent. Ignored when `resample=True`. - ##### **`atol`** (`float`, default: `1e-8` ) – Absolute tolerance used to validate that this DataArray and `other` share coordinates when `resample=False`. Ignored when `resample=True`. - ##### **`normalize_strategy`** (`(per_volume, per_slice, shared)`, default: `"per_volume"` ) – Intensity normalisation strategy. - `"per_volume"`: rescale each input to `[0, 1]` independently over its full volume. - `"per_slice"`: rescale each 2D slice independently. - `"shared"`: rescale both volumes together using a shared `[min, max]` range, preserving the absolute-intensity relationship between the two inputs. - ##### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Coordinate values along `slice_mode` at which to extract slices. Slices are selected by nearest-neighbour lookup. If not provided, all coordinate values from this DataArray are used. - ##### **`slice_mode`** (`str`, default: `"z"` ) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension to slice. Native voxel dimensions (`"k"`, `"j"`, `"i"`) are not valid slice modes. After slicing, each panel must be 2D. - ##### **`transpose`** (`bool`, default: `False` ) – Whether to swap the row/column display dims of each slice panel. - ##### **`alpha`** (`float or ndarray`, default: `None` ) – Opacity of the composite image, either a single value or a per-voxel array matching the shape of the displayed slices. If not provided, the image is fully opaque. - ##### **`show_titles`** (`bool`, default: `True` ) – Whether to display subplot titles showing the slice coordinate. - ##### **`show_axis_labels`** (`bool`, default: `True` ) – Whether to display axis labels (with units when available). - ##### **`show_axis_ticks`** (`bool`, default: `True` ) – Whether to display axis tick labels. - ##### **`show_axes`** (`bool`, default: `True` ) – Whether to show axis decorations. When `False`, overrides `show_axis_labels` and `show_axis_ticks`. - ##### **`fontsize`** (`float`, default: `None` ) – Base font size for all text elements. Subplot titles use `fontsize` directly; axis labels use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - ##### **`yincrease`** (`bool`, default: `False` ) – Whether the y-axis increases upward (`True`) or downward (`False`). - ##### **`xincrease`** (`bool`, default: `True` ) – Whether the x-axis increases to the right (`True`) or left (`False`). - ##### **`bg_color`** (`str`, default: `"black"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - ##### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - ##### **`figure`** (`Figure`, default: `None` ) – Existing figure to draw into. If not provided, a new figure is created. - ##### **`axes`** (`ndarray`, default: `None` ) – Existing 2D array of `matplotlib.axes.Axes` to draw into. If not provided, new axes are created inside `figure`. - ##### **`nrows`** (`int`, default: `None` ) – Number of rows in the subplot grid. If not provided, computed automatically. - ##### **`ncols`** (`int`, default: `None` ) – Number of columns in the subplot grid. If not provided, computed automatically. - ##### **`dpi`** (`int`, default: `None` ) – Figure resolution in dots per inch. Ignored when `figure` is provided. - ##### **`resample_interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation method used when resampling oblique (non-axis-aligned) voxel-to-world data onto an axis-aligned world grid for display. Distinct from `resample_kwargs`, which controls resampling `other` onto this DataArray's grid for compositing. - ##### **`resample_fill_value`** (`float`, default: `None` ) – Value assigned to voxels outside this DataArray's/`other`'s field of view after display resampling. If not provided, defaults to each array's own minimum value. Returns: - `VolumePlotter` – Object managing the figure, axes, and coordinate mapping for overlays. Examples: ```pycon >>> import xarray as xr >>> import confusius # Register accessor. >>> fixed = xr.open_zarr("fixed.zarr")["power_doppler"] >>> moving = xr.open_zarr("moving.zarr")["power_doppler"] >>> plotter = fixed.fusi.plot.composite(moving) ``` ```pycon >>> # Same-grid comparison without resampling, with joint scaling. >>> plotter = fixed.fusi.plot.composite( ... registered_moving, resample=False, normalize_strategy="shared" ... ) ``` #### contours ```python contours( colors: dict[int | str, str] | str | None = None, linewidths: float = 1.5, linestyles: str = "solid", slice_mode: str = "z", slice_coords: list[Hashable] | None = None, transpose: bool = False, fontsize: float | None = None, yincrease: bool = False, xincrease: bool = True, bg_color: str = "black", fg_color: str | None = None, figure: Figure | None = None, axes: NDArray[Any] | None = None, **kwargs, ) -> VolumePlotter ``` Plot mask contours as a grid of 2D slice panels. Displays contour lines for each labeled region across a grid of subplots. See `confusius.plotting.plot_contours` for full details. Parameters: - ##### **`colors`** (`dict[int | str, str] or str`, default: `None` ) – Color specification for contour lines. A `dict` maps each label (integer index or region acronym string) to a color; a `str` applies one color to all regions. If not provided, colors are derived from `attrs["cmap"]` and `attrs["norm"]` when present, otherwise from the `tab10`/`tab20` colormap. - ##### **`linewidths`** (`float`, default: `1.5` ) – Width of contour lines in points. - ##### **`linestyles`** (`str`, default: `"solid"` ) – Line style for contour lines (e.g. `"solid"`, `"dashed"`). - ##### **`slice_mode`** (`str`, default: `"z"` ) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension to slice. Native voxel dimensions (`"k"`, `"j"`, `"i"`) are not valid slice modes. After slicing, each panel must be 2D. - ##### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Coordinate values along `slice_mode` at which to extract slices. Slices are selected by nearest-neighbour lookup. If not provided, all coordinate values along `slice_mode` are used. - ##### **`transpose`** (`bool`, default: `False` ) – Whether to swap the row/column display dims of each slice panel. - ##### **`fontsize`** (`float`, default: `None` ) – Base font size for text elements. Subplot titles use `fontsize` directly; axis labels use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - ##### **`yincrease`** (`bool`, default: `False` ) – Whether the y-axis increases upward (`True`) or downward (`False`). - ##### **`xincrease`** (`bool`, default: `True` ) – Whether the x-axis increases to the right (`True`) or left (`False`). - ##### **`bg_color`** (`str`, default: `"black"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - ##### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - ##### **`figure`** (`Figure`, default: `None` ) – Existing figure to draw into. If not provided, a new figure is created. - ##### **`axes`** (`ndarray`, default: `None` ) – Existing 2D array of `matplotlib.axes.Axes` to draw into. If not provided, new axes are created inside `figure`. - ##### **`**kwargs`** – Additional keyword arguments passed to `matplotlib.axes.Axes.plot`. Returns: - `VolumePlotter` – Object managing the figure, axes, and coordinate mapping for overlays. Examples: ```pycon >>> import xarray as xr >>> import confusius # Register accessor. >>> mask = xr.open_zarr("output.zarr")["roi_mask"] >>> plotter = mask.fusi.plot.contours(colors={1: "red", 2: "blue"}) ``` ```pycon >>> # Overlay contours on an existing volume plot. >>> volume = xr.open_zarr("output.zarr")["power_doppler"] >>> plotter = volume.fusi.plot.volume(slice_mode="z") >>> plotter.add_contours(mask, colors="yellow") ``` #### draw\_napari\_labels ```python draw_napari_labels( labels_layer_name: str = "labels", viewer: Viewer | None = None, **plot_kwargs, ) -> tuple[Viewer, Labels] ``` Open napari to interactively paint integer labels over fUSI data. Displays the data as an image layer and adds an empty Labels layer on top. The user paints integer labels directly on the image using napari's brush tool. After painting, pass the returned Labels layer to `extract_labels_from_layer` to obtain an integer label map in the same spatial coordinates as the data. Oblique or sheared input requires interpolation for display and nearest-neighbor interpolation back to the original grid; boundaries and thin regions can change. Prefer an existing axis-aligned scanner frame when available. Parameters: - ##### **`labels_layer_name`** (`str`, default: `"labels"` ) – Name assigned to the Labels layer added to the viewer. - ##### **`viewer`** (`Viewer`, default: `None` ) – Existing napari viewer to add layers to. If not provided, a new viewer is created. - ##### **`**plot_kwargs`** – Additional keyword arguments forwarded to `plot_napari` for the image layer (e.g. `colormap`, `contrast_limits`). Returns: - **`viewer`** ( `Viewer` ) – The napari viewer instance with the image and Labels layers. - **`labels_layer`** ( `Labels` ) – The empty Labels layer initialised to zeros. After painting labels in the viewer, pass it to `extract_labels_from_layer` to convert the paintings to an integer label map. Examples: ```pycon >>> import xarray as xr >>> import confusius # Register accessor. >>> pwd = xr.open_zarr("output.zarr")["power_doppler"].compute() >>> # Display time-averaged image with an interactive Labels layer. >>> viewer, labels_layer = pwd.mean("time").fusi.plot.draw_napari_labels() >>> # … paint labels in the viewer … >>> # Convert painted labels to an integer label map. >>> label_map = pwd.mean("time").fusi.plot.extract_labels_from_layer(labels_layer) ``` #### extract\_labels\_from\_layer ```python extract_labels_from_layer( labels_layer: Labels, ) -> DataArray ``` Convert a napari Labels layer to an integer label map DataArray. Extracts painted labels onto the data's original voxel grid, using nearest-neighbor resampling when the display grid differs. Label IDs are preserved, but boundaries and thin regions can change. Drawing on an existing axis-aligned grid avoids this interpolation round trip. Parameters: - ##### **`labels_layer`** (`Labels`) – A Labels layer populated by the user (e.g. via `draw_napari_labels`). Integer values identify distinct regions; zero is the background. Returns: - `DataArray` – Integer DataArray with the same spatial dimensions and coordinates as the data. Zero values indicate background (unlabelled) voxels. Examples: ```pycon >>> import xarray as xr >>> import confusius # Register accessor. >>> pwd = xr.open_zarr("output.zarr")["power_doppler"].compute() >>> viewer, labels_layer = pwd.mean("time").fusi.plot.draw_napari_labels() >>> # … paint labels in the viewer … >>> label_map = pwd.mean("time").fusi.plot.extract_labels_from_layer(labels_layer) >>> # Use the label map for region-based analysis. >>> from confusius.extract import extract_with_labels >>> signals = extract_with_labels(pwd, label_map) ``` #### napari ```python napari( show_colorbar: bool = True, show_scale_bar: bool = True, dim_order: tuple[str, ...] | None = None, viewer: Viewer | None = None, layer_type: Literal["image", "labels", "auto"] = "auto", resample_interpolation: Literal[ "linear", "nearest", "bspline" ] | None = None, resample_fill_value: float | None = None, **layer_kwargs, ) -> tuple[Viewer, Image | Labels] ``` Display data in napari viewer. Parameters: - ##### **`show_colorbar`** (`bool`, default: `True` ) – Whether to show the colorbar. Only applies to image layers. - ##### **`show_scale_bar`** (`bool`, default: `True` ) – Whether to show the scale bar. - ##### **`dim_order`** (`tuple[str, ...]`, default: `None` ) – Dimension ordering for the spatial axes (last three dimensions). If not provided, singleton spatial dimensions (e.g. the elevation axis of a single-slice acquisition) are placed first so the canvas always shows the two axes that actually vary; otherwise the dimensions' native ordering in `data` is used. - ##### **`viewer`** (`Viewer`, default: `None` ) – Existing napari viewer to add the layer to. If not provided, a new viewer is created. - ##### **`layer_type`** (`(image, labels, auto)`, default: `"image"` ) – Type of layer to create. Use "image" for fUSI data and "labels" for ROI masks, segmentations, or other label data. "auto" picks "labels" for integer or boolean data and "image" otherwise. - ##### **`resample_interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation method used when resampling oblique (non-axis-aligned) voxel-to-world data onto an axis-aligned world grid for display. If not provided, defaults to `"nearest"` for labels layers (including those inferred with `layer_type="auto"`) and `"linear"` for image layers. - ##### **`resample_fill_value`** (`float`, default: `None` ) – Value assigned to voxels outside this DataArray's field of view after resampling oblique data. If not provided, defaults to its own minimum value. - ##### **`**layer_kwargs`** – Additional keyword arguments passed to the layer creation method. For image layers, if `data.attrs` contains `"cmap"` and `"colormap"` is not in `layer_kwargs`, the attribute is used as the colormap. Returns: - **`viewer`** ( `Viewer` ) – The napari viewer instance with the layer added. - **`layer`** ( `Image or Labels` ) – The layer added to the viewer. > [!NOTE] > > **Notes** > > If all spatial dimensions have coordinates, their spacing is used as the scale parameter for napari to ensure correct world scaling. If any spatial dimension is missing coordinates, no scaling is applied. The spacing is computed as the median difference between consecutive coordinate values. > > For unitary voxel dimensions (e.g., a single-slice elevation axis in 2D+t data), the spacing cannot be inferred from consecutive coordinate differences. In that case, `.fusi.spacing` derives it from the voxel-to-world affine column norm instead. For unitary non-voxel dimensions with no affine to fall back on, unit spacing is assumed and a warning is emitted. Examples: ```pycon >>> import xarray as xr >>> import confusius # Register accessor. >>> data = xr.open_zarr("output.zarr")["iq"] >>> viewer, layer = data.fusi.plot.napari() ``` ```pycon >>> # Custom contrast limits >>> viewer, layer = data.fusi.plot.napari(contrast_limits=(0, 100)) ``` ```pycon >>> # Different dimension ordering (e.g., depth, elevation, lateral) >>> viewer, layer = data.fusi.plot.napari(dim_order=("y", "z", "x")) ``` ```pycon >>> # Add a second dataset as a new layer in an existing viewer >>> viewer, layer = data1.fusi.plot.napari() >>> viewer, layer = data2.fusi.plot.napari(viewer=viewer) ``` ```pycon >>> # Display ROI labels (e.g., segmentation mask) >>> roi_mask = xr.open_zarr("output.zarr")["roi_mask"] >>> viewer, layer = roi_mask.fusi.plot.napari(layer_type="labels") ``` ```pycon >>> # Overlay labels on existing image >>> viewer, layer = data.fusi.plot.napari() >>> viewer, layer = roi_mask.fusi.plot.napari(viewer=viewer, layer_type="labels") ``` #### stat\_map ```python stat_map( bg_volume: DataArray | None = None, slice_coords: list[Hashable] | None = None, slice_mode: str = "z", transpose: bool = False, bg_kwargs: dict[str, Any] | None = None, cmap: str | Colormap | None = None, norm: Normalize | None = None, vmin: float | None = None, vmax: float | None = None, auto_range: bool = True, alpha: float | DataArray | None = None, threshold: float | None = None, threshold_mode: Literal["lower", "upper"] = "lower", show_colorbar: bool = True, cbar_label: str | None = None, cbar_kwargs: dict[str, Any] | None = None, show_titles: bool = True, show_axis_labels: bool = True, show_axis_ticks: bool = True, show_axes: bool = True, fontsize: float | None = None, yincrease: bool = False, xincrease: bool = True, bg_color: str = "black", fg_color: str | None = None, figure: Figure | None = None, axes: NDArray[Any] | Axes | None = None, nrows: int | None = None, ncols: int | None = None, dpi: int | None = None, resample_interpolation: Literal[ "linear", "nearest", "bspline" ] = "linear", resample_fill_value: float | None = None, ) -> VolumePlotter ``` Plot this statistical map, optionally over `bg_volume`. Self is the statistical map. See `confusius.plotting.plot_stat_map` for full details. Parameters: - ##### **`bg_volume`** (`DataArray`, default: `None` ) – Background anatomical volume, plotted underneath this DataArray. When `alpha` is not provided, this DataArray fully covers `bg_volume` wherever it has a value; `bg_volume` only shows through where this DataArray is masked out by `threshold`. Lower `alpha` to blend the two layers instead. Must share `slice_mode` and, after squeezing, the same display dimensions as this DataArray. If not provided, this DataArray is plotted on its own. - ##### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Coordinate values along `slice_mode` at which to extract slices. Slices are selected by nearest-neighbour lookup. If not provided, all coordinate values from `bg_volume` (or this DataArray when `bg_volume` is not provided) along `slice_mode` are used. - ##### **`slice_mode`** (`str`, default: `"z"` ) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension to slice. Native voxel dimensions (`"k"`, `"j"`, `"i"`) are not valid slice modes. After slicing, each panel must be 2D. - ##### **`transpose`** (`bool`, default: `False` ) – Whether to swap the row/column display dims of each slice panel. - ##### **`bg_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments forwarded to `plot_volume` for the background layer (e.g. `cmap`, `vmin`, `vmax`, `norm`, `alpha`, `roi_labels`). Ignored when `bg_volume` is not provided. Layout and text styling (`slice_coords`, `slice_mode`, `show_titles`, `fontsize`, etc.) are controlled by this method's own parameters instead, so that both layers share consistent styling. - ##### **`cmap`** (`str or Colormap`, default: `None` ) – Colormap for this DataArray. If not provided, the default depends on `auto_range` and the sign of this DataArray (see below); an explicit `cmap` is always used as-is regardless of `auto_range`. - ##### **`norm`** (`Normalize`, default: `None` ) – Normalization instance (e.g. `TwoSlopeNorm`, `BoundaryNorm`, `LogNorm`) for cases `vmin`/`vmax`/`auto_range` can't express. When provided, `vmin`, `vmax`, and `auto_range`'s range computation are bypassed entirely; `cmap` still follows the usual rules above. - ##### **`vmin`** (`float`, default: `None` ) – Lower bound of the colormap. If not provided, defaults to the minimum value of this DataArray, computed over the full array rather than just the displayed slices. Ignored when `norm` is provided, when `auto_range=True` and this DataArray has only non-negative values, or when `auto_range=True`, this DataArray spans both signs and `vmax` is given on its own (see `auto_range`). - ##### **`vmax`** (`float`, default: `None` ) – Upper bound of the colormap. If not provided, defaults to the maximum value of this DataArray, computed over the full array rather than just the displayed slices. Ignored when `norm` is provided, when `auto_range=True` and this DataArray has only non-positive values, or when `auto_range=True`, this DataArray spans both signs and `vmin` is given on its own (see `auto_range`). - ##### **`auto_range`** (`bool`, default: `True` ) – Whether to pick the colormap range and default colormap automatically based on the sign of this DataArray: - Both positive and negative values: diverging, symmetric `[-m, m]` range where `m = max(|vmin|, |vmax|)` over the bounds actually provided, falling back to the largest magnitude in this DataArray when neither is given, with `cmap` defaulting to `"coolwarm"` — the right choice for diverging statistics where the sign is meaningful (e.g. t-statistics, correlation coefficients, PCA/ICA component maps). - Only non-negative values: sequential `[0, vmax]` range, with `cmap` defaulting to `"viridis"` — the right choice for non-diverging statistics where only magnitude matters (e.g. R², F-statistics). - Only non-positive values: sequential `[vmin, 0]` range, with `cmap` defaulting to `"viridis_r"` (reversed, so that values near zero map to the same end of the colormap in both the non-negative and non-positive cases). Set to `False` to use the resolved `vmin`/`vmax` directly with no zero-anchoring (`cmap` then defaults to `"coolwarm"` regardless of sign). - ##### **`alpha`** (`float or DataArray`, default: `None` ) – Opacity of this DataArray's overlay: a single scalar value, or a 3D DataArray sharing this DataArray's dims, shape, and coordinates (for independent per-slice, per-voxel opacity, e.g. to fade out low-magnitude voxels instead of masking them out with `threshold`). A per-voxel opacity must be a DataArray, not a bare array, so it can be validated and aligned against this DataArray; note it is validated against self, not `bg_volume`. If not provided, the colormap's own alpha channel is respected. - ##### **`threshold`** (`float`, default: `None` ) – Threshold applied to the absolute value of this DataArray. See `threshold_mode` for the masking direction. If not provided, no thresholding is applied. - ##### **`threshold_mode`** (`(lower, upper)`, default: `"lower"` ) – Controls how `threshold` is applied: - `"lower"`: set pixels below `threshold` (in absolute value) to NaN. - `"upper"`: set pixels above `threshold` (in absolute value) to NaN. - ##### **`show_colorbar`** (`bool`, default: `True` ) – Whether to add a shared colorbar to the figure. - ##### **`cbar_label`** (`str`, default: `None` ) – Label for the colorbar. - ##### **`cbar_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments forwarded to `matplotlib.figure.Figure.colorbar` (e.g. `shrink`, `fraction`, `pad`, `aspect`). Useful to shrink the colorbar when it spans a multi-panel grid, since the defaults are sized for a single axes. - ##### **`show_titles`** (`bool`, default: `True` ) – Whether to display subplot titles showing the slice coordinate. - ##### **`show_axis_labels`** (`bool`, default: `True` ) – Whether to display axis labels (with units when available). - ##### **`show_axis_ticks`** (`bool`, default: `True` ) – Whether to display axis tick labels. - ##### **`show_axes`** (`bool`, default: `True` ) – Whether to show all axis decorations (spines, ticks, labels). When `False`, overrides `show_axis_labels` and `show_axis_ticks`. - ##### **`fontsize`** (`float`, default: `None` ) – Base font size for all text elements. Subplot titles use `fontsize` directly; axis labels and the colorbar label use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - ##### **`yincrease`** (`bool`, default: `False` ) – Whether the y-axis increases upward (`True`) or downward (`False`). - ##### **`xincrease`** (`bool`, default: `True` ) – Whether the x-axis increases to the right (`True`) or left (`False`). - ##### **`bg_color`** (`str`, default: `"black"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - ##### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - ##### **`figure`** (`Figure`, default: `None` ) – Existing figure to draw into. If not provided, a new figure is created. - ##### **`axes`** (`ndarray or Axes`, default: `None` ) – Existing axes to draw into: either a single `matplotlib.axes.Axes` or a 2D array of them. Must contain exactly as many elements as there are slices. A single `Axes` is wrapped automatically and limits the plot to one slice. If not provided, new axes are created inside `figure`. - ##### **`nrows`** (`int`, default: `None` ) – Number of rows in the subplot grid. If not provided, computed automatically. - ##### **`ncols`** (`int`, default: `None` ) – Number of columns in the subplot grid. If not provided, computed automatically. - ##### **`dpi`** (`int`, default: `None` ) – Figure resolution in dots per inch. Ignored when `figure` is provided. - ##### **`resample_interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation method used when resampling oblique (non-axis-aligned) voxel-to-world data/`bg_volume` onto an axis-aligned world grid for display. Applied to both, since they share one `VolumePlotter`. - ##### **`resample_fill_value`** (`float`, default: `None` ) – Value assigned to voxels outside this DataArray's/`bg_volume`'s field of view after display resampling. If not provided, defaults to each array's own minimum value. Returns: - `VolumePlotter` – Object managing the figure, axes, and coordinate mapping for overlays. Examples: ```pycon >>> import xarray as xr >>> import confusius # Register accessor. >>> anatomical = xr.open_zarr("output.zarr")["power_doppler"] >>> t_map = xr.open_zarr("output.zarr")["t_stat"] >>> plotter = t_map.fusi.plot.stat_map(bg_volume=anatomical, slice_mode="z") ``` ```pycon >>> # Blend the overlay with the background instead of fully covering it. >>> plotter = t_map.fusi.plot.stat_map(bg_volume=anatomical, alpha=0.6) ``` ```pycon >>> # Non-diverging statistic (e.g. R²): sequential range and colormap >>> # picked automatically since r2_map has only non-negative values. >>> r2_map = xr.open_zarr("output.zarr")["r2"] >>> plotter = r2_map.fusi.plot.stat_map(anatomical) ``` #### volume ```python volume( slice_coords: list[Hashable] | None = None, slice_mode: str | None = None, transpose: bool = False, nrows: int | None = None, ncols: int | None = None, threshold: float | None = None, threshold_mode: Literal["lower", "upper"] = "lower", cmap: str | Colormap | None = None, norm: Normalize | None = None, vmin: float | None = None, vmax: float | None = None, alpha: float | DataArray | None = None, show_colorbar: bool = True, cbar_label: str | None = None, cbar_kwargs: dict[str, Any] | None = None, show_titles: bool = True, show_axis_labels: bool = True, show_axis_ticks: bool = True, show_axes: bool = True, fontsize: float | None = None, yincrease: bool = False, xincrease: bool = True, bg_color: str = "black", fg_color: str | None = None, figure: Figure | None = None, axes: NDArray[Any] | None = None, dpi: int | None = None, resample_interpolation: Literal[ "linear", "nearest", "bspline" ] = "linear", resample_fill_value: float | None = None, ) -> VolumePlotter ``` Plot 2D slices of a volume as a matplotlib subplot grid. See `confusius.plotting.plot_volume` for full details. Parameters: - ##### **`slice_coords`** (`list[Hashable]`, default: `None` ) – Coordinate values along `slice_mode` at which to extract slices. Slices are selected by nearest-neighbour lookup. If not provided, all coordinate values along `slice_mode` are used. - ##### **`slice_mode`** (`str`, default: `None` ) – World dimension (`"z"`, `"y"`, `"x"`) or extra non-voxel dimension to slice. Native voxel dimensions (`"k"`, `"j"`, `"i"`) are not valid slice modes. If not provided, planar data is sliced along its singleton world dimension and full 3D data is sliced along `"z"`. After slicing, each panel must be 2D. - ##### **`transpose`** (`bool`, default: `False` ) – Whether to swap the row/column display dims of each slice panel. - ##### **`nrows`** (`int`, default: `None` ) – Number of rows in the subplot grid. If not provided, computed automatically together with `ncols` to produce a near-square layout. - ##### **`ncols`** (`int`, default: `None` ) – Number of columns in the subplot grid. If not provided, computed automatically together with `nrows`. - ##### **`threshold`** (`float`, default: `None` ) – Threshold applied to `|data|`. See `threshold_mode` for the masking direction. If not provided, no thresholding is applied. - ##### **`threshold_mode`** (`(lower, upper)`, default: `"lower"` ) – Controls how `threshold` is applied: - `"lower"`: set pixels where `|data| < threshold` to NaN. - `"upper"`: set pixels where `|data| > threshold` to NaN. - ##### **`cmap`** (`str or Colormap`, default: `None` ) – Colormap. When not provided, falls back to `data.attrs["cmap"]` if present, otherwise `"gray"`. - ##### **`norm`** (`Normalize`, default: `None` ) – Normalization instance (e.g. `BoundaryNorm` for integer label maps). When not provided, falls back to `data.attrs["norm"]` if present. When a norm is active, `vmin` and `vmax` are ignored. - ##### **`vmin`** (`float`, default: `None` ) – Lower bound of the colormap. Defaults to the 2nd percentile. Ignored when `norm` is provided explicitly (that is, not just inherited from data attributes). - ##### **`vmax`** (`float`, default: `None` ) – Upper bound of the colormap. Defaults to the 98th percentile. Ignored when `norm` is provided explicitly (that is, not just inherited from data attributes). - ##### **`alpha`** (`float or DataArray`, default: `None` ) – Opacity of the image: a single scalar value, or a 3D DataArray sharing this DataArray's dims, shape, and coordinates (for independent per-slice, per-voxel opacity). A per-voxel opacity must be a DataArray, not a bare array, so it can be validated and aligned against this DataArray. If not provided, the colormap's own alpha channel is respected. - ##### **`show_colorbar`** (`bool`, default: `True` ) – Whether to add a shared colorbar to the figure. - ##### **`cbar_label`** (`str`, default: `None` ) – Label for the colorbar. - ##### **`cbar_kwargs`** (`dict`, default: `None` ) – Additional keyword arguments forwarded to `matplotlib.figure.Figure.colorbar` (e.g. `shrink`, `fraction`, `pad`, `aspect`). Useful to shrink the colorbar when it spans a multi-panel grid, since the defaults are sized for a single axes. - ##### **`show_titles`** (`bool`, default: `True` ) – Whether to display subplot titles showing the slice coordinate. - ##### **`show_axis_labels`** (`bool`, default: `True` ) – Whether to display axis labels (with units when available). - ##### **`show_axis_ticks`** (`bool`, default: `True` ) – Whether to display axis tick labels. - ##### **`show_axes`** (`bool`, default: `True` ) – Whether to show all axis decorations (spines, ticks, labels). When `False`, overrides `show_axis_labels` and `show_axis_ticks`. - ##### **`fontsize`** (`float`, default: `None` ) – Base font size for all text elements. Subplot titles use `fontsize` directly; axis labels and colorbar label use `0.9 * fontsize`; tick labels use `0.85 * fontsize`. If not provided, uses the active Matplotlib defaults. - ##### **`yincrease`** (`bool`, default: `False` ) – Whether the y-axis increases upward (`True`) or downward (`False`). - ##### **`xincrease`** (`bool`, default: `True` ) – Whether the x-axis increases to the right (`True`) or left (`False`). - ##### **`bg_color`** (`str`, default: `"black"` ) – Background color for the figure and axes. Any matplotlib-compatible color string (e.g. `"black"`, `"white"`, `"#1a1a2e"`). - ##### **`fg_color`** (`str`, default: `None` ) – Color for text, labels, ticks, and spines. If not provided, derived automatically from `bg_color` using the WCAG relative luminance formula (white on dark backgrounds, black on light ones). - ##### **`figure`** (`Figure`, default: `None` ) – Existing figure to draw into. If not provided, a new figure is created. - ##### **`axes`** (`ndarray`, default: `None` ) – Existing 2D array of `matplotlib.axes.Axes` to draw into. If not provided, new axes are created inside `figure`. - ##### **`dpi`** (`int`, default: `None` ) – Figure resolution in dots per inch. Ignored when `figure` is provided. - ##### **`resample_interpolation`** (`(linear, nearest, bspline)`, default: `"linear"` ) – Interpolation method used when resampling oblique (non-axis-aligned) voxel-to-world data onto an axis-aligned world grid for display. - ##### **`resample_fill_value`** (`float`, default: `None` ) – Value assigned to voxels outside this DataArray's field of view after resampling oblique data. If not provided, defaults to its own minimum value. Returns: - `VolumePlotter` – Object managing the figure, axes, and coordinate mapping for overlays. Raises: - `ValueError` – If `slice_mode` is not a dimension of the data. - `ValueError` – If the data is not 3D after squeezing unitary dimensions. - `ValueError` – If `axes` is provided but does not contain enough elements for all slices. Examples: ```pycon >>> import xarray as xr >>> import confusius # Register accessor. >>> data = xr.open_zarr("output.zarr")["power_doppler"] >>> plotter = data.fusi.plot.volume(slice_mode="z") ``` ```pycon >>> # dB-scale data with upper threshold to suppress far-field noise. >>> plotter = data.fusi.scale.db().fusi.plot.volume( ... slice_mode="z", ... threshold=-60, ... threshold_mode="upper", ... cmap="hot", ... bg_color="black", ... ) ``` ### FUSIRegistrationAccessor Accessor for registration operations on fUSI data. Parameters: - #### **`xarray_obj`** (`DataArray`) – The VoxelData array to wrap. Examples: ```pycon >>> import xarray as xr >>> data = xr.open_zarr("output.zarr")["power_doppler"] >>> registered = data.fusi.register.volumewise(reference_time=0) ``` Methods: - **`to_volume`** – Register this volume to a fixed reference volume. - **`volumewise`** – Register all volumes to a reference time point or a reference volume. #### to\_volume ```python to_volume( fixed: DataArray, *, fixed_mask: DataArray | None = None, moving_mask: DataArray | None = None, transform: Literal[ "translation", "rigid", "affine", "bspline" ] = "rigid", metric: Literal[ "correlation", "mattes_mi" ] = "correlation", number_of_histogram_bins: int = 50, metric_sampling_percentage: float | None = None, metric_sampling_seed: int | None = None, learning_rate: float | Literal["auto"] = "auto", number_of_iterations: int = 100, convergence_minimum_value: float = 1e-06, convergence_window_size: int = 10, initialization: Literal[ "center_geometry", "center_moments" ] | NDArray[floating] | None = "center_geometry", optimizer_weights: list[float] | None = None, mesh_size: tuple[int, int, int] = (10, 10, 10), use_multi_resolution: bool = False, shrink_factors: Sequence[int] = (6, 2, 1), smoothing_sigmas: Sequence[int] = (6, 2, 1), resample: bool = False, resample_interpolation: Literal[ "linear", "bspline" ] = "linear", fill_value: float | None = None, sitk_threads: int = -1, show_progress: bool = False, plot_metric: bool = True, plot_composite: bool = True, progress_plotter: Callable[..., RegistrationProgress] | None = None, abort_event: Event | None = None, ) -> tuple[ DataArray, NDArray[floating] | DataArray | None, RegistrationDiagnostics, ] ``` Register this volume to a fixed reference volume. Parameters: - ##### **`fixed`** (`DataArray`) – Reference volume to register to. - ##### **`fixed_mask`** (`DataArray`, default: `None` ) – Boolean mask for the fixed volume. - ##### **`moving_mask`** (`DataArray`, default: `None` ) – Boolean mask for this moving volume. - ##### **`transform`** (`(translation, rigid, affine, bspline)`, default: `"translation"` ) – Type of transform to use for registration. - ##### **`metric`** (`(correlation, mattes_mi)`, default: `"correlation"` ) – Similarity metric for registration. - ##### **`number_of_histogram_bins`** (`int`, default: `50` ) – Number of histogram bins (only used when `metric="mattes_mi"`). - ##### **`metric_sampling_percentage`** (`float`, default: `None` ) – Percentage of voxels randomly sampled when computing the metric, in `(0, 1]`. If not provided, all voxels are used without random sampling. - ##### **`metric_sampling_seed`** (`int`, default: `None` ) – Seed for random metric sampling. Only used when `metric_sampling_percentage < 1`. If not provided, SimpleITK's default `sitkWallClock` seed is used. - ##### **`learning_rate`** (`float or auto`, default: `"auto"` ) – Optimizer step size in normalised units (after `SetOptimizerScalesFromPhysicalShift`). `"auto"` re-estimates the rate at every iteration. A float uses that value directly; if registration diverges or fails to converge, reduce it. - ##### **`number_of_iterations`** (`int`, default: `100` ) – Maximum number of optimizer iterations. - ##### **`convergence_minimum_value`** (`float`, default: `1e-6` ) – Convergence threshold for early stopping. - ##### **`convergence_window_size`** (`int`, default: `10` ) – Window size for convergence check. - ##### **`initialization`** (`(center_geometry, center_moments)`, default: `"center_geometry"` ) – Initial transform mapping `fixed` to `moving` coordinates, applied before optimization: - `"center_geometry"`: aligns image centers. - `"center_moments"`: aligns centers of mass. - `(N+1, N+1)` homogeneous affine matrix: uses a precomputed affine transform. - `None`: uses the identity transform. For `transform="bspline"`, centering modes are ignored but affine initialization is supported. - ##### **`optimizer_weights`** (`list of float`, default: `None` ) – Per-parameter weights applied on top of auto-estimated scales via `SetOptimizerWeights()`. If not provided, no additional weighting is applied. The weight for each parameter is multiplied into the effective step size: `0` freezes a parameter, values in `(0, 1)` slow it down, `1` leaves it unchanged. For the 3D Euler transform the order is `[angleX, angleY, angleZ, tx, ty, tz]`; to disable rotations around x and y use `[0, 0, 1, 1, 1, 1]`. - ##### **`mesh_size`** (`tuple of int`, default: `(10, 10, 10)` ) – BSpline mesh size. Only used when `transform="bspline"`. - ##### **`use_multi_resolution`** (`bool`, default: `False` ) – Whether to use a multi-resolution pyramid during registration. - ##### **`shrink_factors`** (`sequence of int`, default: `(6, 2, 1)` ) – Downsampling factor at each pyramid level, from coarsest to finest. Only used when `use_multi_resolution=True`. - ##### **`smoothing_sigmas`** (`sequence of int`, default: `(6, 2, 1)` ) – Gaussian smoothing sigma (in voxels) at each pyramid level, from coarsest to finest. Only used when `use_multi_resolution=True`. - ##### **`resample`** (`bool`, default: `False` ) – Whether to resample the moving volume into the fixed volume's space. When `False` (the default), only the transform is estimated and the moving volume is returned unchanged. - ##### **`resample_interpolation`** (`(linear, bspline)`, default: `"linear"` ) – Interpolation method used for the final resample step. - ##### **`fill_value`** (`float`, default: `None` ) – Fill value for voxels outside the moving image's field of view after resampling. If not provided, defaults to the minimum of the moving image. See `register_volume`. - ##### **`sitk_threads`** (`int`, default: `-1` ) – Number of threads SimpleITK may use internally. - ##### **`show_progress`** (`bool`, default: `False` ) – Whether to display a live progress plot during registration. - ##### **`plot_metric`** (`bool`, default: `True` ) – Whether to include the optimizer metric curve in the progress plot. Ignored when `show_progress=False`. - ##### **`plot_composite`** (`bool`, default: `True` ) – Whether to include a fixed/moving composite overlay in the progress plot. Ignored when `show_progress=False`. - ##### **`progress_plotter`** (`callable`, default: `None` ) – Custom progress reporter factory. If not provided, the default `MatplotlibRegistrationProgressPlotter` is used. See `register_volume`. - ##### **`abort_event`** (`Event`, default: `None` ) – Cooperative cancellation flag. Returns: - **`registered`** ( `DataArray` ) – Registered volume. When `resample=True`, resampled onto the fixed grid; otherwise the original moving volume with registration metadata added. - **`affine`** ( `(N+1, N+1) numpy.ndarray or xarray.DataArray or None` ) – Estimated registration transform. For linear transforms, a homogeneous affine matrix. For `transform="bspline"`, a DataArray encoding the B-spline control-point grid. - **`diagnostics`** ( `RegistrationDiagnostics` ) – Per-iteration metric values and optimizer stop condition. See `register_volume`. Examples: ```pycon >>> moving.fusi.register.to_volume(fixed) >>> moving.fusi.register.to_volume(fixed, resample=True) >>> moving.fusi.register.to_volume(fixed, show_progress=True) ``` #### volumewise ```python volumewise( *, reference_time: int | None = None, fixed: DataArray | None = None, n_jobs: int = -1, transform: Literal[ "translation", "rigid", "affine" ] = "rigid", metric: Literal[ "correlation", "mattes_mi" ] = "correlation", fixed_intensity_scaling: Literal["none", "db", "sqrt"] | float | None = None, moving_intensity_scaling: Literal["none", "db", "sqrt"] | float = "none", number_of_histogram_bins: int = 50, learning_rate: float | Literal["auto"] = 0.01, number_of_iterations: int = 100, convergence_minimum_value: float = 1e-06, convergence_window_size: int = 10, initialization: Literal[ "center_geometry", "center_moments" ] | None = "center_geometry", optimizer_weights: list[float] | None = None, use_multi_resolution: bool = False, shrink_factors: Sequence[int] = (6, 2, 1), smoothing_sigmas: Sequence[int] = (6, 2, 1), resample_interpolation: Literal[ "linear", "bspline" ] = "linear", fill_value: float | None = None, show_progress: bool = True, progress_reporter: VolumewiseProgressReporter | None = None, abort_event: Event | None = None, keep_diagnostics: bool = False, ) -> DataArray ``` Register all volumes to a reference time point or a reference volume. Parameters: - ##### **`reference_time`** (`int`, default: `None` ) – Index of the time point to use as registration target. If not provided, and `fixed` is not provided either, the first time point (`0`) is used. Cannot be combined with `fixed`. - ##### **`fixed`** (`DataArray`, default: `None` ) – Spatial-only VoxelData array on the same voxel grid to register every frame to, for example the mean of a few low-motion frames. Must have no `time` dimension. If not provided, the frame at `reference_time` is used. Cannot be combined with `reference_time`. - ##### **`n_jobs`** (`int`, default: `-1` ) – Number of parallel jobs. -1 uses all available CPUs. Use 1 for serial processing. - ##### **`transform`** (`(translation, rigid, affine)`, default: `"translation"` ) – Type of transform to use for registration. - ##### **`metric`** (`(correlation, mattes_mi)`, default: `"correlation"` ) – Similarity metric for registration. - ##### **`fixed_intensity_scaling`** (`(none, db, sqrt)`, default: `"none"` ) – Intensity transform applied to `fixed`, only for the registration optimizer. Only allowed together with `fixed`; the reference frame selected by `reference_time` always uses `moving_intensity_scaling`. If not provided, `fixed` is scaled with `moving_intensity_scaling` too. - ##### **`moving_intensity_scaling`** (`(none, db, sqrt)`, default: `"none"` ) – Intensity transform applied to every frame, only for the registration optimizer. Floats apply power scaling with that exponent; `"sqrt"` is an alias for `0.5`. Returned/resampled data keeps the original input intensities. - ##### **`number_of_histogram_bins`** (`int`, default: `50` ) – Number of histogram bins (only used when `metric="mattes_mi"`). - ##### **`learning_rate`** (`float or auto`, default: `0.01` ) – Optimizer step size in normalised units (after `SetOptimizerScalesFromPhysicalShift`). `"auto"` re-estimates the rate at every iteration. A float uses that value directly; if registration diverges or fails to converge, reduce it. - ##### **`number_of_iterations`** (`int`, default: `100` ) – Maximum number of optimizer iterations. - ##### **`convergence_minimum_value`** (`float`, default: `1e-6` ) – Convergence threshold for early stopping. - ##### **`convergence_window_size`** (`int`, default: `10` ) – Window size for convergence check. - ##### **`initialization`** (`(center_geometry, center_moments)`, default: `"center_geometry"` ) – Initial transform mapping `fixed` to `moving` coordinates, applied before optimization: - `"center_geometry"`: aligns image centers. - `"center_moments"`: aligns centers of mass. - `None`: uses the identity transform. - ##### **`optimizer_weights`** (`list of float`, default: `None` ) – Per-parameter weights applied on top of auto-estimated scales via `SetOptimizerWeights()`. If not provided, no additional weighting is applied. The weight for each parameter is multiplied into the effective step size: `0` freezes a parameter, values in `(0, 1)` slow it down, `1` leaves it unchanged. For the 3D Euler transform the order is `[angleX, angleY, angleZ, tx, ty, tz]`; to disable rotations around x and y use `[0, 0, 1, 1, 1, 1]`. - ##### **`use_multi_resolution`** (`bool`, default: `False` ) – Whether to use a multi-resolution pyramid during registration. - ##### **`shrink_factors`** (`sequence of int`, default: `(6, 2, 1)` ) – Downsampling factor at each pyramid level, from coarsest to finest. Only used when `use_multi_resolution=True`. - ##### **`smoothing_sigmas`** (`sequence of int`, default: `(6, 2, 1)` ) – Gaussian smoothing sigma (in voxels) at each pyramid level, from coarsest to finest. Only used when `use_multi_resolution=True`. - ##### **`resample_interpolation`** (`(linear, bspline)`, default: `"linear"` ) – Interpolation method used for the final resample step. - ##### **`fill_value`** (`float`, default: `None` ) – Fill value for voxels outside each moving volume's field of view after resampling. If not provided, defaults to that volume's minimum value. - ##### **`show_progress`** (`bool`, default: `True` ) – Whether to display a progress bar while registering volumes. - ##### **`progress_reporter`** (`VolumewiseProgressReporter`, default: `None` ) – Thread-safe reporter notified whenever one frame completes. If not provided, no per-frame callback is used. - ##### **`abort_event`** (`Event`, default: `None` ) – Cooperative cancellation flag shared across frames. - ##### **`keep_diagnostics`** (`bool`, default: `False` ) – Whether to keep per-frame registration diagnostics on the result. See `register_volumewise` for the full description. Returns: - `DataArray` – Registered data with same coordinates and attributes. Examples: ```pycon >>> data.fusi.register.volumewise(reference_time=0) >>> data.fusi.register.volumewise(reference_time=0, transform="translation") >>> fixed = data.isel(time=slice(0, 10)).mean("time") >>> data.fusi.register.volumewise(fixed=fixed) ``` ### FUSIScaleAccessor Accessor for scaling operations on fUSI data. This accessor provides various scaling transformations commonly used in functional ultrasound imaging analysis. Parameters: - #### **`xarray_obj`** (`DataArray`) – The DataArray to wrap. Examples: ```pycon >>> import xarray as xr >>> data = xr.DataArray([1, 10, 100, 1000]) >>> data.fusi.scale.db(factor=20) array([-60., -40., -20., 0.]) ``` Methods: - **`db`** – Convert data to decibel scale relative to maximum value. - **`log`** – Apply natural logarithm to data. - **`power`** – Apply power scaling to data. #### db ```python db(factor: int | None = None) -> DataArray ``` Convert data to decibel scale relative to maximum value. Parameters: - ##### **`factor`** (`int`, default: `None` ) – Scaling factor for decibel conversion. Use 10 for power quantities, 20 for amplitude quantities. If not provided, defaults to 20 for complex-valued (typically beamformed IQ signals) data and 10 otherwise (typically power Doppler signals). Returns: - `DataArray` – Data in decibel scale. Values are in range `[factor * log(min/max), 0]` dB. > [!NOTE] > > **Notes** > > Warnings are suppressed for zero/negative values, which are set to `-inf`. > > If the input data is backed by Dask (lazily loaded), the global maximum is computed eagerly when this method is called. This avoids re-triggering a full array scan on each frame access (e.g. during napari playback), at the cost of a one-time upfront computation. Examples: ```pycon >>> data = xr.DataArray([1, 10, 100, 1000]) >>> data.fusi.scale.db(factor=20) array([-60., -40., -20., 0.]) ``` #### log ```python log() -> DataArray ``` Apply natural logarithm to data. Returns: - `DataArray` – Natural logarithm of the data. Examples: ```pycon >>> import numpy as np >>> data = xr.DataArray([1, np.e, np.e**2]) >>> data.fusi.scale.log() array([0., 1., 2.]) ``` #### power ```python power(exponent: float = 0.5) -> DataArray ``` Apply power scaling to data. Parameters: - ##### **`exponent`** (`float`, default: `0.5` ) – Power exponent to apply. Default is 0.5 (square root). Use 2.0 for squaring, etc. Returns: - `DataArray` – Power-scaled data. Examples: ```pycon >>> data = xr.DataArray([1, 4, 9, 16]) >>> data.fusi.scale.power(exponent=0.5) # Square root array([1., 2., 3., 4.]) ``` ### apply\_affine ```python apply_affine( da: DataArray, affine: NDArray[float64] | str, inplace: bool = False, ) -> DataArray ``` Apply a world-space affine to a DataArray's world coordinates. The transform is composed into the DataArray's `VoxelToWorldIndex`, derived world coordinates are regenerated, and existing `attrs["affines"]` entries are re-expressed against the new world frame. Parameters: - #### **`da`** (`DataArray`) – Input scan with voxel-to-world geometry (a `VoxelToWorldIndex`). - #### **`affine`** (`(4, 4) numpy.ndarray or str`) – Homogeneous world-space affine matrix to apply. If a string, it is looked up as a key in `da.attrs["affines"]`. - #### **`inplace`** (`bool`, default: `False` ) – Whether to modify the DataArray in-place. Returns: - `DataArray` – `da` with updated spatial coordinates and updated `attrs["affines"]`. No entry is removed: when `affine` is a string, that key is kept as the identity, since the world frame now is that named space. Raises: - `ValueError` – If `da` lacks voxel-to-world geometry, if `affine` shape does not match the DataArray's voxel-to-world affine, or if `affine` is a string and `da` has no `"affines"` entry in `attrs`. - `KeyError` – If `affine` is a string not present in `da.attrs["affines"]`. Examples: ```pycon >>> import numpy as np >>> import xarray as xr >>> import confusius # noqa: F401 >>> data = xr.DataArray( ... np.zeros((3, 4)), ... dims=["j", "i"], ... coords={"j": np.arange(3), "i": np.arange(4)}, ... ) >>> data = data.fusi.affine.set_voxel_to_world(np.eye(3)) >>> shift = np.eye(3) >>> shift[:2, 2] = [10.0, 5.0] >>> result = data.fusi.affine.apply(shift) >>> float(result.fusi.affine.voxel_to_world[0, 2]) 10.0 ``` ### create\_voxeldata ```python create_voxeldata( data: ArrayLike | Array, *, dims: Sequence[str], extra_coords: Mapping[str, ArrayLike | DataArray] | None = None, time: ArrayLike | DataArray | None = None, pose: ArrayLike | DataArray | None = None, k: ArrayLike | DataArray | None = None, j: ArrayLike | DataArray | None = None, i: ArrayLike | DataArray | None = None, dt: float | None = None, t0: float | ArrayLike = 0.0, volume_acquisition_reference: VolumeAcquisitionReference = "start", volume_acquisition_duration: float | None = None, spacing: Sequence[float] | None = None, origin: Sequence[float] | None = None, direction: ArrayLike | None = None, voxel_to_world: ArrayLike | None = None, units: str = "mm", name: str | None = None, attrs: dict[str, Any] | None = None, ) -> DataArray ``` Build a VoxelData array from a raw array. Parameters: - #### **`data`** (`ArrayLike`) – Raw array. - #### **`dims`** (`sequence[str]`) – Input dimension names. Core dimensions are: - `i`/`j`/`k`: native voxel dimensions, - `pose`: probe pose dimension, - `time`: time dimension. Any other extra dimensions are allowed. The returned DataArray will have dimensions reordered following the VoxelData model: `(extra_dims, time, pose, k, j, i)`. - #### **`extra_coords`** (`mapping[str, ArrayLike or DataArray]`, default: `None` ) – Coordinates for non-core dimensions only. - #### **`time`** (`ArrayLike or DataArray`, default: `None` ) – Floating coordinates for the `time` dimension. A 2D `(n_time, npose)` array or DataArray gives each pose its own real timestamps directly (poses acquired sequentially rather than simultaneously) rather than a single shared `time` axis: there is no single answer for "the" time of a `(pose, k, j, i)` voxel any more than there is a single answer for its `z`/`y`/`x` position, so `time` requires a scalar `pose` selection first, exactly like world coordinates already do. Requires a `pose` dimension in `dims` with a matching length. A 2D `time` is not itself an index (xarray dimension coordinates must be 1D), so `.sel(time=...)` is unavailable until a pose is selected; after that, `.set_xindex("time")` promotes the resulting 1D `time` back into a real, selectable index. - #### **`pose`** (`ArrayLike or DataArray`, default: `None` ) – Integer coordinates for the `pose` dimension. - #### **`k`** (`ArrayLike or DataArray`, default: `None` ) – Integer native voxel coordinates for the corresponding voxel dimensions. If not provided, dense zero-based coordinates are generated. - #### **`j`** (`ArrayLike or DataArray`, default: `None` ) – Integer native voxel coordinates for the corresponding voxel dimensions. If not provided, dense zero-based coordinates are generated. - #### **`i`** (`ArrayLike or DataArray`, default: `None` ) – Integer native voxel coordinates for the corresponding voxel dimensions. If not provided, dense zero-based coordinates are generated. - #### **`dt`** (`float`, default: `None` ) – Time spacing in seconds, used when `time` is not provided. For multi-pose arrays, `dt` is shared across poses. - #### **`t0`** (`float or ArrayLike`, default: `0.0` ) – First time coordinate value when `dt` is used. For multi-pose arrays, a 1D `t0` with one value per pose generates a pose-dependent `(time, pose)` time coordinate using the shared `dt`. - #### **`volume_acquisition_reference`** (`('start', 'center', 'end')`, default: `"start"` ) – Time reference stored on generated `time` coordinates. - #### **`volume_acquisition_duration`** (`float`, default: `None` ) – Acquisition duration stored on generated `time` coordinates. If not provided, it defaults to `dt` for a plain 1D `time` coordinate, or to the smallest nonzero gap between `t0` values for a pose-dependent `(time, pose)` coordinate—`dt` there is the repetition period between successive samples of the *same* pose, not one pose's own acquisition time. Poses sharing the same `t0` (e.g. a stacked-linear-probe pose acquired simultaneously with another) don't count toward the gap. - #### **`spacing`** (`sequence[float]`, default: `None` ) – World spacing in `z/y/x` order. Mutually exclusive with `voxel_to_world`. - #### **`origin`** (`sequence[float]`, default: `None` ) – World origin in `z/y/x` order. If not provided, ConfUSIus probe defaults are used. - #### **`direction`** (`ArrayLike`, default: `None` ) – 3x3 direction matrix in world `z/y/x` row and voxel `k/j/i` column order. - #### **`voxel_to_world`** (`ArrayLike`, default: `None` ) – 4x4 homogeneous affine in world `z/y/x` row and voxel `k/j/i` column order, or an `(npose, 4, 4)` stack of one such affine per pose when a `pose` dimension is present in `dims`. Mutually exclusive with `spacing`, `origin`, and `direction`. - #### **`units`** (`str`, default: `"mm"` ) – Physical unit shared by every derived world coordinate. - #### **`name`** (`str`, default: `None` ) – DataArray name. - #### **`attrs`** (`dict`, default: `None` ) – DataArray attributes. Returns: - `DataArray` – VoxelData array with native voxel dimensions and world coordinates. Raises: - `ValueError` – If `dims` uses world `z`/`y`/`x` names instead of native voxel names, if a pose-stacked `voxel_to_world` is given without a matching `pose` dimension, if `dims` has a `pose` dimension and `voxel_to_world` is not a matching per-pose stack, or if dimensions, coordinates, geometry, timing, or VoxelData validation otherwise fail. ### db\_scale ```python db_scale( data: DataArray, factor: int | None = None ) -> DataArray ``` Convert data to decibel scale relative to maximum value. Parameters: - #### **`data`** (`DataArray`) – Input `DataArray`. - #### **`factor`** (`int`, default: `None` ) – Scaling factor for decibel conversion. Use 10 for power quantities, 20 for amplitude quantities. If not provided, defaults to 20 for complex-valued (typically beamformed IQ signals) data and 10 otherwise (typically power Doppler signals). Returns: - `DataArray` – Data in decibel scale. Values are in range `[factor * log(min/max), 0]` dB. > [!NOTE] > > **Notes** > > Warnings are suppressed for zero/negative values, which are set to `-inf`. > > If the input data is backed by Dask (lazily loaded), the global maximum is computed eagerly when this function is called. This avoids re-triggering a full array scan on each frame access (e.g. during napari playback), at the cost of a one-time upfront computation. Examples: ```pycon >>> import xarray as xr >>> import numpy as np >>> data = xr.DataArray([1, 10, 100, 1000]) >>> db_scale(data, factor=20) ``` ### get\_bounding\_box ```python get_bounding_box(data: DataArray) -> DataArray ``` Return the world-space bounding box of a VoxelData array. The box encloses the full extent of every voxel, not just the voxel centers: the voxel-coordinate range of each `k`/`j`/`i` dim is extended by half a voxel at both ends before mapping through the voxel-to-world affine. The voxel size is `fusi.spacing` (so a singleton dim spans one voxel, and irregular voxel coordinates are rejected). The result is exact for any affine, including oblique ones, without materializing the lazily derived world coordinates: each world coordinate is an affine function of the voxel coordinates, so its extremes over the grid are attained at the corners of the (extended) voxel-coordinate box. Parameters: - #### **`data`** (`DataArray`) – VoxelData array, normalized with ensure\_voxeldata (scalar-indexed voxel dims are restored as singletons before computing the bounds). Returns: - `(2, 3) xarray.DataArray` – Bounding box with dims `(bound, component)`, where `bound` is `["min", "max"]` and `component` is `["z", "y", "x"]`, and a `units` attr from the voxel-to-world index. Pose-dependent geometry adds a leading `pose` dim carrying the input's `pose` coordinate, one bounding box per pose. Raises: - `TypeError` – If `data` is not an `xarray.DataArray`. - `ValueError` – If `data` is not a valid VoxelData array, or if a voxel dim has irregular coordinates (no defined voxel extent). Examples: ```pycon >>> bbox = get_bounding_box(volume) >>> bbox.sel(bound="min", component="z").item() ``` ### get\_relative\_affine ```python get_relative_affine( da: DataArray, other: DataArray, via: str ) -> NDArray[float64] ``` Return the affine mapping `da`'s world space into `other`'s. Computes `inv(other.attrs["affines"][via]) @ da.attrs["affines"][via]`, giving the transform that takes coordinates expressed in `da`'s world frame and expresses them in `other`'s world frame. Both arrays must carry an `"affines"` dict in their `attrs` with the key `via`. Parameters: - #### **`da`** (`DataArray`) – The source scan (origin world space). - #### **`other`** (`DataArray`) – The scan whose world space is the target. - #### **`via`** (`str`) – Key into `attrs["affines"]` that names the shared intermediate coordinate space used to bridge the two world frames (e.g. `"world_to_lab"`). Returns: - `(ndarray, shape(4, 4))` – Homogeneous affine matrix mapping `da`'s world coordinates to `other`'s world coordinates. Raises: - `KeyError` – If `via` is not present in `da.attrs["affines"]` or `other.attrs["affines"]`. - `ValueError` – If either array does not have an `"affines"` entry in its `attrs`. ### log\_scale ```python log_scale(data: DataArray) -> DataArray ``` Apply natural logarithm to data. Parameters: - #### **`data`** (`DataArray`) – Input data array. Returns: - `DataArray` – Natural logarithm of the data. > [!NOTE] > > **Notes** > > Warnings are suppressed for zero/negative values, which are set to `-inf/nan`. Examples: ```pycon >>> import xarray as xr >>> import numpy as np >>> data = xr.DataArray([1, np.e, np.e**2]) >>> log_scale(data) ``` ### power\_scale ```python power_scale( data: DataArray, exponent: float = 0.5 ) -> DataArray ``` Apply power scaling to data. Parameters: - #### **`data`** (`DataArray`) – Input data array. - #### **`exponent`** (`float`, default: `0.5` ) – Power exponent to apply. Default is 0.5 (square root). Use 2.0 for squaring, etc. Returns: - `DataArray` – Power-scaled data. Examples: ```pycon >>> import xarray as xr >>> data = xr.DataArray([1, 4, 9, 16]) >>> power_scale(data, exponent=0.5) # Square root ``` ### reindex\_voxels ```python reindex_voxels(da: DataArray) -> DataArray ``` Rebase voxel coordinates to dense positions without moving world coordinates. A VoxelData array's stored `voxel_to_world` affine is defined in terms of voxel *coordinate values*, which stay unchanged across cropping or striding by design (see `VoxelToWorldIndex`). Because of this, the affine generally does not describe where voxel *position* `(0, ..., 0)` sits in world space, or the world distance between consecutive positions, once `da` has been cropped or strided from a larger array. This replaces each voxel dimension's coordinate with `0, 1, ..., dim - 1` and rebuilds `voxel_to_world` so the resulting affine directly maps those dense positions to `da`'s existing world coordinates, producing a DataArray whose affine is directly usable by software that assumes dense, zero-based voxel indices (e.g. ITK, nilearn). Parameters: - #### **`da`** (`DataArray`) – Input scan with voxel-to-world geometry. Returns: - `DataArray` – `da` with voxel coordinates rebased to `0, 1, ..., dim - 1` and an updated `voxel_to_world` affine. World coordinates are unchanged. For pose-dependent geometry, `voxel_to_world` stays a per-pose stack: each pose keeps its own origin and direction (spacing is shared, per the equal-scale invariant), so rebasing is unambiguous per pose and every pose is reindexed at once. Raises: - `ValueError` – If `da` lacks voxel-to-world geometry, or if world spacing is undefined for any voxel dimension. ### reindex\_voxels\_like ```python reindex_voxels_like( data: DataArray, reference: DataArray, *, atol: float = 1e-06, ) -> DataArray ``` Rebase voxel coordinates onto `reference`'s voxel labels. `data` and `reference`'s `voxel_to_world` affines can differ even when they describe the exact same world grid: the affine is defined in terms of voxel *coordinate values*, so two arrays occupying identical world positions can still carry different affines if their voxel dimensions happen to be labeled differently (e.g. `reference` was cropped or strided from a larger array, while `data` was freshly built with dense labels). This verifies the two occupy the same world grid, then relabels `data`'s voxel coordinates and affine to match `reference`'s exactly, so the two become directly alignable (`.sel()`, arithmetic, `xarray.align`, ...) by voxel label as well as by world position. Parameters: - #### **`data`** (`DataArray`) – Input scan with voxel-to-world geometry, physically aligned with `reference`. - #### **`reference`** (`DataArray`) – DataArray whose voxel labels and affine `data` should adopt. - #### **`atol`** (`float`, default: `1e-6` ) – Absolute tolerance, in `reference`'s physical units, for the world-coordinate alignment check between `data` and `reference`. Returns: - `DataArray` – `data` with voxel coordinates and `voxel_to_world` replaced by `reference`'s. World coordinates are unchanged, since `data` and `reference` are verified to already occupy the same world grid. Raises: - `ValueError` – If `data` or `reference` lacks voxel-to-world geometry, if their voxel dimensions or shapes differ, or if their world coordinates do not match within `atol`. # Examples # Atlas-based region correlation matrix This example shows an end-to-end regional functional connectivity (FC) analysis: briefly register a single-slice fUSI recording to an Allen-space template, bring the Allen Mouse Brain Atlas into the recording's native space, extract region-averaged signals, and visualize their pairwise correlation with `plot_matrix`. We use an awake freely-running acquisition from subject `CR022`, session `20201007`, in the Nunez-Elizalde 2022 dataset[^/dev/examples/_built/connectivity/atlas_correlation_matrix/#collection], and the Pepe, Mariani 2026 fUSI template, which carries the affine transform required to bring it into Allen Common Coordinate Framework (CCF) space. For the full registration workflow, including the diagnostic plots and saving the resampled atlas for reuse, see [Register a recording to an Allen fUSI template](). ## Fetch the recording and the template The recording is a single coronal slice imaged for approximately 4 minutes at 3.33 Hz. Registration works on a static anatomical image, so we use the temporal mean, converted to decibels for a more stable dynamic range. ```python from pathlib import Path import matplotlib as mpl import numpy as np import xarray as xr import confusius as cf # Adapt background color to the current Matplotlib style. bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) xr.set_options(display_expand_data=False) template = cf.datasets.fetch_template_pepe_mariani_2026() bids_root = cf.datasets.fetch_nunez_elizalde_2022( subjects="CR022", sessions="20201007", tasks="spontaneous", acqs="slice02", ) ``` ``` If you use this template in your work, please cite the following source: Pepe, C., Mariani, J.-C., Urosevic, M., Gini, S., Stuefer, A., Ricci, F., Galbusera, A., Iurilli, G., & Gozzi, A. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (fUSI). bioRxiv. https://doi.org/10.64898/2026.02.05.704055 ``` ``` If you use this dataset in your work, please cite the following source: Nunez-Elizalde, A. O., Krumin, M., Reddy, C. B., Montaldo, G., Urban, A., Harris, K. D., & Carandini, M. (2022). Neural correlates of blood flow measured by ultrasound. Neuron, 110(10), 1631–1640.e4. https://doi.org/10.1016/j.neuron.2022.02.012 ``` ```python data_path = ( Path(bids_root) / "sub-CR022" / "ses-20201007" / "fusi" / "sub-CR022_ses-20201007_task-spontaneous_acq-slice02_pwd.nii.gz" ) # The recording's timepoints are not perfectly uniformly spaced so we resample to a # uniform grid before any time-domain processing (filtering below requires it). data = cf.timing.resample_to_uniform_time(cf.load(data_path)) moving = data.mean(dim="time").fusi.scale.db().compute() moving ``` ```text /tmp/ipykernel_44010/415482052.py:10: UserWarning: Time coordinate is non-uniform; using the median step to build a uniform target grid. data = cf.timing.resample_to_uniform_time(cf.load(data_path)) ``` xarray.DataArray 'sub-CR022\_ses-20201007\_task-spontaneous\_acq-slice02\_pwd' - k: 1 - j: 125 - i: 80 - [ ] \-18.69 -18.57 -18.5 -18.46 -18.38 ... -13.37 -13.36 -13.33 -13.28 ``` array([[[-18.687918, -18.569138, -18.501255, ..., -17.95731 , -17.939037, -17.964108], [-18.535534, -18.445446, -18.356527, ..., -17.885906, -17.885307, -17.89217 ], [-18.489779, -18.407112, -18.328487, ..., -17.793518, -17.804783, -17.829914], ..., [-12.315687, -13.394132, -13.825216, ..., -13.536499, -13.521715, -13.627706], [-12.319636, -13.353613, -13.749182, ..., -13.457008, -13.386365, -13.444547], [-12.376516, -13.219033, -13.602533, ..., -13.36433 , -13.333445, -13.279077]]], shape=(1, 125, 80), dtype=float32) ``` - [x] Coordinates: (6) - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 120 121 122 123 124 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124]) ``` - i (i) int64 0 1 2 3 4 5 6 ... 74 75 76 77 78 79 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]) ``` - z (k, j, i) float64 0.4 0.4 0.4 0.4 ... 0.4 0.4 0.4 0.4 ☐ ☐ **units :** mm ``` array([[[0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], ..., [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001]]], shape=(1, 125, 80)) ``` - y (k, j, i) float64 2.996 2.996 2.996 ... 8.988 8.988 ☐ ☐ **units :** mm ``` array([[[2.99584007, 2.99584007, 2.99584007, ..., 2.99584007, 2.99584007, 2.99584007], [3.04416007, 3.04416007, 3.04416007, ..., 3.04416007, 3.04416007, 3.04416007], [3.09248007, 3.09248007, 3.09248007, ..., 3.09248007, 3.09248007, 3.09248007], ..., [8.89087997, 8.89087997, 8.89087997, ..., 8.89087997, 8.89087997, 8.89087997], [8.93919997, 8.93919997, 8.93919997, ..., 8.93919997, 8.93919997, 8.93919997], [8.98751996, 8.98751996, 8.98751996, ..., 8.98751996, 8.98751996, 8.98751996]]], shape=(1, 125, 80)) ``` - x (k, j, i) float64 \-3.95 -3.85 -3.75 ... 3.85 3.95 ☐ ☐ **units :** mm ``` array([[[-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], ..., [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007]]], shape=(1, 125, 80)) ``` - [ ] Indexes: (4) - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 115, 116, 117, 118, 119, 120, 121, 122, 123, 124], dtype='int64', name='j', length=125)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79], dtype='int64', name='i')) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (27) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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.991680000000001\] **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} **units :** dB **scaling :** 10\*log10(x/max) We don't describe the registration process here to keep the notebook focused on seed-based connectivity, but the key steps are: - Get an initial affine transform using napari's "Transform" tool. - Use `register_volume` to refine the alignment. - Resample the Allen atlas into the recording's native space with `resample_like`. For the full walkthrough, see [Register a recording to an Allen fUSI template](). > [!NOTE] > > **Registration and atlas resampling** > > ```python > napari_affine = np.array( > [ > [1.0, 0.0, 0.0, 5.594638656430411], > [0.0, 1.0, 0.0, -2.50293925701927], > [0.0, 0.0, 1.0, 5.6650243788545875], > [0.0, 0.0, 0.0, 1.0], > ] > ) > initialization = np.linalg.inv(napari_affine) > > # Crop the template to a thin band around the recording's expected location to improve > # registration speed and visualization. > target_z = napari_affine[0, 3] + moving.fusi.origin["z"] > fixed = template.sel(z=slice(target_z - 1.0, target_z + 1.0)) > > registered, affine, diagnostics = cf.registration.register_volume( > moving=moving, > fixed=fixed, > transform_type="affine", > metric="correlation", > convergence_window_size=100, > number_of_iterations=500, > learning_rate=1, > initialization=initialization, > show_progress=False, > ) > > world_to_sform = template.attrs["affines"]["world_to_sform"] > subject_to_atlas = world_to_sform @ np.linalg.inv(affine) > > atlas = cf.datasets.fetch_brainglobe_atlas("allen_mouse_100um", check_latest=False) > atlas_native = atlas.atlas.resample_like(moving, subject_to_atlas) > ``` ## Extract region signals and compute their correlation matrix `get_masks` accepts parent acronyms from the Allen ontology (e.g. `"SSp-bfd"`) and automatically aggregates every descendant region, so we can request a handful of coarse regions per area of interest instead of individual cortical layers or thalamic nuclei. We pick three regions each from cortex, hippocampus, thalamus, and hypothalamus. We extract left and right hemispheres separately via `get_masks`'s `sides` argument: combining both sides into one mask would average left/right signals together and hide bilateral FC and interhemispheric differences. `get_masks` names each layer's `mask` coordinate with the acronym suffixed by `_L`/`_R`, and `extract_with_labels` carries that name through to the output `region` coordinate, so the two hemispheres' masks can be stacked and extracted in a single call. Within each area the left hemisphere is ordered lateral-to-medial and the right medial-to-lateral, so each area block reads as one continuous sweep across the slice. ```python groups = { "cortex": ["RSPv", "MOp", "SSp-bfd"], "hippocampus": ["DG", "CA3", "CA2"], "thalamus": ["PO", "VPM", "RT"], "hypothalamus": ["PH", "LHA", "ZI"], } # Allen CCF ids for each area's parent division, used below to color the group strips # with the atlas's own official colors instead of arbitrary ones. division_ids = { "cortex": 315, "hippocampus": 1089, "thalamus": 549, "hypothalamus": 1097, } group_colors = { area: "#{:02x}{:02x}{:02x}".format( *atlas.atlas.lookup.loc[division_id, "rgb_triplet"] ) for area, division_id in division_ids.items() } region_acronyms = [acronym for acronyms in groups.values() for acronym in acronyms] region_order = [] group_labels = [] for area, acronyms in groups.items(): region_order += [f"{acronym}_L" for acronym in reversed(acronyms)] region_order += [f"{acronym}_R" for acronym in acronyms] group_labels += [area] * (2 * len(acronyms)) sides = ["left"] * len(region_acronyms) + ["right"] * len(region_acronyms) masks = atlas_native.atlas.get_masks(region_acronyms * 2, sides=sides) signals = cf.extract.extract_with_labels(data, masks, reduction="mean") # extract_with_labels does not guarantee any particular region order, so reindex # explicitly into the left-right sweep computed above. signals = signals.sel(region=region_order) signals ``` xarray.DataArray 'sub-CR022\_ses-20201007\_task-spontaneous\_acq-slice02\_pwd' - time: 755 - region: 24 - [ ] dask.array\
Array Chunk
Bytes 70.78 kiB 70.78 kiB
Shape (755, 24) (755, 24)
Dask graph 1 chunks in 15 graph layers
Data type float32 numpy.ndarray
- [x] Coordinates: (2) - time (time) float64 11.0 11.3 11.6 ... 236.9 237.2 ☐ ☐ **units :** s **volume\_acquisition\_reference :** start **volume\_acquisition\_duration :** 0.3 ``` array([ 10.998, 11.298, 11.598, ..., 236.598, 236.898, 237.198], shape=(755,)) ``` - region (region) \) [Download .py]() [Download .ipynb]() [^/dev/examples/_built/connectivity/atlas_correlation_matrix/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. # Atlas-based seed connectivity maps This example computes voxel-wise seed-based functional connectivity maps: register a single-slice fUSI recording to an Allen-space template, bring an Allen Mouse Brain Atlas into the recording's native space, pick four atlas regions of interest as seeds, and correlate each seed's signal against every voxel with `SeedBasedMaps`. Each resulting map is displayed with `plot_stat_map`, using the resampled Allen template volume as background. We use an awake freely-running acquisition from subject `CR022`, session `20201007`, in the Nunez-Elizalde 2022 dataset[^/dev/examples/_built/connectivity/atlas_seed_map/#collection], and the Pepe, Mariani 2026 fUSI template, which carries the affine transform required to bring it into Allen Common Coordinate Framework (CCF) space. For the full registration workflow, see [Register a recording to an Allen fUSI template](). ## Fetch the recording and register to the Allen atlas The recording is a single coronal slice imaged for approximately 4 minutes at 3.33 Hz. Registration works on a static anatomical image, so we use the temporal mean, converted to decibels for a more stable dynamic range. ```python 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 # Adapt background color to the current Matplotlib style. bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) xr.set_options(display_expand_data=False) template = cf.datasets.fetch_template_pepe_mariani_2026().compute() bids_root = cf.datasets.fetch_nunez_elizalde_2022( subjects="CR022", sessions="20201007", tasks="spontaneous", acqs="slice02" ) ``` ``` If you use this template in your work, please cite the following source: Pepe, C., Mariani, J.-C., Urosevic, M., Gini, S., Stuefer, A., Ricci, F., Galbusera, A., Iurilli, G., & Gozzi, A. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (fUSI). bioRxiv. https://doi.org/10.64898/2026.02.05.704055 ``` ``` If you use this dataset in your work, please cite the following source: Nunez-Elizalde, A. O., Krumin, M., Reddy, C. B., Montaldo, G., Urban, A., Harris, K. D., & Carandini, M. (2022). Neural correlates of blood flow measured by ultrasound. Neuron, 110(10), 1631–1640.e4. https://doi.org/10.1016/j.neuron.2022.02.012 ``` ```python data_path = ( Path(bids_root) / "sub-CR022" / "ses-20201007" / "fusi" / "sub-CR022_ses-20201007_task-spontaneous_acq-slice02_pwd.nii.gz" ) # The recording's timepoints are not perfectly uniformly spaced so we resample to a # uniform grid before any time-domain processing (filtering below requires it). data = cf.timing.resample_to_uniform_time(cf.load(data_path)) moving = data.mean(dim="time").fusi.scale.db().compute() ``` ```text /tmp/ipykernel_49581/3634357438.py:10: UserWarning: Time coordinate is non-uniform; using the median step to build a uniform target grid. data = cf.timing.resample_to_uniform_time(cf.load(data_path)) ``` We don't describe the registration process here to keep the notebook focused on seed-based connectivity, but the key steps are: - Get an initial affine transform using napari's "Transform" tool. - Use `register_volume` to refine the alignment. - Resample the Allen atlas into the recording's native space with `resample_like`. For the full walkthrough, see [Register a recording to an Allen fUSI template](). > [!NOTE] > > **Registration and atlas resampling** > > ```python > napari_affine = np.array( > [ > [1.0, 0.0, 0.0, 5.594638656430411], > [0.0, 1.0, 0.0, -2.50293925701927], > [0.0, 0.0, 1.0, 5.6650243788545875], > [0.0, 0.0, 0.0, 1.0], > ] > ) > initialization = np.linalg.inv(napari_affine) > > # Crop the template to a thin band around the recording's expected location to improve > # registration speed and visualization. > target_z = napari_affine[0, 3] + moving.fusi.origin["z"] > fixed = template.sel(z=slice(target_z - 1.0, target_z + 1.0)) > > registered, affine, diagnostics = cf.registration.register_volume( > moving=moving, > fixed=fixed, > transform_type="affine", > metric="correlation", > convergence_window_size=100, > number_of_iterations=500, > learning_rate=1, > initialization=initialization, > show_progress=False, > ) > > world_to_sform = template.attrs["affines"]["world_to_sform"] > subject_to_atlas = world_to_sform @ np.linalg.inv(affine) > > atlas = cf.datasets.fetch_brainglobe_atlas("allen_mouse_100um", check_latest=False) > atlas_native = atlas.atlas.resample_like(moving, subject_to_atlas) > ``` ## Choose seed regions We pick one seed from each of four functional systems, to contrast their connectivity patterns: the primary somatosensory barrel field cortex (`"SSp-bfd"`), a classic resting-state seed with a strong, well-documented bilateral (interhemispheric) correlation signature; the retrosplenial cortex (`"RSP"`), a default-mode-like hub expected to correlate broadly across much of cortex; the hippocampus (`"HIP"`), which should instead correlate with a more localized, hippocampal-formation-restricted network; and the ventral posteromedial thalamic nucleus (`"VPM"`), a somatosensory relay nucleus expected to correlate with `"SSp-bfd"` through the thalamocortical pathway. All seeds are taken from the right hemisphere only, so that any left-hemisphere correlation in the resulting maps reflects genuine interhemispheric connectivity rather than the seed leaking into its own mask. `get_masks` returns a stacked `(mask, k, j, i)` integer DataArray—one layer per requested region—which `SeedBasedMaps` accepts directly as `seed_masks`. ```python seed_masks = atlas_native.atlas.get_masks( ["SSp-bfd", "RSP", "HIP", "VPM"], sides="right" ) ``` ## Smooth and compute nuisance regressors We lightly smooth the recording spatially with `smooth_volume` (0.1 mm FWHM) to improve the voxel-wise SNR before extracting confound signals and fitting the seed-based maps. As in the correlation-matrix example, we regress out an aCompCor component extracted from white-matter voxels (the Allen ontology's `"fiber tracts"` division) together with a `low_cutoff` high-pass cosine filter for slow drift. Both are passed to `SeedBasedMaps` via `clean_kwargs`, which cleans the full voxel-wise recording *before* extracting the seed signals, so seeds and voxels are preprocessed consistently. ```python data = cf.spatial.smooth_volume(data, fwhm=0.1) white_matter = atlas_native.atlas.get_masks("fiber tracts").isel(mask=0) acompcor = cf.signal.compute_compcor_confounds( data, noise_mask=white_matter, n_components=1, variance_threshold=0.95 ) ``` ## Compute the seed-based correlation maps `SeedBasedMaps` extracts each seed's average signal, correlates it against every voxel in the recording, and returns one Pearson r map per seed, stacked along a `region` dimension. ```python mapper = cf.connectivity.SeedBasedMaps( seed_masks=seed_masks, clean_kwargs={"low_cutoff": 0.01, "filter_method": "cosine", "confounds": acompcor}, ) mapper.fit(data) mapper.maps_ ``` xarray.DataArray 'sub-CR022\_ses-20201007\_task-spontaneous\_acq-slice02\_pwd' - region: 4 - k: 1 - j: 125 - i: 80 - [ ] dask.array\
Array Chunk
Bytes 156.25 kiB 156.25 kiB
Shape (4, 1, 125, 80) (4, 1, 125, 80)
Dask graph 1 chunks in 54 graph layers
Data type float32 numpy.ndarray
- [x] Coordinates: (7) - region (region) \ ``` - region PandasIndex ☐ ☐ ``` PandasIndex(Index(['SSp-bfd_R', 'RSP_R', 'HIP_R', 'VPM_R'], dtype='str', name='region')) ``` - [ ] Attributes: (28) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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.991680000000001\] **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} **long\_name :** Pearson r **cmap :** coolwarm **norm :** \ ## Plot the seed maps Each seed's map is plotted with `plot_stat_map` over the resampled Allen template volume, with `vmax=0.8` fixing the colormap to a shared range so the four seeds are directly comparable. `mapper.maps_` stacks all four seed maps along a `region` dimension, so a single `plot_stat_map` call with `slice_mode="region"` plots one panel per seed as long as the background is broadcast to the same `region` dimension first. We outline each seed's own ROI with `VolumePlotter.add_contours`, leaving `colors` unset so each region is drawn in its canonical Allen color (read from the atlas mask's `attrs["cmap"]`/`attrs["norm"]`, the same convention used by `get_masks` elsewhere). ```python # coolwarm's white midpoint reads as a washed-out hole on a dark background, so switch # to berlin (Crameri's perceptually uniform diverging colormap, black midpoint) when the # current Matplotlib style is dark. is_dark_theme = sum(mpl.colors.to_rgb(bg_color)) / 3 < 0.5 cmap = "berlin" if is_dark_theme else None # Broadcast the shared background and brain outline across a "region" dimension # matching mapper.maps_, so plot_stat_map can slice both by region in one call. bg_by_region = atlas_native.template.expand_dims(region=mapper.maps_.region) fig, axes = plt.subplots(2, 2, figsize=(8, 6), constrained_layout=True) fig.patch.set_facecolor(bg_color) plotter = cf.plotting.plot_stat_map( mapper.maps_, bg_volume=bg_by_region, slice_mode="region", cmap=cmap, vmax=0.8, threshold=0.2, cbar_label="Pearson correlation", show_axes=False, figure=fig, axes=axes, bg_color=bg_color, ) plotter.add_contours(seed_masks.rename(mask="region"), linewidths=1.5) _ = fig.suptitle("Seed-based connectivity maps", fontsize=16) ``` --- **Total running time:** 12.8 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/connectivity/atlas_seed_map/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. # Searchlight decoding of a continuous variable This example maps which parts of a fUSI recording carry information about how fast a rat is moving with a searchlight: a small cross-validated model run over the local neighborhood of every voxel. Unlike a GLM, which asks voxel by voxel whether one voxel's signal tracks the regressor, the searchlight asks whether the local pattern around each voxel can predict it, picking up information carried jointly by neighboring voxels rather than by any one alone. We follow the experimental setting and dataset of [Cybis Pereira et al. 2026]()[^/dev/examples/_built/decoding/searchlight_speed/#collection], decoding locomotion speed from a single sagittal plane, and compare the searchlight map against a GLM fit on the same data. Both analyses receive the same preprocessing steps, so the only thing that differs between them is univariate versus multivariate, which is the comparison we actually want to make. ## Load the recording and the tracking data ```python from functools import partial from pathlib import Path import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np import pandas as pd import xarray as xr from sklearn.linear_model import RidgeCV import confusius as cf # Adapt background color to the current Matplotlib style. bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) xr.set_options(display_expand_data=False) subject = "rat75" session = "20220524" acq = "slice32" bids_root = cf.datasets.fetch_cybis_pereira_2026( datasets="rawdata", subjects=subject, sessions=session, acqs=acq, ) session_dir = Path(bids_root) / f"sub-{subject}" / f"ses-{session}" stem = f"sub-{subject}_ses-{session}_task-openfield" pwd_path = session_dir / "fusi" / f"{stem}_acq-{acq}_pwd.nii.gz" motion_path = session_dir / "motion" / f"{stem}_tracksys-DLC_acq-{acq}_motion.tsv" data = cf.load(pwd_path).compute() data ``` ``` If you use this dataset in your work, please cite the following source: Cybis Pereira, F., Castedo, S. H., Meur-Diebolt, S. L., Ialy-Radio, N., Bhattacharya, S., Ferrier, J., Osmanski, B. F., Cocco, S., Monasson, R., Pezet, S., & Tanter, M. (2026). A vascular code for speed in the spatial navigation system. Cell Reports, 45(1). https://doi.org/10.1016/j.celrep.2025.116791 ``` xarray.DataArray 'sub-rat75\_ses-20220524\_task-openfield\_acq-slice32\_pwd' - time: 1440 - k: 1 - j: 112 - i: 128 - [ ] 192.7 197.2 226.9 229.9 278.7 278.7 ... 61.31 60.61 64.25 65.22 55.23 ``` array([[[[ 192.71379271, 197.1830354 , 226.89889524, ..., 135.64715222, 117.71528254, 111.25769231], [ 269.26946815, 218.92078634, 218.29339147, ..., 153.14631565, 135.36809639, 122.19806324], [ 469.46957151, 696.08320236, 741.61495988, ..., 116.9231638 , 116.41497862, 112.41130004], ..., [ 72.70813918, 67.33206102, 67.73276301, ..., 100.03939077, 95.61359 , 101.95938025], [ 76.24533181, 68.25491978, 73.33637818, ..., 79.11982739, 89.33515235, 90.30539562], [ 77.10922277, 85.88419607, 80.21954388, ..., 70.46910931, 78.98406757, 83.14898008]]], [[[ 102.03228554, 154.92695187, 208.98264674, ..., 76.35214763, 70.93058492, 92.42799846], [ 128.40719979, 179.70018173, 266.95304282, ..., 79.35330346, 70.45420204, 82.7213379 ], [ 197.83338619, 292.06213106, 453.36528013, ..., ... [ 55.2049498 , 55.06794489, 61.32079278, ..., 68.94497935, 72.57032896, 67.73033591], [ 65.66832994, 62.9659692 , 64.82018124, ..., 66.59579324, 58.19578412, 47.85324591]]], [[[ 57.94911664, 55.762271 , 60.17339521, ..., 133.26805649, 109.66913578, 81.76589334], [ 63.21644599, 66.81321546, 80.94457473, ..., 106.51902626, 90.90147175, 75.77422113], [ 85.8301978 , 92.77473431, 91.98078457, ..., 84.56545788, 87.36666125, 74.77339123], ..., [ 59.61792382, 57.51713315, 49.45440719, ..., 85.92554608, 74.22148882, 88.33563124], [ 55.38937908, 53.42711824, 56.3874491 , ..., 65.18981048, 79.52728571, 72.33484683], [ 53.38009269, 51.48751045, 64.40242875, ..., 64.25058854, 65.21577368, 55.2309584 ]]]], shape=(1440, 1, 112, 128)) ``` - [x] Coordinates: (7) - time (time) float64 0.5 1.0 1.5 ... 719.0 719.5 720.0 ☐ ☐ **units :** s **volume\_acquisition\_reference :** start **volume\_acquisition\_duration :** 0.5000249999999369 ``` array([5.000000e-01, 1.000025e+00, 1.500025e+00, ..., 7.190184e+02, 7.195184e+02, 7.200184e+02], shape=(1440,)) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 107 108 109 110 111 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]) ``` - i (i) int64 0 1 2 3 4 5 ... 123 124 125 126 127 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]) ``` - z (k, j, i) float64 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 ☐ ☐ **units :** mm ``` array([[[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]]], shape=(1, 112, 128)) ``` - y (k, j, i) float64 5.0 5.0 5.0 ... 15.94 15.94 15.94 ☐ ☐ **units :** mm ``` array([[[ 5. , 5. , 5. , ..., 5. , 5. , 5. ], [ 5.09856 , 5.09856 , 5.09856 , ..., 5.09856 , 5.09856 , 5.09856 ], [ 5.19712 , 5.19712 , 5.19712 , ..., 5.19712 , 5.19712 , 5.19712 ], ..., [15.74303978, 15.74303978, 15.74303978, ..., 15.74303978, 15.74303978, 15.74303978], [15.84159978, 15.84159978, 15.84159978, ..., 15.84159978, 15.84159978, 15.84159978], [15.94015978, 15.94015978, 15.94015978, ..., 15.94015978, 15.94015978, 15.94015978]]], shape=(1, 112, 128)) ``` - x (k, j, i) float64 \-6.985 -6.875 ... 6.875 6.985 ☐ ☐ **units :** mm ``` array([[[-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], ..., [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027]]], shape=(1, 112, 128)) ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0.5, 1.0000250000000002, 1.500025, 2.00005, 2.50005, 3.0000750000000003, 3.500075, 4.0001, 4.5001, 5.000125, ... 715.518325, 716.018325, 716.5183499999999, 717.0183499999999, 717.518375, 718.018375, 718.5183999999999, 719.0183999999999, 719.518425, 720.018425], dtype='float64', name='time', length=1440)) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 102, 103, 104, 105, 106, 107, 108, 109, 110, 111], dtype='int64', name='j', length=112)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], dtype='int64', name='i', length=128)) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (21) **sform\_code :** 2 **qform\_code :** 1 **manufacturer :** Iconeus **manufacturers\_model\_name :** Iconeus One **probe\_type :** linear **probe\_model :** IcoPrime light **probe\_number\_of\_elements :** 128 **probe\_pitch :** 0.11 **probe\_radius\_of\_curvature :** 0 **ProbeElevationAperture :** 1.5 **ProbeElevationFocus :** 8 **plane\_wave\_angles :** \[-7.0, -5.0, -3.0, -1.0, 1.0, 3.0, 5.0, 7.0\] **IsHadamard :** True **UltrafastSamplingFrequency :** 400.0 **clutter\_filter\_window\_duration :** 0.2 **clutter\_filters :** \['svd:remove\_first\_60\_components'\] **power\_doppler\_integration\_duration :** 0.2 **probe\_center\_frequency :** 15.625 **pulse\_repetition\_frequency :** 3200.0 **transmit\_voltage :** 15 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} ## Correct for motion The rat moves freely, so a real analysis should first correct volume-to-volume brain motion. We skip it here to keep the example fast to build, at the cost of a little statistical robustness in the maps below. > [!NOTE] > > **Recommended in a real analysis** > > Register every volume to a reference frame with `register_volumewise` before building the regressors, then continue with the corrected `data`. Removing motion-driven variance sharpens the speed maps and their statistics. See the [Motion correction of a single recording]() example for the full workflow and diagnostics. > > ```python > data = cf.registration.register_volumewise(data, learning_rate=1) > ``` ## Build the speed regressor The animal is tracked with DeepLabCut at 50 frames per second. We take the frame-to-frame displacement of the body marker, smooth it with a one second centered rolling mean, and resample it onto the fUSI volume acquisition times. ```python fps = 50 motion_df = pd.read_csv(motion_path, sep="\t") squared_diff = motion_df[["body_x", "body_y"]].diff() ** 2 speed_df = fps * (squared_diff["body_x"] + squared_diff["body_y"]) ** 0.5 speed_df[0] = 0 speed = ( xr.DataArray( speed_df, dims=["time"], coords={"time": 1 / fps * np.arange(len(speed_df))}, name="speed", ) .rolling(time=fps, min_periods=1, center=True) .mean() ) speed = speed.interp(time=data.time, method="linear").ffill("time") ``` ## What the searchlight should actually predict The speed signal above is an instantaneous behavioral variable, and the power Doppler signal contains a proxy of the neural signal encoding the speed signal through the neurovascular coupling. With the neurovascular coupling inherent delay, asking a decoder to predict the instantaneous trace from that signal sets it an impossible target. We therefore decode an *hemodynamically convolved* speed regressor. We build it with the modified Claron 2021 HRF, a rodent fUSI response function, and read it straight off a first-level design matrix. Building the design matrix here serves double duty: its `speed` column is the searchlight target, and the whole matrix is what we hand the GLM later, so the two analyses are guaranteed to see the same regressor. ```python modified_claron2021 = partial(cf.glm.claron2021_hrf, beta=6.7) events = pd.DataFrame( { "onset": data.time.values, "duration": data.time.volume_acquisition_duration, "modulation": speed.values, "trial_type": "speed", } ) confounds = cf.signal.compute_compcor_confounds( data, variance_threshold=0.05, n_components=3, ) design_matrix = cf.glm.make_first_level_design_matrix( data.time.values, events=events, hrf_model=modified_claron2021, drift_model="cosine", low_cutoff=0.01, confounds=confounds, ) ``` The `speed` column of the design matrix is the raw speed trace convolved with the HRF. We read it out as the searchlight target. ```python speed_regressor = design_matrix["speed"].to_numpy() ``` ## Preprocess the data exactly as the GLM does The point of this example is to compare a univariate analysis with a multivariate one, which only works if everything *else* is held equal. So the decoder gets the same preprocessing the GLM applies. **Spatial smoothing.** `FirstLevelModel(smoothing_fwhm=0.3)` smooths each run with `smooth_volume` before fitting. We apply the same call with the same value, rather than leaving the searchlight to work on unsmoothed data while the GLM enjoys the noise reduction. **Drift and confound removal.** Power Doppler carries slow drift and nuisance signal that have nothing to do with locomotion. The GLM handles them with cosine drift and CompCor regressors in its design; a decoder has no design matrix, so we remove the same regressors from the data up front with `clean`, reusing the design matrix's own nuisance columns. The target is cleaned with the same regressors as the data, so both sides have the same nuisance structure removed. This mirrors what the GLM does implicitly when it fits the speed regressor and the nuisance regressors jointly. ```python smoothing_fwhm = 0.3 confounds_np = design_matrix.drop(columns="speed").to_numpy() confounds = xr.DataArray( confounds_np, dims=["time", "confound"], coords={"time": data.time} ) cleaned = cf.signal.clean( cf.spatial.smooth_volume(data, smoothing_fwhm), standardize_method="zscore", confounds=confounds, ) target = cf.signal.clean( xr.DataArray( speed_regressor, dims=["time"], coords={"time": data.time}, name="speed" ), standardize_method="zscore", confounds=confounds, ) ``` ## Run the searchlight Running searchlight through the entire field of view is costly because this means fitting a few tens of thousands of models. Here we will create a mask to only run the searchlight algorithm in the top half of the field of view. A real region-of-interest mask, such as an intensity-thresholded brain mask, or an anatomical brain region mask, would be of good use here just the same. Two details matter for fUSI data: - `radius` is in the units of the data's spatial coordinates, not in voxel indices. Each neighborhood is the set of voxels within `radius` millimeters of the center. - Consecutive fUSI volumes are strongly autocorrelated. Cross-validating with shuffled folds would put near-duplicate volumes in both the training and test sets and inflate the scores. `SearchLight` therefore builds contiguous temporal folds by default. Each fold also needs to be long enough to contain both quiet and active periods, since the animal moves in bursts, which is why we keep the fold count low. The estimator is a `RidgeCV`: ridge regression that selects its own penalty from a grid. Neighboring fUSI voxels are highly correlated, and the right amount of regularization varies across the plane. By default `RidgeCV` picks the penalty by leave-one-out generalized cross-validation, which does put temporally adjacent volumes in its train and test sets, unlike the contiguous folds we use for the outer searchlight cross-validation. That is not a problem: the penalty search runs entirely inside each outer training fold and never sees the outer test fold, so it only affects which penalty is chosen. ```python estimator = RidgeCV(alphas=np.logspace(0, 10, 5)) mask = xr.ones_like(cleaned.isel(time=0), dtype=bool).where(cleaned.y < 10, False) searchlight = cf.decoding.SearchLight( estimator=estimator, mask=mask, radius=0.6, cv=3, n_jobs=-1 ) searchlight.fit(cleaned, target.values) searchlight.scores_ ``` xarray.DataArray - k: 1 - j: 112 - i: 128 - [ ] \-0.04186 -0.04583 -0.05186 -0.0598 -0.06268 ... nan nan nan nan nan ``` array([[[-0.04185839, -0.04582578, -0.05186282, ..., -0.15304316, -0.18103184, -0.16589569], [-0.04157417, -0.0436027 , -0.04272867, ..., -0.14518918, -0.49228095, -0.58586544], [-0.03736185, -0.03926947, -0.03986719, ..., -0.25195268, -0.31287728, -0.43389237], ..., [ nan, nan, nan, ..., nan, nan, nan], [ nan, nan, nan, ..., nan, nan, nan], [ nan, nan, nan, ..., nan, nan, nan]]], shape=(1, 112, 128)) ``` - [x] Coordinates: (6) - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 107 108 109 110 111 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]) ``` - i (i) int64 0 1 2 3 4 5 ... 123 124 125 126 127 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]) ``` - z (k, j, i) float64 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 ☐ ☐ **units :** mm ``` [14336 values with dtype=float64] ``` - y (k, j, i) float64 5.0 5.0 5.0 ... 15.94 15.94 15.94 ☐ ☐ **units :** mm ``` [14336 values with dtype=float64] ``` - x (k, j, i) float64 \-6.985 -6.875 ... 6.875 6.985 ☐ ☐ **units :** mm ``` [14336 values with dtype=float64] ``` - [ ] Indexes: (4) - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 102, 103, 104, 105, 106, 107, 108, 109, 110, 111], dtype='int64', name='j', length=112)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], dtype='int64', name='i', length=128)) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (23) **sform\_code :** 2 **qform\_code :** 1 **manufacturer :** Iconeus **manufacturers\_model\_name :** Iconeus One **probe\_type :** linear **probe\_model :** IcoPrime light **probe\_number\_of\_elements :** 128 **probe\_pitch :** 0.11 **probe\_radius\_of\_curvature :** 0 **ProbeElevationAperture :** 1.5 **ProbeElevationFocus :** 8 **plane\_wave\_angles :** \[-7.0, -5.0, -3.0, -1.0, 1.0, 3.0, 5.0, 7.0\] **IsHadamard :** True **UltrafastSamplingFrequency :** 400.0 **clutter\_filter\_window\_duration :** 0.2 **clutter\_filters :** \['svd:remove\_first\_60\_components'\] **power\_doppler\_integration\_duration :** 0.2 **probe\_center\_frequency :** 15.625 **pulse\_repetition\_frequency :** 3200.0 **transmit\_voltage :** 15 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} **units :** R² **long\_name :** Searchlight CV score ## Compare against a GLM We now fit a GLM with the design matrix built earlier, so it tests the same HRF-convolved speed regressor against the same drift model, on data smoothed with the same kernel. ```python glm = cf.glm.FirstLevelModel(smoothing_fwhm=smoothing_fwhm, mask=mask) glm.fit(data, design_matrices=design_matrix) z_scores = glm.compute_contrast("speed") ``` ## Compare the two maps The searchlight reports a cross-validated coefficient of determination, so values at or below zero mean the local neighborhood predicts the speed regressor no better than the fold mean; we clip the color scale at zero. The GLM reports a z-score for the speed contrast. Both maps cover the whole plane. ```python is_dark_theme = sum(mpl.colors.to_rgb(bg_color)) / 3 < 0.5 searchlight_cmap = "inferno" if is_dark_theme else "inferno_r" glm_cmap = "berlin" if is_dark_theme else "coolwarm" background_data = data.mean("time").fusi.scale.db() fig, axes = plt.subplots(1, 2, figsize=(10, 3), constrained_layout=True) searchlight.scores_.fusi.plot.stat_map( bg_volume=background_data, axes=axes[0], cmap=searchlight_cmap, vmin=0, auto_range=False, cbar_label="Cross-validated $R^2$", show_titles=False, bg_color=bg_color, ) axes[0].set_title("Searchlight decoding of speed") z_scores.where(mask).fusi.plot.stat_map( bg_volume=background_data, axes=axes[1], cmap=glm_cmap, cbar_label="z-score", show_titles=False, bg_color=bg_color, ) axes[1].set_title("GLM, same regressor") for ax in axes: ax.set_aspect("equal") ``` We quantify the spatial agreement between the two maps by the Dice overlap of their top 5 percent of voxels. ```python top_scores = searchlight.scores_.squeeze(drop=True) top_z = z_scores.squeeze(drop=True) selected_scores = top_scores >= top_scores.quantile(0.95) selected_z = top_z >= top_z.quantile(0.95) dice = float( 2 * (selected_scores & selected_z).sum() / (selected_scores.sum() + selected_z.sum()) ) print(f"Top-5% overlap, Dice = {dice:.3f}") ``` ```text Top-5% overlap, Dice = 0.462 ``` The overlap is partial: the two maps answer related but different questions. The GLM is univariate and asks, at each voxel, whether that voxel's signal tracks the speed regressor. The searchlight is multivariate and cross-validated, and asks whether the local pattern around each voxel predicts the regressor in held-out blocks of time. --- **Total running time:** 66.7 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/decoding/searchlight_speed/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. # 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 correlation[^/dev/examples/_built/decomposition/fastica_single_recording/#1]. 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]()[^/dev/examples/_built/decomposition/fastica_single_recording/#collection] as in the [PCA example](). See the [Datasets]() user guide for more details on how to download this dataset using ConfUSIus. ```python 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 # 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 = cf.datasets.fetch_nunez_elizalde_2022( subjects="CR022", sessions="20201011", tasks="spontaneous", acqs="slice03", ) ``` ``` If you use this dataset in your work, please cite the following source: Nunez-Elizalde, A. O., Krumin, M., Reddy, C. B., Montaldo, G., Urban, A., Harris, K. D., & Carandini, M. (2022). Neural correlates of blood flow measured by ultrasound. Neuron, 110(10), 1631–1640.e4. https://doi.org/10.1016/j.neuron.2022.02.012 ``` ```python 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 ``` xarray.DataArray 'sub-CR022\_ses-20201011\_task-spontaneous\_acq-slice03\_pwd' - time: 751 - k: 1 - j: 114 - i: 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) ``` - [x] Coordinates: (7) - time (time) float64 10.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,)) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 109 110 111 112 113 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113]) ``` - i (i) int64 0 1 2 3 4 5 6 ... 74 75 76 77 78 79 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]) ``` - z (k, j, i) float64 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0 ☐ ☐ **units :** mm ``` array([[[1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], ..., [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.]]], shape=(1, 114, 80)) ``` - y (k, j, i) float64 2.73 2.73 2.73 ... 8.19 8.19 8.19 ☐ ☐ **units :** mm ``` array([[[2.73007989, 2.73007989, 2.73007989, ..., 2.73007989, 2.73007989, 2.73007989], [2.77839989, 2.77839989, 2.77839989, ..., 2.77839989, 2.77839989, 2.77839989], [2.82671989, 2.82671989, 2.82671989, ..., 2.82671989, 2.82671989, 2.82671989], ..., [8.09359979, 8.09359979, 8.09359979, ..., 8.09359979, 8.09359979, 8.09359979], [8.14191979, 8.14191979, 8.14191979, ..., 8.14191979, 8.14191979, 8.14191979], [8.19023979, 8.19023979, 8.19023979, ..., 8.19023979, 8.19023979, 8.19023979]]], shape=(1, 114, 80)) ``` - x (k, j, i) float64 \-3.95 -3.85 -3.75 ... 3.85 3.95 ☐ ☐ **units :** mm ``` array([[[-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], ..., [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007]]], shape=(1, 114, 80)) ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 10.608, 10.908, 11.208, 11.508000000000001, 11.808, 12.108, 12.408, 12.708, 13.008000000000001, 13.308, ... 232.995, 233.29500000000002, 233.595, 233.895, 234.195, 234.495, 234.79500000000002, 235.095, 235.395, 235.695], dtype='float64', name='time', length=751)) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 104, 105, 106, 107, 108, 109, 110, 111, 112, 113], dtype='int64', name='j', length=114)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79], dtype='int64', name='i')) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (25) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_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 transform correction with `register_volumewise`. This is the same preprocessing step used in the [PCA example](), and it helps avoid components dominated by motion artefacts. ```python data = cf.registration.register_volumewise(data, learning_rate=1e-2) ``` ## 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](). ```python data_std = cf.signal.standardize(data) ``` With `mode="temporal"`, FastICA operates on the same `(time, space)` 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). ```python ica_t = cf.decomposition.FastICA( n_components=10, mode="temporal", random_state=42, fun="cube", max_iter=500, ) signals_t = ica_t.fit_transform(data_std) signals_t ``` xarray.DataArray - 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) ``` - [x] Coordinates: (2) - time (time) float64 10.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) int64 0 1 2 3 4 5 6 7 8 9 ☐ ☐ ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) ``` - [x] Attributes: (1) **long\_name :** FastICA signals Plotting the spatial mixing weights alongside the independent time courses gives a first sense of what each component captures. ```python 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_stat_map( component_map, axes=fig.add_subplot(gs[i, 0]), slice_mode="component", cmap="coolwarm", 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 `(space, 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. ```python ica_s = cf.decomposition.FastICA( n_components=10, mode="spatial", random_state=42, fun="cube", max_iter=500 ) signals_s = ica_s.fit_transform(data_std) signals_s ``` xarray.DataArray - 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) ``` - [x] Coordinates: (2) - time (time) float64 10.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) int64 0 1 2 3 4 5 6 7 8 9 ☐ ☐ ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) ``` - [x] Attributes: (1) **long\_name :** FastICA signals ### Spatial maps and time courses `maps_` is a `(component, j, i)` 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. ```python 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_stat_map( component_map, axes=fig.add_subplot(gs[i, 0]), slice_mode="component", cmap="coolwarm", 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:** 182.8 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/decomposition/fastica_single_recording/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. [^/dev/examples/_built/decomposition/fastica_single_recording/#1]: Hyvärinen, A., and Oja, E. (2000). "Independent component analysis: algorithms and applications". *Neural Networks*, 13(4-5), 411-430. # NMF on a single fUSI recording This example shows how to use non-negative matrix factorization (NMF) to decompose a fUSI recording into non-negative spatial maps and their associated non-negative time courses. It complements the [PCA]() and [FastICA]() examples in the same gallery. NMF is unique among the decomposers in ConfUSIus because it requires strictly non-negative inputs[^/dev/examples/_built/decomposition/nmf_single_recording/#1]. Power Doppler fUSI signals are non-negative by construction, so they pass the constraint directly. However, raw power is dominated by each voxel's baseline intensity, which can make bright vessels dominate the factorization. A practical workaround is to center and scale each voxel across time, then split the standardized signal into separate positive and negative channels, preserving NMF's non-negativity constraint. NMF can then discover additive components in above-baseline and below-baseline fluctuations separately. ## Load a fUSI recording We use the same spontaneous activity recording from the [Nunez-Elizalde 2022 dataset]()[^/dev/examples/_built/decomposition/nmf_single_recording/#collection] as in the [PCA]() and [FastICA]() examples. See the [Datasets]() user guide for more details on how to download this dataset using ConfUSIus. ```python from pathlib import Path import matplotlib as mpl import matplotlib.pyplot as plt import xarray as xr import confusius as cf # 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 = cf.datasets.fetch_nunez_elizalde_2022( subjects="CR022", sessions="20201011", tasks="spontaneous", acqs="slice03", ) ``` ``` If you use this dataset in your work, please cite the following source: Nunez-Elizalde, A. O., Krumin, M., Reddy, C. B., Montaldo, G., Urban, A., Harris, K. D., & Carandini, M. (2022). Neural correlates of blood flow measured by ultrasound. Neuron, 110(10), 1631–1640.e4. https://doi.org/10.1016/j.neuron.2022.02.012 ``` ```python 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 ``` xarray.DataArray 'sub-CR022\_ses-20201011\_task-spontaneous\_acq-slice03\_pwd' - time: 751 - k: 1 - j: 114 - i: 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) ``` - [x] Coordinates: (7) - time (time) float64 10.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,)) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 109 110 111 112 113 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113]) ``` - i (i) int64 0 1 2 3 4 5 6 ... 74 75 76 77 78 79 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]) ``` - z (k, j, i) float64 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0 ☐ ☐ **units :** mm ``` array([[[1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], ..., [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.]]], shape=(1, 114, 80)) ``` - y (k, j, i) float64 2.73 2.73 2.73 ... 8.19 8.19 8.19 ☐ ☐ **units :** mm ``` array([[[2.73007989, 2.73007989, 2.73007989, ..., 2.73007989, 2.73007989, 2.73007989], [2.77839989, 2.77839989, 2.77839989, ..., 2.77839989, 2.77839989, 2.77839989], [2.82671989, 2.82671989, 2.82671989, ..., 2.82671989, 2.82671989, 2.82671989], ..., [8.09359979, 8.09359979, 8.09359979, ..., 8.09359979, 8.09359979, 8.09359979], [8.14191979, 8.14191979, 8.14191979, ..., 8.14191979, 8.14191979, 8.14191979], [8.19023979, 8.19023979, 8.19023979, ..., 8.19023979, 8.19023979, 8.19023979]]], shape=(1, 114, 80)) ``` - x (k, j, i) float64 \-3.95 -3.85 -3.75 ... 3.85 3.95 ☐ ☐ **units :** mm ``` array([[[-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], ..., [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007]]], shape=(1, 114, 80)) ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 10.608, 10.908, 11.208, 11.508000000000001, 11.808, 12.108, 12.408, 12.708, 13.008000000000001, 13.308, ... 232.995, 233.29500000000002, 233.595, 233.895, 234.195, 234.495, 234.79500000000002, 235.095, 235.395, 235.695], dtype='float64', name='time', length=751)) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 104, 105, 106, 107, 108, 109, 110, 111, 112, 113], dtype='int64', name='j', length=114)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79], dtype='int64', name='i')) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (25) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} # Correct for brain motion As in the [PCA]() and [FastICA]() examples, we first perform a rigid transformation correction with `register_volumewise` to mitigate brain motion. ```python data = cf.registration.register_volumewise(data, learning_rate=1e-2) ``` ## Standardize for NMF NMF requires non-negative inputs, but raw Power Doppler is dominated by each voxel's baseline intensity rather than the temporal fluctuations we want to group. We therefore: 1. Z-score each voxel's time series with `standardize` to remove its mean and put voxels on a comparable scale. 2. Split the standardized signal into separate positive and negative parts. This keeps the sign information— above-baseline versus below-baseline fluctuations—while still presenting a non-negative matrix to NMF. ```python z = cf.signal.standardize(data) data_nmf = xr.concat( [z.clip(min=0), (-z).clip(min=0)], dim=xr.IndexVariable("sign", ["pos", "neg"]), ) ``` ## Fit temporal NMF `NMF` wraps the familiar scikit-learn `NMF` estimator while preserving VoxelData array metadata and coordinates. With `mode="temporal"` (the default), it fits on `(time, space)` and returns: - `maps_`: non-negative spatial maps. Because we split the input into positive and negative channels, the maps here have shape `(component, sign, k, j, i)`. - `fit_transform`: non-negative time courses of shape `(time, component)`. ```python nmf_t = cf.decomposition.NMF(n_components=10, random_state=0, max_iter=1000) signals = nmf_t.fit_transform(data_nmf) signals ``` xarray.DataArray - time: 751 - component: 10 - [ ] 0.1626 0.013 0.0 0.1839 0.0006832 ... 0.04623 0.0 0.3545 0.0 0.06981 ``` array([[0.16255698, 0.01299934, 0. , ..., 0.17457205, 0.55600464, 0. ], [0.08055934, 0. , 0. , ..., 0.05789696, 0.51770186, 0. ], [0.02368935, 0. , 0.023984 , ..., 0.0156738 , 0.35746408, 0. ], ..., [0.09453033, 0.01987509, 0. , ..., 0.0882846 , 0.26667225, 0. ], [0.20231232, 0.01959528, 0. , ..., 0.10854763, 0.13271652, 0. ], [0.18283577, 0.01475037, 0. , ..., 0.35445145, 0. , 0.06981277]], shape=(751, 10), dtype=float32) ``` - [x] Coordinates: (2) - time (time) float64 10.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) int64 0 1 2 3 4 5 6 7 8 9 ☐ ☐ ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) ``` - [x] Attributes: (1) **long\_name :** NMF signals ## Reconstruction error `reconstruction_err_` is the Frobenius norm of `X - WH`, where `W` are the spatial maps and `H` the time courses. It gives a sense of how well the chosen number of components explains the standardized data. A quantitative model-selection procedure is out of scope here, but the trace is useful when sweeping `n_components` and looking for diminishing returns. ```python print(f"reconstruction_err_: {nmf_t.reconstruction_err_:.3f}") print(f"n_iter_: {nmf_t.n_iter_}") ``` ```text reconstruction_err_: 1911.557 n_iter_: 663 ``` ## Spatial maps and time courses Looking at the spatial maps and the associated time courses side by side is a useful first sanity check. Here each component has two map panels: one for above-baseline fluctuations (`pos`) and one for below-baseline fluctuations (`neg`). Localized, anatomically plausible structure paired with a clear transient in the time course tends to reflect a coherent spatiotemporal pattern, while diffuse maps paired with noisy or drift-like fluctuations often indicate residual motion or physiological artefacts. ```python n_show = 10 fig = plt.figure(figsize=(11.5, 12.0), constrained_layout=True) fig.patch.set_facecolor(bg_color) gs = fig.add_gridspec(n_show, 2, width_ratios=[1.4, 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 = nmf_t.maps_.isel(component=comp) vmax = float(component_map.max()) map_gs = gs[i, 0].subgridspec(1, 2, wspace=0.02) for j, sign in enumerate(["pos", "neg"]): cf.plotting.plot_stat_map( component_map.sel(sign=sign), axes=fig.add_subplot(map_gs[0, j]), slice_mode="z", vmax=vmax, show_axes=False, show_colorbar=False, show_titles=False, bg_color=bg_color, ) signals.sel(component=comp).plot(ax=axes_tc[i], lw=1.1) axes_tc[i].set_title(f"Component {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 NMF: positive/negative maps and time courses (first 10 components)", fontsize=21, ) ``` ## Spatial NMF `NMF` also accepts `mode="spatial"`, which transposes the data to `(space, time)` before fitting. The output convention is identical to temporal mode — `maps_` still holds the non-negative spatial maps (here with `pos`/`neg` channels) and `fit_transform` returns their non-negative time courses — so the choice between the two modes mirrors the temporal/spatial choice offered by [PCA]() and [FastICA](). ```python nmf_s = cf.decomposition.NMF( n_components=10, mode="spatial", random_state=0, max_iter=1000 ) signals_s = nmf_s.fit_transform(data_nmf) signals_s ``` xarray.DataArray - time: 751 - component: 10 - [ ] \-48.81 -107.2 23.77 475.0 274.9 ... 34.6 -75.95 839.4 361.2 -68.61 ``` array([[ -48.810776 , -107.21934 , 23.766613 , ..., 1357.0479 , 305.8229 , -81.77524 ], [ -46.59765 , -101.35976 , 89.079865 , ..., 1052.711 , 200.53772 , -314.652 ], [ -48.18569 , -88.049095 , 50.88957 , ..., 991.1305 , 59.319008 , -347.92282 ], ..., [ -52.89242 , -145.73181 , 5.8395586, ..., 1228.5333 , 263.19995 , -264.38028 ], [ -48.556282 , -139.85196 , 34.902374 , ..., 1233.1041 , 494.40372 , -215.82959 ], [ -40.809776 , -106.204285 , -33.665638 , ..., 839.35266 , 361.19443 , -68.61046 ]], shape=(751, 10), dtype=float32) ``` - [x] Coordinates: (2) - time (time) float64 10.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) int64 0 1 2 3 4 5 6 7 8 9 ☐ ☐ ``` array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) ``` - [x] Attributes: (1) **long\_name :** NMF signals ### Spatial maps and time courses As in temporal mode, we inspect the positive and negative spatial maps and their corresponding time courses side by side. ```python n_show = 10 fig = plt.figure(figsize=(11.5, 12.0), constrained_layout=True) fig.patch.set_facecolor(bg_color) gs = fig.add_gridspec(n_show, 2, width_ratios=[1.4, 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 = nmf_s.maps_.isel(component=comp) vmax = float(component_map.max()) map_gs = gs[i, 0].subgridspec(1, 2, wspace=0.02) for j, sign in enumerate(["pos", "neg"]): cf.plotting.plot_stat_map( component_map.sel(sign=sign), axes=fig.add_subplot(map_gs[0, j]), slice_mode="z", 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"Component {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 NMF: positive/negative maps and time courses (first 10 components)", fontsize=21, ) ``` --- **Total running time:** 190.6 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/decomposition/nmf_single_recording/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. [^/dev/examples/_built/decomposition/nmf_single_recording/#1]: Lee, D. D., and Seung, H. S. (1999). "Learning the parts of objects by non-negative matrix factorization". *Nature*, 401(6755), 788-791. # PCA on a single fUSI recording This example shows how to use principal component analysis (PCA) to decompose a fUSI recording into principal axes of variance. ConfUSIus supports two PCA orientations: - `mode="temporal"` (default): center across time for each voxel, then find orthogonal voxel-space directions that maximize variance of projected time samples. - `mode="spatial"`: center across voxels for each time point (after transposition), then find orthogonal spatial maps that maximize variance across voxels. Both return time courses with shape `(time, component)` via `transform`, but the statistical objective is applied along different dimensions. PCA finds an ordered orthogonal set of axes in feature space[^/dev/examples/_built/decomposition/pca_single_recording/#1] such that projection onto the first *k* axes captures the maximum possible variance among all *k*-dimensional linear projections. Equivalently, these axes are the dominant eigenvectors of the covariance matrix, or of the correlation matrix if variables are properly standardized. If you are interested in linear covariance structure in your fUSI data, PCA is a useful place to start. ## Load a fUSI recording To demonstrate the use of PCA on fUSI data, we use a spontaneous activity recording from the [Nunez-Elizalde 2022 dataset]()[^/dev/examples/_built/decomposition/pca_single_recording/#collection]. See the [Datasets]() user guide for more details on how to download this dataset using ConfUSIus. ```python 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 # 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 = cf.datasets.fetch_nunez_elizalde_2022( subjects="CR022", sessions="20201011", tasks="spontaneous", acqs="slice03", ) ``` ``` If you use this dataset in your work, please cite the following source: Nunez-Elizalde, A. O., Krumin, M., Reddy, C. B., Montaldo, G., Urban, A., Harris, K. D., & Carandini, M. (2022). Neural correlates of blood flow measured by ultrasound. Neuron, 110(10), 1631–1640.e4. https://doi.org/10.1016/j.neuron.2022.02.012 ``` ```python 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 ``` xarray.DataArray 'sub-CR022\_ses-20201011\_task-spontaneous\_acq-slice03\_pwd' - time: 751 - k: 1 - j: 114 - i: 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) ``` - [x] Coordinates: (7) - time (time) float64 10.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,)) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 109 110 111 112 113 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113]) ``` - i (i) int64 0 1 2 3 4 5 6 ... 74 75 76 77 78 79 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]) ``` - z (k, j, i) float64 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0 ☐ ☐ **units :** mm ``` array([[[1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], ..., [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.]]], shape=(1, 114, 80)) ``` - y (k, j, i) float64 2.73 2.73 2.73 ... 8.19 8.19 8.19 ☐ ☐ **units :** mm ``` array([[[2.73007989, 2.73007989, 2.73007989, ..., 2.73007989, 2.73007989, 2.73007989], [2.77839989, 2.77839989, 2.77839989, ..., 2.77839989, 2.77839989, 2.77839989], [2.82671989, 2.82671989, 2.82671989, ..., 2.82671989, 2.82671989, 2.82671989], ..., [8.09359979, 8.09359979, 8.09359979, ..., 8.09359979, 8.09359979, 8.09359979], [8.14191979, 8.14191979, 8.14191979, ..., 8.14191979, 8.14191979, 8.14191979], [8.19023979, 8.19023979, 8.19023979, ..., 8.19023979, 8.19023979, 8.19023979]]], shape=(1, 114, 80)) ``` - x (k, j, i) float64 \-3.95 -3.85 -3.75 ... 3.85 3.95 ☐ ☐ **units :** mm ``` array([[[-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], ..., [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007]]], shape=(1, 114, 80)) ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 10.608, 10.908, 11.208, 11.508000000000001, 11.808, 12.108, 12.408, 12.708, 13.008000000000001, 13.308, ... 232.995, 233.29500000000002, 233.595, 233.895, 234.195, 234.495, 234.79500000000002, 235.095, 235.395, 235.695], dtype='float64', name='time', length=751)) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 104, 105, 106, 107, 108, 109, 110, 111, 112, 113], dtype='int64', name='j', length=114)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79], dtype='int64', name='i')) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (25) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_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 transform correction with `register_volumewise`. This is a common preprocessing step for fUSI data, and it can help avoid spurious components driven by brain motion. ```python # The `learning_rate` controls the step size of the optimization. A value of 1e-2 is # a common default that balances convergence speed and stability for typical fUSI data. data = cf.registration.register_volumewise(data, learning_rate=1e-2) ``` ## Temporal PCA (`mode="temporal"`) Before performing PCA, we standardize the recording by centering and scaling each voxel's time series to zero mean and unit variance. The `standardize` function can be used for this purpose. This ensures that PCA captures patterns of correlation rather than patterns of covariance; otherwise, PCA may be dominated by high-variance voxels, such as those near large blood vessels, which may not be of primary interest. ```python data_std = cf.signal.standardize(data) ``` In ConfUSIus, the `PCA` model wraps the familiar scikit-learn `PCA` model while preserving the VoxelData array metadata and coordinates. `PCA` expects the same arguments as the scikit-learn model, such as `n_components` for the number of principal components to compute, and `random_state` for reproducibility (see the API documentation for more details). By default, PCA uses `mode="temporal"` (fit on `(time, space)`). A `mode="spatial"` option is also available, analogous to spatial ICA. Here, we fit a PCA model with all available components. ```python pca_t = cf.decomposition.PCA(random_state=0, mode="temporal") signals_t = pca_t.fit_transform(data_std) signals_t ``` xarray.DataArray - time: 751 - component: 751 - [ ] 73.07 -8.361 -4.076 11.58 11.83 ... 0.01418 -0.8985 -0.297 1.711e-05 ``` array([[ 7.3066635e+01, -8.3605938e+00, -4.0763493e+00, ..., 1.0819744e+00, 1.6663108e+00, 4.4641160e-06], [ 5.2037067e+01, 2.2309378e+01, -1.4225268e+01, ..., 8.1317788e-01, -4.4685724e-01, -5.8623887e-06], [ 4.5891502e+01, 2.2932816e+01, -2.1960232e+01, ..., -7.2219506e-02, -7.4409738e-02, 8.7668259e-06], ..., [ 7.4077156e+01, -5.9905930e+00, 4.1795883e+00, ..., 1.2390769e+00, 8.1436560e-02, 5.4601655e-06], [ 7.5606865e+01, -6.2057152e+00, 1.7687122e+01, ..., -1.1357114e+00, -4.9661902e-01, -5.1508582e-06], [ 5.1808727e+01, -1.8981846e+01, 1.5910517e+01, ..., -8.9845270e-01, -2.9696307e-01, 1.7107752e-05]], shape=(751, 751), dtype=float32) ``` - [x] Coordinates: (2) - time (time) float64 10.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) int64 0 1 2 3 4 5 ... 746 747 748 749 750 ☐ ☐ ``` array([ 0, 1, 2, ..., 748, 749, 750], shape=(751,)) ``` - [x] Attributes: (1) **long\_name :** PCA signals ## Explained variance `explained_variance_ratio_` gives the fraction of total variance captured along each selected principal axis. For centered data with shape `(time, space)`, the rank is at most `min(space, time - 1)`, so a final near-zero entry appears in the spectrum; we omit it here for clarity. The scree plot on the left highlights the rapid decay of successive components, while the cumulative curve on the right shows how quickly the total explained variance saturates. These plots can help guide the choice of how many components to retain for further analysis, for example by selecting the "elbow" of the screen plot or a threshold for cumulative variance. ```python variance_ratio = pca_t.explained_variance_ratio_.isel(component=slice(None, -1)) component_ids = variance_ratio.component.values + 1 cumulative_variance = np.cumsum(variance_ratio.values) * 100 fig, axes = plt.subplots(1, 2, figsize=(10, 3.4), constrained_layout=True) axes[0].plot( component_ids, variance_ratio.values * 100, marker="o", ms=4, color="tab:blue" ) axes[0].set_yscale("log") axes[0].set_xlabel("Principal component") axes[0].set_ylabel("Explained variance (%)") axes[0].set_title("Scree plot") axes[1].plot(component_ids, cumulative_variance, marker="o", ms=4, color="tab:blue") axes[1].set_xlabel("Principal component") axes[1].set_ylabel("Cumulative explained variance (%)") _ = axes[1].set_title("Cumulative variance") ``` ## Temporal PCA maps and corresponding time courses `maps_` stores principal axes in voxel space as a `(component, k, j, i)` DataArray. `transform` returned the associated temporal scores in `signals_t`, a `(time, component)` DataArray. Looking at both together is more informative than either alone. The maps show the dominant covariance structure in space, while the scores show how strongly each pattern is expressed over time. ```python n_show = 6 fig = plt.figure(figsize=(10.5, 8.5), 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)): var = float(pca_t.explained_variance_ratio_.sel(component=comp)) * 100 component_map = pca_t.maps_.isel(component=[comp]) cf.plotting.plot_stat_map( component_map, axes=fig.add_subplot(gs[i, 0]), slice_mode="component", 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"PC {comp + 1} ({var:.1f} %)") axes_tc[i].set_xlabel("") axes_tc[i].set_ylabel("") for ax in axes_tc[:-1]: ax.tick_params(labelbottom=False) axes_tc[-1].set_xlabel("Time (s)") _ = fig.suptitle("Temporal PCA maps and time courses (first 6 components)", fontsize=21) ``` ## Spatial PCA (`mode="spatial"`) Spatial PCA transposes the matrix to `(space, time)` before decomposition. In this orientation, variance maximization is performed across voxels, yielding orthogonal spatial maps as principal components. ```python pca_s = cf.decomposition.PCA(random_state=0, mode="spatial") signals_s = pca_s.fit_transform(data_std) signals_s ``` xarray.DataArray - time: 751 - component: 751 - [ ] \-3.187e+04 -1.548e+03 883.6 -3.861e+03 ... 1.22 39.7 15.21 0.0007041 ``` array([[-3.1866018e+04, -1.5477708e+03, 8.8362970e+02, ..., -4.1422390e+01, -7.7635834e+01, -1.7511325e-04], [-7.8375811e+03, -7.9947441e+03, 1.9465931e+03, ..., -3.7357880e+01, 1.9214939e+01, -5.8000605e-04], [-7.6417998e+03, -1.1311187e+04, 3.1012839e+03, ..., 7.6817799e+00, 5.1758451e+00, -2.5194747e-04], ..., [-3.3448461e+04, 1.1590634e+03, 5.9401001e+03, ..., -5.0094185e+01, -3.7692327e+00, -5.7497656e-04], [-3.1455812e+04, 6.7948394e+03, 4.7766562e+03, ..., 4.7841347e+01, 2.4876919e+01, -2.5275841e-04], [-2.8597014e+04, 7.5429858e+03, 2.4295175e+02, ..., 3.9704731e+01, 1.5206790e+01, 7.0413668e-04]], shape=(751, 751), dtype=float32) ``` - [x] Coordinates: (2) - time (time) float64 10.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) int64 0 1 2 3 4 5 ... 746 747 748 749 750 ☐ ☐ ``` array([ 0, 1, 2, ..., 748, 749, 750], shape=(751,)) ``` - [x] Attributes: (1) **long\_name :** PCA signals As in temporal mode, we inspect maps and corresponding time courses jointly. ```python n_show = 6 fig = plt.figure(figsize=(10.5, 8.5), 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)): var = float(pca_s.explained_variance_ratio_.sel(component=comp)) * 100 component_map = pca_s.maps_.isel(component=[comp]) vmax = float(np.abs(component_map).max()) cf.plotting.plot_stat_map( component_map, axes=fig.add_subplot(gs[i, 0]), slice_mode="component", cmap="coolwarm", 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"PC {comp + 1} ({var:.1f} %)") axes_tc[i].set_xlabel("") axes_tc[i].set_ylabel("") for ax in axes_tc[:-1]: ax.tick_params(labelbottom=False) axes_tc[-1].set_xlabel("Time (s)") _ = fig.suptitle("Spatial PCA maps and time courses (first 6 components)", fontsize=21) ``` --- **Total running time:** 180.7 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/decomposition/pca_single_recording/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. [^/dev/examples/_built/decomposition/pca_single_recording/#1]: We usually consider voxels as features and time points as samples, but PCA can be applied in either orientation. # First-level GLM analysis of fUSI data The General Linear Model (GLM) is the workhorse of task-based neuroimaging analysis. It treats every voxel independently and asks a simple question: how much of this voxel's time course can be explained by the experimental paradigm, once we account for nuisance signals such as slow drifts and physiological noise? Fitting that model voxel-by-voxel turns a fUSI recording into a statistical map that highlights where the brain responded to the stimulus. In this example we run a complete first-level (single-subject) GLM on stimulus-evoked fUSI data from the [Khallaf et al. 2026 dataset]()[^/dev/examples/_built/glm/first_level/#collection]—functional ultrasound imaging of a naked mole-rat exposed to repeated olfactory stimulation. The notebook will go through the following steps: 1. **Fetch and load** the five fUSI recordings. 2. **Register** the recording to a reference template aligned with an anatomical atlas, so we can define masks and draw region boundaries in a common space. 3. **Choose a fUSI-specific HRF** and **extract CompCor noise regressors** from a non-task region. 4. **Fit** a `FirstLevelModel` across all runs and **compute a contrast** for the stimulation condition. 5. **Threshold** the resulting map for statistical significance and visualize it. > [!WARNING] > > **Download size** > > Running this notebook fetches the five recordings for subject `5622`, session `IPM` (about 200 MB each, \~1 GB in total) into the ConfUSIus dataset cache. ## Fetch the olfactory-stimulation recordings Using `fetch_khallaf_2026`, we take subject `5622`, session `IPM`, and the `resampled` reconstruction (the runs already aligned to a common within-session space), which yields five task runs. ```python import warnings from functools import partial from pathlib import Path import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np import pandas as pd import scipy.stats as sps import xarray as xr import confusius as cf # Adapt background color to the current Matplotlib style. bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) is_dark_theme = sum(mpl.colors.to_rgb(bg_color)) / 3 < 0.5 # Keep notebook output compact for large DataArray displays. The coordinates section is # left expanded on purpose; `display_expand_data` alone does not cover the attributes. xr.set_options(display_expand_data=False, display_expand_attrs=False) template_pepe_mariani = cf.datasets.fetch_template_pepe_mariani_2026() bids_root = cf.datasets.fetch_khallaf_2026( datasets="rawdata", subjects="5622", sessions="IPM", reconstruction="resampled", ) pwd_path_pattern = ( Path(bids_root) / "sub-5622" / "ses-IPM" / "fusi" / "sub-5622_ses-IPM_task-olfactory_rec-resampled_run-*_space-5622run1_pwd.nii" ) pwd_paths = sorted(Path(bids_root).rglob(str(pwd_path_pattern.relative_to(bids_root)))) ``` ``` If you use this template in your work, please cite the following source: Pepe, C., Mariani, J.-C., Urosevic, M., Gini, S., Stuefer, A., Ricci, F., Galbusera, A., Iurilli, G., & Gozzi, A. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (fUSI). bioRxiv. https://doi.org/10.64898/2026.02.05.704055 ``` ``` If you use this dataset in your work, please cite the following source: Khallaf, M. A., Hart, D. W., Luo, W., Murad, F., Cybis Pereira, F., Mendez-Aranda, D., Hagenah, N., Rossi, A., Bégay, V., Okrouhlík, J., Krautwurst, D., Ngalameno, M. K., Ganswindt, A., Barker, A. J., Šumbera, R., Knaden, M., Pezet, S., Woehler, A., Hansson, B. S., Bennett, N. C., & Lewin, G. R. (2026). A queen odour mediates reproductive suppression in a eusocial mammal. Nature. https://doi.org/10.1038/s41586-026-10772-5 ``` The experimental paradigm is a **block design**: the stimulus (odour puff) is presented in five 30 s blocks (`trial_type` `"active"`, onsets at 30, 90, 150, 210 and 270 s) interleaved with 30 s of baseline. The single `events.tsv` table in the root of the dataset applies to every run, capturing the same `"active"` regressor that we are interested in. ```python events_path = Path(bids_root) / "events.tsv" events = pd.read_csv(events_path, sep="\t") events ``` | | trial\_type | onset | duration | | --- | --- | --- | --- | | 0 | active | 30 | 30 | | 1 | active | 90 | 30 | | 2 | active | 150 | 30 | | 3 | active | 210 | 30 | | 4 | active | 270 | 30 | ## Load the recordings and fix their spatial convention This dataset is stored with a spatial convention that differs from the [spatial convention that ConfUSIus assumes]() (the axes are labelled differently, the depth direction is flipped, and the stored world-space affine is not metric). We correct these differences so the data conform to ConfUSIus's spatial convention. The collapsed `_load_and_prepare_fusi` helper explains each step; otherwise, continue to the next cell. > [!NOTE] > > **Code for `_load_and_prepare_fusi` helper** > > ```python > def _load_and_prepare_fusi(pwd_path: Path) -> xr.DataArray: > """Load fUSI data and prepare geometry for analysis and visualization. > > The Khallaf et al. 2026 dataset is stored with a different spatial convention than > ConfUSIus. The main differences are: > > - The dataset uses `(k, j, i) = (depth, elevation, lateral)` instead of `(elevation, > depth, lateral)`. > - The depth direction is toward the transducer instead of away. > - The files' `sform` affines target a non-metric custom world space that isn't > documented. Because its `sform_code` is non-zero, this affine is used to define > the world coordinates. > > In this function, we prepare the data as follows: > > 1. Permute "k" and "j" so native voxel dim `k` carries elevation and `j` carries > depth. > 2. Apply the `world_to_qform` affine to the coordinates to have metric > coordinates. > 3. Cyclically permute "z", "y", "x" in world space so elevation lands on `z`, > depth on `y`, and lateral on `x` (`world_to_qform` leaves them on `x`, > `z`, and `y` respectively). > 4. Flip "x" to restore a proper (non-reflective) direction matrix. > 5. Flip "y" to have a depth direction "away" from the transducer, and flip "z" > to match the atlas orientation. > 6. Convert the coordinates units to millimeter. > > """ > with warnings.catch_warnings(): > # This dataset omits FrameAcquisitionDuration, which the fUSI-BIDS validator > # flags on load; it is irrelevant to this analysis, so we silence it. > warnings.filterwarnings( > "ignore", message="fUSI-BIDS validation warning", category=UserWarning > ) > da = cf.load(pwd_path) > > # 1. Relabel the k/j axes (no data copy needed): `create_voxeldata` accepts > # `dims` in any order -- it builds world coordinates from whichever dim is > # *named* k/j/i, independent of its world axis position, and transposes to > # canonical order internally. So swapping the k/j *labels* on the still-raw > # array is enough; the underlying values never need to move. (Avoid > # `.transpose().rename()` on the already voxel-to-world-index-backed array > # instead, which hits an upstream xarray bug: > # CoordinateTransformIndexingAdapter.shape ignores the adapter's own transposed > # dim order and returns the untransposed transform's shape, desyncing dims from > # shape.) > relabeled_dims = tuple({"k": "j", "j": "k"}.get(dim, dim) for dim in da.dims) > > # `voxel_to_world`'s columns still need the matching k/j swap, so that column 0 > # (now labeled `j` on the input) keeps its original k-associated coefficients > # and vice versa. > permute_kj = np.eye(4) > permute_kj[[0, 1]] = permute_kj[[1, 0]] > swapped_voxel_to_world = da.fusi.affine.voxel_to_world @ permute_kj > > da = cf.create_voxeldata( > da.values, > dims=relabeled_dims, > time=da.time, > voxel_to_world=swapped_voxel_to_world, > attrs=da.attrs, > name=str(da.name) if da.name is not None else None, > ) > > # 2. Apply the `world_to_qform` affine to the coordinates to have metric > # coordinates. This is the affine that settles which world row (z/y/x) each > # voxel column ends up mapping to, so it is applied in full (rotation > # included) -- the voxel-to-world index can represent rotations exactly, > # unlike a plain z/y/x DataArray's independent 1D coordinates. > da.fusi.affine.apply(da.affines["world_to_qform"], inplace=True) > > # 3. `world_to_qform` (checked empirically) lands elevation on world "x", > # depth on world "z", and lateral on world "y" -- a 3-cycle, not a pairwise > # swap. Permute rows so elevation->z, depth->y, lateral->x, matching > # ConfUSIus's convention. > permute_zyx_cycle = np.zeros((4, 4)) > permute_zyx_cycle[0, 2] = 1 # z <- old x (elevation) > permute_zyx_cycle[1, 0] = 1 # y <- old z (depth) > permute_zyx_cycle[2, 1] = 1 # x <- old y (lateral) > permute_zyx_cycle[3, 3] = 1 > da.fusi.affine.apply(permute_zyx_cycle, inplace=True) > > # 4. Flip "x" (lateral). The cyclic permutation above leaves the direction matrix > # improper (det -1, a reflection): `plot_volume`'s world-grid resampling > # (`slice_mode="z"`/`"y"`/`"x"`) silently produces empty/garbage slices for a > # reflection. Mirroring one axis restores a proper rotation (det +1). > flip_x = np.eye(4) > flip_x[2, 2] = -1 > flip_x[2, 3] = da.x.max().item() + da.x.min().item() > da.fusi.affine.apply(flip_x, inplace=True) > > # 5. Flip "y" to have a depth direction "away" from the transducer, and flip "z" > # to match the atlas orientation. Two more mirrors keep the direction matrix > # proper (each flip multiplies det by -1; three total flips, including step 4, > # nets back to det +1). > flip_y = np.eye(4) > flip_y[1, 1] = -1 > flip_y[1, 3] = da.y.max().item() + da.y.min().item() > da.fusi.affine.apply(flip_y, inplace=True) > > flip_z = np.eye(4) > flip_z[0, 0] = -1 > flip_z[0, 3] = da.z.max().item() + da.z.min().item() > da.fusi.affine.apply(flip_z, inplace=True) > > # 6. Convert the coordinates units to millimeter (nice to have). > m_to_mm = np.eye(4) > m_to_mm[:3, :3] *= 1e3 > da.fusi.affine.apply(m_to_mm, inplace=True) > da.fusi.affine.set_units("mm", inplace=True) > > return da > ``` ```python fusi_list = [_load_and_prepare_fusi(pwd_path) for pwd_path in pwd_paths] ``` Averaging each run over time and then across runs gives a single, high-SNR power-Doppler volume. This averaged image carries no task information, but it is a clean anatomical reference that we use for registration in the next step. ```python average_fusi = xr.concat([fusi.mean("time") for fusi in fusi_list], dim="extra").mean( "extra" ) ``` ## Bring the data into a common anatomical space To interpret the activation map anatomically (and to define the masks the analysis needs) we register the averaged recording to the Pepe, Mariani et al. 2026 mouse fUSI template, which is itself registered to the Allen Mouse Brain atlas. This gives us a common space in which we can draw region boundaries and pull out anatomically defined masks. `fetch_brainglobe_atlas` gives us the Allen atlas as an `xarray.Dataset`, holding the `template`, `annotation` and `hemispheres` volumes on a common grid plus an `.atlas` accessor for structure queries. The template carries its scanner-to-Allen transform in `world_to_sform`. Composing its inverse with the estimated registration transform maps atlas coordinates back to the recording's world space. The registration itself gives us the transform between template and average fUSI image. We initialize the registration from a coarse manual alignment (`napari_transform`) that we previously obtained using [napari's manual transform tool](). The call returns the resampled moving image, the estimated transform, and a diagnostics object; here we only keep the transform. > [!NOTE] > > Registration results are sensitive to their arguments. See the [registration examples]() for guidance on inspecting convergence and tuning the optimizer. ```python # Copied and pasted transform after manual transformation in napari. napari_transform = np.array( [ [7.96845195e-01, 1.42854004e-01, -3.83286557e-04, 2.48554433e01], [-1.12364739e-01, 8.50118815e-01, -7.18591162e-05, 3.43594205e01], [3.26392590e-04, 3.23725114e-04, 1.00007398e00, 6.61602528e00], [0.0, 0.0, 0.0, 1.0], ] ) _, template_to_fusi_transform, _ = cf.registration.register_volume( average_fusi, template_pepe_mariani, transform_type="affine", learning_rate="auto", initialization=np.linalg.inv(napari_transform), ) ``` The estimated transform maps template scanner coordinates back onto the recording's world space. Compose it with the inverse scanner-to-Allen transform to obtain the atlas-to-recording mapping. With that affine in hand, `resample_like` reslices each volume onto the atlas grid. We resample the averaged image (for display) and every individual run (the GLM input). ```python atlas = cf.datasets.fetch_brainglobe_atlas("allen_mouse_100um") atlas_to_fusi_transform = template_to_fusi_transform @ np.linalg.inv( template_pepe_mariani.affines["world_to_sform"] ) resampled_average_in_atlas = cf.registration.resample_like( average_fusi, atlas.annotation, atlas_to_fusi_transform ) resampled_fusi_list = [] for fusi in fusi_list: resampled_fusi = cf.registration.resample_like( fusi, atlas.annotation, atlas_to_fusi_transform ) resampled_fusi_list.append(resampled_fusi) ``` ## Choose a hemodynamic response function A stimulus does not produce an instantaneous change in the power Doppler signal: the vascular response is usually delayed through the neurovascular coupling. The GLM accounts for this by convolving the stimulation boxcar with a **hemodynamic response function (HRF)**. ConfUSIus offers different types of HRFs, some of them originally proposed for fMRI analysis. Here we use `claron2021_hrf`, an inverse-gamma HRF proposed for functional ultrasound, rather than a canonical BOLD HRF. We tune its `beta` scale parameter to `6.7` to obtain a faster peak response (around 2–3 seconds). ```python modified_claron2021 = partial(cf.glm.claron2021_hrf, beta=6.7) # Sample the kernel on a fine grid to visualize its shape. hrf_kernel = modified_claron2021(dt=1.0) hrf_time = np.linspace(0, 32, len(hrf_kernel)) fig, ax = plt.subplots(figsize=(7, 3), facecolor=bg_color) ax.plot(hrf_time, hrf_kernel, color="#d93a54") ax.set_xlabel("Time since stimulus onset (s)") ax.set_ylabel("Response (a.u.)") _ = ax.set_title("Claron et al. 2021 fUSI HRF (beta=6.7)") ``` ## Model physiological noise with CompCor Beyond the stimulus response, the signal contains structured nuisance fluctuations. `compute_compcor_confounds` extracts the leading principal components from a noise region—here the atlas `"fiber tracts"`, that ideally carries little task signal and global vascular fluctuations—and we add them to the design as nuisance regressors (anatomical CompCor). We take three components per run. ```python confounds = [ cf.signal.compute_compcor_confounds( fusi, noise_mask=atlas.atlas.get_masks("fiber tracts")[0], n_components=3, ) for fusi in resampled_fusi_list ] ``` ## Fit the first-level GLM We hand the model specification to `FirstLevelModel` up front: the HRF, the `"cosine"` drift model that high-pass filters slow scanner/physiological drifts below `0.01` Hz, and the AR(1) noise model that accounts for temporal autocorrelation in the residuals. `fit` then takes the runs together with the `events` table and the per-run CompCor `confounds`, assembles a design matrix for each run internally, and fits it to the data voxel by voxel. We apply a light Gaussian spatial smoothing (0.3 mm FWHM per axis) to boost SNR and fit every voxel, leaving the anatomical masking to the thresholding step below. Passing the list of runs together combines them with a fixed-effects model. ```python glm = cf.glm.FirstLevelModel( smoothing_fwhm=0.3, hrf_model=modified_claron2021, drift_model="cosine", low_cutoff=0.01, noise_model="ar1", show_progress=True ) glm.fit(resampled_fusi_list, events=events, confounds=confounds) ``` ``` FirstLevelModel(hrf_model=functools.partial(, beta=6.7), show_progress=True, smoothing_fwhm=0.3) ``` **In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.** ☑ FirstLevelModel iFitted **Parameters** | | | | | --- | --- | --- | | | hrf\_model | functools.par...60\>, beta=6.7) | | | smoothing\_fwhm | 0.3 | | | show\_progress | True | | | drift\_model | 'cosine' | | | low\_cutoff | 0.01 | | | drift\_order | 1 | | | fir\_delays | None | | | noise\_model | 'ar1' | | | minimize\_memory | True | | | oversampling | 50 | | | min\_onset | \-24.0 | | | uniformity\_tolerance | 0.01 | | | mask | None | **Fitted attributes** | Name | Type | Value | | --- | --- | --- | | design\_matrices\_ | list | \[ acti...00 1.0 , acti...00 1.0 , acti...00 1.0 , acti...00 1.0 , ...\] | | results\_ | list | \[\, \, \, \, ...\] | ## Inspect the design matrix The fit assembled one design matrix per run (through `make_first_level_design_matrix`) and stored them on the fitted model as `design_matrices_`. Each run gets its own matrix because its CompCor regressors are estimated from its own data. Pulling the first run's matrix back out lets us see exactly what the model fit. Visualizing that matrix makes the model concrete. Each column is a regressor and each row a volume (time runs top to bottom). The leftmost `active` column shows the HRF-convolved stimulation blocks; the CompCor and drift columns follow, and the constant column models the baseline. ```python design_matrix = glm.design_matrices_[0] _ = cf.plotting.plot_design_matrix( design_matrix, title="Design matrix (first run)", index_yaxis=True, bg_color=bg_color, ) ``` ## Compute and display the activation map `compute_contrast` turns the fitted model into a statistical map from a *contrast*: a weight vector over the design-matrix columns. Before computing it, we can look at the contrast itself with `plot_contrast_matrix`, which lays the weights over the design regressors and makes explicit that the `"active"` contrast simply selects the stimulation regressor while ignoring the CompCor, drift, and constant nuisance columns. ```python _ = cf.plotting.plot_contrast_matrix( "active", design_matrix, cmap="coolwarm", bg_color=bg_color ) ``` The `"active"` contrast then tests, at every voxel, whether that stimulation regressor has a non-zero effect, and returns a z-score map. We display it over a range of atlas slices with the region boundaries drawn on top for anatomical context. ```python z_score = glm.compute_contrast("active") z_1d = resampled_average_in_atlas.z.isel(j=0, i=0).values slice_coords = z_1d[(z_1d > 5.5) & (z_1d < 9)][::4] cmap = "berlin" if is_dark_theme else None plotter = cf.plotting.plot_stat_map( z_score, slice_coords=slice_coords, nrows=3, vmax=10, bg_color=bg_color, cmap=cmap, fontsize=20, ) _ = plotter.add_contours( atlas.annotation, linewidths=0.6, slice_coords=slice_coords, alpha=0.4, ) ``` ## Threshold for statistical significance The raw z-map shows the effect at every voxel; we might want to keep only those that are statistically significant while controlling for the many thousands of simultaneous tests. `apply_statistical_threshold` applies a multiple-comparison correction (here Holm family-wise-error control at `alpha=0.01`, restricted to the whole-brain `"root"` mask from the atlas), followed by a cluster-extent threshold that drops surviving clusters smaller than 30 voxels. It returns the map with the non-surviving voxels zeroed out, along with the z-value at which the correction cut. A hard threshold is not the only way to show significance. Rather than hiding the sub-threshold voxels outright, we can let significance drive the *opacity* of the overlay, which keeps the sub-threshold structure visible while still making the significant clusters stand out. For that we convert the z-scores into two-sided p-values and correct them with the same Holm procedure via `adjust_pvalues`, which sets untested voxels (those outside the mask) to `1.0`. `1 - adjusted_p_values` then gives an opacity scale that we can hand to the plot as an alpha map. ```python thresholded_zscore, threshold = cf.stats.apply_statistical_threshold( z_score, mask=atlas.atlas.get_masks("root")[0], alpha=0.01, method="holm", cluster_threshold=30, ) p_values = z_score.copy(deep=True) p_values.values = np.clip(2.0 * sps.norm.sf(np.abs(z_score)), 0.0, 1.0) adjusted_p_values = cf.stats.adjust_pvalues( p_values, mask=atlas.atlas.get_masks("root")[0], method="holm" ) ``` > [!TIP] > > **Explore the result interactively** > > The thresholded map, the anatomical reference, and the atlas labels can be loaded together into a [napari]() viewer for 3D exploration: > > ```python > viewer, _ = resampled_average_in_atlas.fusi.plot() > viewer, _ = thresholded_zscore.fusi.plot( > viewer=viewer, contrast_limits=(-10, 10) > ) > cf.plotting.plot_napari(atlas.annotation, viewer=viewer, layer_type="labels") > ``` Finally, we overlay the z-map on the mean power Doppler image (in dB) with the atlas contours, giving a single figure that places the odour response in its anatomical context. ```python plotter = cf.plotting.plot_stat_map( z_score, bg_volume=resampled_average_in_atlas.fusi.scale.db(), slice_coords=slice_coords, nrows=3, bg_kwargs={"vmin": -20, "vmax": 0}, bg_color=bg_color, alpha=1 - adjusted_p_values, cmap=cmap, fontsize=20, ) _ = plotter.add_contours( atlas.annotation, linewidths=0.6, slice_coords=slice_coords, alpha=0.4, ) ``` --- **Total running time:** 114.7 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/glm/first_level/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. # Lagged first-level GLM analysis of fUSI data with a continuous regressor This example reproduces the lagged-GLM analysis reported by [Cybis Pereira *et al.* 2026]()[^/dev/examples/_built/glm/first_level_continuous/#collection], which uses functional ultrasound imaging of a freely moving rat exploring an open field with its body position tracked from video. Unlike the block-design task in the [First-level GLM analysis of fUSI data]() example, the regressor of interest is a *continuous* variable measured throughout the recording, the animal's locomotion speed, and we ask at every voxel how much of the power Doppler time course tracks it at different lags. Rather than convolving the speed regressor with a hemodynamic response function, the analysis shifts it across a range of temporal lags and fits one model per lag, much like a voxel-wise cross-correlation between speed and the vascular signal. The notebook goes through: 1. **Fetch and load** one open-field recording and its motion track. 2. **Build an animal speed regressor** from the animal's pose estimation. 3. **Encode speed** as a per-volume parametric modulator and **extract CompCor** noise regressors. 4. **Fit** a `FirstLevelModel` at a range of temporal lags between speed and signal. 5. **Threshold** the resulting maps and visualize the response at each lag. ## Fetch the recording `fetch_cybis_pereira_2026` downloads the fUSI-BIDS dataset. We take subject `rat75`, session `20220524`, acquisition `slice32` of the `openfield` task, which gives one power Doppler recording together with its motion track. ```python from pathlib import Path import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np import pandas as pd import xarray as xr import confusius as cf # Adapt background color to the current Matplotlib style. bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) is_dark_theme = sum(mpl.colors.to_rgb(bg_color)) / 3 < 0.5 # Keep notebook output compact for large DataArray displays. The coordinates section is # left expanded on purpose; `display_expand_data` alone does not cover the attributes. xr.set_options(display_expand_data=False, display_expand_attrs=False) subject = "rat75" session = "20220524" acq = "slice32" bids_root = cf.datasets.fetch_cybis_pereira_2026( datasets="rawdata", subjects=subject, sessions=session, acqs=acq, ) pwd_path = ( Path(bids_root) / f"sub-{subject}" / f"ses-{session}" / "fusi" / f"sub-{subject}_ses-{session}_task-openfield_acq-{acq}_pwd.nii.gz" ) motion_path = ( Path(bids_root) / f"sub-{subject}" / f"ses-{session}" / "motion" / f"sub-{subject}_ses-{session}_task-openfield_tracksys-DLC_acq-{acq}_motion.tsv" ) ``` ``` If you use this dataset in your work, please cite the following source: Cybis Pereira, F., Castedo, S. H., Meur-Diebolt, S. L., Ialy-Radio, N., Bhattacharya, S., Ferrier, J., Osmanski, B. F., Cocco, S., Monasson, R., Pezet, S., & Tanter, M. (2026). A vascular code for speed in the spatial navigation system. Cell Reports, 45(1). https://doi.org/10.1016/j.celrep.2025.116791 ``` ## Load the recording `cf.load` reads the power Doppler NIfTI into an `xarray.DataArray`. We call `.compute()` to pull it fully into memory, since it is reused across many model fits below. ```python data = cf.load(pwd_path).compute() data ``` xarray.DataArray 'sub-rat75\_ses-20220524\_task-openfield\_acq-slice32\_pwd' - time: 1440 - k: 1 - j: 112 - i: 128 - [ ] 192.7 197.2 226.9 229.9 278.7 278.7 ... 61.31 60.61 64.25 65.22 55.23 ``` array([[[[ 192.71379271, 197.1830354 , 226.89889524, ..., 135.64715222, 117.71528254, 111.25769231], [ 269.26946815, 218.92078634, 218.29339147, ..., 153.14631565, 135.36809639, 122.19806324], [ 469.46957151, 696.08320236, 741.61495988, ..., 116.9231638 , 116.41497862, 112.41130004], ..., [ 72.70813918, 67.33206102, 67.73276301, ..., 100.03939077, 95.61359 , 101.95938025], [ 76.24533181, 68.25491978, 73.33637818, ..., 79.11982739, 89.33515235, 90.30539562], [ 77.10922277, 85.88419607, 80.21954388, ..., 70.46910931, 78.98406757, 83.14898008]]], [[[ 102.03228554, 154.92695187, 208.98264674, ..., 76.35214763, 70.93058492, 92.42799846], [ 128.40719979, 179.70018173, 266.95304282, ..., 79.35330346, 70.45420204, 82.7213379 ], [ 197.83338619, 292.06213106, 453.36528013, ..., ... [ 55.2049498 , 55.06794489, 61.32079278, ..., 68.94497935, 72.57032896, 67.73033591], [ 65.66832994, 62.9659692 , 64.82018124, ..., 66.59579324, 58.19578412, 47.85324591]]], [[[ 57.94911664, 55.762271 , 60.17339521, ..., 133.26805649, 109.66913578, 81.76589334], [ 63.21644599, 66.81321546, 80.94457473, ..., 106.51902626, 90.90147175, 75.77422113], [ 85.8301978 , 92.77473431, 91.98078457, ..., 84.56545788, 87.36666125, 74.77339123], ..., [ 59.61792382, 57.51713315, 49.45440719, ..., 85.92554608, 74.22148882, 88.33563124], [ 55.38937908, 53.42711824, 56.3874491 , ..., 65.18981048, 79.52728571, 72.33484683], [ 53.38009269, 51.48751045, 64.40242875, ..., 64.25058854, 65.21577368, 55.2309584 ]]]], shape=(1440, 1, 112, 128)) ``` - [x] Coordinates: (7) - time (time) float64 0.5 1.0 1.5 ... 719.0 719.5 720.0 ☐ ☐ **units :** s **volume\_acquisition\_reference :** start **volume\_acquisition\_duration :** 0.5000249999999369 ``` array([5.000000e-01, 1.000025e+00, 1.500025e+00, ..., 7.190184e+02, 7.195184e+02, 7.200184e+02], shape=(1440,)) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 107 108 109 110 111 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]) ``` - i (i) int64 0 1 2 3 4 5 ... 123 124 125 126 127 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]) ``` - z (k, j, i) float64 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 ☐ ☐ **units :** mm ``` array([[[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]]], shape=(1, 112, 128)) ``` - y (k, j, i) float64 5.0 5.0 5.0 ... 15.94 15.94 15.94 ☐ ☐ **units :** mm ``` array([[[ 5. , 5. , 5. , ..., 5. , 5. , 5. ], [ 5.09856 , 5.09856 , 5.09856 , ..., 5.09856 , 5.09856 , 5.09856 ], [ 5.19712 , 5.19712 , 5.19712 , ..., 5.19712 , 5.19712 , 5.19712 ], ..., [15.74303978, 15.74303978, 15.74303978, ..., 15.74303978, 15.74303978, 15.74303978], [15.84159978, 15.84159978, 15.84159978, ..., 15.84159978, 15.84159978, 15.84159978], [15.94015978, 15.94015978, 15.94015978, ..., 15.94015978, 15.94015978, 15.94015978]]], shape=(1, 112, 128)) ``` - x (k, j, i) float64 \-6.985 -6.875 ... 6.875 6.985 ☐ ☐ **units :** mm ``` array([[[-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], ..., [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027]]], shape=(1, 112, 128)) ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0.5, 1.0000250000000002, 1.500025, 2.00005, 2.50005, 3.0000750000000003, 3.500075, 4.0001, 4.5001, 5.000125, ... 715.518325, 716.018325, 716.5183499999999, 717.0183499999999, 717.518375, 718.018375, 718.5183999999999, 719.0183999999999, 719.518425, 720.018425], dtype='float64', name='time', length=1440)) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 102, 103, 104, 105, 106, 107, 108, 109, 110, 111], dtype='int64', name='j', length=112)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], dtype='int64', name='i', length=128)) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (21) **sform\_code :** 2 **qform\_code :** 1 **manufacturer :** Iconeus **manufacturers\_model\_name :** Iconeus One **probe\_type :** linear **probe\_model :** IcoPrime light **probe\_number\_of\_elements :** 128 **probe\_pitch :** 0.11 **probe\_radius\_of\_curvature :** 0 **ProbeElevationAperture :** 1.5 **ProbeElevationFocus :** 8 **plane\_wave\_angles :** \[-7.0, -5.0, -3.0, -1.0, 1.0, 3.0, 5.0, 7.0\] **IsHadamard :** True **UltrafastSamplingFrequency :** 400.0 **clutter\_filter\_window\_duration :** 0.2 **clutter\_filters :** \['svd:remove\_first\_60\_components'\] **power\_doppler\_integration\_duration :** 0.2 **probe\_center\_frequency :** 15.625 **pulse\_repetition\_frequency :** 3200.0 **transmit\_voltage :** 15 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} ## Correct for motion The rat moves freely, so a real analysis should first correct volume-to-volume brain motion. We skip it here to keep the example fast to build, at the cost of a little statistical robustness in the maps below. > [!NOTE] > > **Recommended in a real analysis** > > Register every volume to a reference frame with `register_volumewise` before building the regressors, then continue with the corrected `data`. Removing motion-driven variance sharpens the speed maps and their statistics. See the [Motion correction of a single recording]() example for the full workflow and diagnostics. > > ```python > data = cf.registration.register_volumewise(data, learning_rate=1) > ``` ## Build the animal speed regressor from the pose estimation The motion `.tsv` holds the body position (`body_x`, `body_y`) tracked with [DeepLabCut]() at 50 fps. We take the frame-to-frame Euclidean displacement, scale it by the frame rate to obtain speed, smooth it with a 1 s centered rolling mean, and resample it onto the fUSI volume times. ```python fps = 50 motion_df = pd.read_csv(motion_path, sep="\t") squared_diff = motion_df.diff() ** 2 speed_df = fps * (squared_diff["body_x"] + squared_diff["body_y"]) ** 0.5 speed_df[0] = 0 speed = ( xr.DataArray( speed_df, dims=["time"], coords={"time": 1 / fps * np.arange(len(speed_df))}, name="speed", ) .rolling(time=fps, min_periods=1, center=True) .mean() ) speed = speed.interp(time=data.time, method="linear").ffill("time") fig, ax = plt.subplots(figsize=(7, 3), facecolor=bg_color) ax.plot(speed.time, speed, color="#d93a54") ax.set_xlabel("Time (s)") _ = ax.set_ylabel("Animal speed (cm/s)") ``` ## Encode speed as a parametric modulator We build an `events` table with one entry per fUSI volume, each carrying the speed at that volume as its `modulation`, which turns speed into a single continuous regressor. ```python events = pd.DataFrame( { "onset": data.time, "duration": [data.time.volume_acquisition_duration] * len(data), "modulation": speed, "trial_type": ["speed"] * len(data), } ) ``` ## Model physiological noise with CompCor `compute_compcor_confounds` extracts the three leading principal components of the highest-variance voxels (the top 5%) to add as nuisance regressors (temporal CompCor). ```python confounds = cf.signal.compute_compcor_confounds( data, variance_threshold=0.05, n_components=3, ) ``` ## Build the model and design matrix We instantiate the `FirstLevelModel` with a light 0.3 mm Gaussian spatial smoothing (the default AR(1) noise model handles temporal autocorrelation) and build the design matrix with `make_first_level_design_matrix`: the speed regressor, the CompCor confounds, a `"cosine"` drift basis that high-pass filters drifts below `0.01` Hz, and a constant. ```python glm = cf.glm.FirstLevelModel(smoothing_fwhm=0.3) design_matrix = cf.glm.make_first_level_design_matrix( data.time.values, events=events, drift_model="cosine", low_cutoff=0.01, confounds=confounds, ) ``` ## Fit the GLM across temporal lags The vascular response follows a change in speed with a delay. Instead of fixing that delay with an HRF, we sweep it: for each temporal lag we shift *only* the speed regressor, leaving the confounds, drift, and data window fixed. We fit the model, and compute the `"speed"` contrast. This yields one z-map per lag, like a cross-correlation between speed and the signal of each voxel. Holding the window fixed across lags keeps the maps comparable, and a positive lag means the signal responds after the change in speed. `run_lagged_glm` returns the per-lag maps, which we stack along a `lag` dimension whose coordinate is the delay in seconds (one volume per lag). The original study sweeps a wider window (about -2 to 10 s); here we use a short positive range to keep the example quick. ```python def run_lagged_glm( data: xr.DataArray, glm: cf.glm.FirstLevelModel, design_matrix: pd.DataFrame, lags: range, ) -> list[xr.DataArray]: """Fit the GLM at a range of lags, shifting only the speed regressor.""" max_lag = max(lags) n = len(design_matrix) # Fixed data window and time-locked nuisance regressors, shared by every lag. data_window = data[max_lag:] fixed_design = design_matrix.iloc[max_lag:] z_scores = [] for lag in lags: design = fixed_design.copy() # Shift only the regressor of interest: speed at volume t - lag predicts the # signal at volume t. design["speed"] = design_matrix["speed"].iloc[max_lag - lag : n - lag].values glm.fit([data_window], design_matrices=[design]) z_scores.append(glm.compute_contrast("speed")) return z_scores lags = range(9) z_scores = run_lagged_glm(data, glm, design_matrix, lags=lags) z_score = xr.concat(z_scores, dim="lag").assign_coords( lag=data.fusi.spacing["time"] * np.asarray(lags) ) z_score.lag.attrs["units"] = data.time.units ``` ## Threshold and display the maps `apply_statistical_threshold` applies a Bonferroni correction at `alpha=0.001` followed by a 30-voxel cluster-extent threshold, zeroing the voxels that do not survive. We plot the thresholded z-map at each lag over the mean power Doppler image (in dB). ```python thresholded_zscore, threshold = cf.stats.apply_statistical_threshold( z_score, alpha=0.001, method="bonferroni", cluster_threshold=30, ) cmap = "berlin" if is_dark_theme else None _ = thresholded_zscore.fusi.plot.stat_map( bg_volume=data.mean("time").fusi.scale.db().expand_dims(lag=z_score.lag), slice_mode="lag", nrows=3, cmap=cmap, threshold=threshold, bg_color=bg_color, fontsize=20, ) ``` --- **Total running time:** 24.3 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/glm/first_level_continuous/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. # ConfUSIus and Xarray 101 This example demonstrates how to use ConfUSIus to load and handle fUSI data as DataArray instances. We will use a small subset of the Nunez-Elizalde 2022 dataset[^/dev/examples/_built/io/confusius_xarray_101/#collection] and use a few basic Xarray operations to inspect, subset, and summarize the data. ## Fetch one recording from the Nunez-Elizalde 2022 dataset ConfUSIus provides convenient functions to download public datasets. The data is cached after the first download, so subsequent runs will be faster. The `fetch_nunez_elizalde_2022` function allows you to specify which subjects, sessions, tasks, and acquisitions to download. Here we select one recording from subject `CR022`, session `20201011`, task `spontaneous`, and acquisition `slice03`. ```python from pathlib import Path import matplotlib as mpl import matplotlib.pyplot as plt import xarray as xr import confusius as cf # 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 = cf.datasets.fetch_nunez_elizalde_2022( subjects="CR022", sessions="20201011", tasks="spontaneous", acqs="slice03", ) ``` ``` If you use this dataset in your work, please cite the following source: Nunez-Elizalde, A. O., Krumin, M., Reddy, C. B., Montaldo, G., Urban, A., Harris, K. D., & Carandini, M. (2022). Neural correlates of blood flow measured by ultrasound. Neuron, 110(10), 1631–1640.e4. https://doi.org/10.1016/j.neuron.2022.02.012 ``` ## Load the recording as a DataArray The downloaded dataset is stored following the [Brain Imaging Data Structure (BIDS)](), so we can use the `bids_root` path to construct the path to the desired recording. The `confusius.load` function reads any compatible fUSI file (NIfTI, Iconeus SCAN, Zarr, etc.) and returns a DataArray with the data values, coordinates, and metadata all in one instance. In the recording representation below, note the named dimensions, world coordinates, and representative acquisition attributes attached to the array. ```python 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) data ``` xarray.DataArray 'sub-CR022\_ses-20201011\_task-spontaneous\_acq-slice03\_pwd' - time: 751 - k: 1 - j: 114 - i: 80 - [ ] dask.array\
Array Chunk
Bytes 26.13 MiB 26.13 MiB
Shape (751, 1, 114, 80) (751, 1, 114, 80)
Dask graph 1 chunks in 3 graph layers
Data type float32 numpy.ndarray
- [x] Coordinates: (7) - time (time) float64 10.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,)) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 109 110 111 112 113 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113]) ``` - i (i) int64 0 1 2 3 4 5 6 ... 74 75 76 77 78 79 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]) ``` - z (k, j, i) float64 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0 ☐ ☐ **units :** mm ``` [9120 values with dtype=float64] ``` - y (k, j, i) float64 2.73 2.73 2.73 ... 8.19 8.19 8.19 ☐ ☐ **units :** mm ``` [9120 values with dtype=float64] ``` - x (k, j, i) float64 \-3.95 -3.85 -3.75 ... 3.85 3.95 ☐ ☐ **units :** mm ``` [9120 values with dtype=float64] ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 10.608, 10.908, 11.208, 11.508000000000001, 11.808, 12.108, 12.408, 12.708, 13.008000000000001, 13.308, ... 232.995, 233.29500000000002, 233.595, 233.895, 234.195, 234.495, 234.79500000000002, 235.095, 235.395, 235.695], dtype='float64', name='time', length=751)) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 104, 105, 106, 107, 108, 109, 110, 111, 112, 113], dtype='int64', name='j', length=114)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79], dtype='int64', name='i')) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (25) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} ConfUSIus also registers a `.fusi` accessor on every DataArray, which exposes fUSI-specific helpers and methods. For example, we can inspect the sampling step along each dimension. Note that for this recording, the temporal step isn't defined because the time coordinate isn't regularly sampled. See the [Working with Xarray]() guide for a broader overview of the accessor. ```python data.fusi.spacing ``` ```text {'time': None, 'k': 0.4000000059604645, 'j': 0.04831999912858009, 'i': 0.10000000149011612} ``` ## Lazy loading and when to compute ConfUSIus loads recordings lazily, so the underlying array starts out as a Dask array. This is convenient for working with large datasets that don't fit in memory, such as large beamformed IQ data. However, lazily loaded arrays may be chunked in ways that are not ideal for all operations. For example, chunking across the time dimension is ideal for processing volume-by-volume (such as when visualizing a recording in napari), but it will make temporal operations (e.g., averaging over time, frequency filtering, etc.) less efficient. Thus, when working with smaller recordings that do fit in memory, it is often simpler to call `.compute` right after loading to get a regular in-memory array. This is not required, but it is recommended to make subsequent operations more straightforward. ```python data = data.compute() ``` ## Positional and coordinate-based indexing Once the recording is in memory, we can keep using normal Xarray operations. A useful distinction is between `.isel`, which indexes by integer position, and `.sel`, which indexes by coordinate values. ```python first_50_volumes = data.isel(time=slice(0, 50)) first_50_volumes ``` xarray.DataArray 'sub-CR022\_ses-20201011\_task-spontaneous\_acq-slice03\_pwd' - time: 50 - k: 1 - j: 114 - i: 80 - [ ] 4.464e+03 5.588e+03 5.064e+03 ... 2.841e+04 2.673e+04 2.532e+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, ... [17493.861 , 17775.041 , 23547.389 , ..., 28452.746 , 22698.502 , 23594.04 ], [19309.525 , 22344.94 , 17757.36 , ..., 27594.635 , 24383.32 , 30229.205 ]]], [[[ 4762.957 , 4783.239 , 4716.9546, ..., 4631.195 , 5891.131 , 5686.8545], [ 5645.5835, 5345.232 , 4722.929 , ..., 6068.9824, 5480.8853, 5593.3716], [ 6165.6123, 5114.455 , 5317.0586, ..., 5124.0215, 6636.563 , 5232.51 ], ..., [21292.963 , 19519.293 , 23920.36 , ..., 28057.277 , 28513.908 , 25043.06 ], [19704.613 , 21517.111 , 22497.361 , ..., 26705.44 , 23562.979 , 24071.332 ], [22085.182 , 19549.383 , 20934.074 , ..., 28412.838 , 26725.537 , 25321.049 ]]]], shape=(50, 1, 114, 80), dtype=float32) ``` - [x] Coordinates: (7) - time (time) float64 10.61 10.91 11.21 ... 25.01 25.31 ☐ ☐ **units :** s **volume\_acquisition\_reference :** start **volume\_acquisition\_duration :** 0.3 ``` array([10.608, 10.908, 11.208, 11.508, 11.808, 12.108, 12.408, 12.708, 13.008, 13.308, 13.608, 13.908, 14.208, 14.508, 14.808, 15.108, 15.408, 15.708, 16.008, 16.308, 16.608, 16.908, 17.208, 17.508, 17.808, 18.108, 18.408, 18.708, 19.008, 19.308, 19.608, 19.908, 20.208, 20.508, 20.808, 21.108, 21.408, 21.708, 22.008, 22.308, 22.608, 22.908, 23.208, 23.508, 23.808, 24.108, 24.408, 24.708, 25.008, 25.308]) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 109 110 111 112 113 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113]) ``` - i (i) int64 0 1 2 3 4 5 6 ... 74 75 76 77 78 79 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]) ``` - z (k, j, i) float64 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0 ☐ ☐ **units :** mm ``` array([[[1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], ..., [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.], [1., 1., 1., ..., 1., 1., 1.]]], shape=(1, 114, 80)) ``` - y (k, j, i) float64 2.73 2.73 2.73 ... 8.19 8.19 8.19 ☐ ☐ **units :** mm ``` array([[[2.73007989, 2.73007989, 2.73007989, ..., 2.73007989, 2.73007989, 2.73007989], [2.77839989, 2.77839989, 2.77839989, ..., 2.77839989, 2.77839989, 2.77839989], [2.82671989, 2.82671989, 2.82671989, ..., 2.82671989, 2.82671989, 2.82671989], ..., [8.09359979, 8.09359979, 8.09359979, ..., 8.09359979, 8.09359979, 8.09359979], [8.14191979, 8.14191979, 8.14191979, ..., 8.14191979, 8.14191979, 8.14191979], [8.19023979, 8.19023979, 8.19023979, ..., 8.19023979, 8.19023979, 8.19023979]]], shape=(1, 114, 80)) ``` - x (k, j, i) float64 \-3.95 -3.85 -3.75 ... 3.85 3.95 ☐ ☐ **units :** mm ``` array([[[-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], ..., [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007]]], shape=(1, 114, 80)) ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 10.608, 10.908, 11.208, 11.508000000000001, 11.808, 12.108, 12.408, 12.708, 13.008000000000001, 13.308, 13.608, 13.908, 14.208, 14.508000000000001, 14.808, 15.108, 15.408, 15.708, 16.008, 16.308, 16.608, 16.908, 17.208000000000002, 17.508, 17.808, 18.108, 18.408, 18.708000000000002, 19.008, 19.308, 19.608, 19.908, 20.208000000000002, 20.508, 20.808, 21.108, 21.408, 21.708000000000002, 22.008, 22.308, 22.608, 22.908, 23.208000000000002, 23.508, 23.808, 24.108, 24.408, 24.708000000000002, 25.008, 25.308], dtype='float64', name='time')) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 104, 105, 106, 107, 108, 109, 110, 111, 112, 113], dtype='int64', name='j', length=114)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79], dtype='int64', name='i')) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (25) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} Here we define a region of interest (ROI) in world coordinates. This is often more meaningful than index-based slicing because the bounds are expressed directly in physical units (e.g., millimeters) rather than in terms of array indices, which may not be as intuitive to interpret. > [!WARNING] > > **World-coordinate `.sel` requires an axis-aligned recording** > > `.sel` works because this recording's voxel-to-world affine is axis-aligned (no rotation or shear), so a range of `y`/`x` values maps onto a rectangular block of `j`/`i` indices. When the affine is oblique, a range of world coordinates no longer picks out an axis-aligned block of voxels, so `.sel` cannot be used for ROI selection. Instead, index into `k`/`j`/`i` directly or resample the recording. ```python roi = data.sel(y=slice(3.5, 7.5), x=slice(-2.0, 2.0)) roi ``` xarray.DataArray 'sub-CR022\_ses-20201011\_task-spontaneous\_acq-slice03\_pwd' - time: 751 - k: 1 - j: 83 - i: 40 - [ ] 5.411e+04 2.929e+04 1.632e+04 ... 2.982e+04 2.934e+04 2.825e+04 ``` array([[[[ 54114.15 , 29293.102, 16318.03 , ..., 70195.4 , 82424.75 , 53491.15 ], [ 36852.598, 23997.521, 17537.834, ..., 87855.99 , 89461.02 , 50858.67 ], [ 31055.729, 20669.076, 19216.28 , ..., 90254.805, 65013.676, 93702.836], ..., [ 28217.041, 28518.54 , 36518.164, ..., 35137.496, 37473.96 , 31445.836], [ 36724.688, 34043.195, 41564.836, ..., 28682.494, 36342.758, 24813.594], [ 36131.27 , 39894.926, 44742.293, ..., 31363.957, 32790.47 , 26770.775]]], [[[ 34392.016, 19327.553, 14827.421, ..., 62507.297, 56234.023, 32206.031], [ 33328.855, 18415.244, 14050.276, ..., 121733.43 , 88763.02 , 64205.09 ], [ 40385.535, 16530.572, 16047.016, ..., 120963.68 , ... [ 33237.37 , 35654.13 , 52624.965, ..., 37240.01 , 28125.756, 25826.408], [ 40038.098, 42877.617, 51304.074, ..., 34593.01 , 26241.9 , 26714.23 ]]], [[[ 44361.438, 18625.635, 17125.664, ..., 81257.48 , 56526.516, 47173.05 ], [ 42567.773, 19387.824, 16338.876, ..., 133328.88 , 57100.35 , 45971.17 ], [ 31899.297, 19105.168, 18343.734, ..., 120351.74 , 52336.94 , 68195.125], ..., [ 27940.451, 28368.473, 37943.44 , ..., 30510.564, 30199.082, 26136.422], [ 31668.438, 32992.008, 43480.77 , ..., 30339.107, 32981.223, 26465.889], [ 40846.68 , 44659.457, 47308.09 , ..., 29822.707, 29339.736, 28245.74 ]]]], shape=(751, 1, 83, 40), dtype=float32) ``` - [x] Coordinates: (7) - time (time) float64 10.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,)) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 16 17 18 19 20 ... 94 95 96 97 98 ☐ ☐ ``` array([16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98]) ``` - i (i) int64 20 21 22 23 24 ... 55 56 57 58 59 ☐ ☐ ``` array([20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59]) ``` - z (k, j, i) float64 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0 ☐ ☐ **units :** mm ``` [3320 values with dtype=float64] ``` - y (k, j, i) float64 3.503 3.503 3.503 ... 7.465 7.465 ☐ ☐ **units :** mm ``` [3320 values with dtype=float64] ``` - x (k, j, i) float64 \-1.95 -1.85 -1.75 ... 1.85 1.95 ☐ ☐ **units :** mm ``` [3320 values with dtype=float64] ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 10.608, 10.908, 11.208, 11.508000000000001, 11.808, 12.108, 12.408, 12.708, 13.008000000000001, 13.308, ... 232.995, 233.29500000000002, 233.595, 233.895, 234.195, 234.495, 234.79500000000002, 235.095, 235.395, 235.695], dtype='float64', name='time', length=751)) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], dtype='int64', name='j')) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59], dtype='int64', name='i')) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (25) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} ## Plot a regional time course To summarize the ROI over time, we average over the spatial dimensions. With Xarray, reductions such as `.mean` take dimension names like `("k", "j", "i")` rather than integer axis indices, which makes the intent much clearer than the NumPy-style `axis=(...)` equivalent. Note that the underlying array dimensions are the voxel-space `k`/`j`/`i` axes; the world `z`/`y`/`x` coordinates attached to them are what makes coordinate-based `.sel` calls like the ROI selection above possible. ```python roi_trace = roi.mean(("k", "j", "i")) roi_trace ``` xarray.DataArray 'sub-CR022\_ses-20201011\_task-spontaneous\_acq-slice03\_pwd' - time: 751 - [ ] 6.635e+04 6.656e+04 6.645e+04 ... 6.653e+04 6.783e+04 6.571e+04 ``` array([66348.41 , 66557.016, 66450.04 , 67012.49 , 68144.78 , 68372.79 , 67921.1 , 68469.25 , 68890.71 , 67069.58 , 65436.324, 64262.84 , 65602.41 , 65938.49 , 66399.56 , 65153.65 , 63270.293, 65269.168, 67039.164, 67905.305, 68734.445, 66779.84 , 65605.56 , 66292.42 , 67121.12 , 67980.32 , 68977.66 , 69240.414, 67765.61 , 66217.65 , 63970.117, 67184.92 , 68357.95 , 65556.1 , 65583.58 , 68190.2 , 67335.26 , 67154.64 , 66480.61 , 66404.164, 65663.086, 66078.74 , 65832.52 , 64738.445, 66136.05 , 63371.164, 62459.168, 63203.66 , 64275.445, 63713.633, 61376.543, 59790.59 , 57591.227, 55730.71 , 56647.2 , 54759.715, 51221.914, 52965.773, 55159.824, 57615.84 , 58986.402, 59964.27 , 60136.88 , 60874.65 , 58757.555, 58061.04 , 56182.293, 56041.047, 57076.32 , 58261.273, 56741.527, 56287.008, 62313.113, 62347.316, 63996.84 , 64713.707, 63709.555, 60253.766, 60759.66 , 56687.83 , 54837.04 , 53100.68 , 55049.348, 54970.59 , 55355.984, 57016.164, 57815.85 , 58763.09 , 57906.36 , 55199.902, 52410.145, 53630.35 , 53393.18 , 53412.445, 52818.207, 52445.332, 54071.344, 53151.9 , 55913.324, 54888.117, 53394.56 , 54705.332, 54446.906, 51611.812, 53421.26 , 51009.047, 53835.297, 55573.95 , 54263.43 , 58182.863, 57732.68 , 57438.51 , 56453.465, 56926.09 , 57313.305, 59335.188, 58494.94 , 58023.418, 58466.703, 56910.598, ... 62800.184, 64596.082, 60621.746, 58751.543, 57612.87 , 55740.2 , 53596.93 , 52002.83 , 52876.074, 51686.086, 52161.203, 52125.19 , 56288.27 , 55193.66 , 55703.406, 54894.25 , 54649.594, 55288. , 53983.594, 55507.773, 55747.605, 55503.895, 57926.18 , 57712.594, 57642.18 , 56274.074, 56258.555, 56250.11 , 57111.58 , 56504.676, 57320.49 , 55064.59 , 55482.023, 54983.844, 55895.94 , 53971.42 , 53948.85 , 54405.176, 55606.496, 55052.133, 56424.19 , 57774. , 60459.79 , 61509.203, 62188.14 , 62561.008, 61988.496, 62394.887, 62853.71 , 62584.25 , 62289.47 , 60180.613, 58560.766, 59464.75 , 59374.035, 58590.332, 58315.652, 57802.043, 58368.395, 56826.805, 58152.406, 59723.254, 60707.84 , 59914.598, 60431.367, 58878.004, 58409.24 , 59576.805, 59844.785, 60277.582, 59497.55 , 58059.453, 57816.06 , 57585.453, 60758.086, 59732.105, 60550.277, 61746.934, 61891.395, 62957.465, 64076.844, 64144.215, 65131.863, 66848.12 , 69217.64 , 66896.79 , 67886.78 , 65924.86 , 66506.016, 66236.22 , 65263.81 , 66744.086, 63441.13 , 61573.98 , 61871.69 , 63669.99 , 65150.76 , 65755.06 , 65188.97 , 62844.523, 62633.242, 61690.93 , 60862.31 , 61990.387, 61120.254, 65677.25 , 67520.95 , 62751.688, 62723.195, 63534.07 , 65492.758, 67111.61 , 66533.625, 67830.9 , 65706.05 ], dtype=float32) ``` - [x] Coordinates: (1) - time (time) float64 10.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,)) ``` - [ ] Attributes: (25) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} The resulting 1D DataArray keeps the time coordinate from the original recording, so plotting it with Xarray's built-in plotting function automatically uses the time values on the *x*-axis. As one slightly more advanced example, we can also compute a rolling mean with Xarray to smooth short-term fluctuations, while still keeping the same labeled time axis. ```python roi_trace_smooth = roi_trace.rolling(time=15, center=True).mean() fig, ax = plt.subplots(figsize=(7, 3), facecolor=bg_color) roi_trace.plot(ax=ax, color="#d93a54", label="ROI mean") roi_trace_smooth.plot(ax=ax, color="#3ad9a4", label="Rolling mean (15 samples)") ax.set_title("Mean power Doppler intensity in a central ROI") ax.set_xlabel("Time (s)") ax.set_ylabel("Power Doppler intensity (a.u.)") _ = ax.legend(loc="upper right") ``` ## Compute and plot the mean power Doppler volume This recording has a single brain slice, so averaging over time leaves a 2D image. We convert it to decibel-scale and display it using ConfUSIus's `plot_volume` and `.fusi.scale` accessors. ```python mean_db = data.mean("time").fusi.scale.db() plotter = cf.plotting.plot_volume( mean_db, cmap="gray", cbar_label="Power Doppler (dB)", bg_color=bg_color, ) ``` ## Mask low-intensity pixels with `where` Xarray's `.where` keeps the same coordinates and dimensions while masking values that do not satisfy a condition. Here we suppress low-intensity pixels in the mean image before plotting it again. ```python mean_db_masked = mean_db.where(mean_db > -20) plotter = mean_db_masked.fusi.plot.volume( cmap="gray", cbar_label="Power Doppler (dB)", bg_color=bg_color ) ``` ## Save a processed result ConfUSIus can also save DataArrays back to disk through the `.fusi.save` method. For example, we can save the masked mean image as a NIfTI file. ```python mean_db_masked.fusi.save( "sub-CR022_ses-20201011_task-spontaneous_acq-slice03_desc-maskedmean_pwd.nii.gz" ) ``` --- **Total running time:** 10.1 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/io/confusius_xarray_101/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. # Create a VoxelData array from a MAT file This example downloads a power Doppler MAT file from the public dataset accompanying [Rabut *et al.* (2024)]()[^/dev/examples/_built/io/create_voxeldata_from_mat/#1][^/dev/examples/_built/io/create_voxeldata_from_mat/#collection] and wraps it in a VoxelData array with `create_voxeldata`. Since MAT files can contain any custom data, ConfUSIus cannot read them directly. The point of this example is to show the shortest path from a lab-specific MAT file array plus metadata to a VoxelData array. This example then reproduces figure 4D from Rabut *et al.* (2024) using a simple general linear model. ```python from functools import partial from pathlib import Path import h5py import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np import pandas as pd import pooch import xarray as xr from remotezip import RemoteZip import confusius as cf from confusius.glm import FirstLevelModel, gamma_hrf # Adapt colors to the current Matplotlib style. bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) is_dark_theme = sum(mpl.colors.to_rgb(bg_color)) / 3 < 0.5 stat_cmap = "berlin" if is_dark_theme else None # Keep notebook output compact for large DataArray displays. _ = xr.set_options(display_expand_attrs=False, display_expand_data=False) ``` ## Download one recording from the Rabut *et al.* (2024) dataset The Caltech record stores all data in a single ZIP archive. We extract a single MAT file from the archive to avoid downloading the full dataset. The MAT file contains a power Doppler recording of a human subject performing a simple task, with accompanying timestamps, task labels, and ultrasound metadata. ```python RECORD_URL = "https://data.caltech.edu/api/records/f3y3k-em558/files/data.zip/content" RECORD_DOI = "https://doi.org/10.22002/f3y3k-em558" MEMBER = "data/human/S2R1.mat" cache_dir = Path(pooch.os_cache("confusius")) / "rabut_2024_human_glm" cache_dir.mkdir(parents=True, exist_ok=True) mat_path = cache_dir / Path(MEMBER).name if not mat_path.exists(): with RemoteZip(RECORD_URL) as archive: with archive.open(MEMBER) as source, mat_path.open("wb") as target: target.write(source.read()) ``` ## Load the power Doppler array and metadata This MAT file v7.3 is HDF5-backed, so we use `h5py.File` to open it. We flip the lateral axis to match the orientation of the original paper figures. ```python with h5py.File(mat_path, "r") as mat: doppler = np.flip(mat["dop"][()].astype("float32"), axis=1) task = mat["task"][:, 0].astype("float32") timestamps = mat["timestamps"][0] wavelength = float(mat["UF/Lambda"][0, 0]) run_label = "".join(map(chr, mat["run_label"][()].ravel().astype(int))).strip() print(run_label) print(f"Data shape: {doppler.shape}") ``` ```text Session 2 Run 1 - Connect-the-dots; 60 seconds off, 30 seconds on; 3 blocks Data shape: (270, 128, 240) ``` ## Wrap the raw array with `create_voxeldata` The MATLAB array is `(time, i, j)` natively: 128 lateral positions matching the probe pitch, and 240 depth samples. ConfUSIus adds the missing singleton `k` axis and returns the canonical `(time, k, j, i)` layout, with `z`/`y`/`x` attached as world coordinates. The timestamps have small acquisition jitter, so we pass them as an exact coordinate. Following the authors' analysis code, we use the acoustic wavelength from `UF.Lambda` for axial (`y`) spacing and the 0.3 mm probe pitch for lateral (`x`) spacing. With no explicit `x0`/`y0`, ConfUSIus applies its default convention of using the probe surface as the origin. See the [Spatial Conventions]() guide for details. ```python power_doppler = cf.create_voxeldata( doppler, dims=("time", "i", "j"), time=timestamps, spacing=(1.0, wavelength, 0.3), name="power_doppler", attrs={"source": RECORD_DOI, "source_member": MEMBER}, ) # We crop depths between 10 and 30 mm to focus on the part of the image with good SNR. power_doppler = power_doppler.sel(y=slice(10.0, 30.0)) time_step = float(np.median(np.diff(timestamps))) power_doppler = cf.timing.resample_to_uniform_time(power_doppler, step=time_step) task_da = xr.DataArray(task, dims="time", coords={"time": timestamps}) task_da = cf.timing.resample_to_uniform_time( task_da, start=float(power_doppler.time[0]), stop=float(power_doppler.time[-1]), step=time_step, method="nearest", ) power_doppler ``` xarray.DataArray 'power\_doppler' - time: 270 - k: 1 - j: 97 - i: 128 - [ ] 2.106e+11 2.707e+11 3.04e+11 ... 2.945e+11 2.652e+11 2.58e+11 ``` array([[[[2.10577752e+11, 2.70662992e+11, 3.04037921e+11, ..., 1.40320457e+11, 1.24857090e+11, 1.24517777e+11], [2.14506717e+11, 2.85069410e+11, 3.43384588e+11, ..., 1.39885674e+11, 1.25527794e+11, 1.19126106e+11], [2.28316201e+11, 2.33571320e+11, 2.40582951e+11, ..., 1.35647912e+11, 1.29280238e+11, 1.03471677e+11], ..., [2.79672553e+11, 3.12990728e+11, 3.13475138e+11, ..., 2.91363127e+11, 2.69979779e+11, 2.63551336e+11], [2.83264451e+11, 2.91544793e+11, 2.94284689e+11, ..., 3.15664105e+11, 3.20567771e+11, 2.88501793e+11], [2.64020197e+11, 3.03924871e+11, 2.98198532e+11, ..., 3.05831346e+11, 2.91014967e+11, 2.96275345e+11]]], [[[2.40716349e+11, 2.83655930e+11, 2.91034399e+11, ..., 1.40351816e+11, 1.28603267e+11, 1.16492993e+11], [2.23595692e+11, 2.71761261e+11, 3.31479122e+11, ..., 1.50428025e+11, 1.35002079e+11, 1.19212335e+11], [2.60899865e+11, 2.21150151e+11, 2.92841685e+11, ..., ... [2.65342910e+11, 2.97345876e+11, 3.04962175e+11, ..., 3.06916491e+11, 2.83746206e+11, 2.60658774e+11], [3.05788289e+11, 3.07075613e+11, 2.99803378e+11, ..., 3.13945457e+11, 2.89581498e+11, 2.67835310e+11]]], [[[2.67361944e+11, 2.70543176e+11, 2.89336459e+11, ..., 1.52472945e+11, 1.36244838e+11, 1.19496065e+11], [1.91441994e+11, 2.21729587e+11, 2.77770207e+11, ..., 1.55218772e+11, 1.39294130e+11, 1.27337898e+11], [1.93200882e+11, 2.30568772e+11, 2.62727549e+11, ..., 1.46543788e+11, 1.28960111e+11, 1.17371699e+11], ..., [2.76503560e+11, 2.92960109e+11, 3.06788041e+11, ..., 2.80144019e+11, 2.67253187e+11, 2.87594611e+11], [2.90532295e+11, 2.77860581e+11, 3.09633155e+11, ..., 2.86310105e+11, 2.79444390e+11, 2.55675875e+11], [2.93681136e+11, 3.02512800e+11, 3.20574489e+11, ..., 2.94487654e+11, 2.65201844e+11, 2.58004632e+11]]]], shape=(270, 1, 97, 128), dtype=float32) ``` - [x] Coordinates: (7) - time (time) float64 0.0 1.614 3.228 ... 432.6 434.2 ☐ ☐ **units :** s **volume\_acquisition\_reference :** start **volume\_acquisition\_duration :** 1.613999999994121 ``` array([ 0. , 1.614, 3.228, ..., 430.938, 432.552, 434.166], shape=(270,)) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 49 50 51 52 53 ... 142 143 144 145 ☐ ☐ ``` array([ 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145]) ``` - i (i) int64 0 1 2 3 4 5 ... 123 124 125 126 127 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]) ``` - z (k, j, i) float64 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 ☐ ☐ **units :** mm ``` [12416 values with dtype=float64] ``` - y (k, j, i) float64 10.16 10.16 10.16 ... 29.88 29.88 ☐ ☐ **units :** mm ``` [12416 values with dtype=float64] ``` - x (k, j, i) float64 \-19.05 -18.75 ... 18.75 19.05 ☐ ☐ **units :** mm ``` [12416 values with dtype=float64] ``` - [ ] Indexes: (5) - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], dtype='int64', name='i', length=128)) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145], dtype='int64', name='j')) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0.0, 1.613999999994121, 3.227999999988242, 4.841999999982363, 6.455999999976484, 8.069999999970605, 9.683999999964726, 11.297999999958847, 12.911999999952968, 14.52599999994709, ... 419.63999999847147, 421.2539999984656, 422.8679999984597, 424.48199999845383, 426.09599999844795, 427.7099999984421, 429.3239999984362, 430.9379999984303, 432.55199999842444, 434.16599999841856], dtype='float64', name='time', length=270)) ``` - [ ] Attributes: (2) **source :** https://doi.org/10.22002/f3y3k-em558 **source\_member :** data/human/S2R1.mat ## Plot the mean power Doppler image Once the custom file is represented as a DataArray, ConfUSIus plotting helpers work the same way as they do for built-in loaders. ```python mean_doppler = power_doppler.mean("time").fusi.scale.db() plotter = mean_doppler.fusi.plot.volume( cbar_label="Power Doppler (dB)", bg_color=bg_color, ) ``` ## Motion-correct, smooth, and fit a simple task GLM The paper reports rigid-body motion correction, 2D Gaussian spatial smoothing, temporal smoothing, detrending, and baseline scaling before the GLM. We reproduce all of these steps with ConfUSIus below. ```python # The default learning rate is conservative; for this recording, 1.0 recovers the motion # better. registered = power_doppler.fusi.register.volumewise(learning_rate=1.0) # The paper reports a 2D Gaussian smoothing kernel with FWHM = 0.471 mm in both # spatial dimensions. smoothed = cf.spatial.smooth_volume(registered, fwhm=0.471) # The paper reports a simple moving average for temporal smoothing. filtered = smoothed.rolling(time=6, min_periods=1).mean() # The paper reports baseline scaling to percent signal change relative to the mean of # the rest blocks. baseline = filtered.where(task_da == 0).mean("time") scaled = 100 * filtered / baseline time_values = power_doppler.time.values edges = np.diff(np.r_[0.0, task_da.values, 0.0]) starts = np.flatnonzero(edges == 1.0) stops = np.flatnonzero(edges == -1.0) stop_times = np.r_[time_values, time_values[-1] + time_step][stops] events = pd.DataFrame( { "onset": time_values[starts], "duration": stop_times - time_values[starts], "trial_type": "task", } ) # We handle slow drift in FirstLevelModel with a cosine drift model. The task regressor # is convolved with the single-gamma human fUSI HRF reported in the paper (`τ = 0.7`, `δ # = 3 s`, `n = 3`). tau = 0.7 n = 3 human_fusi_hrf = partial( gamma_hrf, time_length=16.0, peak_delay=(n - 1) * tau, dispersion=tau, onset=3.0, ) model = FirstLevelModel( hrf_model=human_fusi_hrf, noise_model="ols", drift_model="cosine", low_cutoff=0.002 ) model.fit(scaled, events=events) z_map = model.compute_contrast("task") z_map ``` xarray.DataArray 'zscore' - k: 1 - j: 97 - i: 128 - [ ] \-3.894 -3.584 -3.501 -3.76 -3.985 ... -8.179 -10.94 -13.72 -12.56 ``` array([[[ -3.89375359, -3.58374338, -3.50061794, ..., -8.74608039, -7.79345128, -6.99100112], [ -1.51661279, -1.08157274, -0.89801282, ..., -7.08660521, -5.8910446 , -4.75516962], [ 2.85558525, 3.98647844, 3.8977053 , ..., -4.53545883, -4.30102864, -3.78234777], ..., [ 1.85772519, 3.81928748, 6.30792831, ..., -9.40418385, -12.4964889 , -12.27100534], [ 1.8068629 , 3.91858836, 5.82987122, ..., -10.92740772, -13.19299566, -12.42946494], [ 1.52253314, 3.68687344, 4.26127091, ..., -10.94084976, -13.72436883, -12.56125771]]], shape=(1, 97, 128)) ``` - [x] Coordinates: (6) - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 49 50 51 52 53 ... 142 143 144 145 ☐ ☐ ``` array([ 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145]) ``` - i (i) int64 0 1 2 3 4 5 ... 123 124 125 126 127 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]) ``` - z (k, j, i) float64 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 ☐ ☐ **units :** mm ``` [12416 values with dtype=float64] ``` - y (k, j, i) float64 10.16 10.16 10.16 ... 29.88 29.88 ☐ ☐ **units :** mm ``` [12416 values with dtype=float64] ``` - x (k, j, i) float64 \-19.05 -18.75 ... 18.75 19.05 ☐ ☐ **units :** mm ``` [12416 values with dtype=float64] ``` - [ ] Indexes: (4) - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145], dtype='int64', name='j')) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], dtype='int64', name='i', length=128)) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (6) **source :** https://doi.org/10.22002/f3y3k-em558 **source\_member :** data/human/S2R1.mat **reference\_time :** 0 **motion\_params :** rot\_x rot\_y rot\_z trans\_x trans\_y \\ time 0.000 0.000000e+00 -0.000000e+00 0.000000 0.000000 0.000000 1.614 -2.385743e-20 -1.537921e-21 -0.000113 0.018049 -0.035453 3.228 1.587875e-20 2.536183e-21 0.000429 0.013651 -0.029464 4.842 -1.246469e-20 1.004451e-20 -0.002249 0.057678 -0.034945 6.456 -3.078363e-20 4.139844e-21 -0.000514 0.018776 -0.026100 ... ... ... ... ... ... 427.710 1.354237e-20 6.209528e-21 -0.002377 0.481931 -0.201410 429.324 7.081140e-21 -1.168633e-22 -0.002686 0.494626 -0.206359 430.938 1.402747e-20 -4.711797e-21 -0.002658 0.489819 -0.202756 432.552 7.098228e-21 5.861992e-21 -0.002656 0.486414 -0.201453 434.166 1.627462e-20 -1.658643e-21 -0.002366 0.479012 -0.197452 trans\_z mean\_fd max\_fd rms\_fd final\_metric\_value \\ time 0.000 0.000000e+00 0.038819 0.041227 0.038837 -1.000000 1.614 -4.776291e-19 0.010275 0.020129 0.011103 -0.955446 3.228 3.178707e-19 0.032327 0.067029 0.035230 -0.965001 4.842 -2.490902e-19 0.021443 0.047030 0.023788 -0.925374 6.456 -6.162456e-19 0.029920 0.043512 0.030456 -0.959677 ... ... ... ... ... ... 427.710 2.714131e-19 0.008720 0.014461 0.009051 -0.722658 429.324 1.417576e-19 0.005579 0.006129 0.005584 -0.722090 430.938 2.805783e-19 0.003594 0.003639 0.003594 -0.724732 432.552 1.424177e-19 0.004982 0.010523 0.005636 -0.736636 434.166 3.257384e-19 0.000000 0.000000 0.000000 -0.743093 n\_iterations status time 0.000 31 completed 1.614 14 completed 3.228 32 completed 4.842 46 completed 6.456 20 completed ... ... ... 427.710 12 completed 429.324 12 completed 430.938 13 completed 432.552 13 completed 434.166 12 completed \[270 rows x 12 columns\] **long\_name :** zscore **cmap :** coolwarm ## Plot the GLM map The z-map can be overlaid on the mean Doppler image with ConfUSIus's statistical map plotting helper. This is a lightweight reproduction of the Fig. 4D GLM map; here we threshold at the top 5% of positive task z-scores. ```python active_threshold = float(z_map.quantile(0.95)) active_voxels = z_map > active_threshold plotter = z_map.fusi.plot.stat_map( bg_volume=mean_doppler, threshold=active_threshold, cmap=stat_cmap, cbar_label="task z-score", bg_color=bg_color, ) ``` ## Plot the strongest task-positive voxels We average the Doppler traces from those task-positive voxels. This gives a quick sanity check that the voxels highlighted by the GLM follow the task blocks more clearly than the whole-plane average. ```python active_signal = scaled.where(active_voxels).mean(("k", "j", "i")) mean_signal = scaled.mean(("k", "j", "i")) fig, ax = plt.subplots(figsize=(9, 4), facecolor=bg_color) mean_signal.plot(ax=ax, color="#808080", label="Whole-plane mean") active_signal.plot(ax=ax, color="#d93a54", label="Top task-positive voxels") for idx, event in enumerate(events.itertuples()): label = "Task on" if idx == 0 else None ax.axvspan( event.onset, event.onset + event.duration, color="#3ad9a4", alpha=0.12, label=label, ) ax.set_title("Task-positive voxel time course") ax.set_xlabel("Time (s)") ax.set_ylabel("Signal (% rest baseline)") _ = ax.legend(loc="upper left") ``` --- **Total running time:** 40.7 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/io/create_voxeldata_from_mat/#collection]: This example downloads an external archive, not a collection release. See [The ConfUSIus Dataset Collection]() for the fUSI-BIDS studies and templates hosted by ConfUSIus. [^/dev/examples/_built/io/create_voxeldata_from_mat/#1]: Rabut, Claire, et al. “Functional Ultrasound Imaging of Human Brain Activity through an Acoustically Transparent Cranial Window.” *Science Translational Medicine*, vol. 16, no. 749, May 2024, p. eadj3143. DOI.org (Crossref), [https://doi.org/10.1126/scitranslmed.adj3143](). # The ConfUSIus Dataset Collection ConfUSIus provides access to public fUSI datasets and brain templates from several studies. The [datasets user guide]() describes the available data, fetchers, and source publications; the [collection repository]() documents preparation recipes, provenance, and per-source licenses. This example introduces the collection's organization and access conventions. The other [gallery examples]() demonstrate how to use these data for registration, decomposition, connectivity, and statistical analysis. ## Browse the collection The public S3 bucket contains two prefixes, `datasets/` and `templates/`, with immutable releases under `//`. The `last_versions.conf` catalog lists the latest published versions; each release's `manifest.json` records its file paths, sizes, and SHA-256 checksums. Fetchers resolve the latest published release on the first download, then reuse the cached version. Pass `refresh=True` to check for a newer release without overwriting an earlier one. Set `data_dir` for a particular fetch, or the `CONFUSIUS_DATA` environment variable to choose the download-cache directory for all fetchers (for example, `export CONFUSIUS_DATA=/path/to/cache`). `data_dir` takes priority; otherwise the platform cache directory is used by default. `list_datasets` prints the available fetchers, full dataset/template sizes, and whether their metadata is cached locally, without downloading data. ```python from pathlib import Path import matplotlib as mpl import xarray as xr import confusius as cf xr.set_options(display_expand_data=False) bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) cf.datasets.list_datasets() ``` ``` Available Datasets ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┓ ┃ Fetch function ┃ Size ┃ On disk ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━┩ │ fetch_nunez_elizalde_2022 │ 6.983 GB │ ✓ │ │ fetch_pereira_2025 │ 29.96 GB │ ✗ │ │ fetch_cybis_pereira_2026 │ 12.88 GB │ ✓ │ │ fetch_pepe_mariani_2026 │ 37.56 GB │ ✓ │ │ fetch_landemard_2026 │ 42.04 GB │ ✗ │ │ fetch_khallaf_2026 │ 24.02 GB │ ✓ │ │ fetch_template_brunner_grillet_2021 │ 16.34 MB │ ✗ │ │ fetch_template_pepe_mariani_2026 │ 5.508 MB │ ✓ │ └─────────────────────────────────────┴──────────┴─────────┘ ``` ## Data formats Datasets follow fUSI-BIDS, the functional ultrasound extension of [BIDS](); see the [current specification draft](). Images are stored as [NIfTI]() files with acquisition, processing, and timing metadata in JSON sidecars. `confusius.load` loads both automatically, merging matching sidecars according to the BIDS inheritance principle: more specific metadata overrides less specific metadata. Templates are reference NIfTI volumes for spatial alignment; their fetchers return VoxelData arrays directly. The [gallery]() shows how to use collection datasets and templates in analysis workflows. Each dataset and template retains its own license. Fetchers print source citations by default (`print_citation=False` disables this); citations and per-source licenses are also listed on the [citing page](). ## A small structural preview As a quick view of the collection, fetch one structural ultrasound image from [Cybis Pereira et al. (2026)](). Dataset fetchers accept study-specific filters such as subject, session, acquisition, and datatype. Matching JSON sidecars and supporting BIDS metadata are included, and downloaded files are verified against the release manifest. For a fuller introduction to downloading, loading, and manipulating recordings, see [ConfUSIus and Xarray 101](). ```python bids_root = Path( cf.datasets.fetch_cybis_pereira_2026( datasets="rawdata", subjects="rat75", sessions="20220523", acqs="slice32", datatypes="susi", ) ) image_path = ( bids_root / "sub-rat75" / "ses-20220523" / "susi" / "sub-rat75_ses-20220523_acq-slice32_rec-minframe2d_pwd.nii.gz" ) assert image_path.with_suffix("").with_suffix(".json").is_file() image = cf.load(image_path).compute() assert {"k", "j", "i"}.issubset(image.dims) print("Release:", bids_root.name, "\nVoxel dimensions:", dict(image.sizes)) ``` ``` If you use this dataset in your work, please cite the following source: Cybis Pereira, F., Castedo, S. H., Meur-Diebolt, S. L., Ialy-Radio, N., Bhattacharya, S., Ferrier, J., Osmanski, B. F., Cocco, S., Monasson, R., Pezet, S., & Tanter, M. (2026). A vascular code for speed in the spatial navigation system. Cell Reports, 45(1). https://doi.org/10.1016/j.celrep.2025.116791 ``` ```text Release: 1.0.0 Voxel dimensions: {'k': 1, 'j': 112, 'i': 128} ``` This is a static anatomical reference, with vessels that can serve as alignment landmarks. Display it in the source's metric `qform` space, as in [two-session registration](). ```python image_qform = image.fusi.affine.apply(image.attrs["affines"]["world_to_qform"]) plotter = image_qform.fusi.scale.db().fusi.plot.volume( cmap="gray", cbar_label="Power Doppler (dB)", bg_color=bg_color, ) ``` ## Explore the data further The worked analysis examples show how to answer scientific questions with these recordings, including the preprocessing, models, and visualizations: - [Lagged GLM](): how does vascular activity relate to locomotion speed? Reproduce the Cybis Pereira analysis with a continuous behavioral regressor at different temporal lags. - [PCA](): which spatial patterns explain the dominant variance in a recording? Extract component maps and time courses. - [Task GLM](): which regions respond to olfactory stimulation in the Khallaf recordings? Fit and visualize a stimulus contrast. - [Regional connectivity](): how do atlas-defined regional signals covary in the Nunez-Elizalde recordings? Beyond these examples, the ConfUSIus dataset collection can support further analyses and cross-study meta-analyses, taking each study's acquisition and processing differences into account. See the [citing page]() for source references and licenses. --- **Total running time:** 4.8 s [Launch in Binder]() [Download .py]() [Download .ipynb]() # Load multi-pose recordings This example shows how to assemble a multi-pose fUSI recording that was not acquired through an Iconeus SCAN file, by loading each pose as a separate NIfTI file and stacking them into a single VoxelData array with pose-dependent voxel-to-world geometry. The Pepe, Mariani et al. (2026) dataset[^/dev/examples/_built/io/load_multipose_recordings/#collection] contains transcranial mouse resting-state recordings acquired with a linear probe stepped across several positions. In the fUSI-BIDS export, each probe position is one file, distinguished by the `chunk-` entity (`chunk-0`, `chunk-1`, ...); this is exactly the "Other Systems" case described in the [Multi-Pose Imaging guide]() — data must be assembled manually rather than loaded as one file, unlike Iconeus SCAN's `3Dscan`/`4Dscan` modes. ## Fetch one recording's chunks We select one subject, session, and acquisition so only the four chunks belonging to a single recording are downloaded, rather than the full multi-gigabyte dataset. ```python import re from pathlib import Path import matplotlib as mpl import xarray as xr import confusius as cf # Adapt background color to the current Matplotlib style. bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) xr.set_options(display_expand_data=False) subject = "cp230420a" session = "1MEDISOses5" acq = "3dfusi" bids_root = cf.datasets.fetch_pepe_mariani_2026( datasets="rawdata", subjects=subject, sessions=session, acqs=acq, datatypes="fusi", ) ``` ``` If you use this dataset in your work, please cite the following source: Pepe, C., Mariani, J.-C., Urosevic, M., Gini, S., Stuefer, A., Ricci, F., Galbusera, A., Iurilli, G., & Gozzi, A. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (Fusi). https://doi.org/10.64898/2026.02.05.704055 ``` ## Locate and load every chunk Each chunk is an ordinary single-pose fUSI recording — `(time, k, j, i)` with a singleton `k`, since a linear probe images one elevation slice per pose — with its own `voxel_to_world` affine derived from that file's own NIfTI header. We sort by the `chunk-` index so poses end up in acquisition order, then load and compute each one (they're small enough to fit comfortably in memory). ```python fusi_dir = Path(bids_root) / f"sub-{subject}" / f"ses-{session}" / "fusi" chunk_glob = ( f"sub-{subject}_ses-{session}_task-rest_acq-{acq}_probe-linear_chunk-*_pwd.nii.gz" ) chunk_paths = sorted( fusi_dir.glob(chunk_glob), key=lambda p: int(re.search(r"chunk-(\d+)", p.name).group(1)), ) chunks = [cf.load(p).compute() for p in chunk_paths] npose = len(chunks) print(f"{npose} poses, each shaped {chunks[0].dims} = {chunks[0].shape}") ``` ```text 4 poses, each shaped ('time', 'k', 'j', 'i') = (1500, 1, 82, 128) ``` Each chunk's own origin confirms the poses sit at different physical positions along elevation (`z`), stepped by about 1 mm, and were acquired with slightly offset start times — this is the per-pose geometry we want to preserve, not collapse into one shared affine. `y`/`x` origins don't vary across chunks, so we only show `z`/`time`. ```python origins = [chunk.fusi.origin for chunk in chunks] print("z origins:", tuple(origin["z"] for origin in origins)) print("time origins:", tuple(origin["time"] for origin in origins)) ``` ```text z origins: (0.00559999980032444, -0.9943992495536804, -1.9943994283676147, -2.9953997135162354) time origins: (0.0, 0.6, 1.2, 1.8) ``` ## Stack the poses into one pose-dependent DataArray `stack_poses` concatenates independently loaded single-pose grids into one DataArray with a `pose` dimension, one voxel-to-world affine per pose. Poses were also acquired sequentially rather than simultaneously, so each chunk's `time` coordinate is offset from the others by a fraction of the repetition time; when per-pose timestamps differ, `stack_poses` keeps them as a pose-dependent `(time, pose)`-shaped `time` coordinate rather than collapsing them into one shared time axis. ```python multipose = cf.multipose.stack_poses(chunks) ``` ## Pose-transparent vs. pose-specific geometry Some geometry queries are well-defined without picking a pose: voxel spacing must be identical across poses (a stacked affine with differing scale is rejected at construction), so `.fusi.spacing` works directly on the multi-pose array. Origin and direction, however, are inherently single-grid concepts—there is no one answer for "the origin" of a stack of differently-positioned grids—so they require selecting a scalar pose first. > [!WARNING] > > **World-coordinate `.sel()` requires a scalar pose** > > `.sel(z=..., y=..., x=...)` raises `ValueError` on pose-dependent data unless `pose` was already reduced to a scalar, e.g. `.isel(pose=0).sel(z=..., y=..., x=...)`. Resolving a world coordinate to a voxel means picking one pose's affine first; ConfUSIus never resamples poses onto a shared grid implicitly. ```python pose0_origin = multipose.isel(pose=0).fusi.origin pose3_origin = multipose.isel(pose=3).fusi.origin print("spacing (pose-transparent):", multipose.fusi.spacing) print("z origins (pose 0, 3):", (pose0_origin["z"], pose3_origin["z"])) print("time origins (pose 0, 3):", (pose0_origin["time"], pose3_origin["time"])) ``` ```text spacing (pose-transparent): {'time': 2.400000000000091, 'pose': 1.0, 'k': 0.4000000059604645, 'j': 0.10000000149011612, 'i': 0.10999999940395355} z origins (pose 0, 3): (0.00559999980032444, -2.9953997135162354) time origins (pose 0, 3): (0.0, 1.8) ``` ## Visualize each pose We plot the temporal mean of each pose side by side with `plot_volume`'s `slice_mode="pose"`, which labels each panel by its `pose` value. > [!WARNING] > > **World-coordinate slice modes require a scalar pose** > > For the same reason as `.sel`, `plot_volume(..., slice_mode="z")` (or `"y"`/`"x"`) also raises `ValueError` on pose-dependent data, since display is always in world space. Slicing along `pose` itself works fine, since it doesn't require picking one affine. ```python mean_db = multipose.mean("time").fusi.scale.db().isel(k=0) plotter = cf.plotting.plot_volume( mean_db, slice_mode="pose", cbar_label="Power Doppler (dB)", bg_color=bg_color, yincrease=True, ) ``` ## Consolidate into a single volume Since every pose here shares the same rotation and is offset by a pure translation along elevation, `consolidate_poses` can merge `pose` and the swept voxel dimension (`k`, the default) into one physically ordered axis — the same operation `load_scan`-produced `3Dscan`/`4Dscan` data goes through, now working directly off the DataArray's own per-pose geometry rather than a separately stored affine. ```python consolidated = cf.multipose.consolidate_poses(multipose) consolidated ``` xarray.DataArray 'scan\_data' - time: 1500 - k: 4 - j: 82 - i: 128 - [ ] 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ``` array([[[[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]], [[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]], [[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., ... ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]], [[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]], [[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]]]], shape=(1500, 4, 82, 128)) ``` - [x] Coordinates: (8) - time (time) float64 0.0 2.4 4.8 ... 3.595e+03 3.598e+03 ☐ ☐ **units :** s **volume\_acquisition\_reference :** start **volume\_acquisition\_duration :** 2.2 ``` array([0.0000e+00, 2.4000e+00, 4.8000e+00, ..., 3.5928e+03, 3.5952e+03, 3.5976e+03], shape=(1500,)) ``` - k (k) int64 0 1 2 3 ☐ ☐ ``` array([0, 1, 2, 3]) ``` - slice\_time (time, k) float64 1.8 1.2 0.6 ... 3.598e+03 3.598e+03 ☐ ☐ **units :** s **volume\_acquisition\_reference :** start **volume\_acquisition\_duration :** 0.3999999999999999 ``` array([[1.8000e+00, 1.2000e+00, 6.0000e-01, 0.0000e+00], [4.2000e+00, 3.6000e+00, 3.0000e+00, 2.4000e+00], [6.6000e+00, 6.0000e+00, 5.4000e+00, 4.8000e+00], ..., [3.5946e+03, 3.5940e+03, 3.5934e+03, 3.5928e+03], [3.5970e+03, 3.5964e+03, 3.5958e+03, 3.5952e+03], [3.5994e+03, 3.5988e+03, 3.5982e+03, 3.5976e+03]], shape=(1500, 4)) ``` - j (j) int64 0 1 2 3 4 5 6 ... 76 77 78 79 80 81 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81]) ``` - i (i) int64 0 1 2 3 4 5 ... 123 124 125 126 127 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]) ``` - z (k, j, i) float64 \-2.995 -2.995 ... 0.0056 0.0056 ☐ ☐ **units :** mm ``` [41984 values with dtype=float64] ``` - y (k, j, i) float64 \-0.0369 -0.0369 ... -8.137 -8.137 ☐ ☐ **units :** mm ``` [41984 values with dtype=float64] ``` - x (k, j, i) float64 0.0112 0.1212 ... 13.87 13.98 ☐ ☐ **units :** mm ``` [41984 values with dtype=float64] ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0.0, 2.4, 4.8, 7.199999999999999, 9.6, 12.0, 14.399999999999999, 16.8, 19.2, 21.599999999999998, ... 3576.0, 3578.4, 3580.7999999999997, 3583.2, 3585.6, 3588.0, 3590.4, 3592.7999999999997, 3595.2, 3597.6], dtype='float64', name='time', length=1500)) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0, 1, 2, 3], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81], dtype='int64', name='j')) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], dtype='int64', name='i', length=128)) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (25) **sform\_code :** 2 **manufacturer :** Iconeus **manufacturers\_model\_name :** Iconeus One **probe\_type :** linear **probe\_model :** IcoPrime **probe\_number\_of\_elements :** 128 **probe\_pitch :** 0.11 **probe\_radius\_of\_curvature :** 0 **depth :** \[1, 10\] **sequence\_name :** default **clutter\_filter\_window\_duration :** 0.4 **clutter\_filters :** \['Fixed-threshold SVD \[60-200\]'\] **power\_doppler\_integration\_duration :** 0.4 **probe\_center\_frequency :** 15.625 **transmit\_frequency :** 15.625 **pulse\_repetition\_frequency :** 5500 **transmit\_voltage :** 25 **probe\_focal\_width :** 0.4 **probe\_aperture :** 1.5 **probe\_focal\_depth :** 8 **plane\_wave\_angles :** \[-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10\] **compound\_sampling\_frequency :** 500 **task\_name :** Sedation **task\_description :** Sedated mouse in stereotaxic frame. **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} The consolidated volume has a genuine `k` extent of `npose` elevation slices (each pose here contributes exactly one, since the probe is linear) and no more `pose` dimension, positioned by real physical location rather than acquisition order. ```python plotter = cf.plotting.plot_volume( consolidated.mean("time").fusi.scale.db(), slice_mode="z", cmap="gray", cbar_label="Power Doppler (dB)", bg_color=bg_color, yincrease=True, ) ``` --- **Total running time:** 11.4 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/io/load_multipose_recordings/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. # Register a recording to an Allen fUSI template This example shows the full workflow for aligning a single-slice fUSI recording to one of ConfUSIus's Allen-space fUSI templates: fetch the recording and the template, initialize and refine the registration, inspect diagnostic overlays, resample the Allen Mouse Brain Atlas onto the recording's native grid, and save the aligned atlas for reuse. We use an awake freely-running acquisition from subject `CR022`, session `20201007`, in the Nunez-Elizalde 2022 dataset[^/dev/examples/_built/registration/register_to_allen_fusi_template/#collection], and the Pepe, Mariani 2026 fUSI template, which carries the affine transform required to bring it into Allen Common Coordinate Framework (CCF) space. ## Fetch the recording and the template The recording is a single coronal slice imaged for approximately 4 minutes at 3.33 Hz. Registration works on a static anatomical image, so we use the temporal mean, converted to decibels for a more stable dynamic range. ```python 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 # Adapt background color to the current Matplotlib style. bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) xr.set_options(display_expand_data=False) # The ConfUSIus datasets directory is a convenient place to cache the resampled atlas. CONFUSIUS_DATA_DIR = cf.datasets.get_datasets_dir() template = cf.datasets.fetch_template_pepe_mariani_2026().compute() bids_root = cf.datasets.fetch_nunez_elizalde_2022( subjects="CR022", sessions="20201007", tasks="spontaneous", acqs="slice02" ) ``` ``` If you use this template in your work, please cite the following source: Pepe, C., Mariani, J.-C., Urosevic, M., Gini, S., Stuefer, A., Ricci, F., Galbusera, A., Iurilli, G., & Gozzi, A. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (fUSI). bioRxiv. https://doi.org/10.64898/2026.02.05.704055 ``` ``` If you use this dataset in your work, please cite the following source: Nunez-Elizalde, A. O., Krumin, M., Reddy, C. B., Montaldo, G., Urban, A., Harris, K. D., & Carandini, M. (2022). Neural correlates of blood flow measured by ultrasound. Neuron, 110(10), 1631–1640.e4. https://doi.org/10.1016/j.neuron.2022.02.012 ``` ```python data_path = ( Path(bids_root) / "sub-CR022" / "ses-20201007" / "fusi" / "sub-CR022_ses-20201007_task-spontaneous_acq-slice02_pwd.nii.gz" ) moving = cf.load(data_path).mean(dim="time").fusi.scale.db().compute() moving ``` xarray.DataArray 'sub-CR022\_ses-20201007\_task-spontaneous\_acq-slice02\_pwd' - k: 1 - j: 125 - i: 80 - [ ] \-18.69 -18.57 -18.5 -18.46 -18.38 ... -13.37 -13.36 -13.33 -13.28 ``` array([[[-18.687603 , -18.568825 , -18.500853 , ..., -17.956799 , -17.938179 , -17.963793 ], [-18.534925 , -18.445103 , -18.356045 , ..., -17.885395 , -17.88491 , -17.8918 ], [-18.489155 , -18.406622 , -18.328014 , ..., -17.79321 , -17.804344 , -17.82952 ], ..., [-12.314958 , -13.39374 , -13.824751 , ..., -13.536394 , -13.521338 , -13.627226 ], [-12.319063 , -13.352967 , -13.748854 , ..., -13.456759 , -13.3859215, -13.444571 ], [-12.375979 , -13.218855 , -13.602358 , ..., -13.363948 , -13.333373 , -13.279113 ]]], shape=(1, 125, 80), dtype=float32) ``` - [x] Coordinates: (6) - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 120 121 122 123 124 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124]) ``` - i (i) int64 0 1 2 3 4 5 6 ... 74 75 76 77 78 79 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]) ``` - z (k, j, i) float64 0.4 0.4 0.4 0.4 ... 0.4 0.4 0.4 0.4 ☐ ☐ **units :** mm ``` array([[[0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], ..., [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001], [0.40000001, 0.40000001, 0.40000001, ..., 0.40000001, 0.40000001, 0.40000001]]], shape=(1, 125, 80)) ``` - y (k, j, i) float64 2.996 2.996 2.996 ... 8.988 8.988 ☐ ☐ **units :** mm ``` array([[[2.99584007, 2.99584007, 2.99584007, ..., 2.99584007, 2.99584007, 2.99584007], [3.04416007, 3.04416007, 3.04416007, ..., 3.04416007, 3.04416007, 3.04416007], [3.09248007, 3.09248007, 3.09248007, ..., 3.09248007, 3.09248007, 3.09248007], ..., [8.89087997, 8.89087997, 8.89087997, ..., 8.89087997, 8.89087997, 8.89087997], [8.93919997, 8.93919997, 8.93919997, ..., 8.93919997, 8.93919997, 8.93919997], [8.98751996, 8.98751996, 8.98751996, ..., 8.98751996, 8.98751996, 8.98751996]]], shape=(1, 125, 80)) ``` - x (k, j, i) float64 \-3.95 -3.85 -3.75 ... 3.85 3.95 ☐ ☐ **units :** mm ``` array([[[-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], ..., [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007], [-3.95000005, -3.85000005, -3.75000004, ..., 3.75000007, 3.85000007, 3.95000007]]], shape=(1, 125, 80)) ``` - [ ] Indexes: (4) - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 115, 116, 117, 118, 119, 120, 121, 122, 123, 124], dtype='int64', name='j', length=125)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79], dtype='int64', name='i')) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (27) **sform\_code :** 2 **qform\_code :** 2 **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\_center\_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 **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.991680000000001\] **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\] **transmit\_voltage :** 25.0 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} **units :** dB **scaling :** 10\*log10(x/max) ## Register the recording to the template The template is a 3D volume but the recording is a single slice. We can still register the recording to the template, but we need to initialize the registration with a rough guess of where the recording sits in the template, otherwise the registration algorithm may not converge to the right slice. To initialize the registration, we use an affine transform obtained using [napari's manual transform tool]() by placing the recording at an approximate location on the template. The transform is not perfect (notice the slight misalignment toward the bottom of the field of view), but it is close enough to allow the registration algorithm to converge to a good solution. `register_volume` expects a transform mapping `fixed` (template) world coordinates to `moving` (recording) world coordinates, so we invert the napari affine—which instead describes how to place the recording *into* the template's coordinate system—before using it as `initialization`. ```python # Copied and pasted transform after manual transformation in napari. napari_affine = np.array( [ [1.0, 0.0, 0.0, 5.594638656430411], [0.0, 1.0, 0.0, -2.50293925701927], [0.0, 0.0, 1.0, 5.6650243788545875], [0.0, 0.0, 0.0, 1.0], ] ) initialization = np.linalg.inv(napari_affine) # Crop the template to a thin band around the recording's expected location to improve # registration speed and visualization. target_z = napari_affine[0, 3] + moving.fusi.origin["z"] fixed = template.sel(z=slice(target_z - 1.0, target_z + 1.0)).fusi.scale.db() initialized = cf.registration.resample_like(moving, fixed, initialization) _ = cf.plotting.plot_composite( fixed, initialized, slice_coords=[target_z], normalize_strategy="per_slice", bg_color=bg_color, ) ``` We use an affine transform: on top of the rotation and translation a rigid transform would allow, it also captures small scale and shear differences between the recording and the template. ```python registered, affine, _ = cf.registration.register_volume( moving=moving, fixed=fixed, transform_type="affine", metric="correlation", convergence_window_size=50, number_of_iterations=500, learning_rate=1.0, initialization=initialization, show_progress=True, ) ``` The initialization was already close, so the refinement is small. Comparing the overlay before and after registration, alignment is slightly better after the affine refinement, most noticeably around the anterior choroidal arteries in the bottom part of the field of view. ```python fig, axes = plt.subplots(1, 2, figsize=(10, 4)) fig.patch.set_facecolor(bg_color) for ax, moving_view, title in [ (axes[0], initialized, "Manual initialization"), (axes[1], registered, "Affine registration refinement"), ]: cf.plotting.plot_composite( fixed, moving_view, axes=ax, slice_coords=[target_z], normalize_strategy="per_slice", bg_color=bg_color, ) ax.set_title(title) _ = fig.suptitle("Template (red) / recording (cyan)") ``` ## Resample the Allen atlas onto the recording's native grid The template is not itself expressed in Allen space, but it carries the affine transform to get there in `template.attrs["affines"]["world_to_sform"]`. Composing it with the inverse of the estimated registration affine gives a single transform from the recording's native coordinates directly to Allen atlas coordinates. `resample_like` then brings the atlas's template volume, annotations, and hemisphere map onto the recording's grid in one call. ```python world_to_sform = template.attrs["affines"]["world_to_sform"] subject_to_atlas = world_to_sform @ np.linalg.inv(affine) atlas = cf.datasets.fetch_brainglobe_atlas("allen_mouse_100um", check_latest=False) resampled_atlas = atlas.atlas.resample_like(moving, subject_to_atlas) ``` ## Save and reload the resampled atlas Registration and atlas resampling are expensive, so for downstream analyses of the same recording it is worth caching the aligned atlas once and reloading it later. `save_atlas` writes the arrays together with the structure hierarchy and meshes, so the reloaded atlas is immediately usable for masks, meshes, and plotting. ```python store_path = f"{CONFUSIUS_DATA_DIR}/resampled_allen_mouse_100um.zarr" cf.io.save_atlas(resampled_atlas, store_path, mode="w") del resampled_atlas resampled_atlas = cf.io.load_atlas(store_path) plotter = cf.plotting.plot_volume( moving, slice_mode="z", cmap="gray", show_colorbar=False, bg_color=bg_color ) _ = plotter.add_contours(resampled_atlas.annotation) ``` --- **Total running time:** 107.4 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/registration/register_to_allen_fusi_template/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. # Registration of two sessions from the same subject This example shows how to align two power Doppler images acquired from the same subject in different sessions. We use `register_volume` with a rigid transform, which is appropriate when the imaged anatomy is the same but the probe placement differs slightly between the two recordings. We pick two `susi` acquisitions from the [Cybis Pereira 2026 dataset]()[^/dev/examples/_built/registration/register_volume_same_subject/#collection] using `fetch_cybis_pereira_2026`: subject `rat75`, slice `slice32`, recorded on consecutive days (sessions `20220523` and `20220524`). ## Fetch and load both recordings Each recording is a single power Doppler image of one slice. We convert it to decibels for both display and registration, which is usually more stable on the log-compressed dynamic range. We also set the coordinate system to the NIfTI `qform`'s space, because the `sform` coordinate system is currently unusable as it is a custom non-metric space. ```python 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 # Adapt background color to the current Matplotlib style. bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) xr.set_options(display_expand_data=False) sessions = ["20220523", "20220524"] acq = "slice32" bids_root = cf.datasets.fetch_cybis_pereira_2026( datasets="rawdata", subjects="rat75", sessions=sessions, datatypes="susi", acqs=acq, ) ``` ``` If you use this dataset in your work, please cite the following source: Cybis Pereira, F., Castedo, S. H., Meur-Diebolt, S. L., Ialy-Radio, N., Bhattacharya, S., Ferrier, J., Osmanski, B. F., Cocco, S., Monasson, R., Pezet, S., & Tanter, M. (2026). A vascular code for speed in the spatial navigation system. Cell Reports, 45(1). https://doi.org/10.1016/j.celrep.2025.116791 ``` ```python def _load_angio_for_registration(session: str) -> xr.DataArray: """Load an angio acquisition and scale its intensity for registration.""" path = ( Path(bids_root) / "sub-rat75" / f"ses-{session}" / "susi" / f"sub-rat75_ses-{session}_acq-{acq}_rec-minframe2d_pwd.nii.gz" ) angio = cf.load(path).fusi.scale.db().compute() return angio.fusi.affine.apply(angio.affines["world_to_qform"]) fixed = _load_angio_for_registration(sessions[0]) fixed ``` xarray.DataArray 'sub-rat75\_ses-20220523\_acq-slice32\_rec-minframe2d\_pwd' - k: 1 - j: 112 - i: 128 - [ ] \-19.87 -18.5 -16.81 -17.79 -14.17 ... -21.8 -21.61 -22.72 -22.63 ``` array([[[-19.86943521, -18.49816633, -16.81460287, ..., -20.34940616, -20.95297252, -21.0594714 ], [-20.37608604, -18.82660631, -16.94106874, ..., -19.85784515, -21.06570853, -21.44495125], [-20.51718032, -20.88758297, -19.33940193, ..., -20.89870803, -22.26240509, -22.35662323], ..., [-15.87173364, -16.1433349 , -15.9975841 , ..., -20.36189552, -20.24644136, -20.41257363], [-19.52239672, -18.86263447, -18.68976199, ..., -20.05567394, -20.46102047, -20.02077658], [-21.18090479, -21.5516875 , -22.47616461, ..., -21.60698958, -22.72244543, -22.63380267]]], shape=(1, 112, 128)) ``` - [x] Coordinates: (6) - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 107 108 109 110 111 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]) ``` - i (i) int64 0 1 2 3 4 5 ... 123 124 125 126 127 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]) ``` - z (k, j, i) float64 1.776e-15 1.776e-15 ... 1.776e-15 ☐ ☐ **units :** mm ``` [14336 values with dtype=float64] ``` - y (k, j, i) float64 5.0 5.0 5.0 ... 15.94 15.94 15.94 ☐ ☐ **units :** mm ``` [14336 values with dtype=float64] ``` - x (k, j, i) float64 \-6.985 -6.875 ... 6.875 6.985 ☐ ☐ **units :** mm ``` [14336 values with dtype=float64] ``` - [ ] Indexes: (4) - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 102, 103, 104, 105, 106, 107, 108, 109, 110, 111], dtype='int64', name='j', length=112)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], dtype='int64', name='i', length=128)) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (23) **sform\_code :** 5 **qform\_code :** 1 **clutter\_filter\_window\_duration :** 0.2 **clutter\_filters :** \['svd:remove\_first\_60\_components'\] **IsHadamard :** True **manufacturer :** Iconeus **manufacturers\_model\_name :** Iconeus One **plane\_wave\_angles :** \[-3.0, -1.0, 1.0, 3.0\] **power\_doppler\_integration\_duration :** 0.2 **ProbeElevationAperture :** 1.5 **ProbeElevationFocus :** 8 **probe\_model :** IcoPrime light **probe\_number\_of\_elements :** 128 **probe\_pitch :** 0.11 **probe\_radius\_of\_curvature :** 0 **probe\_type :** linear **UltrafastSamplingFrequency :** 500.0 **probe\_center\_frequency :** 15.625 **pulse\_repetition\_frequency :** 2000.0 **transmit\_voltage :** 15 **affines :** {'world\_to\_sform': array(\[\[ 1.28309483e-17, -5.62707691e-03, 1.15276755e-01, -2.23374989e-01\], \[-4.77415914e-19, 1.24250907e-01, 4.02555295e-03, -1.50084791e+00\], \[-3.22580659e-02, -5.19576136e-20, 6.51250392e-18, -4.62365597e-01\], \[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00\]\]), 'world\_to\_qform': array(\[\[ 1.00000000e+00, 7.45635985e-34, -1.62669570e-32, 1.77635684e-15\], \[ 1.07125958e-33, 1.00000000e+00, -6.72286444e-19, 0.00000000e+00\], \[ 4.39209002e-34, 3.16910450e-18, 1.00000000e+00, 0.00000000e+00\], \[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00\]\])} **units :** dB **scaling :** 10\*log10(x/max) ```python moving = _load_angio_for_registration(sessions[1]) moving ``` xarray.DataArray 'sub-rat75\_ses-20220524\_acq-slice32\_rec-minframe2d\_pwd' - k: 1 - j: 112 - i: 128 - [ ] \-22.22 -22.53 -23.2 -20.78 -20.63 ... -25.88 -25.99 -25.86 -25.84 ``` array([[[-22.22170345, -22.53014493, -23.1997532 , ..., -19.9998522 , -21.87579216, -22.24978228], [-22.7455659 , -21.95198542, -20.08606197, ..., -22.08711121, -23.75721916, -23.68565874], [-22.5436206 , -22.84849361, -19.73018955, ..., -22.7340935 , -24.03095053, -24.51766265], ..., [-24.44969569, -23.36497407, -23.45954239, ..., -24.8960767 , -25.14390549, -25.16290915], [-25.51832687, -25.14719523, -24.78346509, ..., -25.87028958, -26.06853857, -26.2992882 ], [-24.93807723, -24.42417414, -24.06650479, ..., -25.98793503, -25.85946308, -25.83676812]]], shape=(1, 112, 128)) ``` - [x] Coordinates: (6) - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 107 108 109 110 111 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]) ``` - i (i) int64 0 1 2 3 4 5 ... 123 124 125 126 127 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]) ``` - z (k, j, i) float64 1.776e-15 1.776e-15 ... 1.776e-15 ☐ ☐ **units :** mm ``` [14336 values with dtype=float64] ``` - y (k, j, i) float64 5.0 5.0 5.0 ... 15.94 15.94 15.94 ☐ ☐ **units :** mm ``` [14336 values with dtype=float64] ``` - x (k, j, i) float64 \-6.985 -6.875 ... 6.875 6.985 ☐ ☐ **units :** mm ``` [14336 values with dtype=float64] ``` - [ ] Indexes: (4) - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 102, 103, 104, 105, 106, 107, 108, 109, 110, 111], dtype='int64', name='j', length=112)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], dtype='int64', name='i', length=128)) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (23) **sform\_code :** 5 **qform\_code :** 1 **clutter\_filter\_window\_duration :** 0.2 **clutter\_filters :** \['svd:remove\_first\_60\_components'\] **IsHadamard :** True **manufacturer :** Iconeus **manufacturers\_model\_name :** Iconeus One **plane\_wave\_angles :** \[-7.0, -5.0, -3.0, -1.0, 1.0, 3.0, 5.0, 7.0\] **power\_doppler\_integration\_duration :** 0.2 **ProbeElevationAperture :** 1.5 **ProbeElevationFocus :** 8 **probe\_model :** IcoPrime light **probe\_number\_of\_elements :** 128 **probe\_pitch :** 0.11 **probe\_radius\_of\_curvature :** 0 **probe\_type :** linear **UltrafastSamplingFrequency :** 400.0 **probe\_center\_frequency :** 15.625 **pulse\_repetition\_frequency :** 3200.0 **transmit\_voltage :** 15 **affines :** {'world\_to\_sform': array(\[\[ 1.28246348e-17, -1.08396994e-02, 1.13934288e-01, -3.21704996e-01\], \[-3.74197435e-19, 1.23898326e-01, 9.96795933e-03, -1.49673076e+00\], \[-3.22580659e-02, 5.17612180e-20, 6.45905865e-18, -4.62365597e-01\], \[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00\]\]), 'world\_to\_qform': array(\[\[ 1.00000000e+00, 6.58614509e-34, -2.77831769e-32, 1.77635684e-15\], \[ 6.91900880e-34, 1.00000000e+00, 5.31740400e-18, -1.77635684e-15\], \[-9.17339236e-33, 3.35821769e-18, 1.00000000e+00, 0.00000000e+00\], \[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00\]\])} **units :** dB **scaling :** 10\*log10(x/max) ## Inspect the misalignment before registration We visualize the alignment with `plot_composite`, which resamples `moving` onto `fixed`'s grid and draws the two as a red/cyan composite: matched anatomy appear in white, while any residual red/cyan fringe reveals the displacement that `register_volume` will correct. ```python _ = cf.plotting.plot_composite(fixed, moving, bg_color=bg_color) ``` ## Run the registration A rigid transform captures the rotation and translation difference between the two sessions. `register_volume` returns three values: 1. the moving image (only aligned to the fixed grid if `resample=True` is used); 2. the rigid transform matrix that maps fixed-world coordinates to moving-world coordinates; 3. a `RegistrationDiagnostics` dataclass holding the per-iteration metric values and the optimizer stop condition, which we use below to plot the convergence curve. > [!TIP] > > **Watch registration progress live** > > Pass `show_progress=True` to `register_volume` to follow the optimization in real time. A live matplotlib window opens during the call and updates at every iteration with both the similarity-metric curve and a fixed/moving composite overlay. It is the fastest way to tell whether the optimizer is making progress, stuck in a local minimum, or diverging—and to decide which arguments to tweak from the warning above. > [!WARNING] > > **Registration is sensitive to its arguments** > > The result depends heavily on the choice of `transform_type`, `metric`, `learning_rate`, `number_of_iterations`, `convergence_window_size`, `initialization`, and the multi-resolution settings (`use_multi_resolution`, `shrink_factors`, `smoothing_sigmas`). The default values are usually a good starting point and work well in many cases, but you should definitely try different arguments (start with the default!). If the result is not satisfactory, inspect the `RegistrationDiagnostics` convergence curve and the post-registration overlay, and sweep these arguments until you get a stable, well-converged result. ```python registered, rigid_transform, diagnostics = cf.registration.register_volume( moving=moving, fixed=fixed, transform_type="rigid", show_progress=True ) print(f"Rigid transform:\n{rigid_transform}") ``` ```text Rigid transform: [[ 1.00000000e+00 -1.19034059e-20 -6.54108347e-21 1.24629613e-19] [ 1.15250165e-20 9.98481665e-01 -5.50850618e-02 -1.77089580e-01] [ 7.18685176e-21 5.50850618e-02 9.98481665e-01 8.54466682e-01] [ 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00]] ``` ## Check the alignment after registration Plotting the same fixed/moving overlay before and after registration makes the correction obvious: the red/cyan fringe in the first panel should be replaced by a more uniformly desaturated gray in the second. ```python fig, axes = plt.subplots(1, 2, figsize=(10, 4)) fig.patch.set_facecolor(bg_color) for ax, moving_view, title in [ (axes[0], moving, "Before"), (axes[1], registered, "After"), ]: cf.plotting.plot_composite(fixed, moving_view, axes=ax, bg_color=bg_color) ax.set_title(title) _ = fig.suptitle("Fixed (red) / moving (cyan)") ``` ## Inspect convergence with the registration diagnostics `diagnostics.metric_values` holds the optimizer's similarity-metric value at each iteration. With the default `metric="correlation"`, SimpleITK minimizes the negative normalized cross-correlation, so a lower (more negative) value means a better fit. The curve typically drops sharply at the start and then plateaus. ```python fig, ax = plt.subplots(figsize=(7, 3)) fig.patch.set_facecolor(bg_color) ax.plot(diagnostics.metric_values, color="#d93a54") ax.set_xlabel("Iteration") ax.set_ylabel(f"Similarity metric ({diagnostics.metric})") _ = ax.set_title(diagnostics.stop_condition) ``` The resulting rigid transform is encoded in physical units and can be reused, composed with other transforms, or applied to additional volumes from the same session with `resample_volume`. ## Refine with a B-spline transform A rigid transform only models a single global rotation and translation. Any residual local mismatch—e.g. small elastic tissue deformation between sessions—needs a nonrigid model. `register_volume` with `transform_type="bspline"` fits a local displacement field on top of an initial transform, here the rigid transform found above. > [!WARNING] > > **B-spline registration may need different parameters than rigid** > > Successful non-linear registration is often very dependent on the choice of parameters. If the B-spline fails to converge, or if deformations are unrealistic, try adjusting the `learning_rate`, `mesh_size`, and multi-resolution settings. The default values are usually a good starting point, but you may need to experiment to find the right combination for your data. Here, we use a non-default mesh size of `(6, 6, 6)` to allow less local deformations that the default `(10, 10, 10)` mesh would allow. ```python registered_bspline, bspline_transform, diagnostics_bspline = ( cf.registration.register_volume( moving=moving, fixed=fixed, transform_type="bspline", mesh_size=(6, 6, 6), initialization=rigid_transform, show_progress=True, ) ) bspline_transform ``` xarray.DataArray - component: 3 - k: 9 - j: 9 - i: 9 - [ ] 2.446e-11 1.445e-11 2.629e-13 -2.999e-13 ... -944.7 -880.4 -1.443e+04 ``` array([[[[ 2.44616698e-11, 1.44484791e-11, 2.62859289e-13, ..., -6.26982592e-13, -1.56365582e-13, -7.90370420e-14], [ 1.15342388e-13, 6.90479319e-14, 1.72158500e-14, ..., -2.38988771e-13, -5.57211671e-14, 2.54469768e-14], [-2.38811607e-13, -9.43048277e-14, -1.34315767e-13, ..., -8.80149572e-14, 3.83163216e-14, 2.63967445e-14], ..., [-4.96908630e-13, -9.22612340e-15, 2.95080254e-13, ..., 1.73566828e-13, 1.03905806e-13, 1.98780949e-14], [ 5.43107970e-13, 1.13174620e-13, 2.23168727e-13, ..., 1.22176029e-13, 6.08686734e-14, 4.53592778e-15], [ 7.57823610e-12, 1.06704151e-12, 4.77735897e-13, ..., 3.52840819e-13, 3.92308755e-13, 5.21167113e-14]], [[ 2.86645954e-14, 1.69309703e-14, 3.08022927e-16, ..., -7.34708725e-16, -1.83231814e-16, -9.26169326e-17], [ 1.35160147e-16, 8.09115255e-17, 2.01738220e-17, ..., -2.80051054e-16, -6.52949989e-17, 2.98191946e-17], [-2.79843450e-16, -1.10507980e-16, -1.57393470e-16, ..., -1.03137404e-16, 4.48997088e-17, 3.09321483e-17], ... -8.53885452e-02, -5.12609487e-02, -3.29177644e-03], [ 5.38392227e-02, 5.28263527e-02, 1.57630664e-01, ..., -1.13840418e-01, -1.79660251e-01, -1.29396378e-01], [ 5.00260645e-02, 9.87169946e-02, 1.30456415e-01, ..., -1.10705902e+00, -1.03171647e+00, -1.69072326e+01]], [[ 4.05319469e+04, -4.34203801e+02, -3.82659902e+02, ..., -5.42208301e+02, 1.16451354e+01, 2.86627875e+03], [-7.15266846e+02, -5.74385030e+01, -9.30286741e+01, ..., -7.30891012e+01, 2.25372488e+01, 2.33233436e+02], [-4.75713045e+02, 8.94186020e+01, 6.18394722e+01, ..., -9.78460534e+01, 1.04610451e+01, 7.00358257e+00], ..., [-6.87111194e+01, 9.11473464e+01, 2.13182839e+02, ..., -7.28685118e+01, -4.37448493e+01, -2.80912210e+00], [ 4.59450856e+01, 4.50807271e+01, 1.34518183e+02, ..., -9.71486494e+01, -1.53317697e+02, -1.10423725e+02], [ 4.26910294e+01, 8.42426870e+01, 1.11328338e+02, ..., -9.44737287e+02, -8.80441796e+02, -1.44282062e+04]]]], shape=(3, 9, 9, 9)) ``` - [x] Coordinates: (7) - component (component) \ ``` - [x] Attributes: (4) **type :** bspline\_transform **transform\_type :** bspline\_transform **order :** 3 **affines :** {'bspline\_initialization': \[\[1.0, -1.1903405882410274e-20, -6.541083465249015e-21, 1.2462961252218086e-19\], \[1.1525016541509118e-20, 0.9984816653136261, -0.05508506179108768, -0.17708957996671354\], \[7.186851759894228e-21, 0.05508506179108768, 0.9984816653136261, 0.8544666816293937\], \[0.0, 0.0, 0.0, 1.0\]\]} ## Check the alignment after B-spline refinement Comparing the rigid-only result against the B-spline-refined result shows the extra local correction the B-spline step adds on top of rigid, especially in deeper vessels where the brain might have contracted slightly between sessions. ```python print(f"Rigid final metric: {diagnostics.final_metric_value:.4f}") print(f"Rigid + B-spline final metric: {diagnostics_bspline.final_metric_value:.4f}") ``` ```text Rigid final metric: -0.8197 Rigid + B-spline final metric: -0.8952 ``` ```python fig, axes = plt.subplots(1, 2, figsize=(10, 4)) fig.patch.set_facecolor(bg_color) for ax, moving_view, title in [ (axes[0], registered, "Rigid"), (axes[1], registered_bspline, "Rigid + B-spline"), ]: cf.plotting.plot_composite(fixed, moving_view, axes=ax, bg_color=bg_color) ax.set_title(title) _ = fig.suptitle("Fixed (red) / moving (cyan)") ``` ## Inspect B-spline convergence ```python fig, ax = plt.subplots(figsize=(7, 3)) fig.patch.set_facecolor(bg_color) ax.plot(diagnostics_bspline.metric_values, color="#d93a54") ax.set_xlabel("Iteration") ax.set_ylabel(f"Similarity metric ({diagnostics_bspline.metric})") _ = ax.set_title(diagnostics_bspline.stop_condition) ``` ## Visualize the deformation To see the deformation directly, sample the B-spline transform onto `fixed`'s grid with `sample_displacement_field_like`, which returns one displacement vector per voxel of the reference grid. Sampling `bspline_transform` includes the rigid pre-affine it was initialized with, giving the *combined* rigid + B-spline warp; dropping that pre-affine leaves only the *local* B-spline deformation. The sampled field follows the pull convention (fixed-to-moving): at each fixed-grid point it indicates where to look in `moving`. That is correct for warping, but less intuitive to read as motion, so for the figure below we invert the field and plot the approximate moving-to-fixed displacement instead. The inverse is only approximate, because a B-spline transform has no closed-form inverse and must be inverted through its sampled displacement field. ```python # Combined rigid + B-spline field. composite_field = cf.registration.sample_displacement_field_like( bspline_transform, fixed ) # Local-only field: copy the transform and drop the rigid pre-affine before sampling. local_bspline = bspline_transform.copy() local_bspline.attrs = {**local_bspline.attrs, "affines": {}} local_field = cf.registration.sample_displacement_field_like(local_bspline, fixed) # For display, invert the pull fields so the arrows show apparent motion toward # `fixed`, rather than the fixed-to-moving lookup direction SimpleITK uses for # resampling. composite_inverse_field = cf.registration.invert_displacement_field(composite_field) local_inverse_field = cf.registration.invert_displacement_field(local_field) ``` A quiver plot draws the in-plane displacement as arrows over the anatomy, so the *direction* of the warp is easy to read. The field carries a leading `component` axis labeled by the native voxel dim names, so we use the `i` and `j` displacement components (lateral and axial), drop the degenerate `k` (elevation) of the single slice, and subsample the dense grid so the arrows stay legible. Displacements are still plotted against the world `x`/`y` coordinates attached to the `i`/`j` dims. ```python fig, axes = plt.subplots(1, 2, figsize=(8, 3)) fig.patch.set_facecolor(bg_color) for ax, background, field, title in [ (axes[0], fixed, composite_inverse_field, "Approximate inverse combined warp"), (axes[1], fixed, local_inverse_field, "Approximate inverse local B-spline warp"), ]: cf.plotting.plot_volume(background, axes=ax, show_colorbar=False, bg_color=bg_color) # Lateral (i/x) and axial (j/y) displacement of the single slice. disp_x = field.sel(component="i").squeeze(drop=True) disp_y = field.sel(component="j").squeeze(drop=True) # Subsample to roughly 20 arrows along the shorter axis so the field stays readable. step = max(1, min(disp_x.sizes["j"], disp_x.sizes["i"]) // 20) # x/y are (j, i)-shaped axis-aligned coordinates: x varies only along i, y only # along j, so a single row/column recovers the 1D axis values for the meshgrid. x_1d = disp_x["x"].isel(j=0).values y_1d = disp_x["y"].isel(i=0).values grid_x, grid_y = np.meshgrid(x_1d[::step], y_1d[::step]) u = disp_x.values[::step, ::step] v = disp_y.values[::step, ::step] arrows = ax.quiver(grid_x, grid_y, u, v, np.hypot(u, v), angles="xy", cmap="autumn") fig.colorbar(arrows, ax=ax, label="displacement (mm)", shrink=0.75) ax.set_aspect("equal") ax.set_title(title) ax.set_xlabel("x (mm)") ax.set_ylabel("y (mm)") # Arrow lengths are autoscaled per panel, so compare directions and patterns, not # absolute length between the two. fig.tight_layout() ``` --- **Total running time:** 50.5 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/registration/register_volume_same_subject/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. # Motion correction of a single recording This example shows how to correct volume-to-volume brain motion in one fUSI recording with `register_volumewise`. For this example, we use a short subset of an open-field 2D+t recording from the [Cybis Pereira 2026 dataset]()[^/dev/examples/_built/registration/volumewise_motion_correction/#collection]. After volumewise registration, we inspect three things that are useful in practice: - the motion diagnostics returned by `create_motion_dataframe`; - a representative voxel trace before and after registration; - a compact raster view showing how volumewise registration stabilized the recording. ## Fetch and load a short motion-corrupted window The selected acquisition is a recording from a single 2D slice, so the data shape is `(time, k=1, j, i)`. ```python 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 bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"]) xr.set_options(display_expand_data=False) subject = "rat75" session = "20220523" acq = "slice32" start_frame = 220 n_frames = 120 bids_root = cf.datasets.fetch_cybis_pereira_2026( datasets="rawdata", subjects=subject, sessions=session, acqs=acq, ) ``` ``` If you use this dataset in your work, please cite the following source: Cybis Pereira, F., Castedo, S. H., Meur-Diebolt, S. L., Ialy-Radio, N., Bhattacharya, S., Ferrier, J., Osmanski, B. F., Cocco, S., Monasson, R., Pezet, S., & Tanter, M. (2026). A vascular code for speed in the spatial navigation system. Cell Reports, 45(1). https://doi.org/10.1016/j.celrep.2025.116791 ``` ```python pwd_path = ( Path(bids_root) / f"sub-{subject}" / f"ses-{session}" / "fusi" / f"sub-{subject}_ses-{session}_task-openfield_acq-{acq}_pwd.nii.gz" ) data = cf.load(pwd_path).isel(time=slice(start_frame, start_frame + n_frames)).compute() data ``` xarray.DataArray 'sub-rat75\_ses-20220523\_task-openfield\_acq-slice32\_pwd' - time: 120 - k: 1 - j: 112 - i: 128 - [ ] 85.26 116.2 244.4 326.7 220.4 89.93 ... 35.0 31.79 27.72 32.78 35.08 ``` array([[[[ 85.26095862, 116.22205806, 244.40663846, ..., 81.49985325, 72.6656052 , 84.75890358], [ 94.30669678, 168.48894542, 370.41283569, ..., 77.94853297, 75.73787961, 80.90911372], [ 77.33881401, 85.13303516, 107.57782349, ..., 74.0115976 , 84.09536198, 80.31733092], ..., [ 370.21667854, 293.17560413, 283.58816699, ..., 56.02650583, 45.62883065, 44.52256835], [ 144.96484446, 147.96791438, 137.12604831, ..., 50.16398943, 65.04411943, 63.9631805 ], [ 87.65224311, 78.54026893, 66.43983123, ..., 65.42629254, 73.38085286, 70.04022849]]], [[[ 389.93191371, 641.48380496, 1115.08191581, ..., 96.22182585, 74.23019108, 82.51393297], [ 359.81698545, 559.63210265, 864.14534089, ..., 127.96252563, 110.07846579, 93.85637685], [ 536.68046542, 982.18576292, 1179.88564376, ..., ... [ 55.50365905, 54.83884733, 50.36205525, ..., 29.36021998, 23.86886065, 23.52851437], [ 43.72547414, 35.05400817, 30.45671777, ..., 29.9262347 , 33.29741235, 33.45943534]]], [[[ 100.0586512 , 195.05050789, 474.78743741, ..., 48.95520004, 38.07772308, 40.22238393], [ 191.54714471, 299.21769241, 349.65676208, ..., 39.14976869, 32.07769081, 32.27574316], [ 145.40563748, 155.22389012, 239.30868664, ..., 36.21480796, 35.76955383, 34.44912312], ..., [ 154.07177111, 134.59463793, 117.71987771, ..., 35.25394061, 30.67498287, 32.57377449], [ 60.53621928, 60.32421715, 51.64683992, ..., 35.02957069, 32.21901694, 26.78651635], [ 43.42175231, 40.90431247, 39.44968175, ..., 27.71583093, 32.78066543, 35.08132628]]]], shape=(120, 1, 112, 128)) ``` - [x] Coordinates: (7) - time (time) float64 88.4 88.8 89.2 ... 135.6 136.0 ☐ ☐ **units :** s **volume\_acquisition\_reference :** start **volume\_acquisition\_duration :** 0.4000000000000057 ``` array([ 88.4014 , 88.8014 , 89.2014 , 89.601425, 90.001425, 90.401425, 90.801425, 91.20145 , 91.60145 , 92.00145 , 92.40145 , 92.801475, 93.201475, 93.601475, 94.001475, 94.4015 , 94.8015 , 95.2015 , 95.6015 , 96.001525, 96.401525, 96.801525, 97.20155 , 97.60155 , 98.00155 , 98.40155 , 98.801575, 99.201575, 99.601575, 100.001575, 100.4016 , 100.8016 , 101.2016 , 101.6016 , 102.001625, 102.401625, 102.801625, 103.201625, 103.60165 , 104.00165 , 104.40165 , 104.80165 , 105.201675, 105.601675, 106.001675, 106.401675, 106.8017 , 107.2017 , 107.6017 , 108.0017 , 108.401725, 108.801725, 109.201725, 109.601725, 110.00175 , 110.40175 , 110.80175 , 111.20175 , 111.601775, 112.001775, 112.401775, 112.8018 , 113.2018 , 113.6018 , 114.0018 , 114.401825, 114.801825, 115.201825, 115.601825, 116.00185 , 116.40185 , 116.80185 , 117.20185 , 117.601875, 118.001875, 118.401875, 118.801875, 119.2019 , 119.6019 , 120.0019 , 120.4019 , 120.801925, 121.201925, 121.601925, 122.001925, 122.40195 , 122.80195 , 123.20195 , 123.60195 , 124.001975, 124.401975, 124.801975, 125.201975, 125.602 , 126.002 , 126.402 , 126.802 , 127.202025, 127.602025, 128.002025, 128.402025, 128.80205 , 129.20205 , 129.60205 , 130.002075, 130.402075, 130.802075, 131.202075, 131.6021 , 132.0021 , 132.4021 , 132.8021 , 133.202125, 133.602125, 134.002125, 134.402125, 134.80215 , 135.20215 , 135.60215 , 136.00215 ]) ``` - k (k) int64 0 ☐ ☐ ``` array([0]) ``` - j (j) int64 0 1 2 3 4 5 ... 107 108 109 110 111 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]) ``` - i (i) int64 0 1 2 3 4 5 ... 123 124 125 126 127 ☐ ☐ ``` array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]) ``` - z (k, j, i) float64 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 ☐ ☐ **units :** mm ``` array([[[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]]], shape=(1, 112, 128)) ``` - y (k, j, i) float64 5.0 5.0 5.0 ... 15.94 15.94 15.94 ☐ ☐ **units :** mm ``` array([[[ 5. , 5. , 5. , ..., 5. , 5. , 5. ], [ 5.09856 , 5.09856 , 5.09856 , ..., 5.09856 , 5.09856 , 5.09856 ], [ 5.19712 , 5.19712 , 5.19712 , ..., 5.19712 , 5.19712 , 5.19712 ], ..., [15.74303978, 15.74303978, 15.74303978, ..., 15.74303978, 15.74303978, 15.74303978], [15.84159978, 15.84159978, 15.84159978, ..., 15.84159978, 15.84159978, 15.84159978], [15.94015978, 15.94015978, 15.94015978, ..., 15.94015978, 15.94015978, 15.94015978]]], shape=(1, 112, 128)) ``` - x (k, j, i) float64 \-6.985 -6.875 ... 6.875 6.985 ☐ ☐ **units :** mm ``` array([[[-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], ..., [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027], [-6.98499966, -6.87499966, -6.76499966, ..., 6.76500027, 6.87500027, 6.98500027]]], shape=(1, 112, 128)) ``` - [ ] Indexes: (5) - time PandasIndex ☐ ☐ ``` PandasIndex(Index([ 88.4014, 88.8014, 89.20139999999999, 89.601425, 90.001425, 90.401425, 90.801425, 91.20145, 91.60145, 92.00145, ... 132.4021, 132.8021, 133.202125, 133.602125, 134.002125, 134.402125, 134.80215, 135.20215, 135.60215, 136.00215], dtype='float64', name='time', length=120)) ``` - k PandasIndex ☐ ☐ ``` PandasIndex(Index([0], dtype='int64', name='k')) ``` - j PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 102, 103, 104, 105, 106, 107, 108, 109, 110, 111], dtype='int64', name='j', length=112)) ``` - i PandasIndex ☐ ☐ ``` PandasIndex(Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ... 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], dtype='int64', name='i', length=128)) ``` - z y x VoxelToWorldIndex ☐ ☐ ``` ``` - [ ] Attributes: (21) **sform\_code :** 2 **qform\_code :** 1 **manufacturer :** Iconeus **manufacturers\_model\_name :** Iconeus One **probe\_type :** linear **probe\_model :** IcoPrime light **probe\_number\_of\_elements :** 128 **probe\_pitch :** 0.11 **probe\_radius\_of\_curvature :** 0 **ProbeElevationAperture :** 1.5 **ProbeElevationFocus :** 8 **plane\_wave\_angles :** \[-3.0, -1.0, 1.0, 3.0\] **IsHadamard :** True **UltrafastSamplingFrequency :** 500.0 **clutter\_filter\_window\_duration :** 0.2 **clutter\_filters :** \['svd:remove\_first\_60\_components'\] **power\_doppler\_integration\_duration :** 0.2 **probe\_center\_frequency :** 15.625 **pulse\_repetition\_frequency :** 2000.0 **transmit\_voltage :** 15 **affines :** {'world\_to\_sform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\]), 'world\_to\_qform': array(\[\[1., 0., 0., 0.\], \[0., 1., 0., 0.\], \[0., 0., 1., 0.\], \[0., 0., 0., 1.\]\])} ## Register every frame to the first frame Volumewise registration can be applied using `register_volumewise`. By default, each volume (or frame in this case) is registered to the first acquired volume. You may choose any other reference timepoint using the `reference_time`. Alternatively, pass any spatial-only volume on the same grid as `fixed`, for example the mean of a few low-motion frames. Here, we're trying to correct for rigid motion, that is, translation and rotations. The default learning rate is conservative; for this recording, `1.0` recovers the inter-frame shifts better. Depending on the recording, the dynamic range of the data can be quite large, here we use decibel scaling to help the optimizer find the correct alignment. ```python registered = cf.registration.register_volumewise( data, transform="rigid", moving_intensity_scaling="db", metric="correlation", learning_rate=1.0, ) ``` `register_volumewise` adds a `motion_params` attribute to the motion-corrected DataArray. `motion_params` is a DataFrame created using `create_motion_dataframe` and containing the motion parameters and registration diagnostics: rotations (`rot_x`, `rot_y`, `rot_z`), translations (`trans_x`, `trans_y`, `trans_z`), the framewise displacement (FD)[^/dev/examples/_built/registration/volumewise_motion_correction/#1] summaries (`mean_fd`, `max_fd`, `rms_fd`), and the optimizer summaries (`final_metric_value`, `n_iterations`) added by `register_volumewise`. Since we registered a 2D+t recording, only the rotation along the z-axis and the translations along the x- and y-axes are non-zero. ```python motion_df = registered.attrs["motion_params"] motion_df.head() ``` | | rot\_x | rot\_y | rot\_z | trans\_x | trans\_y | trans\_z | mean\_fd | max\_fd | rms\_fd | final\_metric\_value | n\_iterations | status | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | time | | | | | | | | | | | | | | 88.401400 | 0.000000e+00 | \-0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000e+00 | 0.000988 | 0.002016 | 0.001064 | \-1.000000 | 31 | completed | | 88.801400 | \-1.719862e-21 | \-1.379504e-21 | \-0.000198 | 0.001800 | 0.000109 | \-1.800995e-20 | 0.036117 | 0.042061 | 0.036201 | \-0.950641 | 32 | completed | | 89.201400 | 5.060800e-21 | 1.657826e-21 | 0.000507 | 0.023651 | \-0.020930 | 5.297817e-20 | 0.038505 | 0.058450 | 0.039453 | \-0.973224 | 33 | completed | | 89.601425 | \-3.202291e-21 | 3.400277e-22 | 0.002885 | 0.025834 | \-0.046818 | \-3.353837e-20 | 0.003992 | 0.006048 | 0.004089 | \-0.971765 | 30 | completed | | 90.001425 | \-5.642973e-21 | 3.699684e-22 | 0.002614 | 0.032245 | \-0.045404 | \-5.909230e-20 | 0.008309 | 0.012347 | 0.008534 | \-0.969238 | 37 | completed | ## Plot the motion diagnostics ConfUSIus provides the function `plot_motion_diagnostics` to plot `motion_params` using four panels: - The first two panels show the rotations and translations found at each frame. - The third panel shows the mean, maximum, and RMS of the framewise displacement (FD). The FD is a metric used to quantify brain motion between consecutive volumes by summing the absolute temporal derivatives of the realignment parameters (translations and rotations)[^/dev/examples/_built/registration/volumewise_motion_correction/#1]. - The fourth panel shows the final registration metric and iteration count for each volume. Volumes showing outlier metric values or reaching the maximum number of iterations configured in `register_volumewise` are likely to not be registered correctly. In that case, you might want to tweak the registration parameters, or remove the corresponding volumes from further analysis. ```python fig, axes = cf.plotting.plot_motion_diagnostics(motion_df) fig.patch.set_facecolor(bg_color) ``` ## Compare a representative time series before and after registration To highlight the effect of the volumewise registration, we pick the voxel with the largest temporal standard deviation in the unregistered recording. In practice that usually lands on a large vessel or near the borders of the brain, where brain motion can induce high intensity changes. ```python std_map = data.std("time") highest_std_voxel_idx = std_map.argmax(dim=["k", "j", "i"]) highest_std_voxel = std_map.isel(highest_std_voxel_idx) voxel_before = data.isel(highest_std_voxel_idx) voxel_after = registered.isel(highest_std_voxel_idx) x_value = highest_std_voxel.x.values y_value = highest_std_voxel.y.values units = highest_std_voxel.x.units xy_position = f"(x, y) = ({x_value:.2f}, {y_value:.2f}) {units}" fig, axs = plt.subplots(1, 2, figsize=(9, 3.5), constrained_layout=True) fig.patch.set_facecolor(bg_color) plotter = ( data.mean("time") .fusi.scale.db() .fusi.plot.volume(axes=axs[0], bg_color=bg_color, show_colorbar=False) ) # Explicitly show the high std voxel on the brain plot. axs[0].scatter(highest_std_voxel.x, highest_std_voxel.y, color="r") axs[1].plot(voxel_before["time"], voxel_before, label="Before", lw=1.6, alpha=0.8) axs[1].plot(voxel_after["time"], voxel_after, label="After", lw=1.6) axs[1].set_xlabel("Time (s)") axs[1].set_ylabel("Power Doppler intensity") axs[1].set_title(f"Voxel at {xy_position}") _ = axs[1].legend(frameon=False, ncols=2) ``` ## Check the alignment over time A compact way to inspect the correction inside the notebook is to follow one column across time. Motion appears as slanted or wobbling vessel traces in this `y × time` raster, while a good correction makes those traces more horizontal and stable (note that this will only clearly capture translation motion in the y axis). ```python raster_before = data.fusi.scale.db().sel(z=0, x=x_value) raster_after = registered.fusi.scale.db().sel(z=0, x=x_value) fig, axes = plt.subplots(1, 2, figsize=(10, 4), constrained_layout=True, sharey=True) fig.patch.set_facecolor(bg_color) vmin = float(np.nanpercentile(raster_before, 2)) vmax = float(np.nanpercentile(raster_before, 99.8)) for ax, raster, title in [ (axes[0], raster_before, "Before motion correction"), (axes[1], raster_after, "After motion correction"), ]: ax.imshow( raster.T, aspect="auto", origin="upper", cmap="gray", vmin=vmin, vmax=vmax, extent=[ float(raster["time"].values[0]), float(raster["time"].values[-1]), float(raster["y"].values[-1]), float(raster["y"].values[0]), ], ) ax.set_title(title) ax.set_xlabel("Time (s)") _ = axes[0].set_ylabel("y (mm)") ``` For a full visual check of the corrected movie, open the result in napari with `plot_napari` or inspect it in the ConfUSIus plugin. We intentionally do not embed a full before/after GIF here because it would make the rendered notebook unnecessarily heavy. For a full preprocessing workflow, you would usually run the same correction on the complete recording before downstream QC, decomposition, or connectivity analysis. --- **Total running time:** 28.8 s [Launch in Binder]() [Download .py]() [Download .ipynb]() [^/dev/examples/_built/registration/volumewise_motion_correction/#collection]: See [The ConfUSIus Dataset Collection]() for available studies, formats, licenses, and reproducible subset downloads. [^/dev/examples/_built/registration/volumewise_motion_correction/#1]: Power, J. D., Barnes, K. A., Snyder, A. Z., Schlaggar, B. L. & Petersen, S. E. Spurious but systematic correlations in functional connectivity MRI networks arise from subject motion. Neuroimage 59, 2142-2154 # Optional # Changelog ## 0.9.0.dev0 Current development version for the next ConfUSIus release. ### 💥 Breaking changes - Use `fetch_template_brunner_grillet_2021` instead of `fetch_template_huang_2025` for the vascular mouse template. The fetcher uses the published Brunner, Grillet 2021 release, with paper and dataset citations and a CC BY 4.0 license. Voxel values and Allen-grid geometry are unchanged; downloads use a new `brunner-grillet-2021-template` cache directory ([\#517]()). - Rename `labels_from_layer` to `extract_labels_from_layer`, including the `.fusi.plot` accessor ([\#512]()). - Atlas Datasets use `template` instead of `reference`, matching BrainGlobe's naming. Use `atlas.atlas.template` or `atlas["template"]`; update custom atlas Datasets and regenerate previously saved atlas stores ([\#510]()). - `plot_napari` and its `data.fusi.plot.napari()` and `data.fusi.plot()` accessors default to `layer_type="auto"` instead of `"image"`: integer and boolean data open as a labels layer, other data as an image layer. Pass `layer_type="image"` to display integer data as an image ([\#504]()). ### ⚡ Performance - `plot_napari` opens labels data carrying an `rgb_lookup` attribute, such as atlas annotations, faster by building the colormap from the lookup instead of scanning the whole volume ([\#504]()). ### 🐛 Fixes - Labels painted over oblique or sheared data in napari now return to the original reference voxel grid using nearest-neighbor interpolation, rather than retaining the display grid or being assigned incorrect geometry. The drawing documentation explains interpolation limits and recommends an existing axis-aligned frame ([\#512]()). - Fetching a cached BrainGlobe atlas no longer warns or checks the network repeatedly for regions with no upstream mesh, such as Allen mouse RSPd4. Mesh availability is remembered per annotation-set version; available meshes deleted locally are still downloaded again ([\#508]()). ### 📚 Documentation - The documentation site publishes [`llms.txt`]() and `llms-full.txt` for AI assistants, along with a Markdown copy of every page and a "Copy as Markdown" button in the page header ([\#513]()). ### 🖼 Napari plugin - Boolean arrays open as labels layers ([\#504]()). ## 0.8.0 Released 2026-10-13. ### 💥 Breaking changes - Dataset and template fetchers now download immutable, SHA-256-verified releases from S3, with hosting sponsored by AWS Open Data, instead of OSF or the Edmond archive. Releases are cached under `//`; `refresh=True` selects the latest published release without overwriting earlier versions. Delete old OSF dataset/template cache directories and fetch again to reclaim space and use the new layouts. Only remove downloaded collection caches under your `data_dir`, `CONFUSIUS_DATA`, or platform cache directory—not personal recordings, analysis outputs, or BrainGlobe caches. Angiography datatype directories and filters now use `susi` instead of `angio`, and the Huang template filename uses `2025` ([\#490]()). - fUSI-BIDS metadata now follows draft v0.0.14: renamed frequency/voltage fields, including internal attributes `probe_central_frequency` → `probe_center_frequency` and `probe_voltage` → `transmit_voltage`, updated probe/virtual-source validation, and removal of redundant integration-stride attributes for Power Doppler, B-mode, and axial velocity. JSON processing windows use milliseconds, including ConfUSIus-prefixed B-mode and axial-velocity durations; internal attributes use the time coordinate's units. JSON acquisition timing remains in seconds. No legacy JSON aliases or automatic legacy-unit detection are provided ([\#486]()). - `resample_volume` and `resample_like`'s `interpolation` parameter defaults to `"auto"` instead of `"linear"`: `"nearest"` is picked for integer-dtype data (e.g. atlas region labels) and `"linear"` otherwise, so resampling an integer mask no longer silently blends label values unless `interpolation` is explicitly overridden ([\#436]()). - `register_volumewise`'s `intensity_scaling` is renamed to `moving_intensity_scaling`, and a new `fixed_intensity_scaling` scales a user-provided `fixed` volume separately (only allowed together with `fixed`; it defaults to `moving_intensity_scaling`). Both are now also exposed on `data.fusi.register.volumewise` ([\#437]()). - `get_meshes` / `get_atlas_meshes` take `regions`/`sides` instead of `region`/`side` and return a `{acronym: (vertices, faces)}` dict with one entry per requested region, under `_L`/`_R`-suffixed keys for single-hemisphere requests ([\#448]()). - `load_scan` stores SCAN v2 probe aperture as `probe_aperture` instead of `probe_elevation_aperture`, so BIDS sidecars export it as `ProbeAperture` ([\#478]()). - `load_scan` stores the SCAN v1 acquisition date as `iconeus_datetime` in ISO 8601 format (e.g. `2021-10-22T14:14:12+00:00`), matching SCAN v2, instead of `iconeus_date` ([\#478]()). ### ✨ Enhancements - `load_scan` now supports SCAN v2 `4Dscan` recordings acquired with the Iconeus IcoPrime-4D MultiArray probe, preserving multi-pose geometry and acquisition timing ([\#478]()). - Masks are no longer required to be strictly boolean dtype: any binary numeric mask (0 and at most one non-zero value, e.g. `{0, 1}` or `{0.0, 5.0}`) is now accepted and coerced to boolean, covering masks written by tools without a boolean dtype (e.g. FSL/NiBabel NIfTI masks stored as float) ([\#418]()). - `register_volumewise` and `data.fusi.register.volumewise` accept a `fixed` VoxelData volume (for example the mean of a few low-motion frames) to register every frame to, as an alternative to `reference_time` ([\#436]()). - `FirstLevelModel` accepts `show_progress=True` to display a progress bar over the runs being fitted ([\#442]()). - New `get_bounding_box`, also available as `data.fusi.affine.bounding_box`, returning a VoxelData array's world-space bounding box enclosing the full extent of its voxels, one per pose for pose-dependent geometry ([\#446]()). - `fetch_brainglobe_atlas` downloads every region mesh in one batched call on the first fetch, so meshes are available offline afterwards ([\#448]()). ### ⚡ Performance - EchoFrame DAT loading is faster and uses less resident memory by reopening read-only memory maps per Dask chunk instead of copying each acquisition block, addressing slow DAT-based IQ processing ([\#479]()). SCAN v2 uses the same worker-side reader, keeping payloads lazy without caching whole-file mappings ([\#478]()). - `fetch_brainglobe_atlas`'s `reference`/`annotation` are now backed by lazy `dask.array.Array` data instead of being eagerly materialized to `numpy.ndarray` at fetch time, since `BrainGlobeAtlas`'s v3 API forces this even when only metadata or a small region is needed ([\#415]()). - `FirstLevelModel.fit` is roughly twice as fast, with the larger gain on the default `noise_model="ar1"` ([\#442]()). - `get_atlas_meshes` with `clip=True` no longer materializes the full world-coordinate grid of an oblique atlas ([\#446]()). ### 🐛 Fixes - Nunez-Elizalde dataset task and acquisition filters now retain session-level derivatives without those entities, including registered atlas labels needed by the GUI documentation images ([\#490]()). - `resample_volume` and `register_volume` now raise a `ValueError` when `fill_value` cannot be represented as an integer, but `moving` is an integer-dtype ([\#481]()). - `register_volume`'s live progress plot no longer draws every slice in the composite overlay for volumes with many slices, which made the mosaic slow to render and hard to read. The composite now shows at most 9 evenly spaced slices by default (a 3x3 grid), configurable via the new `max_composite_slices` parameter (`None` restores the previous behaviour of plotting every slice) ([\#368]()). - `consolidate_poses`'s regularity check no longer rejects realistic stage jitter on small pose steps (e.g. a 100 um step previously tolerated only \~1 um of jitter under a pure 1% relative tolerance). The check now combines a new `atol` parameter (default: 5 um, converted to the array's own world units) with `rtol`, matching typical stepper-motor stage repeatability at small step sizes while keeping `rtol` in control at larger ones ([\#363]()). - `plot_stat_map` and `plot_matrix` now honor a `vmin` or `vmax` passed on its own under `auto_range=True`: a lone bound sets the symmetric range to `[-|bound|, |bound|]` ([\#445]()). - `FirstLevelModel.compute_contrast` no longer emits a divide-by-zero `RuntimeWarning` on recordings containing voxels with no variance over time, such as those outside the recorded field of view ([\#442]()). - `apply_affine` keeps the applied key in `.attrs["affines"]` as the identity when the affine is given by key ([\#463]()). - `convert_echoframe_dat_to_zarr` now preserves time coordinate attributes when passing `block_times` ([\#473]()). ### 📚 Documentation - Docs now build with Zensical 0.0.69 (up from 0.0.55), which validates cross-reference links in strict mode; the broken autoref links that surfaced are fixed, and docstring types such as `xarray.DataArray` link to the xarray docs instead of resolving to the `confusius.xarray` subpackage ([\#500]()). ### 🖼 Napari plugin - Renaming a layer now immediately updates selectors in Registration, Signals, Quality Control, Video, and Data → Save without changing the selected layer ([\#493]()). - The registration panel's within-scan mode can register every frame to a fixed layer, with its own intensity scaling, as an alternative to a reference time index ([\#376]()). - The signal plotter's mouse source now updates when Shift is pressed while the cursor is already resting on a voxel, not only while moving; a live signal (mouse voxel, a point, or a label region) can now be pinned so it persists across source-mode switches instead of being dropped, reusable wherever stored signals are — imported and pinned signals now share one concept (`StoredSignal`) throughout the signal store, plotter, and manager ([\#429]()). - Mouse-driven signal plotting no longer lags or jitters on large recordings — updates are now throttled to the display refresh rate instead of running unbounded on every raw mouse-move event ([\#429]()). - A DVARS trace computed in the Quality Control panel is now plotted in the Signals panel as a stored signal, opening that plot if needed, instead of in a separate QC plot tab ([\#429]()). ## 0.7.1 Released 2026-09-16. ### ⚡ Performance - `compute_compcor_confounds` no longer computes a full SVD, extracting components several times faster on large recordings or broad noise masks ([\#434]()). ### 📚 Documentation - Fixed Rich table alignment in gallery examples while preserving wrapped citations ([\#490]()). - Added a Dataset Collection tour covering public S3 releases, fUSI-BIDS metadata, verified subset downloads, a structural image preview, and links to worked analyses, with footnote links from the other examples ([\#490]()). - Clarified when to use `.compute()` or `.persist()` before repeated partial reads from gzip-compressed NIfTI files ([\#441]()). ### 🖼 Napari plugin - Scrolling the sidebar with the mouse wheel no longer gets hijacked by whichever combo box or spin box the cursor happens to be over ([\#431]()). - New Points/Labels layers created from the signals panel now copy the reference image's units and axis labels, so napari keeps rendering units instead of warning about inconsistent units ([\#459]()). - The Save panel now saves a 3D labels layer with a 4D recording as template instead of failing with a `VoxelToWorldIndex` error ([\#459]()). - Saving a user-drawn layer without a template now recognises napari 0.9's default axis names and writes units in short form (`mm`), so the saved file matches the image it was drawn on ([\#459]()). - Loading a video next to a single-slice recording no longer adds a spurious `z` slider when the slice sits at a nonzero world position, and the video layer now uses the same world axis labels as the recording. Rolling the displayed axes (Ctrl+E) onto a single-slice axis with a video loaded no longer crashes ([\#452]()). ## 0.7.0 Released 2026-08-31. ### 💥 Breaking changes **VoxelData model ([\#278]()):** - **ConfUSIus' canonical dims changed from `(...extra, time, pose, z, y, x)` to `(...extra, time, pose, k, j, i)`.** World coordinates `z`/`y`/`x` are no longer stored dimensions: they're derived lazily, per voxel, from a single voxel-to-world affine owned by a custom xarray index (`VoxelToWorldIndex`) attached to native voxel dims `k`/`j`/`i`. This lets ConfUSIus represent oblique, rotated, or sheared acquisitions and registration outputs exactly, without resampling onto an axis-aligned grid. Every loader (`load_scan`, `load_nifti`, AUTC, EchoFrame), registration function, plotting path, napari layer, and I/O round trip (Zarr, NIfTI) was migrated to this model; `confusius.validation.validate_voxeldata`/`ensure_voxeldata` enforce it unconditionally. - World-space `units` moved off the `z`/`y`/`x` coordinates' `.attrs` onto `VoxelToWorldIndex` as a single shared property, exposed via `data.fusi.affine.units` and set with `data.fusi.affine.set_units`. Setting `data.coords["z"].attrs["units"]` directly no longer has any effect: world coordinates are always regenerated fresh from the index. - Multi-pose data now carries pose-dependent voxel-to-world geometry. Sequentially acquired multi-pose data correspondingly carries a pose-dependent, `(time, pose)`-shaped `time` coordinate holding each pose's own real acquisition timestamps directly, replacing the old 1D `time` + `pose_time` sidecar coordinate convention. New `stack_poses` assembles independently loaded single-pose grids (e.g. one NIfTI file per probe position) into a single pose-dependent DataArray; `load_scan`'s `3Dscan`/`4Dscan` modes, `consolidate_poses` (which also dropped its `affines_key` parameter and now always reads per-pose positions from the primary voxel-to-world geometry), and `correct_slice_timings` all build on this. - `extract_with_mask`/`extract_with_labels` now require canonical VoxelData input for both `data` and `mask`/`labels`, and check alignment via the full voxel-to-world affine (not just matching `k`/`j`/`i` integer ranges) — two arrays on different physical grids that happened to share voxel-index ranges no longer silently pass as aligned. `validate_atlas` similarly no longer accepts a plain, non-indexed atlas shape. - Split `validate_mask`/`validate_labels` into a pure check (`mask`/`data` must already be canonical VoxelData; returns `None`) and new `ensure_mask`/ `ensure_labels` (canonicalize via `ensure_voxeldata`, then validate; returns the canonicalized/coerced array) — mirroring `validate_voxeldata`/`ensure_voxeldata`. Callers that relied on `validate_mask`/`validate_labels`'s return value should switch to `ensure_mask`/`ensure_labels`. - `PCA`/`FastICA`/ `NMF` are now VoxelData-only; the previously documented dual-input support for an already-reduced `(time, region)` signals table (e.g. `extract_with_labels` output) is removed, along with the resulting `feature_names_in_` attribute. Decomposing a signals table is regular tabular PCA/ICA/NMF with no spatial structure to track, so use scikit-learn directly for that instead. `unmask` correspondingly now always requires a VoxelData mask and always returns a VoxelData array. - `cf.io.load`'s `.zarr` branch and the napari Zarr reader now reject stores that don't contain `attrs["voxel_to_world"]`, instead of silently loading them as non-canonical data. Use `xarray.open_zarr` directly for a foreign Zarr store. - Renamed "physical" to "world" throughout the public API (`attrs["affines"]` keys such as `world_to_sform`, function/parameter names, docs) to describe the coordinate *space*, reserving "physical units" for the mm-vs-voxel-index unit distinction (standard ITK/NIfTI usage). `affine_to` was renamed to `get_relative_affine`. **Follow-on API cleanup ([\#322]()):** - Renamed `create_fusi_dataarray` to `create_voxeldata`, `validate_fusi` to `validate_voxeldata`, `ensure_fusi` to `ensure_voxeldata`, and `canonicalize_fusi` to `canonicalize_voxeldata`. Removed the separate `create_iq_dataarray`, `validate_iq`, and `ensure_iq` APIs; IQ data is now built with `create_voxeldata`, with `transmit_frequency` and `beamforming_sound_velocity` passed via `attrs` and validated using `require_velocity_attrs=True`. - Removed `consolidate_poses`'s `sweep_dim` parameter. The swept voxel dimension is now always auto-detected from the per-pose voxel-to-world geometry (the pose-translation direction matched against each voxel dimension's world-space direction); a sweep that isn't cleanly aligned with a single voxel dimension can never form the regular grid consolidation requires, so no override was needed. **Other:** - DataFrame `confounds` passed to `FirstLevelModel.fit` or `make_first_level_design_matrix` must now have a `time` column matching the run's `time` coordinates. `confound_names` can no longer be combined with `confounds` that already carry names ([\#398]()). ### ✨ Enhancements **VoxelData model:** - Added `reindex_voxels`/ `reindex_voxels_like` to `.fusi.affine`, rebasing voxel-space coordinates to dense positions via plain 4x4 matrix composition ([\#278]()). - `plot_volume` (and napari layers) always displays in world space, like nilearn: an axis-aligned world plane (`z`/`y`/`x`), or a non-spatial dim (e.g. `slice_mode="pose"` facets a multi-pose array over its poses). Oblique voxel-to-world data is always resampled onto the world-axis-aligned frame for display, each volume keeping its own native per-axis resolution, controllable via new `resample_interpolation`/`resample_fill_value` parameters on `plot_volume`/`plot_composite`/`plot_stat_map`/`VolumePlotter`/ `.fusi.plot.napari`; `transpose=True` swaps which display dim is drawn on rows versus columns ([\#278]()). - NIfTI and Zarr I/O round-trip oblique/rotated/sheared voxel-to-world geometry exactly; `load_nifti` composes the full primary qform/sform affine into `voxel_to_world` instead of decomposing it into axis-aligned scale/origin ([\#278]()). - Added `ensure_voxeldata` to canonicalize and validate VoxelData inputs with one call, and added `create_voxeldata` to build VoxelData from a raw array plus higher-level metadata (`dt`, spacing, axis origins, attrs). It attaches regularly spaced world coordinates, `units` metadata, and validates the result before returning it ([\#322]()). **Other:** - Added `VolumePlotter.add_stat_map`, the overlay-only counterpart of `plot_stat_map` ([\#392]()). - `clean`, `regress_confounds`, `censor_samples`, and `interpolate_samples` now accept NumPy `confounds` and `sample_mask` (time along the first axis); they take the signals' `time` coordinates and warn since alignment cannot be verified, as do DataArrays without `time` coordinates. `confounds` can also be a DataFrame with a `time` column, validated like DataArray `time` coordinates; its other columns must be numeric and unique. `FirstLevelModel.fit` and `make_first_level_design_matrix` now accept `confounds` as a `(time, n_confounds)` DataArray, validated against the run's `time` coordinates ([\#398]()). - `register_volume` now supports random metric sampling via `metric_sampling_percentage` (`None` by default, disabling random sampling), with optional deterministic seeding via `metric_sampling_seed`, to speed up large affine or B-spline registrations ([\#396]()). - `register_volume` gained `fixed_intensity_scaling`/`moving_intensity_scaling` and `register_volumewise` gained `intensity_scaling` (all `"none"` by default) to rescale the images passed to the registration optimizer without affecting the returned/resampled data: `"db"`, `"sqrt"` (an alias for `0.5`), or any positive float exponent for power scaling. **\[Napari plugin\]** The Registration panel exposes the same selectors ([\#405]()). - `db_scale` and `data.fusi.scale.db` now default `factor` to `20` for complex-valued (amplitude) data and `10` otherwise, instead of always defaulting to `10` ([\#414]()). - `load_nifti` now follows the BIDS inheritance principle for matching JSON sidecars, so shared metadata stored at the dataset root or parent folders is preserved when loading recordings ([\#359]()). ### ⚡ Performance - `Atlas.get_masks`/`get_atlas_masks` no longer forces `xarray.concat` to recompute and compare the full lazily derived world-coordinate grid across every requested region (all layers share one grid by construction), the dominant cost for multi-region calls; it also now scans the annotation volume once per 8-region batch via a bitmask lookup instead of once per region. Together, a `get_masks([...])` call over dozens of regions (e.g. combining all of an ontology's major divisions into one coarse map) is over an order of magnitude faster ([\#412]()). ### 🐛 Fixes - `clean`, `regress_confounds`, `censor_samples`, and `interpolate_samples` now handle signals with pose-dependent `(time, pose)` `time` coordinates: `confounds` and `sample_mask` are aligned with the whole-volume time (as `consolidate_poses` computes it), NumPy inputs take that time, and signals are interpolated pose by pose ([\#398]()). - `plot_volume`/`plot_composite` now default planar VoxelData arrays to their singleton world dimension and preserve singleton display axes for explicit spatial slicing. `plot_napari`/`fusi.plot.napari` now default singleton spatial axes to sliders instead of the canvas, regardless of how the voxel-to-world affine maps them ([\#407]()). - `save_nifti` now always writes both a qform and sform (previously sform was silently dropped when no secondary affine had been explicitly recorded) ([\#278]()). - `load_scan` now opens Iconeus SCAN v1 files marked as `4DscanCustom` ([\#406]()). - `plot_composite` no longer produces a blank/NaN composite when either input has been scaled with `.fusi.scale.db()`; the `-inf` values `db_scale` assigns to zero-valued voxels are now excluded from the normalization bounds ([\#370]()). - `.fusi.scale.db()` and `.fusi.scale.log()` no longer emit a `RuntimeWarning` for zero/negative values when applied to Dask-backed data ([\#379]()). - `FirstLevelModel.fit` no longer errors on multi-pose data. Its implicit all-True mask (used when no `mask` is passed) now covers `pose` when the input has it, instead of collapsing to a single pose; the explicit-`mask` path no longer rejects a `pose`-carrying mask either ([\#278]()). - `plot_napari` and the napari plugin no longer crash on a non-spatial dimension with string coordinates (e.g. a recording-id stack dim); such a dimension now falls back to scale 1/origin 0 like any other missing world geometry ([\#409]()). ### 🔧 Maintenance - **\[Napari plugin\]** ConfUSIus now requires napari 0.9.0 or newer ([\#413]()). ### 🔧 Maintenance - Bumped the `brainglobe-atlasapi` dependency to v3 ([\#412]()). ## 0.6.1 Released 2026-08-07. ### ✨ Enhancements - Added `fetch_pereira_2025` and `fetch_pepe_mariani_2026` for the new OSF-hosted fUSI-BIDS re-exports ([\#361]()). - `load_echoframe_dat` now returns a ConfUSIus-ordered `(time, z, y, x)` DataArray and defaults `meta_path` to the sibling `ScanParameters.mat` file ([\#343]()). - New `confusius.decoding` module with `SearchLight`, which maps how well a cross-validated scikit-learn estimator predicts a target from the local neighborhood of each voxel ([\#334]()). ### 🐛 Fixes - Time resampling now keeps floating-point input dtypes and avoids unnecessary SciPy interpolation copies, reducing memory use for large `float32` arrays ([\#353]()). - NIfTI loading now keeps nibabel data lazy under Dask, EchoFrame `.dat` loading is now lazily chunked, and EchoFrame metadata reads current `xAxis`/`zAxis` fields ([\#343]()). - Confound regression now z-scores confounds when `standardize_confounds=True`, so motion-confound cleaning removes fluctuations without regressing baseline-related signal by default ([\#351]()). ## 0.6.0 Released 2026-07-18. ### 💥 Breaking changes - The `Atlas` class has been replaced by an `xarray.Dataset` with a registered `.atlas` accessor. Fetch an atlas by name with `fetch_brainglobe_atlas` and call operations through `ds.atlas.*` (`ds.atlas.get_masks`, `ds.atlas.get_meshes`, `ds.atlas.search`, `ds.atlas.ancestors`, `ds.atlas.resample_like`); `resample_like` now returns a Dataset. Name-based loading moved to `confusius.datasets`; atlas construction from a loaded BrainGlobe atlas is now internal to the datasets module ([\#274]()). - `resample_volume` and `resample_like` now use `fill_value` instead of `default_value` for out-of-field-of-view resampling, matching `register_volume` and the progress-plot resampling API. - Motion diagnostics helpers now require actual affine matrices: `extract_motion_parameters`, `compute_framewise_displacement`, and `create_motion_dataframe` no longer accept `None` placeholders in their affine lists ([\#302]()). - Renamed the public BIDS table I/O helpers to match the rest of ConfUSIus: `read_events` → `load_events`, and `write_events` → `save_events` ([\#294]()). - `fetch_landemard_2026` now resolves the dataset from OSF project `7cf9g` instead of `dkseb`. Existing local caches may need a one-time `refresh=True` to replace the cached OSF file index before downloading or checking for upstream updates ([\#311]()). - Axial velocity processing now always uses the standard Kasai estimator (`arg(mean(R1))`); the `estimation_method` and `absolute_velocity` arguments were removed, the corresponding metadata fields were dropped, and `spatial_kernel` now defaults to `3` and accepts explicit `(z, y, x)` sizes ([\#313]()). ### ✨ Enhancements - Atlases are now serializable: save and reload a complete atlas, including its structure hierarchy and region meshes, with `save_atlas` / `load_atlas`. The region `.obj` meshes are bundled into the Zarr store, so a reloaded atlas renders meshes without the BrainGlobe cache ([\#274]()). - `validate_atlas_dataset` checks that a Dataset is a well-formed atlas ([\#274]()). - `load_scan` now opens binary Iconeus SCAN v2 files in addition to HDF5 SCAN v1 files, detecting the format automatically. SCAN v2 support is experimental: data, timing, voxel spacing, the depth origin, provenance (subject/session/project/scan/experimenter, serial number, acquisition datetime), and BIDS-corresponding acquisition settings (probe model, center/transmit frequencies, pitch, focal depth, imaging depth, PRF, plane-wave angles, SVD low cutoff, power-Doppler integration window) are recovered (lateral and elevation axes are centered on zero). A `physical_to_lab` affine is derived from a header block read as a 6DOF probe pose (experimental, assumed convention), and a BPS sidecar composes a `physical_to_brain` affine as for v1. Multi-pose layouts are inferred ([\#317]()). - **\[Napari plugin\]** Added an interactive registration panel for volume alignment in napari, including linear and non-linear transforms, progress preview, manual and automatic initialization, saving/loading transforms, and forward/inverse transform application ([\#216]()). - Added `plot_motion_diagnostics` to visualize motion-correction summaries from `motion_params` tables returned by `register_volumewise` ([\#302]()). - Added `plot_design_matrix` to visualize a first-level GLM design matrix as a heatmap, with regressor names along the top and an optional acquisition-time y-axis (`index_yaxis`) ([\#331]()). - Added `plot_contrast_matrix` to visualize a GLM contrast, given as a string expression or a numeric vector/matrix, as a weight strip aligned with the design regressors ([\#331]()). - `create_motion_dataframe` now always reports all named rotation / translation axes exposed by the affine dimensionality, even when one spatial axis is singleton ([\#302]()). - Added `load_physio` to load BIDS physio TSV files with column names and metadata from the JSON sidecar, synthesizing a `time` column when needed; the napari plugin now uses it for imported signal tables ([\#294]()). - Added `fetch_khallaf_2026` for downloading the Khallaf et al. (2026) naked mole-rat fUSI dataset from Edmond, with `datasets`, `subjects`, `sessions`, `runs`, `reconstruction`, and `sourcedata` filters ([\#319]()). - Added standalone cosine high-pass filtering via `filter_cosine`, and `clean` can now use it with `filter_method="cosine"` ([\#321]()). ### 🐛 Fixes - Axial velocity estimation now scales the Kasai phase increment by the requested autocorrelation `lag`, so multi-volume lags no longer overestimate velocity ([\#313]()). - NIfTI loading no longer crashes when a sidecar `VolumeTiming` length disagrees with the actual data. ConfUSIus now ignores the malformed sidecar timing, falls back to `pixdim[4]` when available, and otherwise warns before using frame indices ([\#304]()). - Motion parameter tables from `create_motion_dataframe` now label rotations and translations by the coordinate names `x`/`y`/`z` instead of by raw transform-component order, so canonical ConfUSIus arrays stored as `(z, y, x)` no longer mislabel in-plane motion ([\#301]()). - **\[Napari plugin\]** The signal import dialog now finds BIDS physio files ending in `.tsv.gz`, keeps the x-axis cursor visible for imported-only plots when enabled, and lets you import multiple signal files in one go ([\#294]()). - Opening a `.scan` file that is not the legacy HDF5-based Iconeus format now raises a clear error that points users to newer SCAN v2 files and to converting them to NIfTI with Iconeus tools first ([\#297]()). - Plotting functions now accept a slice dimension reduced to a scalar coordinate by a single-index selection, so `plot_contours(atlas.annotation.sel(z=6))` works like `sel(z=[6])` ([\#296]()). ### 📚 Documentation - Fixed velocity sign interpretation in [Beamformed IQ user guide]() ([\#313]()). - Added a [searchlight decoding example]() that decodes locomotion speed from a single fUSI plane and compares the searchlight map against a matched GLM ([\#334]()). - Added a first-level GLM example that fits a voxel-wise first-level model to a stimulus-evoked olfactory task with the [Khallaf et al. 2026]() fUSI dataset ([\#320]()). ### 🔧 Maintenance - \[Example Gallery\]: pandas DataFrame outputs in the example gallery now render with clean, theme-aware notebook styling instead of a fully-bordered table ([\#307]()). - \[Example Gallery\]: Cells can now hide their code behind a collapsed callout with a `collapse` cell tag, with optional custom title and type (`collapse[]: `), i.e. `# %% tags=["collapse[warning]: Collapsed warning"]` ([\#309](<https://github.com/confusius-tools/confusius/pull/309>)). - \[Example Gallery\]: Hovering a gallery card reveals the example's first paragraph as an overlay over the card ([\#327](<https://github.com/confusius-tools/confusius/pull/327>)). ## 0.5.2 Released 2026-07-10. ### 🔧 Maintenance - Python 3.14 now keeps `xarray[accel]` everywhere except macOS Intel, where ConfUSIus falls back to plain `xarray` to avoid a `numba` / `llvmlite` build failure caused by napari's macOS Intel `numba<=0.62.1` cap. ## 0.5.1 Released 2026-07-10. ### ✨ Enhancements - **\[Napari plugin\]** Added a `File > Open Sample` entries for a [Nunez-Elizalde 2022](<https://confusius.tools/dev/citing/#nunez-elizalde-et-al-2022>) mouse recording and for a pair of [Cybis Pereira 2026](<https://confusius.tools/dev/citing/#cybis-pereira-et-al-2026>) rat recordings. Samples are fetched on demand, shows download progress with an abort button, and only downloads the matching raw fUSI files instead of the full dataset ([\#273](<https://github.com/confusius-tools/confusius/pull/273>)). - Dataset fetchers now print the citation to use for the fetched data and accept a `print_citation` argument to silence it. The template fetchers `fetch_template_huang_2025` and `fetch_template_pepe_mariani_2026` also expose the citation on the returned DataArray as `da.attrs["citation"]` ([\#279](<https://github.com/confusius-tools/confusius/pull/279>)). - Dataset fetchers called with `refresh=True` now re-download cached files whose upstream MD5 changed, comparing the cached dataset index against the freshly fetched one instead of only checking whether the file exists; downloads are additionally verified against the index MD5. A locally cached dataset whose `dataset_index.json` predates this format is detected on fetch and reported with a clear error naming the directory to delete and re-fetch, rather than being silently mishandled. Affects `fetch_cybis_pereira_2026`, `fetch_nunez_elizalde_2022`, and `fetch_landemard_2026` ([\#261](<https://github.com/confusius-tools/confusius/pull/261>)). - Added `sample_displacement_field`, `sample_displacement_field_like`, and `invert_displacement_field` to sample a B-spline (or composite affine + B-spline) registration transform into a dense displacement field and invert it via SimpleITK's `InvertDisplacementFieldImageFilter`. `resample_volume` and `resample_like` now also accept displacement fields directly, so a saved B-spline transform's inverse can be applied without a closed-form inverse ([\#235](<https://github.com/confusius-tools/confusius/pull/235>)). ### 🐛 Fixes - Saving to Zarr (via `save` or `DataArray.fusi.save`) now works for data carrying affines or other numpy-valued attributes: nested numpy arrays are stored as lists and non-serializable attrs (e.g. matplotlib colormaps) are dropped with a warning, matching the NIfTI sidecar behaviour ([\#284](<https://github.com/confusius-tools/confusius/pull/284>)). - B-spline control-point DataArrays returned by `register_volume` no longer have their per-axis grid geometry (spacing, origin, domain) swapped between axes on anisotropic images. The bug was invisible on isotropic data, which is why it went unnoticed since it shipped [\#235](<https://github.com/confusius-tools/confusius/pull/235>). - `plot_napari` no longer sets `viewer.scale_bar.unit` (and the napari 0.7.0 `FutureWarning` is gone for good). The previous workaround in [\#271](<https://github.com/confusius-tools/confusius/pull/271>) is no longer needed: napari ≥ 0.7.1 infers the scale bar unit from the layer's `units` attribute, which `plot_napari` already forwards from the spatial coordinates. - `plot_volume` and friends no longer crash on matplotlib ≥ 3.11 when a `threshold` is set. `LinearSegmentedColormap.from_list` now requires strictly monotonic `(value, color)` pairs, and the threshold gray band could collide with neighbouring cmap entries at the boundary values. - `plot_volume`, `plot_stat_map`, `plot_composite`, and `VolumePlotter.add_contours` no longer silently reorder panels when `slice_mode`'s own coordinate isn't already sorted (e.g. a `region` dimension built from an arbitrary list of acronyms, or a descending `z`). Only the two display dimensions are sorted for plotting geometry now ([\#268](<https://github.com/confusius-tools/confusius/pull/268>)). ### 📚 Documentation - The [same-subject registration example](<https://confusius.tools/dev/examples/_built/registration/register_volume_same_subject/index.md>) now follows the rigid registration step with a B-spline refinement, showing the extra local correction it adds and how its parameters differ from the rigid step's ([\#235](<https://github.com/confusius-tools/confusius/pull/235>)). - Long output in gallery examples—warnings, text reprs, tracebacks, and rich-rendered text such as the dataset citation banner—now wraps instead of showing a horizontal scrollbar ([\#285](<https://github.com/confusius-tools/confusius/pull/285>)). ### 🔧 Maintenance - Raised the minimum supported versions to **napari 0.7.1** and **matplotlib 3.11**. - The example-gallery build tool now accepts specific example scripts as arguments (`uv run python tools/build_gallery.py docs/examples/01_io/01_confusius_xarray_101.py`), running only those; the rest of the gallery is still rendered, taken from cache if present or built without outputs ([\#285](<https://github.com/confusius-tools/confusius/pull/285>)). ## 0.5.0 Released 2026-07-07. ### 💥 Breaking changes - Registration now takes a single `initialization` parameter in place of `centering_initialization` and `initial_transform`. `initialization` accepts `"center_geometry"`, `"center_moments"`, a homogeneous affine matrix or `None` for an identity initialization. Affects `register_volume`, `register_volumewise`, and the `data.fusi.register` accessor ([\#215](<https://github.com/confusius-tools/confusius/pull/215>)). ### ✨ Enhancements - Added `plot_matrix` for plotting 2D matrices (e.g. connectivity or correlation matrices), with optional lower/diagonal triangle masking, grid lines, and a `groups` parameter that annotates contiguous label runs with colored rectangle strips—useful for marking anatomical groupings (e.g. cortex, thalamus) when there are too many individual labels to read ([\#243](<https://github.com/confusius-tools/confusius/pull/243>)). - **\[Napari plugin\]** Integer-dtype files (e.g. atlas annotations, ROI masks) opened via the `confusius` CLI, the Data Panel, or the native napari file readers (drag-and-drop / **File \> Open**) are now added as a `Labels` layer with per-label colors, instead of an `Image` layer with the wrong colormap ([\#257](<https://github.com/confusius-tools/confusius/pull/257>)). - **\[Napari plugin\]** Added **Events** panel to annotate temporal events within Napari. Events shade the signal plot; active event names appear in the time overlay; load from / save to a BIDS `.tsv` ([\#176](<https://github.com/confusius-tools/confusius/pull/176>)). - `confusius.bids` module is now public with new `load_events` and `save_events` ([\#176](<https://github.com/confusius-tools/confusius/pull/176>)). - Added a `datasets` CLI namespace, listed in `confusius --help`: `confusius datasets --list` prints the table of available datasets, their sizes, and whether each is cached on disk. A bare `confusius PATH...` still launches the viewer ([\#234](<https://github.com/confusius-tools/confusius/pull/234>)). - Added `NMF` for non-negative matrix factorization of fUSI time series, wrapping `sklearn.decomposition.NMF` with the same xarray-aware `fit`/`transform`/`inverse_transform` interface as `PCA` and `FastICA`. Both `mode='temporal'` and `mode='spatial'` are supported ([\#211](<https://github.com/confusius-tools/confusius/pull/211>)). - Added `adjust_pvalues` for generic multiple-comparison correction of p-value maps, and `apply_statistical_threshold` to threshold z-scaled statistical DataArrays with the same family-wise-error (Bonferroni, Šidák, Holm, Holm-Šidák, Simes-Hochberg, Hommel) or false-discovery-rate (Benjamini-Hochberg, Benjamini-Yekutieli) corrections, plus an optional cluster-extent threshold ([\#204](<https://github.com/confusius-tools/confusius/pull/204>)). - Added `fetch_landemard_2026` for downloading the Landemard et al. (2026) fUSI-BIDS dataset from OSF, with `datasets`, `subjects`, `acqs`, and `datatypes` filters ([\#228](<https://github.com/confusius-tools/confusius/pull/230>)). - Added `plot_stat_map` (and the matching `data.fusi.plot.stat_map` accessor) for plotting statistical maps, optionally overlaid fully opaque on a background anatomical volume. `vmin`/`vmax` default to the data's actual min/max, and `auto_range=True` (default) picks both the colormap range and colormap from the data's sign: diverging symmetric `[-m, m]` with `"coolwarm"` when both signed, sequential `[0, vmax]` with `"viridis"` when non-negative, or `[vmin, 0]` with `"viridis_r"` when non-positive ([\#242](<https://github.com/confusius-tools/confusius/pull/242>)). - `plot_volume` and `plot_stat_map` (and their `data.fusi.plot.*` accessors) now accept `cbar_kwargs`, forwarded to `matplotlib.figure.Figure.colorbar`—useful to shrink a shared colorbar down to size on a multi-panel grid ([\#242](<https://github.com/confusius-tools/confusius/pull/242>)). - `apply_affine` and the `data.fusi.affine.apply` accessor now accept a string naming a key in `attrs["affines"]`, instead of requiring the affine matrix itself ([\#247](<https://github.com/confusius-tools/confusius/pull/247>)). - Plotting functions that slice along `slice_mode` ( `plot_volume`, `plot_contours`, `plot_composite`, and the `VolumePlotter` methods) now support non-numeric coordinates (e.g. region/mask labels), so a single call can slice a stacked connectivity or ROI map by label instead of looping over `.sel()` per panel ([\#250](<https://github.com/confusius-tools/confusius/pull/250>)). ### 🐛 Fixes - `plot_volume` and other image plotting functions now raise a clear `ValueError` when `vmin`/`vmax` (or a passed-in `norm`) resolve to a non-finite value, instead of crashing deep inside `matplotlib.colors.LinearSegmentedColormap.from_list` with an opaque `IndexError` ([\#259](<https://github.com/confusius-tools/confusius/pull/259>)). - `save_nifti` now drops attrs that cannot be serialized to JSON as-is (e.g. matplotlib `ListedColormap`/`BoundaryNorm` objects) instead of writing their `str()` repr into the sidecar, which could corrupt fields such as `cmap` on reload. A warning lists the dropped keys. `confusius.load` now rebuilds `cmap`/`norm` from `rgb_lookup` when they are missing, so atlas-derived masks and annotations keep their canonical colors after a save/load round-trip. **\[Napari plugin\]** The reader now falls back to the `"gray"` colormap (with a napari warning) instead of crashing when a layer's `cmap` attr is not a valid napari colormap name ([\#255](<https://github.com/confusius-tools/confusius/pull/255>)). - `Atlas.get_masks` now suffixes the `mask` coordinate with `_L`/`_R` for `sides="left"`/`"right"`, so requesting the same region on both hemispheres no longer produces duplicate `mask` values. `extract_with_labels` no longer requires unique region ids across stacked mask layers—a layer is already identified by its position along `mask`—so `get_masks` output can be passed straight through without manual relabeling ([\#249](<https://github.com/confusius-tools/confusius/pull/249>)). - `apply_affine` now rescales the `voxdim` attribute of the spatial coordinates along with the coordinate values ([\#245](<https://github.com/confusius-tools/confusius/pull/245>)). - `clean` now supports `ensure_finite=True` to repair non-finite `signals` and `confounds` by interpolating along time, fills censored boundary samples from the nearest kept sample before filtering, and accepts `interpolate_kwargs` for pre-scrubbing interpolation ([\#239](<https://github.com/confusius-tools/confusius/pull/239>)). - Image plotting functions now leave `alpha` unset by default (`None`), so a colormap's built-in alpha channel is respected ([\#225](<https://github.com/confusius-tools/confusius/pull/225>)). - `load_nifti` no longer drops affines loaded from the JSON sidecar (e.g. `bspline_initialization` written by the registration pipeline) when merging in the NIfTI qform/sform affines ([\#222](<https://github.com/confusius-tools/confusius/pull/222>)). - `save_nifti` no longer maps non-time additional axes to the NIfTI 4th slot. When additional axes are present in the DataArray, a degenerate length-1 `time` axis is inserted at NIfTI axis 4 (NIfTI's conventional time slot) so non-time additional axes always land at NIfTI axes 5, 6, 7. The original dim name for each additional axis is always written to the sidecar as `ConfUSIusDim{N}Name` (with `N` in 4, 5, 6, matching the 0-based NIfTI axis of the extra dim). The matching `ConfUSIusDim{N}Coordinates` entry is only written when the coord cannot be reconstructed from `pixdim` (i.e. when the coord does not start at 0 with regular spacing); otherwise the spacing is stored in `pixdim` and the coord is rebuilt as `step * arange(size)` on load. Attributes are preserved in `ConfUSIusDim{N}Attributes` entries. ([\#223](<https://github.com/confusius-tools/confusius/pull/223>)). ### 📚 Documentation - Add an [NMF example](<https://confusius.tools/dev/examples/_built/decomposition/nmf_single_recording/index.md>) to the gallery, demonstrating the z-score + absolute-value standardization that makes signed fUSI signals NMF-compatible. - Add an [atlas-based region correlation matrix example](<https://confusius.tools/dev/examples/_built/connectivity/atlas_correlation_matrix/index.md>) to the gallery, demonstrating registration to the Pepe-Mariani 2026 template, resampling the Allen Mouse Brain Atlas onto a recording's native grid, and plotting a region correlation matrix with `plot_matrix`'s `groups` annotation ([\#243](<https://github.com/confusius-tools/confusius/pull/243>)). ### 🔧 Maintenance - Simplified the NIfTI save path: time and extra-dimension voxel spacings are now written directly to the header `pixdim` instead of through nibabel's `set_zooms` (that was overwritten anyways), dropping a redundant spatial write that the qform immediately overwrote. Behavior is unchanged. ([\#253](<https://github.com/confusius-tools/confusius/pull/253>)). ## 0.4.0 *Released 2026-06-25.* ### ✨ Enhancements - The `confusius` CLI now accepts multiple fUSI data files in a single invocation (e.g. `confusius fixed.nii moving.nii`). Each file is added as its own image layer, named after the file's basename ([\#206](<https://github.com/confusius-tools/confusius/pull/206>)). - `data.fusi.affine.apply` now accepts affines with rotation and shear. The axis-aligned part updates the 1D `z`/`y`/`x` coordinates and the method returns the residual orientation as a 4x4 affine (the identity for diagonal affines) for the caller to use as they wish ([\#188](<https://github.com/confusius-tools/confusius/pull/188>)). - Add `smoothing_fwhm` parameter to `FirstLevelModel`. Smoothing is applied to each run before model fitting ([\#201](<https://github.com/confusius-tools/confusius/pull/201>)). ### ⚡ Performance - `process_iq_blocks` now uses `dask.array.map_blocks` for non-overlapping outer IQ windows and batches overlapping windows with explicit overlap before mapping blocks, reducing Dask overhead in common blockwise processing workflows ([\#190](<https://github.com/confusius-tools/confusius/pull/190>)). ### 🐛 Fixes - Masks are now coerced to boolean by `validate_mask` (added `return_dtype_as_bool` parameter that defaults to `True`) to avoid DataArrays using *positional indexing*. Previously these masks could select the wrong voxels or, for `register_volume`, silently disable the metric mask ([\#197](<https://github.com/confusius-tools/confusius/pull/197>)). - `process_iq_blocks` now handles strongly overlapping IQ windows without corrupting the output time dimension, so power Doppler and related IQ reducers work when `window_stride < window_width / 2` ([\#192](<https://github.com/confusius-tools/confusius/pull/192>)). - `load_nifti` now anchors `physical_to_qform` to the same physical frame as the primary (sform) coordinates, so the stored qform affine maps the array's physical coordinates to qform world space ([\#187](<https://github.com/confusius-tools/confusius/pull/187>)). - `save_nifti` now preserves each affine's own translation, so a NIfTI file with sform and qform round-trips through `load_nifti`/`save_nifti` without corrupting the qform ([\#187](<https://github.com/confusius-tools/confusius/pull/187>)). - **\[Napari plugin\]** Fixed the Signals plot x-axis for volumes without a time dimension. It now follows the slider axis world coordinates, with a matching label and dropdown option ([\#180](<https://github.com/confusius-tools/confusius/pull/180>)). ## 0.3.0 *Released 2026-05-27.* ### 💥 Breaking changes - `register_volume` now also returns a `RegistrationDiagnostics` dataclass with the per-iteration metric values, final metric value, iteration count, optimizer stop condition, and the metric name. `register_volumewise` always adds per-frame `final_metric_value` and `n_iterations` columns to `motion_params`, and exposes the full per-frame diagnostics list under `attrs["registration_diagnostics"]` only when called with `keep_diagnostics=True` to avoid retaining the full optimizer metric trace by default ([\#139](<https://github.com/confusius-tools/confusius/pull/139>)). - Renamed `validate_voxeldata` to `validate_fusi_dataarray` ([\#153](<https://github.com/confusius-tools/confusius/pull/153>)). ### ✨ Enhancements - Added a `mask` argument to the `PCA`, `FastICA`, `SeedBasedMaps`, and `FirstLevelModel` estimators, restricting fitting and projection to the selected voxels. Output maps retain the full spatial geometry, with voxels outside the mask set to `0` ([\#155](<https://github.com/confusius-tools/confusius/pull/155>)). - Added `plot_composite`, `VolumePlotter.add_composite`, and a matching `data.fusi.plot.composite` accessor that render two volumes as a red/cyan RGB overlay ([\#145](<https://github.com/confusius-tools/confusius/pull/145>)). - Added `datatypes` filter to `fetch_cybis_pereira_2026`, allowing downloads to be scoped to specific BIDS datatype directories (`"fusi"`, `"angio"`, `"motion"`) ([\#141](<https://github.com/confusius-tools/confusius/pull/141>)). - Added `fetch_template_huang_2025` for downloading and loading the Huang et al. vascular mouse template from OSF, with cache/refresh behavior matching existing template fetchers ([\#162](<https://github.com/confusius-tools/confusius/pull/162>)). - Added `show_progress` to volumewise registration so joblib progress output can be disabled in scripted or quiet workflows ([\#126](<https://github.com/confusius-tools/confusius/pull/126>)). - Added a reusable `validate_fusi_dataarray` validator and refactored IQ/registration validation to use it. Core dimension coordinates are now validated as 1D, numeric, finite, and strictly increasing, while extra/non-dimension coordinates remain allowed ([\#153](<https://github.com/confusius-tools/confusius/pull/153>)). - Added shared `fontsize` parameter to `plot_volume`, `plot_contours`, and carpet plotting entry points so text sizing is consistent across all plotting APIs ([\#128](<https://github.com/confusius-tools/confusius/pull/128>)). - Replaced plotting `black_bg` with explicit `bg_color` and `fg_color` controls for clearer visual customization ([\#124](<https://github.com/confusius-tools/confusius/pull/124>)). - Added `FastICA` transformer for independent component analysis of fUSI recordings, with the same xarray-aware `fit` / `transform` / `inverse_transform` API as `PCA` ([\#118](<https://github.com/confusius-tools/confusius/pull/118>)). - Added example gallery helper utilities to streamline writing and maintaining docs examples ([\#102](<https://github.com/confusius-tools/confusius/pull/102>)). ### ⚡ Performance - Top-level `confusius` and `confusius.xarray` namespaces now use [SPEC-0001](<https://scientific-python.org/specs/spec-0001/>) PEP 562 lazy loading. Submodules and exported functions are only imported on first access, reducing `import confusius` overhead for workflows that use a subset of the package. ### 🐛 Fixes - Fixed `resample_like` and `resample_volume` filling out-of-FOV voxels with `0.0` when resampling onto a larger grid. This caused a bright background artifact for dB-scaled data (where 0 is maximum intensity). The `default_value` parameter now defaults to `float(moving.min())` instead of `0.0`. `register_volume` gains a `fill_value` parameter that overrides the default for both the final resampled output and the live progress composite overlay ([\#138](<https://github.com/confusius-tools/confusius/pull/138>)). - Fixed plotting hover information silently disappearing when the returned `VolumePlotter` was not held in a variable (e.g. `obj.fusi.plot.volume().show()`). The hover manager is now kept alive until the figure is closed ([\#148](<https://github.com/confusius-tools/confusius/pull/148>)). - Fixed napari x-axis extent computation to ignore the interactive cursor guide line, preventing incorrect plot bounds ([\#111](<https://github.com/confusius-tools/confusius/pull/111>)). ### 📚 Documentation - Added a [Registration of two sessions from the same subject](<https://confusius.tools/dev/examples/registration/register_volume_same_subject.py>) example demonstrating `register_volume`, the new diagnostics, and confusius's `plot_composite` overlay pattern for inspecting alignment before and after registration ([\#139](<https://github.com/confusius-tools/confusius/pull/139>)). ### 🔧 Maintenance - Switched documentation hosting to GitHub Pages with `mike` versioning and automatic PR preview deployments ([\#134](<https://github.com/confusius-tools/confusius/pull/134>)). ## 0.2.0 *Released 2026-05-05.* First official public beta release of ConfUSIus. ### ✨ Highlights - ConfUSIus now covers the core alpha roadmap, including I/O, beamformed IQ processing, registration, quality control, atlas integration, signal processing, decomposition, functional connectivity, and general linear model workflows. - The package provides both a Python API and a napari plugin for interactive data loading, visualization, signal inspection, and quality control. ### 📝 Notes - `0.1.0` was used only to reserve the `confusius` project name on PyPI and is not a supported public release. `0.2.0` is therefore the first official public release series for ConfUSIus. # Community ConfUSIus is developed in the open. This page gathers ways to contribute, report issues, and connect with the people maintaining the project. ## Core Developers - --- Postdoctoral researcher, [Cortexlab](<https://www.ucl.ac.uk/brain-sciences/cortexlab>). - --- Independent researcher. ## Acknowledgments We gratefully acknowledge Mark S. Blumberg, PhD, and Brett Dwyer, Research Associate, of the University of Iowa Department of Psychological and Brain Sciences, for their feedback on the ConfUSIus GUI plugin. We also thank them for providing an example SCAN v2 file that helped guide the implementation of the experimental SCAN v2 loader. ## Contributing We welcome contributions to ConfUSIus. This guide will help you get started. ### Getting Started 1. Open an issue to discuss your idea or bug fix. 2. Fork the repository on GitHub. 3. Clone your fork locally. 4. Set up the development environment. 5. Make your changes. 6. Submit a pull request. ### Development Installation ```bash # Clone your fork git clone https://github.com/confusius-tools/confusius.git cd confusius # Install with development dependencies uv sync # Run tests just test # Run pre-commit hooks just pre-commit ``` ### Code Style - Use [ruff](<https://docs.astral.sh/ruff/>) for linting and formatting. - Run pre-commits (e.g., using [prek](<https://prek.j178.dev/>)) and tests before committing. - Add tests for new functionality. - Update documentation as needed. ### Documentation Documentation is built on GitHub Actions and deployed to a separate GitHub Pages repository ([confusius-tools/confusius-docs](<https://github.com/confusius-tools/confusius-docs>)). Every pull request gets an automatic preview, with a link posted as a comment on the PR. The preview is cleaned up automatically when the PR is closed. To build and serve the docs locally: ```bash just sd ``` **Adding documentation images.** Image generators live in `docs/images/<topic>/generate.py` (outputs are gitignored; only the script is committed). If you add a new one: - Add it to the `just generate-doc-images` recipe in `justfile`. - Register it in the **Generate documentation images** step of `.github/workflows/docs.yml`. - If it uses an already-cached dataset, add the script path to that dataset's `hashFiles(...)` call in the workflow so the cache invalidates when the script changes. If it needs a new dataset, add a dedicated cache step. **Adding examples.** Example scripts live in `docs/examples/` and are discovered automatically by the gallery builder — no cache update needed (the cache key already covers `docs/examples/**/*.py`). After adding a script, add its built output path to the `nav` in `zensical.toml`. ### Reporting Issues If you find a bug or have a feature request: 1. Check if the issue already exists. 2. Create a new issue with a clear description. 3. Include steps to reproduce (for bugs). 4. Include code examples if applicable. ### Code of Conduct This project follows the [Contributor Covenant Code of Conduct](<https://github.com/confusius-tools/confusius/blob/main/CODE_OF_CONDUCT.md>). By participating, you are expected to uphold it. # Citing ConfUSIus If you use ConfUSIus in your research, please cite it using the following reference: > Le Meur-Diebolt, S., & Cybis Pereira, F. (2026). ConfUSIus (v0.8.0). Zenodo. https://doi.org/10.5281/zenodo.18611124 Or in BibTeX format: ```bibtex @software{confusius, author = {Le Meur-Diebolt, Samuel and Cybis Pereira, Felipe}, title = {ConfUSIus}, year = {2026}, publisher = {Zenodo}, version = {v0.8.0}, doi = {10.5281/zenodo.18611124}, url = {https://doi.org/10.5281/zenodo.18611124} } ``` --- ## Third-Party Libraries ConfUSIus stands on the shoulders of giants. It is built on top of many excellent open-source projects, without which it could not exist. If you use the features listed below, please consider citing the corresponding projects to support these efforts. ### BrainGlobe The `atlas` module uses the [BrainGlobe Atlas API](<https://brainglobe.info>) to interface with neuroanatomical atlases. If you use the atlas features in your research, please also cite BrainGlobe: > Claudi, F., Petrucco, L., Tyson, A. L., Branco, T., Margrie, T. W., & Portugues, R. (2020). BrainGlobe Atlas API: a common interface for neuroanatomical atlases. *Journal of Open Source Software*, 5(54), 2668. https://doi.org/10.21105/joss.02668 Or in BibTeX format: ```bibtex @article{brainglobe, author = {Claudi, Federico and Petrucco, Luigi and Tyson, Adam L. and Branco, Tiago and Margrie, Troy W. and Portugues, Ruben}, title = {{BrainGlobe} {Atlas} {API}: a common interface for neuroanatomical atlases}, journal = {Journal of Open Source Software}, year = {2020}, volume = {5}, number = {54}, pages = {2668}, doi = {10.21105/joss.02668}, url = {https://doi.org/10.21105/joss.02668} } ``` ### Napari The [ConfUSIus GUI](<https://confusius.tools/gui/overview>) is built on top of [napari](<https://napari.org>), a powerful multi-dimensional image viewer for Python. If you use the ConfUSIus GUI in your research, please also cite napari: > napari contributors (2019). napari: a multi-dimensional image viewer for Python. Zenodo. https://doi.org/10.5281/zenodo.3555620 Or in BibTeX format: ```bibtex @software{napari, author = {{napari contributors}}, title = {napari: a multi-dimensional image viewer for {Python}}, year = {2019}, publisher = {Zenodo}, doi = {10.5281/zenodo.3555620}, url = {https://doi.org/10.5281/zenodo.3555620} } ``` ### Nilearn The `signal`, `glm`, and `connectivity` modules contain code derived from [Nilearn](<https://nilearn.github.io>). If you use these modules in your research, please also cite Nilearn: > Nilearn contributors (2023). Nilearn. Zenodo. https://doi.org/10.5281/zenodo.8397156 Or in BibTeX format: ```bibtex @software{nilearn, author = {{Nilearn contributors}}, title = {Nilearn}, year = {2023}, publisher = {Zenodo}, doi = {10.5281/zenodo.8397156}, url = {https://doi.org/10.5281/zenodo.8397156} } ``` ### SimpleITK The `registration` module uses [SimpleITK](<https://simpleitk.org>) for image registration and resampling. If you use the registration features in your research, please also cite SimpleITK: > Beare, R., Lowekamp, B., & Yaniv, Z. (2018). Image Segmentation, Registration and Characterization in R with SimpleITK. *Journal of Statistical Software*, 86(8), 1–35. https://doi.org/10.18637/jss.v086.i08 Or in BibTeX format: ```bibtex @article{simpleitk, author = {Beare, Richard and Lowekamp, Bradley and Yaniv, Ziv}, title = {Image Segmentation, Registration and Characterization in {R} with {SimpleITK}}, journal = {Journal of Statistical Software}, year = {2018}, volume = {86}, number = {8}, pages = {1--35}, doi = {10.18637/jss.v086.i08}, url = {https://doi.org/10.18637/jss.v086.i08} } ``` ## Datasets ### Nunez-Elizalde et al. (2022) The `fetch_nunez_elizalde_2022` function provides a fUSI-BIDS conversion of this dataset. If you use it, please cite: > Nunez-Elizalde, A. O., Krumin, M., Reddy, C. B., Montaldo, G., Urban, A., Harris, K. D., & Carandini, M. (2022). Neural correlates of blood flow measured by ultrasound. *Neuron*, 110(10), 1631–1640.e4. https://doi.org/10.1016/j.neuron.2022.02.012 Or in BibTeX format: ```bibtex @article{nunez-elizalde_neural_2022, title = {Neural correlates of blood flow measured by ultrasound}, volume = {110}, issn = {08966273}, url = {https://linkinghub.elsevier.com/retrieve/pii/S0896627322001775}, doi = {10.1016/j.neuron.2022.02.012}, language = {en}, number = {10}, journal = {Neuron}, author = {Nunez-Elizalde, Anwar O. and Krumin, Michael and Reddy, Charu Bai and Montaldo, Gabriel and Urban, Alan and Harris, Kenneth D. and Carandini, Matteo}, month = may, year = {2022}, pages = {1631--1640.e4}, } ``` License: **[CC BY 4.0](<https://creativecommons.org/licenses/by/4.0/>)**. ### Cybis Pereira et al. (2026) The `fetch_cybis_pereira_2026` function provides a fUSI-BIDS conversion of this dataset. If you use it, please cite: > Cybis Pereira, F., Castedo, S. H., Meur-Diebolt, S. L., Ialy-Radio, N., Bhattacharya, S., Ferrier, J., Osmanski, B. F., Cocco, S., Monasson, R., Pezet, S., & Tanter, M. (2026). A vascular code for speed in the spatial navigation system. *Cell Reports*, 45(1). https://doi.org/10.1016/j.celrep.2025.116791 Or in BibTeX format: ```bibtex @article{cybispereiraVascularCodeSpeed2026, title = {A Vascular Code for Speed in the Spatial Navigation System}, author = {Cybis Pereira, Felipe and Castedo, Sebastian H. and {Meur-Diebolt}, Samuel Le and {Ialy-Radio}, Nathalie and Bhattacharya, Soumee and Ferrier, Jeremy and Osmanski, Bruno F{\'e}lix and Cocco, Simona and Monasson, Remi and Pezet, Sophie and Tanter, Micka{\"e}l}, year = 2026, month = jan, journal = {Cell Reports}, volume = {45}, number = {1}, publisher = {Elsevier}, issn = {2211-1247}, doi = {10.1016/j.celrep.2025.116791}, urldate = {2025-12-30}, langid = {english}, keywords = {animal speed,cerebral blood volume,continuous attractor network, CP: neuroscience,freely moving,functional ultrasound imaging, hippocampus,locomotion,path integration,spatial navigation}, } ``` License: **[CC BY 4.0](<https://creativecommons.org/licenses/by/4.0/>)**. ### Landemard et al. (2026) The `fetch_landemard_2026` function provides a fUSI-BIDS re-export of this dataset. If you use it, please cite: > Landemard, A., Krumin, M., Harris, K. D., & Carandini, M. (2026). Brainwide blood volume reflects opposing neural populations. *Nature*. https://doi.org/10.1038/s41586-026-10350-9 Or in BibTeX format: ```bibtex @article{landemard_brainwide_2026, title = {Brainwide blood volume reflects opposing neural populations}, author = {Landemard, Agn{\`e}s and Krumin, Michael and Harris, Kenneth D. and Carandini, Matteo}, year = {2026}, month = apr, journal = {Nature}, publisher = {Springer Science and Business Media {LLC}}, doi = {10.1038/s41586-026-10350-9}, url = {https://doi.org/10.1038/s41586-026-10350-9} } ``` License: **[CC BY-NC 4.0](<https://creativecommons.org/licenses/by-nc/4.0/>)**. ### Pereira et al. (2025) The `fetch_pereira_2025` function provides a fUSI-BIDS re-export of this dataset. If you use it, please cite: > Pereira, M., Droguerre, M., Valdebenito, M., Vidal, L., Marcy, G., Benkeder, S., Marchal, P., Comte, J.-C., Pascual, O., Zimmer, L., & Vidal, B. (2025). Induction of haemodynamic travelling waves by glial-related vasomotion in a rat model of neuroinflammation: Implications for functional neuroimaging. *eBioMedicine*, 116, 105777. https://doi.org/10.1016/j.ebiom.2025.105777 ```bibtex @article{pereira_induction_2025, title = {Induction of haemodynamic travelling waves by glial-related vasomotion in a rat model of neuroinflammation: implications for functional neuroimaging}, volume = {116}, doi = {10.1016/j.ebiom.2025.105777}, journal = {eBioMedicine}, author = {Pereira, Mickaël and Droguerre, Marine and Valdebenito, Marco and Vidal, Louis and Marcy, Guillaume and Benkeder, Sarah and Marchal, Paul and Comte, Jean-Christophe and Pascual, Olivier and Zimmer, Luc and Vidal, Benjamin}, year = {2025}, pages = {105777}, } ``` License: **[CC BY 4.0](<https://creativecommons.org/licenses/by/4.0/>)**. ### Pepe, Mariani et al. (2026) The `fetch_pepe_mariani_2026` function provides a fUSI-BIDS re-export of this dataset. If you use it, please cite: > Pepe, C., Mariani, J.-C., Urosevic, M., Gini, S., Stuefer, A., Ricci, F., Galbusera, A., Iurilli, G., & Gozzi, A. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (Fusi). https://doi.org/10.64898/2026.02.05.704055 ```bibtex @misc{pepe_structural_2026, title = {Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging ({Fusi})}, doi = {10.64898/2026.02.05.704055}, url = {http://biorxiv.org/lookup/doi/10.64898/2026.02.05.704055}, author = {Pepe, Chiara and Mariani, Jean-Charles and Urosevic, Mila and Gini, Silvia and Stuefer, Alexia and Ricci, Fabio and Galbusera, Alberto and Iurilli, Giuliano and Gozzi, Alessandro}, year = {2026}, } ``` License: **[CC BY 4.0](<https://creativecommons.org/licenses/by/4.0/>)**. ### Khallaf et al. (2026) The `fetch_khallaf_2026` function provides a fUSI-BIDS conversion of this dataset. If you use it, please cite: > Khallaf, M. A., Hart, D. W., Luo, W., Murad, F., Cybis Pereira, F., Mendez-Aranda, D., Hagenah, N., Rossi, A., Bégay, V., Okrouhlík, J., Krautwurst, D., Ngalameno, M. K., Ganswindt, A., Barker, A. J., Šumbera, R., Knaden, M., Pezet, S., Woehler, A., Hansson, B. S., Bennett, N. C., & Lewin, G. R. (2026). A queen odour mediates reproductive suppression in a eusocial mammal. *Nature*. https://doi.org/10.1038/s41586-026-10772-5 Or in BibTeX format: ```bibtex @article{khallafQueenOdourMediates2026, title = {A Queen Odour Mediates Reproductive Suppression in a Eusocial Mammal}, author = {Khallaf, Mohammed A. and Hart, Daniel W. and Luo, Wenhan and Murad, Firdevs and Cybis Pereira, Felipe and {Mendez-Aranda}, Daniel and Hagenah, Nicole and Rossi, Alice and B{\'e}gay, Val{\'e}rie and Okrouhl{\'i}k, Jan and Krautwurst, Dietmar and Ngalameno, Mungo Kisinza and Ganswindt, Andre and Barker, Alison J. and {\v S}umbera, Radim and Knaden, Markus and Pezet, Sophie and Woehler, Andrew and Hansson, Bill S. and Bennett, Nigel C. and Lewin, Gary R.}, year = 2026, month = jul, journal = {Nature}, issn = {0028-0836, 1476-4687}, doi = {10.1038/s41586-026-10772-5}, url = {https://doi.org/10.1038/s41586-026-10772-5}, urldate = {2026-07-15}, } ``` License: **[CC0 1.0](<https://creativecommons.org/publicdomain/zero/1.0/>)**. ## Templates ### Brunner, Grillet et al. (2021) The `fetch_template_brunner_grillet_2021` function provides an Allen-aligned vascular mouse template. If you use this template in your research, please cite the paper and source dataset: > Brunner, C., Grillet, M., Urban, A., Roska, B., Montaldo, G., & Macé, E. (2021). Whole-brain functional ultrasound imaging in awake head-fixed mice. *Nature Protocols*, 16, 3547–3571. https://doi.org/10.1038/s41596-021-00548-8 > > Macé, E., & Montaldo, G. (2020). Whole-brain visually evoked activity maps in head-fixed awake mice (June 2021 metadata update). *Zenodo*. https://doi.org/10.5281/zenodo.4905862 Or in BibTeX format: ```bibtex @article{brunner2021, title = {Whole-brain functional ultrasound imaging in awake head-fixed mice}, author = {Brunner, Clément and Grillet, Micheline and Urban, Alan and Roska, Botond and Montaldo, Gabriel and Macé, Emilie}, journal = {Nature Protocols}, volume = {16}, number = {7}, pages = {3547--3571}, year = {2021}, doi = {10.1038/s41596-021-00548-8}, } @dataset{mace_montaldo2020, title = {Whole-brain visually evoked activity maps in head-fixed awake mice}, author = {Macé, Emilie and Montaldo, Gabriel}, publisher = {Zenodo}, year = {2020}, note = {June 2021 metadata update}, doi = {10.5281/zenodo.4905862}, } ``` The template conversion reorders axes and assigns the Allen 50 µm coordinate grid without changing vascular voxel values. Also cite Wang et al. (2020) for the Allen CCF reference framework: [https://doi.org/10.1016/j.cell.2020.04.007](<https://doi.org/10.1016/j.cell.2020.04.007>). License: **[CC BY 4.0](<https://creativecommons.org/licenses/by/4.0/>)**. ### Pepe, Mariani et al. (2026) The `fetch_template_pepe_mariani_2026` function provides a mouse fUSI template derived from Pepe, Mariani et al. (2026). If you use this template in your research, please also cite the corresponding article: > Pepe, C., Mariani, J.-C., Urosevic, M., Gini, S., Stuefer, A., Ricci, F., Galbusera, A., Iurilli, G., & Gozzi, A. (2026). Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging (fUSI). *bioRxiv*. https://doi.org/10.64898/2026.02.05.704055 Or in BibTeX format: ```bibtex @article{pepe2026fusi, author = {Pepe, Chiara and Mariani, Jean-Charles and Urosevic, Mila and Gini, Silvia and Stuefer, Alexia and Ricci, Fabio and Galbusera, Alberto and Iurilli, Giuliano and Gozzi, Alessandro}, title = {Structural and dynamic embedding of the mouse functional connectome revealed by functional ultrasound imaging ({fUSI})}, journal = {bioRxiv}, year = {2026}, publisher = {Cold Spring Harbor Laboratory}, doi = {10.64898/2026.02.05.704055}, url = {https://doi.org/10.64898/2026.02.05.704055} } ``` License: **[CC BY 4.0](<https://creativecommons.org/licenses/by/4.0/>)**.