Comparing grip with Alternative Layout Engines
Source:vignettes/articles/grip-vs-alternatives.Rmd
grip-vs-alternatives.RmdWhy this comparison article was rewritten
grip is no longer just an R implementation of the
classical GRIP algorithm. The package now contains:
- a quality-first GRIP workflow for ordinary graphs,
- weighted geometry-aware sister APIs,
- and geodesic scoring and polishing utilities.
That means there are now two different comparison questions:
- how the current
gripengine compares with other R layout engines on ordinary unweighted graphs, - how geometry-aware weighted
gripmethods behave on weighted graphs, where plain topology is not the whole problem.
This article treats those as separate tracks.
Shared scoring idea
score.layout() can score a realized layout from any
source. That remains one of the most useful parts of the package,
because it lets you compare candidate layouts even when they come from
different packages.
score_layout <- function(coords, edges, n,
clusters = NULL,
edge.crossings = "auto",
sample.size.stress = 2000L,
sample.size.nonedge = 5000L) {
score.layout(
coords = coords,
edges = edges,
n = n,
clusters = clusters,
sample.size.stress = sample.size.stress,
sample.size.nonedge = sample.size.nonedge,
stress.seed = 42L,
nonedge.seed = 42L,
edge.crossings = edge.crossings
)
}For weighted geometry-aware graphs, the package also provides a stronger metric:
Those GKK/LGKK-style tools are advanced public experimental utilities rather than the default onboarding path, and they are used in the second half of this article for the weighted comparison track.
plot_layout <- function(coords,
edges,
title,
vertex.col = "#1F3B73",
edge.col = "gray80",
cex = 0.55) {
if (ncol(coords) == 2L) {
plot(
coords[, 1], coords[, 2],
asp = 1,
pch = 16,
cex = cex,
col = vertex.col,
xlab = "",
ylab = "",
axes = FALSE,
main = title
)
if (!is.null(edges) && nrow(edges) > 0) {
segments(
coords[edges[, 1], 1], coords[edges[, 1], 2],
coords[edges[, 2], 1], coords[edges[, 2], 2],
col = edge.col
)
}
} else {
plot.layout(
coords,
edges,
projection = "ortho",
azimuth = 35,
elevation = 22,
vertex.col = vertex.col,
edge.col = edge.col,
main = title
)
}
}Track 1: external-package comparisons on unweighted graphs
For this track the comparison remains package-to-package:
igraphgraphlayoutsgrip
To keep the article lightweight, the cross-package results are precomputed and bundled as an RDS file. The reproduction script is still:
inst/scripts/precompute-vs-alternatives.R
res.path <- system.file(
"extdata", "vs_alternatives", "benchmark_results.rds",
package = "grip"
)
if (!nzchar(res.path)) {
stop(
"Precomputed benchmark results not found. ",
"Run inst/scripts/precompute-vs-alternatives.R first."
)
}
res <- readRDS(res.path)Benchmark 1: Zachary karate club
This is still a useful small-graph sanity check. The graph is small enough that single-scale methods can compete directly.
karate.display <- res$karate$scores[, c(
"method",
"sampled.stress",
"edge.length.cv",
"edge.crossings",
"sampled.nonedge.sep.ratio",
"cluster.separation"
)]
knitr::kable(
karate.display,
digits = 4,
row.names = FALSE,
caption = "Karate club: unweighted comparison metrics."
)| method | sampled.stress | edge.length.cv | edge.crossings | sampled.nonedge.sep.ratio | cluster.separation |
|---|---|---|---|---|---|
| FR (igraph) | 1.2202 | 0.3581 | 68 | 0.3670 | 2.7566 |
| KK (igraph) | 0.6136 | 0.2806 | 73 | 0.3021 | 2.2528 |
| DrL (igraph) | 0.6049 | 0.4680 | 120 | 0.2372 | 1.6584 |
| Stress (graphlayouts) | 0.5196 | 0.2611 | 78 | 0.2747 | 2.2240 |
| grip default | 14.5495 | 0.2915 | 75 | 0.2769 | 2.1344 |
| grip tuned | 14.4619 | 0.2933 | 86 | 0.2842 | 2.0532 |
k <- res$karate
par(mfrow = c(2, 3), mar = c(1, 1, 3, 1), bg = "white")
plot_layout(k$layouts$fr, k$edges, "FR (igraph)", cex = 0.6)
plot_layout(k$layouts$kk, k$edges, "KK (igraph)", cex = 0.6)
plot_layout(k$layouts$drl, k$edges, "DrL (igraph)", cex = 0.6)
plot_layout(k$layouts$stress, k$edges, "Stress (graphlayouts)", cex = 0.6)
plot_layout(k$layouts$grip.default, k$edges, "grip default", cex = 0.6)
plot_layout(k$layouts$grip.tuned, k$edges, "grip tuned", cex = 0.6)
On graphs this small, grip does not have a special
structural advantage. That is an honest result and a useful reminder:
multiscale methods are not automatically best on every graph.
Benchmark 2: 12x12 mesh
Meshes are a good structured topological benchmark because a strong layout should recover an orderly grid with uniform edge lengths and few or no crossings.
mesh.display <- res$mesh$scores[, c(
"method",
"sampled.stress",
"edge.length.cv",
"edge.crossings",
"sampled.nonedge.sep.ratio"
)]
knitr::kable(
mesh.display,
digits = 4,
row.names = FALSE,
caption = "12x12 mesh: unweighted comparison metrics."
)| method | sampled.stress | edge.length.cv | edge.crossings | sampled.nonedge.sep.ratio |
|---|---|---|---|---|
| FR (igraph) | 2.7864 | 0.1735 | 0 | 0.9731 |
| KK (igraph) | 0.4856 | 0.0135 | 0 | 1.3356 |
| DrL (igraph) | 6.4914 | 0.3580 | 0 | 0.5613 |
| Stress (graphlayouts) | 0.8911 | 0.0138 | 0 | 1.3367 |
| grip default | 23.6269 | 0.0491 | 0 | 1.1585 |
| grip mesh preset | 20.2976 | 0.0495 | 0 | 1.2130 |
m <- res$mesh
par(mfrow = c(2, 3), mar = c(1, 1, 3, 1), bg = "white")
plot_layout(m$layouts$fr, m$edges, "FR (igraph)", cex = 0.5)
plot_layout(m$layouts$kk, m$edges, "KK (igraph)", cex = 0.5)
plot_layout(m$layouts$drl, m$edges, "DrL (igraph)", cex = 0.5)
plot_layout(m$layouts$stress, m$edges, "Stress (graphlayouts)", cex = 0.5)
plot_layout(m$layouts$grip.default, m$edges, "grip default", cex = 0.5)
plot_layout(m$layouts$grip.mesh, m$edges, "grip mesh preset", cex = 0.5)
This benchmark shows a different pattern from the karate graph: on a
canonical structured family like a mesh, the current grip
default is already robust and produces a clean uncrossed layout. The
mesh preset still helps a bit, but it now acts as a modest
family-specific refinement rather than a rescue from a bad default. At
the same time, stress and KK remain very strong on a regular
lattice.
What the unweighted track tells us
The external-package benchmark is still useful, but its scope is now clear:
- it compares engines on ordinary unweighted graphs,
- it is a fair way to evaluate layout quality with a shared scoring function,
- but it does not answer the geometry-aware weighted-layout question.
That second question needs a different benchmark track.
Track 2: geometry-aware comparisons on weighted graphs
For weighted graphs with meaningful edge lengths, a pure
topology-only comparison is no longer enough. The weighted track in
grip is built around:
weighted.grip()- optional experimental LGKK-based refinement
- and full geodesic-KK scoring.
The small example below is built and scored live in the vignette. It uses a plain mesh topology whose edge lengths are induced by a curved saddle surface.
weighted.mesh <- mesh.surface.graph(
5, 5,
surface = "saddle",
amplitude = 0.8
)
coords.combinatorial <- grip(
weighted.mesh$edges,
n = weighted.mesh$n,
dim = 3,
preset = "mesh",
seed = 1
)
coords.weighted <- weighted.grip(
weighted.mesh$edges,
n = weighted.mesh$n,
edge_weights = weighted.mesh$edge_weights,
dim = 3,
preset = "mesh",
seed = 1
)
coords.weighted.lgkk <- weighted.grip(
weighted.mesh$edges,
n = weighted.mesh$n,
edge_weights = weighted.mesh$edge_weights,
dim = 3,
preset = "mesh",
lgkk_polish_rounds = 6L,
seed = 1
)
gkk.prepared <- prepare.geodesic.kk(
weighted.mesh$edges,
n = weighted.mesh$n,
edge_weights = weighted.mesh$edge_weights
)
weighted.summary <- do.call(
rbind,
list(
cbind(
method = "Combinatorial GRIP",
score.geodesic.kk(
coords.combinatorial,
prepared = gkk.prepared
)[, c(
"gkk.weighted.rel.rmse",
"gkk.weighted.rmse",
"gkk.mean.rel.path.error"
)]
),
cbind(
method = "Weighted GRIP",
score.geodesic.kk(
coords.weighted,
prepared = gkk.prepared
)[, c(
"gkk.weighted.rel.rmse",
"gkk.weighted.rmse",
"gkk.mean.rel.path.error"
)]
),
cbind(
method = "Weighted GRIP + LGKK polish",
score.geodesic.kk(
coords.weighted.lgkk,
prepared = gkk.prepared
)[, c(
"gkk.weighted.rel.rmse",
"gkk.weighted.rmse",
"gkk.mean.rel.path.error"
)]
)
)
)
knitr::kable(
weighted.summary,
digits = 4,
row.names = FALSE,
caption = "Weighted mesh surface: geodesic-KK comparison metrics."
)| method | gkk.weighted.rel.rmse | gkk.weighted.rmse | gkk.mean.rel.path.error |
|---|---|---|---|
| Combinatorial GRIP | 0.1938 | 7.1503 | 0.0978 |
| Weighted GRIP | 0.1034 | 3.7756 | 0.0538 |
| Weighted GRIP + LGKK polish | 0.0030 | 0.1066 | 0.0012 |
par(mfrow = c(2, 2), mar = c(1, 1, 3, 1), bg = "white")
plot_layout(weighted.mesh$coords_surface, weighted.mesh$edges, "Target geometry")
plot_layout(coords.combinatorial, weighted.mesh$edges, "Combinatorial GRIP")
plot_layout(coords.weighted, weighted.mesh$edges, "Weighted GRIP")
plot_layout(coords.weighted.lgkk, weighted.mesh$edges, "Weighted GRIP + LGKK polish")
This weighted section is not a package-versus-package benchmark in
the old sense. Instead, it answers the more relevant question for modern
grip:
- what happens when the graph metric matters?
On this example the progression is exactly what the current package design aims for:
- combinatorial GRIP gives a topology-respecting layout,
- weighted GRIP improves geodesic fidelity,
- weighted GRIP plus experimental LGKK polish improves it further.
So what should you compare with what?
For ordinary unweighted graphs:
- compare
gripwithigraphandgraphlayouts, - use
score.layout()to evaluate all of them on the same graph, - and expect the answer to depend on graph family and graph size.
For weighted geometric graphs:
- start with
weighted.grip(), - use 3D as the primary layout space when the geometry demands it,
- and evaluate candidate layouts with geodesic-aware criteria such as
score.geodesic.kk().
That is the main conceptual change in the package since the original version of this article was written.
Reproducing the external benchmark bundle
To regenerate the precomputed external-package results:
source(system.file("scripts", "precompute-vs-alternatives.R", package = "grip"))The weighted section of this article is generated directly inside the vignette, so it always reflects the current package implementation.
Session info for the bundled external results
res$session_info
#> R version 4.5.2 (2025-10-31)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Tahoe 26.3.1
#>
#> Matrix products: default
#> BLAS: /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
#>
#> locale:
#> [1] C.UTF-8/C.UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8
#>
#> time zone: America/New_York
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] graphlayouts_1.2.3 igraph_2.2.2 grip_0.1.0 testthat_3.3.2
#>
#> loaded via a namespace (and not attached):
#> [1] desc_1.4.3 R6_2.6.1 magrittr_2.0.4 pkgconfig_2.0.3
#> [5] lifecycle_1.0.5 cli_3.6.5 pkgload_1.5.0 compiler_4.5.2
#> [9] rprojroot_2.1.1 tools_4.5.2 pkgbuild_1.4.8 brio_1.1.5
#> [13] Rcpp_1.1.1 otel_0.2.0 rlang_1.1.7