Skip to content

Merge with current CANLab Core#8

Merged
Michael-Sun merged 34 commits into
Michael-Sun:masterfrom
canlab:master
Jul 13, 2026
Merged

Merge with current CANLab Core#8
Michael-Sun merged 34 commits into
Michael-Sun:masterfrom
canlab:master

Conversation

@Michael-Sun

Copy link
Copy Markdown
Owner

No description provided.

torwager and others added 30 commits June 12, 2026 15:21
… from repo

Make the BMRK3 6-levels pain dataset self-contained in the repo so
load_image_set('bmrk3') no longer depends on the separately-distributed
bmrk3_6levels_pain_dataset.mat (figshare).

- Gzip the 33 per-subject niftis (bmrk3_6levels_subject0XX.nii -> .nii.gz)
  and update single_trial_image_names in bmrk3_6levels_metadata.mat to .gz.
- Rebuild load_bmrk3 to construct the 198-image object directly from the
  per-subject images + metadata: fmri_data(names, brainmask) -> remove_empty,
  with metadata concatenated subject-major. Verified bit-identical (.dat, .Y,
  .metadata_table, volInfo, removed_voxels) to the old .mat. Drop the temporary
  'bmrk3_lightweight' loader.
- SPM's spm_vol cannot read .nii.gz, so make the mediation image readers
  transparently gunzip on input and clean up temp .nii on exit (onCleanup):
  image_eval_function_multisubj (mediation_brain_multilevel) and
  tor_extract_rois (mediation_brain_results extraction). Verified the
  multilevel mediation walkthrough runs end-to-end on the gzipped data.
- Point Part 2 of the SVM decoding tutorial and sample_datasets.md at the
  repo-built load_image_set('bmrk3').

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nit test

- docs/ROI_extraction_methods_roadmap.md: internal catalog of all ROI/atlas/
  parcel extraction methods (object methods + standalone + legacy), a
  "which method should I use" decision guide, call-graph, and the
  resampling-direction notes. Documents that apply_atlas is referenced in
  class help headers but has no implementation (use apply_parcellation /
  @atlas/extract_data).
- docs/extract_roi_data_howto.md: runnable help doc with sample code using
  load_image_set/load_atlas, extract_roi_averages, apply_parcellation, and
  barplot_columns / lineplot_columns / line_plot_multisubject (bmrk3). All
  code blocks verified to execute under MATLAB R2026a.
- Unit_tests/data_extraction/canlab_test_extract_roi_methods.m: function-based
  test (3/3 pass) verifying cross-method ROI-mean identity (<1e-6 same space),
  synthetic known-value recovery, and resampling tolerance (corr>0.99,
  rel err<0.1). Auto-discovered by canlab_run_all_tests.
- docs/ROI_extraction_streamlining_plan.md: proposal to consolidate extraction
  onto an apply_parcellation + canlab_pattern_similarity core and
  deprecate-then-remove orphaned/legacy functions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…roadmap and figures

- Add docs/Workflows.md hub; link it from Object_methods.md "Cross-cutting topics".
- Move the ROI extraction roadmap and how-to into docs/workflows/.
- Rewrite the roadmap as a public-facing, didactic overview: ROI means, pattern /
  signature responses (mask/network averages and weight-map dot-products via
  apply_mask), gray/white/CSF tissue compartments, components, spheres around
  coordinates, and hand-drawn ROIs. Catalog table gains an "operates on" column
  (object classes / .nii-.img) and drops file paths; new section explains automatic
  resampling onto a common grid (assumes shared MNI space) and manual resample_space.
- Add a graphical roadmap (workflows/ROI_data_extraction_roadmap.pptx/.png) built from
  the codemap template via docs/_codemap_tools/build_roi_roadmap.py, and embed the PNG.
- Embed generated example figures (ROI montage, barplot_columns, lineplot_columns,
  line_plot_multisubject) in the how-to and move the quick-reference table to the top.
- Fix relative links after the move; add __pycache__/ to .gitignore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ision table

Add two rows to "Which method should I use?" in the ROI extraction roadmap
covering how to obtain the region definition an extraction method consumes:
load a labeled atlas by keyword (load_atlas) and select/flatten named regions
(select_atlas_subset), with a pointer to the how-to walkthrough.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Initial plan

* Fix reshape error in render_blobs when cmaprange has equal min/max values

Co-authored-by: torwager <6262700+torwager@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: torwager <6262700+torwager@users.noreply.github.com>
* Initial plan

* Fix write function performance for filenames with underscores

- Replace str2num with str2double in make_img_filename to avoid eval-based
  path searches that are extremely slow on clusters with large MATLAB paths.
  str2double handles non-numeric strings by returning NaN (fast) instead of
  triggering a full MATLAB function path search via eval.
- Fix warning('off') to use warning('off', 'all') with proper save/restore
  state pattern, fixing deprecated usage in MATLAB R2020a+.
- Fix O(N^2) char concatenation loop in slice write path to use cell array
  pre-allocation (O(N)) instead of repeated char() matrix building.

Fixes slow write for filenames like TYPEB_FITHRF.nii where str2num called
with the identifier would trigger expensive MATLAB path lookups.

Co-authored-by: torwager <6262700+torwager@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: torwager <6262700+torwager@users.noreply.github.com>
…mator (#77)

* Add glm_map object class scaffold

New scikit-learn-style estimator class that bundles GLM design
specification, fitted result maps (statistic_image), and design
diagnostics in one container. Composition over fmri_glm_design_matrix
(wrapped in .design) and fmri_data.regress (the compute engine).

- classdef with stored properties (design/level/is_timeseries,
  betas/t/contrast maps, vif/leverage/collinearity diagnostics,
  provenance) and true Dependent accessors (X, TR, onsets, durations,
  regressor_names, num_*, is_fitted) that read through to .design.
- Implemented + MATLAB-verified: diagnostics (VIF/cVIF/leverage/
  condition number/rank/redundant-column report), add_contrasts,
  threshold, montage, table, plot_design, summary, check_properties,
  private select_map.
- Documented stubs with planned field mappings: fit (wraps regress),
  build_design (wraps fmri_glm_design_matrix.build), import_SPM
  (SPM12/SPM25 -> .design).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* glm_map: implement fit() over fmri_data.regress

fit() is now a working orchestration layer: validates the design,
attaches obj.X to the fmri_data, assembles regress() arguments
(robust/AR/threshold/contrasts/names), runs regress, and unpacks the
outputs into the object's statistic_image maps (betas, t,
contrast_estimates, contrast_t) plus sigma, dfe, residuals. Records
fit_parameters and runs diagnostics() for the full VIF/cVIF/leverage/
collinearity set. AR models are gated on is_timeseries.

Also resolve multi-image result maps for display: select_map now parses
an optional image index (bare scalar or 'wh_image'), montage selects it
when given, and table auto-selects a single image (required because
statistic_image.table handles one image at a time).

Verified end-to-end on load_image_set('emotionreg'): fit -> dfe=28,
betas [35676x2], diagnostics populated, threshold and atlas-labeled
table() both working.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* glm_map: implement build_design() for event/1st-level designs

build_design() delegates to fmri_glm_design_matrix.build, which convolves
onsets/durations with the basis set and assembles design.xX.X. After the
call the Dependent obj.X and obj.regressor_names read through to the built
design, so event-mode fit() works the same as direct mode.

Also harden the Dependent X / regressor_names accessors: the wrapped
design seeds xX as a 0x0 struct array (constructor uses 'name',{}), so
guard with isscalar() before indexing .X / .name to avoid an
"isempty: not enough input arguments" error pre-build.

Verified: glm_map(fmri_glm_design_matrix) -> build_design -> X [200x19]
from onsets, regressor names read through, event-mode fit() completes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* glm_map: implement import_SPM() for SPM12/SPM25 first-level models

import_SPM() populates a glm_map from an SPM structure, an SPM.mat path,
or a directory containing one. Because fmri_glm_design_matrix mirrors
SPM's schema, the import is a guarded copy of substructs into the wrapped
design: xY.RT -> TR, nscan, xBF, Sess (onsets/durations/names/pmods/
covariates), and xX (matrix + names). Sets level=1 and is_timeseries=true.
Optional 'load_betas' reads beta_*.nii/.img from SPM.swd into obj.betas,
labeled by xX.name. Whole-substruct copy is resilient to SPM12 vs SPM25
auxiliary-field differences.

Verified on a synthetic SPM struct: TR/onsets/condition_names/regressor
names read through correctly, and fit() against a matching timeseries
yields dfe = nscan - nregressors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Document glm_map in CLAUDE.md and image_vector see-also

Add glm_map to the object-architecture class list in CLAUDE.md and to the
image_vector :See also: block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add unit tests for glm_map (canlab_test_glm_map)

12 functiontests covering construction and Dependent property
read-through, design diagnostics (incl. rank-deficiency detection),
direct-mode (2nd-level) fit over fmri_data.regress with contrasts,
threshold delegation, event-mode build_design via the wrapped
fmri_glm_design_matrix, import_SPM + event-mode fit on a synthetic SPM
struct, and the main input-validation error paths (no design, AR without
timeseries, contrast size mismatch).

Auto-discovered by canlab_run_all_tests. All 12 pass (~5s).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* glm_map: mirror regress out-struct, add diagnostics, narrative summary

Restructure glm_map around fmri_data.regress's results structure and make
regress return a glm_map object instead of a plain struct.

- glm_map properties now mirror the regress `out` fields, with nested
  .input_parameters / .input_image_metadata / .diagnostics structs, .df and
  .sigma kept as fmri_data, and out-struct-name aliases (.b, .con_t,
  .contrast_images, .resid, .variable_names, .C). Constructor re-casts a
  regress-style struct; disp lists all properties.
- fmri_data.regress returns glm_map(out) (struct-compatible via aliases),
  replaces deprecated getvif with VIF.m, and adds contrast VIFs (cVIF) to
  out.diagnostics.Contrast_variance_inflation_factors.
- diagnostics: consolidate on Variance_inflation_factors / Leverages names
  and add per-observation Cook's distance (Cooks_distance); fit auto-computes
  residuals for it and drops them unless requested.
- summary: print a narrative report (analysis, model, diagnostics, and
  per-predictor/contrast significant-voxel counts at threshold).
- Add docs/workflows/regression_with_glm_map.m walkthrough; update unit
  tests and CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs/workflows: expand glm_map regression walkthrough to mirror regress examples

Use a real predictor (Reappraisal_Success) from the emotionreg metadata_table
and demonstrate the full set of fmri_data.regress example options against the
returned glm_map object: regressor/analysis naming, displaying result maps
(montage/surface/orthviews/get_wh_image), nested .diagnostics with a VIF/
leverage plot, a 2nd-level CSF nuisance covariate, gray-matter masking + FDR,
outlier detection and exclusion, robust regression, residual output,
brain-predicts-behavior and map-writing (commented), the estimator path with
fit/summary, re-thresholding, and a first-level event design. Fix find/replace
artifacts (.obj -> .dat) from the prior revision. Verified all computational
sections run end-to-end on the sample dataset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* glm_map: add validate_object to guarantee full nested-struct schema

Add @glm_map/validate_object, which fills any missing fields in the nested
option/diagnostic structs (.input_parameters, .input_image_metadata,
.diagnostics incl. .collinearity_report, and .fit_parameters) from canonical
field templates without overwriting existing values. The templates are the
single source of truth for the valid field names produced by
fmri_data.regress and glm_map.diagnostics.

- glm_map constructor now calls validate_object before returning, so even a
  freshly constructed (or struct-recast) object always exposes the complete
  schema for every nested struct.
- fmri_data.regress calls validate_object on the glm_map output before
  returning it.

Also picks up a docs/workflows tweak (run diagnostics(g) before reading
.diagnostics fields). glm_map unit tests pass (14/14).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fmri_glm_design_matrix: hi-res microtime pipeline + method audit; glm_map.replace_basis_set

Rebuild the design-matrix convolution on a high-resolution (16 samples/sec,
TR-independent) pipeline and audit/fix the class methods.

get_session_X: build a 16 Hz onset/epoch indicator per condition, convolve
with the per-condition basis (resampled to 16 Hz via xBF.dt), and downsample
to TR with getPredictors('dsrate', res*TR) -- the same engine onsets2fmridesign
uses. Matches onsets2fmridesign to corr >= 0.999 across TR = 0.8/1/1.3/2/2.5
(incl. fractional TRs), handles event durations (epochs), parametric
modulators, multiple sessions, and per-condition basis sets. Default basis is
now the canonical SPM HRF sampled at 16 Hz.

Audit fixes:
- build_single_trial: same hi-res timing fix; index xX(1) (dot-assign to an
  empty struct failed); correct off-by-one in the intercept (iB) indices.
- build/build_single_trial check_model: make the " for Condition N" basis-name
  suffix idempotent so repeated build() calls don't keep appending.
- add: condition_names is now correctly shared across sessions (or accepts
  session-major names), per the documented behavior.
- replace_basis_set: harmonize struct fields between basis builders
  (spm_get_bf vs fmri_spline_basis) so assignment no longer errors, and expand
  xBF to one-per-condition so replacing one condition keeps the others.
- import_onsets: implement (was a non-functional stub) reading onset/duration/
  name columns from a CSV/Excel file or table.
- plot: drop a dead `bf = obj.xBF.bf` line that errored for multi-condition xBF.

glm_map: add replace_basis_set, which delegates to the design's
replace_basis_set, rebuilds X, clears fit results/contrasts/diagnostics that no
longer apply (restoring the empty schema via validate_object), and either
re-fits (if data supplied) or refreshes design diagnostics.

Tests: add event-design timing checks (canonical HRF -> 3 cols; match
onsets2fmridesign across TRs) and a replace_basis_set test. 16/16 glm_map
tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* glm_map: interest/nuisance roles, interest-only diagnostics report, plotDesign hook; import_onsets FSL/SPM inputs

Regressor roles: add wh_interest / wh_nuisance / wh_intercept Dependent
indicators to glm_map. Event designs read the partition from design.xX
(iH = of interest, iC/iG = nuisance, iB = intercept); direct/group designs
detect the intercept (constant columns) and use a new nuisance_columns
property to mark covariates of no interest.

diagnostics(): now computes VIFs/cVIFs and the condition number for the full
design AND for the regressors of interest only (nuisance covariates removed),
storing both (full values stay in the main fields). By default it prints a
narrative report that explains each metric's range/interpretation, warns on
problematic cases (VIF/cVIF > threshold, condition number > 30/100, high
Cook's distance, rank deficiency), and flags when nuisance covariates
substantially inflate the of-interest VIFs (indicating correlation between
events of interest and nuisance variables).

plot_design(): hooks into plotDesign -- when an event design has <= 12 event
types of interest, draws color-coded regressor line plots with event/duration
boxes for the events of interest, alongside a heat map of the full design
matrix (incl. nuisance covariates). All design heat maps use
'YDir','reverse' (observation 0 at top) and axis tight.

import_onsets(): handles FSL/tabular files (CSV/Excel/table) with onset,
duration, and a condition column that may be a string name OR an integer
event-type code, and SPM-style cell arrays of onsets, durations, and
parametric modulators (one cell per condition / event type).

validate_object: diagnostics template extended with the new fields.
Tests: add role-indicator, interest-only-diagnostics, and import_onsets
variant tests. 19/19 glm_map tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* glm_map.plot_design: render actual basis-convolved regressors per event type

Replace the plotDesign call (which re-convolved onsets with its own canonical
HRF and ignored the model's basis set) with direct rendering of the object's
built design matrix. Each event type of interest gets its own panel showing
its actual regressor(s) -- one line per basis function, so multi-basis designs
(e.g. spline or HRF+derivatives) display correctly -- with filled boxes marking
each event and its duration (box-drawing borrowed from drawbox). The full
design-matrix heat map (incl. nuisance covariates, YDir reverse + axis tight)
is shown alongside. Per-condition columns are located by basis-function counts
at the front of the design (verified A->cols 1:4, B->col 5 for a 4-spline +
canonical mix). 19/19 glm_map tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* glm_map/design: orthogonal contrast sets, design efficiency, import interest/nuisance flagging

create_orthogonal_contrast_set: new method on @glm_map and
@fmri_glm_design_matrix. Uses create_orthogonal_contrast_set.m to assign a set
of mutually orthogonal, zero-sum contrasts spanning the regressors of interest
(glm_map.wh_interest / design xX.iH), with placeholder names; nuisance and
intercept columns get zero weight. If no regressors of interest are defined,
it explains how to set them and errors gracefully.

diagnostics(): now reports design efficiency for contrasts via
calcEfficiency.m -- using entered contrasts, or an auto orthogonal set
spanning the regressors of interest when none are entered. When no regressors
of interest are defined and no contrasts exist, efficiency is skipped with an
informative note (other diagnostics still run). Efficiency fields added to the
diagnostics struct / validate_object schema.

Import interest/nuisance flagging: events entered via import_onsets become the
of-interest regressors (the design's H partition); covariates of no interest
become nuisance. import_SPM now derives the interest/nuisance/intercept
partition from SPM event semantics (task regressors named '...*bf(...)',
session 'constant', everything else nuisance) -- SPM's own xX.iH is typically
empty for event-related first-level models -- preserving SPM's original
indices under xX.spm_*.

glm_map.import_onsets: new pass-through method that imports into the wrapped
fmri_glm_design_matrix (FSL/tabular or SPM-style cells), bootstrapping the
design from 'TR'/'nscan' if needed, then builds when enough info is available
or prints what is missing and how to attach it.

Tests: add orthogonal-contrast-set, efficiency, and import/SPM-flagging tests.
22/22 glm_map tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* glm_map: scaled condition number, unit-norm heat map, obs/regressor summary, time axis

Fix two reported issues and improve summaries:

- diagnostics: the condition number is now computed after scaling each column
  to unit L2 norm (the Belsley scaled condition index). It is scale-invariant
  like VIF, so a well-conditioned design no longer reports a huge condition
  number that was purely an artifact of the tiny event regressors vs the unit
  intercept (e.g. 329 -> 1.74, consistent with VIFs ~1).

- plot_design heat map: scale each regressor column to unit norm for display,
  so small-amplitude event regressors are visible instead of being washed out
  to black by the large-norm intercept. Event-panel x-axis labelled
  'Time (seconds)' (time base is always seconds).

- disp() and summary() now report the number of observations and regressors
  with the of-interest / nuisance / intercept breakdown, and list each
  regressor's role. (g.X already exposes the design matrix, reading through to
  design.xX.X; verified by test.)

Tests: add a scaled-condition-number regression test (raw cond large, scaled
small, VIFs low, g.X == design.xX.X). 23/23 glm_map tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* glm_map: rename diagnostics method -> run_diagnostics (resolve property/method name clash)

glm_map had both a .diagnostics property (the results struct, mirroring
fmri_data.regress out.diagnostics) and a diagnostics() method that computes it.
MATLAB tolerates this (property via dot-access, method via function syntax,
as statistic_image does with threshold), but g.diagnostics(args) silently
resolves to property indexing and errors, which is a footgun.

Rename the verb to run_diagnostics and keep the noun .diagnostics property
(the load-bearing name: it mirrors regress's out.diagnostics and is read in 60+
places). Update the internal callers (fit, replace_basis_set), help examples,
the workflow doc, CLAUDE.md, and tests. Call as: g = run_diagnostics(g, ...);
results are stored in g.diagnostics. 23/23 glm_map tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* glm_map: VIF/cVIF plots in plot_design, high-pass-filter assessment in run_diagnostics, workflow sec 13

plot_design: when diagnostics have been computed, draw a VIF figure in the
scn_spm_design_check style -- orange markers with severity reference lines at
VIF = 1/2/4/8 -- showing VIFs of the regressors of interest in the full design
and (when nuisance indicators exist) without nuisance covariates, plus contrast
VIFs (cVIF) when present. Falls back to full-design VIFs only.

run_diagnostics: for timeseries designs (is_timeseries) only, compute the
cumulative power by frequency for each regressor of interest and each contrast
(borrowing cumulative_power / fft_calc from scn_spm_choose_hpfilter), and
recommend a high-pass filter cutoff that keeps the maximum low-frequency
variance loss below 5% -- separately for regressors and for contrasts. Stored
in .diagnostics.hpfilter and printed in the report (with the more conservative
of the two as the suggested cutoff). The scaled condition number is unchanged.

docs/workflows section 13: add a simple [1 -1] A-vs-B contrast and show
plot_design before vs after run_diagnostics (the VIF figure appears after),
and display the recommended HP filter. All method calls use run_diagnostics().

Tests: add an HP-filter / cumulative-power test (timeseries vs group). 24/24
glm_map tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* onsets2fmridesign: fix R2024b crash from stale pad() call (builtin shadows canlab_pad)

CanlabCore's pad.m was renamed to canlab_pad.m, but onsets2fmridesign still
called pad() in the TR-resolution delta fallback. In R2024b+, 'pad' resolves to
MATLAB's builtin string pad, which errors ("First argument must be text") on the
numeric onset vector. On CI (no Image Processing Toolbox), padarray is
unavailable so the fallback was taken and crashed -- failing
canlab_test_glm_map/test_event_design_matches_onsets2fmridesign, which calls
onsets2fmridesign as a reference (the whole function runs even though the test
uses only the X output).

Replace the padarray/pad try-catch with an explicit one-sided zero-pad to the
run length (matching canlab_pad), removing the toolbox/version-fragile
dependency. X is unaffected (computed before this block). 24/24 glm_map tests
pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix remaining stale pad() callers -> canlab_pad (builtin pad shadows renamed pad.m on R2024b+)

CanlabCore's pad.m was renamed to canlab_pad.m; three callers still used pad(),
which now resolves to MATLAB's builtin string pad and errors on numeric input
in R2024b+ (same root cause as the onsets2fmridesign fix).

- smooth_timeseries.m: pad -> canlab_pad (count semantics unchanged; correct).
- nonlin_parammod_predfun.m (hrf): pad -> canlab_pad; the 2nd arg is a vector,
  so canlab_pad pads hrf to its length (unchanged behavior).
- nonlin_parammod_predfun.m (yhi): the call passed a *target* length to pad,
  but canlab_pad takes a *count* of zeros; under the surrounding
  `if length(yhi) < runDuration` the intent is to pad yhi to runDuration, so
  use canlab_pad(yhi, runDuration - length(yhi)).

Smoke-tested: smooth_timeseries (50->50) and nonlin_parammod_predfun
(yhi length == runDuration) both run without the pad crash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…flows

Add full documentation for the glm_map object and two end-to-end workflows in
the same style as the existing object pages and the ROI-extraction workflow.

- docs/glm_map_methods.md: object reference (intro, first-level time-series vs
  second-level static-design modes, ways to create, design notes, methods by
  area, properties, workflow links). Linked from Object_methods.md (hierarchy
  + class table).

- docs/workflows/glm_map_first_level_* : roadmap + how-to for single-subject
  event-related time-series modeling — importing onsets (FSL tables, SPM-style
  cells, SPM.mat), basis sets, simulating data, contrasts, diagnostics
  (VIF/cVIF with/without nuisance, efficiency, high-pass filter), AR fitting,
  thresholding/visualization. Synthetic 3-run, 4-event design with nuisance
  covariates; embedded design/VIF/montage figures + a graphical roadmap
  (.pptx/.png via codemap_lib).

- docs/workflows/glm_map_second_level_* : roadmap + how-to for group analysis
  on emotionreg contrast images — object creation via fmri_data.regress or the
  glm_map API, outlier handling, WM/CSF covariates and normalize_gm_by_wm_csf,
  OLS vs robust, diagnostics, thresholding/visualization. Embedded figures +
  graphical roadmap.

- Linked both workflows in Workflows.md; added the two codemap builders under
  docs/_codemap_tools; removed the superseded
  docs/workflows/regression_with_glm_map.m and updated the CLAUDE.md pointer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…signs)

fmri_data.regress auto-adds an intercept when none is detected, which (a) broke
contrasts on designs whose columns already include intercepts (size mismatch),
and (b) added a spurious all-ones column to multi-run first-level designs that
already carry per-run baseline columns.

- glm_map.fit now prepares the design before fitting: it enforces a single
  overall intercept as the LAST column via intercept.m semantics ('which'/'end')
  -- an existing intercept is moved (never duplicated), one is added if absent,
  and multi-run designs with per-run baseline columns are left as-is -- then
  passes 'nointercept' to regress. Regressor names stay aligned, and the
  prepared design is recorded on direct-mode objects so g.X / g.regressor_names
  match the fit. The model is fit first (all betas), then contrasts applied.
- Contrast vectors that are one or more elements short are padded with 0 weights
  for the intercept, in both add_contrasts (entry time) and fit (final width),
  so contrasts can omit the intercept entry. add_contrasts errors only when a
  contrast is WIDER than the design.

Supporting fixes (exposed by multi-run designs):
- VIF.m: when it adds a temporary intercept (no all-ones column present) and a
  contrast is supplied, pad the contrast with a 0 weight so cVIF's width matches
  the augmented design (was a dimension-mismatch error).
- fmri_glm_design_matrix.build: give multi-run designs complete, UNIQUE column
  names -- run-prefixed interest/covariate names ('Sn2 Cue-BF1') plus per-run
  baseline names ('Sn2 constant') -- so repeated condition names no longer
  collide (regress's contrast table requires unique names) and every column is
  named.

Tests: add a multi-run contrast-fit regression test and an add_contrasts
size-handling test (short pads, too-long errors). Docs note the intercept/
contrast handling and that an intercept name should be supplied. 25/25 glm_map
tests pass; regress tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ition)

Add per-run nuisance covariates to test_fit_multisession_contrast and assert
the interest/nuisance/intercept partition (4/6/2), unique complete names, and a
contrast fit with no spurious intercept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First-level roadmap/how-to + glm_map fit:
- Recommend AR(4) over AR(1) for time-series error models (better captures
  BOLD serial autocorrelation; ~ARMA(1,1)).
- fit() now checks AR dependencies up front (fgls/Econometrics,
  aryule/Signal Processing) and errors with a clear, actionable message if
  either toolbox is missing, instead of failing mid per-voxel fit.
- Document the AR toolbox dependencies in fit.m, the roadmap, the how-to,
  and the object page.
- Note that g.is_timeseries = true also switches run_diagnostics into
  time-series mode (HP-filter cumulative-power assessment).
- Reference fmri_data.sim_data for synthetic signal+noise data (with its
  real single-predictor interface), alongside the X*betas+noise construction
  used for a custom convolved design.
- Regenerate the first-level roadmap PNG/PPTX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fmri_data.sim_data can now generate data from an explicit design matrix
instead of (or in addition to) its built-in region mean-shift / correlation
model:
  - 'design' X  [n images x k regressors]; sets the number of images.
  - 'betas'     [k x v] coefficient maps -> data = (X*betas)' + noise, or
                [k x 1] one response placed in the true-signal region.
Errors clearly if betas is the wrong size or missing. The default path is
unchanged (refactored the region-mask resolution into a local helper shared
by both paths). Documented with examples in the help block.

Use it in the first-level workflow docs as the one-line synthetic-data step:
  sim = sim_data(template, 'design', g.X, 'betas', betas_true);

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a single-line region-name readout to canlab_orthviews that names the
atlas region under the crosshair, updating live on click/drag/reposition.
On first open the viewer tries to attach the canlab2024 atlas; if it can't
be loaded, the strip shows a one-line hint on how to attach one with
'AddAtlasLabel'. Lookup uses the atlas's own affine (dense int16 label
volume cached on the figure), so it is O(1) per redraw and independent of
underlay space.

New commands: 'AddAtlasLabel' / 'RemoveAtlasLabel'. Displaying an atlas
object also makes it the readout source.

Display tweaks:
- Coordinate titles 10->14pt; region label 11->14pt bold.
- Move the region label into a clear band above the colorbar so the two
  no longer collide.
- Render coordinate titles as explicit text children instead of the axes
  Title (the panels use `axis off`, which suppressed the Title in the
  export/print path).

Tests: 6 new cases (strip present, no-atlas hint, remove hides strip,
real-atlas lookup correctness, atlas-display attaches); panel-count
assertion now ignores the tagged helper axes. 40/40 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g9Q2z55GY7EfMXoCkg9Px
Add an atlas region layer to the canlab_niivue web viewer and a lightweight
statistic_image.orthviews_niivue convenience method.

Viewer (assets/canlab_niivue_viewer.js, canlab_niivue.m):
- Attach an atlas (on by default; 'noatlas' to disable) as a hidden int16
  NIFTI_INTENT_LABEL layer. The crosshair readout names the region under the
  cursor, and only that single region is highlighted via an "Atlas region"
  dropdown (Outline default / Shaded / Off), placed in the controls bar.
- Atlas written with pinfo=[1;0;0] so raw voxel value == label index (SPM's
  default int rescaling broke NiiVue's label-LUT indexing).
- Single-region highlight = mask colormapLabel LUT alpha to the current code;
  fixed high-contrast color; bolder outline via a small, clearly-marked,
  graceful-fallback patch to the vendored niivue.js (atlasOutlineWidth).
- Folder bundles are auto-served over a local HTTP server on open (ES-module
  imports are blocked on file://).

New method:
- @statistic_image/orthviews_niivue.m — thin wrapper over canlab_niivue that
  writes a standalone page to a temp folder and opens it in the browser (the
  web counterpart to orthviews).

Sample/emotionreg_ttest.html regenerated to include the atlas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the interactive viewers throughout the documentation:

- Object_methods.md: add canlab_orthviews + canlab_niivue to the group t-test
  one-liner block; expand the "Visualizing images and results" bullets and the
  helpers table (atlas region readouts; orthviews_niivue shortcut).
- Per-class "Display and visualization" sections (image_vector, fmri_data,
  statistic_image, region, atlas): add an "Interactive viewers" callout;
  statistic_image also lists the new orthviews_niivue method.
- Workflows.md: fuller visualization list (montage / surface /
  canlab_results_fmridisplay / table, plus the interactive viewers).
- glm_map workflow how-tos + roadmaps: viewer callouts in the visualize steps.
  Second-level how-to embeds a live NiiVue viewer of its own result and shows
  surface(t, 'foursurfaces_hcp') with a static figure.
- canlab_niivue_guide.md: atlas region readout/highlight, auto-served folder
  mode, 'atlas'/'noatlas' options, orthviews_niivue shortcut; demo regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* bug fixes to canlab scripts, and completely revamped searchlight_disti

* finished searchlight_disti_Lukas function

---------

Co-authored-by: Vivek Sagar <VivekSagar2016@u.northwestern.edu>
* Integrate two old_to_integrate tests; harden test discovery

Convert two quarantined standalone scripts into real matlab.unittest tests
and move them into the discovered suite, and make the runner robust against
non-test files that match the canlab_test_*.m glob.

- image_vector/canlab_test_check_roi_extraction.m: converts
  old_to_integrate/check_roi_extraction.m. The original printed PASS/FAIL but
  never asserted. Now asserts multi-region 'unique_mask_values' extraction and
  invariance of the region averages across a write->reload round-trip
  (RelTol 1e-3 / AbsTol 1e-2 absorbs single-precision NIfTI rounding).
  Complements canlab_test_extract_roi (single-mask only).
- image_vector/canlab_test_resampling_pattern_expression.m: converts
  old_to_integrate/resampling_pattern_expression_unit_test1.m, which only
  plotted. Now asserts SIIPS pattern expression is stable (corr > 0.99) across
  default/nearest/spline resampling; skips if SIIPS is unavailable.
- helpers/canlab_safe_suite_from_file.m + canlab_run_all_tests.m: wrap
  TestSuite.fromFile so a stray non-test canlab_test_*.m is warn-skipped
  instead of throwing NonTestFile and aborting discovery of the whole suite.
- canlab_test_runner_robustness.m: pins that behavior (non-test file ->
  warn + empty + ok=false; valid file -> loads).

Old originals removed from old_to_integrate (jackknife_similarity_unit_test.m
stays - it is a characterization/figure script, not a unit test).

Full default suite after: 147 passed, 0 failed, 2 incomplete (pre-existing
environment skips).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Convert 5 xval_* tests to matlab.unittest; move to predictive_model/

The five xval_*_unit_test.m files at the Unit_tests root were genuine tests
(real assertions, no graphics, deterministic, bundled DPSP / synthetic data)
but were plain-assert functions named outside the canlab_test_* convention,
so the runner never discovered them.

Convert each into a functiontests file under a new predictive_model/ subdir:
the model is fit once in setupOnce (skipped via assumeTrue if the DPSP sample
data is absent) and cached, and the assertions are split into focused test
functions using tc.verify* for granular reporting. Behavior preserved.

- predictive_model/canlab_test_xval_SVM.m
- predictive_model/canlab_test_xval_SVR.m
- predictive_model/canlab_test_xval_regression_multisubject.m
- predictive_model/canlab_test_xval_regression_multisubject_featureselect.m
- predictive_model/canlab_test_xval_discriminant_classifier.m
- helpers/canlab_get_dpsp_hot_warm.m: shared DPSP Hot/Warm loader.

Old xval_*_unit_test.m removed. New suite: 25 test points, all pass, ~7.5s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Convert predictive_model_unit_test to matlab.unittest (minimal nboot/nperm)

predictive_model_unit_test.m exercised the @predictive_model object's own
public API (fit/predict/score/crossval/bootstrap/weight_map_object/
permutation_test/calibrate/select_features/grid_search/stability_selection/
pipeline/newapi routing/regression+report/summary). This is non-redundant
with the xval_* tests, which cover the legacy wrapper functions rather than
the object methods - so it is worth keeping.

Convert to predictive_model/canlab_test_predictive_model_api.m: the shared
fit -> crossval -> bootstrap chain is computed once in setupOnce and cached;
the rest are focused test functions using tc.verify*; visualisation methods
(plot/confusionchart/montage) are smoke-tested and skipped on a headless
runner via canlab_classify_environment_error.

Drop the bootstrap/permutation/stability counts to minimum-viable values
(nboot 25->5, nperm 10->3, stability nboot 20->5) since this is a smoke test,
not real inference; assertions are structural (shapes/ranges/p-floor), so the
counts only need to exercise the code paths. Runtime 131s -> 73s locally; the
residual cost is the high-dimensional (194,676-feature) cross-validation, not
the resampling counts. Skipped if DPSP_hotwarm is not on the path.

Old predictive_model_unit_test.m removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* predictive_model API test: mask to sparse gray-matter ROI for speed

The dominant cost was cross-validating on the full ~195k-voxel volume. In
setupOnce, restrict hw_obj to gray matter (gray_matter_mask.img) and then
deterministically thin to every 8th voxel (~20k features). hw_obj and X stay
in lockstep, so weight_map_object / montage back-project consistently, and
all assertions are structural so masking does not change them. Falls back to
the full volume if the mask is not on the path.

Runtime: 131s (original) -> 73s (minimal nboot/nperm) -> ~20s now. All 17
test points still pass; model behaviour unchanged (AUC ~0.83).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…our pipeline (#82)

* fmridisplay: stateful handle class with live layers + controller widget

Converts fmridisplay from a value class storing baked graphics into a
handle class whose blob layers retain their source data and render
options, so they can be re-rendered in place. Implements Phases 1-2, 5,
and 6 of VISUALIZATION_OVERHAUL_NOTES.md (the deep render-pipeline
unification and engine consolidation remain deferred, per doc section 11).

- classdef fmridisplay < handle. A full call-site audit found no
  value->handle aliasing hazard; every site uses the safe
  `o2 = method(o2, ...)` self-reassign idiom, so back-compat is preserved.
- addblobs: each activation_maps{} layer now retains source_region,
  source_object (e.g. a statistic_image, enabling re-threshold up and
  down), render_args, wh_montage/wh_surface, applied_threshold.
- New @fmridisplay methods: refresh, rethreshold, set_colormap,
  set_opacity (live in-place re-render on montage views), and
  controller(obj) (uifigure control panel bound to the instance).
- Fix: a bare 'colormap' flag (montage(r, o2, 'colormap')) was a
  pre-existing crash in render_blobs (which requires an n x 3 matrix);
  addblobs now strips it before forwarding. Un-breaks
  canlab_help_5_regression_walkthrough.
- New tests: Unit_tests/image_vector/canlab_test_fmridisplay_handle.m.

Verified: full unit tier 157 pass / 0 fail / 2 skip; walkthroughs 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: graceful, informative errors for addblobs misuse

Replaces cryptic MATLAB errors when addblobs is called incorrectly:

- addblobs(o2) with no region -> instead of "Not enough input arguments"
  (MATLAB:minrhs), explain that a region/clusters second argument is
  required and show the montage(t) + addblobs(o2, region(t)) usage.
- addblobs(o2, badtype) -> name the class received and how to convert,
  with a stable error id (fmridisplay:addblobs:badRegion).
- addblobs(img) on image data (forgot to build a display) -> a new
  @image_vector/addblobs stub replaces "Undefined function 'addblobs'
  for input arguments of type 'statistic_image'" with a redirect to
  canlab_results_fmridisplay/montage then addblobs(o2, region(img)).

(Note: surface(o2) on an fmridisplay is valid — it adds a surface view —
so it is intentionally left working.)

Verified: viz unit + help_examples 42/0, walkthroughs 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: unify surfaces as managed views; silence per-slice readout

Surfaces are now first-class managed views (design notes 4.2 / 7.4), so
blobs act across montages and surfaces together:

- Factor surface drawing out of addblobs into a shared render_layer_surfaces
  method that derives colors from each layer's stored render_args. addblobs,
  surface (pull-in), and refresh all route through it.
- surface(o2) registers on the SAME handle and pulls in any blob layers that
  already exist, so a surface added AFTER addblobs still shows the blobs.
- refresh (hence rethreshold / set_colormap / set_opacity) re-renders surfaces
  too, not just montages.
- Multi-surface keywords 'foursurfaces' / 'foursurfaces_hcp' /
  'foursurfaces_freesurfer' add the four canonical views (L/R lateral + medial)
  as separate registered views on the same object, instead of being ignored.
- Silence the per-slice "N voxels displayed, M not displayed on these slices"
  readout in render_blobs (commented out, with a re-enable note).
- Tests: +4 surface-integration cases in canlab_test_fmridisplay_handle
  (13/13 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: fix surface re-paint/erase and route opacity to surfaces

Fixes two issues with surfaces as managed views:

- Re-painting an already-colored surface (addblobs twice, addblobs after
  registering a new surface, or refresh) used to poison the saved restore
  data: render_on_surface stashes the *current* FaceVertexCData as the
  "gray" to restore, so a second paint saved the blob colors. removeblobs
  then could not clear the surface. render_layer_surfaces now restores the
  saved gray before re-painting a previously-painted surface, so removeblobs
  reliably returns every registered surface to anatomy.
- set_opacity (and the controller opacity slider) now reaches surfaces:
  render_layer_surfaces applies a constant 'transvalue' as the surface
  patch FaceAlpha. set_colormap already propagated via render_args.

Both are consequences of surfaces being unified views: add/remove/recolor/
opacity now act on montages and every registered surface together.

Tests: +2 regression cases (repeat-paint clears, opacity dims surface);
canlab_test_fmridisplay_handle 15/15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: robust to closed windows, layer-aware rethreshold, live controller

Addresses several interactive-use issues:

- Closed figure windows no longer crash addblobs/removeblobs/refresh. New
  prune_dead_views drops montage/surface views whose figures were closed
  (with a short note); surface erase/render also filter to live handles.
- rethreshold is now layer-aware and the montage path keeps the real source:
  image_vector.montage passes the original object via a new addblobs
  'source_object' override, so a montage(t) layer retains its statistic_image
  and re-thresholds by p-value. rethreshold dispatches by source class
  (statistic_image -> p-value, fmri_data/mask -> raw value, region ->
  magnitude cutoff) and honors 'layers'.
- Controller is now live: rebuilt in place on repeat calls, auto-refreshed by
  addblobs/removeblobs (update_controller via a Transient controller_handle
  property), each control initialized from the layer's current state, and the
  threshold field is type-aware (p-value vs raw). HandleVisibility='on' so
  `close all` closes it.

Tests: +4 cases (closed-figure prune, montage source class, raw rethreshold,
controller auto-update + state); canlab_test_fmridisplay_handle 19/19.
checkcode clean; canlab_test_display 4/4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: plan deferred bypass-method -> managed object conversion (surface/orthviews)

Record in VISUALIZATION_OVERHAUL_NOTES.md section 11 that image_vector.surface
and image_vector.orthviews still return raw handles (unlike montage), and the
plan to make them return a managed fmridisplay: opt-in flag first (~24 call
sites use the handle return), mirroring image_vector.montage with
'source_object' retention. Captures T. Wager's keyword-parity idea: pass the
direction keyword + remaining varargin through to addbrain (as the hcp surfaces
already do) to expose addbrain's full surface set with minimal new code, with a
caveat that the parametric surface_cutaway types (coronal_slabs, ycut_mm) live
in region.surface and would need a second step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: refine deferred surface-keyword plan (addbrain already covers cutaways)

Correct an earlier caveat: addbrain.m:844 already dispatches the cutaway/slab
keywords (coronal_slabs_4, *_cutaway, slabs) to
canlab_canonical_brain_surface_cutaways, so the planned addbrain pass-through
covers cutaways in one mechanism (no separate step). Record that supporting all
addbrain keywords in @fmridisplay/surface needs bare-keyword parsing + control-
opt stripping + post-render view/lighting handling (moderate, not piecemeal),
and that making the default surface 'left_cutaway' (in @fmridisplay/surface and
image_vector.surface) is a back-compat-affecting decision for the §7.4 phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: surface legend fix + remove_legend + controller upgrades

Legend / colorbar:
- render_on_surface now parents the colorbar legend to the SURFACE's figure
  instead of gcf, so it no longer lands on the montage window.
- New remove_legend(obj) method deletes surface colorbar legends (keeps blobs).

Controller:
- Command-line rethreshold / set_colormap / set_opacity now sync an open
  controller: controls update IN PLACE when the layer count is unchanged
  (rebuild only on addblobs/removeblobs), so the threshold/colormap/opacity
  fields reflect command-line changes without destroying controls mid-callback.
- Each GUI action echoes the equivalent code line to the command window
  (e.g. `han = set_colormap(han, 'color', [1 0 0]);`), using the bound variable
  name (inputname, preserved across rebuilds).
- More colormaps (split / warm / cool / winter) plus a 'solid colour…' entry
  that opens the built-in uisetcolor palette.
- Cosmetic: light-green figure, bound-variable name in the title bar, opacity
  slider tick marks removed.

Deferred (documented in section 11): per-layer surface visibility (Visible
toggle hides montage blobs only) — needs a visible flag respected across all
surface-draw paths; do it with the multi-layer compositing work. True
perceptual colormaps (inferno/viridis) also deferred.

Tests: +3 (legend on surface figure, remove_legend, rethreshold->controller
sync); canlab_test_fmridisplay_handle 22/22; checkcode clean; display 4/4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: hide internal helper methods from the public surface

Declare the internal helpers activate_figures, prune_dead_views, refresh,
render_layer_surfaces, and update_controller as Hidden (signature-only block in
fmridisplay.m; bodies stay in their @fmridisplay/*.m files), so they no longer
appear in methods(obj) / tab-completion. Hidden rather than Access=private so
existing external callers keep working: activate_figures is used by
help-example scripts (c_univariate_contrast_maps*.m) and refresh by the unit
tests. All user-facing methods remain public.

Test: +1 (public vs internal method surface, and internal methods still
callable); canlab_test_fmridisplay_handle 23/23.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: surface colormaps match montage; foursurfaces gets its own figure

Colormap fix (render_layer_surfaces):
- render_on_surface does not understand maxcolor/mincolor, so warm/cool/winter
  (max/min colour ramps) and solid colours were silently dropped on surfaces and
  fell back to the default split hot/cool (blue negatives under "warm";
  cool/winter not updating). Now the layer's colour spec is translated into
  pos_colormap/neg_colormap ramps (surface_colormaps_from_args) and the tokens
  render_on_surface can't read are stripped. Solid colours map the same colour on
  both signs (no stray default-cool negatives). Verified: warm surface map B=0.0,
  cool/winter produce distinct surface colormaps.
  (Residual: the split hot/cool map still differs slightly montage-vs-surface due
  to the two engines' colormap construction; full match is the deferred
  render-pipeline unification.)

Figure fix (surface / add_multiple_surfaces):
- surface(o2, 'foursurfaces*') opened the 2x2 layout in the current figure, so
  without a preceding `figure;` it drew over the montage and showed nothing. It
  now reuses an empty current figure or opens a new one.

Tests: +2 (surface colormap follows max/min colour; foursurfaces uses its own
figure); canlab_test_fmridisplay_handle 25/25; checkcode clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: foursurfaces layout — white figure, axes off

In add_multiple_surfaces, set the layout figure Color to white and turn the
four surface axes Visible/Color off, so no axes box or grey background shows
behind the surfaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: foursurfaces — turn off all axes in the layout figure

The 2x2 layout figure also contained a stray default-position axes (created
during rendering) that showed a visible frame behind the surfaces. Turn off
Visible on every axes in the layout figure (surfaces, colorbar axes, and the
stray one); surface patches and colorbars are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: fix legend after set_colormap; add 'mango' colormap; plan update

- legend() no longer errors after set_colormap switches a layer to a single
  colormap. Root cause: refresh re-rendered the montage but did not update the
  layer's legend-defining .mincolor/.maxcolor, so a stale 2-row split mincolor
  made legend take the split path and index past the 2-element cmaprange.
  refresh now keeps .mincolor/.maxcolor in sync (as addblobs does), and legend
  has a defensive guard: a split-shaped mincolor without 4 cmaprange anchors
  falls back to a single map instead of erroring.
- Controller: new 'mango' colormap option ->
  set_colormap(obj, 'splitcolor', {[.5 0 1] [0 .8 .3] [1 .2 1] [1 1 .3]}); the
  dropdown reflects it and the action echoes the code line.
- Plan: record the agreed long-term colour architecture (one central
  value->colour mapping module feeding slice and surface renderers; surfaces in
  true-colour RGB; subsumes the unify-pipeline / compositing / RGB-surface items).

Tests: +1 (legend after set_colormap); canlab_test_fmridisplay_handle 26/26.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay controller: redesign + mango/seafire; legend single-range to plan

Controller:
- ~2x wider, two compact rows per layer: Opacity slider + Visible on row 1;
  a type-aware threshold slider + Colors dropdown on row 2. Bigger text,
  shorter re-render button, fixed-width opacity/colors controls.
- Threshold is now a slider: p-value slider with ticks at .001/.005/.01/.05/.1
  for statistic_image layers; raw |x| slider anchored at 0 and the 99.9th
  percentile of |data| for fmri_data/region layers.
- Colormap menu: rename 'mango' -> 'split (mango)' and add 'seafire'
  (set_colormap splitcolor {[0 0 1] [.2 .8 .6] [1 .4 .1] [1 1 0]}); dropdown
  reflects both and echoes the code line.

Plan: recorded that the single-range colorbar legend (warm/cool should be ONE
colorbar spanning zero, not split) is part of the central value->colour mapping
work — render_blobs' positive-percentile cmaprange and render_on_surface's
pos+neg colorbars both stem from the duplicated range/colorbar logic.

Tests: +1 (threshold slider + mango/seafire); canlab_test_fmridisplay_handle 27/27.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay controller: narrower single-column redesign, log p-slider, swatches

- Revert to a narrower (460px) single-column layout (the wide 2-col version cut
  off the colormap dropdown). Each layer panel: colormap title stripe, opacity
  slider, threshold slider, Colors dropdown + live preview swatch, Visible.
- Threshold p-value slider is now LOG scale (value = log10 p), ticks at
  .001/.005/.01/.05/.1 so .05-.1 sit close together and .001-.005 spread out.
  Raw |x| slider unchanged (linear, 0..99.9th pct).
- Colormap preview swatch next to the dropdown and a full-width title stripe,
  both updating live with the layer's colormap.
- Footer groups Re-render / Remove legend / Close buttons.
- Fonts +4pt. Window colour set by FIG_COLOR at the top of controller.m
  (now [1 .5 0]).
- seafire colours changed to {[.4 0 1] [.2 .9 .5] [1 .3 .2] [1 .8 .4]}.

Tests updated for the log slider; canlab_test_fmridisplay_handle 27/27.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: add squeeze_figure to remove montage figure white space

A montage figure is typically much taller than the slices it contains (they
occupy a ~40% horizontal band, leaving big top/bottom white margins).
squeeze_figure(obj) shrinks each montage figure's height and remaps the slice
axes to fill it, scaled so each slice keeps its pixel size, aspect ratio, and
horizontal placement — only the empty vertical margins are removed. Opt-in
(doesn't change montage defaults); skips figures that also contain surfaces.
Verified: 670 -> 258 px, slice pixel size preserved to 99.9%.

Test: +1 (squeeze_figure preserves slice size); canlab_test_fmridisplay_handle 28/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: mango default, toggle-legend, remove-layer, swatch picker, p->0

- mango is now the DEFAULT blob colormap (addblobs default split colours).
- Controller footer: 'Remove legend' -> 'Toggle legend' (on/off); dropped the
  Close button.
- Per-layer 'Remove layer' button + new remove_layer(obj, k) method (removes a
  single layer and re-renders remaining layers on surfaces; removeblobs still
  removes all).
- Clicking the colormap swatch opens the colour picker (set/re-pick a solid
  colour; works around uidropdown not re-firing on the already-selected item).
- p-value threshold slider extends below .001 down to ~0 (1e-6 floor, '~0' tick).
- Confirmed rethreshold already supports cluster-extent 'k' (e.g. 'k', 50),
  passed through to threshold(); verified it shrinks blobs as k grows.

Tests: +2 (default mango, remove_layer); slider-range test updated;
canlab_test_fmridisplay_handle 30/30.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* viz: add central canlab_colormap value->RGB component (step 1 of colour pipeline)

First step of the central value->colour mapping (VISUALIZATION_OVERHAUL_NOTES.md):
a single source of truth that turns data values into RGB for one colormap type,
independent of the renderer, so montage/surface/legend can agree exactly.

canlab_colormap (fmridisplay_helper_functions/canlab_colormap.m):
- static factories single / split / solid / indexed, plus from_render_args(args,
  cmaprange) bridging the existing layer render_args.
- map(values) -> Nx3 RGB, matching render_blobs (split: minpos->maxpos for
  positives, maxneg->minneg for negatives; single: mincolor->maxcolor; solid:
  constant; indexed: round+index; uncoloured -> NaN).
- legend_samples(n) -> one colorbar's samples (split spans zero in a single bar,
  the basis for fixing the single-range legend); lut(n) -> nx3 table.

Tests: Unit_tests/visualization/canlab_test_canlab_colormap.m (14 cases) all pass;
no regression in the fmridisplay handle suite (44/44 total). Not yet wired into
the renderers — surfaces -> true-colour RGB is the next step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* viz: render surfaces in true-colour RGB via central canlab_colormap (step 2)

render_on_surface gains an opt-in 'truecolor', <canlab_colormap> option: after
it samples per-vertex values, it colours each vertex via map() as N x 3 RGB,
composites over the anatomy gray (anatomy_to_rgb preserves curvature shading)
for uncoloured vertices, and sets FaceVertexCData=RGB with CDataMapping direct.
Default (option absent) is unchanged, so the @image_vector/surface bypass path
still uses indexed colour.

render_layer_surfaces builds canlab_colormap.from_render_args(args, cmaprange)
and passes it, so the managed display's surfaces now match the montage colours
exactly (warm = red->yellow, no blue negatives; verified) and removeblobs/erase
still restore gray. Surfaces are now N x 3 true colour, which is the basis for
multi-layer compositing and per-layer surface visibility.

Tests: +1 (surface true-colour via central map); canlab_test_fmridisplay_handle
31/31; canlab_colormap 14/14; display 4/4 — no regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* viz: fix black uncoloured surface areas on first true-colour render

A fresh surface is displayed via a solid FaceColor ([.5 .5 .5]) but its
FaceVertexCData can hold a stale dark value (~0.13). The true-colour path used
that stale CData as the anatomy gray, so uncoloured areas looked near-black on
the first render (re-render worked because eraseblobs had rewritten the CData
to a proper gray).

Fix: anatomy_to_rgb now prefers the surface's solid FaceColor (the gray
actually on screen) when FaceColor is a 1x3 RGB triplet, and render_on_surface
saves that proper gray to UserData so eraseblobs restores it too.

Test: +1 (first-render uncoloured areas are gray, not black);
canlab_test_fmridisplay_handle 32/32; display 4/4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* viz: multi-layer surface compositing in true-colour RGB (step 3)

Now that surfaces are true-colour RGB, layers composite instead of overwriting.

- render_layer_surfaces paints a layer onto the CURRENT surface colours (the
  erase-before-repaint is gone), so a new layer's coloured vertices win and its
  uncoloured vertices keep whatever is underneath (lower layers / gray). The
  anatomy gray is saved once (save-once UserData) so erase/removeblobs still
  restore gray, not a composite.
- New composite_surfaces(obj[, wh_surface]) does a full recompute: reset each
  surface to gray, then repaint every layer bottom->top. refresh (hence
  rethreshold/set_colormap/set_opacity) and remove_layer use it; addblobs
  composites a single new layer on top incrementally.

Verified: a broad green mask under narrower split stats shows green where the
stats don't cover and split on top; remove_layer recomposites the remainder;
removeblobs restores gray.

Tests: +1 (multi-layer compositing + remove_layer recomposite);
canlab_test_fmridisplay_handle 33/33; display 4/4 — no regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* viz: per-layer surface visibility + opacity blending + sign-aware default colormap

- Surfaces now BLEND layers by opacity (render_on_surface 'truecolor_alpha'):
  a layer's coloured vertices = a*colour + (1-a)*below, so a semi-transparent
  layer lets the one below show through, matching montages. render_layer_surfaces
  passes the layer's transvalue as the alpha (no more whole-patch FaceAlpha).
- Per-layer surface visibility: a layer with visible==false is skipped in
  render_layer_surfaces/composite_surfaces; the controller's Visible toggle sets
  the flag, hides the montage blobs, and recomposites the surfaces.
- Sign-aware default colormap (addblobs, only when no explicit colour given):
  binary mask -> solid; positive-only -> warm; negative-only -> cool; mixed +/-
  -> mango split. Propagates to montage and surface via the central map.

Tests: +2 (surface visibility, sign-aware default), opacity test updated for
blending; canlab_test_fmridisplay_handle 35/35; display 4/4; colormap 14/14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay/surface: addbrain keyword pass-through + centralized composites

@fmridisplay/surface no longer hard-codes a per-direction if/elseif switch.
It parses the reserved pairs (direction/orientation/axes), accepts a bare
direction token (surface(o2,'thalamus')), and passes remaining args through to
addbrain(dir, extra{:}) — so any eligible addbrain surface/region keyword now
works in the managed display automatically.

addbrain owns the default (lateral) view + lighting per keyword; surface only
special-cases bigbrain left/right (different addbrain surface) and mirrors the
azimuth 180 deg for a medial view. Managed multi-surface layouts keep identical
view angles; a couple of standalone lateral azimuths move to addbrain's
surface-specific convention.

Per T. Wager's suggestion, the brainstem left/right and caudate left/right
composite handle-sets move from surface.m into addbrain (alongside limbic/bg/
*_group), with their oblique 3D views and help/region entries.

help fmridisplay/surface now lists the common directions, the bare-token form,
and points to help addbrain for the full keyword list.

Tests: +test_surface_addbrain_passthrough, +test_surface_medial_flips_azimuth
(handle suite 37/37; display 4/4 unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay/surface: informative error for unknown addbrain keyword

When a bare direction token is not a recognized addbrain keyword, addbrain
errors with a terse 'Unknown method.'. surface() now catches that and raises
fmridisplay:surface:unknownDirection naming the offending token and pointing to
`help addbrain` (e.g. clarifies cutaways are 'left_cutaway'/'right_cutaway', not
'cutaway_left'). All other addbrain errors pass through unchanged.

Test: +test_surface_unknown_keyword_informative_error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay: host the legend in the controller; figure legends off by default

The colour stripe in each controller layer panel is now the legend: taller,
with numeric end labels underneath (from the layer's cmaprange, 1 sig fig).
Split (+/-) maps label both extremes with 0 in the centre; single ramps label
the two ends. Labels refresh on rethreshold/recolor. Opacity/threshold/colors/
visible rows shifted down to make room.

Figure colorbars are now OFF by default: @image_vector/montage no longer draws
the montage-figure legend unless 'legend' is passed; render_layer_surfaces /
composite_surfaces take a show_legend flag (default false -> 'nolegend' to
render_on_surface) so surfaces have no colorbar by default. The fmridisplay
constructor and montage now accept 'legend'/'nolegend' without warning.

Toggle legend now toggles colorbars on the montage/surface FIGURES (for export)
and works both ways: ON targets the montage figure explicitly (fixing the bug
where legend() drew into the controller uifigure via gcf and never reappeared)
and re-renders surfaces with colorbars; OFF removes them. State in controller
appdata; montage legend axes tagged for clean removal.

render_layer_surfaces gains a 4th arg (show_legend); its methods(Hidden)
declaration in fmridisplay.m is updated to match (requires a fresh class load).

Tests: +test_controller_shows_legend_labels, +test_montage_figure_legend_off_by_default;
surface-legend tests force legends on; pull-in test checks vertex colouring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay/surface: fix mixed isosurface+isocaps (coronal_slabs_4) error

surface() did `if strcmp(h(1).FaceColor,'interp')`, which errors ("Dot indexing
is not supported for variables of this type") when addbrain returns OLD-STYLE
numeric handles (doubles) instead of graphics objects -- e.g. coronal_slabs_4
returns 8 patch handles. It would also have blanket-grayed the whole set,
destroying the isocaps that use 'interp' to show the anatomy cross-section.

Replace with a per-object loop using get/set (works on numeric handles too):
keep any 'interp' colouring, set the rest to flat gray, make all opaque.

Test: +test_surface_mixed_patch_handles. QA: handle 41/41, display 4/4,
colormap 14/14 (interactive).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* render_blobs: route montage colours through central canlab_colormap

render_blobs no longer re-derives split/single colours inline. It builds one
canlab_colormap (split or single) from the parsed colours + cmaprange before the
slice loop and colours each slice via a shared central_map_slice helper -- the
same mapping render_on_surface already uses -- so montage and surface colours
come from a single implementation.

Verified pixel-identical to the old renderer on split/single/solid sample
montages (displayed-colour maxdiff: single 0, split 4e-12 float-eps, solid 0).
The only raw-CData differences were out-of-range values the old code stored at
invisible alpha=0 voxels, which the central map correctly clamps.

Retired the now-dead duplicated colour logic: splitcolor_Z_to_slicecdat and the
cdat2/cdatminneg/cdatmaxneg colour matrices.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* canlab_colormap: add colorbar_ramp; route controller legend stripe through it

Adds canlab_colormap.colorbar_ramp(n) -- the continuous legend-bar ramp with no
threshold gap (unlike map/legend_samples): for split it is the negative ramp
(minneg->maxneg) then the positive ramp (minpos->maxpos), reading
extreme-neg ... 0 ... extreme-pos.

The controller's per-layer colour stripe + preview swatch (the in-app legend)
now come from from_render_args(args).colorbar_ramp(64) instead of bespoke
colormap_tor calls, so the stripe, the rendered blobs, and surfaces share one
colour source. +2 colorbar_ramp tests (colormap suite 14->16).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmridisplay colour: single-range legend fix + perceptual colormaps

Single-range colorbar legend:
- get_default_cmaprange: a single-ramp (warm/cool) map on SIGNED data now spans
  the full robust range THROUGH ZERO ([prctile2 prctile98]) instead of a
  positive-only [p10 p90] that hid negatives. Positive/negative-only data and
  split maps are unchanged.
- render_on_surface gains a 'single_colorbar' flag (set by render_layer_surfaces
  from the central-map type) so single-ramp/solid/continuous maps draw ONE
  colorbar over the full range, not a misleading pos+neg pair. Split maps still
  get two. Verified: warm range [2.82 4.86] -> [-2.98 6.07]; surface colorbars
  2 -> 1; split/solid montage colours unchanged.

Perceptual colormaps:
- canlab_colormap gains a 'continuous' type (value range mapped continuously
  through an n x 3 LUT, distinct from atlas-style 'indexed').
- new canlab_perceptual_colormap(name, n): viridis/inferno/magma/plasma
  (embedded matplotlib anchors, interpolated) + turbo/parula (built-ins).
- controller dropdown offers all six; selecting one routes through set_colormap
  -> render_args 'colormap' -> central continuous map, so montage, surface, and
  the controller stripe all render it (one colorbar).

Tests: +single-range legend (2), +continuous/perceptual (3, colormap suite ->19).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

* Add CI workflow to codespell master on push and PRs

GitHub Actions workflow runs codespell on every push to master and on
pull requests targeting master. The workflow is pinned to a commit SHA
for reproducibility and uses 'permissions: contents: read' for safety.

Co-Authored-By: Claude Code 2.1.138 / Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add codespell configuration

Configure codespell to skip vendored / data / build artifacts and to
ignore short variable names, MATLAB built-in function names, proper
names, and domain abbreviations that would otherwise be flagged.

The ignore-regex skips URLs (so we don't "fix" typos in third-party
links) and the base64-encoded image data embedded by MATLAB
live-script export in docs/markdown_tutorials/*.m.

Co-Authored-By: Claude Code 2.1.138 / Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix ambiguous typos requiring context review

These typos have multiple plausible corrections (e.g. trough -> through
or trough). Fixed manually after reading each occurrence's context:

- regoin -> region (@atlas/threshold.m: brain region splitting)
- interals -> intervals (@canlab_dataset/plot_var.m: 95% confidence intervals)
- converstion -> conversion (×2: @fmri_data/fmri_data.m, Model_building_tools/design_matrix.m)
- extacted -> extracted (×2: @fmri_data/fmri_data.m, @fmri_timeseries/fmri_timeseries.m)
- interation -> iteration (@fmri_glm_design_matrix/robustfit.m: algorithm iteration)
- exlude -> exclude (@image_vector/image_similarity_plot.m: exclude empty data)
- followd -> followed (@image_vector/orthviews.m: followed by integer)
- pring -> print (Cifti_plotting/plot_surface_map.m: figure to print)
- numbe -> number (GLM_Batch_tools/spm_splines.README)
- nexted -> nested (HRF_Est_Toolbox4/.../testFMinSearchNew.m: nested function)
- fiels -> fields (Image_computation_tools/image_eval_function_multisubj.m: .n fields)
- shoul -> should (Image_computation_tools/image_histogram1d.m: trailing comment fragment)
- inbetween -> in between (×2: Model_building_tools/design_matrix.m)
- specificy -> specify (OptimizeDesign11/.../README_gst_notes)
- propotions -> proportions (OptimizeDesign11/core_functions/calcFreqDev.m)
- defauls -> defaults (OptimizeDesign11/other_functions/designsim_gui_script.m)
- cicles -> circles (Statistics_tools/.../xval_SVM_BKedit25Dec.m: plot legend)
- foor -> for (Statistics_tools/cluster_confusion_matrix.m)
- decription -> description (×5 in Visualization_functions/riverplot/*.m)
- psace -> space (canlab_canonical_brains/.../spherical_icosahedral_interpolation.m)
- covert -> convert (diagnostics/effect_size_map.m: convert to power estimate)
- coverge -> converge (diagnostics/fmri_mask_thresh_canlab.m: mixture models converge)
- sensivity -> sensitivity (web_repository_tools/...: sensitivity, specificity, PPV)
- warpper -> wrapper (×2 CBIG_RF_*.sh: wrapper script)

Co-Authored-By: Claude Code 2.1.138 / Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix non-ambiguous typos with codespell -w

Single-suggestion typos applied automatically by `codespell -w`, plus a
few targeted reverts for false positives the auto-fix had introduced:

  * `desc.prctiles` (struct field, documented public API) reverted from
    the `percentiles` "fix" in @image_vector/descriptives.m,
    Image_computation_tools/mean_image.m, and the docs table.
  * `efficency.m` function name kept (must match file name); sibling
    `efficiency.m` correctly renamed to its filename.
  * `groupt` variable in diagnostics/effect_size_map.m kept (script-level
    example variable); inline `codespell:ignore` annotation added.
  * `continguous` and `classfy` in docs intentionally retained as they
    reference real source identifiers; inline pragmas added.
  * Pipe-column alignment in canlab_glm_dsgninfo.txt restored after
    word-length changes (because, performed, with, details).

The dominant fixes by frequency: saggital -> sagittal (40 occurrences),
expermental -> experimental (28), dispaly -> display (22), atleast -> at
least (17, all in comments), aproach -> approach (15), concensus ->
consensus (12), initalize -> initialize (12), fucntion -> function (9),
signficant -> significant (8), homogenous -> homogeneous (7), accomodate
-> accommodate (7), re-used -> reused (7), efficency -> efficiency (in
efficiency.m only), analagous -> analogous (6), and ~230 unique
single-suggestion fixes across docs, comments, strings, and code.

Also fixes a long-standing latent bug in
Visualization_functions/tor_wedge_plot.m where `handels(i).texth(1) =
...` was a typo of the return variable `handles` -- the text-handle
assignment was being discarded into a phantom variable.

Co-Authored-By: Claude Code 2.1.138 / Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Code 2.1.138 / Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Tor Wager <torwager@gmail.com>
* Harden walkthrough CI tests: snapshots + fault-tolerant harness

The nightly tests-walkthroughs job had never passed (42/42 red). It ran the
CANlab_help_examples tutorials end-to-end via evalc() with no hardening, so
the first orthviews/surface/prompt/missing-data error on the headless runner
failed the whole test.

Decouple the tests from the live tutorials and harden them:

- Add verbatim snapshots of the 10 walkthroughs under walkthroughs/private/
  (genpath-excluded, so they never shadow the real tutorials on CI). Refresh
  by overwriting from example_help_files/.
- Add helpers/canlab_run_walkthrough_snapshot.m: runs a snapshot %%-cell by
  cell, headless, each cell in its own try/catch in a shared workspace, so a
  graphics-only section that fails on a headless runner does not abort the
  compute sections.
- Add helpers/canlab_classify_environment_error.m: buckets caught errors into
  graphics / input / data / cascade / genuine (centralizes the heuristics
  previously inlined in canlab_test_help_examples). Environment buckets are
  skipped (Incomplete); only genuine errors fail, with an informative report
  naming the section, offending line, and error id.
- Rewrite the 10 canlab_test_walkthrough_*.m wrappers to run their snapshot
  through the harness instead of evalc'ing the external script.
- Add walkthroughs/README.md documenting the design, refresh process, and why
  these stay a separate nightly tier (~5 min local / ~15-20 min CI) rather
  than folding into the fast per-push suite.

Full suite now: 10 passed, 0 failed, 0 incomplete. Two genuine tutorial bugs
this surfaced (write-without-overwrite in walkthrough 3; dat_descrip ->
metadata_table in 4b) are fixed in CANlab_help_examples and the snapshots
refreshed accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Walkthrough CI: handle missing Image Processing Toolbox

First CI run of the hardened nightly surfaced 3 failures, all the same
environment gap: read_nifti_volume falls back to niftiinfo (Image Processing
Toolbox), which the runner did not provision (only Statistics + Signal were
installed), so plot(obj) failed with Undefined function 'niftiinfo'.

- Provision Image_Processing_Toolbox in tests-walkthroughs.yml so plot() and
  NIfTI I/O actually run on CI.
- Safety net: canlab_classify_environment_error now buckets an
  UndefinedFunction error for a known optional-toolbox function (niftiinfo,
  niftiread, niftiwrite, cfg_getfile) as 'capability' -> skipped, not failed,
  so a missing toolbox can never spuriously redden the nightly. Genuine
  missing-function bugs still classify as 'genuine'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rning all zeros (#70)

* Initial plan

* Fix deprecated avifile/addframe and all-zeros timeseries_extract_slice

Co-authored-by: torwager <6262700+torwager@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: torwager <6262700+torwager@users.noreply.github.com>
Co-authored-by: Tor Wager <torwager@gmail.com>
…rrcoef dep) (#85)

Adds a RobustToolbox checkout + addpath to tests-walkthroughs.yml so the
regression walkthrough can resolve weighted_corrcoef. Verified via a headless
workflow_dispatch run: 10 tests, 0 failures, 0 errors, 1 expected skip.
* CLAUDE.md: document the matlab.unittest harness and CI

The Tests section still claimed "there is no test harness." Master now has
canlab_run_all_tests (matlab.unittest), a canlab_test_*.m naming/discovery
convention, a fault-tolerant walkthrough integration tier, and GitHub Actions
CI (test.yml, tests-walkthroughs.yml, codespell.yml). Document the runner
options, layout, Passed/Failed/Incomplete semantics, and prerequisites
(CanlabCore + Neuroimaging_Pattern_Masks + SPM on path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* CLAUDE.md: note walkthrough tier's RobustToolbox/CANlab_help_examples deps

Reflects the dependency surfaced by the nightly walkthrough fix (#85): the
walkthrough tier needs RobustToolbox on the path (weighted_corrcoef) in
addition to the unit-tier prerequisites, and the nightly CI checks out
CANlab_help_examples and RobustToolbox.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
torwager and others added 2 commits July 4, 2026 13:56
The codespell gate (merged via #53) was red on master because newer code
(merged via #53/#70/#79) contained typos the original sweep never saw.

Fix real comment typos: exlude->exclude, meaninful->meaningful
(canlab_pattern_similarity.m); INTIALIZE->INITIALIZE
(hansen_neurotransmitter_maps.m); and saggital/immmediately/iamges/waht/
sigificant/Treshold in the vendored walkthrough snapshot scripts
(Unit_tests/walkthroughs/private/canlab_help_*.m). All are in comments.

Whitelist 'hask' -- an intentional "has key" anonymous-function helper in
canlab_colormap.m and @fmridisplay/render_layer_surfaces.m. codespell now
exits clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…isualization walkthrough (#87)

* fmridisplay: stateful controller, multiview, uniform colour scaling, fixes

Interactive controller
- Auto-launch on construction (gated off in -batch / 'nocontroller'); restores
  the current figure so it never hijacks montage/surface rendering; opens
  top-right to avoid overlap.
- Threshold slider fixes: normalised [0 1] scale for raw |x| (uislider is
  unreliable on tiny ranges), double-cast (data-derived thresholds are single),
  numeric entry field, out-of-range shows an empty map.
- Split-colormap legend: 4 numeric labels + gap, live-updating with cmaprange.
- Colour bar renders via uiimage (fills the cell); larger legend font.
- Echoed code + title use the caller's real workspace variable name.

Layouts / entry points
- New fmridisplay.multiview method exposing all canlab_results_fmridisplay
  compositions (compact/compact2/full/multirow/hcp/freesurfer/subcortex/...).
- canlab_results_fmridisplay reuses a passed-in object: composes onto it if it
  has no montages, else just adds blobs (long-standing reuse preserved).
- fmridisplay.montage default routes through multiview('compact') (sag+axial
  combo, matching fmri_data.montage) and echoes the equivalent call.
- montage pulls existing blob layers onto a newly added montage; surface
  multi-keyword path (foursurfaces_hcp, ...) routes through addbrain.

Uniform colour scaling
- Shared canlab_default_cmaprange: robust per-arm percentile default used by
  montages, render_on_surface, and the surface path (no more min/max vs
  percentile divergence).

Consistency / robustness fixes
- Shared canlab_is_uifigure: never draw slices/surfaces into the controller
  uifigure (open a fresh figure instead) across all entry points.
- removeblobs clears surface-only layers (no montage blobhandles).
- Multi-image objects handled without errors: montage() lays out a per-image
  (multirow) display (fixed existing_axes expansion for a single placeholder
  axis with many slices); addblobs() uses the first image only.
- Hide the per-axes "..." interaction toolbar on montage slices and rendered
  surfaces (canlab_hide_axes_toolbar).

Tests
- Add regression tests: existing_axes expansion, multi-image montage/addblobs,
  multiview compose+pull-in; update the split-legend label test. 47/47 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZJacdhhByLGXqGHH9YQeo

* fmridisplay: route all montage colour modes through central canlab_colormap

Unify montage value->colour with the surface path by building the central
canlab_colormap for EVERY mode, not just split/single. Previously solid,
continuous ('colormap' LUT) and indexed ('indexmap'/atlas) blobs used legacy
inline render_blobs paths that bypassed canlab_colormap; the indexmap path was
also buggy (broken sub2ind assignment). Now:

- render_blobs builds central_cm for split, single, solid, continuous and
  indexed, and colours every slice via central_map_slice (one code path).
- canlab_colormap.map is NaN/Inf-robust for indexed and continuous, matching
  the legacy guards, since blob slices contain out-of-blob NaN voxels.

Montage and surface of the same data now produce identical value->colour
mappings across all colour modes (verified max|dRGB|=0), and the buggy indexmap
rendering is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fmridisplay: unify indexed/atlas colours in controller + surface

Route indexed (indexmap/atlas) layers through the central canlab_colormap
everywhere, so montage, surface, and the controller legend all agree:

- canlab_colormap.from_render_args now builds an 'indexed' colormap for
  indexmap layers (was falling through to the default mango split).
- controller: indexed layers show an 'indexed (atlas)' label, the region's
  solid colour as the swatch/stripe (not the whole palette), and blank the
  meaningless numeric range labels.
- render_layer_surfaces: index/atlas surfaces render through the same
  true-colour central-colormap path as every other mode (nearest-neighbour
  sampling to preserve integer indices), replacing the render_on_surface
  legacy indexmap branch.

Verified: surface vertex colours equal cmap(region_index) exactly (d=0) and
match the montage; split/single/solid/continuous unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add visualization walkthrough docs (multi-page guide + figures)

Multi-page markdown walkthrough of CanlabCore visualization methods:
getting started, montages, surfaces, colormaps, the fmridisplay
controller GUI with command-line equivalents, and atlases/regions.
Includes per-section figure generators (_gen/) and embedded figures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Walkthrough revisions: canlab_orthviews/niivue, custom montage, catalogs, master script

Applies the 8 approved revisions to the visualization walkthrough (docs only):
- §1: OO-API intro (methods return manipulable fmridisplay objects); add
  canlab_orthviews (SPM-free) + pointer to the canlab_niivue web viewer.
- §2.6: custom montage via fmridisplay.montage (orientation/slice_range/spacing).
- §3.4/§3.6: addbrain help pointers + full surface & composite catalog.
- §5.4: montage-set and surface-layout catalog.
- §6: atlas object help, atlas-type table, Neuroimaging_Pattern_Masks dependency.
- Clean surface figures without colorbar legends; new 01_canlab_orthviews.png
  and 02_custom_montage.png.
- Add master script _gen/visualization_walkthrough.m (one section per page,
  regenerates all figures; ran clean in R2026a); referenced from §1 and index.
- Remove temporary _gen/gen_revise.m; fix R2026a func(){:} syntax in docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: link visualization walkthrough from Object_methods; organize README doc sections

- Object_methods.md "Visualizing images and results": add a callout linking the
  new visualization_walkthrough/index.md as the hands-on starting point.
- README.md: reorganize the documentation list into "In this repository" vs
  "Online", adding the visualization walkthrough, workflows, and markdown
  tutorials sections alongside the existing Object methods reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 495f687ab3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -1,718 +0,0 @@
function [results_obj, stats, indx] = searchlightLukas(dat, varargin)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore the documented searchlightLukas method

Deleting this method removes the @image_vector/fmri_data dispatch target for users who call the documented searchlightLukas(dat, ...) API. I checked repo-wide with rg after the deletion and there is no remaining searchlightLukas.m definition (only the unrelated searchlight_disti_Lukas helper), while docs/image_vector_methods.md and docs/fmri_data_methods.md still list searchlightLukas, so existing scripts following the docs will now fail with an undefined method/function error. Keep a compatibility wrapper or remove/update the documented API with a migration path.

Useful? React with 👍 / 👎.

torwager and others added 2 commits July 12, 2026 23:19
…ata (#86)

* Add fmri_surface_data object for CIFTI/GIFTI surface & grayordinate data

New CANlab object `fmri_surface_data` (subclass of image_vector), the surface/
grayordinate analogue of fmri_data. Wraps the HCP CIFTI grayordinate standard as
a true CANlab object with full fmri_data-style method parity, running natively in
MATLAB with no external toolbox (sole exception: ica, which needs GIFT/icatb like
the base class).

Highlights:
- Native CIFTI-2 + GIFTI readers/writers (Surface_tools/canlab_read|write_cifti|
  gifti), bit-exact vs cifti_read/gifti oracles; proven to work with those tools
  removed from the path.
- Object: construction from CIFTI/GIFTI/struct/key-value; brain_model replaces
  volInfo (volInfo populated for the subcortical sub-block); no empty-squeezing
  (.dat always full; remove_empty/replace_empty are no-ops).
- Interop: reconstruct_image, to_fmri_data, compare_space (0/1/2/3), write.
- Volume<->surface: vol2surf / surf2vol via vendored CBIG RF warps (vol->surf->
  vol r~1.0), fully native.
- Data/analysis: cat/horzcat, mean, apply_mask, threshold (+cluster-extent 'k'),
  ttest, regress, predict, ica (predict/ttest/ica delegate to fmri_data via a
  proxy and remap results).
- Rendering: surface (native 4-panel + on any addbrain/MNI surface via volume
  resampling), render_on_surface, plot; reparse_contiguous (mesh graph),
  apply_parcellation, surface_region.
- Docs: docs/fmri_surface_data_{design_plan,methods}.md + runnable walkthrough.m.
- Tests: Unit_tests/surface_data/ (7 files, 41 tests passing).

Also: fix a stray non-functional debug line in @image_vector/ica.m that errored
ica() for all image_vector subclasses; document the fs_LR/fsaverage standard-mesh
correspondence in addbrain.m; add deprecation pointers in the external-dependent
Cifti_plotting readers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add fmri_surface_data docs, workflow how-to, and a CIFTI sample map

- docs/fmri_surface_data_methods.md: per-class methods page (properties +
  methods grouped by area), integrated into docs/Object_methods.md (class
  hierarchy + object-classes table) and docs/Workflows.md.
- docs/workflows/fmri_surface_data_howto.md: intro workflow — load surface data
  and render it natively; map volumetric data to the surface (vol2surf) and back
  (surf2vol / to_fmri_data); parcellation, thresholding, and group analysis.
  Includes three rendered example figures.
- Methods page includes a summarized inventory of surface atlases / maps available
  in the companion Neuroimaging_Pattern_Masks repository.
- Sample data: bundle one small HCP Open Access group myelin map
  (Sample_datasets/CIFTI_surface_examples/) with a provenance README; additional
  surface atlases are resolved from Neuroimaging_Pattern_Masks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Review pass: make fmri_surface_data examples runnable and docs accurate

Verified the whole object end-to-end (41 tests pass; walkthrough runs both with
and without Neuroimaging_Pattern_Masks). Fixes from the review:

- apply_mask: now falls back to the object's .mask property when no mask arg is
  passed (matching the documented behavior), with a clear error if neither is
  set. Added a unit test.
- Walkthrough (CanlabCore/docs/fmri_surface_data_walkthrough.m): use the bundled
  HCP myelin map as the primary object so it runs with only CanlabCore; guard the
  subcortex / atlas / parcellation sections on NPM availability; fix the
  parcellation demo (previously skipped because spaces never matched).
- How-to (docs/workflows/fmri_surface_data_howto.md): fix Section E/F snippets that
  referenced undefined variables (mydata, subj1..3, scores) and called regress
  before setting the design; they now match the runnable walkthrough.
- Docs accuracy: regress is native OLS (not "delegated"); cluster-extent threshold
  is implemented (not "planned"); ica is a class override (not inherited);
  canlab_surface_vertexcolors signature includes graycolor.
- Method help examples (vol2surf, surface, surf2vol, canlab_read_cifti): use the
  bundled emotionreg / myelin sample data instead of a private-repo image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmri_surface_data: mask volume-only methods; redirect orthviews/montage/slices; doc links

- Volume-only image_vector methods (flip, isosurface, interpolate, resample_space,
  extract_gray_white_csf, searchlight, slice_movie, trim_mask, read_from_file,
  extract_roi_averages, ...) are overridden in a methods(Hidden) block: they no
  longer clutter methods(obj)/tab-completion, and calling one raises a clear
  fmri_surface_data:unsupportedMethod error pointing to the surface equivalent.
  The class stays a full image_vector subclass.
- orthviews / montage / slices now route the subcortical grayordinates through
  to_fmri_data and call the fmri_data method (meaningful subcortex display);
  cortex-only objects get a clear "use surface(obj)" error.
- Added test_volume_only_methods_hidden; documented in the methods page
  ("Volume-only methods (redirected or masked)").
- Made the walkthrough / design-plan references in the docs into links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmri_surface_data: rename property intent -> imagetype; label_table now a MATLAB table

- Object property `intent` renamed to `imagetype` (holds
  dscalar/dtseries/dlabel/func/shape/label). The CIFTI/GIFTI reader & writer
  structs keep the faithful `intent`/`intents` field names at the I/O boundary;
  the constructor/write map between `obj.imagetype` and the CIFTI intent.
- `label_table` property is now a MATLAB `table` (variables key, name, rgba)
  instead of a struct array — nicer to view/query. Boundary converters
  (private struct2labeltable / labeltable2struct) translate to/from the native
  readers' struct arrays; apply_parcellation accepts either format.
- Updated tests (assert imagetype + istable(label_table)) and the methods docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fmri_surface_data: harmonize surface() colours with the volume visualization pipeline

surface() / render_on_surface() now accept the same colour vocabulary as the
master visualization pipeline (addblobs / set_colormap): clim / cmaprange,
colormap / colormapname (single sequential map), pos_colormap / neg_colormap,
splitcolor, maxcolor / mincolor, and color (solid). surface() forwards any colour
option straight through to render_on_surface, and the via-volume path forwards
the compatible options to image_vector/render_on_surface. So the same options
colour surface data and volume blobs. Added test_harmonized_color_options; updated
surface() help and the methods page. (Level 1 of the visualization harmonization;
the stateful fmridisplay-layer integration is the next step.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Integrate fmri_surface_data into the stateful fmridisplay (surface-native layers)

Level 2 of the visualization harmonization: an fmri_surface_data can now be added
to a managed fmridisplay and driven by the same controls as volume data.

- @fmridisplay/addblobs: detects an fmri_surface_data and dispatches to a new
  @fmridisplay/add_surface_blobs, which registers it as a SURFACE-NATIVE layer
  (source_surface) rather than converting to a volume region.
- @fmridisplay/render_layer_surfaces: paints surface-native layers by colouring
  the per-vertex data DIRECTLY on any registered mesh whose vertex count matches
  the object's space (fs_LR-32k / fsaverage-164k), at full fidelity, using the
  same central canlab_colormap value->colour map as montages (so colours match).
  set_colormap / set_opacity / removeblobs / refresh / the controller act on the
  layer like a volume layer. Non-matching meshes are skipped; a surface layer has
  no montage representation (use to_fmri_data / surf2vol for that).
- Fix @fmri_surface_data/isempty: image_vector/isempty treats an empty volInfo as
  empty, but a cortex-only surface object legitimately has an empty volInfo, so a
  fully-populated cortical map wrongly reported empty. Now empty iff .dat has no rows.
- Added canlab_test_surface_fmridisplay (addblobs paints, set_colormap/removeblobs
  propagate, non-matching/no-surface handled). Full surface suite: 47 passing.
- Documented the managed-display integration in the methods page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Surface viz: surface() returns a managed fmridisplay and renders under the controller

Redesign the fmri_surface_data surface() UX so the stateful display is the
single, self-consistent entry point:

- fmri_surface_data/surface(obj) native mode now returns a managed fmridisplay
  (not a raw-handle struct): it adds the surface set that MATCHES the object's
  space (fsLR_32k -> foursurfaces_hcp, fsaverage_164k -> foursurfaces_freesurfer)
  and paints the data as a managed surface-native layer. This is what makes the
  colormap changeable after rendering (set_colormap / removeblobs / refresh now
  act on the surfaces). Non-default surftype (midthickness/sphere) is honored by
  swapping the managed patches' geometry.

- fmridisplay/surface(o2, obj[, keyword]) detects an fmri_surface_data argument,
  adds its native surfaces (matching set by default, or an explicit keyword) and
  paints it via add_surface_blobs, all under controller management -- so
  "o2 = surface(o2, obj)" actually renders instead of silently dropping obj into
  the addbrain pass-through.

- Fix a hemisphere-assignment bug: addbrain relabels the foursurfaces_* patches
  with the group keyword, erasing their left/right tags, which made the native
  layer paint LEFT data onto BOTH hemispheres. render_layer_surfaces now resolves
  hemisphere by tag, falling back to vertex x-centroid sign (left < 0 < right).

- Cross-space request (e.g. fsaverage meshes for fs_LR data) now emits a clear
  fmridisplay:render_layer_surfaces:spacemismatch warning instead of rendering
  something wrong -- a surface object has no volume to resample onto a foreign
  mesh, so the fix is to add the matching surfaces (surface(obj) does so).

- add_surface_blobs normalizes color aliases (clim -> cmaprange, colormapname ->
  colormap, named colormap string -> numeric LUT) so a managed surface layer
  honors the same color vocabulary as the volume path.

- Clarify the .geom property doc: it holds ATTACHED custom mesh geometry from a
  .surf.gii and is empty for a data-only CIFTI (that is expected) -- standard
  meshes are loaded on demand by surface_space, not cached in .geom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Tests: cover managed surface() return, hemisphere correctness, space-mismatch warning

- canlab_test_surface_fmridisplay: add tests that surface(obj) returns a managed
  fmridisplay and paints each hemisphere with ITS OWN data (regression for the
  foursurfaces tag/x-centroid hemisphere fix); that surface(o2, obj) adds+paints
  on an existing display; and that an explicit cross-space request warns with
  fmridisplay:render_layer_surfaces:spacemismatch and does not mispaint.
- canlab_test_surface_render: update the native-render tests to the managed
  fmridisplay return (via a cortex_patches helper) instead of the old struct.

All surface_data tests pass (50 passed / 0 failed; 1 incomplete = ICA toolbox
absent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Docs: document surface() managed return, set_colormap workflow, space-mismatch behavior

Update the method reference (dev + readthedocs copies), the runnable walkthrough,
and the how-to to show that surface(obj) returns a managed fmridisplay, how to
recolor live with set_colormap, how surface(o2, obj) adds+paints on an existing
display, and the spacemismatch warning for cross-space surfaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Surface viz: make the managed surface layer fully stateful (controller, opacity, rethreshold, colormap)

The surface-native layer was bolted onto the stateful display but did not
integrate with the controller-driven machinery. Fixes so it behaves like a
volume layer:

- Controller sync: add_surface_blobs now calls update_controller (like addblobs),
  so the auto-launched controller reflects the new surface layer instead of
  keeping its empty state until the user reopens it by hand.

- Opacity: paint_surface_native_layer now saves the anatomy gray to the patch
  UserData on first paint, and composite_surfaces resets to gray when ANY patch
  in a view carries saved anatomy. Previously the layer blended against its OWN
  colour (base == painted colour), so set_opacity was a no-op. Now opacity blends
  toward gray and restores fully at 1.

- Rethreshold: add a surface-native branch that stores the threshold as a
  per-vertex magnitude cutoff (applied at paint time) instead of building a
  region from the object -- a cortical surface object has no matching volInfo, so
  the old image_vector path produced "Illegal size for mask.dat" / "Mask has
  multiple images" warnings and did not actually threshold the surface.

- Colormap keeps the threshold: canlab_colormap.map preserved NaN for the split
  map but not for 'single' (maxcolor/mincolor) or 'solid' (color) -- clamp01(NaN)
  became 0, painting uncoloured/thresholded vertices with the min endpoint. Now
  both preserve NaN, so set_colormap no longer resets a rethreshold and the
  medial wall stays gray under a single/solid map.

- refresh skips the montage render_blobs path for surface-native layers (they
  have no volume mapdata); the surfaces are redrawn by composite_surfaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Surface viz: render subcortical grayordinates for mixed objects (surface + montage)

A mixed grayordinate object (cortex + subcortical voxels, e.g. a 91k dscalar)
previously rendered only its cortex; the subcortical data was dropped.

- surface(obj) / surface(o2, obj): when the object has a subcortical sub-block,
  add its voxels as a second managed VOLUME layer (via to_fmri_data), so the
  subcortical anatomical meshes the foursurfaces set draws (thalamus, brainstem,
  cerebellum, ...) are painted. The layer is confined to the subcortical meshes
  via skip_cortex_nv (in render_layer_surfaces) so it never bleeds onto the
  cortical surfaces / medial wall, which the surface-native cortex layer owns.
  Cortex and subcortex share one colour range unless the caller passes
  clim/cmaprange, and both respond to set_colormap/rethreshold/set_opacity/the
  controller together.

- montage(o2, obj): detect an fmri_surface_data argument, build the montage, and
  render the subcortical grayordinates on the slices (instead of ignoring obj).
  Cortex-only objects warn and point to surface(obj).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Tests + docs: managed surface layer statefulness and subcortical rendering

- Tests: opacity blends toward gray and restores; rethreshold applies a per-vertex
  magnitude cutoff with no volInfo warning; set_colormap preserves the threshold;
  a mixed object renders cortex + subcortex without bleeding onto the medial wall;
  montage(o2, obj) draws the subcortical grayordinates. Update the render tests
  for the cortex+subcortex two-layer result.
- Docs: document mixed-object subcortical rendering and the surface rethreshold
  (magnitude cutoff) in the method reference.

All surface_data tests pass (54 passed / 0 failed; 1 incomplete = ICA toolbox
absent); canlab_colormap (19) and fmridisplay_handle (47) suites pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Surface viz: strip view-selectors before render_blobs; keep the controller in sync when adding surfaces

- addblobs consumed 'wh_surface'/'wh_montage' (and variants) but still forwarded
  them verbatim to render_blobs, which warned "Unknown input string option:
  wh_surface" whenever a surface layer was added to a display that also has
  montages (e.g. surface(o2, obj, 'foursurfaces_hcp') after montage(o2, obj), or
  the subcortical volume layer surface() adds for a mixed object). Strip these
  view-selectors from varargin after parsing, so they never reach render_blobs
  nor persist in render_args (where refresh would forward them again).

- @fmridisplay/surface now calls update_controller after adding a surface view
  (both the single-surface and foursurfaces_* multi paths), so an open controller
  stays bound to the same object and reflects the current views. No new controller
  is ever created (update_controller is a no-op when none is open; fmridisplay is
  a handle class so surface(o2, ...) mutates o2 in place).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Colormap: collapse a 4-element split cmaprange to full span for single/continuous maps

Recolouring a split-range layer to a single (maxcolor/mincolor) or continuous
(numeric 'colormap', e.g. hot) map reused the layer's 4-element split cmaprange
verbatim, so the map took [lo hi] = range(1:2) -- the NEGATIVE arm only. Every
value above the negative arm's top then clamped to the max colour: with hot that
is white, so a mixed grayordinate object's subcortical surfaces (whose values sit
in the positive arm) rendered ~96% white after set_colormap(o, 'colormap', hot).

canlab_colormap.single / .continuous now collapse a 4-element range to its full
span [range(1) range(4)] (mirroring how .split calls expand_split_range). Verified
visually across surface + volume, cortex + subcortex, with hot / parula /
maxcolor-mincolor / solid: no white artifacts, and the standard volume path is
unchanged (set_colormap hot on a volume montage+surface stays correct). Adds a
regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* load_image_set: add surface/volumetric map sets and a categorized 'list'

New loadable keywords (files in Neuroimaging_Pattern_Masks/spatial_basis_functions):
- hcp_ica15 / hcp_ica25 / hcp_ica50 (aliases hcp15/hcp25/hcp50): HCP resting-state
  group-ICA components, returned as fmri_surface_data (91k grayordinates).
- spectral_bases (alias hcp_bases): 200 spectral (Laplacian eigenmap) basis
  functions -> fmri_surface_data.
- mito_maps (alias mito): mitochondrial energetic-capacity maps (CI, CII, CIV,
  MitoD, MRC, TRC) -> fmri_data (6 maps), with names.

CIFTI/grayordinate sets return fmri_surface_data; volumetric sets return fmri_data.

load_image_set('list') now prints a series of CATEGORIZED tables, each under a
descriptive title -- multivariate signatures, person-level datasets, network/ICA/
topic maps, surface (CIFTI) map sets, gradients & basis maps, and meta-analysis/
receptor maps -- and RETURNS a struct whose fields are those tables (previously it
returned a single table). The signatures table is unchanged and is the .signatures
field. Help header documents 'list' prominently and the new keywords.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* load_atlas: register Tian subcortical scale atlases and add a categorized 'list'

- Register the Tian 3T subcortical atlas at explicit granularity scales:
  tian_s1 / tian_s2 / tian_s3 / tian_s4 (16 / 32 / 50 / 54 regions), each with an
  _fsl6 (MNI152NLin6Asym) variant; default is fmriprep20. The existing tian_3t
  keyword already loads the finest scale (S4). Removes a stale commented-out
  schaefer400 block (that file is a 32-region network-level parcellation, already
  covered by yeo17networks, not the 400-parcel Schaefer).
- load_atlas('list') prints categorized tables of atlas keywords (combined, cortex,
  subcortical, thalamus/hypothalamus, brainstem/cerebellum, networks) and returns a
  struct of those tables, mirroring load_image_set('list'). Help header documents it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Docs + tests for the load_image_set / load_atlas registry updates

- New workflow page docs/workflows/load_image_set_howto.md: discovering sets with
  load_image_set('list'), loading datasets, applying signatures (apply_mask), image
  similarity to networks/topics (image_similarity_plot), dual regression
  (dual_regression), and surface (CIFTI) map sets. Linked from docs/Workflows.md.
- canlab_test_load_registries.m: verifies the new surface/volumetric keywords return
  the right object types and map counts, that both 'list' options return a struct of
  tables, and that the Tian S1-S4 atlases load with the expected region counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Test: skip canlab_test_load_registries under GitHub Actions

This registry test needs the full Neuroimaging_Pattern_Masks data (large CIFTI /
.mat binaries). It is headless-safe and its per-test assumeFail guards already
prevent CI failures, but by request it is now kept out of the CI unit tier: setupOnce
assumeFail's the whole file when GITHUB_ACTIONS is set, so it is skipped (Incomplete)
in CI and runs normally when executed locally. teardownOnce is guarded so the early
skip does not error on the unset figvis field (which would otherwise flip filtered
tests to failed).

Local: 6 passed / 0 failed. Simulated CI: 0 failed / 6 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Test: include canlab_test_load_registries in the GitHub Actions suite

The required data files (HCP-ICA / spectral CIFTI, mito_maps.mat, and the Tian
S1-S4 atlas objects) are all committed to the public Neuroimaging_Pattern_Masks
repo that CI checks out, and the test is headless-safe, so it runs in the CI unit
tier. Removes the GITHUB_ACTIONS skip added earlier; the per-test assumeFail guards
remain so a missing file is skipped (Incomplete) rather than failing CI, and the
teardownOnce guard stays as defensive cleanup.

Verified with GITHUB_ACTIONS set: 6 passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Docs: make explicit that vol2surf/surf2vol ARE the CBIG registration-fusion mappers

State clearly, in the method help headers and the surface docs, that vol2surf and
surf2vol are native reimplementations of the CBIG Registration-Fusion (RF-ANTs)
MNI152<->fsaverage mappers (Wu et al. 2018, Human Brain Mapping): vol2surf is a
line-for-line equivalent of CBIG_RF_projectMNI2fsaverage using the identical
vendored warp, driven by SPM's volInfo.mat instead of FreeSurfer's MRIread, so no
FreeSurfer or Connectome Workbench is needed. surf2vol is the native inverse using
the same warp. Also note that CBIG's heavier fsaverage2Vol ribbon-fill script
requires FreeSurfer + the CBIG MARS toolbox + external mask geometry (not bundled)
and is intentionally not wired in. Adds the Wu et al. 2018 reference. Docs only; no
code changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add resample_surface: native surface-to-surface resampling for fmri_surface_data

New method resample_surface(obj, target_space) resamples cortical data between
surface spaces natively (no FreeSurfer / Connectome Workbench):

- fsaverage_164k <-> fs_LR_32k (HCP CIFTI) via the vendored HCP registration
  ("deformed") sphere that expresses fsaverage vertices in the fs_LR-aligned
  frame, so both meshes share one spherical frame; resampled with the bundled
  spherical barycentric / nearest-neighbor engine.
- fsaverage_164k <-> fsaverage6/5/4 as the exact nested icosahedral subset
  (verified: first 40962 verts form a valid ic6 mesh, 81920 faces).

Accuracy/speed: barycentric ("linear") is the default for continuous data
(round-trip corr ~0.9999 vs ~0.998 for nearest); interpolation weights depend
only on geometry, so they are computed once and applied to all maps as a sparse
matrix multiply -- a 50-map object costs about the same as one map (~10s one-time
weight build for fsaverage<->fs_LR). Binary masks and .dlabel images auto-select
nearest to preserve discrete values; 'interp' overrides. Subcortical (voxel)
models are carried through unchanged. resample_surface(obj,'list') prints the
available target spaces + aliases.

Tests (canlab_test_surface_resample, 8/8 pass; core cases use a synthetic
fsaverage object from bundled assets, so no NPM needed): fsaverage->fs_LR, exact
nested downsample, round-trip accuracy, binary->nearest auto, interp override,
multi-map weight reuse, subcortex carried through.

Docs: a prominent "Surface spaces" section (fsaverage164k/6/5/4, fs_LR-32k, the
91k grayordinate model) with resolutions and references (Fischl 1999, Van Essen
2012, Glasser 2013); clarified that .geom holds ATTACHED CUSTOM meshes only and
standard meshes load on demand; and resample_surface method entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add onavg (OpenNeuro Average) as a resample_surface template space

onavg (Feilong et al. 2024, Nature Methods; CC0) is an equal-area cortical
surface template. TemplateFlow tpl-onavg ships onavg spheres registered to the
fs_LR frame (space-fsLR), which is the same common frame resample_surface already
uses -- so onavg resamples to fsaverage / fs_LR with the existing native spherical
engine, no FreeSurfer/Workbench.

- Vendor the space-fsLR onavg registration spheres (den-41k = 40962 verts/hemi,
  den-10k = 10242) as .mat (single verts / uint32 faces, ~2.35 MB total) under
  canlab_canonical_brains/Canonical_brains_surfaces/onavg/, with a provenance
  README (source, CC0 license, reference).
- resample_surface gains target/source spaces 'onavg' / 'onavg_41k' and
  'onavg_10k' (aliases onavg41k / onavg10k). onavg is a non-nested tessellation,
  so it always interpolates (no exact-subset fast path). Verified: fsaverage ->
  onavg -> fsaverage round-trip corr 0.9999; fs_LR -> onavg works.
- Test + docs: onavg round-trip test; onavg rows in the 'list' output and the
  Surface spaces table (with reference).

Only the registration spheres are vendored (enough to resample). onavg group
statistics (sulcal depth, curvature, vertex area) on GIN are not bundled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Surface viz: auto-resample surface data onto a mismatched standard mesh

Previously, rendering an fmri_surface_data layer onto a surface in a different
space (e.g. fs_LR data on an fsaverage foursurfaces_freesurfer mesh) skipped the
mesh with a spacemismatch warning. Now the painter resamples the data onto that
mesh's space automatically and paints it.

- resample_surface now accepts an isosurface PATCH HANDLE or a mesh struct (with
  .vertices) as the target, resolving the space by per-hemisphere vertex count
  (32492 -> fs_LR, 163842 -> fsaverage_164k, 40962/10242/2562 -> fsaverage6/5/4).
  A non-standard vertex count errors clearly (cannot resample onto an arbitrary
  mesh; use surf2vol + render_on_surface).
- render_layer_surfaces/paint_surface_native_layer: a cortical patch whose vertex
  count differs from the data but is a recognized standard mesh triggers an
  on-the-fly resample_surface (nearest, for render speed), CACHED on the layer
  (obj.activation_maps{k}.resampled) so set_colormap / rethreshold / set_opacity
  re-renders are instant (verified 0.4s first paint, 0.02s cached re-render). Only
  truly non-standard meshes remain unpainted (spacemismatch warning, reworded).
  Threshold is applied per hemisphere via a shared helper so native and resampled
  meshes behave identically.

Tests: canlab_test_surface_fmridisplay's mismatch test now asserts auto-resample
+ paint (was: warn + not painted); a resample test covers patch/mesh-struct
targets and the non-standard-count error. Full surface suite 64 passed / 0 failed
(1 incomplete = ICA); volume fmridisplay_handle 47/47 unaffected.

Docs: document rendering auto-resample and the patch/mesh target of
resample_surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Surface viz: render surface data onto ARBITRARY MNI isosurfaces via volume projection

Auto-resampling handled cross-space *standard* meshes (fs_LR<->fsaverage), but
arbitrary MNI isosurfaces with no spherical registration (addbrain 'hires left',
'cutaway', pial meshes) still warned and went unpainted. Now they are painted by
projecting the data to a volume and sampling it at the mesh vertices.

- New public method to_display_volume(obj): project any surface/grayordinate object
  to an MNI fmri_data -- cortex resampled to fsaverage (resample_surface, auto
  nearest for labels) then surf2vol, MERGED with subcortex (to_fmri_data). The
  private obj_to_volume now delegates to it, so render_on_surface(any_surf, arbitrary
  patch) works for fs_LR data too (previously fsaverage-only) and includes subcortex.
- render_layer_surfaces/paint_surface_native_layer: a patch whose vertex count is
  not a recognized standard cortical mesh now projects via to_display_volume (cached
  on the layer as display_volume) and paints with image_vector.render_on_surface
  using the layer's truecolor map + opacity; the layer threshold is applied to the
  volume. Only a genuinely failing projection leaves a mesh unpainted. Verified on
  the Gordon atlas: 'hires left' and 'cutaway' render cortex + subcortex, no warning,
  ~1.7-3.6s first paint, cached re-renders fast.

Tests: to_display_volume returns a cortical MNI volume; rendering onto an addbrain
'hires left' mesh paints it (guarded). Full surface suite 64 passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Surface viz: 'unique'/'solid' color modes + a single source of truth for auto-selection

- New helper canlab_color_mode(dat) is the single source of truth for choosing a
  blob colour mode: 'solid' (<=2 distinct nonzero values, i.e. a binary mask),
  'unique' (an atlas / .dlabel parcellation, or a few integer labels), or
  'colormap' (continuous / many values). Cutoff 1000 for a generic integer map;
  atlas and .dlabel/.label objects are always 'unique' regardless of region count
  (Gordon-333, Julich-314, etc. exceed the old 300 orthviews cutoff).

- fmri_surface_data surface rendering (add_surface_blobs) honours an explicit
  'unique' or 'solid' flag and, when no colour spec is given, auto-selects the mode
  via canlab_color_mode. 'unique' -> one solid colour per region (scn_standard_colors
  as an indexed colormap); 'solid' -> a single colour for all in-data vertices.
  surface(o2, surf_obj) / surface(surf_obj) / addblobs route the flags through.
  Verified: surface(gordon_dlabel) now auto-renders a unique-colour parcellation
  (cortex + subcortex); surface(obj,'solid') and 'unique' work explicitly;
  continuous maps still default to a colormap.

Tests: canlab_color_mode classification + surface unique/solid/auto (14/14 in the
fmridisplay suite; full surface suite 66 passed / 0 failed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* render_blobs: ignore 'unique'/'solid' color-mode flags; controller bg -> light sea green

- render_blobs now treats 'unique' and 'solid' as recognized (ignored) options
  instead of emitting "Unknown input string option:unique". These flags are handled
  upstream (region/montage colortype, add_surface_blobs), so montage(atl,'unique')
  no longer prints the spurious warning. addblobs already suppresses its own
  unknown-option warning.
- fmridisplay controller window background changed from orange to light sea green
  ([0.125 0.698 0.667]).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fmridisplay controller: never change the current figure (montage axes consistency)

Root cause of montage slices intermittently rendering in the wrong figure/axes: the
controller is a uifigure, and opening OR rebuilding it (auto-launch in the
constructor, update_controller after addblobs/set_colormap, or an explicit
controller() call) could leave it as gcf, so the next gca/gcf-based slice drawing
targeted the controller or a stale figure. The constructor already guarded its own
launch; this moves the guard INTO controller() via onCleanup so EVERY entry path
(including the early update-in-place return) restores the caller's current figure.

Timing-neutral (the controller still opens when it did before), so it does not
disturb figure-timing behavior -- verified the full fmridisplay_handle suite (47/47,
incl. test_foursurfaces_uses_dedicated_figure) and the surface suite (67 passed)
still pass. The bug is interactive (real-window focus) and does not reproduce
headless, but this addresses the mechanism directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fmridisplay controller: working 'unique (per region)' colormap dropdown item

The colormap dropdown listed 'indexed (atlas)' but on_colormap had no case for it
(a no-op). Renamed it to 'unique (per region)' and wired it up: selecting it applies
one solid colour per region via set_colormap('indexmap', scn_standard_colors(n)) on
the layer, and current_colormap_label reports 'unique (per region)' for an indexed
layer. The dropdown now offers colormap options, 'unique (per region)', and
'solid colour…'. Verified: option present, initial value mapping, and the underlying
set_colormap(indexmap) action; fmridisplay_handle (47/47) and surface_fmridisplay
(14/14) suites pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* region/surface: 'unique'/'solid' colour modes + auto unique for atlases

surface() on a volumetric atlas (via image_vector/surface -> region/surface)
rendered the integer region indices through a CONTINUOUS colormap (blended at
borders), and 'unique' produced an "Unknown input string option" warning.

region/surface now:
- accepts 'unique'/'solid' flags (no warning);
- 'unique' -> one solid colour per region (scn_standard_colors as an indexed
  colormap; render_on_surface uses nearest automatically), matching region/montage;
- 'solid' -> a single colour;
- when no explicit colour spec is given, auto-selects the mode via the shared
  canlab_color_mode. region2imagevec stores integer region indices for an atlas
  (-> 'unique') but continuous .Z values for a statistic map (-> 'colormap'), so
  surface(atlas) now auto-renders unique colours while surface(t) keeps its
  colormap (verified: no regression on continuous maps).

Test added (guarded on the julich atlas); surface_data suite 9/9, full surface +
fmridisplay_handle suites 114 passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Michael-Sun Michael-Sun merged commit facc69f into Michael-Sun:master Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants