--- title: "Read, validate, and save clustering results" name: objects-results description: Interpret the cluster4d_result contract and preserve its provenance. 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{Read, validate, and save clustering results} %\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.62 ) 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 )) ``` A clustering is useful only if its labels can be tied back to the mask, feature space, and physical geometry that produced them. `cluster4d_result` keeps those pieces together. Use its public fields and validate the object against the original inputs before interpretation or export. ## What does a valid result look like? ```{r result-data, include=FALSE} syn <- generate_synthetic_volume( scenario = "gaussian_blobs", dims = c(12, 12, 4), n_clusters = 4, n_time = 20, seed = 308 ) ``` ```{r result-fit} fit <- cluster4d( syn$vec, syn$mask, n_clusters = 4, method = "slic", preserve_k = TRUE, max_iterations = 6, parallel = FALSE ) receipt <- validate_cluster4d(fit, syn$vec, syn$mask) receipt$summary ``` ```{r result-check, include=FALSE} stopifnot(receipt$valid, length(receipt$errors) == 0L) stopifnot(identical(fit$labels, fit$cluster)) stopifnot(identical(fit$label_ids, seq_len(fit$actual_k))) stopifnot(nrow(fit$centers) == fit$actual_k) stopifnot(nrow(fit$coord_centers) == fit$actual_k) stopifnot(all(is.finite(fit$centers)), all(is.finite(fit$coord_centers))) count_components <- function(label_array, label) { offsets <- as.matrix(expand.grid(-1L:1L, -1L:1L, -1L:1L)) offsets <- offsets[rowSums(abs(offsets)) > 0L, , drop = FALSE] target <- label_array == label seen <- array(FALSE, dim(label_array)) components <- 0L while (any(target & !seen)) { components <- components + 1L seed <- which(target & !seen, arr.ind = TRUE)[1L, ] queue <- matrix(seed, nrow = 1L) while (nrow(queue)) { current <- queue[1L, ] queue <- queue[-1L, , drop = FALSE] if (seen[current[1L], current[2L], current[3L]]) next seen[current[1L], current[2L], current[3L]] <- TRUE candidates <- sweep(offsets, 2L, current, `+`) inside <- rowSums(candidates < 1L) == 0L & candidates[, 1L] <= dim(label_array)[1L] & candidates[, 2L] <= dim(label_array)[2L] & candidates[, 3L] <= dim(label_array)[3L] candidates <- candidates[inside, , drop = FALSE] if (nrow(candidates)) { keep <- target[candidates] & !seen[candidates] if (any(keep)) queue <- rbind(queue, candidates[keep, , drop = FALSE]) } } } components } label_array <- as.array(methods::as(fit$clusvol, "DenseNeuroVol")) connectivity_diagnostic <- data.frame( label = fit$label_ids, voxels = tabulate(fit$labels, nbins = fit$actual_k), components_26 = vapply( fit$label_ids, function(label) count_components(label_array, label), integer(1) ) ) stopifnot(all(connectivity_diagnostic$components_26 == 1L)) ``` The validation call independently checks the label schema, centers, physical centroids, mask ordering, and geometry. Calling it without `vec` and `mask` cannot certify all of those relationships. ## How are labels related to centers? | Field | Meaning | |---|---| | `labels` | Contiguous positive IDs in included-mask voxel order | | `cluster` | An identical compatibility alias of `labels` | | `label_ids` | The label represented by each row of both center matrices | | `centers` | Means in the original feature space; rows are labels | | `coord_centers` | Mean physical coordinates in millimetres; rows are labels | | `actual_k` | Number of final labels; may differ from a requested target when topology makes the target infeasible | | `clusvol` | A `neuroim2::ClusteredNeuroVol` carrying labels and mask geometry | Never index a center matrix by an undocumented label ordering. The explicit `label_ids` mapping is the contract. ## What should you notice in a slice? ```{r result-plot, echo=FALSE, fig.cap='One axial slice of the validated SLIC fit. Colours identify labels, not an ordering or effect magnitude. A slice is a visual screen, not proof of 3-D connectivity.', fig.alt='Axial grid divided into four colored regions; connectivity through other slices cannot be inferred from this panel alone.'} okabe_ito <- c("#0072B2", "#D55E00", "#009E73", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442", "#000000") plot(fit, view = "axial", colors = okabe_ito) ``` **What to notice.** The figure screens label coverage and gross spatial shape, but it cannot establish that each label has one connected component in 3-D. Colour numbers are arbitrary; compare partitions with defined metrics, not by matching hues. ```{r connectivity-diagnostic, echo=FALSE} knitr::kable( connectivity_diagnostic, caption = "Observed 26-neighbor component count for every final label." ) ``` The fitted result has one 26-neighbor component per label on this fixture. That statement comes from an explicit whole-volume traversal and hidden assertion, not from the middle slice. Use the connectivity definition declared by the method when reproducing this diagnostic. ## What belongs in a saved receipt? Save the fitted object together with the original mask identity and analysis metadata. At minimum retain `method`, `parameters`, `actual_k`, `provenance`, and the result of `validate_cluster4d()`. For comparison and export workflows, continue to [Validate and compare clusterings](validate-compare.html) and [Visualize and export results](visualize-export.html).