Canonical Basin Complex Workflow
Source:vignettes/basin_complex_workflow_vignette.Rmd
basin_complex_workflow_vignette.RmdStart with Finding your way around gflow for input conventions, the complete catalog, and migration advice. The example graphs and fields give smaller structural comparisons.
One constructor, five basin families
create.basin.complex() is the recommended constructor
for new basin analyses in gflow. Specialized
historical/grid constructors have separate schemas. It validates the
graph and field once, dispatches to one of five basin construction
families, and returns the same basin_complex schema from
every family.
method |
Structure computed | Typical use |
|---|---|---|
"trajectory_flow" |
Flow trajectories and terminal basins | Deterministic field-following basins |
"superlevel_merge_tree" |
Plateau-aware graph level-set merge tree | Basin births, mergers, and persistence |
"geodesic_reachability" |
Monotone reachability supports | Overlapping attraction supports |
"rtcb" |
Relaxed trajectory-constrained basins | Bounded departures from monotone paths |
"overlap_cell_complex" |
Ascending-by-descending overlap cells | Joint maxima/minima decomposition |
The remainder of this vignette uses one small two-dimensional grid only to make the interface executable and inspectable. Scientific basin validation should use the higher-dimensional, scale-path inputs appropriate to the analysis.
Graph and field
The graph is undirected. Each adjacency entry has a parallel edge-length entry, and vertex identifiers are one-based.
edges <- matrix(
c(
1, 2, 1, 1, 4, 1, 2, 3, 1, 2, 5, 1,
3, 6, 1, 4, 5, 1, 4, 7, 1, 5, 6, 1,
5, 8, 1, 6, 9, 1, 7, 8, 1, 8, 9, 1
),
ncol = 3,
byrow = TRUE
)
adjacency <- edge_lengths <- vector("list", 9)
for (i in seq_len(nrow(edges))) {
from <- edges[i, 1]
to <- edges[i, 2]
adjacency[[from]] <- c(adjacency[[from]], to)
adjacency[[to]] <- c(adjacency[[to]], from)
edge_lengths[[from]] <- c(edge_lengths[[from]], edges[i, 3])
edge_lengths[[to]] <- c(edge_lengths[[to]], edges[i, 3])
}
field <- c(0, 1, 0, 1, 3, 1, 0, 1, 2)
vertex_mass <- rep(1 / length(field), length(field))Direction, mass, and density
direction = "max" constructs ascending basins,
"min" constructs descending basins, and "both"
requests both orientations. The overlap-cell method requires
"both".
vertex.mass represents empirical or quadrature support.
It is used for basin mass summaries and mass-threshold refinement.
vertex.density instead modulates trajectory edge scores
when method.params$modulation is "DENSITY" or
"DENSITY_EDGELEN". The constructor never substitutes one
for the other.
flow <- create.basin.complex(
adjacency,
edge_lengths,
field,
method = "trajectory_flow",
direction = "max",
vertex.mass = vertex_mass,
method.params = list(
modulation = "CLOSEST",
edge.length.quantile.thld = 1
)
)
flow$status
#> [1] "ok"
get.basin.table(flow)[
,
c("basin.id", "type", "retained.support.mass")
]
#> basin.id type retained.support.mass
#> 1 basin_max_v00000005 max 0.7777778
#> 2 basin_max_v00000009 max 0.3333333For density-modulated flow, provide density explicitly:
density_flow <- create.basin.complex(
adjacency,
edge_lengths,
field,
method = "trajectory_flow",
direction = "max",
vertex.density = exp(field),
method.params = list(
modulation = "DENSITY",
edge.length.quantile.thld = 1
)
)
density_flow$status
#> [1] "ok"Level-set merge tree
The level-set method contracts exact field plateaus and records component births, elder-rule mergers, and component-root deaths. Use it to query connected graph level sets. A field estimated elsewhere at several smoothing scales can also be analyzed separately at each scale.
tree <- create.basin.complex(
adjacency,
edge_lengths,
field,
method = "superlevel_merge_tree",
direction = "max"
)
tree$status
#> [1] "ok"
get.basin.merge.tree(tree, required = TRUE)$merge.table
#> event.id direction losing.basin.id surviving.basin.id
#> 1 merge_max_00000001 max basin_max_v00000009 basin_max_v00000005
#> merge.plateau.id merge.vertices merge.level birth.level death.level
#> 1 plateau_00000006 6 1 2 1
#> persistence event.status merge.vertex.ids
#> 1 1 ok 6Construct one object at every retained scale value, then compare
merge tables and memberships between adjacent scale values. The scale
parameter belongs to the conditional-expectation estimator, not to
create.basin.complex() itself; record it alongside each
returned object.
Reachability, RTCB, and overlap cells
reachability <- create.basin.complex(
adjacency,
edge_lengths,
field,
method = "geodesic_reachability",
direction = "max",
method.params = list(edge.length.quantile.thld = 1)
)
consensus <- create.basin.complex(
adjacency,
edge_lengths,
field,
method = "rtcb",
direction = "max",
method.params = list(
edge.length.quantile.thld = 1,
n.min = 1L,
m.min = 0
)
)
cells <- create.basin.complex(
adjacency,
edge_lengths,
field,
method = "overlap_cell_complex",
direction = "both"
)
vapply(
list(
geodesic_reachability = reachability,
rtcb = consensus,
overlap_cell_complex = cells
),
function(x) x$status,
character(1)
)
#> geodesic_reachability rtcb overlap_cell_complex
#> "ok" "ok" "ok"Membership is not assignment
get.basin.membership() exposes the raw many-to-many
relation between vertices and basins. Overlap is allowed.
primary.assignment is an optional convenience label
selected by the method’s primary.assignment.policy; it does
not replace membership and can have NA labels and
not_applicable status. A missing assignment is not evidence
of missing membership. RTCB currently supplies no backend primary
assignment; overlap cells also have no primary assignment.
head(get.basin.membership(reachability))
#> vertex direction basin.id membership.weight membership.status
#> 1 1 max basin_max_v00000005 1.0 raw
#> 2 2 max basin_max_v00000005 1.0 raw
#> 3 3 max basin_max_v00000005 0.5 raw
#> 4 3 max basin_max_v00000009 0.5 raw
#> 5 4 max basin_max_v00000005 1.0 raw
#> 6 5 max basin_max_v00000005 1.0 raw
#> source.stage is.primary vertex.id
#> 1 construction FALSE 1
#> 2 construction FALSE 2
#> 3 construction FALSE 3
#> 4 construction FALSE 3
#> 5 construction FALSE 4
#> 6 construction FALSE 5
get.basin.assignment(reachability)
#> vertex direction basin.id assignment.weight assignment.status
#> 1 1 max <NA> NA not_applicable
#> 2 2 max <NA> NA not_applicable
#> 3 3 max <NA> NA not_applicable
#> 4 4 max <NA> NA not_applicable
#> 5 5 max <NA> NA not_applicable
#> 6 6 max <NA> NA not_applicable
#> 7 7 max <NA> NA not_applicable
#> 8 8 max <NA> NA not_applicable
#> 9 9 max <NA> NA not_applicable
#> assignment.policy root.vertex next.vertex vertex.id root.vertex.id
#> 1 none NA NA 1 <NA>
#> 2 none NA NA 2 <NA>
#> 3 none NA NA 3 <NA>
#> 4 none NA NA 4 <NA>
#> 5 none NA NA 5 <NA>
#> 6 none NA NA 6 <NA>
#> 7 none NA NA 7 <NA>
#> 8 none NA NA 8 <NA>
#> 9 none NA NA 9 <NA>
#> next.vertex.id
#> 1 <NA>
#> 2 <NA>
#> 3 <NA>
#> 4 <NA>
#> 5 <NA>
#> 6 <NA>
#> 7 <NA>
#> 8 <NA>
#> 9 <NA>Downstream code that needs complete basin supports should use
get.basin.membership(). After refinement, use the basin
table’s retained.support.vertices for retained support.
Code that requires exactly one basin per vertex must choose and record
an assignment policy.
Refinement
Refinement stages are controlled with simplify.params.
They execute in a fixed order and are recorded in
provenance$refinement.stages:
- relative-value filtering;
- extrema clustering;
- geometric filtering;
- support filtering; and
- expansion.
Stage applicability depends on the construction family. Unsupported combinations fail validation rather than silently doing nothing.
refined <- create.basin.complex(
adjacency,
edge_lengths,
field,
method = "trajectory_flow",
direction = "max",
method.params = list(edge.length.quantile.thld = 1),
simplify.params = list(
relative.value = list(enabled = TRUE)
)
)
refined$provenance$refinement.stages
#> stage status parameters input.basin.count.max
#> 1 relative.value completed TRUE, 1..... 2
#> 2 maxima.clustering disabled FALSE, 0.15 2
#> 3 minima.clustering disabled FALSE, 0.15 2
#> 4 geometric.filter disabled FALSE, 0.... 2
#> 5 support.filter disabled FALSE, 0.... 2
#> 6 expansion not_applicable FALSE, n.... 2
#> input.basin.count.min output.basin.count.max output.basin.count.min
#> 1 0 2 0
#> 2 0 2 0
#> 3 0 2 0
#> 4 0 2 0
#> 5 0 2 0
#> 6 0 2 0
#> input.membership.count output.membership.count retained.basin.ids
#> 1 10 10 basin_ma....
#> 2 10 10 basin_ma....
#> 3 10 10 basin_ma....
#> 4 10 10 basin_ma....
#> 5 10 10 basin_ma....
#> 6 10 10 basin_ma....
#> removed.basin.ids warnings diagnostics summary.snapshot
#> 1 c(max1 =....
#> 2 c(max1 =....
#> 3 c(max1 =....
#> 4 c(max1 =....
#> 5 c(max1 =....
#> 6 c(max1 =....Archived objects and retired constructors
The following public constructors are retired and deliberately raise
a gflow_basin_lifecycle_error:
-
compute.gfc(); -
compute.basins.of.attraction(); -
compute.gfc.trajectory(); -
compute.gfc.flow(); and -
create.basin.cx().
Use create.basin.complex() for all new work.
as.basin.complex() exists only for compatible archived
results. Conversion requires the archived object and its original graph;
a field must also be supplied if the archive did not retain one.
Conversion provenance records the source class and retained archive so
that migration remains auditable.
canonical <- as.basin.complex(
archived_object,
adj.list = original_adjacency,
edge.length.list = original_edge_lengths,
field = original_field
)Result checks
Every result carries construction status, resolved parameters, provenance, diagnostics, basin summaries, membership, and optional method-specific structures.
objects <- list(flow, tree, reachability, consensus, cells)
stopifnot(all(vapply(
objects,
function(x) inherits(x, "basin_complex") && identical(x$status, "ok"),
logical(1)
)))
names(flow)
#> [1] "method" "direction" "n.vertices"
#> [4] "graph.input" "field" "parameters"
#> [7] "provenance" "extrema" "basin.table"
#> [10] "membership" "assignment" "merge.table"
#> [13] "trajectory.forest" "cell.complex" "diagnostics"
#> [16] "warnings" "status" "raw"
flow$provenance$construction
#> $status
#> [1] "complete"
#>
#> $completed
#> [1] TRUE
#>
#> $membership.weight.policy
#> [1] "equal_share_set_membership"
#>
#> $assignment.policy
#> [1] "backend_primary"