//! [`Graph`], an undirected graph topology supporting addition and deletion. use std::num::NonZeroUsize; use typed_generational_arena::{Arena, Index}; use crate::maps::ElementMap; use crate::models; use crate::traits::{ GraphIsomorphism, GraphTopology, GraphTopologyAddition, GraphTopologyConversion, GraphTopologyDeletion, Incidence, 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; /// An [`ElementMap`] for [`Graph`] vertices. pub type GraphVertexMap = ElementMap; /// An [`ElementMap`] for [`Graph`] edges. pub type GraphEdgeMap = ElementMap; /// An [`Incidence`] for [`Graph`]. pub type GraphIncidence = Incidence; #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct VertexSlot(usize); // TODO: Benchmark the cost of the index shift. An alternative could be to init Graph::incidences with two dummy entries to avoid 0. #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct IncidenceSlot(NonZeroUsize); impl IncidenceSlot { fn new(index: usize) -> Self { IncidenceSlot(NonZeroUsize::new(index + 1).unwrap()) } fn index(self) -> usize { self.0.get() - 1 } } /// Referenced by [`Vertex`] as a type parameter of the underlying arena. Not intended for direct /// external use. #[doc(hidden)] #[derive(Clone)] pub struct VertexIncidenceHeader { incidence_count: usize, first_incidence: Option, } /// Referenced by [`Edge`] as a type parameter of the underlying arena. Not intended for direct /// external use. #[doc(hidden)] #[derive(Copy, Clone)] pub struct IncidenceEntry { next: Option, adjacent: VertexSlot, } struct IncidentEdgeCursor { incidence: Option, } impl IncidentEdgeCursor { fn next(&mut self, graph: &Graph) -> Option { graph.step_incidence(&mut self.incidence).map(|(_, e)| e) } } /// 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, } impl IncidenceCursor for GraphIncidenceCursor { fn next(&mut self, graph: &Graph) -> Option { graph .step_incidence(&mut self.incidence) .map(|(vs, e)| Incidence { vertex: graph.vertices.get_idx(vs.0).unwrap(), edge: graph.normalize_edge(e), }) } } /// 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. /// /// # Cloning /// /// [`Vertex`] and [`Edge`] handles obtained from a clone are valid, and refer to the same vertex or /// edge in the original, and vice versa. However, once cloned, the two instances no longer affect /// each other. /// /// [`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 #[derive(Clone)] pub struct Graph { // TODO: Arena index and generation types could be externalized to Graph. vertices: Arena, incidences: Arena, } impl Graph { /// Creates an empty graph instance with no vertices or edges. pub fn new() -> Self { Self { vertices: Arena::new(), incidences: Arena::new(), } } /// 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, u: Vertex, v: Vertex) -> Edge { let edge = self.incidences.insert(IncidenceEntry { next: self.vertices[u].first_incidence.take(), adjacent: VertexSlot(v.arr_idx()), }); self.vertices[u].incidence_count += 1; self.vertices[u].first_incidence = Some(IncidenceSlot::new(edge.arr_idx())); edge } fn remove_incidence_pair(&mut self, e: Edge) -> (Edge, IncidenceEntry, IncidenceEntry) { let f = self .incidences .get_idx(e.arr_idx() ^ 1) .expect("invalid paired incidence index, corrupt internal data state"); let e_entry = self .incidences .remove(e) .expect("attempt to delete an invalid edge"); let f_entry = self .incidences .remove(f) .expect("cannot read paired incidence, corrupt internal data state"); (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. fn update_incidence_list( &mut self, e: Edge, source: VertexSlot, next: Option, is_loop: bool, ) { let source_vertex = self .vertices .get_idx(source.0) .expect("missing incident vertex, corrupt internal data state"); let vertex_header = &mut self.vertices[source_vertex]; vertex_header.incidence_count -= if is_loop { 2 } else { 1 }; let first = vertex_header .first_incidence .expect("incident vertex without incidences, corrupt internal data state"); if first.index() == e.arr_idx() { vertex_header.first_incidence = next; } else { let graph: &Graph = self; let (_, previous) = self .raw_incidences(source_vertex) .find(|(_, f)| { graph.incidences[*f] .next .is_some_and(|i| i.index() == e.arr_idx()) }) .expect("cannot find previous incidence, corrupt internal data state"); self.incidences[previous].next = next; } } fn raw_incidences(&self, v: Vertex) -> impl Iterator { let mut incidence = self.vertices[v].first_incidence; std::iter::from_fn(move || self.step_incidence(&mut incidence)) } fn step_incidence(&self, incidence: &mut Option) -> Option<(VertexSlot, Edge)> { // TODO: Benchmark storing full Index (one read, larger entries) vs. slot + get_idx() (two reads, smaller entries). let current = (*incidence)?; let e = self.incidences.get_idx(current.index()).unwrap(); let entry = self.incidences[e]; *incidence = entry.next; Some((entry.adjacent, e)) } fn normalize_edge(&self, e: Edge) -> Edge { let i = e.arr_idx(); if i & 1 == 0 { e } else { self.incidences.get_idx(i ^ 1).unwrap() } } } impl Default for Graph { fn default() -> Self { Self::new() } } impl GraphTopology for Graph { type Vertex = Vertex; type Edge = Edge; type IncidenceCursor = GraphIncidenceCursor; fn vertex_count(&self) -> usize { self.vertices.len() } fn vertex_map(&self, default: T) -> ElementMap { ElementMap::new(default, |v| v.arr_idx(), self.vertex_capacity()) } fn edge_count(&self) -> usize { self.incidences.len() / 2 } fn edge_map(&self, default: T) -> ElementMap { ElementMap::new(default, |e| e.arr_idx() / 2, self.edge_capacity()) } fn degree(&self, v: Self::Vertex) -> usize { self.vertices[v].incidence_count } fn are_adjacent(&self, u: Self::Vertex, v: Self::Vertex) -> bool { self.adjacent_vertices(u).any(|x| x == v) } fn vertices(&self) -> impl Iterator { self.vertices.iter().map(|(i, _)| i) } fn adjacent_vertices(&self, v: Self::Vertex) -> impl Iterator { self.raw_incidences(v) .map(|(vs, _)| self.vertices.get_idx(vs.0).unwrap()) } fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) { let v = self .vertices .get_idx(self.incidences[e].adjacent.0) .unwrap(); let f = self.incidences.get_idx(e.arr_idx() ^ 1).unwrap(); let u = self .vertices .get_idx(self.incidences[f].adjacent.0) .unwrap(); (u, v) } fn edges(&self) -> impl Iterator { self.incidences .iter() .filter(|(i, _)| i.arr_idx() % 2 == 0) .map(|(i, _)| i) } fn incident_edges(&self, v: Self::Vertex) -> impl Iterator { self.raw_incidences(v).map(|(_, e)| self.normalize_edge(e)) } fn incidences(&self, v: Self::Vertex) -> impl Iterator { self.raw_incidences(v).map(|(vs, e)| Incidence { vertex: self.vertices.get_idx(vs.0).unwrap(), edge: self.normalize_edge(e), }) } fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor { GraphIncidenceCursor { incidence: self.vertices[v].first_incidence, } } } impl GraphTopologyAddition for Graph { fn vertex_capacity(&self) -> usize { self.vertices.capacity() } fn edge_capacity(&self) -> usize { self.incidences.capacity() / 2 } fn reserve_vertices(&mut self, additional: usize) { self.vertices.reserve(additional); } fn reserve_edges(&mut self, additional: usize) { self.incidences.reserve(additional * 2); } fn add_vertex(&mut self) -> Self::Vertex { self.vertices.insert(VertexIncidenceHeader { incidence_count: 0, first_incidence: None, }) } fn add_edge(&mut self, u: Self::Vertex, v: Self::Vertex) -> Self::Edge { let first = self.add_incidence(u, v); self.add_incidence(v, u); first } } // TODO: Benchmark delete with storing "previous" in O(1) vs. linear lookup in O(degree). impl GraphTopologyDeletion for Graph { fn delete_vertex(&mut self, v: Self::Vertex) { let v_header = self .vertices .remove(v) .expect("attempt to delete an invalid vertex"); let mut cursor = IncidentEdgeCursor { incidence: v_header.first_incidence, }; while let Some(e) = cursor.next(self) { // Since v is being deleted, there are no update_incidence_list() calls for e, no need // to fix v's incidence list. let (f, e_entry, f_entry) = self.remove_incidence_pair(e); if e_entry.adjacent != f_entry.adjacent { self.update_incidence_list(f, e_entry.adjacent, f_entry.next, false); } else { cursor.incidence = f_entry.next; } } } 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); self.update_incidence_list(f, e_entry.adjacent, f_entry.next, false); } else if f_entry.next.is_some_and(|i| e.arr_idx() == i.index()) { self.update_incidence_list(f, e_entry.adjacent, e_entry.next, true); } else { self.update_incidence_list(e, e_entry.adjacent, f_entry.next, true); } } } impl GraphTopologyConversion for Graph { fn convert(source: &G) -> (Self, GraphIsomorphism) { let mut edge_map = source.edge_map(Edge::new(usize::MAX, usize::MAX)); let (result, vertex_map) = models::convert_to_addition(source, Vertex::new(usize::MAX, usize::MAX), |e, new_e| { edge_map[e] = new_e }); let iso = GraphIsomorphism::new(&result, vertex_map, edge_map); (result, iso) } } impl From<&H> for Graph { fn from(source: &H) -> Self { models::convert_to_addition::( source, Vertex::new(usize::MAX, usize::MAX), |_, _| {}, ) .0 } } #[cfg(test)] mod trait_tests { use super::Graph; use crate::models::AppendGraph; crate::graph_topology_tests!(Graph); crate::graph_topology_addition_tests!(Graph); crate::graph_topology_deletion_tests!(Graph); crate::graph_topology_addition_deletion_tests!(Graph); crate::graph_topology_clone_tests!(Graph); crate::graph_topology_addition_clone_tests!(Graph); crate::graph_topology_conversion_tests!(Graph, AppendGraph); crate::graph_topology_addition_conversion_tests!(Graph, AppendGraph); crate::from_tests!(Graph, AppendGraph); } #[cfg(test)] mod tests { use super::*; use crate::testing::fixtures::MakeTestGraph; #[test] fn incident_vertices_paired_index() { let (graph, [v0, v1], e) = Graph::single_edge(); let f = graph .incidences .get_idx(e.arr_idx() + 1) .expect("paired index should be valid"); let (u0, u1) = graph.incident_vertices(f); assert!( (u0 == v0 && u1 == v1) || (u0 == v1 && u1 == v0), "unexpected incident vertices {u0:?} and {u1:?} for edge {f:?}" ); } #[test] fn delete_edge_paired_index() { let (mut graph, _, e) = Graph::single_edge(); let f = graph .incidences .get_idx(e.arr_idx() + 1) .expect("paired index should be valid"); graph.delete_edge(f); assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); } #[test] fn delete_edge_loop_paired_index() { let (mut graph, _, e) = Graph::loop_edge(); let f = graph .incidences .get_idx(e.arr_idx() + 1) .expect("paired index should be valid"); graph.delete_edge(f); assert_eq!(graph.edge_count(), 0, "unexpected edge count after delete"); } #[test] fn reused_slot_returns_old_value() { let (mut graph, [_, v], _) = Graph::single_edge(); let mut map = graph.vertex_map(0); map[v] = 99; graph.delete_vertex(v); let u = graph.add_vertex(); assert_eq!( v.arr_idx(), u.arr_idx(), "precondition: new vertex {v:?} should reuse slot of deleted vertex {u:?}" ); // ElementMap uses raw indices, not vertex identity. A new vertex u reusing the slot // of previously deleted v sees the old value. Callers must reinitialize stale slots // after deletion. assert_eq!( map[u], 99, "new vertex reusing slot of deleted vertex should return old value" ); } }