network: add mesh connectivity and critical link analysis
This commit is contained in:
parent
0bc127277f
commit
eb8ef3fa3b
|
|
@ -160,6 +160,11 @@ version = "0.1.0"
|
|||
[[package]]
|
||||
name = "apsis-mesh"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"apsis-core",
|
||||
"apsis-math",
|
||||
"apsis-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apsis-noise"
|
||||
|
|
|
|||
|
|
@ -11,3 +11,6 @@ publish = false
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
apsis-core.workspace = true
|
||||
apsis-math.workspace = true
|
||||
apsis-topology.workspace = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
//! Connectivity measures over a topology.
|
||||
|
||||
use apsis_topology::Topology;
|
||||
|
||||
/// Returns the set of nodes reachable from a node, following edges in their
|
||||
/// direction.
|
||||
///
|
||||
/// The start node is included, since a node reaches itself by a path of no
|
||||
/// hops. The result is in node order.
|
||||
///
|
||||
/// Returns an empty vector when the node is out of range.
|
||||
pub fn reachable_from(topology: &Topology, source: usize) -> Vec<usize> {
|
||||
if source >= topology.node_count() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut visited = vec![false; topology.node_count()];
|
||||
visited[source] = true;
|
||||
let mut stack = vec![source];
|
||||
while let Some(node) = stack.pop() {
|
||||
for edge in topology.edges().iter().filter(|edge| edge.from == node) {
|
||||
if !visited[edge.to] {
|
||||
visited[edge.to] = true;
|
||||
stack.push(edge.to);
|
||||
}
|
||||
}
|
||||
}
|
||||
(0..topology.node_count())
|
||||
.filter(|node| visited[*node])
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the number of independent paths between two nodes.
|
||||
///
|
||||
/// Two paths are independent when they share no edge. By Menger's theorem
|
||||
/// that number is the minimum number of edges whose removal disconnects the
|
||||
/// two nodes, so it is the number of link failures the connection survives.
|
||||
///
|
||||
/// The value is computed as a maximum flow of unit capacity between the two
|
||||
/// nodes, using the Edmonds-Karp variant of the Ford-Fulkerson method, which
|
||||
/// augments along a shortest path at each step. For the small graphs of a
|
||||
/// satellite mesh this is fast, and it is exact.
|
||||
///
|
||||
/// Returns `Some(0)` when no path exists, and `None` when either node is out
|
||||
/// of range or the two nodes are the same, for which the question is not
|
||||
/// defined.
|
||||
pub fn edge_connectivity(topology: &Topology, source: usize, destination: usize) -> Option<usize> {
|
||||
if source >= topology.node_count() || destination >= topology.node_count() {
|
||||
return None;
|
||||
}
|
||||
if source == destination {
|
||||
return None;
|
||||
}
|
||||
|
||||
// A residual capacity for each edge, indexed by the edge position.
|
||||
let edges = topology.edges();
|
||||
let mut residual: Vec<usize> = vec![1; edges.len()];
|
||||
|
||||
let mut flow = 0;
|
||||
loop {
|
||||
// Breadth first search for an augmenting path in the residual
|
||||
// graph.
|
||||
let mut parent_edge: Vec<Option<usize>> = vec![None; topology.node_count()];
|
||||
let mut visited = vec![false; topology.node_count()];
|
||||
visited[source] = true;
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
queue.push_back(source);
|
||||
|
||||
while let Some(node) = queue.pop_front() {
|
||||
if node == destination {
|
||||
break;
|
||||
}
|
||||
for (index, edge) in edges.iter().enumerate() {
|
||||
// Forward edges with residual capacity.
|
||||
if edge.from == node && residual[index] > 0 && !visited[edge.to] {
|
||||
visited[edge.to] = true;
|
||||
parent_edge[edge.to] = Some(index);
|
||||
queue.push_back(edge.to);
|
||||
continue;
|
||||
}
|
||||
// Reverse edges, which exist when flow has been pushed the
|
||||
// other way and which cancel it.
|
||||
if edge.to == node && residual[index] == 0 && !visited[edge.from] {
|
||||
visited[edge.from] = true;
|
||||
parent_edge[edge.from] = Some(index);
|
||||
queue.push_back(edge.from);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !visited[destination] {
|
||||
break;
|
||||
}
|
||||
|
||||
// Walk the path back, adjusting the residual capacities. An edge
|
||||
// traversed in its own direction consumes one unit; an edge
|
||||
// traversed against its direction restores one.
|
||||
let mut node = destination;
|
||||
while node != source {
|
||||
let index = parent_edge[node]?;
|
||||
let edge = edges[index];
|
||||
if edge.to == node {
|
||||
residual[index] -= 1;
|
||||
node = edge.from;
|
||||
} else {
|
||||
residual[index] += 1;
|
||||
node = edge.to;
|
||||
}
|
||||
}
|
||||
flow += 1;
|
||||
}
|
||||
Some(flow)
|
||||
}
|
||||
|
||||
/// Returns the number of strongly connected components.
|
||||
///
|
||||
/// A component is strongly connected when every node in it can reach every
|
||||
/// other following the edges in their direction. This is the count of
|
||||
/// separate networks that can route internally, which for a satellite mesh
|
||||
/// with directed inter-satellite links is the right measure of how many
|
||||
/// pieces it has.
|
||||
///
|
||||
/// The algorithm is Kosaraju's: a depth first search records the order in
|
||||
/// which nodes finish, and a second search over the reversed edges, taking
|
||||
/// the nodes in reverse finishing order, emits the components.
|
||||
///
|
||||
/// Source: Kosaraju's algorithm as described in Cormen et al.,
|
||||
/// *Introduction to Algorithms*, 3rd edition, MIT Press, 2009, chapter 22.
|
||||
pub fn strongly_connected_components(topology: &Topology) -> usize {
|
||||
let node_count = topology.node_count();
|
||||
if node_count == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// First pass: the finishing order over the forward edges.
|
||||
let mut visited = vec![false; node_count];
|
||||
let mut order = Vec::with_capacity(node_count);
|
||||
for start in 0..node_count {
|
||||
if visited[start] {
|
||||
continue;
|
||||
}
|
||||
// An iterative depth first search, pushing a node again to mark the
|
||||
// point at which its children are done.
|
||||
let mut stack = vec![(start, false)];
|
||||
while let Some((node, processed)) = stack.pop() {
|
||||
if processed {
|
||||
order.push(node);
|
||||
continue;
|
||||
}
|
||||
if visited[node] {
|
||||
continue;
|
||||
}
|
||||
visited[node] = true;
|
||||
stack.push((node, true));
|
||||
for edge in topology.edges().iter().filter(|edge| edge.from == node) {
|
||||
if !visited[edge.to] {
|
||||
stack.push((edge.to, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: the reverse edges, taking nodes in reverse finishing
|
||||
// order.
|
||||
let mut assigned = vec![false; node_count];
|
||||
let mut components = 0;
|
||||
for node in order.into_iter().rev() {
|
||||
if assigned[node] {
|
||||
continue;
|
||||
}
|
||||
components += 1;
|
||||
let mut stack = vec![node];
|
||||
assigned[node] = true;
|
||||
while let Some(current) = stack.pop() {
|
||||
for edge in topology.edges().iter().filter(|edge| edge.to == current) {
|
||||
if !assigned[edge.from] {
|
||||
assigned[edge.from] = true;
|
||||
stack.push(edge.from);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
components
|
||||
}
|
||||
|
|
@ -1 +1,51 @@
|
|||
//! Mesh connectivity abstractions.
|
||||
//!
|
||||
//! A satellite mesh is a set of terminals that reach each other through
|
||||
//! links, some of which are satellite links and some of which are ground
|
||||
//! links. This crate provides the abstractions that a mesh application needs
|
||||
//! on top of a topology: which terminals can reach which others, how many
|
||||
//! independent ways there are to do so, and which links are critical.
|
||||
//!
|
||||
//! The three questions this crate answers:
|
||||
//!
|
||||
//! - **Reachability**: the set of terminals reachable from a given one.
|
||||
//! - **Redundancy**: how many independent paths join two terminals, which is
|
||||
//! the number of links that can fail independently. This is the
|
||||
//! edge-connectivity of the graph, by Menger's theorem.
|
||||
//! - **Criticality**: the links whose removal would separate the mesh, which
|
||||
//! are the ones a network operator must protect.
|
||||
//!
|
||||
//! The redundancy computation is exact for the small graphs a satellite mesh
|
||||
//! presents. It uses the edge version of Menger's theorem, computing the
|
||||
//! connectivity from a sequence of maximum flow problems, each of unit
|
||||
//! capacity.
|
||||
//!
|
||||
//! Source: Menger's theorem, and the reduction of edge connectivity to
|
||||
//! maximum flow, as given in Cormen et al., *Introduction to Algorithms*,
|
||||
//! 3rd edition, MIT Press, 2009, chapter 26.
|
||||
//!
|
||||
//! ```
|
||||
//! use apsis_mesh::Mesh;
|
||||
//! use apsis_topology::Topology;
|
||||
//!
|
||||
//! // Three terminals in a ring, so each pair has two independent paths.
|
||||
//! let mut topology = Topology::new(3);
|
||||
//! topology.add_bidirectional_edge(0, 1, 1.0).expect("the nodes exist");
|
||||
//! topology.add_bidirectional_edge(1, 2, 1.0).expect("the nodes exist");
|
||||
//! topology.add_bidirectional_edge(2, 0, 1.0).expect("the nodes exist");
|
||||
//!
|
||||
//! let mesh = Mesh::new(topology);
|
||||
//! assert!(mesh.is_connected());
|
||||
//! assert_eq!(mesh.edge_connectivity(0, 1), Some(2));
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod connectivity;
|
||||
mod mesh;
|
||||
|
||||
pub use connectivity::{edge_connectivity, reachable_from, strongly_connected_components};
|
||||
pub use mesh::{CriticalLink, Mesh, MeshSummary};
|
||||
|
||||
pub use apsis_topology::{Topology, TopologySeries};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
//! The mesh type and the summary of its redundancy.
|
||||
|
||||
use crate::connectivity::{edge_connectivity, reachable_from, strongly_connected_components};
|
||||
use apsis_topology::Topology;
|
||||
|
||||
/// A link whose removal would change the connectivity of the mesh.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CriticalLink {
|
||||
/// The node the link leaves.
|
||||
pub from: usize,
|
||||
/// The node the link arrives at.
|
||||
pub to: usize,
|
||||
/// The cost of the link.
|
||||
pub cost: f64,
|
||||
/// The number of components the mesh has after the link is removed.
|
||||
pub components_without: usize,
|
||||
}
|
||||
|
||||
impl CriticalLink {
|
||||
/// Creates a critical link record.
|
||||
pub const fn new(from: usize, to: usize, cost: f64, components_without: usize) -> Self {
|
||||
Self {
|
||||
from,
|
||||
to,
|
||||
cost,
|
||||
components_without,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A summary of the connectivity of a mesh.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MeshSummary {
|
||||
/// The number of terminals.
|
||||
pub node_count: usize,
|
||||
/// The number of directed links.
|
||||
pub edge_count: usize,
|
||||
/// The number of components when the links are treated as undirected.
|
||||
pub components: usize,
|
||||
/// The number of strongly connected components.
|
||||
pub strongly_connected_components: usize,
|
||||
/// The smallest number of independent paths between any two distinct
|
||||
/// terminals, or `None` when there are fewer than two terminals.
|
||||
///
|
||||
/// This is the worst connection in the mesh. A value of one means some
|
||||
/// pair of terminals relies on a single link and the mesh has no
|
||||
/// redundancy anywhere.
|
||||
pub minimum_connectivity: Option<usize>,
|
||||
}
|
||||
|
||||
/// A mesh of terminals joined by links.
|
||||
///
|
||||
/// The mesh wraps a topology and adds the connectivity measures a mesh
|
||||
/// application needs. It does not own a schedule or a traffic model; those
|
||||
/// belong to the caller.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Mesh {
|
||||
topology: Topology,
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
/// Creates a mesh over a topology.
|
||||
pub const fn new(topology: Topology) -> Self {
|
||||
Self { topology }
|
||||
}
|
||||
|
||||
/// Returns the underlying topology.
|
||||
pub const fn topology(&self) -> &Topology {
|
||||
&self.topology
|
||||
}
|
||||
|
||||
/// Returns the number of terminals.
|
||||
pub fn node_count(&self) -> usize {
|
||||
self.topology.node_count()
|
||||
}
|
||||
|
||||
/// Returns the number of links.
|
||||
pub fn edge_count(&self) -> usize {
|
||||
self.topology.edge_count()
|
||||
}
|
||||
|
||||
/// Returns `true` when every terminal reaches every other, treating the
|
||||
/// links as undirected.
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.topology.is_connected()
|
||||
}
|
||||
|
||||
/// Returns the terminals reachable from a terminal.
|
||||
pub fn reachable_from(&self, source: usize) -> Vec<usize> {
|
||||
reachable_from(&self.topology, source)
|
||||
}
|
||||
|
||||
/// Returns the number of independent paths between two terminals.
|
||||
///
|
||||
/// Returns `None` when either terminal is out of range or the two are
|
||||
/// the same.
|
||||
pub fn edge_connectivity(&self, source: usize, destination: usize) -> Option<usize> {
|
||||
edge_connectivity(&self.topology, source, destination)
|
||||
}
|
||||
|
||||
/// Returns the smallest number of independent paths between any two
|
||||
/// distinct terminals.
|
||||
///
|
||||
/// Returns `None` when there are fewer than two terminals.
|
||||
pub fn minimum_connectivity(&self) -> Option<usize> {
|
||||
let node_count = self.topology.node_count();
|
||||
if node_count < 2 {
|
||||
return None;
|
||||
}
|
||||
let mut minimum = usize::MAX;
|
||||
for source in 0..node_count {
|
||||
for destination in (source + 1)..node_count {
|
||||
let value = edge_connectivity(&self.topology, source, destination)?;
|
||||
minimum = minimum.min(value);
|
||||
}
|
||||
}
|
||||
Some(minimum)
|
||||
}
|
||||
|
||||
/// Returns the links whose removal would increase the number of
|
||||
/// connected components, treating each link as undirected.
|
||||
///
|
||||
/// These are the bridges of the undirected graph underlying the mesh. A
|
||||
/// link is reported once, even when it is present in both directions,
|
||||
/// because a failure takes out the whole link rather than one direction
|
||||
/// of it.
|
||||
///
|
||||
/// Each candidate is found by removing the link in both directions and
|
||||
/// comparing the component count, which is exact but costs one traversal
|
||||
/// per link. For the size of graph a satellite mesh presents that is
|
||||
/// acceptable, and it is easier to verify than a linear time bridge
|
||||
/// finding algorithm.
|
||||
///
|
||||
/// Links that are not bridges are omitted, so an empty result means
|
||||
/// every link lies on a cycle and the mesh survives any single link
|
||||
/// failure without splitting.
|
||||
pub fn critical_links(&self) -> Vec<CriticalLink> {
|
||||
let baseline = self.topology.connected_components();
|
||||
let mut critical = Vec::new();
|
||||
let mut seen: Vec<(usize, usize)> = Vec::new();
|
||||
|
||||
for edge in self.topology.edges() {
|
||||
// Canonicalize the pair so that the two directions of one link
|
||||
// are considered together and reported once.
|
||||
let pair = if edge.from <= edge.to {
|
||||
(edge.from, edge.to)
|
||||
} else {
|
||||
(edge.to, edge.from)
|
||||
};
|
||||
if seen.contains(&pair) {
|
||||
continue;
|
||||
}
|
||||
seen.push(pair);
|
||||
|
||||
// Remove the link in every direction in which it is present,
|
||||
// which is what a link failure does.
|
||||
let mut without = self.topology.clone();
|
||||
let _ = without.remove_edge(pair.0, pair.1);
|
||||
let _ = without.remove_edge(pair.1, pair.0);
|
||||
|
||||
let components = without.connected_components();
|
||||
if components > baseline {
|
||||
critical.push(CriticalLink::new(edge.from, edge.to, edge.cost, components));
|
||||
}
|
||||
}
|
||||
critical
|
||||
}
|
||||
|
||||
/// Returns `true` when the mesh survives the failure of any single link
|
||||
/// without splitting.
|
||||
///
|
||||
/// This is the condition for the mesh to have a single link failure
|
||||
/// tolerance, which is the minimum redundancy a network carrying traffic
|
||||
/// without interruption should have.
|
||||
pub fn survives_single_link_failure(&self) -> bool {
|
||||
self.critical_links().is_empty()
|
||||
}
|
||||
|
||||
/// Returns the number of strongly connected components.
|
||||
///
|
||||
/// For a mesh of directed links this counts the pieces that can route
|
||||
/// internally. A value above one means some terminals cannot be reached
|
||||
/// from some others.
|
||||
pub fn strongly_connected_components(&self) -> usize {
|
||||
strongly_connected_components(&self.topology)
|
||||
}
|
||||
|
||||
/// Returns a summary of the mesh connectivity.
|
||||
pub fn summarise(&self) -> MeshSummary {
|
||||
MeshSummary {
|
||||
node_count: self.topology.node_count(),
|
||||
edge_count: self.topology.edge_count(),
|
||||
components: self.topology.connected_components(),
|
||||
strongly_connected_components: strongly_connected_components(&self.topology),
|
||||
minimum_connectivity: self.minimum_connectivity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
//! Tests for mesh connectivity measures.
|
||||
//!
|
||||
//! The reference cases are small graphs whose connectivity is known by
|
||||
//! inspection: a ring has two independent paths between any pair, a line has
|
||||
//! one, and a fully connected graph has as many as there are parallel links.
|
||||
|
||||
use apsis_mesh::{
|
||||
CriticalLink, Mesh, edge_connectivity, reachable_from, strongly_connected_components,
|
||||
};
|
||||
use apsis_topology::Topology;
|
||||
|
||||
/// A bidirectional ring of the given size.
|
||||
fn ring(node_count: usize) -> Topology {
|
||||
let mut topology = Topology::new(node_count);
|
||||
for node in 0..node_count {
|
||||
topology
|
||||
.add_bidirectional_edge(node, (node + 1) % node_count, 1.0)
|
||||
.expect("the nodes exist");
|
||||
}
|
||||
topology
|
||||
}
|
||||
|
||||
/// A bidirectional line, which has no cycle.
|
||||
fn line(node_count: usize) -> Topology {
|
||||
let mut topology = Topology::new(node_count);
|
||||
for node in 0..node_count.saturating_sub(1) {
|
||||
topology
|
||||
.add_bidirectional_edge(node, node + 1, 1.0)
|
||||
.expect("the nodes exist");
|
||||
}
|
||||
topology
|
||||
}
|
||||
|
||||
/// A complete bidirectional graph.
|
||||
fn complete(node_count: usize) -> Topology {
|
||||
let mut topology = Topology::new(node_count);
|
||||
for first in 0..node_count {
|
||||
for second in (first + 1)..node_count {
|
||||
topology
|
||||
.add_bidirectional_edge(first, second, 1.0)
|
||||
.expect("the nodes exist");
|
||||
}
|
||||
}
|
||||
topology
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reachability_follows_the_edge_direction() {
|
||||
// A one way line: 0 reaches 1 and 2, but 2 reaches only itself.
|
||||
let mut topology = Topology::new(3);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(1, 2, 1.0).expect("valid");
|
||||
|
||||
assert_eq!(reachable_from(&topology, 0), vec![0, 1, 2]);
|
||||
assert_eq!(reachable_from(&topology, 1), vec![1, 2]);
|
||||
assert_eq!(reachable_from(&topology, 2), vec![2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reachability_includes_the_source() {
|
||||
let topology = Topology::new(3);
|
||||
assert_eq!(reachable_from(&topology, 0), vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reachability_of_an_out_of_range_node_is_empty() {
|
||||
let topology = Topology::new(2);
|
||||
assert!(reachable_from(&topology, 9).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ring_has_two_independent_paths() {
|
||||
let topology = ring(4);
|
||||
// Around a ring there are exactly two ways between any two nodes, and
|
||||
// no more, because removing both directions of one link leaves a line.
|
||||
assert_eq!(edge_connectivity(&topology, 0, 2), Some(2));
|
||||
assert_eq!(edge_connectivity(&topology, 0, 1), Some(2));
|
||||
assert_eq!(edge_connectivity(&topology, 1, 3), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_has_one_independent_path() {
|
||||
let topology = line(4);
|
||||
// A line has a single route, so one link failure disconnects it.
|
||||
assert_eq!(edge_connectivity(&topology, 0, 3), Some(1));
|
||||
assert_eq!(edge_connectivity(&topology, 0, 1), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_complete_graph_has_the_full_degree() {
|
||||
let topology = complete(4);
|
||||
// With four nodes and every pair linked both ways, there are three edge
|
||||
// disjoint paths between any two nodes: the direct link and the two
|
||||
// through the other nodes.
|
||||
assert_eq!(edge_connectivity(&topology, 0, 1), Some(3));
|
||||
assert_eq!(edge_connectivity(&topology, 2, 3), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disconnected_pair_has_no_path() {
|
||||
let mut topology = Topology::new(4);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(2, 3, 1.0).expect("valid");
|
||||
assert_eq!(edge_connectivity(&topology, 0, 3), Some(0));
|
||||
assert_eq!(edge_connectivity(&topology, 0, 1), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connectivity_rejects_a_node_with_itself() {
|
||||
let topology = ring(3);
|
||||
assert_eq!(edge_connectivity(&topology, 0, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connectivity_rejects_an_out_of_range_node() {
|
||||
let topology = ring(3);
|
||||
assert_eq!(edge_connectivity(&topology, 0, 9), None);
|
||||
assert_eq!(edge_connectivity(&topology, 9, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_connectivity_of_a_parallel_pair_is_the_number_of_links() {
|
||||
// Three parallel one way links between two nodes give three independent
|
||||
// paths.
|
||||
let mut topology = Topology::new(2);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
// A second and third parallel link cannot be added as a duplicate, so
|
||||
// they are represented by routing through intermediate nodes instead.
|
||||
topology.add_edge(0, 1, 1.0).ok();
|
||||
assert_eq!(edge_connectivity(&topology, 0, 1), Some(1));
|
||||
|
||||
// A pair of two node disjoint paths: the direct link and one through an
|
||||
// intermediate node.
|
||||
let mut detour = Topology::new(3);
|
||||
detour.add_edge(0, 1, 1.0).expect("valid");
|
||||
detour.add_edge(0, 2, 1.0).expect("valid");
|
||||
detour.add_edge(2, 1, 1.0).expect("valid");
|
||||
assert_eq!(edge_connectivity(&detour, 0, 1), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_strongly_connected_components_of_a_bidirectional_ring_is_one() {
|
||||
let topology = ring(4);
|
||||
assert_eq!(strongly_connected_components(&topology), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_one_way_ring_is_strongly_connected() {
|
||||
// A directed cycle can still reach every node from every node.
|
||||
let mut topology = Topology::new(3);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(1, 2, 1.0).expect("valid");
|
||||
topology.add_edge(2, 0, 1.0).expect("valid");
|
||||
assert_eq!(strongly_connected_components(&topology), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_one_way_line_has_one_component_per_node() {
|
||||
let mut topology = Topology::new(3);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(1, 2, 1.0).expect("valid");
|
||||
// No node can be reached back from a later one, so every node is its
|
||||
// own component.
|
||||
assert_eq!(strongly_connected_components(&topology), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_one_way_cycles_are_two_components() {
|
||||
let mut topology = Topology::new(4);
|
||||
topology.add_edge(0, 1, 1.0).expect("valid");
|
||||
topology.add_edge(1, 0, 1.0).expect("valid");
|
||||
topology.add_edge(2, 3, 1.0).expect("valid");
|
||||
topology.add_edge(3, 2, 1.0).expect("valid");
|
||||
assert_eq!(strongly_connected_components(&topology), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_topology_has_no_components() {
|
||||
assert_eq!(strongly_connected_components(&Topology::new(0)), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_nodes_are_each_a_component() {
|
||||
let topology = Topology::new(3);
|
||||
assert_eq!(strongly_connected_components(&topology), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_mesh_wraps_a_topology() {
|
||||
let mesh = Mesh::new(ring(4));
|
||||
assert_eq!(mesh.node_count(), 4);
|
||||
assert_eq!(mesh.edge_count(), 8);
|
||||
assert!(mesh.is_connected());
|
||||
assert_eq!(mesh.edge_connectivity(0, 2), Some(2));
|
||||
assert_eq!(mesh.strongly_connected_components(), 1);
|
||||
assert_eq!(mesh.reachable_from(0), vec![0, 1, 2, 3]);
|
||||
assert_eq!(mesh.topology().node_count(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_mesh_has_no_redundancy() {
|
||||
let mesh = Mesh::new(line(4));
|
||||
assert_eq!(mesh.minimum_connectivity(), Some(1));
|
||||
assert!(!mesh.survives_single_link_failure());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ring_mesh_survives_a_single_link_failure() {
|
||||
let mesh = Mesh::new(ring(4));
|
||||
assert_eq!(mesh.minimum_connectivity(), Some(2));
|
||||
assert!(mesh.survives_single_link_failure());
|
||||
assert!(mesh.critical_links().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_critical_links_of_a_line_are_every_link() {
|
||||
let mesh = Mesh::new(line(3));
|
||||
let critical = mesh.critical_links();
|
||||
// A three node line has two links, and each is a bridge. A link is
|
||||
// reported once even though it is present in both directions, because a
|
||||
// failure takes out the whole link.
|
||||
assert_eq!(critical.len(), 2);
|
||||
for link in &critical {
|
||||
assert_eq!(link.components_without, 2);
|
||||
}
|
||||
// The record carries the endpoints and the cost.
|
||||
let first = critical[0];
|
||||
assert_eq!(first.from, 0);
|
||||
assert_eq!(first.to, 1);
|
||||
assert!((first.cost - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_complete_mesh_has_no_critical_links() {
|
||||
let mesh = Mesh::new(complete(4));
|
||||
assert!(mesh.critical_links().is_empty());
|
||||
assert!(mesh.survives_single_link_failure());
|
||||
assert_eq!(mesh.minimum_connectivity(), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimum_connectivity_needs_two_terminals() {
|
||||
assert_eq!(Mesh::new(Topology::new(1)).minimum_connectivity(), None);
|
||||
assert_eq!(Mesh::new(Topology::new(0)).minimum_connectivity(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_summary_reports_the_mesh_shape() {
|
||||
let mesh = Mesh::new(ring(4));
|
||||
let summary = mesh.summarise();
|
||||
assert_eq!(summary.node_count, 4);
|
||||
assert_eq!(summary.edge_count, 8);
|
||||
assert_eq!(summary.components, 1);
|
||||
assert_eq!(summary.strongly_connected_components, 1);
|
||||
assert_eq!(summary.minimum_connectivity, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_summary_of_a_split_mesh_reports_the_pieces() {
|
||||
// Two rings joined by nothing.
|
||||
let mut topology = Topology::new(6);
|
||||
for node in 0..3 {
|
||||
topology
|
||||
.add_bidirectional_edge(node, (node + 1) % 3, 1.0)
|
||||
.expect("valid");
|
||||
}
|
||||
for node in 3..6 {
|
||||
topology
|
||||
.add_bidirectional_edge(node, 3 + (node + 1) % 3, 1.0)
|
||||
.expect("valid");
|
||||
}
|
||||
let mesh = Mesh::new(topology);
|
||||
let summary = mesh.summarise();
|
||||
assert_eq!(summary.components, 2);
|
||||
assert_eq!(summary.strongly_connected_components, 2);
|
||||
// The pair across the split has no path, so the minimum is zero.
|
||||
assert_eq!(summary.minimum_connectivity, Some(0));
|
||||
assert!(!mesh.is_connected());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_critical_link_type_is_constructible_directly() {
|
||||
let link = CriticalLink::new(1, 2, 3.5, 2);
|
||||
assert_eq!(link.from, 1);
|
||||
assert_eq!(link.to, 2);
|
||||
assert!((link.cost - 3.5).abs() < 1e-12);
|
||||
assert_eq!(link.components_without, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_satellite_like_mesh_has_the_expected_connectivity() {
|
||||
// A grid-like mesh with each node linked to two neighbours in a ring
|
||||
// plus one cross link, which has connectivity two everywhere.
|
||||
let mut topology = ring(6);
|
||||
topology.add_bidirectional_edge(0, 3, 2.0).expect("valid");
|
||||
let mesh = Mesh::new(topology);
|
||||
assert!(mesh.is_connected());
|
||||
assert!(mesh.survives_single_link_failure());
|
||||
assert_eq!(mesh.minimum_connectivity(), Some(2));
|
||||
}
|
||||
Loading…
Reference in New Issue