Tracing and Diagnosing Layouts
Source:vignettes/grip-trace-and-diagnostics.Rmd
grip-trace-and-diagnostics.RmdWhy trace a layout?
Most graph-layout functions return only the final coordinate matrix. That is enough for plotting, but it hides the most useful diagnostic information:
- how the multiscale hierarchy opened,
- whether the geometry stabilized gradually or abruptly,
- whether a parameter setting improved edge-length regularity or only moved the final picture around,
- and whether a weighted solve is actually getting closer to the intended metric geometry.
The trace APIs in grip make those questions visible:
-
trace.grip()for combinatorial layouts, -
trace.weighted.grip()for weighted geometry-aware layouts.
Tracing follows the same package decision rule:
- ordinary unweighted or topology-first graphs: trace
trace.grip(), - weighted geometry-aware graphs: trace
trace.weighted.grip(), - real-data layout search: add trace only when a shortlist or one promising solve needs explanation,
- advanced GKK/LGKK hooks can appear in weighted traces, but they remain public experimental add-ons rather than the default reason to trace.
plot.trace.frame <- function(coords, edges, main = "", vertex.col = "black") {
active <- stats::complete.cases(coords[, 1:2, drop = FALSE])
xy <- coords[active, 1:2, drop = FALSE]
xlim <- range(xy[, 1])
ylim <- range(xy[, 2])
xpad <- 0.08 * diff(xlim)
ypad <- 0.08 * diff(ylim)
if (!is.finite(xpad) || xpad == 0) xpad <- 0.2
if (!is.finite(ypad) || ypad == 0) ypad <- 0.2
plot(
xy[, 1], xy[, 2],
type = "n",
asp = 1,
axes = FALSE,
xlab = "",
ylab = "",
xlim = xlim + c(-xpad, xpad),
ylim = ylim + c(-ypad, ypad),
main = main
)
active.edges <- edges[active[edges[, 1]] & active[edges[, 2]], , drop = FALSE]
if (nrow(active.edges) > 0) {
apply(active.edges, 1, function(e) {
graphics::segments(
coords[e[1], 1], coords[e[1], 2],
coords[e[2], 1], coords[e[2], 2],
col = "gray82"
)
})
}
points(xy[, 1], xy[, 2], pch = 16, cex = 0.55, col = vertex.col)
}
pick.trace.frames <- function(trace.obj) {
n <- length(trace.obj$frames)
unique(c(1L, max(2L, floor((n + 1L) / 2L)), n))
}A first combinatorial trace
The example below traces a 2D layout of a small mesh. Using
trace = "level" keeps the vignette readable by recording
the coarse initialization, the start of each new level, and the final
layout.
mesh.edges <- edges.mesh(5, 5)
mesh.trace <- trace.grip(
mesh.edges,
n = 25,
dim = 2,
preset = "mesh",
rounds = 12,
final_rounds = 16,
trace = "level",
diagnostics = "light",
seed = 1
)
knitr::kable(mesh.trace$meta)| frame | phase | level_index | misf_level | round_in_level | active_vertices |
|---|---|---|---|---|---|
| 1 | init | 1 | 1 | 0 | 12 |
| 2 | level_start | 2 | 0 | 0 | 25 |
| 3 | final | 2 | 0 | 16 | 25 |
knitr::kable(mesh.trace$diagnostics[, c(
"frame",
"edge.length.cv",
"median.edge.length",
"sampled.nonedge.sep.ratio"
)], digits = 3)| frame | edge.length.cv | median.edge.length | sampled.nonedge.sep.ratio |
|---|---|---|---|
| 1 | 0.380 | 234.009 | 0.109 |
| 2 | 0.346 | 35.365 | 0.172 |
| 3 | 0.073 | 27.020 | 0.969 |
sel <- pick.trace.frames(mesh.trace)
op <- par(mfrow = c(1, length(sel)), mar = c(1.2, 1.2, 3, 1.2), bg = "white")
on.exit(par(op), add = TRUE)
for (idx in sel) {
plot.trace.frame(
mesh.trace$frames[[idx]],
mesh.edges,
main = paste(mesh.trace$meta$phase[[idx]], "(frame", idx, ")")
)
}


The two most useful tables are:
-
meta, which explains where each frame came from, -
diagnostics, which tracks simple quality signals frame by frame.
Level traces versus round traces
When you need more detail, trace = "round" records
intermediate refinement rounds within each active level.
mesh.trace.round <- trace.grip(
mesh.edges,
n = 25,
dim = 2,
preset = "mesh",
rounds = 6,
final_rounds = 8,
trace = "round",
trace.every = 4,
diagnostics = "light",
seed = 1
)
data.frame(
trace_mode = c("level", "round"),
n.frames = c(length(mesh.trace$frames), length(mesh.trace.round$frames))
)
#> trace_mode n.frames
#> 1 level 3
#> 2 round 6Use:
-
trace = "level"for most reporting and teaching, -
trace = "round"when you are diagnosing a tuning issue.
Weighted traces
Weighted traces answer a slightly different question. Instead of only asking whether the layout becomes visually cleaner, we can also ask whether it moves toward a target geometry.
Here the graph is a mesh whose edge lengths come from a curved 3D saddle surface.
weighted.mesh <- mesh.surface.graph(
4, 4,
surface = "saddle",
amplitude = 0.8
)
weighted.trace <- trace.weighted.grip(
weighted.mesh$edges,
n = weighted.mesh$n,
edge_weights = weighted.mesh$edge_weights,
dim = 3,
preset = "mesh",
rounds = 8,
final_rounds = 12,
trace = "level",
diagnostics = "light",
target_coords = weighted.mesh$coords_surface,
seed = 2
)
knitr::kable(weighted.trace$meta)| frame | phase | level_index | misf_level | round_in_level | active_vertices |
|---|---|---|---|---|---|
| 1 | init | 1 | 1 | 0 | 12 |
| 2 | level_start | 2 | 0 | 0 | 16 |
| 3 | final | 2 | 0 | 12 | 16 |
knitr::kable(weighted.trace$diagnostics[, c(
"frame",
"edge.length.cv",
"sampled.nonedge.sep.ratio",
"procrustes.rmse"
)], digits = 3)| frame | edge.length.cv | sampled.nonedge.sep.ratio | procrustes.rmse |
|---|---|---|---|
| 1 | 0.381 | 0.154 | 0.887 |
| 2 | 0.303 | 0.411 | 0.430 |
| 3 | 0.103 | 1.088 | 0.360 |
sel.w <- pick.trace.frames(weighted.trace)
op <- par(mfrow = c(1, length(sel.w)), mar = c(1.2, 1.2, 3, 1.2), bg = "white")
on.exit(par(op), add = TRUE)
for (idx in sel.w) {
frame.xy <- project.3d(weighted.trace$frames[[idx]], azimuth = 35, elevation = 20)
plot.trace.frame(
frame.xy,
weighted.mesh$edges,
main = paste(weighted.trace$meta$phase[[idx]], "(frame", idx, ")"),
vertex.col = "#1F3B73"
)
}


The important extra column is procrustes.rmse. When a
meaningful target geometry exists, it gives a direct per-frame measure
of whether the layout is getting closer to that target up to rigid
motion and scale.
Trace objects are analysis objects
A trace object is more than a list of pictures. It can support:
- frame-by-frame diagnostics,
- manual inspection of where a solve destabilizes,
- teaching demonstrations of the multiscale hierarchy,
- and regression checks for future algorithm changes.
The most useful components are:
-
final: the final coordinate matrix, -
frames: the stored coordinate frames, -
meta: frame provenance, -
diagnostics: per-frame metrics, -
lgkk.polish: optional post-layout LGKK result when enabled as an advanced experimental refinement step.
Practical guidance
- Start with
trace = "level"anddiagnostics = "light". - Use
trace = "round"only when you need finer-grained diagnostics. - Supply
target_coordswhen a meaningful geometric target exists. - Prefer weighted traces for weighted graph families; they are much more interpretable than tracing the combinatorial solver on a graph with nontrivial edge lengths.