From b5cc3b1c3f81f68bbd2584e3045d24ee63527a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Fri, 24 Jul 2026 20:55:41 +0200 Subject: [PATCH] Add documentation for Graph --- src/models/graph.rs | 83 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/src/models/graph.rs b/src/models/graph.rs index ed86205..c4da097 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -1,12 +1,26 @@ +//! [`Graph`], an undirected graph topology supporting addition and deletion. + use typed_generational_arena::{Arena, Index}; use crate::maps::{EdgeMap, VertexMap}; use crate::traits::{GraphTopology, GraphTopologyDeletion, IncidenceCursor}; +/// An opaque handle identifying a vertex in a [`Graph`]. +/// +/// Handles remain valid until the vertex is explicitly deleted. Obtain via graph methods like +/// [`Graph::add_vertex`] and [`Graph::vertices`]. pub type Vertex = Index; + +/// An opaque handle identifying an edge in a [`Graph`]. +/// +/// Handles remain valid until the edge is explicitly deleted, or one of its endpoint vertices is +/// deleted. Obtain via graph methods like [`Graph::add_edge`] and [`Graph::edges`]. pub type Edge = Index; +/// A [`VertexMap`] for [`Graph`] vertices. pub type GraphVertexMap = VertexMap; + +/// An [`EdgeMap`] for [`Graph`] edges. pub type GraphEdgeMap = EdgeMap; #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -15,11 +29,18 @@ struct VertexSlot(usize); #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct IncidenceSlot(usize); +// TODO: Check if VertexIncidenceHeader and IncidenceEntry can be made smaller. Currently they both take 24 bytes (on 64bit), see https://stackoverflow.com/a/79653173 +/// `pub` because [`Vertex`] references it as a type parameter of the underlying arena. Not +/// intended for direct external use. +#[doc(hidden)] pub struct VertexIncidenceHeader { incidence_count: usize, first_incidence: Option, } +/// `pub` because [`Edge`] references it as a type parameter of the underlying arena. Not intended +/// for direct external use. +#[doc(hidden)] #[derive(Copy, Clone)] pub struct IncidenceEntry { next: Option, @@ -36,6 +57,9 @@ impl IncidentEdgeCursor { } } +/// A resumable cursor over the incidences of a single vertex in a [`Graph`]. +/// +/// Obtain via [`Graph::incidence_cursor`]. See [`IncidenceCursor`] on usage guidance. #[derive(Copy, Clone)] pub struct GraphIncidenceCursor { incidence: Option, @@ -49,6 +73,49 @@ impl IncidenceCursor for GraphIncidenceCursor { } } +/// An undirected graph that supports adding vertices and edges, and deleting them. +/// +/// `Graph` is suited for workloads that need to modify the graph structure over time, i.e. adding +/// amd removing vertices and edges. [`Vertex`] and [`Edge`] handles remain valid until the vertex +/// or edge they identify is explicitly deleted. Use [`AppendGraph`] instead if you only need to +/// append vertices and edges. +/// +/// Incidences are stored as interleaved adjacency lists in a generational [`Arena`]. In general, +/// vertex neighborhood traversals result in scattered memory accesses. +/// +/// # Examples +/// +/// ``` +/// use grapherity::prelude::*; +/// use grapherity::models::Graph; +/// +/// let mut graph = Graph::new(); +/// let v1 = graph.add_vertex(); +/// let v2 = graph.add_vertex(); +/// let _e = graph.add_edge(v1, v2); +/// assert!(graph.are_adjacent(v1, v2)); +/// graph.delete_vertex(v1); +/// assert_eq!(graph.degree(v2), 0); +/// ``` +/// +/// # Time and space complexity +/// +/// Both [`add_vertex`] and [`add_edge`] run in amortised *O(1)* time. [`degree`] runs in *O(1)* +/// time since vertex degrees are stored. [`delete_vertex`] runs in *O(degree(v))* time. +/// [`delete_edge`] runs in *O(degree(u) + degree(v))* time, where *u* and *v* are the edge's +/// endpoints. +/// +/// Space complexity is *O(|V| + |E|)*, but freed slots for vertices and edges are not compacted and +/// their allocation is never reclaimed. Capacity only grows. If additions and deletions are both +/// planned, performing deletions first allows freed slots to be reused by subsequent additions, +/// potentially avoiding reallocation. +/// +/// [`add_vertex`]: Self::add_vertex +/// [`add_edge`]: Self::add_edge +/// [`degree`]: Self::degree +/// [`delete_vertex`]: Self::delete_vertex +/// [`delete_edge`]: Self::delete_edge +/// [`AppendGraph`]: crate::models::AppendGraph pub struct Graph { // TODO: Arena index and generation types could be externalized to Graph. vertices: Arena, @@ -56,6 +123,7 @@ pub struct Graph { } impl Graph { + /// Creates an empty graph instance with no vertices or edges. pub fn new() -> Self { Self { vertices: Arena::new(), @@ -63,8 +131,8 @@ impl Graph { } } - // Adds a single incidence of an edge, which is composed by two such incidences, to the - // incidences arena, and returns its index. + /// Adds a single incidence of an edge, which is composed by two such incidences, to the + /// incidences arena, and returns its index. fn add_incidence(&mut self, v1: Vertex, v2: Vertex) -> Edge { let edge = self.incidences.insert(IncidenceEntry { next: self.vertices[v1].first_incidence.take(), @@ -91,8 +159,9 @@ impl Graph { (f, e_entry, f_entry) } - // Updates the source vertex incidence list after the incidence "e" was deleted from the - // incidence arena. "next" is the next incidence after "e" in the source vertex incidence list. + /// Updates the `source` vertex incidence list after the incidence `e` was deleted from the + /// incidence arena. `next` is the next incidence after `e` in the `source` vertex incidence + /// list. fn update_incidence_list( &mut self, e: Edge, @@ -267,10 +336,10 @@ impl GraphTopologyDeletion for Graph { } } - // The incidence entries are removed before patching the linked lists. This is safe because - // update_incidence_list() searches by raw slot index (IncidenceSlot.0) and only dereferences - // the predecessor, never the removed entries themselves. fn delete_edge(&mut self, e: Self::Edge) { + // The incidence entries are removed before patching the linked lists. This is safe because + // `update_incidence_list` searches by raw slot index (IncidenceSlot.0) and only + // dereferences the predecessor, never the removed entries themselves. let (f, e_entry, f_entry) = self.remove_incidence_pair(e); if e_entry.adjacent != f_entry.adjacent { self.update_incidence_list(e, f_entry.adjacent, e_entry.next, false);