docs: bilingual comments for handover and topology

This commit is contained in:
ApsisRS contributors 2026-10-02 01:24:45 +08:00
parent 891fe0b37b
commit c442acc6e5
4 changed files with 320 additions and 0 deletions

View File

@ -4,9 +4,13 @@ use crate::error::{HandoverError, HandoverErrorKind};
use crate::policy::HandoverPolicy; use crate::policy::HandoverPolicy;
/// A satellite that could serve the terminal. /// A satellite that could serve the terminal.
///
/// 一颗可能服务终端的卫星。
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Candidate { pub struct Candidate {
/// The satellite identifier. /// The satellite identifier.
///
/// 卫星标识符。
pub satellite: usize, pub satellite: usize,
/// The quality of the link to this satellite. /// The quality of the link to this satellite.
/// ///
@ -14,17 +18,29 @@ pub struct Candidate {
/// carrier to noise ratio, or any other measure of how good the link is. /// carrier to noise ratio, or any other measure of how good the link is.
/// The policy thresholds are expressed in the same units. The value must /// The policy thresholds are expressed in the same units. The value must
/// be finite. /// be finite.
///
/// 到该卫星链路的质量。
///
/// 其含义由调用方定义:仰角、接收功率、载噪比,或任何其他衡量链路
/// 好坏的度量。策略阈值以相同单位表达。该值必须有限。
pub quality: f64, pub quality: f64,
/// The time until this satellite stops being available, in seconds. /// The time until this satellite stops being available, in seconds.
/// ///
/// This is the remaining time in the access window. It lets the policy /// This is the remaining time in the access window. It lets the policy
/// prefer a satellite that will last, and it is reported so a caller can /// prefer a satellite that will last, and it is reported so a caller can
/// plan the next decision. /// plan the next decision.
///
/// 到该卫星不再可用所剩的时间,单位为秒。
///
/// 这是可见性窗口中的剩余时间。它使策略可以偏好将持续更久的卫星,
/// 并被报告出来以便调用方规划下一步决策。
pub remaining_seconds: f64, pub remaining_seconds: f64,
} }
impl Candidate { impl Candidate {
/// Creates a candidate. /// Creates a candidate.
///
/// 创建一个候选。
pub const fn new(satellite: usize, quality: f64, remaining_seconds: f64) -> Self { pub const fn new(satellite: usize, quality: f64, remaining_seconds: f64) -> Self {
Self { Self {
satellite, satellite,
@ -34,6 +50,8 @@ impl Candidate {
} }
/// Returns `true` when the candidate is usable. /// Returns `true` when the candidate is usable.
///
/// 当该候选可用时返回 `true`。
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
self.quality.is_finite() self.quality.is_finite()
&& self.remaining_seconds.is_finite() && self.remaining_seconds.is_finite()
@ -42,32 +60,54 @@ impl Candidate {
} }
/// What the terminal should do at one instant. /// What the terminal should do at one instant.
///
/// 终端在某一瞬时应做的动作。
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum HandoverDecision { pub enum HandoverDecision {
/// Keep the current server. /// Keep the current server.
///
/// 保持当前服务星。
Stay, Stay,
/// Switch from one satellite to another. /// Switch from one satellite to another.
///
/// 从一颗卫星切换到另一颗。
HandOver { HandOver {
/// The satellite being left. /// The satellite being left.
///
/// 被离开的卫星。
from: usize, from: usize,
/// The satellite being taken up. /// The satellite being taken up.
///
/// 被接替的卫星。
to: usize, to: usize,
/// The quality of the satellite being left. /// The quality of the satellite being left.
///
/// 被离开卫星的质量。
from_quality: f64, from_quality: f64,
/// The quality of the satellite being taken up. /// The quality of the satellite being taken up.
///
/// 被接替卫星的质量。
to_quality: f64, to_quality: f64,
}, },
/// Establish service for the first time. /// Establish service for the first time.
///
/// 首次建立服务。
Acquire { Acquire {
/// The satellite to use. /// The satellite to use.
///
/// 要使用的卫星。
satellite: usize, satellite: usize,
}, },
/// Give up service, because no candidate meets the minimum quality. /// Give up service, because no candidate meets the minimum quality.
///
/// 放弃服务,因为没有候选满足最低质量。
Drop, Drop,
} }
impl HandoverDecision { impl HandoverDecision {
/// Returns `true` when the decision changes the serving satellite. /// Returns `true` when the decision changes the serving satellite.
///
/// 当该决策改变服务卫星时返回 `true`。
pub const fn is_change(self) -> bool { pub const fn is_change(self) -> bool {
matches!(self, Self::HandOver { .. } | Self::Acquire { .. }) matches!(self, Self::HandOver { .. } | Self::Acquire { .. })
} }
@ -76,6 +116,11 @@ impl HandoverDecision {
/// ///
/// Returns `None` for [`HandoverDecision::Drop`] and for /// Returns `None` for [`HandoverDecision::Drop`] and for
/// [`HandoverDecision::Stay`] with no current server. /// [`HandoverDecision::Stay`] with no current server.
///
/// 返回决策之后终端所服务的卫星。
///
/// 对 [`HandoverDecision::Drop`] 以及没有当前服务星的
/// [`HandoverDecision::Stay`] 返回 `None`。
pub const fn resulting_satellite(self) -> Option<usize> { pub const fn resulting_satellite(self) -> Option<usize> {
match self { match self {
Self::HandOver { to, .. } => Some(to), Self::HandOver { to, .. } => Some(to),
@ -86,6 +131,8 @@ impl HandoverDecision {
} }
/// Returns a short name for the decision. /// Returns a short name for the decision.
///
/// 返回该决策的简称。
pub const fn name(self) -> &'static str { pub const fn name(self) -> &'static str {
match self { match self {
Self::Stay => "stay", Self::Stay => "stay",
@ -97,6 +144,8 @@ impl HandoverDecision {
} }
/// Validates a candidate list against the policy. /// Validates a candidate list against the policy.
///
/// 针对策略校验候选列表。
fn validate(policy: &HandoverPolicy, candidates: &[Candidate]) -> Result<(), HandoverError> { fn validate(policy: &HandoverPolicy, candidates: &[Candidate]) -> Result<(), HandoverError> {
policy.validate()?; policy.validate()?;
if candidates.is_empty() { if candidates.is_empty() {
@ -113,6 +162,8 @@ fn validate(policy: &HandoverPolicy, candidates: &[Candidate]) -> Result<(), Han
} }
// A duplicate would make the choice between two entries ambiguous // A duplicate would make the choice between two entries ambiguous
// and would let a caller weight a satellite by listing it twice. // and would let a caller weight a satellite by listing it twice.
// 重复项会使两项之间的选择变得含糊,并让调用方通过列出两次来
// 加重某卫星的权重。
if candidates[..index] if candidates[..index]
.iter() .iter()
.any(|earlier| earlier.satellite == candidate.satellite) .any(|earlier| earlier.satellite == candidate.satellite)
@ -181,6 +232,8 @@ pub fn decide_with_history(
} }
/// Applies the rules with an optional dwell time. /// Applies the rules with an optional dwell time.
///
/// 以可选驻留时间施加各条规则。
fn decide_internal( fn decide_internal(
policy: &HandoverPolicy, policy: &HandoverPolicy,
current: Option<usize>, current: Option<usize>,
@ -201,6 +254,7 @@ fn decide_internal(
// Rule 1: no current server, so acquire the best that meets the // Rule 1: no current server, so acquire the best that meets the
// minimum, or drop. // minimum, or drop.
// 规则 1:没有当前服务星,故获取满足最低质量的最优者,否则放弃。
let Some(current_satellite) = current else { let Some(current_satellite) = current else {
return Ok(if best.quality >= policy.minimum_quality { return Ok(if best.quality >= policy.minimum_quality {
HandoverDecision::Acquire { HandoverDecision::Acquire {
@ -213,6 +267,7 @@ fn decide_internal(
// Rule 2: the current server is no longer a candidate, so it has become // Rule 2: the current server is no longer a candidate, so it has become
// unavailable and the terminal must move. // unavailable and the terminal must move.
// 规则 2:当前服务星不再是候选,故它已不可用,终端必须迁移。
let Some(current_candidate) = current_candidate else { let Some(current_candidate) = current_candidate else {
return Ok(if best.quality >= policy.minimum_quality { return Ok(if best.quality >= policy.minimum_quality {
HandoverDecision::HandOver { HandoverDecision::HandOver {
@ -228,10 +283,13 @@ fn decide_internal(
// Rule 3: the current server has fallen below the minimum, so abandon // Rule 3: the current server has fallen below the minimum, so abandon
// it. This is a forced handover and the dwell time does not apply. // it. This is a forced handover and the dwell time does not apply.
// 规则 3:当前服务星已跌至最低质量以下,故放弃它。这是强制切换,
// 驻留时间不适用。
if current_candidate.quality < policy.minimum_quality { if current_candidate.quality < policy.minimum_quality {
if best.satellite == current_satellite { if best.satellite == current_satellite {
// The best candidate is the current server, which is too weak, // The best candidate is the current server, which is too weak,
// so there is nothing to move to. // so there is nothing to move to.
// 最优候选就是当前服务星,而它太弱,故无处可迁。
return Ok(HandoverDecision::Drop); return Ok(HandoverDecision::Drop);
} }
return Ok(if best.quality >= policy.minimum_quality { return Ok(if best.quality >= policy.minimum_quality {
@ -248,6 +306,7 @@ fn decide_internal(
// Rule 4: consider a handover for a quality gain, which the dwell time // Rule 4: consider a handover for a quality gain, which the dwell time
// may refuse. // may refuse.
// 规则 4:为质量增益考虑切换,驻留时间可能拒绝它。
if best.satellite != current_satellite if best.satellite != current_satellite
&& best.quality > current_candidate.quality + policy.handover_margin && best.quality > current_candidate.quality + policy.handover_margin
{ {

View File

@ -9,6 +9,11 @@ use crate::error::{HandoverError, HandoverErrorKind};
/// current server must be abandoned, the margin decides when a better /// current server must be abandoned, the margin decides when a better
/// candidate is worth the switch, and the dwell time limits how often /// candidate is worth the switch, and the dwell time limits how often
/// switches may happen at all. /// switches may happen at all.
///
/// 切换策略的参数。
///
/// 三个参数协同工作。最低质量决定当前服务星何时必须被放弃,余量决定
/// 更优候选何时值得切换,驻留时间则限制切换至多能多频繁发生。
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct HandoverPolicy { pub struct HandoverPolicy {
/// The quality below which the current server is abandoned regardless of /// The quality below which the current server is abandoned regardless of
@ -17,24 +22,40 @@ pub struct HandoverPolicy {
/// Quality is a number the caller defines, and the policy is expressed /// 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 /// in the same units. A value of 0.2 on a normalised scale means the
/// server must stay above a fifth of full quality. /// server must stay above a fifth of full quality.
///
/// 低于该质量时,无论还有什么可用,当前服务星都被放弃。
///
/// 质量是调用方定义的数,策略以相同单位表达。在归一化尺度上取 0.2
/// 表示服务星必须保持在全质量的五分之一以上。
pub minimum_quality: f64, pub minimum_quality: f64,
/// The amount by which a candidate must exceed the current server's /// The amount by which a candidate must exceed the current server's
/// quality before a handover is worthwhile. /// quality before a handover is worthwhile.
/// ///
/// This is the hysteresis that suppresses oscillation between two /// This is the hysteresis that suppresses oscillation between two
/// satellites of similar quality. /// satellites of similar quality.
///
/// 候选超出当前服务星质量多少才值得切换的量。
///
/// 这是抑制质量相近的两颗卫星之间振荡的滞回量。
pub handover_margin: f64, pub handover_margin: f64,
/// The minimum time between handovers, in seconds. /// The minimum time between handovers, in seconds.
/// ///
/// A handover is refused when the previous one was more recent than /// A handover is refused when the previous one was more recent than
/// this, unless the current server has fallen below the minimum quality, /// this, unless the current server has fallen below the minimum quality,
/// in which case the link would fail anyway. /// in which case the link would fail anyway.
///
/// 两次切换之间的最短时间,单位为秒。
///
/// 若上一次切换比这更近,则切换被拒绝,除非当前服务星已跌至最低
/// 质量以下,此时链路无论如何都会失效。
pub minimum_dwell_seconds: f64, pub minimum_dwell_seconds: f64,
} }
impl Default for HandoverPolicy { impl Default for HandoverPolicy {
/// Returns a policy with a minimum quality of 0.2, a margin of 0.1, and /// Returns a policy with a minimum quality of 0.2, a margin of 0.1, and
/// a dwell time of five seconds. /// a dwell time of five seconds.
///
/// 返回最低质量为 0.2、余量为 0.1、驻留时间为五秒的策略。
fn default() -> Self { fn default() -> Self {
Self { Self {
minimum_quality: 0.2, minimum_quality: 0.2,
@ -46,6 +67,8 @@ impl Default for HandoverPolicy {
impl HandoverPolicy { impl HandoverPolicy {
/// Creates a policy from its three parameters. /// Creates a policy from its three parameters.
///
/// 由其三个参数创建策略。
pub const fn new( pub const fn new(
minimum_quality: f64, minimum_quality: f64,
handover_margin: f64, handover_margin: f64,
@ -65,6 +88,12 @@ impl HandoverPolicy {
/// not be negative. The minimum quality may be anything finite, because /// not be negative. The minimum quality may be anything finite, because
/// a caller may place it outside the range of the quality scale to /// a caller may place it outside the range of the quality scale to
/// disable the forced handover. /// disable the forced handover.
///
/// 当策略参数可用时返回 `true`。
///
/// 余量不得为负,因为负余量会使切换到更差候选也值得做;驻留时间也
/// 不得为负。最低质量可以是任何有限值,因为调用方可以把它放在质量
/// 尺度范围之外以禁用强制切换。
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
self.minimum_quality.is_finite() self.minimum_quality.is_finite()
&& self.handover_margin.is_finite() && self.handover_margin.is_finite()
@ -74,6 +103,8 @@ impl HandoverPolicy {
} }
/// Validates the policy, returning the reason when it is not usable. /// Validates the policy, returning the reason when it is not usable.
///
/// 校验策略,不可用时返回原因。
pub fn validate(&self) -> Result<(), HandoverError> { pub fn validate(&self) -> Result<(), HandoverError> {
if !self.is_valid() { if !self.is_valid() {
return Err( return Err(
@ -91,6 +122,10 @@ impl HandoverPolicy {
/// The best candidate is the one with the greatest quality, with ties /// The best candidate is the one with the greatest quality, with ties
/// broken by the lower satellite identifier so that the result is /// broken by the lower satellite identifier so that the result is
/// deterministic. /// deterministic.
///
/// 返回该策略下的最优候选。
///
/// 最优候选是质量最大者,并列时以较小的卫星标识符打破,使结果确定。
pub fn best<'a>(&self, candidates: &'a [Candidate]) -> Option<&'a Candidate> { pub fn best<'a>(&self, candidates: &'a [Candidate]) -> Option<&'a Candidate> {
candidates.iter().max_by(|first, second| { candidates.iter().max_by(|first, second| {
first first
@ -99,28 +134,43 @@ impl HandoverPolicy {
.unwrap_or(core::cmp::Ordering::Equal) .unwrap_or(core::cmp::Ordering::Equal)
// Reversed, so that among equal qualities the lower // Reversed, so that among equal qualities the lower
// identifier wins in the maximum. // identifier wins in the maximum.
// 反转,使质量相等时较小的标识符在取最大中胜出。
.then_with(|| second.satellite.cmp(&first.satellite)) .then_with(|| second.satellite.cmp(&first.satellite))
}) })
} }
} }
/// A record of one handover that has occurred. /// A record of one handover that has occurred.
///
/// 一次已发生切换的记录。
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct HandoverRecord { pub struct HandoverRecord {
/// The satellite handed over from. /// The satellite handed over from.
///
/// 被切换离开的卫星。
pub from: usize, pub from: usize,
/// The satellite handed over to. /// The satellite handed over to.
///
/// 被切换至的卫星。
pub to: usize, pub to: usize,
/// The time of the handover, in seconds. /// The time of the handover, in seconds.
///
/// 该切换的时间,单位为秒。
pub seconds: f64, pub seconds: f64,
/// The quality of the satellite handed over from. /// The quality of the satellite handed over from.
///
/// 被切换离开卫星的质量。
pub from_quality: f64, pub from_quality: f64,
/// The quality of the satellite handed over to. /// The quality of the satellite handed over to.
///
/// 被切换至卫星的质量。
pub to_quality: f64, pub to_quality: f64,
} }
impl HandoverRecord { impl HandoverRecord {
/// Creates a record. /// Creates a record.
///
/// 创建一条记录。
pub const fn new( pub const fn new(
from: usize, from: usize,
to: usize, to: usize,
@ -138,6 +188,8 @@ impl HandoverRecord {
} }
/// Returns the gain in quality that the handover achieved. /// Returns the gain in quality that the handover achieved.
///
/// 返回该切换所取得的质量增益。
pub fn quality_gain(&self) -> f64 { pub fn quality_gain(&self) -> f64 {
self.to_quality - self.from_quality self.to_quality - self.from_quality
} }
@ -149,6 +201,11 @@ impl HandoverRecord {
/// has to supply the candidates at each step. It is the form a simulation or /// 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 /// a control loop uses, where the decision made at one step affects the one
/// at the next through the dwell time. /// at the next through the dwell time.
///
/// 跟踪服务卫星与切换历史。
///
/// 跟踪器拥有策略与决策历史,故调用方只需在每一步提供候选。这是仿真或
/// 控制回路所用的形式——其中一步的决策经驻留时间影响下一步。
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct HandoverTracker { pub struct HandoverTracker {
policy: HandoverPolicy, policy: HandoverPolicy,
@ -159,6 +216,8 @@ pub struct HandoverTracker {
impl HandoverTracker { impl HandoverTracker {
/// Creates a tracker with no serving satellite. /// Creates a tracker with no serving satellite.
///
/// 创建没有服务卫星的跟踪器。
pub fn new(policy: HandoverPolicy) -> Self { pub fn new(policy: HandoverPolicy) -> Self {
Self { Self {
policy, policy,
@ -169,6 +228,8 @@ impl HandoverTracker {
} }
/// Creates a tracker that is already serving a satellite. /// Creates a tracker that is already serving a satellite.
///
/// 创建已在服务某卫星的跟踪器。
pub fn serving(policy: HandoverPolicy, satellite: usize) -> Self { pub fn serving(policy: HandoverPolicy, satellite: usize) -> Self {
Self { Self {
policy, policy,
@ -179,26 +240,36 @@ impl HandoverTracker {
} }
/// Returns the policy. /// Returns the policy.
///
/// 返回策略。
pub fn policy(&self) -> HandoverPolicy { pub fn policy(&self) -> HandoverPolicy {
self.policy self.policy
} }
/// Returns the satellite currently being served, if any. /// Returns the satellite currently being served, if any.
///
/// 返回当前正在服务的卫星(若有)。
pub fn current(&self) -> Option<usize> { pub fn current(&self) -> Option<usize> {
self.serving self.serving
} }
/// Returns the time of the last handover, if there has been one. /// Returns the time of the last handover, if there has been one.
///
/// 返回最后一次切换的时间(若曾发生)。
pub fn last_handover_seconds(&self) -> Option<f64> { pub fn last_handover_seconds(&self) -> Option<f64> {
self.last_handover_seconds self.last_handover_seconds
} }
/// Returns the handover history, in time order. /// Returns the handover history, in time order.
///
/// 按时间次序返回切换历史。
pub fn history(&self) -> &[HandoverRecord] { pub fn history(&self) -> &[HandoverRecord] {
&self.history &self.history
} }
/// Returns the number of handovers that have occurred. /// Returns the number of handovers that have occurred.
///
/// 返回已发生的切换次数。
pub fn handover_count(&self) -> usize { pub fn handover_count(&self) -> usize {
self.history.len() self.history.len()
} }
@ -257,6 +328,10 @@ impl HandoverTracker {
/// ///
/// Returns `None` when fewer than two handovers have occurred, because a /// Returns `None` when fewer than two handovers have occurred, because a
/// single interval is not a mean. /// single interval is not a mean.
///
/// 返回两次切换之间的平均时间,单位为秒。
///
/// 发生的切换少于两次时返回 `None`,因为单个间隔算不上均值。
pub fn mean_interval_seconds(&self) -> Option<f64> { pub fn mean_interval_seconds(&self) -> Option<f64> {
if self.history.len() < 2 { if self.history.len() < 2 {
return None; return None;
@ -270,6 +345,11 @@ impl HandoverTracker {
/// The rate is measured across the whole history, from the first /// The rate is measured across the whole history, from the first
/// handover to the last. Returns `None` when there are fewer than two /// handover to the last. Returns `None` when there are fewer than two
/// handovers or when they all occur at the same instant. /// handovers or when they all occur at the same instant.
///
/// 返回整个历史上单位时间的切换次数。
///
/// 速率跨越整个历史度量,从首次切换到末次。切换少于两次或它们都
/// 发生在同一瞬时时返回 `None`。
pub fn handover_rate_per_second(&self) -> Option<f64> { pub fn handover_rate_per_second(&self) -> Option<f64> {
let interval = self.mean_interval_seconds()?; let interval = self.mean_interval_seconds()?;
if interval <= 0.0 { if interval <= 0.0 {
@ -283,6 +363,11 @@ impl HandoverTracker {
/// The value is the sum over the history, which can be negative when a /// 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 /// handover was forced by a falling quality rather than chosen for a
/// gain. /// gain.
///
/// 返回切换所获得的质量总和。
///
/// 该值是对整个历史求和,当切换是被下降的质量所迫而非为增益所选择
/// 时可为负。
pub fn total_quality_gain(&self) -> f64 { pub fn total_quality_gain(&self) -> f64 {
self.history.iter().map(HandoverRecord::quality_gain).sum() self.history.iter().map(HandoverRecord::quality_gain).sum()
} }

View File

@ -3,22 +3,35 @@
use crate::error::{TopologyError, TopologyErrorKind}; use crate::error::{TopologyError, TopologyErrorKind};
/// One link in a topology. /// One link in a topology.
///
/// 拓扑中的一条链路。
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Edge { pub struct Edge {
/// The index of the first endpoint. /// The index of the first endpoint.
///
/// 第一个端点的下标。
pub from: usize, pub from: usize,
/// The index of the second endpoint. /// The index of the second endpoint.
///
/// 第二个端点的下标。
pub to: usize, pub to: usize,
/// The cost of traversing the link. /// The cost of traversing the link.
/// ///
/// The meaning is the caller's: a delay in seconds, a hop count, or any /// The meaning is the caller's: a delay in seconds, a hop count, or any
/// other measure a routing algorithm minimises. It must be finite and /// other measure a routing algorithm minimises. It must be finite and
/// positive, so that adding edges never shortens a path. /// positive, so that adding edges never shortens a path.
///
/// 穿越该链路的代价。
///
/// 其含义由调用方定义:以秒计的时延、跳数,或路由算法所最小化的
/// 任何其他度量。它必须有限且为正,使添加边绝不缩短路径。
pub cost: f64, pub cost: f64,
} }
impl Edge { impl Edge {
/// Creates an edge. /// Creates an edge.
///
/// 创建一条边。
pub const fn new(from: usize, to: usize, cost: f64) -> Self { pub const fn new(from: usize, to: usize, cost: f64) -> Self {
Self { from, to, cost } Self { from, to, cost }
} }
@ -27,6 +40,10 @@ impl Edge {
/// ///
/// A link between two satellites carries traffic both ways, so most /// A link between two satellites carries traffic both ways, so most
/// topologies add both directions. /// topologies add both directions.
///
/// 返回端点互换后的同一条边。
///
/// 两颗卫星之间的链路双向承载业务,故多数拓扑会添加两个方向。
pub const fn reversed(self) -> Self { pub const fn reversed(self) -> Self {
Self { Self {
from: self.to, from: self.to,
@ -44,6 +61,13 @@ impl Edge {
/// both directions. This is deliberate, because an inter-satellite link may /// both directions. This is deliberate, because an inter-satellite link may
/// have different capacity in each direction and a ground link is often /// have different capacity in each direction and a ground link is often
/// asymmetric. /// asymmetric.
///
/// 某一瞬时的节点与链路图。
///
/// 节点数在构造时固定,节点以其下标标识。边是有向的:从一节点向另一
/// 节点添加边不会添加反向边,故需要双向链路的调用方要添加两个方向。
/// 这是有意的,因为星间链路每个方向的容量可能不同,而地面链路往往
/// 不对称。
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct Topology { pub struct Topology {
node_count: usize, node_count: usize,
@ -57,6 +81,13 @@ pub struct Topology {
/// satellites, into cubic work. The index holds positions rather than /// satellites, into cubic work. The index holds positions rather than
/// destinations so that `remove_edge`, which invalidates positions, can /// destinations so that `remove_edge`, which invalidates positions, can
/// repair it in one pass. /// repair it in one pass.
///
/// 各节点出发的边在 `edges` 中的位置。
///
/// 该索引使成员判断成为常数时间测试。没有它,询问某边是否已存在要
/// 扫描整个边表,构建含 `E` 条边的图耗费 `O(E^2)`,这把星座的可见性
/// 遍历(对卫星数二次)变成三次的工作量。该索引保存位置而非目的地,
/// 使会使位置失效的 `remove_edge` 能一遍修好它。
outgoing: Vec<Vec<usize>>, outgoing: Vec<Vec<usize>>,
} }
@ -65,6 +96,10 @@ impl Topology {
/// ///
/// This is not a `const fn` because it sizes the adjacency index, which /// This is not a `const fn` because it sizes the adjacency index, which
/// allocates. /// allocates.
///
/// 以给定节点数创建空拓扑。
///
/// 它不是 `const fn`,因为它要为邻接索引确定大小,而这会分配。
pub fn new(node_count: usize) -> Self { pub fn new(node_count: usize) -> Self {
let mut outgoing = Vec::new(); let mut outgoing = Vec::new();
outgoing.resize_with(node_count, Vec::new); outgoing.resize_with(node_count, Vec::new);
@ -76,21 +111,29 @@ impl Topology {
} }
/// Returns the number of nodes. /// Returns the number of nodes.
///
/// 返回节点的数量。
pub const fn node_count(&self) -> usize { pub const fn node_count(&self) -> usize {
self.node_count self.node_count
} }
/// Returns the number of edges. /// Returns the number of edges.
///
/// 返回边的数量。
pub fn edge_count(&self) -> usize { pub fn edge_count(&self) -> usize {
self.edges.len() self.edges.len()
} }
/// Returns `true` when there are no edges. /// Returns `true` when there are no edges.
///
/// 当没有边时返回 `true`。
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.edges.is_empty() self.edges.is_empty()
} }
/// Returns the edges. /// Returns the edges.
///
/// 返回各条边。
pub fn edges(&self) -> &[Edge] { pub fn edges(&self) -> &[Edge] {
&self.edges &self.edges
} }
@ -101,6 +144,11 @@ impl Topology {
/// cost is not finite and positive, or when the edge already exists. /// cost is not finite and positive, or when the edge already exists.
/// Rejecting a duplicate rather than replacing it keeps a topology free /// Rejecting a duplicate rather than replacing it keeps a topology free
/// of silent contradictions about the cost of a link. /// of silent contradictions about the cost of a link.
///
/// 添加一条有向边。
///
/// 任一端点下标越界、代价非有限且为正,或该边已存在时返回错误。拒绝
/// 重复边而非替换它,使拓扑不会对某条链路的代价产生无声的矛盾。
pub fn add_edge(&mut self, from: usize, to: usize, cost: f64) -> Result<(), TopologyError> { pub fn add_edge(&mut self, from: usize, to: usize, cost: f64) -> Result<(), TopologyError> {
if from >= self.node_count || to >= self.node_count { if from >= self.node_count || to >= self.node_count {
return Err(TopologyError::new(TopologyErrorKind::NoSuchNode) return Err(TopologyError::new(TopologyErrorKind::NoSuchNode)
@ -124,6 +172,11 @@ impl Topology {
/// If the second direction fails after the first succeeds, the first is /// If the second direction fails after the first succeeds, the first is
/// rolled back, so the topology is left unchanged when the call reports /// rolled back, so the topology is left unchanged when the call reports
/// an error. /// an error.
///
/// 以相同代价在两个方向添加边。
///
/// 若第一个方向成功后第二个方向失败,则回滚第一个,故调用报告错误
/// 时拓扑保持不变。
pub fn add_bidirectional_edge( pub fn add_bidirectional_edge(
&mut self, &mut self,
first: usize, first: usize,
@ -133,6 +186,7 @@ impl Topology {
self.add_edge(first, second, cost)?; self.add_edge(first, second, cost)?;
if let Err(error) = self.add_edge(second, first, cost) { if let Err(error) = self.add_edge(second, first, cost) {
// Remove the first direction so the topology is unchanged. // Remove the first direction so the topology is unchanged.
// 移除第一个方向,使拓扑保持不变。
self.remove_edge_unchecked(first, second); self.remove_edge_unchecked(first, second);
return Err(error); return Err(error);
} }
@ -140,6 +194,8 @@ impl Topology {
} }
/// Returns `true` when an edge exists from one node to another. /// Returns `true` when an edge exists from one node to another.
///
/// 当存在从一节点到另一节点的边时返回 `true`。
pub fn has_edge(&self, from: usize, to: usize) -> bool { pub fn has_edge(&self, from: usize, to: usize) -> bool {
self.outgoing.get(from).is_some_and(|positions| { self.outgoing.get(from).is_some_and(|positions| {
positions positions
@ -151,6 +207,10 @@ impl Topology {
/// Returns the cost of the edge between two nodes. /// Returns the cost of the edge between two nodes.
/// ///
/// Returns `None` when there is no such edge. /// Returns `None` when there is no such edge.
///
/// 返回两节点之间边的代价。
///
/// 不存在该边时返回 `None`。
pub fn cost(&self, from: usize, to: usize) -> Option<f64> { pub fn cost(&self, from: usize, to: usize) -> Option<f64> {
let positions = self.outgoing.get(from)?; let positions = self.outgoing.get(from)?;
positions positions
@ -162,6 +222,10 @@ impl Topology {
/// Removes the edge between two nodes. /// Removes the edge between two nodes.
/// ///
/// Returns an error when there is no such edge. /// Returns an error when there is no such edge.
///
/// 移除两节点之间的边。
///
/// 不存在该边时返回错误。
pub fn remove_edge(&mut self, from: usize, to: usize) -> Result<(), TopologyError> { pub fn remove_edge(&mut self, from: usize, to: usize) -> Result<(), TopologyError> {
if !self.has_edge(from, to) { if !self.has_edge(from, to) {
return Err(TopologyError::new(TopologyErrorKind::NoSuchEdge) return Err(TopologyError::new(TopologyErrorKind::NoSuchEdge)
@ -177,6 +241,11 @@ impl Topology {
/// rebuilt from the surviving edges in one pass. Removal is rarer than /// rebuilt from the surviving edges in one pass. Removal is rarer than
/// insertion, so paying for a rebuild here is cheaper than keeping the /// insertion, so paying for a rebuild here is cheaper than keeping the
/// index correct incrementally. /// index correct incrementally.
///
/// 移除调用方已确认存在的边。
///
/// 移除边会重编号其后的每个位置,故索引由存活的边一遍重建。移除比
/// 插入少见,故此处付出一次重建的代价比增量地保持索引正确更便宜。
fn remove_edge_unchecked(&mut self, from: usize, to: usize) { fn remove_edge_unchecked(&mut self, from: usize, to: usize) {
self.edges self.edges
.retain(|edge| !(edge.from == from && edge.to == to)); .retain(|edge| !(edge.from == from && edge.to == to));
@ -184,6 +253,8 @@ impl Topology {
} }
/// Rebuilds the adjacency index from the current edge list. /// Rebuilds the adjacency index from the current edge list.
///
/// 由当前边表重建邻接索引。
fn rebuild_index(&mut self) { fn rebuild_index(&mut self) {
for positions in &mut self.outgoing { for positions in &mut self.outgoing {
positions.clear(); positions.clear();
@ -197,6 +268,10 @@ impl Topology {
/// ///
/// The result is in the order the edges were added, so it is /// The result is in the order the edges were added, so it is
/// deterministic for a given construction sequence. /// deterministic for a given construction sequence.
///
/// 返回自某节点一跳可达的节点下标。
///
/// 结果按边被添加的次序排列,故对给定的构建序列是确定的。
pub fn neighbours(&self, node: usize) -> Vec<usize> { pub fn neighbours(&self, node: usize) -> Vec<usize> {
self.outgoing self.outgoing
.get(node) .get(node)
@ -205,11 +280,15 @@ impl Topology {
} }
/// Returns the number of edges leaving a node. /// Returns the number of edges leaving a node.
///
/// 返回离开某节点的边数。
pub fn out_degree(&self, node: usize) -> usize { pub fn out_degree(&self, node: usize) -> usize {
self.outgoing.get(node).map_or(0, Vec::len) self.outgoing.get(node).map_or(0, Vec::len)
} }
/// Returns the number of edges arriving at a node. /// Returns the number of edges arriving at a node.
///
/// 返回到达某节点的边数。
pub fn in_degree(&self, node: usize) -> usize { pub fn in_degree(&self, node: usize) -> usize {
self.edges.iter().filter(|edge| edge.to == node).count() self.edges.iter().filter(|edge| edge.to == node).count()
} }
@ -218,6 +297,10 @@ impl Topology {
/// ///
/// The result has one entry per node, in node order, each listing the /// The result has one entry per node, in node order, each listing the
/// neighbours of that node. /// neighbours of that node.
///
/// 返回每个节点的邻接。
///
/// 结果每个节点一项,按节点次序排列,各自列出该节点的邻居。
pub fn adjacency(&self) -> Vec<Vec<usize>> { pub fn adjacency(&self) -> Vec<Vec<usize>> {
(0..self.node_count) (0..self.node_count)
.map(|node| self.neighbours(node)) .map(|node| self.neighbours(node))
@ -231,6 +314,11 @@ impl Topology {
/// the first thing to check about a constellation snapshot: a /// the first thing to check about a constellation snapshot: a
/// constellation that has split into pieces can still carry traffic /// constellation that has split into pieces can still carry traffic
/// within each piece but not between them. /// within each piece but not between them.
///
/// 把边视为无向时返回连通分量的数量。
///
/// 它回答该拓扑构成多少个独立网络,这是检查星座快照时要看的第一件
/// 事:已裂成数块的星座仍可在各块内承载业务,但块与块之间不行。
pub fn connected_components(&self) -> usize { pub fn connected_components(&self) -> usize {
if self.node_count == 0 { if self.node_count == 0 {
return 0; return 0;
@ -243,6 +331,7 @@ impl Topology {
} }
components += 1; components += 1;
// A breadth first walk over the edges in both directions. // A breadth first walk over the edges in both directions.
// 沿两个方向的边做广度优先遍历。
let mut stack = vec![start]; let mut stack = vec![start];
visited[start] = true; visited[start] = true;
while let Some(node) = stack.pop() { while let Some(node) = stack.pop() {
@ -266,6 +355,8 @@ impl Topology {
/// Returns `true` when every node can reach every other node, treating /// Returns `true` when every node can reach every other node, treating
/// the edges as undirected. /// the edges as undirected.
///
/// 把边视为无向时,当每个节点都能到达其余每个节点,返回 `true`。
pub fn is_connected(&self) -> bool { pub fn is_connected(&self) -> bool {
self.node_count <= 1 || self.connected_components() == 1 self.node_count <= 1 || self.connected_components() == 1
} }
@ -274,6 +365,10 @@ impl Topology {
/// ///
/// This is a crude measure of the network's extent, useful for comparing /// This is a crude measure of the network's extent, useful for comparing
/// snapshots rather than for routing. /// snapshots rather than for routing.
///
/// 返回所有边的代价之和。
///
/// 这是对网络规模的一个粗略度量,用于比较快照而非用于路由。
pub fn total_cost(&self) -> f64 { pub fn total_cost(&self) -> f64 {
self.edges.iter().map(|edge| edge.cost).sum() self.edges.iter().map(|edge| edge.cost).sum()
} }

View File

@ -4,12 +4,18 @@ use crate::error::{TopologyError, TopologyErrorKind};
use crate::graph::Topology; use crate::graph::Topology;
/// One topology and the time from which it applies. /// One topology and the time from which it applies.
///
/// 一个拓扑及其开始适用的时间。
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct TopologySample { pub struct TopologySample {
/// The time from which this topology applies, in seconds from the /// The time from which this topology applies, in seconds from the
/// caller's origin. /// caller's origin.
///
/// 该拓扑开始适用的时间,以距调用方原点的秒数表示。
pub seconds: f64, pub seconds: f64,
/// The topology in force from that time. /// The topology in force from that time.
///
/// 自该时间起生效的拓扑。
pub topology: Topology, pub topology: Topology,
} }
@ -32,6 +38,14 @@ impl TopologySample {
/// ///
/// The times must be strictly increasing. Two entries at the same time would /// The times must be strictly increasing. Two entries at the same time would
/// make the topology at that instant ambiguous. /// make the topology at that instant ambiguous.
///
/// 拓扑随时间变化的序列。
///
/// 该序列是阶跃函数。下标 `i` 处的项自其自身时间起适用,直到下标
/// `i + 1` 处项的时间;最后一项适用于其后所有时间。查询首项之前的时间
/// 返回第一个拓扑,故该序列的行为像是向前向后都延伸。
///
/// 各时间必须严格递增。同一时间的两项会使该瞬时的拓扑含糊。
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct TopologySeries { pub struct TopologySeries {
samples: Vec<TopologySample>, samples: Vec<TopologySample>,
@ -42,6 +56,10 @@ impl TopologySeries {
/// ///
/// Returns an error when the list is empty, when a time is not finite, /// Returns an error when the list is empty, when a time is not finite,
/// or when the times are not strictly increasing. /// or when the times are not strictly increasing.
///
/// 由样本列表创建序列。
///
/// 列表为空、某时间非有限,或各时间非严格递增时返回错误。
pub fn new(samples: Vec<TopologySample>) -> Result<Self, TopologyError> { pub fn new(samples: Vec<TopologySample>) -> Result<Self, TopologyError> {
if samples.is_empty() { if samples.is_empty() {
return Err(TopologyError::new(TopologyErrorKind::EmptySeries)); return Err(TopologyError::new(TopologyErrorKind::EmptySeries));
@ -71,6 +89,12 @@ impl TopologySeries {
/// ///
/// Returns an error when the lists differ in length or when the series /// Returns an error when the lists differ in length or when the series
/// itself is not valid. /// itself is not valid.
///
/// 由分别给出的时间列表与拓扑列表创建序列。
///
/// 两个列表必须长度相同。
///
/// 列表长度不同或序列自身无效时返回错误。
pub fn from_parts(times: Vec<f64>, topologies: Vec<Topology>) -> Result<Self, TopologyError> { pub fn from_parts(times: Vec<f64>, topologies: Vec<Topology>) -> Result<Self, TopologyError> {
if times.len() != topologies.len() { if times.len() != topologies.len() {
return Err( return Err(
@ -90,11 +114,15 @@ impl TopologySeries {
} }
/// Returns the samples. /// Returns the samples.
///
/// 返回各样本。
pub fn samples(&self) -> &[TopologySample] { pub fn samples(&self) -> &[TopologySample] {
&self.samples &self.samples
} }
/// Returns the number of samples. /// Returns the number of samples.
///
/// 返回样本的数量。
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.samples.len() self.samples.len()
} }
@ -104,16 +132,25 @@ impl TopologySeries {
/// A series built through either constructor is never empty, so this /// A series built through either constructor is never empty, so this
/// always returns `false`; it exists because a collection type should /// always returns `false`; it exists because a collection type should
/// offer it alongside [`TopologySeries::len`]. /// offer it alongside [`TopologySeries::len`].
///
/// 当没有样本时返回 `true`。
///
/// 经任一构造函数构建的序列都不会为空,故这总是返回 `false`;它的
/// 存在是因为集合类型应在 [`TopologySeries::len`] 之外一并提供它。
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.samples.is_empty() self.samples.is_empty()
} }
/// Returns the first time in the series. /// Returns the first time in the series.
///
/// 返回序列中的第一个时间。
pub fn start_seconds(&self) -> f64 { pub fn start_seconds(&self) -> f64 {
self.samples.first().map_or(0.0, |sample| sample.seconds) self.samples.first().map_or(0.0, |sample| sample.seconds)
} }
/// Returns the last time in the series. /// Returns the last time in the series.
///
/// 返回序列中的最后一个时间。
pub fn end_seconds(&self) -> f64 { pub fn end_seconds(&self) -> f64 {
self.samples.last().map_or(0.0, |sample| sample.seconds) self.samples.last().map_or(0.0, |sample| sample.seconds)
} }
@ -125,12 +162,20 @@ impl TopologySeries {
/// last index. /// last index.
/// ///
/// Returns `None` when the time is not finite or the series is empty. /// Returns `None` when the time is not finite or the series is empty.
///
/// 返回某时间处适用的样本下标。
///
/// 该下标是时间不晚于查询的最后一个项,故序列之前的时间返回零,
/// 序列之后的时间返回最后的下标。
///
/// 时间非有限或序列为空时返回 `None`。
pub fn index_at(&self, seconds: f64) -> Option<usize> { pub fn index_at(&self, seconds: f64) -> Option<usize> {
if !seconds.is_finite() || self.samples.is_empty() { if !seconds.is_finite() || self.samples.is_empty() {
return None; return None;
} }
// The samples are ordered, so a binary search gives the insertion // 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. // point of the query, and the entry before it is the one in force.
// 样本已排序,故二分查找给出查询的插入点,其前一项即生效的那项。
match self match self
.samples .samples
.binary_search_by(|sample| sample.seconds.partial_cmp(&seconds).expect("finite times")) .binary_search_by(|sample| sample.seconds.partial_cmp(&seconds).expect("finite times"))
@ -144,6 +189,10 @@ impl TopologySeries {
/// Returns the topology in force at a time. /// Returns the topology in force at a time.
/// ///
/// Returns `None` when the time is not finite or the series is empty. /// Returns `None` when the time is not finite or the series is empty.
///
/// 返回某时间处生效的拓扑。
///
/// 时间非有限或序列为空时返回 `None`。
pub fn topology_at(&self, seconds: f64) -> Option<&Topology> { pub fn topology_at(&self, seconds: f64) -> Option<&Topology> {
Some(&self.samples.get(self.index_at(seconds)?)?.topology) Some(&self.samples.get(self.index_at(seconds)?)?.topology)
} }
@ -154,6 +203,12 @@ impl TopologySeries {
/// for the last entry. /// for the last entry.
/// ///
/// Returns `None` when the index is out of range. /// Returns `None` when the index is out of range.
///
/// 返回某下标处样本适用的区间。
///
/// 上界是下一样本的时间,对最后一项为正无穷。
///
/// 下标越界时返回 `None`。
pub fn interval_of(&self, index: usize) -> Option<(f64, f64)> { pub fn interval_of(&self, index: usize) -> Option<(f64, f64)> {
let sample = self.samples.get(index)?; let sample = self.samples.get(index)?;
let end = self let end = self
@ -168,6 +223,11 @@ impl TopologySeries {
/// This is the number of samples less one, since each change introduces /// 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 /// a new entry. It measures how dynamic the network is: a series with
/// many changes needs routing that accounts for them. /// many changes needs routing that accounts for them.
///
/// 返回拓扑变化的次数。
///
/// 它是样本数减一,因为每次变化引入一个新项。它衡量网络的动态
/// 程度:变化多的序列需要能考虑到这些变化的路由。
pub fn change_count(&self) -> usize { pub fn change_count(&self) -> usize {
self.samples.len().saturating_sub(1) self.samples.len().saturating_sub(1)
} }
@ -178,6 +238,13 @@ impl TopologySeries {
/// mean and the result covers the changes that have a measured interval. /// mean and the result covers the changes that have a measured interval.
/// ///
/// Returns `None` when there is only one sample. /// Returns `None` when there is only one sample.
///
/// 返回变化之间的平均间隔,单位为秒。
///
/// 最后一项的持续时间无界,故它被排除在均值之外,结果涵盖有可测
/// 间隔的那些变化。
///
/// 只有一个样本时返回 `None`。
pub fn mean_interval_seconds(&self) -> Option<f64> { pub fn mean_interval_seconds(&self) -> Option<f64> {
if self.samples.len() < 2 { if self.samples.len() < 2 {
return None; return None;
@ -193,18 +260,27 @@ impl TopologySeries {
/// one link contributes two, since one link changed. /// one link contributes two, since one link changed.
/// ///
/// Returns zero for a series of one sample. /// Returns zero for a series of one sample.
///
/// 返回整个序列中边变化的次数。
///
/// 每个拓扑与其前一个比较,统计出现与消失的边。相差一条链路的拓扑
/// 贡献二,因为有一条链路发生了变化。
///
/// 单样本序列返回零。
pub fn edge_changes(&self) -> usize { pub fn edge_changes(&self) -> usize {
let mut changes = 0; let mut changes = 0;
for pair in self.samples.windows(2) { for pair in self.samples.windows(2) {
let before = &pair[0].topology; let before = &pair[0].topology;
let after = &pair[1].topology; let after = &pair[1].topology;
// Edges present before but not after. // Edges present before but not after.
// 之前存在而之后不存在的边。
changes += before changes += before
.edges() .edges()
.iter() .iter()
.filter(|edge| !after.has_edge(edge.from, edge.to)) .filter(|edge| !after.has_edge(edge.from, edge.to))
.count(); .count();
// Edges present after but not before. // Edges present after but not before.
// 之后存在而之前不存在的边。
changes += after changes += after
.edges() .edges()
.iter() .iter()
@ -220,6 +296,11 @@ impl TopologySeries {
/// value is taken from the first entry. A caller that changes the node /// value is taken from the first entry. A caller that changes the node
/// count between entries is comparing two different networks and should /// count between entries is comparing two different networks and should
/// build two series instead. /// build two series instead.
///
/// 返回序列中的节点数。
///
/// 每个项都应描述同一组节点,故该值取自第一项。在各项之间改变节点
/// 数的调用方是在比较两个不同的网络,应改为构建两个序列。
pub fn node_count(&self) -> usize { pub fn node_count(&self) -> usize {
self.samples self.samples
.first() .first()