diff --git a/Cargo.toml b/Cargo.toml index b063356..0ccf888 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "grapherity" -version = "0.2.1" +version = "0.2.3" authors = ["Stefan Müller"] edition = "2024" rust-version = "1.85.0" diff --git a/README.md b/README.md index 1b37b81..9954e5b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,63 @@ # grapherity -Library for graph models and algorithms. +Graph models and algorithms. -This library is still experimental and its API may therefore change frequently. +This crate provides data structures to model graphs and to perform algorithms on these data structures. The functionality presented is designed to be easy to use, flexible, and performant. + +Currently supported are: + +* Undirected graph types `Graph` and `AppendGraph`, built on a flat, index-based adjacency list, +* Maps to freely associate custom data with vertices and edges, +* Connectivity and pathing algorithms: Dijkstra's algorithm, DFS, and BFS in different variants, +* Exposed traits to implement custom graph data structures or algorithms. + +For more information, see the [documentation](https://docs.rs/grapherity/latest/grapherity/). + +## Example + +``` +// Brings commonly used traits into scope. +use grapherity::prelude::*; +use grapherity::models::Graph; +use grapherity::algorithms; + +let mut graph = Graph::new(); +// Creates a map for edge weights with a default weight. +let mut weights = graph.edge_map(1); +// Adds three vertices and two edges. +let v1 = graph.add_vertex(); +let v2 = graph.add_vertex(); +let v3 = graph.add_vertex(); +graph.add_edge(v1, v2); +let e = graph.add_edge(v2, v3); +// Sets a non-default weight for the second edge. +weights[e] = 5; + +// Checks the number of vertices. +assert_eq!(graph.vertex_count(), 3); +// Checks the number of edges. +assert_eq!(graph.edge_count(), 2); +// Checks that v1 and v2 are adjacent. +assert!(graph.are_adjacent(v1, v2)); +// Checks that v1 and v3 are not adjacent. +assert!(!graph.are_adjacent(v1, v3)); +// Checks the sum of all vertex degrees. +let sum: usize = graph.vertices().map(|v| graph.degree(v)).sum(); +assert_eq!(sum, 1 + 2 + 1); + +// Calls Dijkstra's algorithm using the edge weights to find the shortest path from v1 to v3. +let result = algorithms::dijkstra(&graph, v1, |e| weights[e]); +assert_eq!(result.distances[v3], Some(1 + 5)); +assert_eq!(result.predecessors[v3], Some(v2)); + +// Deletes the middle vertex and its incident edges. +graph.delete_vertex(v2); + +// Calls Dijkstra's algorithm again on the now disconnected graph. +let result = algorithms::dijkstra(&graph, v1, |e| weights[e]); +assert_eq!(result.distances[v3], None); +assert_eq!(result.predecessors[v3], None); +``` ## License diff --git a/src/algorithms.rs b/src/algorithms.rs index 730ed4c..b974441 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -122,12 +122,12 @@ where G: GraphTopology, { let mut predecessors = graph.vertex_map(None); - let (distances, _) = bfs_impl(graph, source, |neighbor, predecessor| { + let result = bfs_impl(graph, source, |neighbor, predecessor| { predecessors[neighbor] = Some(predecessor); true }); BfsResult { - distances, + distances: result.distances, predecessors, } } @@ -136,7 +136,7 @@ pub fn bfs_distances(graph: &G, source: G::Vertex) -> VertexMap(graph: &G, source: G::Vertex, target: G::Vertex) -> Option @@ -154,14 +154,15 @@ where if predicate(source) { return Some((source, 0)); } - bfs_impl(graph, source, |neighbor, _| !predicate(neighbor)).1 + bfs_impl(graph, source, |neighbor, _| !predicate(neighbor)).found } -fn bfs_impl( - graph: &G, - source: G::Vertex, - mut on_discover: F, -) -> (VertexMap>, Option<(G::Vertex, u32)>) +struct BfsImplResult { + distances: VertexMap>, + found: Option<(V, u32)>, +} + +fn bfs_impl(graph: &G, source: G::Vertex, mut on_discover: F) -> BfsImplResult where G: GraphTopology, F: FnMut(G::Vertex, G::Vertex) -> bool, @@ -178,13 +179,19 @@ where let distance = distances[v].unwrap() + 1; distances[neighbor] = Some(distance); if !on_discover(neighbor, v) { - return (distances, Some((neighbor, distance))); + return BfsImplResult { + distances, + found: Some((neighbor, distance)), + }; } queue.push_back(neighbor); } } } - (distances, None) + BfsImplResult { + distances, + found: None, + } } pub struct DfsResult { @@ -197,12 +204,12 @@ where G: GraphTopology, { let mut predecessors = graph.vertex_map(None); - let (visited, _) = dfs_impl(graph, source, |neighbor, predecessor| { + let result = dfs_impl(graph, source, |neighbor, predecessor| { predecessors[neighbor] = Some(predecessor); true }); DfsResult { - visited, + visited: result.visited, predecessors, } } @@ -211,7 +218,7 @@ pub fn dfs_visited(graph: &G, source: G::Vertex) -> VertexMap(graph: &G, source: G::Vertex, target: G::Vertex) -> bool @@ -229,14 +236,15 @@ where if predicate(source) { return Some(source); } - dfs_impl(graph, source, |neighbor, _| !predicate(neighbor)).1 + dfs_impl(graph, source, |neighbor, _| !predicate(neighbor)).found } -fn dfs_impl( - graph: &G, - source: G::Vertex, - mut on_discover: F, -) -> (VertexMap, Option) +struct DfsImplResult { + visited: VertexMap, + found: Option, +} + +fn dfs_impl(graph: &G, source: G::Vertex, mut on_discover: F) -> DfsImplResult where G: GraphTopology, F: FnMut(G::Vertex, G::Vertex) -> bool, @@ -248,7 +256,10 @@ where while let Some((v, predecessor)) = stack.pop() { if let Some(p) = predecessor { if !on_discover(v, p) { - return (visited, Some(v)); + return DfsImplResult { + visited, + found: Some(v), + }; } } for neighbor in graph.adjacent_vertices(v) { @@ -258,7 +269,10 @@ where } } } - (visited, None) + DfsImplResult { + visited, + found: None, + } } pub fn dfs_find_path(graph: &G, source: G::Vertex, target: G::Vertex) -> Option> diff --git a/src/lib.rs b/src/lib.rs index bffe3a4..ffb77ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,132 @@ +//! Graph models and algorithms. +//! +//! This crate provides data structures to model graphs and to perform algorithms on these data +//! structures. The functionality presented is designed to be easy to use, flexible, and performant. +//! +//! Currently supported are: +//! +//! * Undirected graph types [`Graph`] and [`AppendGraph`] built on a flat, index-based adjacency +//! list, +//! * [`VertexMap`] and [`EdgeMap`] to freely associate custom data with vertices and edges, +//! * Connectivity and pathing algorithms: [Dijkstra's algorithm], [DFS], and [BFS] in different +//! variants, +//! * Exposed traits to implement custom graph data structures or algorithms. +//! +//! # Usage example +//! +//! This example demonstrates some of this library's features. +//! +//! ``` +//! // Brings commonly used traits into scope. +//! use grapherity::prelude::*; +//! use grapherity::models::Graph; +//! use grapherity::algorithms; +//! +//! let mut graph = Graph::new(); +//! // Creates a map for edge weights with a default weight. +//! let mut weights = graph.edge_map(1); +//! // Adds three vertices and two edges. +//! let v1 = graph.add_vertex(); +//! let v2 = graph.add_vertex(); +//! let v3 = graph.add_vertex(); +//! graph.add_edge(v1, v2); +//! let e = graph.add_edge(v2, v3); +//! // Sets a non-default weight for the second edge. +//! weights[e] = 5; +//! +//! // Checks the number of vertices. +//! assert_eq!(graph.vertex_count(), 3); +//! // Checks the number of edges. +//! assert_eq!(graph.edge_count(), 2); +//! // Checks that v1 and v2 are adjacent. +//! assert!(graph.are_adjacent(v1, v2)); +//! // Checks that v1 and v3 are not adjacent. +//! assert!(!graph.are_adjacent(v1, v3)); +//! // Checks the sum of all vertex degrees. +//! let sum: usize = graph.vertices().map(|v| graph.degree(v)).sum(); +//! assert_eq!(sum, 1 + 2 + 1); +//! +//! // Calls Dijkstra's algorithm using the edge weights to find the shortest path from v1 to v3. +//! let result = algorithms::dijkstra(&graph, v1, |e| weights[e]); +//! assert_eq!(result.distances[v3], Some(1 + 5)); +//! assert_eq!(result.predecessors[v3], Some(v2)); +//! +//! // Deletes the middle vertex and its incident edges. +//! graph.delete_vertex(v2); +//! +//! // Calls Dijkstra's algorithm again on the now disconnected graph. +//! let result = algorithms::dijkstra(&graph, v1, |e| weights[e]); +//! assert_eq!(result.distances[v3], None); +//! assert_eq!(result.predecessors[v3], None); +//! ``` +//! +//! # Graph types +//! +//! [`Graph`] and [`AppendGraph`] both implement [`GraphTopology`] using a flat, index-based adjacency +//! list to store edges, which means that vertex and edge insertions happen in `O(1)`. Degree +//! lookups are also `O(1)`. +//! +//! [`Graph`] additionally implements [`GraphTopologyDeletion`], thereby supporting deletion of +//! vertices and edges. +//! +//! [`AppendGraph`] does not support deletion, vertices and edges can only be added, allowing it to +//! be smaller and more performant compared to [`Graph`] by not requiring the per-element generation +//! tracking needed for stable handles after deletion. +//! +//! # Vertices and edges +//! +//! Graphs in this library use stable indices [`GraphTopology::Vertex`] and [`GraphTopology::Edge`] +//! to denote vertices and edges. This means that vertices and edges returned by graph functions +//! never change, even if the underlying graph data structure is mutated. However, vertices and +//! edges can become invalid when the denoted element is deleted. Code using an invalid vertex or +//! edge in a graph method will panic. +//! +//! ```should_panic +//! # use grapherity::prelude::*; +//! # use grapherity::models::Graph; +//! let mut graph = Graph::new(); +//! let v = graph.add_vertex(); +//! graph.delete_vertex(v); +//! // This will panic because 'v' was deleted and is not valid anymore. +//! graph.degree(v); +//! ``` +//! +//! [`VertexMap`] and [`EdgeMap`] are not required to panic if provided with an invalid +//! vertex or edge. +//! +//! Vertices and edges are provided by the graph and not intended to be generated by users. +//! +//! # Loops and multi-edges +//! +//! Loops and multi edges are supported as well. In the following example, `v1` will have two loops +//! and two edges to `v2`. +//! +//! ``` +//! # use grapherity::prelude::*; +//! # use grapherity::models::Graph; +//! let mut graph = Graph::new(); +//! let v1 = graph.add_vertex(); +//! let v2 = graph.add_vertex(); +//! for _ in 0..2 { +//! graph.add_edge(v1, v1); +//! graph.add_edge(v1, v2); +//! } +//! assert_eq!(graph.edge_count(), 4); +//! assert_eq!(graph.degree(v1), 6); +//! ``` +//! +//! [BFS]: crate::algorithms::bfs +//! [Dijkstra's algorithm]: crate::algorithms::dijkstra +//! [DFS]: crate::algorithms::dfs +//! [`EdgeMap`]: crate::maps::EdgeMap +//! [`VertexMap`]: crate::maps::VertexMap +//! [`AppendGraph`]: crate::models::AppendGraph +//! [`Graph`]: crate::models::Graph +//! [`GraphTopology`]: crate::traits::GraphTopology +//! [`GraphTopology::Edge`]: crate::traits::GraphTopology::Edge +//! [`GraphTopology::Vertex`]: crate::traits::GraphTopology::Vertex +//! [`GraphTopologyDeletion`]: crate::traits::GraphTopologyDeletion + pub mod algorithms; pub mod maps; pub mod models; diff --git a/src/maps.rs b/src/maps.rs index 72b8a29..699587d 100644 --- a/src/maps.rs +++ b/src/maps.rs @@ -13,7 +13,7 @@ impl VertexMap { } } - // Reads beyond capacity() are valid and return the default value. + // Reads beyond 'capacity' are valid and return the default value. pub fn capacity(&self) -> usize { self.inner.capacity() } @@ -48,7 +48,7 @@ impl EdgeMap { } } - // Reads beyond capacity() are valid and return the default value. + // Reads beyond 'capacity' are valid and return the default value. pub fn capacity(&self) -> usize { self.inner.capacity() } diff --git a/src/models.rs b/src/models.rs index 5f556aa..d6b95a4 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,5 +1,7 @@ pub mod append_graph; pub mod graph; +// TODO: Compressed-sparse-row graph model. + pub use append_graph::{AppendGraph, AppendGraphEdgeMap, AppendGraphVertexMap}; pub use graph::{Graph, GraphEdgeMap, GraphVertexMap}; diff --git a/src/testing/maps_testing.rs b/src/testing/maps_testing.rs index 4bf9be4..e8b5362 100644 --- a/src/testing/maps_testing.rs +++ b/src/testing/maps_testing.rs @@ -53,8 +53,8 @@ macro_rules! vertex_map_tests { let mut graph = <$T>::new(); graph.add_vertex(); let mut map = graph.vertex_map(42); - let initial_len = map.capacity(); - while graph.vertex_capacity() <= initial_len { + let capacity_before = map.capacity(); + while graph.vertex_capacity() <= capacity_before { graph.add_vertex(); } assert!( @@ -197,8 +197,8 @@ macro_rules! edge_map_tests { let v2 = graph.add_vertex(); graph.add_edge(v1, v2); let mut map = graph.edge_map(42); - let initial_len = map.capacity(); - while graph.edge_capacity() <= initial_len { + let capacity_before = map.capacity(); + while graph.edge_capacity() <= capacity_before { graph.add_edge(v1, v2); } assert!( diff --git a/src/traits.rs b/src/traits.rs index 91924bf..096a316 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,35 +2,244 @@ use crate::maps::{EdgeMap, VertexMap}; // TODO: Add functions to reserve memory for vertices and edges. // TODO: Split out GraphTopologyAddition trait. +/// A trait representing an undirected graph topology. +/// +/// An undirected graph is a set of vertices and undirected edges, where each edge connects either +/// exactly two vertices or one vertex with itself (loop edge). This trait provides methods for +/// querying a graph topology, iterating over vertices and edges, and adding new vertices and edges. +/// +/// # Vertices and Edges +/// +/// Vertices and edges are identified by opaque handles ([`Vertex`] and [`Edge`]) that implement +/// [`Copy`] and [`Eq`]. Handles remain valid for the lifetime of the graph unless the graph also +/// implements [`GraphTopologyDeletion`] and the item identified by the handle is explicitly +/// deleted. +/// +/// Methods accepting vertices or edges as parameters panic if the handle was invalidated by a +/// deletion, and return incorrect results if the handle was not produced by this graph instance. +/// +/// # Deletion +/// +/// This trait covers graph construction and querying only. To delete vertices and edges, see +/// [`GraphTopologyDeletion`]. +/// +/// [`Edge`]: GraphTopology::Edge +/// [`Vertex`]: GraphTopology::Vertex pub trait GraphTopology { + /// An opaque, stable handle identifying a vertex. type Vertex: Copy + Eq; + + /// An opaque, stable handle identifying an edge. type Edge: Copy + Eq; + + /// A resumable position in the incidence list of a vertex. + /// + /// Prefer [`incidences`](Self::incidences) for straightforward iteration. A cursor is useful + /// when an algorithm needs to pause traversal, perform other graph queries or mutations, and + /// then continue from where it left off. This cursor type can be obtained via + /// [`incidence_cursor`](Self::incidence_cursor). type IncidenceCursor: IncidenceCursor; + /// Returns the number of vertices in the graph. fn vertex_count(&self) -> usize; + + /// Returns the total number of vertices the graph can hold without reallocating. fn vertex_capacity(&self) -> usize; + + /// Creates and returns a [`VertexMap`] with every slot initialised to `default`. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::Graph; + /// let mut graph = Graph::new(); + /// let v1 = graph.add_vertex(); + /// let mut labels = graph.vertex_map("z"); + /// assert_eq!(labels[v1], "z"); + /// labels[v1] = "a"; + /// assert_eq!(labels[v1], "a"); + /// + /// // A new vertex is immediately available for read and write. + /// let v2 = graph.add_vertex(); + /// assert_eq!(labels[v2], "z"); + /// labels[v2] = "b"; + /// assert_eq!(labels[v2], "b"); + /// ``` fn vertex_map(&self, default: T) -> VertexMap; + + /// Returns the number of edges in the graph. fn edge_count(&self) -> usize; + + /// Returns the total number of edges the graph can hold without reallocating. fn edge_capacity(&self) -> usize; + + /// Creates and returns an [`EdgeMap`] with every slot initialised to `default`. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::Graph; + /// let mut graph = Graph::new(); + /// let v1 = graph.add_vertex(); + /// let v2 = graph.add_vertex(); + /// let e1 = graph.add_edge(v1, v2); + /// let mut weights = graph.edge_map(5); + /// assert_eq!(weights[e1], 5); + /// weights[e1] = 1; + /// assert_eq!(weights[e1], 1); + /// + /// // A new edge is immediately available for read and write. + /// let e2 = graph.add_edge(v1, v2); + /// assert_eq!(weights[e2], 5); + /// weights[e2] = 2; + /// assert_eq!(weights[e2], 2); + /// ``` fn edge_map(&self, default: T) -> EdgeMap; + + /// Returns the degree of `v`, i.e. the number of incident edges, where each loop contributes 2. + /// + /// # Panics + /// + /// Panics if `v` is not a valid vertex of this graph. fn degree(&self, v: Self::Vertex) -> usize; + + /// Returns `true` if there is at least one edge between `v1` and `v2`, and `false` otherwise. + /// + /// A vertex is adjacent to itself if and only if it has a loop edge. + /// + /// # Panics + /// + /// Panics if `v1` or `v2` is not a valid vertex of this graph. fn are_adjacent(&self, v1: Self::Vertex, v2: Self::Vertex) -> bool; + + /// Returns an iterator over all vertices in the graph. fn vertices(&self) -> impl Iterator; + + /// Returns an iterator over all vertices adjacent to `v`. + /// + /// Algorithms may prefer this over [`incidences`](Self::incidences) if the edges are not + /// required, since implementors may be able to provide an iterator faster than the trivial + /// mapping. The complexity of this method must not exceed that of + /// [`incidences`](Self::incidences). + /// + /// # Panics + /// + /// Panics if `v` is not a valid vertex of this graph. fn adjacent_vertices(&self, v: Self::Vertex) -> impl Iterator; + + /// Returns the two endpoints of edge `e`, which are identical if and only if `e` is a loop + /// edge. + /// + /// # Panics + /// + /// Panics if `e` is not a valid edge of this graph. fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex); + + /// Returns an iterator over all edges in the graph. fn edges(&self) -> impl Iterator; + + /// Returns an iterator over all edges incident to `v`. + /// + /// Algorithms may prefer this over [`incidences`](Self::incidences) if the vertices are not + /// required, since implementors may be able to provide an iterator faster than the trivial + /// mapping. The complexity of this method must not exceed that of + /// [`incidences`](Self::incidences). + /// + /// # Panics + /// + /// Panics if `v` is not a valid vertex of this graph. fn incident_edges(&self, v: Self::Vertex) -> impl Iterator; + + /// Returns an iterator over all incidences of `v`, i.e. vertex-edge pairs `(u, e)` such that + /// `u` is adjacent to `v` and `e` is an edge between them. + /// + /// Use [`incidence_cursor`](Self::incidence_cursor) instead for a traversal that needs to + /// suspend and resume across other state updates. + /// + /// # Panics + /// + /// Panics if `v` is not a valid vertex of this graph. fn incidences(&self, v: Self::Vertex) -> impl Iterator; + + /// Returns a cursor over all incidences of `v`, analogously to + /// [`incidences`](Self::incidences), initially positioned before the first one. + /// + /// See [`IncidenceCursor`](Self::IncidenceCursor) for when to prefer a cursor over + /// [`incidences`](Self::incidences). + /// + /// # Panics + /// + /// Panics if `v` is not a valid vertex of this graph. fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor; + + /// Adds a new isolated vertex and returns its handle. fn add_vertex(&mut self) -> Self::Vertex; + + /// Adds a new edge between `v1` and `v2` and returns its handle. + /// + /// # Panics + /// + /// Panics if `v1` or `v2` is not a valid vertex of this graph. fn add_edge(&mut self, v1: Self::Vertex, v2: Self::Vertex) -> Self::Edge; } +/// A trait that adds deletion operations to an undirected graph topology. +/// +/// This trait provides methods for deletion of vertices and edges in an undirected graph. These +/// operations will invalidate handles to all deleted vertices and edges. Methods accepting invalid +/// vertices or edges as parameters panic. pub trait GraphTopologyDeletion: GraphTopology { + /// Deletes the vertex `v` and all its incident edges from the graph. Note that this also + /// invalidates the handles of all edges incident to `v`. + /// + /// # Panics + /// + /// Panics if `v` is not a valid vertex of this graph. fn delete_vertex(&mut self, v: Self::Vertex); + + /// Deletes the edge `e` from the graph. This operation only invalidates `e` and no other vertex + /// or edge handles. + /// + /// # Panics + /// + /// Panics if `e` is not a valid edge of this graph. fn delete_edge(&mut self, e: Self::Edge); } +/// A cursor for traversing the incidences of a vertex one step at a time. +/// +/// Cursors are obtained via [`GraphTopology::incidence_cursor`]. See +/// [`GraphTopology::IncidenceCursor`] for guidance on when to prefer a cursor over +/// [`GraphTopology::incidences`]. pub trait IncidenceCursor: Copy { + /// Advances the cursor and returns the next incidence as `Some((u, e))`, or `None` if the + /// traversal is exhausted. + /// + /// # Examples + /// + /// ``` + /// # use grapherity::prelude::*; + /// # use grapherity::models::Graph; + /// // Constructs a graph with two vertices connected to `v`. + /// let mut graph = Graph::new(); + /// let v = graph.add_vertex(); + /// for _ in 0..2 { + /// let u = graph.add_vertex(); + /// graph.add_edge(u, v); + /// } + /// + /// // Iterates over the incidences of `v` with a cursor. + /// let mut c1 = graph.incidence_cursor(v); + /// assert!(c1.next(&graph).is_some()); + /// let mut c2 = c1; + /// // Continues iteration with original cursor. + /// assert!(c1.next(&graph).is_some()); + /// assert!(c1.next(&graph).is_none()); + /// // Iterates over the last incidence again with the copied cursor. + /// assert!(c2.next(&graph).is_some()); + /// assert!(c2.next(&graph).is_none()); + /// ``` fn next(&mut self, graph: &G) -> Option<(G::Vertex, G::Edge)>; }