Documentation
¶
Overview ¶
Package core - deterministic, thread-safe in-memory graphs for serious work.
----------------------------------------------------------------------------- -- WHAT ---------------------------------------------------------------------
A single, composable Graph type G=(V,E) with predictable iteration order, strict sentinel errors, explicit configuration flags, and no ambient globals.
The package provides a stable, auditable contract suitable for algorithms that require reproducibility (tests, golden outputs, deterministic traversals) and a simple concurrency model for safe parallel reads and controlled mutations.
----------------------------------------------------------------------------- -- WHY ----------------------------------------------------------------------
Determinism First: Public enumeration order is documented and stable. This removes flakiness from order-sensitive tests and prevents reproducibility drift.
Concurrency Without Drama: Two RWMutexes (muVert, muEdgeAdj) separate vertex/config state from topology state (edges/adjacency) to reduce contention while remaining simple.
Mixed-Mode Mastery: Per-edge directedness overrides are explicit and legal only when mixed-mode is enabled. Violations return ErrMixedEdgesNotAllowed; there is no silent fallback.
Identity Discipline: Default Edge IDs are monotonic textual identifiers ("e1","e2",...). This is useful for stable logs, diffs, and external correlation.
Practical API Surface: One Graph with explicit flags (Directed/Weighted/Multi/Loops/Mixed) instead of multiple graph types that fragment algorithms.
No Magic, No Surprises: Sentinel errors only (errors.Is), explicit capability flags, and no hidden global state. If a capability is disabled, the package returns the relevant ErrX.
----------------------------------------------------------------------------- -- WHEN ---------------------------------------------------------------------
- Need reproducible graph results (order-sensitive logic, golden tests).
- Need to combine directed/undirected/loops/multi-edges within one instance.
- Need safe concurrent reads with a clear lock model and controlled mutations.
- Need stable Edge IDs across clones/views for debugging/analytics.
----------------------------------------------------------------------------- -- CONTRACT LAWS ------------------------------------------------------------
Determinism law:
- Vertices() returns Vertex.ID values sorted lexicographically ascending.
- Edges(), GetNamedEdges(), and Neighbors() return edges sorted by Edge.ID ascending.
- NeighborIDs() returns unique adjacent vertex IDs sorted lexicographically ascending.
- AdjacencyList() returns fresh per-vertex edge-ID slices sorted by Edge.ID ascending; map key iteration order remains Go-map order and is therefore not deterministic.
Construction / validation law:
- NewGraph and NewMixedGraph validate public GraphOption inputs and return errors; constructor validation is part of the public contract.
- AddEdge validates public EdgeOption inputs before lock acquisition, vertex auto-creation, or edge publication.
Ownership / aliasing law:
- Detached container/value surfaces include Vertices(), NeighborIDs(), AdjacencyList(), Stats(), counts, and membership predicates.
- Alias-based catalog accessors include VerticesMap(), GetEdge(), GetNamedEdges(), Edges(), and Neighbors().
- Alias-based accessors may return freshly allocated containers, but the contained *Vertex/*Edge values alias live catalog records.
- Structural records MUST be treated as immutable once published in a graph: Vertex.ID, Edge.ID, Edge.From, Edge.To, Edge.Weight, and Edge.Directed.
- Vertex.Metadata is caller-managed payload. Clone/View operations shallow-copy the Metadata map pointer; core neither deep-copies nor synchronizes it.
Compatibility law:
- InternalVertices() is retained only as a deprecated compatibility surface. New code must not treat it as an internal-storage escape hatch.
Concurrency law:
- Public methods are safe for concurrent use according to their documented locks.
- Retained aliased pointers do not extend lock scope, do not provide snapshot isolation, and do not remain membership proofs after later mutations.
----------------------------------------------------------------------------- -- DESIGN INVARIANTS --------------------------------------------------------
Deterministic ordering (public contract): - Vertices() → IDs sorted lexicographically ascending. - Edges() → by Edge.ID ascending (string lex order). Note: lex order is over strings, so "e10" sorts between "e1" and "e2". Auto-generated IDs are monotonic in numeric suffix, but API ordering is defined by string sort order. - NeighborIDs(id) → unique IDs sorted lexicographically ascending. - Neighbors(id) → edges sorted by Edge.ID ascending.
Sentinel errors only; compare with errors.Is. Exported operations return stable package-level sentinels. Callers must not rely on string matching.
No mutation of caller-owned inputs. Views/clones are explicit and named; they never mutate the source graph.
Concurrency model: two RWMutexes with fixed lock order. - muVert guards the vertex catalog and configuration flags. - muEdgeAdj guards the edge catalog and sparse adjacency index. - When both locks are needed, the required order is muVert -> muEdgeAdj. - Internal adjacency cleanup helpers never acquire muVert; callers either already hold the needed vertex lock or are performing edge-only mutations.
Storage truth model: catalogs vs indexes. - g.vertices is the authoritative vertex-membership catalog. - g.edges is the authoritative edge-membership catalog. - g.adjacencyList is a private sparse edge index, not a second vertex catalog. - Empty adjacency buckets may be absent internally. - Therefore, absence of adjacencyList[id] does NOT imply absence of vertex id. - Public AdjacencyList() reconstructs a full graph-facing snapshot from g.vertices and includes isolated vertices with empty edge-ID slices. - Matrix-shaped representations belong outside this storage layer; use matrix.NewAdjacencyMatrix(graph, options) or matrix.BuildDenseAdjacency(vertices, edges, options) when a complete dense/sparse mathematical matrix is required.
----------------------------------------------------------------------------- -- CONFIGURATION (GraphOption) ----------------------------------------------
GraphOption values are applied only during construction (NewGraph/NewMixedGraph). After construction, flags are immutable.
WithDirected(defaultDirected bool) Sets the default edge orientation for newly created edges.
WithMixedEdges() Enables per-edge directedness overrides (WithEdgeDirected). Without it, AddEdge(..., WithEdgeDirected(...)) → ErrMixedEdgesNotAllowed.
WithWeighted() Permits non-zero edge weights. Without it, AddEdge(weight!=0) → ErrBadWeight.
WithMultiEdges() Permits parallel edges between identical endpoints. Without it, a second AddEdge(from,to,...) → ErrMultiEdgeNotAllowed.
WithLoops() Permits self-loops (from==to). Without it, AddEdge(v,v,...) → ErrLoopNotAllowed.
Helper constructor:
- NewMixedGraph(opts ...GraphOption) = NewGraph(WithMixedEdges(), opts...) Ensures mixed-mode is enabled before any other options are applied.
----------------------------------------------------------------------------- -- EDGE OPTIONS (EdgeOption) ------------------------------------------------
EdgeOption values configure a single edge during AddEdge. Options are applied sequentially in call order. The first option returning an error aborts AddEdge without mutating the graph.
WithEdgeDirected(directed bool) Overrides Directed for this edge. Contract: allowed only when MixedEdges()==true; otherwise ErrMixedEdgesNotAllowed.
WithID(id string) Assigns a custom Edge.ID for the new edge. Contract:
id must be non-empty, else returns ErrEmptyEdgeID.
id must be globally unique within the graph, else returns ErrEdgeIDConflict. Note:
If id matches the canonical auto-ID form "eN", the graph advances its internal auto-ID counter to avoid future collisions.
----------------------------------------------------------------------------- -- ERROR SET (sentinels) ----------------------------------------------------
- ErrEmptyVertexID - empty vertex ID is illegal.
- ErrVertexNotFound - referenced vertex does not exist.
- ErrEdgeNotFound - referenced edge does not exist.
- ErrBadWeight - non-zero weight in an unweighted graph.
- ErrLoopNotAllowed - self-loop when loops are disabled.
- ErrMultiEdgeNotAllowed - parallel edge when multi-edges are disabled.
- ErrMixedEdgesNotAllowed - per-edge directed override when mixed-mode is disabled.
- ErrEmptyEdgeID - empty edge ID is illegal (WithID / SetEdgeID).
- ErrEdgeIDConflict - edge ID collision (WithID / SetEdgeID).
----------------------------------------------------------------------------- -- LIFECYCLE MAPS -----------------------------------------------------------
Graph lifecycle (configuration → build → query → transform):
g := core.NewGraph(WithDirected(false), WithWeighted())
_ = g.AddVertex("A")
_ = g.AddVertex("B")
eid, err := g.AddEdge("A", "B", 10) // undirected by default here
es := g.Edges() // deterministic (Edge.ID asc, lex order)
in, out, und, _ := g.Degree("A") // (in,out,undirected)
s := g.Stats() // O(V+E) snapshot of flags & counts
g2 := g.Clone() // deep copy, preserves edge IDs
uv := core.UnweightedView(g) // same topology, weight=0, weighted=false
Vertex lifecycle:
- Create → AddVertex(id)
- Check → HasVertex(id)
- Remove → RemoveVertex(id) // removes incident edges deterministically
- Enumerate → Vertices() // sorted lex asc
- Inspect → VerticesMap() // shallow copy (ID → *Vertex)
Edge lifecycle:
- Create → AddEdge(from,to,weight, opts...) // policy enforced by sentinels
- Check → HasEdge(from,to) // O(1) membership
- Inspect → GetEdge(edgeID), Edges() // Edges() sorted by ID
- Rename → SetEdgeID(oldID,newID) // updates catalog + adjacency atomically under lock
- Remove → RemoveEdge(edgeID) // mirrors handled automatically
- Filter → FilterEdges(pred) // O(E) + cleanup
Adjacency & Neighborhood:
- Neighbors(id) → []*Edge // sorted by Edge.ID (lex asc)
- NeighborIDs(id) → []string // unique, sorted lex asc
- AdjacencyList() → map[id][]edgeID // per-vertex lists sorted by Edge.ID
Cloning & Views:
- CloneEmpty() - copy flags + vertices, no edges, carry nextEdgeID.
- Clone() - deep copy (flags + vertices + edges + adjacency), carry nextEdgeID.
- UnweightedView(g) - same topology, weight=0, Weighted()=false. Preserves Edge.ID values and carries the edge-ID counter to prevent collisions if you later add edges to the derived graph.
- InducedSubgraph(g, keep) - keep subset of vertices + incident edges. Preserves Edge.ID values and carries the edge-ID counter for the same reason.
----------------------------------------------------------------------------- -- COMPLEXITY SUMMARY -------------------------------------------------------
AddVertex / HasVertex / HasEdge O(1) amortized AddEdge O(1) amortized (topologically atomic) RemoveEdge O(1) amortized + adjacency cleanup cost RemoveVertex O(E) (scans edge catalog to remove incidents) Vertices / Edges O(V log V) / O(E log E) for ordering Neighbors / NeighborIDs O(d log d) (d = degree(id)) AdjacencyList O(V+E) assemble + per-vertex sort Degree O(E) (scans edges to ensure correct in-degree) CloneEmpty / Clone O(V) / O(V+E) Clear O(1) (map reinit + counter reset) Stats O(V+E)
----------------------------------------------------------------------------- -- NON-GOALS ----------------------------------------------------------------
- No immutable persistent graph structure. - No automatic deep-copy policy for Metadata. - No silent policy fallback for weighted / mixed / loop / multigraph behavior. - No hidden mutation channel through accessor APIs. - No package-level snapshot isolation beyond documented method scope.
----------------------------------------------------------------------------- -- AI-HINT (LLM/Copilot/ChatGPT/Claude/Gemini/Qwen guidance) ----------------
Deterministic ordering is part of the public contract. Prefer Vertices()/Edges()/NeighborIDs() outputs directly (already sorted).
To use per-edge directedness, you MUST enable mixed-mode at construction time: g := core.NewMixedGraph(WithDirected(false)) _, err := g.AddEdge("A","B", 0, WithEdgeDirected(true))
Unweighted graphs must add edges with weight==0; otherwise AddEdge returns ErrBadWeight: g := core.NewGraph() // unweighted by default _, err := g.AddEdge("A","B", 5) // => ErrBadWeight
No silent fallbacks: loops/multi-edges/mixed-mode violations return explicit sentinels. Use errors.Is(err, core.ErrLoopNotAllowed) etc.
WithID provides stable external references: _, err := g.AddEdge("A","B", 0, WithID("stable-edge-1")) If you choose IDs of the form "eN", the graph advances its internal counter past N.
Cloning and views preserve textual Edge.ID sequence via nextEdgeID carry-over. If you Clone() then AddEdge(), the new edge ID continues monotonic growth.
Stats() is an O(V+E) snapshot suitable for diagnostics and tests. If the graph is mutated concurrently, treat Stats() as best-effort telemetry, not as a correctness-critical synchronization primitive.
Concurrency model: public methods manage locking internally. Avoid holding external locks around core methods to prevent lock-order coupling.
Use InducedSubgraph/UnweightedView to derive subproblems without mutating the input graph. Derived graphs preserve Edge.ID values and keep future AddEdge() IDs unique by carrying the internal edge-ID counter forward.
Do not treat g.adjacencyList as the vertex catalog. It is a sparse edge index.
Preserve isolated vertices in public AdjacencyList() by reading g.vertices, not by forcing empty internal adjacency buckets to exist.
----------------------------------------------------------------------------- -- See also: docs/CORE.md for algorithmic notes, proofs, and extended examples.
Package core defines deterministic, thread-safe in-memory graphs and the foundational types (Graph/Vertex/Edge) used across lvlath.
Design invariants (public contract, enforced across the codebase):
- Determinism: public enumeration order is stable and documented (Vertices by ID asc, Edges by Edge.ID asc, NeighborIDs by ID asc).
- Sentinel errors only: exported operations return package-level sentinels and are checked via errors.Is; no fmt-wrapping of those sentinels.
- No hidden global state: behavior is explicit via options; no ambient randomness.
- Concurrency: vertex catalog and edge/adjacency storage are protected by separate RWMutexes (muVert, muEdgeAdj) to reduce contention.
Notes:
- Graph configuration flags are set only during construction (NewGraph + GraphOption). After construction they are immutable, so internal code may read them without locks to avoid lock inversion in hot paths.
Example (BetweennessCentrality) ¶
Example_betweennessCentrality demonstrates the identification of a "critical artery" in a global logistics network using Betweenness Stress Centrality. Two densely connected communities (clusters) are linked by a single bridging edge. The bridging edge carries all shortest-path traffic between the clusters, making it the highest-betweenness edge. CONTEXT: "The Global Transit Bottleneck"
- You are the lead architect of a global supply chain monitoring system. The graph represents two massive economic zones (Cluster A and Cluster B), each with high internal redundancy. However, they are connected by a single transit corridor (the "Suez-Link"). Your mission is to quantify the "Structural Stress" on this link. If this single edge fails, 100% of inter-cluster trade is paralyzed.
Scenario:
- Vertices are hubs/warehouses, edges are direct transport corridors.
- You have two dense regions (two cities / two warehouse clusters).
- Exactly one corridor connects the regions (a bridge edge).
Why this matters (criticality):
- If that corridor fails, inter-region delivery collapses immediately.
- Even BEFORE failure, that corridor experiences maximal “load” because almost all cross-region shortest paths must traverse it.
MATHEMATICAL MODEL (Edge Stress):
For a graph partitioned into two disjoint sets V_A and V_B, where all paths between sets must traverse a single bridge edge (e_bridge), the "Load" (L) is:
L(e_bridge) = |V_A| * |V_B|
This represents the total number of unique shortest-path pairs (s, t) such that s ∈ V_A and t ∈ V_B. In this topology, the bridge edge carries the maximum possible Betweenness Centrality.
Closed-form load (for this topology):
- Every pair (a in A, b in B) must traverse the bridge.
- Therefore bridgeLoad = |A| * |B|.
Implementation:
- Stage 1: Construct two clusters of vertices with rich internal connections.
- Stage 2: Link the clusters with a single edge and identify this edge.
- Stage 3: Calculate the number of unique shortest-path pairs that traverse the bridge (betweenness load).
Behavior highlights:
- The identified bridge edge is an articulation link between clusters (its removal would disconnect the graph).
- The bridge's betweenness load equals the product of cluster sizes, as every inter-cluster pair of vertices must communicate via this edge.
Inputs:
- None (graph structure is deterministic).
Returns:
- None (prints the critical edge ID and its computed load).
Errors:
- Any unexpected error is printed and the example returns early.
Complexity:
- Graph construction: O(V^2) for dense cluster edges. Identifying the bridge and computing load: O(V + E).
CORE PACKAGE LEVERAGE:
- Topology Verification: Uses GetEdge(id) for O(1) validation of critical links.
- Connectivity Analysis: Leverages Incidence(v) to inspect the local "fan-out" of a hub vertex and identify the bridging edge among local connections.
- Inventory Integrity: Uses the deterministic Vertices() sequence to partition and calculate global load factors without external state tracking.
package main
import (
"fmt"
"strings"
"github.com/lvlath/go/core"
)
func main() {
// Constants for simulation scale (4x4 clusters for the example output)
const clusterSize = 4
const bridgeID = "e13"
// Stage 1: Infrastructure Construction
// We initialize a non-directed graph representing physical transport corridors.
g, _ := core.NewGraph(core.WithDirected(false))
// Pre-allocate slices to avoid repeated allocations in loops.
vertsA := make([]string, clusterSize)
vertsB := make([]string, clusterSize)
for i := 0; i < clusterSize; i++ {
vertsA[i] = fmt.Sprintf("A%d", i)
vertsB[i] = fmt.Sprintf("B%d", i)
}
// Build two Cliques (fully connected clusters).
// This simulates high-density metropolitan or regional warehouse networks.
for i := 0; i < clusterSize; i++ {
for j := i + 1; j < clusterSize; j++ {
_, _ = g.AddEdge(vertsA[i], vertsA[j], 0)
_, _ = g.AddEdge(vertsB[i], vertsB[j], 0)
}
}
// Stage 2: The Critical Integration (The Bottleneck)
// We link the two clusters through a single point of failure.
_, err := g.AddEdge(vertsA[0], vertsB[0], 0)
if err != nil {
fmt.Printf("Critical failure during bridge creation: %v\n", err)
return
}
// Stage 3: Structural Analysis
// Verify the bridge exists and analyze its impact.
bridge, err := g.GetEdge(bridgeID)
if err != nil {
fmt.Printf("Link verification failed: %v\n", err)
return
}
// Calculate Stress Load: L = |V_A| * |V_B|.
// We use core.Vertices() to perform a census of the economic zones.
var countA, countB int
for _, v := range g.Vertices() {
if strings.HasPrefix(v, "A") {
countA++
} else if strings.HasPrefix(v, "B") {
countB++
}
}
stressLoad := countA * countB
// Stage 4: Reporting and Verification
// Verify that hub A0 is indeed a proxy by analyzing its neighbors.
// Use NeighborIDs for a quick inspection of local connections.
neighbors, _ := g.NeighborIDs(vertsA[0])
var isBottleneckFound bool
for _, nID := range neighbors {
if nID == vertsB[0] {
isBottleneckFound = true
break
}
}
// Output results using stable identifiers for documentation.
if isBottleneckFound {
fmt.Printf("Analysis: Critical Link Identified: %s (%s)\n", bridgeID, bridge.From+"-"+bridge.To)
fmt.Printf("Load: Betweenness Stress Factor = %d paths\n", stressLoad)
}
}
Output: Analysis: Critical Link Identified: e13 (A0-B0) Load: Betweenness Stress Factor = 16 paths
Example (CascadingFailures) ¶
Example_cascadingFailures demonstrates a cascading failure scenario in a power grid network. A highly connected hub node is removed to simulate a substation failure, and the impact on network connectivity is measured. Example_cascadingFailures demonstrates cascading-failure analysis in a power grid. CONTEXT:
- You are a Resilience Architect for the 'Aethelgard' energy grid.
- A critical infrastructure node (Hub) is targeted by a cyber-kinetic strike.
- Objective: Predict the "Cascade Collapse Index" before the physical failure occurs.
Scenario:
- You operate a smart-city grid graph: vertices are substations, edges are physical lines.
- An incident (physical fault / cyberattack) disables a single high-degree hub substation.
- Your job is to quantify whether the grid “degrades gracefully” or splits into islands.
Why this matters (criticality):
- In real grids, the most dangerous failures are not “one line is down” but “a cut point is down”.
- A single vertex can be a topological single point of failure (cut-vertex).
- You need fast “what-if” evaluation without corrupting the production topology.
MATHEMATICAL MODEL:
- Survival Coefficient (Resilience Ratio) 'R': R = N'_LCC / (N_LCC - 1) Measures how much of the Giant Component (LCC) remains after the hub's evaporation.
- Fragility Index 'Φ': Φ = 1 - (Σ deg(v_adj) / deg(v_target)) Quantifies topological dependency. A high Φ indicates that neighbors are dangerously dependent on the target node for their connectivity.
Metric (resilience ratio):
- Let N_LCC be the size of the Largest Connected Component (LCC) BEFORE the incident.
- Let N'_LCC be the size of the LCC AFTER removing the incident vertex.
- Resilience ratio: R = N'_LCC / (N_LCC - 1)
- Interpretation:
- R close to 1 -> removal barely hurts connectivity.
- R close to 0 -> removal fractures the grid into small islands.
Implementation:
- Stage 1: Build two dense clusters (districts) connected only via a single hub.
- Stage 2: Clone() the topology and RemoveVertex(hub) in the clone (sandbox simulation).
- Stage 3: Compute LCC size via BFS using NeighborIDs (deterministic neighbor ordering).
CORE PACKAGE LEVERAGE:
- Snapshot Isolation: Uses core.Clone() to spawn a "shadow reality" for destructive testing without mutating the production graph.
- Atomic Cleanup: core.RemoveVertex(id) ensures no orphaned edges remain, providing a clean state for the subsequent BFS traversal.
- Structural Inspection: Uses core.Degree and core.AdjacentVertices to compute second-order topological metrics (Φ).
Inputs:
- None (graph structure is hard-coded).
Returns:
- None (prints the resilience ratio R).
Errors:
- Any unexpected error is printed and the example returns early.
Complexity:
- Building and scanning the graph: O(V + E). BFS for components: O(V + E).
package main
import (
"fmt"
"github.com/lvlath/go/core"
)
func main() {
// ---- Stage 1: Infrastructure Synthesis ----
const clusterSize = 4
var (
err error
neighbors []string
hubID = "Hub-Central"
districtA = []string{"A1", "A2", "A3", "A4"}
districtB = []string{"B1", "B2", "B3", "B4"}
)
g, _ := core.NewGraph(core.WithWeighted(), core.WithDirected(false))
// Construct two dense districts (Cliques)
for i := 0; i < clusterSize; i++ {
for j := i + 1; j < clusterSize; j++ {
if _, err = g.AddEdge(districtA[i], districtA[j], 1.0); err != nil {
fmt.Println(err)
return
}
if _, err = g.AddEdge(districtB[i], districtB[j], 1.0); err != nil {
fmt.Println(err)
return
}
}
}
// Link districts through a single strategic Hub (the single point of failure)
for i := 0; i < clusterSize; i++ {
_, _ = g.AddEdge(hubID, districtA[i], 1.0)
_, _ = g.AddEdge(hubID, districtB[i], 1.0)
}
// ---- Stage 2: Pre-Collapse Fragility Analysis (Φ) ----
_, _, hubDegree, _ := g.Degree(hubID)
neighbors, _ = g.NeighborIDs(hubID)
var neighborDegree, sumNeighborDegrees int
for _, nID := range neighbors {
_, _, neighborDegree, _ = g.Degree(nID)
sumNeighborDegrees += neighborDegree
}
// Φ = 1 - (Average Neighbor Connectivity / Hub Connectivity)
phi := 1.0 - (float64(sumNeighborDegrees) / float64(hubDegree))
// ---- Stage 3: Sandbox Simulation (The Blackout) ----
// core.Clone() creates a perfect isolated sandbox for destructive analysis
sandbox := g.Clone()
if err = sandbox.RemoveVertex(hubID); err != nil {
fmt.Printf("Critical failure during simulation: %v\n", err)
return
}
// ---- Stage 4: Topological Impact Assessment (BFS) ----
// Expert-grade LCC (Largest Connected Component) calculation
calcLCC := func(graph *core.Graph) int {
var (
maxSize int
allVertices = graph.Vertices()
visited = make(map[string]bool, len(allVertices))
queue = make([]string, 0, len(allVertices))
)
for _, root := range allVertices {
if visited[root] {
continue
}
// Component Discovery
currentSize := 0
queue = append(queue[:0], root) // Reset queue without re-allocating
visited[root] = true
for len(queue) > 0 {
u := queue[0]
queue = queue[1:]
currentSize++
adj, _ := graph.NeighborIDs(u)
for _, v := range adj {
if !visited[v] {
visited[v] = true
queue = append(queue, v)
}
}
}
if currentSize > maxSize {
maxSize = currentSize
}
}
return maxSize
}
nLCC := calcLCC(g) // Giant component before attack
npLCC := calcLCC(sandbox) // Giant component after hub removal
// R = N'_LCC / (N_LCC - 1)
resilience := float64(npLCC) / float64(nLCC-1)
// ---- Stage 5: Executive Decision ----
fmt.Printf("--- Aethelgard Grid Resilience Report ---\n")
fmt.Printf("Target Hub Degree: %d\n", hubDegree)
fmt.Printf("Fragility Index (Φ): %.2f\n", phi)
fmt.Printf("Resilience Ratio (R): %.2f\n", resilience)
if resilience < 0.6 {
fmt.Println("STATUS: CRITICAL. System fragmentation imminent. Initiating bypass protocols.")
} else {
fmt.Println("STATUS: STABLE. Topology supports graceful degradation.")
}
}
Output: --- Aethelgard Grid Resilience Report --- Target Hub Degree: 8 Fragility Index (Φ): -3.00 Resilience Ratio (R): 0.50 STATUS: CRITICAL. System fragmentation imminent. Initiating bypass protocols.
Example (NeuralEvolution) ¶
Example_neuralEvolution simulates dynamic evolution of a neural network graph structure. It starts with a sparse, weighted graph (few connections), then adds a new neuron (vertex) with new connections, and finally removes an existing connection. The degree of a particular neuron is tracked through these modifications to illustrate network plasticity. CONTEXT: "Synapse-X" - The Structural Learning Engine
- In traditional neural networks, "learning" is merely updating weights in a static matrix. In Project Synapse-X, we simulate biological neuroplasticity where the graph itself is a living organism. When associations weaken, synapses are physically destroyed (Pruning) to reclaim memory and reduce entropy. When new concepts emerge, the graph spawns new vertices and edges (Evolution).
Scenario:
- Vertices are neurons (or concepts), edges are synapses (or associations).
- Weights are connection strengths (requires Weighted graph).
- Learning can create new neurons (AddVertex), strengthen/insert synapses (AddEdge), and prune unused synapses (RemoveEdge).
WHY THIS IS CRITICAL (The Engineering Edge):
- Algorithmic Efficiency: In large-scale brains, "zeroing a weight" still keeps the connection in the adjacency list, forcing O(N^2) or O(E_total) scans. Using core.RemoveEdge(id) physically cleans the topology, ensuring neighborhood traversals (via core.AdjacentVertices) only visit active, meaningful synapses.
- Topological Integrity: core.AddVertex(id) allows the network to expand its associative memory dynamically without re-initializing the system.
MATHEMATICAL MODEL (Structural Homeostasis):
- Network Density (D): D = (2 * |E|) / (|V| * (|V| - 1)). The system monitors D to prevent a "connectivity explosion" (over-wiring).
- Pruning Logic: When a synapse decays, the system identifies the topological link via NeighborIDs() and Edge verification, then executes core.RemoveEdge(id) to maintain energy efficiency.
Implementation:
- Stage 1: Build a sparse weighted graph.
- Stage 2: Add a new neuron and connect it.
- Stage 3: Remove one existing edge (synaptic pruning).
- Stage 4: Query Degree at each stage.
Inputs:
- None (uses deterministic graph modifications).
Returns:
- None (prints the tracked degree values).
Errors:
- Any unexpected error is printed and the example returns early.
Complexity:
- Graph updates (add/remove): O(1) each amortized. Degree queries: O(d) per query.
CORE PACKAGE LEVERAGE:
- Targeted Retrieval: Using EdgeBetween(u, v) provides O(1) or O(d) access to specific synapses, avoiding expensive global Edge() scans.
- Amortized O(1) Updates: Add/Remove operations leverage core's map-based architecture for high-frequency structural shifts.
package main
import (
"fmt"
"github.com/lvlath/go/core"
)
func main() {
// ---- PHASE 1: Initial Cognitive Seed (Sparse Substrate) ----
// Initialize an undirected, weighted graph representing the base neural cluster.
g, _ := core.NewGraph(core.WithDirected(false), core.WithWeighted())
// Primary synaptic pathways (Initial Knowledge)
// AddEdge returns (ID, error). We use "_" as we track them by topology later.
_, _ = g.AddEdge("0", "1", 0.5)
_, _ = g.AddEdge("1", "2", 0.8)
_, _ = g.AddEdge("3", "4", 1.2)
// Capture baseline plasticity: connectivity of Neuron "2".
// Degree returns (in, out, total, error).
_, _, degInit, _ := g.Degree("2")
// ---- PHASE 2: Evolutionary Expansion (Learning Spike) ----
// A new concept "5" emerges, forging a strong bond with the existing hub (Neuron 2).
if err := g.AddVertex("5"); err != nil {
return
}
// Forging new synapses based on conceptual proximity.
_, _ = g.AddEdge("5", "2", 0.7)
_, _ = g.AddEdge("5", "4", 0.4)
// Audit: Neuron "2" degree increases as it integrates the new concept.
_, _, degAfterAdd, _ := g.Degree("2")
// ---- PHASE 3: Synaptic Pruning (Homeostatic Optimization) ----
// The system detects that the synapse between "1" and "2" has become "stale".
// To prune it, we surgically identify its ID from the active Edges list.
var targetID string
for _, e := range g.Edges() {
// In an undirected graph, we check both directions for the From/To pair.
if (e.From == "1" && e.To == "2") || (e.From == "2" && e.To == "1") {
targetID = e.ID
break
}
}
// Execute physical decommissioning of the connection.
if targetID != "" {
if err := g.RemoveEdge(targetID); err != nil {
return
}
}
// Final State: The network is optimized and ready for the next learning cycle.
_, _, degAfterRem, _ := g.Degree("2")
// ---- OUTPUT: Structural Pulse Monitoring ----
// This confirms the successful growth and pruning cycles of the system.
fmt.Printf("deg[2][0]=%d\n", degInit)
fmt.Printf("deg[2][1]=%d\n", degAfterAdd)
fmt.Printf("deg[2][2]=%d\n", degAfterRem)
}
Output: deg[2][0]=1 deg[2][1]=2 deg[2][2]=1
Index ¶
- Variables
- type Edge
- type EdgeOption
- type Graph
- func (g *Graph) AddEdge(from, to string, weight float64, opts ...EdgeOption) (string, error)
- func (g *Graph) AddVertex(id string) error
- func (g *Graph) AdjacencyList() map[string][]string
- func (g *Graph) Clear()
- func (g *Graph) Clone() *Graph
- func (g *Graph) CloneEmpty() *Graph
- func (g *Graph) Degree(id string) (in, out, undirected int, err error)
- func (g *Graph) Directed() bool
- func (g *Graph) EdgeCount() int
- func (g *Graph) Edges() []*Edge
- func (g *Graph) FilterEdges(pred func(Edge) bool) ([]Edge, error)
- func (g *Graph) GetEdge(edgeID string) (*Edge, error)
- func (g *Graph) GetNamedEdges() []*Edge
- func (g *Graph) HasDirectedEdges() bool
- func (g *Graph) HasEdge(from, to string) bool
- func (g *Graph) HasVertex(id string) bool
- func (g *Graph) InternalVertices() map[string]*Vertexdeprecated
- func (g *Graph) Looped() bool
- func (g *Graph) MixedEdges() bool
- func (g *Graph) Multigraph() bool
- func (g *Graph) NeighborIDs(id string) ([]string, error)
- func (g *Graph) Neighbors(id string) ([]*Edge, error)
- func (g *Graph) RemoveEdge(eid string) error
- func (g *Graph) RemoveEdgesWhere(pred func(Edge) bool) (int, error)
- func (g *Graph) RemoveVertex(id string) error
- func (g *Graph) SetEdgeID(oldID, newID string) error
- func (g *Graph) Stats() *GraphStats
- func (g *Graph) ValidateEdgeID(ID string) error
- func (g *Graph) VertexCount() int
- func (g *Graph) Vertices() []string
- func (g *Graph) VerticesMap() map[string]*Vertex
- func (g *Graph) Weighted() bool
- type GraphOption
- type GraphStats
- type Nilable
- type Vertex
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrEmptyVertexID signals that the provided vertex identifier is empty. // // Contract: // - Any API that accepts a vertex ID MUST reject "" with this sentinel. ErrEmptyVertexID = errors.New("core: vertex ID is empty") // ErrVertexNotFound indicates that a referenced vertex does not exist. // // Contract: // - Returned by query/mutation APIs that require a pre-existing vertex. ErrVertexNotFound = errors.New("core: vertex not found") // ErrEdgeNotFound indicates that a referenced edge (by Edge.ID) was not found. // // Contract: // - Returned by edge-removal or lookup routines. ErrEdgeNotFound = errors.New("core: edge not found") // ErrBadWeight reports a non-zero weight on an unweighted graph. // // Contract: // - On graphs without WithWeighted(), only weight == 0 is allowed. ErrBadWeight = errors.New("core: bad weight for unweighted graph") ErrNaNInf = errors.New("core: weight is NaN or Inf") // ErrLoopNotAllowed reports a self-loop attempt when loops are disabled. // // Contract: // - WithLoops() must be set to allow edges (v -> v). ErrLoopNotAllowed = errors.New("core: self-loop not allowed") // ErrEmptyEdgeID signals that an explicit edge ID was required but empty. // // Contract: // - AddEdge(..., WithID("")) MUST return ErrEmptyEdgeID. // - SetEdgeID(old,"") MUST return ErrEmptyEdgeID. // // Notes: // - This sentinel is about edge identifiers (Edge.ID), not vertex IDs. ErrEmptyEdgeID = errors.New("core: empty edge ID") // ErrEdgeIDConflict signals that an explicit edge ID collides with an existing edge. // // Contract: // - AddEdge(..., WithID(id)) MUST return ErrEdgeIDConflict if id is already present. // - SetEdgeID(old,id) MUST return ErrEdgeIDConflict if id is already present. // // Determinism: // - Collision checks are pure map membership checks; no iteration order dependence. ErrEdgeIDConflict = errors.New("core: edge ID already exists") // ErrMultiEdgeNotAllowed reports a parallel edge attempt when multi-edges are disabled. // // Contract: // - WithMultiEdges() must be set to allow (u,v) duplication (or directional duplicates). ErrMultiEdgeNotAllowed = errors.New("core: multi-edges not allowed") // ErrMixedEdgesNotAllowed reports a per-edge directedness override on a non-mixed graph. // // Contract: // - WithMixedEdges() (or NewMixedGraph) must be set before any WithEdgeDirected(...) override. ErrMixedEdgesNotAllowed = errors.New("core: mixed-mode per-edge overrides not allowed") // ErrNilGraphOption reports a nil GraphOption passed to NewGraph or NewMixedGraph. // // Contract: // - NewGraph(nil) MUST return ErrNilGraphOption. // - NewGraph(..., nil, ...) MUST return ErrNilGraphOption. // - NewMixedGraph(..., nil, ...) MUST preserve this sentinel via delegation to NewGraph. // // Notes: // - Nil constructor options are invalid public inputs, not no-ops and not panics. ErrNilGraphOption = errors.New("core: nil graph option") // ErrNilEdgeOption reports a nil EdgeOption passed to AddEdge. // // Contract: // - AddEdge(..., nil) MUST return ErrNilEdgeOption. // - AddEdge(..., opt1, nil, opt2) MUST return ErrNilEdgeOption. // // Notes: // - Nil per-edge options are rejected during fail-fast validation before any lock // acquisition, vertex auto-creation, or edge publication. ErrNilEdgeOption = errors.New("core: nil edge option") // ErrInvalidEdgeOption reports that an EdgeOption attempted to mutate // topology-owned edge fields after AddEdge had already validated the public input. // // Contract: // - AddEdge owns Edge.From, Edge.To, and Edge.Weight after initial validation. // - EdgeOption values may set Edge.ID via WithID. // - EdgeOption values may set Edge.Directed only through an explicitly allowed // mixed-mode policy. // - Endpoint or weight mutation is rejected before ID assignment, catalog insertion, // or adjacency mutation. // // Notes: // - Directedness mutation on a non-mixed graph is classified separately as // ErrMixedEdgesNotAllowed because it is a policy violation, not an endpoint/weight // corruption attempt. ErrInvalidEdgeOption = errors.New("core: invalid edge option mutation") // ErrNilEdgePredicate reports a nil predicate passed to an edge-filtering API. // // Contract: // - FilterEdges(nil) MUST return ErrNilEdgePredicate. // - RemoveEdgesWhere(nil) MUST return ErrNilEdgePredicate. // - Nil predicates are invalid public input, not no-ops and not panics. ErrNilEdgePredicate = errors.New("core: nil edge predicate") )
Functions ¶
This section is empty.
Types ¶
type Edge ¶
type Edge struct {
// ID is a unique string identifier for the edge (auto-generated by default, or provided via WithID).
// Once published in a Graph, it MUST be treated as immutable.
ID string
// From is the source vertex ID (for undirected edges this is one endpoint).
// Once published in a Graph, it MUST be treated as immutable.
From string
// To is the destination vertex ID (for undirected edges this is the other endpoint).
// Once published in a Graph, it MUST be treated as immutable.
To string
// Weight is the edge cost/capacity; must be 0 unless the graph is WithWeighted().
// Once published in a Graph, it MUST be treated as immutable.
Weight float64
// Directed = true means the edge is asymmetric (From -> To only).
// Directed = false means the edge is symmetric (From <-> To, adjacency mirrored).
// Once published in a Graph, it MUST be treated as immutable.
Directed bool
}
Edge is the canonical connection record used by Graph; the unique key is Edge.ID. Endpoints are stored as vertex IDs to keep Graph storage compact and deterministic.
Implementation:
- Stage 1: AddEdge constructs a baseline Edge with graph defaults.
- Stage 2: AddEdge applies EdgeOptions sequentially (first error aborts).
- Stage 3: Edge is registered in the edge catalog and adjacency is updated.
Ownership / Mutation Law:
- Graph query methods may return *Edge aliases to catalog records.
- Once published in a Graph, Edge.ID, Edge.From, Edge.To, Edge.Weight, and Edge.Directed MUST be treated as immutable by callers.
- If detached mutable ownership is required, callers must allocate their own copy.
Behavior highlights:
- ID is unique within a graph for the graph lifetime.
- For undirected edges (Directed=false), adjacency is mirrored.
Inputs:
- ID: unique edge identifier; auto-generated unless WithID is used.
- From/To: endpoint vertex IDs.
- Weight: must be 0 unless the graph is WithWeighted().
- Directed: effective directionality (may be overridden per-edge only in mixed mode).
Returns:
- N/A (data type).
Errors:
- N/A (data type).
Determinism:
- Edge.ID is the stable ordering key for public enumeration.
Complexity:
- N/A (data type).
Notes:
- Auto-generated IDs are of the form "eN" where N is a monotonically increasing counter.
AI-Hints:
- Treat returned *Edge pointers from getters as read-only to avoid data races.
func (*Edge) IsNil ¶
IsNil reports whether the receiver should be treated as nil when stored inside interfaces.
Implementation:
- Stage 1: Compare the receiver pointer to nil.
- Stage 2: Return the result without dereferencing.
Behavior highlights:
- Safe for typed-nil stored inside interfaces (no panic).
- Reflect-free nil detection used by validators and test helpers via core.Nilable.
Returns:
- bool: true iff receiver == nil.
Errors:
- None.
Determinism:
- Deterministic.
Complexity:
- Time O(1), Space O(1).
Notes:
- Keep this method trivial; do not add deep validation.
AI-Hints:
- Use Nilable-aware helpers to correctly detect typed nils behind interfaces.
type EdgeOption ¶
EdgeOption configures a single edge during AddEdge.
Implementation:
- Stage 1: AddEdge performs stateless validation, including nil EdgeOption rejection.
- Stage 2: AddEdge builds a baseline Edge from the validated endpoints, weight, and graph default directedness.
- Stage 3: AddEdge applies EdgeOptions sequentially; the first error aborts edge publication.
- Stage 4: AddEdge validates that options did not mutate topology-owned fields before assigning an ID or publishing adjacency.
Behavior highlights:
- Options MUST NOT panic as part of the public contract.
- A nil EdgeOption is rejected with ErrNilEdgeOption before invocation.
- Options MUST NOT mutate global graph state as a side effect.
- Options may set only Edge.ID and, when mixed mode is enabled, Edge.Directed.
- Options MUST NOT mutate Edge.From, Edge.To, or Edge.Weight.
- Options should be O(1) and deterministic.
Inputs:
- g: the owning graph; options may inspect immutable policy flags and the edge catalog only as documented by the specific option.
- e: the unpublished edge being configured.
Returns:
- error: nil on success; otherwise a stable sentinel error (ErrMixedEdgesNotAllowed, ErrEmptyEdgeID, ErrEdgeIDConflict, ErrInvalidEdgeOption, ...).
Errors:
- Nil option values are rejected by AddEdge with ErrNilEdgeOption before invocation.
- Endpoint or weight mutation is rejected by AddEdge with ErrInvalidEdgeOption.
- Directedness override without mixed mode is rejected with ErrMixedEdgesNotAllowed.
Determinism:
- Deterministic given deterministic inputs; option order is call-order stable.
Complexity:
- Time O(1) per option, Space O(1).
Notes:
- AddEdge applies non-nil options under the edge/adjacency write lock; options must not take additional graph locks or call graph methods.
AI-Hints:
- Keep options local: set ID or allowed directedness only; never rewrite endpoints.
func WithEdgeDirected ¶
func WithEdgeDirected(directed bool) EdgeOption
WithEdgeDirected overrides directedness for a single edge.
Implementation:
- Stage 1: Validate that the graph was constructed with WithMixedEdges().
- Stage 2: Assign e.Directed = directed.
Behavior highlights:
- Requires WithMixedEdges mode; otherwise returns ErrMixedEdgesNotAllowed.
Inputs:
- directed: desired directedness for this edge.
Returns:
- EdgeOption: per-edge mutator.
Errors:
- ErrMixedEdgesNotAllowed: if the graph does not allow per-edge directedness overrides.
Determinism:
- Deterministic; constant-time flag set.
Complexity:
- Time O(1), Space O(1).
Notes:
- This option does not change endpoints; only directionality.
AI-Hints:
- Use mixed mode only when you truly need both orientations in one graph.
func WithID ¶
func WithID(id string) EdgeOption
WithID assigns a custom identifier to the edge created by AddEdge.
Implementation:
- Stage 1: Validate id is non-empty (ErrEmptyEdgeID).
- Stage 2: Under AddEdge's edge lock, check the edge catalog for collisions (ErrEdgeIDConflict).
- Stage 3: Set e.ID = id.
- Stage 4: If id matches the canonical auto-ID form "eN", bump the auto-ID counter so that future auto-generated IDs never collide with the explicit "eN".
Behavior highlights:
- Works regardless of mixed mode.
- Deterministic: collision is a strict membership check, not a scan.
Inputs:
- id: desired edge identifier; must be non-empty and globally unique within the graph.
Returns:
- EdgeOption: per-edge mutator.
Errors:
- ErrEmptyEdgeID: if id == "".
- ErrEdgeIDConflict: if id is already present in the edge catalog.
Determinism:
- Deterministic; does not depend on map iteration order.
Complexity:
- Time O(1), Space O(1).
Notes:
- If you choose IDs of the canonical auto form "eN", the graph will advance its auto-ID counter past N.
AI-Hints:
- Use WithID for stable external references (golden tests, trace correlation, interop).
- Prefer non-auto-shaped IDs (e.g., "road_A_B") when you do not want to affect the auto-ID counter.
type Graph ¶
type Graph struct {
// contains filtered or unexported fields
}
Graph is a thread-safe, deterministic in-memory graph storage kernel.
Contract role:
- Graph stores vertices and edges; it does not implement graph algorithms.
- Algorithm packages must consume Graph through documented public methods (Vertices, Edges, Neighbors, NeighborIDs, AdjacencyList, Stats, ...), not by assuming internal map shapes.
Concurrency model:
- muVert protects the vertex catalog and immutable configuration flags.
- muEdgeAdj protects the edge catalog and the private sparse adjacency index.
- If a method needs both locks, it must acquire muVert before muEdgeAdj.
- Edge-only mutations do not acquire muVert because they do not change vertex membership.
Storage model:
- vertices is the authoritative vertex catalog: if vertices[id] exists, the vertex exists.
- edges is the authoritative edge catalog: if edges[eid] exists, the edge exists.
- adjacencyList is a private sparse edge index: fromID -> toID -> edgeID -> unit.
- adjacencyList is NOT a complete vertex mirror. Isolated vertices may have no top-level adjacency bucket internally.
- Missing adjacencyList[id] means “no indexed outgoing/incident bucket currently stored”, not “vertex id is absent”. Vertex existence is resolved through vertices.
Public adjacency surfaces:
- Use (*Graph).AdjacencyList() for a full graph-facing adjacency snapshot. It includes every current vertex from vertices, including isolated vertices with empty slices.
- Use package matrix helpers for matrix-shaped representations, e.g. matrix.NewAdjacencyMatrix(graph, options) or matrix.BuildDenseAdjacency(vertices, edges, options).
Configuration flags:
- directed/weighted/allowMulti/allowLoops/allowMixed are set during construction and treated as immutable afterwards.
ID generation:
- Auto edge IDs are "eN" where N is a monotonically increasing counter.
- WithID and SetEdgeID bump the counter when assigning canonical "eN" IDs.
Determinism:
- Public enumeration order is stable and documented at package level.
- Internal map iteration order is never part of the contract.
Notes:
- Clone/view code must preserve edge IDs and nextEdgeID to prevent future collisions.
- Do not add public APIs that expose adjacencyList directly; it is an implementation index.
AI-Hints:
- Do not “fix” isolated vertices by storing fake adjacencyList[id][id] empty buckets.
- Do not read or write adjacencyList without muEdgeAdj.
- Do not read or write vertices without muVert.
func InducedSubgraph ¶
InducedSubgraph creates a non-mutating induced subgraph containing only vertices selected by keep.
Implementation:
- Stage 1: Acquire GLOBAL READ LOCK (Vert + EdgeAdj).
- Stage 2: Filter and copy vertices.
- Stage 3: Filter and copy edges (only if both endpoints exist in 'keep').
Concurrency snapshot:
- Atomic: Prevents "phantom edges" where an endpoint might be deleted concurrently.
Behavior highlights:
- Does not mutate the source graph.
- Preserves Edge.ID, Directed, and Weight for retained edges.
- Drops all edges that cross the cut (one endpoint not kept).
Inputs:
- g: source graph (must be non-nil by caller convention).
- keep: map of vertex IDs to retain; keep[id]==true means "retain id".
Returns:
- *Graph: a new graph containing only the induced topology.
Errors:
- None.
Determinism:
- Deterministic for a fixed source snapshot; uses read locks for stable catalogs.
Complexity:
- Time O(V+E), Space O(V'+E') where V'/E' are retained sizes.
Notes:
- The order of iteration over keep is irrelevant; retention is membership-based.
- Vertex.Metadata is shallow-copied (pointer copy).
AI-Hints:
- Use InducedSubgraph to focus algorithms on a region of interest without changing the original graph.
func NewGraph ¶
func NewGraph(opts ...GraphOption) (*Graph, error)
NewGraph constructs an empty graph and applies GraphOption values deterministically.
Implementation:
- Stage 1: Validate the option list shape and reject nil GraphOption values.
- Stage 2: Allocate empty vertex/edge catalogs and adjacency map.
- Stage 3: Apply options in call order (left-to-right).
- Stage 4: Publish the fully configured graph on success only.
Behavior highlights:
- Construction-time options only; subsequent behavior is controlled by method contracts.
- nextEdgeID starts at 0; the first auto-generated edge becomes "e1".
- Constructor validation is part of the public contract; no panic-based option handling.
Inputs:
- opts: zero or more GraphOption values applied in call order.
Returns:
- *Graph: configured empty graph on success.
- error: nil on success; otherwise a stable sentinel error.
Errors:
- ErrNilGraphOption: if opts contains a nil GraphOption value.
- Any stable sentinel returned by a provided GraphOption.
Determinism:
- Deterministic option validation order and deterministic option application order.
Complexity:
- Time O(len(opts)), Space O(1) excluding map growth.
Notes:
- Nil GraphOption validation happens before graph allocation and publication.
- On error, no partially configured graph is returned to the caller.
AI-Hints:
- Keep option sets explicit to make tests reproducible.
- Check the returned error with errors.Is; do not rely on panic recovery.
func NewMixedGraph ¶
func NewMixedGraph(opts ...GraphOption) (*Graph, error)
NewMixedGraph creates a new Graph that allows per-edge directedness overrides via EdgeOption, while preserving deterministic option application order.
Implementation:
- Stage 1: Prepend WithMixedEdges() to the caller-provided options.
- Stage 2: Delegate to NewGraph(...) to validate and apply options deterministically.
Behavior highlights:
- Enables WithEdgeDirected(...) on AddEdge; without mixed-mode this is rejected.
- Does not mutate the caller's opts slice (no hidden side-effects).
Inputs:
- opts: additional GraphOption values applied after enabling mixed-mode.
Returns:
- *Graph: a fresh configured instance with allowMixed enabled.
- error: nil on success; otherwise a stable sentinel error.
Errors:
- ErrNilGraphOption: if opts contains a nil GraphOption value.
- Any stable sentinel returned by a provided GraphOption via NewGraph.
Determinism:
- Options are applied left-to-right, with WithMixedEdges() always first.
Complexity:
- Time O(len(opts)), Space O(len(opts)) for the composed options slice.
Notes:
- Prefer this constructor when you plan to mix directed and undirected edges in one graph.
AI-Hints:
- Use NewMixedGraph(...) instead of remembering to prepend WithMixedEdges() manually.
func UnweightedView ¶
UnweightedView creates a non-mutating view of the input graph where weights are forced to 0 and the resulting graph reports Weighted()==false, while preserving topology and IDs.
Implementation:
- Stage 1: Acquire GLOBAL READ LOCK (Vert + EdgeAdj) to ensure atomic snapshot.
- Stage 2: Copy vertices (shallow Metadata).
- Stage 3: Copy edges (forcing Weight=0).
- Stage 4: Release locks.
Concurrency snapshot:
- Atomic: The view represents a consistent state at the moment of creation.
- No "gap" between vertex and edge copying.
Behavior highlights:
- Does not mutate the source graph.
- Preserves Edge.ID and Directed for every edge; only Weight is changed.
- Preserves determinism rules of the core package (ordering is defined by public APIs).
Inputs:
- g: source graph (must be non-nil by caller convention).
Returns:
- *Graph: a new graph instance with identical topology and Weight forced to zero.
Errors:
- None.
Determinism:
- Deterministic for a fixed source snapshot; uses read locks for stable catalogs.
Complexity:
- Time O(V+E), Space O(V+E) for the copied catalogs.
Notes:
- This is a *view* implemented as a copy: the returned graph is independent and mutable.
- Vertex.Metadata is shallow-copied (pointer copy); if deep-copy is required, callers must do it externally.
AI-Hints:
- Use UnweightedView when an algorithm requires zero weights but you must preserve original weights elsewhere.
func (*Graph) AddEdge ¶
AddEdge creates a new edge from `from` to `to` with an optional weight and per-edge options. The method enforces graph capabilities (weighted, multi-edge, loop, and mixed-edge policy) and publishes catalog/adjoining adjacency state as one topology transaction.
Transactional Safety:
- This operation is TOPOLOGICALLY ATOMIC for edge publication.
- It acquires muVert.Lock() and muEdgeAdj.Lock() in that order.
- While both locks are held, endpoints cannot be concurrently removed and the edge catalog/adjoining adjacency buckets cannot be observed half-published.
- If endpoints are missing, they are created as part of the same transaction before edge publication.
Implementation:
- Stage 1: Validate stateless public inputs: endpoint IDs, weight policy, loop policy, and nil EdgeOption values.
- Stage 2: Acquire muVert.Lock() to start the topology transaction.
- Stage 3: Ensure `from` and `to` exist in the vertex catalog.
- Stage 4: Acquire muEdgeAdj.Lock() to protect edge catalog and adjacency mutation.
- Stage 5: Reject forbidden parallel edges before allocation/publication.
- Stage 6: Build a baseline unpublished Edge from validated endpoints, weight, and graph default directedness.
- Stage 7: Apply EdgeOptions in call order.
- Stage 8: Re-validate option effects: endpoints and weight are immutable; directedness override requires mixed mode; loop policy remains enforced.
- Stage 9: Assign an explicit or generated Edge.ID and bump the auto-ID counter when needed.
- Stage 10: Publish the edge in the catalog and update primary/mirrored adjacency buckets.
- Stage 11: Release locks through deferred unlocks.
Behavior highlights:
- Strict sentinel errors only (classify with errors.Is).
- No panics as part of the public contract.
- Nil EdgeOption values fail fast before lock acquisition, vertex auto-creation, or adjacency mutation.
- Endpoint and weight mutation by custom EdgeOption values is rejected before edge publication.
- Directedness mutation is allowed only when mixed mode is enabled.
- No lock gap exists between endpoint creation and edge insertion.
Inputs:
- from: source vertex ID; must be non-empty.
- to: destination vertex ID; must be non-empty.
- weight: edge weight; must be finite and must be 0 unless WithWeighted() was set.
- opts: optional per-edge mutators; each value must be non-nil.
Returns:
- string: assigned Edge.ID (auto-generated or explicit).
- error: nil on success; otherwise a sentinel error.
Errors:
- ErrEmptyVertexID: if from == "" or to == "".
- ErrBadWeight: if weight != 0 on an unweighted graph.
- ErrNaNInf: if weight is NaN/Inf.
- ErrLoopNotAllowed: if from == to and loops are disabled.
- ErrNilEdgeOption: if opts contains a nil EdgeOption value.
- ErrMultiEdgeNotAllowed: if a parallel edge is attempted and multi-edges are disabled.
- ErrInvalidEdgeOption: if an EdgeOption mutates Edge.From, Edge.To, or Edge.Weight.
- ErrMixedEdgesNotAllowed: if directedness is changed without mixed mode.
- ErrEmptyEdgeID / ErrEdgeIDConflict: from WithID / ValidateEdgeID rules.
Determinism:
- Option validation order is stable (call order).
- Option application order is stable (call order).
- Auto-ID assignment uses a monotonic counter (no randomness, no time).
Complexity:
- Time O(1) amortized.
- Space O(1) amortized.
Notes:
- AddEdge may auto-create missing endpoint vertices before a later EdgeOption error occurs. Such vertices are valid catalog state; the edge itself is not published on error.
- Vertices are created while muVert is held to prevent concurrent RemoveVertex from deleting endpoints during edge publication.
AI-Hints:
- Use WithID for stable cross-run edge references.
- For undirected edges, adjacency is mirrored automatically.
- Do not write custom EdgeOption values that rewrite endpoints or weights.
func (*Graph) AddVertex ¶
AddVertex inserts a vertex if missing.
Implementation:
- Stage 1: Validate non-empty ID (ErrEmptyVertexID).
- Stage 2: Acquire muVert write lock.
- Stage 3: If the ID already exists, return nil without mutation.
- Stage 4: Allocate and publish a Vertex record in the authoritative vertex catalog.
Behavior highlights:
- Idempotent: adding an existing vertex is a no-op.
- Initializes Metadata to a non-nil map for convenient caller use.
- Does not create adjacency buckets. Internal adjacencyList is a sparse edge index; edge insertion creates buckets lazily.
Inputs:
- id: vertex identifier; must be non-empty.
Returns:
- error: nil on success; otherwise a sentinel error.
Errors:
- ErrEmptyVertexID: if id == "".
Determinism:
- Deterministic map membership/update logic; no iteration-order dependency.
Complexity:
- Time O(1) amortized, Space O(1) amortized.
Notes:
- Vertex existence is represented only by g.vertices.
- Public AdjacencyList() will still include this vertex with an empty slice until incident edges are added.
AI-Hints:
- Use AddVertex when an isolated vertex is semantically meaningful.
- Prefer AddVertex in setup when you want explicit vertex presence before adding edges.
func (*Graph) AdjacencyList ¶
AdjacencyList returns a snapshot mapping each "from" vertex ID to the list of incident edge IDs. Each slice is sorted by Edge.ID ascending for deterministic per-vertex enumeration.
Storage distinction:
- Internal g.adjacencyList is a sparse edge index and may omit isolated vertices.
- This public method reconstructs the complete vertex-domain view from g.vertices.
Implementation:
- Stage 1: Acquire muVert.RLock(), then muEdgeAdj.RLock() in package lock order.
- Stage 2: Allocate a result map with one key for every current vertex.
- Stage 3: Scan the sparse adjacency index and append existing edge IDs into the corresponding result[from] slice.
- Stage 4: Sort each per-vertex edge-ID slice by Edge.ID ascending.
- Stage 5: Return the detached map/slice snapshot.
Behavior highlights:
- Includes isolated vertices with empty slices.
- Returned map and slices are detached containers; callers may mutate them safely.
- Edge IDs identify catalog edges; callers can use GetEdge if they need edge records.
- Does not expose internal adjacency maps.
Returns:
- map[string][]string: vertex ID -> sorted edge IDs for the current snapshot.
Errors:
- None (pure query).
Determinism:
- Per-vertex slices are sorted by Edge.ID ascending.
- Map key iteration remains Go-map order; use Vertices() for deterministic key traversal.
Complexity:
- Time O(V + A + Σ sort(d(v))), where A is indexed adjacency entries.
- Space O(V + A) for detached containers.
Notes:
- For matrix-shaped adjacency, use the matrix package rather than depending on this storage snapshot as a dense representation.
- Use Vertices() to obtain deterministic key order if needed.
AI-Hints:
- If you need stable iteration over keys, do: keys := g.Vertices(); then read result[key].
- This is the right API when tests must prove isolated vertices are still present.
- Do not “repair” internal adjacencyList to include isolated vertices; this method handles that boundary.
func (*Graph) Clear ¶
func (g *Graph) Clear()
Clear resets the graph to an empty state while preserving configuration flags.
Implementation:
- Stage 1: Acquire muVert and muEdgeAdj write locks to perform an atomic reset.
- Stage 2: Reinitialize vertices, edges, and adjacencyList maps.
- Stage 3: Reset nextEdgeID to 0 (future auto edge IDs resume from "e1").
- Stage 4: Release locks.
Behavior highlights:
- Preserves flags: Directed default, Weighted, MultiEdges, Loops, MixedMode.
- Drops all vertices and edges.
- Resets ID counter deterministically.
Inputs:
- None.
Returns:
- None.
Errors:
- None.
Determinism:
- Deterministic.
Complexity:
- Time O(1) for map reallocation, Space O(1) new empty maps (old maps become GC-eligible).
Notes:
- Not safe to call concurrently with readers/writers; it acquires both write locks.
- After Clear(), the graph is equivalent to a newly constructed graph with the same options.
AI-Hints:
- Prefer Clear() for reuse in benchmarks/tests to avoid repeated allocations from NewGraph.
func (*Graph) Clone ¶
Clone returns a deep topology copy of the Graph: configuration, vertices, edges, and adjacency.
Implementation:
- Stage 1: Acquire BOTH muVert and muEdgeAdj Rlocks (atomic snapshot).
- Stage 2: Create new Graph and copy configuration.
- Stage 3: Copy vertices (shallow metadata).
- Stage 4: Copy edges and rebuild adjacency.
- Stage 5: Release locks.
Behavior highlights:
- ATOMIC: The clone represents the graph state at a single instant.
- Preserves Edge.ID, endpoints, weights, and directedness.
- Vertex.Metadata is shallow-copied (shared pointer).
Inputs:
- None.
Returns:
- *Graph: cloned graph instance.
Errors:
- None.
Determinism:
- Deterministic for a fixed source graph state.
Complexity:
- Time O(V + E), Space O(V + E).
Notes:
- Edges are copied as new Edge structs; callers must still treat returned edges as immutable.
- nextEdgeID carry-over ensures future AddEdge IDs remain monotonic on the clone.
AI-Hints:
- Use Clone when algorithms need a sandbox graph to mutate without affecting the original.
func (*Graph) CloneEmpty ¶
CloneEmpty returns a new Graph with identical configuration and vertices, but no edges.
Implementation:
- Stage 1: Acquire muVert and muEdgeAdj read locks to snapshot flags and vertex catalog safely.
- Stage 2: Construct a new Graph with equivalent GraphOptions (flags only).
- Stage 3: Carry over nextEdgeID to preserve the textual edge ID sequence on the clone.
- Stage 4: Copy vertices (shallow metadata pointer copy) and initialize empty per-vertex adjacency maps.
- Stage 5: Return the clone.
Behavior highlights:
- Preserves configuration flags (Directed default, Weighted, MultiEdges, Loops, MixedMode).
- Preserves vertex identities.
- Drops all edges (edge catalog and adjacency remain empty).
Inputs:
- None.
Returns:
- *Graph: new graph instance with copied flags and vertices, no edges.
Errors:
- None.
Determinism:
- Deterministic for a fixed source graph state.
Complexity:
- Time O(V), Space O(V).
Notes:
- Vertex.Metadata is shallow-copied (the map pointer is reused). If deep copy is required, callers must implement it externally.
- nextEdgeID is carried over to prevent future auto-generated IDs from "rewinding".
AI-Hints:
- Use CloneEmpty when you need the same vertex universe but want to rebuild topology from scratch.
func (*Graph) Degree ¶
Degree returns the degree components of the given vertex ID.
Math Policy (Contract Anchor):
- Directed edges contribute to in/out only.
- Undirected edges contribute to undirected only.
- Directed self-loop (id -> id): Contributes +1 to 'in' AND +1 to 'out'.
- Undirected self-loop (id - id): Contributes +2 to 'undirected' (classic graph theory).
Implementation:
- Stage 1: Validate id and vertex existence under locks.
- Stage 2: Scan ALL graph edges (g.edges) to identify incident connections. This is an O(E) operation required to correctly calculate in-degree for directed edges without maintaining a separate expensive reverse index.
Inputs:
- id: vertex identifier.
Returns:
- in: number of incoming directed edges (e.To == id)
- out: number of outgoing directed edges (e.From == id)
- undirected: contribution from undirected edges
- err: ErrEmptyVertexID or ErrVertexNotFound.
Errors:
- ErrEmptyVertexID: if id is empty.
- ErrVertexNotFound: if the vertex does not exist in the graph.
Determinism:
- Deterministic result (counting is order-independent).
Complexity:
- Time O(E), Space O(1), where E is the total number of edges in the graph. Note: This is an O(E) operation to ensure correct in-degree calculation without maintaining a separate reverse index.
Notes:
- This method acquires global read locks on vertices and edges.
AI-Hints:
- Directed self-loops increase the total degree sum by 2 (1 in + 1 out).
- Undirected self-loops increase the total degree sum by 2 (2 undirected).
- Use Degree() when you need loop-aware, policy-defined degree semantics.
- Be aware of O(E) cost on very large graphs.
func (*Graph) Directed ¶
Directed reports the graph-wide default directedness applied to newly created edges. Per-edge overrides require mixed-mode (MixedEdges()==true).
Implementation:
- Stage 1: Acquire muVert read lock to observe configuration consistently.
- Stage 2: Return the immutable default directedness flag.
Behavior highlights:
- Pure policy query: does not scan edges.
Returns:
- bool: true if new edges default to directed orientation.
Errors:
- None.
Determinism:
- Deterministic for a fixed graph instance.
Complexity:
- Time O(1), Space O(1).
Notes:
- This does not indicate whether the graph currently contains directed edges (use HasDirectedEdges() or Stats().DirectedEdgeCount for that).
AI-Hints:
- Use g.Directed() to decide default edge semantics when generating topology programmatically.
func (*Graph) EdgeCount ¶
EdgeCount returns the current number of edges in the graph.
Implementation:
- Stage 1: Acquire muEdgeAdj read lock for a consistent catalog snapshot.
- Stage 2: Return len(g.edges).
Behavior highlights:
- O(1) fast-path.
- No allocations.
- Independent of map iteration order.
Inputs:
- None.
Returns:
- int: number of edges currently present.
Errors:
- None (pure query).
Determinism:
- Deterministic for a fixed graph state.
Complexity:
- Time O(1), Space O(1).
Notes:
- Counts the edge catalog entries; undirected edges still count as 1 edge.
AI-Hints:
- Prefer EdgeCount() over len(Edges()) to avoid O(E log E) sorting cost.
func (*Graph) Edges ¶
Edges returns all edges sorted by Edge.ID ascending (stable, deterministic order).
Implementation:
- Stage 1: Acquire muEdgeAdj read lock for a stable catalog snapshot.
- Stage 2: Copy all *Edge pointers into a pre-sized slice.
- Stage 3: Sort the slice by Edge.ID ascending.
- Stage 4: Return the sorted slice.
Behavior highlights:
- Deterministic ordering independent of Go map iteration order.
- Returns pointers to live catalog edges (read-only by convention).
Inputs:
- None.
Returns:
- []*Edge: all edges sorted by Edge.ID ascending.
Errors:
- None (pure query).
Determinism:
- Deterministic order by contract: Edge.ID ascending.
Complexity:
- Time O(E log E), Space O(E).
Notes:
- Prefer EdgeCount() when you need only counts (O(1) vs O(E log E)).
- The returned slice is newly allocated; callers may retain and reorder it safely.
- Do not mutate the returned *Edge objects.
AI-Hints:
- Use Edges() for stable logs/golden outputs and deterministic diffing.
- If you need only IDs, consider extracting IDs and comparing sorted slices in tests.
func (*Graph) FilterEdges ¶
FilterEdges returns detached copies of all edges for which pred returns true.
Implementation:
- Stage 1: Reject nil predicate with ErrNilEdgePredicate.
- Stage 2: Acquire muEdgeAdj.RLock() for a stable edge-catalog snapshot.
- Stage 3: Scan the edge catalog and pass detached Edge values to pred.
- Stage 4: Append matching copies to the result slice.
- Stage 5: Sort returned copies by Edge.ID ascending.
Behavior highlights:
- Read-only query; does not mutate edge catalog or adjacency index.
- Predicate receives detached Edge values, not live catalog pointers.
- Returned slice and Edge values are caller-owned copies.
Inputs:
- pred: non-nil pure predicate over an Edge value copy.
Returns:
- []Edge: matching detached edge copies sorted by Edge.ID ascending.
- error: nil on success; otherwise a sentinel error.
Errors:
- ErrNilEdgePredicate: if pred == nil.
Determinism:
- Output order is deterministic: Edge.ID ascending.
- Predicate call order is not part of the contract.
Complexity:
- Time O(E log E) due to final sorting, Space O(k) for k matches.
Notes:
- Use RemoveEdgesWhere for mutating bulk deletion.
AI-Hints:
- Do not reintroduce destructive filtering under this name; FilterEdges is a query surface.
- If caller needs pointers, they can GetEdge(match.ID) explicitly after filtering.
func (*Graph) GetEdge ¶
GetEdge returns the edge with the given edgeID, or ErrEdgeNotFound if absent.
Implementation:
- Stage 1: Acquire muEdgeAdj read lock for a consistent catalog snapshot.
- Stage 2: Lookup g.edges[edgeID].
- Stage 3: Return the pointer (read-only by convention) or ErrEdgeNotFound.
Behavior highlights:
- O(1) average lookup time.
- Does not allocate.
- Does not mutate graph state.
Inputs:
- edgeID: edge identifier (Edge.ID) to lookup.
Returns:
- *Edge: pointer to the cataloged edge (treat as read-only).
- error: nil on success; ErrEdgeNotFound if missing.
Errors:
- ErrEdgeNotFound: if edgeID is not present in the edge catalog.
Determinism:
- Deterministic for a fixed graph state (pure map membership check).
Complexity:
- Time O(1) average, Space O(1).
Notes:
- The returned pointer aliases the live catalog object.
- Edge.ID, Edge.From, Edge.To, Edge.Weight, and Edge.Directed MUST be treated as immutable once the edge is published in the graph.
- Retaining the pointer does not pin membership and does not extend graph locks.
- If detached mutable ownership is required, copy the Edge value externally.
AI-Hints:
- Use errors.Is(err, ErrEdgeNotFound) to branch without string matching.
- Prefer GetEdge over scanning Edges() when you already have the ID.
func (*Graph) GetNamedEdges ¶
GetNamedEdges returns all edges whose ID is not in the canonical auto-generated “eN” form.
Implementation:
- Stage 1: Acquire muEdgeAdj read lock.
- Stage 2: Filter edges whose IDs do not match matchesAutoIDPattern.
- Stage 3: Sort results by Edge.ID asc for deterministic order.
Behavior highlights: - Deterministic output order (Edge.ID lex asc). - The returned slice container is freshly allocated. - Slice elements alias live catalog edges. - Structural edge fields MUST be treated as immutable once published in a graph.
Returns:
- []*Edge: edges with non-auto-shaped IDs, sorted by ID.
Determinism:
- Deterministic output order (lexicographic ID sort).
Complexity:
- Time O(E log E), Space O(E).
NOTES:
- Reordering or truncating the returned slice does not mutate the graph.
- Retaining a returned *Edge does not pin graph membership or extend graph locks.
- Use external value copies if detached mutable edge ownership is required.
func (*Graph) HasDirectedEdges ¶
HasDirectedEdges reports whether at least one edge with Directed == true exists.
Implementation:
- Stage 1: Acquire muEdgeAdj read lock for a stable catalog snapshot.
- Stage 2: Scan edge catalog and return true on first directed edge.
- Stage 3: If none found, return false.
Behavior highlights:
- Early-exit scan (best-case O(1) if a directed edge is found early).
- Does not allocate.
Inputs:
- None.
Returns:
- bool: true if any directed edge exists; otherwise false.
Errors:
- None (pure query).
Determinism:
- Deterministic for a fixed graph state (existence check).
Complexity:
- Time O(E) worst-case, Space O(1).
Notes:
- In mixed-mode graphs, this can be true even if the default orientation is undirected.
AI-Hints:
- Use HasDirectedEdges() as a cheap gate for algorithms that need directed semantics.
func (*Graph) HasEdge ¶
HasEdge reports whether at least one edge exists from 'from' to 'to'. It is safe to call with unknown vertex IDs; missing adjacency buckets return false. Implementation:
- Stage 1: Reject empty IDs early (fast-path, no locks).
- Stage 2: Acquire muEdgeAdj read lock for a stable adjacency snapshot.
- Stage 3: Probe adjacency buckets defensively and return membership.
Behavior highlights:
- Undirected edges are mirrored on insertion, so HasEdge works in both directions.
- Missing vertices or missing adjacency buckets return false (never panic).
Inputs:
- from: source vertex ID (non-empty for meaningful queries).
- to: destination vertex ID (non-empty for meaningful queries).
Returns:
- bool: true if at least one edge from->to exists, otherwise false.
Errors:
- N/A (pure query; never returns sentinel errors).
Determinism:
- Deterministic; relies only on map membership (no iteration order).
Complexity:
- Time O(1), Space O(1).
Notes:
- Uses adjacency buckets instead of scanning edges for performance.
AI-Hints:
- Use HasEdge as an O(1) membership check; do not build temporary slices just to test existence.
- For undirected graphs, checking (to,from) is equivalent due to mirrored adjacency.
func (*Graph) HasVertex ¶
HasVertex reports whether the vertex ID exists (empty ID ⇒ false).
Implementation:
- Stage 1: Reject empty ID (fast-path).
- Stage 2: Acquire muVert read lock and check catalog membership.
Inputs:
- id: vertex identifier.
Returns:
- bool: true iff the vertex is present.
Errors:
- None (pure query).
Determinism:
- Deterministic; map membership only.
Complexity:
- Time O(1), Space O(1).
AI-Hints:
- Use HasVertex as a cheap admission check before operations that do not auto-create vertices.
func (*Graph) InternalVertices
deprecated
InternalVertices returns a compatibility copy of the vertex catalog.
Deprecated:
- Historical versions exposed the live internal vertices map directly.
- That behavior bypassed synchronization and allowed catalog-membership mutation outside graph methods.
- The method is retained only as a compatibility alias and now delegates to VerticesMap().
Behavior highlights:
- The returned map container is detached from the graph.
- The contained *Vertex values still alias catalog records.
Notes:
- Prefer VerticesMap() in new code.
- This method is intentionally no longer an internal-storage escape hatch.
AI-Hints:
- Do not write code that depends on InternalVertices mutating graph membership.
func (*Graph) Looped ¶
Looped reports whether self-loops (from==to) are permitted by policy. If false, AddEdge(v,v,...) rejects the operation with ErrLoopNotAllowed.
Implementation:
- Stage 1: Acquire muVert read lock to observe configuration consistently.
- Stage 2: Return the immutable loops policy flag.
Returns:
- bool: true if self-loops are permitted.
Errors:
- None.
Determinism:
- Deterministic for a fixed graph instance.
Complexity:
- Time O(1), Space O(1).
Notes:
- This is a policy flag; existing self-loops can only exist if this was enabled at creation time.
AI-Hints:
- Gate loop-sensitive algorithms by g.Looped() before assuming v->v edges may exist.
func (*Graph) MixedEdges ¶
MixedEdges reports whether per-edge Directed overrides are permitted via EdgeOption (specifically WithEdgeDirected(...)) during AddEdge.
Implementation:
- Stage 1: Acquire muVert read lock to observe configuration consistently.
- Stage 2: Return the immutable mixed-mode policy flag.
Returns:
- bool: true if per-edge Directed overrides are permitted.
Errors:
- None.
Determinism:
- Deterministic for a fixed graph instance.
Complexity:
- Time O(1), Space O(1).
Notes:
- If false, AddEdge(..., WithEdgeDirected(...)) returns ErrMixedEdgesNotAllowed.
AI-Hints:
- Prefer NewMixedGraph(...) when you intend to mix directed and undirected edges in one instance.
func (*Graph) Multigraph ¶
Multigraph reports whether parallel edges between the same endpoints are permitted by policy. If false, AddEdge(from,to,...) rejects duplicates with ErrMultiEdgeNotAllowed.
Implementation:
- Stage 1: Acquire muVert read lock to observe configuration consistently.
- Stage 2: Return the immutable multi-edge policy flag.
Returns:
- bool: true if parallel edges are permitted.
Errors:
- None.
Determinism:
- Deterministic for a fixed graph instance.
Complexity:
- Time O(1), Space O(1).
Notes:
- Multi-edge checks in AddEdge are membership checks over adjacency buckets.
AI-Hints:
- If you need multi-edges, enable WithMultiEdges() at construction time; this is immutable later.
func (*Graph) NeighborIDs ¶
NeighborIDs returns the unique set of vertex IDs adjacent to id, sorted lexicographically ascending.
Adjacency policy:
- For each edge returned by Neighbors(id):
- If e.From == id, include e.To.
- Else if !e.Directed and e.To == id, include e.From.
Implementation:
- Stage 1: Call Neighbors(id) to obtain incident edges and enforce validation.
- Stage 2: Build a set of adjacent vertex IDs.
- Stage 3: Convert the set to a slice and sort lexicographically.
- Stage 4: Return the sorted slice.
Behavior highlights:
- Unique output: duplicates are removed.
- Deterministic output order (lex asc).
Inputs:
- id: vertex identifier.
Returns:
- []string: unique adjacent vertex IDs, sorted lex asc.
- error: propagated from Neighbors(id).
Errors:
- Propagates ErrEmptyVertexID / ErrVertexNotFound from Neighbors(id).
Determinism:
- Deterministic output order by contract (lex asc).
Complexity:
- Time O(d + k log k), Space O(k), where d is incident edges and k is unique neighbors.
Notes:
- For directed edges, only outgoing neighbors are included (consistent with Neighbors policy).
AI-Hints:
- Use NeighborIDs when building traversal frontiers to avoid edge duplication.
- If you need both in/out neighbors for directed graphs, define a dedicated API explicitly.
func (*Graph) Neighbors ¶
Neighbors returns all edges incident to the given vertex id under the graph's neighborhood policy.
Neighborhood policy:
- Directed edges: include only edges with e.From == id (outgoing edges).
- Undirected edges: include incident edges (mirrored adjacency is used); self-loops appear once.
Implementation:
- Stage 1: Validate id is non-empty (ErrEmptyVertexID).
- Stage 2: Acquire muVert read lock and muEdgeAdj read lock (in that order) for a consistent snapshot.
- Stage 3: Validate vertex existence (ErrVertexNotFound).
- Stage 4: Collect incident edges by scanning adjacencyList[id] buckets and mapping edge IDs to *Edge.
- Stage 5: Sort the result by Edge.ID ascending.
- Stage 6: Return the sorted slice.
Behavior highlights:
- Deterministic ordering by Edge.ID ascending.
- The returned slice container is detached and safe to retain, re-slice, or reorder locally.
- The slice elements alias live catalog edges.
- Edge.ID, Edge.From, Edge.To, Edge.Weight, and Edge.Directed MUST be treated as immutable once an edge is published in a graph.
- Safe against concurrent vertex removal due to consistent lock ordering.
Inputs:
- id: vertex identifier.
Returns:
- []*Edge: incident edges under the defined policy, sorted by Edge.ID asc.
- error: nil on success; otherwise a sentinel error.
Errors:
- ErrEmptyVertexID: if id == "".
- ErrVertexNotFound: if the vertex does not exist.
Determinism:
- Deterministic order by contract: Edge.ID ascending.
Complexity:
- Time O(d log d), Space O(d), where d is the number of incident edges collected.
Notes:
- This is a zero-copy inspection surface for expert callers.
- Retaining a returned *Edge does not pin graph membership or extend graph locks.
- If detached mutable edge state is required, copy the Edge value externally.
AI-Hints:
- Use Neighbors(id) for deterministic iteration in algorithms.
- Use NeighborIDs(id) when you need unique adjacent vertex IDs rather than edges.
func (*Graph) RemoveEdge ¶
RemoveEdge deletes one edge by ID and unlinks its sparse adjacency references.
Implementation:
- Stage 1: Validate non-empty edge ID (ErrEmptyEdgeID).
- Stage 2: Acquire muEdgeAdj.Lock() because only edge catalog and adjacency index mutate.
- Stage 3: Lookup the edge in the authoritative edge catalog.
- Stage 4: Delete the edge catalog entry and remove its adjacency references.
- Stage 5: Prune empty sparse adjacency buckets.
Behavior highlights:
- Does not remove endpoint vertices, even if they become isolated.
- Does not acquire muVert because vertex membership is unchanged.
- Leaves public AdjacencyList() able to report isolated endpoints through g.vertices.
Inputs:
- eid: edge identifier; must be non-empty.
Returns:
- error: nil on success; otherwise a sentinel error.
Errors:
- ErrEmptyEdgeID: if eid == "".
- ErrEdgeNotFound: if no edge with eid exists.
Determinism:
- Deterministic final graph state; no iteration-order dependency.
Complexity:
- Time O(B) because cleanup may scan sparse buckets; O(1) average without cleanup cost.
- Space O(1).
Notes:
- Edge deletion changes edge topology, not vertex membership.
AI-Hints:
- Do not add muVert locking here; it creates unnecessary contention and can invert lock order.
- Removing the last incident edge of a vertex must not remove the vertex from g.vertices.
func (*Graph) RemoveEdgesWhere ¶
RemoveEdgesWhere removes every edge for which pred returns true.
Implementation:
- Stage 1: Reject nil predicate with ErrNilEdgePredicate.
- Stage 2: Acquire muEdgeAdj write lock for an atomic removal pass.
- Stage 3: Scan the edge catalog once.
- Stage 4: Pass each predicate a detached Edge value copy.
- Stage 5: For matching edges, remove adjacency and delete from the catalog.
- Stage 6: Cleanup empty adjacency buckets once after the bulk mutation.
Behavior highlights:
- Removes matching edges; keeps non-matching edges.
- The predicate receives a value copy, so it cannot mutate cataloged edge fields.
- The graph remains catalog/adjacency-consistent after every successful call.
- Predicate execution occurs while muEdgeAdj is held; predicates must be pure and must not call graph methods or try to mutate the graph.
Inputs:
- pred: non-nil pure predicate over a detached Edge value.
Returns:
- int: number of removed edges.
- error: nil on success; otherwise a sentinel error.
Errors:
- ErrNilEdgePredicate: if pred == nil.
Determinism:
- Deterministic for a deterministic predicate; map scan order does not affect the final set.
Complexity:
- Time O(E + B), where B is adjacency cleanup bucket count.
- Space O(1) extra.
Notes:
- This method is the preferred value-copy bulk-removal API.
- Public AdjacencyList() still reports isolated vertices after their last edge is removed.
AI-Hints:
- Use RemoveEdgesWhere for contract-safe bulk deletion.
- Do not call g.Edges, g.Neighbors, AddEdge, RemoveEdge, or RemoveVertex from pred.
func (*Graph) RemoveVertex ¶
RemoveVertex deletes a vertex and all incident edges.
Implementation:
- Stage 1: Validate non-empty ID (ErrEmptyVertexID).
- Stage 2: Acquire muVert.Lock(), then muEdgeAdj.Lock() in the package lock order.
- Stage 3: Verify vertex presence in the authoritative vertex catalog.
- Stage 4: Scan the edge catalog once and remove every edge whose From or To is id.
- Stage 5: For each removed edge, unlink adjacency buckets and delete the edge catalog entry.
- Stage 6: Delete the vertex catalog entry.
- Stage 7: Remove any remaining sparse adjacency index references for that vertex.
Behavior highlights:
- This is a topology rewrite: vertex membership, edge catalog, and adjacency index change together.
- Removes directed incoming, directed outgoing, undirected, loop, and parallel incident edges.
- Leaves no adjacency references to the removed vertex.
Inputs:
- id: vertex identifier to remove; must be non-empty.
Returns:
- error: nil on success; otherwise a sentinel error.
Errors:
- ErrEmptyVertexID: if id == "".
- ErrVertexNotFound: if id is absent from the vertex catalog.
Determinism:
- Deterministic final graph state; map scan order does not affect the result.
Complexity:
- Time O(E + B), where E is edge count and B is adjacency bucket count touched/scanned.
- Space O(1) extra.
Notes:
- This method is intentionally heavier than RemoveEdge because it changes vertex membership.
- It is the only operation that should call cleanupAdjacencyVertex.
AI-Hints:
- Keep the lock order muVert -> muEdgeAdj. Reversing it can deadlock with AddEdge.
- Do not call public graph methods while both locks are held.
- Prefer building subgraphs/views when you need “logical removal” without mutating the original.
func (*Graph) SetEdgeID ¶
SetEdgeID renames an existing edge’s identifier from oldID to newID.
Implementation:
- Stage 1: Validate inputs (non-empty).
- Stage 2: Acquire muEdgeAdj write lock.
- Stage 3: Lookup oldID (ErrEdgeNotFound) and ensure newID is free (ErrEdgeIDConflict).
- Stage 4: Update edge catalog key and edge.ID.
- Stage 5: Rewrite adjacency buckets that store the edge ID (from->to and mirror if undirected).
- Stage 6: Cleanup empty adjacency buckets.
- Stage 7: If newID is canonical "eN", bump auto-ID counter to avoid future collisions.
Inputs:
- oldID: existing edge identifier (must be non-empty).
- newID: desired edge identifier (must be non-empty).
Returns:
- error: nil on success; otherwise a sentinel error.
Errors:
- ErrEmptyEdgeID: if oldID == "" or newID == "".
- ErrEdgeNotFound: if oldID does not exist.
- ErrEdgeIDConflict: if newID already exists.
Determinism:
- Deterministic; map membership checks and deterministic updates.
Complexity:
- Time O(1) average (map updates); Space O(1).
Notes:
- This operation is atomic with respect to edge queries due to muEdgeAdj write lock.
AI-Hints:
- Use SetEdgeID to migrate from auto-IDs to stable IDs after building a graph topology.
func (*Graph) Stats ¶
func (g *Graph) Stats() *GraphStats
Stats produces a deterministic, read-only diagnostic summary of configuration flags, catalog sizes, and edge directedness counts.
Concurrency:
- StrictSnapshot: Stats holds muVert.RLock and muEdgeAdj.RLock together in package lock order.
- The returned GraphStats is detached and remains valid after unlock.
- Stats is a diagnostic snapshot, not a long-lived synchronization primitive.
Implementation:
- Stage 1: Acquire muVert.RLock(), snapshot immutable configuration flags and vertex count, then release muVert.RLock().
- Stage 2: Acquire muEdgeAdj.RLock(), snapshot edge count and scan the edge catalog once, then release muEdgeAdj.RLock().
- Stage 3: Return the populated GraphStats value object.
Behavior highlights:
- Avoids holding both locks simultaneously, reducing contention for diagnostics.
- The returned *GraphStats is detached from the graph and immutable by convention.
- Under concurrent mutation, vertex/config fields and edge counters may come from different read phases; callers must not treat it as a linearizable topology snapshot.
Returns:
- *GraphStats: detached summary of policy flags and catalog counters.
Errors:
- None.
Determinism:
- Deterministic for a fixed graph state.
- Under concurrent mutation, each phase is internally consistent, but the whole result is best-effort telemetry rather than a strict transaction snapshot.
Complexity:
- Time O(E), Space O(1) plus the returned struct.
Notes:
- DirectedDefault reports the default policy for new edges, not whether directed edges exist.
- Use HasDirectedEdges or DirectedEdgeCount when gating algorithms by current topology.
AI-Hints:
- Use Stats() to gate algorithms quickly (e.g., ensure Weighted==true before reading weights).
- Do not use Stats() as a strict concurrent admission primitive unless the caller owns external synchronization.
func (*Graph) ValidateEdgeID ¶
ValidateEdgeID checks whether ID is non-empty and not already in use.
Implementation:
- Stage 1: Reject empty IDs (ErrEmptyEdgeID).
- Stage 2: Under muEdgeAdj read lock, check edge catalog membership (ErrEdgeIDConflict).
Inputs:
- ID: desired edge identifier.
Returns:
- error: nil if valid; otherwise a sentinel.
Errors:
- ErrEmptyEdgeID: if ID == "".
- ErrEdgeIDConflict: if ID already exists.
Complexity:
- Time O(1), Space O(1).
func (*Graph) VertexCount ¶
VertexCount returns the current number of vertices in the graph.
Implementation:
- Stage 1: Acquire muVert read lock.
- Stage 2: Return len(g.vertices).
Returns:
- int: number of vertices.
Errors:
- None (pure query).
Determinism:
- Deterministic for a fixed graph state.
Complexity:
- Time O(1), Space O(1).
AI-Hints:
- Prefer VertexCount() over len(Vertices()) to avoid O(V log V) sorting costs.
func (*Graph) Vertices ¶
Vertices returns all vertex IDs in lexicographic ascending order.
Implementation:
- Stage 1: Acquire muVert read lock and copy vertex IDs into a slice.
- Stage 2: Sort the slice ascending.
- Stage 3: Return the sorted slice.
Behavior highlights:
- Stable enumeration surface used for determinism in higher-level algorithms.
Inputs:
- None.
Returns:
- []string: sorted vertex IDs.
Errors:
- None (pure query).
Determinism:
- Deterministic output order (lex asc).
Complexity:
- Time O(V log V), Space O(V).
AI-Hints:
- Use Vertices() for reproducible traversal seeds and stable test assertions.
func (*Graph) VerticesMap ¶
VerticesMap returns a fresh map copy of the vertex catalog (ID -> *Vertex).
Implementation:
- Stage 1: Acquire muVert read lock.
- Stage 2: Allocate a new map sized to the catalog.
- Stage 3: Copy ID -> *Vertex entries into the new map.
Behavior highlights:
- The returned map container is detached from the graph and may be retained, re-keyed, or discarded by the caller without holding graph locks.
- The contained *Vertex values alias catalog records.
- Vertex.ID MUST be treated as immutable once the vertex is published in a Graph.
- Vertex.Metadata remains caller-managed payload; core neither deep-copies nor synchronizes metadata contents.
Returns:
- map[string]*Vertex: fresh map container whose values alias catalog vertices.
Errors:
- None (pure query).
Determinism:
- Deterministic for membership; returned map iteration order is not deterministic (Go map rule).
Complexity:
- Time O(V), Space O(V).
Notes:
- Use Vertices() when you need deterministic ordering.
- Retaining a returned *Vertex does not pin graph membership or extend graph locks.
AI-Hints:
- Prefer VerticesMap() for detached catalog membership snapshots without sorting.
- Treat Vertex.ID as immutable; if you need mutable detached state, allocate your own copies.
func (*Graph) Weighted ¶
Weighted reports the construction-time "weighted" capability flag. If false, AddEdge rejects non-zero weights with ErrBadWeight.
Implementation:
- Stage 1: Acquire muVert read lock to observe configuration consistently.
- Stage 2: Return the immutable flag value.
Behavior highlights:
- Pure query: no mutation, no iteration, no allocations.
Returns:
- bool: true if non-zero weights are permitted.
Errors:
- None.
Determinism:
- Deterministic for a fixed graph instance (flags are immutable after construction).
Complexity:
- Time O(1), Space O(1).
Notes:
- This reports a policy flag, not whether any stored edge currently has Weight != 0.
AI-Hints:
- Gate weighted algorithms by g.Weighted() before reading edge.Weight.
type GraphOption ¶
GraphOption mutates only a newly constructed Graph inside NewGraph.
Implementation:
- Stage 1: NewGraph validates the option list shape and rejects nil option values.
- Stage 2: NewGraph allocates empty catalogs.
- Stage 3: Options are applied deterministically in call order.
- Stage 4: The configured graph is published only if every option succeeds.
Behavior highlights:
- Options are construction-time only.
- Options MUST NOT panic as part of the public contract.
- A nil GraphOption is rejected with ErrNilGraphOption before invocation.
- Option implementations may mutate only the graph being constructed.
Inputs:
- g: the graph instance being constructed.
Returns:
- error: nil on success; otherwise a stable sentinel error.
Errors:
- Nil option values are rejected by NewGraph/NewMixedGraph with ErrNilGraphOption.
- Option implementations may return stable sentinels for invalid configuration.
Determinism:
- Deterministic application order (left-to-right).
Complexity:
- Time O(1) per option, Space O(1).
Notes:
- Core avoids hidden global state; behavior changes must be explicit via options.
- On constructor error, no partially configured graph is published to the caller.
AI-Hints:
- Prefer explicit options over implicit defaults when writing reproducible tests.
- Return sentinels; do not panic or mutate external state from a GraphOption.
func WithDirected ¶
func WithDirected(defaultDirected bool) GraphOption
WithDirected sets the default directedness for all future edges created in this graph. Per-edge overrides are only allowed in mixed mode (WithMixedEdges + WithEdgeDirected).
Implementation:
- Stage 1: Store defaultDirected into g.directed during construction.
Behavior highlights:
- Affects only the default for future edges (not existing edges).
Inputs:
- defaultDirected: true for directed-by-default graphs; false for undirected-by-default.
Returns:
- GraphOption: construction-time mutator.
Errors:
- None.
Determinism:
- Deterministic; constant-time flag set.
Complexity:
- Time O(1), Space O(1).
Notes:
- Mixed mode controls whether individual edges may override Directed.
AI-Hints:
- Use WithMixedEdges() if your workload needs both directed and undirected edges in one graph.
- Sets only the default; per-edge override requires WithMixedEdges().
func WithLoops ¶
func WithLoops() GraphOption
WithLoops permits self-loops (edges from a vertex to itself).
Implementation:
- Stage 1: Set g.allowLoops = true during construction.
Behavior highlights:
- Without this option, AddEdge(v,v,...) returns ErrLoopNotAllowed.
Returns:
- GraphOption: construction-time mutator.
Errors:
- None.
Determinism:
- Deterministic; constant-time flag set.
Complexity:
- Time O(1), Space O(1).
AI-Hints:
- Keep loops disabled unless your math model explicitly includes self transitions.
- Without this, AddEdge(v,v,...) returns ErrLoopNotAllowed.
func WithMixedEdges ¶
func WithMixedEdges() GraphOption
WithMixedEdges enables per-edge directedness overrides (mixed mode). In mixed mode, individual edges may specify Directed=true/false via EdgeOption.
Implementation:
- Stage 1: Set g.allowMixed = true during construction.
Behavior highlights:
- Without this option, WithEdgeDirected(...) returns ErrMixedEdgesNotAllowed.
Returns:
- GraphOption: construction-time mutator.
Errors:
- None.
Determinism:
- Deterministic; constant-time flag set.
Complexity:
- Time O(1), Space O(1).
AI-Hints:
- Mixed mode is a capability flag; keep it off if all edges share the same orientation.
func WithMultiEdges ¶
func WithMultiEdges() GraphOption
WithMultiEdges permits parallel edges between the same endpoints.
Implementation:
- Stage 1: Set g.allowMulti = true during construction.
Behavior highlights:
- Without this option, AddEdge on an existing (from,to) bucket returns ErrMultiEdgeNotAllowed.
Returns:
- GraphOption: construction-time mutator.
Errors:
- None.
Determinism:
- Deterministic; constant-time flag set.
Complexity:
- Time O(1), Space O(1).
AI-Hints:
- Enable multi-edges for multigraph models (e.g., multiple relations between entities).
- Without this, a second AddEdge(from,to,...) returns ErrMultiEdgeNotAllowed.
func WithWeighted ¶
func WithWeighted() GraphOption
WithWeighted enables non-zero edge weights in this graph.
Implementation:
- Stage 1: Set g.weighted = true during construction.
Behavior highlights:
- Without this option, AddEdge rejects weight != 0 with ErrBadWeight.
Returns:
- GraphOption: construction-time mutator.
Errors:
- None.
Determinism:
- Deterministic; constant-time flag set.
Complexity:
- Time O(1), Space O(1).
AI-Hints:
- Keep graphs unweighted if you only need topology; it simplifies inputs and tests.
- Without this, AddEdge(weight!=0) returns ErrBadWeight.
type GraphStats ¶
type GraphStats struct {
// DirectedDefault is the graph-wide default orientation (true=directed).
DirectedDefault bool
// Weighted is true when non-zero edge weights are permitted.
Weighted bool
// AllowsMulti is true when parallel edges are permitted.
AllowsMulti bool
// AllowsLoops is true when self-loops are permitted.
AllowsLoops bool
// MixedMode is true when per-edge Directed overrides are permitted.
MixedMode bool
// VertexCount is the number of vertices present in the graph.
VertexCount int
// EdgeCount is the number of edges present in the graph.
EdgeCount int
// DirectedEdgeCount is the number of edges with Directed == true.
DirectedEdgeCount int
// UndirectedEdgeCount is the number of edges with Directed == false.
UndirectedEdgeCount int
}
GraphStats is a read-only result object summarizing graph state.
Implementation:
- Filled by (*Graph).Stats() by scanning catalogs (implementation elsewhere).
Behavior highlights:
- Pure data type: no methods, no side effects.
Determinism:
- Field meanings are stable and part of the public contract.
Complexity:
- N/A (data type).
AI-Hints:
- Use Stats() in tests as a deterministic admission check (counts, feature flags).
type Nilable ¶
type Nilable interface {
// IsNil reports whether the receiver should be treated as nil.
// It MUST be side-effect free, deterministic, non-allocating, and MUST NOT panic.
IsNil() bool
}
Nilable provides an explicit, reflect-free mechanism to treat typed-nil receivers stored inside interfaces as nil during validation and testing.
Implementation:
- Stage 1: Callers accept an interface value that may hold a typed nil pointer.
- Stage 2: If the dynamic value implements Nilable, callers invoke IsNil().
- Stage 3: If IsNil reports true, the value is treated as nil by the caller.
Behavior highlights:
- Avoids reflect in foundational validators and test helpers.
- Keeps nil-detection O(1) and deterministic.
- Optional: types that do not implement Nilable are checked with regular `== nil`.
Returns:
- bool: true if the receiver should be treated as nil.
Errors:
- None. IsNil MUST NOT panic and MUST NOT allocate.
Determinism:
- Deterministic and side-effect free (required).
Complexity:
- Time O(1), Space O(1).
Notes:
- In Go, an interface can be non-nil while holding a typed nil pointer.
AI-Hints:
- Implement IsNil() on pointer-backed types that are commonly stored behind interfaces.
type Vertex ¶
type Vertex struct {
// ID uniquely identifies a vertex within a single Graph instance.
ID string
// Metadata holds arbitrary caller-managed payload.
// Clone/View operations shallow-copy this map pointer.
// core does not synchronize metadata contents.
Metadata map[string]interface{}
}
Vertex is the canonical node record used by Graph; the unique key is Vertex.ID. Metadata is an opaque, caller-managed payload that the core does not interpret.
Memory Policy (Aliasing & Ownership): - Graph query methods may return *Vertex aliases to catalog records. - Vertex.ID MUST be treated as immutable once the vertex is published in a Graph. - Metadata is a REFERENCE TYPE (map pointer). - Clone and view operations perform a SHALLOW COPY of this pointer. - Result: the source graph and its clones/views SHARE the same underlying Metadata maps. - The caller is responsible for deep-copying Metadata if total isolation is required.
Implementation:
- Stage 1: Graph stores vertices in a map keyed by Vertex.ID.
- Stage 2: Algorithms treat Metadata as an opaque pointer; Clone is shallow.
Behavior highlights:
- Vertex identity is stable for the lifetime of the graph.
- Metadata ownership belongs to the caller; core never mutates it.
Inputs:
- ID: unique identifier within a Graph; must be non-empty.
- Metadata: arbitrary user payload; may be nil.
Returns:
- N/A (data type).
Errors:
- N/A (data type).
Determinism:
- Vertex.ID is the stable ordering key for public enumeration.
Complexity:
- N/A (data type).
Notes:
- Graph.Clone copies the Metadata map pointer; callers must deep-copy if required.
AI-Hints:
- Prefer short, stable IDs for deterministic logs and golden tests.
- Keep Metadata small and immutable if used concurrently.
func (*Vertex) IsNil ¶
IsNil reports whether the receiver should be treated as nil when stored inside interfaces.
Implementation:
- Stage 1: Compare the receiver pointer to nil.
- Stage 2: Return the result without dereferencing.
Behavior highlights:
- Safe for typed-nil stored inside interfaces (no panic).
- Reflect-free nil detection used by validators and test helpers via core.Nilable.
Returns:
- bool: true iff receiver == nil.
Errors:
- None.
Determinism:
- Deterministic.
Complexity:
- Time O(1), Space O(1).
Notes:
- Keep this method trivial; do not add deep validation.
AI-Hints:
- Implement the same pattern for other pointer-backed core types that appear behind interfaces.