Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
67 changes: 67 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: tests

on:
push:
branches: [master, claude_code_dev]
pull_request:
branches: [master]
workflow_dispatch:

jobs:
matlab-tests:
name: MATLAB ${{ matrix.matlab-release }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
matlab-release: [R2024b]

steps:
- name: Checkout CanlabCore
uses: actions/checkout@v4
with:
path: CanlabCore

- name: Checkout Neuroimaging_Pattern_Masks
uses: actions/checkout@v4
with:
repository: canlab/Neuroimaging_Pattern_Masks
path: Neuroimaging_Pattern_Masks

- name: Clone SPM25
run: git clone --depth=1 --branch 25.01.02 https://github.com/spm/spm.git "${GITHUB_WORKSPACE}/spm"

- name: Set up MATLAB
uses: matlab-actions/setup-matlab@v2
with:
release: ${{ matrix.matlab-release }}
# README lists Statistics and Signal Processing as required.
# Without these, functions like nanmean / filtfilt fall back to
# SPM's bundled compat shims, which is fragile.
products: |
Statistics_and_Machine_Learning_Toolbox
Signal_Processing_Toolbox
cache: true

- name: Run unit tests
uses: matlab-actions/run-command@v2
with:
command: |
addpath(genpath('CanlabCore'));
addpath(genpath('Neuroimaging_Pattern_Masks'));
% NB: addpath, not genpath — SPM ships compat shims under
% external/fieldtrip/compat that shadow MATLAB builtins (flip,
% isfile, etc.) and break matlab.unittest discovery.
addpath('spm');
cd CanlabCore/CanlabCore/Unit_tests;
results = canlab_run_all_tests('JUnit', fullfile(getenv('GITHUB_WORKSPACE'), 'test-results.xml'));
if any([results.Failed]); error('CanlabCore:tests:failed', '%d failed, %d incomplete', sum([results.Failed]), sum([results.Incomplete])); end

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.matlab-release }}
path: test-results.xml
if-no-files-found: warn
74 changes: 74 additions & 0 deletions .github/workflows/tests-walkthroughs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: tests-walkthroughs

# Tier B integration tests: runs the canlab_help_* walkthroughs from
# the CANlab_help_examples repository end-to-end as a nightly job.
# These are slower and depend on more sibling repositories than the
# fast unit suite (test.yml), so they're not part of the per-push CI.

on:
schedule:
# Run nightly at 07:00 UTC (~midnight Pacific / ~02:00 Central).
- cron: '0 7 * * *'
workflow_dispatch:

jobs:
matlab-walkthroughs:
name: MATLAB ${{ matrix.matlab-release }} walkthroughs on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
matlab-release: [R2024b]

steps:
- name: Checkout CanlabCore
uses: actions/checkout@v4
with:
path: CanlabCore

- name: Checkout Neuroimaging_Pattern_Masks
uses: actions/checkout@v4
with:
repository: canlab/Neuroimaging_Pattern_Masks
path: Neuroimaging_Pattern_Masks

- name: Checkout CANlab_help_examples
uses: actions/checkout@v4
with:
repository: canlab/CANlab_help_examples
path: CANlab_help_examples

- name: Clone SPM25
run: git clone --depth=1 --branch 25.01.02 https://github.com/spm/spm.git "${GITHUB_WORKSPACE}/spm"

- name: Set up MATLAB
uses: matlab-actions/setup-matlab@v2
with:
release: ${{ matrix.matlab-release }}
products: |
Statistics_and_Machine_Learning_Toolbox
Signal_Processing_Toolbox
cache: true

- name: Run walkthrough tests
uses: matlab-actions/run-command@v2
with:
command: |
addpath(genpath('CanlabCore'));
addpath(genpath('Neuroimaging_Pattern_Masks'));
addpath(genpath('CANlab_help_examples'));
% NB: addpath, not genpath — SPM compat shims would shadow
% MATLAB builtins; see test.yml for the same rationale.
addpath('spm');
cd CanlabCore/CanlabCore/Unit_tests;
results = canlab_run_all_tests('Walkthroughs', 'only', 'JUnit', fullfile(getenv('GITHUB_WORKSPACE'), 'walkthrough-results.xml'));
if any([results.Failed]); error('CanlabCore:walkthroughs:failed', '%d failed, %d incomplete', sum([results.Failed]), sum([results.Incomplete])); end

- name: Upload walkthrough results
if: always()
uses: actions/upload-artifact@v4
with:
name: walkthrough-results-${{ matrix.matlab-release }}
path: walkthrough-results.xml
if-no-files-found: warn
116 changes: 116 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

CanlabCore is a MATLAB toolbox for MRI/fMRI/PET analysis from the Cognitive and Affective Neuroscience Lab (PI: Tor Wager). The core abstraction is a small set of object types that wrap neuroimaging data and provide a consistent, high-level method surface (`plot`, `predict`, `ica`, `threshold`, `apply_atlas`, `montage`, `surface`, ...). There is **no build system, no test runner, and no linter** — code is loaded onto the MATLAB path and exercised interactively or via user scripts.

## Setup and "running" the toolbox

- From a directory **above** the cloned repos, run `canlab_toolbox_setup` in MATLAB. It searches for sibling CANlab repos (CanlabCore, Neuroimaging_Pattern_Masks, CANlab_help_examples, MediationToolbox, RobustToolbox, etc.), adds each to the path with subfolders, and offers to `git clone` any that are missing.
- Required dependencies: MATLAB + Statistics Toolbox + Signal Processing Toolbox + **SPM12** (https://www.fil.ion.ucl.ac.uk/spm/). SPM is used heavily for image I/O (`spm_vol`, `spm_read_vols`, `spm_orthviews`).
- The companion repo `Neuroimaging_Pattern_Masks` (already added as a working directory here) provides the atlases, signatures, and meta-analysis maps that `load_atlas` and `load_image_set` resolve by keyword. Many methods silently depend on its files being on the path.

## Tests

There is no test harness. What exists:
- `CanlabCore/Unit_tests/` — three standalone scripts (`check_roi_extraction.m`, `jackknife_similarity_unit_test.m`, `resampling_pattern_expression_unit_test1.m`). Run by `cd`'ing in MATLAB and calling the function name.
- `@fmri_data/predict_test_suite.m` and `@fmri_data/validate_object.m` — broader sanity checks invoked on a constructed object (e.g. `validate_object(my_fmri_data)`).
- For ad-hoc verification, the canonical pattern is to load a sample dataset (`load_image_set('emotionreg')` or files under `CanlabCore/Sample_datasets/`) and run the method end-to-end.

## Object architecture (the part you must understand to be productive)

Almost everything is built around a single design idea: **brain images are stored flat as a `[voxels × images]` matrix in the object's `.dat` field, with `volInfo` carrying the inverse mapping back to 3-D space.** This lets generic statistical/ML code operate on `.dat` without knowing about neuroimaging, while `reconstruct_image`, `orthviews`, `montage`, `surface`, etc. recover the spatial view on demand.

### Class hierarchy

`image_vector` is the abstract superclass. The classes you will actually instantiate are its subclasses:

- **`fmri_data`** — the workhorse. Holds 4-D fMRI/PET/contrast data plus `.X` (predictors), `.Y` (outcomes), `.covariates`, `.images_per_session`, etc. Most analysis methods (`predict`, `ica`, `regress`, `searchlight`, `mahal`, `preprocess`) live here.
- **`statistic_image`** — t/p/effect-size maps. Knows about thresholds; `threshold(...)` re-thresholds without losing the underlying values.
- **`atlas`** — labeled parcellations. Has `.probability_maps`, `.labels`, `.label_descriptions`, and methods like `select_atlas_subset`, `merge_atlases`, `downsample_parcellation`, `atlas2region`.
- **`fmri_mask_image`** — binary masks (mostly legacy; many newer methods accept a plain `fmri_data` or `image_vector` as a mask).

Other top-level classes (not subclasses of `image_vector`):
- **`region`** — list of contiguous clusters. Produced by `region(statistic_image)` and consumed by `montage`, `table`, `surface`, `extract_data`. The bridge between voxelwise maps and ROI summaries.
- **`fmridisplay`** — a registered handle bag for a montage/surface figure. Workflow is `o2 = canlab_results_fmridisplay(...)`, then `addblobs(o2, region(t))`, `removeblobs(o2)`, `addblobs(o2, ..., 'nolegend')`. The point is that the figure persists; you swap blob layers in/out without re-rendering anatomy.
- **`brainpathway` / `brainpathway_multisubject`** — connectivity / pathway-modeling objects.
- **`canlab_dataset`** — generic subject × variable behavioral/clinical data container with its own `glm`, `mediation`, `scatterplot`, etc.
- **`fmri_glm_design_matrix`**, **`fmri_timeseries`**, **`predictive_model`** — specialized containers for design matrices, raw timeseries, and ML model artifacts.

### MATLAB `@class` directories

Each class lives in `CanlabCore/@<classname>/`. Files in that directory are methods of that class, dispatched via the first argument. The constructor is `@classname/classname.m`. **Adding a method = dropping a `.m` file into the `@class/` folder** with `function out = methodname(obj, ...)`. There is no methods block to edit; `methods(obj)` discovers them from disk. Because `fmri_data`, `statistic_image`, and `atlas` all subclass `image_vector`, methods defined in `@image_vector/` are inherited by all of them — a method only needs to be redefined in a subclass directory if its behavior differs.

### Provenance and "removed" bookkeeping

Two invariants that recur across nearly every method:

1. **`history`** — a cell array of strings appended to by methods. New methods that mutate the object should push a one-line description.
2. **`removed_voxels` / `removed_images`** — when voxels or images are dropped (e.g. `remove_empty`, `apply_mask`), the object shrinks `.dat` and records which rows/columns were removed. `replace_empty(obj)` re-expands `.dat` to the original shape, padded with zeros, so downstream code that needs full-space indexing can rely on it. Many bugs in this codebase historically came from forgetting to call `replace_empty` or `remove_empty` at the right point — when in doubt, call `replace_empty` before reasoning about voxel positions and `remove_empty` before doing math across `.dat` rows.

## Canonical workflows (use these as templates)

```matlab
% Group analysis end-to-end
imgs = load_image_set('emotionreg'); % fmri_data with sample images
plot(imgs); descriptives(imgs); % QC
t = ttest(imgs); % statistic_image
t = threshold(t, .005, 'unc', 'k', 10); % cluster-extent threshold
r = region(t); % region object, one per blob
table(t); % printed/atlas-labeled results
o2 = canlab_results_fmridisplay(t, 'full'); % registered montage+surface figure
montage(r, 'regioncenters', 'colormap'); % per-blob mini-montage
```

```matlab
% ROI extraction against an atlas
atl = load_atlas('canlab2024'); % keyword-resolved atlas
parcel_means = apply_parcellation(imgs, atl); % images x parcels
```

```matlab
% Cross-validated prediction
[cv, stats, optout] = predict(imgs, 'algorithm_name','cv_lassopcr', 'nfolds',5);
```

## Layout

- `CanlabCore/@*/` — the object classes described above.
- `CanlabCore/Statistics_tools/`, `Visualization_functions/`, `Data_processing_tools/`, `Image_thresholding/`, `Model_building_tools/`, `Reporting/` — function libraries called by the class methods. Edit here when a method's logic is not class-specific.
- `CanlabCore/Data_extraction/` — `load_atlas.m`, `load_image_set.m` (keyword resolvers), `extract_*` helpers.
- `CanlabCore/GLM_Batch_tools/` — `canlab_glm_subject_levels` / `canlab_glm_group_levels`, an SPM12-driven first/second-level batch system. Driven by a `DSGN` struct; see `canlab_glm_dsgninfo.txt` and `canlab_glm_README.txt`.
- `CanlabCore/HRF_Est_Toolbox2/` and `HRF_Est_Toolbox4/` — Lindquist-lab HRF estimation (Logit, sFIR, spline, canonical).
- `CanlabCore/OptimizeDesign11/` — genetic-algorithm fMRI design optimization.
- `CanlabCore/Cifti_plotting/`, `Parcellation_tools/`, `Cluster_contig_region_tools/`, `ROI_drawing_tools/`, `Image_space_tools/`, `Image_computation_tools/` — domain-specific helpers.
- `CanlabCore/External/` — vendored third-party toolboxes (`matlab_bgl`, `spider`, `lasso`, `boundedline`, `export_fig`, `BCT`, `umap`, ...). Treat as read-only; don't refactor.
- `CanlabCore/Sample_datasets/` — small datasets used by examples and walkthroughs.
- `CanlabCore/Unit_tests/` — sparse standalone test scripts (see Tests above).
- `nipype/`, `docs/`, `docs_sphinx_old/` — Python wrappers and old docs; rarely touched.

## Conventions worth knowing

- **First argument is always the object** (`function out = method(obj, varargin)`); methods are typically called as `method(obj, ...)` rather than `obj.method(...)`, though both work.
- **`varargin` keyword pairs** are the universal option style. Existing methods use a hand-rolled `for i=1:length(varargin), switch varargin{i}, case 'foo', foo = varargin{i+1};` loop. **New functions should use `inputParser` instead** — see the next section.
- **Many methods accept either a filename, an `fmri_data`, or another `image_vector` subclass** as their "image-like" argument and dispatch via `isa(...)`. Preserve that polymorphism when editing.
- **Spatial alignment is not implicit.** Methods that combine two image objects generally either error or call `resample_space(a, b)` first; if you write a new combiner, do the same — don't assume two objects share `volInfo`.
- **`.asv` files are MATLAB autosave artifacts** and are gitignored; ignore them. A few committed `*_old.m` files are intentional legacy fallbacks (e.g. `region2imagevec_old.m`, `predictive_model_old.m`) — don't delete them without checking callers.

## When writing new functions

These rules apply to new code. Existing code does not need to be retrofitted.

1. **Name stand-alone functions `canlab_<function_name>`.** This namespaces the function so it does not collide with future external toolboxes the user may add to their path. Class methods (files inside `@class/`) are exempt — they're already namespaced by the class.

2. **Match the documentation format in `CanlabCore/Misc_utilities/documentation_template.m`.** That template defines the section ordering (Usage, Inputs, Outputs, Examples, References, etc.) and comment style readthedocs expects. Open it before writing the help block; copy the structure rather than improvising.

3. **Use `inputParser` for variable input arguments**, following the `INPUT PARSER TEMPLATE` section of `documentation_template.m`. Retain the explanatory comments inside that block — they're a teaching scaffold for future readers, not noise. Implement the `'plot'`, `'verbose'`, `'doplot'`, and `'doverbose'` parameters whenever the function has plotting or chatter that the caller might want to suppress.

4. **Include a runnable example in the help block** that loads or creates a test dataset (e.g. `load_image_set('emotionreg')`, `sim_data`, or a synthetic array) and demonstrates the function with a few of the most common options. The example should be copy-pasteable: someone with CanlabCore on their path should be able to highlight it and run it.

## Documentation pointers

- Function-by-function reference: https://canlabcore.readthedocs.org/en/latest/
- Walkthroughs and batch-script examples (the best way to learn the API): https://github.com/canlab/CANlab_help_examples
- Lab landing page: https://canlab.github.io
61 changes: 50 additions & 11 deletions CanlabCore/@atlas/assign_vals.m
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
function [dat, tbl] = assign_vals(atl, varargin)
% ASSIGN_VALS Assigns numeric values to atlas regions and creates an fmri_data object.
% assign_vals Assign numeric values to atlas regions and create an fmri_data object.
%
% [dat, tbl] = assignvals2atlas(atl, 'reg_names', reg_names, 'vals', vals, 'sort', true)
% Take an atlas object and a vector of values associated with named
% regions, and produce an fmri_data object whose voxel values are the
% assigned region value, plus a table mapping each region name to its
% assigned value. Region names are matched against the atlas region
% shorttitles (as produced by atlas2region).
%
% Inputs:
% atl - An atlas structure (with fields like .labels, .data)
% reg_names - Cell array of region names (must match atlas region shorttitles)
% vals - Numeric values (same length as reg_names)
% sort - (Optional) Logical flag to sort output table by value (default = true)
% :Usage:
% ::
%
% Outputs:
% dat - fmri_data object with assigned region values
% tbl - Table of regions and assigned values
% [dat, tbl] = assign_vals(atl, 'reg_names', reg_names, 'vals', vals, 'sort', true)
%
% Author: Michael Sun, Ph.D. 4/23/2025
% :Inputs:
%
% **atl:**
% An atlas-class object (with fields .labels, .dat, etc.).
%
% :Optional Inputs:
%
% **'reg_names':**
% Cell array (or string array) of region names to assign values to.
% Names must match atlas region shorttitles. Default: atl.labels.
%
% **'vals':**
% Numeric vector of values to assign, the same length as reg_names.
% Default: zeros(numel(atl.labels), 1).
%
% **'sort':**
% Logical flag to sort output table by value in descending order.
% Default: true.
%
% :Outputs:
%
% **dat:**
% fmri_data object with each region's voxel values set to the
% assigned value.
%
% **tbl:**
% MATLAB table of region shorttitles and assigned values.
%
% :Examples:
% ::
%
% [dat, tbl] = assign_vals(atl, 'reg_names', {'Reg1' 'Reg2'}, ...
% 'vals', [1 2], 'sort', true);
%
% :See also:
% - atlas2region
% - region2fmri_data
%
% ..
% Author: Michael Sun, Ph.D. 4/23/2025
% ..

% Parse optional inputs
parser = inputParser;
Expand Down
Loading
Loading