This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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 and no linter; code is loaded onto the MATLAB path and exercised interactively or via user scripts. There is now a matlab.unittest test runner and GitHub Actions CI (see Tests).
- From a directory above the cloned repos, run
canlab_toolbox_setupin 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 togit cloneany 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 thatload_atlasandload_image_setresolve by keyword. Many methods silently depend on its files being on the path.
There is a real matlab.unittest harness under CanlabCore/Unit_tests/. Prerequisite: CanlabCore and Neuroimaging_Pattern_Masks and SPM (SPM12 or SPM25) must be on the path — most tests load atlases/sample images and will error otherwise. The walkthrough tier additionally needs RobustToolbox on the path (it supplies weighted_corrcoef, used by plot_correlation_samefig's robust branch).
Runner. canlab_run_all_tests.m is the entry point (returns a matlab.unittest.TestResult array):
cd CanlabCore/Unit_tests
results = canlab_run_all_tests; % unit tier; walkthroughs SKIPPED by default
results = canlab_run_all_tests('Walkthroughs','only'); % only the slow walkthrough integration tests
results = canlab_run_all_tests('Walkthroughs','include');% both tiers
results = canlab_run_all_tests('Tag','Core'); % select/deselect by tag ('~RequiresMasks' to skip)
results = canlab_run_all_tests('JUnit','test-results.xml'); % write JUnit XML (CI)It globs canlab_test_*.m recursively (excluding old_to_integrate/) and runs each via TestSuite.fromFile. To run/debug one file directly, use runtests('canlab_test_xxx.m').
Naming convention matters. Test files are namespaced canlab_test_*.m to avoid path collisions. matlab.unittest's TestSuite.fromFolder only auto-discovers files whose names start or end with "test", so the prefix-canlab_ form is invisible to it — that's why the runner does its own glob. Name any new test canlab_test_<thing>.m and it is picked up automatically.
Layout. Unit_tests/<class-or-area>/canlab_test_*.m (e.g. fmri_data/, image_vector/, statistic_image/, atlas/, predictive_model/, region/, workflows/, diagnostics/); Unit_tests/helpers/ (shared fixtures like canlab_get_sample_thresholded_t); Unit_tests/walkthroughs/ (slow end-to-end integration tier). Legacy still present: the old standalone scripts, @fmri_data/predict_test_suite.m, @fmri_data/validate_object.m.
Result semantics. CI gates on Failed only (if any([results.Failed]); error(...); end). Incomplete = "Filtered by assumption": the test called assumeFail/assumeTrue and was skipped because an environment precondition (a data file, a toolbox, a display) was missing — not a failure. A couple of Incomplete results is normal and environment-dependent. Verified locally (R2026a + SPM25): unit tier 261 passed / 0 failed / 2 incomplete (~9 min); walkthrough tier 10/10 passed.
Walkthrough tier is fault-tolerant by design. canlab_run_walkthrough_snapshot runs vendored snapshot copies of the CANlab_help_examples scripts (walkthroughs/private/canlab_help_*.m) section by section, so the tier does not depend on the external help repo. canlab_classify_environment_error downgrades environment-limited sections (missing toolbox, no graphics/display, invalid surface handles) to logged skips rather than failures — so a headless or partially-provisioned machine still passes. Don't "fix" a skipped graphics section by forcing it to run.
CI. .github/workflows/test.yml runs the unit tier (R2024b + Statistics + Signal Processing, clones SPM25, checks out Neuroimaging_Pattern_Masks, uploads JUnit XML). tests-walkthroughs.yml runs the walkthrough tier nightly (cron) and additionally checks out CANlab_help_examples and RobustToolbox — the latter supplies weighted_corrcoef, needed by the regression walkthrough; without it the nightly fails with an UndefinedFunction error. codespell.yml is a spell-check action over comments/docs.
For quick ad-hoc verification (not a formal test), the canonical pattern is still to load a sample dataset (load_image_set('emotionreg') or files under CanlabCore/Sample_datasets/) and run the method end-to-end.
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.
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 likeselect_atlas_subset,merge_atlases,downsample_parcellation,atlas2region.fmri_mask_image— binary masks (mostly legacy; many newer methods accept a plainfmri_dataorimage_vectoras a mask).
Other top-level classes (not subclasses of image_vector):
region— list of contiguous clusters. Produced byregion(statistic_image)and consumed bymontage,table,surface,extract_data. The bridge between voxelwise maps and ROI summaries.fmridisplay— a registered handle bag for a montage/surface figure. Workflow iso2 = canlab_results_fmridisplay(...), thenaddblobs(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 ownglm,mediation,scatterplot, etc.fmri_glm_design_matrix,fmri_timeseries,predictive_model— specialized containers for design matrices, raw timeseries, and ML model artifacts.glm_map— scikit-learn-style estimator for mass-univariate GLM / multiple regression. Its property names mirror the fields of the results structurefmri_data.regressbuilds (the variableoutin that method): the design spec (wrapping anfmri_glm_design_matrixin.design, or a direct.X), the fitted result maps (.betas/.t/.contrast_estimates/.contrast_tasstatistic_image;.df/.sigma/.residualsasfmri_data), and three nested option/diagnostic structs.input_parameters,.input_image_metadata,.diagnostics(VIF, contrast VIF, leverage, condition number, collinearity). The historical out-struct field names are available as read/write aliases (.b→.betas,.con_t→.contrast_t,.contrast_images→.contrast_estimates,.resid→.residuals,.variable_names→.regressor_names,.C→.contrasts). Two workflows: (1) callfmri_data.regressdirectly — it now returns a glm_map (not a struct) — and query the object; (2) build an estimator:g = glm_map(...),add_contrasts,run_diagnostics,g = fit(g, fmri_data_obj), thenthreshold/table/montage. (The results live in the.diagnosticsproperty; the method that computes them isrun_diagnosticsto avoid a property/method name clash.)glm_map(out_struct)re-casts a regress-style struct into an object. Supports 1st-level event designs (build_design,import_onsets,import_SPM) and 2nd-level/group designs;fmri_data.regressis the compute engine. Object reference:docs/glm_map_methods.md. Workflows (roadmap + how-to each):docs/workflows/glm_map_first_level_*anddocs/workflows/glm_map_second_level_*.
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.
Two invariants that recur across nearly every method:
history— a cell array of strings appended to by methods. New methods that mutate the object should push a one-line description.removed_voxels/removed_images— when voxels or images are dropped (e.g.remove_empty,apply_mask), the object shrinks.datand records which rows/columns were removed.replace_empty(obj)re-expands.datto 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 callreplace_emptyorremove_emptyat the right point — when in doubt, callreplace_emptybefore reasoning about voxel positions andremove_emptybefore doing math across.datrows.
% 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% ROI extraction against an atlas
atl = load_atlas('canlab2024'); % keyword-resolved atlas
parcel_means = apply_parcellation(imgs, atl); % images x parcels% Cross-validated prediction
[cv, stats, optout] = predict(imgs, 'algorithm_name','cv_lassopcr', 'nfolds',5);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 aDSGNstruct; seecanlab_glm_dsgninfo.txtandcanlab_glm_README.txt.CanlabCore/HRF_Est_Toolbox2/andHRF_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.
- First argument is always the object (
function out = method(obj, varargin)); methods are typically called asmethod(obj, ...)rather thanobj.method(...), though both work. vararginkeyword pairs are the universal option style. Existing methods use a hand-rolledfor i=1:length(varargin), switch varargin{i}, case 'foo', foo = varargin{i+1};loop. New functions should useinputParserinstead — see the next section.- Many methods accept either a filename, an
fmri_data, or anotherimage_vectorsubclass as their "image-like" argument and dispatch viaisa(...). 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 sharevolInfo. .asvfiles are MATLAB autosave artifacts and are gitignored; ignore them. A few committed*_old.mfiles are intentional legacy fallbacks (e.g.region2imagevec_old.m,predictive_model_old.m) — don't delete them without checking callers.
These rules apply to new code. Existing code does not need to be retrofitted.
-
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. -
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. -
Use
inputParserfor variable input arguments, following theINPUT PARSER TEMPLATEsection ofdocumentation_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. -
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.
- 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