//! [`FrozenGraph`], an undirected graph topology supporting only one-time construction. use std::ops::IndexMut; use crate::maps::ElementMap; use crate::traits::{ GraphIsomorphism, GraphTopology, GraphTopologyConversion, Incidence, IncidenceCursor, }; /// An opaque handle identifying a vertex in a [`FrozenGraph`]. /// /// Handles are stable for the lifetime of the graph. Obtain via graph methods like /// [`FrozenGraph::vertices`]. #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Vertex(usize); /// An opaque handle identifying an edge in a [`FrozenGraph`]. /// /// Handles are stable for the lifetime of the graph. Obtain via graph methods like /// [`FrozenGraph::edges`]. #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Edge(usize); /// An [`ElementMap`] for [`FrozenGraph`] vertices. pub type FrozenGraphVertexMap = ElementMap; /// An [`ElementMap`] for [`FrozenGraph`] edges. pub type FrozenGraphEdgeMap = ElementMap; /// An [`Incidence`] for [`FrozenGraph`]. pub type FrozenGraphIncidence = Incidence; #[derive(Copy, Clone)] struct IncidenceEntry { adjacent: Vertex, opposite: usize, } /// A resumable cursor over the incidences of a single vertex in a [`FrozenGraph`]. /// /// Obtain via [`FrozenGraph::incidence_cursor`]. See [`IncidenceCursor`] on usage guidance. #[derive(Copy, Clone)] pub struct FrozenGraphIncidenceCursor { current: usize, end: usize, } impl IncidenceCursor for FrozenGraphIncidenceCursor { fn next(&mut self, graph: &FrozenGraph) -> Option { if self.current < self.end { let incidence = graph.normalize_incidence(self.current); self.current += 1; Some(incidence) } else { None } } } /// An undirected graph that supports only one-time construction, and no further modification. /// /// `FrozenGraph` is optimised for workloads that repeatedly query a graph, but do not require /// changing it once it is constructed. [`Vertex`] and [`Edge`] handles are never invalidated. Use /// [`AppendGraph`] instead if you need to incrementally add vertices or edges, or [`Graph`] if you /// need to remove vertices or edges after initial construction. /// /// `FrozenGraph` uses a compressed-sparse data representation. Incidences are stored as contiguous /// adjacency lists per vertex in a single flat [`Vec`]. Vertex neighborhood traversals are /// therefore cache-friendly without index jumps. /// /// # Examples /// /// ``` /// use grapherity::prelude::*; /// use grapherity::models::{FrozenGraph, AppendGraph}; /// /// // Constructs a FrozenGraph instance via AppendGraph. /// let mut graph = AppendGraph::new(); /// let u = graph.add_vertex(); /// let v = graph.add_vertex(); /// let e = graph.add_edge(u, v); /// let (graph, isomorphism) = FrozenGraph::convert(&graph); /// /// // Queries the FrozenGraph instance via the returned map. /// assert!(graph.are_adjacent(isomorphism.vertex(u), isomorphism.vertex(v))); /// ``` /// /// # Time and space complexity /// /// [`degree`] runs in *O(1)* time. Space complexity is *O(|V| + |E|)*. /// /// # 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. /// /// [`degree`]: Self::degree /// [`AppendGraph`]: crate::models::AppendGraph /// [`Graph`]: crate::models::Graph #[derive(Clone)] pub struct FrozenGraph { vertices: Vec, incidences: Vec, } impl FrozenGraph { fn raw_incidences(&self, v: Vertex) -> impl Iterator { self.vertices[v.0]..self.vertices[v.0 + 1] } fn normalize_edge(&self, e: usize) -> Edge { let f = self.incidences[e].opposite; Edge(if e < f { e } else { f }) } fn normalize_incidence(&self, i: usize) -> FrozenGraphIncidence { let entry = &self.incidences[i]; Incidence { vertex: entry.adjacent, edge: Edge(if i < entry.opposite { i } else { entry.opposite }), } } } impl GraphTopology for FrozenGraph { type Vertex = Vertex; type Edge = Edge; type IncidenceCursor = FrozenGraphIncidenceCursor; fn vertex_count(&self) -> usize { self.vertices.len() - 1 } fn vertex_map(&self, default: T) -> ElementMap { ElementMap::new(default, |v| v.0, self.vertex_count()) } fn edge_count(&self) -> usize { self.incidences.len() / 2 } // TODO: Find a way to reduce the needed capacity for the edge ElementMap. This could be improved by finding a small vertex cover and store those vertices first. See https://en.wikipedia.org/wiki/Vertex_cover#Approximate_evaluation fn edge_map(&self, default: T) -> ElementMap { // "incidences.len() - 1" would underflow for an edge-free graph. ElementMap::new(default, |e| e.0, self.incidences.len().saturating_sub(1)) } fn degree(&self, v: Self::Vertex) -> usize { self.vertices[v.0 + 1] - self.vertices[v.0] } fn are_adjacent(&self, u: Self::Vertex, v: Self::Vertex) -> bool { self.adjacent_vertices(u).any(|x| x == v) } fn vertices(&self) -> impl Iterator { (0..self.vertices.len() - 1).map(Vertex) } fn adjacent_vertices(&self, v: Self::Vertex) -> impl Iterator { self.raw_incidences(v).map(|i| self.incidences[i].adjacent) } fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) { let entry = &self.incidences[e.0]; (self.incidences[entry.opposite].adjacent, entry.adjacent) } fn edges(&self) -> impl Iterator { self.incidences .iter() .enumerate() .filter_map(|(i, x)| (i < x.opposite).then_some(Edge(i))) } fn incident_edges(&self, v: Self::Vertex) -> impl Iterator { self.raw_incidences(v).map(|i| self.normalize_edge(i)) } fn incidences(&self, v: Self::Vertex) -> impl Iterator { self.raw_incidences(v).map(|i| self.normalize_incidence(i)) } fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor { FrozenGraphIncidenceCursor { current: self.vertices[v.0], end: self.vertices[v.0 + 1], } } } impl GraphTopologyConversion for FrozenGraph { fn convert(source: &G) -> (Self, GraphIsomorphism) { // Builds vertex list with sentinel and vertex map. let mut vertex_map = source.vertex_map(Vertex(usize::MAX)); let mut vertices = Vec::with_capacity(source.vertex_count() + 1); let mut offset = 0; for (i, v) in source.vertices().enumerate() { vertex_map[v] = Vertex(i); vertices.push(offset); offset += source.degree(v); } vertices.push(offset); // Builds incidence list and edge map. Uses "usize::MAX" as "unset" here: an edge can never // be normalized to "usize::MAX", since it must be the smaller index of two incidences. const UNSET: usize = usize::MAX; let mut edge_map = source.edge_map(Edge(UNSET)); let mut incidences = Vec::with_capacity(source.edge_count() * 2); for v in source.vertices() { for Incidence { vertex: u, edge: e } in source.incidences(v) { let em = edge_map.index_mut(e); if em.0 == UNSET { // Adds the first incidence for the edge, but we don't know the opposite, yet. *em = Edge(incidences.len()); incidences.push(IncidenceEntry { adjacent: vertex_map[u], opposite: 0, }); } else { // Adds the second incidence for the edge and fills the opposite for the first. incidences[em.0].opposite = incidences.len(); incidences.push(IncidenceEntry { adjacent: vertex_map[u], opposite: em.0, }); } } } let result = Self { vertices, incidences, }; let iso = GraphIsomorphism::new(&result, vertex_map, edge_map); (result, iso) } } impl From<&G> for FrozenGraph { fn from(graph: &G) -> Self { Self::convert(graph).0 } } #[cfg(test)] mod trait_tests { use super::FrozenGraph; use crate::models::{AppendGraph, Graph}; crate::graph_topology_tests!(FrozenGraph); crate::graph_topology_clone_tests!(FrozenGraph); crate::graph_topology_conversion_tests!(FrozenGraph, AppendGraph); crate::graph_topology_deletion_conversion_tests!(FrozenGraph, Graph); crate::graph_topology_addition_deletion_conversion_tests!(FrozenGraph, Graph); crate::from_tests!(FrozenGraph, AppendGraph); } #[cfg(test)] mod tests { use super::*; use crate::testing::fixtures::MakeTestGraph; use crate::traits::GraphTopology; #[test] fn incident_vertices_opposite_handle() { let (graph, [v0, v1], e) = FrozenGraph::single_edge(); let f = Edge(graph.incidences[e.0].opposite); 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 csr_sentinel_and_offsets_standard() { let (graph, ..) = FrozenGraph::standard(); assert_eq!( *graph.vertices.last().unwrap(), graph.incidences.len(), "sentinel should equal total number of incidences" ); assert!( graph.vertices.windows(2).all(|w| w[0] <= w[1]), "incidence offsets of vertices should be monotonically non-decreasing" ); } #[test] fn csr_sentinel_and_offsets_empty() { let graph = FrozenGraph::empty(); assert_eq!( graph.vertices, vec![0], "empty graph should have a single sentinel entry of 0" ); assert!( graph.incidences.is_empty(), "empty graph should have no incidences" ); } #[test] fn opposite_symmetry() { let (graph, ..) = FrozenGraph::standard(); for (i, inc) in graph.incidences.iter().enumerate() { let opp = inc.opposite; assert_ne!(opp, i, "incidence should not equal its own opposite"); assert_eq!( graph.incidences[opp].opposite, i, "opposite of opposite of incidence should equal the incidence" ); } } }