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.grip() API makes those questions visible
through the same metric choice as grip():
-
trace.grip(metric = "hop")for combinatorial layouts, -
trace.grip(metric = "edge_length")for edge-length geometry-aware layouts.
Tracing follows the same package decision rule:
- ordinary unweighted or topology-first graphs: trace
trace.grip(metric = "hop"), - weighted geometry-aware graphs: trace
trace.grip(metric = "edge_length"), - 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.grip(metric = "edge_length",
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.
Where to go next
For an API map, see Finding your way around grip. For reproducible bundles and reference scoring, see Synthetic graph families and layout examples. Both are installed vignettes; the broader gallery and interactive explorer are website-only articles.
-
Getting Started with gripgives the short overview of the main APIs. -
Weighted Graph Layouts with gripfocuses on weighted solving and scoring. -
Choosing Layouts for Real Datashows how trace fits into a real-data selection workflow.