--- title: "Build consensus and stitch across slices" name: consensus-stitch description: Understand the two independent Slice-MSF controls for ensembles and volumetric topology. output: rmarkdown::html_vignette: toc: yes toc_depth: 2 css: albers.css includes: in_header: albers-header.html params: family: red base_size: 13 content_width: 72 preset: homage resource_files: - albers.css - albers.js - albers-header.html - fonts vignette: | %\VignetteIndexEntry{Build consensus and stitch across slices} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE, fig.align = "center", fig.retina = 2, out.width = "100%", fig.width = 7, fig.asp = 0.55 ) if (requireNamespace("albersdown", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)) { ggplot2::theme_set(albersdown::theme_albers(family = params$family, preset = params$preset)) } library(neurocluster) ``` ```{r albers-classes, echo=FALSE, results='asis'} cat(sprintf( paste0(''), params$family, params$preset, params$content_width )) ``` Slice-MSF has two controls that solve different problems. `consensus = TRUE` combines multiple seeded DCT-subspace runs. `stitch_z = TRUE` adds graph edges across axial slices. One changes ensemble evidence; the other changes which spatial connections are possible. > **Experimental method.** Slice-MSF is available for explicit evaluation but > is not currently recommended automatically or for production use. Read the > [Slice-MSF evaluation contract](slice-msf.html) before interpreting this > vignette; local regression repairs are complete, but release-level > recertification is still pending. ## What does stitching change? The following example uses one run so stitching is the only changed control. Natural segmentation (`target_k_global = -1`) is used because a global exact-K target is unavailable when slices are independent. ```{r stitch-data, include=FALSE} set.seed(7) dims <- c(8L, 8L, 4L) n_time <- 32L tt <- 0:(n_time - 1L) dct_basis <- vapply( seq_len(15L), function(k) cos(pi * (tt + 0.5) * k / n_time), numeric(n_time) ) values <- array(0, c(dims, n_time)) truth_array <- array(0L, dims) for (z in seq_len(dims[3])) { for (y in seq_len(dims[2])) { for (x in seq_len(dims[1])) { region <- 1L + (x - 1L) %/% 2L + 4L * ((y - 1L) %/% 4L) truth_array[x, y, z] <- region coefficients <- rnorm(15L, sd = 0.08) coefficients[region] <- 1 coefficients[min(15L, region + 4L)] <- 0.5 values[x, y, z, ] <- as.vector(dct_basis %*% coefficients) + rnorm(n_time, sd = 0.02) } } } syn <- list( vec = neuroim2::NeuroVec( values, neuroim2::NeuroSpace(c(dims, n_time)) ), mask = neuroim2::NeuroVol( array(1, dims), neuroim2::NeuroSpace(dims) ), truth = as.integer(truth_array), dims = dims ) stopifnot(length(unique(syn$truth)) == 8L, !anyNA(syn$truth)) ``` ```{r stitch-fits} independent <- slice_msf( syn$vec, syn$mask, target_k_global = -1, r = 6, compactness = 10, min_size = 2, num_runs = 1, consensus = FALSE, stitch_z = FALSE, seed = 17 ) stitched <- slice_msf( syn$vec, syn$mask, target_k_global = -1, r = 6, compactness = 10, min_size = 2, num_runs = 1, consensus = FALSE, stitch_z = TRUE, seed = 17 ) ``` ```{r stitch-check, include=FALSE} v_independent <- validate_cluster4d(independent, syn$vec, syn$mask) v_stitched <- validate_cluster4d(stitched, syn$vec, syn$mask) stopifnot(v_independent$valid, v_stitched$valid) stopifnot(!isTRUE(independent$metadata$topology$volumetric)) stopifnot(isTRUE(stitched$metadata$topology$volumetric)) stopifnot(all(independent$labels > 0), all(stitched$labels > 0)) stopifnot(independent$actual_k > 1L, stitched$actual_k > 1L) adjacent_slice_ari <- function(fit) { labels <- array(fit$labels, syn$dims) mean(vapply(seq_len(syn$dims[3] - 1L), function(z) { clustering_accuracy( as.vector(labels[, , z]), as.vector(labels[, , z + 1L]) )$ari }, numeric(1))) } continuity <- data.frame( topology = c("independent slices", "stitched 3-D graph"), mean_adjacent_slice_ari = c( adjacent_slice_ari(independent), adjacent_slice_ari(stitched) ) ) stopifnot(all(is.finite(continuity$mean_adjacent_slice_ari))) align_to_truth <- function(labels) { ids <- sort(unique(labels)) map <- vapply(ids, function(id) { counts <- table(syn$truth[labels == id]) as.integer(names(counts)[which.max(counts)]) }, integer(1)) unname(map[match(labels, ids)]) } ``` ```{r continuity-summary, echo=FALSE} knitr::kable( transform(continuity, mean_adjacent_slice_ari = round(mean_adjacent_slice_ari, 3)), caption = "Label-invariant continuity across adjacent axial slices." ) ``` ```{r stitch-figure, echo=FALSE, fig.cap='Adjacent axial slices from independent-slice and stitched Slice-MSF fits. Each fitted subparcel is colored by its majority known region, giving the same eight-color meaning in all panels.', fig.alt='Four maps compare slices two and three under independent-slice and stitched graph topologies.'} palette_cb <- c("#0072B2", "#D55E00", "#009E73", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442", "#000000") label_array <- function(fit) array(align_to_truth(fit$labels), dim = syn$dims) a <- label_array(independent) b <- label_array(stitched) old_par <- par(mfrow = c(2, 2), mar = c(1.2, 1.2, 2.3, 0.6)) image(a[, , 2], col = palette_cb, axes = FALSE, main = "independent: z = 2"); box() image(a[, , 3], col = palette_cb, axes = FALSE, main = "independent: z = 3"); box() image(b[, , 2], col = palette_cb, axes = FALSE, main = "stitched: z = 2"); box() image(b[, , 3], col = palette_cb, axes = FALSE, main = "stitched: z = 3"); box() par(old_par) ``` **What to notice.** The stitched fit is allowed to propagate parcel identity through axial edges; the independent fit is not. The adjacent-slice ARI is label-invariant and changes from `r sprintf("%.3f", continuity$mean_adjacent_slice_ari[1])` to `r sprintf("%.3f", continuity$mean_adjacent_slice_ari[2])` on this fixture. Its direction is descriptive of this fixture, not a promise that stitching improves a scientific endpoint. ## What does consensus change? For `num_runs > 1`, Slice-MSF constructs seeded DCT subspaces and fuses the run partitions. The seed is recorded and the caller’s random-number stream is preserved. Consensus is not a promise of improved accuracy; it is a defined ensemble procedure whose stability and quality must be evaluated for the task. ```{r consensus-fit} ensemble <- slice_msf( syn$vec, syn$mask, target_k_global = 8, r = 6, compactness = 10, min_size = 2, num_runs = 3, consensus = TRUE, use_features = TRUE, ensemble_fraction = 0.6, stitch_z = TRUE, seed = 17 ) ``` ```{r consensus-check, include=FALSE} v_ensemble <- validate_cluster4d(ensemble, syn$vec, syn$mask) stopifnot(v_ensemble$valid, ensemble$actual_k == 8L) stopifnot(length(ensemble$metadata$ensemble$runs) == 3L) stopifnot(identical(ensemble$metadata$ensemble$seed, 17L)) run_signatures <- vapply( ensemble$runs, function(run) paste(run$labels, collapse = ","), character(1) ) consensus_stability <- data.frame( run = seq_along(ensemble$runs), run_clusters = vapply( ensemble$runs, function(run) length(unique(run$labels)), integer(1) ), ari_to_consensus = vapply( ensemble$runs, function(run) clustering_accuracy(run$labels, ensemble$labels)$ari, numeric(1) ) ) stopifnot(length(unique(run_signatures)) == 3L) stopifnot(all(consensus_stability$run_clusters > 1L)) stopifnot(all(is.finite(consensus_stability$ari_to_consensus))) ``` ```{r consensus-stability-table, echo=FALSE} knitr::kable( transform(consensus_stability, ari_to_consensus = round(ari_to_consensus, 3)), caption = "The three seeded runs are nondegenerate and distinct; ARI measures each run's agreement with the fused eight-parcel result." ) ``` ```{r consensus-stability-figure, echo=FALSE, fig.cap='Agreement of each distinct seeded run with the final consensus partition. Variation across runs is visible rather than hidden behind the fused result.', fig.alt='Three orange points show adjusted Rand agreement with consensus, ranging from roughly 0.68 to 0.73.'} plot(consensus_stability$run, consensus_stability$ari_to_consensus, type = "b", pch = 17, lwd = 2, col = "#E69F00", bty = "l", xaxt = "n", ylim = c(0, 1), xlab = "seeded run", ylab = "ARI to consensus") axis(1, at = consensus_stability$run) ``` The exact-K claim above is scoped to this connected mask and requested target; the hidden check makes the vignette fail if it changes. The natural K, requested K, pre/post sizes, repair direction, and effective run controls are available in `ensemble$metadata$exact_k_repair`. Consensus does not erase run-to-run variation: the table and figure expose it. For parameter tuning and comparison, continue to [Choose parameters](choose-parameters.html) and [Validate and compare clusterings](validate-compare.html).