Synthetic graph families and layout examples
Source:vignettes/synthetic-graph-families.Rmd
synthetic-graph-families.RmdChoose a synthetic family to make a layout question reproducible. A tree isolates branching and edge-length effects; a masked grid adds holes and bottlenecks; a surface bundle supplies reference coordinates. These examples use small graphs to demonstrate the workflow, not to rank layout methods. For the complete function catalog and score definitions, see Finding your way around grip.
Choose the kind of input you need
| Need | Public entry points | Output |
|---|---|---|
| Topology alone |
edges.path(), edges.mesh(),
edges.kary.tree(), the other primitive
edges.*() generators |
Two-column edge matrix; no bundled reference coordinates or lengths. |
| A finite perforated grid |
keep.periodic.holes(),
keep.asymmetric.notches(),
keep.slit.channels(),
keep.staggered.windows(), then
occupied.mesh.surface.graph()
|
A keep matrix followed by a complete weighted graph bundle. |
| A recursive pattern | Square mask.*() helpers, cubic
mask.cube.*() arrays, or triangle/tetrahedron masks, then
the matching recursive.*.surface.graph()
|
A mask followed by a recursively generated weighted bundle. |
| A regular reference surface |
mesh.surface.graph(),
cylinder.surface.graph(),
sphere.surface.graph(),
torus.surface.graph()
|
Edges, lengths, 3D reference coordinates, and parameters. |
| Irregular surfaces or volumes | The irregular.*.surface.graph() and
irregular.*.solid.graph() families |
Deterministic geometric constructions; “irregular” does not imply random sampling. |
| Triangular-lattice or polyhedral topology | The three triangulated.*.surface.graph() families |
Graph edges and reference coordinates; the public bundles do not expose triangular faces. |
| Randomly sampled rectangle |
sampled.rectangle.surface.graph() or plural
sampled.rectangle.surface.graphs()
|
One graph or a sequence on a shared sampled point set. |
| Intrinsic lengths without an embedding | kary.tree.weighted.graph() |
Tree edges, lengths, depths, parents, and edge metadata. |
The website
gallery illustrates more families. Use complete public bundles;
standalone embeddings and specialized edge builders used inside them are
internal functions. A name ending in surface.graph is not a
promise of a triangle mesh: occupied-cube families, for example, connect
occupied cells and can include interior vertices.
Read a bundle before using it
library(grip)
mesh <- mesh.surface.graph(3, 4, surface = "saddle", normalize = "none")
names(mesh)
#> [1] "edges" "n" "edge_weights" "coords_surface"
#> [5] "coords_param" "weight_scale" "family" "surface"
#> [9] "connectivity" "normalize" "label"
c(vertices = mesh$n, edges = nrow(mesh$edges),
reference_dimensions = ncol(mesh$coords_surface))
#> vertices edges reference_dimensions
#> 12 17 3
head(mesh$edges)
#> [,1] [,2]
#> [1,] 1 5
#> [2,] 1 2
#> [3,] 2 6
#> [4,] 2 3
#> [5,] 3 7
#> [6,] 3 4| Field | Meaning and ordering |
|---|---|
edges |
Integer matrix with two columns, one undirected edge per row; endpoints are 1-based vertex indices. |
n |
Total number of vertices. Use this instead of
max(edges) so isolated vertices are retained. |
edge_weights |
Positive lengths parallel to edge rows. These are traversal lengths, not strengths. |
coords_surface |
When supplied, an n by 3 reference-coordinate matrix.
Row i is vertex i, in the same order as the
edge endpoints. Tree bundles omit it. |
coords_param |
When supplied, coordinates before the geometric deformation. Often
n by 2, but polyhedra and volumetric families can use three
columns. These are not automatically layout targets. |
weight_scale |
Divisor used to normalize raw lengths: supplied length equals raw
length divided by weight_scale. The reference coordinates
remain in their original units. |
normalize, family, label, and
construction controls |
Record the selected normalization and family-specific settings. Exact fields differ by constructor. |
Most geometric families induce lengths from the Euclidean distances
between reference endpoints. kary.tree.weighted.graph()
assigns lengths intrinsically. Sampled rectangles also return
raw_edge_weights (3D endpoint distances) and
iknn_witness_edge_weights (intersection-neighbor
construction diagnostics). Even with graph_space = "param",
the final sampled-rectangle lengths use 3D endpoint distances;
graph_space changes graph construction.
Do not assume a shared schema beyond the documented constructor
fields. The plural sampled-rectangle function returns a container with
$graphs, $k, $k_statistics, and
shared coordinate matrices; pass one member of $graphs to
the layout workflow. It sorts and deduplicates the supplied neighborhood
sizes. No public bundle used here supplies faces or
triangles. score.surface() needs explicit
triangle connectivity supplied separately; it cannot infer a surface
from these edge lists.
Ordering is part of reproducibility. The rectangular mesh enumerates each row from left to right. Mask matrices use display orientation (top row first, left column first); occupied-cell coordinates and graph indices share that order. Sphere bundles put the north pole first, then latitude rings from north to south, then the south pole. Trees put the root first and enumerate children by parent and child slot. For other families, retain the constructor’s returned order rather than reconstructing it from a drawing. Reordering rows requires remapping edges and every reference and metadata field together.
Example 1: a tree with intrinsic edge lengths
Use a primitive path when you need only topology:
path.edges <- edges.path(7)
path.coords <- grip(path.edges, n = 7, dim = 2, preset = "tree", seed = 21)
stopifnot(identical(dim(path.edges), c(6L, 2L)))A complete weighted tree separates depth-dependent tapering from child-slot asymmetry. Here a binary tree has three levels below the root (15 vertices). It has no generating reference coordinates; a graph-distance diagnostic is therefore the appropriate first score.
tree <- kary.tree.weighted.graph(
k = 2, depth = 3, depth_decay = 0.8,
branch_spread = 0.25, normalize = "none"
)
knitr::kable(head(tree$edge_table), digits = 3)| parent | child | parent_depth | child_depth | branch_index | raw_weight | edge_weight |
|---|---|---|---|---|---|---|
| 1 | 2 | 0 | 1 | 1 | 0.875 | 0.875 |
| 1 | 3 | 0 | 1 | 2 | 1.125 | 1.125 |
| 2 | 4 | 1 | 2 | 1 | 0.700 | 0.700 |
| 2 | 5 | 1 | 2 | 2 | 0.900 | 0.900 |
| 3 | 6 | 1 | 2 | 1 | 0.700 | 0.700 |
| 3 | 7 | 1 | 2 | 2 | 0.900 | 0.900 |
tree.coords <- grip(tree$edges, n = tree$n,
edge_weights = tree$edge_weights,
metric = "edge_length", length_normalization = "none",
dim = 2, preset = "tree", seed = 21)
plot.layout(tree.coords, edges = tree$edges,
vertex.col = tree$vertex_depth + 1L, pch = 16,
main = "Intrinsic lengths on a binary tree")
tree.score <- score.layout(tree.coords, edges = tree$edges, n = tree$n,
edge_weights = tree$edge_weights,
sample.size.stress = 150, stress.seed = 21,
edge.crossings = "never")
knitr::kable(tree.score[, c("sampled.stress", "edge.length.cv")], digits = 3)| sampled.stress | edge.length.cv |
|---|---|
| 0.346 | 0.522 |
parent has one entry per vertex and uses 0 for the
root’s missing parent; vertex_depth starts at 0.
edge_table follows the edge rows and records parent, child,
both depths, child slot, raw length, and normalized length. Unequal
drawn edges are intentional here, so a low edge-length coefficient of
variation is not the goal. Weighted sampled stress evaluates chords
against tree-path distances after fitting scale. Branching tree metrics
cannot in general be preserved exactly as Euclidean separations in two
dimensions.
Example 2: a recursive graph with holes
A keep mask describes which cells survive each subdivision. The
border of a 3 by 3 mask removes the center at each step. At level 2
there are 64 retained cells. This example uses a flat lift
(amplitude = 0) and omits lengths when fitting and scoring,
so both operations use hop counts.
keep <- mask.border(3)
keep
#> [,1] [,2] [,3]
#> [1,] TRUE TRUE TRUE
#> [2,] TRUE FALSE TRUE
#> [3,] TRUE TRUE TRUE
carpet <- recursive.mask.grid.surface.graph(
mask = keep, level = 2, surface = "saddle", amplitude = 0
)
stopifnot(carpet$n == 64L, nrow(carpet$coords_param) == carpet$n)
carpet.coords <- grip(carpet$edges, n = carpet$n,
metric = "hop", dim = 2, preset = "carpet", seed = 22)
op <- par(mfrow = c(1, 2), mar = c(3, 3, 2, 1))
plot.layout(carpet$coords_param, edges = carpet$edges, pch = 16, cex = 0.5,
main = "Generating grid")
plot.layout(carpet.coords, edges = carpet$edges, pch = 16, cex = 0.5,
main = "Hop-metric drawing")
par(op)
carpet.score <- score.layout(carpet.coords, edges = carpet$edges, n = carpet$n,
sample.size.stress = 300, stress.seed = 22,
edge.crossings = "never")
carpet.score$sampled.stress
#> [1] 0.1061113The layout preserves the graph’s vertex and edge identities, but it
need not reproduce the original square arrangement. Holes and narrow
connections make this a useful example for tracing. A different keep
pattern can disconnect a graph or isolate cells; grip()
packs components by default. Such packing is not evidence of distances
between disconnected components.
Square and cubic recursive grids require level >= 1.
Triangle and tetrahedron recursions and triangulated polyhedra also
allow level 0. Triangle masks have four named child slots; tetrahedron
masks have four corner slots. A cubic keep array is not interchangeable
with either. Increasing depth multiplies the retained cells, so inspect
n before launching a layout or all-pairs score.
Example 3: graph fidelity versus reference recovery
An octahedron subdivided once gives 18 vertices and a
three-dimensional reference configuration. The two fits below keep
topology, vertex order, supplied lengths, seed, dimension, preset, and
iteration controls fixed. Both receive the same unnormalized
lengths. Hop mode uses those lengths for adjacent-edge forces;
edge-length mode additionally uses them for graph neighborhoods and
hierarchy construction. Its solver normalization is set to
"none" to retain the same input length scale.
surface <- triangulated.polyhedron.surface.graph(
base = "octahedron", level = 1, surface = "inflated", normalize = "none"
)
common <- list(edges = surface$edges, n = surface$n,
edge_weights = surface$edge_weights, dim = 3,
preset = "mesh", rounds = 40, final_rounds = 60, seed = 23)
fits <- list(
hop = do.call(grip, c(common, list(metric = "hop"))),
edge_length = do.call(grip, c(common, list(
metric = "edge_length", length_normalization = "none")))
)
stopifnot(surface$n == 18L,
all(vapply(fits, function(x) all(is.finite(x)), logical(1))))
reference.scores <- lapply(fits, score.coordinates,
reference = surface$coords_surface,
alignment = "similarity")
summary <- do.call(rbind, lapply(names(fits), function(method) {
graph.score <- score.layout(
fits[[method]], edges = surface$edges, n = surface$n,
edge_weights = surface$edge_weights, sample.size.stress = 200,
stress.seed = 23, edge.crossings = "never"
)
data.frame(method = method,
weighted_chord_stress = graph.score$sampled.stress,
reference_relative_rmse = reference.scores[[method]]$relative_rmse,
alignment_scale = reference.scores[[method]]$scale)
}))
knitr::kable(summary, digits = 3)| method | weighted_chord_stress | reference_relative_rmse | alignment_scale |
|---|---|---|---|
| hop | 0.124 | 0.01 | 0.039 |
| edge_length | 0.124 | 0.01 | 0.039 |
Both drawings are scored against the same weighted graph
distances, with the same pair sampling settings. The coordinate
score separately uses the same reference and vertex correspondence,
fitting translation, an orthogonal transformation (including
reflection), and uniform scale. Relative coordinate RMSE is root mean
squared vertex displacement divided by the reference’s RMS radius.
Similarity alignment removes overall size differences, so it does not
establish recovery of absolute length scale. Use
alignment = "rigid" when scale must count as error.
op <- par(mfrow = c(1, 3), mar = c(1, 1, 2, 1))
plot.layout(surface$coords_surface, edges = surface$edges,
projection = "ortho", main = "Reference")
for (method in names(fits)) {
plot.layout(reference.scores[[method]]$coords, edges = surface$edges,
projection = "ortho",
main = if (method == "hop") "Hop metric" else "Edge-length metric",
vertex.col = "#215A87")
}
par(op)These are static orthographic views of 3D coordinates, not new 2D fits. Scores above use all three columns. The generating coordinates provide one reference: graph symmetries, bending, missing constraints, and the choice of objective can permit other drawings. Matching graph-path distances, matching endpoint separations, and recovering this reference are distinct goals. Here the rounded scores agree closely, showing that the two metric choices can give similar results on a symmetric graph. One graph and one seed do not establish general superiority of either mode.
Reproducibility and practical limits
Primitive generators and the regular, recursive, and irregular
families are deterministic for fixed construction arguments. The
sampled-rectangle family uses random sampling: a supplied integer
seed fixes that sample and restores the caller’s R
random-number state; seed = NULL uses and advances the
current stream. A plural call samples once for all requested
neighborhood sizes. Graph sampling and layout fitting have separate
seeds.
sample.one <- sampled.rectangle.surface.graph(n = 20, k = 4, seed = 24)
sample.two <- sampled.rectangle.surface.graph(n = 20, k = 4, seed = 24)
stopifnot(identical(sample.one$edges, sample.two$edges),
identical(sample.one$coords_surface, sample.two$coords_surface))
c(vertices = sample.one$n, edges = nrow(sample.one$edges))
#> vertices edges
#> 20 54For grip(), pass an explicit seed (the
default is 6); NULL uses the current time rather than the
caller’s R random stream. Record the package version, graph arguments,
vertex order, graph and solver normalization, layout controls, and
scoring seeds. Fixed seeds aid repeatability within an implementation;
they are not promises of identical floating-point results across future
versions and platforms.
The surface and solid bundles here return three reference columns,
while grip() fits either 2D or 3D. Higher-dimensional
layout workflows are described in the function guide; changing layout dimension
does not create a higher-dimensional reference bundle. Sphere and
wrapped families have minimum grid-size restrictions; intrinsic trees
can have depth 0, with one vertex and no edges. Use constructor help for
parameter bounds.
No example here needs Shiny, rgl, an interactive device, or a browser. For larger families, first inspect the graph size and use modest sampled scores; full shortest-path matrices and route caches can dominate memory. Optional interactive exploration and the gallery are website follow-ups. The installed weighted-layout guide and real-data guide develop the corresponding workflows.