Skip to content

feat(connectivity): add CAP analysis#116

Draft
sdiebolt wants to merge 43 commits into
mainfrom
feat/caps
Draft

feat(connectivity): add CAP analysis#116
sdiebolt wants to merge 43 commits into
mainfrom
feat/caps

Conversation

@sdiebolt
Copy link
Copy Markdown
Member

@sdiebolt sdiebolt commented May 12, 2026

Closes #114.

Summary

  • Adds CAP class implementing co-activation pattern analysis via k-means clustering of fUSI volumes
  • Supports three clustering geometries: "correlation" (Pearson, default), "cosine", and "euclidean"
  • Custom Lloyd-style cosine k-means with k-means++ initialization for correlation/cosine metrics; sklearn KMeans for Euclidean
  • fit() accepts a list of recordings (or a single DataArray); labels are stored per-recording so recording boundaries are respected for temporal metrics
  • predict() assigns new recordings to fitted CAPs
  • compute_temporal_metrics() returns temporal fraction, episode counts, persistence (time-coord-aware, handles irregular sampling), transition frequency, and transition probability matrix
  • select_n_clusters() helper with elbow, silhouette, Davies-Bouldin, and variance-ratio criteria
  • NaN detection in _prepare_data with a clear error pointing to background-voxel z-scoring as the likely cause

sdiebolt added 3 commits May 11, 2026 19:44
Remove shape-only and attribute-only tests in favour of the correctness
and invariant tests already present. Reuse the shared `sample_4d_volume`
fixture where a single recording suffices.
@sdiebolt sdiebolt requested review from FelipeCybis and Copilot May 12, 2026 09:58
@sdiebolt sdiebolt self-assigned this May 12, 2026
@sdiebolt sdiebolt added the enhancement New feature or request label May 12, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new confusius.connectivity.CAP estimator implementing co-activation pattern (CAP) analysis via k-means clustering of fUSI volumes, including temporal dynamics metrics and cluster-count selection utilities.

Changes:

  • Introduces CAP class with cosine/correlation (custom spherical k-means) and euclidean (sklearn KMeans) clustering backends.
  • Adds temporal metrics computation (fraction, counts, persistence, transitions) and select_n_clusters() helper with multiple criteria.
  • Adds unit tests for fit(), predict(), and temporal metrics; exports CAP from confusius.connectivity.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
src/confusius/connectivity/cap.py New CAP estimator, clustering routines, temporal metrics, and cluster selection helper.
src/confusius/connectivity/init.py Exposes CAP in the connectivity public API.
tests/unit/test_connectivity/test_cap.py Adds unit tests covering CAP fitting, prediction, and metrics.
docs/includes/abbreviations.md Adds CAP/CAPs/SSQ abbreviations for documentation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/confusius/connectivity/cap.py
Comment thread src/confusius/connectivity/cap.py
Comment thread src/confusius/connectivity/cap.py Outdated
Comment thread src/confusius/connectivity/cap.py Outdated
Comment thread src/confusius/connectivity/cap.py
Comment thread tests/unit/test_connectivity/test_cap.py Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 12, 2026

Codecov Report

❌ Patch coverage is 97.41379% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/confusius/connectivity/cap.py 97.66% 8 Missing ⚠️
src/confusius/signal/confounds.py 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

sdiebolt added 13 commits May 12, 2026 11:15
- Fix empty-cluster zero-vector bug: mask empty centers to -inf before
  argmax so they cannot beat valid centers with negative similarity
- Store _spatial_dims during fit; transpose recordings in predict to
  guarantee feature alignment regardless of input dim order
- Fix docstring: caps_.attrs["long_name"] is "CAP" not "Co-activation patterns"
- Rename frames → volumes throughout (function, units string, docstrings,
  variable name, test assertions and comments)
…or tests

Add tests for update_rule='mean', metric='cosine', metric='euclidean'
predict, NaN input, invalid update_rule, and all select_n_clusters
methods and error conditions.
Two changes:

1. fit() and select_n_clusters(): replace xr.concat() with np.concatenate()
   on stacked numpy arrays, then preprocess in-place. This eliminates one
   full-data copy for correlation/cosine metrics (~2 GB savings at typical
   scale of 50 recordings × 300 volumes × 32k voxels).

2. predict() euclidean path: replace (n_samples × n_caps × n_features) 3D
   broadcast with the identity ||x-c||² = ||x||² + ||c||² - 2x·c, reducing
   peak allocation from ~236 MB to ~7 KB per recording.
Add 11 tests targeting previously uncovered paths:
- empty cluster warning (also exercises coincident-center init branch,
  empty-mask branch in Lloyd loop, and empty-cluster cleanup)
- mean update rule in the cosine k-means loop
- cosine predict path (_preprocess in_place=False)
- NaN input in predict()
- integer n_init (multiple restarts) and invalid n_init
- select_n_clusters(): euclidean KMeans path, invalid metric/update_rule,
  empty recordings list, NaN input

Remaining 3% is unreachable code (RuntimeError guard, dead _find_elbow
single-value branch, external Rich Progress integration).
- _run_multi_cosine_kmeans: replace None-sentinel initialization with
  direct assignment from the first seed; remove the unreachable
  RuntimeError guard that required best_centers to be None after at
  least one k-means run.
- _find_elbow: remove n==1 early-return; select_n_clusters validates
  len(cluster_range) >= 2 before calling this function.
Add test_best_restart_selected: with n_clusters=10 and random_state=0,
restart 1 produces lower inertia than restart 0, so n_init=2 gives
different labels than n_init=1. The test would fail if the best-restart
update assignment were dropped.

Data is constructed locally with a fixed seed to be independent of the
session-scoped rng fixture's advancing state.
- Add `scores_` attribute (list of DataArrays parallel to `labels_`)
  computed by `fit()`: cosine similarity for correlation/cosine metrics,
  negative L2 distance for euclidean (higher = stronger assignment)
- Add `score_samples()` method mirroring `predict()` for scoring new data
- Add `score_threshold` parameter to `compute_temporal_metrics()`: volumes
  below threshold are censored (treated as -1) so they act as episode
  breaks and are excluded from all metric numerators while the total-volume
  denominator stays fixed, matching the motion-scrubbing convention
- Change default `update_rule` from "weighted" to "mean", the theoretically
  correct update for spherical k-means; update docstring accordingly
Add Parameters, Returns, and Notes sections to _relative_luminance
and _auto_fg_color; include WCAG 2.1 spec link in _relative_luminance.
Expand _create_deterministic_time_series docstring with Returns section.
…r_confounds

A Dask array chunked along time produced silently wrong (all near-zero)
variance values, causing an unhelpful "all voxels have zero variance" error
instead of a clear indication of the root cause. validate_time_series already
checks for time chunking, so passing the signal through it is sufficient.
…unds

noise_mask.values.flatten() used the mask's own dimension order, which
silently misaligned with signals.stack(space=spatial_dims) when the mask
had its spatial dims in a different order. validate_mask catches name and
coordinate mismatches but not ordering differences, so wrong (background,
zero-variance) voxels were selected. Fixed by stacking the mask with the
same spatial_dims as the signals before extracting values.
@sdiebolt sdiebolt force-pushed the main branch 7 times, most recently from 601802d to c92fe20 Compare May 14, 2026 03:11
@sdiebolt sdiebolt force-pushed the main branch 2 times, most recently from 59f4b05 to a0a999d Compare May 14, 2026 11:35
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 14, 2026

📖 Doc preview: https://confusius.tools/pr-preview/pr-116/

sdiebolt and others added 26 commits May 18, 2026 08:59
* feat: `cybis_pereira_2026` dataset should accept datatype filter

* docs(changelog): add datatypes filter entry for cybis_pereira_2026

* docs(changelog): fill in PR number 141
Co-authored-by: sdiebolt <3774850+sdiebolt@users.noreply.github.com>
Two separate commits to confusius-docs (git push + gh api PUT for
versions.json) raced with deploy-docs on PR merge, causing the API call
to fail with a stale SHA. Silently swallowed by 2>/dev/null.

Combines both into one git commit using a local jq edit, and adds
set -euo pipefail for visibility.
* fix(plotting): keep hover manager alive after plotter is GC'd

matplotlib's CallbackRegistry holds bound-method callbacks as
WeakMethod, so the hover manager was collected as soon as the
returning VolumePlotter went out of scope (e.g.
`atlas.annotation[80:81].fusi.plot.volume().show()`), silently
disabling status-bar hover.

Anchor active managers in a module-level set and discard them on
matplotlib's close_event (and on clear()), mirroring the pattern
used by stateful mpl helpers such as MultiCursor. Also rename
_RegionHoverManager to _HoverManager.

* docs: add changelog entry

* fix: add docstrings to module-level constant

* add regressiont test for the hover manager
…ume (#139)

* feat(registration): return diagnostics from register_volume

Adds a RegistrationDiagnostics dataclass that carries the per-iteration
metric values, final metric value, iteration count, optimizer stop
condition, and the metric name. register_volume always attaches a
sitkIterationEvent observer (regardless of show_progress) and returns
the diagnostics as a third element of the result tuple, so callers can
plot convergence curves, detect runs that failed to converge, and
compare runs without re-instrumenting the optimizer.

Closes #138

* feat(registration): propagate diagnostics through volumewise and accessor

register_volumewise now collects the per-frame RegistrationDiagnostics
and exposes them in two ways: as a list under
result.attrs["registration_diagnostics"], and as new final_metric_value
and n_iterations columns on the motion_params DataFrame. The xarray
.fusi.register.to_volume accessor's return annotation is also extended
to reflect the new 3-tuple shape.

* docs(registration): add register_volume two-acquisitions example

Walks through aligning two power Doppler volumes from different
sessions of the same animal with register_volume, using confusius's
plot_volume overlay pattern for the before/after composites and the
new RegistrationDiagnostics to plot the convergence curve. Adds two
callouts: a warning about the sensitivity of the optimizer arguments
and a tip pointing at show_progress=True for the live progress plot.

* docs(changelog): document registration diagnostics return value

Adds a Breaking changes section to the 0.3.0.dev0 entry for the new
register_volume 3-tuple return signature, a matching Enhancements
bullet for the RegistrationDiagnostics dataclass and volumewise
propagation, and a Documentation entry linking the new example page.

* docs(changelog): point registration diagnostics entries to PR #139

* ci(docs): invalidate Nunez-Elizalde cache for the new registration example

The example fetches CR020 sessions 20191120/22 that are not part of the
files the existing cache key hashed, so CI kept restoring the stale
cache subset. Add the example path to the hashFiles list so the key
changes whenever the example changes, forcing a re-fetch that pulls in
the sessions it needs.

* ci(docs): pre-fetch dataset files before the gallery build

The gallery builds each example twice (light + dark) in separate
kernels. When the first run triggers an actual OSF download, its
rich-progress bar lands in the cell's stream outputs while the second
run finds the files already cached and emits nothing. The light/dark
zip in tools/gallery/render.py then fails with
"argument 2 is shorter than argument 1".

Pre-fetch the specific subject/session/task/acq subsets used by the
gallery examples before invoking build_gallery.py so both runs see a
warm cache and produce identical output counts. The call is idempotent
on already-cached files, so it is a no-op on subsequent runs.

* docs: quick touchups in the registration notebook

Consistency with the first 101 notebook, fix spaces around em-dashes, etc.

* fix(gallery): keep executing past cell errors so tracebacks render inline

NotebookClient previously ran with the default allow_errors=False and
the gallery re-raised CellExecutionError as a RuntimeError, aborting
the entire docs build the moment one example hit a runtime error.

allow_errors=True lets execution continue: failing cells emit a regular
'error' output that the renderer (tools/gallery/render.py) already
inlines into the rendered Markdown — exactly what a normal Jupyter
notebook would show after a failed run. A broken example now degrades
to "renders with a visible traceback in the docs" instead of breaking
CI for every other example.

* fix(gallery): pair light/dark cell outputs without strict-zip crash

Light and dark builds run in independent kernels, so non-deterministic
stream messages (one-time warnings, OSF download progress bars, etc.)
can occasionally leave the two output lists at different lengths.
zip(..., strict=True) then raised "argument 2 is shorter than argument
1" and aborted the entire gallery, even when the rendered page would
have been fine without the offending output.

Switch to a plain zip() that drops the trailing extras of the longer
side and emits a warning so the asymmetry stays visible during local
and CI builds. Keep a structural sanity check on the cell-count level:
mismatched cell counts after filtering internal cells still raise,
since that always indicates the two executions diverged at the source-
notebook level.

* test(gallery): cover continue-past-cell-errors behaviour

Replace the old test that asserted CellExecutionError was re-raised as
a RuntimeError with one that pins the new contract: a failing cell
emits an `error` output (later inlined by the renderer) and execution
continues to the next cell instead of aborting the gallery build.

* test(gallery): cover light/dark output asymmetry

Add a regression test pinning the new renderer contract: when one of
the two themed builds emits an extra stream output (e.g. a one-time
warning or download bar), the renderer pairs what it can, drops the
trailing extras, and emits a UserWarning instead of crashing. Also
drop a defensive cell-count pre-check on the way in — the outer
strict-zip already raises on that internal invariant, and per
CLAUDE.md we shouldn't validate scenarios the rest of the pipeline
already guarantees.

* docs(gallery): no need to pre-fetch data anymore

* docs: remove duplicated information from changelog entries

* feat(registration): gate full register_volumewise diagnostics behind keep_diagnostics

Each per-frame RegistrationDiagnostics carries the full per-iteration
metric trace, which adds up to substantial extra memory on long
recordings. Make the diagnostics list opt-in via a new
keep_diagnostics flag (default False) on register_volumewise (and the
.fusi.register.volumewise xarray accessor), so the heavy payload is
only attached to attrs["registration_diagnostics"] when explicitly
requested.

The cheap per-frame summaries (final_metric_value, n_iterations,
one value each) are still appended to motion_params unconditionally
— useful for spotting frames that failed to converge and inexpensive
enough to ship by default.

* docs(examples): rewrite registration walkthrough for plot_composite

Switch the two-acquisitions registration example from the per-channel
plot_volume + add_volume overlay onto plot_composite, and move the
underlying data from Nunez-Elizalde 2022 (CR020 fUSI mean) to a pair of
Cybis Pereira 2026 rat75 angio scans recorded on consecutive days.

The markdown is rewritten to match: red/blue + blended purple language
becomes red/cyan + desaturated grey, the resample_like/identity-matrix
preamble is gone, and the new section explains why moving.z is forced
onto fixed.z before plotting (resampling otherwise lands outside the
fixed slab and returns an empty image). show_progress, the wider
convergence window and the larger learning rate are kept inline so the
optimizer reaches a stable fit on this dataset.

* ci(docs): centralize dataset prefetching in a single script

Add tools/prefetch_doc_datasets.py listing every fetch_* call made by
the userguide image generators and the gallery examples, with args
mirroring each call site exactly. Wire it into .github/workflows/docs.yml
to run once before the image-generation and gallery-build steps so both
consume warm caches, and switch the Nunez-Elizalde and Cybis Pereira
dataset cache keys to hash only the prefetch script - it is now the
single source of truth for what the docs pipeline downloads.

Update AGENTS.md so contributors adding a new image generator or
example know to add the matching fetch call to the prefetch script
rather than touching the cache key directly.

* refactor(docs): fail the gallery build on light/dark output mismatch

The renderer used to warn and drop trailing outputs when a code cell
produced different numbers of outputs in its light and dark passes -
useful while debugging, but it silently hid real divergence between
the two rendered notebooks.

Replace the warn-and-drop with a ValueError naming the offending cell
and the most common cause (one-shot side effects like dataset
downloads or first-import warnings). The dataset-prefetch step added
in the previous commit means all known sources of legitimate
divergence are now eliminated before the gallery runs.

* docs(gallery): remove show_progress=True from register_volume

* test(gallery): expect ValueError for light/dark output mismatch

Mirrors the renderer change in d302cd5: the test used to assert the
warn-and-drop behavior, so it failed on the strict version. Rename
the test, switch pytest.warns to pytest.raises, and drop the markdown
assertion since the renderer no longer produces output for mismatched
cells.

* actually use show_progress

* fix(ci): move gallery cache out of .cache/ to survive zensical build

The 'Build documentation' step runs 'zensical build --clean --strict',
and zensical's --clean wipes the entire .cache/ directory before
populating it with its own files. That destroyed .cache/gallery/
between the gallery build and the cache action's post-step, leaving
the cache empty at save time and triggering 'Path Validation Error:
Path(s) specified in the action for caching do(es) not exist, hence
no cache is being saved.'

Move the gallery cache to .gallery-cache/ at the repo root so it sits
outside zensical's reach. Also update the mkdir step, the cache
action's path, and the .gitignore entry.

* docs: link example in changelog to .py and not ephemeral .md

* docs(gallery): harmonize plots facecolor

* docs(gallery): rename example

* fix(registration): use data min as default fill for out-of-FOV voxels

When resampling moves a volume onto a larger fixed grid, voxels outside
the original FOV were filled with 0.0. For dB-scaled data this is the
maximum intensity, producing a bright artifact in the composite overlay
and the final registered output.

- register_volume: add fill_value param (default: moving.min()) applied
  to both the final sitk.Resample and the progress plotter composite
- RegistrationProgressPlotter: replace fill_value with resample_kwargs
  dict forwarding default_value, interpolation, and sitk_threads
- add_composite / plot_composite: replace fill_value with resample_kwargs
  forwarded to resample_like; auto-injects default_value=data2.min()

* docs(gallery): remove fig.show() calls from registration example

* fix(docs): affine -> rigid in example notebook

* docs(changelog): document fill_value fix for out-of-FOV voxels in dB data

* docs: minor touchups in registration example

* refactor(registration): move fill_value default to resample_like/resample_volume

Filling out-of-FOV voxels with data.min() is a resampling concern, not a
plotting one. Shift the default from the plotting layer into resample_volume
and resample_like (default_value: float | None = None → moving.min()).

Remove the redundant auto-injection from add_composite; update changelog
to credit the fix at the right layer.

* fix(xarray): thread fill_value and resample_kwargs through xarray accessors

* chore: rename example notebook

* chore: minor docstring update

---------

Co-authored-by: Samuel Le Meur-Diebolt <samuel@diebolt.io>
Compute moving.min() only when fill_value is actually needed for resampling/progress composites to avoid unnecessary work.

Raise ValueError with supported interpolation names in progress plotting instead of bubbling KeyError.

Also fix docs references to the renamed registration example in changelog and dataset prefetch comments.
* feat(decomposition): add FastICA transformer

Wrap sklearn FastICA with the same xarray-aware decomposition API as PCA so component analyses preserve fUSI metadata. Tighten PCA and FastICA tests around sklearn parity and validation behavior to keep the wrappers aligned.

* test(decomposition): stabilize FastICA wrapper tests

Use deterministic FastICA test configurations and synthetic data where convergence on tiny random fixtures was flaky across CI platforms. Keep the assertions focused on wrapper behavior instead of solver luck.

* docs: add FastICA to decomposition guide

Document FastICA alongside PCA on the decomposition user guide placeholder page so users can discover both available transformers while the full guide is still under construction.

* docs: enforce optional/boolean/private-helper docstring conventions

- Replace `type or None, default: None` with `type, optional` throughout.
- Use "If not provided, ..." instead of "If \`None\`, ...".
- Use "Whether to ..." for boolean parameters instead of "If True/False, ...".
- Require full NumPy docstrings on private helper methods.
- Document all three rules in AGENTS.md.

* refactor(decomposition): extract shared base class for PCA and FastICA

Add _BaseFUSIDecomposer with all shared xarray bookkeeping:
fit_transform, transform, inverse_transform, _prepare_data,
_reshape_component_matrix, _reshape_mean, _store_fit_metadata,
_store_feature_names, _get_time_coord, and __sklearn_is_fitted__.

PCA and FastICA now only contain __init__ and fit; FastICA also keeps
_reshape_feature_component_matrix for its mixing_ attribute.

Drop the forced float64 cast in _prepare_data and inverse_transform —
sklearn handles type promotion internally, and PCA accepts float32.

* docs(decomposition): add FastICA changelog entry and PCA gallery example

- Add FastICA entry to 0.3.0.dev0 changelog with PR #118 link.
- Add decomposition gallery section with a PCA example covering scree
  plot, component maps, time courses, and variance-threshold denoising
  using the Nunez-Elizalde 2022 dataset.

* feat(docs): add gallery ordering, standardize step, and transparent figures

- Rename example dirs with numeric prefixes (01_io, 02_decomposition) so the
  gallery builder resolves sections in the correct order; discover.py strips the
  prefix so built URLs stay clean (io/, decomposition/).
- Add _gallery_dark_theme bool to the gallery builder theme-setup cell and make
  figure/savefig facecolor transparent ('none') across all themes, removing the
  need for per-notebook dark-theme detection.
- Update confusius_xarray_101.py to use _gallery_dark_theme from the builder
  instead of inspecting rcParams.
- Rewrite pca_single_recording.py: standardize input with cf.signal.standardize,
  replace matplotlib image panels with cf.plotting.plot_volume, add thumbnail
  tag to component-maps cell, show a single frame (not the time mean) in the
  reconstruction comparison.
- Add Decomposition section to zensical.toml nav after Getting Started.

* fix(docs): detect dark theme from axes.facecolor, not an injected variable

_gallery_dark_theme would be undefined when notebooks are run in Binder or
downloaded directly. Instead, read axes.facecolor, which the builder still
sets to a solid theme colour (#ffffff / #111720) while figure.facecolor is
kept transparent. In standalone usage the matplotlib default (#ffffff) means
dark_theme = False, which is correct.

* docs(examples): remove redundant figure.patch.set_alpha(0) calls

The gallery builder now sets figure.facecolor to 'none' globally, so
explicit patch alpha resets are no longer needed.

* feat(docs): enable glightbox on gallery output images

Removed the skip-lightbox class from _image_tag so glightbox's auto mode
picks up rendered cell outputs. The auto_themed setting in zensical.toml
handles the #only-light / #only-dark dual images correctly.

* fix(plotting): respect transparent rcParam in VolumePlotter; single-row comparison

VolumePlotter._ensure_figure no longer overrides figure.patch.set_facecolor
when rcParams['figure.facecolor'] is 'none', so the gallery builder's
transparent background setting is respected without per-cell patch calls.

PCA example: replace three separate plot_volume figures with a single
pre-made 1x3 subplot figure, eliminating the single-panel padding artefact
and putting all comparison panels in one row.

* revert(plotting): undo VolumePlotter facecolor change; restore patch.set_alpha(0)

Reverts the confusius-side rcParam check. Gallery scripts explicitly call
plotter.figure.patch.set_alpha(0) (or fig.patch.set_alpha(0) for pre-made
figures) after each plot_volume call instead.

* fix(docs): fix ordering of the cards display in the examples page

* docs(examples): explain theme color setup and simplify transparency handling

* docs: improve docstrings for PCA/FastICA

* docs(examples): split PCA analysis from SVD denoising notebooks

* feat(decomposition): add spatial ICA mode and rename components_ to maps_

Add `mode` parameter to FastICA ("spatial"/"temporal", default "spatial").
Spatial mode fits on the transposed (voxels, time) matrix and finds spatially
independent maps, matching FSL MELODIC's default behaviour and giving a
better-determined problem for fUSI data where voxels >> time points.

Rename `components_` to `maps_` across PCA and FastICA to reflect that the
attribute stores spatial maps (loadings for PCA, IC maps or unmixing directions
for FastICA), not the independent components themselves (which are returned by
transform()). Update tests, docs, and user guide accordingly.

Remove exploratory SVD denoising and Marchenko-Pastur heuristic example scripts
that are still under investigation.

* refactor(decomposition): drop mixing_ attribute and _reshape_feature_component_matrix from FastICA

mixing_ was redundant in spatial mode (just maps_.T reshaped) and unused by
any internal logic in both modes. The base class transform/inverse_transform
delegate entirely to _estimator, so no caller needed it.

* docs(examples): overhaul PCA example and add it to nav

Rewrite the PCA example with richer explanations of the decomposition
orientation (temporal PCA, voxels as features), orthogonality of maps,
and uncorrelated time courses. Replace the separate time-course plot with
a combined 6-row layout (spatial map + time course side by side) using
plot_volume on per-row axes with symmetric color limits. Add the FastICA
example to the zensical.toml nav.

* docs(examples): add FastICA example and polish both decomposition notebooks

Add the FastICA example with expanded prose that compares ICA to PCA,
links back to the PCA notebook, and mirrors its section structure. Apply
symmetric color limits (vmin=-vmax) to the 12-component overview grids
in both notebooks. Fix broken cross-references: instance attributes and
constructor parameters now link to the class, and the invalid
fit(param) syntax is removed.

* docs(changelog): add fontsize plotting API entry for #128

* docs(changelog): add maintenance section with GitHub Pages migration (#134)

* test(decomposition): parametrize FastICA tests over both modes and replace shape checks with reference comparisons

- Drop test_fit_transform_returns_dataarray from both test_fastica and
  test_pca — shape/dim assertions are covered implicitly by the reference
  implementation tests.
- Parametrize test_wrapper_matches_sklearn_attributes and
  test_inverse_transform_matches_sklearn over ["spatial", "temporal"].
  The spatial branch reconstructs the reference manually: fit sklearn on
  X.T, extract spatial maps via sklearn.transform(X.T).T, compute time
  courses as (X - mean) @ maps.T, and verify transform output, maps_,
  mean_, whitening_ absence, and n_iter_ against that reference.
  The inverse_transform branch verifies time_courses @ spatial_maps + mean.
- Decomposition module coverage remains at 100%.

* refactor: prefix example names by the order it will appear in the zensical sidebar menu

* feat(decomposition): add temporal mode to PCA and FastICA

Both PCA and FastICA now support mode="temporal" (fit on (time, voxels)) alongside
the existing mode="spatial". The shared spatial projection logic (previously a
_SpatialFastICAProxy shim in FastICA) is lifted into _BaseFUSIDecomposer as
_spatial_transform / _spatial_inverse_transform, used by both classes.

FastICA default hyperparameters are updated to align with FSL MELODIC: fun="cube"
(pow3) and max_iter=500.

Example scripts are expanded to demonstrate both modes side by side.

* docs: adapt gallery plot colors to docs style

* docs: restore axes facecolor in examples

* fix(docs): rename examples for ordering

* Update docs/examples/03_decomposition/02_fastica_single_recording.py

Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>

* Update docs/examples/02_registration/01_register_volume_same_subject.py

Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>

* Update docs/examples/03_decomposition/02_fastica_single_recording.py

Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>

* Update src/confusius/decomposition/fastica.py

Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>

* Update src/confusius/decomposition/fastica.py

Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>

* Update docs/examples/03_decomposition/01_pca_single_recording.py

Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>

* Update src/confusius/decomposition/fastica.py

Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>

* Update docs/examples/03_decomposition/02_fastica_single_recording.py

Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>

* Update docs/examples/03_decomposition/02_fastica_single_recording.py

Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>

---------

Co-authored-by: Felipe Cybis Pereira <felipe.cybispereira@gmail.com>
Co-authored-by: Felipe Cybis Pereira <41338087+FelipeCybis@users.noreply.github.com>
* fix(docs): make gallery cache branch-agnostic

Remove binder ref from gallery execution cache fingerprint so expensive gallery builds are reused across branches. Rewrite the Binder button URL on cached markdown during build so links still target the current branch/ref.

* feat(docs): cache generated docs images in CI

Add a dedicated Actions cache for docs/images outputs and skip image generation on cache hits to speed up docs builds.
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Samuel Le Meur-Diebolt <samuel@diebolt.io>
…158)

Bumps [marocchino/sticky-pull-request-comment](https://github.com/marocchino/sticky-pull-request-comment) from 2 to 3.
- [Release notes](https://github.com/marocchino/sticky-pull-request-comment/releases)
- [Commits](marocchino/sticky-pull-request-comment@v2...v3)

---
updated-dependencies:
- dependency-name: marocchino/sticky-pull-request-comment
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Samuel Le Meur-Diebolt <samuel@diebolt.io>
* test(xarray): add wrapper forwarding tests

* test(xarray): cover remaining accessor branches

* test(xarray): split connectivity wrapper forwarding cases
Co-authored-by: sdiebolt <3774850+sdiebolt@users.noreply.github.com>
* feat(datasets): add Huang 2025 template fetcher

* docs(changelog): add PR link for Huang template

* docs(citing): harmonize citation style and licenses

* cite peer-reviewed verion for Cybis Pereira 2026

---------

Co-authored-by: Felipe Cybis Pereira <felipe.cybispereira@gmail.com>
* feat(validation): add generic dataarray validators

Introduce validate_fusi_dataarray for shared structural validation, refactor IQ validation to build on it, and rename validate_iq to validate_iq_dataarray.

* test(iq): fix stale error message patterns and non-monotonic time fixtures

Update three match strings to reflect new validate_fusi_dataarray messages,
and fix two time coordinate arrays that were accidentally non-monotonic
(duplicate/decreasing values), causing the general validator to fire before
the Butterworth-specific regularity check.

* fix(validation): import spacing helper from coordinates

* refactor(validation): reuse fusi validator in registration

* refactor(registration): enforce strict fusi validation

* test(validation): clarify coord policy and enforce invariants

* refactor(validation): require numeric core coordinates

* docs(changelog): note validation API breaking changes

* refactor(validation): scope regular-spacing checks

Add regular_spacing_dims selector to validate_fusi_dataarray with spatial default. Update motion to enforce spatial-only regular spacing. Integrate validation in smooth_volume while preserving missing-coordinate behavior. Standardize shared test sample DataArrays with spatial voxdim metadata and migrate validation tests to shared 3D+t fixture with explicit 2D+t coverage.

* refactor(validation): refine regular-spacing dim modes

Support regular_spacing_dims values spatial/core/all/explicit sequence. Spatial remains default. Core checks canonical dims; all and explicit dims skip non-numeric coordinates. Update validation tests to use shared 3D+t fixture patterns and cover new mode semantics.

* test(spatial): cover smooth validation re-raise path

Add regression test for smooth_volume branch that re-raises non-missing-coordinate validation errors from validate_fusi_dataarray.

* feat(validation): simplify regular-spacing dim selection

* test(xarray): use 3dt fixtures in wrapper tests

* refactor: put dimension coordinate checks inside `_validate_dimension_coordinate`

* Explicify missing coordinates for dims when required spacing

* test(validation): cover single-dim regular spacing selector

---------

Co-authored-by: Felipe Cybis Pereira <felipe.cybispereira@gmail.com>
…el GLM (#155)

* feat(connectivity): optimize masked SeedBasedMaps path

* test(decomposition): cover masked reconstruction paths

* refactor(decomposition): use unmask for masked reconstruction

* test(decomposition): tolerate float drift in PCA noise variance

* test(connectivity): cover masked SeedBasedMaps branches

* refactor(glm): unify masked first-level handling

* fix(masking): align masked dimension ordering

* refactor(validation): add exact mask dim option

Add  to  and use it in GLM and decomposition to remove duplicated full-spatial-dim checks.

* fix(tests): fix assertion matching

* refactor(decomposition): apply mask via extract_with_mask

Replace manual stack/reindex/ravel masking in _prepare_data with the
extract_with_mask helper, so the unmask round-trip uses its matching
counterpart. require_exact_dims on validate_mask already guarantees
mask/data dim order, making the explicit coordinate alignment redundant.

Drop the X_stacked plumbing through _store_fit_metadata and remove the
write-only _feature_coord_, _full_feature_coord_, and _feature_mask_
attributes; reconstruction relies solely on _reconstruction_mask_.

* docs(changelog): add mask argument entry for #155

---------

Co-authored-by: Felipe Cybis Pereira <felipe.cybispereira@gmail.com>
* chore: prepare v0.3.0 release

* chore: misc formatting
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](actions/github-script@v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sdiebolt <3774850+sdiebolt@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement co-activation pattern (CAP) analysis

3 participants