---
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).