RunMaps API Reference

Run maps: runs joined by labeled links, and functions that build them for common control and duplicate layouts. See Run Maps for a worked example. The main entry points are RunMap, link!, linked_runs, and schedule_uniform_controls.

RunMaps.RunMap — Type
RunMap{N}

A multimap over a single pool of nodes of type N, expressing directed, typed relationships between them (e.g. "run X is validated by control Y", "run X is a duplicate of run Y"). Each edge is labeled with an open/extensible Symbol relation_type (e.g. :positive, :negative, :blank, :duplicate, or any caller-defined label) and may carry optional metadata.

Any node may simultaneously be the subject of some edges and the linked node of others – role is purely relative to a given edge, never a node-level property. N is opaque and caller-supplied – RunMap has no notion of what a node represents.

See also: link!, linked_runs, linking_runs, runs.

source
RunMaps.RunMap — Method
RunMap(d::AbstractDict) -> RunMap

Reconstruct a RunMap from a dict of the shape produced by JSON.lower(::RunMap)/json_to_runmap: a top-level "runs" node list (so orphan nodes round-trip) plus an "edges" list of {"run","linked_run","relation_type","metadata"} objects.

Since JSON has no native type system beyond string/number/bool/array/object, a node that was e.g. a Symbol or UUID before serialization comes back as a String – N is inferred from the parsed values' actual types, same as the DataFrame reverse constructor. The one exception is a (row=, copy=) composite node (used for duplicated-well identifiers), which parses to a Dict("row"=>..,"copy"=>..) with no residual type information; that shape is special-cased back into a NamedTuple so it round-trips correctly.

source
RunMaps.RunMap — Method
RunMap(df::DataFrames.AbstractDataFrame; run_col=:run, linked_run_col=:linked_run,
       type_col=:relation_type, metadata_col=:metadata) -> RunMap

Reverse of DataFrame(::RunMap) – reconstructs a RunMap by link!-ing every row. N is promote_type(eltype(run_col), eltype(linked_run_col)); falls back to Any when the two columns don't share a common concrete type (a single node pool can't be Int-and-String-but-not-Any). metadata_col defaults to :metadata to match DataFrame(::RunMap)'s output column, so the two are round-trip compatible by default; pass metadata_col=nothing to ignore a metadata column.

source
RunMaps.add_run! — Method
add_run!(map::RunMap, run) -> map

Register run as a node with no edges. Idempotent – a no-op if run is already registered. The only registration function: every node, regardless of what role(s) it ends up playing in any edge, is a "run."

source
RunMaps.assign_groups_MILP — Method
assign_groups_MILP(n::Int, capacity::Int; kwargs...) -> Vector{Int}

Convenience overload for the unweighted/homogeneous case (every item has weight 1) – equivalent to assign_groups_MILP(fill(1, n), capacity; kwargs...).

source
RunMaps.assign_groups_MILP — Method
assign_groups_MILP(weights::Vector{Int}, capacity::Int; optimizer=_default_optimizer[], timelimit=100) -> Vector{Int}

MILP formulation of the same weighted group-assignment problem as assign_groups_greedy: partition items (given by weights) into the minimum number of groups whose total weight never exceeds capacity. Unlike the greedy first-fit-decreasing heuristic, this is exact (given enough time) for general/mixed weights.

The candidate group count G is sized from running greedy first as a valid upper bound (G = length(unique(assign_groups_greedy(weights, capacity)))), since cld(n, capacity) is only a valid bound for uniform weights.

Throws ArgumentError if any weight exceeds capacity.

Keyword Arguments

  • optimizer: the JuMP-compatible optimizer used to solve the model, defaulting to Gurobi.Optimizer (requires a Gurobi license). A license-free alternative such as HiGHS.Optimizer can be passed instead.
  • timelimit: time limit in seconds for the solver.
source
RunMaps.assign_groups_greedy — Method
assign_groups_greedy(n::Int, capacity::Int; kwargs...) -> Vector{Int}

Convenience overload for the unweighted/homogeneous case (every item has weight 1) – equivalent to assign_groups_greedy(fill(1, n), capacity; kwargs...).

source
RunMaps.assign_groups_greedy — Method
assign_groups_greedy(weights::Vector{Int}, capacity::Int; kwargs...) -> Vector{Int}

Partition items (given by weights, e.g. cluster sizes) into groups whose total weight never exceeds capacity, using first-fit-decreasing (FFD): sort items by weight descending, place each into the first group with enough remaining capacity, opening a new group otherwise.

For uniform weights (e.g. every item weight 1) FFD still fills each group to capacity before opening the next, so it achieves the exact optimal group count cld(n, capacity) – a strict generalization of the previous unweighted behavior, not a change to it. For general/mixed weights FFD is a well-known good heuristic, not guaranteed exact – see assign_groups_MILP for an exact (given enough time) alternative.

Throws ArgumentError if any weight exceeds capacity (an item that can never fit in any group). Accepts and ignores extra kwargs so it is interchangeable with assign_groups_MILP behind group_solvers.

source
RunMaps.component_sizes — Method
component_sizes(map::RunMap) -> Vector{Int}

Size (total node count) of each connected component in map, equivalent to n_nodes.(components(map)) but computed without materializing the component sub-maps.

source
RunMaps.components — Method
components(map::RunMap{N}) -> Vector{RunMap{N}}

Connected components of map, treated as an undirected graph over its single node pool (an edge of any relation_type connects its two endpoints regardless of direction). Each component is returned as an independent RunMap{N} containing exactly that component's nodes, edges (with their original relation_type labels), and edge metadata.

A node registered via add_run! but never linked to anything forms its own singleton component of size 1.

map is not mutated, and the returned maps share no mutable state with it. Order of the returned vector is unspecified.

See also: n_components, n_nodes, component_sizes.

source
RunMaps.default_linked_run_id_factory — Method
default_linked_run_id_factory(group_index::Int, role::Symbol, replicate_index::Int) -> Symbol

Default linked_run_id_factory for schedule_uniform_controls. Produces a readable, debuggable Symbol of the form role_gGROUP_REPLICATE, e.g. :positive_g1_1, :positive_g1_2, :negative_g3_1.

group_index is the group's 1-based position in the canonical, contiguously renumbered group ordering used by schedule_uniform_controls, not necessarily any raw id returned by the underlying group-assignment solver. replicate_index ranges over 1:role_counts[role] within that group/role.

source
RunMaps.edge_metadata — Method
edge_metadata(map::RunMap, run, linked_run, relation_type) -> Union{Nothing,Dict{Symbol,Any}}

Metadata attached to the (run, linked_run, relation_type) edge, or nothing if that edge doesn't exist. An edge with no metadata set returns an empty Dict, distinguishing "no edge" from "edge with no metadata."

source
RunMaps.edges — Method
edges(map::RunMap) -> Vector{<:NamedTuple}

All edges as (run, linked_run, relation_type, metadata) named tuples.

source
RunMaps.has_link — Method
has_link(map::RunMap, run, linked_run; type=nothing) -> Bool

Whether an edge exists from run to linked_run, optionally restricted to one relation_type.

source
RunMaps.link! — Method
link!(map::RunMap, run, linked_run, relation_type; metadata=nothing) -> map

Create an edge from run to linked_run labeled relation_type (a Symbol or AbstractString, canonicalized to Symbol). Implicitly registers run and linked_run if they aren't already known.

Idempotent on graph structure – relinking an existing edge does not duplicate it. If metadata is given, it is merged into the edge's existing metadata rather than replacing it, so a repeated link! call can't silently drop metadata a previous call attached.

source
RunMaps.linked_runs — Method
linked_runs(map::RunMap, run; type=nothing) -> Vector

Nodes linked from run (i.e. run is the subject of the edge), optionally filtered to one relation_type. Returns an empty vector if run is unregistered or has no matching edges (unknown nodes are treated as having zero edges, not an error).

source
RunMaps.linking_runs — Method
linking_runs(map::RunMap, linked_run; type=nothing) -> Vector

Runs for which linked_run is the linked node, optionally filtered to one relation_type. Returns an empty vector if linked_run is unregistered or has no matching edges.

source
RunMaps.n_nodes — Method
n_nodes(map::RunMap) -> Int

Total number of registered nodes in map. An alias for n_runs, kept as a separate name for capacity-accounting call sites (e.g. "how many wells does this component need") that don't want to imply every node is semantically a "run."

source
RunMaps.remove_run! — Method
remove_run!(map::RunMap, run) -> map

Deregister run, cascading to unlink every edge that touches it – both edges where run is the subject (forward[run]) and edges where run is the linked node of some other run (backward[run]), since a node's edges are no longer segregated by role. No-op if run was never registered.

source
RunMaps.roles — Method
roles(map::RunMap, linked_run) -> Vector{Symbol}

Distinct relation types (roles) linked_run plays, across the runs that link to it. Generic across relation kinds – e.g. a :duplicate relation's "role" reads just as naturally as a control's role.

source
RunMaps.roles — Method
roles(map::RunMap) -> Vector{Symbol}

Every distinct relation type used anywhere in map (e.g. [:positive, :negative, :duplicate]), regardless of which nodes carry them. Empty if map has no edges.

source
RunMaps.runs — Method
runs(map::RunMap) -> Vector

Every registered node (the single pool).

source
RunMaps.schedule_controls! — Method
schedule_controls!(map::RunMap, active_runs::Vector, control_pools::AbstractDict,
                    role_counts::AbstractDict, cap::Int;
                    solver::String="greedy", kwargs...) -> map

Extend map in place with "pooled controls" scheduling: partition active_runs into groups capped at cap total nodes each (matching n_nodes/component_sizes), and within each group, claim an exclusive subset of each role's supplied control pool (control_pools[role], sized by role_counts[role]) and link! every run in the group to every control claimed for that group (dense within the group, same semantics as schedule_uniform_controls). Since linking is dense, each group's role_counts values are the guaranteed minimum number of controls (per role) every run in that group receives.

Controls are claimed exclusively per group (never shared across groups), so each resulting group remains its own connected component, the same disjoint-component property schedule_uniform_controls's groups have. control_pools itself is never mutated – an internal cursor tracks how much of each role's pool has been claimed.

Cluster-aware partitioning: nodes among active_runs that are already connected via edges map has (e.g. :duplicate edges from schedule_duplicates) are grouped as one atomic unit – the partitioner never splits an existing cluster across groups, so a group's size and connected-component identity are preserved exactly as intended even when chaining after a stage that already created edges. This is implemented by treating each cluster's size as its weight in a weighted bin-packing problem (see assign_groups_greedy/assign_groups_MILP). With no prior edges among active_runs (the common non-chained case), every cluster is a singleton and this reduces to the original unweighted partitioning.

A new function alongside (not replacing) schedule_uniform_controls: this one draws controls from a caller-supplied, finite pool of existing runs (e.g. runs meant to serve as controls) and extends an already-built map – the mechanism that makes it possible to chain after schedule_duplicates, e.g. schedule_controls!(schedule_duplicates(runs, 2), ...) (after capturing active_runs = runs(map) from the duplicate stage's output).

Arguments

  • map: existing RunMap to extend, e.g. the output of schedule_duplicates.
  • active_runs: the runs that need controls (e.g. runs(map) right after schedule_duplicates, before anything else has been added).
  • control_pools: Dict{Symbol,Vector} mapping role -> the pool of existing run ids available to serve that role.
  • role_counts: number of controls to claim per role per group, e.g. Dict(:positive=>1, :negative=>1). Same validation as schedule_uniform_controls: non-empty, positive values, Symbol/ AbstractString keys canonicalized to Symbol, no duplicates after canonicalization. Every role must also have a matching key in control_pools.
  • cap: maximum total node count (runs + controls) of any resulting group/component. Must exceed sum(values(role_counts)), and no existing cluster among active_runs may exceed cap - sum(values(role_counts)) (else ArgumentError – a cluster too large to fit in any group cannot be silently split).

Keyword Arguments

  • solver: group-assignment algorithm, "greedy" (default) or "MILP".
  • kwargs...: forwarded to the chosen solver.

Throws ArgumentError if any role's pool has insufficient supply for the number of groups the partition requires. active_runs empty is a no-op.

source
RunMaps.schedule_duplicates — Method
schedule_duplicates(runs::Vector{R}, n_duplicates::Int;
                     duplicate_id_factory::Function=default_duplicate_id_factory) where {R} -> RunMap{N}

Build a brand-new RunMap linking every run in runs to n_duplicates freshly created duplicate nodes via link!(map, run, dup, :duplicate) (run is the edge's subject, the duplicate is its linked node).

Every run is registered even when n_duplicates == 0, so the run is still present for a downstream stage (e.g. schedule_controls!) to see.

Since this builds a fresh map, runs(map) immediately after calling gives exactly "originals + duplicates, nothing else yet" – the natural active_runs input for chaining into schedule_controls!.

Arguments

  • runs: flat list of run node ids to duplicate.
  • n_duplicates: number of duplicate nodes to create per run. Must be >= 0.

Keyword Arguments

Node type resolution

The resulting RunMap{N} uses N = promote_type(eltype(runs), typeof(first generated id)), falling back to Any whenever the run type and the generated-id type don't coincide – mirrors schedule_uniform_controls's resolution rule. When runs is empty or n_duplicates == 0, no id is ever generated, so the result is typed RunMap{R} directly.

source
RunMaps.schedule_uniform_controls — Method
schedule_uniform_controls(runs::Vector{R}, role_counts, cap::Int;
                           solver::String="greedy",
                           linked_run_id_factory::Function=default_linked_run_id_factory,
                           kwargs...) where {R} -> RunMap{N}

Build a brand-new RunMap implementing "uniform shared controls" scheduling: partition runs into groups capped at cap total nodes each (matching n_nodes/component_sizes), and within each group create sum(values(role_counts)) new control nodes (counts given per role, e.g. Dict(:positive=>1, :negative=>1)) linked from every run in that group (dense within the group).

Controls are never shared across groups, so each resulting group is automatically its own connected component – recover the grouping later via components/component_sizes; no extra metadata is stored on the map for this purpose.

Arguments

  • runs: flat list of run node ids to schedule. Duplicates are not deduplicated – pass a Vector with the identity/multiplicity you intend.
  • role_counts: number of controls to create per role, e.g. Dict(:positive=>1, :negative=>1). Keys may be Symbol or AbstractString (canonicalized to Symbol, matching link!); must be non-empty with strictly positive values.
  • cap: maximum total node count (runs + controls) of any resulting group/component. Must exceed sum(values(role_counts)).

Keyword Arguments

  • solver: group-assignment algorithm, "greedy" (default) or "MILP". For the homogeneous/unweighted runs handled here both are exact and produce the same number of groups; MILP is provided as an extensible scaffold rather than a better answer – see assign_groups_MILP.
  • linked_run_id_factory: (group_index::Int, role::Symbol, replicate_index::Int) -> id called once per control node to create; defaults to default_linked_run_id_factory.
  • kwargs...: forwarded to the chosen solver (e.g. optimizer, timelimit for "MILP").

Node type resolution

The resulting RunMap{N} uses N = promote_type(eltype(runs), typeof(first generated id)), falling back to Any whenever the run type and the generated-id type don't coincide (e.g. Int runs with the default Symbol-returning factory build RunMap{Any}) – a single node pool cannot hold both types under a narrower parameterization. When runs is empty, no id is ever generated, so the result is typed RunMap{R} directly.

See also: group_solvers, components.

source
RunMaps.set_metadata! — Method
set_metadata!(map::RunMap, run, linked_run, relation_type, data::AbstractDict) -> map

Merge data into the metadata of an existing edge. Throws ArgumentError if (run, linked_run, relation_type) is not an edge in map.

source
RunMaps.unlink! — Function
unlink!(map::RunMap, run, linked_run, relation_type=nothing) -> map

Remove the edge from run to linked_run labeled relation_type, along with its metadata. If relation_type is nothing (the default), removes all edges between run and linked_run regardless of type.

No-op if no matching edge exists. run and linked_run remain registered nodes even if this leaves them without any edges.

source