Files
grapherity/src/models/frozen_graph.rs
T

323 lines
11 KiB
Rust

//! [`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<T> = ElementMap<Vertex, T>;
/// An [`ElementMap`] for [`FrozenGraph`] edges.
pub type FrozenGraphEdgeMap<T> = ElementMap<Edge, T>;
/// An [`Incidence`] for [`FrozenGraph`].
pub type FrozenGraphIncidence = Incidence<Vertex, Edge>;
#[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<FrozenGraph> for FrozenGraphIncidenceCursor {
fn next(&mut self, graph: &FrozenGraph) -> Option<FrozenGraphIncidence> {
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<usize>,
incidences: Vec<IncidenceEntry>,
}
impl FrozenGraph {
fn raw_incidences(&self, v: Vertex) -> impl Iterator<Item = usize> {
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<T: Clone>(&self, default: T) -> ElementMap<Self::Vertex, T> {
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<T: Clone>(&self, default: T) -> ElementMap<Self::Edge, T> {
// "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<Item = Self::Vertex> {
(0..self.vertices.len() - 1).map(Vertex)
}
fn adjacent_vertices(&self, v: Self::Vertex) -> impl Iterator<Item = Self::Vertex> {
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<Item = Self::Edge> {
self.incidences
.iter()
.enumerate()
.filter_map(|(i, x)| (i < x.opposite).then_some(Edge(i)))
}
fn incident_edges(&self, v: Self::Vertex) -> impl Iterator<Item = Self::Edge> {
self.raw_incidences(v).map(|i| self.normalize_edge(i))
}
fn incidences(&self, v: Self::Vertex) -> impl Iterator<Item = FrozenGraphIncidence> {
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<G: GraphTopology>(source: &G) -> (Self, GraphIsomorphism<G, Self>) {
// 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<G: GraphTopology> 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"
);
}
}
}