---
title: "Evaluate Slice-MSF (experimental)"
name: slice-msf
description: Inspect Slice-MSF's feature geometry, natural and exact-K modes, feasibility gates, and provenance before deciding whether to evaluate it.
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{Evaluate Slice-MSF (experimental)}
%\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%"
)
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
))
```
> **Status: experimental and not recommended automatically.** Slice-MSF remains
> available for explicit evaluation, but `suggest_cluster4d_params()` will not
> select it. Earlier implementations could collapse distinct feature vectors to
> zero-distance edges through reliability weighting and could produce
> singleton-dominated exact-K partitions. Those paths have been rejected or
> replaced, have regression gates, and pass local package recertification at
> this source revision. Broader real-data validation and hosted release evidence
> are still pending. Validate the method against a named estimand on your own
> data before considering downstream use.
## Which interface are you evaluating?
The direct and unified interfaces answer different cluster-count questions.
`slice_msf()` defaults to its natural Felzenszwalb-Huttenlocher (FH)
segmentation. `cluster4d(..., method = "slice_msf")` always requests exactly
`n_clusters` and runs the shared connected exact-K repair when necessary.
| Control | Direct `slice_msf()` default | Unified `cluster4d()` Slice-MSF default |
|---|---:|---:|
| Count policy | natural (`target_k_global = -1`) | exact requested `n_clusters` |
| DCT mode count | `r` = 12 | `r` = 12 |
| FH control | `compactness = 5` | `spatial_weight = 0.2`, mapped to compactness 2 |
| Minimum size | 80 | automatic from expected parcel size, between 2 and 20 |
| Base runs | 3 | 1 |
| Consensus | on | off |
| Cross-slice edges | on | on |
| Reliability weight | `gamma = 0` | `gamma = 0` |
| Axial sketch smoothing | `z_mult = 0` | `z_mult = 0` |
These differences are large enough that results from the two defaults are not
interchangeable. State the entry point and effective controls in any evaluation.
## What similarity does Slice-MSF use?
For each included voxel, Slice-MSF linearly detrends and z-scores the time
series, projects it onto non-DC DCT-II modes, applies any run-specific positive
mode weights, and normalizes the sketch to unit length. If `z_mult > 0`, each
mode is smoothed along the axial direction and the resulting voxel sketch is
renormalized. An adjacent edge then has feature dissimilarity
\[
d(i,j) = 1 - \operatorname{clamp}(u_i^\top u_j, -1, 1).
\]
A zero-information sketch remains the zero vector rather than inventing a
direction; its edge dissimilarity is therefore one to every neighbor.
The signed adjacent-pair temporal correlation returned by `slice_msf_single()`
is diagnostic only; it does not weight or mask an edge. `gamma` is retained for
API compatibility but must be zero. A positive value fails closed with condition
class `slice_msf_unsupported_gamma` rather than changing the geometry.
The unified `spatial_weight` is also easy to misread. For Slice-MSF it is not a
convex feature/spatial blend. The wrapper computes
`compactness = 10 * spatial_weight`, and the direct implementation then uses FH
scale `2 / (compactness + 1)`. Larger values therefore reduce the FH component
scale; feature distance remains the cosine dissimilarity above.
## Compare natural and requested exact K
Use one deterministic fixture and hold all controls except the count policy
fixed. The known labels here are an evaluation aid, not a claim that this
synthetic scenario represents an empirical dataset.
```{r slice-data}
syn <- generate_synthetic_volume(
scenario = "gaussian_blobs", dims = c(12, 12, 4),
n_clusters = 4, n_time = 32, noise_sd = 0.05, seed = 42
)
```
```{r natural-and-exact}
natural <- slice_msf(
syn$vec, syn$mask, target_k_global = -1,
r = 8, compactness = 4, min_size = 2,
num_runs = 1, consensus = FALSE,
stitch_z = TRUE, nbhd = 8, gamma = 0, z_mult = 0, seed = 17
)
exact <- cluster4d(
syn$vec, syn$mask, n_clusters = 4, method = "slice_msf",
spatial_weight = 0.4, min_size = 2, r = 8,
num_runs = 1, consensus = FALSE,
stitch_z = TRUE, gamma = 0, z_mult = 0, seed = 17,
verbose = FALSE
)
```
```{r summarize-fits, echo=FALSE}
summarize_fit <- function(mode, fit) {
trace <- fit$metadata$exact_k_repair
sizes <- tabulate(fit$labels)
data.frame(
mode = mode,
natural_K = trace$natural_k,
requested_K = if (is.null(trace$requested_k)) NA_integer_ else trace$requested_k,
final_K = fit$actual_k,
smallest = min(sizes),
largest = max(sizes),
repair_direction = trace$direction,
repair_ran = trace$ran,
ARI_to_fixture_truth = clustering_accuracy(fit$labels, syn$truth)$ari
)
}
fit_summary <- rbind(
summarize_fit("natural", natural),
summarize_fit("requested exact K", exact)
)
knitr::kable(
transform(fit_summary, ARI_to_fixture_truth = round(ARI_to_fixture_truth, 3)),
caption = "Count, size, repair, and fixture-specific accuracy diagnostics."
)
```
```{r fit-contract-checks, include=FALSE}
stopifnot(validate_cluster4d(natural, syn$vec, syn$mask)$valid)
stopifnot(validate_cluster4d(exact, syn$vec, syn$mask)$valid)
stopifnot(natural$metadata$exact_k_repair$natural_k == natural$actual_k)
stopifnot(is.null(natural$metadata$exact_k_repair$requested_k))
stopifnot(!natural$metadata$exact_k_repair$ran)
stopifnot(exact$metadata$exact_k_repair$requested_k == 4L)
stopifnot(exact$actual_k == 4L, min(tabulate(exact$labels)) >= 2L)
stopifnot(all(is.finite(fit_summary$ARI_to_fixture_truth)))
```
Natural mode applies no count repair. Exact mode first makes each label a
connected component, then uses adjacent Ward merges when it is over target or
deterministic feature-weighted minimum-spanning-tree bisections when it is under
target. Candidate ties are resolved in stable label and voxel order. The result
is therefore reproducible for the same data, mask, controls, and package build.
The table reports ARI alongside count and size diagnostics so a favorable score
cannot hide a structurally invalid partition. On real data there is no known
truth label; replace this column with a registered endpoint such as held-out
homogeneity, cross-session stability, or agreement with an external reference.
## How is feasibility enforced?
For exact K, each final parcel must be connected under the selected graph and
contain at least `min_size` voxels. A disconnected mask imposes a minimum K equal
to its number of connected components. For component sizes \(n_c\), the
minimum-size constraint imposes maximum K
\(\sum_c \lfloor n_c / \text{min_size} \rfloor\). A request outside those
bounds fails instead of silently relaxing K or parcel size.
```{r infeasible-example}
infeasible <- tryCatch(
cluster4d(
syn$vec, syn$mask, n_clusters = 4, method = "slice_msf",
spatial_weight = 0.4, min_size = 200, r = 8,
num_runs = 1, consensus = FALSE, seed = 17, verbose = FALSE
),
cluster4d_exact_k_infeasible = identity
)
data.frame(
reason = infeasible$reason,
requested_K = infeasible$target_k,
minimum_K = infeasible$minimum_k,
maximum_K = infeasible$maximum_k,
voxels = infeasible$n_voxels,
minimum_size = infeasible$min_cluster_size,
natural_K = infeasible$current_k
)
```
```{r infeasible-check, include=FALSE}
stopifnot(inherits(infeasible, "cluster4d_exact_k_infeasible"))
stopifnot(infeasible$reason == "minimum_cluster_size")
stopifnot(infeasible$target_k > infeasible$maximum_k)
```
The structured condition exposes `reason`, `target_k`, `minimum_k`,
`maximum_k`, `n_voxels`, `min_cluster_size`, and `current_k`. Catch that class
when a pipeline needs to record infeasibility separately from implementation
errors. A global exact-K request also requires `stitch_z = TRUE`; independent
slices do not define one connected volumetric repair graph.
## Which provenance should be retained?
Both count modes populate `metadata$exact_k_repair`. Retain at least:
- `natural_k`, `requested_k`, `direction`, and `ran`;
- `pre_sizes`, `post_sizes`, `min_cluster_size`, and `connectivity`;
- `effective_gamma`, `effective_z_mult`, `seed`, `num_runs`, and `consensus`;
- `operations` when exact-K repair ran.
The operation list can be long, so reports should summarize it rather than print
every merge or split. Preserve the full result object as the audit artifact.
For multiple runs, also retain `metadata$ensemble$runs`, which records the DCT
frequencies and weights for each seeded base partition. Consensus weights runs
uniformly; temporal-smoothness diagnostics do not alter those weights.
## What should you test before using it?
Treat the method as a candidate estimand implementation, not a speed shortcut.
Before use, define the included mask, connectivity, count policy, minimum size,
feature controls, seed, and scientific endpoint. Then verify determinism,
connectedness, cluster-size constraints, perturbation behavior, and held-out or
external validity on representative data. A successful synthetic example is a
smoke test, not release evidence.
Continue to [Build consensus and stitch across slices](consensus-stitch.html)
for the ensemble/topology controls and [Validate and compare
clusterings](validate-compare.html) for a comparison workflow.