//! The handover policy, its record of past decisions, and the tracker. use crate::decision::Candidate; use crate::error::{HandoverError, HandoverErrorKind}; /// The parameters of a handover policy. /// /// The three parameters work together. The minimum quality decides when the /// current server must be abandoned, the margin decides when a better /// candidate is worth the switch, and the dwell time limits how often /// switches may happen at all. #[derive(Debug, Clone, Copy, PartialEq)] pub struct HandoverPolicy { /// The quality below which the current server is abandoned regardless of /// what else is available. /// /// Quality is a number the caller defines, and the policy is expressed /// in the same units. A value of 0.2 on a normalised scale means the /// server must stay above a fifth of full quality. pub minimum_quality: f64, /// The amount by which a candidate must exceed the current server's /// quality before a handover is worthwhile. /// /// This is the hysteresis that suppresses oscillation between two /// satellites of similar quality. pub handover_margin: f64, /// The minimum time between handovers, in seconds. /// /// A handover is refused when the previous one was more recent than /// this, unless the current server has fallen below the minimum quality, /// in which case the link would fail anyway. pub minimum_dwell_seconds: f64, } impl Default for HandoverPolicy { /// Returns a policy with a minimum quality of 0.2, a margin of 0.1, and /// a dwell time of five seconds. fn default() -> Self { Self { minimum_quality: 0.2, handover_margin: 0.1, minimum_dwell_seconds: 5.0, } } } impl HandoverPolicy { /// Creates a policy from its three parameters. pub const fn new( minimum_quality: f64, handover_margin: f64, minimum_dwell_seconds: f64, ) -> Self { Self { minimum_quality, handover_margin, minimum_dwell_seconds, } } /// Returns `true` when the policy parameters are usable. /// /// The margin must not be negative, since a negative margin would make a /// handover worth doing to a worse candidate, and the dwell time must /// not be negative. The minimum quality may be anything finite, because /// a caller may place it outside the range of the quality scale to /// disable the forced handover. pub fn is_valid(&self) -> bool { self.minimum_quality.is_finite() && self.handover_margin.is_finite() && self.handover_margin >= 0.0 && self.minimum_dwell_seconds.is_finite() && self.minimum_dwell_seconds >= 0.0 } /// Validates the policy, returning the reason when it is not usable. pub fn validate(&self) -> Result<(), HandoverError> { if !self.is_valid() { return Err( HandoverError::new(HandoverErrorKind::InvalidPolicy).with_detail(format!( "minimum quality {}, margin {}, dwell {}", self.minimum_quality, self.handover_margin, self.minimum_dwell_seconds )), ); } Ok(()) } /// Returns the best candidate under the policy. /// /// The best candidate is the one with the greatest quality, with ties /// broken by the lower satellite identifier so that the result is /// deterministic. pub fn best<'a>(&self, candidates: &'a [Candidate]) -> Option<&'a Candidate> { candidates.iter().max_by(|first, second| { first .quality .partial_cmp(&second.quality) .unwrap_or(core::cmp::Ordering::Equal) // Reversed, so that among equal qualities the lower // identifier wins in the maximum. .then_with(|| second.satellite.cmp(&first.satellite)) }) } } /// A record of one handover that has occurred. #[derive(Debug, Clone, Copy, PartialEq)] pub struct HandoverRecord { /// The satellite handed over from. pub from: usize, /// The satellite handed over to. pub to: usize, /// The time of the handover, in seconds. pub seconds: f64, /// The quality of the satellite handed over from. pub from_quality: f64, /// The quality of the satellite handed over to. pub to_quality: f64, } impl HandoverRecord { /// Creates a record. pub const fn new( from: usize, to: usize, seconds: f64, from_quality: f64, to_quality: f64, ) -> Self { Self { from, to, seconds, from_quality, to_quality, } } /// Returns the gain in quality that the handover achieved. pub fn quality_gain(&self) -> f64 { self.to_quality - self.from_quality } } /// Tracks the serving satellite and the history of handovers. /// /// The tracker owns the policy and the decision history, so a caller only /// has to supply the candidates at each step. It is the form a simulation or /// a control loop uses, where the decision made at one step affects the one /// at the next through the dwell time. #[derive(Debug, Clone, PartialEq)] pub struct HandoverTracker { policy: HandoverPolicy, serving: Option, last_handover_seconds: Option, history: Vec, } impl HandoverTracker { /// Creates a tracker with no serving satellite. pub fn new(policy: HandoverPolicy) -> Self { Self { policy, serving: None, last_handover_seconds: None, history: Vec::new(), } } /// Creates a tracker that is already serving a satellite. pub fn serving(policy: HandoverPolicy, satellite: usize) -> Self { Self { policy, serving: Some(satellite), last_handover_seconds: None, history: Vec::new(), } } /// Returns the policy. pub fn policy(&self) -> HandoverPolicy { self.policy } /// Returns the satellite currently being served, if any. pub fn current(&self) -> Option { self.serving } /// Returns the time of the last handover, if there has been one. pub fn last_handover_seconds(&self) -> Option { self.last_handover_seconds } /// Returns the handover history, in time order. pub fn history(&self) -> &[HandoverRecord] { &self.history } /// Returns the number of handovers that have occurred. pub fn handover_count(&self) -> usize { self.history.len() } /// Applies the policy to a set of candidates at a time. /// /// The tracker updates its state according to the decision and returns /// that decision, so a caller can both react to it and rely on the /// tracker having recorded it. /// /// Returns an error when the candidates or the policy are not usable. pub fn step( &mut self, seconds: f64, candidates: &[Candidate], ) -> Result { let decision = crate::decision::decide_with_history( &self.policy, self.serving, self.last_handover_seconds, candidates, seconds, )?; match decision { crate::decision::HandoverDecision::HandOver { from, to, from_quality, to_quality, } => { self.history.push(HandoverRecord::new( from, to, seconds, from_quality, to_quality, )); self.serving = Some(to); self.last_handover_seconds = Some(seconds); } crate::decision::HandoverDecision::Acquire { satellite } => { self.serving = Some(satellite); self.last_handover_seconds = Some(seconds); } crate::decision::HandoverDecision::Stay => {} crate::decision::HandoverDecision::Drop => { self.serving = None; } } Ok(decision) } /// Returns the mean time between handovers, in seconds. /// /// Returns `None` when fewer than two handovers have occurred, because a /// single interval is not a mean. pub fn mean_interval_seconds(&self) -> Option { if self.history.len() < 2 { return None; } let span = self.history.last()?.seconds - self.history.first()?.seconds; Some(span / (self.history.len() - 1) as f64) } /// Returns the number of handovers per unit time over the history. /// /// The rate is measured across the whole history, from the first /// handover to the last. Returns `None` when there are fewer than two /// handovers or when they all occur at the same instant. pub fn handover_rate_per_second(&self) -> Option { let interval = self.mean_interval_seconds()?; if interval <= 0.0 { return None; } Some(1.0 / interval) } /// Returns the total quality gained by handing over. /// /// The value is the sum over the history, which can be negative when a /// handover was forced by a falling quality rather than chosen for a /// gain. pub fn total_quality_gain(&self) -> f64 { self.history.iter().map(HandoverRecord::quality_gain).sum() } }