288 lines
9.5 KiB
Rust
288 lines
9.5 KiB
Rust
//! 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<usize>,
|
|
last_handover_seconds: Option<f64>,
|
|
history: Vec<HandoverRecord>,
|
|
}
|
|
|
|
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<usize> {
|
|
self.serving
|
|
}
|
|
|
|
/// Returns the time of the last handover, if there has been one.
|
|
pub fn last_handover_seconds(&self) -> Option<f64> {
|
|
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<crate::decision::HandoverDecision, HandoverError> {
|
|
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<f64> {
|
|
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<f64> {
|
|
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()
|
|
}
|
|
}
|