7 Commits

Author SHA1 Message Date
warrence c131f520f4 Add crate doc front-page, and update readme 2026-07-14 20:01:08 +02:00
warrence b6c27ea605 Merge branch 'main' into docs 2026-07-14 09:54:17 +02:00
warrence 5b30151010 Merge pull request 'Minor fixes' (#5) from fixes-minor into main
Reviewed-on: #5
2026-07-13 10:58:50 +02:00
warrence 2f549cc506 Update package version 2026-07-13 10:49:25 +02:00
warrence cf31ee2f01 Replace VertexMap/EdgeMap.len with capacity
Rename EdgeMap.len to capacity.
Add VertexMap.capacity and EdgeMap.capacity.
Deprecate VertexMap.len and EdgeMap.len.
2026-07-13 10:48:16 +02:00
warrence e58261ba47 Add BfsImplResult and DfsImplResult as named return structs 2026-07-10 13:12:30 +02:00
warrence 818b37ec15 Add linter warning for missing docs 2026-07-10 10:18:39 +02:00
7 changed files with 252 additions and 40 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "grapherity" name = "grapherity"
version = "0.2.1" version = "0.2.2"
authors = ["Stefan Müller"] authors = ["Stefan Müller"]
edition = "2024" edition = "2024"
rust-version = "1.85.0" rust-version = "1.85.0"
+57 -2
View File
@@ -1,8 +1,63 @@
# grapherity # 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 ## License
+36 -22
View File
@@ -122,12 +122,12 @@ where
G: GraphTopology, G: GraphTopology,
{ {
let mut predecessors = graph.vertex_map(None); 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); predecessors[neighbor] = Some(predecessor);
true true
}); });
BfsResult { BfsResult {
distances, distances: result.distances,
predecessors, predecessors,
} }
} }
@@ -136,7 +136,7 @@ pub fn bfs_distances<G>(graph: &G, source: G::Vertex) -> VertexMap<G::Vertex, Op
where where
G: GraphTopology, G: GraphTopology,
{ {
bfs_impl(graph, source, |_, _| true).0 bfs_impl(graph, source, |_, _| true).distances
} }
pub fn bfs_find<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> Option<u32> pub fn bfs_find<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> Option<u32>
@@ -154,14 +154,15 @@ where
if predicate(source) { if predicate(source) {
return Some((source, 0)); return Some((source, 0));
} }
bfs_impl(graph, source, |neighbor, _| !predicate(neighbor)).1 bfs_impl(graph, source, |neighbor, _| !predicate(neighbor)).found
} }
fn bfs_impl<G, F>( struct BfsImplResult<V: Copy> {
graph: &G, distances: VertexMap<V, Option<u32>>,
source: G::Vertex, found: Option<(V, u32)>,
mut on_discover: F, }
) -> (VertexMap<G::Vertex, Option<u32>>, Option<(G::Vertex, u32)>)
fn bfs_impl<G, F>(graph: &G, source: G::Vertex, mut on_discover: F) -> BfsImplResult<G::Vertex>
where where
G: GraphTopology, G: GraphTopology,
F: FnMut(G::Vertex, G::Vertex) -> bool, F: FnMut(G::Vertex, G::Vertex) -> bool,
@@ -178,13 +179,19 @@ where
let distance = distances[v].unwrap() + 1; let distance = distances[v].unwrap() + 1;
distances[neighbor] = Some(distance); distances[neighbor] = Some(distance);
if !on_discover(neighbor, v) { if !on_discover(neighbor, v) {
return (distances, Some((neighbor, distance))); return BfsImplResult {
distances,
found: Some((neighbor, distance)),
};
} }
queue.push_back(neighbor); queue.push_back(neighbor);
} }
} }
} }
(distances, None) BfsImplResult {
distances,
found: None,
}
} }
pub struct DfsResult<V: Copy> { pub struct DfsResult<V: Copy> {
@@ -197,12 +204,12 @@ where
G: GraphTopology, G: GraphTopology,
{ {
let mut predecessors = graph.vertex_map(None); 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); predecessors[neighbor] = Some(predecessor);
true true
}); });
DfsResult { DfsResult {
visited, visited: result.visited,
predecessors, predecessors,
} }
} }
@@ -211,7 +218,7 @@ pub fn dfs_visited<G>(graph: &G, source: G::Vertex) -> VertexMap<G::Vertex, bool
where where
G: GraphTopology, G: GraphTopology,
{ {
dfs_impl(graph, source, |_, _| true).0 dfs_impl(graph, source, |_, _| true).visited
} }
pub fn dfs_find<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> bool pub fn dfs_find<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> bool
@@ -229,14 +236,15 @@ where
if predicate(source) { if predicate(source) {
return Some(source); return Some(source);
} }
dfs_impl(graph, source, |neighbor, _| !predicate(neighbor)).1 dfs_impl(graph, source, |neighbor, _| !predicate(neighbor)).found
} }
fn dfs_impl<G, F>( struct DfsImplResult<V: Copy> {
graph: &G, visited: VertexMap<V, bool>,
source: G::Vertex, found: Option<V>,
mut on_discover: F, }
) -> (VertexMap<G::Vertex, bool>, Option<G::Vertex>)
fn dfs_impl<G, F>(graph: &G, source: G::Vertex, mut on_discover: F) -> DfsImplResult<G::Vertex>
where where
G: GraphTopology, G: GraphTopology,
F: FnMut(G::Vertex, G::Vertex) -> bool, F: FnMut(G::Vertex, G::Vertex) -> bool,
@@ -248,7 +256,10 @@ where
while let Some((v, predecessor)) = stack.pop() { while let Some((v, predecessor)) = stack.pop() {
if let Some(p) = predecessor { if let Some(p) = predecessor {
if !on_discover(v, p) { if !on_discover(v, p) {
return (visited, Some(v)); return DfsImplResult {
visited,
found: Some(v),
};
} }
} }
for neighbor in graph.adjacent_vertices(v) { for neighbor in graph.adjacent_vertices(v) {
@@ -258,7 +269,10 @@ where
} }
} }
} }
(visited, None) DfsImplResult {
visited,
found: None,
}
} }
pub fn dfs_find_path<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> Option<Vec<G::Edge>> pub fn dfs_find_path<G>(graph: &G, source: G::Vertex, target: G::Vertex) -> Option<Vec<G::Edge>>
+131
View File
@@ -1,3 +1,134 @@
//! 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
#![warn(missing_docs)]
pub mod algorithms; pub mod algorithms;
pub mod maps; pub mod maps;
pub mod models; pub mod models;
+15 -5
View File
@@ -13,9 +13,14 @@ impl<V: Copy, T: Clone> VertexMap<V, T> {
} }
} }
// Reads beyond len() 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()
}
#[deprecated(since = "0.2.2", note = "use 'capacity' instead")]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.inner.len() self.capacity()
} }
pub fn sync<G: GraphTopology<Vertex = V>>(&mut self, graph: &G) { pub fn sync<G: GraphTopology<Vertex = V>>(&mut self, graph: &G) {
@@ -48,9 +53,14 @@ impl<E: Copy, T: Clone> EdgeMap<E, T> {
} }
} }
// Reads beyond len() 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()
}
#[deprecated(since = "0.2.2", note = "use 'capacity' instead")]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.inner.len() self.capacity()
} }
pub fn sync<G: GraphTopology<Edge = E>>(&mut self, graph: &G) { pub fn sync<G: GraphTopology<Edge = E>>(&mut self, graph: &G) {
@@ -87,7 +97,7 @@ impl<E: Copy, T: Clone> EntityMap<E, T> {
} }
} }
pub fn len(&self) -> usize { pub fn capacity(&self) -> usize {
self.data.len() self.data.len()
} }
+2
View File
@@ -1,5 +1,7 @@
pub mod append_graph; pub mod append_graph;
pub mod graph; pub mod graph;
// TODO: Compressed-sparse-row graph model.
pub use append_graph::{AppendGraph, AppendGraphEdgeMap, AppendGraphVertexMap}; pub use append_graph::{AppendGraph, AppendGraphEdgeMap, AppendGraphVertexMap};
pub use graph::{Graph, GraphEdgeMap, GraphVertexMap}; pub use graph::{Graph, GraphEdgeMap, GraphVertexMap};
+10 -10
View File
@@ -53,16 +53,16 @@ macro_rules! vertex_map_tests {
let mut graph = <$T>::new(); let mut graph = <$T>::new();
graph.add_vertex(); graph.add_vertex();
let mut map = graph.vertex_map(42); let mut map = graph.vertex_map(42);
let initial_len = map.len(); let capacity_before = map.capacity();
while graph.vertex_capacity() <= initial_len { while graph.vertex_capacity() <= capacity_before {
graph.add_vertex(); graph.add_vertex();
} }
assert!( assert!(
map.len() < graph.vertex_capacity(), map.capacity() < graph.vertex_capacity(),
"precondition: map is stale before sync" "precondition: map is stale before sync"
); );
map.sync(&graph); map.sync(&graph);
assert_eq!(map.len(), graph.vertex_capacity()); assert_eq!(map.capacity(), graph.vertex_capacity());
} }
#[test] #[test]
@@ -109,7 +109,7 @@ macro_rules! vertex_map_deletion_tests {
let capacity_before = graph.vertex_capacity(); let capacity_before = graph.vertex_capacity();
graph.delete_vertex(v2); graph.delete_vertex(v2);
map.sync(&graph); map.sync(&graph);
assert_eq!(map.len(), capacity_before); assert_eq!(map.capacity(), capacity_before);
assert_eq!(map[v1], 5); assert_eq!(map[v1], 5);
} }
@@ -197,16 +197,16 @@ macro_rules! edge_map_tests {
let v2 = graph.add_vertex(); let v2 = graph.add_vertex();
graph.add_edge(v1, v2); graph.add_edge(v1, v2);
let mut map = graph.edge_map(42); let mut map = graph.edge_map(42);
let initial_len = map.len(); let capacity_before = map.capacity();
while graph.edge_capacity() <= initial_len { while graph.edge_capacity() <= capacity_before {
graph.add_edge(v1, v2); graph.add_edge(v1, v2);
} }
assert!( assert!(
map.len() < graph.edge_capacity(), map.capacity() < graph.edge_capacity(),
"precondition: map is stale before sync" "precondition: map is stale before sync"
); );
map.sync(&graph); map.sync(&graph);
assert_eq!(map.len(), graph.edge_capacity()); assert_eq!(map.capacity(), graph.edge_capacity());
} }
#[test] #[test]
@@ -259,7 +259,7 @@ macro_rules! edge_map_deletion_tests {
let capacity_before = graph.edge_capacity(); let capacity_before = graph.edge_capacity();
graph.delete_edge(e2); graph.delete_edge(e2);
map.sync(&graph); map.sync(&graph);
assert_eq!(map.len(), capacity_before); assert_eq!(map.capacity(), capacity_before);
assert_eq!(map[e1], 5); assert_eq!(map[e1], 5);
} }