network: add shortest path and interval routing
This commit is contained in:
parent
84e62abe52
commit
033c85a338
|
|
@ -195,6 +195,11 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "apsis-routing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"apsis-core",
|
||||
"apsis-math",
|
||||
"apsis-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apsis-rs"
|
||||
|
|
|
|||
|
|
@ -11,3 +11,6 @@ publish = false
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
apsis-core.workspace = true
|
||||
apsis-math.workspace = true
|
||||
apsis-topology.workspace = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,277 @@
|
|||
//! Shortest path computation over a topology.
|
||||
//!
|
||||
//! The algorithm is Dijkstra's, using a binary heap keyed by the tentative
|
||||
//! distance. Costs are positive, so a node's distance is final when it is
|
||||
//! first removed from the heap, which makes the algorithm exact.
|
||||
//!
|
||||
//! The heap is a minimum heap over a reversed ordering key, because the
|
||||
//! standard library's [`BinaryHeap`](std::collections::BinaryHeap) is a
|
||||
//! maximum heap. The key is the tentative distance, and ties are broken by
|
||||
//! the node index so that the result is deterministic for a given topology.
|
||||
|
||||
use crate::error::{RoutingError, RoutingErrorKind};
|
||||
use crate::route::{Route, RouteLeg, RouteStep};
|
||||
use apsis_topology::{Topology, TopologySeries};
|
||||
use core::cmp::Ordering;
|
||||
use std::collections::BinaryHeap;
|
||||
|
||||
/// A heap entry holding a node and its tentative distance.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
struct Candidate {
|
||||
/// The tentative distance from the source.
|
||||
distance: f64,
|
||||
/// The node index.
|
||||
node: usize,
|
||||
}
|
||||
|
||||
impl Eq for Candidate {}
|
||||
|
||||
impl Ord for Candidate {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// Reverse the distance comparison so that the smallest distance is
|
||||
// the greatest element, which is what the maximum heap needs. Equal
|
||||
// distances are ordered by node index so that the sequence of
|
||||
// relaxations is reproducible.
|
||||
other
|
||||
.distance
|
||||
.partial_cmp(&self.distance)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
.then_with(|| other.node.cmp(&self.node))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Candidate {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a single source shortest path computation.
|
||||
///
|
||||
/// The distances are to every node reachable from the source, and the
|
||||
/// predecessor map records the edge by which each node was reached. Both are
|
||||
/// what a routing table needs, and a single computation fills a whole row of
|
||||
/// it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PathTree {
|
||||
/// The source node.
|
||||
pub source: usize,
|
||||
/// The distance to each node, or infinity when it is unreachable.
|
||||
pub distances: Vec<f64>,
|
||||
/// The predecessor of each node on the shortest path, if any.
|
||||
pub predecessors: Vec<Option<usize>>,
|
||||
}
|
||||
|
||||
impl PathTree {
|
||||
/// Returns the distance to a node.
|
||||
///
|
||||
/// Returns `None` when the node index is out of range or the node is
|
||||
/// unreachable.
|
||||
pub fn distance(&self, node: usize) -> Option<f64> {
|
||||
let distance = *self.distances.get(node)?;
|
||||
distance.is_finite().then_some(distance)
|
||||
}
|
||||
|
||||
/// Returns `true` when the node is reachable from the source.
|
||||
pub fn is_reachable(&self, node: usize) -> bool {
|
||||
self.distance(node).is_some()
|
||||
}
|
||||
|
||||
/// Returns the number of reachable nodes, including the source itself.
|
||||
pub fn reachable_count(&self) -> usize {
|
||||
self.distances
|
||||
.iter()
|
||||
.filter(|value| value.is_finite())
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Reconstructs the route to a node.
|
||||
///
|
||||
/// Returns an error when the node is out of range or unreachable.
|
||||
pub fn route_to(&self, destination: usize, topology: &Topology) -> Result<Route, RoutingError> {
|
||||
if destination >= self.distances.len() {
|
||||
return Err(RoutingError::new(RoutingErrorKind::NoSuchNode)
|
||||
.with_detail(format!("node {destination} of {}", self.distances.len())));
|
||||
}
|
||||
if !self.is_reachable(destination) {
|
||||
return Err(RoutingError::new(RoutingErrorKind::NoRoute)
|
||||
.with_detail(format!("no route from {} to {destination}", self.source)));
|
||||
}
|
||||
|
||||
// Walk the predecessor chain back to the source, then reverse it.
|
||||
let mut nodes = vec![destination];
|
||||
let mut current = destination;
|
||||
while current != self.source {
|
||||
let previous = self.predecessors[current].ok_or_else(|| {
|
||||
RoutingError::new(RoutingErrorKind::NoRoute)
|
||||
.with_detail(format!("the predecessor chain breaks at {current}"))
|
||||
})?;
|
||||
nodes.push(previous);
|
||||
current = previous;
|
||||
}
|
||||
nodes.reverse();
|
||||
|
||||
let mut steps = Vec::with_capacity(nodes.len().saturating_sub(1));
|
||||
for pair in nodes.windows(2) {
|
||||
let cost = topology.cost(pair[0], pair[1]).ok_or_else(|| {
|
||||
RoutingError::new(RoutingErrorKind::NoRoute).with_detail(format!(
|
||||
"the link from {} to {} is absent",
|
||||
pair[0], pair[1]
|
||||
))
|
||||
})?;
|
||||
steps.push(RouteStep::new(pair[0], pair[1], cost));
|
||||
}
|
||||
Ok(Route::new(nodes, steps))
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the shortest paths from one source to every reachable node.
|
||||
///
|
||||
/// Returns an error when the source node is out of range.
|
||||
///
|
||||
/// Source: Dijkstra, E. W., *A note on two problems in connexion with
|
||||
/// graphs*, Numerische Mathematik 1, 1959.
|
||||
pub fn shortest_paths_from(topology: &Topology, source: usize) -> Result<PathTree, RoutingError> {
|
||||
if source >= topology.node_count() {
|
||||
return Err(RoutingError::new(RoutingErrorKind::NoSuchNode)
|
||||
.with_detail(format!("node {source} of {}", topology.node_count())));
|
||||
}
|
||||
|
||||
let mut distances = vec![f64::INFINITY; topology.node_count()];
|
||||
let mut predecessors = vec![None; topology.node_count()];
|
||||
let mut settled = vec![false; topology.node_count()];
|
||||
distances[source] = 0.0;
|
||||
|
||||
let mut heap = BinaryHeap::new();
|
||||
heap.push(Candidate {
|
||||
distance: 0.0,
|
||||
node: source,
|
||||
});
|
||||
|
||||
while let Some(Candidate { distance, node }) = heap.pop() {
|
||||
// A stale entry: the node already has a shorter distance.
|
||||
if settled[node] {
|
||||
continue;
|
||||
}
|
||||
settled[node] = true;
|
||||
|
||||
for edge in topology.edges().iter().filter(|edge| edge.from == node) {
|
||||
let candidate = distance + edge.cost;
|
||||
if candidate < distances[edge.to] {
|
||||
distances[edge.to] = candidate;
|
||||
predecessors[edge.to] = Some(node);
|
||||
heap.push(Candidate {
|
||||
distance: candidate,
|
||||
node: edge.to,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PathTree {
|
||||
source,
|
||||
distances,
|
||||
predecessors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the shortest route between two nodes.
|
||||
///
|
||||
/// Returns an error when either node is out of range, when they are the
|
||||
/// same node, or when no route exists.
|
||||
pub fn shortest_path(
|
||||
topology: &Topology,
|
||||
source: usize,
|
||||
destination: usize,
|
||||
) -> Result<Route, RoutingError> {
|
||||
if source == destination {
|
||||
return Err(RoutingError::new(RoutingErrorKind::SameEndpoint)
|
||||
.with_detail(format!("both endpoints are node {source}")));
|
||||
}
|
||||
let tree = shortest_paths_from(topology, source)?;
|
||||
tree.route_to(destination, topology)
|
||||
}
|
||||
|
||||
/// Returns the shortest route valid across an interval of a topology series.
|
||||
///
|
||||
/// The route is computed over the links that are present throughout the
|
||||
/// interval, so it remains valid for the whole of it. The interval is taken
|
||||
/// from the start of the sample in force at `start_seconds` to the start of
|
||||
/// the sample in force at `end_seconds`, and the links of every sample in
|
||||
/// that span are intersected.
|
||||
///
|
||||
/// The returned leg records the interval the route is valid for, which is
|
||||
/// bounded by the next topology change after `end_seconds`.
|
||||
///
|
||||
/// Returns an error when either node is out of range or the same, when the
|
||||
/// interval is not ordered, or when no route survives the whole interval.
|
||||
///
|
||||
/// Source: the store and forward routing model over a time-varying graph is
|
||||
/// the one described in Fraire, J. A. et al., *Routing in Delay-Tolerant
|
||||
/// Networks Comprising Heterogeneous Populations of Deterministic Link
|
||||
/// Dynamics*, IEEE Transactions on Mobile Computing, 2020.
|
||||
pub fn route_over_series(
|
||||
series: &TopologySeries,
|
||||
source: usize,
|
||||
destination: usize,
|
||||
start_seconds: f64,
|
||||
end_seconds: f64,
|
||||
) -> Result<RouteLeg, RoutingError> {
|
||||
if !start_seconds.is_finite() || !end_seconds.is_finite() || end_seconds < start_seconds {
|
||||
return Err(
|
||||
RoutingError::new(RoutingErrorKind::InvalidInterval).with_detail(format!(
|
||||
"the interval runs from {start_seconds} to {end_seconds}"
|
||||
)),
|
||||
);
|
||||
}
|
||||
if source >= series.node_count() || destination >= series.node_count() {
|
||||
return Err(
|
||||
RoutingError::new(RoutingErrorKind::NoSuchNode).with_detail(format!(
|
||||
"nodes {source} and {destination} of {}",
|
||||
series.node_count()
|
||||
)),
|
||||
);
|
||||
}
|
||||
if source == destination {
|
||||
return Err(RoutingError::new(RoutingErrorKind::SameEndpoint)
|
||||
.with_detail(format!("both endpoints are node {source}")));
|
||||
}
|
||||
|
||||
let first = series.index_at(start_seconds).ok_or_else(|| {
|
||||
RoutingError::new(RoutingErrorKind::InvalidInterval)
|
||||
.with_detail("the series has no sample at the start of the interval")
|
||||
})?;
|
||||
let last = series.index_at(end_seconds).ok_or_else(|| {
|
||||
RoutingError::new(RoutingErrorKind::InvalidInterval)
|
||||
.with_detail("the series has no sample at the end of the interval")
|
||||
})?;
|
||||
if last > series.len() - 1 {
|
||||
return Err(RoutingError::new(RoutingErrorKind::SeriesTooShort)
|
||||
.with_detail("the interval extends past the series"));
|
||||
}
|
||||
|
||||
// Intersect the links of every sample spanned by the interval, taking
|
||||
// the cost from the first sample that has each link.
|
||||
let mut intersection = Topology::new(series.node_count());
|
||||
let reference = &series.samples()[first].topology;
|
||||
for edge in reference.edges() {
|
||||
let present_everywhere = (first..=last).all(|index| {
|
||||
series.samples()[index]
|
||||
.topology
|
||||
.has_edge(edge.from, edge.to)
|
||||
});
|
||||
if present_everywhere {
|
||||
// The link was checked above, so a failure here would mean the
|
||||
// reference topology contradicts itself.
|
||||
let _ = intersection.add_edge(edge.from, edge.to, edge.cost);
|
||||
}
|
||||
}
|
||||
|
||||
let route = shortest_path(&intersection, source, destination)?;
|
||||
|
||||
// The leg is valid until the topology changes after the interval ends.
|
||||
let end_of_validity = series
|
||||
.interval_of(last)
|
||||
.map_or(end_seconds, |(_, next)| next);
|
||||
Ok(RouteLeg::new(route, start_seconds, end_of_validity))
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
//! Errors reported while routing.
|
||||
|
||||
use core::fmt;
|
||||
|
||||
/// The category of a routing error.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum RoutingErrorKind {
|
||||
/// A node index is outside the range of the graph.
|
||||
NoSuchNode,
|
||||
/// No route exists between the two nodes.
|
||||
NoRoute,
|
||||
/// The two endpoints are the same node.
|
||||
SameEndpoint,
|
||||
/// The interval is empty or not ordered.
|
||||
InvalidInterval,
|
||||
/// The topology series does not cover the requested interval.
|
||||
SeriesTooShort,
|
||||
}
|
||||
|
||||
impl fmt::Display for RoutingErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let text = match self {
|
||||
Self::NoSuchNode => "no such node",
|
||||
Self::NoRoute => "no route exists between these nodes",
|
||||
Self::SameEndpoint => "the source and destination are the same node",
|
||||
Self::InvalidInterval => "the interval is not usable",
|
||||
Self::SeriesTooShort => "the topology series does not cover the interval",
|
||||
};
|
||||
f.write_str(text)
|
||||
}
|
||||
}
|
||||
|
||||
/// A failure while routing.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RoutingError {
|
||||
kind: RoutingErrorKind,
|
||||
detail: Option<String>,
|
||||
}
|
||||
|
||||
impl RoutingError {
|
||||
/// Creates an error with the given category.
|
||||
pub fn new(kind: RoutingErrorKind) -> Self {
|
||||
Self { kind, detail: None }
|
||||
}
|
||||
|
||||
/// Attaches a description of the values that caused the failure.
|
||||
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
|
||||
self.detail = Some(detail.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the category of this failure.
|
||||
pub fn kind(&self) -> RoutingErrorKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
/// Returns the detail text attached to this error, if any.
|
||||
pub fn detail(&self) -> Option<&str> {
|
||||
self.detail.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RoutingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.detail {
|
||||
Some(detail) => write!(f, "{}: {detail}", self.kind),
|
||||
None => write!(f, "{}", self.kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for RoutingError {}
|
||||
|
|
@ -1 +1,54 @@
|
|||
//! Routing over time-varying satellite topologies.
|
||||
//!
|
||||
//! The topology of a satellite network changes as the satellites move, so a
|
||||
//! route is only valid over an interval. This crate computes least cost
|
||||
//! routes through a [`Topology`](apsis_topology::Topology) and reports the
|
||||
//! route together with the time it remains valid.
|
||||
//!
|
||||
//! Two forms of routing are provided:
|
||||
//!
|
||||
//! - [`shortest_path`] finds the least cost route through the topology in
|
||||
//! force at one instant.
|
||||
//! - [`route_over_series`] finds a route that is valid for a whole interval,
|
||||
//! by taking the intersection of the topologies across that interval.
|
||||
//!
|
||||
//! The second form is the one a store and forward network needs. A route
|
||||
//! through a topology that changes halfway along it may not exist in the new
|
||||
//! topology, so a route is only useful if its every link survives the
|
||||
//! interval it is planned for.
|
||||
//!
|
||||
//! The algorithm is Dijkstra's, with a binary heap. Costs must be positive,
|
||||
//! which the topology layer enforces, so the algorithm is exact: the first
|
||||
//! time a node is removed from the heap its distance is final.
|
||||
//!
|
||||
//! Source: Dijkstra, E. W., *A note on two problems in connexion with
|
||||
//! graphs*, Numerische Mathematik 1, 1959, and the binary heap formulation
|
||||
//! as given in Cormen et al., *Introduction to Algorithms*, 3rd edition,
|
||||
//! MIT Press, 2009, chapter 24.
|
||||
//!
|
||||
//! ```
|
||||
//! use apsis_routing::shortest_path;
|
||||
//! use apsis_topology::Topology;
|
||||
//!
|
||||
//! // A three node line: 0 - 1 - 2, with the direct link 0 - 2 absent.
|
||||
//! let mut topology = Topology::new(3);
|
||||
//! topology.add_edge(0, 1, 1.0).expect("the nodes exist");
|
||||
//! topology.add_edge(1, 2, 1.0).expect("the nodes exist");
|
||||
//!
|
||||
//! let route = shortest_path(&topology, 0, 2).expect("a route exists");
|
||||
//! assert_eq!(route.nodes, vec![0, 1, 2]);
|
||||
//! assert!((route.cost - 2.0).abs() < 1e-12);
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod dijkstra;
|
||||
mod error;
|
||||
mod route;
|
||||
|
||||
pub use dijkstra::{PathTree, route_over_series, shortest_path, shortest_paths_from};
|
||||
pub use error::{RoutingError, RoutingErrorKind};
|
||||
pub use route::{Route, RouteLeg, RouteStep};
|
||||
|
||||
pub use apsis_topology::{Topology, TopologySample, TopologySeries};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
//! The route types.
|
||||
|
||||
/// One hop of a route.
|
||||
///
|
||||
/// A step records the link traversed and the cost of that link, so a caller
|
||||
/// can attribute a route's total cost to its parts.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RouteStep {
|
||||
/// The node the step leaves.
|
||||
pub from: usize,
|
||||
/// The node the step arrives at.
|
||||
pub to: usize,
|
||||
/// The cost of the link.
|
||||
pub cost: f64,
|
||||
}
|
||||
|
||||
impl RouteStep {
|
||||
/// Creates a step.
|
||||
pub const fn new(from: usize, to: usize, cost: f64) -> Self {
|
||||
Self { from, to, cost }
|
||||
}
|
||||
}
|
||||
|
||||
/// A route between two nodes.
|
||||
///
|
||||
/// The node list includes both endpoints, so a route of one hop has two
|
||||
/// nodes and one step, and a route from a node to itself has one node and no
|
||||
/// steps.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Route {
|
||||
/// The nodes in order, from the source to the destination.
|
||||
pub nodes: Vec<usize>,
|
||||
/// The links traversed, in order.
|
||||
pub steps: Vec<RouteStep>,
|
||||
/// The total cost of the route.
|
||||
pub cost: f64,
|
||||
}
|
||||
|
||||
impl Route {
|
||||
/// Creates a route from its nodes and steps.
|
||||
///
|
||||
/// The cost is taken as the sum of the step costs, so a caller cannot
|
||||
/// make the total disagree with the parts.
|
||||
pub fn new(nodes: Vec<usize>, steps: Vec<RouteStep>) -> Self {
|
||||
let cost = steps.iter().map(|step| step.cost).sum();
|
||||
Self { nodes, steps, cost }
|
||||
}
|
||||
|
||||
/// Returns the number of hops.
|
||||
pub fn hop_count(&self) -> usize {
|
||||
self.steps.len()
|
||||
}
|
||||
|
||||
/// Returns the source node.
|
||||
///
|
||||
/// Returns `None` when the route has no nodes.
|
||||
pub fn source(&self) -> Option<usize> {
|
||||
self.nodes.first().copied()
|
||||
}
|
||||
|
||||
/// Returns the destination node.
|
||||
///
|
||||
/// Returns `None` when the route has no nodes.
|
||||
pub fn destination(&self) -> Option<usize> {
|
||||
self.nodes.last().copied()
|
||||
}
|
||||
|
||||
/// Returns the route traversed in the opposite direction.
|
||||
///
|
||||
/// The costs are unchanged, because a route reversed traverses the same
|
||||
/// links. This is only correct when every link is present in both
|
||||
/// directions with the same cost, which a caller must ensure.
|
||||
pub fn reversed(&self) -> Self {
|
||||
let nodes = self.nodes.iter().rev().copied().collect();
|
||||
let steps = self
|
||||
.steps
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|step| RouteStep::new(step.to, step.from, step.cost))
|
||||
.collect();
|
||||
Self::new(nodes, steps)
|
||||
}
|
||||
|
||||
/// Returns `true` when the route passes through a node.
|
||||
pub fn passes_through(&self, node: usize) -> bool {
|
||||
self.nodes.contains(&node)
|
||||
}
|
||||
|
||||
/// Returns the position of a node in the route, if it is on it.
|
||||
pub fn position_of(&self, node: usize) -> Option<usize> {
|
||||
self.nodes.iter().position(|candidate| *candidate == node)
|
||||
}
|
||||
}
|
||||
|
||||
/// A route with the interval over which it is valid.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RouteLeg {
|
||||
/// The route.
|
||||
pub route: Route,
|
||||
/// The time from which the route is valid, in seconds.
|
||||
pub start_seconds: f64,
|
||||
/// The time until which the route is valid, in seconds.
|
||||
///
|
||||
/// This is the time of the topology change that invalidates the route.
|
||||
/// It is infinite when the route is valid for all later times.
|
||||
pub end_seconds: f64,
|
||||
}
|
||||
|
||||
impl RouteLeg {
|
||||
/// Creates a route leg.
|
||||
pub const fn new(route: Route, start_seconds: f64, end_seconds: f64) -> Self {
|
||||
Self {
|
||||
route,
|
||||
start_seconds,
|
||||
end_seconds,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the duration for which the route holds, in seconds.
|
||||
///
|
||||
/// Returns infinity when the route is valid for all later times.
|
||||
pub fn duration_seconds(&self) -> f64 {
|
||||
self.end_seconds - self.start_seconds
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
//! Tests for shortest path routing.
|
||||
|
||||
use apsis_routing::{
|
||||
PathTree, Route, RouteStep, RoutingErrorKind, route_over_series, shortest_path,
|
||||
shortest_paths_from,
|
||||
};
|
||||
use apsis_topology::{Topology, TopologySample, TopologySeries};
|
||||
|
||||
/// A topology with the given directed edges and unit costs.
|
||||
fn topology_with(node_count: usize, edges: &[(usize, usize)]) -> Topology {
|
||||
let mut topology = Topology::new(node_count);
|
||||
for &(from, to) in edges {
|
||||
topology.add_edge(from, to, 1.0).expect("the nodes exist");
|
||||
}
|
||||
topology
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_direct_link_is_the_shortest_route() {
|
||||
let mut topology = Topology::new(3);
|
||||
topology.add_edge(0, 1, 5.0).expect("valid");
|
||||
topology.add_edge(0, 2, 1.0).expect("valid");
|
||||
topology.add_edge(2, 1, 1.0).expect("valid");
|
||||
|
||||
// The direct link costs five, the two hop route costs two.
|
||||
let route = shortest_path(&topology, 0, 1).expect("a route exists");
|
||||
assert_eq!(route.nodes, vec![0, 2, 1]);
|
||||
assert!((route.cost - 2.0).abs() < 1e-12);
|
||||
assert_eq!(route.hop_count(), 2);
|
||||
assert_eq!(route.source(), Some(0));
|
||||
assert_eq!(route.destination(), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_longer_route_in_hops_can_be_cheaper_in_cost() {
|
||||
let mut topology = Topology::new(4);
|
||||
// A cheap path of three hops against an expensive direct link.
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(1, 2, 1.0).expect("valid");
|
||||
topology.add_edge(2, 3, 1.0).expect("valid");
|
||||
topology.add_edge(0, 3, 10.0).expect("valid");
|
||||
|
||||
let route = shortest_path(&topology, 0, 3).expect("a route exists");
|
||||
assert_eq!(route.nodes, vec![0, 1, 2, 3]);
|
||||
assert!((route.cost - 3.0).abs() < 1e-12);
|
||||
assert_eq!(route.hop_count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cost_is_the_sum_of_the_step_costs() {
|
||||
let mut topology = Topology::new(3);
|
||||
topology.add_edge(0, 1, 2.5).expect("valid");
|
||||
topology.add_edge(1, 2, 1.5).expect("valid");
|
||||
let route = shortest_path(&topology, 0, 2).expect("a route exists");
|
||||
assert!((route.cost - 4.0).abs() < 1e-12);
|
||||
let summed: f64 = route.steps.iter().map(|step| step.cost).sum();
|
||||
assert!((route.cost - summed).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_route_records_its_steps() {
|
||||
let mut topology = Topology::new(3);
|
||||
topology.add_edge(0, 1, 2.0).expect("valid");
|
||||
topology.add_edge(1, 2, 3.0).expect("valid");
|
||||
let route = shortest_path(&topology, 0, 2).expect("a route exists");
|
||||
assert_eq!(route.steps.len(), 2);
|
||||
assert_eq!(route.steps[0], RouteStep::new(0, 1, 2.0));
|
||||
assert_eq!(route.steps[1], RouteStep::new(1, 2, 3.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_directed_topology_may_have_no_route() {
|
||||
// The link only goes one way, so the reverse route does not exist.
|
||||
let mut topology = Topology::new(2);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
assert!(shortest_path(&topology, 0, 1).is_ok());
|
||||
let error = shortest_path(&topology, 1, 0).expect_err("no route");
|
||||
assert_eq!(error.kind(), RoutingErrorKind::NoRoute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disconnected_destination_has_no_route() {
|
||||
let mut topology = Topology::new(4);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(2, 3, 1.0).expect("valid");
|
||||
let error = shortest_path(&topology, 0, 3).expect_err("no route");
|
||||
assert_eq!(error.kind(), RoutingErrorKind::NoRoute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_same_endpoint_is_rejected() {
|
||||
let topology = topology_with(2, &[(0, 1)]);
|
||||
let error = shortest_path(&topology, 0, 0).expect_err("the endpoints are equal");
|
||||
assert_eq!(error.kind(), RoutingErrorKind::SameEndpoint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_out_of_range_node_is_rejected() {
|
||||
let topology = topology_with(2, &[(0, 1)]);
|
||||
assert_eq!(
|
||||
shortest_path(&topology, 0, 5)
|
||||
.expect_err("no such node")
|
||||
.kind(),
|
||||
RoutingErrorKind::NoSuchNode
|
||||
);
|
||||
assert_eq!(
|
||||
shortest_paths_from(&topology, 5)
|
||||
.expect_err("no such node")
|
||||
.kind(),
|
||||
RoutingErrorKind::NoSuchNode
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_path_tree_covers_every_reachable_node() {
|
||||
let mut topology = Topology::new(4);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(1, 2, 1.0).expect("valid");
|
||||
// Node 3 is isolated.
|
||||
let tree = shortest_paths_from(&topology, 0).expect("valid");
|
||||
assert_eq!(tree.source, 0);
|
||||
assert_eq!(tree.distance(0), Some(0.0));
|
||||
assert_eq!(tree.distance(1), Some(1.0));
|
||||
assert_eq!(tree.distance(2), Some(2.0));
|
||||
assert_eq!(tree.distance(3), None);
|
||||
assert!(tree.is_reachable(2));
|
||||
assert!(!tree.is_reachable(3));
|
||||
assert_eq!(tree.reachable_count(), 3);
|
||||
assert_eq!(tree.distance(9), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_path_tree_reconstructs_each_route() {
|
||||
let mut topology = Topology::new(4);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(1, 2, 1.0).expect("valid");
|
||||
topology.add_edge(2, 3, 1.0).expect("valid");
|
||||
let tree = shortest_paths_from(&topology, 0).expect("valid");
|
||||
let route = tree.route_to(3, &topology).expect("a route exists");
|
||||
assert_eq!(route.nodes, vec![0, 1, 2, 3]);
|
||||
assert!((route.cost - 3.0).abs() < 1e-12);
|
||||
|
||||
// The route to the source is the source alone.
|
||||
let self_route = tree.route_to(0, &topology).expect("a route exists");
|
||||
assert_eq!(self_route.nodes, vec![0]);
|
||||
assert_eq!(self_route.hop_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_path_tree_rejects_an_unreachable_destination() {
|
||||
let topology = topology_with(2, &[(0, 1)]);
|
||||
let tree = shortest_paths_from(&topology, 1).expect("valid");
|
||||
let error = tree.route_to(0, &topology).expect_err("no route");
|
||||
assert_eq!(error.kind(), RoutingErrorKind::NoRoute);
|
||||
let error = tree.route_to(9, &topology).expect_err("no such node");
|
||||
assert_eq!(error.kind(), RoutingErrorKind::NoSuchNode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cheaper_route_replaces_a_more_expensive_one() {
|
||||
// The heap must settle the nearer node first even when it is discovered
|
||||
// second, which is the case that a naive breadth first search gets
|
||||
// wrong.
|
||||
let mut topology = Topology::new(4);
|
||||
topology.add_edge(0, 1, 10.0).expect("valid");
|
||||
topology.add_edge(0, 2, 1.0).expect("valid");
|
||||
topology.add_edge(2, 3, 1.0).expect("valid");
|
||||
topology.add_edge(3, 1, 1.0).expect("valid");
|
||||
|
||||
let route = shortest_path(&topology, 0, 1).expect("a route exists");
|
||||
assert_eq!(route.nodes, vec![0, 2, 3, 1]);
|
||||
assert!((route.cost - 3.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_cost_route_to_the_source_is_reported() {
|
||||
let topology = topology_with(2, &[(0, 1)]);
|
||||
let tree = shortest_paths_from(&topology, 0).expect("valid");
|
||||
assert_eq!(tree.distance(0), Some(0.0));
|
||||
assert_eq!(tree.predecessors[0], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_route_can_be_reversed() {
|
||||
let mut topology = Topology::new(3);
|
||||
topology.add_bidirectional_edge(0, 1, 2.0).expect("valid");
|
||||
topology.add_bidirectional_edge(1, 2, 3.0).expect("valid");
|
||||
let route = shortest_path(&topology, 0, 2).expect("a route exists");
|
||||
let reversed = route.reversed();
|
||||
assert_eq!(reversed.nodes, vec![2, 1, 0]);
|
||||
assert!((reversed.cost - route.cost).abs() < 1e-12);
|
||||
assert_eq!(reversed.steps[0], RouteStep::new(2, 1, 3.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_route_reports_whether_it_passes_through_a_node() {
|
||||
let mut topology = Topology::new(4);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(1, 2, 1.0).expect("valid");
|
||||
let route = shortest_path(&topology, 0, 2).expect("a route exists");
|
||||
assert!(route.passes_through(1));
|
||||
assert!(!route.passes_through(3));
|
||||
assert_eq!(route.position_of(1), Some(1));
|
||||
assert_eq!(route.position_of(3), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_topology_has_no_routes() {
|
||||
let topology = Topology::new(3);
|
||||
let error = shortest_path(&topology, 0, 1).expect_err("no route");
|
||||
assert_eq!(error.kind(), RoutingErrorKind::NoRoute);
|
||||
let tree = shortest_paths_from(&topology, 0).expect("valid");
|
||||
assert_eq!(tree.reachable_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_route_type_is_constructible_directly() {
|
||||
let route = Route::new(vec![0, 1], vec![RouteStep::new(0, 1, 2.0)]);
|
||||
assert!((route.cost - 2.0).abs() < 1e-12);
|
||||
assert_eq!(route.hop_count(), 1);
|
||||
// An empty route has no endpoints.
|
||||
let empty = Route::new(Vec::new(), Vec::new());
|
||||
assert_eq!(empty.source(), None);
|
||||
assert_eq!(empty.destination(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_route_over_a_series_uses_the_links_present_throughout() {
|
||||
// The link 1 to 2 exists at both ends of the interval but is absent in
|
||||
// the middle, so no route through it survives the interval. The
|
||||
// two hop route via node 3 is present throughout.
|
||||
let series = TopologySeries::new(vec![
|
||||
TopologySample::new(0.0, topology_with(4, &[(0, 1), (1, 2), (0, 3), (3, 2)])),
|
||||
TopologySample::new(10.0, topology_with(4, &[(0, 1), (0, 3), (3, 2)])),
|
||||
TopologySample::new(20.0, topology_with(4, &[(0, 1), (1, 2), (0, 3), (3, 2)])),
|
||||
])
|
||||
.expect("the times increase");
|
||||
|
||||
let leg = route_over_series(&series, 0, 2, 0.0, 20.0).expect("a route exists");
|
||||
assert_eq!(leg.route.nodes, vec![0, 3, 2]);
|
||||
assert!((leg.route.cost - 2.0).abs() < 1e-12);
|
||||
// The leg is valid until the next change after the interval.
|
||||
assert_eq!(leg.start_seconds, 0.0);
|
||||
assert!(leg.end_seconds.is_infinite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_route_over_a_series_reports_its_validity() {
|
||||
let series = TopologySeries::new(vec![
|
||||
TopologySample::new(0.0, topology_with(2, &[(0, 1)])),
|
||||
TopologySample::new(10.0, topology_with(2, &[(0, 1)])),
|
||||
TopologySample::new(20.0, topology_with(2, &[])),
|
||||
])
|
||||
.expect("the times increase");
|
||||
|
||||
// A route planned over the first two samples is valid until the third.
|
||||
let leg = route_over_series(&series, 0, 1, 0.0, 10.0).expect("a route exists");
|
||||
assert_eq!(leg.end_seconds, 20.0);
|
||||
assert!((leg.duration_seconds() - 20.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_route_over_a_series_fails_when_no_link_survives() {
|
||||
// The only link is present at the start and absent at the end, so it
|
||||
// does not survive the interval.
|
||||
let series = TopologySeries::new(vec![
|
||||
TopologySample::new(0.0, topology_with(2, &[(0, 1)])),
|
||||
TopologySample::new(10.0, topology_with(2, &[])),
|
||||
])
|
||||
.expect("the times increase");
|
||||
let error = route_over_series(&series, 0, 1, 0.0, 10.0).expect_err("no route");
|
||||
assert_eq!(error.kind(), RoutingErrorKind::NoRoute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_route_over_a_series_validates_its_arguments() {
|
||||
let series = TopologySeries::new(vec![TopologySample::new(0.0, topology_with(2, &[(0, 1)]))])
|
||||
.expect("valid");
|
||||
|
||||
assert_eq!(
|
||||
route_over_series(&series, 0, 1, 10.0, 0.0)
|
||||
.expect_err("the interval runs backwards")
|
||||
.kind(),
|
||||
RoutingErrorKind::InvalidInterval
|
||||
);
|
||||
assert_eq!(
|
||||
route_over_series(&series, 0, 5, 0.0, 0.0)
|
||||
.expect_err("no such node")
|
||||
.kind(),
|
||||
RoutingErrorKind::NoSuchNode
|
||||
);
|
||||
assert_eq!(
|
||||
route_over_series(&series, 0, 0, 0.0, 0.0)
|
||||
.expect_err("the endpoints are equal")
|
||||
.kind(),
|
||||
RoutingErrorKind::SameEndpoint
|
||||
);
|
||||
assert_eq!(
|
||||
route_over_series(&series, 0, 1, f64::NAN, 0.0)
|
||||
.expect_err("the interval is not finite")
|
||||
.kind(),
|
||||
RoutingErrorKind::InvalidInterval
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_path_tree_type_is_public() {
|
||||
// The type must be nameable by a caller that stores a routing table.
|
||||
fn takes_tree(_tree: &PathTree) {}
|
||||
let topology = topology_with(2, &[(0, 1)]);
|
||||
let tree = shortest_paths_from(&topology, 0).expect("valid");
|
||||
takes_tree(&tree);
|
||||
}
|
||||
Loading…
Reference in New Issue