323 lines
11 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|
|
}
|