apsis-rs/crates/network/apsis-topology/src/series.rs

229 lines
8.1 KiB
Rust

//! The time series of topologies.
use crate::error::{TopologyError, TopologyErrorKind};
use crate::graph::Topology;
/// One topology and the time from which it applies.
#[derive(Debug, Clone, PartialEq)]
pub struct TopologySample {
/// The time from which this topology applies, in seconds from the
/// caller's origin.
pub seconds: f64,
/// The topology in force from that time.
pub topology: Topology,
}
impl TopologySample {
/// Creates a sample.
///
/// 创建一个样本。
pub const fn new(seconds: f64, topology: Topology) -> Self {
Self { seconds, topology }
}
}
/// A sequence of topologies over time.
///
/// The sequence is a step function. The entry at index `i` applies from its
/// own time until the time of the entry at index `i + 1`, and the last entry
/// applies for all later times. Querying a time before the first entry
/// returns the first topology, so the series behaves as if it extends
/// backwards as well as forwards.
///
/// The times must be strictly increasing. Two entries at the same time would
/// make the topology at that instant ambiguous.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TopologySeries {
samples: Vec<TopologySample>,
}
impl TopologySeries {
/// Creates a series from a list of samples.
///
/// Returns an error when the list is empty, when a time is not finite,
/// or when the times are not strictly increasing.
pub fn new(samples: Vec<TopologySample>) -> Result<Self, TopologyError> {
if samples.is_empty() {
return Err(TopologyError::new(TopologyErrorKind::EmptySeries));
}
let mut previous = f64::NEG_INFINITY;
for (index, sample) in samples.iter().enumerate() {
if !sample.seconds.is_finite() {
return Err(TopologyError::new(TopologyErrorKind::InvalidTime)
.with_detail(format!("entry {index} has time {}", sample.seconds)));
}
if sample.seconds <= previous {
return Err(
TopologyError::new(TopologyErrorKind::UnorderedTimes).with_detail(format!(
"entry {index} at {} does not follow {previous}",
sample.seconds
)),
);
}
previous = sample.seconds;
}
Ok(Self { samples })
}
/// Creates a series from times and topologies as separate lists.
///
/// The two lists must have the same length.
///
/// Returns an error when the lists differ in length or when the series
/// itself is not valid.
pub fn from_parts(times: Vec<f64>, topologies: Vec<Topology>) -> Result<Self, TopologyError> {
if times.len() != topologies.len() {
return Err(
TopologyError::new(TopologyErrorKind::EmptySeries).with_detail(format!(
"{} times against {} topologies",
times.len(),
topologies.len()
)),
);
}
let samples = times
.into_iter()
.zip(topologies)
.map(|(seconds, topology)| TopologySample::new(seconds, topology))
.collect();
Self::new(samples)
}
/// Returns the samples.
pub fn samples(&self) -> &[TopologySample] {
&self.samples
}
/// Returns the number of samples.
pub fn len(&self) -> usize {
self.samples.len()
}
/// Returns `true` when there are no samples.
///
/// A series built through either constructor is never empty, so this
/// always returns `false`; it exists because a collection type should
/// offer it alongside [`TopologySeries::len`].
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
/// Returns the first time in the series.
pub fn start_seconds(&self) -> f64 {
self.samples.first().map_or(0.0, |sample| sample.seconds)
}
/// Returns the last time in the series.
pub fn end_seconds(&self) -> f64 {
self.samples.last().map_or(0.0, |sample| sample.seconds)
}
/// Returns the index of the sample that applies at a time.
///
/// The index is the last entry whose time is at or before the query, so
/// a time before the series returns zero and a time after it returns the
/// last index.
///
/// Returns `None` when the time is not finite or the series is empty.
pub fn index_at(&self, seconds: f64) -> Option<usize> {
if !seconds.is_finite() || self.samples.is_empty() {
return None;
}
// The samples are ordered, so a binary search gives the insertion
// point of the query, and the entry before it is the one in force.
match self
.samples
.binary_search_by(|sample| sample.seconds.partial_cmp(&seconds).expect("finite times"))
{
Ok(exact) => Some(exact),
Err(0) => Some(0),
Err(insert) => Some(insert - 1),
}
}
/// Returns the topology in force at a time.
///
/// Returns `None` when the time is not finite or the series is empty.
pub fn topology_at(&self, seconds: f64) -> Option<&Topology> {
Some(&self.samples.get(self.index_at(seconds)?)?.topology)
}
/// Returns the interval over which the sample at an index applies.
///
/// The upper bound is the time of the next sample, or positive infinity
/// for the last entry.
///
/// Returns `None` when the index is out of range.
pub fn interval_of(&self, index: usize) -> Option<(f64, f64)> {
let sample = self.samples.get(index)?;
let end = self
.samples
.get(index + 1)
.map_or(f64::INFINITY, |next| next.seconds);
Some((sample.seconds, end))
}
/// Returns the number of times the topology changes.
///
/// This is the number of samples less one, since each change introduces
/// a new entry. It measures how dynamic the network is: a series with
/// many changes needs routing that accounts for them.
pub fn change_count(&self) -> usize {
self.samples.len().saturating_sub(1)
}
/// Returns the mean interval between changes, in seconds.
///
/// The last entry has unbounded duration, so it is excluded from the
/// mean and the result covers the changes that have a measured interval.
///
/// Returns `None` when there is only one sample.
pub fn mean_interval_seconds(&self) -> Option<f64> {
if self.samples.len() < 2 {
return None;
}
let span = self.end_seconds() - self.start_seconds();
Some(span / self.change_count() as f64)
}
/// Returns the number of edge changes across the series.
///
/// Each topology is compared with the one before it, counting the edges
/// that appear and the edges that disappear. A topology that differs by
/// one link contributes two, since one link changed.
///
/// Returns zero for a series of one sample.
pub fn edge_changes(&self) -> usize {
let mut changes = 0;
for pair in self.samples.windows(2) {
let before = &pair[0].topology;
let after = &pair[1].topology;
// Edges present before but not after.
changes += before
.edges()
.iter()
.filter(|edge| !after.has_edge(edge.from, edge.to))
.count();
// Edges present after but not before.
changes += after
.edges()
.iter()
.filter(|edge| !before.has_edge(edge.from, edge.to))
.count();
}
changes
}
/// Returns the number of nodes in the series.
///
/// Every entry is expected to describe the same set of nodes, so the
/// value is taken from the first entry. A caller that changes the node
/// count between entries is comparing two different networks and should
/// build two series instead.
pub fn node_count(&self) -> usize {
self.samples
.first()
.map_or(0, |sample| sample.topology.node_count())
}
}