Merge remote-tracking branch 'origin/bm/algorithms-shortest-path' into bm/dino-closure
# Conflicts:
# .gitignore
# crates/algorithms/src/shortest_paths/astar/impl.rs
# crates/algorithms/src/shortest_paths/common/path.rs
# crates/algorithms/src/shortest_paths/common/transit.rs
# crates/algorithms/src/shortest_paths/dijkstra/iter.rs
# crates/algorithms/src/shortest_paths/dijkstra/mod.rs
# crates/algorithms/src/shortest_paths/mod.rs
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..d02eb85
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,59 @@
+crates/algorithms/tests/cases/**/* binary=true linguist-generated=true
+crates/algorithms/tests/cases/shortest_path/max_dense_long_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_sparse_random_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/sparse_random_02.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/almost_line_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/almost_line_01.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/line_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_sparse_random_01.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_03.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_01.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_03.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/spfa_killer_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/example_01.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/grid_swirl_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_star_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_01.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_dense_random_01.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_sparse_random_02.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_04.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/example_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_dense_random_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/sparse_random_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_dense_long_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_04.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/sparse_random_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/spfa_killer_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/almost_line_01.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/grid_random_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_dense_zero_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/example_01.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_dense_random_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_sparse_random_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_sparse_random_02.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/almost_line_02.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/almost_line_02.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_02.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/sparse_random_01.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_handmade_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/line_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_star_01.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_01.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/sparse_random_01.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/sparse_random_02.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/example_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/grid_random_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/small_02.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_01.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/almost_line_00.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_dense_random_01.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_sparse_random_01.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_star_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_star_01.in filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_handmade_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/grid_swirl_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/max_dense_zero_00.out filter=lfs diff=lfs merge=lfs -text
+crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_00.in filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 14f7167..776fb0d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,7 +23,7 @@
strategy:
fail-fast: false
matrix:
- toolchain: [ 1.65, stable, nightly ]
+ toolchain: [ 1.75, stable, nightly ]
steps:
- name: Checkout source code
@@ -78,7 +78,7 @@
strategy:
fail-fast: false
matrix:
- toolchain: [ 1.65, stable, nightly ]
+ toolchain: [ 1.75, stable, nightly ]
steps:
- name: Checkout source code
@@ -119,7 +119,7 @@
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly
- components: ${{ matrix.toolchain == 'nightly' && 'rustfmt clippy llvm-tools-preview miri' || 'rustfmt clippy' }}
+ components: rustfmt clippy llvm-tools-preview miri
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
diff --git a/.gitignore b/.gitignore
index c0dfc31..f4602b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Generated by Cargo
/target/
Cargo.lock
-
+/crates/algorithms/tests/cases/problems
+venv
crates/algorithms/benches/input
diff --git a/Cargo.toml b/Cargo.toml
index e34d026..503db1e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -49,6 +49,7 @@
petgraph-test-utils = { path = "crates/test-utils", default-features = false }
petgraph-dino = { path = "crates/dino", default-features = false }
petgraph-utils = { path = "crates/utils", default-features = false }
+numi = { path = "crates/numi", default-features = false }
[dependencies]
petgraph-adjacency-matrix = { workspace = true, optional = true, default-features = false }
diff --git a/crates/algorithms/Cargo.toml b/crates/algorithms/Cargo.toml
index c7807c4..8e9dec9 100644
--- a/crates/algorithms/Cargo.toml
+++ b/crates/algorithms/Cargo.toml
@@ -9,7 +9,7 @@
petgraph-core = { workspace = true, features = ["alloc"] }
#petgraph-graph = { workspace = true, optional = true }
#petgraph-adjacency-matrix = { workspace = true, optional = true }
-funty.workspace = true
+numi = { workspace = true }
# Like `std::collection::HashMap`, `IndexMap` is built on top of `hashbrown`, but because
# we're already using `IndexMap` in a lot of crates I do not want to pull in `hashbrown`
# separately as it would be another version to maintain. This way we only have one version.
@@ -18,7 +18,6 @@
fixedbitset.workspace = true
error-stack.workspace = true
hashbrown = { workspace = true, features = ['ahash'] }
-num-traits = "0.2.17"
either = "1.9.0"
dary_heap = "0.3.6"
@@ -29,6 +28,7 @@
#petgraph = { workspace = true, default-features = true, features = ['proptest', 'stable-graph'] }
# petgraph is only available in integration tests, while we use petgraph-graph in unit tests
#petgraph-graph = { workspace = true, features = ['stable'] }
+numi = { workspace = true, features = ["ordered-float-impl"] }
petgraph-dino = { workspace = true }
petgraph-utils = { workspace = true }
petgraph-proptest = { workspace = true }
@@ -37,11 +37,13 @@
approx = "0.5.1"
criterion = "0.5.1"
rand = "0.8.5"
-#petgraph-test-utils.workspace = true
itertools = "0.10.5"
ordered-float = "4.1.1"
serde_json = "1.0.107"
static_assertions = "1.1.0"
+snapbox = { version = "0.4.15" }
+libtest-mimic = "0.6.1"
+ignore = "0.4.21"
[features]
default = ["std"]
@@ -76,3 +78,7 @@
[[bench]]
name = "tree"
harness = false
+
+[[test]]
+name = "shortest_paths"
+harness = false
diff --git a/crates/algorithms/src/lib.rs b/crates/algorithms/src/lib.rs
index 5c9ea82..1565897 100644
--- a/crates/algorithms/src/lib.rs
+++ b/crates/algorithms/src/lib.rs
@@ -1,4 +1,3 @@
-#![feature(return_position_impl_trait_in_trait)]
//! Graph algorithms.
//!
//! It is a goal to gradually migrate the algorithms to be based on graph traits
diff --git a/crates/algorithms/src/shortest_paths/astar/heuristic.rs b/crates/algorithms/src/shortest_paths/astar/heuristic.rs
index ecb6f3f..a6bccb6 100644
--- a/crates/algorithms/src/shortest_paths/astar/heuristic.rs
+++ b/crates/algorithms/src/shortest_paths/astar/heuristic.rs
@@ -1,4 +1,5 @@
-use petgraph_core::{base::MaybeOwned, GraphStorage, Node};
+use numi::borrow::Moo;
+use petgraph_core::{GraphStorage, Node};
pub trait GraphHeuristic<S>
where
@@ -6,18 +7,17 @@
{
type Value;
- fn estimate<'a>(&self, source: Node<'a, S>, target: Node<'a, S>)
- -> MaybeOwned<'a, Self::Value>;
+ fn estimate<'a>(&self, source: Node<'a, S>, target: Node<'a, S>) -> Moo<'a, Self::Value>;
}
impl<S, F, T> GraphHeuristic<S> for F
where
S: GraphStorage,
- F: for<'a> Fn(Node<'a, S>, Node<'a, S>) -> MaybeOwned<'a, T>,
+ F: for<'a> Fn(Node<'a, S>, Node<'a, S>) -> Moo<'a, T>,
{
type Value = T;
- fn estimate<'a>(&self, source: Node<'a, S>, target: Node<'a, S>) -> MaybeOwned<'a, T> {
+ fn estimate<'a>(&self, source: Node<'a, S>, target: Node<'a, S>) -> Moo<'a, T> {
self(source, target)
}
}
diff --git a/crates/algorithms/src/shortest_paths/astar/impl.rs b/crates/algorithms/src/shortest_paths/astar/impl.rs
index ed28e0c..263d1a8 100644
--- a/crates/algorithms/src/shortest_paths/astar/impl.rs
+++ b/crates/algorithms/src/shortest_paths/astar/impl.rs
@@ -1,14 +1,14 @@
use alloc::vec::Vec;
-use core::{hash::Hash, ops::Add};
+use core::hash::Hash;
use error_stack::{Report, Result};
use fxhash::FxBuildHasher;
use hashbrown::HashMap;
-use num_traits::Zero;
-use petgraph_core::{id::FlaggableGraphId, Graph, GraphStorage, Node};
+use numi::num::{identity::Zero, ops::AddRef};
+use petgraph_core::{Graph, GraphStorage, Node};
use crate::shortest_paths::{
- astar::{error::AStarError, heuristic::GraphHeuristic},
+ astar::{error::AStarError, heuristic::GraphHeuristic, AStarMeasure},
common::{
connections::Connections,
cost::{Cost, GraphCost},
@@ -47,8 +47,7 @@
S: GraphStorage,
S::NodeId: FlaggableGraphId<S> + Eq + Hash,
E: GraphCost<S>,
- E::Value: PartialOrd + Ord + Zero + Clone + 'graph,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: AStarMeasure,
H: GraphHeuristic<S, Value = E::Value>,
C: Connections<'graph, S> + 'parent,
{
@@ -122,7 +121,8 @@
let connections = self.connections.connections(&node);
for edge in connections {
- let alternative = &self.distances[node.id()] + self.edge_cost.cost(edge).as_ref();
+ let alternative =
+ self.distances[node.id()].add_ref(self.edge_cost.cost(edge).as_ref());
let (u, v) = edge.endpoints();
let neighbour = if u.id() == node.id() { v } else { u };
@@ -133,7 +133,8 @@
}
}
- let guess = &alternative + self.heuristic.estimate(neighbour, self.target).as_ref();
+ let guess =
+ alternative.add_ref(self.heuristic.estimate(neighbour, self.target).as_ref());
self.distances.insert(neighbour.id(), alternative);
if self.predecessor_mode == PredecessorMode::Record {
diff --git a/crates/algorithms/src/shortest_paths/astar/measure.rs b/crates/algorithms/src/shortest_paths/astar/measure.rs
new file mode 100644
index 0000000..dc8878a
--- /dev/null
+++ b/crates/algorithms/src/shortest_paths/astar/measure.rs
@@ -0,0 +1,67 @@
+use numi::num::{identity::Zero, ops::AddRef};
+
+/// Automatically implemented trait for types that can be used as a measure in the Dijkstra
+///
+/// This trait is implemented for all types that implement the supertraits mentioned in the trait
+/// definition.
+/// These traits either originate from [`core`] or [`numi`].
+/// Special attention must be paid to the [`AddRef`] trait, which is a proxy trait which is
+/// implemented for types that implement: `&Self: Add<&Self, Output = Self>`.
+///
+/// # Example
+///
+/// ```rust
+/// use core::num::Wrapping;
+/// use core::num::Saturating;
+/// use ordered_float::NotNan;
+/// use petgraph_algorithms::shortest_paths::astar::AStarMeasure;
+/// use static_assertions::assert_impl_all;
+///
+/// // Some examples of types that implement DijkstraMeasure
+/// assert_impl_all!(u8: AStarMeasure);
+/// assert_impl_all!(u16: AStarMeasure);
+/// assert_impl_all!(u32: AStarMeasure);
+/// assert_impl_all!(u64: AStarMeasure);
+/// assert_impl_all!(u128: AStarMeasure);
+/// assert_impl_all!(usize: AStarMeasure);
+///
+/// assert_impl_all!(i8: AStarMeasure);
+/// assert_impl_all!(i16: AStarMeasure);
+/// assert_impl_all!(i32: AStarMeasure);
+/// assert_impl_all!(i64: AStarMeasure);
+/// assert_impl_all!(i128: AStarMeasure);
+/// assert_impl_all!(isize: AStarMeasure);
+///
+/// assert_impl_all!(NotNan<f32>: AStarMeasure);
+/// assert_impl_all!(NotNan<f64>: AStarMeasure);
+///
+/// assert_impl_all!(Wrapping<u8>: AStarMeasure);
+/// assert_impl_all!(Wrapping<u16>: AStarMeasure);
+/// assert_impl_all!(Wrapping<u32>: AStarMeasure);
+/// assert_impl_all!(Wrapping<u64>: AStarMeasure);
+/// assert_impl_all!(Wrapping<u128>: AStarMeasure);
+/// assert_impl_all!(Wrapping<usize>: AStarMeasure);
+///
+/// assert_impl_all!(Wrapping<i8>: AStarMeasure);
+/// assert_impl_all!(Wrapping<i16>: AStarMeasure);
+/// assert_impl_all!(Wrapping<i32>: AStarMeasure);
+/// assert_impl_all!(Wrapping<i64>: AStarMeasure);
+/// assert_impl_all!(Wrapping<i128>: AStarMeasure);
+///
+/// assert_impl_all!(Saturating<u8>: AStarMeasure);
+/// assert_impl_all!(Saturating<u16>: AStarMeasure);
+/// assert_impl_all!(Saturating<u32>: AStarMeasure);
+/// assert_impl_all!(Saturating<u64>: AStarMeasure);
+/// assert_impl_all!(Saturating<u128>: AStarMeasure);
+/// assert_impl_all!(Saturating<usize>: AStarMeasure);
+///
+/// assert_impl_all!(Saturating<i8>: AStarMeasure);
+/// assert_impl_all!(Saturating<i16>: AStarMeasure);
+/// assert_impl_all!(Saturating<i32>: AStarMeasure);
+/// assert_impl_all!(Saturating<i64>: AStarMeasure);
+/// assert_impl_all!(Saturating<i128>: AStarMeasure);
+/// assert_impl_all!(Saturating<isize>: AStarMeasure);
+/// ```
+pub trait AStarMeasure: Clone + PartialOrd + Ord + AddRef<Self, Output = Self> + Zero {}
+
+impl<T> AStarMeasure for T where T: Clone + PartialOrd + Ord + AddRef<Self, Output = Self> + Zero {}
diff --git a/crates/algorithms/src/shortest_paths/astar/mod.rs b/crates/algorithms/src/shortest_paths/astar/mod.rs
index 065c765..fa9935d 100644
--- a/crates/algorithms/src/shortest_paths/astar/mod.rs
+++ b/crates/algorithms/src/shortest_paths/astar/mod.rs
@@ -1,14 +1,15 @@
+//! A* shortest path algorithm.
mod error;
mod heuristic;
mod r#impl;
+mod measure;
#[cfg(test)]
mod tests;
use alloc::vec::Vec;
-use core::{hash::Hash, marker::PhantomData, ops::Add};
+use core::{hash::Hash, marker::PhantomData};
use error_stack::Result;
-use num_traits::Zero;
use petgraph_core::{
edge::marker::{Directed, Undirected},
Direction,
@@ -17,20 +18,30 @@
DirectedGraphStorage, Graph, GraphDirectionality, GraphStorage, Node,
};
-use self::{error::AStarError, r#impl::AStarImpl};
-use super::{common::transit::PredecessorMode, ShortestDistance, ShortestPath};
-use crate::{
- polyfill::IteratorExt,
- shortest_paths::{
- astar::heuristic::GraphHeuristic,
- common::{
- connections::{outgoing_connections, Connections},
- cost::{Cost, DefaultCost, GraphCost},
- route::{DirectRoute, Route},
- },
+use self::r#impl::AStarImpl;
+pub use self::{error::AStarError, heuristic::GraphHeuristic, measure::AStarMeasure};
+use super::{
+ common::{
+ connections::{outgoing_connections, Connections},
+ cost::{Cost, DefaultCost, GraphCost},
+ route::{DirectRoute, Route},
+ transit::PredecessorMode,
},
+ ShortestDistance, ShortestPath,
};
+use crate::polyfill::IteratorExt;
+/// A* shortest path algorithm.
+///
+/// A* is a shortest path algorithm that uses a heuristic to guide the search.
+/// It is an extension of Dijkstra's algorithm, and is guaranteed to find the shortest path if the
+/// heuristic is admissible.
+/// A heuristic is admissible if it never overestimates the cost to reach the goal.
+///
+/// This implementation of A* is generic over the graph directionality, edge cost, and heuristic.
+///
+/// Edge weights need to implement [`AStarMeasure`], a trait that is automatically implemented for
+/// all types that satisfy the constraints of the algorithm.
pub struct AStar<D, E, H>
where
D: GraphDirectionality,
@@ -42,6 +53,43 @@
}
impl AStar<Directed, DefaultCost, ()> {
+ /// Create a new A* instance with the default edge cost and no heuristic.
+ ///
+ /// You won't be able to run A* without providing a heuristic using [`Self::with_heuristic`].
+ ///
+ /// If instantiated for a directed graph, [`AStar`] will not implement [`ShortestPath`] and
+ /// [`ShortestDistance`] on undirected graphs.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use numi::borrow::Moo;
+ /// use petgraph_algorithms::shortest_paths::{AStar, ShortestPath};
+ /// use petgraph_core::{edge::marker::Directed, GraphStorage, Node};
+ /// use petgraph_dino::{DiDinoGraph, DinoStorage};
+ ///
+ /// // TODO: heuristic utils
+ /// fn heuristic<'graph, S>(source: Node<'graph, S>, target: Node<'graph, S>) -> Moo<'graph, i32>
+ /// where
+ /// S: GraphStorage<NodeWeight = (i32, i32), EdgeWeight = i32>,
+ /// {
+ /// let source = source.weight();
+ /// let target = target.weight();
+ ///
+ /// Moo::Owned((source.0 - target.0).abs() + (source.1 - target.1).abs())
+ /// }
+ ///
+ /// let algorithm = AStar::directed().with_heuristic(heuristic);
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node((0, 1)).id();
+ /// let b = *graph.insert_node((2, 2)).id();
+ ///
+ /// graph.insert_edge(5, &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b).expect("path exists");
+ /// assert_eq!(path.cost().into_value(), 5);
+ /// ```
pub fn directed() -> Self {
Self {
edge_cost: DefaultCost,
@@ -106,8 +154,7 @@
S: DirectedGraphStorage,
S::NodeId: FlaggableGraphId<S> + Eq + Hash,
E: GraphCost<S>,
- E::Value: PartialOrd + Ord + Zero + Clone + 'graph,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: AStarMeasure,
H: GraphHeuristic<S, Value = E::Value>,
{
AStarImpl::new(
@@ -134,8 +181,7 @@
S: GraphStorage,
S::NodeId: FlaggableGraphId<S> + Eq + Hash,
E: GraphCost<S>,
- E::Value: PartialOrd + Ord + Zero + Clone + 'graph,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: AStarMeasure,
H: GraphHeuristic<S, Value = E::Value>,
{
AStarImpl::new(
@@ -158,8 +204,7 @@
S: GraphStorage,
S::NodeId: FlaggableGraphId<S> + Eq + Hash,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: AStarMeasure,
H: GraphHeuristic<S, Value = E::Value>,
{
type Cost = E::Value;
@@ -228,8 +273,7 @@
S: GraphStorage,
S::NodeId: FlaggableGraphId<S> + Eq + Hash,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: AStarMeasure,
H: GraphHeuristic<S, Value = E::Value>,
{
type Cost = E::Value;
@@ -299,8 +343,7 @@
S: DirectedGraphStorage,
S::NodeId: FlaggableGraphId<S> + Eq + Hash,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: AStarMeasure,
H: GraphHeuristic<S, Value = E::Value>,
{
type Cost = E::Value;
@@ -369,8 +412,7 @@
S: DirectedGraphStorage,
S::NodeId: FlaggableGraphId<S> + Eq + Hash,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: AStarMeasure,
H: GraphHeuristic<S, Value = E::Value>,
{
type Cost = E::Value;
diff --git a/crates/algorithms/src/shortest_paths/astar/tests.rs b/crates/algorithms/src/shortest_paths/astar/tests.rs
index 00ca85e..f13146b 100644
--- a/crates/algorithms/src/shortest_paths/astar/tests.rs
+++ b/crates/algorithms/src/shortest_paths/astar/tests.rs
@@ -1,8 +1,9 @@
use alloc::{vec, vec::Vec};
use core::sync::atomic::{AtomicUsize, Ordering};
+use numi::borrow::Moo;
use ordered_float::NotNan;
-use petgraph_core::{base::MaybeOwned, edge::marker::Directed, Edge, GraphStorage, Node};
+use petgraph_core::{edge::marker::Directed, Edge, GraphStorage, Node};
use petgraph_dino::{DiDinoGraph, DinoStorage, EdgeId, NodeId};
use petgraph_utils::{graph, GraphCollection};
@@ -73,15 +74,14 @@
] as EdgeId
);
-const fn no_heuristic<'a, S>(_: Node<'a, S>, _: Node<'a, S>) -> MaybeOwned<'a, usize>
+const fn no_heuristic<'a, S>(_: Node<'a, S>, _: Node<'a, S>) -> Moo<'a, usize>
where
S: GraphStorage,
{
- MaybeOwned::Owned(0)
+ Moo::Owned(0)
}
// TODO: multigraph
-// TODO: more test cases
#[test]
fn directed_path_between() {
@@ -179,10 +179,10 @@
assert!(cost.is_none());
}
-fn manhattan_distance<'a, S>(
- source: Node<'a, S>,
- target: Node<'a, S>,
-) -> MaybeOwned<'a, NotNan<f32>>
+fn manhattan_distance<'graph, S>(
+ source: Node<'graph, S>,
+ target: Node<'graph, S>,
+) -> Moo<'graph, NotNan<f32>>
where
S: GraphStorage<NodeWeight = Point>,
{
@@ -192,16 +192,16 @@
let distance =
NotNan::new(source.manhattan_distance(*target)).expect("distance should be a number");
- MaybeOwned::Owned(distance)
+ Moo::Owned(distance)
}
-fn ensure_not_nan<S>(edge: Edge<S>) -> MaybeOwned<'_, NotNan<f32>>
+fn ensure_not_nan<S>(edge: Edge<S>) -> Moo<'_, NotNan<f32>>
where
S: GraphStorage<EdgeWeight = f32>,
{
let weight = NotNan::new(*edge.weight()).expect("weight should be a number");
- MaybeOwned::Owned(weight)
+ Moo::Owned(weight)
}
#[test]
@@ -211,6 +211,7 @@
let astar = AStar::directed()
.with_edge_cost(ensure_not_nan)
.with_heuristic(manhattan_distance);
+
let route = astar.path_between(&graph, &nodes.a, &nodes.f).unwrap();
let (path, cost) = route.into_parts();
@@ -264,15 +265,15 @@
] as EdgeId
);
-fn admissible_inconsistent<'a, S>(source: Node<'a, S>, target: Node<'a, S>) -> MaybeOwned<'a, usize>
+fn admissible_inconsistent<'a, S>(source: Node<'a, S>, target: Node<'a, S>) -> Moo<'a, usize>
where
S: GraphStorage,
S::NodeWeight: AsRef<str>,
{
match source.weight().as_ref() {
- "A" => MaybeOwned::Owned(9),
- "B" => MaybeOwned::Owned(6),
- _ => MaybeOwned::Owned(0),
+ "A" => Moo::Owned(9),
+ "B" => Moo::Owned(6),
+ _ => Moo::Owned(0),
}
}
@@ -323,7 +324,7 @@
#[test]
fn optimal_runtime() {
// Needed to bind the lifetime of the closure, does some compiler magic.
- fn bind<S, T>(closure: impl Fn(Edge<S>) -> MaybeOwned<T>) -> impl Fn(Edge<S>) -> MaybeOwned<T> {
+ fn bind<S, T>(closure: impl Fn(Edge<S>) -> Moo<T>) -> impl Fn(Edge<S>) -> Moo<T> {
closure
}
@@ -334,7 +335,7 @@
let astar = AStar::directed()
.with_edge_cost(bind(|edge: Edge<DinoStorage<char, usize, Directed>>| {
CALLS.fetch_add(1, Ordering::SeqCst);
- MaybeOwned::Borrowed(edge.weight())
+ Moo::Borrowed(edge.weight())
}))
.with_heuristic(no_heuristic);
diff --git a/crates/algorithms/src/shortest_paths/bellman_ford/error.rs b/crates/algorithms/src/shortest_paths/bellman_ford/error.rs
new file mode 100644
index 0000000..9117483
--- /dev/null
+++ b/crates/algorithms/src/shortest_paths/bellman_ford/error.rs
@@ -0,0 +1,20 @@
+use core::fmt::{Display, Formatter};
+
+use error_stack::Context;
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum BellmanFordError {
+ NodeNotFound,
+ NegativeCycle,
+}
+
+impl Display for BellmanFordError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Self::NodeNotFound => write!(f, "node not found"),
+ Self::NegativeCycle => write!(f, "negative cycle"),
+ }
+ }
+}
+
+impl Context for BellmanFordError {}
diff --git a/crates/algorithms/src/shortest_paths/bellman_ford/impl.rs b/crates/algorithms/src/shortest_paths/bellman_ford/impl.rs
new file mode 100644
index 0000000..9e44259
--- /dev/null
+++ b/crates/algorithms/src/shortest_paths/bellman_ford/impl.rs
@@ -0,0 +1,371 @@
+use alloc::vec::Vec;
+use core::hash::Hash;
+
+use error_stack::{Report, Result};
+use fxhash::FxBuildHasher;
+use hashbrown::HashMap;
+use numi::num::{identity::Zero, ops::AddRef};
+use petgraph_core::{Graph, GraphStorage, Node};
+
+use super::error::BellmanFordError;
+use crate::shortest_paths::{
+ bellman_ford::{measure::BellmanFordMeasure, CandidateOrder},
+ common::{
+ connections::Connections,
+ cost::GraphCost,
+ queue::double_ended::DoubleEndedQueue,
+ transit::{reconstruct_paths_between, PredecessorMode},
+ },
+ Cost, Path, Route,
+};
+
+fn small_label_first<'graph, S, E>(
+ node: Node<'graph, S>,
+ cost: E::Value,
+ queue: &mut DoubleEndedQueue<'graph, S, E::Value>,
+) -> bool
+where
+ S: GraphStorage,
+ S::NodeId: Eq + Hash,
+ E: GraphCost<S>,
+ E::Value: BellmanFordMeasure,
+{
+ let Some(item) = queue.peek_front() else {
+ // queue is empty, therefore we can just simply push the cost
+ return queue.push_front(node, cost);
+ };
+
+ if &cost < item.priority() {
+ // the cost is smaller than the current smallest cost, therefore we push it to the front
+
+ return queue.push_front(node, cost);
+ }
+
+ // the cost is larger than the current smallest cost, therefore we push it to the back
+ queue.push_back(node, cost)
+}
+
+fn large_label_last<'graph, S, E>(
+ node: Node<'graph, S>,
+ cost: E::Value,
+ queue: &mut DoubleEndedQueue<'graph, S, E::Value>,
+) -> bool
+where
+ S: GraphStorage,
+ S::NodeId: Eq + Hash,
+ E: GraphCost<S>,
+ E::Value: BellmanFordMeasure,
+{
+ if queue.is_empty() {
+ // queue is empty, therefore we can just simply push the cost
+ return queue.push_back(node, cost);
+ }
+
+ // always push the item to the back of the queue
+ let did_push = queue.push_back(node, cost);
+
+ let Some(average) = queue.average_priority() else {
+ // this only happens if the length is 0 or we were unable to convert the length (usize)
+ // to the value type (E::Value)
+ return did_push;
+ };
+
+ loop {
+ // TODO: should we panic here instead? This should never happen.
+ let Some(front) = queue.peek_front() else {
+ // this should never happen, but if it does, we can just stop
+ // (previous check for empty queue should have caught this)
+ return did_push;
+ };
+
+ if *front.priority() <= average {
+ // the front item is smaller than the average, therefore we can stop
+ return did_push;
+ }
+
+ let Some(front) = queue.pop_front() else {
+ // this should never happen, but if it does, we can just stop
+ // (previous check for empty queue should have caught this)
+ return did_push;
+ };
+
+ let (node, priority) = front.into_parts();
+ queue.push_back(node, priority);
+ }
+}
+
+struct Heuristic<'graph, S>
+where
+ S: GraphStorage,
+{
+ enabled: bool,
+ recent_update: HashMap<&'graph S::NodeId, (&'graph S::NodeId, &'graph S::NodeId)>,
+ predecessor: HashMap<&'graph S::NodeId, &'graph S::NodeId>,
+}
+
+impl<'graph, S> Heuristic<'graph, S>
+where
+ S: GraphStorage,
+ S::NodeId: Eq + Hash,
+{
+ fn new(enabled: bool) -> Self {
+ Self {
+ enabled,
+ recent_update: HashMap::default(),
+ predecessor: HashMap::default(),
+ }
+ }
+
+ fn update(
+ &mut self,
+ source: &'graph S::NodeId,
+ target: &'graph S::NodeId,
+ ) -> core::result::Result<(), &'graph S::NodeId> {
+ if !self.enabled {
+ return Ok(());
+ }
+
+ // source = u
+ // target = v
+
+ // the heuristic is used to find a negative cycle before it is fully constructed.
+ // this is done via an implied check over multiple iterations.
+ // if it happens that some earlier update added the target node (as signified by the recent
+ // update) we know,
+ // that we have a negative cycle
+ // as the same node would be on the same path twice.
+ if let Some((u, v)) = self.recent_update.get(source) {
+ if target == *u || target == *v {
+ return Err(target);
+ }
+ }
+
+ if self.predecessor.get(target) == Some(&source) {
+ if let Some(previous) = self.recent_update.get(source) {
+ self.recent_update.insert(target, *previous);
+ }
+ } else {
+ self.recent_update.insert(target, (source, target));
+ }
+
+ self.predecessor.insert(target, source);
+ Ok(())
+ }
+}
+
+pub(super) struct ShortestPathFasterImpl<'graph: 'parent, 'parent, S, E, G>
+where
+ S: GraphStorage,
+ E: GraphCost<S>,
+{
+ graph: &'graph Graph<S>,
+ source: Node<'graph, S>,
+
+ edge_cost: &'parent E,
+ connections: G,
+
+ predecessor_mode: PredecessorMode,
+ candidate_order: CandidateOrder,
+ negative_cycle_heuristics: bool,
+
+ distances: HashMap<&'graph S::NodeId, E::Value, FxBuildHasher>,
+ predecessors: HashMap<&'graph S::NodeId, Vec<Node<'graph, S>>, FxBuildHasher>,
+}
+
+impl<'graph: 'parent, 'parent, S, E, G> ShortestPathFasterImpl<'graph, 'parent, S, E, G>
+where
+ S: GraphStorage,
+ S::NodeId: Eq + Hash,
+ E: GraphCost<S>,
+ E::Value: BellmanFordMeasure,
+ G: Connections<'graph, S>,
+{
+ pub(super) fn new(
+ graph: &'graph Graph<S>,
+
+ edge_cost: &'parent E,
+ connections: G,
+
+ source: &'graph S::NodeId,
+
+ predecessor_mode: PredecessorMode,
+ candidate_order: CandidateOrder,
+ negative_cycle_heuristics: bool,
+ ) -> Result<Self, BellmanFordError> {
+ let source_node = graph
+ .node(source)
+ .ok_or_else(|| Report::new(BellmanFordError::NodeNotFound))?;
+
+ let mut distances = HashMap::with_hasher(FxBuildHasher::default());
+ distances.insert(source, E::Value::zero());
+
+ let mut predecessors = HashMap::with_hasher(FxBuildHasher::default());
+ predecessors.insert(source, Vec::new());
+
+ let mut this = Self {
+ graph,
+ source: source_node,
+ edge_cost,
+ connections,
+ predecessor_mode,
+ candidate_order,
+ negative_cycle_heuristics,
+ distances,
+ predecessors,
+ };
+
+ // TODO: reconstruct negative cycle (needs `NodeId` to have additional trait bounds for
+ // error-stack)
+ if this.relax().is_err() {
+ return Err(Report::new(BellmanFordError::NegativeCycle));
+ }
+
+ Ok(this)
+ }
+
+ /// Inner Relaxation Loop for the Bellman-Ford algorithm, an implementation of SPFA.
+ ///
+ /// Based on [networkx](https://github.com/networkx/networkx/blob/f93f0e2a066fc456aa447853af9d00eec1058542/networkx/algorithms/shortest_paths/weighted.py#L1363)
+ fn relax(&mut self) -> core::result::Result<(), &'graph S::NodeId> {
+ // we always need to record predecessors to be able to skip relaxations
+ let mut queue = DoubleEndedQueue::new();
+ let mut heuristic: Heuristic<'graph, S> = Heuristic::new(self.negative_cycle_heuristics);
+ let mut occurrences = HashMap::new();
+ let num_nodes = self.graph.num_nodes();
+
+ queue.push_back(self.source, E::Value::zero());
+
+ while let Some(item) = queue.pop_front() {
+ let (source, priority) = item.into_parts();
+
+ // skip relaxations if any of the predecessors of node are in the queue
+ if let Some(predecessors) = self.predecessors.get(source.id()) {
+ if predecessors
+ .iter()
+ .any(|node| queue.contains_node(node.id()))
+ {
+ continue;
+ }
+ }
+
+ let edges = self.connections.connections(&source);
+
+ for edge in edges {
+ let (u, v) = edge.endpoints();
+ let target = if u.id() == source.id() { v } else { u };
+
+ let alternative = priority.add_ref(self.edge_cost.cost(edge).as_ref());
+
+ if let Some(distance) = self.distances.get(target.id()) {
+ if alternative == *distance {
+ self.predecessors
+ .entry(target.id())
+ .or_insert_with(Vec::new)
+ .push(source);
+ continue;
+ }
+
+ if alternative >= *distance {
+ continue;
+ }
+ }
+
+ if let Err(node) = heuristic.update(source.id(), target.id()) {
+ self.predecessors
+ .entry(target.id())
+ .or_insert_with(Vec::new)
+ .push(source);
+ return Err(node);
+ };
+
+ // we have a concrete problem here: we do not update the priority in the queue
+ // if it is larger.
+ // Could we in theory use the HashBrown API here instead, referencing the item in
+ // question?
+ let did_push = match self.candidate_order {
+ CandidateOrder::SmallFirst => {
+ small_label_first::<S, E>(target, alternative.clone(), &mut queue)
+ }
+ CandidateOrder::LargeLast => {
+ large_label_last::<S, E>(target, alternative.clone(), &mut queue)
+ }
+ CandidateOrder::Naive => queue.push_back(target, alternative.clone()),
+ };
+
+ if did_push {
+ let count = occurrences.entry(target.id()).or_insert(0usize);
+ *count += 1;
+
+ // If the heuristic failed (or is disabled) this is the fail-safe mechanism
+ // to detect any negative cycles.
+ // We know that a shortest path can at most go through n nodes, therefore we
+ // can detect a negative cycle,
+ // if we have visited the same node n times.
+ //
+ // As we can only detect a negative cycle quite late this is the worst case and
+ // the heuristic should be used instead.
+ if *count == num_nodes {
+ // negative cycle detected
+ return Err(target.id());
+ }
+ }
+
+ self.distances.insert(target.id(), alternative);
+
+ // re-use the same buffer so that we don't need to allocate a new one
+ let predecessors = self
+ .predecessors
+ .entry(target.id())
+ .or_insert_with(Vec::new);
+ predecessors.clear();
+ predecessors.push(source);
+ }
+ }
+
+ Ok(())
+ }
+
+ pub(crate) fn between(mut self, target: &S::NodeId) -> Option<Route<'graph, S, E::Value>> {
+ let cost = self.distances.remove(target)?;
+ let target = self.graph.node(target)?;
+
+ let transit = if self.predecessor_mode == PredecessorMode::Record {
+ reconstruct_paths_between(&self.predecessors, self.source.id(), target)
+ .next()
+ .unwrap_or_else(Vec::new)
+ } else {
+ Vec::new()
+ };
+
+ Some(Route::new(
+ Path::new(self.source, transit, target),
+ Cost::new(cost),
+ ))
+ }
+
+ pub(crate) fn all(self) -> impl Iterator<Item = Route<'graph, S, E::Value>> {
+ let Self {
+ graph,
+ source,
+ predecessor_mode,
+ distances,
+ predecessors,
+ ..
+ } = self;
+
+ distances
+ .into_iter()
+ .filter_map(|(target, cost)| graph.node(target).map(|target| (target, cost)))
+ .map(move |(target, cost)| {
+ let transit = if predecessor_mode == PredecessorMode::Record {
+ reconstruct_paths_between(&predecessors, source.id(), target)
+ .next()
+ .unwrap_or_else(Vec::new)
+ } else {
+ Vec::new()
+ };
+
+ Route::new(Path::new(source, transit, target), Cost::new(cost))
+ })
+ }
+}
diff --git a/crates/algorithms/src/shortest_paths/bellman_ford/measure.rs b/crates/algorithms/src/shortest_paths/bellman_ford/measure.rs
new file mode 100644
index 0000000..d0ac726
--- /dev/null
+++ b/crates/algorithms/src/shortest_paths/bellman_ford/measure.rs
@@ -0,0 +1,88 @@
+use core::{
+ iter::Sum,
+ ops::{Add, Div},
+};
+
+use numi::{
+ cast::{CastFrom, TryCastFrom},
+ num::{identity::Zero, ops::AddRef},
+};
+
+/// Automatically implemented trait for types that can be used as a measure in the Bellman-Ford
+/// algorithm.
+///
+/// This trait is implemented for all types that implement the supertraits mentioned in the trait
+/// definition.
+/// These traits either originate from [`core`] or [`numi`].
+/// Special attention must be paid to the [`AddRef`] trait, which is a proxy trait which is
+/// implemented for types that implement: `&Self: Add<&Self, Output = Self>`.
+///
+/// # Example
+///
+/// ```rust
+/// use core::num::{Wrapping, Saturating};
+/// use ordered_float::NotNan;
+/// use petgraph_algorithms::shortest_paths::bellman_ford::BellmanFordMeasure;
+/// use static_assertions::assert_impl_all;
+///
+/// // Some examples of types that implement BellmanFordMeasure
+/// assert_impl_all!(u8: BellmanFordMeasure);
+/// assert_impl_all!(u16: BellmanFordMeasure);
+/// assert_impl_all!(u32: BellmanFordMeasure);
+/// assert_impl_all!(u64: BellmanFordMeasure);
+/// assert_impl_all!(u128: BellmanFordMeasure);
+/// assert_impl_all!(usize: BellmanFordMeasure);
+///
+/// assert_impl_all!(i8: BellmanFordMeasure);
+/// assert_impl_all!(i16: BellmanFordMeasure);
+/// assert_impl_all!(i32: BellmanFordMeasure);
+/// assert_impl_all!(i64: BellmanFordMeasure);
+/// assert_impl_all!(i128: BellmanFordMeasure);
+/// assert_impl_all!(isize: BellmanFordMeasure);
+///
+/// assert_impl_all!(f32: BellmanFordMeasure);
+/// assert_impl_all!(f64: BellmanFordMeasure);
+/// // `OrderedFloat` is not supported because `AddRef` isn't implemented, see:
+/// // https://github.com/reem/rust-ordered-float/issues/145
+/// assert_impl_all!(NotNan<f32>: BellmanFordMeasure);
+/// assert_impl_all!(NotNan<f64>: BellmanFordMeasure);
+///
+/// assert_impl_all!(Wrapping<u8>: BellmanFordMeasure);
+/// assert_impl_all!(Wrapping<u16>: BellmanFordMeasure);
+/// assert_impl_all!(Wrapping<u32>: BellmanFordMeasure);
+/// assert_impl_all!(Wrapping<u64>: BellmanFordMeasure);
+/// assert_impl_all!(Wrapping<u128>: BellmanFordMeasure);
+/// assert_impl_all!(Wrapping<usize>: BellmanFordMeasure);
+///
+/// assert_impl_all!(Wrapping<i8>: BellmanFordMeasure);
+/// assert_impl_all!(Wrapping<i16>: BellmanFordMeasure);
+/// assert_impl_all!(Wrapping<i32>: BellmanFordMeasure);
+/// assert_impl_all!(Wrapping<i64>: BellmanFordMeasure);
+/// assert_impl_all!(Wrapping<i128>: BellmanFordMeasure);
+///
+/// // `Saturating` currently does not implement `Sum`, see:
+/// // https://github.com/rust-lang/libs-team/issues/303
+/// ```
+pub trait BellmanFordMeasure:
+ Clone
+ + PartialOrd
+ + Add<Self, Output = Self>
+ + AddRef<Self, Output = Self>
+ + Div<Self, Output = Self>
+ + for<'a> Sum<&'a Self>
+ + TryCastFrom<usize>
+ + Zero
+{
+}
+
+impl<T> BellmanFordMeasure for T where
+ T: Clone
+ + PartialOrd
+ + Add<Self, Output = Self>
+ + AddRef<Self, Output = Self>
+ + Div<Self, Output = Self>
+ + for<'a> Sum<&'a Self>
+ + TryCastFrom<usize>
+ + Zero
+{
+}
diff --git a/crates/algorithms/src/shortest_paths/bellman_ford/mod.rs b/crates/algorithms/src/shortest_paths/bellman_ford/mod.rs
new file mode 100644
index 0000000..ecc32b8
--- /dev/null
+++ b/crates/algorithms/src/shortest_paths/bellman_ford/mod.rs
@@ -0,0 +1,540 @@
+//! An implementation of Bellman-Ford's shortest path algorithm.
+mod error;
+mod r#impl;
+mod measure;
+#[cfg(test)]
+mod tests;
+
+use alloc::vec::Vec;
+use core::hash::Hash;
+
+use error_stack::Result;
+use petgraph_core::{
+ edge::marker::{Directed, Undirected},
+ DirectedGraphStorage, Graph, GraphDirectionality, GraphStorage, Node,
+};
+
+use self::r#impl::ShortestPathFasterImpl;
+pub use self::{error::BellmanFordError, measure::BellmanFordMeasure};
+use super::{
+ common::{
+ connections::outgoing_connections,
+ cost::{DefaultCost, GraphCost},
+ transit::PredecessorMode,
+ },
+ Cost, DirectRoute, Route, ShortestDistance, ShortestPath,
+};
+use crate::polyfill::IteratorExt;
+
+/// The order in which candidates are inserted into the queue.
+///
+/// The order in which candidates are inserted into the queue can have a significant impact on the
+/// performance of the algorithm.
+///
+/// [`CandidateOrder::SmallFirst`] is the default, as it is an easy to implement heuristic, with
+/// little overhead which can exhibit good performance improvements in practice.
+///
+/// # See Also
+///
+/// - <https://www.researchgate.net/publication/274174007_An_Improved_SPFA_Algorithm_for_Single-Source_Shortest_Path_Problem_Using_Forward_Star_Data_Structure>
+// TODO: add citation about the different orders
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
+pub enum CandidateOrder {
+ /// # Naive
+ ///
+ /// Push the item to the back of the queue.
+ Naive,
+
+ /// # Small Label First (SSF)
+ ///
+ /// Checks if the current value is smaller than the next value, if that is the case, push it to
+ /// the front, otherwise push it to the back.
+ #[default]
+ SmallFirst,
+
+ /// # Large Label Last (LLL)
+ ///
+ /// Push the item to the back of the queue.
+ /// Calculate the average value of the queue and as long as the next value larger than the
+ /// average value, pop it from the front and push it to the back.
+ LargeLast,
+}
+
+/// An implementation of Bellman-Ford's shortest path algorithm.
+///
+/// Bellman-Ford's algorithm is an algorithm for finding the shortest paths between a source node
+/// and all reachable nodes in a graph.
+/// Unlike Dijkstra's algorithm, Bellman-Ford's algorithm can be used on graphs with negative edge
+/// weights, as long as the graph does not contain a negative cycle reachable from the source.
+///
+/// This implementation is generic over the directionality of the graph and the cost function.
+///
+/// Edge weights need to implement [`BellmanFordMeasure`], a trait that is automatically implemented
+/// for all types that satisfy the constraints.
+///
+/// The internal implementation makes uses of Shortest Path Faster Algorithm (SPFA) which is a
+/// variant of Bellman-Ford's algorithm, with an average time complexity of `O(|E|)` on random
+/// graphs and a worst case complexity of `O(|V| * |E|)`.
+pub struct BellmanFord<D, E> {
+ direction: D,
+ edge_cost: E,
+ candidate_order: CandidateOrder,
+ negative_cycle_heuristics: bool,
+}
+
+impl BellmanFord<Directed, DefaultCost> {
+ /// Create a new instance of `BellmanFord` for a directed graph.
+ ///
+ /// If instantiated for a directed graph, [`BellmanFord`] will not implement [`ShortestPath`]
+ /// and [`ShortestDistance`] on undirected graphs.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use petgraph_algorithms::shortest_paths::{BellmanFord, ShortestPath};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// let algorithm = BellmanFord::directed();
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge(2, &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ ///
+ /// let path = algorithm.path_between(&graph, &b, &a);
+ /// assert!(path.is_none());
+ /// ```
+ pub fn directed() -> Self {
+ Self {
+ direction: Directed,
+ edge_cost: DefaultCost,
+ candidate_order: CandidateOrder::default(),
+ negative_cycle_heuristics: true,
+ }
+ }
+}
+
+impl BellmanFord<Undirected, DefaultCost> {
+ /// Create a new instance of `BellmanFord` for an undirected graph.
+ ///
+ /// If instantiated for an undirected graph, [`BellmanFord`] will treat a directed graph as
+ /// undirected.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use petgraph_algorithms::shortest_paths::{BellmanFord, ShortestPath};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// let algorithm = BellmanFord::undirected();
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge(2, &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ ///
+ /// let path = algorithm.path_between(&graph, &b, &a);
+ /// assert!(path.is_some());
+ /// ```
+ pub fn undirected() -> Self {
+ Self {
+ direction: Undirected,
+ edge_cost: DefaultCost,
+ candidate_order: CandidateOrder::default(),
+ negative_cycle_heuristics: true,
+ }
+ }
+}
+
+impl<D, E> BellmanFord<D, E>
+where
+ D: GraphDirectionality,
+{
+ /// Set the cost function for the algorithm.
+ ///
+ /// By default the algorithm will use the edge weight as the cost, this function allows you to
+ /// override that behaviour,
+ /// transforming a previously unsupported graph weight into a supported one.
+ ///
+ /// For all supported functions see [`GraphCost`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use numi::borrow::Moo;
+ /// use petgraph_algorithms::shortest_paths::{BellmanFord, ShortestPath};
+ /// use petgraph_core::{Edge, GraphStorage};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// fn edge_cost<S>(edge: Edge<S>) -> Moo<usize>
+ /// where
+ /// S: GraphStorage,
+ /// S::EdgeWeight: AsRef<str>,
+ /// {
+ /// edge.weight().as_ref().len().into()
+ /// }
+ ///
+ /// let algorithm = BellmanFord::directed().with_edge_cost(edge_cost);
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge("AB", &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ /// ```
+ pub fn with_edge_cost<S, F>(self, edge_cost: F) -> BellmanFord<D, F>
+ where
+ S: GraphStorage,
+ F: GraphCost<S>,
+ {
+ BellmanFord {
+ direction: self.direction,
+ edge_cost,
+ candidate_order: self.candidate_order,
+ negative_cycle_heuristics: self.negative_cycle_heuristics,
+ }
+ }
+
+ /// Set the candidate order for the algorithm.
+ ///
+ /// By default the algorithm uses the [`CandidateOrder::SmallFirst`] order, see
+ /// [`CandidateOrder`] for the different pros and cons.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use petgraph_algorithms::shortest_paths::{
+ /// bellman_ford::CandidateOrder, BellmanFord, ShortestPath,
+ /// };
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// let algorithm = BellmanFord::directed().with_candidate_order(CandidateOrder::LargeLast);
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge(2, &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ /// ```
+ pub fn with_candidate_order(self, candidate_order: CandidateOrder) -> Self {
+ Self {
+ direction: self.direction,
+ edge_cost: self.edge_cost,
+ candidate_order,
+ negative_cycle_heuristics: self.negative_cycle_heuristics,
+ }
+ }
+
+ /// Enable or disable negative cycle heuristics.
+ ///
+ /// By default the algorithm will use negative cycle heuristics.
+ /// Negative cycle heuristics help to detect negative cycles in the graph early.
+ /// If the graph contains a negative cycle, the algorithm will return an error.
+ ///
+ /// In theory such heuristic can exhibit false-positives, but these haven't been observed in
+ /// practice.
+ /// The heuristic is based on the implementation of networkx and allows the algorithm to not
+ /// exhibit worst case time complexity in the case of a negative cycle.
+ ///
+ /// It is recommended to leave this option enabled.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use petgraph_algorithms::shortest_paths::{BellmanFord, ShortestPath};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// let algorithm = BellmanFord::directed().with_negative_cycle_heuristics(false);
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge(2, &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ /// ```
+ pub fn with_negative_cycle_heuristics(self, negative_cycle_heuristics: bool) -> Self {
+ Self {
+ direction: self.direction,
+ edge_cost: self.edge_cost,
+ candidate_order: self.candidate_order,
+ negative_cycle_heuristics,
+ }
+ }
+}
+
+impl<S, E> ShortestPath<S> for BellmanFord<Undirected, E>
+where
+ S: GraphStorage,
+ S::NodeId: PartialEq + Eq + Hash,
+ E: GraphCost<S>,
+ E::Value: BellmanFordMeasure,
+{
+ type Cost = E::Value;
+ type Error = BellmanFordError;
+
+ fn path_to<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ target: &'graph S::NodeId,
+ ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>>, Self::Error> {
+ let iter = self.path_from(graph, target)?;
+
+ Ok(iter.map(Route::reverse))
+ }
+
+ fn path_from<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ source: &'graph <S as GraphStorage>::NodeId,
+ ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>>, Self::Error> {
+ ShortestPathFasterImpl::new(
+ graph,
+ &self.edge_cost,
+ Node::<'graph, S>::connections as fn(&Node<'graph, S>) -> _,
+ source,
+ PredecessorMode::Record,
+ self.candidate_order,
+ self.negative_cycle_heuristics,
+ )
+ .map(ShortestPathFasterImpl::all)
+ }
+
+ fn path_between<'graph>(
+ &self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ target: &'graph S::NodeId,
+ ) -> Option<Route<'graph, S, Self::Cost>> {
+ ShortestPathFasterImpl::new(
+ graph,
+ &self.edge_cost,
+ Node::<'graph, S>::connections as fn(&Node<'graph, S>) -> _,
+ source,
+ PredecessorMode::Record,
+ self.candidate_order,
+ self.negative_cycle_heuristics,
+ )
+ .ok()?
+ .between(target)
+ }
+
+ fn every_path<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter: Vec<_> = graph
+ .nodes()
+ .map(move |node| self.path_from(graph, node.id()))
+ .collect_reports()?;
+
+ Ok(iter.into_iter().flatten())
+ }
+}
+
+impl<S, E> ShortestDistance<S> for BellmanFord<Undirected, E>
+where
+ S: GraphStorage,
+ S::NodeId: Eq + Hash,
+ E: GraphCost<S>,
+ E::Value: BellmanFordMeasure,
+{
+ type Cost = E::Value;
+ type Error = BellmanFordError;
+
+ fn distance_to<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ target: &'graph S::NodeId,
+ ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>>, Self::Error> {
+ let iter = self.distance_from(graph, target)?;
+
+ Ok(iter.map(DirectRoute::reverse))
+ }
+
+ fn distance_from<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ source: &'graph <S as GraphStorage>::NodeId,
+ ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter = ShortestPathFasterImpl::new(
+ graph,
+ &self.edge_cost,
+ Node::<'graph, S>::connections as fn(&Node<'graph, S>) -> _,
+ source,
+ PredecessorMode::Discard,
+ self.candidate_order,
+ self.negative_cycle_heuristics,
+ )?;
+
+ Ok(iter.all().map(Into::into))
+ }
+
+ fn distance_between<'graph>(
+ &self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ target: &'graph S::NodeId,
+ ) -> Option<Cost<Self::Cost>> {
+ ShortestPathFasterImpl::new(
+ graph,
+ &self.edge_cost,
+ Node::<'graph, S>::connections as fn(&Node<'graph, S>) -> _,
+ source,
+ PredecessorMode::Discard,
+ self.candidate_order,
+ self.negative_cycle_heuristics,
+ )
+ .ok()?
+ .between(target)
+ .map(Route::into_cost)
+ }
+
+ fn every_distance<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter: Vec<_> = graph
+ .nodes()
+ .map(move |node| self.distance_from(graph, node.id()))
+ .collect_reports()?;
+
+ Ok(iter.into_iter().flatten())
+ }
+}
+
+impl<S, E> ShortestPath<S> for BellmanFord<Directed, E>
+where
+ S: DirectedGraphStorage,
+ S::NodeId: Eq + Hash,
+ E: GraphCost<S>,
+ E::Value: BellmanFordMeasure,
+{
+ type Cost = E::Value;
+ type Error = BellmanFordError;
+
+ fn path_from<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ ShortestPathFasterImpl::new(
+ graph,
+ &self.edge_cost,
+ outgoing_connections as fn(&Node<'graph, S>) -> _,
+ source,
+ PredecessorMode::Record,
+ self.candidate_order,
+ self.negative_cycle_heuristics,
+ )
+ .map(ShortestPathFasterImpl::all)
+ }
+
+ fn path_between<'graph>(
+ &self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ target: &'graph S::NodeId,
+ ) -> Option<Route<'graph, S, Self::Cost>> {
+ ShortestPathFasterImpl::new(
+ graph,
+ &self.edge_cost,
+ outgoing_connections as fn(&Node<'graph, S>) -> _,
+ source,
+ PredecessorMode::Record,
+ self.candidate_order,
+ self.negative_cycle_heuristics,
+ )
+ .ok()?
+ .between(target)
+ }
+
+ fn every_path<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter: Vec<_> = graph
+ .nodes()
+ .map(move |node| self.path_from(graph, node.id()))
+ .collect_reports()?;
+
+ Ok(iter.into_iter().flatten())
+ }
+}
+
+impl<S, E> ShortestDistance<S> for BellmanFord<Directed, E>
+where
+ S: DirectedGraphStorage,
+ S::NodeId: Eq + Hash,
+ E: GraphCost<S>,
+ E::Value: BellmanFordMeasure,
+{
+ type Cost = E::Value;
+ type Error = BellmanFordError;
+
+ fn distance_from<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ source: &'graph <S as GraphStorage>::NodeId,
+ ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>>, Self::Error> {
+ let iter = ShortestPathFasterImpl::new(
+ graph,
+ &self.edge_cost,
+ outgoing_connections as fn(&Node<'graph, S>) -> _,
+ source,
+ PredecessorMode::Discard,
+ self.candidate_order,
+ self.negative_cycle_heuristics,
+ )?;
+
+ Ok(iter.all().map(Into::into))
+ }
+
+ fn distance_between<'graph>(
+ &self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ target: &'graph S::NodeId,
+ ) -> Option<Cost<Self::Cost>> {
+ ShortestPathFasterImpl::new(
+ graph,
+ &self.edge_cost,
+ outgoing_connections as fn(&Node<'graph, S>) -> _,
+ source,
+ PredecessorMode::Discard,
+ self.candidate_order,
+ self.negative_cycle_heuristics,
+ )
+ .ok()?
+ .between(target)
+ .map(Route::into_cost)
+ }
+
+ fn every_distance<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter: Vec<_> = graph
+ .nodes()
+ .map(move |node| self.distance_from(graph, node.id()))
+ .collect_reports()?;
+
+ Ok(iter.into_iter().flatten())
+ }
+}
diff --git a/crates/algorithms/src/shortest_paths/bellman_ford/tests.rs b/crates/algorithms/src/shortest_paths/bellman_ford/tests.rs
new file mode 100644
index 0000000..fa55e4b
--- /dev/null
+++ b/crates/algorithms/src/shortest_paths/bellman_ford/tests.rs
@@ -0,0 +1,281 @@
+use alloc::vec::Vec;
+use core::array;
+
+use petgraph_core::{
+ edge::marker::{Directed, Undirected},
+ Graph, GraphStorage, ManagedGraphId,
+};
+use petgraph_dino::{DiDinoGraph, DinoStorage, EdgeId, NodeId};
+use petgraph_utils::{graph, GraphCollection};
+
+use super::BellmanFord;
+use crate::shortest_paths::{
+ bellman_ford::error::BellmanFordError,
+ common::tests::{expected, TestCase},
+ ShortestPath,
+};
+
+graph!(
+ /// Uses the graph from networkx
+ ///
+ /// <https://github.com/networkx/networkx/blob/main/networkx/algorithms/shortest_paths/tests/test_weighted.py>
+ factory(networkx) => DiDinoGraph<&'static str, f32>;
+ [
+ a: "A",
+ b: "B",
+ c: "C",
+ d: "D",
+ e: "E",
+ ] as NodeId, [
+ ab: a -> b: 10f32,
+ ac: a -> c: 5f32,
+ bd: b -> d: 1f32,
+ bc: b -> c: 2f32,
+ de: d -> e: 1f32,
+ cb: c -> b: 3f32,
+ cd: c -> d: 5f32,
+ ce: c -> e: 2f32,
+ ea: e -> a: 7f32,
+ ed: e -> d: 6f32,
+ ] as EdgeId
+);
+
+#[test]
+fn source_does_not_exist() {
+ let GraphCollection { mut graph, .. } = networkx::create();
+
+ let f = *graph.insert_node("F").id();
+ graph.remove_node(&f);
+
+ let spfa = BellmanFord::directed();
+ let Err(received) = spfa.path_from(&graph, &f) else {
+ panic!("Expected an error");
+ };
+ let error = received.current_context();
+
+ assert_eq!(error, &BellmanFordError::NodeNotFound);
+}
+
+#[test]
+fn negative_cycle_heuristic() {
+ let mut graph = DiDinoGraph::new();
+
+ let a = *graph.insert_node("A").id();
+ let b = *graph.insert_node("B").id();
+ let c = *graph.insert_node("C").id();
+ let d = *graph.insert_node("D").id();
+
+ graph.insert_edge(-1f32, &a, &b);
+ graph.insert_edge(-1f32, &b, &c);
+ graph.insert_edge(-1f32, &c, &d);
+ graph.insert_edge(3f32, &d, &a);
+
+ let spfa = BellmanFord::directed();
+ assert!(spfa.every_path(&graph).is_ok());
+
+ let ca = *graph.insert_edge(1.999, &c, &a).id();
+ assert!(spfa.every_path(&graph).is_err());
+
+ *graph.edge_mut(&ca).unwrap().weight_mut() = 2.0;
+ assert!(spfa.every_path(&graph).is_ok());
+}
+
+fn cycle_graph<const N: usize, S>() -> (Graph<S>, [S::NodeId; N], [S::EdgeId; N])
+where
+ S: GraphStorage<NodeWeight = usize, EdgeWeight = f32>,
+ S::NodeId: ManagedGraphId + Copy,
+ S::EdgeId: ManagedGraphId + Copy,
+{
+ let mut graph = Graph::new();
+
+ let nodes: [_; N] = array::from_fn(|index| *graph.insert_node(index).id());
+
+ let edges: [_; N] = array::from_fn(|index| {
+ *graph
+ .insert_edge(1f32, &nodes[index], &nodes[(index + 1) % N])
+ .id()
+ });
+
+ (graph, nodes, edges)
+}
+
+fn complete_graph<const N: usize, S>() -> (Graph<S>, [S::NodeId; N], Vec<S::EdgeId>)
+where
+ S: GraphStorage<NodeWeight = usize, EdgeWeight = f32>,
+ S::NodeId: ManagedGraphId + Copy,
+ S::EdgeId: ManagedGraphId + Copy,
+{
+ let mut graph = Graph::new();
+
+ let nodes: [_; N] = array::from_fn(|index| *graph.insert_node(index).id());
+
+ let mut edges = Vec::with_capacity(N * (N - 1) / 2);
+
+ for i in 0..N {
+ for j in i + 1..N {
+ edges.push(*graph.insert_edge(1f32, &nodes[i], &nodes[j]).id());
+ }
+ }
+
+ (graph, nodes, edges)
+}
+
+fn path_graph<const N: usize, S>() -> (Graph<S>, [S::NodeId; N], Vec<S::EdgeId>)
+where
+ S: GraphStorage<NodeWeight = usize, EdgeWeight = f32>,
+ S::NodeId: ManagedGraphId + Copy,
+ S::EdgeId: ManagedGraphId + Copy,
+{
+ let mut graph = Graph::new();
+
+ let nodes: [_; N] = array::from_fn(|index| *graph.insert_node(index).id());
+
+ let mut edges = Vec::with_capacity(N - 1);
+
+ for i in 0..N - 1 {
+ edges.push(*graph.insert_edge(1f32, &nodes[i], &nodes[i + 1]).id());
+ }
+
+ (graph, nodes, edges)
+}
+
+#[test]
+fn negative_cycle_multigraph_directed() {
+ let (mut graph, nodes, _) = cycle_graph::<5, DinoStorage<_, _>>();
+
+ let spfa = BellmanFord::directed();
+ assert!(spfa.every_path(&graph).is_ok());
+
+ // add negative cycle between nodes 1 and 2
+ *graph.insert_edge(-7f32, &nodes[1], &nodes[2]).id();
+ assert!(spfa.every_path(&graph).is_err());
+}
+
+#[test]
+fn negative_cycle_multigraph_undirected() {
+ let (mut graph, nodes, _) = cycle_graph::<5, DinoStorage<_, _, Undirected>>();
+
+ let spfa = BellmanFord::undirected();
+ assert!(spfa.every_path(&graph).is_ok());
+
+ // add negative cycle between nodes 1 and 2
+ *graph.insert_edge(-3f32, &nodes[1], &nodes[2]).id();
+ assert!(spfa.every_path(&graph).is_err());
+}
+
+#[test]
+fn negative_self_cycle() {
+ let mut graph = DiDinoGraph::new();
+
+ let a = *graph.insert_node("A").id();
+
+ graph.insert_edge(-1f32, &a, &a);
+
+ let spfa = BellmanFord::directed();
+ assert!(spfa.every_path(&graph).is_err());
+}
+
+#[test]
+fn zero_cycle() {
+ let (mut graph, nodes, _) = cycle_graph::<5, DinoStorage<_, _>>();
+
+ let spfa = BellmanFord::directed();
+ assert!(spfa.every_path(&graph).is_ok());
+
+ // add zero cycle between nodes 2 and 3
+ let edge = *graph.insert_edge(-4f32, &nodes[2], &nodes[3]).id();
+ assert!(spfa.every_path(&graph).is_ok());
+
+ // increase that cycle to a negative cycle
+ *graph.edge_mut(&edge).unwrap().weight_mut() = -4.0001f32;
+ assert!(spfa.every_path(&graph).is_err());
+}
+
+#[test]
+fn negative_weight() {
+ let (mut graph, nodes, _) = cycle_graph::<5, DinoStorage<_, _, Directed>>();
+
+ let spfa = BellmanFord::directed();
+ assert!(spfa.every_path(&graph).is_ok());
+
+ // add negative weight to edge between nodes 1 and 2
+ graph.insert_edge(-3f32, &nodes[1], &nodes[2]);
+
+ let expected = expected!(nodes; [
+ 0 -()> 0: 0f32,
+ 0 -()> 1: 1f32,
+ 0 -(1)> 2: -2f32,
+ 0 -(1, 2)> 3: -1f32,
+ 0 -(1, 2, 3)> 4: 0f32,
+ ]);
+
+ TestCase::new(&graph, &spfa, &expected).assert_path_from(&nodes[0]);
+}
+
+#[test]
+fn not_connected() {
+ let (mut graph, nodes, _) = complete_graph::<6, DinoStorage<_, _>>();
+
+ let g = *graph.insert_node(10).id();
+ let h = *graph.insert_node(11).id();
+ let i = *graph.insert_node(12).id();
+
+ graph.insert_edge(1f32, &g, &h);
+ graph.insert_edge(1f32, &g, &i);
+
+ let spfa = BellmanFord::directed();
+
+ let expected = expected!(nodes; [
+ 0 -()> 0: 0f32,
+ 0 -()> 1: 1f32,
+ 0 -()> 2: 1f32,
+ 0 -()> 3: 1f32,
+ 0 -()> 4: 1f32,
+ 0 -()> 5: 1f32,
+ ]);
+
+ TestCase::new(&graph, &spfa, &expected).assert_path_from(&nodes[0]);
+}
+
+#[test]
+fn negative_cycle_not_connected() {
+ let (mut graph, nodes, _) = complete_graph::<6, DinoStorage<_, _>>();
+
+ let g = *graph.insert_node(10).id();
+ let h = *graph.insert_node(11).id();
+ let i = *graph.insert_node(12).id();
+
+ graph.insert_edge(1f32, &g, &h);
+ graph.insert_edge(1f32, &h, &i);
+ graph.insert_edge(-3f32, &i, &g);
+
+ let spfa = BellmanFord::directed();
+
+ let expected = expected!(nodes; [
+ 0 -()> 0: 0f32,
+ 0 -()> 1: 1f32,
+ 0 -()> 2: 1f32,
+ 0 -()> 3: 1f32,
+ 0 -()> 4: 1f32,
+ 0 -()> 5: 1f32,
+ ]);
+
+ TestCase::new(&graph, &spfa, &expected).assert_path_from(&nodes[0]);
+}
+
+#[test]
+fn path() {
+ let (graph, nodes, _) = path_graph::<5, DinoStorage<_, _>>();
+
+ let spfa = BellmanFord::directed();
+
+ let expected = expected!(nodes; [
+ 0 -()> 0: 0f32,
+ 0 -()> 1: 1f32,
+ 0 -(1)> 2: 2f32,
+ 0 -(1, 2)> 3: 3f32,
+ 0 -(1, 2, 3)> 4: 4f32,
+ ]);
+
+ TestCase::new(&graph, &spfa, &expected).assert_path_from(&nodes[0]);
+}
diff --git a/crates/algorithms/src/shortest_paths/common/closures.rs b/crates/algorithms/src/shortest_paths/common/closures.rs
new file mode 100644
index 0000000..8d701aa
--- /dev/null
+++ b/crates/algorithms/src/shortest_paths/common/closures.rs
@@ -0,0 +1,66 @@
+use core::marker::PhantomData;
+
+use numi::borrow::Moo;
+use petgraph_core::{Edge, GraphStorage, Node};
+
+use crate::shortest_paths::GraphCost;
+
+pub fn cost<S, T>(closure: impl Fn(Edge<S>) -> Moo<T>) -> impl Fn(Edge<S>) -> Moo<T> {
+ closure
+}
+
+pub fn heuristic<S, T>(
+ closure: impl for<'graph> Fn(Node<'graph, S>, Node<'graph, S>) -> Moo<'graph, T>,
+) -> impl for<'graph> Fn(Node<'graph, S>, Node<'graph, S>) -> Moo<'graph, T>
+where
+ S: GraphStorage,
+{
+ closure
+}
+
+#[cfg(test)]
+mod test {
+ use numi::borrow::Moo;
+ use petgraph_dino::DiDinoGraph;
+
+ use crate::shortest_paths::{
+ common::closures::{cost, heuristic},
+ AStar, Dijkstra, ShortestPath,
+ };
+
+ #[test]
+ fn bind_cost() {
+ let closure = cost(|edge| Moo::Borrowed(edge.weight()));
+
+ let algorithm = Dijkstra::undirected().with_edge_cost(closure);
+
+ let mut graph = DiDinoGraph::new();
+ let a = *graph.insert_node("A").id();
+ let b = *graph.insert_node("B").id();
+
+ graph.insert_edge(7, &a, &b);
+
+ let path = algorithm.path_between(&graph, &a, &b).expect("path exists");
+ }
+
+ #[test]
+ fn bind_heuristic() {
+ let closure = heuristic(|source, target| Moo::Owned(0i32));
+
+ let algorithm = AStar::undirected().with_heuristic(closure);
+
+ let mut graph = DiDinoGraph::new();
+ let a = *graph.insert_node("A").id();
+ let b = *graph.insert_node("B").id();
+
+ graph.insert_edge(7i32, &a, &b);
+
+ let path = algorithm.path_between(&graph, &a, &b).expect("path exists");
+ }
+}
+
+// pub fn bind_heuristic<S, T>(
+// closure: impl Fn(Node<S>, Node<S>) -> Moo<T>,
+// ) -> impl Fn(Node<S>, Node<S>) -> Moo<T> {
+// closure
+// }
diff --git a/crates/algorithms/src/shortest_paths/common/cost.rs b/crates/algorithms/src/shortest_paths/common/cost.rs
index 8169b0a..7cc6277 100644
--- a/crates/algorithms/src/shortest_paths/common/cost.rs
+++ b/crates/algorithms/src/shortest_paths/common/cost.rs
@@ -1,6 +1,7 @@
use core::{borrow::Borrow, fmt::Display};
-use petgraph_core::{base::MaybeOwned, Edge, GraphStorage};
+use numi::borrow::Moo;
+use petgraph_core::{Edge, GraphStorage};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Cost<T>(T);
@@ -59,17 +60,17 @@
{
type Value;
- fn cost<'a>(&self, edge: Edge<'a, S>) -> MaybeOwned<'a, Self::Value>;
+ fn cost<'graph>(&self, edge: Edge<'graph, S>) -> Moo<'graph, Self::Value>;
}
impl<S, F, T> GraphCost<S> for F
where
S: GraphStorage,
- F: Fn(Edge<S>) -> MaybeOwned<T>,
+ F: Fn(Edge<S>) -> Moo<T>,
{
type Value = T;
- fn cost<'a>(&self, edge: Edge<'a, S>) -> MaybeOwned<'a, T> {
+ fn cost<'graph>(&self, edge: Edge<'graph, S>) -> Moo<'graph, T> {
self(edge)
}
}
@@ -80,14 +81,15 @@
{
type Value = S::EdgeWeight;
- fn cost<'a>(&self, edge: Edge<'a, S>) -> MaybeOwned<'a, S::EdgeWeight> {
- MaybeOwned::Borrowed(edge.weight())
+ fn cost<'graph>(&self, edge: Edge<'graph, S>) -> Moo<'graph, S::EdgeWeight> {
+ Moo::Borrowed(edge.weight())
}
}
#[cfg(test)]
mod tests {
- use petgraph_core::{base::MaybeOwned, edge::marker::Directed, Edge, GraphStorage};
+ use numi::borrow::Moo;
+ use petgraph_core::{edge::marker::Directed, Edge, GraphStorage};
use petgraph_dino::DinoStorage;
use crate::shortest_paths::common::cost::{DefaultCost, GraphCost};
@@ -99,7 +101,7 @@
{
}
- fn maybe_edge_cost<S>(edge: Edge<S>) -> MaybeOwned<'_, usize>
+ fn maybe_edge_cost<S>(edge: Edge<S>) -> Moo<'_, usize>
where
S: GraphStorage,
S::EdgeWeight: AsRef<[u8]>,
diff --git a/crates/algorithms/src/shortest_paths/common/mod.rs b/crates/algorithms/src/shortest_paths/common/mod.rs
index a1fd686..ea32da1 100644
--- a/crates/algorithms/src/shortest_paths/common/mod.rs
+++ b/crates/algorithms/src/shortest_paths/common/mod.rs
@@ -1,3 +1,5 @@
+//! Common utility types, traits and functions for shortest paths algorithms.
+mod closures;
pub(super) mod connections;
pub(super) mod cost;
pub(super) mod path;
diff --git a/crates/algorithms/src/shortest_paths/common/path.rs b/crates/algorithms/src/shortest_paths/common/path.rs
index b79088e..25a4192 100644
--- a/crates/algorithms/src/shortest_paths/common/path.rs
+++ b/crates/algorithms/src/shortest_paths/common/path.rs
@@ -1,28 +1,33 @@
-use core::fmt::{Debug, Display, Formatter};
-use std::{
+use alloc::vec::Vec;
+use core::{
cmp::Ordering,
+ fmt::{Debug, Display, Formatter},
hash::{Hash, Hasher},
iter::once,
};
use petgraph_core::{GraphStorage, Node};
-pub struct Path<'a, S>
+pub struct Path<'graph, S>
where
S: GraphStorage,
{
- source: Node<'a, S>,
- target: Node<'a, S>,
+ source: Node<'graph, S>,
+ target: Node<'graph, S>,
- transit: Vec<Node<'a, S>>,
+ transit: Vec<Node<'graph, S>>,
}
-impl<'a, S> Path<'a, S>
+impl<'graph, S> Path<'graph, S>
where
S: GraphStorage,
{
#[must_use]
- pub const fn new(source: Node<'a, S>, transit: Vec<Node<'a, S>>, target: Node<'a, S>) -> Self {
+ pub fn new(
+ source: Node<'graph, S>,
+ transit: Vec<Node<'graph, S>>,
+ target: Node<'graph, S>,
+ ) -> Self {
Self {
source,
target,
@@ -31,22 +36,22 @@
}
#[must_use]
- pub const fn source(&self) -> Node<'a, S> {
+ pub const fn source(&self) -> Node<'graph, S> {
self.source
}
#[must_use]
- pub const fn target(&self) -> Node<'a, S> {
+ pub const fn target(&self) -> Node<'graph, S> {
self.target
}
#[must_use]
- pub fn transit(&self) -> &[Node<'a, S>] {
+ pub fn transit(&self) -> &[Node<'graph, S>] {
&self.transit
}
#[must_use]
- pub fn to_vec(self) -> Vec<Node<'a, S>> {
+ pub fn to_vec(self) -> Vec<Node<'graph, S>> {
let mut vec = Vec::with_capacity(self.transit.len() + 2);
vec.push(self.source);
@@ -56,7 +61,7 @@
vec
}
- pub fn iter(&self) -> impl Iterator<Item = Node<'a, S>> + '_ {
+ pub fn iter(&self) -> impl Iterator<Item = Node<'graph, S>> + '_ {
once(self.source)
.chain(self.transit.iter().copied())
.chain(once(self.target))
@@ -80,10 +85,10 @@
#[cfg(test)]
static_assertions::assert_impl_all!(Path<'_, petgraph_dino::DinoStorage<(), ()>>: Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Send, Sync);
-impl<'a, S> Debug for Path<'a, S>
+impl<'graph, S> Debug for Path<'graph, S>
where
S: GraphStorage,
- Node<'a, S>: Debug,
+ Node<'graph, S>: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Path")
@@ -123,10 +128,10 @@
}
}
-impl<'a, S> PartialEq for Path<'a, S>
+impl<'graph, S> PartialEq for Path<'graph, S>
where
S: GraphStorage,
- Node<'a, S>: PartialEq,
+ Node<'graph, S>: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
(&self.source, &self.target, &self.transit)
@@ -134,17 +139,17 @@
}
}
-impl<'a, S> Eq for Path<'a, S>
+impl<'graph, S> Eq for Path<'graph, S>
where
S: GraphStorage,
- Node<'a, S>: Eq,
+ Node<'graph, S>: Eq,
{
}
-impl<'a, S> PartialOrd for Path<'a, S>
+impl<'graph, S> PartialOrd for Path<'graph, S>
where
S: GraphStorage,
- Node<'a, S>: PartialOrd,
+ Node<'graph, S>: PartialOrd,
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
(&self.source, &self.transit, &self.target).partial_cmp(&(
@@ -155,10 +160,10 @@
}
}
-impl<'a, S> Ord for Path<'a, S>
+impl<'graph, S> Ord for Path<'graph, S>
where
S: GraphStorage,
- Node<'a, S>: Ord,
+ Node<'graph, S>: Ord,
{
fn cmp(&self, other: &Self) -> Ordering {
(&self.source, &self.transit, &self.target).cmp(&(
@@ -169,22 +174,22 @@
}
}
-impl<'a, S> Hash for Path<'a, S>
+impl<'graph, S> Hash for Path<'graph, S>
where
S: GraphStorage,
- Node<'a, S>: Hash,
+ Node<'graph, S>: Hash,
{
fn hash<H: Hasher>(&self, state: &mut H) {
(&self.source, &self.target, &self.transit).hash(state);
}
}
-impl<'a, S> IntoIterator for Path<'a, S>
+impl<'graph, S> IntoIterator for Path<'graph, S>
where
S: GraphStorage,
{
- type IntoIter = alloc::vec::IntoIter<Node<'a, S>>;
- type Item = Node<'a, S>;
+ type IntoIter = alloc::vec::IntoIter<Node<'graph, S>>;
+ type Item = Node<'graph, S>;
fn into_iter(self) -> Self::IntoIter {
self.to_vec().into_iter()
diff --git a/crates/algorithms/src/shortest_paths/common/queue/double_ended.rs b/crates/algorithms/src/shortest_paths/common/queue/double_ended.rs
index b436b88..2083b63 100644
--- a/crates/algorithms/src/shortest_paths/common/queue/double_ended.rs
+++ b/crates/algorithms/src/shortest_paths/common/queue/double_ended.rs
@@ -1,49 +1,181 @@
use alloc::collections::VecDeque;
+use core::{
+ hash::Hash,
+ iter::Sum,
+ ops::{Add, Div},
+};
+
+use fxhash::FxBuildHasher;
+use hashbrown::HashSet;
+use numi::{
+ cast::{CastFrom, CastTo, TryCastFrom},
+ num::identity::Zero,
+};
+use petgraph_core::{GraphStorage, Node};
+
+pub(in crate::shortest_paths) struct DoubleEndedQueueItem<'graph, S, T>
+where
+ S: GraphStorage,
+{
+ node: Node<'graph, S>,
+
+ priority: T,
+}
+
+impl<'graph, S, T> DoubleEndedQueueItem<'graph, S, T>
+where
+ S: GraphStorage,
+{
+ pub(in crate::shortest_paths) fn node(&self) -> Node<'graph, S> {
+ self.node
+ }
+
+ pub(in crate::shortest_paths) fn priority(&self) -> &T {
+ &self.priority
+ }
+
+ pub(in crate::shortest_paths) fn into_parts(self) -> (Node<'graph, S>, T) {
+ (self.node, self.priority)
+ }
+}
// Newtype for VecDeque<T> to avoid exposing the VecDeque type as we may decide to reimplement this.
-pub(in crate::shortest_paths) struct DoubleEndedQueue<T>(VecDeque<T>);
-
-impl<T> DoubleEndedQueue<T>
+pub(in crate::shortest_paths) struct DoubleEndedQueue<'graph, S, T>
where
- T: PartialEq,
+ S: GraphStorage,
+{
+ queue: VecDeque<DoubleEndedQueueItem<'graph, S, T>>,
+ active: HashSet<&'graph S::NodeId, FxBuildHasher>,
+}
+
+impl<'graph, S, T> DoubleEndedQueue<'graph, S, T>
+where
+ S: GraphStorage,
+ S::NodeId: Eq + Hash,
{
pub(in crate::shortest_paths) fn new() -> Self {
- Self(VecDeque::new())
+ Self {
+ queue: VecDeque::new(),
+ active: HashSet::with_hasher(FxBuildHasher::default()),
+ }
}
pub(in crate::shortest_paths) fn with_capacity(capacity: usize) -> Self {
- Self(VecDeque::with_capacity(capacity))
+ Self {
+ queue: VecDeque::with_capacity(capacity),
+ active: HashSet::with_capacity_and_hasher(capacity, FxBuildHasher::default()),
+ }
}
- pub(in crate::shortest_paths) fn push_front(&mut self, item: T) {
- self.0.push_front(item);
+ // TODO: this has potential for speedup by not needing to update the priority
+ fn update_priority(&mut self, node: Node<'graph, S>, priority: T) {
+ let id = node.id();
+
+ if let Some(item) = self.queue.iter_mut().find(|item| item.node.id() == id) {
+ item.priority = priority;
+ }
}
- pub(in crate::shortest_paths) fn push_back(&mut self, item: T) {
- self.0.push_back(item);
+ pub(in crate::shortest_paths) fn push_front(
+ &mut self,
+ node: Node<'graph, S>,
+ priority: T,
+ ) -> bool {
+ if !self.active.insert(node.id()) {
+ self.update_priority(node, priority);
+ return false;
+ }
+
+ self.queue
+ .push_front(DoubleEndedQueueItem { node, priority });
+ true
}
- pub(in crate::shortest_paths) fn pop_front(&mut self) -> Option<T> {
- self.0.pop_front()
+ pub(in crate::shortest_paths) fn push_back(
+ &mut self,
+ node: Node<'graph, S>,
+ priority: T,
+ ) -> bool {
+ if !self.active.insert(node.id()) {
+ self.update_priority(node, priority);
+ return false;
+ }
+
+ self.queue
+ .push_back(DoubleEndedQueueItem { node, priority });
+ true
}
- pub(in crate::shortest_paths) fn pop_back(&mut self) -> Option<T> {
- self.0.pop_back()
+ pub(in crate::shortest_paths) fn pop_front(
+ &mut self,
+ ) -> Option<DoubleEndedQueueItem<'graph, S, T>> {
+ let value = self.queue.pop_front()?;
+
+ self.active.remove(value.node.id());
+
+ Some(value)
}
- pub(in crate::shortest_paths) fn front(&self) -> Option<&T> {
- self.0.front()
+ pub(in crate::shortest_paths) fn pop_back(
+ &mut self,
+ ) -> Option<DoubleEndedQueueItem<'graph, S, T>> {
+ let value = self.queue.pop_back()?;
+
+ self.active.remove(value.node.id());
+
+ Some(value)
}
- pub(in crate::shortest_paths) fn back(&self) -> Option<&T> {
- self.0.back()
+ pub(in crate::shortest_paths) fn peek_front(
+ &self,
+ ) -> Option<&DoubleEndedQueueItem<'graph, S, T>> {
+ self.queue.front()
+ }
+
+ pub(in crate::shortest_paths) fn peek_back(
+ &self,
+ ) -> Option<&DoubleEndedQueueItem<'graph, S, T>> {
+ self.queue.back()
}
pub(in crate::shortest_paths) fn is_empty(&self) -> bool {
- self.0.is_empty()
+ self.queue.is_empty()
}
- pub(in crate::shortest_paths) fn contains(&self, item: &T) -> bool {
- self.0.contains(item)
+ pub(in crate::shortest_paths) fn len(&self) -> usize {
+ self.queue.len()
+ }
+
+ pub(in crate::shortest_paths) fn contains_node(&self, node: &S::NodeId) -> bool {
+ self.active.contains(node)
+ }
+}
+
+impl<'graph, S, T> DoubleEndedQueue<'graph, S, T>
+where
+ S: GraphStorage,
+{
+ pub(in crate::shortest_paths) fn average_priority(&self) -> Option<T>
+ where
+ T: Zero + Div<Output = T> + Add<Output = T> + for<'a> Sum<&'a T> + TryCastFrom<usize>,
+ {
+ let (front, back) = self.queue.as_slices();
+
+ let front_sum: T = front.iter().map(|item| &item.priority).sum();
+ let back_sum: T = back.iter().map(|item| &item.priority).sum();
+
+ let total_sum: T = front_sum + back_sum;
+
+ if self.queue.is_empty() {
+ return None;
+ }
+
+ let length: T = TryCastFrom::try_cast_from(self.queue.len()).ok()?;
+
+ if length.is_zero() {
+ return None;
+ }
+
+ Some(total_sum / length)
}
}
diff --git a/crates/algorithms/src/shortest_paths/common/route.rs b/crates/algorithms/src/shortest_paths/common/route.rs
index ef9031a..23829fc 100644
--- a/crates/algorithms/src/shortest_paths/common/route.rs
+++ b/crates/algorithms/src/shortest_paths/common/route.rs
@@ -1,28 +1,30 @@
-use core::fmt::{Debug, Display};
-use std::hash::Hash;
+use core::{
+ fmt::{Debug, Display},
+ hash::Hash,
+};
use petgraph_core::{GraphStorage, Node};
use crate::shortest_paths::common::{cost::Cost, path::Path};
-pub struct Route<'a, S, T>
+pub struct Route<'graph, S, T>
where
S: GraphStorage,
{
- path: Path<'a, S>,
+ path: Path<'graph, S>,
cost: Cost<T>,
}
-impl<'a, S, T> Route<'a, S, T>
+impl<'graph, S, T> Route<'graph, S, T>
where
S: GraphStorage,
{
- pub const fn new(path: Path<'a, S>, cost: Cost<T>) -> Self {
+ pub const fn new(path: Path<'graph, S>, cost: Cost<T>) -> Self {
Self { path, cost }
}
- pub const fn path(&self) -> &Path<'a, S> {
+ pub const fn path(&self) -> &Path<'graph, S> {
&self.path
}
@@ -34,11 +36,11 @@
self.cost
}
- pub fn into_path(self) -> Path<'a, S> {
+ pub fn into_path(self) -> Path<'graph, S> {
self.path
}
- pub fn into_parts(self) -> (Path<'a, S>, Cost<T>) {
+ pub fn into_parts(self) -> (Path<'graph, S>, Cost<T>) {
(self.path, self.cost)
}
@@ -55,10 +57,10 @@
#[cfg(test)]
static_assertions::assert_impl_all!(Route<'_, petgraph_dino::DinoStorage<(), ()>, &'static str>: Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Send, Sync);
-impl<'a, S, T> Debug for Route<'a, S, T>
+impl<'graph, S, T> Debug for Route<'graph, S, T>
where
S: GraphStorage,
- Node<'a, S>: Debug,
+ Node<'graph, S>: Debug,
T: Debug,
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
@@ -83,7 +85,7 @@
}
}
-impl<'a, S, T> Clone for Route<'a, S, T>
+impl<'graph, S, T> Clone for Route<'graph, S, T>
where
S: GraphStorage,
T: Clone,
@@ -96,10 +98,10 @@
}
}
-impl<'a, S, T> PartialEq for Route<'a, S, T>
+impl<'graph, S, T> PartialEq for Route<'graph, S, T>
where
S: GraphStorage,
- Path<'a, S>: PartialEq,
+ Path<'graph, S>: PartialEq,
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
@@ -107,18 +109,18 @@
}
}
-impl<'a, S, T> Eq for Route<'a, S, T>
+impl<'graph, S, T> Eq for Route<'graph, S, T>
where
S: GraphStorage,
- Path<'a, S>: Eq,
+ Path<'graph, S>: Eq,
T: Eq,
{
}
-impl<'a, S, T> PartialOrd for Route<'a, S, T>
+impl<'graph, S, T> PartialOrd for Route<'graph, S, T>
where
S: GraphStorage,
- Path<'a, S>: PartialOrd,
+ Path<'graph, S>: PartialOrd,
T: PartialOrd,
{
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
@@ -127,10 +129,10 @@
}
}
-impl<'a, S, T> Ord for Route<'a, S, T>
+impl<'graph, S, T> Ord for Route<'graph, S, T>
where
S: GraphStorage,
- Path<'a, S>: Ord,
+ Path<'graph, S>: Ord,
T: Ord,
{
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
@@ -138,10 +140,10 @@
}
}
-impl<'a, S, T> Hash for Route<'a, S, T>
+impl<'graph, S, T> Hash for Route<'graph, S, T>
where
S: GraphStorage,
- Path<'a, S>: Hash,
+ Path<'graph, S>: Hash,
T: Hash,
{
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
@@ -149,21 +151,21 @@
}
}
-pub struct DirectRoute<'a, S, T>
+pub struct DirectRoute<'graph, S, T>
where
S: GraphStorage,
{
- source: Node<'a, S>,
- target: Node<'a, S>,
+ source: Node<'graph, S>,
+ target: Node<'graph, S>,
cost: Cost<T>,
}
-impl<'a, S, T> DirectRoute<'a, S, T>
+impl<'graph, S, T> DirectRoute<'graph, S, T>
where
S: GraphStorage,
{
- pub const fn new(source: Node<'a, S>, target: Node<'a, S>, cost: Cost<T>) -> Self {
+ pub const fn new(source: Node<'graph, S>, target: Node<'graph, S>, cost: Cost<T>) -> Self {
Self {
source,
target,
@@ -171,11 +173,11 @@
}
}
- pub const fn source(&self) -> &Node<'a, S> {
+ pub const fn source(&self) -> &Node<'graph, S> {
&self.source
}
- pub const fn target(&self) -> &Node<'a, S> {
+ pub const fn target(&self) -> &Node<'graph, S> {
&self.target
}
@@ -183,7 +185,7 @@
&self.cost
}
- pub fn into_endpoints(self) -> (Node<'a, S>, Node<'a, S>) {
+ pub fn into_endpoints(self) -> (Node<'graph, S>, Node<'graph, S>) {
(self.source, self.target)
}
@@ -191,7 +193,7 @@
self.cost
}
- pub fn into_parts(self) -> (Node<'a, S>, Node<'a, S>, Cost<T>) {
+ pub fn into_parts(self) -> (Node<'graph, S>, Node<'graph, S>, Cost<T>) {
(self.source, self.target, self.cost)
}
@@ -209,10 +211,10 @@
#[cfg(test)]
static_assertions::assert_impl_all!(DirectRoute<'_, petgraph_dino::DinoStorage<(), ()>, &'static str>: Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Send, Sync);
-impl<'a, S, T> Debug for DirectRoute<'a, S, T>
+impl<'graph, S, T> Debug for DirectRoute<'graph, S, T>
where
S: GraphStorage,
- Node<'a, S>: Debug,
+ Node<'graph, S>: Debug,
T: Debug,
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
@@ -240,7 +242,7 @@
}
}
-impl<'a, S, T> Clone for DirectRoute<'a, S, T>
+impl<'graph, S, T> Clone for DirectRoute<'graph, S, T>
where
S: GraphStorage,
T: Clone,
@@ -254,10 +256,10 @@
}
}
-impl<'a, S, T> PartialEq for DirectRoute<'a, S, T>
+impl<'graph, S, T> PartialEq for DirectRoute<'graph, S, T>
where
S: GraphStorage,
- Node<'a, S>: PartialEq,
+ Node<'graph, S>: PartialEq,
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
@@ -265,18 +267,18 @@
}
}
-impl<'a, S, T> Eq for DirectRoute<'a, S, T>
+impl<'graph, S, T> Eq for DirectRoute<'graph, S, T>
where
S: GraphStorage,
- Node<'a, S>: Eq,
+ Node<'graph, S>: Eq,
T: Eq,
{
}
-impl<'a, S, T> PartialOrd for DirectRoute<'a, S, T>
+impl<'graph, S, T> PartialOrd for DirectRoute<'graph, S, T>
where
S: GraphStorage,
- Node<'a, S>: PartialOrd,
+ Node<'graph, S>: PartialOrd,
T: PartialOrd,
{
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
@@ -289,10 +291,10 @@
}
}
-impl<'a, S, T> Ord for DirectRoute<'a, S, T>
+impl<'graph, S, T> Ord for DirectRoute<'graph, S, T>
where
S: GraphStorage,
- Node<'a, S>: Ord,
+ Node<'graph, S>: Ord,
T: Ord,
{
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
@@ -300,10 +302,10 @@
}
}
-impl<'a, S, T> Hash for DirectRoute<'a, S, T>
+impl<'graph, S, T> Hash for DirectRoute<'graph, S, T>
where
S: GraphStorage,
- Node<'a, S>: Hash,
+ Node<'graph, S>: Hash,
T: Hash,
{
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
@@ -311,12 +313,12 @@
}
}
-impl<'a, S, T> From<Route<'a, S, T>> for DirectRoute<'a, S, T>
+impl<'graph, S, T> From<Route<'graph, S, T>> for DirectRoute<'graph, S, T>
where
S: GraphStorage,
T: Clone,
{
- fn from(route: Route<'a, S, T>) -> Self {
+ fn from(route: Route<'graph, S, T>) -> Self {
Self {
source: route.path.source(),
target: route.path.target(),
diff --git a/crates/algorithms/src/shortest_paths/common/tests.rs b/crates/algorithms/src/shortest_paths/common/tests.rs
index 1b4b9d5..0839623 100644
--- a/crates/algorithms/src/shortest_paths/common/tests.rs
+++ b/crates/algorithms/src/shortest_paths/common/tests.rs
@@ -16,14 +16,22 @@
/// readable
macro_rules! expected {
(@rule $nodes:ident; $source:ident; ($($path:ident),*) ;$target:ident; $cost:literal) => {
- Expect {
+ $crate::shortest_paths::common::tests::Expect {
source: $nodes.$source,
target: $nodes.$target,
transit: alloc::vec![$($nodes.$path),*],
cost: $cost,
}
};
- ($nodes:ident; [$($source:ident -( $($path:ident),* )> $target:ident : $cost:literal),* $(,)?]) => {
+ (@rule $nodes:ident; $source:literal; ($($path:literal),*) ;$target:literal; $cost:literal) => {
+ $crate::shortest_paths::common::tests::Expect {
+ source: $nodes[$source],
+ target: $nodes[$target],
+ transit: alloc::vec![$($nodes[$path]),*],
+ cost: $cost,
+ }
+ };
+ ($nodes:ident; [$($source:tt -( $($path:tt),* )> $target:tt : $cost:literal),* $(,)?]) => {
alloc::vec![
$(expected!(@rule $nodes; $source; ($($path),*) ;$target; $cost)),*
]
@@ -32,35 +40,32 @@
pub(in crate::shortest_paths) use expected;
-pub(in crate::shortest_paths) struct Expect<G, T>
-where
- G: GraphExt,
-{
- pub(in crate::shortest_paths) source: <G::Storage as GraphStorage>::NodeId,
- pub(in crate::shortest_paths) target: <G::Storage as GraphStorage>::NodeId,
+pub(in crate::shortest_paths) struct Expect<N, T> {
+ pub(in crate::shortest_paths) source: N,
+ pub(in crate::shortest_paths) target: N,
- pub(in crate::shortest_paths) transit: Vec<<G::Storage as GraphStorage>::NodeId>,
+ pub(in crate::shortest_paths) transit: Vec<N>,
pub(in crate::shortest_paths) cost: T,
}
-pub(in crate::shortest_paths) struct TestCase<'a, G, A, T>
+pub(in crate::shortest_paths) struct TestCase<'a, S, A, T>
where
- G: GraphExt,
+ S: GraphStorage,
{
- graph: &'a Graph<G::Storage>,
+ graph: &'a Graph<S>,
algorithm: &'a A,
- expected: &'a [Expect<G, T>],
+ expected: &'a [Expect<S::NodeId, T>],
}
-impl<'a, G, A, T> TestCase<'a, G, A, T>
+impl<'a, S, A, T> TestCase<'a, S, A, T>
where
- G: GraphExt,
+ S: GraphStorage,
{
pub(crate) const fn new(
- graph: &'a Graph<G::Storage>,
+ graph: &'a Graph<S>,
algorithm: &'a A,
- expected: &'a [Expect<G, T>],
+ expected: &'a [Expect<<S as GraphStorage>::NodeId, T>],
) -> Self {
Self {
graph,
@@ -70,17 +75,15 @@
}
}
-impl<'a, G, A, T> TestCase<'a, G, A, T>
+impl<'a, S, A, T> TestCase<'a, S, A, T>
where
- G: GraphExt,
- <G::Storage as GraphStorage>::NodeId: Eq + Hash + Debug + Display,
- A: ShortestPath<G::Storage, Cost = T>,
+ S: GraphStorage,
+ S::NodeId: Eq + Hash + Debug + Display,
+ A: ShortestPath<S, Cost = T>,
T: PartialEq + Debug,
{
- fn assert_path_routes(
- &self,
- routes: Result<impl Iterator<Item = Route<'a, G::Storage, T>>, A::Error>,
- ) {
+ #[track_caller]
+ fn assert_path_routes(&self, routes: Result<impl Iterator<Item = Route<'a, S, T>>, A::Error>) {
let mut routes: HashMap<_, _> = routes
.unwrap()
.map(|route| {
@@ -116,24 +119,23 @@
self.assert_path_routes(self.algorithm.every_path(self.graph));
}
- pub(in crate::shortest_paths) fn assert_path_from(
- &self,
- source: &<G::Storage as GraphStorage>::NodeId,
- ) {
+ #[track_caller]
+ pub(in crate::shortest_paths) fn assert_path_from(&self, source: &S::NodeId) {
self.assert_path_routes(self.algorithm.path_from(self.graph, source));
}
}
-impl<'a, G, A, T> TestCase<'a, G, A, T>
+impl<'a, S, A, T> TestCase<'a, S, A, T>
where
- G: GraphExt,
- <G::Storage as GraphStorage>::NodeId: Eq + Hash + Debug + Display,
- A: ShortestDistance<G::Storage, Cost = T>,
+ S: GraphStorage,
+ S::NodeId: Eq + Hash + Debug + Display,
+ A: ShortestDistance<S, Cost = T>,
T: PartialEq + Debug,
{
+ #[track_caller]
fn assert_distance_routes(
&self,
- routes: Result<impl Iterator<Item = DirectRoute<'a, G::Storage, T>>, A::Error>,
+ routes: Result<impl Iterator<Item = DirectRoute<'a, S, T>>, A::Error>,
) {
let mut routes: HashMap<_, _> = routes
.unwrap()
@@ -171,21 +173,8 @@
self.assert_distance_routes(self.algorithm.every_distance(self.graph));
}
- pub(in crate::shortest_paths) fn assert_distance_from(
- &self,
- source: &<G::Storage as GraphStorage>::NodeId,
- ) {
+ #[track_caller]
+ pub(in crate::shortest_paths) fn assert_distance_from(&self, source: &S::NodeId) {
self.assert_distance_routes(self.algorithm.distance_from(self.graph, source));
}
}
-
-pub(in crate::shortest_paths) trait GraphExt {
- type Storage: GraphStorage;
-}
-
-impl<S> GraphExt for Graph<S>
-where
- S: GraphStorage,
-{
- type Storage = S;
-}
diff --git a/crates/algorithms/src/shortest_paths/common/transit.rs b/crates/algorithms/src/shortest_paths/common/transit.rs
index 62f1d4e..8921358 100644
--- a/crates/algorithms/src/shortest_paths/common/transit.rs
+++ b/crates/algorithms/src/shortest_paths/common/transit.rs
@@ -1,10 +1,7 @@
-use alloc::vec::Vec;
-use core::hash::Hash;
-use petgraph_core::{
- id::{AttributeGraphId, AttributeStorage},
- GraphStorage, Node,
-};
+
+use hashbrown::{HashMap, HashSet};
+use petgraph_core::{GraphStorage, Node};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(in crate::shortest_paths) enum PredecessorMode {
@@ -45,3 +42,80 @@
path
}
+
+/// Reconstruct all simple paths between two nodes without those nodes being part of the path.
+///
+/// This has been adapted from the [NetworkX implementation](https://github.com/networkx/networkx/blob/f93f0e2a066fc456aa447853af9d00eec1058542/networkx/algorithms/shortest_paths/generic.py#L655)
+pub(in crate::shortest_paths) fn reconstruct_paths_between<'a, 'graph, S, H>(
+ predecessors: &'a HashMap<&'graph S::NodeId, Vec<Node<'graph, S>>, H>,
+ source: &'graph S::NodeId,
+ target: Node<'graph, S>,
+) -> impl Iterator<Item = Vec<Node<'graph, S>>> + 'a
+where
+ S: GraphStorage,
+ S::NodeId: Eq + Hash,
+ H: BuildHasher,
+{
+ let mut seen = HashSet::new();
+ seen.insert(target.id());
+
+ let mut stack = vec![(target, 0usize)];
+ let mut top = 0;
+ // by using a `yielded` boolean, we're able to suspend and resume, as in the first iteration we
+ // early return and then try "again" in the next iteration but do not early return again.
+ let mut yielded = false;
+ let mut exhausted = false;
+
+ iter::from_fn(move || {
+ if exhausted {
+ return None;
+ }
+
+ loop {
+ let (node, index) = stack[top];
+
+ if !yielded && node.id() == source {
+ // "yield" result
+ yielded = true;
+
+ // we skip the first element (target) and last element (source)
+ let mut path: Vec<_> = stack.get(1..top)?.iter().map(|(node, _)| *node).collect();
+ path.reverse();
+
+ return Some(path);
+ }
+
+ if predecessors[node.id()].len() > index {
+ stack[top].1 = index + 1;
+ let next = predecessors[node.id()][index];
+ if !seen.insert(next.id()) {
+ // value already seen
+ continue;
+ }
+
+ top += 1;
+
+ if top == stack.len() {
+ stack.push((next, 0));
+ } else {
+ stack[top] = (next, 0);
+ }
+ } else {
+ seen.remove(&node.id());
+
+ match top.checked_sub(1) {
+ Some(new_top) => top = new_top,
+ None => {
+ // we have exhausted all paths
+ exhausted = true;
+ break;
+ }
+ }
+ }
+
+ yielded = false;
+ }
+
+ None
+ })
+}
diff --git a/crates/algorithms/src/shortest_paths/deprecated_bellman_ford.rs b/crates/algorithms/src/shortest_paths/deprecated_bellman_ford.rs
deleted file mode 100644
index 25f6761..0000000
--- a/crates/algorithms/src/shortest_paths/deprecated_bellman_ford.rs
+++ /dev/null
@@ -1,298 +0,0 @@
-//! Bellman-Ford algorithms.
-
-use alloc::{vec, vec::Vec};
-
-use funty::Floating;
-use petgraph_core::deprecated::visit::{
- EdgeRef, IntoEdges, IntoNodeIdentifiers, NodeCount, NodeIndexable, VisitMap, Visitable,
-};
-
-use crate::{error::NegativeCycleError, shortest_paths::FloatMeasure};
-
-#[derive(Debug, Clone)]
-pub struct Paths<NodeId, EdgeWeight> {
- pub distances: Vec<EdgeWeight>,
- pub predecessors: Vec<Option<NodeId>>,
-}
-
-/// \[Generic\] Compute shortest paths from node `source` to all other.
-///
-/// Using the [Bellman–Ford algorithm][bf]; negative edge costs are
-/// permitted, but the graph must not have a cycle of negative weights
-/// (in that case it will return an error).
-///
-/// On success, return one vec with path costs, and another one which points
-/// out the predecessor of a node along a shortest path. The vectors
-/// are indexed by the graph's node indices.
-///
-/// [bf]: https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm
-///
-/// # Example
-/// ```rust
-/// use petgraph_algorithms::shortest_paths::bellman_ford;
-/// use petgraph_core::edge::Undirected;
-/// use petgraph_graph::{Graph, NodeIndex};
-///
-/// let mut g = Graph::new();
-/// let a = g.add_node(()); // node with no weight
-/// let b = g.add_node(());
-/// let c = g.add_node(());
-/// let d = g.add_node(());
-/// let e = g.add_node(());
-/// let f = g.add_node(());
-/// g.extend_with_edges(&[
-/// (0, 1, 2.0),
-/// (0, 3, 4.0),
-/// (1, 2, 1.0),
-/// (1, 5, 7.0),
-/// (2, 4, 5.0),
-/// (4, 5, 1.0),
-/// (3, 4, 1.0),
-/// ]);
-///
-/// // Graph represented with the weight of each edge
-/// //
-/// // 2 1
-/// // a ----- b ----- c
-/// // | 4 | 7 |
-/// // d f | 5
-/// // | 1 | 1 |
-/// // \------ e ------/
-///
-/// let path = bellman_ford(&g, a);
-/// assert!(path.is_ok());
-/// let path = path.unwrap();
-/// assert_eq!(path.distances, vec![0.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
-/// assert_eq!(
-/// path.predecessors,
-/// vec![None, Some(a), Some(b), Some(a), Some(d), Some(e)]
-/// );
-///
-/// // Node f (indice 5) can be reach from a with a path costing 6.
-/// // Predecessor of f is Some(e) which predecessor is Some(d) which predecessor is Some(a).
-/// // Thus the path from a to f is a <-> d <-> e <-> f
-///
-/// let graph_with_neg_cycle = Graph::<(), f32, Undirected>::from_edges(&[
-/// (0, 1, -2.0),
-/// (0, 3, -4.0),
-/// (1, 2, -1.0),
-/// (1, 5, -25.0),
-/// (2, 4, -5.0),
-/// (4, 5, -25.0),
-/// (3, 4, -1.0),
-/// ]);
-///
-/// assert!(bellman_ford(&graph_with_neg_cycle, NodeIndex::new(0)).is_err());
-/// ```
-pub fn bellman_ford<G>(
- g: G,
- source: G::NodeId,
-) -> Result<Paths<G::NodeId, G::EdgeWeight>, NegativeCycleError>
-where
- G: NodeCount + IntoNodeIdentifiers + IntoEdges + NodeIndexable,
- G::EdgeWeight: FloatMeasure,
-{
- let ix = |i| g.to_index(i);
-
- // Step 1 and Step 2: initialize and relax
- let (distances, predecessors) = bellman_ford_initialize_relax(g, source);
-
- // Step 3: check for negative weight cycle
- for i in g.node_identifiers() {
- for edge in g.edges(i) {
- let j = edge.target();
- let w = *edge.weight();
- if distances[ix(i)] + w < distances[ix(j)] {
- return Err(NegativeCycleError);
- }
- }
- }
-
- Ok(Paths {
- distances,
- predecessors,
- })
-}
-
-/// \[Generic\] Find the path of a negative cycle reachable from node `source`.
-///
-/// Using the [find_negative_cycle][nc]; will search the Graph for negative cycles using
-/// [Bellman–Ford algorithm][bf]. If no negative cycle is found the function will return `None`.
-///
-/// If a negative cycle is found from source, return one vec with a path of `NodeId`s.
-///
-/// The time complexity of this algorithm should be the same as the Bellman-Ford (O(|V|·|E|)).
-///
-/// [nc]: https://blogs.asarkar.com/assets/docs/algorithms-curated/Negative-Weight%20Cycle%20Algorithms%20-%20Huang.pdf
-/// [bf]: https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm
-///
-/// # Example
-/// ```rust
-/// use petgraph_algorithms::shortest_paths::find_negative_cycle;
-/// use petgraph_core::edge::Directed;
-/// use petgraph_graph::{Graph, NodeIndex};
-///
-/// let graph_with_neg_cycle = Graph::<(), f32, Directed>::from_edges(&[
-/// (0, 1, 1.),
-/// (0, 2, 1.),
-/// (0, 3, 1.),
-/// (1, 3, 1.),
-/// (2, 1, 1.),
-/// (3, 2, -3.),
-/// ]);
-///
-/// let path = find_negative_cycle(&graph_with_neg_cycle, NodeIndex::new(0));
-/// assert_eq!(
-/// path,
-/// Some([NodeIndex::new(1), NodeIndex::new(3), NodeIndex::new(2)].to_vec())
-/// );
-/// ```
-pub fn find_negative_cycle<G>(g: G, source: G::NodeId) -> Option<Vec<G::NodeId>>
-where
- G: NodeCount + IntoNodeIdentifiers + IntoEdges + NodeIndexable + Visitable,
- G::EdgeWeight: FloatMeasure,
-{
- let ix = |i| g.to_index(i);
- let mut path = Vec::<G::NodeId>::new();
-
- // Step 1: initialize and relax
- let (distance, predecessor) = bellman_ford_initialize_relax(g, source);
-
- // Step 2: Check for negative weight cycle
- 'outer: for i in g.node_identifiers() {
- for edge in g.edges(i) {
- let j = edge.target();
- let w = *edge.weight();
- if distance[ix(i)] + w < distance[ix(j)] {
- // Step 3: negative cycle found
- let start = j;
- let mut node = start;
- let mut visited = g.visit_map();
- // Go backward in the predecessor chain
- loop {
- let ancestor = match predecessor[ix(node)] {
- Some(predecessor_node) => predecessor_node,
- None => node, // no predecessor, self cycle
- };
- // We have only 2 ways to find the cycle and break the loop:
- // 1. start is reached
- if ancestor == start {
- path.push(ancestor);
- break;
- }
- // 2. some node was reached twice
- else if visited.is_visited(&ancestor) {
- // Drop any node in path that is before the first ancestor
- let pos = path
- .iter()
- .position(|&p| p == ancestor)
- .expect("we should always have a position");
- path = path[pos..path.len()].to_vec();
-
- break;
- }
-
- // None of the above, some middle path node
- path.push(ancestor);
- visited.visit(ancestor);
- node = ancestor;
- }
- // We are done here
- break 'outer;
- }
- }
- }
-
- if !path.is_empty() {
- // Users will probably need to follow the path of the negative cycle
- // so it should be in the reverse order than it was found by the algorithm.
- path.reverse();
- Some(path)
- } else {
- None
- }
-}
-
-// Perform Step 1 and Step 2 of the Bellman-Ford algorithm.
-#[inline(always)]
-fn bellman_ford_initialize_relax<G>(
- g: G,
- source: G::NodeId,
-) -> (Vec<G::EdgeWeight>, Vec<Option<G::NodeId>>)
-where
- G: NodeCount + IntoNodeIdentifiers + IntoEdges + NodeIndexable,
- G::EdgeWeight: FloatMeasure,
-{
- // Step 1: initialize graph
- let mut predecessor = vec![None; g.node_bound()];
- let mut distance = vec![G::EdgeWeight::INFINITY; g.node_bound()];
- let ix = |i| g.to_index(i);
- distance[ix(source)] = G::EdgeWeight::POS_ZERO;
-
- // Step 2: relax edges repeatedly
- for _ in 1..g.node_count() {
- let mut did_update = false;
- for i in g.node_identifiers() {
- for edge in g.edges(i) {
- let j = edge.target();
- let w = *edge.weight();
- if distance[ix(i)] + w < distance[ix(j)] {
- distance[ix(j)] = distance[ix(i)] + w;
- predecessor[ix(j)] = Some(i);
- did_update = true;
- }
- }
- }
- if !did_update {
- break;
- }
- }
- (distance, predecessor)
-}
-
-#[cfg(test)]
-mod tests {
- use alloc::format;
-
- use petgraph_core::edge::{Directed, Undirected};
- use petgraph_graph::Graph;
- use proptest::prelude::*;
-
- use crate::shortest_paths::_bellman_ford;
-
- #[cfg(not(miri))]
- proptest! {
- #[test]
- fn positive_weights_undirected_always_ok(mut graph in any::<Graph<(), f32, Undirected, u8>>()) {
- for weight in graph.edge_weights_mut() {
- *weight = weight.abs();
- }
-
- for node in graph.node_indices() {
- _bellman_ford(&graph, node).expect("should be possible");
- }
- }
-
- #[test]
- fn positive_weights_directed_always_ok(mut graph in any::<Graph<(), f32, Directed, u8>>()) {
- for weight in graph.edge_weights_mut() {
- *weight = weight.abs();
- }
-
- for node in graph.node_indices() {
- _bellman_ford(&graph, node).expect("should be possible");
- }
- }
-
- #[test]
- fn negative_path_never_empty(graph in any::<Graph<(), f32, Directed, u8>>()) {
- for node in graph.node_indices() {
- let path = _bellman_ford::find_negative_cycle(&graph, node);
-
- if let Some(path) = path {
- prop_assert!(!path.is_empty());
- }
- }
- }
- }
-}
diff --git a/crates/algorithms/src/shortest_paths/dijkstra/iter.rs b/crates/algorithms/src/shortest_paths/dijkstra/iter.rs
index 9352e95..c998f95 100644
--- a/crates/algorithms/src/shortest_paths/dijkstra/iter.rs
+++ b/crates/algorithms/src/shortest_paths/dijkstra/iter.rs
@@ -1,11 +1,11 @@
use alloc::vec::Vec;
-use core::{hash::Hash, ops::Add};
+use core::hash::Hash;
use std::mem;
use error_stack::{Report, Result};
use fxhash::FxBuildHasher;
-use hashbrown::{hash_map::Entry, HashMap};
-use num_traits::Zero;
+use hashbrown::HashMap;
+use numi::num::{identity::Zero, ops::AddRef};
use petgraph_core::{
id::{AttributeGraphId, AttributeStorage, FlaggableGraphId},
Graph, GraphStorage, Node,
@@ -20,7 +20,7 @@
route::Route,
transit::{reconstruct_path_to, PredecessorMode},
},
- dijkstra::DijkstraError,
+ dijkstra::{measure::DijkstraMeasure, DijkstraError},
};
pub(super) struct DijkstraIter<'graph: 'parent, 'parent, S, E, G>
@@ -28,7 +28,7 @@
S: GraphStorage,
S::NodeId: FlaggableGraphId<S> + AttributeGraphId<S>,
E: GraphCost<S>,
- E::Value: Ord,
+ E::Value: DijkstraMeasure,
{
graph: &'graph Graph<S>,
queue: PriorityQueue<'graph, S, E::Value>,
@@ -54,8 +54,7 @@
S: GraphStorage,
S::NodeId: FlaggableGraphId<S> + AttributeGraphId<S>,
E: GraphCost<S>,
- E::Value: PartialOrd + Ord + Zero + Clone + 'graph,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: DijkstraMeasure,
G: Connections<'graph, S>,
{
pub(super) fn new(
@@ -103,8 +102,7 @@
S: GraphStorage,
S::NodeId: FlaggableGraphId<S> + AttributeGraphId<S>,
E: GraphCost<S>,
- E::Value: PartialOrd + Ord + Zero + Clone + 'graph,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: DijkstraMeasure,
G: Connections<'graph, S>,
{
type Item = Route<'graph, S, E::Value>;
diff --git a/crates/algorithms/src/shortest_paths/dijkstra/measure.rs b/crates/algorithms/src/shortest_paths/dijkstra/measure.rs
new file mode 100644
index 0000000..9598b76
--- /dev/null
+++ b/crates/algorithms/src/shortest_paths/dijkstra/measure.rs
@@ -0,0 +1,82 @@
+use numi::num::{identity::Zero, ops::AddRef};
+
+/// Automatically implemented trait for types that can be used as a measure in the Dijkstra
+/// algorithm.
+///
+/// This trait is implemented for all types that implement the supertraits mentioned in the trait
+/// definition.
+/// These traits either originate from [`core`] or [`numi`].
+/// Special attention must be paid to the [`AddRef`] trait, which is a proxy trait which is
+/// implemented for types that implement: `&Self: Add<&Self, Output = Self>`.
+///
+/// # Note on floating point types
+///
+/// This trait is not implemented for both [`f32`] and [`f64`], because they do not implement
+/// [`Ord`], which is required for the binary heap used in the Dijkstra algorithm.
+/// You can instead use [`ordered_float::NotNan`], which is a wrapper type that implements [`Ord`].
+///
+/// Be aware that [`ordered_float::OrderedFloat`] does not implement [`AddRef`] and cannot be used,
+/// for a reason as to why see [this issue](https://github.com/reem/rust-ordered-float/issues/145).
+///
+/// You can read about the reason why [`f32`] and [`f64`] do not implement [`Ord`]
+/// in the Rust documentation [here](https://doc.rust-lang.org/std/primitive.f32.html).
+///
+/// # Example
+///
+/// ```rust
+/// use core::num::Wrapping;
+/// use core::num::Saturating;
+/// use ordered_float::{NotNan, OrderedFloat};
+/// use petgraph_algorithms::shortest_paths::dijkstra::DijkstraMeasure;
+/// use static_assertions::assert_impl_all;
+///
+/// // Some examples of types that implement DijkstraMeasure
+/// assert_impl_all!(u8: DijkstraMeasure);
+/// assert_impl_all!(u16: DijkstraMeasure);
+/// assert_impl_all!(u32: DijkstraMeasure);
+/// assert_impl_all!(u64: DijkstraMeasure);
+/// assert_impl_all!(u128: DijkstraMeasure);
+/// assert_impl_all!(usize: DijkstraMeasure);
+///
+/// assert_impl_all!(i8: DijkstraMeasure);
+/// assert_impl_all!(i16: DijkstraMeasure);
+/// assert_impl_all!(i32: DijkstraMeasure);
+/// assert_impl_all!(i64: DijkstraMeasure);
+/// assert_impl_all!(i128: DijkstraMeasure);
+/// assert_impl_all!(isize: DijkstraMeasure);
+///
+/// // f32 and f64 are not implemented because they are not Ord
+/// // use `ordered_float::NotNan` instead.
+/// assert_impl_all!(NotNan<f32>: DijkstraMeasure);
+/// assert_impl_all!(NotNan<f64>: DijkstraMeasure);
+///
+/// assert_impl_all!(Wrapping<u8>: DijkstraMeasure);
+/// assert_impl_all!(Wrapping<u16>: DijkstraMeasure);
+/// assert_impl_all!(Wrapping<u32>: DijkstraMeasure);
+/// assert_impl_all!(Wrapping<u64>: DijkstraMeasure);
+/// assert_impl_all!(Wrapping<u128>: DijkstraMeasure);
+/// assert_impl_all!(Wrapping<usize>: DijkstraMeasure);
+///
+/// assert_impl_all!(Wrapping<i8>: DijkstraMeasure);
+/// assert_impl_all!(Wrapping<i16>: DijkstraMeasure);
+/// assert_impl_all!(Wrapping<i32>: DijkstraMeasure);
+/// assert_impl_all!(Wrapping<i64>: DijkstraMeasure);
+/// assert_impl_all!(Wrapping<i128>: DijkstraMeasure);
+///
+/// assert_impl_all!(Saturating<u8>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<u16>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<u32>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<u64>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<u128>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<usize>: DijkstraMeasure);
+///
+/// assert_impl_all!(Saturating<i8>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<i16>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<i32>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<i64>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<i128>: DijkstraMeasure);
+/// assert_impl_all!(Saturating<isize>: DijkstraMeasure);
+/// ```
+pub trait DijkstraMeasure: Clone + PartialOrd + Ord + AddRef<Self, Output = Self> + Zero {}
+
+impl<T> DijkstraMeasure for T where T: Clone + PartialOrd + Ord + AddRef<Self, Output = Self> + Zero {}
diff --git a/crates/algorithms/src/shortest_paths/dijkstra/mod.rs b/crates/algorithms/src/shortest_paths/dijkstra/mod.rs
index a785137..69732e4 100644
--- a/crates/algorithms/src/shortest_paths/dijkstra/mod.rs
+++ b/crates/algorithms/src/shortest_paths/dijkstra/mod.rs
@@ -1,13 +1,14 @@
+//! An implementation of Dijkstra's shortest path algorithm.
mod error;
mod iter;
+mod measure;
#[cfg(test)]
mod tests;
use alloc::vec::Vec;
-use core::{hash::Hash, marker::PhantomData, ops::Add};
+use core::{hash::Hash, marker::PhantomData};
use error_stack::Result;
-use num_traits::Zero;
use petgraph_core::{
edge::marker::{Directed, Undirected},
Direction,
@@ -16,20 +17,33 @@
DirectedGraphStorage, Graph, GraphDirectionality, GraphStorage, Node,
};
-pub(crate) use self::error::DijkstraError;
use self::iter::DijkstraIter;
-use super::common::{connections::outgoing_connections, transit::PredecessorMode};
-use crate::{
- polyfill::IteratorExt,
- shortest_paths::{
- common::{
- cost::{DefaultCost, GraphCost},
- route::{DirectRoute, Route},
- },
- ShortestDistance, ShortestPath,
+pub use self::{error::DijkstraError, measure::DijkstraMeasure};
+use super::{
+ common::{
+ connections::outgoing_connections,
+ cost::{DefaultCost, GraphCost},
+ route::{DirectRoute, Route},
+ transit::PredecessorMode,
},
+ ShortestDistance, ShortestPath,
};
+use crate::polyfill::IteratorExt;
+/// An implementation of Dijkstra's shortest path algorithm.
+///
+/// Dijkstra's algorithm is an algorithm for finding the shortest paths between a source node and
+/// all reachable nodes in a graph.
+/// A limitation of Dijkstra's algorithm is that it does not work for graphs with negative edge
+/// weights.
+///
+/// This implementation is generic over the directionality of the graph and the cost function.
+///
+/// Edge weights need to implement [`DijkstraMeasure`], a trait that is automatically implemented
+/// for all types that satisfy the constraints.
+///
+/// This implementation makes use of a binary heap, giving a time complexity of `O(|E| + |V| log
+/// |E|/|V| log |V|)`
pub struct Dijkstra<D, E>
where
D: GraphDirectionality,
@@ -40,6 +54,31 @@
}
impl Dijkstra<Directed, DefaultCost> {
+ /// Create a new instance of `Dijkstra` for a directed graph.
+ ///
+ /// If instantiated for a directed graph, [`Dijkstra`] will not implement [`ShortestPath`] and
+ /// [`ShortestDistance`] on undirected graphs.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use petgraph_algorithms::shortest_paths::{Dijkstra, ShortestPath};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// let algorithm = Dijkstra::directed();
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge(2, &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ ///
+ /// let path = algorithm.path_between(&graph, &b, &a);
+ /// assert!(path.is_none());
+ /// ```
#[must_use]
pub fn directed() -> Self {
Self {
@@ -50,6 +89,31 @@
}
impl Dijkstra<Undirected, DefaultCost> {
+ /// Create a new instance of `Dijkstra` for an undirected graph.
+ ///
+ /// If instantiated for an undirected graph, [`Dijkstra`] will treat a directed graph as an
+ /// undirected graph.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use petgraph_algorithms::shortest_paths::{Dijkstra, ShortestPath};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// let algorithm = Dijkstra::undirected();
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge(2, &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ ///
+ /// let path = algorithm.path_between(&graph, &b, &a);
+ /// assert!(path.is_some());
+ /// ```
#[must_use]
pub fn undirected() -> Self {
Self {
@@ -63,6 +127,41 @@
where
D: GraphDirectionality,
{
+ /// Set the cost function for the algorithm.
+ ///
+ /// By default the algorithm will use the edge weight as the cost, this function allows you to
+ /// override that behaviour,
+ /// transforming a previously unsupported graph weight into a supported one.
+ ///
+ /// For all supported functions see [`GraphCost`].
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use numi::borrow::Moo;
+ /// use petgraph_algorithms::shortest_paths::{Dijkstra, ShortestPath};
+ /// use petgraph_core::{Edge, GraphStorage};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// fn edge_cost<S>(edge: Edge<S>) -> Moo<usize>
+ /// where
+ /// S: GraphStorage,
+ /// S::EdgeWeight: AsRef<str>,
+ /// {
+ /// edge.weight().as_ref().len().into()
+ /// }
+ ///
+ /// let algorithm = Dijkstra::directed().with_edge_cost(edge_cost);
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge("AB", &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ /// ```
pub fn with_edge_cost<S, F>(self, edge_cost: F) -> Dijkstra<D, F>
where
S: GraphStorage,
@@ -80,8 +179,7 @@
S: GraphStorage,
S::NodeId: FlaggableGraphId<S> + AttributeGraphId<S>,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: DijkstraMeasure,
{
type Cost = E::Value;
type Error = DijkstraError;
@@ -123,13 +221,49 @@
}
}
+impl<S, E> ShortestPath<S> for Dijkstra<Directed, E>
+where
+ S: DirectedGraphStorage,
+ S::NodeId: Eq + Hash,
+ E: GraphCost<S>,
+ E::Value: DijkstraMeasure,
+{
+ type Cost = E::Value;
+ type Error = DijkstraError;
+
+ fn path_from<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ DijkstraIter::new(
+ graph,
+ &self.edge_cost,
+ outgoing_connections as fn(&Node<'graph, S>) -> _,
+ source,
+ PredecessorMode::Record,
+ )
+ }
+
+ fn every_path<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter = graph
+ .nodes()
+ .map(move |node| self.path_from(graph, node.id()))
+ .collect_reports::<Vec<_>>()?;
+
+ Ok(iter.into_iter().flatten())
+ }
+}
+
impl<S, E> ShortestDistance<S> for Dijkstra<Undirected, E>
where
S: GraphStorage,
S::NodeId: FlaggableGraphId<S> + AttributeGraphId<S>,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: DijkstraMeasure,
{
type Cost = E::Value;
type Error = DijkstraError;
@@ -173,51 +307,12 @@
}
}
-impl<S, E> ShortestPath<S> for Dijkstra<Directed, E>
-where
- S: DirectedGraphStorage,
- S::NodeId: FlaggableGraphId<S> + AttributeGraphId<S>,
- E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
-{
- type Cost = E::Value;
- type Error = DijkstraError;
-
- fn path_from<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph Graph<S>,
- source: &'graph S::NodeId,
- ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
- DijkstraIter::new(
- graph,
- &self.edge_cost,
- outgoing_connections as fn(&Node<'graph, S>) -> _,
- source,
- PredecessorMode::Record,
- )
- }
-
- fn every_path<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph Graph<S>,
- ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
- let iter = graph
- .nodes()
- .map(move |node| self.path_from(graph, node.id()))
- .collect_reports::<Vec<_>>()?;
-
- Ok(iter.into_iter().flatten())
- }
-}
-
impl<S, E> ShortestDistance<S> for Dijkstra<Directed, E>
where
S: DirectedGraphStorage,
S::NodeId: FlaggableGraphId<S> + AttributeGraphId<S>,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
+ E::Value: DijkstraMeasure,
{
type Cost = E::Value;
type Error = DijkstraError;
diff --git a/crates/algorithms/src/shortest_paths/dijkstra/tests.rs b/crates/algorithms/src/shortest_paths/dijkstra/tests.rs
index cc26098..0386ab0 100644
--- a/crates/algorithms/src/shortest_paths/dijkstra/tests.rs
+++ b/crates/algorithms/src/shortest_paths/dijkstra/tests.rs
@@ -1,6 +1,7 @@
use alloc::vec::Vec;
-use petgraph_core::{base::MaybeOwned, Edge, GraphStorage};
+use numi::borrow::Moo;
+use petgraph_core::{Edge, GraphStorage};
use petgraph_dino::{DiDinoGraph, EdgeId, NodeId};
use petgraph_utils::{graph, GraphCollection};
@@ -37,7 +38,7 @@
fn networkx_directed_expect_from(
nodes: &networkx::NodeCollection<NodeId>,
-) -> Vec<Expect<networkx::Graph, i32>> {
+) -> Vec<Expect<NodeId, i32>> {
expected!(nodes; [
a -()> a: 0,
a -(c)> b: 8,
@@ -69,7 +70,6 @@
);
// TODO: multigraph
-// TODO: more test cases
#[test]
fn path_from_directed_default_edge_cost() {
@@ -93,7 +93,7 @@
fn random_directed_expect_from(
nodes: &random::NodeCollection<NodeId>,
-) -> Vec<Expect<random::Graph, usize>> {
+) -> Vec<Expect<NodeId, usize>> {
expected!(nodes; [
a -()> a: 0,
a -()> b: 5,
@@ -104,12 +104,12 @@
])
}
-fn edge_cost<S>(edge: Edge<S>) -> MaybeOwned<'_, usize>
+fn edge_cost<S>(edge: Edge<S>) -> Moo<'_, usize>
where
S: GraphStorage,
S::EdgeWeight: AsRef<[u8]>,
{
- MaybeOwned::Owned(edge.weight().as_ref().len())
+ Moo::Owned(edge.weight().as_ref().len())
}
#[test]
@@ -134,7 +134,7 @@
fn networkx_undirected_expect_from(
nodes: &networkx::NodeCollection<NodeId>,
-) -> Vec<Expect<networkx::Graph, i32>> {
+) -> Vec<Expect<NodeId, i32>> {
expected!(nodes; [
a -()> a: 0,
a -(c)> b: 7,
@@ -166,7 +166,7 @@
fn random_undirected_expect_from(
nodes: &random::NodeCollection<NodeId>,
-) -> Vec<Expect<random::Graph, usize>> {
+) -> Vec<Expect<NodeId, usize>> {
expected!(nodes; [
a -()> a: 0,
a -()> b: 5,
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/impl.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/impl.rs
index 7977f68..3deb5a9 100644
--- a/crates/algorithms/src/shortest_paths/floyd_warshall/impl.rs
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/impl.rs
@@ -1,8 +1,11 @@
use alloc::vec::Vec;
use error_stack::{Report, Result};
-use num_traits::{CheckedAdd, Zero};
-use petgraph_core::{base::MaybeOwned, id::LinearGraphId, Graph, GraphStorage, Node};
+use numi::{
+ borrow::Moo,
+ num::{checked::CheckedAdd, identity::Zero},
+};
+use petgraph_core::{id::LinearGraphId, Graph, GraphStorage, Node};
use crate::shortest_paths::{
common::{
@@ -10,15 +13,15 @@
route::Route,
transit::PredecessorMode,
},
- floyd_warshall::{error::FloydWarshallError, matrix::SlotMatrix},
+ floyd_warshall::{error::FloydWarshallError, matrix::SlotMatrix, FloydWarshallMeasure},
Path,
};
-pub(super) fn init_directed_edge_distance<'graph, S, E>(
- matrix: &mut SlotMatrix<'graph, S, MaybeOwned<'graph, E::Value>>,
+pub(super) fn init_directed_edge_distance<'graph: 'this, 'this, S, E>(
+ matrix: &mut SlotMatrix<'graph, S, Moo<'this, E::Value>>,
u: &S::NodeId,
v: &S::NodeId,
- value: Option<MaybeOwned<'graph, E::Value>>,
+ value: Option<Moo<'this, E::Value>>,
) where
S: GraphStorage,
S::NodeId: LinearGraphId<S> + Clone,
@@ -28,11 +31,11 @@
matrix.set(u, v, value);
}
-pub(super) fn init_undirected_edge_distance<'graph, S, E>(
- matrix: &mut SlotMatrix<'graph, S, MaybeOwned<'graph, E::Value>>,
+pub(super) fn init_undirected_edge_distance<'graph: 'this, 'this, S, E>(
+ matrix: &mut SlotMatrix<'graph, S, Moo<'this, E::Value>>,
u: &S::NodeId,
v: &S::NodeId,
- value: Option<MaybeOwned<'graph, E::Value>>,
+ value: Option<Moo<'this, E::Value>>,
) where
S: GraphStorage,
S::NodeId: LinearGraphId<S> + Clone,
@@ -108,12 +111,11 @@
path.reverse();
path
}
-
-type InitEdgeDistanceFn<'graph, S, E> = fn(
- &mut SlotMatrix<'graph, S, MaybeOwned<'graph, <E as GraphCost<S>>::Value>>,
+type InitEdgeDistanceFn<'graph, 'this, S, E> = fn(
+ &mut SlotMatrix<'graph, S, Moo<'this, <E as GraphCost<S>>::Value>>,
&<S as GraphStorage>::NodeId,
&<S as GraphStorage>::NodeId,
- Option<MaybeOwned<'graph, <E as GraphCost<S>>::Value>>,
+ Option<Moo<'this, <E as GraphCost<S>>::Value>>,
);
type InitEdgePredecessorFn<'graph, S> = fn(
@@ -133,19 +135,20 @@
predecessor_mode: PredecessorMode,
- init_edge_distance: InitEdgeDistanceFn<'graph, S, E>,
+ init_edge_distance: InitEdgeDistanceFn<'graph, 'parent, S, E>,
init_edge_predecessor: InitEdgePredecessorFn<'graph, S>,
- distances: SlotMatrix<'graph, S, MaybeOwned<'graph, E::Value>>,
+ distances: SlotMatrix<'graph, S, Moo<'parent, E::Value>>,
predecessors: SlotMatrix<'graph, S, &'graph S::NodeId>,
}
+// TODO: relax `NodeId` requirements or make `Send + Sync + 'static` across the board
impl<'graph: 'parent, 'parent, S, E> FloydWarshallImpl<'graph, 'parent, S, E>
where
S: GraphStorage,
S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
E: GraphCost<S>,
- E::Value: PartialOrd + CheckedAdd + Zero + Clone + 'graph,
+ E::Value: FloydWarshallMeasure,
{
pub(super) fn new(
graph: &'graph Graph<S>,
@@ -154,7 +157,7 @@
predecessor_mode: PredecessorMode,
- init_edge_distance: InitEdgeDistanceFn<'graph, S, E>,
+ init_edge_distance: InitEdgeDistanceFn<'graph, 'parent, S, E>,
init_edge_predecessor: InitEdgePredecessorFn<'graph, S>,
) -> Result<Self, FloydWarshallError> {
let distances = SlotMatrix::new(graph);
@@ -199,11 +202,8 @@
}
for node in self.graph.nodes() {
- self.distances.set(
- node.id(),
- node.id(),
- Some(MaybeOwned::Owned(E::Value::zero())),
- );
+ self.distances
+ .set(node.id(), node.id(), Some(Moo::Owned(E::Value::zero())));
if self.predecessor_mode == PredecessorMode::Record {
self.predecessors.set(node.id(), node.id(), Some(node.id()));
@@ -241,8 +241,9 @@
}
}
- self.distances
- .set(i, j, Some(MaybeOwned::Owned(alternative)));
+ // TODO: check for diagonal here and apply suggestion from paper
+
+ self.distances.set(i, j, Some(Moo::Owned(alternative)));
if self.predecessor_mode == PredecessorMode::Record {
let predecessor = self.predecessors.get(k, j).copied();
@@ -263,7 +264,7 @@
let mut result: Result<(), FloydWarshallError> = Ok(());
for index in negative_cycles {
- let Some(node) = self.distances.resolve(index).map(MaybeOwned::into_owned) else {
+ let Some(node) = self.distances.resolve(index).map(Moo::into_owned) else {
continue;
};
@@ -278,7 +279,7 @@
pub(super) fn filter(
self,
- mut filter: impl FnMut(Node<S>, Node<S>) -> bool + 'parent,
+ mut filter: impl FnMut(Node<'graph, S>, Node<'graph, S>) -> bool + 'parent,
) -> impl Iterator<Item = Route<'graph, S, E::Value>> + 'parent {
let Self {
graph,
@@ -314,4 +315,37 @@
Route::new(Path::new(source, transit, target), Cost::new(cost))
})
}
+
+ pub(super) fn pick(
+ self,
+ source: &S::NodeId,
+ target: &S::NodeId,
+ ) -> Option<Route<'graph, S, E::Value>> {
+ let Self {
+ graph,
+ distances,
+ predecessors,
+ predecessor_mode,
+ ..
+ } = self;
+
+ let source_node = graph.node(source)?;
+ let target_node = graph.node(target)?;
+
+ let cost = distances.get(source, target)?;
+ let transit = match predecessor_mode {
+ PredecessorMode::Discard => Vec::new(),
+ PredecessorMode::Record => reconstruct_path(&predecessors, source, target)
+ .into_iter()
+ .filter_map(|id| graph.node(id))
+ .collect(),
+ };
+
+ let cost = Cost::new(cost.clone().into_owned());
+
+ Some(Route::new(
+ Path::new(source_node, transit, target_node),
+ cost,
+ ))
+ }
}
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/matrix.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/matrix.rs
index e814326..83e86e7 100644
--- a/crates/algorithms/src/shortest_paths/floyd_warshall/matrix.rs
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/matrix.rs
@@ -1,7 +1,7 @@
use alloc::vec::Vec;
+use numi::borrow::Moo;
use petgraph_core::{
- base::MaybeOwned,
id::{IndexMapper, LinearGraphId},
Graph, GraphStorage,
};
@@ -30,7 +30,7 @@
}
}
- fn reverse(&self, to: usize) -> Option<MaybeOwned<T>> {
+ fn reverse(&self, to: usize) -> Option<Moo<T>> {
match self {
Self::Store(mapper) => mapper.reverse(to),
Self::Discard => None,
@@ -38,22 +38,22 @@
}
}
-pub(super) struct SlotMatrix<'a, S, T>
+pub(super) struct SlotMatrix<'graph, S, T>
where
- S: GraphStorage + 'a,
+ S: GraphStorage + 'graph,
S::NodeId: LinearGraphId<S>,
{
- mapper: MatrixIndexMapper<<S::NodeId as LinearGraphId<S>>::Mapper<'a>>,
+ mapper: MatrixIndexMapper<<S::NodeId as LinearGraphId<S>>::Mapper<'graph>>,
matrix: Vec<Option<T>>,
length: usize,
}
-impl<'a, S, T> SlotMatrix<'a, S, T>
+impl<'graph, S, T> SlotMatrix<'graph, S, T>
where
S: GraphStorage,
S::NodeId: LinearGraphId<S>,
{
- pub(crate) fn new(graph: &'a Graph<S>) -> Self {
+ pub(crate) fn new(graph: &'graph Graph<S>) -> Self {
let length = graph.num_nodes();
let mapper = MatrixIndexMapper::Store(<S::NodeId as LinearGraphId<S>>::index_mapper(
graph.storage(),
@@ -113,7 +113,7 @@
self.matrix[source * self.length + target].as_ref()
}
- pub(crate) fn resolve(&self, index: usize) -> Option<MaybeOwned<S::NodeId>> {
+ pub(crate) fn resolve(&self, index: usize) -> Option<Moo<S::NodeId>> {
self.mapper.reverse(index)
}
}
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/measure.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/measure.rs
new file mode 100644
index 0000000..cfd67be
--- /dev/null
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/measure.rs
@@ -0,0 +1,54 @@
+use numi::num::{checked::CheckedAdd, identity::Zero};
+
+/// A trait for types that can be used as edge weights in the Floyd-Warshall algorithm.
+///
+/// This trait is automatically implemented for all types that implement the supertraits mentioned
+/// in the trait definition.
+///
+/// These traits either originate from [`core`] or [`numi`].
+///
+/// # Note on floating point types
+///
+/// This trait is not implemented for both [`f32`] and [`f64`], because they do not implement
+/// [`CheckedAdd`] which is required for the Floyd-Warshall algorithm.
+/// You can instead use [`ordered_float::NotNan`], which is a wrapper type that implements
+/// [`CheckedAdd`].
+///
+/// The reason that [`f32`] and [`f64`] do not implement [`CheckedAdd`] is because they do not
+/// have the concept of an overflow, which is required for [`CheckedAdd`].
+/// If a value gets to large it will instead become [`f32::INFINITY`] or [`f64::INFINITY`].
+///
+/// # Example
+///
+/// ```rust
+/// use core::num::Wrapping;
+/// use core::num::Saturating;
+/// use ordered_float::NotNan;
+/// use petgraph_algorithms::shortest_paths::floyd_warshall::FloydWarshallMeasure;
+/// use static_assertions::assert_impl_all;
+///
+/// // Some examples of types that implement DijkstraMeasure
+/// assert_impl_all!(u8: FloydWarshallMeasure);
+/// assert_impl_all!(u16: FloydWarshallMeasure);
+/// assert_impl_all!(u32: FloydWarshallMeasure);
+/// assert_impl_all!(u64: FloydWarshallMeasure);
+/// assert_impl_all!(u128: FloydWarshallMeasure);
+/// assert_impl_all!(usize: FloydWarshallMeasure);
+///
+/// assert_impl_all!(i8: FloydWarshallMeasure);
+/// assert_impl_all!(i16: FloydWarshallMeasure);
+/// assert_impl_all!(i32: FloydWarshallMeasure);
+/// assert_impl_all!(i64: FloydWarshallMeasure);
+/// assert_impl_all!(i128: FloydWarshallMeasure);
+/// assert_impl_all!(isize: FloydWarshallMeasure);
+///
+/// // f32 and f64 are not implemented because they do not implement CheckedAdd
+/// // use `ordered_float::NotNan` instead.
+/// assert_impl_all!(NotNan<f32>: FloydWarshallMeasure);
+/// assert_impl_all!(NotNan<f64>: FloydWarshallMeasure);
+///
+/// // see `CheckedAdd` why `Wrapping<T>` and `Saturating<T>` are not implemented
+/// ```
+pub trait FloydWarshallMeasure: Clone + PartialOrd + CheckedAdd + Zero {}
+
+impl<T> FloydWarshallMeasure for T where T: Clone + PartialOrd + CheckedAdd + Zero {}
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/mod.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/mod.rs
index df0d64f..36f662e 100644
--- a/crates/algorithms/src/shortest_paths/floyd_warshall/mod.rs
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/mod.rs
@@ -1,35 +1,54 @@
+//! An implementation of the Floyd-Warshall shortest path algorithm.
mod error;
mod r#impl;
mod matrix;
+mod measure;
#[cfg(test)]
mod tests;
use core::marker::PhantomData;
use error_stack::Result;
-use num_traits::{CheckedAdd, Zero};
use petgraph_core::{
edge::marker::{Directed, Undirected},
id::LinearGraphId,
Graph, GraphStorage,
};
-use crate::shortest_paths::{
+use self::r#impl::{
+ init_directed_edge_distance, init_directed_edge_predecessor, init_undirected_edge_distance,
+ init_undirected_edge_predecessor, FloydWarshallImpl,
+};
+pub use self::{error::FloydWarshallError, measure::FloydWarshallMeasure};
+use super::{
common::{
cost::{DefaultCost, GraphCost},
route::{DirectRoute, Route},
transit::PredecessorMode,
},
- floyd_warshall::{
- error::FloydWarshallError,
- r#impl::{
- init_directed_edge_distance, init_directed_edge_predecessor,
- init_undirected_edge_distance, init_undirected_edge_predecessor, FloydWarshallImpl,
- },
- },
- ShortestDistance, ShortestPath,
+ Cost, ShortestDistance, ShortestPath,
};
+/// An implementation of the Floyd-Warshall shortest path algorithm.
+///
+/// The Floyd-Warshall algorithm is an algorithm for finding shortest paths in a weighted graph with
+/// positive or negative edge weights (but with no negative cycles).
+/// A single execution of the algorithm will find the lengths (summed weights) of the shortest paths
+/// between all pairs of vertices, though it does not return details of the paths themselves.
+///
+/// The implementation chosen overcomes this limitation by storing the predecessor of each node in
+/// an associated matrix.
+///
+/// The algorithm is implemented for both directed and undirected graphs (undirected graphs are
+/// simply treated as directed graphs with the same edge in both directions).
+///
+/// Edge weights need to implement [`FloydWarshallMeasure`], a trait that is automatically
+/// implemented for all types that satisfy the constraints. The graph id for edges need to be
+/// linear, refer to the documentation of the graph type used for further information if graph ids
+/// implement [`LinearGraphId`].
+///
+/// The time complexity of the algorithm is `O(|V|^3)`, where `|V|` is the number of nodes in the
+/// graph.
pub struct FloydWarshall<D, E> {
edge_cost: E,
@@ -37,6 +56,31 @@
}
impl FloydWarshall<Directed, DefaultCost> {
+ /// Creates a new instance of the Floyd-Warshall shortest path algorithm for directed graphs.
+ ///
+ /// If instantiated for a directed graph, the algorithm will not implement the [`ShortestPath`]
+ /// and [`ShortestDistance`] traits for undirected graphs.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use petgraph_algorithms::shortest_paths::{FloydWarshall, ShortestPath};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// let algorithm = FloydWarshall::directed();
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge(7, &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ ///
+ /// let path = algorithm.path_between(&graph, &b, &a);
+ /// assert!(path.is_none());
+ /// ```
#[must_use]
pub fn directed() -> Self {
Self {
@@ -47,6 +91,30 @@
}
impl FloydWarshall<Undirected, DefaultCost> {
+ /// Creates a new instance of the Floyd-Warshall shortest path algorithm for undirected graphs.
+ ///
+ /// If used on a directed graph, the algorithm will treat the graph as undirected.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use petgraph_algorithms::shortest_paths::{FloydWarshall, ShortestPath};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// let algorithm = FloydWarshall::undirected();
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge(7, &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ ///
+ /// let path = algorithm.path_between(&graph, &b, &a);
+ /// assert!(path.is_some());
+ /// ```
#[must_use]
pub fn undirected() -> Self {
Self {
@@ -57,6 +125,42 @@
}
impl<D, E> FloydWarshall<D, E> {
+ /// Sets the cost function to use for the algorithm.
+ ///
+ /// By default the algorithm will use the edge weight as cost, this enables the use of a custom
+ /// edge cost function, which may transform the edge weight, which is initially incompatible
+ /// with the [`FloydWarshall`] implementation into one that is.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use numi::borrow::Moo;
+ /// use petgraph_algorithms::shortest_paths::{FloydWarshall, ShortestPath};
+ /// use petgraph_core::{Edge, GraphStorage};
+ /// use petgraph_dino::DiDinoGraph;
+ ///
+ /// fn edge_cost<S>(edge: Edge<S>) -> Moo<usize>
+ /// where
+ /// S: GraphStorage,
+ /// S::EdgeWeight: AsRef<str>,
+ /// {
+ /// edge.weight().as_ref().len().into()
+ /// }
+ ///
+ /// let algorithm = FloydWarshall::directed().with_edge_cost(edge_cost);
+ ///
+ /// let mut graph = DiDinoGraph::new();
+ /// let a = *graph.insert_node("A").id();
+ /// let b = *graph.insert_node("B").id();
+ ///
+ /// graph.insert_edge("AB", &a, &b);
+ ///
+ /// let path = algorithm.path_between(&graph, &a, &b);
+ /// assert!(path.is_some());
+ ///
+ /// let path = algorithm.path_between(&graph, &b, &a);
+ /// assert!(path.is_none());
+ /// ```
pub fn with_edge_cost<S, F>(self, edge_cost: F) -> FloydWarshall<D, F>
where
S: GraphStorage,
@@ -74,7 +178,7 @@
S: GraphStorage,
S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + CheckedAdd + Zero + Clone + 'a,
+ E::Value: FloydWarshallMeasure,
{
type Cost = E::Value;
type Error = FloydWarshallError;
@@ -124,11 +228,7 @@
)
.ok()?;
- r#impl
- .filter(|source_node, target_node| {
- source_node.id() == source && target_node.id() == target
- })
- .next()
+ r#impl.pick(source, target)
}
fn every_path<'graph: 'this, 'this>(
@@ -142,7 +242,7 @@
init_undirected_edge_distance::<S, E>,
init_undirected_edge_predecessor::<S>,
)
- .map(|r#impl| r#impl.filter(|_, _| true))
+ .map(move |r#impl| r#impl.filter(|_, _| true))
}
}
@@ -151,11 +251,65 @@
S: GraphStorage,
S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + CheckedAdd + Zero + Clone + 'a,
+ E::Value: FloydWarshallMeasure,
{
type Cost = E::Value;
type Error = FloydWarshallError;
+ fn distance_to<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ target: &'graph S::NodeId,
+ ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter = FloydWarshallImpl::new(
+ graph,
+ &self.edge_cost,
+ PredecessorMode::Discard,
+ init_undirected_edge_distance::<S, E>,
+ init_undirected_edge_predecessor::<S>,
+ )?;
+
+ Ok(iter
+ .filter(move |_, target_node| target_node.id() == target)
+ .map(From::from))
+ }
+
+ fn distance_from<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter = FloydWarshallImpl::new(
+ graph,
+ &self.edge_cost,
+ PredecessorMode::Discard,
+ init_undirected_edge_distance::<S, E>,
+ init_undirected_edge_predecessor::<S>,
+ )?;
+
+ Ok(iter
+ .filter(move |source_node, _| source_node.id() == source)
+ .map(From::from))
+ }
+
+ fn distance_between<'graph>(
+ &self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ target: &'graph S::NodeId,
+ ) -> Option<Cost<Self::Cost>> {
+ let iter = FloydWarshallImpl::new(
+ graph,
+ &self.edge_cost,
+ PredecessorMode::Discard,
+ init_undirected_edge_distance::<S, E>,
+ init_undirected_edge_predecessor::<S>,
+ )
+ .ok()?;
+
+ iter.pick(source, target).map(Route::into_cost)
+ }
+
fn every_distance<'graph: 'this, 'this>(
&'this self,
graph: &'graph Graph<S>,
@@ -177,7 +331,7 @@
S: GraphStorage,
S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + CheckedAdd + Zero + Clone + 'a,
+ E::Value: FloydWarshallMeasure,
{
type Cost = E::Value;
type Error = FloydWarshallError;
@@ -228,11 +382,7 @@
)
.ok()?;
- r#impl
- .filter(|source_node, target_node| {
- source_node.id() == source && target_node.id() == target
- })
- .next()
+ r#impl.pick(source, target)
}
fn every_path<'graph: 'this, 'this>(
@@ -246,7 +396,7 @@
init_directed_edge_distance::<S, E>,
init_directed_edge_predecessor::<S>,
)
- .map(|r#impl| r#impl.filter(|_, _| true))
+ .map(move |r#impl| r#impl.filter(|_, _| true))
}
}
@@ -255,11 +405,65 @@
S: GraphStorage,
S::NodeId: LinearGraphId<S> + Clone + Send + Sync + 'static,
E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + CheckedAdd + Zero + Clone + 'a,
+ E::Value: FloydWarshallMeasure,
{
type Cost = E::Value;
type Error = FloydWarshallError;
+ fn distance_to<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ target: &'graph S::NodeId,
+ ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter = FloydWarshallImpl::new(
+ graph,
+ &self.edge_cost,
+ PredecessorMode::Discard,
+ init_directed_edge_distance::<S, E>,
+ init_directed_edge_predecessor::<S>,
+ )?;
+
+ Ok(iter
+ .filter(move |_, target_node| target_node.id() == target)
+ .map(From::from))
+ }
+
+ fn distance_from<'graph: 'this, 'this>(
+ &'this self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
+ let iter = FloydWarshallImpl::new(
+ graph,
+ &self.edge_cost,
+ PredecessorMode::Discard,
+ init_directed_edge_distance::<S, E>,
+ init_directed_edge_predecessor::<S>,
+ )?;
+
+ Ok(iter
+ .filter(move |source_node, _| source_node.id() == source)
+ .map(From::from))
+ }
+
+ fn distance_between<'graph>(
+ &self,
+ graph: &'graph Graph<S>,
+ source: &'graph S::NodeId,
+ target: &'graph S::NodeId,
+ ) -> Option<Cost<Self::Cost>> {
+ let iter = FloydWarshallImpl::new(
+ graph,
+ &self.edge_cost,
+ PredecessorMode::Discard,
+ init_directed_edge_distance::<S, E>,
+ init_directed_edge_predecessor::<S>,
+ )
+ .ok()?;
+
+ iter.pick(source, target).map(Route::into_cost)
+ }
+
fn every_distance<'graph: 'this, 'this>(
&'this self,
graph: &'graph Graph<S>,
diff --git a/crates/algorithms/src/shortest_paths/floyd_warshall/tests.rs b/crates/algorithms/src/shortest_paths/floyd_warshall/tests.rs
index a41d3f9..c1d970b 100644
--- a/crates/algorithms/src/shortest_paths/floyd_warshall/tests.rs
+++ b/crates/algorithms/src/shortest_paths/floyd_warshall/tests.rs
@@ -1,4 +1,4 @@
-use alloc::vec::Vec;
+use alloc::{vec, vec::Vec};
use hashbrown::HashSet;
use petgraph_dino::{DiDinoGraph, EdgeId, NodeId};
@@ -42,7 +42,7 @@
] as EdgeId
);
-fn uniform_expect(nodes: &uniform::NodeCollection<NodeId>) -> Vec<Expect<uniform::Graph, usize>> {
+fn uniform_expect(nodes: &uniform::NodeCollection<NodeId>) -> Vec<Expect<NodeId, usize>> {
expected!(nodes; [
a -()> a: 0,
a -()> b: 1,
@@ -149,7 +149,7 @@
fn directed_weighted_expect(
nodes: &weighted::NodeCollection<NodeId>,
-) -> Vec<Expect<weighted::Graph, usize>> {
+) -> Vec<Expect<NodeId, usize>> {
expected!(nodes; [
a -()> a: 0,
a -()> b: 1,
@@ -244,7 +244,7 @@
fn undirected_weighted_expect(
nodes: &weighted::NodeCollection<NodeId>,
-) -> Vec<Expect<weighted::Graph, usize>> {
+) -> Vec<Expect<NodeId, usize>> {
expected!(nodes; [
a -()> a: 0,
a -()> b: 1,
diff --git a/crates/algorithms/src/shortest_paths/mod.rs b/crates/algorithms/src/shortest_paths/mod.rs
index 8652104..fb51d08 100644
--- a/crates/algorithms/src/shortest_paths/mod.rs
+++ b/crates/algorithms/src/shortest_paths/mod.rs
@@ -1,15 +1,69 @@
-// mod astar;
+//! # Shortest Path Module
+//!
+//! This module contains traits and implementations for shortest path and shortest distance
+//! algorithms.
+//! These algorithms are used to find the shortest path or distance between two nodes in a graph.
+//!
+//! ## Traits
+//!
+//! - [`ShortestPath`]: Any implementation of this trait can be used to find the shortest path,
+//! depending on algorithm some restrictions may apply.
+//! - [`ShortestDistance`]: Any implementation of this trait can be used to find the shortest
+//! distance, depending on algorithm some restrictions may apply.
+//!
+//! ## Implementations
+//!
+//! These are the algorithms implemented in `petgraph` itself, companion crates may implement other
+//! algorithms.
+//!
+//! - [`AStar`]: An implementation of the A* shortest path algorithm.
+//! - [`BellmanFord`]: An implementation of the Bellman-Ford shortest path algorithm.
+//! - [`Dijkstra`]: An implementation of the Dijkstra's shortest path algorithm.
+//! - [`FloydWarshall`]: An implementation of the Floyd-Warshall shortest path algorithm.
+//!
+//! ## Usage
+//!
+//! Each algorithm provides methods to find the shortest path or distance from a source node to a
+//! target node, from a source node to all other nodes, and between all pairs of nodes.
+//!
+//! The [`ShortestPath`] trait provides methods to find paths, while the [`ShortestDistance`] trait
+//! provides methods to find distances.
+//! The difference between a path and a distance is that a path includes the sequence of nodes and
+//! edges, while a distance is just the sum of the weights of the edges.
+//!
+//! ## Example
+//!
+//! ```rust
+//! use petgraph_algorithms::shortest_paths::{Dijkstra, ShortestPath};
+//! use petgraph_dino::DiDinoGraph;
+//!
+//! let mut graph = DiDinoGraph::new();
+//! let a = *graph.insert_node("A").id();
+//! let b = *graph.insert_node("B").id();
+//! graph.insert_edge(7, &a, &b);
+//!
+//! let dijkstra = Dijkstra::directed();
+//! let path = dijkstra.path_between(&graph, &a, &b);
+//! assert!(path.is_some());
+//!
+//! let path = dijkstra.path_between(&graph, &b, &a);
+//! assert!(path.is_none());
+//! ```
+
+pub mod astar;
+pub mod bellman_ford;
mod common;
-mod dijkstra;
-mod floyd_warshall;
+pub mod dijkstra;
+pub mod floyd_warshall;
use error_stack::{Context, Result};
use petgraph_core::{Graph, GraphStorage};
pub use self::{
// astar::AStar,
+ bellman_ford::BellmanFord,
common::{
- cost::Cost,
+ cost::{Cost, GraphCost},
path::Path,
route::{DirectRoute, Route},
},
@@ -17,13 +71,60 @@
floyd_warshall::FloydWarshall,
};
+/// # Shortest Path
+///
+/// A shortest path algorithm is an algorithm that finds a path between two nodes in a graph such
+/// that the sum of the weights of its constituent edges is minimized.
+///
+/// Different algorithms exist to solve this problem, each with its own trade-offs, refer to the
+/// different algorithms for more information.
+///
+/// ## Shortest Path vs Shortest Distance
+///
+/// The shortest path is the path between two nodes that has the lowest cost.
+/// The shortest distance is the cost of the shortest path.
+/// You should prefer shortest distance algorithms over shortest path algorithms if you are only
+/// interested in the cost of the shortest path.
+///
+/// # Example
+///
+/// ```rust
+/// use petgraph_algorithms::shortest_paths::{Dijkstra, ShortestPath};
+/// use petgraph_dino::DiDinoGraph;
+///
+/// let mut graph = DiDinoGraph::new();
+/// let a = *graph.insert_node("A").id();
+/// let b = *graph.insert_node("B").id();
+/// let c = *graph.insert_node("C").id();
+///
+/// graph.insert_edge(7, &a, &b);
+/// graph.insert_edge(5, &b, &c);
+/// graph.insert_edge(3, &a, &c);
+///
+/// let dijkstra = Dijkstra::directed();
+/// let path = dijkstra.path_between(&graph, &a, &c).expect("path exists");
+///
+/// assert_eq!(path.cost().into_value(), 3);
+///
+/// assert_eq!(path.path().source().id(), &a);
+/// assert_eq!(path.path().target().id(), &c);
+///
+/// assert!(path.path().transit().is_empty());
+/// ```
pub trait ShortestPath<S>
where
S: GraphStorage,
{
+ /// The cost of the shortest path.
type Cost;
+
+ /// The error that can occur when computing the shortest path.
type Error: Context;
+ /// Returns the shortest path from the source to the target.
+ ///
+ /// Returns an iterator over all routes in the graph that end at the given target node.
+ // TODO: example?
fn path_to<'graph: 'this, 'this>(
&'this self,
graph: &'graph Graph<S>,
@@ -34,6 +135,11 @@
Ok(iter.filter(move |route| route.path().target().id() == target))
}
+ /// Returns the shortest path from the source to the target.
+ ///
+ /// This is also known as `SSSP` (Single Source Shortest Path) problem.
+ ///
+ /// Returns an iterator over all routes in the graph that start at the given source node.
fn path_from<'graph: 'this, 'this>(
&'this self,
graph: &'graph Graph<S>,
@@ -44,6 +150,9 @@
Ok(iter.filter(move |route| route.path().source().id() == source))
}
+ /// Returns the shortest path from the source to the target.
+ ///
+ /// This will return [`None`] if no path exists between the source and the target.
fn path_between<'graph>(
&self,
graph: &'graph Graph<S>,
@@ -55,19 +164,46 @@
.find(|route| route.path().target().id() == target)
}
+ /// Returns an iterator over all shortest paths in the graph.
+ ///
+ /// This is also known as `APSP` (All Pairs Shortest Path) problem.
+ ///
+ /// Returns an iterator over all routes in the graph.
fn every_path<'graph: 'this, 'this>(
&'this self,
graph: &'graph Graph<S>,
) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error>;
}
+/// # Shortest Distance
+///
+/// A shortest distance algorithm is an algorithm that finds the distance between two nodes in a
+/// graph such that the sum of the weights of its constituent edges is minimized.
+///
+/// Different algorithms exist to solve this problem, each with its own trade-offs, refer to the
+/// different algorithms for more information.
+///
+/// ## Shortest Path vs Shortest Distance
+///
+/// The shortest path is the path between two nodes that has the lowest cost.
+/// The shortest distance is the cost of the shortest path.
+/// You should prefer shortest distance algorithms over shortest path algorithms if you are only
+/// interested in the cost of the shortest path.
+// TODO: example?
pub trait ShortestDistance<S>
where
S: GraphStorage,
{
+ /// The cost of the shortest path.
type Cost;
+
+ /// The error that can occur when computing the shortest path.
type Error: Context;
+ /// Returns the shortest distance from the source to the target.
+ ///
+ /// Returns an iterator over all direct routes in the graph that end at the given target node.
+ /// A [`DirectRoute`] does not contain the nodes traversed, only the source and target nodes.
fn distance_to<'graph: 'this, 'this>(
&'this self,
graph: &'graph Graph<S>,
@@ -77,6 +213,13 @@
Ok(iter.filter(move |route| route.target().id() == target))
}
+
+ /// Returns the shortest distance from the source to the target.
+ ///
+ /// This is also known as `SSSP` (Single Source Shortest Path) problem.
+ ///
+ /// Returns an iterator over all direct routes in the graph that start at the given source node.
+ /// A [`DirectRoute`] does not contain the nodes traversed, only the source and target nodes.
fn distance_from<'graph: 'this, 'this>(
&'this self,
graph: &'graph Graph<S>,
@@ -86,6 +229,10 @@
Ok(iter.filter(move |route| route.source().id() == source))
}
+
+ /// Returns the shortest distance from the source to the target.
+ ///
+ /// This will return [`None`] if no path exists between the source and the target.
fn distance_between<'graph>(
&self,
graph: &'graph Graph<S>,
@@ -97,6 +244,13 @@
.find(move |route| route.source().id() == source && route.target().id() == target)
.map(|route| route.into_cost())
}
+
+ /// Returns an iterator over all shortest distances in the graph.
+ ///
+ /// This is also known as `APSP` (All Pairs Shortest Path) problem.
+ ///
+ /// Returns an iterator over all direct routes in the graph.
+ /// A [`DirectRoute`] does not contain the nodes traversed, only the source and target nodes.
fn every_distance<'graph: 'this, 'this>(
&'this self,
graph: &'graph Graph<S>,
diff --git a/crates/algorithms/src/shortest_paths/shortest_path_faster/error.rs b/crates/algorithms/src/shortest_paths/shortest_path_faster/error.rs
deleted file mode 100644
index b35f836..0000000
--- a/crates/algorithms/src/shortest_paths/shortest_path_faster/error.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use core::fmt::{Display, Formatter};
-
-use error_stack::Context;
-
-#[derive(Debug)]
-pub enum ShortestPathFasterError {
- NodeNotFound,
-}
-
-impl Display for ShortestPathFasterError {
- fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
- match self {
- Self::NodeNotFound => write!(f, "node not found"),
- }
- }
-}
-
-impl Context for ShortestPathFasterError {}
diff --git a/crates/algorithms/src/shortest_paths/shortest_path_faster/iter.rs b/crates/algorithms/src/shortest_paths/shortest_path_faster/iter.rs
deleted file mode 100644
index f6c8e8d..0000000
--- a/crates/algorithms/src/shortest_paths/shortest_path_faster/iter.rs
+++ /dev/null
@@ -1,212 +0,0 @@
-use core::{hash::Hash, ops::Add};
-
-use error_stack::{Report, Result};
-use fxhash::FxBuildHasher;
-use hashbrown::{HashMap, HashSet};
-use num_traits::{Bounded, Zero};
-use petgraph_core::{Graph, GraphStorage, Node};
-
-use super::error::ShortestPathFasterError;
-use crate::shortest_paths::{
- common::{
- connections::Connections, cost::GraphCost, double_ended_queue::DoubleEndedQueue,
- intermediates::reconstruct_intermediates,
- },
- Cost, Path, Route,
-};
-
-pub(super) struct ShortestPathFasterIter<'graph: 'parent, 'parent, S, E, G>
-where
- S: GraphStorage,
- E: GraphCost<S>,
- E::Value: Ord,
-{
- graph: &'graph Graph<S>,
-
- queue: DoubleEndedQueue<&'graph S::NodeId>,
-
- edge_cost: &'parent E,
- connections: G,
-
- source: Node<'graph, S>,
-
- num_nodes: usize,
-
- iteration: usize,
- next: Option<&'graph S::NodeId>,
-
- // candidate_order: SPFACandidateOrder,
- distances: HashMap<&'graph S::NodeId, E::Value, FxBuildHasher>,
- predecessors: HashMap<&'graph S::NodeId, Option<Node<'graph, S>>, FxBuildHasher>,
- in_queue: HashSet<&'graph S::NodeId, FxBuildHasher>,
-}
-
-impl<'graph: 'parent, 'parent, S, E, G> ShortestPathFasterIter<'graph, 'parent, S, E, G>
-where
- S: GraphStorage,
- S::NodeId: PartialEq + Eq + Hash,
- E: GraphCost<S>,
- E::Value: PartialOrd + Ord + Zero + Bounded + Clone + 'graph,
- for<'a> &'a E::Value: Add<Output = E::Value>,
- G: Connections<'graph, S>,
-{
- pub(super) fn new(
- graph: &'graph Graph<S>,
-
- edge_cost: &'parent E,
- connections: G,
-
- source: &'graph S::NodeId,
- // candidate_order: SPFACandidateOrder,
- ) -> Result<Self, ShortestPathFasterError> {
- let source_node = graph
- .node(source)
- .ok_or_else(|| Report::new(ShortestPathFasterError::NodeNotFound))?;
-
- let mut queue = DoubleEndedQueue::new();
- queue.push_back(source);
-
- let mut distances = HashMap::with_hasher(FxBuildHasher::default());
- distances.insert(source, E::Value::zero());
-
- let mut predecessors = HashMap::with_hasher(FxBuildHasher::default());
- predecessors.insert(source, None);
-
- let in_queue = HashSet::with_hasher(FxBuildHasher::default());
-
- Ok(Self {
- graph,
- queue,
- edge_cost,
- connections,
- source: source_node,
- num_nodes: graph.num_nodes(),
- iteration: 0,
- next: Some(source),
- // candidate_order,
- distances,
- predecessors,
- in_queue,
- })
- }
-
- fn has_cycle(&self) -> bool {
- let mut visited = HashSet::with_hasher(FxBuildHasher::default());
- let mut on_stack = HashSet::with_hasher(FxBuildHasher::default());
- let mut stack = Vec::new();
-
- for node in self.predecessors.keys() {
- if !visited.contains(*node) {
- while let Some(Some(pre)) = self.predecessors.get(*node) {
- let predecessor_id = pre.id();
- if !visited.contains(predecessor_id) {
- visited.insert(predecessor_id);
- on_stack.insert(predecessor_id);
- stack.push(predecessor_id);
- } else {
- if on_stack.contains(predecessor_id) {
- return true;
- }
- break;
- }
- }
-
- while let Some(p) = stack.pop() {
- on_stack.remove(p);
- }
- }
- }
- false
- }
-}
-
-impl<'graph: 'parent, 'parent, S, E, G> Iterator
- for ShortestPathFasterIter<'graph, 'parent, S, E, G>
-where
- S: GraphStorage,
- S::NodeId: PartialEq + Eq + Hash,
- E: GraphCost<S>,
- E::Value: PartialOrd + Ord + Zero + Bounded + Clone + 'graph,
- for<'a> &'a E::Value: Add<Output = E::Value>,
- G: Connections<'graph, S>,
-{
- type Item = Route<'graph, S, E::Value>;
-
- // The concrete implementation is the SPFA (Shortest Path Faster Algorithm) algorithm, which is
- // a variant of Bellman-Ford that uses a queue to avoid unnecessary relaxation.
- // https://en.wikipedia.org/wiki/Shortest_path_faster_algorithm
- // We've made use of optimization techniques for candidate order
- // as well as a variation to terminate on negative cycles.
- // https://konaeakira.github.io/posts/using-the-shortest-path-faster-algorithm-to-find-negative-cycles.html
- fn next(&mut self) -> Option<Self::Item> {
- let default_distance = E::Value::max_value();
-
- let node_id = self.next?;
- let node = self.graph.node(&node_id).expect("node to be present");
- let connections = self.connections.connections(&node);
-
- for edge in connections {
- let (u, v) = edge.endpoints();
- let target = if v.id() == node_id { u.id() } else { v.id() };
-
- let next_distance_cost = &self.distances[&node_id] + self.edge_cost.cost(edge).as_ref();
-
- let distance = self.distances.get(target).unwrap_or(&default_distance);
-
- if next_distance_cost >= *distance {
- continue;
- }
-
- self.distances.insert(target, next_distance_cost);
- self.predecessors.insert(
- target,
- Some(self.graph.node(node_id).expect("node to exist")),
- );
-
- self.iteration += 1;
-
- if self.iteration == self.num_nodes {
- self.iteration = 0;
- if self.has_cycle() {
- // A shortest path can at most go to n nodes, therefore we
- // terminate early if we detected a cycle at the nth iteration
- return None;
- }
- }
-
- if !self.in_queue.contains(target) {
- self.queue.push_back(target);
- self.in_queue.insert(target);
- }
- }
-
- let Some(node) = self.queue.pop_front() else {
- // No more elements in the queue, we're done.
- self.next = None;
- return None;
- };
- self.in_queue.remove(node);
-
- self.next = Some(node);
-
- // we're currently visiting the node that has the shortest distance, therefore we know
- // that the distance is the shortest possible
- let distance = self.distances[node_id].clone();
- let intermediates = reconstruct_intermediates(&self.predecessors, node);
-
- let path = Path {
- source: self.source,
- target: self.graph.node(node).expect("node to exist"),
- intermediates,
- };
-
- Some(Route {
- path,
- cost: Cost(distance),
- })
- }
-
- fn size_hint(&self) -> (usize, Option<usize>) {
- (0, Some(self.num_nodes))
- }
-}
diff --git a/crates/algorithms/src/shortest_paths/shortest_path_faster/mod.rs b/crates/algorithms/src/shortest_paths/shortest_path_faster/mod.rs
deleted file mode 100644
index af281d5..0000000
--- a/crates/algorithms/src/shortest_paths/shortest_path_faster/mod.rs
+++ /dev/null
@@ -1,276 +0,0 @@
-mod error;
-mod iter;
-#[cfg(test)]
-mod tests;
-
-use core::{hash::Hash, ops::Add};
-
-use error_stack::Result;
-use num_traits::{Bounded, Zero};
-use petgraph_core::{
- edge::marker::{Directed, Undirected},
- DirectedGraphStorage, Graph, GraphDirectionality, GraphStorage, Node,
-};
-
-use self::{error::ShortestPathFasterError, iter::ShortestPathFasterIter};
-use super::{
- common::{
- connections::outgoing_connections,
- cost::{DefaultCost, GraphCost},
- },
- DirectRoute, Route, ShortestDistance, ShortestPath,
-};
-use crate::polyfill::IteratorExt;
-
-#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
-pub enum SPFACandidateOrder {
- #[default]
- SmallFirst,
- LargeLast,
-}
-
-pub struct ShortestPathFaster<D, E> {
- direction: D,
- edge_cost: E,
- candidate_order: SPFACandidateOrder,
-}
-
-impl ShortestPathFaster<Directed, DefaultCost> {
- pub fn directed() -> Self {
- Self {
- direction: Directed,
- edge_cost: DefaultCost,
- candidate_order: Default::default(),
- }
- }
-}
-
-impl ShortestPathFaster<Undirected, DefaultCost> {
- pub fn undirected() -> Self {
- Self {
- direction: Undirected,
- edge_cost: DefaultCost,
- candidate_order: Default::default(),
- }
- }
-}
-
-impl<D, E> ShortestPathFaster<D, E>
-where
- D: GraphDirectionality,
-{
- pub fn with_edge_cost<S, F>(self, edge_cost: F) -> ShortestPathFaster<D, F>
- where
- S: GraphStorage,
- F: GraphCost<S>,
- {
- ShortestPathFaster {
- direction: self.direction,
- edge_cost,
- candidate_order: Default::default(),
- }
- }
-
- pub fn without_edge_cost(self) -> ShortestPathFaster<D, ()> {
- ShortestPathFaster {
- direction: self.direction,
- edge_cost: (),
- candidate_order: Default::default(),
- }
- }
-
- pub fn with_candidate_order(self, candidate_order: SPFACandidateOrder) -> Self {
- Self {
- direction: self.direction,
- edge_cost: self.edge_cost,
- candidate_order,
- }
- }
-}
-
-impl<S, E> ShortestPath<S> for ShortestPathFaster<Undirected, E>
-where
- S: GraphStorage,
- S::NodeId: PartialEq + Eq + Hash,
- E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Bounded + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
-{
- type Cost = E::Value;
- type Error = ShortestPathFasterError;
-
- fn path_from<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph petgraph_core::Graph<S>,
- source: &'graph <S as GraphStorage>::NodeId,
- ) -> Result<impl Iterator<Item = super::Route<'graph, S, Self::Cost>>, Self::Error> {
- ShortestPathFasterIter::new(
- graph,
- &self.edge_cost,
- Node::<'graph, S>::connections as fn(&Node<'graph, S>) -> _,
- source,
- // self.candidate_order,
- )
- }
-
- fn path_to<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph Graph<S>,
- target: &'graph S::NodeId,
- ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>>, Self::Error> {
- let iter = self.path_from(graph, target)?;
-
- Ok(iter.map(Route::reverse))
- }
-
- fn every_path<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph petgraph_core::Graph<S>,
- ) -> Result<impl Iterator<Item = super::Route<'graph, S, Self::Cost>> + 'this, Self::Error>
- {
- let iter = graph
- .nodes()
- .map(move |node| self.path_from(graph, node.id()))
- .collect_reports::<Vec<_>>()?;
-
- Ok(iter.into_iter().flatten())
- }
-}
-
-impl<S, E> ShortestDistance<S> for ShortestPathFaster<Undirected, E>
-where
- S: GraphStorage,
- S::NodeId: Eq + Hash,
- E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Zero + Bounded + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
-{
- type Cost = E::Value;
- type Error = ShortestPathFasterError;
-
- fn distance_from<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph petgraph_core::Graph<S>,
- source: &'graph <S as GraphStorage>::NodeId,
- ) -> Result<impl Iterator<Item = super::DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error>
- {
- let iter = ShortestPathFasterIter::new(
- graph,
- &self.edge_cost,
- Node::<'graph, S>::connections as fn(&Node<'graph, S>) -> _,
- source,
- // self.candidate_order,
- )?;
-
- Ok(iter.map(|route| DirectRoute {
- source: route.path.source,
- target: route.path.target,
- cost: route.cost,
- }))
- }
-
- fn distance_to<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph Graph<S>,
- target: &'graph S::NodeId,
- ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>>, Self::Error> {
- let iter = self.distance_from(graph, target)?;
-
- Ok(iter.map(DirectRoute::reverse))
- }
-
- fn every_distance<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph petgraph_core::Graph<S>,
- ) -> Result<impl Iterator<Item = super::DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error>
- {
- let iter = graph
- .nodes()
- .map(move |node| self.distance_from(graph, node.id()))
- .collect_reports::<Vec<_>>()?;
-
- Ok(iter.into_iter().flatten())
- }
-}
-
-impl<S, E> ShortestPath<S> for ShortestPathFaster<Directed, E>
-where
- S: DirectedGraphStorage,
- S::NodeId: Eq + Hash,
- E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Zero + Bounded + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
-{
- type Cost = E::Value;
- type Error = ShortestPathFasterError;
-
- fn path_from<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph Graph<S>,
- source: &'graph S::NodeId,
- ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
- ShortestPathFasterIter::new(
- graph,
- &self.edge_cost,
- outgoing_connections as fn(&Node<'graph, S>) -> _,
- source,
- // self.candidate_order,
- )
- }
-
- fn every_path<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph Graph<S>,
- ) -> Result<impl Iterator<Item = Route<'graph, S, Self::Cost>> + 'this, Self::Error> {
- let iter = graph
- .nodes()
- .map(move |node| self.path_from(graph, node.id()))
- .collect_reports::<Vec<_>>()?;
-
- Ok(iter.into_iter().flatten())
- }
-}
-
-impl<S, E> ShortestDistance<S> for ShortestPathFaster<Directed, E>
-where
- S: DirectedGraphStorage,
- S::NodeId: Eq + Hash,
- E: GraphCost<S>,
- for<'a> E::Value: PartialOrd + Ord + Zero + Zero + Bounded + Clone + 'a,
- for<'a> &'a E::Value: Add<Output = E::Value>,
-{
- type Cost = E::Value;
- type Error = ShortestPathFasterError;
-
- fn distance_from<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph Graph<S>,
- source: &'graph <S as GraphStorage>::NodeId,
- ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>>, Self::Error> {
- let iter = ShortestPathFasterIter::new(
- graph,
- &self.edge_cost,
- outgoing_connections as fn(&Node<'graph, S>) -> _,
- source,
- // self.candidate_order,
- )?;
-
- Ok(iter.map(|route| DirectRoute {
- source: route.path.source,
- target: route.path.target,
- cost: route.cost,
- }))
- }
-
- fn every_distance<'graph: 'this, 'this>(
- &'this self,
- graph: &'graph Graph<S>,
- ) -> Result<impl Iterator<Item = DirectRoute<'graph, S, Self::Cost>> + 'this, Self::Error> {
- let iter = graph
- .nodes()
- .map(move |node| self.distance_from(graph, node.id()))
- .collect_reports::<Vec<_>>()?;
-
- Ok(iter.into_iter().flatten())
- }
-}
diff --git a/crates/algorithms/src/shortest_paths/shortest_path_faster/tests.rs b/crates/algorithms/src/shortest_paths/shortest_path_faster/tests.rs
deleted file mode 100644
index 679bde9..0000000
--- a/crates/algorithms/src/shortest_paths/shortest_path_faster/tests.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use petgraph_dino::{DiDinoGraph, EdgeId, NodeId};
-use petgraph_utils::{graph, GraphCollection};
-
-use super::ShortestPathFaster;
-use crate::shortest_paths::{
- common::tests::{assert_path, path_from},
- ShortestPath,
-};
-
-graph!(
- /// Uses the graph from networkx
- ///
- /// <https://github.com/networkx/networkx/blob/main/networkx/algorithms/shortest_paths/tests/test_weighted.py>
- factory(networkx) => DiDinoGraph<&'static str, i32>;
- [
- a: "A",
- b: "B",
- c: "C",
- d: "D",
- e: "E",
- ] as NodeId, [
- ab: a -> b: 10,
- ac: a -> c: 5,
- bd: b -> d: 1,
- bc: b -> c: 2,
- de: d -> e: 1,
- cb: c -> b: 3,
- cd: c -> d: 5,
- ce: c -> e: 2,
- ea: e -> a: 7,
- ed: e -> d: 6,
- ] as EdgeId
-);
-
-#[test]
-fn path_from_directed_default_edge_cost() {
- let GraphCollection {
- graph,
- nodes,
- edges,
- } = networkx::create();
-
- let spfa = ShortestPathFaster::directed();
- // let received = path_from(&graph, &nodes.a, &spfa);
-
- let res = spfa
- .path_from(&graph, &nodes.a)
- .unwrap()
- .map(|v| {
- v.path
- .to_vec()
- .into_iter()
- .map(|q| q.weight())
- .collect::<Vec<_>>()
- })
- .collect::<Vec<_>>();
- dbg!(&res);
- let expected = [
- (nodes.a, 0, &[nodes.a, nodes.a] as &[_]),
- (nodes.b, 8, &[nodes.a, nodes.c, nodes.b]),
- (nodes.c, 5, &[nodes.a, nodes.c]),
- (nodes.d, 9, &[nodes.a, nodes.c, nodes.b, nodes.d]),
- (nodes.e, 7, &[nodes.a, nodes.c, nodes.e]),
- ];
-
- // assert_path(received, &expected);
- assert!(false)
-}
diff --git a/crates/algorithms/tests/cases/shortest_path/almost_line_00.in b/crates/algorithms/tests/cases/shortest_path/almost_line_00.in
new file mode 100644
index 0000000..0636a0b
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/almost_line_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ddcc091c3d39eef908e4de002e1b498778ad8230111c524896f4b0f7c6e9230d
+size 11500830
diff --git a/crates/algorithms/tests/cases/shortest_path/almost_line_00.out b/crates/algorithms/tests/cases/shortest_path/almost_line_00.out
new file mode 100644
index 0000000..d702078
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/almost_line_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:46a253236925af5a56ca1af935ff2abc65b3a4dbe1495f55d5eb8287d590a584
+size 2033784
diff --git a/crates/algorithms/tests/cases/shortest_path/almost_line_01.in b/crates/algorithms/tests/cases/shortest_path/almost_line_01.in
new file mode 100644
index 0000000..c36cd94
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/almost_line_01.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cee45e304f24a482ad1d29b36a864e862b0bdeafae5bf30b33932d5c5d13790e
+size 11499200
diff --git a/crates/algorithms/tests/cases/shortest_path/almost_line_01.out b/crates/algorithms/tests/cases/shortest_path/almost_line_01.out
new file mode 100644
index 0000000..0d61d9b
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/almost_line_01.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1d47fc7006a6c8ce49872dc7c8a9cf762aadcf29522a50016b370b20bdff9b16
+size 2034810
diff --git a/crates/algorithms/tests/cases/shortest_path/almost_line_02.in b/crates/algorithms/tests/cases/shortest_path/almost_line_02.in
new file mode 100644
index 0000000..826762e
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/almost_line_02.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9d5c9229f8271d289ad59f1b3177655ff3f4b498f21344c9bdc5d1bbf973a27e
+size 7555754
diff --git a/crates/algorithms/tests/cases/shortest_path/almost_line_02.out b/crates/algorithms/tests/cases/shortest_path/almost_line_02.out
new file mode 100644
index 0000000..6a0a00b
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/almost_line_02.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7ae2e0ff962b6545b1c4056d126bdf0fcf1f8787b169aad61622c13b41099571
+size 1829364
diff --git a/crates/algorithms/tests/cases/shortest_path/example_00.in b/crates/algorithms/tests/cases/shortest_path/example_00.in
new file mode 100644
index 0000000..51e9cb8
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/example_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0868d99aa063f973bc47eb5747fd8873936754f9ab8bf295eaaf930946bed156
+size 51
diff --git a/crates/algorithms/tests/cases/shortest_path/example_00.out b/crates/algorithms/tests/cases/shortest_path/example_00.out
new file mode 100644
index 0000000..4faea9b
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/example_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:92ef26703823a4c8b2cc311103c1d5f8e6118dd5bf83127307ce077d9f4b65f8
+size 17
diff --git a/crates/algorithms/tests/cases/shortest_path/example_01.in b/crates/algorithms/tests/cases/shortest_path/example_01.in
new file mode 100644
index 0000000..613de1c
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/example_01.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b1576608937a0946ebbb394d511c701d7c01d8a094e5f6e02eb80ecf5d0283d9
+size 15
diff --git a/crates/algorithms/tests/cases/shortest_path/example_01.out b/crates/algorithms/tests/cases/shortest_path/example_01.out
new file mode 100644
index 0000000..311c450
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/example_01.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee3aa64bb94a50845d5024cd4bd20202a4567aed5cd5328c0d97e9920775fc28
+size 3
diff --git a/crates/algorithms/tests/cases/shortest_path/grid_random_00.in b/crates/algorithms/tests/cases/shortest_path/grid_random_00.in
new file mode 100644
index 0000000..9d88f14
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/grid_random_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ce2978c41a2adcfc884152fc3612e9c498fab217c5b5a918917d0e83c54beca1
+size 10921348
diff --git a/crates/algorithms/tests/cases/shortest_path/grid_random_00.out b/crates/algorithms/tests/cases/shortest_path/grid_random_00.out
new file mode 100644
index 0000000..f4df973
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/grid_random_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f34e8e95d3536526bc166b5adc11b463c390163c1796f299308a47f8b4f89e72
+size 1252
diff --git a/crates/algorithms/tests/cases/shortest_path/grid_swirl_00.in b/crates/algorithms/tests/cases/shortest_path/grid_swirl_00.in
new file mode 100644
index 0000000..2a5099d
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/grid_swirl_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:18eb066b7261108b833de2331760ac0b5acaf6e92fa5648d9d5da6425d4f3ad5
+size 9954866
diff --git a/crates/algorithms/tests/cases/shortest_path/grid_swirl_00.out b/crates/algorithms/tests/cases/shortest_path/grid_swirl_00.out
new file mode 100644
index 0000000..36f28d8
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/grid_swirl_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:45ca68142b57bfee7f002364555d7bf02dcbd25e462815db2d45c823e61cf584
+size 1512442
diff --git a/crates/algorithms/tests/cases/shortest_path/line_00.in b/crates/algorithms/tests/cases/shortest_path/line_00.in
new file mode 100644
index 0000000..e04bfe3
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/line_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1c34397c506a4d93e1eff89b396d697f28839453a3902e4d881ffc223766b7d9
+size 12277783
diff --git a/crates/algorithms/tests/cases/shortest_path/line_00.out b/crates/algorithms/tests/cases/shortest_path/line_00.out
new file mode 100644
index 0000000..903e1f7
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/line_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7c6e51b3ce0757fe443c631b3cdfeac2216cea30b79661aa904f8187b631d41e
+size 6777789
diff --git a/crates/algorithms/tests/cases/shortest_path/max_dense_long_00.in b/crates/algorithms/tests/cases/shortest_path/max_dense_long_00.in
new file mode 100644
index 0000000..7defbbd
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_dense_long_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2892ece395211550e57aefc31f1417ca7c691d6c9b27eaf4ebab006b42fd6b4e
+size 8768774
diff --git a/crates/algorithms/tests/cases/shortest_path/max_dense_long_00.out b/crates/algorithms/tests/cases/shortest_path/max_dense_long_00.out
new file mode 100644
index 0000000..85ff094
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_dense_long_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6dcc5b976e90a465e0b1d7fe3cf65b4bceb29f56f6d58a6927bdf05668fe7d03
+size 5437
diff --git a/crates/algorithms/tests/cases/shortest_path/max_dense_random_00.in b/crates/algorithms/tests/cases/shortest_path/max_dense_random_00.in
new file mode 100644
index 0000000..c8851d0
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_dense_random_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:64c301019647dc4349a22d4e6434e5feebdb83277eefd42180c62caa66bfc5f0
+size 8774280
diff --git a/crates/algorithms/tests/cases/shortest_path/max_dense_random_00.out b/crates/algorithms/tests/cases/shortest_path/max_dense_random_00.out
new file mode 100644
index 0000000..2d8ba92
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_dense_random_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:93f9b983001b1ad49b6c8fd8c2e27ba98f2233975594853b580b18b34bfa162d
+size 89
diff --git a/crates/algorithms/tests/cases/shortest_path/max_dense_random_01.in b/crates/algorithms/tests/cases/shortest_path/max_dense_random_01.in
new file mode 100644
index 0000000..04e2ba9
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_dense_random_01.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b9acf2b13b48cc4095574365880eb87a2042a83a2b7a8150037a321746d32f53
+size 8774117
diff --git a/crates/algorithms/tests/cases/shortest_path/max_dense_random_01.out b/crates/algorithms/tests/cases/shortest_path/max_dense_random_01.out
new file mode 100644
index 0000000..fb4730b
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_dense_random_01.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3de1bcfa0bfcc3ee770ed37b5589540f86a32185342b2d798c2664c7a3d6d518
+size 72
diff --git a/crates/algorithms/tests/cases/shortest_path/max_dense_zero_00.in b/crates/algorithms/tests/cases/shortest_path/max_dense_zero_00.in
new file mode 100644
index 0000000..c776df0
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_dense_zero_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2f8ea9e13435f6306e8fb27959ceef9895f0d5ddf7faf977b684d24fefcaf2e1
+size 4836119
diff --git a/crates/algorithms/tests/cases/shortest_path/max_dense_zero_00.out b/crates/algorithms/tests/cases/shortest_path/max_dense_zero_00.out
new file mode 100644
index 0000000..04a3318
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_dense_zero_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4303004b151251b7956ab2483e28bae4c0e36bbd69cca31d7c60fb0ad2c0a66d
+size 12
diff --git a/crates/algorithms/tests/cases/shortest_path/max_sparse_random_00.in b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_00.in
new file mode 100644
index 0000000..e95d563
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e34c7ed7c0d4b4835ab4fd659585a77cdcb4dc1563c92179e24aa652eb0dd476
+size 11500586
diff --git a/crates/algorithms/tests/cases/shortest_path/max_sparse_random_00.out b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_00.out
new file mode 100644
index 0000000..311c450
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee3aa64bb94a50845d5024cd4bd20202a4567aed5cd5328c0d97e9920775fc28
+size 3
diff --git a/crates/algorithms/tests/cases/shortest_path/max_sparse_random_01.in b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_01.in
new file mode 100644
index 0000000..246e695
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_01.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8aafa7cb1de15c876c7445d55ee9ac3c0c535eb57382dafdb9e1845fad444d97
+size 11500446
diff --git a/crates/algorithms/tests/cases/shortest_path/max_sparse_random_01.out b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_01.out
new file mode 100644
index 0000000..8b36e1b
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_01.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:901fcb995b5ade65c8e159881c9c575ff3b9af1b25738120f06ae8cf6981acae
+size 198
diff --git a/crates/algorithms/tests/cases/shortest_path/max_sparse_random_02.in b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_02.in
new file mode 100644
index 0000000..f715e71
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_02.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6994f28e61bcad12d5a0d6feaa10df94f36c1eb88c32ac844ce18319c132a4cd
+size 11500081
diff --git a/crates/algorithms/tests/cases/shortest_path/max_sparse_random_02.out b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_02.out
new file mode 100644
index 0000000..b3d03f8
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_sparse_random_02.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6c98e7fec768b9fe59965f3b9b5181ae7c35e35ee57edac49c189c578a61ba0d
+size 267
diff --git a/crates/algorithms/tests/cases/shortest_path/max_star_00.in b/crates/algorithms/tests/cases/shortest_path/max_star_00.in
new file mode 100644
index 0000000..77580fa
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_star_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:095e698480f5ca855e715fea307baa6a6414d384b517cd194dc77fda396ecdff
+size 10833928
diff --git a/crates/algorithms/tests/cases/shortest_path/max_star_00.out b/crates/algorithms/tests/cases/shortest_path/max_star_00.out
new file mode 100644
index 0000000..66aaaab
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_star_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8e36190f48f36e4fb918417f71f25170b6c87c8177b90198d26252e00079c18e
+size 81
diff --git a/crates/algorithms/tests/cases/shortest_path/max_star_01.in b/crates/algorithms/tests/cases/shortest_path/max_star_01.in
new file mode 100644
index 0000000..e41e300
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_star_01.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:36bdce1c462760d4c4f5c5e76c42bf06d6f134b71bf34eb39c7b994596ce55bc
+size 11777953
diff --git a/crates/algorithms/tests/cases/shortest_path/max_star_01.out b/crates/algorithms/tests/cases/shortest_path/max_star_01.out
new file mode 100644
index 0000000..0d41a21
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/max_star_01.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9a7b41bbf2d24cd5ce10a32a675acf937cf3df56776d13dd075fc3df252024ae
+size 40
diff --git a/crates/algorithms/tests/cases/shortest_path/small_00.in b/crates/algorithms/tests/cases/shortest_path/small_00.in
new file mode 100644
index 0000000..3ebba19
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:57bcca3d9e99a79b2a3e2c560a944dac6f0647461ab51384b2e853179128396d
+size 877
diff --git a/crates/algorithms/tests/cases/shortest_path/small_00.out b/crates/algorithms/tests/cases/shortest_path/small_00.out
new file mode 100644
index 0000000..c4aeb7d
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1cda3f5e6b776c0087aec2c2482c35162a0d34ee43b5601d192d8c1710d874da
+size 35
diff --git a/crates/algorithms/tests/cases/shortest_path/small_01.in b/crates/algorithms/tests/cases/shortest_path/small_01.in
new file mode 100644
index 0000000..882d182
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_01.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a4f286748128eddb8f61856d21a22d75c1e3f23bd24120e064df4a98493d4917
+size 863
diff --git a/crates/algorithms/tests/cases/shortest_path/small_01.out b/crates/algorithms/tests/cases/shortest_path/small_01.out
new file mode 100644
index 0000000..b331389
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_01.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:17c0c7e5885e153991b09bb5736b0b5a277838c7dfb2c3f5a0ad646c4838f6ef
+size 45
diff --git a/crates/algorithms/tests/cases/shortest_path/small_02.in b/crates/algorithms/tests/cases/shortest_path/small_02.in
new file mode 100644
index 0000000..3a986f0
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_02.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27c35f6fcc19c459e0efd8c3b22b69e598bba4ea14eac9e72682b06a1d2c6e8a
+size 855
diff --git a/crates/algorithms/tests/cases/shortest_path/small_02.out b/crates/algorithms/tests/cases/shortest_path/small_02.out
new file mode 100644
index 0000000..d3b6cea
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_02.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ad694a5746d6f0073c119b361ec87b4a790ac8fa48f37404b4ad81f71ee6f609
+size 18
diff --git a/crates/algorithms/tests/cases/shortest_path/small_03.in b/crates/algorithms/tests/cases/shortest_path/small_03.in
new file mode 100644
index 0000000..86920fb
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_03.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ebbd224df70dc5a1ad97601b5c0da26455f90e42b8ea037adca37e4991098228
+size 862
diff --git a/crates/algorithms/tests/cases/shortest_path/small_03.out b/crates/algorithms/tests/cases/shortest_path/small_03.out
new file mode 100644
index 0000000..b6622ad
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_03.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a64711f7c61aae4f6e601ee09ca8e1e5533da592d87118898ff6cd66d1442594
+size 31
diff --git a/crates/algorithms/tests/cases/shortest_path/small_04.in b/crates/algorithms/tests/cases/shortest_path/small_04.in
new file mode 100644
index 0000000..21fb36f
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_04.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:850e20f1b0980ca4e7041b8c8c3d14d822c09d5af9f268530c99e7758f5e2576
+size 868
diff --git a/crates/algorithms/tests/cases/shortest_path/small_04.out b/crates/algorithms/tests/cases/shortest_path/small_04.out
new file mode 100644
index 0000000..dd9bd37
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/small_04.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1a14dbe2303ef65ec8df9e16d0a48d47fac2ed2828936af9c356e61f3e1fe8c2
+size 40
diff --git a/crates/algorithms/tests/cases/shortest_path/sparse_random_00.in b/crates/algorithms/tests/cases/shortest_path/sparse_random_00.in
new file mode 100644
index 0000000..851d73d
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/sparse_random_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:46f93d9708bfac3ce5242665581947369001ec916210a1e7650e23ec0c42bf61
+size 9582610
diff --git a/crates/algorithms/tests/cases/shortest_path/sparse_random_00.out b/crates/algorithms/tests/cases/shortest_path/sparse_random_00.out
new file mode 100644
index 0000000..311c450
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/sparse_random_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee3aa64bb94a50845d5024cd4bd20202a4567aed5cd5328c0d97e9920775fc28
+size 3
diff --git a/crates/algorithms/tests/cases/shortest_path/sparse_random_01.in b/crates/algorithms/tests/cases/shortest_path/sparse_random_01.in
new file mode 100644
index 0000000..0d1ff35
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/sparse_random_01.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ce729353f2b0487710ce2e2518efddfee782390e6a508912f940da8e8ff933a2
+size 9665753
diff --git a/crates/algorithms/tests/cases/shortest_path/sparse_random_01.out b/crates/algorithms/tests/cases/shortest_path/sparse_random_01.out
new file mode 100644
index 0000000..311c450
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/sparse_random_01.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee3aa64bb94a50845d5024cd4bd20202a4567aed5cd5328c0d97e9920775fc28
+size 3
diff --git a/crates/algorithms/tests/cases/shortest_path/sparse_random_02.in b/crates/algorithms/tests/cases/shortest_path/sparse_random_02.in
new file mode 100644
index 0000000..b4f202a
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/sparse_random_02.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e175334fe70c3dd3a31e4e34388afa996d7bb7051239dc56af87f03caf758255
+size 8210248
diff --git a/crates/algorithms/tests/cases/shortest_path/sparse_random_02.out b/crates/algorithms/tests/cases/shortest_path/sparse_random_02.out
new file mode 100644
index 0000000..38d9620
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/sparse_random_02.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e6e03e75ae2d0aa8da3e1bca2f62bca8c5637a4993fb28f1ffb3b4a4af88ec93
+size 131
diff --git a/crates/algorithms/tests/cases/shortest_path/spfa_killer_00.in b/crates/algorithms/tests/cases/shortest_path/spfa_killer_00.in
new file mode 100644
index 0000000..98078d1
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/spfa_killer_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8da03b49ee73a638e6d3cc4217c0bf644b92cda197e20f60cdf9093d7aeb85e5
+size 7731684
diff --git a/crates/algorithms/tests/cases/shortest_path/spfa_killer_00.out b/crates/algorithms/tests/cases/shortest_path/spfa_killer_00.out
new file mode 100644
index 0000000..6cf110e
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/spfa_killer_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:761c118bcfb6e73ebc10b0b2c2be83eae8023889465e3b06986b6e8267e635c5
+size 1748673
diff --git a/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_handmade_00.in b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_handmade_00.in
new file mode 100644
index 0000000..43523fc
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_handmade_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c4b803ff9113fa1308dca99ac0c8bd494faa8d3fa3256f24c9b09f2c97d371d2
+size 26
diff --git a/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_handmade_00.out b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_handmade_00.out
new file mode 100644
index 0000000..315fba6
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_handmade_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:46066aede3e055309cafe3371ccafcfeb8188c598cb5f7c4171d0fd0a3f846aa
+size 12
diff --git a/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_00.in b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_00.in
new file mode 100644
index 0000000..a6d56dc
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_00.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e9b1bbcbe57f62b7b9b96ecbd7064c80587a44f476971bd6a5109c0bc87253ad
+size 9319483
diff --git a/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_00.out b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_00.out
new file mode 100644
index 0000000..5373aff
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_00.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:72cddb1daaa2122527535c381e5c659de149d61fad545e80ef048810a105aa0c
+size 60
diff --git a/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_01.in b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_01.in
new file mode 100644
index 0000000..3529ccf
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_01.in
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:54ec025cf331683d9c5b72fcec87536564a5303361dfd2013eefddf801ee646e
+size 8355202
diff --git a/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_01.out b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_01.out
new file mode 100644
index 0000000..9f9c781
--- /dev/null
+++ b/crates/algorithms/tests/cases/shortest_path/wrong_dijkstra_killer_01.out
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2564cd63ffd8c46e7b44b5515c5eded6feecbb5f04dd3cb3bd6bc67da6b4c537
+size 3184996
diff --git a/crates/algorithms/tests/shortest_paths/harness.rs b/crates/algorithms/tests/shortest_paths/harness.rs
new file mode 100644
index 0000000..0930eb2
--- /dev/null
+++ b/crates/algorithms/tests/shortest_paths/harness.rs
@@ -0,0 +1,371 @@
+//! [`Harness`] for discovering test inputs and asserting against snapshot files
+//!
+//! Taken from [snapbox](https://docs.rs/snapshot) and adapted to make matrix tests possible.
+
+use std::path::Path;
+
+use hashbrown::{HashMap, HashSet};
+use ignore::{
+ overrides::{Override, OverrideBuilder},
+ WalkBuilder,
+};
+use libtest_mimic::Trial;
+use snapbox::{
+ report::{write_diff, Palette},
+ Action, Assert, Data, DataFormat, NormalizeNewlines,
+};
+
+pub struct Harness<S, T> {
+ root: std::path::PathBuf,
+ overrides: Option<Override>,
+ each: Option<&'static [&'static str]>,
+ setup: S,
+ test: T,
+ action: Action,
+}
+
+impl<S, T, I, E> Harness<S, T>
+where
+ I: std::fmt::Display,
+ E: std::fmt::Display,
+ S: Fn(std::path::PathBuf) -> Case + Send + Sync + 'static,
+ T: Fn(&Path, &'static str) -> Result<I, E> + Send + Sync + 'static + Clone,
+{
+ pub fn new(root: impl Into<std::path::PathBuf>, setup: S, test: T) -> Self {
+ Self {
+ root: root.into(),
+ overrides: None,
+ setup,
+ // in theory we would want to do this via a type-state instead,
+ // but I can't be bothered to put in the extra effort, unless we upstream this.
+ each: None,
+ test,
+ action: Action::Verify,
+ }
+ }
+
+ /// Path patterns for selecting input files
+ ///
+ /// This used gitignore syntax
+ pub fn select<'p>(mut self, patterns: impl IntoIterator<Item = &'p str>) -> Self {
+ let mut overrides = OverrideBuilder::new(&self.root);
+ for line in patterns {
+ overrides.add(line).unwrap();
+ }
+ self.overrides = Some(overrides.build().unwrap());
+ self
+ }
+
+ pub fn each(mut self, names: &'static [&'static str]) -> Self {
+ self.each = Some(names);
+ self
+ }
+
+ /// Read the failure action from an environment variable
+ pub fn action_env(mut self, var_name: &str) -> Self {
+ let action = Action::with_env_var(var_name);
+ self.action = action.unwrap_or(self.action);
+ self
+ }
+
+ /// Override the failure action
+ pub fn action(mut self, action: Action) -> Self {
+ self.action = action;
+ self
+ }
+
+ fn trials(&self, name: &'static str) -> impl IntoIterator<Item = Trial> + '_ {
+ let mut walk = WalkBuilder::new(&self.root);
+ walk.standard_filters(false);
+ let tests = walk.build().filter_map(|entry| {
+ let entry = entry.unwrap();
+ let is_dir = entry.file_type().map(|f| f.is_dir()).unwrap_or(false);
+ let path = entry.into_path();
+ if let Some(overrides) = &self.overrides {
+ overrides
+ .matched(&path, is_dir)
+ .is_whitelist()
+ .then_some(path)
+ } else {
+ Some(path)
+ }
+ });
+
+ tests.into_iter().map(move |path| {
+ let case = (self.setup)(path);
+
+ let test = self.test.clone();
+ let trial_name = if name.is_empty() {
+ case.name.clone()
+ } else {
+ format!("{name}::{}", case.name)
+ };
+ let action = self.action;
+
+ Trial::test(trial_name, move || {
+ let actual = test(&case.fixture, name)?;
+ let actual = actual.to_string();
+
+ let verify = Verifier::new();
+ verify.assert(&case.fixture, &case.expected, actual)?;
+ Ok(())
+ })
+ .with_ignored_flag(action == Action::Ignore)
+ })
+ }
+
+ /// Run tests
+ pub fn test(self) -> ! {
+ let each = self.each.unwrap_or(&[""]);
+
+ let tests = each.iter().flat_map(|name| self.trials(name)).collect();
+
+ let args = libtest_mimic::Arguments::from_args();
+ libtest_mimic::run(&args, tests).exit()
+ }
+}
+
+struct Solution {
+ cost: u64,
+ edges: usize,
+
+ path: Vec<Connection>,
+}
+
+#[derive(Debug, Copy, Clone)]
+struct Connection {
+ source: usize,
+ target: usize,
+}
+
+struct Graph {
+ nodes: usize,
+ edges_len: usize,
+
+ source: usize,
+ target: usize,
+
+ edges: HashMap<(usize, usize), u64>,
+}
+
+macro_rules! next {
+ ($iter:ident as $ty:ty) => {
+ $iter
+ .next()
+ .expect("expected token")
+ .parse::<$ty>()
+ .expect(concat!("is ", stringify!($ty)))
+ };
+}
+
+struct Verifier {
+ palette: Palette,
+}
+
+impl Verifier {
+ fn new() -> Self {
+ Self {
+ palette: Palette::color(),
+ }
+ }
+
+ fn parse_out(value: String) -> Option<Solution> {
+ let mut lines = value.lines();
+
+ let (cost, edges) = {
+ let line = lines.next().expect("at least one line");
+ let mut iter = line.split_whitespace();
+
+ let cost = next!(iter as i128);
+ if cost < 0 {
+ return None;
+ }
+ let cost = cost as u64;
+
+ let edges = next!(iter as usize);
+
+ (cost, edges)
+ };
+
+ let mut path = Vec::with_capacity(edges);
+
+ for _ in 0..edges {
+ let line = lines.next().expect("expected line");
+ let mut iter = line.split_whitespace();
+
+ let source = next!(iter as usize);
+ let target = next!(iter as usize);
+
+ path.push(Connection { source, target });
+ }
+
+ Some(Solution { cost, edges, path })
+ }
+
+ fn parse_in(value: String) -> Graph {
+ let mut lines = value.lines();
+
+ let (nodes, edges_len, source, target) = {
+ let line = lines.next().expect("at least one line");
+ let mut iter = line.split_whitespace();
+ let nodes = next!(iter as usize);
+ let edges_len = next!(iter as usize);
+ let source = next!(iter as usize);
+ let target = next!(iter as usize);
+
+ (nodes, edges_len, source, target)
+ };
+
+ let mut edges = HashMap::with_capacity(edges_len);
+
+ for _ in 0..edges_len {
+ let line = lines.next().expect("expected line");
+ let mut iter = line.split_whitespace();
+ let source = next!(iter as usize);
+ let target = next!(iter as usize);
+ let weight = next!(iter as u64);
+
+ edges.insert((source, target), weight);
+ }
+
+ Graph {
+ nodes,
+ edges_len,
+ source,
+ target,
+ edges,
+ }
+ }
+
+ fn assert(&self, graph: &Path, expected: &Path, received: String) -> snapbox::Result<()> {
+ let graph = std::fs::read_to_string(graph).map_err(snapbox::Error::new)?;
+ let graph = Self::parse_in(graph);
+
+ let expected = std::fs::read_to_string(expected).map_err(snapbox::Error::new)?;
+ let expected = Self::parse_out(expected);
+
+ let received = Self::parse_out(received);
+
+ self.verify(graph, expected, received)
+ }
+
+ /// Implementation of the algorithm used in `library-checker-problems`
+ ///
+ /// see:
+ /// <https://github.com/yosupo06/library-checker-problems/blob/1115a1bf77a8f3d66ab95e46a693a841cd4a7098/graph/shortest_path/checker.cpp>
+ fn verify(
+ &self,
+ graph: Graph,
+ expected: Option<Solution>,
+ received: Option<Solution>,
+ ) -> snapbox::Result<()> {
+ let (expected, received) = match (expected, received) {
+ (Some(expected), Some(received)) => (expected, received),
+ (None, None) => return Ok(()),
+ (Some(_), None) => panic!(
+ "{}: expected path, but didn't receive one",
+ self.palette.error("path existence differs")
+ ),
+ (None, Some(_)) => panic!(
+ "{}: received path, but didn't expect one",
+ self.palette.error("path existence differs")
+ ),
+ };
+
+ let first = expected.path.first().expect("at least one edge");
+ let last = expected.path.last().expect("at least one edge");
+
+ if first.source != graph.source {
+ panic!(
+ "{}: path starts at wrong node",
+ self.palette.error("path differs")
+ );
+ }
+
+ if last.target != graph.target {
+ panic!(
+ "{}: path ends at wrong node",
+ self.palette.error("path differs")
+ );
+ }
+
+ let mut used = HashSet::new();
+ used.insert(graph.source);
+
+ let mut total = 0;
+
+ for index in 0..expected.edges {
+ let current = expected.path[index];
+
+ if index + 1 < expected.edges {
+ let next = expected.path[index + 1];
+
+ if current.target != next.source {
+ panic!(
+ "{}: teleporting between {}th edge and {}th edge from vertex {} to {}",
+ self.palette.error("teleportation"),
+ index,
+ index + 1,
+ current.target,
+ next.source
+ );
+ }
+ }
+
+ let Some(&cost) = graph.edges.get(&(current.source, current.target)) else {
+ panic!(
+ "{}: edge from {} to {} doesn't exist",
+ self.palette.error("edge existence"),
+ current.source,
+ current.target
+ );
+ };
+
+ if used.contains(¤t.target) {
+ panic!(
+ "{}: vertex {} is used twice",
+ self.palette.error("vertex usage"),
+ current.target
+ );
+ }
+
+ used.insert(current.target);
+ total += cost;
+ }
+
+ if total != received.cost {
+ panic!(
+ "{}: total weights differ between calculated ({}) and received path ({})",
+ self.palette.error("total weight differs"),
+ total,
+ received.cost
+ )
+ }
+
+ if total > expected.cost {
+ panic!(
+ "{}: not the shortest path, shortest {}, submitted {}",
+ self.palette.error("not shortest"),
+ expected.cost,
+ total
+ )
+ }
+
+ if total < expected.cost {
+ panic!(
+ "{}: submitted solution shorter than judge's solution, submitted {}, judge {}",
+ self.palette.error("shorter"),
+ total,
+ expected.cost
+ )
+ }
+
+ Ok(())
+ }
+}
+
+pub struct Case {
+ pub name: String,
+ pub fixture: std::path::PathBuf,
+ pub expected: std::path::PathBuf,
+}
diff --git a/crates/algorithms/tests/shortest_paths/main.rs b/crates/algorithms/tests/shortest_paths/main.rs
new file mode 100644
index 0000000..5ce7a3d
--- /dev/null
+++ b/crates/algorithms/tests/shortest_paths/main.rs
@@ -0,0 +1,149 @@
+mod harness;
+
+use std::{
+ error::Error,
+ fs::File,
+ io::{BufRead, BufReader, Lines},
+ path::{Path, PathBuf},
+};
+
+use petgraph_algorithms::shortest_paths::{
+ bellman_ford::CandidateOrder, BellmanFord, Dijkstra, FloydWarshall, Route, ShortestPath,
+};
+use petgraph_dino::{DiDinoGraph, DinoStorage, EdgeId, NodeId};
+use snapbox::{utils::normalize_lines, Action};
+
+use crate::harness::{Case, Harness};
+
+fn setup(input_path: PathBuf) -> Case {
+ let name = input_path
+ .file_stem()
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .to_string();
+
+ let expected = input_path.with_extension("out");
+
+ Case {
+ name,
+ fixture: input_path,
+ expected,
+ }
+}
+
+struct Parse {
+ graph: DiDinoGraph<usize, u64>,
+ source: NodeId,
+ target: NodeId,
+ nodes: Vec<NodeId>,
+ edges: Vec<EdgeId>,
+}
+
+fn read(input_path: &Path) -> Result<Parse, Box<dyn Error>> {
+ let file = File::open(input_path)?;
+ let reader = BufReader::new(file);
+
+ let input = reader.lines();
+ parse(input)
+}
+
+fn parse(mut input: Lines<impl BufRead>) -> Result<Parse, Box<dyn Error>> {
+ let meta = input.next().unwrap()?;
+ let meta = meta
+ .split_whitespace()
+ .map(|x| x.parse::<usize>())
+ .collect::<Result<Vec<_>, _>>()?;
+
+ let n = meta[0];
+ let m = meta[1];
+ let s = meta[2];
+ let t = meta[3];
+
+ let mut graph = DiDinoGraph::new();
+
+ let mut nodes = Vec::with_capacity(n);
+ for index in 0..n {
+ nodes.push(*graph.insert_node(index).id());
+ }
+
+ let mut edges = Vec::with_capacity(m);
+ for _ in 0..m {
+ let edge = input.next().unwrap()?;
+ let edge = edge
+ .split_whitespace()
+ .map(|x| x.parse::<usize>())
+ .collect::<Result<Vec<_>, _>>()?;
+
+ let u = edge[0];
+ let v = edge[1];
+ let w = edge[2] as u64;
+
+ let id = *graph.insert_edge(w, &nodes[u], &nodes[v]).id();
+ edges.push(id);
+ }
+
+ let source = nodes[s];
+ let target = nodes[t];
+
+ Ok(Parse {
+ graph,
+ source,
+ target,
+ nodes,
+ edges,
+ })
+}
+
+fn dump(route: Route<DinoStorage<usize, u64>, u64>) -> String {
+ let x = route.cost().into_value();
+ // transit are all intermediate nodes, we have to additional nodes (source and target)
+ // so we add `+ 2`, edges are between them, so we subtract `- 1`, resulting in `+ 1`
+ let y = route.path().transit().len() + 1;
+
+ let mut nodes: Vec<_> = route.into_path().into_iter().map(|x| *x.weight()).collect();
+
+ let mut output = vec![];
+ output.push(format!("{x} {y}"));
+
+ output.extend(
+ nodes
+ .iter()
+ .zip(nodes.iter().skip(1))
+ .map(|(u, v)| format!("{u} {v}")),
+ );
+
+ let mut output = output.join("\n");
+ output.push('\n');
+ output
+}
+
+fn test(input_path: &Path, name: &'static str) -> Result<String, Box<dyn Error>> {
+ let Parse {
+ graph,
+ source,
+ target,
+ ..
+ } = read(input_path)?;
+
+ let route = match name {
+ "dijkstra" => Dijkstra::directed().path_between(&graph, &source, &target),
+ "bellman_ford" => BellmanFord::directed().path_between(&graph, &source, &target),
+ "floyd_warshall" => FloydWarshall::directed().path_between(&graph, &source, &target),
+ _ => unreachable!(),
+ };
+
+ let Some(route) = route else {
+ return Ok(normalize_lines("-1\n"));
+ };
+
+ Ok(normalize_lines(&dump(route)))
+}
+
+pub fn main() {
+ Harness::new("tests/cases/shortest_path", setup, test)
+ .select(["*.in"])
+ .each(&["dijkstra", "bellman_ford", "floyd_warshall"])
+ .action(Action::Verify)
+ .test();
+}
diff --git a/crates/algorithms/tests/test_components_scc.rs b/crates/algorithms/tests/test_components_scc.BAK
similarity index 100%
rename from crates/algorithms/tests/test_components_scc.rs
rename to crates/algorithms/tests/test_components_scc.BAK
diff --git a/crates/algorithms/tests/test_cycles_feedback_arc_set.rs b/crates/algorithms/tests/test_cycles_feedback_arc_set.BAK
similarity index 100%
rename from crates/algorithms/tests/test_cycles_feedback_arc_set.rs
rename to crates/algorithms/tests/test_cycles_feedback_arc_set.BAK
diff --git a/crates/algorithms/tests/test_heuristics_matching.rs b/crates/algorithms/tests/test_heuristics_matching.BAK
similarity index 100%
rename from crates/algorithms/tests/test_heuristics_matching.rs
rename to crates/algorithms/tests/test_heuristics_matching.BAK
diff --git a/crates/algorithms/tests/test_isomorphism.rs b/crates/algorithms/tests/test_isomorphism.BAK
similarity index 100%
rename from crates/algorithms/tests/test_isomorphism.rs
rename to crates/algorithms/tests/test_isomorphism.BAK
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index b1317d3..f63d839 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -5,6 +5,7 @@
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+numi = { workspace = true }
fixedbitset = { workspace = true, optional = true }
bitvec = "1.0.1"
funty.workspace = true
diff --git a/crates/core/src/base/mod.rs b/crates/core/src/base/mod.rs
deleted file mode 100644
index 8ee1bf9..0000000
--- a/crates/core/src/base/mod.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-//! Base types for the core crate.
-//!
-//! These are mostly analogue types that are needed to represent specific constructs that are not
-//! possible in `core` Rust.
-//!
-//! You should not rely on these types outside of petgraph specific API code, as they are very much
-//! temporary and a stand-in for a more general solution. This may include moving some of the types
-//! into their own crate.
-//!
-//! This currently includes:
-//! * [`MaybeOwned`]: A type that can either be owned or borrowed, an analogue to [`Cow`].
-//!
-//! [`Cow`]: std::borrow::Cow
-pub(crate) mod owned;
-
-pub use self::owned::MaybeOwned;
-// ^ TODO: into own crate
diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs
index 06e731a..ec86092 100644
--- a/crates/core/src/error.rs
+++ b/crates/core/src/error.rs
@@ -19,7 +19,7 @@
}
#[cfg(not(feature = "std"))]
-impl Context for Error {}
+impl error_stack::Context for Error {}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
diff --git a/crates/core/src/id/linear.rs b/crates/core/src/id/linear.rs
index 18789bb..d5c2f55 100644
--- a/crates/core/src/id/linear.rs
+++ b/crates/core/src/id/linear.rs
@@ -1,4 +1,6 @@
-use crate::{base::owned::MaybeOwned, id::GraphId, storage::GraphStorage};
+use numi::borrow::Moo;
+
+use crate::{id::GraphId, storage::GraphStorage};
/// Index mapper for a graph.
///
@@ -102,10 +104,8 @@
/// # Example
///
/// ```
- /// use petgraph_core::{
- /// base::MaybeOwned,
- /// id::{IndexMapper, LinearGraphId},
- /// };
+ /// use numi::borrow::Moo;
+ /// use petgraph_core::id::{IndexMapper, LinearGraphId};
/// use petgraph_dino::{DiDinoGraph, NodeId};
///
/// let mut graph = DiDinoGraph::new();
@@ -117,23 +117,23 @@
/// let mut mapper = NodeId::index_mapper(graph.storage());
///
/// let mapped = mapper.index(&a);
- /// assert_eq!(mapper.reverse(mapped), Some(MaybeOwned::Borrowed(&a)));
+ /// assert_eq!(mapper.reverse(mapped), Some(Moo::Borrowed(&a)));
/// ```
- fn reverse(&self, to: usize) -> Option<MaybeOwned<Id>>;
+ fn reverse(&self, to: usize) -> Option<Moo<Id>>;
}
/// Linear graph identifier.
///
/// A linear graph identifier is a graph identifier that has a linear mapping to a `usize` value,
-/// that mapping _may_ be continuous or discrete.
+/// that mapping must be continuous .
pub trait LinearGraphId<S>: GraphId + Sized
where
S: GraphStorage,
{
/// The index mapper for this graph identifier.
- type Mapper<'a>: IndexMapper<Self>
+ type Mapper<'graph>: IndexMapper<Self>
where
- S: 'a;
+ S: 'graph;
/// Get the index mapper for this graph identifier.
///
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index 8af05c7..ff52808 100644
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -121,7 +121,6 @@
extern crate alloc;
pub mod attributes;
-pub mod base;
#[deprecated(since = "0.1.0")]
pub mod deprecated;
pub mod edge;
diff --git a/crates/core/src/node/mod.rs b/crates/core/src/node/mod.rs
index 215ed31..b4f01ad 100644
--- a/crates/core/src/node/mod.rs
+++ b/crates/core/src/node/mod.rs
@@ -53,8 +53,6 @@
{
storage: &'a S,
- // TODO: we might want to consider making these `MaybeOwned`, to allow for `Node` that cannot
- // borrow data.
id: &'a S::NodeId,
weight: &'a S::NodeWeight,
}
diff --git a/crates/dino/Cargo.toml b/crates/dino/Cargo.toml
index 6fdb400..758ab55 100644
--- a/crates/dino/Cargo.toml
+++ b/crates/dino/Cargo.toml
@@ -6,6 +6,7 @@
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+numi = { workspace = true }
petgraph-core = { workspace = true, features = ['alloc'] }
error-stack.workspace = true
diff --git a/crates/dino/src/lib.rs b/crates/dino/src/lib.rs
index 9860115..0d12dfa 100644
--- a/crates/dino/src/lib.rs
+++ b/crates/dino/src/lib.rs
@@ -101,7 +101,6 @@
//! ### Iterating over nodes and edges
// TODO
//!
-#![feature(return_position_impl_trait_in_trait)]
#![warn(missing_docs)]
#![no_std]
diff --git a/crates/dino/src/slab/mod.rs b/crates/dino/src/slab/mod.rs
index 14aea45..6321d35 100644
--- a/crates/dino/src/slab/mod.rs
+++ b/crates/dino/src/slab/mod.rs
@@ -7,7 +7,8 @@
use alloc::{vec, vec::Vec};
use core::{fmt::Debug, hash::Hash, marker::PhantomData, ptr};
-use petgraph_core::{base::MaybeOwned, id::IndexMapper};
+use numi::borrow::Moo;
+use petgraph_core::id::IndexMapper;
use crate::slab::entry::{Entry, State};
pub(crate) use crate::slab::{generation::Generation, id::EntryId, key::Key};
@@ -442,12 +443,8 @@
self.lookup[from.into_id().index()].expect("tried to access vacant entry")
}
- fn reverse(&self, to: usize) -> Option<MaybeOwned<K>> {
- self.reverse
- .get(to)
- .copied()
- .flatten()
- .map(MaybeOwned::from)
+ fn reverse(&self, to: usize) -> Option<Moo<K>> {
+ self.reverse.get(to).copied().flatten().map(Moo::from)
}
}
diff --git a/crates/numi/Cargo.toml b/crates/numi/Cargo.toml
new file mode 100644
index 0000000..0e83c28
--- /dev/null
+++ b/crates/numi/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "numi"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+ordered-float = { version = "4.2.0", optional = true }
+
+[dev-dependencies]
+static_assertions = "1.1.0"
+
+[features]
+ordered-float-impl = ["dep:ordered-float"]
diff --git a/crates/numi/README.md b/crates/numi/README.md
new file mode 100644
index 0000000..1bf2e89
--- /dev/null
+++ b/crates/numi/README.md
@@ -0,0 +1,13 @@
+# Numi
+
+`numi` is a crate for primitives with a focus on numerical traits, akin to existing implementations such
+as [`num-traits`].
+The current goal of `numi` is to be a companion crate to [`num-traits`], instead of a replacement.
+
+Libraries should strive to implement these traits on their own, but as a convenience feature for early adopters default
+implementations have been provided. If you are a library author and would like to implement these traits, please open an
+issue so that the default implementations can be removed.
+
+The home of numi is yet to be decided until publication but will be
+
+[`num-traits`]: https://crates.io/crates/num-traits
diff --git a/crates/core/src/base/owned.rs b/crates/numi/src/borrow.rs
similarity index 65%
rename from crates/core/src/base/owned.rs
rename to crates/numi/src/borrow.rs
index 75c2592..711bbdd 100644
--- a/crates/core/src/base/owned.rs
+++ b/crates/numi/src/borrow.rs
@@ -1,25 +1,28 @@
-//! Collection of utility types and traits for working with values that may be owned or borrowed.
use core::{
cmp::Ordering,
fmt::{Display, Formatter},
hash::{Hash, Hasher},
};
-/// A type that may be owned or borrowed.
+/// # Maybe Owned Object
///
-/// This is analogous to [`Cow`], but without the `ToOwned` trait requirement (and therefore without
-/// the `alloc` requirement).
+/// This is analogous to [`Cow`], but without the additional [`ToOwned`] trait requirement which
+/// makes it unsuitable for `no_std` environments.
+///
+/// ## Name
+///
+/// The name `Moo` is a play on `Cow`, and is also a reference to the fact that cows moo.
+///
+/// > I totally didn't come up with the name first and then try to find an abbreviation that fit.
///
/// [`Cow`]: alloc::borrow::Cow
#[derive(Debug, Copy, Clone)]
-pub enum MaybeOwned<'a, T> {
- /// A borrowed value.
+pub enum Moo<'a, T> {
Borrowed(&'a T),
- /// An owned value.
Owned(T),
}
-impl<T> Display for MaybeOwned<'_, T>
+impl<T> Display for Moo<'_, T>
where
T: Display,
{
@@ -28,30 +31,30 @@
}
}
-impl<'a, T> From<T> for MaybeOwned<'a, T> {
+impl<'a, T> From<T> for Moo<'a, T> {
fn from(value: T) -> Self {
Self::Owned(value)
}
}
-impl<'a, T> From<&'a T> for MaybeOwned<'a, T> {
+impl<'a, T> From<&'a T> for Moo<'a, T> {
fn from(value: &'a T) -> Self {
Self::Borrowed(value)
}
}
-impl<'a, T> MaybeOwned<'a, T> {
+impl<'a, T> Moo<'a, T> {
/// Returns a mutable reference to the owned value, if any.
///
/// # Example
///
/// ```
- /// use petgraph_core::base::MaybeOwned;
+ /// use numi::borrow::Moo;
///
- /// let mut value = MaybeOwned::Owned(42);
+ /// let mut value = Moo::Owned(42);
/// assert_eq!(value.as_mut(), Some(&mut 42));
///
- /// let mut value = MaybeOwned::Borrowed(&42);
+ /// let mut value = Moo::Borrowed(&42);
/// assert_eq!(value.as_mut(), None);
/// ```
pub fn as_mut(&mut self) -> Option<&mut T> {
@@ -61,19 +64,19 @@
}
}
- /// Converts the `MaybeOwned` into an owned value.
+ /// Converts the `Moo` into an owned value.
///
/// This is a no-op if the value is already owned, and requires cloning if it is borrowed.
///
/// # Example
///
/// ```
- /// use petgraph_core::base::MaybeOwned;
+ /// use numi::borrow::Moo;
///
- /// let value = MaybeOwned::Owned(42);
+ /// let value = Moo::Owned(42);
/// assert_eq!(value.into_owned(), 42);
///
- /// let value = MaybeOwned::Borrowed(&42);
+ /// let value = Moo::Borrowed(&42);
/// assert_eq!(value.into_owned(), 42);
/// ```
pub fn into_owned(self) -> T
@@ -87,7 +90,7 @@
}
}
-impl<T> AsRef<T> for MaybeOwned<'_, T> {
+impl<T> AsRef<T> for Moo<'_, T> {
fn as_ref(&self) -> &T {
match self {
Self::Borrowed(value) => value,
@@ -96,7 +99,7 @@
}
}
-impl<T> PartialEq<T> for MaybeOwned<'_, T>
+impl<T> PartialEq<T> for Moo<'_, T>
where
T: PartialEq,
{
@@ -105,7 +108,7 @@
}
}
-impl<T> PartialEq for MaybeOwned<'_, T>
+impl<T> PartialEq for Moo<'_, T>
where
T: PartialEq,
{
@@ -114,9 +117,9 @@
}
}
-impl<T> Eq for MaybeOwned<'_, T> where T: Eq {}
+impl<T> Eq for Moo<'_, T> where T: Eq {}
-impl<T> PartialOrd<T> for MaybeOwned<'_, T>
+impl<T> PartialOrd<T> for Moo<'_, T>
where
T: PartialOrd,
{
@@ -125,7 +128,7 @@
}
}
-impl<T> PartialOrd for MaybeOwned<'_, T>
+impl<T> PartialOrd for Moo<'_, T>
where
T: PartialOrd,
{
@@ -134,7 +137,7 @@
}
}
-impl<T> Ord for MaybeOwned<'_, T>
+impl<T> Ord for Moo<'_, T>
where
T: Ord,
{
@@ -143,7 +146,7 @@
}
}
-impl<T> Hash for MaybeOwned<'_, T>
+impl<T> Hash for Moo<'_, T>
where
T: Hash,
{
diff --git a/crates/numi/src/cast.rs b/crates/numi/src/cast.rs
new file mode 100644
index 0000000..66b562f
--- /dev/null
+++ b/crates/numi/src/cast.rs
@@ -0,0 +1,172 @@
+use crate::primitive::{FromPrimitive, ToPrimitive, TryFromPrimitive};
+
+/// Cast a value from a different type.
+///
+/// This corresponds to the `as` keyword in Rust.
+///
+/// This is currently implemented for all the following types which implement `ToPrimitive` and
+/// `FromPrimitive`, which allows transparent conversion between them.
+///
+/// This means you are able to cast from [`Wrapping<u8>`] to [`Wrapping<u16>`] and [`u32`].
+///
+/// Unlike [`From`] this type does not have a blanked implementation for: `impl<T> From<T> for T`,
+/// instead it chooses to implement a blanket implementation which utilizes [`ToPrimitive`] and
+/// [`FromPrimitive`], as casting is a lossy process (corresponding to the `as` keyword), it only
+/// makes logical sense to blanket implement it on these traits instead of an identity
+/// implementation.
+///
+/// # Note
+///
+/// Unlike [`From`] and [`Into`], this trait has the following limitations, due to specialization
+/// concerns:
+/// * Implementing [`CastFrom`] does not automatically implement [`CastTo`].
+/// * [`CastTo`] is not implemented on the identity.
+///
+/// # Example
+///
+/// ```
+/// use std::num::{Saturating, Wrapping};
+///
+/// use numi::cast::CastTo;
+/// # #[cfg(feature = "ordered-float-impl")]
+/// use ordered_float::OrderedFloat;
+///
+/// let a = Wrapping(1u8);
+/// let b: Wrapping<u16> = a.cast_to();
+///
+/// // we can even cast across types!
+/// let a = Wrapping(1u8);
+/// let b: Saturating<u16> = a.cast_to();
+///
+/// # #[cfg(feature = "ordered-float-impl")]
+/// # fn ordered_float() {
+/// // ... and even across crates!
+/// // (this conversion requires the `ordered-float-impl` feature)
+/// let a = Wrapping(1u8);
+/// let b: OrderedFloat<f32> = a.cast_to();
+/// # }
+/// # #[cfg(feature = "ordered-float-impl")]
+/// # ordered_float();
+/// ```
+pub trait CastFrom<T> {
+ /// Cast a value from a different type.
+ ///
+ /// Might be lossy.
+ fn cast_from(value: T) -> Self;
+}
+
+impl<T, U> CastFrom<U> for T
+where
+ U: ToPrimitive,
+ T: FromPrimitive,
+{
+ fn cast_from(value: U) -> Self {
+ T::from_primitive(value)
+ }
+}
+
+/// Cast a value from a different type.
+///
+/// Fallible version of [`CastFrom`].
+pub trait TryCastFrom<T>: Sized {
+ /// The error type returned when the conversion fails.
+ type Error;
+
+ /// Cast a value from a different type.
+ ///
+ /// Might be lossy.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the conversion fails.
+ fn try_cast_from(value: T) -> Result<Self, Self::Error>;
+}
+
+impl<T, U> TryCastFrom<U> for T
+where
+ U: ToPrimitive,
+ T: TryFromPrimitive,
+{
+ type Error = T::Error;
+
+ fn try_cast_from(value: U) -> Result<Self, Self::Error> {
+ T::try_from_primitive(value)
+ }
+}
+
+/// Cast a value to as different type.
+///
+/// This corresponds to the `as` keyword in Rust.
+///
+/// This is the inverse of [`CastFrom`], and is implemented for all types that implement
+/// [`CastFrom`].
+/// You should prefer to implement [`CastFrom`] instead of this trait.
+pub trait CastTo<T> {
+ fn cast_to(self) -> T;
+}
+
+impl<T, U> CastTo<U> for T
+where
+ U: CastFrom<T>,
+{
+ fn cast_to(self) -> U {
+ U::cast_from(self)
+ }
+}
+
+/// Cast a value to as different type.
+///
+/// Fallible version of [`CastTo`].
+///
+/// This is the inverse of [`TryCastFrom`], and is implemented for all types that implement
+/// [`TryCastFrom`].
+pub trait TryCastTo<T>: Sized {
+ /// The error type returned when the conversion fails.
+ type Error;
+
+ /// Cast a value to as different type.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the conversion fails.
+ fn try_cast_to(self) -> Result<T, Self::Error>;
+}
+
+impl<T, U> TryCastTo<U> for T
+where
+ U: TryCastFrom<T>,
+{
+ type Error = U::Error;
+
+ fn try_cast_to(self) -> Result<U, Self::Error> {
+ U::try_cast_from(self)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use core::num::{Saturating, Wrapping};
+
+ use static_assertions::assert_impl_all;
+
+ use crate::cast::CastTo;
+
+ // can we do normal `as`?
+ assert_impl_all!(u8: CastTo<u16>);
+ assert_impl_all!(u16: CastTo<u8>);
+ assert_impl_all!(u16: CastTo<Wrapping<u16>>);
+
+ // can we do `as` with wrapping?
+ assert_impl_all!(Wrapping<u16>: CastTo<u16>);
+ assert_impl_all!(u16: CastTo<Wrapping<u16>>);
+
+ // can we do `as` with saturating?
+ assert_impl_all!(u16: CastTo<Saturating<u16>>);
+ assert_impl_all!(Saturating<u16>: CastTo<u16>);
+
+ assert_impl_all!(Saturating<u16>: CastTo<u8>);
+ assert_impl_all!(Saturating<u16>: CastTo<Wrapping<u32>>);
+
+ #[test]
+ fn compile() {}
+}
diff --git a/crates/numi/src/lib.rs b/crates/numi/src/lib.rs
new file mode 100644
index 0000000..77af42c
--- /dev/null
+++ b/crates/numi/src/lib.rs
@@ -0,0 +1,15 @@
+//! # Numi
+//!
+//! `numi` is a crate with a collection of common utility traits and abstractions for working with
+//! numbers and borrowed data.
+//!
+//! The library has been developed in conjunction with [`petgraph`], but is not specific to it.
+// TODO: extensive documentation with examples
+
+#![no_std]
+
+pub mod borrow;
+pub mod cast;
+mod macros;
+pub mod num;
+pub mod primitive;
diff --git a/crates/numi/src/macros.rs b/crates/numi/src/macros.rs
new file mode 100644
index 0000000..d0ba4a4
--- /dev/null
+++ b/crates/numi/src/macros.rs
@@ -0,0 +1,41 @@
+macro_rules! all_the_numbers {
+ ($name:ident) => {
+ $name!(u8);
+ $name!(u16);
+ $name!(u32);
+ $name!(u64);
+ $name!(u128);
+ $name!(usize);
+
+ $name!(i8);
+ $name!(i16);
+ $name!(i32);
+ $name!(i64);
+ $name!(i128);
+ $name!(isize);
+
+ $name!(f32);
+ $name!(f64);
+ };
+
+ (@typed $name:ident) => {
+ $name!(@int u8);
+ $name!(@int u16);
+ $name!(@int u32);
+ $name!(@int u64);
+ $name!(@int u128);
+ $name!(@int usize);
+
+ $name!(@int i8);
+ $name!(@int i16);
+ $name!(@int i32);
+ $name!(@int i64);
+ $name!(@int i128);
+ $name!(@int isize);
+
+ $name!(@float f32);
+ $name!(@float f64);
+ };
+}
+
+pub(crate) use all_the_numbers;
diff --git a/crates/numi/src/num/checked.rs b/crates/numi/src/num/checked.rs
new file mode 100644
index 0000000..4361e9a
--- /dev/null
+++ b/crates/numi/src/num/checked.rs
@@ -0,0 +1,163 @@
+//! Checked arithmetic operations.
+// TODO: more traits?
+use crate::macros::all_the_numbers;
+
+/// Addition that checks for overflow.
+///
+/// # Note
+///
+/// This trait is not implemented for [`Wrapping<T>`] because `checked_add` is defined as whenever
+/// an overflow happens, which is intentional for this type.
+///
+/// This trait is not implemented for [`Saturating<T>`] because `checked_add` is defined as whenever
+/// which does not have a concept of overflow.
+pub trait CheckedAdd: Sized {
+ /// Adds two numbers, checking for overflow. If overflow happens, `None` is returned.
+ ///
+ /// # Purity
+ ///
+ /// This function is pure.
+ fn checked_add(&self, rhs: &Self) -> Option<Self>;
+}
+
+/// Subtraction that checks for overflow.
+///
+/// # Note
+///
+/// This trait is not implemented for [`Wrapping<T>`] because `checked_sub` is defined as whenever
+/// an overflow happens, which is intentional for this type.
+///
+/// This trait is not implemented for [`Saturating<T>`] because `checked_sub` is defined as whenever
+/// which does not have a concept of overflow.
+pub trait CheckedSub: Sized {
+ /// Subtracts two numbers, checking for overflow. If overflow happens, `None` is returned.
+ ///
+ /// # Purity
+ ///
+ /// This function is pure.
+ fn checked_sub(&self, rhs: &Self) -> Option<Self>;
+}
+
+/// Multiplication that checks for overflow.
+///
+/// # Note
+///
+/// This trait is not implemented for [`Wrapping<T>`] because `checked_mul` is defined as whenever
+/// an overflow happens, which is intentional for this type.
+///
+/// This trait is not implemented for [`Saturating<T>`] because `checked_mul` is defined as whenever
+/// which does not have a concept of overflow.
+pub trait CheckedMul: Sized {
+ /// Multiplies two numbers, checking for overflow. If overflow happens, `None` is returned.
+ ///
+ /// # Purity
+ ///
+ /// This function is pure.
+ fn checked_mul(&self, rhs: &Self) -> Option<Self>;
+}
+
+/// Division that checks for overflow.
+///
+/// # Note
+///
+/// This trait is not implemented for [`Wrapping<T>`] because `checked_div` is defined as whenever
+/// an overflow happens, which is intentional for this type.
+///
+/// This trait is not implemented for [`Saturating<T>`] because `checked_div` is defined as whenever
+/// which does not have a concept of overflow.
+pub trait CheckedDiv: Sized {
+ /// Divides two numbers, checking for overflow. If overflow happens, `None` is returned.
+ ///
+ /// # Purity
+ ///
+ /// This function is pure.
+ fn checked_div(&self, rhs: &Self) -> Option<Self>;
+}
+
+macro_rules! impl_checked_add {
+ (@int $ty:ty) => {
+ impl CheckedAdd for $ty {
+ #[inline]
+ fn checked_add(&self, rhs: &Self) -> Option<Self> {
+ <$ty>::checked_add(*self, *rhs)
+ }
+ }
+
+ impl CheckedSub for $ty {
+ #[inline]
+ fn checked_sub(&self, rhs: &Self) -> Option<Self> {
+ <$ty>::checked_sub(*self, *rhs)
+ }
+ }
+
+ impl CheckedMul for $ty {
+ #[inline]
+ fn checked_mul(&self, rhs: &Self) -> Option<Self> {
+ <$ty>::checked_mul(*self, *rhs)
+ }
+ }
+
+ impl CheckedDiv for $ty {
+ #[inline]
+ fn checked_div(&self, rhs: &Self) -> Option<Self> {
+ <$ty>::checked_div(*self, *rhs)
+ }
+ }
+ };
+
+ (@float $ty:ty) => {};
+}
+
+all_the_numbers!(@typed impl_checked_add);
+
+#[cfg(feature = "ordered-float-impl")]
+impl<T> CheckedAdd for ordered_float::NotNan<T>
+where
+ T: ordered_float::FloatCore,
+{
+ fn checked_add(&self, rhs: &Self) -> Option<Self> {
+ let lhs = self.into_inner();
+ let rhs = rhs.into_inner();
+
+ Self::new(lhs + rhs).ok()
+ }
+}
+
+#[cfg(feature = "ordered-float-impl")]
+impl<T> CheckedSub for ordered_float::NotNan<T>
+where
+ T: ordered_float::FloatCore,
+{
+ fn checked_sub(&self, rhs: &Self) -> Option<Self> {
+ let lhs = self.into_inner();
+ let rhs = rhs.into_inner();
+
+ Self::new(lhs - rhs).ok()
+ }
+}
+
+#[cfg(feature = "ordered-float-impl")]
+impl<T> CheckedMul for ordered_float::NotNan<T>
+where
+ T: ordered_float::FloatCore,
+{
+ fn checked_mul(&self, rhs: &Self) -> Option<Self> {
+ let lhs = self.into_inner();
+ let rhs = rhs.into_inner();
+
+ Self::new(lhs * rhs).ok()
+ }
+}
+
+#[cfg(feature = "ordered-float-impl")]
+impl<T> CheckedDiv for ordered_float::NotNan<T>
+where
+ T: ordered_float::FloatCore,
+{
+ fn checked_div(&self, rhs: &Self) -> Option<Self> {
+ let lhs = self.into_inner();
+ let rhs = rhs.into_inner();
+
+ Self::new(lhs / rhs).ok()
+ }
+}
diff --git a/crates/numi/src/num/identity.rs b/crates/numi/src/num/identity.rs
new file mode 100644
index 0000000..63a40c6
--- /dev/null
+++ b/crates/numi/src/num/identity.rs
@@ -0,0 +1,256 @@
+//! Identity elements for numbers.
+
+use core::num::{Saturating, Wrapping};
+
+use crate::macros::all_the_numbers;
+
+/// Additive identity.
+///
+/// # Laws
+///
+/// ```text
+/// ∀ a ∈ Self: a + 0 = a
+/// ∀ a ∈ Self: 0 + a = a
+/// ```
+///
+/// # Note
+///
+/// This trait is different from [`num_traits::Zero`], as it doesn't have any supertraits and does
+/// not require [`Sized`].
+pub trait Zero {
+ /// Returns the additive identity of `Self`, `0`.
+ ///
+ /// # Purity
+ ///
+ /// This function is pure.
+ fn zero() -> Self;
+
+ /// Returns `true` if `self` is equal to the additive identity.
+ ///
+ /// # Purity
+ ///
+ /// This function is pure.
+ fn is_zero(&self) -> bool;
+}
+
+macro_rules! impl_zero {
+ (@int $ty:ty) => {
+ impl Zero for $ty {
+ #[inline]
+ fn zero() -> Self {
+ 0
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ *self == 0
+ }
+ }
+ };
+
+ (@float $ty:ty) => {
+ impl Zero for $ty {
+ #[inline]
+ fn zero() -> Self {
+ 0.0
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ *self == 0.0
+ }
+ }
+ };
+}
+
+all_the_numbers!(@typed impl_zero);
+
+impl<T> Zero for Wrapping<T>
+where
+ T: Zero,
+{
+ #[inline]
+ fn zero() -> Self {
+ Self(T::zero())
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+impl<T> Zero for Saturating<T>
+where
+ T: Zero,
+{
+ #[inline]
+ fn zero() -> Self {
+ Self(T::zero())
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+#[cfg(feature = "ordered-float-impl")]
+impl<T> Zero for ordered_float::OrderedFloat<T>
+where
+ T: Zero,
+{
+ #[inline]
+ fn zero() -> Self {
+ Self(T::zero())
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+#[cfg(feature = "ordered-float-impl")]
+impl<T> Zero for ordered_float::NotNan<T>
+where
+ T: Copy + Zero,
+{
+ #[inline]
+ fn zero() -> Self {
+ // SAFETY: `T::zero()` is always a valid `NotNan<T>`.
+ unsafe { Self::new_unchecked(T::zero()) }
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.into_inner().is_zero()
+ }
+}
+
+/// Multiplicative identity.
+///
+/// # Laws
+///
+/// ```text
+/// ∀ a ∈ Self: a * 1 = a
+/// ∀ a ∈ Self: 1 * a = a
+/// ```
+///
+/// # Note
+///
+/// This trait is different from [`num_traits::One`], as it doesn't have any supertraits and does
+/// not require [`Sized`].
+pub trait One {
+ /// Returns the multiplicative identity of `Self`, `1`.
+ ///
+ /// # Purity
+ ///
+ /// This function is pure.
+ fn one() -> Self;
+
+ /// Returns `true` if `self` is equal to the multiplicative identity.
+ ///
+ /// # Purity
+ ///
+ /// This function is pure.
+ fn is_one(&self) -> bool;
+}
+
+macro_rules! impl_one {
+ (@int $ty:ty) => {
+ impl One for $ty {
+ #[inline]
+ fn one() -> Self {
+ 1
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ *self == 1
+ }
+ }
+ };
+
+ (@float $ty:ty) => {
+ impl One for $ty {
+ #[inline]
+ fn one() -> Self {
+ 1.0
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ let difference = *self - 1.0;
+
+ // abs isn't available in no_std
+ difference < Self::EPSILON && difference > -Self::EPSILON
+ }
+ }
+ };
+}
+
+all_the_numbers!(@typed impl_one);
+
+impl<T> One for Wrapping<T>
+where
+ T: One,
+{
+ #[inline]
+ fn one() -> Self {
+ Self(T::one())
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.0.is_one()
+ }
+}
+
+impl<T> One for Saturating<T>
+where
+ T: One,
+{
+ #[inline]
+ fn one() -> Self {
+ Self(T::one())
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.0.is_one()
+ }
+}
+
+#[cfg(feature = "ordered-float-impl")]
+impl<T> One for ordered_float::OrderedFloat<T>
+where
+ T: One,
+{
+ #[inline]
+ fn one() -> Self {
+ Self(T::one())
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.0.is_one()
+ }
+}
+
+#[cfg(feature = "ordered-float-impl")]
+impl<T> One for ordered_float::NotNan<T>
+where
+ T: Copy + One,
+{
+ #[inline]
+ fn one() -> Self {
+ // SAFETY: `T::one()` is always a valid `NotNan<T>`.
+ unsafe { Self::new_unchecked(T::one()) }
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.into_inner().is_one()
+ }
+}
diff --git a/crates/numi/src/num/mod.rs b/crates/numi/src/num/mod.rs
new file mode 100644
index 0000000..ef7eb64
--- /dev/null
+++ b/crates/numi/src/num/mod.rs
@@ -0,0 +1,3 @@
+pub mod checked;
+pub mod identity;
+pub mod ops;
diff --git a/crates/numi/src/num/ops.rs b/crates/numi/src/num/ops.rs
new file mode 100644
index 0000000..5f86e47
--- /dev/null
+++ b/crates/numi/src/num/ops.rs
@@ -0,0 +1,30 @@
+use core::ops::Add;
+
+// proxy trait to not need to carry around where clause.
+/// Automatically implemented trait for adding a reference to a reference.
+///
+/// This trait is implemented for all types that implement `Add<&T, Output = T>` on `&T`.
+///
+/// This is a helper trait that shouldn't need to be implemented directly.
+/// The reason for it's existence is due to limitations in the Rust trait system.
+/// To have a similar bound on a trait that requires this as a supertrait one would need to use
+/// `where for<'a> &'a Self: Add<&'a Self, Output = Self>`, but that means that this where clause
+/// would need to be repeated every time the trait is used.
+///
+/// This limitation is known as [`implied bounds`](https://github.com/rust-lang/rust/issues/44491).
+pub trait AddRef<Rhs = Self> {
+ type Output;
+
+ fn add_ref(&self, rhs: &Rhs) -> Self::Output;
+}
+
+impl<T, Rhs, Output> AddRef<Rhs> for T
+where
+ for<'a> &'a T: Add<&'a Rhs, Output = Output>,
+{
+ type Output = Output;
+
+ fn add_ref(&self, rhs: &Rhs) -> Self::Output {
+ self + rhs
+ }
+}
diff --git a/crates/numi/src/primitive.rs b/crates/numi/src/primitive.rs
new file mode 100644
index 0000000..0b14317
--- /dev/null
+++ b/crates/numi/src/primitive.rs
@@ -0,0 +1,314 @@
+//! Conversion of arbitrary types to primitive types.
+//!
+//! This module is a collection of traits used to convert arbitrary types to primitive types.
+//!
+//! You usually don't want to use this, instead you should use [`numi::cast::CastTo`] or
+//! [`numi::cast::TryCastTo`] which are more ergonomic and provide the ability to cast between
+//! different types that can be converted from primitive types.
+use core::{
+ convert::Infallible,
+ num::{Saturating, Wrapping},
+};
+
+#[cfg(feature = "ordered-float-impl")]
+use ordered_float::{NotNan, OrderedFloat};
+
+pub trait Primitive: Copy {
+ // sadly we cannot have something like `from<T> where T: Primitive` because we cannot limit the
+ // implementations. Therefore don't know what we can expect and therefore cannot `as` them.
+ // We could in theory due this through sealing of the type, but this is something I'd like to
+ // avoid.
+
+ fn into_u8(self) -> u8;
+ fn into_u16(self) -> u16;
+ fn into_u32(self) -> u32;
+ fn into_u64(self) -> u64;
+ fn into_u128(self) -> u128;
+ fn into_usize(self) -> usize;
+
+ fn into_i8(self) -> i8;
+ fn into_i16(self) -> i16;
+ fn into_i32(self) -> i32;
+ fn into_i64(self) -> i64;
+ fn into_i128(self) -> i128;
+ fn into_isize(self) -> isize;
+
+ fn into_f32(self) -> f32;
+ fn into_f64(self) -> f64;
+
+ // fn into_bool(self) -> bool;
+ // fn into_char(self) -> char;
+}
+
+struct Proxy<T>(T);
+
+// proxy removes the `into_` prefix, makes it possible to more easily use in macros
+impl<T> Proxy<T>
+where
+ T: Primitive,
+{
+ fn u8(self) -> u8 {
+ self.0.into_u8()
+ }
+
+ fn u16(self) -> u16 {
+ self.0.into_u16()
+ }
+
+ fn u32(self) -> u32 {
+ self.0.into_u32()
+ }
+
+ fn u64(self) -> u64 {
+ self.0.into_u64()
+ }
+
+ fn u128(self) -> u128 {
+ self.0.into_u128()
+ }
+
+ fn usize(self) -> usize {
+ self.0.into_usize()
+ }
+
+ fn i8(self) -> i8 {
+ self.0.into_i8()
+ }
+
+ fn i16(self) -> i16 {
+ self.0.into_i16()
+ }
+
+ fn i32(self) -> i32 {
+ self.0.into_i32()
+ }
+
+ fn i64(self) -> i64 {
+ self.0.into_i64()
+ }
+
+ fn i128(self) -> i128 {
+ self.0.into_i128()
+ }
+
+ fn isize(self) -> isize {
+ self.0.into_isize()
+ }
+
+ fn f32(self) -> f32 {
+ self.0.into_f32()
+ }
+
+ fn f64(self) -> f64 {
+ self.0.into_f64()
+ }
+}
+
+macro_rules! impl_primitive {
+ ($ty:ident) => {
+ impl Primitive for $ty {
+ fn into_u8(self) -> u8 {
+ self as u8
+ }
+
+ fn into_u16(self) -> u16 {
+ self as u16
+ }
+
+ fn into_u32(self) -> u32 {
+ self as u32
+ }
+
+ fn into_u64(self) -> u64 {
+ self as u64
+ }
+
+ fn into_u128(self) -> u128 {
+ self as u128
+ }
+
+ fn into_usize(self) -> usize {
+ self as usize
+ }
+
+ fn into_i8(self) -> i8 {
+ self as i8
+ }
+
+ fn into_i16(self) -> i16 {
+ self as i16
+ }
+
+ fn into_i32(self) -> i32 {
+ self as i32
+ }
+
+ fn into_i64(self) -> i64 {
+ self as i64
+ }
+
+ fn into_i128(self) -> i128 {
+ self as i128
+ }
+
+ fn into_isize(self) -> isize {
+ self as isize
+ }
+
+ fn into_f32(self) -> f32 {
+ self as f32
+ }
+
+ fn into_f64(self) -> f64 {
+ self as f64
+ }
+ }
+
+ impl FromPrimitive for $ty {
+ fn from_primitive<T>(value: T) -> Self
+ where
+ T: ToPrimitive,
+ {
+ Proxy(value.into_primitive()).$ty()
+ }
+ }
+ };
+ ($($ty:ident),*) => {
+ $(impl_primitive!($ty);)*
+ };
+}
+
+impl_primitive!(
+ u8, u16, u32, u64, u128, usize, // unsigned integers
+ i8, i16, i32, i64, i128, isize, // signed integers
+ f32, f64 // floating point numbers
+);
+
+pub trait ToPrimitive {
+ type Primitive: Primitive;
+
+ fn as_primitive(&self) -> Self::Primitive;
+ fn into_primitive(self) -> Self::Primitive;
+}
+
+impl<T> ToPrimitive for T
+where
+ T: Primitive,
+{
+ type Primitive = T;
+
+ fn as_primitive(&self) -> Self::Primitive {
+ *self
+ }
+
+ fn into_primitive(self) -> Self::Primitive {
+ self
+ }
+}
+
+pub trait FromPrimitive {
+ fn from_primitive<T>(value: T) -> Self
+ where
+ T: ToPrimitive;
+}
+
+pub trait TryFromPrimitive: Sized {
+ type Error;
+
+ /// Try to convert a primitive value to `Self`.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if the conversion fails.
+ fn try_from_primitive<T>(value: T) -> Result<Self, Self::Error>
+ where
+ T: ToPrimitive;
+}
+
+impl<T> TryFromPrimitive for T
+where
+ T: FromPrimitive,
+{
+ // once never type is stable, we can use `!` instead of `Infallible`
+ type Error = Infallible;
+
+ fn try_from_primitive<U>(value: U) -> Result<Self, Self::Error>
+ where
+ U: ToPrimitive,
+ {
+ Ok(Self::from_primitive(value))
+ }
+}
+
+macro_rules! impl_newtype {
+ ($name:ident<$(|)? $($inner:ident)|*>) => {
+ $(
+ impl FromPrimitive for $name<$inner>
+ {
+ fn from_primitive<T>(value: T) -> Self where T: ToPrimitive {
+ Self(Proxy(value.into_primitive()).$inner())
+ }
+ }
+ )*
+
+ $(
+ impl ToPrimitive for $name<$inner>
+ {
+ type Primitive = $inner;
+
+ fn as_primitive(&self) -> Self::Primitive {
+ self.0
+ }
+
+ fn into_primitive(self) -> Self::Primitive {
+ self.0
+ }
+ }
+ )*
+ };
+}
+
+impl_newtype!(Wrapping<
+ | u8 | u16 | u32 | u64 | u128 | usize
+ | i8 | i16 | i32 | i64 | i128 | isize
+ | f32 | f64
+ >);
+
+impl_newtype!(Saturating<
+ | u8 | u16 | u32 | u64 | u128 | usize
+ | i8 | i16 | i32 | i64 | i128 | isize
+ | f32 | f64
+ >);
+
+#[cfg(feature = "ordered-float-impl")]
+impl_newtype!(OrderedFloat<
+ | f32 | f64
+ >);
+
+#[cfg(feature = "ordered-float-impl")]
+impl TryFromPrimitive for NotNan<f32> {
+ type Error = ordered_float::FloatIsNan;
+
+ fn try_from_primitive<T>(value: T) -> Result<Self, Self::Error>
+ where
+ T: ToPrimitive,
+ {
+ let value = value.into_primitive().into_f32();
+
+ Self::new(value)
+ }
+}
+
+#[cfg(feature = "ordered-float-impl")]
+impl TryFromPrimitive for NotNan<f64> {
+ type Error = ordered_float::FloatIsNan;
+
+ fn try_from_primitive<T>(value: T) -> Result<Self, Self::Error>
+ where
+ T: ToPrimitive,
+ {
+ let value = value.into_primitive().into_f64();
+
+ Self::new(value)
+ }
+}
diff --git a/justfile b/justfile
index bd41110..5be2e82 100644
--- a/justfile
+++ b/justfile
@@ -124,3 +124,35 @@
# Run the test suite and generate a coverage report
coverage *arguments: install-llvm-cov
cargo llvm-cov nextest --workspace --all-features --all-targets {{arguments}}
+
+######################################################################
+## Utilities
+######################################################################
+
+[private]
+clone-or-pull url path:
+ git clone "{{url}}" "{{path}}" 2>/dev/null || git -C "{{path}}" pull
+
+[private]
+generate-problem category name:
+ @echo "Generating problem {{name}}"
+ @cd "{{repo}}/crates/algorithms/tests/cases/problems" && ./venv/bin/python generate.py -p {{name}}
+
+ @echo "Copying files of problem {{name}}"
+ @mkdir -p "{{repo}}/crates/algorithms/tests/cases/{{name}}"
+ @rm -rf "{{repo}}/crates/algorithms/tests/cases/{{name}}/*"
+
+ @cp -r "{{repo}}/crates/algorithms/tests/cases/problems/{{category}}/{{name}}/in/" "{{repo}}/crates/algorithms/tests/cases/{{name}}/"
+ @cp -r "{{repo}}/crates/algorithms/tests/cases/problems/{{category}}/{{name}}/out/" "{{repo}}/crates/algorithms/tests/cases/{{name}}/"
+
+generate-problems:
+ # pull or clone repository
+ @just clone-or-pull https://github.com/yosupo06/library-checker-problems "{{repo}}/crates/algorithms/tests/cases/problems"
+ # create python virtual environment
+ @python3 -m venv crates/algorithms/tests/cases/problems/venv
+ # install dependencies
+ @crates/algorithms/tests/cases/problems/venv/bin/pip install -r crates/algorithms/tests/cases/problems/requirements.txt
+ # generate problems
+ @just generate-problem graph shortest_path
+
+
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
index 9ec1184..9666a12 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,3 @@
[toolchain]
-channel = "nightly-2023-10-18"
+channel = "nightly-2023-11-26"
components = ['rustfmt', 'clippy', 'llvm-tools-preview', 'miri']