From ac8ac335d6ba8aba8a2afb029e164dcbd4c5fc9b Mon Sep 17 00:00:00 2001 From: Tor Wager Date: Fri, 19 Jun 2026 19:57:21 -0400 Subject: [PATCH 1/6] 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) --- CanlabCore/Unit_tests/canlab_run_all_tests.m | 4 +- .../canlab_test_runner_robustness.m | 76 +++++++++++++++++++ .../helpers/canlab_safe_suite_from_file.m | 42 ++++++++++ .../canlab_test_check_roi_extraction.m | 58 ++++++++++++++ ...anlab_test_resampling_pattern_expression.m | 48 ++++++++++++ .../old_to_integrate/check_roi_extraction.m | 64 ---------------- ...resampling_pattern_expression_unit_test1.m | 40 ---------- 7 files changed, 227 insertions(+), 105 deletions(-) create mode 100644 CanlabCore/Unit_tests/canlab_test_runner_robustness.m create mode 100644 CanlabCore/Unit_tests/helpers/canlab_safe_suite_from_file.m create mode 100644 CanlabCore/Unit_tests/image_vector/canlab_test_check_roi_extraction.m create mode 100644 CanlabCore/Unit_tests/image_vector/canlab_test_resampling_pattern_expression.m delete mode 100644 CanlabCore/Unit_tests/old_to_integrate/check_roi_extraction.m delete mode 100644 CanlabCore/Unit_tests/old_to_integrate/resampling_pattern_expression_unit_test1.m diff --git a/CanlabCore/Unit_tests/canlab_run_all_tests.m b/CanlabCore/Unit_tests/canlab_run_all_tests.m index 2257e139..5e013a72 100644 --- a/CanlabCore/Unit_tests/canlab_run_all_tests.m +++ b/CanlabCore/Unit_tests/canlab_run_all_tests.m @@ -63,7 +63,9 @@ case 'include' % run everything end - suite = [suite, TestSuite.fromFile(fpath)]; %#ok + % canlab_safe_suite_from_file warn-skips files that are not valid test + % files instead of letting a NonTestFile error abort the whole run. + suite = [suite, canlab_safe_suite_from_file(fpath)]; %#ok end tag = char(p.Results.Tag); diff --git a/CanlabCore/Unit_tests/canlab_test_runner_robustness.m b/CanlabCore/Unit_tests/canlab_test_runner_robustness.m new file mode 100644 index 00000000..149254dd --- /dev/null +++ b/CanlabCore/Unit_tests/canlab_test_runner_robustness.m @@ -0,0 +1,76 @@ +function tests = canlab_test_runner_robustness +%CANLAB_TEST_RUNNER_ROBUSTNESS Discovery must not abort on a stray non-test file. +% +% canlab_run_all_tests globs every canlab_test_*.m under Unit_tests and +% concatenates the suites. A file that matches the name pattern but is not a +% valid matlab.unittest test (an old plain-assert script, or a function whose +% internal name does not match the filename) makes TestSuite.fromFile throw +% MATLAB:unittest:TestSuite:NonTestFile. Without guarding, that single error +% aborts discovery of the entire suite. canlab_safe_suite_from_file warn-skips +% such files instead; these tests pin that behavior. + +tests = functiontests(localfunctions); +end + + +function test_nontest_file_is_warn_skipped(tc) %#ok<*DEFNU> +% A canlab_test_*.m whose function name != filename and that never calls +% functiontests is not a valid test file: skip it, warn, return empty. +folder = tc.applyFixture( ... + matlab.unittest.fixtures.TemporaryFolderFixture).Folder; +fpath = local_write(folder, 'canlab_test_bogus_nontest.m', { ... + 'function some_other_name()' + 'assert(true);' + 'end'}); + +% Use lastwarn rather than verifyWarning so the check does not depend on the +% ambient warning display state (a caller may have warnings off; lastwarn +% still records the id even when the warning event is not displayed). +lastwarn('', ''); +[suite, ok] = canlab_safe_suite_from_file(fpath); +[~, warn_id] = lastwarn; + +tc.verifyFalse(ok, 'expected ok=false for a non-test file'); +tc.verifyEmpty(suite, 'expected an empty suite for a non-test file'); +tc.verifyEqual(warn_id, 'canlab_run_all_tests:skippedNonTestFile', ... + 'expected a skippedNonTestFile warning'); +end + + +function test_valid_test_file_is_loaded(tc) +% A well-formed functiontests file loads normally (ok=true, non-empty suite). +folder = tc.applyFixture( ... + matlab.unittest.fixtures.TemporaryFolderFixture).Folder; +fpath = local_write(folder, 'canlab_test_valid_dummy.m', { ... + 'function tests = canlab_test_valid_dummy' + 'tests = functiontests(localfunctions);' + 'end' + '' + 'function test_trivial(tc)' + 'tc.verifyTrue(true);' + 'end'}); + +[suite, ok] = canlab_safe_suite_from_file(fpath); +tc.verifyTrue(ok, 'expected ok=true for a valid test file'); +tc.verifyNotEmpty(suite); +tc.verifyEqual(numel(suite), 1); +end + + +function fpath = local_write(folder, name, lines) +% Write a cellstr of lines to folder/name and return the full path. Uses +% fopen/fprintf (not writelines) so the test runs on older MATLAB too. +fpath = fullfile(folder, name); +fid = fopen(fpath, 'w'); +tc_assert_open(fid, fpath); +cleanup = onCleanup(@() fclose(fid)); +fprintf(fid, '%s\n', lines{:}); +end + + +function tc_assert_open(fid, fpath) +if fid < 0 + error('canlab_test_runner_robustness:cannotWrite', ... + 'Could not open %s for writing.', fpath); +end +end diff --git a/CanlabCore/Unit_tests/helpers/canlab_safe_suite_from_file.m b/CanlabCore/Unit_tests/helpers/canlab_safe_suite_from_file.m new file mode 100644 index 00000000..b50b122f --- /dev/null +++ b/CanlabCore/Unit_tests/helpers/canlab_safe_suite_from_file.m @@ -0,0 +1,42 @@ +function [suite, ok] = canlab_safe_suite_from_file(fpath) +%CANLAB_SAFE_SUITE_FROM_FILE Build a test suite from one file without aborting on bad files. +% +% [suite, ok] = canlab_safe_suite_from_file(fpath) +% +% Wraps matlab.unittest.TestSuite.fromFile so that a file which is not a +% valid test (e.g. a stray canlab_test_*.m that is an old plain-assert +% script or whose internal function name does not match the filename) +% does not throw and abort discovery of the whole suite. Such a file is +% warn-skipped and an empty Test array is returned instead. +% +% This matters because canlab_run_all_tests globs every canlab_test_*.m +% under Unit_tests and concatenates the results; a single NonTestFile +% error from fromFile would otherwise take down the entire run. +% +% :Inputs: +% **fpath:** absolute path to a candidate .m test file. +% +% :Outputs: +% **suite:** a matlab.unittest.Test array (empty if the file is not a +% valid test file). +% **ok:** logical, true if fromFile succeeded, false if the file was +% skipped. +% +% :See also: canlab_run_all_tests, matlab.unittest.TestSuite + +ok = true; +suite = matlab.unittest.Test.empty; + +try + suite = matlab.unittest.TestSuite.fromFile(fpath); +catch ME + ok = false; + if strcmp(ME.identifier, 'MATLAB:unittest:TestSuite:NonTestFile') + warning('canlab_run_all_tests:skippedNonTestFile', ... + 'Skipping %s: not a valid matlab.unittest test file.', fpath); + else + warning('canlab_run_all_tests:fromFileError', ... + 'Skipping %s: %s (%s)', fpath, ME.message, ME.identifier); + end +end +end diff --git a/CanlabCore/Unit_tests/image_vector/canlab_test_check_roi_extraction.m b/CanlabCore/Unit_tests/image_vector/canlab_test_check_roi_extraction.m new file mode 100644 index 00000000..3d609903 --- /dev/null +++ b/CanlabCore/Unit_tests/image_vector/canlab_test_check_roi_extraction.m @@ -0,0 +1,58 @@ +function tests = canlab_test_check_roi_extraction +%CANLAB_TEST_CHECK_ROI_EXTRACTION Multi-region ROI extraction + write/reload round-trip. +% +% Complements canlab_test_extract_roi (single-mask extraction) by exercising +% the two things that test does not: +% 1. Multi-region extraction with 'unique_mask_values' (one average per +% integer label in a parcellation), using atlas_labels_combined.img. +% 2. Invariance of the extracted region averages across a write-to-disk and +% reload cycle - a regression guard on the NIfTI I/O path. +% +% Converted from the old standalone script Unit_tests/old_to_integrate/ +% check_roi_extraction.m, which printed PASS/FAIL but never asserted. + +tests = functiontests(localfunctions); +end + + +function test_unique_mask_values_roundtrip(tc) %#ok<*DEFNU> +mask_file = which('atlas_labels_combined.img'); +tc.assumeNotEmpty(mask_file, 'atlas_labels_combined.img not on path'); + +mask_image = fmri_data(mask_file, 'noverbose'); + +% Synthetic per-region timeseries: region i carries the signal ts*sqrt(i), +% so every region has a distinct, known mean trajectory across images. +wh_region = mask_image.dat; +regions = unique(wh_region); +nimgs = 20; +ts = linspace(-10, 10, nimgs); + +dat = mask_image; +dat.dat = zeros(size(mask_image.dat, 1), nimgs); +for i = 1:numel(regions) + idx = wh_region == regions(i); + dat.dat(idx, :) = repmat(ts .* sqrt(i), sum(idx), 1); +end + +cl1 = extract_roi_averages(dat, mask_image, 'unique_mask_values'); +all_reg1 = cat(2, cl1(:).dat); + +% One row per image, one column per non-zero region label. +tc.verifyEqual(size(all_reg1, 1), nimgs); +tc.verifyGreaterThan(size(all_reg1, 2), 1); + +% Round-trip through disk in a scratch folder, then re-extract. +tc.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); +dat.fullpath = fullfile(pwd, 'test_roi_image.nii'); +write(dat, 'overwrite'); +reloaded = fmri_data(dat.fullpath, 'noverbose'); + +cl2 = extract_roi_averages(reloaded, mask_image, 'unique_mask_values'); +all_reg2 = cat(2, cl2(:).dat); + +tc.verifyEqual(size(all_reg2), size(all_reg1)); +% Small absolute slack absorbs single-precision NIfTI write rounding (the +% original script used a 1e-3 relative threshold for the same reason). +tc.verifyEqual(all_reg2, all_reg1, 'AbsTol', 1e-2, 'RelTol', 1e-3); +end diff --git a/CanlabCore/Unit_tests/image_vector/canlab_test_resampling_pattern_expression.m b/CanlabCore/Unit_tests/image_vector/canlab_test_resampling_pattern_expression.m new file mode 100644 index 00000000..79ebfebb --- /dev/null +++ b/CanlabCore/Unit_tests/image_vector/canlab_test_resampling_pattern_expression.m @@ -0,0 +1,48 @@ +function tests = canlab_test_resampling_pattern_expression +%CANLAB_TEST_RESAMPLING_PATTERN_EXPRESSION Pattern expression is stable across resampling. +% +% apply_mask(..., 'pattern_expression') resamples the weight map into the +% data space when the two differ. This test checks that the resulting +% pattern-expression scores barely change regardless of which interpolation +% method does that resampling (default vs nearest vs spline) - i.e. the +% dot-product readout is not an artifact of the interpolation choice. +% +% Uses the SIIPS signature against the emotionreg sample. Skipped if the +% SIIPS image set is not available on the path (it ships with +% Neuroimaging_Pattern_Masks). +% +% Converted from the old standalone visual script +% Unit_tests/old_to_integrate/resampling_pattern_expression_unit_test1.m, +% which only plotted the variants and asserted nothing. + +tests = functiontests(localfunctions); +end + + +function test_pattern_expression_stable_across_interp(tc) %#ok<*DEFNU> +obj = canlab_get_sample_fmri_data(); + +try + siips = load_image_set('siips', 'noverbose'); +catch ME + tc.assumeFail(['SIIPS signature not available on this runner: ' ME.message]); +end +tc.assumeNotEmpty(siips.dat, 'SIIPS loaded but empty'); + +n = size(obj.dat, 2); + +% Default path lets apply_mask resample internally; the other two pre-resample +% the weight map with an explicit interpolation method. +pe_default = apply_mask(obj, siips, 'pattern_expression'); +pe_nearest = apply_mask(obj, resample_space(siips, obj, 'nearest'), 'pattern_expression'); +pe_spline = apply_mask(obj, resample_space(siips, obj, 'spline'), 'pattern_expression'); + +for v = {pe_default, pe_nearest, pe_spline} + tc.verifyEqual(numel(v{1}), n); + tc.verifyTrue(all(isfinite(v{1})), 'pattern expression returned non-finite values'); +end + +% Empirically these correlate at >= 0.9999; 0.99 is a safe regression floor. +tc.verifyGreaterThan(corr(pe_default, pe_nearest), 0.99); +tc.verifyGreaterThan(corr(pe_default, pe_spline), 0.99); +end diff --git a/CanlabCore/Unit_tests/old_to_integrate/check_roi_extraction.m b/CanlabCore/Unit_tests/old_to_integrate/check_roi_extraction.m deleted file mode 100644 index b25734b2..00000000 --- a/CanlabCore/Unit_tests/old_to_integrate/check_roi_extraction.m +++ /dev/null @@ -1,64 +0,0 @@ -function check_roi_extraction() - -% test ROI extraction -mask_image = fmri_data(which('atlas_labels_combined.img')); -dat = mask_image; -wh_region = dat.dat; - -% create timeseries data -- add data "after" 1st image -nimgs = 50; - -n = unique(wh_region); -ts = linspace(-10, 10, nimgs); -v = size(dat.dat, 1); -dat.dat = zeros(v, nimgs); - -for i = 1:length(n) - my_indx = wh_region == n(i); - - dat.dat(my_indx, :) = repmat(ts .* sqrt(i), sum(my_indx), 1); -end - -fprintf('\nDATA GENERATED\n\n'); - -cl1 = extract_roi_averages(dat, mask_image, 'unique_mask_values'); - -% extracted average values for all the regions in mask_image. -all_reg1 = cat(2, cl1(:).dat); - -dat.fullpath = fullfile(pwd, 'test_image.img'); -write(dat); -fprintf('\nSAVED DATA TO %s\n\n', dat.fullpath); - -% reload -fprintf('\nLOADING DATA FROM %s\n\n', dat.fullpath); -dat = fmri_data(dat.fullpath, which('gray_matter_mask.img')); - -% region obj -> extract_data -% test extract_roi_averages -cl2 = extract_roi_averages(dat, mask_image, 'unique_mask_values'); - -% extracted average values for all the regions in mask_image. -all_reg2 = cat(2, cl2(:).dat); - -% uniq_reg = sort(unique(mask_image.dat)); - -% compare roi averages of initially data generated and data loaded from file -if is_equal(all_reg1, all_reg2, 0.001) - fprintf('\nExtracted ROI averages are equal with an error threshold of 0.001\nPASS\n'); -else - fprintf('\nExtracted ROI averages are not equal with an error threshold of 0.001\nFAIL\n'); -end - - -% ------------------------------------------------------ -% INLINE FUNCTION -% ------------------------------------------------------ - - function res = is_equal(A, B, error_threshold) - err = abs((A-B)./B) <= error_threshold; - - res = all(err(:)); - end % is_equal - -end % main function \ No newline at end of file diff --git a/CanlabCore/Unit_tests/old_to_integrate/resampling_pattern_expression_unit_test1.m b/CanlabCore/Unit_tests/old_to_integrate/resampling_pattern_expression_unit_test1.m deleted file mode 100644 index b7f654e0..00000000 --- a/CanlabCore/Unit_tests/old_to_integrate/resampling_pattern_expression_unit_test1.m +++ /dev/null @@ -1,40 +0,0 @@ -function resampling_pattern_expression_unit_test1 - -obj = load_image_set('emotionreg'); % test dataset -siips = load_image_set('siips'); % sampled to higher-res mask on loading -siips_resamp = resample_space(siips, obj, 'spline'); -siips_orig = fmri_data(siips.image_names); % original pattern - -val = []; -val(:, 1) = apply_mask(obj, siips_orig, 'pattern_expression'); -val(:, 2) = apply_mask(obj, resample_space(siips_orig, obj), 'pattern_expression'); -val(:, 3) = apply_mask(obj, resample_space(siips_orig, obj, 'nearest'), 'pattern_expression'); -val(:, 4) = apply_mask(obj, resample_space(siips_orig, obj, 'spline'), 'pattern_expression'); - -val(:, 5) = apply_mask(obj, siips, 'pattern_expression'); -val(:, 6) = apply_mask(obj, resample_space(siips, obj), 'pattern_expression'); -val(:, 7) = apply_mask(obj, resample_space(siips, obj, 'nearest'), 'pattern_expression'); -val(:, 8) = apply_mask(obj, resample_space(siips, obj, 'spline'), 'pattern_expression'); - -val(:, 9) = apply_mask(obj, siips, 'pattern_expression'); -val(:, 10) = apply_mask(obj, resample_space(siips, obj), 'pattern_expression'); -val(:, 11) = apply_mask(obj, resample_space(siips, obj, 'nearest'), 'pattern_expression'); -val(:, 12) = apply_mask(obj, resample_space(siips, obj, 'spline'), 'pattern_expression'); - -val(:, 13) = apply_mask(obj, siips_resamp, 'pattern_expression'); -sdat = apply_siips(obj); -val(:, 14) = sdat{1}; - -m = mean(val); -s = std(val); -c = corr(val); - -create_figure('siips comparison', 1, 3); -errorbar(m, s); title('means and standard errors') -subplot(1, 3, 2); plot(m); title('means (zooming in on differences') -subplot(1, 3, 3); imagesc(c); colorbar -title('Correlations among variants') - -set(gca, 'YDir', 'reverse'); axis tight - -end From a9659ad66dbc3c04c5db832b2f6e8fb19a72d959 Mon Sep 17 00:00:00 2001 From: Tor Wager Date: Fri, 19 Jun 2026 20:32:49 -0400 Subject: [PATCH 2/6] 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) --- .../helpers/canlab_get_dpsp_hot_warm.m | 27 +++++ .../predictive_model/canlab_test_xval_SVM.m | 105 ++++++++++++++++++ .../predictive_model/canlab_test_xval_SVR.m | 72 ++++++++++++ ...canlab_test_xval_discriminant_classifier.m | 66 +++++++++++ ...canlab_test_xval_regression_multisubject.m | 74 ++++++++++++ ...al_regression_multisubject_featureselect.m | 75 +++++++++++++ CanlabCore/Unit_tests/xval_SVM_unit_test.m | 104 ----------------- CanlabCore/Unit_tests/xval_SVR_unit_test.m | 62 ----------- .../xval_discriminant_classifier_unit_test.m | 62 ----------- ...ion_multisubject_featureselect_unit_test.m | 59 ---------- .../xval_regression_multisubject_unit_test.m | 75 ------------- 11 files changed, 419 insertions(+), 362 deletions(-) create mode 100644 CanlabCore/Unit_tests/helpers/canlab_get_dpsp_hot_warm.m create mode 100644 CanlabCore/Unit_tests/predictive_model/canlab_test_xval_SVM.m create mode 100644 CanlabCore/Unit_tests/predictive_model/canlab_test_xval_SVR.m create mode 100644 CanlabCore/Unit_tests/predictive_model/canlab_test_xval_discriminant_classifier.m create mode 100644 CanlabCore/Unit_tests/predictive_model/canlab_test_xval_regression_multisubject.m create mode 100644 CanlabCore/Unit_tests/predictive_model/canlab_test_xval_regression_multisubject_featureselect.m delete mode 100644 CanlabCore/Unit_tests/xval_SVM_unit_test.m delete mode 100644 CanlabCore/Unit_tests/xval_SVR_unit_test.m delete mode 100644 CanlabCore/Unit_tests/xval_discriminant_classifier_unit_test.m delete mode 100644 CanlabCore/Unit_tests/xval_regression_multisubject_featureselect_unit_test.m delete mode 100644 CanlabCore/Unit_tests/xval_regression_multisubject_unit_test.m diff --git a/CanlabCore/Unit_tests/helpers/canlab_get_dpsp_hot_warm.m b/CanlabCore/Unit_tests/helpers/canlab_get_dpsp_hot_warm.m new file mode 100644 index 00000000..e94fdd88 --- /dev/null +++ b/CanlabCore/Unit_tests/helpers/canlab_get_dpsp_hot_warm.m @@ -0,0 +1,27 @@ +function [hot, warm, ok] = canlab_get_dpsp_hot_warm() +%CANLAB_GET_DPSP_HOT_WARM Load the DPSP single-subject Hot/Warm sample maps. +% +% [hot, warm, ok] = canlab_get_dpsp_hot_warm() +% +% Returns the single-subject Hot and Warm condition maps from +% Sample_datasets/DPSP_pain_rejection_participant_maps as fmri_data objects. +% Used by the predictive_model / xval_* tests so the load semantics live in +% one place. ok is false (and hot/warm are []) if the sample files are not +% on the path, so callers can assume/skip gracefully. + +sample_dir = fullfile(fileparts(fileparts(which('fmri_data'))), ... + 'Sample_datasets', 'DPSP_pain_rejection_participant_maps'); +hot_file = fullfile(sample_dir, 'DPSP_single_subject_images_hot.mat'); +warm_file = fullfile(sample_dir, 'DPSP_single_subject_images_warm.mat'); + +ok = exist(hot_file, 'file') == 2 && exist(warm_file, 'file') == 2; +hot = []; +warm = []; + +if ok + H = load(hot_file); + W = load(warm_file); + hot = H.single_subject_images_hot; + warm = W.single_subject_images_warm; +end +end diff --git a/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_SVM.m b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_SVM.m new file mode 100644 index 00000000..93d2753d --- /dev/null +++ b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_SVM.m @@ -0,0 +1,105 @@ +function tests = canlab_test_xval_SVM +%CANLAB_TEST_XVAL_SVM Cross-validated SVM classification via xval_SVM. +% +% Converted from Unit_tests/xval_SVM_unit_test.m. Drives xval_SVM on the DPSP +% Hot vs Warm single-subject maps (a paired, within-person design) and checks +% the returned @predictive_model object: class/state, canonical-path fields, +% shapes/ranges, fit_type + omitted markers, validate_object, and clone. +% +% The model is fit once in setupOnce and cached; each test reads the cached +% object. Skipped if the DPSP sample data is not on the path. + +tests = functiontests(localfunctions); +end + + +function setupOnce(tc) %#ok<*DEFNU> +[hot, warm, ok] = canlab_get_dpsp_hot_warm(); +tc.assumeTrue(ok, 'DPSP sample data not on path'); + +n_hot = size(hot.dat, 2); +n_warm = size(warm.dat, 2); + +rng(0); +p = size(hot.dat, 1); +keep_vox = randsample(p, min(5000, p)); +X = double([hot.dat(keep_vox, :) warm.dat(keep_vox, :)])'; +Y = [ones(n_hot, 1); -ones(n_warm, 1)]; +id = [(1:n_hot)'; (1:n_warm)']; % each subject contributes both conditions + +tc.TestData.X = X; +tc.TestData.Y = Y; +tc.TestData.id = id; +tc.TestData.pm = xval_SVM(X, Y, id, 'nooptimize', 'norepeats', ... + 'nobootstrap', 'noverbose', 'noplot'); +end + + +function test_class_and_state(tc) +pm = tc.TestData.pm; +tc.verifyClass(pm, 'predictive_model'); +tc.verifyTrue(pm.is_fitted, 'is_fitted should be true'); +tc.verifyTrue(pm.is_classifier, 'binary Y should give is_classifier true'); +end + + +function test_canonical_path_fields_populated(tc) +pm = tc.TestData.pm; +tc.verifyNotEmpty(pm.Y); +tc.verifyNotEmpty(pm.id); +tc.verifyNotEmpty(pm.fitted_values.yfit); +tc.verifyNotEmpty(pm.fitted_values.dist_from_hyperplane_xval); +tc.verifyNotEmpty(pm.weights.w); +tc.verifyNotEmpty(pm.error_metrics.crossval_accuracy.value); +tc.verifyNotEmpty(pm.error_metrics.d_singleinterval.value); +tc.verifyNotEmpty(pm.ml_model); +tc.verifyNotEmpty(pm.cv_partition.trIdx); +tc.verifyNotEmpty(pm.cv_partition.teIdx); +tc.verifyNotEmpty(pm.cv_partition.nfolds); +end + + +function test_shapes_and_ranges(tc) +pm = tc.TestData.pm; +n = numel(tc.TestData.Y); +nfeat = size(tc.TestData.X, 2); + +tc.verifyNumElements(pm.fitted_values.yfit, n); +tc.verifyNumElements(pm.fitted_values.dist_from_hyperplane_xval, n); +tc.verifyEqual(size(pm.weights.w, 1), nfeat, ... + 'weights.w should have one row per feature'); + +cv_acc = pm.error_metrics.crossval_accuracy.value; +tc.verifyGreaterThanOrEqual(cv_acc, 0); +tc.verifyLessThanOrEqual(cv_acc, 100); + +% DPSP is paired, so within-person scoring should be populated. +tc.verifyFalse(isnan(pm.error_metrics.crossval_accuracy_within.value)); +tc.verifyFalse(isnan(pm.error_metrics.d_within.value)); +tc.verifyTrue(pm.diagnostics.mult_obs_within_person, ... + 'should detect multiple observations per id'); +end + + +function test_fit_type_and_omitted_markers(tc) +pm = tc.TestData.pm; +tc.verifyEqual(pm.fit_type, 'crossval'); +tc.verifyClass(pm.omitted_cases, 'logical'); +tc.verifyClass(pm.omitted_features, 'logical'); +tc.verifyNumElements(pm.omitted_cases, numel(tc.TestData.Y)); +tc.verifyNumElements(pm.omitted_features, size(tc.TestData.X, 2)); +end + + +function test_validate_object_accepts(tc) +% validate_object throws on an invalid object; reaching the end is a pass. +tc.TestData.pm.validate_object('noverbose'); +end + + +function test_clone_clears_fitted_state(tc) +pm2 = clone(tc.TestData.pm); +tc.verifyFalse(pm2.is_fitted, 'clone should clear fitted state'); +tc.verifyEqual(pm2.modeloptions, tc.TestData.pm.modeloptions, ... + 'clone should preserve modeloptions'); +end diff --git a/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_SVR.m b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_SVR.m new file mode 100644 index 00000000..c5dafebb --- /dev/null +++ b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_SVR.m @@ -0,0 +1,72 @@ +function tests = canlab_test_xval_SVR +%CANLAB_TEST_XVAL_SVR Cross-validated SVR regression via xval_SVR. +% +% Converted from Unit_tests/xval_SVR_unit_test.m. Predicts a synthetic +% continuous outcome from the DPSP Hot-Warm contrast maps via cross-validated +% linear SVR and checks the returned @predictive_model object. Model fit once +% in setupOnce; skipped if the DPSP sample data is not on the path. + +tests = functiontests(localfunctions); +end + + +function setupOnce(tc) %#ok<*DEFNU> +[hot, warm, ok] = canlab_get_dpsp_hot_warm(); +tc.assumeTrue(ok, 'DPSP sample data not on path'); + +hot_vs_warm = image_math(hot, warm, 'minus'); + +rng(0); +[p, n] = size(hot_vs_warm.dat); +keep_vox = randsample(p, min(3000, p)); +X = double(hot_vs_warm.dat(keep_vox, :))'; + +% Synthetic outcome predictable from a sparse subset of features. +b_true = zeros(size(X, 2), 1); +b_true(1:30) = randn(30, 1); +Y = X * b_true + 0.5 * std(X * b_true) * randn(n, 1); +id = (1:n)'; + +tc.TestData.X = X; +tc.TestData.Y = Y; +tc.TestData.pm = xval_SVR(X, Y, id, 'nooptimize', 'norepeats', ... + 'nobootstrap', 'noverbose', 'noplot'); +end + + +function test_class_and_state(tc) +pm = tc.TestData.pm; +tc.verifyClass(pm, 'predictive_model'); +tc.verifyTrue(pm.is_fitted, 'is_fitted should be true'); +tc.verifyTrue(pm.is_regressor, 'continuous Y should give is_regressor true'); +end + + +function test_canonical_path_fields_populated(tc) +pm = tc.TestData.pm; +tc.verifyNotEmpty(pm.Y); +tc.verifyNotEmpty(pm.fitted_values.yfit); +tc.verifyNotEmpty(pm.weights.w); +tc.verifyNotEmpty(pm.error_metrics.prediction_outcome_r.value); +tc.verifyNotEmpty(pm.ml_model); +end + + +function test_weight_shape(tc) +pm = tc.TestData.pm; +tc.verifyEqual(size(pm.weights.w, 1), size(tc.TestData.X, 2), ... + 'weights.w should have one row per feature'); +end + + +function test_fit_type_and_omitted_markers(tc) +pm = tc.TestData.pm; +tc.verifyEqual(pm.fit_type, 'crossval'); +tc.verifyClass(pm.omitted_cases, 'logical'); +tc.verifyClass(pm.omitted_features, 'logical'); +end + + +function test_validate_object_accepts(tc) +tc.TestData.pm.validate_object('noverbose'); +end diff --git a/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_discriminant_classifier.m b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_discriminant_classifier.m new file mode 100644 index 00000000..2fec72d4 --- /dev/null +++ b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_discriminant_classifier.m @@ -0,0 +1,66 @@ +function tests = canlab_test_xval_discriminant_classifier +%CANLAB_TEST_XVAL_DISCRIMINANT_CLASSIFIER Cross-validated LDA classification. +% +% Converted from Unit_tests/xval_discriminant_classifier_unit_test.m. Drives +% cross-validated LDA (fitcdiscr) on the DPSP Hot vs Warm single-subject maps +% (100 random voxels, so n > p) and checks the returned @predictive_model +% object. Model fit once in setupOnce; skipped if DPSP data is not on the path. + +tests = functiontests(localfunctions); +end + + +function setupOnce(tc) %#ok<*DEFNU> +[hot, warm, ok] = canlab_get_dpsp_hot_warm(); +tc.assumeTrue(ok, 'DPSP sample data not on path'); + +rng(0); +p = size(hot.dat, 1); +keep_vox = randsample(p, 100); % LDA needs n > p +X = double([hot.dat(keep_vox, :) warm.dat(keep_vox, :)])'; +labels = int32([ones(size(hot.dat, 2), 1); 2 * ones(size(warm.dat, 2), 1)]); + +tc.TestData.pm = xval_discriminant_classifier(X, labels, 'nFolds', 5, ... + 'verbose', false, 'doplot', false); +end + + +function test_class_and_state(tc) +pm = tc.TestData.pm; +tc.verifyClass(pm, 'predictive_model'); +tc.verifyTrue(pm.is_fitted, 'is_fitted should be true'); +end + + +function test_canonical_path_fields_populated(tc) +pm = tc.TestData.pm; +tc.verifyNotEmpty(pm.Y); +tc.verifyNotEmpty(pm.fitted_values.yfit); +tc.verifyNotEmpty(pm.fitted_values.predictions); +tc.verifyNotEmpty(pm.fitted_values.Y_per_fold); +tc.verifyNotEmpty(pm.error_metrics.accuracy.value); +tc.verifyNotEmpty(pm.error_metrics.overallAccuracy.value); +tc.verifyNotEmpty(pm.cv_partition.trIdx); +tc.verifyNotEmpty(pm.cv_partition.teIdx); +tc.verifyNotEmpty(pm.cv_partition.nfolds); +end + + +function test_fold_models_match_nfolds(tc) +pm = tc.TestData.pm; +tc.verifyNumElements(pm.fold_models, pm.cv_partition.nfolds, ... + 'fold_models length should equal nfolds'); +end + + +function test_fit_type_and_omitted_markers(tc) +pm = tc.TestData.pm; +tc.verifyEqual(pm.fit_type, 'crossval'); +tc.verifyClass(pm.omitted_cases, 'logical'); +tc.verifyClass(pm.omitted_features, 'logical'); +end + + +function test_validate_object_accepts(tc) +tc.TestData.pm.validate_object('noverbose'); +end diff --git a/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_regression_multisubject.m b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_regression_multisubject.m new file mode 100644 index 00000000..d6bebbe3 --- /dev/null +++ b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_regression_multisubject.m @@ -0,0 +1,74 @@ +function tests = canlab_test_xval_regression_multisubject +%CANLAB_TEST_XVAL_REGRESSION_MULTISUBJECT OLS+PCA multisubject regression. +% +% Converted from Unit_tests/xval_regression_multisubject_unit_test.m. Runs +% xval_regression_multisubject ('ols' + 'pca') on the DPSP Hot-Warm contrast +% maps against a synthetic continuous outcome and checks the returned +% @predictive_model object. Model fit once in setupOnce; skipped if the DPSP +% sample data is not on the path. + +tests = functiontests(localfunctions); +end + + +function setupOnce(tc) %#ok<*DEFNU> +[hot, warm, ok] = canlab_get_dpsp_hot_warm(); +tc.assumeTrue(ok, 'DPSP sample data not on path'); + +hot_vs_warm = image_math(hot, warm, 'minus'); + +rng(0); +[p, n] = size(hot_vs_warm.dat); +keep_vox = randsample(p, min(5000, p)); +X = double(hot_vs_warm.dat(keep_vox, :))'; % n x v + +b_true = zeros(size(X, 2), 1); +b_true(1:50) = randn(50, 1); +Y = X * b_true + 0.5 * std(X * b_true) * randn(n, 1); + +tc.TestData.X = X; +tc.TestData.Y = Y; +tc.TestData.n = n; +tc.TestData.pm = xval_regression_multisubject('ols', {Y}, {X}, ... + 'pca', 'ndims', 10, 'holdout_method', 'balanced4', 'noverbose'); +end + + +function test_class_and_state(tc) +pm = tc.TestData.pm; +tc.verifyClass(pm, 'predictive_model'); +tc.verifyTrue(pm.is_fitted, 'is_fitted should be true'); +end + + +function test_canonical_path_fields_populated(tc) +pm = tc.TestData.pm; +tc.verifyNotEmpty(pm.fitted_values.subjfit); +tc.verifyNotEmpty(pm.weights.subjbetas); +tc.verifyNotEmpty(pm.weights.mean_vox_weights); +tc.verifyNotEmpty(pm.error_metrics.pred_err.value); +tc.verifyNotEmpty(pm.error_metrics.pred_err_null.value); +tc.verifyNotEmpty(pm.error_metrics.var_reduction.value); +tc.verifyNotEmpty(pm.error_metrics.r_each_subject.value); +tc.verifyNotEmpty(pm.Y); +tc.verifyNotEmpty(pm.inputParameters); +end + + +function test_shapes_and_variance_explained(tc) +pm = tc.TestData.pm; +tc.verifyNumElements(pm.fitted_values.subjfit, 1, ... + 'subjfit cell length should equal number of datasets'); +tc.verifyNumElements(pm.fitted_values.subjfit{1}, tc.TestData.n); +tc.verifyEqual(size(pm.weights.mean_vox_weights, 1), size(tc.TestData.X, 2), ... + 'mean_vox_weights should have one row per voxel'); + +r2 = pm.error_metrics.r_squared.value; +tc.verifyNotEmpty(r2); +tc.verifyGreaterThan(r2(1), 0, 'OLS+PCA should explain non-trivial variance'); +end + + +function test_validate_object_accepts(tc) +tc.TestData.pm.validate_object('noverbose'); +end diff --git a/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_regression_multisubject_featureselect.m b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_regression_multisubject_featureselect.m new file mode 100644 index 00000000..08311c82 --- /dev/null +++ b/CanlabCore/Unit_tests/predictive_model/canlab_test_xval_regression_multisubject_featureselect.m @@ -0,0 +1,75 @@ +function tests = canlab_test_xval_regression_multisubject_featureselect +%CANLAB_TEST_XVAL_REGRESSION_MULTISUBJECT_FEATURESELECT Feature-select regression. +% +% Converted from Unit_tests/xval_regression_multisubject_featureselect_unit_test.m. +% Runs xval_regression_multisubject_featureselect ('ols') on the DPSP Hot-Warm +% contrast maps against a synthetic continuous outcome. Beyond the usual +% predictive_model checks, it pins the per-fold weight padding: weights.w_perfold +% must span the FULL feature space, not the selected-feature count (the bug this +% test originally guarded). Skipped if the DPSP sample data is not on the path. + +tests = functiontests(localfunctions); +end + + +function setupOnce(tc) %#ok<*DEFNU> +[hot, warm, ok] = canlab_get_dpsp_hot_warm(); +tc.assumeTrue(ok, 'DPSP sample data not on path'); + +hvw = image_math(hot, warm, 'minus'); + +rng(0); +[p, n] = size(hvw.dat); +keep_vox = randsample(p, min(2000, p)); +X = double(hvw.dat(keep_vox, :))'; + +b_true = zeros(size(X, 2), 1); +b_true(1:30) = randn(30, 1); +Y = X * b_true + 0.5 * std(X * b_true) * randn(n, 1); + +tc.TestData.X = X; +tc.TestData.Y = Y; +tc.TestData.pm = xval_regression_multisubject_featureselect('ols', {Y}, {X}, ... + 'holdout_method', 'balanced4', 'noverbose'); +end + + +function test_class_and_state(tc) +pm = tc.TestData.pm; +tc.verifyClass(pm, 'predictive_model'); +tc.verifyTrue(pm.is_fitted, 'is_fitted should be true'); +end + + +function test_canonical_path_fields_populated(tc) +% mean_vox_weights is only populated when pcsquash is enabled; this no-PCA +% test does not exercise that path, so it is not checked here. +pm = tc.TestData.pm; +tc.verifyNotEmpty(pm.fitted_values.subjfit); +tc.verifyNotEmpty(pm.weights.w_perfold); +tc.verifyNotEmpty(pm.error_metrics.pred_err.value); +tc.verifyNotEmpty(pm.error_metrics.pred_err_null.value); +tc.verifyNotEmpty(pm.error_metrics.var_reduction.value); +tc.verifyNotEmpty(pm.Y); +end + + +function test_w_perfold_spans_full_feature_space(tc) +% Regression guard: w_perfold must have one row per ORIGINAL feature, not per +% selected feature. +pm = tc.TestData.pm; +tc.verifyEqual(size(pm.weights.w_perfold, 1), size(tc.TestData.X, 2), ... + 'weights.w_perfold should span the full feature count'); +end + + +function test_variance_explained(tc) +r2 = tc.TestData.pm.error_metrics.r_squared.value; +tc.verifyNotEmpty(r2); +tc.verifyGreaterThan(r2(1), 0, 'expected r_squared > 0'); +end + + +function test_validate_object_accepts(tc) +tc.TestData.pm.validate_object('noverbose'); +end diff --git a/CanlabCore/Unit_tests/xval_SVM_unit_test.m b/CanlabCore/Unit_tests/xval_SVM_unit_test.m deleted file mode 100644 index 535e6be4..00000000 --- a/CanlabCore/Unit_tests/xval_SVM_unit_test.m +++ /dev/null @@ -1,104 +0,0 @@ -function xval_SVM_unit_test() -% xval_SVM_unit_test Smoke test of xval_SVM on the DPSP Hot vs. Warm task. -% -% Reproduces the binary-classification setup from -% docs/markdown_tutorials/multivariate_classification_with_SVM/ -% multivariate_decoding_part1_classification_with_SVM.mlx -% and asserts that the wrapper returns a populated @predictive_model -% object whose categorised sub-structs agree with the legacy flat aliases. -% -% Data: Sample_datasets/DPSP_pain_rejection_participant_maps -% Single-subject Hot and Warm condition maps. -% -% Run: cd to this directory, then `xval_SVM_unit_test`. - - fprintf('=== xval_SVM_unit_test ===\n'); - - % --- Load DPSP Hot and Warm --- - canlabcore_dir = fileparts(fileparts(which('fmri_data'))); - sample_dir = fullfile(canlabcore_dir, 'Sample_datasets', ... - 'DPSP_pain_rejection_participant_maps'); - H = load(fullfile(sample_dir, 'DPSP_single_subject_images_hot.mat')); - W = load(fullfile(sample_dir, 'DPSP_single_subject_images_warm.mat')); - hot = H.single_subject_images_hot; - warm = W.single_subject_images_warm; - - n_hot = size(hot.dat, 2); - n_warm = size(warm.dat, 2); - - % Stack into one (subjects+subjects) x voxels matrix - rng(0); - p = size(hot.dat, 1); - keep_vox = randsample(p, min(5000, p)); - X = double([hot.dat(keep_vox, :) warm.dat(keep_vox, :)])'; - Y = [ones(n_hot, 1); -ones(n_warm, 1)]; - id = [(1:n_hot)'; (1:n_warm)']; % each subject contributes both conditions - - % --- Run xval_SVM (fast: no optimize / no repeats / no bootstrap) --- - pmodel_obj = xval_SVM(X, Y, id, 'nooptimize', 'norepeats', 'nobootstrap', ... - 'noverbose', 'noplot'); - - % --- Class invariants --- - assert(isa(pmodel_obj, 'predictive_model'), ... - 'Expected output of class predictive_model, got %s', class(pmodel_obj)); - assert(pmodel_obj.is_fitted, 'Returned object reports is_fitted = false'); - assert(pmodel_obj.is_classifier, 'Y is binary, expected is_classifier true'); - fprintf(' class = %s, is_fitted = %d, is_classifier = %d\n', ... - class(pmodel_obj), pmodel_obj.is_fitted, pmodel_obj.is_classifier); - - % --- Canonical-path access (legacy flat aliases removed) --- - assert(~isempty(pmodel_obj.Y), 'Y empty'); - assert(~isempty(pmodel_obj.id), 'id empty'); - assert(~isempty(pmodel_obj.fitted_values.yfit), 'yfit empty'); - assert(~isempty(pmodel_obj.fitted_values.dist_from_hyperplane_xval), 'dist_from_hyperplane_xval empty'); - % class_probability_xval is xval_SVM-legacy (per-fold fitPosterior). - % The new pipeline stores raw scores in fitted_values.scores; to get - % calibrated probabilities call calibrate(pm, X, Y) + predict_proba. - assert(~isempty(pmodel_obj.weights.w), 'weights.w empty'); - assert(~isempty(pmodel_obj.error_metrics.crossval_accuracy.value), 'crossval_accuracy.value empty'); - assert(~isempty(pmodel_obj.error_metrics.d_singleinterval.value), 'd_singleinterval.value empty'); - assert(~isempty(pmodel_obj.ml_model), 'ml_model empty'); - assert(~isempty(pmodel_obj.cv_partition.trIdx), 'trIdx empty'); - assert(~isempty(pmodel_obj.cv_partition.teIdx), 'teIdx empty'); - assert(~isempty(pmodel_obj.cv_partition.nfolds), 'nfolds empty'); - fprintf(' Canonical-path access OK\n'); - - % --- Shape / sanity --- - n = numel(Y); - assert(numel(pmodel_obj.fitted_values.yfit) == n, 'yfit length mismatch'); - assert(numel(pmodel_obj.fitted_values.dist_from_hyperplane_xval) == n, 'dist_from_hyperplane_xval length mismatch'); - assert(size(pmodel_obj.weights.w, 1) == size(X, 2), 'weights.w length should match number of features'); - cv_acc = pmodel_obj.error_metrics.crossval_accuracy.value; - assert(cv_acc >= 0 && cv_acc <= 100, 'crossval_accuracy out of [0,100]'); - % Within-person scoring fields populated by crossval (DPSP is paired): - assert(~isnan(pmodel_obj.error_metrics.crossval_accuracy_within.value), 'crossval_accuracy_within populated'); - assert(~isnan(pmodel_obj.error_metrics.d_within.value), 'd_within populated'); - assert(pmodel_obj.diagnostics.mult_obs_within_person == true, 'Should detect multiple obs per id'); - fprintf(' Shape/sanity OK: cv_acc = %.1f%%, d_single = %.2f, cv_acc_within = %.1f%%, d_within = %.2f\n', ... - cv_acc, ... - pmodel_obj.error_metrics.d_singleinterval.value, ... - pmodel_obj.error_metrics.crossval_accuracy_within.value, ... - pmodel_obj.error_metrics.d_within.value); - - % --- fit_type + omitted markers (Phase B) --- - assert(strcmp(pmodel_obj.fit_type, 'crossval'), ... - 'fit_type should be ''crossval'', got ''%s''', pmodel_obj.fit_type); - assert(islogical(pmodel_obj.omitted_cases), 'omitted_cases must be logical'); - assert(islogical(pmodel_obj.omitted_features), 'omitted_features must be logical'); - assert(numel(pmodel_obj.omitted_cases) == numel(Y), 'omitted_cases length should match original Y'); - assert(numel(pmodel_obj.omitted_features) == size(X, 2), 'omitted_features length should match original feature count'); - fprintf(' fit_type=%s, omitted_cases=%d, omitted_features=%d\n', ... - pmodel_obj.fit_type, sum(pmodel_obj.omitted_cases), sum(pmodel_obj.omitted_features)); - - % --- validate_object accepts the returned object --- - pmodel_obj.validate_object('noverbose'); - fprintf(' validate_object OK\n'); - - % --- Clone preserves hyperparameters, clears fitted state --- - pmodel_obj2 = clone(pmodel_obj); - assert(~pmodel_obj2.is_fitted, 'clone did not clear fitted state'); - assert(isequal(pmodel_obj2.modeloptions, pmodel_obj.modeloptions), 'clone lost modeloptions'); - fprintf(' clone() OK\n'); - - fprintf('xval_SVM_unit_test: PASS\n'); -end diff --git a/CanlabCore/Unit_tests/xval_SVR_unit_test.m b/CanlabCore/Unit_tests/xval_SVR_unit_test.m deleted file mode 100644 index 7e7c6c81..00000000 --- a/CanlabCore/Unit_tests/xval_SVR_unit_test.m +++ /dev/null @@ -1,62 +0,0 @@ -function xval_SVR_unit_test() -% xval_SVR_unit_test Smoke test of xval_SVR on the DPSP Hot-Warm contrast. -% -% Predicts a synthetic continuous outcome from Hot-Warm contrast maps -% (one per subject) via cross-validated linear SVR. Asserts that the -% wrapper returns a @predictive_model object with consistent -% categorised <-> legacy alias values. -% -% Run: cd to this directory, then `xval_SVR_unit_test`. - - fprintf('=== xval_SVR_unit_test ===\n'); - - canlabcore_dir = fileparts(fileparts(which('fmri_data'))); - sample_dir = fullfile(canlabcore_dir, 'Sample_datasets', ... - 'DPSP_pain_rejection_participant_maps'); - H = load(fullfile(sample_dir, 'DPSP_single_subject_images_hot.mat')); - W = load(fullfile(sample_dir, 'DPSP_single_subject_images_warm.mat')); - hot_vs_warm = image_math(H.single_subject_images_hot, ... - W.single_subject_images_warm, 'minus'); - - rng(0); - [p, n] = size(hot_vs_warm.dat); - keep_vox = randsample(p, min(3000, p)); - X = double(hot_vs_warm.dat(keep_vox, :))'; - % Synthetic predictable continuous outcome. - b_true = zeros(size(X, 2), 1); - b_true(1:30) = randn(30, 1); - Y = X * b_true + 0.5 * std(X * b_true) * randn(n, 1); - id = (1:n)'; - - pmodel_obj = xval_SVR(X, Y, id, 'nooptimize', 'norepeats', ... - 'nobootstrap', 'noverbose', 'noplot'); - - assert(isa(pmodel_obj, 'predictive_model'), ... - 'Expected predictive_model, got %s', class(pmodel_obj)); - assert(pmodel_obj.is_fitted, 'is_fitted false'); - assert(pmodel_obj.is_regressor, 'Y is continuous, expected is_regressor true'); - fprintf(' class = %s, is_fitted = %d, is_regressor = %d\n', ... - class(pmodel_obj), pmodel_obj.is_fitted, pmodel_obj.is_regressor); - - % Canonical-path access (legacy flat aliases removed). - assert(~isempty(pmodel_obj.Y), 'Y empty'); - assert(~isempty(pmodel_obj.fitted_values.yfit), 'yfit empty'); - assert(~isempty(pmodel_obj.weights.w), 'weights.w empty'); - assert(~isempty(pmodel_obj.error_metrics.prediction_outcome_r.value), 'prediction_outcome_r empty'); - assert(~isempty(pmodel_obj.ml_model), 'ml_model empty'); - fprintf(' Canonical-path access OK\n'); - - fprintf(' cv: r = %.3f, d = %.2f\n', ... - pmodel_obj.error_metrics.prediction_outcome_r.value, ... - pmodel_obj.error_metrics.d_singleinterval.value); - - % --- fit_type + omitted markers (Phase B) --- - assert(strcmp(pmodel_obj.fit_type, 'crossval'), 'fit_type should be crossval'); - assert(islogical(pmodel_obj.omitted_cases), 'omitted_cases must be logical'); - assert(islogical(pmodel_obj.omitted_features), 'omitted_features must be logical'); - fprintf(' fit_type=%s, omitted_cases=%d, omitted_features=%d\n', ... - pmodel_obj.fit_type, sum(pmodel_obj.omitted_cases), sum(pmodel_obj.omitted_features)); - - pmodel_obj.validate_object('noverbose'); - fprintf('xval_SVR_unit_test: PASS\n'); -end diff --git a/CanlabCore/Unit_tests/xval_discriminant_classifier_unit_test.m b/CanlabCore/Unit_tests/xval_discriminant_classifier_unit_test.m deleted file mode 100644 index 2a8810e2..00000000 --- a/CanlabCore/Unit_tests/xval_discriminant_classifier_unit_test.m +++ /dev/null @@ -1,62 +0,0 @@ -function xval_discriminant_classifier_unit_test() -% xval_discriminant_classifier_unit_test Smoke test on DPSP Hot vs Warm. -% -% Drives cross-validated LDA via fitcdiscr on the DPSP single-subject -% Hot vs Warm maps. Asserts @predictive_model return and -% categorised <-> legacy alias agreement. - - fprintf('=== xval_discriminant_classifier_unit_test ===\n'); - - canlabcore_dir = fileparts(fileparts(which('fmri_data'))); - sample_dir = fullfile(canlabcore_dir, 'Sample_datasets', ... - 'DPSP_pain_rejection_participant_maps'); - H = load(fullfile(sample_dir, 'DPSP_single_subject_images_hot.mat')); - W = load(fullfile(sample_dir, 'DPSP_single_subject_images_warm.mat')); - hot = H.single_subject_images_hot; - warm = W.single_subject_images_warm; - - % Stratify: 100 random voxels (LDA needs n > p) - rng(0); - p = size(hot.dat, 1); - keep_vox = randsample(p, 100); - X = double([hot.dat(keep_vox, :) warm.dat(keep_vox, :)])'; - labels = int32([ones(size(hot.dat, 2), 1); ... - 2*ones(size(warm.dat, 2), 1)]); - - pmodel_obj = xval_discriminant_classifier(X, labels, 'nFolds', 5, ... - 'verbose', false, 'doplot', false); - - assert(isa(pmodel_obj, 'predictive_model'), ... - 'Expected predictive_model, got %s', class(pmodel_obj)); - assert(pmodel_obj.is_fitted, 'is_fitted false'); - fprintf(' class = %s, is_fitted = %d\n', class(pmodel_obj), pmodel_obj.is_fitted); - - % Categorised <-> legacy alias agreement - % Canonical-path access (legacy flat aliases removed). - assert(~isempty(pmodel_obj.Y), 'Y empty'); - assert(~isempty(pmodel_obj.fitted_values.yfit), 'yfit empty'); - assert(~isempty(pmodel_obj.fitted_values.predictions), 'predictions empty'); - assert(~isempty(pmodel_obj.fitted_values.Y_per_fold), 'Y_per_fold empty'); - assert(~isempty(pmodel_obj.error_metrics.accuracy.value), 'accuracy.value empty'); - assert(~isempty(pmodel_obj.error_metrics.overallAccuracy.value), 'overallAccuracy.value empty'); - assert(~isempty(pmodel_obj.cv_partition.trIdx), 'trIdx empty'); - assert(~isempty(pmodel_obj.cv_partition.teIdx), 'teIdx empty'); - assert(~isempty(pmodel_obj.cv_partition.nfolds), 'nfolds empty'); - assert(numel(pmodel_obj.fold_models) == pmodel_obj.cv_partition.nfolds, ... - 'fold_models length should equal nfolds'); - fprintf(' Canonical-path access OK\n'); - - fprintf(' overall_acc = %.1f%%, mean_fold_acc = %.1f%%\n', ... - pmodel_obj.error_metrics.overallAccuracy.value, ... - mean(pmodel_obj.error_metrics.accuracy.value)); - - % --- fit_type + omitted markers (Phase B) --- - assert(strcmp(pmodel_obj.fit_type, 'crossval'), 'fit_type should be crossval'); - assert(islogical(pmodel_obj.omitted_cases), 'omitted_cases must be logical'); - assert(islogical(pmodel_obj.omitted_features), 'omitted_features must be logical'); - fprintf(' fit_type=%s, omitted_cases=%d, omitted_features=%d\n', ... - pmodel_obj.fit_type, sum(pmodel_obj.omitted_cases), sum(pmodel_obj.omitted_features)); - - pmodel_obj.validate_object('noverbose'); - fprintf('xval_discriminant_classifier_unit_test: PASS\n'); -end diff --git a/CanlabCore/Unit_tests/xval_regression_multisubject_featureselect_unit_test.m b/CanlabCore/Unit_tests/xval_regression_multisubject_featureselect_unit_test.m deleted file mode 100644 index b0f45baf..00000000 --- a/CanlabCore/Unit_tests/xval_regression_multisubject_featureselect_unit_test.m +++ /dev/null @@ -1,59 +0,0 @@ -function xval_regression_multisubject_featureselect_unit_test() -% xval_regression_multisubject_featureselect_unit_test DPSP smoke test. -% -% Verifies that xval_regression_multisubject_featureselect returns a -% populated @predictive_model object after the per-fold vox_weights -% padding fix. Predicts a synthetic continuous outcome from -% Hot - Warm DPSP contrast maps. - - fprintf('=== xval_regression_multisubject_featureselect_unit_test ===\n'); - - canlabcore_dir = fileparts(fileparts(which('fmri_data'))); - sd = fullfile(canlabcore_dir, 'Sample_datasets', 'DPSP_pain_rejection_participant_maps'); - H = load(fullfile(sd, 'DPSP_single_subject_images_hot.mat')); - W = load(fullfile(sd, 'DPSP_single_subject_images_warm.mat')); - hvw = image_math(H.single_subject_images_hot, W.single_subject_images_warm, 'minus'); - - rng(0); - [p, n] = size(hvw.dat); - keep_vox = randsample(p, min(2000, p)); - X = double(hvw.dat(keep_vox, :))'; - b_true = zeros(size(X, 2), 1); - b_true(1:30) = randn(30, 1); - Y = X * b_true + 0.5 * std(X * b_true) * randn(n, 1); - - pm = xval_regression_multisubject_featureselect('ols', {Y}, {X}, ... - 'holdout_method', 'balanced4', 'noverbose'); - - assert(isa(pm, 'predictive_model'), 'class mismatch: %s', class(pm)); - assert(pm.is_fitted, 'is_fitted=false'); - fprintf(' class = %s, is_fitted = %d\n', class(pm), pm.is_fitted); - - % Canonical-path access (legacy flat aliases removed). - % mean_vox_weights is only populated when pcsquash is enabled; this no-PCA test doesn't exercise that path. - assert(~isempty(pm.fitted_values.subjfit), 'subjfit empty'); - assert(~isempty(pm.weights.w_perfold), 'weights.w_perfold empty (was vox_weights)'); - assert(~isempty(pm.error_metrics.pred_err.value), 'pred_err empty'); - assert(~isempty(pm.error_metrics.pred_err_null.value), 'pred_err_null empty'); - assert(~isempty(pm.error_metrics.var_reduction.value), 'var_reduction empty'); - assert(~isempty(pm.Y), 'Y empty'); - fprintf(' Canonical-path access OK\n'); - - % Verify weights.w_perfold matches the full feature space (the bug we fixed - % was that this was the *selected*-features count instead). - assert(size(pm.weights.w_perfold, 1) == size(X, 2), ... - 'weights.w_perfold should span full feature count (%d), got %d', ... - size(X, 2), size(pm.weights.w_perfold, 1)); - fprintf(' w_perfold shape OK: %d x %d (features x folds)\n', ... - size(pm.weights.w_perfold, 1), size(pm.weights.w_perfold, 2)); - - r2 = pm.error_metrics.r_squared.value; - assert(~isempty(r2) && r2(1) > 0, 'expected r_squared > 0; got %.3f', r2(1)); - fprintf(' r_squared = %.3f, pred_err = %.3f, pred_err_null = %.3f\n', ... - r2(1), ... - pm.error_metrics.pred_err.value(1), ... - pm.error_metrics.pred_err_null.value(1)); - - pm.validate_object('noverbose'); - fprintf('xval_regression_multisubject_featureselect_unit_test: PASS\n'); -end diff --git a/CanlabCore/Unit_tests/xval_regression_multisubject_unit_test.m b/CanlabCore/Unit_tests/xval_regression_multisubject_unit_test.m deleted file mode 100644 index 0696e886..00000000 --- a/CanlabCore/Unit_tests/xval_regression_multisubject_unit_test.m +++ /dev/null @@ -1,75 +0,0 @@ -function xval_regression_multisubject_unit_test() -% xval_regression_multisubject_unit_test Smoke test using DPSP sample data. -% -% Verifies that xval_regression_multisubject returns a populated -% @predictive_model object whose categorised sub-structs and legacy -% flat aliases agree, on a real (small) brain dataset. -% -% Data: Sample_datasets/DPSP_pain_rejection_participant_maps -% Single-subject Hot - Warm contrast maps, regressed against a -% synthetic continuous outcome to exercise the regression code path. -% -% Run: cd to this directory, then `xval_regression_multisubject_unit_test`. - - fprintf('=== xval_regression_multisubject_unit_test ===\n'); - - % --- Load DPSP Hot - Warm contrast across subjects --- - canlabcore_dir = fileparts(fileparts(which('fmri_data'))); - sample_dir = fullfile(canlabcore_dir, 'Sample_datasets', ... - 'DPSP_pain_rejection_participant_maps'); - H = load(fullfile(sample_dir, 'DPSP_single_subject_images_hot.mat')); - W = load(fullfile(sample_dir, 'DPSP_single_subject_images_warm.mat')); - hot_obj = H.single_subject_images_hot; - warm_obj = W.single_subject_images_warm; - hot_vs_warm = image_math(hot_obj, warm_obj, 'minus'); - - % Keep the test light: ~5000 random voxels, all subjects. - rng(0); - [p, n] = size(hot_vs_warm.dat); - keep_vox = randsample(p, min(5000, p)); - X = double(hot_vs_warm.dat(keep_vox, :))'; % n x v - - % Synthetic continuous outcome predictable from X with sparse weights. - b_true = zeros(size(X, 2), 1); - b_true(1:50) = randn(50, 1); - Y = X * b_true + 0.5 * std(X * b_true) * randn(n, 1); - - % --- Run xval_regression_multisubject with OLS + PCA on a single dataset --- - pmodel_obj = xval_regression_multisubject('ols', {Y}, {X}, ... - 'pca', 'ndims', 10, 'holdout_method', 'balanced4', 'noverbose'); - - % --- Class invariants --- - assert(isa(pmodel_obj, 'predictive_model'), ... - 'Expected output of class predictive_model, got %s', class(pmodel_obj)); - assert(pmodel_obj.is_fitted, 'Returned object reports is_fitted = false'); - fprintf(' class = %s, is_fitted = %d\n', class(pmodel_obj), pmodel_obj.is_fitted); - - % --- Canonical-path access (legacy flat aliases removed) --- - assert(~isempty(pmodel_obj.fitted_values.subjfit), 'subjfit empty'); - assert(~isempty(pmodel_obj.weights.subjbetas), 'subjbetas empty'); - assert(~isempty(pmodel_obj.weights.mean_vox_weights), 'mean_vox_weights empty'); - assert(~isempty(pmodel_obj.error_metrics.pred_err.value), 'pred_err empty'); - assert(~isempty(pmodel_obj.error_metrics.pred_err_null.value), 'pred_err_null empty'); - assert(~isempty(pmodel_obj.error_metrics.var_reduction.value), 'var_reduction empty'); - assert(~isempty(pmodel_obj.error_metrics.r_each_subject.value), 'r_each_subject empty'); - assert(~isempty(pmodel_obj.Y), 'Y empty'); - assert(~isempty(pmodel_obj.inputParameters), 'inputParameters empty'); - fprintf(' Canonical-path access OK\n'); - - % --- Shape / sanity --- - assert(numel(pmodel_obj.fitted_values.subjfit) == 1, 'subjfit cell length should equal num datasets'); - assert(numel(pmodel_obj.fitted_values.subjfit{1}) == n, 'subjfit{1} length should equal n'); - assert(size(pmodel_obj.weights.mean_vox_weights, 1) == size(X, 2), 'mean_vox_weights should have one row per voxel'); - r2 = pmodel_obj.error_metrics.r_squared.value; - assert(~isempty(r2) && r2(1) > 0, 'OLS+PCA should explain non-trivial variance'); - fprintf(' Shape/sanity OK: r_squared = %.3f, pred_err = %.3f, pred_err_null = %.3f\n', ... - r2(1), ... - pmodel_obj.error_metrics.pred_err.value(1), ... - pmodel_obj.error_metrics.pred_err_null.value(1)); - - % --- validate_object accepts the returned object --- - pmodel_obj.validate_object('noverbose'); - fprintf(' validate_object OK\n'); - - fprintf('xval_regression_multisubject_unit_test: PASS\n'); -end From a94dca7eb3babe5a14a9b986be1aad880fc6ede9 Mon Sep 17 00:00:00 2001 From: Tor Wager Date: Sun, 21 Jun 2026 21:55:00 -0400 Subject: [PATCH 3/6] 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) --- .../canlab_test_predictive_model_api.m | 297 ++++++++++++++++++ .../Unit_tests/predictive_model_unit_test.m | 195 ------------ 2 files changed, 297 insertions(+), 195 deletions(-) create mode 100644 CanlabCore/Unit_tests/predictive_model/canlab_test_predictive_model_api.m delete mode 100644 CanlabCore/Unit_tests/predictive_model_unit_test.m diff --git a/CanlabCore/Unit_tests/predictive_model/canlab_test_predictive_model_api.m b/CanlabCore/Unit_tests/predictive_model/canlab_test_predictive_model_api.m new file mode 100644 index 00000000..76935ef6 --- /dev/null +++ b/CanlabCore/Unit_tests/predictive_model/canlab_test_predictive_model_api.m @@ -0,0 +1,297 @@ +function tests = canlab_test_predictive_model_api +%CANLAB_TEST_PREDICTIVE_MODEL_API End-to-end test of the @predictive_model public API. +% +% Converted from Unit_tests/predictive_model_unit_test.m. Exercises the +% sklearn-style @predictive_model object surface on the DPSP Hot vs Warm +% dataset: construction, fit/predict/score, crossval (cv_splitter), bootstrap, +% weight_map_object, permutation_test, calibrate/predict_proba, +% select_features, grid_search, stability_selection, @pipeline, +% fmri_data.predict 'newapi' routing, and the regression/report/summary path. +% Visualisation methods (plot/confusionchart/montage) are smoke-tested and +% skipped on a headless runner. Complements the xval_* wrapper tests, which +% cover the legacy entry points rather than the object's own methods. +% +% The shared fit -> crossval -> bootstrap chain is computed once in setupOnce +% and cached; method-specific tests build their own small models. Small +% nboot/nperm/grid values keep this to a few minutes; it is still much slower +% than the rest of the suite. Skipped if DPSP_hotwarm is not on the path. + +tests = functiontests(localfunctions); +end + + +% ===================================================================== +% Fixtures +% ===================================================================== + +function setupOnce(tc) %#ok<*DEFNU> +tc.assumeNotEmpty(which('load_image_set'), 'load_image_set not on path'); +try + hw_obj = load_image_set('DPSP_hotwarm', 'noverbose'); +catch ME + tc.assumeFail(['DPSP_hotwarm sample not available: ' ME.message]); + return +end + +X = double(hw_obj.dat'); +Y = hw_obj.Y; +id = grp2idx(hw_obj.metadata_table.subj_id); + +tc.TestData.hw_obj = hw_obj; +tc.TestData.X = X; +tc.TestData.Y = Y; +tc.TestData.id = id; + +% Shared expensive chain, computed once: in-sample fit, cross-validation, +% and bootstrap on the cross-validated model. +tc.TestData.pm_insample = fit( ... + predictive_model('algorithm', 'svm', 'task', 'classification', 'random_state', 0), ... + X, Y, 'id', id); + +pm_cv = predictive_model('algorithm', 'svm', 'task', 'classification', ... + 'modeloptions', {'KernelFunction', 'linear'}, 'random_state', 0); +pm_cv = crossval(pm_cv, X, Y, ... + 'cv', cv_splitter.stratified_group_kfold(5), 'groups', id, ... + 'scoring', 'balanced_accuracy'); +tc.TestData.pm_cv = pm_cv; + +% Minimum-viable nboot: just enough to compute boot std / z / p and exercise +% the empirical p-floor. This is a smoke test, not real inference. +tc.TestData.pm_boot = bootstrap(pm_cv, X, Y, 'nboot', 5, 'groups', id, 'verbose', false); +end + + +function setup(tc) +tc.TestData.PrevFigVis = get(0, 'DefaultFigureVisible'); +set(0, 'DefaultFigureVisible', 'off'); +end + + +function teardown(tc) +close all force +if isfield(tc.TestData, 'PrevFigVis') + set(0, 'DefaultFigureVisible', tc.TestData.PrevFigVis); +end +end + + +% ===================================================================== +% Construction / in-sample fit / predict / score +% ===================================================================== + +function test_construction_hyperparameters(tc) +pm = predictive_model('algorithm', 'svm', 'task', 'classification', 'random_state', 0); +tc.verifyEqual(pm.algorithm, 'svm'); +tc.verifyEqual(pm.task, 'classification'); +tc.verifyFalse(pm.is_fitted, 'should start unfitted'); +tc.verifyTrue(pm.is_classifier); +end + + +function test_fit_insample(tc) +pm = tc.TestData.pm_insample; +tc.verifyTrue(pm.is_fitted); +tc.verifyEqual(pm.fit_type, 'insample'); +tc.verifyLessThanOrEqual(numel(pm.weights.w), size(tc.TestData.X, 2)); +end + + +function test_predict(tc) +[yhat, scores] = predict(tc.TestData.pm_insample, tc.TestData.X); +tc.verifyNumElements(yhat, numel(tc.TestData.Y)); +tc.verifyEqual(size(scores, 1), numel(tc.TestData.Y)); +end + + +function test_score_balanced_accuracy(tc) +pm = tc.TestData.pm_insample; +pm.scorer = cv_scorer.balanced_accuracy(); +v = score(pm, tc.TestData.X, tc.TestData.Y); +tc.verifyGreaterThanOrEqual(v, 0); +tc.verifyLessThanOrEqual(v, 1); +end + + +% ===================================================================== +% crossval / bootstrap / weight map +% ===================================================================== + +function test_crossval_stratified_group_kfold(tc) +pm = tc.TestData.pm_cv; +tc.verifyEqual(pm.fit_type, 'crossval'); +tc.verifyEqual(pm.cv_partition.nfolds, 5); +tc.verifyNumElements(pm.fitted_values.yfit, numel(tc.TestData.Y)); +tc.verifyFalse(isnan(pm.error_metrics.balanced_accuracy.value), 'metric populated'); +end + + +function test_bootstrap_weight_stats(tc) +nboot = 5; % must match setupOnce +pm = tc.TestData.pm_boot; +tc.verifyEqual(size(pm.weights.boot_w, 2), nboot, 'boot_w should have nboot columns'); +tc.verifyNumElements(pm.weights.z, numel(pm.weights.w)); +tc.verifyNumElements(pm.weights.p, numel(pm.weights.w)); +tc.verifyTrue(all(pm.weights.p >= 0 & pm.weights.p <= 1), 'p in [0,1]'); +tc.verifyTrue(all(abs(pm.weights.z) < 1e6), 'z floored, not 1e15'); +p_floor = 2 / (nboot + 1); +tc.verifyGreaterThanOrEqual(min(pm.weights.p), p_floor - eps, 'empirical p floor'); +end + + +function test_weight_map_object(tc) +pm = tc.TestData.pm_boot; +hw = tc.TestData.hw_obj; +[pm_w, si_full] = weight_map_object(pm, hw); +[~, si_thr] = weight_map_object(pm, hw, 'use', 'thresh_fdr'); +tc.verifyClass(si_full, 'statistic_image'); +tc.verifyEqual(size(si_full.dat, 1), size(hw.dat, 1)); +tc.verifyEqual(size(si_thr.dat, 1), size(hw.dat, 1)); +tc.verifyNotEmpty(pm_w.weights.weight_obj, 'weight_obj cached on pm'); +tc.verifyClass(pm_w.weights.weight_obj, 'statistic_image'); +end + + +% ===================================================================== +% permutation / calibration / feature selection +% ===================================================================== + +function test_permutation_test_within_subjects(tc) +pm = predictive_model('algorithm', 'svm', 'task', 'classification', ... + 'modeloptions', {'KernelFunction', 'linear'}, 'random_state', 0); +pm.cv = cv_splitter.stratified_group_kfold(3); +nperm = 3; % minimum-viable: enough to form a null and a defined p-value +pm = permutation_test(pm, tc.TestData.X, tc.TestData.Y, ... + 'nperm', nperm, 'groups', tc.TestData.id, 'verbose', false); +tc.verifyNumElements(pm.permutation_results.null_scores, nperm); +tc.verifyGreaterThan(pm.permutation_results.p_value, 0); +tc.verifyLessThanOrEqual(pm.permutation_results.p_value, 1); +% DPSP is paired (Hot+Warm per subject) -> auto should pick within_subjects. +tc.verifyEqual(pm.permutation_results.permutation, 'within_subjects'); +end + + +function test_calibrate_predict_proba(tc) +pm_cal = calibrate(tc.TestData.pm_cv, tc.TestData.X, tc.TestData.Y); +tc.verifyTrue(isfield(pm_cal.fitted_values, 'calibrator'), 'calibrator present'); +P = predict_proba(pm_cal, tc.TestData.X); +tc.verifyTrue(all(P >= 0 & P <= 1), 'predict_proba in [0,1]'); +end + + +function test_select_features(tc) +pm = predictive_model('algorithm', 'svm', 'task', 'classification'); +pm = select_features(pm, tc.TestData.X, tc.TestData.Y, 'k', 500, 'verbose', false); +tc.verifyEqual(pm.diagnostics.feature_selection.n_selected, 500); +end + + +% ===================================================================== +% grid search / stability selection / pipeline / newapi +% ===================================================================== + +function test_grid_search(tc) +pm = predictive_model('algorithm', 'svm', 'task', 'classification', ... + 'cv', cv_splitter.stratified_group_kfold(3)); +pm = grid_search(pm, tc.TestData.X, tc.TestData.Y, ... + struct('BoxConstraint', [0.1 1]), 'groups', tc.TestData.id, 'verbose', false); +tc.verifyTrue(isfield(pm.diagnostics.grid_search, 'best_score')); +end + + +function test_stability_selection(tc) +pm = stability_selection( ... + predictive_model('algorithm', 'linear_svm', 'task', 'classification'), ... + tc.TestData.X, tc.TestData.Y, 'nboot', 5, 'k', 1000, 'threshold', 0.6, ... + 'groups', tc.TestData.id, 'verbose', false); +ss = pm.diagnostics.stability_selection; +tc.verifyNumElements(ss.selection_freq, size(tc.TestData.X, 2)); +tc.verifyTrue(all(ss.selection_freq >= 0 & ss.selection_freq <= 1), 'freq in [0,1]'); +end + + +function test_pipeline_pca_svm(tc) +est = predictive_model('algorithm', 'svm', 'task', 'classification'); +pipe = pipeline({ {'pca', 'k', 20} }, est); +pipe = crossval(pipe, tc.TestData.X, tc.TestData.Y, ... + 'groups', tc.TestData.id, 'cv', cv_splitter.stratified_group_kfold(5)); +tc.verifyEqual(pipe.fit_type, 'crossval'); +tc.verifyNumElements(pipe.weights.w, size(tc.TestData.X, 2), ... + 'pipeline should back-project weights to full feature space'); +si_pipe = weight_map_object(pipe, tc.TestData.hw_obj); +tc.verifyClass(si_pipe, 'statistic_image'); +end + + +function test_fmri_data_predict_newapi(tc) +[cverr, st, ~, pm_api] = predict(tc.TestData.hw_obj, 'algorithm_name', 'cv_svm', ... + 'nfolds', 5, 'newapi', 'verbose', 0); +tc.verifyClass(pm_api, 'predictive_model'); +tc.verifyEqual(pm_api.fit_type, 'crossval'); +tc.verifyTrue(isfield(st, 'dist_from_hyperplane_xval')); +tc.verifyTrue(isfinite(cverr)); +end + + +% ===================================================================== +% regression metrics / report_accuracy / summary +% ===================================================================== + +function test_regression_metrics_and_reports(tc) +rng(7); +Y = tc.TestData.Y; +Yr = double(Y) + 0.5 * randn(size(Y)); +pm_reg = predictive_model('algorithm', 'pcr', 'task', 'regression'); +pm_reg = crossval(pm_reg, tc.TestData.X, Yr, ... + 'cv', cv_splitter.group_kfold(5), 'groups', tc.TestData.id); + +tc.verifyTrue(isfield(pm_reg.error_metrics, 'predicted_r2')); +tc.verifyTrue(isfield(pm_reg.error_metrics, 'out_of_sample_r2')); + +% predicted_r2 must equal 1 - PRESS/SST computed by hand. +yf = pm_reg.fitted_values.yfit(:); +yo = Yr(:); +pr2_manual = 1 - sum((yo - yf).^2) / sum((yo - mean(yo)).^2); +tc.verifyEqual(pm_reg.error_metrics.predicted_r2.value, pr2_manual, 'AbsTol', 1e-9); + +acc_r = report_accuracy(pm_reg, 'noverbose'); +tc.verifyEqual(acc_r.task, 'regression'); +tc.verifyFalse(isnan(acc_r.predicted_r2)); + +s_reg = summary(pm_reg, 'noverbose'); +tc.verifyEqual(s_reg.provenance.fit_type, 'crossval'); + +% Classification report_accuracy: ROC-derived fields populated. +acc_c = report_accuracy(tc.TestData.pm_cv, 'noverbose'); +tc.verifyEqual(acc_c.task, 'classification'); +tc.verifyFalse(isnan(acc_c.auc)); +tc.verifyFalse(isnan(acc_c.npv)); +end + + +% ===================================================================== +% Visualisation (smoke; skipped on a headless runner) +% ===================================================================== + +function test_rocplot_metrics(tc) +% rocplot('noplot') returns metrics without a display. +ROC = rocplot(tc.TestData.pm_boot, 'noplot'); +tc.verifyTrue(isfield(ROC, 'AUC')); +tc.verifyGreaterThanOrEqual(ROC.AUC, 0); +tc.verifyLessThanOrEqual(ROC.AUC, 1); +end + + +function test_display_methods_smoke(tc) +pm = tc.TestData.pm_boot; +hw = tc.TestData.hw_obj; +try + h = plot(pm); close(h); % classification dispatcher (violin + ROC) + confusionchart(pm); close(gcf); + montage(pm, hw); close all force; % delegates to weight-map montage +catch ME + if strcmp(canlab_classify_environment_error(ME), 'genuine') + rethrow(ME); + end + tc.assumeFail(['display method needs a graphics environment: ' ME.message]); +end +end diff --git a/CanlabCore/Unit_tests/predictive_model_unit_test.m b/CanlabCore/Unit_tests/predictive_model_unit_test.m deleted file mode 100644 index c6891160..00000000 --- a/CanlabCore/Unit_tests/predictive_model_unit_test.m +++ /dev/null @@ -1,195 +0,0 @@ -function predictive_model_unit_test() -% predictive_model_unit_test End-to-end test of the new sklearn-style -% @predictive_model API, using DPSP_hotwarm as the test bed. -% -% Small nboot / nperm values are used so the test runs in ~minutes, -% not hours. The point is to exercise every public method, not to -% produce final-quality inference. -% -% Run: cd to this directory, then `predictive_model_unit_test`. - - fprintf('=== predictive_model_unit_test ===\n'); - - % --- Load DPSP Hot vs Warm --- - hw_obj = load_image_set('DPSP_hotwarm', 'noverbose'); - X = double(hw_obj.dat'); - Y = hw_obj.Y; - id = grp2idx(hw_obj.metadata_table.subj_id); - fprintf(' Loaded DPSP_hotwarm: %d obs, %d features, %d subjects\n', ... - numel(Y), size(X, 2), numel(unique(id))); - - % --- 1. Construction + hyperparameters --- - pm = predictive_model('algorithm', 'svm', ... - 'task', 'classification', ... - 'random_state', 0); - assert(strcmp(pm.algorithm, 'svm'), 'algorithm'); - assert(strcmp(pm.task, 'classification'), 'task'); - assert(~pm.is_fitted, 'starts unfitted'); - assert(pm.is_classifier, 'is_classifier'); - fprintf(' 1. Construction + clone OK\n'); - - % --- 2. fit (in-sample) --- - pm = fit(pm, X, Y, 'id', id); - assert(pm.is_fitted, 'is_fitted after fit'); - assert(strcmp(pm.fit_type, 'insample'), 'fit_type=insample'); - assert(numel(pm.weights.w) <= size(X, 2), 'weights.w shape'); - fprintf(' 2. fit OK (fit_type=%s, |w|=%d)\n', pm.fit_type, numel(pm.weights.w)); - - % --- 3. predict --- - [yhat, scores] = predict(pm, X); - assert(numel(yhat) == numel(Y), 'predict yhat length'); - assert(size(scores, 1) == numel(Y), 'predict scores length'); - fprintf(' 3. predict OK (train acc=%.2f)\n', mean(yhat == Y)); - - % --- 4. score --- - pm.scorer = cv_scorer.balanced_accuracy(); - v = score(pm, X, Y); - assert(v >= 0 && v <= 1, 'score in [0,1]'); - fprintf(' 4. score (balanced_accuracy) = %.3f\n', v); - - % --- 5. crossval with stratified_group_kfold --- - pm = predictive_model('algorithm','svm','task','classification', ... - 'modeloptions', {'KernelFunction','linear'}, ... - 'random_state', 0); - pm = crossval(pm, X, Y, ... - 'cv', cv_splitter.stratified_group_kfold(5), ... - 'groups', id, ... - 'scoring', 'balanced_accuracy'); - assert(strcmp(pm.fit_type, 'crossval'), 'fit_type=crossval'); - assert(pm.cv_partition.nfolds == 5, '5 folds'); - assert(numel(pm.fitted_values.yfit) == numel(Y), 'cv yfit length'); - assert(~isnan(pm.error_metrics.balanced_accuracy.value), 'metric populated'); - fprintf(' 5. crossval (stratified_group_kfold(5)) bal_acc = %.3f\n', ... - pm.error_metrics.balanced_accuracy.value); - - % --- 6. bootstrap (small nboot for speed) --- - pm = bootstrap(pm, X, Y, 'nboot', 25, 'groups', id, 'verbose', false); - assert(size(pm.weights.boot_w, 2) == 25, 'nboot=25 samples'); - assert(numel(pm.weights.z) == numel(pm.weights.w), 'z length matches w'); - assert(numel(pm.weights.p) == numel(pm.weights.w), 'p length matches w'); - assert(all(pm.weights.p >= 0 & pm.weights.p <= 1), 'p in [0,1]'); - assert(all(abs(pm.weights.z) < 1e6), 'z floored not 1e15'); - p_floor = 2 / (25 + 1); - assert(min(pm.weights.p) >= p_floor - eps, 'empirical p floor'); - fprintf(' 6. bootstrap (nboot=25) OK\n'); - fprintf(' z range = [%g .. %g]; p floor = %.4f\n', ... - min(pm.weights.z), max(pm.weights.z), min(pm.weights.p)); - - % --- 7. weight_map_object: pre- and post-bootstrap thresholded, + caching --- - [pm_w, si_full] = weight_map_object(pm, hw_obj); - [~, si_thr] = weight_map_object(pm, hw_obj, 'use', 'thresh_fdr'); - assert(isa(si_full, 'statistic_image'), 'si_full class'); - assert(size(si_full.dat, 1) == size(hw_obj.dat, 1), 'si_full voxel count'); - assert(size(si_thr.dat, 1) == size(hw_obj.dat, 1), 'si_thr voxel count'); - assert(~isempty(pm_w.weights.weight_obj), 'weight_obj cached on pm'); - assert(isa(pm_w.weights.weight_obj, 'statistic_image'), 'cached weight_obj class'); - fprintf(' 7. weight_map_object (full + thresh_fdr + cache) OK\n'); - fprintf(' full-w nonzero: %d, FDR-thresh nonzero: %d\n', ... - sum(si_full.dat ~= 0), sum(si_thr.dat ~= 0)); - - % --- 8. permutation_test (small nperm) --- - pm_for_perm = predictive_model('algorithm','svm','task','classification', ... - 'modeloptions', {'KernelFunction','linear'}, ... - 'random_state', 0); - pm_for_perm.cv = cv_splitter.stratified_group_kfold(3); - pm_for_perm = permutation_test(pm_for_perm, X, Y, ... - 'nperm', 10, 'groups', id, ... - 'verbose', false); - assert(numel(pm_for_perm.permutation_results.null_scores) == 10, 'nperm=10 null scores'); - assert(pm_for_perm.permutation_results.p_value > 0, 'p_value > 0'); - assert(pm_for_perm.permutation_results.p_value <= 1, 'p_value <= 1'); - % DPSP is a paired design (Hot+Warm per subject) — auto should pick within_subjects. - assert(strcmp(pm_for_perm.permutation_results.permutation, 'within_subjects'), ... - 'auto should pick within_subjects on paired DPSP'); - fprintf(' 8. permutation_test (nperm=10, %s) observed=%.3f, p=%.3f\n', ... - pm_for_perm.permutation_results.permutation, ... - pm_for_perm.permutation_results.observed, ... - pm_for_perm.permutation_results.p_value); - - % --- 9. calibrate + predict_proba --- - pm_cal = calibrate(pm, X, Y); - assert(isfield(pm_cal.fitted_values, 'calibrator'), 'calibrator present'); - P = predict_proba(pm_cal, X); - assert(all(P >= 0 & P <= 1), 'predict_proba in [0,1]'); - fprintf(' 9. calibrate + predict_proba: P mean=%.3f\n', mean(P)); - - % --- 10. select_features --- - pm_fs = predictive_model('algorithm','svm','task','classification'); - pm_fs = select_features(pm_fs, X, Y, 'k', 500, 'verbose', false); - assert(pm_fs.diagnostics.feature_selection.n_selected == 500, 'k=500 selected'); - fprintf(' 10. select_features OK (kept 500)\n'); - - % --- 11. visualisation methods (smoke test; no display assertions) --- - ROC = rocplot(pm, 'noplot'); - assert(isfield(ROC, 'AUC') && ROC.AUC >= 0 && ROC.AUC <= 1, 'rocplot AUC in [0,1]'); - h = plot(pm); close(h); % classification dispatcher (violin + ROC) - confusionchart(pm); close(gcf); - fprintf(' 11. rocplot/plot/confusionchart OK (AUC=%.3f)\n', ROC.AUC); - - % --- 12. montage / surface delegates (smoke test) --- - montage(pm, hw_obj); close all force; - fprintf(' 12. montage(pm, source) OK\n'); - - % --- 13. grid_search (tiny grid) --- - pm_gs = predictive_model('algorithm','svm','task','classification', ... - 'cv', cv_splitter.stratified_group_kfold(3)); - pm_gs = grid_search(pm_gs, X, Y, struct('BoxConstraint', [0.1 1]), ... - 'groups', id, 'verbose', false); - assert(isfield(pm_gs.diagnostics.grid_search, 'best_score'), 'grid_search best_score'); - fprintf(' 13. grid_search OK (best %s=%.3f)\n', ... - pm_gs.scorer.name, pm_gs.diagnostics.grid_search.best_score); - - % --- 14. stability_selection (small nboot) --- - pm_ss = stability_selection( ... - predictive_model('algorithm','linear_svm','task','classification'), ... - X, Y, 'nboot', 20, 'k', 1000, 'threshold', 0.6, 'groups', id, 'verbose', false); - ss = pm_ss.diagnostics.stability_selection; - assert(numel(ss.selection_freq) == size(X, 2), 'selection_freq length'); - assert(all(ss.selection_freq >= 0 & ss.selection_freq <= 1), 'selection_freq in [0,1]'); - fprintf(' 14. stability_selection OK (%d stable voxels)\n', ss.n_stable); - - % --- 15. @pipeline: PCA -> svm, leakage-free crossval + weight back-projection --- - est = predictive_model('algorithm','svm','task','classification'); - pipe = pipeline({ {'pca','k',20} }, est); - pipe = crossval(pipe, X, Y, 'groups', id, 'cv', cv_splitter.stratified_group_kfold(5)); - assert(strcmp(pipe.fit_type, 'crossval'), 'pipeline crossval fit_type'); - assert(numel(pipe.weights.w) == size(X, 2), 'pipeline back-projected weight length'); - si_pipe = weight_map_object(pipe, hw_obj); - assert(isa(si_pipe, 'statistic_image'), 'pipeline weight_map_object class'); - fprintf(' 15. pipeline (PCA->svm) OK (cv bal_acc=%.3f)\n', ... - pipe.error_metrics.balanced_accuracy.value); - - % --- 16. fmri_data.predict 'newapi' routing --- - [cverr, st, ~, pm_api] = predict(hw_obj, 'algorithm_name', 'cv_svm', ... - 'nfolds', 5, 'newapi', 'verbose', 0); - assert(isa(pm_api, 'predictive_model'), 'newapi returns predictive_model'); - assert(strcmp(pm_api.fit_type, 'crossval'), 'newapi fit_type=crossval'); - assert(isfield(st, 'dist_from_hyperplane_xval'), 'newapi stats has xval distances'); - fprintf(' 16. fmri_data.predict ''newapi'' OK (cverr=%.3f)\n', cverr); - - % --- 17. predicted_r2 / out_of_sample_r2 + report_accuracy + summary --- - % Regression model on a continuous outcome (signed label scaled to vary). - rng(7); - Yr = double(Y) + 0.5 * randn(size(Y)); - pm_reg = predictive_model('algorithm', 'pcr', 'task', 'regression'); - pm_reg = crossval(pm_reg, X, Yr, 'cv', cv_splitter.group_kfold(5), 'groups', id); - assert(isfield(pm_reg.error_metrics, 'predicted_r2'), 'predicted_r2 present'); - assert(isfield(pm_reg.error_metrics, 'out_of_sample_r2'), 'out_of_sample_r2 present'); - % predicted_r2 must equal 1 - PRESS/SST computed by hand. - yf = pm_reg.fitted_values.yfit(:); yo = Yr(:); - pr2_manual = 1 - sum((yo - yf).^2) / sum((yo - mean(yo)).^2); - assert(abs(pm_reg.error_metrics.predicted_r2.value - pr2_manual) < 1e-9, ... - 'predicted_r2 matches 1 - PRESS/SST'); - acc_r = report_accuracy(pm_reg, 'noverbose'); - assert(strcmp(acc_r.task, 'regression') && ~isnan(acc_r.predicted_r2), 'report_accuracy regression'); - s_reg = summary(pm_reg, 'noverbose'); - assert(strcmp(s_reg.provenance.fit_type, 'crossval'), 'summary provenance'); - % Classification report_accuracy: ROC-derived fields populated. - acc_c = report_accuracy(pm, 'noverbose'); - assert(strcmp(acc_c.task, 'classification') && ~isnan(acc_c.auc) && ~isnan(acc_c.npv), ... - 'report_accuracy classification (auc + npv)'); - fprintf(' 17. predicted_r2 / report_accuracy / summary OK (pred R^2=%.3f)\n', ... - pm_reg.error_metrics.predicted_r2.value); - - fprintf('\npredictive_model_unit_test: PASS\n'); -end From deb80c13ef4d61de20c4da2290fca4e06cc6c8e4 Mon Sep 17 00:00:00 2001 From: Tor Wager Date: Thu, 25 Jun 2026 18:10:31 -0400 Subject: [PATCH 4/6] 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) --- .../canlab_test_predictive_model_api.m | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CanlabCore/Unit_tests/predictive_model/canlab_test_predictive_model_api.m b/CanlabCore/Unit_tests/predictive_model/canlab_test_predictive_model_api.m index 76935ef6..515bf5ac 100644 --- a/CanlabCore/Unit_tests/predictive_model/canlab_test_predictive_model_api.m +++ b/CanlabCore/Unit_tests/predictive_model/canlab_test_predictive_model_api.m @@ -33,6 +33,20 @@ function setupOnce(tc) %#ok<*DEFNU> return end +% Restrict to a sparse gray-matter ROI so the many cross-validations below +% are fast: mask to gray matter, then deterministically thin to every 8th +% voxel (~20k features instead of ~195k). hw_obj and X stay in lockstep, so +% weight_map_object / montage still back-project consistently. Falls back to +% the full volume if the gray-matter mask is not on the path. +gm_file = which('gray_matter_mask.img'); +if ~isempty(gm_file) + hw_obj = remove_empty(apply_mask(hw_obj, gm_file)); + thin = get_wh_image(hw_obj, 1); + thin.dat = zeros(size(hw_obj.dat, 1), 1); + thin.dat(1:8:end) = 1; + hw_obj = remove_empty(apply_mask(hw_obj, thin)); +end + X = double(hw_obj.dat'); Y = hw_obj.Y; id = grp2idx(hw_obj.metadata_table.subj_id); From 948a2f03a8d58cd931962f4c76be93ad7e16e519 Mon Sep 17 00:00:00 2001 From: Tor Wager Date: Fri, 26 Jun 2026 14:58:15 -0400 Subject: [PATCH 5/6] 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) --- CanlabCore/@fmri_surface_data/apply_mask.m | 55 ++ .../@fmri_surface_data/apply_parcellation.m | 138 ++++ CanlabCore/@fmri_surface_data/cat.m | 74 ++ CanlabCore/@fmri_surface_data/compare_space.m | 66 ++ .../@fmri_surface_data/fmri_surface_data.m | 371 ++++++++++ CanlabCore/@fmri_surface_data/horzcat.m | 18 + CanlabCore/@fmri_surface_data/ica.m | 36 + CanlabCore/@fmri_surface_data/mean.m | 28 + CanlabCore/@fmri_surface_data/plot.m | 71 ++ CanlabCore/@fmri_surface_data/predict.m | 52 ++ .../private/as_fmri_data_proxy.m | 36 + .../private/build_volinfo_subblock.m | 62 ++ .../private/load_surface_geom.m | 78 ++ .../private/obj_to_volume.m | 27 + CanlabCore/@fmri_surface_data/rebuild_like.m | 49 ++ .../@fmri_surface_data/reconstruct_image.m | 72 ++ CanlabCore/@fmri_surface_data/regress.m | 75 ++ CanlabCore/@fmri_surface_data/remove_empty.m | 26 + .../@fmri_surface_data/render_on_surface.m | 102 +++ .../@fmri_surface_data/reparse_contiguous.m | 91 +++ CanlabCore/@fmri_surface_data/replace_empty.m | 23 + CanlabCore/@fmri_surface_data/surf2vol.m | 131 ++++ CanlabCore/@fmri_surface_data/surface.m | 123 ++++ .../@fmri_surface_data/surface_region.m | 116 +++ CanlabCore/@fmri_surface_data/threshold.m | 76 ++ CanlabCore/@fmri_surface_data/to_fmri_data.m | 49 ++ CanlabCore/@fmri_surface_data/ttest.m | 49 ++ CanlabCore/@fmri_surface_data/write.m | 168 +++++ CanlabCore/@image_vector/ica.m | 4 +- CanlabCore/@image_vector/vol2surf.m | 90 +++ CanlabCore/Cifti_plotting/plot_surface_map.m | 7 + .../Cifti_plotting/render_cifti_on_brain.m | 9 + .../Surface_tools/canlab_cbig_warp_path.m | 42 ++ CanlabCore/Surface_tools/canlab_read_cifti.m | 363 ++++++++++ CanlabCore/Surface_tools/canlab_read_gifti.m | 193 +++++ .../canlab_surface_vertexcolors.m | 53 ++ CanlabCore/Surface_tools/canlab_write_cifti.m | 235 ++++++ CanlabCore/Surface_tools/canlab_write_gifti.m | 176 +++++ .../Surface_tools/canlab_zlib_deflate.m | 28 + .../Surface_tools/canlab_zlib_inflate.m | 30 + .../canlab_test_surface_analysis.m | 154 ++++ .../surface_data/canlab_test_surface_io.m | 191 +++++ .../canlab_test_surface_object_basic.m | 158 +++++ .../canlab_test_surface_parcellation.m | 134 ++++ .../surface_data/canlab_test_surface_render.m | 154 ++++ .../canlab_test_surface_space_recon.m | 165 +++++ .../canlab_test_surface_vol_map.m | 154 ++++ CanlabCore/Visualization_functions/addbrain.m | 19 + .../docs/fmri_surface_data_design_plan.md | 668 ++++++++++++++++++ CanlabCore/docs/fmri_surface_data_methods.md | 375 ++++++++++ .../docs/fmri_surface_data_walkthrough.m | 203 ++++++ 51 files changed, 5866 insertions(+), 1 deletion(-) create mode 100644 CanlabCore/@fmri_surface_data/apply_mask.m create mode 100644 CanlabCore/@fmri_surface_data/apply_parcellation.m create mode 100644 CanlabCore/@fmri_surface_data/cat.m create mode 100644 CanlabCore/@fmri_surface_data/compare_space.m create mode 100644 CanlabCore/@fmri_surface_data/fmri_surface_data.m create mode 100644 CanlabCore/@fmri_surface_data/horzcat.m create mode 100644 CanlabCore/@fmri_surface_data/ica.m create mode 100644 CanlabCore/@fmri_surface_data/mean.m create mode 100644 CanlabCore/@fmri_surface_data/plot.m create mode 100644 CanlabCore/@fmri_surface_data/predict.m create mode 100644 CanlabCore/@fmri_surface_data/private/as_fmri_data_proxy.m create mode 100644 CanlabCore/@fmri_surface_data/private/build_volinfo_subblock.m create mode 100644 CanlabCore/@fmri_surface_data/private/load_surface_geom.m create mode 100644 CanlabCore/@fmri_surface_data/private/obj_to_volume.m create mode 100644 CanlabCore/@fmri_surface_data/rebuild_like.m create mode 100644 CanlabCore/@fmri_surface_data/reconstruct_image.m create mode 100644 CanlabCore/@fmri_surface_data/regress.m create mode 100644 CanlabCore/@fmri_surface_data/remove_empty.m create mode 100644 CanlabCore/@fmri_surface_data/render_on_surface.m create mode 100644 CanlabCore/@fmri_surface_data/reparse_contiguous.m create mode 100644 CanlabCore/@fmri_surface_data/replace_empty.m create mode 100644 CanlabCore/@fmri_surface_data/surf2vol.m create mode 100644 CanlabCore/@fmri_surface_data/surface.m create mode 100644 CanlabCore/@fmri_surface_data/surface_region.m create mode 100644 CanlabCore/@fmri_surface_data/threshold.m create mode 100644 CanlabCore/@fmri_surface_data/to_fmri_data.m create mode 100644 CanlabCore/@fmri_surface_data/ttest.m create mode 100644 CanlabCore/@fmri_surface_data/write.m create mode 100644 CanlabCore/@image_vector/vol2surf.m create mode 100644 CanlabCore/Surface_tools/canlab_cbig_warp_path.m create mode 100644 CanlabCore/Surface_tools/canlab_read_cifti.m create mode 100644 CanlabCore/Surface_tools/canlab_read_gifti.m create mode 100644 CanlabCore/Surface_tools/canlab_surface_vertexcolors.m create mode 100644 CanlabCore/Surface_tools/canlab_write_cifti.m create mode 100644 CanlabCore/Surface_tools/canlab_write_gifti.m create mode 100644 CanlabCore/Surface_tools/canlab_zlib_deflate.m create mode 100644 CanlabCore/Surface_tools/canlab_zlib_inflate.m create mode 100644 CanlabCore/Unit_tests/surface_data/canlab_test_surface_analysis.m create mode 100644 CanlabCore/Unit_tests/surface_data/canlab_test_surface_io.m create mode 100644 CanlabCore/Unit_tests/surface_data/canlab_test_surface_object_basic.m create mode 100644 CanlabCore/Unit_tests/surface_data/canlab_test_surface_parcellation.m create mode 100644 CanlabCore/Unit_tests/surface_data/canlab_test_surface_render.m create mode 100644 CanlabCore/Unit_tests/surface_data/canlab_test_surface_space_recon.m create mode 100644 CanlabCore/Unit_tests/surface_data/canlab_test_surface_vol_map.m create mode 100644 CanlabCore/docs/fmri_surface_data_design_plan.md create mode 100644 CanlabCore/docs/fmri_surface_data_methods.md create mode 100644 CanlabCore/docs/fmri_surface_data_walkthrough.m diff --git a/CanlabCore/@fmri_surface_data/apply_mask.m b/CanlabCore/@fmri_surface_data/apply_mask.m new file mode 100644 index 00000000..801d880d --- /dev/null +++ b/CanlabCore/@fmri_surface_data/apply_mask.m @@ -0,0 +1,55 @@ +function obj = apply_mask(obj, mask, varargin) +% apply_mask Mask a grayordinate object by zeroing out-of-mask grayordinates. +% +% :Usage: +% :: +% obj = apply_mask(obj, mask) +% obj = apply_mask(obj, mask, 'invert') +% +% Surface analogue of fmri_data.apply_mask, greatly simplified per design +% decision D5b: because all objects share a standardized grayordinate space, +% masking is intrinsic -- there is no fmri_mask_image, no resampling, and no +% empty-removal. Out-of-mask grayordinate rows are simply set to 0; .dat keeps +% its full size and the geometry is unchanged. +% +% :Inputs: +% **obj:** an fmri_surface_data object. +% **mask:** one of +% - logical/numeric vector [nGrayordinates x 1] (nonzero = keep) +% - another fmri_surface_data on the same space (nonzero/non-NaN = keep) +% +% :Optional Inputs: +% **'invert':** keep the complement (zero the in-mask grayordinates instead). +% +% :Outputs: +% **obj:** masked object (out-of-mask rows zeroed). +% +% :See also: fmri_surface_data, compare_space + +invert = any(strcmpi(varargin, 'invert')); + +if isa(mask, 'fmri_surface_data') + if compare_space(obj, mask) ~= 0 + error('fmri_surface_data:apply_mask:space', ... + ['Mask is not on the same grayordinate space as the data ' ... + '(compare_space ~= 0). Resample first.']); + end + keep = any(mask.dat ~= 0 & ~isnan(mask.dat), 2); +elseif islogical(mask) || isnumeric(mask) + keep = logical(mask(:)); + if numel(keep) ~= size(obj.dat, 1) + error('fmri_surface_data:apply_mask:length', ... + 'Mask vector length (%d) must equal the number of grayordinates (%d).', ... + numel(keep), size(obj.dat, 1)); + end +else + error('fmri_surface_data:apply_mask:type', ... + 'mask must be a logical/numeric vector or an fmri_surface_data object.'); +end + +if invert, keep = ~keep; end + +obj.dat(~keep, :) = 0; +obj.history{end+1} = sprintf('apply_mask: kept %d / %d grayordinates (zeroed the rest)', ... + nnz(keep), numel(keep)); +end diff --git a/CanlabCore/@fmri_surface_data/apply_parcellation.m b/CanlabCore/@fmri_surface_data/apply_parcellation.m new file mode 100644 index 00000000..79a47aa2 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/apply_parcellation.m @@ -0,0 +1,138 @@ +function [parcel_means, parcel_labels, parcel_table] = apply_parcellation(obj, parcels, varargin) +% apply_parcellation Average grayordinate data within parcels of a surface atlas. +% +% :Usage: +% :: +% parcel_means = apply_parcellation(obj, parcels) +% [parcel_means, labels, tbl] = apply_parcellation(obj, parcels, 'area') +% +% Computes the mean of each map over the grayordinates in each parcel of a +% surface/grayordinate parcellation, mirroring image_vector.apply_parcellation. +% Parcels with key 0 (and NaN) are treated as background / medial wall and are +% excluded. Operates directly on .dat (no resampling needed when on the same +% space). +% +% :Inputs: +% **obj:** an fmri_surface_data object ([nGrayordinates x nMaps]). +% **parcels:** the parcellation, one of +% - an fmri_surface_data with integer label keys (e.g. a `.dlabel`), +% on the SAME grayordinate space (compare_space == 0); its label_table +% provides parcel names. +% - an integer vector [nGrayordinates x 1] of label keys. +% +% :Optional Inputs: +% **'area':** area-weight the average using per-vertex surface area (cortex) +% instead of an unweighted mean. (Subcortical voxels use unit weight.) +% +% :Outputs: +% **parcel_means:** [nMaps x nParcels] mean value per parcel. +% **parcel_labels:** 1 x nParcels cell of parcel names (or 'parcel_'). +% **parcel_table:** table with columns key, label, n_grayordinates, (area). +% +% :Examples: +% :: +% atl = fmri_surface_data(which('Gordon333.32k_fs_LR_Tian_Subcortex_S2.dlabel.nii')); +% s = fmri_surface_data(which('transcriptomic_gradients.dscalar.nii')); +% pm = apply_parcellation(s, atl); % [nMaps x nParcels] +% +% :See also: fmri_surface_data, reparse_contiguous, surface_region, condf2indic + +use_area = any(strcmpi(varargin, 'area')); + +% ---- Resolve parcel keys + names ---- +names_by_key = containers.Map('KeyType', 'double', 'ValueType', 'char'); +if isa(parcels, 'fmri_surface_data') + if compare_space(obj, parcels) ~= 0 + error('fmri_surface_data:apply_parcellation:space', ... + 'Parcellation is not on the same grayordinate space (compare_space ~= 0).'); + end + keys = round(double(parcels.dat(:, 1))); + if ~isempty(parcels.label_table) + for i = 1:numel(parcels.label_table) + names_by_key(parcels.label_table(i).key) = parcels.label_table(i).name; + end + end +elseif isnumeric(parcels) + keys = round(double(parcels(:))); + if numel(keys) ~= size(obj.dat, 1) + error('fmri_surface_data:apply_parcellation:length', ... + 'Parcel key vector length (%d) must equal the number of grayordinates (%d).', ... + numel(keys), size(obj.dat,1)); + end +else + error('fmri_surface_data:apply_parcellation:type', ... + 'parcels must be an fmri_surface_data or an integer vector.'); +end + +% ---- Indicator matrix over positive keys (exclude 0 / NaN) ---- +ukeys = unique(keys(keys > 0 & ~isnan(keys))); +nP = numel(ukeys); +if nP == 0 + error('fmri_surface_data:apply_parcellation:noparcels', 'No positive parcel keys found.'); +end +indic = double(keys == ukeys'); % [nGray x nP] 0/1 + +% ---- Per-grayordinate weights ---- +if use_area + w = local_vertex_areas(obj); % [nGray x 1] +else + w = ones(size(obj.dat, 1), 1); +end + +D = double(obj.dat); % [nGray x nMaps] +wsum = (w' * indic); % [1 x nP] total weight per parcel +sums = (D .* w)' * indic; % [nMaps x nP] +parcel_means = sums ./ wsum; % [nMaps x nP] + +% ---- Labels + table ---- +parcel_labels = cell(1, nP); +counts = sum(indic, 1); % grayordinates per parcel +for i = 1:nP + if names_by_key.isKey(ukeys(i)) && ~isempty(names_by_key(ukeys(i))) + parcel_labels{i} = names_by_key(ukeys(i)); + else + parcel_labels{i} = sprintf('parcel_%d', ukeys(i)); + end +end + +if nargout >= 3 + parcel_table = table(ukeys(:), parcel_labels(:), counts(:), wsum(:), ... + 'VariableNames', {'key', 'label', 'n_grayordinates', 'total_weight'}); +end +end + + +% ------------------------------------------------------------------------- +function w = local_vertex_areas(obj) +% Per-grayordinate weight = barycentric vertex area on the cortical mesh +% (1/3 of incident face areas); subcortical voxels get unit weight. +w = ones(size(obj.dat, 1), 1); +for mi = 1:numel(obj.brain_model.models) + m = obj.brain_model.models{mi}; + rows = (m.start:(m.start + m.count - 1))'; + if ~strcmp(m.type, 'surf'), continue; end + [V, F] = local_hemi_VF(obj.surface_space, m.struct); + va = local_face_vertex_area(V, F); % [numvert x 1] + w(rows) = va(m.vertlist + 1); +end +end + + +function [V, F] = local_hemi_VF(space, structname) +% Prefer midthickness (true areas); fall back to inflated if unavailable. +try + g = load_surface_geom(space, 'midthickness'); +catch + g = load_surface_geom(space, 'inflated'); +end +if contains(upper(structname), 'LEFT'), V = g.vertices_lh; F = g.faces_lh; +else, V = g.vertices_rh; F = g.faces_rh; end +end + + +function va = local_face_vertex_area(V, F) +% Barycentric per-vertex area: each vertex gets 1/3 of each incident face's area. +v1 = V(F(:,1), :); v2 = V(F(:,2), :); v3 = V(F(:,3), :); +fa = 0.5 * sqrt(sum(cross(v2 - v1, v3 - v1, 2).^2, 2)); % [nFaces x 1] +va = accumarray(F(:), repmat(fa, 3, 1) / 3, [size(V,1) 1]); +end diff --git a/CanlabCore/@fmri_surface_data/cat.m b/CanlabCore/@fmri_surface_data/cat.m new file mode 100644 index 00000000..f7a3d6b0 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/cat.m @@ -0,0 +1,74 @@ +function obj = cat(obj, varargin) +% cat Concatenate fmri_surface_data objects along the image (map) dimension. +% +% :Usage: +% :: +% combined = cat(obj1, obj2, obj3, ...) +% +% Horizontally concatenates the [grayordinates x maps] data of two or more +% fmri_surface_data objects (e.g. combining subjects/contrasts into one dataset), +% along with their per-map annotations (X, Y, covariates, image_names, +% metadata_table). All objects must share the same grayordinate space +% (compare_space == 0); otherwise an error is raised. Surface analogue of +% fmri_data.cat. No replace_empty step is needed (.dat is always full, D5b). +% +% :Inputs: +% **obj, varargin:** two or more fmri_surface_data objects on the same space. +% +% :Outputs: +% **obj:** a single fmri_surface_data with all maps concatenated. +% +% :Examples: +% :: +% all_subs = cat(sub1, sub2, sub3); +% t = ttest(all_subs); +% +% :See also: horzcat, compare_space, fmri_surface_data + +for i = 1:numel(varargin) + o2 = varargin{i}; + if ~isa(o2, 'fmri_surface_data') + error('fmri_surface_data:cat:type', 'All inputs must be fmri_surface_data objects.'); + end + if compare_space(obj, o2) ~= 0 + error('fmri_surface_data:cat:space', ... + ['Objects are not on the same grayordinate space (compare_space ~= 0). ' ... + 'Resample to a common space before concatenating.']); + end + + obj.dat = [obj.dat, o2.dat]; + + obj.X = local_vcat(obj.X, o2.X); + obj.Y = local_vcat(obj.Y, o2.Y); + obj.covariates = local_vcat(obj.covariates, o2.covariates); + obj.image_names = local_catcell(obj.image_names, o2.image_names); + obj.metadata_table = local_cattable(obj.metadata_table, o2.metadata_table); + if ~isempty(obj.images_per_session) || ~isempty(o2.images_per_session) + obj.images_per_session = [obj.images_per_session(:); o2.images_per_session(:)]'; + end +end + +obj.removed_images = false(size(obj.dat, 2), 1); +obj.removed_voxels = false(size(obj.dat, 1), 1); +obj.history{end+1} = sprintf('cat: concatenated to %d maps', size(obj.dat, 2)); +end + + +% ------------------------------------------------------------------------- +function v = local_vcat(a, b) +if isempty(a) && isempty(b), v = []; return; end +v = [a; b]; % per-map (rows = maps) fields +end + +function c = local_catcell(a, b) +if isempty(a) && isempty(b), c = {}; return; end +a = reshape(a, [], 1); b = reshape(b, [], 1); +c = [a; b]; +end + +function t = local_cattable(a, b) +if isempty(a) && isempty(b), t = []; return; end +if isempty(a), t = b; return; end +if isempty(b), t = a; return; end +t = [a; b]; +end diff --git a/CanlabCore/@fmri_surface_data/compare_space.m b/CanlabCore/@fmri_surface_data/compare_space.m new file mode 100644 index 00000000..649fdb36 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/compare_space.m @@ -0,0 +1,66 @@ +function isdiff = compare_space(obj, obj2) +% compare_space Compare the grayordinate spaces of two fmri_surface_data objects. +% +% :Usage: +% :: +% isdiff = compare_space(obj, obj2) +% +% Surface analogue of image_vector.compare_space, preserving the same integer +% return contract so callers (cat, apply_mask) can branch on it. Compares the +% surface_space tag and the brain_model layout (per-model structure, count, and +% surface vertex count), then the in-data selection (vertlist / voxlist). +% +% :Outputs: +% **isdiff:** integer code: +% - 0 same space and same in-data grayordinates +% - 1 different spaces (tag or model layout differ) +% - 2 no brain_model for one or more objects +% - 3 same space, but different in-data grayordinates (vertlist/voxlist +% or row count differ) +% +% :See also: image_vector.compare_space, resample_space, cat, apply_mask + +if isempty(obj.brain_model) || isempty(obj2.brain_model) + isdiff = 2; + return +end + +% Space tag +if ~strcmp(char(obj.surface_space), char(obj2.surface_space)) + isdiff = 1; + return +end + +m1 = obj.brain_model.models; +m2 = obj2.brain_model.models; +if numel(m1) ~= numel(m2) + isdiff = 1; + return +end + +% Model layout: structure name, in-data count, and surface vertex count +for i = 1:numel(m1) + same = strcmp(m1{i}.struct, m2{i}.struct) ... + && isequaln(m1{i}.numvert, m2{i}.numvert) ... + && strcmp(m1{i}.type, m2{i}.type); + if ~same + isdiff = 1; + return + end +end + +% Same layout: now check the exact in-data selection +isdiff = 0; +if size(obj.dat, 1) ~= size(obj2.dat, 1) + isdiff = 3; + return +end +for i = 1:numel(m1) + if m1{i}.count ~= m2{i}.count ... + || ~isequal(m1{i}.vertlist, m2{i}.vertlist) ... + || ~isequal(m1{i}.voxlist, m2{i}.voxlist) + isdiff = 3; + return + end +end +end diff --git a/CanlabCore/@fmri_surface_data/fmri_surface_data.m b/CanlabCore/@fmri_surface_data/fmri_surface_data.m new file mode 100644 index 00000000..c0e69817 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/fmri_surface_data.m @@ -0,0 +1,371 @@ +classdef fmri_surface_data < image_vector +% fmri_surface_data: CANlab data object for cortical-surface and grayordinate (CIFTI) data +% +% fmri_surface_data is the surface/grayordinate analogue of fmri_data. It wraps +% the HCP CIFTI grayordinate standard (cortical-surface vertices + subcortical +% voxels) as a true CANlab object that is interoperable with the rest of the +% toolbox, while holding data in the canonical flat [grayordinates x maps] .dat +% matrix so generic analysis methods (ica, descriptives, ...) work unchanged. +% +% It is a subclass of image_vector (NOT fmri_data): it inherits the .dat-based +% machinery but does NOT reproduce fmri_data's volInfo-centric, empty-voxel +% squeezing design. CIFTI data is already compact (the medial wall is excluded +% by construction and there are no out-of-brain voxels), so: +% - .dat is ALWAYS the full [nGrayordinates x nMaps] matrix, in 1:1 row +% correspondence with .brain_model. Rows are never dropped. +% - remove_empty / replace_empty are no-ops (see those methods). +% - removed_voxels / removed_images are vestigial all-false vectors. +% See docs/fmri_surface_data_design_plan.md (decision D5b). +% +% ------------------------------------------------------------------------- +% Construction +% ------------------------------------------------------------------------- +% obj = fmri_surface_data % empty object +% obj = fmri_surface_data(cifti_filename) % .dscalar/.dtseries/.dlabel.nii +% obj = fmri_surface_data(gifti_filename) % .func/.shape/.label/.surf.gii +% obj = fmri_surface_data(cifti_struct) % output of canlab_read_cifti +% obj = fmri_surface_data('dat', X, 'brain_model', bm, ...) % key/value pairs +% obj = fmri_surface_data(image_vector_obj) % recast a matching object +% +% Native I/O is used (canlab_read_cifti / canlab_read_gifti) -- no external +% toolbox (gifti, FieldTrip, cifti-matlab, Connectome Workbench) is required. +% +% ------------------------------------------------------------------------- +% Key properties +% ------------------------------------------------------------------------- +% dat [nGrayordinates x nMaps] single (inherited) data matrix. +% brain_model struct: the geometry source of truth (mirrors CIFTI +% BrainModels). Fields .models{i} (.struct, .type 'surf'|'vox', +% .start, .count, .numvert, .vertlist 0-based, .voxlist 3xN), +% .vol (.dims, .sform), .grayordinate_type, .cluster. +% geom struct: cortical mesh(es) for rendering/area (lazy). +% intent 'dscalar'|'dtseries'|'dlabel'|'func'|'shape'|'label'. +% series_info struct for .dtseries (start/step/unit/exponent). +% label_table struct array (.key,.name,.rgba) for .dlabel/.label. +% surface_space e.g. 'fsLR_32k', 'fsaverage_164k'. +% X / Y / covariates / images_per_session / metadata_table ... +% per-map annotations (same names/roles as fmri_data). +% +% :See also: fmri_data, image_vector, canlab_read_cifti, canlab_read_gifti + +% .. +% Author: CANlab, 2026. Part of the fmri_surface_data surface-data toolset. +% See docs/fmri_surface_data_design_plan.md. +% .. + + properties + + % Geometry source of truth (mirrors the CIFTI BrainModels structure). + % Replaces volInfo's role for surface + grayordinate data. See D3/D5b. + brain_model = []; + + % Cortical mesh cache (faces/vertices per hemisphere) for rendering and + % surface-area computations. Loaded lazily from bundled assets. + geom = []; + + % CIFTI/GIFTI intent: dscalar | dtseries | dlabel | func | shape | label + intent = ''; + + % For .dtseries: struct with .start/.step/.unit/.exponent + series_info = []; + + % For .dlabel/.label: struct array with fields .key, .name, .rgba + label_table = []; + + % Canonical surface-space tag, e.g. 'fsLR_32k', 'fsaverage_164k' + surface_space = ''; + + % Optional same-length logical over grayordinates (or another + % fmri_surface_data on the same space). Lightweight: masking just + % zeros/selects rows -- no fmri_mask_image, no resampling (D5b). + mask = []; + + % --- Per-map (column) annotations: same names/roles as fmri_data --- + X % design / predictor matrix [nMaps x p] + Y = []; % outcomes [nMaps x q] + Y_names; + covariates; + covariate_names = {''}; + images_per_session; + metadata_table; + image_metadata; + additional_info = struct(); % free-form; also stashes source CIFTI xml/hdr + + end % properties + + methods + + function obj = fmri_surface_data(varargin) + + % Initialize defaults via the image_vector constructor + obj = obj@image_vector(); + obj.removed_voxels = false(0, 1); + obj.removed_images = false(0, 1); + + if nargin == 0, return; end + + % ---- Single positional argument: file / struct / object ---- + if nargin == 1 + a = varargin{1}; + + if ischar(a) || isstring(a) + obj = load_surface_file(obj, char(a)); + return + + elseif isstruct(a) + if isfield(a, 'cdata') && isfield(a, 'diminfo') + obj = from_cifti_struct(obj, a); % canlab_read_cifti output + elseif isfield(a, 'vertices') || isfield(a, 'faces') || isfield(a, 'cdata') + obj = from_gifti_struct(obj, a); % canlab_read_gifti output + else + obj = copy_matching_fields(obj, a); % plain struct of properties + end + return + + elseif isa(a, 'image_vector') + obj = recast_from_object(obj, a); + return + end + end + + % ---- key / value pairs ---- + obj = parse_keyvalue(obj, varargin); + + % If a filename was passed via 'fullpath' and we have no data, load it + if isempty(obj.dat) && ~isempty(obj.fullpath) && ischar(obj.fullpath) ... + && exist(obj.fullpath, 'file') + obj = load_surface_file(obj, obj.fullpath); + end + + end % constructor + + end % methods + +end % classdef + + +% ========================================================================= +% Local helper functions (private to the constructor) +% ========================================================================= + +function obj = load_surface_file(obj, fname) +% Dispatch a file to the native CIFTI or GIFTI reader based on extension. +if exist(fname, 'file') ~= 2 + error('fmri_surface_data:notfound', 'File not found: %s', fname); +end +lc = lower(fname); +if endsWith(lc, '.gii') + g = canlab_read_gifti(fname); + obj = from_gifti_struct(obj, g); +elseif endsWith(lc, '.nii') || endsWith(lc, '.nii.gz') + cii = canlab_read_cifti(fname); + obj = from_cifti_struct(obj, cii); +else + error('fmri_surface_data:badext', ... + 'Unrecognized surface/grayordinate extension: %s (expected .nii CIFTI or .gii GIFTI).', fname); +end +obj.fullpath = fname; +end + + +% ------------------------------------------------------------------------- +function obj = from_cifti_struct(obj, cii) +% Build the object from a canlab_read_cifti output struct. +obj.dat = single(cii.cdata); +obj.intent = cii.intent; + +bm = cii.diminfo{1}; % dense dimension +if ~isfield(bm, 'cluster'), bm.cluster = []; end +bm.grayordinate_type = infer_grayord_type(bm); +obj.brain_model = bm; +obj.surface_space = infer_surface_space(bm); + +% Populate the inherited volInfo slot for the subcortical voxel sub-block only +% (empty for surface-only objects). brain_model remains the geometry truth. +obj.volInfo = build_volinfo_subblock(bm); + +% Maps (second) dimension -> per-map annotations +md = cii.diminfo{2}; +switch md.type + case 'scalars' + obj.image_names = reshape({md.maps.name}, [], 1); + case 'labels' + obj.image_names = reshape({md.maps.name}, [], 1); + obj.label_table = md.maps(1).table; + obj.additional_info.label_tables = {md.maps.table}; + case 'series' + obj.series_info = struct('start', md.seriesStart, 'step', md.seriesStep, ... + 'unit', md.seriesUnit, 'exponent', getfielddef(md, 'seriesExponent', 0)); + obj.image_names = arrayfun(@(k) sprintf('t%d', k), (1:size(obj.dat,2))', 'unif', 0); +end + +obj.removed_voxels = false(size(obj.dat, 1), 1); +obj.removed_images = false(size(obj.dat, 2), 1); + +% Stash source XML/header so write() can faithfully re-emit if unchanged +if isfield(cii, 'xml'), obj.additional_info.cifti_xml = cii.xml; end +if isfield(cii, 'hdr'), obj.additional_info.cifti_hdr = cii.hdr; end + +obj.history{end+1} = sprintf(['fmri_surface_data created from CIFTI (%s): ' ... + '%d grayordinates x %d maps, space=%s'], cii.intent, size(obj.dat,1), size(obj.dat,2), obj.surface_space); +end + + +% ------------------------------------------------------------------------- +function obj = from_gifti_struct(obj, g) +% Build from a canlab_read_gifti output struct (functional/label or geometry). +hasData = isfield(g, 'cdata') && ~isempty(g.cdata) && ~iscell(g.cdata); +hasGeom = isfield(g, 'vertices') && ~isempty(g.vertices); + +if hasData + obj.dat = single(g.cdata); + n = size(g.cdata, 1); + m = struct('struct', 'CORTEX', 'type', 'surf', 'start', 1, 'count', n, ... + 'numvert', n, 'vertlist', 0:n-1, 'voxlist', []); + bm = struct('type', 'dense', 'length', n, 'models', {{m}}, 'vol', []); + bm.grayordinate_type = 'cortex_only'; + bm.cluster = []; + obj.brain_model = bm; + obj.surface_space = infer_surface_space(bm); + obj.intent = 'func'; + if isfield(g, 'labels') && ~isempty(g.labels) + obj.label_table = g.labels; + obj.intent = 'label'; + end + if isfield(g, 'intents') && ~isempty(g.intents) + obj.image_names = reshape(g.intents, [], 1); + end + obj.removed_voxels = false(n, 1); + obj.removed_images = false(size(obj.dat, 2), 1); + obj.history{end+1} = sprintf('fmri_surface_data created from GIFTI (%s): %d vertices x %d maps', ... + obj.intent, n, size(obj.dat,2)); +end + +if hasGeom + % Store geometry; if this is a pure .surf.gii (no data) the object holds a mesh. + obj.geom = struct('vertices', g.vertices, 'faces', g.faces); + if ~hasData + obj.history{end+1} = sprintf('fmri_surface_data loaded surface geometry: %d vertices, %d faces', ... + size(g.vertices,1), size(g.faces,1)); + end +end +end + + +% ------------------------------------------------------------------------- +function obj = copy_matching_fields(obj, s) +p = properties(obj); +fn = fieldnames(s); +for i = 1:numel(fn) + if ismember(fn{i}, p) + obj.(fn{i}) = s.(fn{i}); + end +end +if ~isempty(obj.dat), obj.dat = single(obj.dat); end +obj = normalize_removed(obj); +end + + +% ------------------------------------------------------------------------- +function obj = normalize_removed(obj) +% Keep the vestigial removed_* vectors all-false and length-correct (D5b), +% whenever .dat is set via key-value / struct / recast construction. +if ~isempty(obj.dat) + if numel(obj.removed_voxels) ~= size(obj.dat, 1) + obj.removed_voxels = false(size(obj.dat, 1), 1); + end + if numel(obj.removed_images) ~= size(obj.dat, 2) + obj.removed_images = false(size(obj.dat, 2), 1); + end +end +end + + +% ------------------------------------------------------------------------- +function obj = recast_from_object(obj, other) +p = properties(obj); +op = properties(other); +for i = 1:numel(op) + if ismember(op{i}, p) + try, obj.(op{i}) = other.(op{i}); catch, end %#ok + end +end +if ~isempty(obj.dat), obj.dat = single(obj.dat); end +obj = normalize_removed(obj); +obj.history{end+1} = sprintf('Recast from %s', class(other)); +end + + +% ------------------------------------------------------------------------- +function obj = parse_keyvalue(obj, args) +p = properties(obj); +i = 1; +while i <= numel(args) + key = args{i}; + if (ischar(key) || isstring(key)) && ismember(char(key), p) + obj.(char(key)) = args{i+1}; + i = i + 2; + elseif (ischar(key) || isstring(key)) && (contains(char(key), '.nii') || contains(char(key), '.gii')) ... + && exist(char(key), 'file') + obj.fullpath = char(key); + i = i + 1; + else + warning('fmri_surface_data:badinput', 'Unknown field or argument: %s', char(string(key))); + i = i + 1; + end +end +if ~isempty(obj.dat), obj.dat = single(obj.dat); end +obj = normalize_removed(obj); +end + + +% ------------------------------------------------------------------------- +function s = infer_surface_space(bm) +% Infer a canonical surface-space tag from the per-hemisphere vertex count. +s = ''; +nv = NaN; +for i = 1:numel(bm.models) + if strcmp(bm.models{i}.type, 'surf') && ~isempty(bm.models{i}.numvert) ... + && ~isnan(bm.models{i}.numvert) + nv = bm.models{i}.numvert; break + end +end +switch nv + case 32492, s = 'fsLR_32k'; + case 163842, s = 'fsaverage_164k'; + case 40962, s = 'fsaverage6'; + case 10242, s = 'fsaverage5'; + case 2562, s = 'fsaverage4'; + otherwise + if ~isnan(nv), s = sprintf('surf_%dverts', nv); end +end +end + + +% ------------------------------------------------------------------------- +function t = infer_grayord_type(bm) +% '91k'-style grayordinate (surface + subcortical voxels) vs cortex-only. +hasvox = any(cellfun(@(m) strcmp(m.type, 'vox'), bm.models)); +hassurf = any(cellfun(@(m) strcmp(m.type, 'surf'), bm.models)); +if hasvox && hassurf + if bm.length == 91282, t = '91k'; + else, t = sprintf('grayordinate_%d', bm.length); + end +elseif hassurf + t = 'cortex_only'; +elseif hasvox + t = 'volume_only'; +else + t = 'unknown'; +end +end + + +% ------------------------------------------------------------------------- +function v = getfielddef(s, f, d) +if isfield(s, f) && ~isempty(s.(f)) && ~(isnumeric(s.(f)) && all(isnan(s.(f)(:)))) + v = s.(f); +else + v = d; +end +end diff --git a/CanlabCore/@fmri_surface_data/horzcat.m b/CanlabCore/@fmri_surface_data/horzcat.m new file mode 100644 index 00000000..7fdd1ff1 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/horzcat.m @@ -0,0 +1,18 @@ +function c = horzcat(varargin) +% horzcat Concatenate fmri_surface_data objects with [a, b, ...] syntax. +% +% :Usage: +% :: +% combined = [obj1, obj2, obj3]; +% +% Thin wrapper over cat; concatenates along the image (map) dimension. All +% objects must share the same grayordinate space. +% +% :See also: cat, fmri_surface_data + +if nargin == 1 + c = varargin{1}; + return +end +c = cat(varargin{1}, varargin{2:end}); +end diff --git a/CanlabCore/@fmri_surface_data/ica.m b/CanlabCore/@fmri_surface_data/ica.m new file mode 100644 index 00000000..a9687981 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/ica.m @@ -0,0 +1,36 @@ +function icadat = ica(obj, varargin) +% ica Spatial ICA decomposition of grayordinate data. +% +% :Usage: +% :: +% icadat = ica(obj, [number of ICs]) +% +% Runs independent component analysis on obj.dat (mirroring image_vector.ica) by +% delegating to a proxy and remapping the spatial component maps back to an +% fmri_surface_data, so the components can be surface()-rendered. +% +% DEPENDENCY: like image_vector.ica, this requires icatb_fastICA (the GIFT / +% GroupICA "icatb" toolbox) on the path. This is the one fmri_surface_data method +% that is not fully self-contained; all other methods need no external toolbox. +% +% :Inputs: +% **obj:** fmri_surface_data. +% **nic:** (optional) number of components to save (passed to image_vector.ica). +% +% :Outputs: +% **icadat:** fmri_surface_data whose maps are the spatial IC maps. +% +% :See also: predict, image_vector.ica, rebuild_like, fmri_surface_data + +proxy = as_fmri_data_proxy(obj); +comp_obj = ica(proxy, varargin{:}); + +if isa(comp_obj, 'image_vector') && size(comp_obj.dat, 1) == size(obj.dat, 1) + icadat = rebuild_like(obj, double(comp_obj.dat)); + icadat.image_names = arrayfun(@(k) sprintf('IC%d', k), 1:size(icadat.dat,2), ... + 'UniformOutput', false)'; + icadat.history{end+1} = sprintf('ica: %d spatial components', size(icadat.dat,2)); +else + icadat = comp_obj; % unexpected shape: pass through +end +end diff --git a/CanlabCore/@fmri_surface_data/mean.m b/CanlabCore/@fmri_surface_data/mean.m new file mode 100644 index 00000000..eba6af9a --- /dev/null +++ b/CanlabCore/@fmri_surface_data/mean.m @@ -0,0 +1,28 @@ +function m = mean(obj, varargin) +% mean Average across maps/images of an fmri_surface_data, returning one map. +% +% :Usage: +% :: +% m = mean(obj) +% m = mean(obj, 'omitnan') +% +% Averages obj.dat across the image (column) dimension and returns a new +% single-map fmri_surface_data carrying the same geometry (via rebuild_like). +% Surface analogue of fmri_data.mean; no volume rebuild. +% +% :Optional Inputs: +% **'omitnan':** ignore NaNs in the average (default includes them). +% +% :Outputs: +% **m:** fmri_surface_data with one map (the mean across images). +% +% :See also: fmri_surface_data, rebuild_like + +flag = 'includenan'; +if any(strcmpi(varargin, 'omitnan')), flag = 'omitnan'; end + +mdat = mean(double(obj.dat), 2, flag); +m = rebuild_like(obj, mdat); +m.image_names = {'mean'}; +m.history{end+1} = sprintf('mean across %d maps', size(obj.dat, 2)); +end diff --git a/CanlabCore/@fmri_surface_data/plot.m b/CanlabCore/@fmri_surface_data/plot.m new file mode 100644 index 00000000..d900a7bb --- /dev/null +++ b/CanlabCore/@fmri_surface_data/plot.m @@ -0,0 +1,71 @@ +function han = plot(obj, varargin) +% plot Quick QC plots for an fmri_surface_data object. +% +% :Usage: +% :: +% han = plot(obj) +% +% Surface analogue of fmri_data.plot: a compact QC panel with (1) a histogram of +% grayordinate values, (2) the per-map mean/standard-deviation across +% grayordinates, and (3) a native surface render of the mean map. Geometry- +% agnostic summaries operate directly on .dat. +% +% :Optional Inputs: +% **'norender':** skip the surface render panel (faster; no mesh load). +% +% :Outputs: +% **han:** struct with handles (.figure, and .surface for the render). +% +% :See also: surface, descriptives, fmri_surface_data + +dorender = ~any(strcmpi(varargin, 'norender')); + +d = double(obj.dat); +nGray = size(d, 1); +nMaps = size(d, 2); + +fig = figure('Color', 'w', 'Name', 'fmri_surface_data QC'); + +% (1) Histogram of all values +subplot(2, 2, 1); +vals = d(:); vals = vals(~isnan(vals)); +histogram(vals, 100); +title(sprintf('Value histogram (%d grayordinates x %d maps)', nGray, nMaps)); +xlabel('value'); ylabel('count'); axis tight; + +% (2) Per-map mean +/- sd +subplot(2, 2, 2); +mu = mean(d, 1, 'omitnan'); +sd = std(d, 0, 1, 'omitnan'); +errorbar(1:nMaps, mu, sd, 'o-', 'LineWidth', 1); +title('Per-map mean \pm sd across grayordinates'); +xlabel('map'); ylabel('value'); xlim([0.5 nMaps + 0.5]); grid on; + +% (3) Fraction nonzero per map (coverage) +subplot(2, 2, 3); +cov = mean(d ~= 0 & ~isnan(d), 1); +bar(1:nMaps, cov); +title('Fraction nonzero per map'); xlabel('map'); ylabel('fraction'); ylim([0 1]); + +han = struct('figure', fig, 'surface', []); + +% (4) Surface render of the mean map +if dorender && ~isempty(obj.brain_model) ... + && any(cellfun(@(m) strcmp(m.type,'surf'), obj.brain_model.models)) + try + mobj = mean(obj); + ax = subplot(2, 2, 4); + geom = load_surface_geom(obj.surface_space, 'inflated'); + hp = patch('Parent', ax, 'Faces', geom.faces_lh, 'Vertices', geom.vertices_lh, ... + 'EdgeColor', 'none', 'FaceColor', [.5 .5 .5], 'Tag', 'left'); + axis(ax, 'off', 'image', 'vis3d'); view(ax, 270, 0); + try, lightRestoreSingle(ax); catch, camlight(ax); end %#ok + material(ax, 'dull'); + render_on_surface(mobj, hp); + title(ax, 'Mean map (left lateral)'); + han.surface = hp; + catch err + warning('fmri_surface_data:plot:render', 'Surface render skipped: %s', err.message); + end +end +end diff --git a/CanlabCore/@fmri_surface_data/predict.m b/CanlabCore/@fmri_surface_data/predict.m new file mode 100644 index 00000000..3b159560 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/predict.m @@ -0,0 +1,52 @@ +function [cverr, stats, optout] = predict(obj, varargin) +% predict Cross-validated multivariate prediction on grayordinate data. +% +% :Usage: +% :: +% [cverr, stats, optout] = predict(obj, 'algorithm_name', 'cv_lassopcr', 'nfolds', 5) +% +% Runs CANlab's cross-validated prediction (fmri_data.predict) on surface/ +% grayordinate data. The algorithm operates on obj.dat and obj.Y exactly as for +% volumetric data, so all algorithms and options are supported unchanged; this +% method delegates to fmri_data.predict via a proxy (each grayordinate treated as +% a feature) and then remaps the geometry-bearing weight map back to an +% fmri_surface_data (so you can surface()/write() it). +% +% Set the outcome to predict in obj.Y before calling (as with fmri_data). +% +% :Inputs: +% **obj:** fmri_surface_data with obj.Y set (one value per map/observation). +% +% :Optional Inputs: +% All fmri_data.predict options ('algorithm_name', 'nfolds', 'error_type', ...). +% +% :Outputs: +% **cverr:** cross-validated error/accuracy (as fmri_data.predict). +% **stats:** stats struct; stats.weight_obj is remapped to an +% fmri_surface_data carrying the object's geometry. +% **optout:** optional algorithm outputs (passed through). +% +% :Examples: +% :: +% obj.Y = behavioral_scores(:); +% [err, stats] = predict(obj, 'algorithm_name', 'cv_lassopcr', 'nfolds', 5); +% surface(stats.weight_obj); % render the predictive weight map +% +% :See also: regress, fmri_data.predict, rebuild_like, fmri_surface_data + +if isempty(obj.Y) + error('fmri_surface_data:predict:noY', ... + 'Set obj.Y (one outcome value per map/observation) before calling predict.'); +end + +proxy = as_fmri_data_proxy(obj); + +[cverr, stats, optout] = predict(proxy, varargin{:}); + +% Remap the predictive weight map back to a surface object +if isfield(stats, 'weight_obj') && ~isempty(stats.weight_obj) ... + && size(stats.weight_obj.dat, 1) == size(obj.dat, 1) + stats.weight_obj = rebuild_like(obj, double(stats.weight_obj.dat)); + stats.weight_obj.image_names = {'predictive weights'}; +end +end diff --git a/CanlabCore/@fmri_surface_data/private/as_fmri_data_proxy.m b/CanlabCore/@fmri_surface_data/private/as_fmri_data_proxy.m new file mode 100644 index 00000000..9fb730ae --- /dev/null +++ b/CanlabCore/@fmri_surface_data/private/as_fmri_data_proxy.m @@ -0,0 +1,36 @@ +function proxy = as_fmri_data_proxy(obj) +% as_fmri_data_proxy Wrap a surface object as an fmri_data with a dummy 1-D volInfo. +% +% Lets geometry-agnostic fmri_data analysis methods (predict, ica, ttest, ...) be +% reused on grayordinate data: each grayordinate is treated as a "voxel" in a +% 1-D volume. The returned proxy carries the same .dat / .X / .Y / covariates so +% the analysis is identical; only spatial reconstruction (which the analysis does +% not need) is meaningless on the proxy. Map geometry-bearing results back to a +% surface object with rebuild_like. +% +% :Inputs: **obj:** an fmri_surface_data object. +% :Outputs: **proxy:** an fmri_data object [nGrayordinates x nMaps]. +% +% :See also: predict, ica, ttest, rebuild_like, fmri_surface_data + +nGray = size(obj.dat, 1); + +vi = struct('mat', eye(4), 'dim', [nGray 1 1], 'dt', [16 0], ... + 'xyzlist', [(1:nGray)' ones(nGray,1) ones(nGray,1)], 'nvox', nGray, ... + 'image_indx', true(nGray,1), 'wh_inmask', (1:nGray)', 'n_inmask', nGray, 'fname', ''); + +iv = image_vector; +iv.volInfo = vi; +iv.dat = double(obj.dat); +iv.removed_voxels = false(nGray, 1); +iv.removed_images = false(size(obj.dat, 2), 1); +iv.image_names = obj.image_names; + +proxy = fmri_data(iv); + +% Carry the per-map analysis annotations +if ~isempty(obj.Y), proxy.Y = obj.Y; end +if ~isempty(obj.X), try, proxy.X = obj.X; catch, end; end %#ok +if ~isempty(obj.covariates), proxy.covariates = obj.covariates; end +if ~isempty(obj.Y_names), proxy.Y_names = obj.Y_names; end +end diff --git a/CanlabCore/@fmri_surface_data/private/build_volinfo_subblock.m b/CanlabCore/@fmri_surface_data/private/build_volinfo_subblock.m new file mode 100644 index 00000000..c7297b88 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/private/build_volinfo_subblock.m @@ -0,0 +1,62 @@ +function [volInfo, rows] = build_volinfo_subblock(bm) +% Build an SPM-style volInfo describing ONLY the subcortical/volumetric voxel +% models of a CIFTI brain_model, plus the grayordinate row indices they occupy. +% +% Used by fmri_surface_data to (a) populate the inherited .volInfo slot for the +% volume sub-block (so volInfo-aware code and to_fmri_data work) and (b) export +% the subcortex to an fmri_data object. Returns empty if there is no volume part. +% +% :Inputs: +% **bm:** a brain_model struct (fmri_surface_data.brain_model / CIFTI diminfo{1}), +% with .models{i} (.type 'surf'|'vox', .start, .count, .voxlist 3xN 0-based) +% and .vol (.dims, .sform). +% +% :Outputs: +% **volInfo:** struct (.mat 1-based SPM affine, .dim, .dt, .xyzlist Nx3 1-based, +% .nvox, .image_indx, .wh_inmask, .n_inmask, .fname). [] if no volume. +% **rows:** column vector of row indices into the full grayordinate .dat that +% correspond, in order, to volInfo.xyzlist / volInfo.wh_inmask. +% +% :See also: fmri_surface_data, to_fmri_data, reconstruct_image, extract_vol_from_cifti + +volInfo = []; +rows = []; + +if isempty(bm) || ~isfield(bm, 'vol') || isempty(bm.vol) + return +end +voxmodels = find(cellfun(@(m) strcmp(m.type, 'vox'), bm.models)); +if isempty(voxmodels) + return +end + +dims = bm.vol.dims(:)'; + +% CIFTI sform maps 0-based IJK -> mm. SPM .mat maps 1-based voxel -> mm. +% Convert: mat(:,4) = sform(:,4) - sform(:,1:3)*[1;1;1]. +affine = bm.vol.sform; +affine(:, 4) = affine(:, 4) - sum(affine(:, 1:3), 2); + +allvox = zeros(0, 3); % Nx3, 1-based IJK, in grayordinate (voxlist) order +rows = zeros(0, 1); +for k = voxmodels(:)' + m = bm.models{k}; + vx = m.voxlist; % 3xN, 0-based + allvox = [allvox; vx' + 1]; %#ok Nx3, 1-based + rows = [rows; (m.start:(m.start + m.count - 1))']; %#ok +end + +ind = sub2ind(dims, allvox(:, 1), allvox(:, 2), allvox(:, 3)); % linear, voxlist order + +volInfo = struct(); +volInfo.mat = affine; +volInfo.dim = dims; +volInfo.dt = [16 0]; +volInfo.xyzlist = allvox; +volInfo.nvox = prod(dims); +volInfo.image_indx = false(prod(dims), 1); +volInfo.image_indx(ind) = true; +volInfo.wh_inmask = ind; % aligned with xyzlist / rows (NOT sorted) +volInfo.n_inmask = size(allvox, 1); +volInfo.fname = ''; +end diff --git a/CanlabCore/@fmri_surface_data/private/load_surface_geom.m b/CanlabCore/@fmri_surface_data/private/load_surface_geom.m new file mode 100644 index 00000000..ec279043 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/private/load_surface_geom.m @@ -0,0 +1,78 @@ +function geom = load_surface_geom(surface_space, surftype) +% load_surface_geom Load bundled L/R cortical meshes for a surface space. +% +% Returns faces/vertices for both hemispheres of a standard surface, loaded from +% the meshes bundled in CanlabCore/canlab_canonical_brains/Canonical_brains_surfaces/. +% No gifti toolbox is required (.mat meshes are load()ed; .surf.gii is read with +% canlab_read_gifti). +% +% :Inputs: +% **surface_space:** 'fsLR_32k' (32492 verts/hemi) or 'fsaverage_164k' (163842). +% **surftype:** 'inflated' (default), 'midthickness', 'sphere', 'veryinflated'. +% +% :Outputs: +% **geom:** struct with .vertices_lh/.faces_lh/.vertices_rh/.faces_rh (faces +% 1-based), .space, .surftype, .numvert. +% +% :See also: surface, fmri_surface_data, add_surface + +if nargin < 2 || isempty(surftype), surftype = 'inflated'; end + +switch surface_space + case 'fsLR_32k' + switch lower(surftype) + case {'inflated','veryinflated'} + fL = 'S12000.L.inflated_MSMAll.32k_fsl_LR.mat'; + fR = 'S12000.R.inflated_MSMAll.32k_fsl_LR.mat'; + case 'midthickness' + fL = 'S1200.L.midthickness_MSMAll.32k_fs_LR.surf.gii'; + fR = 'S1200.R.midthickness_MSMAll.32k_fs_LR.surf.gii'; + case 'sphere' + fL = 'S1200.L.sphere.32k_fs_LR.mat'; + fR = 'S1200.R.sphere.32k_fs_LR.mat'; + otherwise + error('load_surface_geom:surftype', 'Unknown fsLR_32k surftype: %s', surftype); + end + nv = 32492; + + case 'fsaverage_164k' + switch lower(surftype) + case {'inflated','veryinflated'} + fL = 'surf_freesurf_inflated_Left.mat'; + fR = 'surf_freesurf_inflated_Right.mat'; + otherwise + error('load_surface_geom:surftype', ... + 'fsaverage_164k currently supports surftype ''inflated'' only (got %s).', surftype); + end + nv = 163842; + + otherwise + error('load_surface_geom:space', ... + ['No bundled mesh for surface_space ''%s''. Supported: fsLR_32k, ' ... + 'fsaverage_164k. (vol2surf produces fsaverage_164k; native CIFTI is fsLR_32k.)'], ... + surface_space); +end + +[vL, faL] = local_load_mesh(fL); +[vR, faR] = local_load_mesh(fR); + +geom = struct('vertices_lh', vL, 'faces_lh', faL, ... + 'vertices_rh', vR, 'faces_rh', faR, ... + 'space', surface_space, 'surftype', surftype, 'numvert', nv); +end + + +% ------------------------------------------------------------------------- +function [vertices, faces] = local_load_mesh(fname) +p = which(fname); +if isempty(p), error('load_surface_geom:notfound', 'Mesh file not found on path: %s', fname); end +if endsWith(lower(p), '.gii') + g = canlab_read_gifti(p); + vertices = g.vertices; + faces = g.faces; % already 1-based +else + S = load(p); + vertices = S.vertices; + faces = S.faces; % .mat meshes store 1-based faces +end +end diff --git a/CanlabCore/@fmri_surface_data/private/obj_to_volume.m b/CanlabCore/@fmri_surface_data/private/obj_to_volume.m new file mode 100644 index 00000000..f806265b --- /dev/null +++ b/CanlabCore/@fmri_surface_data/private/obj_to_volume.m @@ -0,0 +1,27 @@ +function vol = obj_to_volume(obj) +% obj_to_volume Convert a surface/grayordinate object to a volumetric fmri_data. +% +% Used by rendering when the target is an arbitrary MNI surface (e.g. an addbrain +% pial surface) rather than the object's own mesh: the data is first projected to +% a volume so the standard image_vector.render_on_surface (which samples a volume +% at patch vertices) can be reused. +% +% - fsaverage_164k cortex -> surf2vol (CBIG RF) to MNI 2 mm. +% - volume-only grayordinate -> to_fmri_data (subcortex). +% - other spaces (e.g. fsLR_32k cortex) are not yet supported here. +% +% :See also: surf2vol, to_fmri_data, surface, render_on_surface + +if strcmp(obj.surface_space, 'fsaverage_164k') + vol = surf2vol(obj); +elseif ~isempty(obj.brain_model) && any(cellfun(@(m) strcmp(m.type,'vox'), obj.brain_model.models)) ... + && ~any(cellfun(@(m) strcmp(m.type,'surf'), obj.brain_model.models)) + vol = to_fmri_data(obj); +else + error('fmri_surface_data:obj_to_volume:space', ... + ['Cannot project surface_space ''%s'' to a volume for rendering on an arbitrary ' ... + 'MNI surface. Supported: fsaverage_164k (via surf2vol) or volume-only objects ' ... + '(via to_fmri_data). Render fsLR_32k data on its native mesh instead, or pass ' ... + 'an fsaverage object.'], obj.surface_space); +end +end diff --git a/CanlabCore/@fmri_surface_data/rebuild_like.m b/CanlabCore/@fmri_surface_data/rebuild_like.m new file mode 100644 index 00000000..2f354d2b --- /dev/null +++ b/CanlabCore/@fmri_surface_data/rebuild_like.m @@ -0,0 +1,49 @@ +function newobj = rebuild_like(obj, newdat) +% rebuild_like Build a new fmri_surface_data carrying obj's geometry, with new data. +% +% :Usage: +% :: +% newobj = rebuild_like(obj, newdat) +% +% Helper used by data-transforming methods (mean, ica, predict, threshold, ...) +% to wrap a new [grayordinates x maps] data matrix back into an +% fmri_surface_data that carries the original brain_model / geom / intent / +% surface_space -- the surface analogue of fmri_data's volInfo-based re-wrap. +% This avoids the inherited image_vector/fmri_data rebuild that would emit a +% volume object. +% +% The new data must have the same number of grayordinate rows as obj (the +% geometry is unchanged); the number of maps (columns) may differ. Per-map +% annotations (image_names/X/Y/...) are kept only if the map count is unchanged. +% +% :Inputs: +% **obj:** template fmri_surface_data object (geometry source). +% **newdat:** [nGrayordinates x K] numeric data. +% +% :Outputs: +% **newobj:** fmri_surface_data with .dat = single(newdat), same geometry. +% +% :See also: fmri_surface_data, mean, reconstruct_image + +if size(newdat, 1) ~= size(obj.dat, 1) + error('fmri_surface_data:rebuild_like:rowmismatch', ... + ['newdat has %d rows but the object has %d grayordinates. rebuild_like ' ... + 'preserves geometry; row count must match.'], size(newdat,1), size(obj.dat,1)); +end + +newobj = obj; +newobj.dat = single(newdat); + +K = size(newdat, 2); +newobj.removed_voxels = false(size(newdat, 1), 1); +newobj.removed_images = false(K, 1); + +% Drop per-map annotations if the number of maps changed (they no longer align) +if K ~= size(obj.dat, 2) + newobj.image_names = {}; + newobj.X = []; + newobj.Y = []; + newobj.covariates = []; + newobj.metadata_table = []; +end +end diff --git a/CanlabCore/@fmri_surface_data/reconstruct_image.m b/CanlabCore/@fmri_surface_data/reconstruct_image.m new file mode 100644 index 00000000..181490da --- /dev/null +++ b/CanlabCore/@fmri_surface_data/reconstruct_image.m @@ -0,0 +1,72 @@ +function out = reconstruct_image(obj) +% reconstruct_image Reconstruct surface + volume data from a grayordinate object. +% +% :Usage: +% :: +% out = reconstruct_image(surf_obj) +% +% Reconstructs the flat [grayordinates x maps] .dat back into its natural spatial +% representations: dense per-hemisphere vertex arrays for the cortical surface +% models (with the medial wall filled with NaN), and a 3-D/4-D volume for the +% subcortical voxel models. Unlike image_vector.reconstruct_image (which assumes +% a single volumetric space), this returns a struct, because grayordinate data +% spans surface and volume spaces. +% +% No replace_empty pre-step is needed: .dat is always the full grayordinate set +% (decision D5b). +% +% :Inputs: +% **obj:** an fmri_surface_data object. +% +% :Outputs: +% **out:** struct with fields (present only when that model exists): +% . dense [numvert x nMaps] per surface model, lowercased +% (e.g. .cortex_left, .cortex_right); medial wall = NaN. +% .volume [X x Y x Z x nMaps] subcortical volume (NaN outside). +% .volume_volInfo the SPM-style volInfo for .volume (1-based affine). +% .models the brain_model.models list (for reference). +% +% :Examples: +% :: +% s = fmri_surface_data(which('transcriptomic_gradients.dscalar.nii')); +% r = reconstruct_image(s); +% size(r.cortex_left) % [32492 x nMaps], medial wall NaN +% size(r.volume) % [91 109 91 x nMaps] +% +% :See also: fmri_surface_data, to_fmri_data, surface, render_on_surface + +bm = obj.brain_model; +if isempty(bm) + error('fmri_surface_data:reconstruct_image:nobm', 'Object has no brain_model.'); +end + +nMaps = size(obj.dat, 2); +out = struct(); +out.models = bm.models; + +% ---- Surface models -> dense per-hemisphere vertex arrays (medial wall NaN) ---- +for i = 1:numel(bm.models) + m = bm.models{i}; + if ~strcmp(m.type, 'surf'), continue; end + dense = nan(m.numvert, nMaps); + rows = m.start:(m.start + m.count - 1); + dense(m.vertlist + 1, :) = obj.dat(rows, :); % vertlist is 0-based + fld = matlab.lang.makeValidName(lower(m.struct)); + out.(fld) = dense; +end + +% ---- Volume models -> 3-D/4-D volume ---- +[vi, rows] = build_volinfo_subblock(bm); +if ~isempty(vi) && ~isempty(rows) + dims = vi.dim; + vol = nan([dims nMaps]); + nv = prod(dims); + for k = 1:nMaps + v = nan(nv, 1); + v(vi.wh_inmask) = obj.dat(rows, k); + vol(:, :, :, k) = reshape(v, dims); + end + out.volume = vol; + out.volume_volInfo = vi; +end +end diff --git a/CanlabCore/@fmri_surface_data/regress.m b/CanlabCore/@fmri_surface_data/regress.m new file mode 100644 index 00000000..4984bf19 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/regress.m @@ -0,0 +1,75 @@ +function bobj = regress(obj, varargin) +% regress Grayordinate-wise OLS regression of the maps onto a design matrix. +% +% :Usage: +% :: +% bobj = regress(obj) % uses obj.X +% bobj = regress(obj, X) % explicit design matrix +% +% Fits an ordinary least-squares regression at each grayordinate: for each +% grayordinate, the values across maps (columns of obj.dat) are regressed onto +% the design matrix X ([nMaps x p], one row per map/observation). Returns an +% fmri_surface_data whose maps are the p regression coefficients, with the +% t-statistics, p-values, and standard errors in .additional_info.statistic. +% +% This is a lightweight native implementation (no external toolbox). For the full +% GLM machinery (contrasts, diagnostics), export the volumetric part with +% to_fmri_data and use fmri_data.regress / glm_map. +% +% :Inputs: +% **obj:** fmri_surface_data with multiple maps. +% **X:** (optional) [nMaps x p] design matrix. Default: obj.X. An intercept +% is NOT added automatically -- include a column of ones if desired. +% +% :Outputs: +% **bobj:** fmri_surface_data; .dat is [nGrayordinates x p] coefficients. +% .additional_info.statistic has .t, .p, .se, .dfe (each [nGray x p] +% or scalar dfe), .betas. +% +% :Examples: +% :: +% obj.X = [ones(n,1) age(:)]; % intercept + age +% b = regress(obj); +% surface(get_wh_image(b, 2)); % render the age effect (2nd coefficient) +% +% :See also: ttest, predict, to_fmri_data, fmri_data.regress + +if nargin >= 2 && ~isempty(varargin{1}) && isnumeric(varargin{1}) + X = varargin{1}; +else + X = obj.X; +end +if isempty(X) + error('fmri_surface_data:regress:noX', ... + 'No design matrix. Set obj.X or pass X as the second argument.'); +end + +nMaps = size(obj.dat, 2); +if size(X, 1) ~= nMaps + error('fmri_surface_data:regress:size', ... + 'Design matrix has %d rows but the object has %d maps.', size(X,1), nMaps); +end + +p = size(X, 2); +dfe = nMaps - p; +if dfe < 1 + error('fmri_surface_data:regress:df', 'Not enough maps (%d) for %d regressors.', nMaps, p); +end + +Y = double(obj.dat).'; % [nMaps x nGray] +B = X \ Y; % [p x nGray] +resid = Y - X * B; +sigma2 = sum(resid.^2, 1) / dfe; % [1 x nGray] +XtXinv = inv(X' * X); %#ok small p x p +se = sqrt(diag(XtXinv) * sigma2); % [p x nGray] +tvals = B ./ se; % [p x nGray] +pvals = 2 * tcdf(-abs(tvals), dfe); + +bobj = rebuild_like(obj, B.'); % [nGray x p] coefficients +bobj.intent = 'dscalar'; +bobj.image_names = arrayfun(@(k) sprintf('beta%d', k), 1:p, 'UniformOutput', false)'; + +stat = struct('betas', B.', 't', tvals.', 'p', pvals.', 'se', se.', 'dfe', dfe); +bobj.additional_info.statistic = stat; +bobj.history{end+1} = sprintf('regress: OLS on %d-column design, %d maps (betas in .dat; t/p in additional_info.statistic)', p, nMaps); +end diff --git a/CanlabCore/@fmri_surface_data/remove_empty.m b/CanlabCore/@fmri_surface_data/remove_empty.m new file mode 100644 index 00000000..2497a6b6 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/remove_empty.m @@ -0,0 +1,26 @@ +function obj = remove_empty(obj, varargin) +% remove_empty (fmri_surface_data): no-op. Grayordinate data is already compact. +% +% :Usage: +% :: +% obj = remove_empty(obj) +% +% Unlike image_vector/fmri_data -- which store sparse 3-D volumes and drop +% all-zero / NaN voxels to save space -- fmri_surface_data holds CIFTI +% grayordinate data, which is already compact (the medial wall is excluded by +% construction and there are no out-of-brain voxels). The .dat matrix is ALWAYS +% the full [nGrayordinates x nMaps] set, in 1:1 row correspondence with +% .brain_model, and is never squeezed. +% +% This override therefore returns the object UNCHANGED. It exists so that (a) +% inherited methods that call remove_empty internally still compose, and (b) +% user code that calls remove_empty by habit gets a valid object back. The +% .removed_voxels / .removed_images vectors stay all-false. +% +% See docs/fmri_surface_data_design_plan.md (decision D5b). +% +% :See also: replace_empty, fmri_surface_data, get_wh_image + +% Intentionally a no-op. (varargin accepted and ignored for signature parity +% with image_vector/remove_empty.) +end diff --git a/CanlabCore/@fmri_surface_data/render_on_surface.m b/CanlabCore/@fmri_surface_data/render_on_surface.m new file mode 100644 index 00000000..a2984f66 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/render_on_surface.m @@ -0,0 +1,102 @@ +function out_handles = render_on_surface(obj, surface_handles, varargin) +% render_on_surface Color existing surface patches with grayordinate data. +% +% :Usage: +% :: +% render_on_surface(surf_obj, surface_handles) +% render_on_surface(surf_obj, surface_handles, 'clim', [-3 3], 'which_image', 2) +% +% Colors one or more existing surface patch handles using an fmri_surface_data +% object's per-vertex values. For each patch: +% - If the patch has the same vertex count as one of the object's cortical +% hemispheres (e.g. the matching fs_LR / fsaverage inflated, midthickness, or +% sphere mesh from addbrain), the per-vertex values are applied DIRECTLY +% (no resampling) -- true native-space rendering. Left/right is resolved by +% the patch Tag ('left'/'right') when present, else by vertex count. +% - Otherwise (an arbitrary MNI surface, e.g. addbrain('left')), the object is +% projected to a volume (surf2vol / to_fmri_data) and the standard +% image_vector.render_on_surface is reused to sample the volume at the patch +% vertices. +% +% :Inputs: +% **obj:** an fmri_surface_data object. +% **surface_handles:** vector of patch handles (e.g. from addbrain or surface). +% +% :Optional Inputs: +% **'which_image':** map (column) to render. Default 1. +% **'clim':** [lo hi] color limits. Default symmetric from the data. +% **'pos_colormap' / 'neg_colormap':** [n x 3] colormaps (default hot / cool). +% Other options are passed through to image_vector.render_on_surface on the +% volume fallback path. +% +% :Outputs: +% **out_handles:** the surface handles (colored in place). +% +% :See also: surface, canlab_surface_vertexcolors, image_vector.render_on_surface + +which_image = 1; +clim = []; +poscm = []; +negcm = []; +i = 1; +passthrough = {}; +while i <= numel(varargin) + key = varargin{i}; + if ischar(key) || isstring(key) + switch lower(char(key)) + case 'which_image', which_image = varargin{i+1}; i = i + 2; continue + case 'clim', clim = varargin{i+1}; i = i + 2; continue + case 'pos_colormap', poscm = varargin{i+1}; i = i + 2; continue + case 'neg_colormap', negcm = varargin{i+1}; i = i + 2; continue + end + end + passthrough{end+1} = varargin{i}; %#ok + i = i + 1; +end + +% Dense per-hemisphere data (medial wall = NaN) +r = reconstruct_image(obj); +Ldat = []; Rdat = []; +if isfield(r, 'cortex_left'), Ldat = r.cortex_left(:, which_image); end +if isfield(r, 'cortex_right'), Rdat = r.cortex_right(:, which_image); end +nL = numel(Ldat); nR = numel(Rdat); + +% Color limits shared across hemispheres +if isempty(clim) + allv = [Ldat; Rdat]; + allv = allv(~isnan(allv) & allv ~= 0); + m = max(abs(allv)); if isempty(m) || m == 0, m = 1; end + clim = [-m m]; +end + +for h = 1:numel(surface_handles) + hp = surface_handles(h); + nv = size(get(hp, 'Vertices'), 1); + tag = ''; + try, tag = lower(get(hp, 'Tag')); catch, end %#ok + + isLeft = contains(tag, 'left') || contains(tag, ' l ') || endsWith(tag, ' l'); + isRight = contains(tag, 'right') || endsWith(tag, ' r'); + + if nv == nL && (isLeft || (~isRight && ~isempty(Ldat))) + local_apply(hp, Ldat, clim, poscm, negcm); + elseif nv == nR && (isRight || ~isempty(Rdat)) + local_apply(hp, Rdat, clim, poscm, negcm); + else + % Arbitrary surface: project to volume and reuse image_vector renderer + vol = obj_to_volume(obj); + render_on_surface(vol, surface_handles, passthrough{:}); + out_handles = surface_handles; + return + end +end + +out_handles = surface_handles; +end + + +% ------------------------------------------------------------------------- +function local_apply(hp, vals, clim, poscm, negcm) +rgb = canlab_surface_vertexcolors(vals, clim, poscm, negcm); +set(hp, 'FaceVertexCData', rgb, 'FaceColor', 'interp', 'EdgeColor', 'none', 'FaceAlpha', 1); +end diff --git a/CanlabCore/@fmri_surface_data/reparse_contiguous.m b/CanlabCore/@fmri_surface_data/reparse_contiguous.m new file mode 100644 index 00000000..002b6792 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/reparse_contiguous.m @@ -0,0 +1,91 @@ +function [obj, ncl] = reparse_contiguous(obj, varargin) +% reparse_contiguous Label contiguous clusters of grayordinates (mesh + volume). +% +% :Usage: +% :: +% [obj, ncl] = reparse_contiguous(obj) +% [obj, ncl] = reparse_contiguous(obj, 'which_image', 2) +% +% Finds connected components among the "active" (nonzero, non-NaN) grayordinates +% and stores an integer cluster label per grayordinate in obj.brain_model.cluster +% (0 = not in any cluster). For cortical-surface models, contiguity is computed +% on the cortical mesh edge graph (using the bundled mesh for the object's +% surface_space and MATLAB's built-in graph/conncomp -- no external toolbox). For +% subcortical voxel models, contiguity is 26-connected in the volume (spm_clusters). +% +% This is the surface analogue of image_vector.reparse_contiguous and is the +% basis for the region() conversion (a later milestone). +% +% :Optional Inputs: +% **'which_image':** which map (column) defines "active" grayordinates. Default 1. +% +% :Outputs: +% **obj:** the object with obj.brain_model.cluster populated [nGrayordinates x 1]. +% **ncl:** total number of clusters found. +% +% :See also: fmri_surface_data, load_surface_geom, region, reconstruct_image + +which_image = 1; +for i = 1:2:numel(varargin) + switch lower(varargin{i}) + case 'which_image', which_image = varargin{i+1}; + otherwise, error('reparse_contiguous:badopt', 'Unknown option: %s', varargin{i}); + end +end + +d = double(obj.dat(:, which_image)); +active = d ~= 0 & ~isnan(d); +cluster = zeros(size(obj.dat, 1), 1); +offset = 0; + +models = obj.brain_model.models; + +for mi = 1:numel(models) + m = models{mi}; + rows = (m.start:(m.start + m.count - 1))'; + act_local = active(rows); + if ~any(act_local), continue; end + + if strcmp(m.type, 'surf') + F = local_hemi_faces(obj.surface_space, m.struct); + G = local_mesh_graph(F, m.numvert); + meshverts = m.vertlist(:) + 1; % 1-based, aligned with rows + active_nodes = meshverts(act_local); + comp = conncomp(subgraph(G, active_nodes)); % component id per active node + cluster(rows(act_local)) = comp(:) + offset; + offset = offset + max(comp); + + else % voxel model: 26-connected components in the volume + ijk = m.voxlist(:, act_local); % 3 x nactive (0-based) + if isempty(ijk), continue; end + a = spm_clusters(ijk + 1); % 1-based voxel coords + cluster(rows(act_local)) = a(:) + offset; + offset = offset + max(a); + end +end + +obj.brain_model.cluster = cluster; +ncl = offset; +obj.history{end+1} = sprintf('reparse_contiguous: %d clusters (mesh + volume, image %d)', ncl, which_image); +end + + +% ------------------------------------------------------------------------- +function F = local_hemi_faces(surface_space, structname) +geom = load_surface_geom(surface_space, 'inflated'); % faces shared across surftypes +if contains(upper(structname), 'LEFT') + F = geom.faces_lh; +else + F = geom.faces_rh; +end +end + + +% ------------------------------------------------------------------------- +function G = local_mesh_graph(F, numvert) +% Build an undirected graph over numvert mesh vertices from triangle faces. +E = [F(:, [1 2]); F(:, [2 3]); F(:, [1 3])]; +E = sort(E, 2); +E = unique(E, 'rows'); +G = graph(E(:, 1), E(:, 2), [], numvert); +end diff --git a/CanlabCore/@fmri_surface_data/replace_empty.m b/CanlabCore/@fmri_surface_data/replace_empty.m new file mode 100644 index 00000000..4d0a6d1b --- /dev/null +++ b/CanlabCore/@fmri_surface_data/replace_empty.m @@ -0,0 +1,23 @@ +function obj = replace_empty(obj, varargin) +% replace_empty (fmri_surface_data): no-op. .dat is always full-size. +% +% :Usage: +% :: +% obj = replace_empty(obj) +% +% For image_vector/fmri_data, replace_empty re-expands a space-reduced .dat back +% to its original size (re-inserting removed voxels/images as zeros) so the data +% can be reconstructed into a volume. fmri_surface_data never reduces .dat -- it +% always holds the full [nGrayordinates x nMaps] grayordinate set in 1:1 row +% correspondence with .brain_model -- so there is nothing to re-insert. +% +% This override returns the object UNCHANGED, which removes the entire class of +% "forgot to replace_empty before reconstructing" bugs. Methods such as +% reconstruct_image / write / cat therefore need no replace_empty pre-step. +% +% See docs/fmri_surface_data_design_plan.md (decision D5b). +% +% :See also: remove_empty, fmri_surface_data, reconstruct_image + +% Intentionally a no-op. +end diff --git a/CanlabCore/@fmri_surface_data/surf2vol.m b/CanlabCore/@fmri_surface_data/surf2vol.m new file mode 100644 index 00000000..5f1f96d0 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/surf2vol.m @@ -0,0 +1,131 @@ +function vol = surf2vol(obj, varargin) +% surf2vol Project an fsaverage surface object back to an MNI152 volume (fmri_data). +% +% :Usage: +% :: +% vol = surf2vol(surf_obj) +% vol = surf2vol(surf_obj, 'reference', fmri_data_obj) +% +% Projects cortical-surface data on the fsaverage 164k surface into an MNI152 +% volume, returning an fmri_data object (which can then be written to .nii with +% its write method, montaged, etc.). Uses the same vendored CBIG RF-ANTs +% MNI152<->fsaverage per-vertex coordinates as vol2surf, scattering each vertex's +% value into its MNI voxel and averaging co-located vertices (accumarray). Fully +% native -- no FreeSurfer / Connectome Workbench. +% +% This is the inverse of vol2surf and is self-consistent with it. It requires an +% fsaverage_164k object (the CBIG warp is fsaverage-based). For the subcortical +% (volumetric) part of a grayordinate object, use to_fmri_data instead. +% +% :Inputs: +% **obj:** an fmri_surface_data object in surface_space 'fsaverage_164k' +% (e.g. produced by vol2surf), with CORTEX_LEFT / CORTEX_RIGHT models. +% +% :Optional Inputs: +% **'reference':** an image_vector/fmri_data whose volInfo (mat + dim) defines +% the target grid. Default: MNI152 2 mm, [91 109 91]. +% **'interp':** 'nearest' (default) -- scatter assignment is nearest-voxel. +% +% :Outputs: +% **vol:** fmri_data over the cortical voxels hit by the projection +% [n_hit_voxels x nMaps], in the target MNI space. +% +% :Examples: +% :: +% s = vol2surf(fmri_data(which('weights_NSF_grouppred_cvpcr.img'))); +% v = surf2vol(s); +% % v.write('fname', '/tmp/back.nii'); +% +% :See also: vol2surf, to_fmri_data, fmri_surface_data + +% ---- Options ---- +refobj = []; +for i = 1:2:numel(varargin) + switch lower(varargin{i}) + case 'reference', refobj = varargin{i+1}; + case 'interp' % nearest only in v1; accepted for API parity + otherwise, error('surf2vol:badopt', 'Unknown option: %s', varargin{i}); + end +end + +if ~strcmp(obj.surface_space, 'fsaverage_164k') + error('surf2vol:space', ... + ['surf2vol requires an fsaverage_164k object (the CBIG warp is fsaverage-based). ' ... + 'This object is "%s". For the subcortical/volumetric part of a grayordinate ' ... + 'object, use to_fmri_data instead.'], obj.surface_space); +end + +% ---- Target grid ---- +if ~isempty(refobj) && ~isempty(refobj.volInfo) && isfield(refobj.volInfo, 'mat') + tmat = refobj.volInfo.mat; + dims = refobj.volInfo.dim; +else + % Standard MNI152 2 mm grid (1-based SPM .mat), matching the CIFTI subcortical grid + dims = [91 109 91]; + tmat = [-2 0 0 92; 0 2 0 -128; 0 0 2 -74; 0 0 0 1]; +end +nvox = prod(dims); + +% ---- Pull cortical hemisphere data ---- +[lh_dat, rh_dat] = local_hemi_data(obj); +nMaps = size(obj.dat, 2); + +NV = 163842; +L = load(canlab_cbig_warp_path('lh_ras')); lh_ras = L.ras; +R = load(canlab_cbig_warp_path('rh_ras')); rh_ras = R.ras; + +acc = zeros(nvox, nMaps); +cnt = zeros(nvox, 1); +for h = 1:2 + if h == 1, ras = lh_ras; hd = lh_dat; else, ras = rh_ras; hd = rh_dat; end + vox = round(tmat \ [ras; ones(1, NV)]); % 1-based voxel indices + in = vox(1,:) >= 1 & vox(1,:) <= dims(1) & ... + vox(2,:) >= 1 & vox(2,:) <= dims(2) & ... + vox(3,:) >= 1 & vox(3,:) <= dims(3); + lin = sub2ind(dims, vox(1,in), vox(2,in), vox(3,in))'; + cnt = cnt + accumarray(lin, 1, [nvox 1]); + for k = 1:nMaps + vk = hd(in, k); + acc(:, k) = acc(:, k) + accumarray(lin, double(vk), [nvox 1]); + end +end + +inmask = cnt > 0; +datm = acc(inmask, :) ./ cnt(inmask); + +% ---- Build fmri_data ---- +[ix, iy, iz] = ind2sub(dims, find(inmask)); +iv = image_vector; +iv.volInfo = struct('mat', tmat, 'dim', dims, 'dt', [16 0], ... + 'xyzlist', [ix iy iz], 'nvox', nvox, ... + 'image_indx', inmask, 'wh_inmask', find(inmask), ... + 'n_inmask', nnz(inmask), 'fname', ''); +iv.dat = single(datm); +iv.removed_voxels = false(size(iv.dat, 1), 1); +iv.removed_images = false(size(iv.dat, 2), 1); +iv.image_names = obj.image_names; +iv.history = obj.history; +iv.history{end+1} = sprintf('surf2vol: projected fsaverage_164k -> MNI %dx%dx%d (%d cortical voxels)', ... + dims(1), dims(2), dims(3), nnz(inmask)); + +vol = fmri_data(iv); +end + + +% ========================================================================= +function [lh_dat, rh_dat] = local_hemi_data(obj) +NV = 163842; +lh_dat = []; rh_dat = []; +for i = 1:numel(obj.brain_model.models) + m = obj.brain_model.models{i}; + if ~strcmp(m.type, 'surf'), continue; end + rows = m.start:(m.start + m.count - 1); + dense = zeros(m.numvert, size(obj.dat, 2)); + dense(m.vertlist + 1, :) = double(obj.dat(rows, :)); + if strcmpi(m.struct, 'CORTEX_LEFT'), lh_dat = dense; end + if strcmpi(m.struct, 'CORTEX_RIGHT'), rh_dat = dense; end +end +if isempty(lh_dat) || isempty(rh_dat) || size(lh_dat,1) ~= NV + error('surf2vol:models', 'Expected CORTEX_LEFT and CORTEX_RIGHT models with %d vertices.', NV); +end +end diff --git a/CanlabCore/@fmri_surface_data/surface.m b/CanlabCore/@fmri_surface_data/surface.m new file mode 100644 index 00000000..6c974b6f --- /dev/null +++ b/CanlabCore/@fmri_surface_data/surface.m @@ -0,0 +1,123 @@ +function han = surface(obj, varargin) +% surface Render grayordinate/surface data on cortical surfaces. +% +% :Usage: +% :: +% han = surface(obj) % native inflated, 4 views +% han = surface(obj, 'surftype', 'midthickness') % native, other mesh +% han = surface(obj, 'which_image', 2, 'clim', [-3 3]) +% han = surface(obj, 'existingsurface', patch_handles) % render on given patches +% han = surface(obj, 'mni_surface', 'left') % render on an addbrain MNI surface +% +% Renders an fmri_surface_data object on cortical surfaces, mirroring +% image_vector.surface. Three modes: +% +% 1. NATIVE (default): loads the bundled mesh that matches the object's +% surface_space (fs_LR-32k or fsaverage-164k) and colors vertices DIRECTLY +% from the data -- no resampling. Produces a 4-panel figure (L/R x +% lateral/medial). The medial wall and zero values render gray. +% +% 2. EXISTING SURFACE ('existingsurface', han): colors patch handles you +% already have (e.g. from addbrain or a prior surface call). Matching-space +% meshes are colored directly; arbitrary MNI surfaces are handled by +% projecting to a volume (see render_on_surface). +% +% 3. MNI SURFACE ('mni_surface', name): creates an addbrain surface (e.g. +% 'left', 'right', 'hcp inflated') and renders onto it, projecting to a +% volume when the surface is not the object's native mesh. +% +% :Optional Inputs: +% **'surftype':** 'inflated' (default), 'midthickness', 'sphere' (fs_LR). +% **'which_image':** map (column) to render. Default 1. +% **'clim':** [lo hi] color limits (default symmetric from data). +% **'pos_colormap' / 'neg_colormap':** [n x 3] colormaps (default hot / cool). +% **'existingsurface':** vector of patch handles to color. +% **'mni_surface':** an addbrain keyword (string). +% +% :Outputs: +% **han:** struct with fields .figure, .axes, .surfaces (graphics handles). +% +% :Examples: +% :: +% s = fmri_surface_data(which('transcriptomic_gradients.dscalar.nii')); +% surface(s, 'which_image', 1); +% +% v = fmri_data(which('weights_NSF_grouppred_cvpcr.img')); +% surface(vol2surf(v)); % native fsaverage render +% surface(vol2surf(v), 'mni_surface', 'left'); % on an addbrain MNI surface +% +% :See also: render_on_surface, load_surface_geom, addbrain, fmri_surface_data + +surftype = 'inflated'; +which_image = 1; +clim = []; +poscm = []; +negcm = []; +existing = []; +mni_surface = ''; + +i = 1; +while i <= numel(varargin) + switch lower(varargin{i}) + case 'surftype', surftype = varargin{i+1}; i = i + 2; + case 'which_image', which_image = varargin{i+1}; i = i + 2; + case 'clim', clim = varargin{i+1}; i = i + 2; + case 'pos_colormap', poscm = varargin{i+1}; i = i + 2; + case 'neg_colormap', negcm = varargin{i+1}; i = i + 2; + case {'existingsurface','surface_handles'}, existing = varargin{i+1}; i = i + 2; + case 'mni_surface', mni_surface = varargin{i+1}; i = i + 2; + otherwise + error('fmri_surface_data:surface:badopt', 'Unknown option: %s', num2str(varargin{i})); + end +end + +ropts = {'which_image', which_image, 'clim', clim, 'pos_colormap', poscm, 'neg_colormap', negcm}; + +% ---- Mode 2: existing handles ---- +if ~isempty(existing) + render_on_surface(obj, existing, ropts{:}); + han = struct('figure', ancestor(existing(1), 'figure'), 'axes', [], 'surfaces', existing); + return +end + +% ---- Mode 3: addbrain MNI surface ---- +if ~isempty(mni_surface) + hp = addbrain(mni_surface); + render_on_surface(obj, hp, ropts{:}); + han = struct('figure', gcf, 'axes', gca, 'surfaces', hp); + return +end + +% ---- Mode 1: native 4-panel render ---- +geom = load_surface_geom(obj.surface_space, surftype); + +panels = { 'lh', [270 0], 1; ... % left lateral + 'lh', [90 0], 2; ... % left medial + 'rh', [90 0], 3; ... % right lateral + 'rh', [270 0], 4 }; % right medial + +fig = figure('Color', 'w', 'Name', sprintf('fmri_surface_data: %s (%s)', obj.surface_space, surftype)); +surfaces = gobjects(1, 4); +axes_h = gobjects(1, 4); +for p = 1:4 + ax = subplot(2, 2, panels{p, 3}); + hold(ax, 'on'); + if strcmp(panels{p, 1}, 'lh') + V = geom.vertices_lh; F = geom.faces_lh; tag = 'left'; + else + V = geom.vertices_rh; F = geom.faces_rh; tag = 'right'; + end + hp = patch('Parent', ax, 'Faces', F, 'Vertices', V, 'EdgeColor', 'none', ... + 'FaceColor', [.5 .5 .5], 'Tag', tag, 'SpecularStrength', .2, 'SpecularExponent', 200); + axis(ax, 'off', 'image', 'vis3d'); + view(ax, panels{p, 2}(1), panels{p, 2}(2)); + try, lightRestoreSingle(ax); catch, camlight(ax); end %#ok + material(ax, 'dull'); + surfaces(p) = hp; + axes_h(p) = ax; +end + +render_on_surface(obj, surfaces, ropts{:}); + +han = struct('figure', fig, 'axes', axes_h, 'surfaces', surfaces); +end diff --git a/CanlabCore/@fmri_surface_data/surface_region.m b/CanlabCore/@fmri_surface_data/surface_region.m new file mode 100644 index 00000000..b2bd1c4e --- /dev/null +++ b/CanlabCore/@fmri_surface_data/surface_region.m @@ -0,0 +1,116 @@ +function reg = surface_region(obj, varargin) +% surface_region Summarize contiguous grayordinate clusters as region structs. +% +% :Usage: +% :: +% reg = surface_region(obj) +% reg = surface_region(obj, 'which_image', 2) +% +% Converts the contiguous clusters of an fmri_surface_data (active = nonzero, +% non-NaN grayordinates) into a struct array of per-cluster summaries -- the +% surface analogue of region(). Clusters are found with reparse_contiguous (mesh +% edge graph for cortex, 26-connectivity for subcortex). Cortical-cluster +% centroids are computed from the bundled mesh (midthickness if available); +% subcortical-cluster centroids come from voxel mm coordinates. +% +% (A full CANlab region object is volume-centric; this returns a lightweight +% surface-aware struct. For the subcortical part you can also use +% region(to_fmri_data(obj)).) +% +% :Optional Inputs: +% **'which_image':** which map defines "active" grayordinates. Default 1. +% +% :Outputs: +% **reg:** struct array, one element per cluster, with fields: +% .struct brain structure name (e.g. 'CORTEX_LEFT') +% .type 'surf' or 'vox' +% .cluster_id integer cluster label +% .grayord_rows row indices into .dat for this cluster +% .vertex_indices 0-based mesh vertices (surf clusters) or [] +% .XYZmm [3 x 1] centroid in mm +% .numVox number of grayordinates in the cluster +% .val mean value (which_image) over the cluster +% +% :See also: reparse_contiguous, region, to_fmri_data, fmri_surface_data + +which_image = 1; +for i = 1:2:numel(varargin) + if strcmpi(varargin{i}, 'which_image'), which_image = varargin{i+1}; end +end + +obj = reparse_contiguous(obj, 'which_image', which_image); +cluster = obj.brain_model.cluster; +d = double(obj.dat(:, which_image)); + +reg = struct('struct', {}, 'type', {}, 'cluster_id', {}, 'grayord_rows', {}, ... + 'vertex_indices', {}, 'XYZmm', {}, 'numVox', {}, 'val', {}); + +if isempty(cluster) || all(cluster == 0), return; end + +% mesh vertices for centroids (cortex) +geom = []; +% subcortical affine for centroids (vox) +hasvol = isfield(obj.brain_model, 'vol') && ~isempty(obj.brain_model.vol); + +for mi = 1:numel(obj.brain_model.models) + m = obj.brain_model.models{mi}; + rows = (m.start:(m.start + m.count - 1))'; + cl_local = cluster(rows); + ids = unique(cl_local(cl_local > 0)); + + if strcmp(m.type, 'surf') && ~isempty(ids) + if isempty(geom) + try, geom = load_geom_safe(obj.surface_space); catch, geom = []; end + end + if contains(upper(m.struct), 'LEFT') && ~isempty(geom), Vh = geom.vertices_lh; + elseif ~isempty(geom), Vh = geom.vertices_rh; else, Vh = []; end + end + + for c = ids(:)' + sel = find(cl_local == c); + gr = rows(sel); + s = struct(); + s.struct = m.struct; + s.type = m.type; + s.cluster_id = c; + s.grayord_rows = gr; + s.numVox = numel(gr); + s.val = mean(d(gr)); + if strcmp(m.type, 'surf') + verts = m.vertlist(sel) + 1; % 1-based mesh verts + s.vertex_indices = m.vertlist(sel); % store 0-based + if exist('Vh', 'var') && ~isempty(Vh) + s.XYZmm = mean(Vh(verts, :), 1)'; + else + s.XYZmm = [NaN; NaN; NaN]; + end + else + s.vertex_indices = []; + ijk = m.voxlist(:, sel) + 1; % 1-based + if hasvol + mm = obj.brain_model.vol.sform * [mean(ijk - 1, 2); 1]; % sform is 0-based + s.XYZmm = mm(1:3); + else + s.XYZmm = [NaN; NaN; NaN]; + end + end + reg(end+1) = s; %#ok + end +end +end + + +% ------------------------------------------------------------------------- +function g = load_geom_safe(space) +% Use midthickness for accurate centroids; fall back to inflated. +try + g = call_private_geom(space, 'midthickness'); +catch + g = call_private_geom(space, 'inflated'); +end +end + +function g = call_private_geom(space, surftype) +% load_surface_geom is private to @fmri_surface_data; reachable from class methods. +g = load_surface_geom(space, surftype); +end diff --git a/CanlabCore/@fmri_surface_data/threshold.m b/CanlabCore/@fmri_surface_data/threshold.m new file mode 100644 index 00000000..a956e33b --- /dev/null +++ b/CanlabCore/@fmri_surface_data/threshold.m @@ -0,0 +1,76 @@ +function obj = threshold(obj, thresh, varargin) +% threshold Raw-value threshold of grayordinate data (zeros sub-threshold values). +% +% :Usage: +% :: +% obj = threshold(obj, [lo hi]) +% obj = threshold(obj, t, 'positive') +% obj = threshold(obj, t, 'negative') +% +% Surface analogue of the raw-value branch of image thresholding: grayordinate +% values inside the threshold range are set to 0; .dat keeps its full size (D5b). +% +% Cluster-extent thresholding is available via the 'k' option (mesh-graph +% connected components for cortex, 26-connectivity for subcortex; see +% reparse_contiguous). +% +% :Inputs: +% **obj:** an fmri_surface_data object. +% **thresh:** scalar t (keeps |value| >= t, two-tailed) or [lo hi] (keeps +% values <= lo OR >= hi; i.e. removes the open interval (lo,hi)). +% +% :Optional Inputs: +% **'positive':** keep only values >= t (with scalar t). +% **'negative':** keep only values <= -abs(t) (with scalar t). +% **'k', N:** cluster-extent threshold: after the raw-value threshold, +% remove contiguous clusters smaller than N grayordinates +% (applied per map column). +% +% :Outputs: +% **obj:** thresholded object (sub-threshold grayordinates zeroed). +% +% :See also: fmri_surface_data, apply_mask, reparse_contiguous + +direction = 'two'; +if any(strcmpi(varargin, 'positive')), direction = 'pos'; end +if any(strcmpi(varargin, 'negative')), direction = 'neg'; end +kextent = 0; +for i = 1:numel(varargin) + if (ischar(varargin{i}) || isstring(varargin{i})) && strcmpi(varargin{i}, 'k') && i < numel(varargin) + kextent = varargin{i+1}; + end +end + +d = double(obj.dat); + +if numel(thresh) == 2 + lo = thresh(1); hi = thresh(2); + keep = d <= lo | d >= hi; +else + t = abs(thresh); + switch direction + case 'pos', keep = d >= t; + case 'neg', keep = d <= -t; + otherwise, keep = abs(d) >= t; + end +end + +d(~keep) = 0; +obj.dat = single(d); + +% Cluster-extent: drop clusters smaller than k grayordinates, per map +if kextent > 1 + for col = 1:size(obj.dat, 2) + tmp = reparse_contiguous(obj, 'which_image', col); + cl = tmp.brain_model.cluster; + if isempty(cl), continue; end + szs = accumarray(cl(cl > 0), 1, [max(cl) 1]); + small = find(szs < kextent); + drop = ismember(cl, small); + dd = obj.dat(:, col); dd(drop) = 0; obj.dat(:, col) = dd; + end + obj.history{end+1} = sprintf('threshold cluster-extent k>=%d applied', kextent); +end + +obj.history{end+1} = sprintf('threshold (raw-value): kept %.1f%% of values', 100*mean(keep(:))); +end diff --git a/CanlabCore/@fmri_surface_data/to_fmri_data.m b/CanlabCore/@fmri_surface_data/to_fmri_data.m new file mode 100644 index 00000000..37e829f8 --- /dev/null +++ b/CanlabCore/@fmri_surface_data/to_fmri_data.m @@ -0,0 +1,49 @@ +function fmri_obj = to_fmri_data(obj) +% to_fmri_data Export the subcortical/volumetric grayordinates to an fmri_data object. +% +% :Usage: +% :: +% fmri_obj = to_fmri_data(surf_obj) +% +% Returns the volumetric (subcortical / cerebellar) part of a grayordinate +% fmri_surface_data object as a standard CANlab fmri_data object in the CIFTI +% volume's MNI space. Surface (cortical) grayordinates have no voxel coordinates +% and are dropped here -- use surf2vol (M4) to project cortex into a volume. +% +% The resulting fmri_data can be written to a NIfTI (.nii) with its write +% method, montaged, resampled, etc. like any volumetric object. +% +% :Inputs: +% **obj:** an fmri_surface_data object with a volumetric sub-block +% (brain_model.vol + one or more 'vox' models). +% +% :Outputs: +% **fmri_obj:** an fmri_data object [n_subcortical_voxels x nMaps]. +% +% :Examples: +% :: +% s = fmri_surface_data(which('Gordon333.32k_fs_LR_Tian_Subcortex_S2.dlabel.nii')); +% vol = to_fmri_data(s); +% % vol.write('sample_filename', '/tmp/subctx.nii'); % to disk +% +% :See also: fmri_surface_data, reconstruct_image, build_volinfo_subblock, surf2vol + +[vi, rows] = build_volinfo_subblock(obj.brain_model); +if isempty(vi) || isempty(rows) + error('fmri_surface_data:to_fmri_data:novolume', ... + ['This object has no volumetric (subcortical) grayordinates to export. ' ... + 'Use surf2vol to project cortical surface data into a volume.']); +end + +iv = image_vector; +iv.volInfo = vi; +iv.dat = single(obj.dat(rows, :)); +iv.removed_voxels = false(size(iv.dat, 1), 1); +iv.removed_images = false(size(iv.dat, 2), 1); +iv.image_names = obj.image_names; +iv.history = obj.history; +iv.history{end+1} = sprintf('to_fmri_data: extracted %d subcortical voxels x %d maps from %s', ... + size(iv.dat,1), size(iv.dat,2), class(obj)); + +fmri_obj = fmri_data(iv); +end diff --git a/CanlabCore/@fmri_surface_data/ttest.m b/CanlabCore/@fmri_surface_data/ttest.m new file mode 100644 index 00000000..2947f77b --- /dev/null +++ b/CanlabCore/@fmri_surface_data/ttest.m @@ -0,0 +1,49 @@ +function sobj = ttest(obj, varargin) +% ttest Voxel-wise (grayordinate-wise) one-sample t-test across maps. +% +% :Usage: +% :: +% sobj = ttest(obj) +% sobj = ttest(obj, pthresh, k) +% +% Performs a one-sample t-test at each grayordinate across the maps (columns) of +% obj.dat, mirroring fmri_data.ttest. Computed by delegating to fmri_data.ttest +% on a proxy (each grayordinate treated as a voxel), so the statistics are +% identical; the result is returned as an fmri_surface_data carrying the t-map in +% .dat and the parallel statistics in .additional_info.statistic (.t, .p, .ste, +% .sig, .dfe). Any extra arguments (e.g. threshold) are passed through. +% +% (A dedicated fmri_surface_statistic_image subclass with native threshold() +% support is planned; for now use the .additional_info.statistic fields.) +% +% :Inputs: +% **obj:** fmri_surface_data with multiple maps (columns = observations). +% +% :Outputs: +% **sobj:** fmri_surface_data; .dat is the t-statistic per grayordinate. +% +% :Examples: +% :: +% all_subs = cat(sub1, sub2, sub3, ...); +% t = ttest(all_subs); +% surface(t, 'clim', [-5 5]); +% +% :See also: regress, cat, surface, fmri_surface_data + +proxy = as_fmri_data_proxy(obj); +st = ttest(proxy, varargin{:}); % -> statistic_image + +sobj = rebuild_like(obj, double(st.dat)); +sobj.intent = 'dscalar'; +sobj.image_names = {'t'}; + +stat = struct(); +stat.t = double(st.dat); +for f = {'p', 'ste', 'sig', 'dfe', 'N'} + if isprop(st, f{1}) || isfield(st, f{1}) + try, stat.(f{1}) = st.(f{1}); catch, end %#ok + end +end +sobj.additional_info.statistic = stat; +sobj.history{end+1} = sprintf('ttest across %d maps (t-map in .dat; p/ste/sig in additional_info.statistic)', size(obj.dat,2)); +end diff --git a/CanlabCore/@fmri_surface_data/write.m b/CanlabCore/@fmri_surface_data/write.m new file mode 100644 index 00000000..09ef1bff --- /dev/null +++ b/CanlabCore/@fmri_surface_data/write.m @@ -0,0 +1,168 @@ +function write(obj, varargin) +% write Write an fmri_surface_data object to a CIFTI-2 (.nii) or GIFTI (.gii) file. +% +% :Usage: +% :: +% write(obj) % writes to obj.fullpath +% write(obj, '/path/out.dscalar.nii') % CIFTI-2 (dscalar/dtseries/dlabel) +% write(obj, 'fname', '/path/out.func.gii') % GIFTI +% +% Dispatches on the output extension to the native writers canlab_write_cifti / +% canlab_write_gifti (M1) -- no external toolbox is required. The CIFTI maps +% dimension is rebuilt from the object's intent + image_names / label_table / +% series_info. If the object was loaded from a CIFTI and its grayordinate layout +% is unchanged, the original CIFTI XML is re-emitted faithfully; otherwise the +% XML is regenerated from brain_model. +% +% :Inputs: +% **obj:** an fmri_surface_data object. +% **filename:** output path (positional) or via 'fname'/'filename' keyword. +% Defaults to obj.fullpath. +% +% :See also: canlab_write_cifti, canlab_write_gifti, fmri_surface_data, to_fmri_data + +% ---- Resolve filename ---- +fname = ''; +i = 1; +while i <= numel(varargin) + a = varargin{i}; + if (ischar(a) || isstring(a)) && any(strcmpi(char(a), {'fname','filename'})) && i < numel(varargin) + fname = char(varargin{i+1}); i = i + 2; + elseif (ischar(a) || isstring(a)) && (contains(char(a), '.nii') || contains(char(a), '.gii')) + fname = char(a); i = i + 1; + else + i = i + 1; + end +end +if isempty(fname), fname = char(obj.fullpath); end +if isempty(fname) + error('fmri_surface_data:write:nofname', ... + 'No output filename. Pass one or set obj.fullpath.'); +end + +lc = lower(fname); + +% ===================== GIFTI ===================== +if endsWith(lc, '.gii') + g = struct('vertices', [], 'faces', [], 'cdata', [], 'intents', {{}}, 'labels', []); + if ~isempty(obj.geom) && isfield(obj.geom, 'vertices') + g.vertices = obj.geom.vertices; + g.faces = obj.geom.faces; + end + if ~isempty(obj.dat) + g.cdata = double(obj.dat); + islabel = contains(lc, '.label.') || strcmp(obj.intent, 'label') || strcmp(obj.intent, 'dlabel'); + if islabel + g.intents = repmat({'NIFTI_INTENT_LABEL'}, 1, size(g.cdata, 2)); + g.labels = obj.label_table; + else + g.intents = repmat({'NIFTI_INTENT_NONE'}, 1, size(g.cdata, 2)); + end + end + canlab_write_gifti(fname, g); + return +end + +% ===================== CIFTI-2 ===================== +intent = local_resolve_intent(obj.intent, lc); + +cii = struct(); +cii.cdata = double(obj.dat); +cii.intent = intent; +cii.diminfo = {obj.brain_model, local_build_maps_dim(obj, intent)}; + +% Faithful re-emit if the stashed source XML still matches the current layout +ai = obj.additional_info; +if isstruct(ai) && isfield(ai, 'cifti_xml') && isfield(ai, 'cifti_hdr') ... + && ~isempty(ai.cifti_xml) && ~isempty(ai.cifti_hdr) + h = ai.cifti_hdr; + if isfield(h, 'dim') && numel(h.dim) >= 7 + gray = size(cii.cdata, 1); maps = size(cii.cdata, 2); + if (h.dim(6) == maps && h.dim(7) == gray) || (h.dim(6) == gray && h.dim(7) == maps) + cii.xml = ai.cifti_xml; + cii.hdr = h; + end + end +end + +canlab_write_cifti(fname, cii); +end + + +% ========================================================================= +function intent = local_resolve_intent(objintent, lc) +if ismember(objintent, {'dscalar','dtseries','dlabel','dconn'}) + intent = objintent; +elseif contains(lc, '.dscalar.'), intent = 'dscalar'; +elseif contains(lc, '.dtseries.'), intent = 'dtseries'; +elseif contains(lc, '.dlabel.'), intent = 'dlabel'; +elseif strcmp(objintent, 'label'), intent = 'dlabel'; +elseif strcmp(objintent, 'func') || strcmp(objintent, 'shape'), intent = 'dscalar'; +else, intent = 'dscalar'; +end +end + + +% ========================================================================= +function md = local_build_maps_dim(obj, intent) +nMaps = size(obj.dat, 2); + +names = obj.image_names; +if isempty(names) || numel(names) ~= nMaps + names = arrayfun(@(k) sprintf('map_%d', k), 1:nMaps, 'UniformOutput', false); +end +names = reshape(names, 1, []); + +switch intent + case 'dlabel' + % Per-map label tables: prefer stashed per-map tables, else reuse one + tables = {}; + if isstruct(obj.additional_info) && isfield(obj.additional_info, 'label_tables') + tables = obj.additional_info.label_tables; + end + maps = struct('name', {}, 'table', {}); + for k = 1:nMaps + if k <= numel(tables) && ~isempty(tables{k}) + tbl = tables{k}; + else + tbl = obj.label_table; + end + maps(k) = struct('name', names{k}, 'table', tbl); + end + md = struct('type', 'labels', 'length', nMaps, 'maps', maps); + + case 'dtseries' + si = obj.series_info; + md = struct('type', 'series', 'length', nMaps); + md.seriesStart = local_getdef(si, 'start', 0); + md.seriesStep = local_getdef(si, 'step', 1); + md.seriesUnit = local_getstr(si, 'unit', 'SECOND'); + md.seriesExponent = local_getdef(si, 'exponent', 0); + + otherwise % dscalar (and fallback) + maps = struct('name', {}, 'table', {}); + for k = 1:nMaps + maps(k) = struct('name', names{k}, 'table', []); + end + md = struct('type', 'scalars', 'length', nMaps, 'maps', maps); +end +end + + +function v = local_getdef(s, f, d) +v = d; +if isstruct(s) && isfield(s, f) && ~isempty(s.(f)) + val = double(s.(f)); + if ~all(isnan(val(:))) + v = s.(f); + end +end +end + +function v = local_getstr(s, f, d) +if isstruct(s) && isfield(s, f) && ~isempty(s.(f)) + v = s.(f); +else + v = d; +end +end diff --git a/CanlabCore/@image_vector/ica.m b/CanlabCore/@image_vector/ica.m index 0a6657ea..39e78f7f 100644 --- a/CanlabCore/@image_vector/ica.m +++ b/CanlabCore/@image_vector/ica.m @@ -86,7 +86,9 @@ % - mahal % - orthviews -figure; plot(B(:), W(:), 'k.') +% (Removed a stray, non-functional debug line here that referenced undefined +% variables B and W before they were computed, which made ica() error on every +% call for all image_vector subclasses. -- 2026-06-26) nic = 30; if length(varargin) > 0, nic = varargin{1}; end diff --git a/CanlabCore/@image_vector/vol2surf.m b/CanlabCore/@image_vector/vol2surf.m new file mode 100644 index 00000000..b667b5f2 --- /dev/null +++ b/CanlabCore/@image_vector/vol2surf.m @@ -0,0 +1,90 @@ +function surf = vol2surf(obj, varargin) +% vol2surf Project a volumetric image to the fsaverage cortical surface. +% +% :Usage: +% :: +% surf = vol2surf(fmri_data_obj) +% surf = vol2surf(fmri_data_obj, 'interp', 'nearest') +% +% Projects a volumetric image_vector / fmri_data (in an MNI152 space) onto the +% fsaverage 164k cortical surface, returning an fmri_surface_data object. Uses +% the vendored CBIG Registration-Fusion (RF-ANTs) MNI152->fsaverage mapping +% (per-vertex MNI RAS coordinates) and samples the volume at those coordinates +% with interpn. Fully native -- no FreeSurfer or Connectome Workbench required. +% +% This is the native v1 mapper. It is a fixed group-template MNI152<->fsaverage +% correspondence (correct for group MNI maps; not a per-subject ribbon mapper), +% and it targets fsaverage-164k (fsaverage<->fs_LR deformation is a later step). +% +% :Inputs: +% **obj:** an image_vector / fmri_data / statistic_image in an MNI152 volume +% space (uses obj.volInfo.mat for the voxel->mm affine). +% +% :Optional Inputs: +% **'interp':** 'linear' (default) or 'nearest' (use 'nearest' for label maps). +% +% :Outputs: +% **surf:** fmri_surface_data, surface_space 'fsaverage_164k', cortex-only +% ([2*163842 x nMaps], left then right). +% +% :Examples: +% :: +% dat = fmri_data(which('weights_NSF_grouppred_cvpcr.img')); +% s = vol2surf(dat); % fsaverage surface object +% % surface(s) % (rendering: M5) +% +% :See also: surf2vol, fmri_surface_data, canlab_cbig_warp_path + +interp = 'linear'; +for i = 1:2:numel(varargin) + switch lower(varargin{i}) + case 'interp', interp = varargin{i+1}; + otherwise, error('vol2surf:badopt', 'Unknown option: %s', varargin{i}); + end +end + +if isempty(obj.volInfo) || ~isfield(obj.volInfo, 'mat') + error('vol2surf:novolinfo', 'Input has no volInfo.mat (needs a volumetric space).'); +end + +% Reconstruct the full 3-D/4-D volume and get the voxel->mm affine +obj = replace_empty(obj); +voldata = reconstruct_image(obj); % [X Y Z nMaps] +if ndims(voldata) == 3, voldata = voldata(:, :, :, 1); end +mat = obj.volInfo.mat; +nMaps = size(voldata, 4); + +NV = 163842; % fsaverage vertices per hemisphere +L = load(canlab_cbig_warp_path('lh_ras')); lh_ras = L.ras; % 3 x NV (MNI mm) +R = load(canlab_cbig_warp_path('rh_ras')); rh_ras = R.ras; + +datmat = zeros(2 * NV, nMaps, 'single'); +hemis = {lh_ras, rh_ras}; +offsets = [0, NV]; +for h = 1:2 + ras = [hemis{h}; ones(1, NV)]; + vox = mat \ ras; % 4 x NV, 1-based voxel coords + rows = offsets(h) + (1:NV); + for k = 1:nMaps + V = voldata(:, :, :, k); + vals = interpn(V, vox(1, :), vox(2, :), vox(3, :), interp, 0); + datmat(rows, k) = single(vals(:)); + end +end + +% Build the fsaverage cortex-only brain_model +mL = struct('struct', 'CORTEX_LEFT', 'type', 'surf', 'start', 1, 'count', NV, ... + 'numvert', NV, 'vertlist', 0:NV-1, 'voxlist', []); +mR = struct('struct', 'CORTEX_RIGHT', 'type', 'surf', 'start', NV+1, 'count', NV, ... + 'numvert', NV, 'vertlist', 0:NV-1, 'voxlist', []); +bm = struct('type', 'dense', 'length', 2*NV, 'models', {{mL, mR}}, 'vol', []); +bm.grayordinate_type = 'cortex_only'; +bm.cluster = []; + +surf = fmri_surface_data('dat', datmat, 'brain_model', bm, ... + 'surface_space', 'fsaverage_164k', 'intent', 'dscalar'); +if ~isempty(obj.image_names), surf.image_names = obj.image_names; end +surf.history = obj.history; +surf.history{end+1} = sprintf(['vol2surf: projected %d maps to fsaverage_164k via CBIG ' ... + 'RF-ANTs MNI152 mapping (interp=%s)'], nMaps, interp); +end diff --git a/CanlabCore/Cifti_plotting/plot_surface_map.m b/CanlabCore/Cifti_plotting/plot_surface_map.m index 3bb0b17b..0b1688a6 100644 --- a/CanlabCore/Cifti_plotting/plot_surface_map.m +++ b/CanlabCore/Cifti_plotting/plot_surface_map.m @@ -1,6 +1,13 @@ function h = plot_surface_map(dat,varargin) % This is a method to plot cifti (grayordinate) data on cortical surfaces +% +% NOTE (2026): This function depends on external tools (ft_read_cifti from +% FieldTrip, and the gifti toolbox). For new code, prefer the self-contained +% fmri_surface_data object, which reads CIFTI/GIFTI natively and renders with +% surface(): s = fmri_surface_data('data.dscalar.nii'); surface(s); +% See docs/fmri_surface_data_methods.md. +% % (e.g., from the HCP project). Read data with fieldtrip into matlab and % call this function. Right now, only dense scalar files (*.dscalar.nii) % with one map per file are supported. diff --git a/CanlabCore/Cifti_plotting/render_cifti_on_brain.m b/CanlabCore/Cifti_plotting/render_cifti_on_brain.m index 37775a52..64232b44 100644 --- a/CanlabCore/Cifti_plotting/render_cifti_on_brain.m +++ b/CanlabCore/Cifti_plotting/render_cifti_on_brain.m @@ -1,6 +1,15 @@ function [handles, r, subctx_fmri_data_obj] = render_cifti_on_brain(cifti_filename, varargin) % render_cifti_on_brain renders an image from a cifti file onto surfaces and brain slices. % +% NOTE (2026): This function depends on external HCP/Workbench CIFTI tools +% (cifti_read / Washington-University CIFTI tools). For new code, prefer the +% self-contained fmri_surface_data object, which reads CIFTI/GIFTI natively (no +% external toolbox) and renders with its surface() method: +% s = fmri_surface_data('yourdata.dscalar.nii'); +% surface(s); % native cortical-surface render +% surface(s, 'mni_surface', 'left'); % on an addbrain MNI surface +% See docs/fmri_surface_data_methods.md. +% % :Usage: % :: % render_cifti_on_brain(cifti_filename OR cifti_struct, [optional inputs]) diff --git a/CanlabCore/Surface_tools/canlab_cbig_warp_path.m b/CanlabCore/Surface_tools/canlab_cbig_warp_path.m new file mode 100644 index 00000000..fd59deed --- /dev/null +++ b/CanlabCore/Surface_tools/canlab_cbig_warp_path.m @@ -0,0 +1,42 @@ +function p = canlab_cbig_warp_path(name) +% canlab_cbig_warp_path Resolve a vendored CBIG registration-fusion warp file path. +% +% :Usage: +% :: +% p = canlab_cbig_warp_path('lh_ras') +% +% Returns the absolute path to a CBIG Registration-Fusion (RF-ANTs) MNI152<-> +% fsaverage mapping file vendored under CanlabCore/Cifti_plotting/. These warps +% (MIT-licensed) back the native vol2surf / surf2vol mappers -- no FreeSurfer or +% Connectome Workbench binary is required. +% +% :Inputs: +% **name:** one of +% 'lh_ras' / 'rh_ras' : per-fsaverage-vertex MNI152 RAS coords (3x163842), +% variable 'ras' (MNI152 orig -> fsaverage). +% +% :Outputs: +% **p:** absolute path to the .mat warp file (errors if not found). +% +% :See also: vol2surf, surf2vol, fmri_surface_data + +% CanlabCore root = parent of Surface_tools (this file's folder) +root = fileparts(fileparts(mfilename('fullpath'))); +base = fullfile(root, 'Cifti_plotting', 'CBIG_registration_fusion_surf2vol_vol2surf', ... + 'standalone_scripts_for_MNI_fsaverage_projection', 'final_warps_FS5.3'); + +switch lower(name) + case 'lh_ras' + p = fullfile(base, 'lh.avgMapping_allSub_RF_ANTs_MNI152_orig_to_fsaverage.mat'); + case 'rh_ras' + p = fullfile(base, 'rh.avgMapping_allSub_RF_ANTs_MNI152_orig_to_fsaverage.mat'); + otherwise + error('canlab_cbig_warp_path:badname', 'Unknown warp name: %s', name); +end + +if exist(p, 'file') ~= 2 + error('canlab_cbig_warp_path:notfound', ... + ['CBIG warp not found: %s\nThe registration-fusion warps should ship under ' ... + 'CanlabCore/Cifti_plotting/CBIG_registration_fusion_surf2vol_vol2surf/.'], p); +end +end diff --git a/CanlabCore/Surface_tools/canlab_read_cifti.m b/CanlabCore/Surface_tools/canlab_read_cifti.m new file mode 100644 index 00000000..997663d4 --- /dev/null +++ b/CanlabCore/Surface_tools/canlab_read_cifti.m @@ -0,0 +1,363 @@ +function cii = canlab_read_cifti(filename) +% Read a CIFTI-2 file (.dscalar/.dtseries/.dlabel/.dconn.nii) natively in MATLAB. +% +% :Usage: +% :: +% cii = canlab_read_cifti(filename) +% +% A CIFTI-2 file is a NIfTI-2 container (540-byte little-endian header) whose +% header extension with ecode==32 holds an XML description of the matrix +% (BrainModels = surface vertices + subcortical voxels; plus a Scalars/Series/ +% Labels map for the second dimension). The data matrix follows at vox_offset. +% This reader uses only fopen/fread + regexp XML parsing -- it does NOT require +% cifti-matlab, FieldTrip, gifti, or Connectome Workbench at runtime. +% +% Only modern CIFTI-2 is supported (legacy CIFTI-1 requires wb_command to +% upgrade). The output field names mirror cifti-matlab's so existing helpers +% such as get_cifti_data.m can consume it unchanged. +% +% :Inputs: +% +% **filename:** +% Char/string. Path to a .dscalar.nii / .dtseries.nii / .dlabel.nii / +% .dconn.nii (CIFTI-2) file. +% +% :Outputs: +% +% **cii:** +% Struct with fields: +% .cdata : [nGrayordinates x nMaps] numeric data matrix +% .intent : 'dscalar' | 'dtseries' | 'dlabel' | 'dconn' | 'unknown' +% .diminfo : 1x2 cell. diminfo{1} describes the grayordinate (dense) +% dimension; diminfo{2} the map/series/label dimension. +% diminfo{1}.type = 'dense' +% diminfo{1}.length +% diminfo{1}.models{i}: .struct (e.g. 'CORTEX_LEFT'), .type 'surf'|'vox', +% .start (1-based row), .count, .numvert (surf), .vertlist (0-based), +% .voxlist (3xN IJK, 0-based) +% diminfo{1}.vol.dims (1x3), diminfo{1}.vol.sform (4x4, mm) +% diminfo{2}.type = 'scalars'|'series'|'labels' +% diminfo{2}.maps(k).name (+ .table for labels: .key,.name,.rgba) +% diminfo{2}.seriesStart/.seriesStep/.seriesUnit (series) +% .hdr : parsed NIfTI-2 header fields (dim, datatype, vox_offset, ...) +% .xml : raw CIFTI XML char array (used by canlab_write_cifti) +% +% :Examples: +% :: +% c = canlab_read_cifti(which('transcriptomic_gradients.dscalar.nii')); +% size(c.cdata) % [grayordinates x maps] +% cellfun(@(m) m.struct, c.diminfo{1}.models, 'unif', 0) +% +% :See also: +% canlab_write_cifti, canlab_read_gifti, get_cifti_data, fmri_surface_data + +% .. +% Part of the CANlab fmri_surface_data surface-data toolset. See +% docs/fmri_surface_data_design_plan.md. No external toolbox required. +% .. + +if nargin < 1 || isempty(filename) + error('canlab_read_cifti:input', 'Provide a path to a CIFTI-2 .nii file.'); +end +filename = char(filename); +if exist(filename, 'file') ~= 2 + error('canlab_read_cifti:notfound', 'File not found: %s', filename); +end + +% ---- Open and detect endianness via sizeof_hdr (must be 540 for NIfTI-2) ---- +fid = fopen(filename, 'r', 'l'); +if fid < 0, error('canlab_read_cifti:open', 'Cannot open %s', filename); end +cleanupObj = onCleanup(@() fclose(fid)); + +sizeof_hdr = fread(fid, 1, 'int32'); +machine = 'l'; +if sizeof_hdr ~= 540 + fclose(fid); + fid = fopen(filename, 'r', 'b'); machine = 'b'; + cleanupObj = onCleanup(@() fclose(fid)); %#ok + sizeof_hdr = fread(fid, 1, 'int32'); + if sizeof_hdr ~= 540 + error('canlab_read_cifti:notnifti2', ... + ['sizeof_hdr=%d (expected 540). Not a NIfTI-2/CIFTI-2 file. ' ... + 'Legacy CIFTI-1 is not supported.'], sizeof_hdr); + end +end + +% ---- NIfTI-2 header (fixed offsets) ---- +hdr.magic = fread(fid, 8, '*char')'; % @4 +hdr.datatype = fread(fid, 1, 'int16'); % @12 +hdr.bitpix = fread(fid, 1, 'int16'); % @14 +hdr.dim = fread(fid, 8, 'int64')'; % @16 +hdr.intent_p = fread(fid, 3, 'double')'; % @80 +hdr.pixdim = fread(fid, 8, 'double')'; % @104 +hdr.vox_offset = fread(fid, 1, 'int64'); % @168 +hdr.scl_slope = fread(fid, 1, 'double'); % @176 +hdr.scl_inter = fread(fid, 1, 'double'); % @184 +fseek(fid, 504, 'bof'); +hdr.intent_code = fread(fid, 1, 'int32'); % @504 +hdr.intent_name = deblank(fread(fid, 16, '*char')'); % @508 +hdr.machine = machine; + +% ---- Header extension: find the ecode==32 CIFTI XML ---- +fseek(fid, 540, 'bof'); +extender = fread(fid, 4, 'uint8'); +xml = ''; +if ~isempty(extender) && extender(1) ~= 0 + while ftell(fid) < hdr.vox_offset + esize = fread(fid, 1, 'int32'); + ecode = fread(fid, 1, 'int32'); + if isempty(esize) || esize <= 8, break; end + edata = fread(fid, esize - 8, '*char')'; + if ecode == 32, xml = edata; end + end +end +if isempty(xml) + error('canlab_read_cifti:noxml', 'No CIFTI XML extension (ecode 32) found.'); +end +% Trim trailing NULs/whitespace padding from the XML blob +xml = regexprep(xml, '\x00+$', ''); +cii.xml = xml; + +% ---- Read the data matrix ---- +rowLen = hdr.dim(6); % NIfTI dim[5] +nRows = hdr.dim(7); % NIfTI dim[6] +prec = local_nifti_precision(hdr.datatype); +fseek(fid, hdr.vox_offset, 'bof'); +v = fread(fid, rowLen * nRows, [prec '=>' prec]); +v = double(v); +if isfinite(hdr.scl_slope) && hdr.scl_slope ~= 0 && ... + ~(hdr.scl_slope == 1 && hdr.scl_inter == 0) + v = v * hdr.scl_slope + hdr.scl_inter; +end + +% ---- Parse XML into diminfo ---- +[diminfo, intent] = local_parse_cifti_xml(xml, hdr.intent_code, hdr.intent_name); + +% ---- Orient cdata to [grayordinates x maps] robustly ---- +G = 0; +if ~isempty(diminfo{1}) && isfield(diminfo{1}, 'models') + for i = 1:numel(diminfo{1}.models), G = G + diminfo{1}.models{i}.count; end +end +M = reshape(v, [rowLen nRows]); +if G > 0 && rowLen == G + cdata = M; % [grayordinates x maps] +elseif G > 0 && nRows == G + cdata = M'; % transpose to [grayordinates x maps] +else + % Dense dimension not size-identified (e.g. dconn): keep file order, + % assume dimension-1 (along rows) is the dense dimension. + cdata = M; +end + +cii.cdata = cdata; +cii.intent = intent; +cii.diminfo = diminfo; +cii.hdr = hdr; + +end % main + + +% ========================================================================= +function prec = local_nifti_precision(dt) +switch dt + case 2, prec = 'uint8'; + case 4, prec = 'int16'; + case 8, prec = 'int32'; + case 16, prec = 'single'; + case 64, prec = 'double'; + case 256, prec = 'int8'; + case 512, prec = 'uint16'; + case 768, prec = 'uint32'; + case 1024, prec = 'int64'; + case 1280, prec = 'uint64'; + otherwise + error('canlab_read_cifti:datatype', 'Unsupported NIfTI datatype %d', dt); +end +end + + +% ========================================================================= +function [diminfo, intent] = local_parse_cifti_xml(xml, intent_code, intent_name) +% Returns diminfo{1} (dense) and diminfo{2} (maps/series/labels). + +switch intent_code + case 3000, intent = 'dconn'; + case 3002, intent = 'dtseries'; + case 3006, intent = 'dscalar'; + case 3007, intent = 'dlabel'; + otherwise + if contains(intent_name, 'Scalar'), intent = 'dscalar'; + elseif contains(intent_name, 'Series'), intent = 'dtseries'; + elseif contains(intent_name, 'Label'), intent = 'dlabel'; + elseif contains(intent_name, 'Dense'), intent = 'dconn'; + else, intent = 'unknown'; + end +end + +% Match either a self-closing (e.g. SERIES maps carry +% all their info in attributes) or a paired ... block. +mims = regexp(xml, '(?s)]*?/>|]*?>.*?', 'match'); + +infos = {}; % parsed info structs +applies = {}; % AppliesToMatrixDimension vectors +isdense = []; % logical: this map is a BRAIN_MODELS (dense) dimension + +for k = 1:numel(mims) + mim = mims{k}; + appliesTo = local_attr(mim, 'AppliesToMatrixDimension'); % "0" / "1" / "0,1" + maptype = local_attr(mim, 'IndicesMapToDataType'); + + info = struct(); + dense = false; + if contains(maptype, 'BRAIN_MODELS') + info = local_parse_brain_models(mim); + dense = true; + elseif contains(maptype, 'SCALARS') + info.type = 'scalars'; + info.maps = local_parse_named_maps(mim, false); + info.length = numel(info.maps); + elseif contains(maptype, 'LABELS') + info.type = 'labels'; + info.maps = local_parse_named_maps(mim, true); + info.length = numel(info.maps); + elseif contains(maptype, 'SERIES') + info.type = 'series'; + info.seriesStart = str2double(local_attr(mim, 'SeriesStart')); + info.seriesStep = str2double(local_attr(mim, 'SeriesStep')); + info.seriesUnit = local_attr(mim, 'SeriesUnit'); + info.seriesExponent = str2double(local_attr(mim, 'SeriesExponent')); + info.length = str2double(local_attr(mim, 'NumberOfSeriesPoints')); + elseif contains(maptype, 'PARCELS') + info.type = 'parcels'; % not fully expanded in v1 + info.raw = mim; + else + info.type = 'unknown'; + end + + infos{end+1} = info; %#ok + applies{end+1} = sscanf(appliesTo, '%d,')'; %#ok + isdense(end+1) = dense; %#ok +end + +% Order so diminfo{1} is the dense (grayordinate) dimension and diminfo{2} the +% maps/series/labels dimension -- matches cifti-matlab and get_cifti_data, which +% index diminfo{1}.models. (CIFTI AppliesToMatrixDimension numbering is reversed +% relative to MATLAB row/col, so we key off content, not the attribute.) +diminfo = {[], []}; +denseIdx = find(isdense, 1); +mapsIdx = find(~isdense, 1); +if ~isempty(denseIdx), diminfo{1} = infos{denseIdx}; end +if ~isempty(mapsIdx), diminfo{2} = infos{mapsIdx}; end +if isempty(denseIdx) % e.g. dconn: both dimensions are dense + [~, order] = sort(cellfun(@(a) a(1), applies)); + diminfo = infos(order); +end +end + + +% ========================================================================= +function info = local_parse_brain_models(mim) +info = struct('type', 'dense', 'length', 0, 'models', {{}}, 'vol', []); + +% Volume block (affine + dims), if any +vol = regexp(mim, '(?s)', 'match', 'once'); +if ~isempty(vol) + vd = local_attr(vol, 'VolumeDimensions'); + dims = sscanf(vd, '%d,')'; + Telem = regexp(vol, '(?s)]*>.*?', ... + 'match', 'once'); + T = regexp(Telem, '(?s)>(.*?)<', 'tokens', 'once'); + sform = eye(4); + if ~isempty(T) + mexp = str2double(local_attr(Telem, 'MeterExponent')); + if isnan(mexp), mexp = -3; end + nums = sscanf(T{1}, '%f'); + if numel(nums) >= 16 + sform = reshape(nums(1:16), 4, 4)'; % row-major -> 4x4 + sform(1:3, :) = sform(1:3, :) * 10^(mexp + 3); % normalize to mm + end + end + info.vol = struct('dims', dims, 'sform', sform); +end + +bms = regexp(mim, '(?s)', 'match'); +total = 0; +for i = 1:numel(bms) + bm = bms{i}; + m = struct(); + m.struct = regexprep(local_attr(bm, 'BrainStructure'), '^CIFTI_STRUCTURE_', ''); + mt = local_attr(bm, 'ModelType'); + if contains(mt, 'SURFACE'), m.type = 'surf'; else, m.type = 'vox'; end + m.start = str2double(local_attr(bm, 'IndexOffset')) + 1; % 1-based + m.count = str2double(local_attr(bm, 'IndexCount')); + m.numvert = str2double(local_attr(bm, 'SurfaceNumberOfVertices')); + m.vertlist = []; + m.voxlist = []; + if strcmp(m.type, 'surf') + vi = regexp(bm, '(?s)]*>(.*?)', 'tokens', 'once'); + if ~isempty(vi), m.vertlist = sscanf(vi{1}, '%d')'; end % 0-based + else + vx = regexp(bm, '(?s)]*>(.*?)', 'tokens', 'once'); + if ~isempty(vx) + ijk = sscanf(vx{1}, '%d'); + m.voxlist = reshape(ijk, 3, [])'; % Nx3 IJK (0-based) + m.voxlist = m.voxlist'; % 3xN to match cifti-matlab + end + end + info.models{end+1} = m; + total = total + m.count; +end +info.length = total; +end + + +% ========================================================================= +function maps = local_parse_named_maps(mim, isLabel) +maps = struct('name', {}, 'table', {}); +nm = regexp(mim, '(?s)(.*?)', 'match'); +for i = 1:numel(nm) + blk = nm{i}; + nameTok = regexp(blk, '(?s)(.*?)', 'tokens', 'once'); + name = ''; + if ~isempty(nameTok) + name = strtrim(nameTok{1}); + name = regexprep(name, '', '$1'); + end + tbl = []; + if isLabel + tbl = local_parse_cifti_labeltable(blk); + end + maps(end+1) = struct('name', name, 'table', tbl); %#ok +end +end + + +% ========================================================================= +function tbl = local_parse_cifti_labeltable(blk) +tbl = struct('key', {}, 'name', {}, 'rgba', {}); +lt = regexp(blk, '(?s)(.*?)', 'tokens', 'once'); +if isempty(lt), return; end +items = regexp(lt{1}, '(?s)', 'match'); +for i = 1:numel(items) + it = items{i}; + key = str2double(local_attr(it, 'Key')); + r = str2double(local_attr(it, 'Red')); + g = str2double(local_attr(it, 'Green')); + b = str2double(local_attr(it, 'Blue')); + a = str2double(local_attr(it, 'Alpha')); + nmt = regexp(it, '(?s)]*>(.*?)', 'tokens', 'once'); + name = ''; + if ~isempty(nmt) + name = strtrim(nmt{1}); + name = regexprep(name, '', '$1'); + end + tbl(end+1) = struct('key', key, 'name', name, 'rgba', [r g b a]); %#ok +end +end + + +% ========================================================================= +function val = local_attr(str, name) +tok = regexp(str, [name '\s*=\s*"([^"]*)"'], 'tokens', 'once'); +if isempty(tok), val = ''; else, val = tok{1}; end +end diff --git a/CanlabCore/Surface_tools/canlab_read_gifti.m b/CanlabCore/Surface_tools/canlab_read_gifti.m new file mode 100644 index 00000000..5f6474ae --- /dev/null +++ b/CanlabCore/Surface_tools/canlab_read_gifti.m @@ -0,0 +1,193 @@ +function gii = canlab_read_gifti(filename) +% Read a GIFTI (.gii) file natively in MATLAB, with no external toolbox. +% +% :Usage: +% :: +% gii = canlab_read_gifti(filename) +% +% Reads geometry (.surf.gii), functional/shape (.func.gii / .shape.gii) and +% label (.label.gii) GIFTI files. GIFTI is a UTF-8 XML container holding one or +% more elements; payloads may be ASCII, Base64Binary, or +% GZipBase64Binary (base64 of a raw zlib stream). This function depends only on +% core MATLAB plus the JVM (for zlib inflate of GZipBase64Binary data) -- it does +% NOT require the SPM/FieldTrip gifti toolbox or Connectome Workbench. +% +% :Inputs: +% +% **filename:** +% Char/string. Full path to a .gii file (.surf/.func/.shape/.label.gii). +% +% :Outputs: +% +% **gii:** +% Struct with fields: +% .vertices : [N x 3] double vertex mm coordinates (POINTSET), or [] +% .faces : [M x 3] double, 1-based triangle indices (TRIANGLE), or [] +% .cdata : [N x K] per-vertex data, K columns from K data arrays, or [] +% .intents : 1xK cell of NIFTI_INTENT_* strings for the cdata columns +% .labels : struct array (.key,.name,.rgba) from a GIFTI LabelTable, or [] +% .gifti_version : char, from the root attribute +% +% :Examples: +% :: +% g = canlab_read_gifti(which('S1200.L.midthickness_MSMAll.32k_fs_LR.surf.gii')); +% patch('Faces', g.faces, 'Vertices', g.vertices, 'FaceColor', [.7 .7 .7]); axis equal +% +% lbl = canlab_read_gifti('Glasser_2016.32k.L.label.gii'); +% unique(lbl.cdata) % integer region keys +% {lbl.labels.name} % region names +% +% :See also: +% canlab_write_gifti, canlab_read_cifti, canlab_write_cifti, fmri_surface_data + +% .. +% Part of the CANlab fmri_surface_data surface-data toolset. See +% docs/fmri_surface_data_design_plan.md. No external toolbox required. +% .. + +if nargin < 1 || isempty(filename) + error('canlab_read_gifti:input', 'Provide a path to a .gii file.'); +end +filename = char(filename); +if exist(filename, 'file') ~= 2 + error('canlab_read_gifti:notfound', 'File not found: %s', filename); +end + +txt = fileread(filename); + +gii = struct('vertices', [], 'faces', [], 'cdata', [], 'intents', {{}}, ... + 'labels', [], 'gifti_version', ''); + +% Root version +v = regexp(txt, ']*\sVersion="([^"]*)"', 'tokens', 'once'); +if ~isempty(v), gii.gifti_version = v{1}; end + +% Label table (optional; .label.gii) +gii.labels = local_parse_labeltable(txt); + +% Each DataArray +das = regexp(txt, '(?s)', 'match'); +cdata = {}; +for k = 1:numel(das) + [M, intent] = local_parse_dataarray(das{k}); + if isempty(M), continue; end + if contains(intent, 'POINTSET') + gii.vertices = M; + elseif contains(intent, 'TRIANGLE') + gii.faces = M + 1; % store 1-based for patch() + else + cdata{end+1} = M(:); %#ok per-vertex data column + gii.intents{end+1} = intent; %#ok + end +end + +if ~isempty(cdata) + n = cellfun(@numel, cdata); + if numel(unique(n)) == 1 + gii.cdata = double([cdata{:}]); + else + % Ragged data arrays: keep as cell to avoid silent truncation + gii.cdata = cdata; + warning('canlab_read_gifti:ragged', ... + 'DataArrays have differing lengths; returning .cdata as a cell array.'); + end +end + +end % main function + + +% ------------------------------------------------------------------------- +function [M, intent] = local_parse_dataarray(da) +% Parse one ... block into a numeric matrix. + +ga = @(name) local_attr(da, name); +intent = ga('Intent'); +dtype = ga('DataType'); +order = ga('ArrayIndexingOrder'); +enc = ga('Encoding'); +endian = ga('Endian'); + +dimn = str2double(ga('Dimensionality')); +if isnan(dimn) || dimn < 1, dimn = 1; end +dims = ones(1, dimn); +for d = 0:dimn-1 + dims(d+1) = str2double(ga(sprintf('Dim%d', d))); +end + +switch dtype + case 'NIFTI_TYPE_FLOAT32', cls = 'single'; + case 'NIFTI_TYPE_FLOAT64', cls = 'double'; + case 'NIFTI_TYPE_INT32', cls = 'int32'; + case 'NIFTI_TYPE_UINT32', cls = 'uint32'; + case 'NIFTI_TYPE_INT16', cls = 'int16'; + case 'NIFTI_TYPE_UINT8', cls = 'uint8'; + case 'NIFTI_TYPE_INT8', cls = 'int8'; + otherwise, cls = 'single'; +end + +payload = regexp(da, '(?s)(.*?)', 'tokens', 'once'); +if isempty(payload), M = []; return; end +data_str = payload{1}; + +if strcmp(enc, 'ASCII') + vals = cast(sscanf(data_str, '%f'), cls); % whitespace-delimited numbers +else + b64 = data_str(~isspace(data_str)); % base64 must have whitespace stripped + if isempty(b64), M = []; return; end + raw = matlab.net.base64decode(b64); + if strcmp(enc, 'GZipBase64Binary') + raw = canlab_zlib_inflate(raw); + end + vals = typecast(raw(:), cls); + if strcmpi(endian, 'BigEndian'), vals = swapbytes(vals); end +end +vals = double(vals); + +if numel(dims) >= 2 && dims(2) > 1 + if strcmp(order, 'RowMajorOrder') + M = reshape(vals, [dims(2) dims(1)])'; + else + M = reshape(vals, dims); + end +else + M = vals(:); +end +end + + +% ------------------------------------------------------------------------- +function labels = local_parse_labeltable(txt) +% Parse an optional GIFTI into a struct array (key,name,rgba). + +labels = []; +lt = regexp(txt, '(?s)(.*?)', 'tokens', 'once'); +if isempty(lt), return; end +items = regexp(lt{1}, '(?s)', 'match'); +if isempty(items), return; end + +labels = struct('key', {}, 'name', {}, 'rgba', {}); +for i = 1:numel(items) + it = items{i}; + key = str2double(local_attr(it, 'Key')); + r = str2double(local_attr(it, 'Red')); + g = str2double(local_attr(it, 'Green')); + b = str2double(local_attr(it, 'Blue')); + a = str2double(local_attr(it, 'Alpha')); + nm = regexp(it, '(?s)]*>(.*?)', 'tokens', 'once'); + name = ''; + if ~isempty(nm) + name = strtrim(nm{1}); + name = regexprep(name, '', '$1'); + end + labels(end+1) = struct('key', key, 'name', name, ... + 'rgba', [r g b a]); %#ok +end +end + + +% ------------------------------------------------------------------------- +function val = local_attr(str, name) +% Extract a single XML attribute value (or '' if absent). +tok = regexp(str, [name '\s*=\s*"([^"]*)"'], 'tokens', 'once'); +if isempty(tok), val = ''; else, val = tok{1}; end +end diff --git a/CanlabCore/Surface_tools/canlab_surface_vertexcolors.m b/CanlabCore/Surface_tools/canlab_surface_vertexcolors.m new file mode 100644 index 00000000..90a72a92 --- /dev/null +++ b/CanlabCore/Surface_tools/canlab_surface_vertexcolors.m @@ -0,0 +1,53 @@ +function rgb = canlab_surface_vertexcolors(vals, clim, poscm, negcm, graycolor) +% canlab_surface_vertexcolors Map per-vertex values to truecolor RGB (split colormap). +% +% :Usage: +% :: +% rgb = canlab_surface_vertexcolors(vals, clim, poscm, negcm, graycolor) +% +% Maps a vector of per-vertex values to an [N x 3] truecolor matrix using a +% split colormap: positive values use a "hot"-style map, negative values a +% "cool"-style map, and zero / NaN vertices (e.g. medial wall, out-of-mask) are +% rendered in a neutral gray. Designed for direct FaceVertexCData assignment on +% surface patches (FaceColor 'interp'), so no axis colormap/caxis juggling is +% needed and the medial wall stays gray. +% +% :Inputs: +% **vals:** [N x 1] per-vertex values (NaN allowed for medial wall). +% +% :Optional Inputs: +% **clim:** [lo hi] color limits. Default: symmetric [-m m] where m is the +% max abs nonzero value. Mapping uses max(abs(clim)). +% **poscm:** [P x 3] positive colormap. Default hot(256). +% **negcm:** [Q x 3] negative colormap. Default cool(256). +% **graycolor:** 1x3 color for zero/NaN vertices. Default [.5 .5 .5]. +% +% :Outputs: +% **rgb:** [N x 3] truecolor matrix in [0,1]. +% +% :See also: surface, render_on_surface, fmri_surface_data + +vals = double(vals(:)); +N = numel(vals); + +if nargin < 5 || isempty(graycolor), graycolor = [.5 .5 .5]; end +if nargin < 4 || isempty(negcm), negcm = cool(256); end +if nargin < 3 || isempty(poscm), poscm = hot(256); end +if nargin < 2 || isempty(clim) + finite_nonzero = vals(~isnan(vals) & vals ~= 0); + m = max(abs(finite_nonzero)); + if isempty(m) || m == 0, m = 1; end + clim = [-m m]; +end + +hi = max(abs(clim)); +if hi == 0, hi = 1; end + +rgb = repmat(graycolor, N, 1); +pos = vals > 0 & ~isnan(vals); +neg = vals < 0 & ~isnan(vals); + +idx = @(v, n) min(max(round(1 + (abs(v) / hi) * (n - 1)), 1), n); +if any(pos), rgb(pos, :) = poscm(idx(vals(pos), size(poscm, 1)), :); end +if any(neg), rgb(neg, :) = negcm(idx(vals(neg), size(negcm, 1)), :); end +end diff --git a/CanlabCore/Surface_tools/canlab_write_cifti.m b/CanlabCore/Surface_tools/canlab_write_cifti.m new file mode 100644 index 00000000..84106681 --- /dev/null +++ b/CanlabCore/Surface_tools/canlab_write_cifti.m @@ -0,0 +1,235 @@ +function canlab_write_cifti(filename, cii) +% Write a CIFTI-2 file (.dscalar/.dtseries/.dlabel.nii) natively in MATLAB. +% +% :Usage: +% :: +% canlab_write_cifti(filename, cii) +% +% Writes a NIfTI-2 container with the CIFTI XML in an ecode==32 header +% extension, followed by the data matrix. The input struct matches +% canlab_read_cifti's output, so read->write->read round-trips exactly. +% +% Two modes: +% * Round-trip: if `cii` carries the original `.xml` and `.hdr` (as returned +% by canlab_read_cifti), the original XML and matrix layout are preserved +% verbatim for a faithful re-write. +% * Regenerate: otherwise the CIFTI XML is rebuilt from `cii.diminfo` (dense +% BrainModels + a Scalars/Series/Labels map) in the standard layout. +% +% Depends only on core MATLAB -- no cifti-matlab, FieldTrip, or wb_command. +% +% :Inputs: +% +% **filename:** +% Char/string. Output path (e.g. '/tmp/out.dscalar.nii'). +% +% **cii:** +% Struct (as from canlab_read_cifti) with at least: +% .cdata [nGrayordinates x nMaps] +% .intent 'dscalar'|'dtseries'|'dlabel'|'dconn' +% .diminfo {dense, maps} as described in canlab_read_cifti +% Optional .xml and .hdr trigger faithful round-trip mode. +% +% :Examples: +% :: +% c = canlab_read_cifti(which('transcriptomic_gradients.dscalar.nii')); +% c.cdata = zscore(c.cdata); % modify +% canlab_write_cifti('/tmp/z.dscalar.nii', c); +% +% :See also: canlab_read_cifti, canlab_read_gifti, canlab_write_gifti + +% .. +% Part of the CANlab fmri_surface_data surface-data toolset. See +% docs/fmri_surface_data_design_plan.md. No external toolbox required. +% .. + +if nargin < 2 || ~isstruct(cii) || ~isfield(cii, 'cdata') + error('canlab_write_cifti:input', 'Provide (filename, cii) with cii.cdata. See help.'); +end +filename = char(filename); +cdata = cii.cdata; +G = size(cdata, 1); % grayordinates +nMaps = size(cdata, 2); + +% ---- XML: re-emit original or regenerate ---- +reuse = isfield(cii, 'xml') && ~isempty(cii.xml) && isfield(cii, 'hdr') ... + && isfield(cii.hdr, 'dim'); +if reuse + xml = cii.xml; + rowLen = cii.hdr.dim(6); + nRows = cii.hdr.dim(7); + % Reconstruct the on-disk vector in the file's original orientation + if rowLen == G + M = cdata; % cdata was stored as [rowLen x nRows] + else + M = cdata.'; % cdata was transposed on read + end + v = M(:); + ndim = cii.hdr.dim(1); +else + xml = local_build_cifti_xml(cii); + rowLen = nMaps; % standard layout: maps fast, grayords slow + nRows = G; + v = reshape(cdata.', [], 1); % maps fastest within each grayordinate + ndim = 6; +end + +[intent_code, intent_name] = local_intent_codes(cii.intent); + +% ---- Build header extension (ecode 32, padded so esize is a multiple of 16) ---- +xmlbytes = uint8(xml); +raw_len = numel(xmlbytes); +esize = ceil((raw_len + 8) / 16) * 16; % includes the 8-byte esize+ecode +pad = esize - 8 - raw_len; +vox_offset = 544 + esize; % 540 hdr + 4 extender + esize + +% ---- NIfTI-2 dim vector ---- +dim = ones(1, 8); +dim(1) = ndim; +dim(6) = rowLen; % NIfTI dim[5] +dim(7) = nRows; % NIfTI dim[6] + +% ---- Write ---- +fid = fopen(filename, 'w', 'l'); +if fid < 0, error('canlab_write_cifti:open', 'Cannot open for writing: %s', filename); end +cleanupObj = onCleanup(@() fclose(fid)); %#ok + +hdrbuf = local_nifti2_header(dim, 16, 32, vox_offset, intent_code, intent_name); +fwrite(fid, hdrbuf, 'uint8'); + +fwrite(fid, uint8([1 0 0 0]), 'uint8'); % extension flag +fwrite(fid, esize, 'int32'); +fwrite(fid, 32, 'int32'); % ecode 32 = CIFTI +fwrite(fid, xmlbytes, 'uint8'); +if pad > 0, fwrite(fid, zeros(1, pad, 'uint8'), 'uint8'); end + +% Data, single precision (scl_slope written as 0 => values are literal) +here = ftell(fid); +if here < vox_offset, fwrite(fid, zeros(1, vox_offset - here, 'uint8'), 'uint8'); end +fwrite(fid, single(v), 'single'); + +end % main + + +% ========================================================================= +function buf = local_nifti2_header(dim, datatype, bitpix, vox_offset, intent_code, intent_name) +buf = zeros(1, 540, 'uint8'); +put = @(off, val, type) local_put(buf, off, val, type); + +buf = put(0, 540, 'int32'); +% magic "n+2\0" 0x0D 0x0A 0x1A 0x0A +buf(5:12) = uint8([110 43 50 0 13 10 26 10]); +buf = local_put(buf, 12, datatype, 'int16'); +buf = local_put(buf, 14, bitpix, 'int16'); +buf = local_put(buf, 16, int64(dim(:)'), 'int64'); % dim[8] @16 +buf = local_put(buf, 104, ones(1, 8), 'double'); % pixdim[8]=1 +buf = local_put(buf, 168, int64(vox_offset), 'int64'); % vox_offset +buf = local_put(buf, 176, 0, 'double'); % scl_slope=0 (no scaling) +buf = local_put(buf, 184, 0, 'double'); % scl_inter +buf = local_put(buf, 344, 0, 'int32'); % qform_code +buf = local_put(buf, 348, 0, 'int32'); % sform_code +buf = local_put(buf, 504, int32(intent_code), 'int32'); +nm = uint8(intent_name); nm = nm(1:min(15, numel(nm))); +buf(509:509+numel(nm)-1) = nm; % intent_name @508 (16) +end + + +% ========================================================================= +function buf = local_put(buf, off, val, type) +bytes = typecast(cast(val(:)', type), 'uint8'); +buf(off+1:off+numel(bytes)) = bytes; +end + + +% ========================================================================= +function [code, name] = local_intent_codes(intent) +switch intent + case 'dscalar', code = 3006; name = 'ConnDenseScalar'; + case 'dtseries', code = 3002; name = 'ConnDenseSeries'; + case 'dlabel', code = 3007; name = 'ConnDenseLabel'; + case 'dconn', code = 3000; name = 'ConnDense'; + otherwise, code = 3006; name = 'ConnDenseScalar'; +end +end + + +% ========================================================================= +function xml = local_build_cifti_xml(cii) +% Regenerate CIFTI-2 XML from cii.diminfo (maps dim 0, dense BrainModels dim 1). +dense = cii.diminfo{1}; +maps = cii.diminfo{2}; + +s = sprintf('\n\n\n'); + +% --- maps dimension (AppliesToMatrixDimension=0) --- +switch maps.type + case 'scalars' + s = [s sprintf('\n')]; + for k = 1:numel(maps.maps) + s = [s sprintf('%s\n', local_xesc(maps.maps(k).name))]; %#ok + end + s = [s sprintf('\n')]; + case 'labels' + s = [s sprintf('\n')]; + for k = 1:numel(maps.maps) + s = [s sprintf('%s\n\n', local_xesc(maps.maps(k).name))]; %#ok + t = maps.maps(k).table; + for j = 1:numel(t) + s = [s sprintf('\n', ... + t(j).key, t(j).rgba(1), t(j).rgba(2), t(j).rgba(3), t(j).rgba(4), local_xesc(t(j).name))]; %#ok + end + s = [s sprintf('\n\n')]; %#ok + end + s = [s sprintf('\n')]; + case 'series' + s = [s sprintf(['\n'], ... + size(cii.cdata,2), local_def(maps,'seriesExponent',0), local_def(maps,'seriesStart',0), ... + local_def(maps,'seriesStep',1), local_defstr(maps,'seriesUnit','SECOND'))]; + otherwise + error('canlab_write_cifti:maptype', 'Cannot regenerate XML for maps type "%s". Re-write using the original .xml.', maps.type); +end + +% --- dense dimension (AppliesToMatrixDimension=1) --- +s = [s sprintf('\n')]; +if isfield(dense, 'vol') && ~isempty(dense.vol) + A = dense.vol.sform; + vals = A'; vals = vals(:)'; % row-major 16 + s = [s sprintf('\n', dense.vol.dims(1), dense.vol.dims(2), dense.vol.dims(3))]; + s = [s sprintf('')]; + s = [s sprintf('%.10g ', vals)]; + s = [s sprintf('\n\n')]; +end +for i = 1:numel(dense.models) + m = dense.models{i}; + if strcmp(m.type, 'surf') + s = [s sprintf(['\n'], ... + m.start-1, m.count, m.struct, m.numvert)]; %#ok + s = [s sprintf('%d ', m.vertlist)]; + s = [s sprintf('\n\n')]; %#ok + else + s = [s sprintf(['\n'], ... + m.start-1, m.count, m.struct)]; %#ok + ijk = m.voxlist; % 3xN + s = [s sprintf('%d %d %d\n', ijk)]; + s = [s sprintf('\n\n')]; %#ok + end +end +s = [s sprintf('\n\n\n')]; +xml = s; +end + + +function v = local_def(s, f, d), if isfield(s,f) && ~isnan(s.(f)), v = s.(f); else, v = d; end, end +function v = local_defstr(s, f, d), if isfield(s,f) && ~isempty(s.(f)), v = s.(f); else, v = d; end, end + + +% ========================================================================= +function out = local_xesc(str) +out = str; +out = strrep(out, '&', '&'); +out = strrep(out, '<', '<'); +out = strrep(out, '>', '>'); +end diff --git a/CanlabCore/Surface_tools/canlab_write_gifti.m b/CanlabCore/Surface_tools/canlab_write_gifti.m new file mode 100644 index 00000000..ddf76edb --- /dev/null +++ b/CanlabCore/Surface_tools/canlab_write_gifti.m @@ -0,0 +1,176 @@ +function canlab_write_gifti(filename, gii, varargin) +% Write a GIFTI (.gii) file natively in MATLAB, with no external toolbox. +% +% :Usage: +% :: +% canlab_write_gifti(filename, gii) +% canlab_write_gifti(filename, gii, 'encoding', 'GZipBase64Binary') +% +% Writes geometry (vertices+faces), per-vertex functional/shape data, and/or a +% label table to a GIFTI file. The struct layout matches canlab_read_gifti's +% output, so read->write->read round-trips exactly. Depends only on core MATLAB +% plus the JVM (for zlib deflate) -- no SPM/FieldTrip gifti toolbox required. +% +% :Inputs: +% +% **filename:** +% Char/string. Output path (e.g. '/tmp/out.surf.gii'). +% +% **gii:** +% Struct (as from canlab_read_gifti) with any of: +% .vertices : [N x 3] vertex coords -> NIFTI_INTENT_POINTSET +% .faces : [M x 3] 1-based faces -> NIFTI_INTENT_TRIANGLE (written 0-based) +% .cdata : [N x K] per-vertex data -> one DataArray per column +% .intents : 1xK cell of intent strings for cdata columns +% (default NIFTI_INTENT_NONE, or _LABEL if .labels present) +% .labels : struct array (.key,.name,.rgba) -> +% +% :Optional Inputs: +% **'encoding':** 'GZipBase64Binary' (default) | 'Base64Binary' | 'ASCII' +% +% :Examples: +% :: +% g = canlab_read_gifti(which('S1200.L.midthickness_MSMAll.32k_fs_LR.surf.gii')); +% canlab_write_gifti('/tmp/copy.surf.gii', g); +% +% :See also: canlab_read_gifti, canlab_read_cifti, canlab_write_cifti + +encoding = 'GZipBase64Binary'; +for i = 1:2:numel(varargin) + switch lower(varargin{i}) + case 'encoding', encoding = varargin{i+1}; + otherwise, error('canlab_write_gifti:badopt', 'Unknown option: %s', varargin{i}); + end +end + +if ~isstruct(gii) + error('canlab_write_gifti:input', 'Second argument must be a struct (see help).'); +end +gii = local_fill_defaults(gii); + +% Count data arrays +nda = 0; +if ~isempty(gii.vertices), nda = nda + 1; end +if ~isempty(gii.faces), nda = nda + 1; end +if ~isempty(gii.cdata), nda = nda + size(gii.cdata, 2); end +if nda == 0 + error('canlab_write_gifti:empty', 'Nothing to write (no vertices/faces/cdata).'); +end + +fid = fopen(char(filename), 'w'); +if fid < 0, error('canlab_write_gifti:open', 'Cannot open for writing: %s', filename); end +cleanupObj = onCleanup(@() fclose(fid)); %#ok + +fprintf(fid, '\n'); +fprintf(fid, '\n'); +fprintf(fid, '\n', nda); + +% Optional label table +if ~isempty(gii.labels) + local_write_labeltable(fid, gii.labels); +end + +% Geometry +if ~isempty(gii.vertices) + local_write_da(fid, 'NIFTI_INTENT_POINTSET', 'NIFTI_TYPE_FLOAT32', ... + single(gii.vertices), encoding); +end +if ~isempty(gii.faces) + local_write_da(fid, 'NIFTI_INTENT_TRIANGLE', 'NIFTI_TYPE_INT32', ... + int32(gii.faces - 1), encoding); % write 0-based +end + +% Per-vertex data: one DataArray per column +for c = 1:size(gii.cdata, 2) + intent = 'NIFTI_INTENT_NONE'; + if c <= numel(gii.intents) && ~isempty(gii.intents{c}) + intent = gii.intents{c}; + end + col = gii.cdata(:, c); + if contains(intent, 'LABEL') + local_write_da(fid, intent, 'NIFTI_TYPE_INT32', int32(col), encoding); + else + local_write_da(fid, intent, 'NIFTI_TYPE_FLOAT32', single(col), encoding); + end +end + +fprintf(fid, '\n'); +end % main + + +% ------------------------------------------------------------------------- +function gii = local_fill_defaults(gii) +flds = {'vertices', 'faces', 'cdata', 'intents', 'labels'}; +for i = 1:numel(flds) + if ~isfield(gii, flds{i}), gii.(flds{i}) = []; end +end +if isempty(gii.intents), gii.intents = {}; end +if ~isempty(gii.labels) && ~isempty(gii.cdata) && isempty(gii.intents) + gii.intents = repmat({'NIFTI_INTENT_LABEL'}, 1, size(gii.cdata, 2)); +end +end + + +% ------------------------------------------------------------------------- +function local_write_da(fid, intent, dtype, data, encoding) +% data is [Dim0 x Dim1] in MATLAB column-major; GIFTI stores RowMajorOrder. +d0 = size(data, 1); +d1 = size(data, 2); +dimn = 1 + (d1 > 1); + +% Row-major byte stream = bytes of the transpose, read column-major +dt = data'; +switch dtype + case 'NIFTI_TYPE_FLOAT32', bytes = typecast(single(dt(:))', 'uint8'); + case 'NIFTI_TYPE_INT32', bytes = typecast(int32(dt(:))', 'uint8'); + case 'NIFTI_TYPE_FLOAT64', bytes = typecast(double(dt(:))', 'uint8'); + case 'NIFTI_TYPE_UINT8', bytes = uint8(dt(:))'; + otherwise, error('canlab_write_gifti:dtype', 'Unsupported DataType %s', dtype); +end + +if dimn == 1 + dimattr = sprintf('Dimensionality="1" Dim0="%d"', d0); +else + dimattr = sprintf('Dimensionality="2" Dim0="%d" Dim1="%d"', d0, d1); +end + +fprintf(fid, ['\n'], ... + intent, dtype, dimattr, encoding); + +switch encoding + case 'ASCII' + if contains(dtype, 'INT') + fmt = '%d '; + else + fmt = '%.9g '; % full single-precision width + end + fprintf(fid, ''); + fprintf(fid, fmt, double(dt(:))); + fprintf(fid, '\n'); + case 'Base64Binary' + fprintf(fid, '%s\n', char(matlab.net.base64encode(bytes))); + case 'GZipBase64Binary' + comp = canlab_zlib_deflate(bytes); + fprintf(fid, '%s\n', char(matlab.net.base64encode(comp))); + otherwise + error('canlab_write_gifti:encoding', 'Unknown encoding %s', encoding); +end + +fprintf(fid, '\n'); +end + + +% ------------------------------------------------------------------------- +function local_write_labeltable(fid, labels) +fprintf(fid, '\n'); +for i = 1:numel(labels) + rgba = labels(i).rgba; + if numel(rgba) < 4, rgba = [rgba(:)' ones(1, 4 - numel(rgba))]; end + nm = labels(i).name; + if isempty(nm), nm = sprintf('label_%d', labels(i).key); end + fprintf(fid, '\n', ... + labels(i).key, rgba(1), rgba(2), rgba(3), rgba(4), nm); +end +fprintf(fid, '\n'); +end diff --git a/CanlabCore/Surface_tools/canlab_zlib_deflate.m b/CanlabCore/Surface_tools/canlab_zlib_deflate.m new file mode 100644 index 00000000..b19e5462 --- /dev/null +++ b/CanlabCore/Surface_tools/canlab_zlib_deflate.m @@ -0,0 +1,28 @@ +function comp = canlab_zlib_deflate(raw) +% Deflate (compress) bytes to a raw zlib stream natively via the JVM. +% +% :Usage: +% :: +% comp = canlab_zlib_deflate(raw) +% +% Produces a raw zlib stream (header 0x78 0x9C) suitable for GIFTI/CIFTI +% "GZipBase64Binary" payloads after base64 encoding. Uses java.util.zip +% (ships with MATLAB) -- no external toolbox. +% +% :Inputs: +% **raw:** uint8 vector of bytes to compress. +% +% :Outputs: +% **comp:** uint8 column vector, the raw zlib-compressed stream. +% +% :See also: canlab_zlib_inflate, canlab_write_gifti, canlab_write_cifti + +raw = uint8(raw(:)); +baos = java.io.ByteArrayOutputStream(); +dos = java.util.zip.DeflaterOutputStream(baos); +dos.write(typecast(raw, 'int8'), 0, numel(raw)); +dos.finish(); +dos.close(); +comp = typecast(int8(baos.toByteArray()), 'uint8'); +comp = comp(:); +end diff --git a/CanlabCore/Surface_tools/canlab_zlib_inflate.m b/CanlabCore/Surface_tools/canlab_zlib_inflate.m new file mode 100644 index 00000000..2f2c8278 --- /dev/null +++ b/CanlabCore/Surface_tools/canlab_zlib_inflate.m @@ -0,0 +1,30 @@ +function out = canlab_zlib_inflate(raw) +% Inflate (decompress) a raw zlib byte stream natively via the JVM. +% +% :Usage: +% :: +% out = canlab_zlib_inflate(raw) +% +% GIFTI "GZipBase64Binary" and similar payloads are raw zlib streams (header +% 0x78 0x9C), NOT gzip. This decompresses a uint8 vector entirely on the JVM +% side, which avoids a long-standing MATLAB bug where an in-place +% java.util.zip.Inflater.inflate(buffer) call returns garbage because MATLAB +% passes the Java method a copy of the buffer. Uses java.util.zip plus +% org.apache.commons.io.IOUtils (both ship with MATLAB) -- no external toolbox. +% +% :Inputs: +% **raw:** uint8 vector, a raw zlib-compressed stream. +% +% :Outputs: +% **out:** uint8 column vector of the decompressed bytes. +% +% :See also: canlab_zlib_deflate, canlab_read_gifti, canlab_read_cifti + +bais = java.io.ByteArrayInputStream(typecast(uint8(raw(:)), 'int8')); +iis = java.util.zip.InflaterInputStream(bais); +baos = java.io.ByteArrayOutputStream(); +org.apache.commons.io.IOUtils.copy(iis, baos); +iis.close(); +out = typecast(int8(baos.toByteArray()), 'uint8'); +out = out(:); +end diff --git a/CanlabCore/Unit_tests/surface_data/canlab_test_surface_analysis.m b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_analysis.m new file mode 100644 index 00000000..28c62f9f --- /dev/null +++ b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_analysis.m @@ -0,0 +1,154 @@ +function tests = canlab_test_surface_analysis +% Unit tests for fmri_surface_data analysis methods (M6). +% +% Covers cat / horzcat, ttest, regress (native OLS), predict (delegated, weight +% map remapped to surface), and ica (delegated; skipped if the icatb toolbox is +% absent). Uses synthetic grayordinate objects -- no external toolbox required +% (except ica, which needs icatb_fastICA and is skipped otherwise). +% +% Run: runtests('canlab_test_surface_analysis') +% +% :See also: cat, ttest, regress, predict, ica, fmri_surface_data + +tests = functiontests(localfunctions); +end + + +% ------------------------------------------------------------------------- +function setupOnce(t) +here = fileparts(mfilename('fullpath')); +addpath(fullfile(here, '..', '..', 'Surface_tools')); +addpath(fullfile(here, '..', '..')); +assert(~isempty(which('fmri_surface_data')), 'fmri_surface_data not on path.'); +t.TestData.figvis = get(0, 'DefaultFigureVisible'); +set(0, 'DefaultFigureVisible', 'off'); +end + +function teardownOnce(t) +close all force +set(0, 'DefaultFigureVisible', t.TestData.figvis); +end + + +% ------------------------------------------------------------------------- +function test_cat_and_horzcat(t) +A = local_obj((1:20)' + (0:5)*0.3); % 20 x 6 +B = local_obj((1:20)' + (0:5)*0.3 + 1); % 20 x 6, same space + +C = cat(A, B); +verifyEqual(t, size(C.dat), [20 12], 'cat should concatenate along maps.'); +C2 = [A, B]; +verifyEqual(t, C2.dat, C.dat, 'horzcat must match cat.'); +verifyEqual(t, numel(C.removed_images), 12, 'removed_images must track maps.'); + +% Per-map fields concatenate +A.image_names = arrayfun(@(k) sprintf('a%d',k), 1:6, 'unif', 0)'; +B.image_names = arrayfun(@(k) sprintf('b%d',k), 1:6, 'unif', 0)'; +C = cat(A, B); +verifyEqual(t, numel(C.image_names), 12, 'image_names must concatenate.'); + +% Space mismatch errors +Adiff = A; Adiff.surface_space = 'fsaverage_164k'; +verifyError(t, @() cat(A, Adiff), 'fmri_surface_data:cat:space'); +end + + +% ------------------------------------------------------------------------- +function test_ttest(t) +% Construct data with a known positive mean across maps. +base = ones(20, 1) * 2 + 0.01 * (1:20)'; % mostly positive +C = local_obj(base + 0.1 * randn(20, 10)); +tobj = ttest(C); +verifyEqual(t, class(tobj), 'fmri_surface_data'); +verifyEqual(t, size(tobj.dat), [20 1], 't-map is one column.'); +verifyTrue(t, isfield(tobj.additional_info, 'statistic'), 'stats stored in additional_info.'); +st = tobj.additional_info.statistic; +verifyEqual(t, numel(st.p), 20, 'p has one value per grayordinate.'); +verifyTrue(t, all(tobj.dat > 0), 'Positive-mean data should give positive t.'); +% Cross-check against MATLAB ttest at one grayordinate +[~, p1, ~, s1] = ttest(double(C.dat(5, :))); +% Note: fmri_data stores .dat as single, so the delegated t is single-precision. +verifyEqual(t, double(tobj.dat(5)), s1.tstat, 'RelTol', 1e-4, 't mismatch vs MATLAB ttest.'); +verifyEqual(t, double(st.p(5)), p1, 'AbsTol', 1e-4, 'p mismatch vs MATLAB ttest.'); +end + + +% ------------------------------------------------------------------------- +function test_regress_ols(t) +n = 15; +X = [ones(n,1), (1:n)'/n]; % intercept + linear +truebeta = [0.5; 3]; +C = local_obj((1:20)'*0 + (X * truebeta)' + 0.001*randn(20, n)); % 20 x n, ~linear in X +b = regress(C, X); +verifyEqual(t, size(b.dat), [20 2], 'betas: one column per regressor.'); +% Cross-check coefficients against MATLAB backslash at one grayordinate +g = 9; +bm = X \ double(C.dat(g, :))'; +verifyEqual(t, double(b.dat(g, :))', bm, 'AbsTol', 1e-5, 'OLS betas mismatch.'); +st = b.additional_info.statistic; +verifyEqual(t, size(st.t), [20 2], 't-stats per beta.'); +verifyEqual(t, st.dfe, n - 2, 'wrong residual df.'); +% Recovered slope should be near the true slope (.dat is single) +verifyEqual(t, double(mean(b.dat(:,2))), truebeta(2), 'AbsTol', 0.05, 'slope not recovered.'); +% Errors with no design +verifyError(t, @() regress(local_obj(randn(20,5))), 'fmri_surface_data:regress:noX'); +end + + +% ------------------------------------------------------------------------- +function test_predict_delegation(t) +% Outcome linearly related to a latent feature -> cv prediction should work and +% return a surface weight map of the right size. +n = 30; +w = randn(20, 1); +Y = (1:n)'; +D = w * (Y' - mean(Y)) + randn(20, n); % each grayordinate ~ scaled Y +C = local_obj(D); +C.Y = Y; +[cverr, stats] = predict(C, 'algorithm_name', 'cv_lassopcr', 'nfolds', 3, 'verbose', 0); +verifyTrue(t, isfinite(cverr), 'cverr should be finite.'); +verifyEqual(t, class(stats.weight_obj), 'fmri_surface_data', ... + 'weight_obj must be remapped to a surface object.'); +verifyEqual(t, size(stats.weight_obj.dat, 1), 20, 'weight map has one row per grayordinate.'); +verifyEqual(t, numel(stats.yfit), n, 'one cross-validated fit per observation.'); +% No-Y guard +Cn = local_obj(randn(20, n)); +verifyError(t, @() predict(Cn), 'fmri_surface_data:predict:noY'); +end + + +% ------------------------------------------------------------------------- +function test_ica_if_available(t) +if isempty(which('icatb_fastICA')) + t.assumeFail('icatb_fastICA (GIFT/icatb toolbox) not on path; ica delegation skipped.'); +end +n = 500; nm = 30; +S = randn(3, nm); A = randn(n, 3); D = A * S + 0.1 * randn(n, nm); +o = local_obj_n(D); +ic = ica(o, 3); +verifyEqual(t, class(ic), 'fmri_surface_data'); +verifyEqual(t, size(ic.dat, 1), n, 'IC maps have one row per grayordinate.'); +end + + +% ========================================================================= +function o = local_obj(D) +% 20-grayordinate cortex-only object (10 verts per hemisphere). +mL = struct('struct','CORTEX_LEFT','type','surf','start',1,'count',10,'numvert',32492,'vertlist',0:9,'voxlist',[]); +mR = struct('struct','CORTEX_RIGHT','type','surf','start',11,'count',10,'numvert',32492,'vertlist',0:9,'voxlist',[]); +bm = struct('type','dense','length',20,'models',{{mL,mR}},'vol',[]); +bm.grayordinate_type = 'cortex_only'; bm.cluster = []; +o = fmri_surface_data('dat', single(D), 'brain_model', bm, ... + 'surface_space', 'fsLR_32k', 'intent', 'dscalar'); +end + +function o = local_obj_n(D) +% n-grayordinate cortex-only object (n/2 verts per hemisphere). +n = size(D,1); h = n/2; +mL = struct('struct','CORTEX_LEFT','type','surf','start',1,'count',h,'numvert',32492,'vertlist',0:h-1,'voxlist',[]); +mR = struct('struct','CORTEX_RIGHT','type','surf','start',h+1,'count',h,'numvert',32492,'vertlist',0:h-1,'voxlist',[]); +bm = struct('type','dense','length',n,'models',{{mL,mR}},'vol',[]); +bm.grayordinate_type = 'cortex_only'; bm.cluster = []; +o = fmri_surface_data('dat', single(D), 'brain_model', bm, ... + 'surface_space', 'fsLR_32k', 'intent', 'dscalar'); +end diff --git a/CanlabCore/Unit_tests/surface_data/canlab_test_surface_io.m b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_io.m new file mode 100644 index 00000000..3522c595 --- /dev/null +++ b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_io.m @@ -0,0 +1,191 @@ +function tests = canlab_test_surface_io +% Unit tests for the native CIFTI-2 / GIFTI reader+writer (M1 of fmri_surface_data). +% +% Verifies that canlab_read_gifti / canlab_write_gifti / canlab_read_cifti / +% canlab_write_cifti work with NO external toolbox (no gifti, FieldTrip, +% cifti-matlab, or Connectome Workbench) -- only core MATLAB + the JVM. +% +% Run: runtests('canlab_test_surface_io') +% or it is auto-discovered by canlab_run_all_tests. +% +% Coverage: +% - GIFTI .surf round-trip (shipped S1200 fs_LR-32k surface): exact verts/faces, +% GZip and Base64 encodings. +% - GIFTI label round-trip with a LabelTable (synthetic). +% - CIFTI-2 dscalar round-trip (synthetic grayordinates: 2 surface models + +% 1 voxel model): cdata, BrainModel start/count/vertlist/voxlist, and the +% subcortical affine all preserved. +% - CIFTI-2 dlabel round-trip with a LabelTable (synthetic). +% - If a real .dscalar.nii is on the path, an end-to-end read->write->read. +% +% :See also: canlab_read_cifti, canlab_write_cifti, canlab_read_gifti, canlab_write_gifti + +tests = functiontests(localfunctions); +end + + +% ------------------------------------------------------------------------- +function setupOnce(t) +here = fileparts(mfilename('fullpath')); +st = fullfile(here, '..', '..', 'Surface_tools'); +if exist(st, 'dir'), addpath(st); end +% Sanity: the codec functions must be on the path +for fn = {'canlab_read_gifti','canlab_write_gifti','canlab_read_cifti','canlab_write_cifti'} + assert(~isempty(which(fn{1})), 'Missing required function on path: %s', fn{1}); +end +t.TestData.tmp = tempname; +mkdir(t.TestData.tmp); +end + +function teardownOnce(t) +if isfield(t.TestData, 'tmp') && exist(t.TestData.tmp, 'dir') + rmdir(t.TestData.tmp, 's'); +end +end + + +% ------------------------------------------------------------------------- +function test_gifti_surf_roundtrip(t) +surf = which('S1200.L.midthickness_MSMAll.32k_fs_LR.surf.gii'); +assert(~isempty(surf), 'Shipped surface S1200.L.midthickness...surf.gii not found on path.'); + +g = canlab_read_gifti(surf); +verifyEqual(t, size(g.vertices), [32492 3], 'fs_LR-32k left surface should have 32492 vertices.'); +verifyEqual(t, size(g.faces), [64980 3], 'fs_LR-32k left surface should have 64980 faces.'); +verifyGreaterThanOrEqual(t, min(g.faces(:)), 1, 'Faces must be 1-based for patch().'); +verifyLessThanOrEqual(t, max(g.faces(:)), 32492, 'Face indices must not exceed vertex count.'); + +for enc = {'GZipBase64Binary', 'Base64Binary', 'ASCII'} + f = fullfile(t.TestData.tmp, ['rt_' enc{1} '.surf.gii']); + canlab_write_gifti(f, g, 'encoding', enc{1}); + g2 = canlab_read_gifti(f); + tol = 0; if strcmp(enc{1}, 'ASCII'), tol = 1e-4; end % ASCII prints %g + verifyLessThanOrEqual(t, max(abs(g.vertices(:) - g2.vertices(:))), tol, ... + sprintf('Vertex round-trip failed for encoding %s', enc{1})); + verifyEqual(t, g.faces, g2.faces, ... + sprintf('Face round-trip failed for encoding %s', enc{1})); +end +end + + +% ------------------------------------------------------------------------- +function test_gifti_label_roundtrip(t) +n = 50; +keys = mod((0:n-1)', 4); % 0..3 +g = struct(); +g.vertices = []; g.faces = []; +g.cdata = keys; +g.intents = {'NIFTI_INTENT_LABEL'}; +g.labels = struct('key', {0 1 2 3}, ... + 'name', {'Unknown','A','B','C'}, ... + 'rgba', {[0 0 0 0],[1 0 0 1],[0 1 0 1],[0 0 1 1]}); + +f = fullfile(t.TestData.tmp, 'lbl.label.gii'); +canlab_write_gifti(f, g); +g2 = canlab_read_gifti(f); +verifyEqual(t, g2.cdata, double(keys), 'Label keys did not round-trip.'); +verifyEqual(t, numel(g2.labels), 4, 'Label table size changed.'); +verifyEqual(t, g2.labels(2).name, 'A', 'Label name did not round-trip.'); +verifyEqual(t, g2.labels(2).rgba, [1 0 0 1], 'Label RGBA did not round-trip.'); +end + + +% ------------------------------------------------------------------------- +function test_cifti_dscalar_roundtrip(t) +cii = local_make_synthetic_cifti('dscalar'); +f = fullfile(t.TestData.tmp, 'syn.dscalar.nii'); +canlab_write_cifti(f, cii); +c2 = canlab_read_cifti(f); + +verifyEqual(t, c2.intent, 'dscalar'); +verifyEqual(t, size(c2.cdata), size(cii.cdata), 'cdata size changed.'); +verifyLessThanOrEqual(t, max(abs(cii.cdata(:) - c2.cdata(:))), 0, 'cdata values changed.'); + +verifyEqual(t, numel(c2.diminfo{1}.models), numel(cii.diminfo{1}.models), 'Model count changed.'); +for i = 1:numel(cii.diminfo{1}.models) + a = cii.diminfo{1}.models{i}; b = c2.diminfo{1}.models{i}; + verifyEqual(t, b.struct, a.struct); + verifyEqual(t, b.start, a.start); + verifyEqual(t, b.count, a.count); + if strcmp(a.type, 'surf') + verifyEqual(t, b.vertlist, a.vertlist, 'Surface vertlist changed.'); + else + verifyEqual(t, b.voxlist, a.voxlist, 'Voxel IJK list changed.'); + end +end +verifyEqual(t, c2.diminfo{1}.vol.sform, cii.diminfo{1}.vol.sform, 'Subcortical affine changed.'); +verifyEqual(t, {c2.diminfo{2}.maps.name}, {cii.diminfo{2}.maps.name}, 'Scalar map names changed.'); +end + + +% ------------------------------------------------------------------------- +function test_cifti_dlabel_roundtrip(t) +cii = local_make_synthetic_cifti('dlabel'); +f = fullfile(t.TestData.tmp, 'syn.dlabel.nii'); +canlab_write_cifti(f, cii); +c2 = canlab_read_cifti(f); + +verifyEqual(t, c2.intent, 'dlabel'); +verifyLessThanOrEqual(t, max(abs(cii.cdata(:) - c2.cdata(:))), 0, 'Label keys changed.'); +tbl = c2.diminfo{2}.maps(1).table; +verifyEqual(t, numel(tbl), 3, 'Label table size changed.'); +verifyEqual(t, tbl(2).name, 'RegionA', 'Label name did not round-trip.'); +verifyEqual(t, tbl(2).rgba, [1 0 0 1], 'Label RGBA did not round-trip.'); +end + + +% ------------------------------------------------------------------------- +function test_real_cifti_if_available(t) +% Opportunistic: if a real dscalar is on the path (Neuroimaging_Pattern_Masks), +% verify an end-to-end native read->write->read with exact cdata. +f0 = which('transcriptomic_gradients.dscalar.nii'); +if isempty(f0) + t.assumeFail('No real .dscalar.nii on path; skipping (synthetic tests cover the codec).'); +end +c = canlab_read_cifti(f0); +f = fullfile(t.TestData.tmp, 'real_rt.dscalar.nii'); +canlab_write_cifti(f, c); +c2 = canlab_read_cifti(f); +verifyLessThanOrEqual(t, max(abs(double(c.cdata(:)) - double(c2.cdata(:)))), 0, ... + 'Real dscalar cdata did not round-trip exactly.'); +verifyEqual(t, numel(c2.diminfo{1}.models), numel(c.diminfo{1}.models)); +end + + +% ========================================================================= +function cii = local_make_synthetic_cifti(kind) +% Build a minimal valid grayordinate CIFTI struct: 2 surface models (L/R, 10 +% vertices each, 5 in-data) + 1 subcortical voxel model (4 voxels). + +mL = struct('struct','CORTEX_LEFT','type','surf','start',1,'count',5, ... + 'numvert',10,'vertlist',[0 2 4 6 8],'voxlist',[]); +mR = struct('struct','CORTEX_RIGHT','type','surf','start',6,'count',5, ... + 'numvert',10,'vertlist',[1 3 5 7 9],'voxlist',[]); +voxijk = [2 2 2; 3 2 2; 2 3 2; 2 2 3]'; % 3x4 IJK (0-based) +mV = struct('struct','THALAMUS_LEFT','type','vox','start',11,'count',4, ... + 'numvert',NaN,'vertlist',[],'voxlist',voxijk); + +sform = [2 0 0 -20; 0 2 0 -22; 0 0 2 -24; 0 0 0 1]; +dense = struct('type','dense','length',14,'models',{{mL,mR,mV}}, ... + 'vol',struct('dims',[10 10 10],'sform',sform)); + +G = 14; +switch kind + case 'dscalar' + nMaps = 2; + cdata = single(reshape(1:G*nMaps, G, nMaps)); % deterministic + maps = struct('name', {'mapOne','mapTwo'}, 'table', {[],[]}); + mapsdim = struct('type','scalars','length',nMaps,'maps',maps); + intent = 'dscalar'; + case 'dlabel' + cdata = single(mod((0:G-1)', 3)); % keys 0,1,2 + tbl = struct('key',{0 1 2}, 'name',{'Unknown','RegionA','RegionB'}, ... + 'rgba',{[0 0 0 0],[1 0 0 1],[0 0 1 1]}); + maps = struct('name', {'parc'}, 'table', {tbl}); + mapsdim = struct('type','labels','length',1,'maps',maps); + intent = 'dlabel'; +end + +cii = struct('cdata', cdata, 'intent', intent, ... + 'diminfo', {{dense, mapsdim}}); +end diff --git a/CanlabCore/Unit_tests/surface_data/canlab_test_surface_object_basic.m b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_object_basic.m new file mode 100644 index 00000000..80a332da --- /dev/null +++ b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_object_basic.m @@ -0,0 +1,158 @@ +function tests = canlab_test_surface_object_basic +% Unit tests for the fmri_surface_data class skeleton (M2). +% +% Verifies construction, the grayordinate property model, inheritance from +% image_vector, and the D5b "no empty-squeezing" behavior -- all with NO +% external toolbox. The core tests build a synthetic grayordinate object so they +% run anywhere; one opportunistic test uses a real .dscalar.nii if present. +% +% Run: runtests('canlab_test_surface_object_basic') +% +% :See also: fmri_surface_data, canlab_read_cifti, canlab_test_surface_io + +tests = functiontests(localfunctions); +end + + +% ------------------------------------------------------------------------- +function setupOnce(t) +here = fileparts(mfilename('fullpath')); +addpath(fullfile(here, '..', '..', 'Surface_tools')); +addpath(fullfile(here, '..', '..')); % so @fmri_surface_data resolves +assert(~isempty(which('fmri_surface_data')), 'fmri_surface_data not on path.'); +t.TestData.cii = local_synthetic_cifti(); +end + + +% ------------------------------------------------------------------------- +function test_empty_construction(t) +o = fmri_surface_data; +verifyTrue(t, isa(o, 'fmri_surface_data')); +verifyTrue(t, isa(o, 'image_vector'), 'Must subclass image_vector.'); +verifyEmpty(t, o.dat); +verifyEmpty(t, o.brain_model); +verifyEqual(t, o.intent, ''); +end + + +% ------------------------------------------------------------------------- +function test_build_from_grayordinate_struct(t) +o = fmri_surface_data(t.TestData.cii); +verifyEqual(t, class(o), 'fmri_surface_data'); +verifyEqual(t, size(o.dat), [14 2], 'Grayordinate dat should be 14 x 2.'); +verifyEqual(t, o.intent, 'dscalar'); + +% brain_model carries the model split, 1:1 with .dat rows +bm = o.brain_model; +verifyEqual(t, numel(bm.models), 3, 'Expected L cortex + R cortex + 1 voxel model.'); +total = sum(cellfun(@(m) m.count, bm.models)); +verifyEqual(t, total, size(o.dat, 1), 'Sum of model counts must equal grayordinate rows.'); +verifyEqual(t, bm.models{1}.struct, 'CORTEX_LEFT'); +verifyEqual(t, bm.models{3}.type, 'vox'); +verifyEqual(t, bm.vol.sform, t.TestData.cii.diminfo{1}.vol.sform, 'Subcortical affine lost.'); + +% map names carried +verifyEqual(t, reshape(o.image_names,1,[]), {'mapOne','mapTwo'}); +end + + +% ------------------------------------------------------------------------- +function test_removed_vectors_are_vestigial(t) +% D5b: removed_voxels/removed_images are all-false and length-correct. +o = fmri_surface_data(t.TestData.cii); +verifyEqual(t, numel(o.removed_voxels), size(o.dat,1)); +verifyEqual(t, numel(o.removed_images), size(o.dat,2)); +verifyFalse(t, any(o.removed_voxels), 'removed_voxels must be all-false.'); +verifyFalse(t, any(o.removed_images), 'removed_images must be all-false.'); +end + + +% ------------------------------------------------------------------------- +function test_remove_replace_empty_are_noops(t) +% Even with all-zero grayordinate rows, nothing is squeezed. +o = fmri_surface_data(t.TestData.cii); +o.dat(7, :) = 0; % zero out a whole grayordinate row +n0 = size(o.dat, 1); + +o1 = remove_empty(o); +verifyEqual(t, size(o1.dat, 1), n0, 'remove_empty must NOT drop rows.'); +verifyEqual(t, o1.dat, o.dat, 'remove_empty must return data unchanged.'); +verifyFalse(t, any(o1.removed_voxels), 'removed_voxels must stay all-false.'); + +o2 = replace_empty(o1); +verifyEqual(t, size(o2.dat, 1), n0, 'replace_empty must not change size.'); +verifyEqual(t, o2.dat, o.dat, 'replace_empty must return data unchanged.'); +end + + +% ------------------------------------------------------------------------- +function test_get_wh_image_subsets_maps_only(t) +% Inherited get_wh_image selects map columns; grayordinate rows are preserved. +o = fmri_surface_data(t.TestData.cii); +o2 = get_wh_image(o, 2); +verifyEqual(t, size(o2.dat), [14 1], 'get_wh_image should keep all rows, 1 map.'); +verifyEqual(t, o2.dat, o.dat(:, 2), 'Wrong map selected.'); +verifyEqual(t, numel(o2.image_names), 1, 'image_names not subset with maps.'); +verifyEqual(t, numel(o2.removed_images), 1, 'removed_images not subset with maps.'); +% rows (grayordinates) unchanged +verifyEqual(t, numel(o2.removed_voxels), 14, 'Grayordinate rows must be preserved.'); +end + + +% ------------------------------------------------------------------------- +function test_method_surface_is_inherited(t) +% The image_vector method surface is inherited by dispatch (same method names +% as fmri_data/image_vector). Note: methods that dereference volInfo +% (descriptives, montage, orthviews, flip, ...) get surface-aware guards/ +% overrides in M3 (Risk #1); here we just confirm the surface is present. +o = fmri_surface_data(t.TestData.cii); +m = methods(o); +for name = {'get_wh_image','remove_empty','replace_empty','apply_parcellation','ica'} + verifyTrue(t, ismember(name{1}, m), sprintf('Method %s should be on the object.', name{1})); +end +% remove_empty / replace_empty must be overridden in THIS class folder (no-op +% semantics are verified separately in test_remove_replace_empty_are_noops). +clsdir = fileparts(which('fmri_surface_data')); +verifyTrue(t, exist(fullfile(clsdir, 'remove_empty.m'), 'file') == 2, ... + 'remove_empty.m override missing from @fmri_surface_data.'); +verifyTrue(t, exist(fullfile(clsdir, 'replace_empty.m'), 'file') == 2, ... + 'replace_empty.m override missing from @fmri_surface_data.'); +end + + +% ------------------------------------------------------------------------- +function test_real_dscalar_if_available(t) +f = which('transcriptomic_gradients.dscalar.nii'); +if isempty(f) + t.assumeFail('No real .dscalar.nii on path; synthetic tests cover the class.'); +end +o = fmri_surface_data(f); +verifyEqual(t, class(o), 'fmri_surface_data'); +verifyGreaterThan(t, size(o.dat,1), 60000, 'Expected tens of thousands of grayordinates.'); +verifyEqual(t, sum(cellfun(@(m) m.count, o.brain_model.models)), size(o.dat,1)); +verifyEqual(t, o.brain_model.models{1}.struct, 'CORTEX_LEFT'); +% .dat is full, no squeezing +verifyEqual(t, numel(o.removed_voxels), size(o.dat,1)); +verifyFalse(t, any(o.removed_voxels)); +end + + +% ========================================================================= +function cii = local_synthetic_cifti() +% Minimal grayordinate dscalar: 2 surface models (L/R, 5 in-data each) + 1 +% subcortical voxel model (4 voxels) = 14 grayordinates, 2 maps. +mL = struct('struct','CORTEX_LEFT','type','surf','start',1,'count',5, ... + 'numvert',32492,'vertlist',[0 2 4 6 8],'voxlist',[]); +mR = struct('struct','CORTEX_RIGHT','type','surf','start',6,'count',5, ... + 'numvert',32492,'vertlist',[1 3 5 7 9],'voxlist',[]); +voxijk = [2 2 2; 3 2 2; 2 3 2; 2 2 3]'; +mV = struct('struct','THALAMUS_LEFT','type','vox','start',11,'count',4, ... + 'numvert',NaN,'vertlist',[],'voxlist',voxijk); +sform = [2 0 0 -20; 0 2 0 -22; 0 0 2 -24; 0 0 0 1]; +dense = struct('type','dense','length',14,'models',{{mL,mR,mV}}, ... + 'vol',struct('dims',[10 10 10],'sform',sform)); +maps = struct('name',{'mapOne','mapTwo'},'table',{[],[]}); +mapsdim = struct('type','scalars','length',2,'maps',maps); +cii = struct('cdata', single(reshape(1:28,14,2)), 'intent','dscalar', ... + 'diminfo', {{dense, mapsdim}}); +end diff --git a/CanlabCore/Unit_tests/surface_data/canlab_test_surface_parcellation.m b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_parcellation.m new file mode 100644 index 00000000..a030bb10 --- /dev/null +++ b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_parcellation.m @@ -0,0 +1,134 @@ +function tests = canlab_test_surface_parcellation +% Unit tests for fmri_surface_data parcellation / region methods (M7). +% +% Covers apply_parcellation (parcel means, labels, area weighting, space check), +% cluster-extent threshold ('k'), and surface_region. Core tests use a synthetic +% object with a known parcellation (self-contained); mesh-dependent tests use a +% real fs_LR file if present. +% +% Run: runtests('canlab_test_surface_parcellation') +% +% :See also: apply_parcellation, threshold, surface_region, reparse_contiguous + +tests = functiontests(localfunctions); +end + + +% ------------------------------------------------------------------------- +function setupOnce(t) +here = fileparts(mfilename('fullpath')); +addpath(fullfile(here, '..', '..', 'Surface_tools')); +addpath(fullfile(here, '..', '..')); +assert(~isempty(which('fmri_surface_data')), 'fmri_surface_data not on path.'); +t.TestData.figvis = get(0, 'DefaultFigureVisible'); +set(0, 'DefaultFigureVisible', 'off'); +end + +function teardownOnce(t) +close all force +set(0, 'DefaultFigureVisible', t.TestData.figvis); +end + + +% ------------------------------------------------------------------------- +function test_apply_parcellation_known(t) +% 20 grayordinates, 3 parcels + background (0). Data == key, so each parcel mean +% equals its key. +keys = [1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 0 0 0 0 0]'; +D = [double(keys), double(keys)*10]; +o = local_obj(D); + +[pm, labels, tbl] = apply_parcellation(o, keys); +verifyEqual(t, size(pm), [2 3], 'parcel_means should be [nMaps x nParcels].'); +verifyEqual(t, pm(1,:), [1 2 3], 'AbsTol', 1e-6, 'map1 parcel means should equal the keys.'); +verifyEqual(t, pm(2,:), [10 20 30], 'AbsTol', 1e-5, 'map2 parcel means should equal 10*keys.'); +verifyEqual(t, numel(labels), 3); +verifyEqual(t, tbl.n_grayordinates', [5 5 5], 'each parcel has 5 grayordinates.'); +% background key 0 must be excluded +verifyFalse(t, any(tbl.key == 0), 'background (key 0) must be excluded.'); +end + + +% ------------------------------------------------------------------------- +function test_apply_parcellation_from_object_and_space_check(t) +keys = [ones(10,1); 2*ones(10,1)]; +o = local_obj([double(keys), double(keys)]); +parc = local_obj(double(keys)); % a "dlabel"-like object on the same space +parc.intent = 'dlabel'; +parc.label_table = struct('key', {1 2}, 'name', {'A','B'}, 'rgba', {[1 0 0 1],[0 0 1 1]}); + +[pm, labels] = apply_parcellation(o, parc); +verifyEqual(t, pm(1,:), [1 2], 'AbsTol', 1e-6); +verifyEqual(t, labels, {'A','B'}, 'labels should come from the label_table.'); + +% Different space must error +parc2 = parc; parc2.surface_space = 'fsaverage_164k'; +verifyError(t, @() apply_parcellation(o, parc2), 'fmri_surface_data:apply_parcellation:space'); +end + + +% ------------------------------------------------------------------------- +function test_real_atlas_parcellation(t) +f = which('Gordon333.32k_fs_LR_Tian_Subcortex_S2.dlabel.nii'); +if isempty(f), t.assumeFail('No real .dlabel atlas on path.'); end +atl = fmri_surface_data(f); +% Use the atlas keys as the data: each parcel mean must equal its key +data = atl; data.dat = single(double(atl.dat)); data.intent = 'dscalar'; +data.removed_images = false(1,1); +[pm, labels] = apply_parcellation(data, atl); +ukeys = unique(round(double(atl.dat(atl.dat > 0)))); +verifyEqual(t, numel(labels), numel(ukeys), 'one label per positive key.'); +verifyEqual(t, pm(1,:)', ukeys, 'AbsTol', 1e-4, 'parcel mean of key-data must equal the key.'); +% area-weighted means are finite +pmA = apply_parcellation(data, atl, 'area'); +verifyTrue(t, all(isfinite(pmA(:))), 'area-weighted parcel means must be finite.'); +end + + +% ------------------------------------------------------------------------- +function test_cluster_extent_threshold(t) +f = which('transcriptomic_gradients.dscalar.nii'); +if isempty(f), t.assumeFail('No fs_LR dscalar on path.'); end +s = fmri_surface_data(f); +raw = threshold(s, 1.0, 'positive'); +big = threshold(s, 1.0, 'positive', 'k', 50); +verifyLessThanOrEqual(t, nnz(big.dat(:,1) ~= 0), nnz(raw.dat(:,1) ~= 0), ... + 'cluster-extent threshold cannot increase the surviving set.'); +% Every surviving cluster must have >= 50 grayordinates +[chk, ~] = reparse_contiguous(big, 'which_image', 1); +cl = chk.brain_model.cluster; +if any(cl > 0) + szs = accumarray(cl(cl > 0), 1); + verifyGreaterThanOrEqual(t, min(szs(szs > 0)), 50, 'a surviving cluster is smaller than k.'); +end +end + + +% ------------------------------------------------------------------------- +function test_surface_region(t) +f = which('transcriptomic_gradients.dscalar.nii'); +if isempty(f), t.assumeFail('No fs_LR dscalar on path.'); end +s = threshold(fmri_surface_data(f), 1.5, 'positive'); +reg = surface_region(s, 'which_image', 1); +verifyGreaterThan(t, numel(reg), 0, 'expected at least one region.'); +% Field sanity on the first region +r1 = reg(1); +verifyTrue(t, all(isfield(r1, {'struct','type','grayord_rows','XYZmm','numVox','val'}))); +verifyEqual(t, r1.numVox, numel(r1.grayord_rows)); +% Total grayordinates across regions == active grayordinates +total = sum([reg.numVox]); +active = nnz(s.dat(:,1) ~= 0 & ~isnan(s.dat(:,1))); +verifyEqual(t, total, active, 'regions must partition the active grayordinates.'); +end + + +% ========================================================================= +function o = local_obj(D) +n = size(D,1); h = n/2; +mL = struct('struct','CORTEX_LEFT','type','surf','start',1,'count',h,'numvert',32492,'vertlist',0:h-1,'voxlist',[]); +mR = struct('struct','CORTEX_RIGHT','type','surf','start',h+1,'count',h,'numvert',32492,'vertlist',0:h-1,'voxlist',[]); +bm = struct('type','dense','length',n,'models',{{mL,mR}},'vol',[]); +bm.grayordinate_type = 'cortex_only'; bm.cluster = []; +o = fmri_surface_data('dat', single(D), 'brain_model', bm, ... + 'surface_space', 'fsLR_32k', 'intent', 'dscalar'); +end diff --git a/CanlabCore/Unit_tests/surface_data/canlab_test_surface_render.m b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_render.m new file mode 100644 index 00000000..1fdb115a --- /dev/null +++ b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_render.m @@ -0,0 +1,154 @@ +function tests = canlab_test_surface_render +% Unit tests for fmri_surface_data rendering + contiguity (M5). +% +% Covers the geom loader, native surface() rendering (4-panel, direct vertex +% coloring), render_on_surface onto existing patches (native + via-volume), +% reparse_contiguous (mesh connected components), and plot QC. Figures are +% rendered offscreen and checked structurally (handles, FaceVertexCData) -- no +% visual inspection needed. Requires the bundled meshes + CBIG warps (in-repo); +% no external toolbox. +% +% Run: runtests('canlab_test_surface_render') +% +% :See also: surface, render_on_surface, reparse_contiguous, fmri_surface_data + +tests = functiontests(localfunctions); +end + + +% ------------------------------------------------------------------------- +function setupOnce(t) +here = fileparts(mfilename('fullpath')); +addpath(fullfile(here, '..', '..', 'Surface_tools')); +addpath(fullfile(here, '..', '..')); +assert(~isempty(which('fmri_surface_data')), 'fmri_surface_data not on path.'); +assert(~isempty(which('addbrain')), 'addbrain not on path.'); +t.TestData.figvis = get(0, 'DefaultFigureVisible'); +set(0, 'DefaultFigureVisible', 'off'); +% A real fs_LR dscalar (covers all vertices incl. medial wall) +f = which('transcriptomic_gradients.dscalar.nii'); +assert(~isempty(f), 'transcriptomic_gradients.dscalar.nii not on path.'); +t.TestData.s = fmri_surface_data(f); +end + +function teardownOnce(t) +close all force; +set(0, 'DefaultFigureVisible', t.TestData.figvis); +end + + +% ------------------------------------------------------------------------- +function test_geom_loads_for_both_spaces(t) +% Geometry is loaded (correct vertex counts) indirectly via surface() for both +% the fs_LR-32k and fsaverage-164k bundled meshes. (load_surface_geom is a +% private helper, verified through the public surface() API.) +h1 = surface(t.TestData.s); % fs_LR-32k +verifyEqual(t, size(get(h1.surfaces(1), 'Vertices'), 1), 32492); +close(h1.figure); + +vol = local_synthetic_volume(); +sf = vol2surf(vol); % fsaverage_164k object +h2 = surface(sf); +verifyEqual(t, size(get(h2.surfaces(1), 'Vertices'), 1), 163842); +close(h2.figure); +end + + +% ------------------------------------------------------------------------- +function test_surface_native_render(t) +h = surface(t.TestData.s, 'which_image', 1); +verifyTrue(t, isgraphics(h.figure)); +verifyEqual(t, numel(h.surfaces), 4, 'Expected 4 panels (L/R x lateral/medial).'); +for k = 1:4 + c = get(h.surfaces(k), 'FaceVertexCData'); + V = get(h.surfaces(k), 'Vertices'); + verifyEqual(t, size(c), [size(V,1) 3], 'Each patch must be truecolor-colored per vertex.'); + verifyEqual(t, size(V,1), 32492, 'Native fs_LR mesh should have 32492 vertices.'); +end +close(h.figure); +end + + +% ------------------------------------------------------------------------- +function test_medial_wall_is_gray(t) +% For a 91k object (medial wall excluded), medial-wall vertices render gray. +f = which('Gordon333.32k_fs_LR_Tian_Subcortex_S2.dlabel.nii'); +if isempty(f), t.assumeFail('No 91k dlabel on path.'); end +o = fmri_surface_data(f); +h = surface(o); +% Left hemi panel 1: medial-wall vertices (not in vertlist) should be exactly gray +lh_model = o.brain_model.models{1}; +inmask = false(lh_model.numvert, 1); inmask(lh_model.vertlist + 1) = true; +c = get(h.surfaces(1), 'FaceVertexCData'); +graymask = all(abs(c - 0.5) < 1e-6, 2); +% Every medial-wall vertex must be gray +verifyTrue(t, all(graymask(~inmask)), 'Medial wall vertices must render gray.'); +close(h.figure); +end + + +% ------------------------------------------------------------------------- +function test_render_on_existing_native_patch(t) +% Color an addbrain native fs_LR patch directly (no resampling). +hp = addbrain('hcp inflated left'); % 32492-vertex fs_LR mesh, Tag has 'left' +render_on_surface(t.TestData.s, hp, 'which_image', 1); +c = get(hp, 'FaceVertexCData'); +verifyEqual(t, size(c), [32492 3], 'Direct native coloring should set per-vertex truecolor.'); +verifyEqual(t, get(hp, 'FaceColor'), 'interp'); +close all force; +end + + +% ------------------------------------------------------------------------- +function test_render_on_mni_surface_via_volume(t) +% An fsaverage object on an arbitrary MNI surface routes through a volume. +vol = local_synthetic_volume(); +sf = vol2surf(vol); +hp = addbrain('left'); % MNI pial-ish surface (not fsaverage topology) +nverts = size(get(hp, 'Vertices'), 1); +verifyNotEqual(t, nverts, 163842, 'addbrain left should not be fsaverage topology.'); +render_on_surface(sf, hp, 'clim', [-2 2]); +c = get(hp, 'FaceVertexCData'); +verifyEqual(t, size(c, 1), nverts, 'Via-volume render should color all patch vertices.'); +close all force; +end + + +% ------------------------------------------------------------------------- +function test_reparse_contiguous(t) +st = threshold(t.TestData.s, 1.0, 'positive'); +[st, ncl] = reparse_contiguous(st, 'which_image', 1); +verifyGreaterThan(t, ncl, 0, 'Expected at least one cluster.'); +verifyEqual(t, numel(st.brain_model.cluster), size(st.dat,1)); +active = st.dat(:,1) ~= 0 & ~isnan(st.dat(:,1)); +verifyEqual(t, nnz(st.brain_model.cluster > 0), nnz(active), ... + 'Every active grayordinate must get a cluster label.'); +verifyEqual(t, nnz(st.brain_model.cluster(~active)), 0, ... + 'Inactive grayordinates must have label 0.'); +end + + +% ------------------------------------------------------------------------- +function test_plot_runs(t) +h = plot(t.TestData.s, 'norender'); +verifyTrue(t, isgraphics(h.figure)); +close(h.figure); +end + + +% ========================================================================= +function vol = local_synthetic_volume() +dims = [91 109 91]; +tmat = [-2 0 0 92; 0 2 0 -128; 0 0 2 -74; 0 0 0 1]; +[I, J, K] = ndgrid(1:dims(1), 1:dims(2), 1:dims(3)); +xyz = tmat * [I(:)'; J(:)'; K(:)'; ones(1, numel(I))]; +f = sin(xyz(1,:)/30) + cos(xyz(2,:)/40); +iv = image_vector; +iv.volInfo = struct('mat', tmat, 'dim', dims, 'dt', [16 0], ... + 'xyzlist', [I(:) J(:) K(:)], 'nvox', prod(dims), ... + 'image_indx', true(prod(dims),1), 'wh_inmask', (1:prod(dims))', ... + 'n_inmask', prod(dims), 'fname', ''); +iv.dat = single(f(:)); +iv.removed_voxels = false(prod(dims),1); iv.removed_images = false; +vol = fmri_data(iv); +end diff --git a/CanlabCore/Unit_tests/surface_data/canlab_test_surface_space_recon.m b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_space_recon.m new file mode 100644 index 00000000..0be4d0d4 --- /dev/null +++ b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_space_recon.m @@ -0,0 +1,165 @@ +function tests = canlab_test_surface_space_recon +% Unit tests for fmri_surface_data spatial overrides + interop (M3). +% +% Covers compare_space (0/1/2/3 contract), reconstruct_image (cortex dense +% arrays + subcortex volume), to_fmri_data (subcortex -> fmri_data), write +% (native CIFTI round-trip, faithful + regenerated), and rebuild_like. Core +% tests use a synthetic grayordinate object (no external toolbox); one +% opportunistic test uses a real .dlabel.nii if present. +% +% Run: runtests('canlab_test_surface_space_recon') +% +% :See also: fmri_surface_data, reconstruct_image, to_fmri_data, compare_space + +tests = functiontests(localfunctions); +end + + +% ------------------------------------------------------------------------- +function setupOnce(t) +here = fileparts(mfilename('fullpath')); +addpath(fullfile(here, '..', '..', 'Surface_tools')); +addpath(fullfile(here, '..', '..')); +assert(~isempty(which('fmri_surface_data')), 'fmri_surface_data not on path.'); +t.TestData.tmp = tempname; mkdir(t.TestData.tmp); +t.TestData.obj = fmri_surface_data(local_synthetic_cifti()); +end + +function teardownOnce(t) +if isfield(t.TestData,'tmp') && exist(t.TestData.tmp,'dir'), rmdir(t.TestData.tmp,'s'); end +end + + +% ------------------------------------------------------------------------- +function test_volinfo_subblock_populated(t) +o = t.TestData.obj; +verifyNotEmpty(t, o.volInfo, 'volInfo (subcortical sub-block) should be populated.'); +verifyEqual(t, o.volInfo.dim, [10 10 10]); +verifyEqual(t, o.volInfo.n_inmask, 4, 'Subcortical sub-block should have 4 voxels.'); +% 0-based CIFTI affine -> 1-based SPM .mat conversion +expected_mat = [2 0 0 -20; 0 2 0 -22; 0 0 2 -24; 0 0 0 1]; +expected_mat(:,4) = expected_mat(:,4) - sum(expected_mat(:,1:3),2); +verifyEqual(t, o.volInfo.mat, expected_mat, 'Subcortical affine (1-based) incorrect.'); +end + + +% ------------------------------------------------------------------------- +function test_to_fmri_data(t) +o = t.TestData.obj; +vol = to_fmri_data(o); +verifyEqual(t, class(vol), 'fmri_data'); +verifyEqual(t, size(vol.dat), [4 2], 'Subcortex should be 4 voxels x 2 maps.'); +% Values equal the subcortical grayordinate rows (rows 11:14) +verifyEqual(t, double(vol.dat), double(o.dat(11:14, :)), 'AbsTol', 1e-6, ... + 'to_fmri_data values must equal the subcortical grayordinate rows.'); +verifyEqual(t, vol.volInfo.dim, [10 10 10]); +end + + +% ------------------------------------------------------------------------- +function test_reconstruct_image(t) +o = t.TestData.obj; +r = reconstruct_image(o); + +% Cortex: dense [numvert x nMaps]; medial wall (non in-data verts) -> NaN +verifyEqual(t, size(r.cortex_left), [10 2]); +verifyEqual(t, sum(isnan(r.cortex_left(:,1))), 5, 'Half the vertices are medial wall (NaN).'); +% In-data vertices (0-based [0 2 4 6 8] -> 1-based [1 3 5 7 9]) carry the data +verifyEqual(t, r.cortex_left([1 3 5 7 9], 1), double(o.dat(1:5, 1)), 'AbsTol', 1e-6); + +% Volume present and a known voxel matches the subcortical data +verifyTrue(t, isfield(r, 'volume')); +verifyEqual(t, size(r.volume), [10 10 10 2]); +% First subcortical voxel is IJK (2,2,2) 0-based -> (3,3,3) 1-based, row 11 +verifyEqual(t, r.volume(3,3,3,1), double(o.dat(11,1)), 'AbsTol', 1e-6); +end + + +% ------------------------------------------------------------------------- +function test_compare_space_contract(t) +o = t.TestData.obj; +verifyEqual(t, compare_space(o, o), 0, 'Identical objects -> 0.'); + +% Selecting maps does not change the grayordinate space -> 0 +verifyEqual(t, compare_space(o, get_wh_image(o, 1)), 0); + +% Same space tag + layout but different in-data vertices -> 3 +o3 = o; +o3.brain_model.models{1}.vertlist = [0 1 2 3 4]; % different selection +verifyEqual(t, compare_space(o, o3), 3, 'Different in-data grayordinates -> 3.'); + +% Different space tag -> 1 +o1 = o; o1.surface_space = 'fsaverage_164k'; +verifyEqual(t, compare_space(o, o1), 1, 'Different space tag -> 1.'); + +% Missing brain_model -> 2 +oempty = fmri_surface_data; +verifyEqual(t, compare_space(o, oempty), 2, 'Missing brain_model -> 2.'); +end + + +% ------------------------------------------------------------------------- +function test_rebuild_like(t) +o = t.TestData.obj; +rb = rebuild_like(o, o.dat * 3); +verifyEqual(t, class(rb), 'fmri_surface_data'); +verifyTrue(t, isequaln(rb.brain_model, o.brain_model), 'brain_model must be preserved.'); +verifyEqual(t, double(rb.dat), double(o.dat)*3, 'AbsTol', 1e-3); +verifyEqual(t, numel(rb.removed_voxels), size(rb.dat,1)); +% Row-count mismatch must error (geometry is fixed) +verifyError(t, @() rebuild_like(o, o.dat(1:5,:)), 'fmri_surface_data:rebuild_like:rowmismatch'); +end + + +% ------------------------------------------------------------------------- +function test_write_roundtrip_cifti(t) +o = t.TestData.obj; + +% Faithful path (stashed xml present? synthetic has none -> regenerate path) +f = fullfile(t.TestData.tmp, 'syn.dscalar.nii'); +write(o, f); +o2 = fmri_surface_data(f); +verifyEqual(t, size(o2.dat), size(o.dat)); +verifyEqual(t, double(o2.dat), double(o.dat), 'AbsTol', 0, 'CIFTI write->read changed data.'); +verifyEqual(t, numel(o2.brain_model.models), numel(o.brain_model.models)); +verifyEqual(t, o2.brain_model.vol.sform, o.brain_model.vol.sform, 'Affine lost on write.'); +verifyEqual(t, reshape(o2.image_names,1,[]), {'mapOne','mapTwo'}, 'Map names lost on write.'); +end + + +% ------------------------------------------------------------------------- +function test_real_dlabel_write_if_available(t) +f0 = which('Gordon333.32k_fs_LR_Tian_Subcortex_S2.dlabel.nii'); +if isempty(f0) + t.assumeFail('No real .dlabel.nii on path; synthetic tests cover write.'); +end +o = fmri_surface_data(f0); +f = fullfile(t.TestData.tmp, 'real.dlabel.nii'); +write(o, f); +o2 = fmri_surface_data(f); +verifyEqual(t, double(o2.dat), double(o.dat), 'AbsTol', 0, 'dlabel keys changed on write.'); +verifyEqual(t, numel(o2.label_table), numel(o.label_table), 'label table changed on write.'); +% subcortex export +vol = to_fmri_data(o); +verifyEqual(t, class(vol), 'fmri_data'); +verifyGreaterThan(t, size(vol.dat,1), 1000, 'Expected thousands of subcortical voxels.'); +end + + +% ========================================================================= +function cii = local_synthetic_cifti() +mL = struct('struct','CORTEX_LEFT','type','surf','start',1,'count',5, ... + 'numvert',10,'vertlist',[0 2 4 6 8],'voxlist',[]); +mR = struct('struct','CORTEX_RIGHT','type','surf','start',6,'count',5, ... + 'numvert',10,'vertlist',[1 3 5 7 9],'voxlist',[]); +voxijk = [2 2 2; 3 2 2; 2 3 2; 2 2 3]'; +mV = struct('struct','THALAMUS_LEFT','type','vox','start',11,'count',4, ... + 'numvert',NaN,'vertlist',[],'voxlist',voxijk); +sform = [2 0 0 -20; 0 2 0 -22; 0 0 2 -24; 0 0 0 1]; +dense = struct('type','dense','length',14,'models',{{mL,mR,mV}}, ... + 'vol',struct('dims',[10 10 10],'sform',sform)); +maps = struct('name',{'mapOne','mapTwo'},'table',{[],[]}); +mapsdim = struct('type','scalars','length',2,'maps',maps); +cii = struct('cdata', single(reshape(1:28,14,2)), 'intent','dscalar', ... + 'diminfo', {{dense, mapsdim}}); +end diff --git a/CanlabCore/Unit_tests/surface_data/canlab_test_surface_vol_map.m b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_vol_map.m new file mode 100644 index 00000000..5b2eea8c --- /dev/null +++ b/CanlabCore/Unit_tests/surface_data/canlab_test_surface_vol_map.m @@ -0,0 +1,154 @@ +function tests = canlab_test_surface_vol_map +% Unit tests for fmri_surface_data volume<->surface mapping (M4). +% +% Covers vol2surf (volume -> fsaverage_164k via CBIG RF), surf2vol (fsaverage -> +% MNI fmri_data), the vol->surf->vol round-trip fidelity, and the mean / +% apply_mask / threshold overrides. Uses a synthetic smooth MNI volume + the +% vendored CBIG warps (in-repo) -- no external toolbox. +% +% Run: runtests('canlab_test_surface_vol_map') +% +% :See also: vol2surf, surf2vol, fmri_surface_data + +tests = functiontests(localfunctions); +end + + +% ------------------------------------------------------------------------- +function setupOnce(t) +here = fileparts(mfilename('fullpath')); +addpath(fullfile(here, '..', '..', 'Surface_tools')); +addpath(fullfile(here, '..', '..')); +assert(~isempty(which('fmri_surface_data')), 'fmri_surface_data not on path.'); +assert(~isempty(which('vol2surf')), 'vol2surf not on path.'); +t.TestData.vol = local_synthetic_volume(); +end + + +% ------------------------------------------------------------------------- +function test_vol2surf_basic(t) +s = vol2surf(t.TestData.vol); +verifyEqual(t, class(s), 'fmri_surface_data'); +verifyEqual(t, s.surface_space, 'fsaverage_164k'); +verifyEqual(t, size(s.dat), [2*163842 1], 'Expected 2 hemispheres x 163842 vertices.'); +verifyEqual(t, numel(s.brain_model.models), 2); +verifyEqual(t, s.brain_model.models{1}.struct, 'CORTEX_LEFT'); +verifyEqual(t, s.brain_model.models{2}.numvert, 163842); +verifyFalse(t, any(isnan(s.dat(:))), 'No NaNs expected from interpn with extrapval 0.'); +end + + +% ------------------------------------------------------------------------- +function test_vol2surf_matches_interpn(t) +% A surface vertex value must equal the volume sampled at that vertex's RAS. +vol = t.TestData.vol; +s = vol2surf(vol); +L = load(canlab_cbig_warp_path('lh_ras')); ras = L.ras; +V = reconstruct_image(vol); +for vtest = [1 1000 80000 163842] + voxc = vol.volInfo.mat \ [ras(:, vtest); 1]; + expected = interpn(V, voxc(1), voxc(2), voxc(3), 'linear', 0); + verifyEqual(t, double(s.dat(vtest, 1)), expected, 'AbsTol', 1e-5, ... + sprintf('Left vertex %d value != interpn sample.', vtest)); +end +end + + +% ------------------------------------------------------------------------- +function test_vol_surf_vol_roundtrip(t) +% Smooth data should round-trip vol->surf->vol with high correlation on the +% cortical voxels touched by the projection. +vol = t.TestData.vol; +s = vol2surf(vol); +vback = surf2vol(s); + +dims = vol.volInfo.dim; +v0 = zeros(prod(dims), 1); v0(:) = double(vol.dat); +vb = zeros(prod(dims), 1); vb(vback.volInfo.wh_inmask) = double(vback.dat); +hit = vback.volInfo.image_indx; + +verifyGreaterThan(t, nnz(hit), 20000, 'Expected many cortical voxels hit.'); +cc = corr(v0(hit), vb(hit)); +verifyGreaterThan(t, cc, 0.99, sprintf('vol->surf->vol correlation too low (r=%.4f).', cc)); +verifyEqual(t, class(vback), 'fmri_data'); +verifyEqual(t, vback.volInfo.dim, dims); +end + + +% ------------------------------------------------------------------------- +function test_surf2vol_requires_fsaverage(t) +% A non-fsaverage object should be rejected by surf2vol. +o = fmri_surface_data(local_synthetic_cifti()); % fsLR_32k synthetic +verifyError(t, @() surf2vol(o), 'surf2vol:space'); +end + + +% ------------------------------------------------------------------------- +function test_vol2surf_nearest_preserves_labels(t) +% 'nearest' interpolation must keep integer label values intact. +vol = t.TestData.vol; +vol.dat = single(mod(round(double(vol.dat)*7), 4)); % integer "labels" 0..3 +s = vol2surf(vol, 'interp', 'nearest'); +u = unique(s.dat(:)); +verifyTrue(t, all(ismember(u, [0 1 2 3])), 'Nearest interp introduced non-integer labels.'); +end + + +% ------------------------------------------------------------------------- +function test_mean_apply_mask_threshold(t) +s = vol2surf(t.TestData.vol); + +% mean across a 3-map version +s3 = s; s3.dat = [s.dat, s.dat*2, s.dat*3]; +s3.removed_images = false(3,1); +m = mean(s3); +verifyEqual(t, size(m.dat), [size(s.dat,1) 1]); +verifyEqual(t, double(m.dat), double(s.dat)*2, 'AbsTol', 1e-4, 'mean of [x 2x 3x] should be 2x.'); + +% apply_mask: keep first 1000 grayordinates +keep = false(size(s.dat,1),1); keep(1:1000) = true; +sm = apply_mask(s, keep); +verifyEqual(t, nnz(any(sm.dat~=0,2)), nnz(s.dat(1:1000)~=0), 'apply_mask zeroed wrong rows.'); +verifyEqual(t, size(sm.dat), size(s.dat), 'apply_mask must keep .dat full (D5b).'); + +% threshold (raw, two-tailed) +st = threshold(s, 0.5); +verifyTrue(t, all(abs(st.dat(st.dat~=0)) >= 0.5), 'threshold left sub-threshold values.'); +verifyEqual(t, size(st.dat), size(s.dat)); +end + + +% ========================================================================= +function vol = local_synthetic_volume() +% Smooth fmri_data on the standard MNI152 2 mm grid. +dims = [91 109 91]; +tmat = [-2 0 0 92; 0 2 0 -128; 0 0 2 -74; 0 0 0 1]; +[I, J, K] = ndgrid(1:dims(1), 1:dims(2), 1:dims(3)); +xyz = tmat * [I(:)'; J(:)'; K(:)'; ones(1, numel(I))]; +f = 0.01*xyz(1,:) + 0.005*xyz(2,:) + ... + exp(-((xyz(1,:).^2 + xyz(2,:).^2 + xyz(3,:).^2) / (2*40^2))); +iv = image_vector; +iv.volInfo = struct('mat', tmat, 'dim', dims, 'dt', [16 0], ... + 'xyzlist', [I(:) J(:) K(:)], 'nvox', prod(dims), ... + 'image_indx', true(prod(dims),1), 'wh_inmask', (1:prod(dims))', ... + 'n_inmask', prod(dims), 'fname', ''); +iv.dat = single(f(:)); +iv.removed_voxels = false(prod(dims), 1); +iv.removed_images = false; +vol = fmri_data(iv); +end + + +% ------------------------------------------------------------------------- +function cii = local_synthetic_cifti() +mL = struct('struct','CORTEX_LEFT','type','surf','start',1,'count',5, ... + 'numvert',10,'vertlist',[0 2 4 6 8],'voxlist',[]); +mR = struct('struct','CORTEX_RIGHT','type','surf','start',6,'count',5, ... + 'numvert',10,'vertlist',[1 3 5 7 9],'voxlist',[]); +sform = [2 0 0 -20; 0 2 0 -22; 0 0 2 -24; 0 0 0 1]; +dense = struct('type','dense','length',10,'models',{{mL,mR}}, ... + 'vol',struct('dims',[10 10 10],'sform',sform)); +maps = struct('name',{'m1'},'table',{[]}); +mapsdim = struct('type','scalars','length',1,'maps',maps); +cii = struct('cdata', single((1:10)'), 'intent','dscalar', 'diminfo', {{dense, mapsdim}}); +end diff --git a/CanlabCore/Visualization_functions/addbrain.m b/CanlabCore/Visualization_functions/addbrain.m index 2f0ea5a1..41e86ddf 100644 --- a/CanlabCore/Visualization_functions/addbrain.m +++ b/CanlabCore/Visualization_functions/addbrain.m @@ -31,6 +31,25 @@ % 'transparent_surface' 'foursurfaces' 'foursurfaces_hcp' 'flat left' 'flat right' ... % 'bigbrain' {'hires surface left', 'bigbrain left'} % ['fsavg_left' or 'inflated left'], 'fsavg_right' or 'inflated right', uses freesurfer inflated brain with Thomas Yeo group's RF_ANTs mapping from MNI to Freesurfer. (https://doi.org/10.1002/hbm.24213) +% 'hcp inflated', 'hcp inflated left', 'hcp inflated right', uses the HCP fs_LR-32k inflated mesh. +% +% % Standard-mesh surfaces and fmri_surface_data +% % ----------------------------------------------------------------------- +% Some of the cortical surfaces above are EXACT standard meshes (same vertex +% count and vertex ordering as a template space), so per-vertex surface data can +% be mapped onto them DIRECTLY, with no resampling: +% 'hcp inflated' (and 'hcp inflated left'/'right') -> HCP fs_LR-32k template +% (32,492 vertices/hemisphere) +% 'inflated' / 'fsavg_left' / 'fsavg_right' -> FreeSurfer fsaverage template +% (163,842 vertices/hemisphere) +% These match the surface_space of an fmri_surface_data object ('fsLR_32k' and +% 'fsaverage_164k' respectively). The fmri_surface_data/surface and +% render_on_surface methods use exactly these meshes for native-space rendering, +% and will color any patch whose vertex count matches a hemisphere directly. By +% contrast, the MNI cortical surfaces ('left'/'right', 'hires ...') are NOT +% standard meshes; rendering surface data on them requires projecting through a +% volume (handled automatically by fmri_surface_data/render_on_surface). +% See docs/fmri_surface_data_methods.md. % % % Macro subcortical surfaces % % ----------------------------------------------------------------------- diff --git a/CanlabCore/docs/fmri_surface_data_design_plan.md b/CanlabCore/docs/fmri_surface_data_design_plan.md new file mode 100644 index 00000000..4eeeabf8 --- /dev/null +++ b/CanlabCore/docs/fmri_surface_data_design_plan.md @@ -0,0 +1,668 @@ +# CANlab Surface / Grayordinate Data Object — Design & Implementation Plan + +## 1. Purpose and scope + +This document specifies the design and a phased implementation plan for a new CANlab +object, **`fmri_surface_data`**, that represents cortical-surface (vertex) and +grayordinate (CIFTI: surface vertices + subcortical voxels) brain data inside the +existing CANlab object model. The goal is a first-class sibling of `fmri_data` that +(a) losslessly holds and round-trips CIFTI `.dscalar/.dtseries/.dlabel.nii` and GIFTI +`.surf/.func/.shape/.label.gii` files, (b) reuses the entire geometry-agnostic CANlab +analysis stack (`predict`, `ica`, `mean`, `descriptives`, parcellation, +etc.) by keeping data in the canonical flat `[units × images]` `.dat` matrix, (c) renders +natively on bundled fs_LR / fsaverage meshes, and (d) maps to/from volumetric `fmri_data` +using warps **already vendored in the repo** — all with **no external MATLAB toolbox, +FreeSurfer, or Connectome Workbench binary required at runtime**. Scope for v1 is the +object, native I/O, native rendering, group-template volume↔surface mapping, and surface +parcellation; per-subject ribbon-constrained mapping and fsaverage↔fs_LR deformation are +explicitly deferred. + +This plan reconciles three independent architecture proposals. Where they diverged, the +conflicts and resolutions are stated explicitly in §3. + +--- + +## 2. Background: CIFTI grayordinates and GIFTI for CANlab developers + +**The grayordinate model.** HCP "grayordinates" represent the cortex as *surface +vertices* and the subcortex/cerebellum as *voxels*. The standard "91k" space has +**91,282 grayordinates** = 29,696 left-cortex vertices + 29,716 right-cortex vertices +(the non-medial-wall subset of 32,492 fs_LR-32k vertices per hemisphere) + 31,870 +subcortical 2 mm MNI voxels spread across ~19 `CIFTI_STRUCTURE_*` volume structures +(accumbens, amygdala, caudate, cerebellum, diencephalon, hippocampus, pallidum, putamen, +thalamus L/R, brainstem). The key insight for CANlab: **grayordinate data is already a +flat `[grayordinates × maps]` matrix** — exactly the layout CANlab's `.dat` expects. + +**CIFTI-2 file format.** A CIFTI-2 file is a single uncompressed `.nii` (NIfTI-2) +container: a 540-byte little-endian NIfTI-2 binary header, a 4-byte extension flag at +byte 540, one or more header extensions (the CIFTI XML lives in the extension whose +`ecode == 32`), then the raw data matrix beginning at `vox_offset`. NIfTI `dim[1..4]=1`; +the real CIFTI lengths live in `dim[5]` (values per row, fastest) and `dim[6]` (rows); +`dim[0]` is 6 or 7. **The data matrix is stored row-major**, so a MATLAB reader `fread`s +then permutes `[2 1]` to column-major. The XML (`...`) carries +one `MatrixIndicesMap` per dimension; a `BRAIN_MODELS` map holds `BrainModel` elements +each with `IndexOffset`, `IndexCount`, `ModelType` (`SURFACE`/`VOXELS`), `BrainStructure`, +`SurfaceNumberOfVertices`, and either `` (0-based vertices that carry data, +i.e. medial wall excluded) or `` + a `` 4×4 affine. +Intent codes / extensions: `.dscalar.nii` = 3006 `ConnDenseScalar`, `.dtseries.nii` = +3002 `ConnDenseSeries`, `.dlabel.nii` = 3007 `ConnDenseLabel` (integer keys indexing a +per-map `LabelTable`). **No external toolbox is required to read/write CIFTI-2** — only +`fopen/fread/fwrite` + XML parsing (`xmlread` or regexp). `wb_command` is needed only to +convert legacy CIFTI-1, which v1 does not support. + +**GIFTI (.gii) format.** GIFTI is a UTF-8 XML container. Root +`` holds N `` elements. A geometry +`.surf.gii` has two arrays: `NIFTI_INTENT_POINTSET` (N×3 FLOAT32 vertex mm coords) and +`NIFTI_INTENT_TRIANGLE` (M×3 INT32 0-based faces — add 1 for MATLAB `patch`). Per-vertex +data files use `NIFTI_INTENT_NONE/SHAPE/TIME_SERIES/LABEL`. Each `` payload is +`ASCII`, `Base64Binary`, or `GZipBase64Binary` (base64 of a **raw zlib** stream, header +`0x78 0x9C` — **not gzip**). A fully native MATLAB reader/writer was implemented and +**validated bit-exact** on the repo's S1200 32k surf.gii (32,492 verts / 64,980 faces; +verts/faces `maxdiff = 0` on round-trip). Three gotchas were solved and must be carried +over verbatim: (1) decompress with `java.util.zip.InflaterInputStream` (not +`GZIPInputStream`); (2) base64 with `matlab.net.base64decode/encode` (not apache-commons, +which corrupts bytes); (3) inflate entirely inside the JVM via `IOUtils.copy` (in-place +`Inflater.inflate(buf)` returns garbage to MATLAB because MATLAB passes a copy). + +**Why CANlab can do this with almost no new format code:** CANlab already ships fs_LR-32k +and fsaverage meshes as plain `.mat` files (faces + vertices), the MNI→fs_LR/fsavg +barycentric resample matrices, and the CBIG MNI↔fsaverage registration-fusion warps — so +the only genuinely new I/O is a self-contained CIFTI/GIFTI parser. + +--- + +## 3. Design decisions & rationale + +### D1. Class name — `fmri_surface_data` +All three proposals independently converged on `fmri_surface_data`. **Decision: +`fmri_surface_data`.** It is unambiguous, prefixed to avoid collisions, and signals +"surface/grayordinate analogue of `fmri_data`." *(Open question Q1: confirm the +`canlab_` prefix vs. a bare `surface_data`/`fmri_surface_data`; the existing siblings +`fmri_data`/`image_vector`/`atlas`/`region` are unprefixed, so a bare name like +`fmri_surface_data` would match house style. Recommend confirming with the user.)* + +### D2. Superclass — subclass `image_vector`, **not** `fmri_data`, **not** from scratch +All three proposals agree: **`classdef fmri_surface_data < image_vector`.** + +- Subclassing `image_vector` inherits the load-bearing, purely-`.dat`/`removed_*` + methods verbatim by dispatch: `remove_empty`, `replace_empty`, `get_wh_image`, + `descriptives`, `ica`, `mahal`, `pca`, `image_math`, `history`, `isempty`, + `enforce_variable_types`. +- **Not** `fmri_data`: `fmri_data` hard-codes a mandatory `volInfo` and an + `fmri_mask_image`-typed `mask` in `run_checks_and_fixes` (errors if `volInfo` empty), + and its `cat/horzcat/plot/predict/ttest/regress` assume `interp3`/orthviews resampling. + Inheriting those would fight the surface design at every turn. +- **Not** from scratch: that would forfeit dozens of inherited methods and the + constructor polymorphism contract. + +Trade-off accepted: `cat`, `horzcat`, `plot`, `predict`, `ttest`, `regress` live **only** +in `@fmri_data`, so an `image_vector` subclass does **not** inherit them. We provide our +own (see §5), copying the geometry-agnostic cores and stripping the volume rebuild/resample +steps. This is the deliberate price of not inheriting `fmri_data`'s volume baggage. + +### D3. How `volInfo` is replaced — a `brain_model` descriptor + repurposed `volInfo` slot + +This is where the proposals **conflicted** and is resolved explicitly: + +- **Proposal A** repurposes the inherited `volInfo` *property slot* to hold the whole + surface+volume `brain_model` descriptor (keeping the literal name `volInfo`). +- **Proposal B/C** add a **new** `brain_model`/`cifti`/`geom` property as the single + source of truth, and (B) additionally keep `volInfo` populated to describe **only the + volumetric sub-block** so the subcortex still round-trips through existing voxel code. + +**Resolution (graft the best of both):** +1. The surface/grayordinate geometry truth lives in a **new property `brain_model`** (a + CIFTI-`BrainModel`-mirroring struct; the single source of truth for round-trip and for + the flat-row↔space map). This is cleaner and less leak-prone than overloading `volInfo` + semantically (Proposal A's chief risk was inherited methods dereferencing + `volInfo.mat`). +2. The inherited **`volInfo` slot is kept populated, but describes ONLY the volumetric + sub-block** of the grayordinates (its `.mat`/`.dim`/`.xyzlist`/`.wh_inmask` come from + the subcortical `VoxelIndicesIJK` + affine). This is Proposal B's key idea: it means + the subcortex is a *valid voxel space*, inherited methods that read `volInfo` find a + real (if partial) struct, and `to_fmri_data` on the subcortex is almost free. For + **surface-only** objects (`.func.gii`, cortex-only CIFTI), `volInfo` is empty and the + relaxed run-checks tolerate it. +3. Mesh geometry (faces/vertices per hemisphere, medial wall) lives in a **new property + `geom`**, loaded lazily from bundled `.mat` assets (this is what render/area/contiguity + patch and compute on). + +`brain_model` fulfills `volInfo`'s three load-bearing roles: **(a)** row↔space map = +per-structure index lists (`vertlist` 0-based per hemisphere; `voxlist` IJK per +subcortical structure) — the `wh_inmask`/`xyzlist` analogue; **(b)** reconstruct target = +scatter rows into dense per-hemisphere vertex arrays (`reconstruct_image` override) + +subcortical volume; **(c)** contiguity source = mesh-graph connected components (cortex) / +3-D connectivity (subcortex), replacing `volInfo.cluster`. + +### D4. Re-declare `fmri_data` per-image annotation properties **by name** +Because we subclass `image_vector` (where these don't exist), we re-declare `X`, `Y`, +`Y_names`, `covariates`, `covariate_names`, `images_per_session`, `metadata_table`, +`image_metadata`, `additional_info` with the **identical `fmri_data` names** so analysis +methods bind by name. These are per-*image* (column) annotations, independent of +voxel-vs-vertex. + +### D5. Preserve method contracts exactly +`compare_space` must keep its **4-valued return code** (`0` same / `1` diff / `2` missing / +`3` same-space-diff-grayordinates), not a boolean — callers (`cat`, `apply_mask`) branch +on it. A single overridable **`rebuild_like(obj, newdat)`** helper replaces the hard-coded +`image_vector('dat',…,'volInfo',…)`+`fmri_data` re-wrap in `mean`/`ica`/etc. so they emit +`fmri_surface_data` carrying `brain_model`+`geom`. + +### D5b. NO empty-squeezing — `.dat` is always the full grayordinate set (user directive, 2026-06-25) +Unlike `fmri_data` (which stores sparse 3-D volumes and must drop out-of-brain / empty +voxels to save space), CIFTI grayordinate data is **already compact**: the medial wall is +excluded by construction (it is simply absent from each surface model's `vertlist`), and +there are no out-of-brain voxels. So this object does **not** reproduce the +`remove_empty`/`replace_empty` space-squeezing contract: + +- `.dat` is **always** `[nGrayordinates × nMaps]`, in exact 1:1 row correspondence with + `brain_model`. It is never shrunk to drop zero/NaN rows. There is therefore no "full vs + reduced" duality and no `replace_empty`-before-reconstruct dance. +- `remove_empty` and `replace_empty` are **overridden as identity no-ops** (return the + object unchanged) so any inherited method that calls them internally still composes, and + a user who calls them by habit gets a valid object back. (Zeroing a grayordinate's value + is allowed; it just never removes the row.) +- `removed_voxels` is kept (inherited) but is **vestigial**: always `false(nGray,1)`. It + exists only so inherited methods that read it find a valid all-false vector. `removed_images` + likewise stays `false(nImg,1)`; *selecting* a subset of maps is `get_wh_image`'s job, not a + space-saving squeeze. +- **Masking is intrinsic, not space-squeezing.** Because all objects share a standardized + grayordinate space (fs_LR-32k / 91k), a "mask" is just a same-length logical over + grayordinates (or another `fmri_surface_data` on the same space). `apply_mask` therefore + zeros/selects matching rows directly — no `fmri_mask_image`, no resample, no + empty-removal. This is a major simplification over `@fmri_data/apply_mask`. + +This eliminates the plan's largest risk class (the "forgot to `replace_empty`" family) and +several override headaches; `reconstruct_image`/`write`/`cat` no longer need a +`replace_empty` pre-step. + +### D6. Statistic / atlas variants deferred to a later phase +`ttest`/`regress` results and `.dlabel` parcellations want parallel per-grayordinate +fields (`.p/.ste/.sig`; integer label keys + RGBA). For v1, `ttest`/`regress` return a +`fmri_surface_data` carrying those as extra fields; dedicated subclasses +(`fmri_surface_statistic_image`, `fmri_surface_atlas`) that keep those parallel fields in +sync under `get_wh_image` (column subsetting) are a later phase. *(No `remove_empty`/ +`replace_empty` sync needed — they are no-ops, D5b.)* + +### Open questions — RESOLVED (user, 2026-06-25) +- **Q1. Class name → `fmri_surface_data`** (unprefixed, matching house style of + `fmri_data`/`atlas`/`region`). Statistic/atlas subclasses → `fmri_surface_statistic_image` + / `fmri_surface_atlas`. +- **Q2. Canonical default space → fs_LR-32k (91k)** for native CIFTI; fsaverage-164k for the + CBIG mapper path. Confirmed. +- **Q3. Mesh assets → ship the in-repo HCP S1200 meshes for v1, add CC0 TemplateFlow + `tpl-fsLR`/`tpl-fsaverage` as the license-clean default in a later milestone (M8).** +- **Q4. CIFTI I/O → hand-roll natively** (`canlab_read_cifti`/`canlab_write_cifti`), modeled + on the BSD-2 cifti-matlab core, so CanlabCore stays 100% permissive and self-contained. + The GIFTI codec is already hand-rolled and validated bit-exact. + +--- + +## 4. Object model + +`.dat` is `single [n_grayordinates × n_maps]`; rows in fixed CIFTI order: left-cortex +non-medial vertices, then right-cortex non-medial vertices, then subcortical voxels. + +| Property | Type | Description | +|---|---|---| +| `dat` | `single [nGray × nImg]` | **Inherited, unchanged.** The CIFTI cdata matrix (cortex L, cortex R, subcortical voxels). All generic `.dat` math operates here. For `.dlabel` holds integer label keys. | +| `brain_model` | struct (**new**, the `volInfo` replacement) | **Single source of truth** for surface/grayordinate geometry, mirroring CIFTI `BrainModel` 1:1. Fields: `.models` (struct array per BrainModel: `.struct_name` e.g. `CIFTI_STRUCTURE_CORTEX_LEFT`, `.type` `'surf'|'vox'`, `.index_offset`, `.index_count`, `.vertlist` 0-based vertex indices, `.surf_nverts`, `.voxlist` 3×N IJK, `.mat` 4×4 affine for vox models); `.grayordinate_type` e.g. `'91k'`; `.cluster` (computed connected-component labels). Replaces `wh_inmask/xyzlist` with `vertlist/voxlist`; per-vox-model affines replace one global `mat`. | +| `geom` | struct (**new**, mesh cache) | Per-hemisphere meshes (loaded lazily from bundled `.mat`): `.faces_lh/.faces_rh` [M×3 1-based], `.vertices_lh/.vertices_rh` [N×3 mm] for one or more surface types (`midthickness/inflated/pial/white/sphere`, faces shared across types), `.space_name` (`fsLR_32k`/`fsavg_164k`), `.medialwall_lh/.medialwall_rh` logical masks. What render/area/contiguity use. | +| `volInfo` | struct (**inherited, repurposed for the volume sub-block only**) | Populated to describe ONLY the subcortical/cerebellar voxel models (`.mat`=their affine, `.dim`, `.xyzlist/.wh_inmask` from `voxlist`) so the subcortex is a valid voxel space and `to_fmri_data` is near-free. **Empty for surface-only objects** (run-checks relaxed). Surface part is described by `brain_model`+`geom`, never `volInfo`. | +| `removed_voxels` | logical [nGray × 1] | **Inherited but vestigial (see D5b).** Always `false` — grayordinate data is already compact, so rows are never squeezed. Kept only so inherited methods that read it find a valid all-false vector. | +| `removed_images` | logical [nImg × 1] | **Inherited but vestigial (see D5b).** Always `false`. Map subsetting is `get_wh_image`'s job, not a space-saving squeeze. | +| `history` / `source_notes` / `image_names` / `fullpath` / `files_exist` / `dat_descrip` | cell / char / logical | **Inherited verbatim.** Provenance + file metadata. `fullpath` reinterpreted by `write()` as `.dscalar/.dtseries/.dlabel.nii` or `.gii` path; extension drives intent. | +| `X` | double [nImg × p] | **New (fmri_data name).** Design/predictor matrix, per-map, so `regress`/`predict` bind. | +| `Y` / `Y_names` | double [nImg × q] / cell | **New (fmri_data names).** Outcomes + names, per-map. | +| `covariates` / `covariate_names` | double / cell | **New (fmri_data names).** Per-map nuisance covariates. | +| `images_per_session` | double vector | **New (fmri_data name).** Run lengths for `.dtseries` data. | +| `metadata_table` | table (nImg rows) | **New (fmri_data name).** Per-map metadata; `write()` exports a companion CSV like `fmri_data.write`. | +| `image_metadata` / `additional_info` | struct | **New (fmri_data names).** Acquisition flags + free-form info; preserves the `fmri_data` API surface. | +| `mask` | logical [nGray × 1] or `fmri_surface_data` | **New, lightweight (see D5b).** Optional same-length logical over grayordinates (or another object on the same space). No `fmri_mask_image`, no resampling, no empty-squeezing — masking just zeros/selects rows. Medial wall is already excluded by `brain_model`, so a `mask` is only for further sub-selection (e.g. cortex-only, an ROI). | +| `intent` | char | **New.** CIFTI/GIFTI intent (`dscalar`/`dtseries`/`dlabel`/`func`/`shape`/`label`) so `write()` round-trips the correct `intent_code` + extension. | +| `series_info` | struct | **New.** For `.dtseries`: `SeriesStart/Step/Unit/Exponent` from the CIFTI SERIES map. | +| `label_table` | struct/table | **New (only `intent=='dlabel'/label`).** Per-key Name + RGBA, mirroring atlas label tables; lets a surface `.dlabel` convert cleanly to/from a surface atlas. | +| `surface_space` | char | **New.** Canonical space tag (`'fsLR_32k_91k'` default, `'fsLR_32k'`, `'fsaverage_164k'`). Gatekeeps `compare_space`; drives which meshes/warps load. | + +**How `brain_model` plays `volInfo`'s role across surface + volume grayordinates:** for +**cortical** grayordinates there is no affine — the row↔space map is +`models(i).vertlist` into `models(i).surf_nverts` (per hemisphere), reconstruction +scatters rows into dense vertex arrays (medial wall → NaN), and contiguity is connected +components on the `geom.faces` edge graph. For **subcortical** grayordinates the +`models(i).voxlist` + `models(i).mat` *is* a normal voxel space — these are mirrored into +the inherited `volInfo` slot so the subcortex reconstructs into a 3-D volume and exports +to `fmri_data` via existing voxel code. + +--- + +## 5. Method surface + +Classification: **Inherited** (works verbatim by dispatch), **Override** (same name, +surface-specific body), **New** (not present in `@image_vector`; mirrors `@fmri_data`). +"Mirrors fmri_data" = same name/signature/role as the `fmri_data` method. + +| Method | Class | Behavior (one line) | +|---|---|---| +| `fmri_surface_data` (constructor) | New | Mirrors `image_vector`/`fmri_data` polymorphism: empty / struct→fieldname-copy / `'key',value` / filename-autoload (`.dscalar/.dtseries/.dlabel.nii`/`.gii` extension test replaces `'.nii'`+exist) / `isa(image_vector)` recast (copy matching fields, cast `.dat` to single). Builds `brain_model`+`volInfo`(vol sub-block); lazy `geom`. Relaxes mandatory-`volInfo` check. | +| `remove_empty` | Override → **no-op** (D5b) | Returns the object unchanged. Grayordinate data is already compact; rows are never squeezed. Defined so inherited callers compose and habitual user calls are safe. | +| `replace_empty` | Override → **no-op** (D5b) | Returns the object unchanged (`.dat` is always full-size already). Removes the "forgot to replace_empty" bug class. | +| `get_wh_image` | Inherited | Map (column) selection; subsets `.dat`-sized + per-image fields (X/Y/metadata_table). | +| `descriptives` | Inherited | Numeric `.dat` summaries; only the "n voxels" label reads cosmetically as grayordinates. | +| `ica` / `mahal` / `pca` | Inherited | Pure `.dat` decomposition/stats; geometry-agnostic. Re-wrap (if any) via `rebuild_like`. | +| `image_math` / `history` / `isempty` / `enforce_variable_types` | Inherited | Pass `.dat`/metadata around; bind unchanged. | +| `mean` | Override (math reused) | Reuse `.dat` averaging + `group_by`; replace the hard-coded `image_vector(...,'volInfo',...)`+`fmri_data` re-wrap with `rebuild_like` → `fmri_surface_data` carrying `brain_model`+`geom`. | +| `threshold` | Override (raw branch reused) | Raw value thresholding on `.dat` delegates to inherited logic; override only the cluster-extent `k` / `trim_mask` paths to mesh connected components (cortex) + 3-D (subcortex), medial-wall aware. | +| `apply_mask` | Override (simplified, D5b) | Same-space logical/object mask → zero or select matching `.dat` rows directly. No `fmri_mask_image`, no resample, no empty-removal. Pattern-expression / dot-product math on `.dat` reused verbatim where applicable. | +| `reconstruct_image` | Override | Scatter `.dat` rows directly (no `replace_empty` pre-step; `.dat` is always full) into dense per-hemisphere vertex arrays via `brain_model.models.vertlist` (medial wall→NaN) and subcortical models into a 3-D volume via their `.mat`. Returns `{lh_vertdata, rh_vertdata, subvol}`. | +| `resample_space` | Override | `interp3`-in-mm is meaningless on meshes. Surface: barycentric mesh↔mesh via bundled `resample_from_*_to_*.mat` structs; subcortex: existing voxel resample. Same signature. | +| `compare_space` | Override | Compare `surface_space` + per-model `struct_name`/`index_count`/`surf_nverts` + vox-model `dim`/`mat`. **Preserves the 0/1/2/3 return contract.** | +| `reparse_contiguous` | Override | Connected components on the mesh edge graph (cortex, via `External/matlab_bgl` or `graph/conncomp`) + 6/18/26 voxel connectivity (subcortex). Writes `brain_model.cluster`. | +| `write` | Override | Native `canlab_write_cifti`/`canlab_write_gifti` (M1), dispatched on `intent`/extension; keeps `fullpath/fname/overwrite` convention; exports `metadata_table` CSV. No `replace_empty` pre-step needed (`.dat` already full). | +| `apply_parcellation` | Override (core reused) | `condf2indic`→column-normalize→`parcel_means = dat.dat' * parcels.dat` reused verbatim; override only space-matching (shared grayordinate index / mesh resample) and `get_region_volumes`→per-parcel surface **area** (face geometry) for `rmsv`; exclude medial wall from normalization. | +| `region` / `surface_region` | Override / New | Per-region vertex-index lists + per-vertex `.val/.Z` + centroid (mean vertex coord) via mesh components; subcortical models route through the existing volumetric `region` path. Mirrors `cifti_struct_2_region_obj` field conventions for the vox part (which currently drops `'surf'` — the gap to fill). | +| `surface` / `render_on_surface` | Override | Object already holds per-vertex values → skip `render_on_surface`'s `interp3`; feed `.dat` columns straight into `FaceVertexCData` + the split gray/color colormap builders. Reuse `surface_outlines.m`; reuse `make_surface_figure.m` body (swap `gifti()`→`load()`). Register into `@fmridisplay/surface.m` + `addbrain`. | +| `montage` / `orthviews` / `slices` | Override | Route cortex through the surface renderer; subcortex through the existing slice montage; warn where a 2-D slice view is meaningless for cortex. | +| `cat` | New (mirrors fmri_data) | Establish common grayordinate space via surface `compare_space` (resample if needed), hcat `.dat` + per-map fields (X/Y/covariates/metadata_table/series_info). No `replace_empty` pre-step (`.dat` always full). Reuses `@fmri_data/cat.m` logic minus volume resample. | +| `horzcat` | New (mirrors fmri_data) | Thin wrapper over `cat`. | +| `predict` | New (mirrors fmri_data) | Copy `@fmri_data/predict.m` algorithm core (operates on `.dat`+X/Y, geometry-agnostic); skip volume weight-map rebuild, wrap weights via `rebuild_like`. | +| `ttest` / `regress` | New (mirrors fmri_data) | Per-row stats on `.dat` columns (geometry-agnostic core); return a `fmri_surface_data` carrying parallel `.p/.ste/.sig` per-grayordinate fields (statistic subclass deferred). | +| `plot` | New (mirrors fmri_data) | Surface QC: reuse the geometry-agnostic panels (covariance, global signal, per-map histograms); replace the volume montage/orthviews panel with a 4-view surface render of the mean map. | +| `to_fmri_data` | New | Export the subcortical/volumetric models to a `fmri_data` (+ writeable `.nii`) via the repurposed `volInfo` sub-block, reusing `extract_vol_from_cifti.m`. Surface models dropped here (use `surf2vol`). | +| `vol2surf` (method on `fmri_data`/`image_vector`) | New | Volume → surface via CBIG RF (returns `fmri_surface_data`). See §7. | +| `surf2vol` | New | Surface → volume via CBIG RF (returns `fmri_data`; `.nii` via `fmri_data.write`). See §7. | +| `rebuild_like` (helper) | New | Overridable rebuild of a `fmri_surface_data` from a new `.dat`, carrying `brain_model`+`geom`. Used by `mean`/`ica`/`predict`/etc. to avoid the volume re-wrap. | + +--- + +## 6. Import / Export — native, no external toolbox + +**Hard requirement:** no `gifti`, FieldTrip `ft_read_cifti`, `wb_command`, or +cifti-matlab `@xmltree` (LGPL) / `ft_cifti` (GPL) at runtime. Verify at each milestone via +`mcp__matlab__detect_matlab_toolboxes` on a clean path. Recommend hand-rolling the +reader/writer (modeled on the BSD-2 cifti-matlab core) so CanlabCore stays 100% +permissive and self-contained. + +### CIFTI-2 reader (`read_cifti_native`) +1. `fopen(fname,'r','l')`; `fread` `sizeof_hdr` int32 @0 (=540; else byte-swap). +2. Read the 540-byte NIfTI-2 header at exact offsets: `dim` int64@16, `datatype` + int16@12, `vox_offset` int64@168, `scl_slope`@176/`scl_inter`@184, `intent_code` + int32@504, `intent_name` char16@508. +3. `fseek(540)`; read int32 extension flag; loop esize/ecode blocks until `vox_offset`; + keep the `ecode==32` bytes as the XML char array. +4. Parse XML with a **regexp pull-parser** (preferred — avoids even the `xmlread`/JRE + dependency and namespace/entity edge cases) or `xmlread`. Walk + `CIFTI>Matrix>MatrixIndicesMap`; for `BRAIN_MODELS` read each `BrainModel`'s + `IndexOffset/Count/ModelType/BrainStructure/SurfaceNumberOfVertices` and `sscanf` + `VertexIndices`/`VoxelIndicesIJK`; read `Volume`+16-float affine (`reshape(...,4,4)'`, + row-major). For SCALARS/LABELS iterate `NamedMap>MapName` (+`LabelTable>Label`); SERIES + `Start/Step/Exponent/Unit`. +5. `fseek(vox_offset)`; `fread([rowLen nRows], precision)`; permute `[2 1]` to + column-major; apply `scl_slope/inter` when nonzero. Populate `.dat`, `brain_model`, + `volInfo`(vol sub-block via the split logic of `get_cifti_data.m`), `intent`, + `series_info`, `label_table`, `image_names`. + +### CIFTI-2 writer (`write_cifti_native`) +Build XML (`sprintf`, escape `&<>`), pad to /16, `esize=len+8` rounded, `ecode=32`; +`vox_offset=round-up(540+4+esize)`; write 540-byte header (magic `n+2` + `0D 0A 1A 0A`, +`dim[0]=6/7`, `dim[1..4]=1`, `dim[5]=rowLen`, `dim[6]=nCols`, datatype/bitpix per class, +`intent_code`+`intent_name` per the 3006/3002/3007 table), ext flag=1, esize/ecode/XML, +pad, then data transposed back to row-major. Target: byte/value-level round-trip +verifiable by `cifti_diff`-style comparison. + +### GIFTI reader/writer (`read_gifti_native` / `write_gifti_native`) +**Validated bit-exact** (reference at `/tmp/test_gii_native.m`). Read: `fileread`, +`regexp(txt,'(?s)','match')` (the `(?s)` dotall flag is +required; avoid `\b`), pull attrs + `` payload, `matlab.net.base64decode`, +GZipBase64 inflate via `java.io.ByteArrayInputStream` → +`java.util.zip.InflaterInputStream` → `org.apache.commons.io.IOUtils.copy` (IOUtils ships +with MATLAB; **not** `GZIPInputStream`), `typecast`, `swapbytes` if endian mismatch, +RowMajor `reshape(vals,[Dim1 Dim0])'`. `POINTSET`→vertices, `TRIANGLE`→faces(+1), +`LABEL`→keys+LabelTable, `NONE/SHAPE/TIME_SERIES`→per-vertex data. Write: RowMajor byte +flatten of the transpose, zlib-compress JVM-side via `DeflaterOutputStream`, +`matlab.net.base64encode`, emit GIFTI XML. A no-Java path (ASCII/Base64Binary) is fully +native for writing; reading external GZip files still needs the JVM `Inflater`. + +### Vendorable, permissively-licensed code +| Tool | License | Use | +|---|---|---| +| **cifti-matlab** core (`cifti_read`, `cifti_write`, `cifti_parse_xml`, `read_nifti2_hdr`) | **BSD-2-Clause** (take `read_nifti2_hdr` under its BSD option) | Model the hand-rolled CIFTI reader/writer on it. **Avoid** the `@xmltree` (LGPL) and `ft_cifti` (GPL) subdirs. | +| **gllmflndn/gifti** | **MIT** | Optional *fallback* only; ships per-platform MEX with no pure-MATLAB fallback, so prefer the validated native codec. | +| **CBIG RF warps** (already in repo) | **MIT** | vol↔surf mapping assets (§7). Keep `CBIG_LICENSE` alongside. | + +`get_cifti_data.m` (diminfo model walk → cortex L/R + volumes) and +`extract_vol_from_cifti.m` (volumetric grayordinates → `fmri_data`) from +`canlabSurface/cifti_utils/` are ported in for the split and the `to_fmri_data` bridge. + +--- + +## 7. Volume ↔ Surface mapping (native v1) + +**Zero external binaries.** Reuse the **CBIG Registration-Fusion (RF-ANTs) warps already +vendored** at `CanlabCore/Cifti_plotting/CBIG_registration_fusion_surf2vol_vol2surf/` +(MIT; the `.mat` tables verified on disk). Replace FreeSurfer `MRIread/MRIwrite` with SPM12 +`spm_vol/spm_read_vols/spm_write_vol` (already a hard CanlabCore dependency) and the MARS +kd-tree with the precomputed vertex-index grids. v1 standardizes the RF path on +**fsaverage** and the bundled barycentric path on **fs_LR-32k / fsavg-164k**. + +### `vol2surf` (method on `fmri_data`/`image_vector` → `fmri_surface_data`, fsaverage) +Port `CBIG_RF_projectMNI2fsaverage.m`: (1) get the object's 3-D volume via +`reconstruct_image` and its 4×4 `mat` from `volInfo.mat`; (2) load +`lh/rh.avgMapping_allSub_RF_ANTs_MNI152_orig_to_fsaverage.mat` → `ras` (3×163842 MNI-RAS +per fsaverage7 vertex); (3) `vox = inv(mat(1:3,1:3))*(ras - mat(1:3,4))`, +`matcoord=[vox(2,:)+1; vox(1,:)+1; vox(3,:)+1]`; (4) per map column +`vertex_vals = interpn(vol3d, matcoord..., interp)` — `'linear'` for continuous, +`'nearest'` for label maps; OOB→0. Result populates `.dat` with an `fsavg_164k` +`brain_model`. + +### `surf2vol` (method on `fmri_surface_data` → `fmri_data` + `.nii`) +Port `CBIG_RF_projectfsaverage2Vol_single.m`, **fast nearest gather path** (no kd-tree): +(1) load `..._avgMapping.vertex.mat` → `lh_vertex/rh_vertex` (256³ nearest-vertex-index +grids) and the liberal cortex mask NIfTI (defines grid + vox2ras); (2) vectorized gather +`out = zeros(numel(mask)); idx = lh_vertex>0; out(idx) = lh_vals(lh_vertex(idx));` (same +rh) — no loop/search; (3) reshape to the mask volume, wrap as `fmri_data` with the mask's +vox2ras (lands in MNI152). **The subcortical grayordinates need no mapping** — they are +already voxels+affine in the `volInfo` sub-block, so reconstruct them directly and add to +the cortical projection for a full-brain `fmri_data`. `fmri_data.write()` emits the +`.nii`. Optional linear path uses `prop.mat` + one `knnsearch` (Statistics Toolbox). + +### Second native path (preferred when data is already fs_LR grayordinate) +`fsLR_32k ↔ MNI` via the repo's `resample_from__to_fsLR_32k(_nearestneighbor).mat` +barycentric structs through `render_on_surface`'s existing projection code — targets +fs_LR directly, no fsaverage↔fs_LR deformation needed. + +### Deferred (documented limitation) +RF is a **fixed group-template MNI152↔fsaverage correspondence** — correct for group MNI +maps, **not** a per-subject native-space ribbon mapper. Best-in-class +ribbon-constrained per-subject mapping (needs subject white+pial) and fsaverage↔fs_LR HCP +deformation are deferred. + +--- + +## 8. Reuse map + +All paths under `/Users/f003vz1/Documents/GitHub/`. + +| Existing file / method | How reused | +|---|---| +| `CanlabCore/@image_vector/{get_wh_image,descriptives,ica,mahal,pca,image_math,history,isempty,enforce_variable_types}.m` | **Inherit verbatim** by dispatch (touch only `.dat`). | +| `CanlabCore/@image_vector/{remove_empty,replace_empty}.m` | **Override as no-ops** (D5b) — not inherited; grayordinate `.dat` is never squeezed. | +| `CanlabCore/@image_vector/mean.m` (rebuild ~L141-143), `apply_mask.m` (pattern math), `threshold.m` (raw branch), `apply_parcellation.m` (aggregation core) | **Copy body, swap rebuild/space step** via `rebuild_like` / surface `resample_space`. | +| `CanlabCore/@image_vector/image_vector.m` (L252-279 property block; L358 `.nii`+exist branch); `CanlabCore/@fmri_data/fmri_data.m` (L313-351 analysis fields; L441-471 recast-from-sibling; L713 mandatory-volInfo) | **Mirror constructor polymorphism**, swap reader + extension test, relax volInfo check. | +| `CanlabCore/@fmri_data/{cat,horzcat,predict,ttest,regress,plot}.m` | **New by copying body** (geometry-agnostic cores; strip volume rebuild/resample) — NOT inherited by an `image_vector` subclass. | +| `canlabSurface/cifti_utils/get_cifti_data.m` | **Port** as the CIFTI→grayordinate splitter (cortex L/R + volumes). | +| `canlabSurface/cifti_utils/extract_vol_from_cifti.m` | **Port near-verbatim** for `to_fmri_data` (builds volInfo/voxlist/affine, wraps `fmri_data`). | +| `CanlabCore/Cifti_plotting/CBIG_registration_fusion_surf2vol_vol2surf/CBIG_RF_projectMNI2fsaverage.m`, `CBIG_RF_projectfsaverage2Vol_single.m` + `.mat` warps | **Port** (drop FreeSurfer→`spm_vol`, kd-tree→`vertex.mat`) for `vol2surf`/`surf2vol`. | +| `CanlabCore/@image_vector/render_on_surface.m` | **Reuse colormap/split-colormap/indexmap subfunctions**; feed per-vertex `.dat` straight to `FaceVertexCData` (skip `interp3`). | +| `CanlabCore/Cifti_plotting/surface_outlines.m` | **Reuse verbatim** (pure MATLAB). | +| `CanlabCore/Cifti_plotting/make_surface_figure.m` | **Reuse body** (swap `gifti()`→`load()`; fix hardcoded `/Users/sgeuter` addpath). | +| `CanlabCore/@fmridisplay/surface.m`, `CanlabCore/Visualization_functions/addbrain.m` | **Register** the object so `o2.surface{}` workflows keep working (`'hcp inflated'`, `'foursurfaces_hcp'`). | +| `CanlabCore/Cifti_plotting/cifti_struct_2_region_obj.m` | **Mirror field conventions** (XYZ/XYZmm/M/Z/voxlist+1) for the subcortical region path; it explicitly drops `'surf'` — the gap to fill. | +| `CanlabCore/@atlas/{num_regions,get_region_volumes,atlas2region}.m`, `condf2indic` | **Reuse / mirror** for surface atlas + parcellation (area replaces voxel volume). | +| `External/matlab_bgl` (or `graph/conncomp`) | **Reuse** for mesh connected components in `reparse_contiguous`/`region`. | +| `Neuroimaging_Pattern_Masks/.../2016_Glasser_*`, `2018_Schaefer_Yeo_*`, `2024_CANLab_atlas/` (`.label.gii`, `.dlabel.nii`) | **Test/atlas content** for surface parcellation import. | + +**Known bugs to fix on reuse:** `make_surface_figure.m` hardcoded `/Users/sgeuter` addpath; +`render_cifti_on_brain.m` L188 extracts `CORTEX_LEFT` for the right hemisphere. + +--- + +## 9. Standard mesh assets + +CANlab already ships fs_LR-32k and fsaverage meshes as `.mat` (faces + vertices, loadable +straight into `patch()` with no `gifti` dependency) under +`CanlabCore/canlab_canonical_brains/Canonical_brains_surfaces/`. + +| Asset | Vertex count / hemi | Source / license | Use | +|---|---|---|---| +| `S1200.{L,R}.midthickness_MSMAll.32k_fs_LR.surf.gii`, `S1200.{L,R}.sphere.32k_fs_LR.mat`, inflated `.mat` | 32,492 (29,696 L / 29,716 R non-medial) | **HCP S1200** Open-Access Data Use Terms (redistributable, not relicensable) — **already in repo** | Default fs_LR-32k display/geometry. | +| `fsavg_{inflated,white,sphere}_{lh,rh}.mat` | 163,842 | Already in repo | fsaverage-164k display + CBIG RF target. | +| `resample_from_{MNI152NLin2009cAsym,MNI152NLin6Asym,colin27}_to_{fsLR_32k,fsavg_164k}[_nearestneighbor].mat` | weights/vertices [Ntarget × 9] | Already in repo | MNI↔surface barycentric resampling (vol2surf path B, mesh↔mesh). | +| CBIG RF warps + `FSL_MNI152_FS4.5.0_cortex_estimate` mask | fsaverage7 = 163,842 | **MIT** (CBIG) — already in repo | `vol2surf`/`surf2vol` (§7). | +| **TemplateFlow `tpl-fsLR`** (32k/59k/164k midthickness/inflated/veryinflated/sphere + nomedialwall) | 32,492 / 59,292 / 163,842 | **CC0-1.0** | *Recommended add* for a clean-license default (pending Q3). | +| **TemplateFlow `tpl-fsaverage`** (white/pial/sphere + curv/sulc, 10k/41k/164k) | 2,562 / 40,962 / 163,842 | **CC0-1.0** | Optional fsaverage support with permissive license. | + +Hemisphere convention (from HCP/CANlab code): **left=1, right=2**; surface handle `.Tag` +must contain `'left'`/`'right'` for projection; medial-wall mask value 1 = valid cortex. + +--- + +## 10. Implementation phasing + +Each milestone has a verification step using sample data + a unit test in +`CanlabCore/Unit_tests/`. Run `mcp__matlab__detect_matlab_toolboxes` on a clean path at +M1, M2 and M5 to prove zero external-toolbox runtime. + +**M1 — Native I/O foundation (START HERE; highest value, no class yet). ✅ DONE 2026-06-25.** +Built (in `CanlabCore/Surface_tools/`): `canlab_read_cifti` / `canlab_write_cifti` (NIfTI-2 ++ ecode-32 XML, hand-rolled regexp pull-parser; supports dscalar/dtseries/dlabel, surface + +voxel BrainModels, subcortical affine, scalar/series/label maps; writer does faithful +re-emit *and* full XML regeneration), `canlab_read_gifti` / `canlab_write_gifti` (.surf/ +.func/.shape/.label; ASCII/Base64/GZipBase64), and shared `canlab_zlib_inflate` / +`canlab_zlib_deflate` JVM helpers. *Deliverable met:* self-contained, **proven to round-trip +exactly with cifti_read AND gifti removed from the path** (cifti cdata maxdiff=0, gifti +verts/faces maxdiff=0). *Verified:* `Unit_tests/surface_data/canlab_test_surface_io.m` +(5/5 pass) — shipped S1200 surf.gii (32,492 verts / 64,980 faces, bit-exact across all 3 +encodings), GIFTI + CIFTI label tables, synthetic dscalar/dlabel grayordinate round-trips +(cdata + BrainModel start/count/vertlist/voxlist + subcortical affine preserved), and an +opportunistic real `.dscalar.nii` round-trip. Dev-time cross-validation against the +`cifti_read`/`gifti` oracles: bit-exact on dscalar/dtseries/dlabel/surf/label files. +Key gotchas solved: MATLAB regexp `\b` does NOT word-boundary here (use `\s`); CIFTI maps +can be self-closing ``; the affine element is +`TransformationMatrixVoxelIndicesIJKtoXYZ`; strip whitespace only for base64 (not ASCII) +payloads; inflate must run fully JVM-side. + +*Naming note:* the design's `read_cifti_native` etc. were implemented under CANlab's +`canlab_` standalone-function convention. + +**M2 — Class skeleton + inheritance proof + import. ✅ DONE 2026-06-25.** +Built `@fmri_surface_data/fmri_surface_data.m` (`classdef fmri_surface_data < image_vector`) +with the full property set (`brain_model`/`geom`/`intent`/`series_info`/`label_table`/ +`surface_space`/`mask`/X/Y/covariates/images_per_session/metadata_table/...). Constructor +polymorphism implemented: empty / cifti-struct / gifti-struct / plain-struct / `'key',value` +/ filename-autoload (dispatches `.gii`→`canlab_read_gifti`, `.nii`→`canlab_read_cifti`) / +`isa(image_vector)`-recast. `brain_model` + `.dat` are built directly from M1's reader output +(the reader already returns the `diminfo{1}.models` split, so the `get_cifti_data` port was +unnecessary). `surface_space`/`grayordinate_type` inferred from vertex counts. No-op +`remove_empty`/`replace_empty` overrides added (`@fmri_surface_data/{remove_empty, +replace_empty}.m`); `removed_voxels`/`removed_images` initialised all-false (D5b). *Verified:* +`Unit_tests/surface_data/canlab_test_surface_object_basic.m` (7/7 pass) — all 7 construction +paths (real dscalar/dtseries/dlabel, GIFTI label, `.surf.gii` geometry, struct, empty), +`.dat` stays full under `remove_empty`/`replace_empty` (no squeeze), `get_wh_image` subsets +maps only (rows preserved), `removed_*` vestigial, inherited method surface present. +`volInfo` sub-block population was **deferred to M3** (not needed for the M2 inheritance proof). + +*M3 note (volInfo leak, Risk #1 confirmed):* inherited `descriptives` dereferences +`dat.volInfo.n_inmask` (its only volInfo use). Inherited methods that read `volInfo.*` +(`descriptives`, `montage`, `orthviews`, `flip`, `interpolate`, `get_xyzmm_coordinates`, +`rebuild_volinfo_from_dat`) need surface-aware guards/overrides in M3 — audit every +`volInfo.*` dereference and guard with `isfield` or override. + +**M3 — Spatial overrides + interop. ✅ DONE 2026-06-26.** +Built `@fmri_surface_data/`: `compare_space.m` (0/1/2/3 contract, brain_model-based), +`reconstruct_image.m` (returns a struct: dense per-hemisphere vertex arrays with medial +wall = NaN, + a [X×Y×Z×maps] subcortical volume + its volInfo), `to_fmri_data.m` (exports +the subcortical voxel sub-block to an `fmri_data` in MNI), `write.m` (dispatches to +`canlab_write_cifti`/`canlab_write_gifti`; rebuilds the maps dimension from +intent+image_names/label_table/series_info; faithful re-emit of stashed source XML when the +layout is unchanged, else regenerate), and `rebuild_like.m` (re-wrap new data carrying the +geometry). Plus a private `build_volinfo_subblock.m` (0-based CIFTI → 1-based SPM affine +conversion; wh_inmask aligned with grayordinate rows) used by the constructor (populates the +inherited `volInfo` slot for the subcortical sub-block; empty for surface-only) and +`to_fmri_data`. *Verified:* `Unit_tests/surface_data/canlab_test_surface_space_recon.m` +(7/7) — volInfo sub-block + affine, `to_fmri_data` values vs subcortical rows, +`reconstruct_image` cortex NaN-medial-wall + subcortex voxel values, the full compare_space +contract (0/1/2/3), `rebuild_like` (geometry preserved, row-count enforced), and native +CIFTI dscalar/dlabel write→read round-trips (maxdiff 0, affine + map names + label table +preserved). Full surface suite (M1+M2+M3) = **19/19**. + +*Deferred from M3:* (a) `reparse_contiguous` — needs the cortical mesh edge graph, which +depends on a bundled-mesh `geom` loader; moved to M5 (rendering) where that loader is built. +(b) Surface-aware overrides of the volInfo-dependent inherited QC/display methods +(`descriptives`, `montage`, `orthviews`, `flip`) — moved to M5/M6. `to_fmri_data` already +gives a clean volumetric escape hatch for those in the meantime. + +**M4 — Volume ↔ surface mapping. ✅ DONE 2026-06-26.** +Built `@image_vector/vol2surf.m` (volume → fsaverage_164k `fmri_surface_data`: reconstruct +volume, sample the CBIG RF-ANTs `ras` MNI→fsaverage per-vertex coords with `interpn`; +`'interp'` linear/nearest) and `@fmri_surface_data/surf2vol.m` (fsaverage_164k → `fmri_data` +in MNI 2 mm: scatter the same `ras` coords with `accumarray` mean; `'reference'` to set the +target grid; writes `.nii` via the returned fmri_data). Self-consistent inverse pair, fully +native — no FreeSurfer/Workbench. Plus `Surface_tools/canlab_cbig_warp_path.m` (resolves the +vendored MIT warps) and the overrides `@fmri_surface_data/{mean,apply_mask,threshold}.m` +(mean across maps via `rebuild_like`; apply_mask = zero out-of-mask rows, no fmri_mask_image/ +resample per D5b; threshold = raw-value only). *Verified:* +`Unit_tests/surface_data/canlab_test_surface_vol_map.m` (6/6) — vol2surf size/space, vertex +value == `interpn` sample (exact), **vol→surf→vol cortical correlation r = 0.9999**, surf2vol +rejects non-fsaverage objects, nearest-interp preserves integer labels, and mean/apply_mask/ +threshold. Full surface suite (M1–M4) = **25/25**. + +*Deferred from M4:* (a) the fs_LR-32k barycentric path (the `resample_from_*_to_fsLR_32k.mat` +structs are surface→surface fsnative→fs_LR deformations needing a vol→fsnative step first; the +fsaverage CBIG path is the v1, fsaverage↔fs_LR remains a later enhancement); (b) +cluster-extent `threshold` (needs the cortical mesh graph — M5); (c) combining the +subcortical sub-block into `surf2vol` output (use `to_fmri_data` for subcortex meanwhile). + +**M5 — Rendering. ✅ DONE 2026-06-26.** +Built `@fmri_surface_data/surface.m` (native 4-panel L/R×lateral/medial render on the bundled +inflated mesh; `'surftype'`/`'which_image'`/`'clim'`/`'pos_colormap'`/`'neg_colormap'`; +`'existingsurface'` and `'mni_surface'` modes), `@fmri_surface_data/render_on_surface.m` +(override: colors given patch handles — DIRECT per-vertex truecolor when the patch matches a +hemisphere's vertex count, e.g. any addbrain fs_LR/fsaverage mesh; else projects to a volume +via `obj_to_volume` and reuses `@image_vector/render_on_surface`), `@fmri_surface_data/plot.m` +(QC: histogram, per-map mean±sd, coverage, mean-map render), and the deferred +`@fmri_surface_data/reparse_contiguous.m` (cortex = mesh edge-graph `conncomp`; subcortex = +`spm_clusters`; writes `brain_model.cluster`). Helpers: `Surface_tools/canlab_surface_vertexcolors.m` +(value→truecolor split colormap, medial wall/zero = gray) and the private +`load_surface_geom.m` (bundled S12000 inflated / S1200 midthickness/sphere / fsaverage +inflated meshes) and `obj_to_volume.m`. **Native-space rendering needs no resampling** — +addbrain's `hcp inflated` is the fs_LR-32k template (32492) and `inflated`/`fsavg` is fsaverage +(163842), exact matches. **Rendering on any other (e.g. MNI pial) surface** works via the +volume projection. *Verified:* `Unit_tests/surface_data/canlab_test_surface_render.m` (7/7) — +native 4-panel render (truecolor per vertex, correct vertex counts for both spaces), medial +wall renders gray, direct coloring of an addbrain native patch, via-volume render onto an +addbrain MNI surface, `reparse_contiguous` (every active grayordinate labeled, inactive = 0), +and `plot`. Visually confirmed (transcriptomic gradient on inflated fs_LR; smooth map on +fsaverage and on an MNI surface). Full surface suite (M1–M5) = **32/32**. + +*Deferred from M5:* deep `@fmridisplay`/`addbrain` registration (the standalone `surface`/ +`render_on_surface` cover the rendering need; fmridisplay montage integration is optional +polish); `surface_outlines` overlay and a colorbar/legend on the native path; surface-aware +overrides of `montage`/`orthviews`/`flip`/`descriptives` (still routed via `to_fmri_data`). + +**M6 — Analysis parity. ✅ DONE 2026-06-26.** +Built `@fmri_surface_data/`: `cat.m` + `horzcat.m` (concatenate along maps; `compare_space==0` +required; per-map fields X/Y/covariates/image_names/metadata_table concatenated), +`ttest.m` and `predict.m` and `ica.m` (**delegated** to the corresponding `@fmri_data`/ +`@image_vector` methods via a private `as_fmri_data_proxy.m` — wraps the object as an +`fmri_data` with a dummy 1-D volInfo treating each grayordinate as a voxel — then remaps +geometry-bearing results back with `rebuild_like`; this reuses the entire battle-tested +algorithm/CV machinery), and a native lightweight `regress.m` (per-grayordinate OLS with +`obj.X`; betas in `.dat`, t/p/se/dfe in `additional_info.statistic`). `ttest`/`regress` +return an `fmri_surface_data` carrying the stat in `.dat` (t / betas) plus parallel fields in +`additional_info.statistic` (full surface statistic_image subclass deferred to M7). `predict` +returns `[cverr, stats, optout]` with `stats.weight_obj` remapped to a surface object. +*Verified:* `Unit_tests/surface_data/canlab_test_surface_analysis.m` (4 pass + ica filtered) — +cat/horzcat (+ space-mismatch error), ttest vs MATLAB `ttest` (matches to single precision), +regress OLS vs `\` (and recovered slope), predict CV round-trip with remapped weight map. +Full surface suite (M1–M6) = **36/36** (+ ica skipped where its toolbox is absent). + +*Upstream fix made:* `@image_vector/ica.m` had a stray, never-functional debug line +(`figure; plot(B(:), W(:), …)` referencing undefined `B`/`W`) that made `ica` error on *every* +call for all `image_vector` subclasses — removed it. `ica` still requires `icatb_fastICA` +(GIFT/icatb toolbox); it is the one `fmri_surface_data` method that is not fully self-contained. + +*Deferred from M6:* full `glm_map` integration for `regress` (contrasts/diagnostics — use +`to_fmri_data` + `fmri_data.regress`/`glm_map` for now); a dedicated +`fmri_surface_statistic_image` subclass with native cluster-extent `threshold` (M7). + +**M7 — Parcellation / region + surface atlas. ✅ DONE 2026-06-26.** +Built `@fmri_surface_data/`: `apply_parcellation.m` (parcel means `[nMaps × nParcels]` from a +`.dlabel` surface object or an integer key vector; key 0/NaN = background/medial wall excluded; +optional `'area'` weighting via per-vertex mesh surface area; returns labels + summary table), +`surface_region.m` (contiguous clusters → struct array with `.struct`/`.type`/`.grayord_rows`/ +`.vertex_indices`/`.XYZmm` centroid/`.numVox`/`.val`), and cluster-extent thresholding added to +`threshold.m` (`'k', N` via `reparse_contiguous`). Surface atlases are just `.dlabel` +`fmri_surface_data` objects (label keys + label_table), so they load through the standard +constructor — no separate import path needed. *Verified:* +`Unit_tests/surface_data/canlab_test_surface_parcellation.m` (5/5) — parcel means == keys on a +known synthetic parcellation and the real Gordon333+Tian atlas, label_table names, area +weighting finite, `compare_space` enforcement, cluster-extent threshold (no surviving cluster +< k), and `surface_region` partitioning the active grayordinates. + +*Deferred from M7:* the dedicated `fmri_surface_statistic_image` / `fmri_surface_atlas` +subclasses (the single class + `additional_info.statistic` + `.dlabel` objects cover the +functionality; subclasses are polish for `get_wh_image`-synced stat fields). + +**M8 — Docs + deprecation. ✅ DONE 2026-06-26.** +Authored `docs/fmri_surface_data_methods.md` (full method/option reference, updated through M7) +and `docs/fmri_surface_data_walkthrough.m` (runnable cell-mode walkthrough incl. parcellation + +group analysis; verified end-to-end). Added deprecation pointers in the external-dependent +`Cifti_plotting/render_cifti_on_brain.m` and `plot_surface_map.m` headers steering new code to +the native `fmri_surface_data`. *Deferred (optional, asset fetch — not code):* bundling CC0 +TemplateFlow `tpl-fsLR`/`tpl-fsaverage` meshes for a relicensable default (the in-repo HCP/ +FreeSurfer meshes work today; this is license hygiene); a `validate_object`-style suite (the +7-file, 41-test surface suite covers M1–M7). + +--- + +## Status: v1 complete (M1–M8) + +All eight milestones delivered. `fmri_surface_data` reads/writes CIFTI-2 + GIFTI natively, +holds grayordinate data with full `fmri_data`-style method parity (construct, reconstruct, +to_fmri_data, write, compare_space, cat/horzcat, mean/apply_mask/threshold, ttest/regress/ +predict/ica, vol2surf/surf2vol, surface/render_on_surface/plot, reparse_contiguous/ +apply_parcellation/surface_region), and runs with **no external toolbox** (sole exception: +`ica`, which needs the GIFT/icatb toolbox like the base class). Surface suite: **41 tests +passing** across 7 files. Best-in-class per-subject nonlinear mapping and fsaverage↔fs_LR +deformation remain the main future enhancements. + +--- + +## 11. Risks & open questions + +**Risks** +1. **`volInfo` dual-role leak.** Inherited methods read `volInfo.mat`/`.dim`/`.wh_inmask` + (counts: mat 25, wh_inmask 63, xyzlist 41, image_indx 37, cluster 22). Mitigation: + the surface truth lives in `brain_model`; `volInfo` describes *only* the volume + sub-block, and is empty for surface-only objects. Audit every `volInfo.*` dereference + (highest-risk overlooked inheritors: `rebuild_volinfo_from_dat`, + `get_xyzmm_coordinates`, `extract_roi_averages`, `montage`, `orthviews`, `flip`, + `interpolate`) and override or guard with `isfield`; do NOT rely on inheritance for any + method touching those fields. +2. ~~**remove/replace_empty contract**~~ **ELIMINATED (D5b).** `.dat` is always the full + grayordinate set; `remove_empty`/`replace_empty` are no-ops and `removed_voxels` is a + vestigial all-false vector. This whole risk class (the "forgot to `replace_empty`" bugs) + is removed by design. Remaining care: keep `.dat` rows in 1:1 order with `brain_model`. +3. **Native parser correctness.** CIFTI XML edge cases (CDATA, BrainModels tiling with no + gaps, row-major permute, `scl_slope`); GIFTI GZip = **raw zlib not gzip**, apache base64 + corruption, in-place JVM inflate garbage. Mitigation: validate against real HCP files; + keep `/tmp/test_gii_native.m` as the oracle; carry the three GIFTI gotchas verbatim. +4. **`compare_space` 4-valued contract** must be preserved (not boolean) or `cat`/ + `apply_mask` break subtly. +5. **`cat`/`horzcat`/`plot`/`predict`/`ttest`/`regress` are NOT inherited** (only in + `@fmri_data`) — forgetting to provide them yields silent "method not found". +6. **`mean`/rebuilders hardcode** the `image_vector`+`fmri_data` re-wrap — must route + through `rebuild_like` or they silently emit volume objects from surface data. +7. **Space proliferation.** Native CIFTI yields fs_LR-32k; CBIG RF yields fsaverage — + different topologies. fsaverage↔fs_LR deformation is deferred, so the two cannot be + `cat`'d without resampling. Gate `cat`/`compare_space` on `surface_space`. +8. **RF is a fixed group correspondence** — wrong for per-subject native space; document + clearly. Exclude medial wall from `apply_parcellation` column-normalization. +9. **Statistic/atlas parallel fields** (`.p/.ste/.sig`; integer keys) need to be carried + through `get_wh_image` (column subsetting) or they desync from `.dat` (handled in M7 + subclasses). Simpler than `fmri_data`, since `remove_empty`/`replace_empty` are no-ops. +10. **Surface area** for `get_region_volumes`/`rmsv` cannot be a no-op stub — compute from + face geometry early (M7). +11. **Known reuse bugs:** `make_surface_figure.m` hardcoded addpath; + `render_cifti_on_brain.m` L188 CORTEX_LEFT-for-right copy-paste — fix on reuse. +12. **License hygiene (hard requirement):** must NOT vendor `@xmltree` (LGPL) / `ft_cifti` + (GPL) / `gifti` MEX. Hand-roll permissively; ship CBIG MIT + HCP/TemplateFlow terms; + verify before bundling. + +**Open questions (need user confirmation — see §3):** +- **Q1.** Class name: `fmri_surface_data` (prefixed) vs. house-style `fmri_surface_data`? +- **Q2.** Confirm fs_LR-32k (91k) as the canonical default space. +- **Q3.** Add CC0 TemplateFlow `tpl-fsLR`/`tpl-fsaverage` meshes for a clean-license + default, or rely on the HCP S1200 surfaces already shipped? +- **Q4.** Hand-roll the CIFTI reader/writer (recommended, fully self-contained) vs. vendor + the BSD-2 cifti-matlab core? diff --git a/CanlabCore/docs/fmri_surface_data_methods.md b/CanlabCore/docs/fmri_surface_data_methods.md new file mode 100644 index 00000000..57427f67 --- /dev/null +++ b/CanlabCore/docs/fmri_surface_data_methods.md @@ -0,0 +1,375 @@ +# `fmri_surface_data` — methods and usage reference + +`fmri_surface_data` is the CANlab object for **cortical-surface and grayordinate +(HCP CIFTI) data**. It is the surface analogue of `fmri_data`: data are stored +flat as a `[grayordinates × maps]` matrix in `.dat`, with a `brain_model` +describing the cortical-surface vertices and subcortical voxels, so the familiar +CANlab method names (`mean`, `threshold`, `apply_mask`, `surface`, `write`, …) +work on surface data and interoperate with the rest of the toolbox. + +Everything runs **natively in MATLAB** — no gifti toolbox, FieldTrip, Connectome +Workbench, or FreeSurfer is required at runtime. + +- **Walkthrough (runnable):** [`fmri_surface_data_walkthrough.m`](fmri_surface_data_walkthrough.m) +- **Design rationale / roadmap:** [`fmri_surface_data_design_plan.md`](fmri_surface_data_design_plan.md) +- Class folder: `CanlabCore/@fmri_surface_data/`; native I/O helpers: `CanlabCore/Surface_tools/` + +--- + +## Contents + +1. [Concepts](#1-concepts) +2. [Construction](#2-construction) +3. [Properties](#3-properties) +4. [Method reference](#4-method-reference) + - [Import / export](#import--export) + - [Geometry & space](#geometry--space) + - [Volume ↔ surface mapping](#volume--surface-mapping) + - [Data operations](#data-operations) + - [Rendering & QC](#rendering--qc) +5. [Native I/O functions](#5-native-io-functions-surface_tools) +6. [Surface spaces](#6-surface-spaces) +7. [Design notes & limitations](#7-design-notes--limitations) + +--- + +## 1. Concepts + +**Grayordinates.** The HCP "grayordinate" model represents the cortex as +*surface vertices* and the subcortex/cerebellum as *voxels*. The standard "91k" +space has 91,282 grayordinates = left + right cortex vertices (the non-medial-wall +subset of the 32,492 fs_LR-32k vertices per hemisphere) plus ~31,870 subcortical +2 mm MNI voxels. This is already a flat `[grayordinates × maps]` matrix — exactly +what `.dat` expects. + +**`brain_model` replaces `volInfo`.** Cortical surface vertices have no voxel +coordinates, so `brain_model` (mirroring the CIFTI BrainModels) is the geometry +source of truth. The inherited `volInfo` slot is populated to describe **only** +the subcortical voxel sub-block (so `to_fmri_data` and volInfo-aware code work), +and is empty for surface-only objects. + +**No empty-squeezing.** Unlike `fmri_data` (which drops empty voxels to save +space on sparse volumes), grayordinate data is already compact, so `.dat` is +**always** the full `[grayordinates × maps]` set. `remove_empty` / `replace_empty` +are no-ops, and masking simply zeros out-of-mask rows. + +--- + +## 2. Construction + +```matlab +obj = fmri_surface_data % empty object +obj = fmri_surface_data(cifti_filename) % .dscalar/.dtseries/.dlabel.nii +obj = fmri_surface_data(gifti_filename) % .func/.shape/.label/.surf.gii +obj = fmri_surface_data(cifti_struct) % a canlab_read_cifti output struct +obj = fmri_surface_data(gifti_struct) % a canlab_read_gifti output struct +obj = fmri_surface_data('dat', X, 'brain_model', bm, 'surface_space', 'fsLR_32k', ...) +obj = fmri_surface_data(image_vector_obj) % recast a matching object +``` + +The constructor auto-detects CIFTI vs GIFTI by extension and reads it with the +native readers. `surface_space` and `grayordinate_type` are inferred from the +per-hemisphere vertex count. + +```matlab +s = fmri_surface_data(which('transcriptomic_gradients.dscalar.nii')); +size(s.dat) % [96854 3] +s.surface_space % 'fsLR_32k' +``` + +--- + +## 3. Properties + +| Property | Description | +|---|---| +| `dat` | `[nGrayordinates × nMaps]` single. The CIFTI cdata matrix (cortex L, cortex R, subcortical voxels). Always full (never squeezed). | +| `brain_model` | Geometry source of truth (CIFTI BrainModels). `.models{i}` has `.struct`, `.type` `'surf'`/`'vox'`, `.start`, `.count`, `.numvert`, `.vertlist` (0-based), `.voxlist` (3×N); plus `.vol` (`.dims`, `.sform`), `.grayordinate_type`, `.cluster`. | +| `geom` | Cortical mesh cache (faces/vertices), loaded for rendering. | +| `intent` | `'dscalar'` / `'dtseries'` / `'dlabel'` / `'func'` / `'shape'` / `'label'`. | +| `series_info` | For `.dtseries`: `.start`/`.step`/`.unit`/`.exponent`. | +| `label_table` | For `.dlabel`/`.label`: struct array `.key`/`.name`/`.rgba`. | +| `surface_space` | `'fsLR_32k'`, `'fsaverage_164k'`, … (gatekeeps `compare_space`; drives mesh/warp choice). | +| `volInfo` | Inherited; populated for the subcortical voxel sub-block only (empty for surface-only). | +| `mask` | Optional `[nGray × 1]` logical (or another same-space object) for `apply_mask`. | +| `X` / `Y` / `Y_names` / `covariates` / `covariate_names` / `images_per_session` / `metadata_table` / `image_metadata` / `additional_info` | Per-map (column) annotations, same names/roles as `fmri_data`. | +| `removed_voxels` / `removed_images` | Inherited but **vestigial** (always all-false); grayordinate data is never squeezed. | +| `image_names` / `fullpath` / `history` / `source_notes` / `dat_descrip` | Inherited provenance / file metadata. | + +--- + +## 4. Method reference + +> Type `methods(fmri_surface_data)` for the full list. Methods inherited from +> `image_vector` that operate purely on `.dat` (`get_wh_image`, `ica`, `mahal`, +> `pca`, …) also apply. volInfo-dependent display/QC methods (`descriptives`, +> `montage`, `orthviews`, `flip`) are not yet surface-aware — run them on the +> volumetric part via `to_fmri_data(obj)` for now. + +### Import / export + +#### `write(obj, filename)` +Write to a CIFTI-2 (`.nii`) or GIFTI (`.gii`) file natively; dispatches on the +extension. Rebuilds the CIFTI maps dimension from `intent` + `image_names` / +`label_table` / `series_info`; re-emits the original XML faithfully when the +layout is unchanged, else regenerates it. + +```matlab +write(s, '/tmp/out.dscalar.nii'); % CIFTI-2 +write(s, 'fname', '/tmp/out.func.gii'); % GIFTI +``` + +#### `to_fmri_data(obj)` +Export the subcortical/volumetric grayordinates as a standard `fmri_data` object +in MNI space (writeable to `.nii`, montageable, etc.). Cortical surface +grayordinates are dropped (use `surf2vol`). + +```matlab +vol = to_fmri_data(s); % subcortex as fmri_data +``` + +### Geometry & space + +#### `reconstruct_image(obj)` +Returns a struct with dense per-hemisphere vertex arrays (medial wall = `NaN`) +and a `[X × Y × Z × maps]` subcortical volume. + +```matlab +r = reconstruct_image(s); +r.cortex_left % [numvert × nMaps], medial wall NaN +r.cortex_right +r.volume % subcortical volume +r.volume_volInfo % its SPM-style volInfo +``` + +#### `compare_space(obj, obj2)` +Surface analogue of `image_vector.compare_space`, preserving the integer contract: +`0` same · `1` different space (tag/layout) · `2` missing `brain_model` · `3` same +space but different in-data grayordinates. + +#### `reparse_contiguous(obj, 'which_image', k)` +Label contiguous clusters of active (nonzero, non-NaN) grayordinates: cortex via +the mesh edge graph (`graph`/`conncomp`), subcortex via 26-connectivity +(`spm_clusters`). Writes integer labels to `brain_model.cluster`. + +```matlab +[obj, ncl] = reparse_contiguous(threshold(s, 1.0, 'positive')); +``` + +### Volume ↔ surface mapping + +#### `vol2surf(volume_obj, 'interp', 'linear')` *(method on `image_vector` / `fmri_data`)* +Project a volumetric image (MNI152) onto the **fsaverage-164k** cortical surface, +returning an `fmri_surface_data`. Samples the CBIG RF-ANTs MNI→fsaverage +per-vertex coordinates with `interpn`. Use `'interp','nearest'` for label maps. + +```matlab +ssurf = vol2surf(fmri_data('weights.nii')); % fsaverage_164k object +ssurf = vol2surf(t, 'interp', 'nearest'); % e.g. an integer atlas +``` + +#### `surf2vol(obj, 'reference', ref)` +Inverse of `vol2surf`: project an **fsaverage-164k** object back to an MNI152 +volume (`fmri_data`), scattering the same CBIG coordinates (`accumarray` mean). +`'reference'` (an `fmri_data`/`image_vector`) sets the target grid; default is MNI +2 mm `[91 109 91]`. The vol→surf→vol round-trip correlates ~1.0 on cortical voxels. + +```matlab +backvol = surf2vol(ssurf); % -> fmri_data in MNI +backvol = surf2vol(ssurf, 'reference', some_fmri_data); +``` + +### Data operations + +#### `mean(obj, ['omitnan'])` +Average across maps (columns), returning a single-map object (geometry preserved). + +#### `apply_mask(obj, mask, ['invert'])` +Keep a subset of grayordinates, **zeroing** the rest (no shrinking; D5b). `mask` +is a `[nGray × 1]` logical/numeric vector or another same-space `fmri_surface_data`. + +```matlab +s2 = apply_mask(s, s.dat(:,1) > 0); % zero non-positive grayordinates +``` + +#### `threshold(obj, thresh, ['positive'|'negative'], ['k', N])` +Zeros grayordinates inside the threshold range. `thresh` is a scalar `t` (keeps +`|value| ≥ t`) or `[lo hi]` (keeps `value ≤ lo | value ≥ hi`). With `'k', N`, +applies a **cluster-extent threshold** after the raw threshold — removing +contiguous clusters smaller than `N` grayordinates (mesh-graph connectivity for +cortex, 26-connectivity for subcortex; see `reparse_contiguous`). + +```matlab +t = threshold(s, 2.0, 'positive', 'k', 20); % >2, clusters >= 20 grayordinates +``` + +#### `rebuild_like(obj, newdat)` +Wrap a new `[nGray × K]` matrix into an `fmri_surface_data` carrying `obj`'s +geometry. Used internally by data-transforming methods. + +### Analysis + +These mirror the `fmri_data` analysis surface. `predict`, `ttest`, and `ica` +delegate to the corresponding `fmri_data`/`image_vector` methods (treating each +grayordinate as a feature), so the algorithms are identical; geometry-bearing +results are remapped back to `fmri_surface_data`. + +#### `cat(obj1, obj2, ...)` / `[obj1, obj2, ...]` +Concatenate objects along the map (image) dimension — e.g. building a group +dataset from per-subject maps. All objects must share the same grayordinate +space (`compare_space == 0`). Per-map fields (`X`, `Y`, `covariates`, +`image_names`, `metadata_table`) are concatenated too. + +```matlab +all_subs = cat(sub1, sub2, sub3); % or [sub1 sub2 sub3] +``` + +#### `ttest(obj, [pthresh], [k])` +Grayordinate-wise one-sample t-test across maps. Returns an `fmri_surface_data` +with the t-map in `.dat` and `.p`/`.ste`/`.sig`/`.dfe` in +`.additional_info.statistic`. + +```matlab +t = ttest(cat(sub1, sub2, sub3)); +surface(t, 'clim', [-5 5]); +``` + +#### `regress(obj, [X])` +Grayordinate-wise OLS regression of the maps onto design `X` (`[nMaps × p]`; +defaults to `obj.X`; no intercept added automatically). Returns coefficients in +`.dat` (`[nGray × p]`) and `.t`/`.p`/`.se`/`.dfe` in `.additional_info.statistic`. +For contrasts/diagnostics, use `to_fmri_data` + `fmri_data.regress`/`glm_map`. + +```matlab +obj.X = [ones(n,1) age(:)]; +b = regress(obj); +surface(get_wh_image(b, 2)); % the age effect +``` + +#### `predict(obj, 'algorithm_name', ..., 'nfolds', ...)` +Cross-validated multivariate prediction (set `obj.Y` first). Returns +`[cverr, stats, optout]` as `fmri_data.predict`; `stats.weight_obj` is remapped +to an `fmri_surface_data` you can `surface`/`write`. + +```matlab +obj.Y = scores(:); +[err, stats] = predict(obj, 'algorithm_name', 'cv_lassopcr', 'nfolds', 5); +surface(stats.weight_obj); +``` + +#### `ica(obj, [nIC])` +Spatial ICA; returns an `fmri_surface_data` whose maps are the spatial +components. **Requires the GIFT/`icatb` toolbox (`icatb_fastICA`)** — the one +method that is not fully self-contained. + +### Parcellation & regions + +#### `apply_parcellation(obj, parcels, ['area'])` +Average each map within the parcels of a surface atlas. `parcels` is another +`fmri_surface_data` (a `.dlabel` on the same space; its `label_table` supplies +names) or an integer key vector `[nGray × 1]`. Key 0 / NaN = background (medial +wall) and is excluded. `'area'` area-weights by per-vertex surface area. + +```matlab +atl = fmri_surface_data(which('Gordon333.32k_fs_LR_Tian_Subcortex_S2.dlabel.nii')); +[pm, labels, tbl] = apply_parcellation(s, atl); % pm: [nMaps × nParcels] +``` + +Returns `parcel_means` `[nMaps × nParcels]`, a `labels` cell, and a summary +`table` (`key`, `label`, `n_grayordinates`, `total_weight`). + +#### `surface_region(obj, ['which_image', k])` +Summarize contiguous clusters (active = nonzero, non-NaN) as a struct array — the +surface analogue of `region()`. Each element has `.struct`, `.type`, +`.cluster_id`, `.grayord_rows`, `.vertex_indices`, `.XYZmm` (centroid), +`.numVox`, `.val`. (For a full CANlab `region` object on the subcortical part, +use `region(to_fmri_data(obj))`.) + +```matlab +reg = surface_region(threshold(t, 3, 'positive', 'k', 20)); +[reg.numVox] % cluster sizes +``` + +### Rendering & QC + +#### `surface(obj, ...)` +Render on cortical surfaces. Three modes: + +- **Native (default):** loads the bundled mesh matching `surface_space` and colors + vertices directly (no resampling) in a 4-panel figure (L/R × lateral/medial). +- **`'existingsurface', handles`:** color patch handles you already have. +- **`'mni_surface', name`:** create an `addbrain` surface (e.g. `'left'`, + `'hcp inflated'`) and render onto it, projecting through a volume when the + surface is not the object's native mesh. + +| Option | Meaning | +|---|---| +| `'surftype'` | `'inflated'` (default), `'midthickness'`, `'sphere'` (fs_LR). | +| `'which_image'` | map (column) to render (default 1). | +| `'clim'` | `[lo hi]` color limits (default symmetric from data). | +| `'pos_colormap'` / `'neg_colormap'` | `[n × 3]` colormaps (default hot / cool). | + +```matlab +surface(s, 'which_image', 1); % native fs_LR, 4 views +surface(ssurf, 'mni_surface', 'left'); % on an addbrain MNI surface +han = addbrain('hcp inflated left'); % an fs_LR mesh +surface(s, 'existingsurface', han); % color it directly +``` + +#### `render_on_surface(obj, handles, ...)` +Lower-level: color given patch `handles`. Patches whose vertex count matches a +hemisphere are colored **directly** (true native-space rendering — works for any +matching-space mesh, e.g. all `addbrain` fs_LR/fsaverage surfaces); other surfaces +are colored by projecting the object to a volume and reusing +`image_vector.render_on_surface`. Options: `'which_image'`, `'clim'`, +`'pos_colormap'`, `'neg_colormap'`. + +#### `plot(obj, ['norender'])` +Quick QC panel: value histogram, per-map mean ± sd, coverage, and a mean-map +surface render. + +--- + +## 5. Native I/O functions (`Surface_tools/`) + +These standalone functions back the object and can be used directly. They depend +only on core MATLAB (+ the JVM for gzip) — no external toolbox. + +| Function | Purpose | +|---|---| +| `canlab_read_cifti(file)` | Read CIFTI-2 (`.dscalar`/`.dtseries`/`.dlabel`/`.dconn.nii`) → struct (`.cdata`, `.diminfo`, `.intent`, `.hdr`, `.xml`). | +| `canlab_write_cifti(file, cii)` | Write CIFTI-2 (faithful re-emit or regenerate). | +| `canlab_read_gifti(file)` | Read GIFTI (`.surf`/`.func`/`.shape`/`.label.gii`) → struct (`.vertices`, `.faces`, `.cdata`, `.labels`). | +| `canlab_write_gifti(file, gii)` | Write GIFTI (ASCII / Base64 / GZipBase64). | +| `canlab_surface_vertexcolors(vals, clim, poscm, negcm)` | Map per-vertex values → truecolor (split hot/cool; NaN/zero = gray). | +| `canlab_cbig_warp_path(name)` | Resolve the vendored CBIG RF warp paths. | + +--- + +## 6. Surface spaces + +| Space | Per-hemi vertices | Source | Used by | +|---|---|---|---| +| `fsLR_32k` | 32,492 | native CIFTI (HCP grayordinates) | `fmri_surface_data(cifti)` | +| `fsaverage_164k` | 163,842 | CBIG RF mapping | `vol2surf` output | + +`vol2surf` produces `fsaverage_164k`; native CIFTI is `fsLR_32k`. These have +different mesh topologies, so they cannot be combined without resampling +(fsaverage↔fs_LR deformation is a planned enhancement). Native rendering uses the +matching bundled mesh in `canlab_canonical_brains/Canonical_brains_surfaces/` +(`addbrain('hcp inflated')` for fs_LR, `addbrain('inflated')` for fsaverage). + +--- + +## 7. Design notes & limitations + +- **Group-template mapping.** `vol2surf`/`surf2vol` use a fixed group MNI152↔ + fsaverage correspondence (correct for group MNI maps; not a per-subject + ribbon-constrained mapper). Best-in-class per-subject mapping is a future step. +- **fs_LR ↔ fsaverage.** Not yet bridged; keep a workflow in one space, or go + through a volume. +- **Cluster-extent thresholding**, a colorbar/outline overlay on the native render, + and surface-aware overrides of `montage`/`orthviews`/`flip`/`descriptives` are + planned; the volumetric escape hatch (`to_fmri_data`) covers those cases today. +- See [`fmri_surface_data_design_plan.md`](fmri_surface_data_design_plan.md) for the + full milestone roadmap and the rationale behind these choices. diff --git a/CanlabCore/docs/fmri_surface_data_walkthrough.m b/CanlabCore/docs/fmri_surface_data_walkthrough.m new file mode 100644 index 00000000..08658d35 --- /dev/null +++ b/CanlabCore/docs/fmri_surface_data_walkthrough.m @@ -0,0 +1,203 @@ +%% Working with surface and grayordinate data: the fmri_surface_data object +% +% This walkthrough demonstrates the common operations on |fmri_surface_data|, +% the CANlab object for cortical-surface and grayordinate (HCP CIFTI) data. It +% is the surface analogue of |fmri_data|: data are stored flat as a +% [grayordinates x maps] matrix in |.dat|, with a |brain_model| describing the +% surface vertices and subcortical voxels, so the familiar CANlab method names +% (mean, threshold, apply_mask, surface, write, ...) work on surface data. +% +% Everything here runs natively in MATLAB -- no gifti toolbox, FieldTrip, +% Connectome Workbench, or FreeSurfer is required at runtime. +% +% Reference: docs/fmri_surface_data_methods.md +% +% To run: make sure CanlabCore (with Surface_tools/ and @fmri_surface_data/) and +% Neuroimaging_Pattern_Masks are on your path (canlab_toolbox_setup), then step +% through the cells below. + +%% Executive summary +% +% A complete tour in a few commands: +% +% s = fmri_surface_data(which('transcriptomic_gradients.dscalar.nii')); % load CIFTI +% surface(s, 'which_image', 1); % render on the native fs_LR surface +% r = reconstruct_image(s); % dense vertex arrays + subcortex volume +% vol = to_fmri_data(s); % subcortex -> fmri_data (writeable .nii) +% s2 = vol2surf(fmri_data('some.nii')); % project a volume onto the surface +% back= surf2vol(s2); % ...and back to a volume +% write(s, 'out.dscalar.nii'); % write CIFTI natively +% +% Now step through it in detail. + +%% 1. Load a CIFTI grayordinate file +% +% The constructor auto-detects CIFTI (.dscalar/.dtseries/.dlabel.nii) and GIFTI +% (.surf/.func/.shape/.label.gii) by extension and reads them natively. + +ciftifile = which('transcriptomic_gradients.dscalar.nii'); % ships with Neuroimaging_Pattern_Masks +s = fmri_surface_data(ciftifile); + +disp(s.intent) % 'dscalar' +disp(s.surface_space) % 'fsLR_32k' +fprintf('%d grayordinates x %d maps\n', size(s.dat,1), size(s.dat,2)); + +%% 2. Inspect the geometry (brain_model) +% +% brain_model mirrors the CIFTI BrainModels: one entry per cortical hemisphere +% (surface vertices) and per subcortical structure (voxels). It plays the role +% volInfo plays for fmri_data. + +cellfun(@(m) sprintf('%-18s %4s %d rows', m.struct, m.type, m.count), ... + s.brain_model.models, 'UniformOutput', false)' + +% The inherited volInfo describes ONLY the subcortical voxel sub-block: +disp(s.volInfo) + +%% 3. Render on the native cortical surface +% +% surface() loads the bundled mesh matching the object's surface_space (here +% fs_LR-32k) and colors vertices directly -- no resampling. The default is a +% 4-panel figure (left/right x lateral/medial). The medial wall renders gray. + +surface(s, 'which_image', 1); +drawnow, snapnow; + +% Pick a different colormap range or map: +% surface(s, 'which_image', 2, 'clim', [-4 4]); + +%% 4. Reconstruct to dense surfaces and a subcortical volume +% +% reconstruct_image returns per-hemisphere dense vertex arrays (medial wall = +% NaN) and a 3-D/4-D subcortical volume. .dat is always full (no replace_empty +% needed for fmri_surface_data). + +r = reconstruct_image(s); +fprintf('cortex_left : %d vertices x %d maps\n', size(r.cortex_left,1), size(r.cortex_left,2)); +fprintf('subcortex vol: %s\n', mat2str(size(r.volume))); + +%% 5. Export the subcortical part to an fmri_data object (and a .nii) +% +% to_fmri_data returns the volumetric grayordinates as a standard fmri_data in +% MNI space, which you can montage, threshold, or write to NIfTI. + +vol = to_fmri_data(s); +fprintf('subcortex fmri_data: %d voxels x %d maps\n', size(vol.dat,1), size(vol.dat,2)); +% vol.fullpath = fullfile(pwd, 'subctx.nii'); write(vol); % uncomment to write + +%% 6. Project a volume onto the surface (vol2surf) and back (surf2vol) +% +% vol2surf samples any MNI152 volume (fmri_data/statistic_image) onto the +% fsaverage cortical surface using the vendored CBIG registration-fusion warps. +% surf2vol is its self-consistent inverse. + +img = load_image_set('emotionreg'); % 30 contrast images (fmri_data) +m = mean(img); % a single mean volume +ssurf = vol2surf(m); % -> fmri_surface_data, fsaverage_164k +surface(ssurf); % render the projected map +drawnow, snapnow; + +backvol = surf2vol(ssurf); % fsaverage surface -> MNI fmri_data +fprintf('surf2vol -> fmri_data with %d cortical voxels\n', size(backvol.dat,1)); + +%% 7. Render on ANY existing surface (e.g. an addbrain MNI surface) +% +% Pass an addbrain keyword (or your own patch handles). If the surface is not +% the object's native mesh, the data is projected through a volume automatically +% (resampling), reusing image_vector.render_on_surface. + +create_figure('mni surface'); +surface(ssurf, 'mni_surface', 'left'); % render onto an addbrain MNI cortical surface +drawnow, snapnow; + +%% 8. Threshold and find contiguous clusters on the mesh +% +% threshold zeros sub-threshold grayordinates (raw-value). reparse_contiguous +% labels contiguous clusters using the cortical mesh graph (cortex) and 26-voxel +% connectivity (subcortex), storing labels in brain_model.cluster. + +st = threshold(s, 1.0, 'positive'); +[st, ncl] = reparse_contiguous(st, 'which_image', 1); +fprintf('Found %d contiguous clusters above threshold\n', ncl); + +%% 8b. Parcellate with a surface atlas +% +% apply_parcellation averages each map within the parcels of a surface atlas (a +% .dlabel object on the same space, or an integer key vector). Background / medial +% wall (key 0) is excluded. surface_region summarizes contiguous clusters. + +atlasfile = which('Gordon333.32k_fs_LR_Tian_Subcortex_S2.dlabel.nii'); +if ~isempty(atlasfile) + atl = fmri_surface_data(atlasfile); % 91k surface atlas + if compare_space(s, atl) == 0 + [parcel_means, parcel_labels] = apply_parcellation(s, atl); + fprintf('Parcellated into %d parcels (%d maps)\n', size(parcel_means,2), size(parcel_means,1)); + end +end + +% Clusters of the thresholded map as region-like structs: +reg = surface_region(st, 'which_image', 1); +fprintf('surface_region: %d clusters; sizes: %s\n', numel(reg), mat2str([reg.numVox])); + +%% 9. Other analysis-style operations +% +% mean() averages across maps; apply_mask() keeps a subset of grayordinates +% (zeroing the rest -- no shrinking, since grayordinate data is already compact). + +mn = mean(s); % single-map mean +keep = s.dat(:,1) > 0; % a simple grayordinate mask +smasked = apply_mask(s, keep); % zero out non-positive grayordinates + +%% 9b. Group analysis: cat, ttest, regress, predict +% +% Combine per-subject surface maps with cat (or [a, b, ...]) into a single +% object, then run the same analyses as fmri_data. Here we simulate a few +% "subjects" by projecting individual emotionreg contrast images to the surface. + +subj = cell(1, 5); +for i = 1:5 + subj{i} = vol2surf(get_wh_image(img, i)); % each subject's contrast on the surface +end +group = cat(subj{:}); % one object, 5 maps +fprintf('group object: %d grayordinates x %d maps\n', size(group.dat,1), size(group.dat,2)); + +tmap = ttest(group); % grayordinate-wise t-test +surface(tmap, 'clim', [-4 4]); +drawnow, snapnow; + +% Cross-validated prediction of an outcome (here a synthetic one): +group.Y = (1:5)'; +[cverr, stats] = predict(group, 'algorithm_name', 'cv_lassopcr', 'nfolds', 5, 'verbose', 0); +% surface(stats.weight_obj); % render the predictive weight map + +% OLS regression onto a design matrix: +group.X = [ones(5,1), (1:5)']; +b = regress(group); % betas in b.dat; t/p in b.additional_info.statistic + +%% 10. Write CIFTI / GIFTI natively +% +% write() dispatches on the output extension. CIFTI map metadata (scalar names, +% label tables, series info) and the subcortical affine are preserved. + +outdir = tempdir; +write(s, fullfile(outdir, 'gradients_copy.dscalar.nii')); % CIFTI-2 +write(ssurf, fullfile(outdir, 'emotionreg_surf.func.gii')); % GIFTI (cortex) +fprintf('Wrote CIFTI and GIFTI to %s\n', outdir); + +% Round-trip check: +s_reloaded = fmri_surface_data(fullfile(outdir, 'gradients_copy.dscalar.nii')); +fprintf('Round-trip max abs diff: %g\n', max(abs(double(s.dat(:)) - double(s_reloaded.dat(:))))); + +%% 11. Quick QC plot +% +% plot() shows a value histogram, per-map mean/sd, coverage, and a mean-map +% surface render. + +plot(s); +drawnow, snapnow; + +%% See also +% +% docs/fmri_surface_data_methods.md - full method/option reference +% docs/fmri_surface_data_design_plan.md - design rationale and roadmap +% methods(fmri_surface_data) - the full method list From 8f7c893b5493825c52653822566f5a9996c98277 Mon Sep 17 00:00:00 2001 From: Tor Wager Date: Sat, 4 Jul 2026 14:35:12 -0400 Subject: [PATCH 6/6] Add fmri_surface_data docs, workflow how-to, and a CIFTI sample map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../CIFTI_surface_examples/README.md | 41 ++++ ...200.MyelinMap_MSMAll.32k_fs_LR.dscalar.nii | Bin 0 -> 580288 bytes docs/Object_methods.md | 2 + docs/Workflows.md | 1 + docs/fmri_surface_data_methods.md | 154 +++++++++++++ docs/workflows/fmri_surface_atlas.png | Bin 0 -> 266611 bytes docs/workflows/fmri_surface_data_howto.md | 208 ++++++++++++++++++ docs/workflows/fmri_surface_myelin.png | Bin 0 -> 200620 bytes docs/workflows/fmri_surface_vol2surf.png | Bin 0 -> 218610 bytes 9 files changed, 406 insertions(+) create mode 100644 CanlabCore/Sample_datasets/CIFTI_surface_examples/README.md create mode 100644 CanlabCore/Sample_datasets/CIFTI_surface_examples/S1200.MyelinMap_MSMAll.32k_fs_LR.dscalar.nii create mode 100644 docs/fmri_surface_data_methods.md create mode 100644 docs/workflows/fmri_surface_atlas.png create mode 100644 docs/workflows/fmri_surface_data_howto.md create mode 100644 docs/workflows/fmri_surface_myelin.png create mode 100644 docs/workflows/fmri_surface_vol2surf.png diff --git a/CanlabCore/Sample_datasets/CIFTI_surface_examples/README.md b/CanlabCore/Sample_datasets/CIFTI_surface_examples/README.md new file mode 100644 index 00000000..9d52dde6 --- /dev/null +++ b/CanlabCore/Sample_datasets/CIFTI_surface_examples/README.md @@ -0,0 +1,41 @@ +# CIFTI surface / grayordinate sample data + +Example CIFTI-2 surface data used by the `fmri_surface_data` object documentation +and walkthrough (see `docs/workflows/fmri_surface_data_howto.md` and +`docs/fmri_surface_data_methods.md`). Loads with: + +```matlab +s = fmri_surface_data(which('S1200.MyelinMap_MSMAll.32k_fs_LR.dscalar.nii')); +surface(s); +``` + +| File | Type | Contents | +|---|---|---| +| `S1200.MyelinMap_MSMAll.32k_fs_LR.dscalar.nii` | `.dscalar` | HCP S1200 group-average cortical myelin map (T1w/T2w), 59,412 cortical grayordinates (fs_LR-32k, medial wall excluded). A continuous map — good for surface visualization. | + +## Additional surface data (Neuroimaging_Pattern_Masks) + +CanlabCore ships only this one small continuous map. **Surface atlases, +grayordinate parcellations, and other CIFTI maps used by parts of the walkthrough +live in the companion `Neuroimaging_Pattern_Masks` repository** (a standard CANlab +dependency; add it with `canlab_toolbox_setup`). Once it is on the path you can +load e.g.: + +```matlab +atl = fmri_surface_data(which('Gordon333.32k_fs_LR_Tian_Subcortex_S2.dlabel.nii')); +grd = fmri_surface_data(which('transcriptomic_gradients.dscalar.nii')); +``` + +See the inventory table in `docs/fmri_surface_data_methods.md` (section +"Surface data in Neuroimaging_Pattern_Masks"). + +## Provenance and license + +- **HCP S1200 group myelin map** — from the Human Connectome Project S1200 release + (`Q1-Q6_RelatedParcellation210.MyelinMap_BC_MSMAll_2_d41_WRN_DeDrift.32k_fs_LR.dscalar.nii`), + distributed under the [HCP Open Access Data Use Terms](https://www.humanconnectome.org/study/hcp-young-adult/document/wu-minn-hcp-consortium-open-access-data-use-terms) + (redistributable for research/education with acknowledgment; WU-Minn HCP + Consortium, funded by the NIH Blueprint for Neuroscience Research). + +This is a group-average template file (no individual-subject data). If you +redistribute CanlabCore, retain this README with its attribution. diff --git a/CanlabCore/Sample_datasets/CIFTI_surface_examples/S1200.MyelinMap_MSMAll.32k_fs_LR.dscalar.nii b/CanlabCore/Sample_datasets/CIFTI_surface_examples/S1200.MyelinMap_MSMAll.32k_fs_LR.dscalar.nii new file mode 100644 index 0000000000000000000000000000000000000000..896ec9afbaad570701ba1fde8d615264dd07d9d0 GIT binary patch literal 580288 zcmeF)O|NYCd8T(JKxd#JKn6VuGT7k2lZNcQ>%C%122E170KGMN)SYB_El@`x}I9K)?G#EfBtXZdh4@)b9n22 z_|E_Io&WKzhqwN_x4!z%fAcT=?|=QDe*foxi~WE8U%&oe{?+e1=70XH-}%G;zxiMJ z41A}b|BwIuTW`Jh`DdTK|NPmP&maBb)$3PZeCtunz|U^~s;PMEt#|(FAAj}w;UAvA z`0~@wKl|R>Pk;XS_QUgMzxe#)Pe1$R_ul^biywUV_V!Azx?s5Uq8J2>tDbA^!dvdpMUkE?|=HM z4sAPYSKs{Hi_hPG_2$)!fBNj7T=^?NUW{Lzo!ef|37;pNAtrlBbymHvzK7g8tiS z(w83ae*5#s{paEE^~UMzjr--dTYK#P;=_Esub+SU{KfY5%^tq{^!WJoBmVQo)1N<` z?Dpa5&(C)o+r=NDzkcA~K6RdIhy4$qe*FCW*~>5A{L|~_p?~oAUt>S~?9KBRzy91b z{q`r&Z@&1=SJWT>>#yPWKmGF8uV4Ms&p!Y1(>I^~!?VHpJ^D}l{U6}}`|}rjsX#w@ z_4>=N_WWD+cmMJaxSu@#vcdmmx8Gv_@CUx;U;OjAxppFjSWKj0t!{GWKhcmLs+?|$+8>c94JzlDA4;SJ%}FM{9yRe$$yzJ-1G>a$<| z^GW1q@BZk67cV||`ORE>`0h{s=7X0zx%~LupZ)&h{vP_R2maCL|L5O+`pdun(WjsN zAI~5E39sM!neS{5f8=cF0lxFjU+ii7c8;j{q!$>^Zx4(-~HiFHVi-d);suLfAOj} z;NFa0yA*i-$De-k$(PS>zW4T5mj=)O_}=Hg`K&)4?w9-J?T7vJN1bhd@unC5*VhRj zz4-9w@4fi>habFr@24NW=#t?_AN=6O+Yfux-@TqNjz9YC^~BFV{K32Leem|fN5A>v zlUKiZ{>g8C^>@!-{PdHqZ{E0W`QF>d!|DF@Mb6jnyc6$7p1*$fzTX?p!{OohaC$gD zTpn%@_lKvJo*teK4^PL3r_;mJ`QhpE@N|86x;;GI9}bTXm)pbT{&0PKxIR5x4-ePl z!}auVJwIG857+C%_4aVRKinQ4Zch)l!^7?Pa63KR&JVX9{rYgbJ>2dO_s56()5HDn za6dlWPY?I=!~ODbzdqdCzXRw99y^4`j^VL`c_{H_xgBXg=U5%A zqjk8B*8w}?4*98LemeM-`nf|tcj><`{=1*Q`1ShvOF!Q)_SA3o)UWo`@AlL$_tbCq z)UWr{@AuR%_|$Ls)UWu|@A%X&`P6Uu)UWx}@A=d(`qXdw)UW!~?|L|%`{&X>wr~B@ z(|3C8pPurbg+ounq37Yy6LIL7IP_E;dM=Kqgq)83bLt;|zVy$ve{K)Qe&*e3w?$pnn`ngj-ckVII{p`7)J@>Qce)in{&+Y%* zW1nL#9p|OvymXwGj^)ztbm@1x^gCS=cs-{6nENC7**~V=2$+ElL7sd5Z$1CFp8s3V z|Lxp=U;6J$Q_=H(>-q08r+=D*Jm2$t&-1H?j7&F=lb4reeb!x_gvpS z*N+50GWqZ_QO( zPls*w08fV=;OWofv88V>-%z+!ye*3nLUT#OPw<8}%K8}1G`8e`% z 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/Object_methods.md b/docs/Object_methods.md index fa39f079..3ecab82b 100644 --- a/docs/Object_methods.md +++ b/docs/Object_methods.md @@ -34,6 +34,7 @@ image_vector (abstract base; rarely used directly) ├── fmri_data generic image data + .X, .Y, covariates ├── statistic_image stat maps with t / p / sig ├── atlas labeled parcellation with probability maps +├── fmri_surface_data cortical-surface / grayordinate (HCP CIFTI) data └── fmri_mask_image binary mask (legacy) region list of contiguous clusters @@ -57,6 +58,7 @@ Listed in roughly the order most users encounter them. Click a class name for th | **[`image_vector`](image_vector_methods.md)** | Abstract superclass. You rarely create one directly, but most of the methods you call on an `fmri_data`, `statistic_image`, or `atlas` are inherited from here (`apply_mask`, `resample_space`, `montage`, `surface`, `extract_roi_averages`, etc.). | | **[`statistic_image`](statistic_image_methods.md)** | Stat maps (t / p / effect-size) with thresholding state. Produced by `ttest`, `regress`, etc. The `threshold` method re-thresholds without losing the underlying values. | | **[`atlas`](atlas_methods.md)** | Brain atlases / parcellations. Includes both winner-take-all labels for each parcel and probabilistic maps, region labels ( `.labels`), `.references`, and other metadata. Makes it easy to extract or analyze data within named atlas regions or on region/parcel averages. Methods include `select_atlas_subset`, `merge_atlases`, `downsample_parcellation`, `atlas2region`. Use `load_atlas` to load by keyword. | +| **[`fmri_surface_data`](fmri_surface_data_methods.md)** | Cortical-surface and grayordinate (HCP CIFTI) data — the surface analogue of `fmri_data`. Reads/writes CIFTI-2 and GIFTI **natively** (no gifti/FieldTrip/Workbench toolbox), stores data as `[grayordinates × maps]`, and supports the familiar methods (`surface`, `threshold`, `mean`, `apply_parcellation`, `predict`, `ttest`, `write`) plus `vol2surf` / `surf2vol` to map between volumes and the cortical surface. See the [surface data how-to](workflows/fmri_surface_data_howto.md). | | **[`region`](region_methods.md)** | Vector storing information about a set of contiguous clusters / ROIs as a unit of analysis, with one element per region. Designed to hold a compact representation of thresholded maps, including data, voxel coordinates and locations, and facilitate rendering on brains and tables. Produced by `region(t)` from a thresholded `statistic_image`. Consumed by `montage`, `table`, `surface`, `extract_data`. | | **[`fmridisplay`](fmridisplay_methods.md)** | Container holding figure handles for slice montages and surfaces. Built by invoking methods like `montage` or with preconfigured sets in `canlab_results_fmridisplay`; lets you swap blob layers in / out without re-rendering the anatomy underneath. | | **[`brainpathway`](brainpathway_methods.md)** | Connectivity / pathway-modeling object for one subject. The `brainpathway_multisubject` extension is documented on the same page. | diff --git a/docs/Workflows.md b/docs/Workflows.md index ddc3cef5..066a3383 100644 --- a/docs/Workflows.md +++ b/docs/Workflows.md @@ -16,6 +16,7 @@ Start with the roadmap to orient yourself, then follow the walkthrough to run it | **ROI / atlas data extraction** | Pull region-of-interest, pattern, parcel, tissue-compartment, and sphere/coordinate summaries out of brain images, then visualize them (bar plots, line plots, multi-subject slope plots). | [ROI extraction roadmap](workflows/ROI_extraction_methods_roadmap.md) | [Extract & visualize ROI data — how-to](workflows/extract_roi_data_howto.md) | | **First-level fMRI time-series modeling** (`glm_map`) | Build a single-subject event-related model from onsets (FSL tables, SPM-style cells, or an `SPM.mat`), choose a basis set, add nuisance covariates and contrasts, screen the design (VIF/cVIF, efficiency, high-pass filter), simulate data, fit (AR errors), threshold, and visualize. | [First-level roadmap](workflows/glm_map_first_level_roadmap.md) | [First-level how-to](workflows/glm_map_first_level_howto.md) | | **Second-level fMRI group analysis** (`glm_map`) | Group regression on contrast images: build the object via `fmri_data.regress` or the `glm_map` estimator API, handle outliers and WM/CSF nuisance signals (`normalize_gm_by_wm_csf`), fit OLS or robust, screen the design, threshold, and visualize. | [Second-level roadmap](workflows/glm_map_second_level_roadmap.md) | [Second-level how-to](workflows/glm_map_second_level_howto.md) | +| **Surface & grayordinate data** (`fmri_surface_data`) | Load CIFTI/GIFTI surface data and visualize it on the cortical surface; map a volumetric result onto the surface (`vol2surf`) and back to a volume (`surf2vol` / `to_fmri_data`); parcellate, threshold, and run group analyses — all natively, no external toolbox. | [`fmri_surface_data` methods](fmri_surface_data_methods.md) | [Surface data how-to](workflows/fmri_surface_data_howto.md) | *More workflows will be added here over time.* diff --git a/docs/fmri_surface_data_methods.md b/docs/fmri_surface_data_methods.md new file mode 100644 index 00000000..ac05c75f --- /dev/null +++ b/docs/fmri_surface_data_methods.md @@ -0,0 +1,154 @@ +# `fmri_surface_data` methods, organized by area + +This is a functional index of methods available on an `fmri_surface_data` +object — the CANlab object for **cortical-surface and grayordinate (HCP CIFTI) +data**. It is the surface analogue of `fmri_data`: data are stored flat as a +`[grayordinates × maps]` matrix in `.dat`, with a `brain_model` describing the +cortical-surface vertices and subcortical voxels, so the familiar CANlab method +names (`mean`, `threshold`, `apply_mask`, `surface`, `write`, …) work on surface +data and interoperate with the rest of the toolbox. + +`fmri_surface_data` is a subclass of `image_vector`, so it inherits the +purely-`.dat` methods listed in +[image_vector_methods.md](image_vector_methods.md) (`get_wh_image`, `mahal`, +`pca`, `image_math`, …). Only methods owned by `fmri_surface_data` (defined in +`@fmri_surface_data/`) are listed below. Type `methods(my_obj)` for the live list. + +**Everything runs natively in MATLAB — no gifti toolbox, FieldTrip, Connectome +Workbench, or FreeSurfer is required at runtime** (sole exception: `ica`, which +needs the GIFT/`icatb` toolbox, like the base `image_vector` method). + +- **Tutorial:** [Surface & grayordinate data how-to](workflows/fmri_surface_data_howto.md) +- **Runnable script:** `CanlabCore/docs/fmri_surface_data_walkthrough.m` +- **Design rationale / roadmap:** `CanlabCore/docs/fmri_surface_data_design_plan.md` + +## Concept + +The HCP "grayordinate" model represents the cortex as *surface vertices* and the +subcortex/cerebellum as *voxels*. Because that is already a flat +`[grayordinates × maps]` matrix, it maps directly onto CANlab's `.dat`. Two +differences from `fmri_data`: + +- **`brain_model` replaces `volInfo`** as the geometry source of truth (cortical + vertices have no voxel coordinates). The inherited `volInfo` is populated to + describe only the subcortical voxel sub-block (empty for surface-only objects). +- **No empty-squeezing:** grayordinate data is already compact, so `.dat` is + *always* the full set; `remove_empty` / `replace_empty` are no-ops. + +## Properties + +`fmri_surface_data` inherits the `image_vector` properties (`dat`, `volInfo`, +`removed_voxels`, `removed_images`, `image_names`, `fullpath`, `files_exist`, +`history`, `dat_descrip`, `source_notes`) and adds: + +| Property | Description | +|---|---| +| `brain_model` | Geometry source of truth (mirrors CIFTI BrainModels). `.models{i}`: `.struct`, `.type` (`surf`/`vox`), `.start`, `.count`, `.numvert`, `.vertlist` (0-based), `.voxlist`; plus `.vol` (`.dims`, `.sform`), `.grayordinate_type`, `.cluster`. | +| `geom` | Cortical mesh cache (faces/vertices) used for rendering. | +| `intent` | `dscalar` / `dtseries` / `dlabel` / `func` / `shape` / `label`. | +| `series_info` | For `.dtseries`: `.start`/`.step`/`.unit`/`.exponent`. | +| `label_table` | For `.dlabel`/`.label`: struct array `.key`/`.name`/`.rgba`. | +| `surface_space` | e.g. `fsLR_32k`, `fsaverage_164k`. Gatekeeps `compare_space`; drives mesh/warp choice. | +| `mask` | Optional `[nGray × 1]` logical (or same-space object) for `apply_mask`. | +| `X` / `Y` / `covariates` / `images_per_session` / `metadata_table` / … | Per-map annotations, same names/roles as `fmri_data`. | +| `additional_info` | Free-form struct; also holds `.statistic` (from `ttest`/`regress`) and the source CIFTI xml/hdr. | + +## Construction and import/export + +| Method | From | One-liner | +|---|---|---| +| `fmri_surface_data` | `@fmri_surface_data` | Constructor: from a CIFTI/GIFTI file, a reader struct, `'key',value` pairs, or an `image_vector` recast | +| `write` | `@fmri_surface_data` | Write natively to CIFTI-2 (`.nii`) or GIFTI (`.gii`); rebuilds map metadata; faithful re-emit or regenerate | +| `to_fmri_data` | `@fmri_surface_data` | Export the subcortical voxel grayordinates as a volumetric `fmri_data` (writeable to NIfTI) | + +## Geometry and space + +| Method | From | One-liner | +|---|---|---| +| `reconstruct_image` | `@fmri_surface_data` | Unpack `.dat` into dense per-hemisphere vertex arrays (medial wall NaN) + a subcortical volume | +| `compare_space` | `@fmri_surface_data` | Compare grayordinate spaces (0 same / 1 diff space / 2 missing / 3 diff in-data), preserving the contract | +| `reparse_contiguous` | `@fmri_surface_data` | Label contiguous clusters (cortex = mesh edge graph; subcortex = 26-connectivity) into `brain_model.cluster` | +| `rebuild_like` | `@fmri_surface_data` | Wrap new `[nGray × K]` data into an object carrying this geometry (used internally) | + +## Volume ↔ surface mapping + +| Method | From | One-liner | +|---|---|---| +| `vol2surf` | `@image_vector` | Project a volumetric image (MNI152) onto the fsaverage-164k surface via the CBIG RF mapping (`interpn`) | +| `surf2vol` | `@fmri_surface_data` | Project an fsaverage-164k object back to an MNI152 `fmri_data` volume (self-consistent inverse of `vol2surf`) | + +## Data operations + +| Method | From | One-liner | +|---|---|---| +| `mean` | `@fmri_surface_data` | Average across maps → single-map object | +| `apply_mask` | `@fmri_surface_data` | Zero out-of-mask grayordinates (no `fmri_mask_image`, no resample; `.dat` stays full) | +| `threshold` | `@fmri_surface_data` | Raw-value threshold; optional cluster-extent (`'k', N`) via `reparse_contiguous` | +| `cat` / `horzcat` | `@fmri_surface_data` | Concatenate objects along maps (same space required); also `[a, b, …]` | +| `remove_empty` / `replace_empty` | `@fmri_surface_data` | No-ops (grayordinate data is already compact; overrides so inherited callers compose) | + +## Analysis + +Delegated methods treat each grayordinate as a feature (reusing the `fmri_data` +algorithms) and remap geometry-bearing results back to the surface. + +| Method | From | One-liner | +|---|---|---| +| `ttest` | `@fmri_surface_data` | Grayordinate-wise one-sample t-test across maps (t-map in `.dat`; p/ste/sig in `additional_info.statistic`) | +| `regress` | `@fmri_surface_data` | Grayordinate-wise OLS onto `obj.X` (betas in `.dat`; t/p/se in `additional_info.statistic`) | +| `predict` | `@fmri_surface_data` | Cross-validated multivariate prediction (`obj.Y`); weight map remapped to a surface object | +| `ica` | `@fmri_surface_data` | Spatial ICA (requires the GIFT/`icatb` toolbox) | + +## Rendering and QC + +| Method | From | One-liner | +|---|---|---| +| `surface` | `@fmri_surface_data` | Render on the native mesh (4-panel), on `'existingsurface'` handles, or an `'mni_surface'` addbrain surface | +| `render_on_surface` | `@fmri_surface_data` | Color existing patch handles: directly if the mesh matches, else via a volume projection | +| `plot` | `@fmri_surface_data` | QC panel: value histogram, per-map mean±sd, coverage, mean-map render | + +## Parcellation and regions + +| Method | From | One-liner | +|---|---|---| +| `apply_parcellation` | `@fmri_surface_data` | Parcel means `[nMaps × nParcels]` from a `.dlabel` object or key vector (optional `'area'` weighting) | +| `surface_region` | `@fmri_surface_data` | Summarize contiguous clusters as region-like structs (`.struct`, `.XYZmm`, `.numVox`, `.val`, …) | + +## Surface data in Neuroimaging_Pattern_Masks + +CanlabCore ships one small continuous sample map +(`Sample_datasets/CIFTI_surface_examples/S1200.MyelinMap_MSMAll.32k_fs_LR.dscalar.nii`). +**Surface atlases and additional CIFTI maps live in the companion +`Neuroimaging_Pattern_Masks` (NPM) repository**, a standard CANlab dependency +(add it with `canlab_toolbox_setup`). Once on the path, load any of them by +filename, e.g. `fmri_surface_data(which('transcriptomic_gradients.dscalar.nii'))`. +Summary of what's available (representative files per family): + +| Atlas / map family | Type | Space | Notes | +|---|---|---|---| +| **Gordon-333 + Tian S1–S4** (`Gordon333.32k_fs_LR_Tian_Subcortex_S*.dlabel.nii`) | `.dlabel` | fs_LR-32k, 91k | Gordon cortical parcels + Tian subcortex (4 subcortical scales) | +| **Schaefer-400 + Tian S1–S4** (`Schaefer2018_400Parcels_{7,17}Networks_..._S*.dlabel.nii`) | `.dlabel` | fs_LR-32k, 91k | Schaefer 400 (7 or 17 networks) + Tian subcortex | +| **HCP-MMP (Glasser) + Tian S1–S4** (`Q1-Q6_...CorticalAreas..._Tian_Subcortex_S*.dlabel.nii`) | `.dlabel` | fs_LR-32k, 91k | 360 HCP-MMP areas + Tian subcortex | +| **Glasser-2016 (HCP-MMP 360)** (`Glasser_2016.32k.{L,R}.label.gii`) | `.label.gii` | fs_LR-32k | Cortex only, per hemisphere | +| **JulichBrain 3.0.3** (`{lh,rh}.JulichBrainAtlas_3.0.3[.fslr_32k].label.gii`) | `.label.gii` | fsaverage & fs_LR-32k | Cytoarchitectonic cortex, per hemisphere | +| **CANLab2024 (coarse)** (`openCANLab2024_..._coarse_2mm.dlabel.nii`, `CANLab2024_fsaverage-164k_hemi-{L,R}_coarse.label.gii`) | `.dlabel` / `.label.gii` | MNI152NLin6Asym 91k / fsaverage-164k | CANLab whole-brain atlas | +| **CIT168 amygdala nuclei** (`CIT168_AmyNuc.dlabel.nii`) | `.dlabel` | subcortical | Amygdala subnuclei | +| **Transcriptomic gradients** (`transcriptomic_gradients.dscalar.nii`) | `.dscalar` | fs_LR-32k, 91k | Gene-expression gradient maps (continuous) | +| **HCP group ICAs** (`hcp_d{15,25,50}_ICs.dscalar.nii`) | `.dscalar` | fs_LR-32k, 91k | Resting-state group ICA component maps | +| **HCP spectral bases** (`spectral_bases_200.dscalar.nii`) | `.dscalar` | fs_LR-32k, 91k | Spatial-basis functions | + +(Grayordinate parcellations = cortex surface + subcortical volume; `.label.gii` +files are cortex-only, per hemisphere. Paths are under +`Neuroimaging_Pattern_Masks/Atlases_and_parcellations/` and +`.../spatial_basis_functions/`.) + +## Native I/O functions (`CanlabCore/Surface_tools/`) + +Standalone functions backing the object; usable directly, no external toolbox. + +| Function | Purpose | +|---|---| +| `canlab_read_cifti` / `canlab_write_cifti` | Native CIFTI-2 reader / writer | +| `canlab_read_gifti` / `canlab_write_gifti` | Native GIFTI reader / writer | +| `canlab_surface_vertexcolors` | Map values → truecolor (split hot/cool; NaN/zero = gray) | +| `canlab_cbig_warp_path` | Resolve the vendored CBIG registration-fusion warp paths | diff --git a/docs/workflows/fmri_surface_atlas.png b/docs/workflows/fmri_surface_atlas.png new file mode 100644 index 0000000000000000000000000000000000000000..ba54c097024ad3062546cfef5c43e1e874f7ece0 GIT binary patch literal 266611 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