docs: bilingual comments for the network tests

This commit is contained in:
ApsisRS contributors 2026-10-02 01:59:43 +08:00
parent 55a1449a12
commit ad05381d61
13 changed files with 316 additions and 0 deletions

View File

@ -3,6 +3,11 @@
//! The parallel form must produce the same pass summaries, in the same
//! order, as the sequential one. Every test compares the two rather than
//! against a constant, so an ordering or content difference is caught.
//!
//! 批量可见性搜索的测试。
//!
//! 并行形式必须产生与顺序形式相同的过境汇总,且次序相同。每个测试都比较二者而非
//! 与常量比较,故次序或内容差异会被捕获。
use apsis_access::{AccessFinder, OrbitSample, find_all, find_all_parallel};
use apsis_coverage::GroundPoint;
@ -17,6 +22,8 @@ const SAMPLE: &str = concat!(
);
/// Returns a finder for a station on the equator.
///
/// 返回赤道上一座站的查找器。
fn finder() -> AccessFinder {
AccessFinder {
station: GroundPoint::new(Geodetic::new(0.0, 0.0, 0.0), 10.0_f64.to_radians()),
@ -30,12 +37,18 @@ fn finder() -> AccessFinder {
/// The rotation is folded into a simple sweep so the track is a plausible
/// ground track; the exact geometry is not what this test is about, only
/// that the batch search reproduces the sequential one.
///
/// 返回一颗卫星的地固样本轨迹。
///
/// 转动被折进一个简单的扫描,使轨迹是一条合理的地面轨迹;本测试关心的不是精确
/// 几何,只是批量搜索复现顺序搜索。
fn track(satellite: &Satellite, samples: usize) -> Vec<OrbitSample> {
(0..samples)
.map(|index| {
let seconds = index as f64 * 60.0;
// The propagated radius sets the shell the synthetic longitude
// sweep runs on, so the track is at a realistic altitude.
// 所传播的半径设定合成经度扫描所在的壳层,故轨迹处于真实的高度。
let radius = satellite
.propagate_minutes(seconds / 60.0)
.expect("the track propagates")
@ -44,6 +57,8 @@ fn track(satellite: &Satellite, samples: usize) -> Vec<OrbitSample> {
// A slow sweep in longitude stands in for the rotation into the
// Earth-fixed frame; the exact geometry is not what this test is
// about, only that the batch search reproduces the sequential one.
// 缓慢的经度扫描代行转入地固系的转动;本测试关心的不是精确几何,只是批量搜索
// 复现顺序搜索。
let angle = seconds / 4_000.0;
let position = Vec3::new(radius * angle.cos(), radius * angle.sin(), radius * 0.3);
OrbitSample::new(seconds, position)
@ -69,6 +84,7 @@ fn a_single_track_matches_the_sequential_form() {
#[test]
fn a_shell_of_tracks_matches_the_sequential_form() {
// Long enough that the parallel path takes the multi thread branch.
// 长到足以使并行路径走多线程分支。
let finder = finder();
let satellite = satellite();
let tracks: Vec<Vec<OrbitSample>> = (0..200).map(|_| track(&satellite, 120)).collect();
@ -88,10 +104,12 @@ fn an_empty_set_of_tracks_is_empty() {
fn a_bad_track_does_not_hide_the_others() {
// A track that is too short or out of order yields an error in both
// forms, at the same position, and the good tracks are still searched.
// 太短或次序不对的轨迹在两种形式中都于同一位置给出错误,而好的轨迹仍被搜索。
let finder = finder();
let satellite = satellite();
let mut tracks = vec![track(&satellite, 120), vec![], track(&satellite, 120)];
// The empty track is a degenerate case; search it as given.
// 空轨迹是退化情形;按原样搜索它。
let sequential = find_all(&finder, &tracks);
let parallel = find_all_parallel(&finder, &tracks);
assert_eq!(parallel, sequential);
@ -103,6 +121,7 @@ fn a_bad_track_does_not_hide_the_others() {
fn the_result_is_in_input_order() {
// Three tracks with different lengths, so each summary differs and the
// ordering is observable.
// 三条长度不同的轨迹,使每个汇总都不同,且次序可观察。
let finder = finder();
let satellite = satellite();
let tracks = vec![

View File

@ -12,6 +12,19 @@
//!
//! with `theta` the orbital position measured from the station and `n` the
//! mean motion. The values used below are computed from those expressions.
//!
//! 可见性窗口查找器的测试。
//!
//! 参考用例是自赤道上、遮蔽十度的一座站看 550 km 高度的圆赤道轨道。对该几何,
//! 仰角只依赖于轨道位置,故升起、峰值与降落时刻有闭式:
//!
//! ```text
//! theta_c = 90 - mask - asin(Re cos(mask) / Rs)
//! t = (theta - theta_0) / n
//! ```
//!
//! 其中 `theta` 是自站量起的轨道位置,`n` 是平运动。下面所用的值即由这些表达式
//! 计算。
use apsis_access::{AccessErrorKind, AccessFinder, OrbitSample, Pass, PassSummary};
use apsis_coverage::GroundPoint;
@ -20,22 +33,34 @@ use apsis_math::Vec3;
use core::f64::consts::{FRAC_PI_2, PI};
/// The Earth equatorial radius, in kilometers.
///
/// 地球赤道半径,单位为千米。
const BODY_RADIUS: f64 = 6_378.137;
/// The orbit radius for a 550 km circular orbit, in kilometers.
///
/// 550 km 圆轨道的轨道半径,单位为千米。
const ORBIT_RADIUS: f64 = BODY_RADIUS + 550.0;
/// The mean motion of the reference orbit, in radians per second.
///
/// This is `sqrt(mu / a^3)` with the WGS 84 gravitational parameter.
///
/// 参考轨道的平运动,单位为弧度每秒。
///
/// 这是使用 WGS 84 引力参数的 `sqrt(mu / a^3)`。
const MEAN_MOTION: f64 = 0.001_094_823_692_885_802;
/// A station on the equator with a ten degree mask.
///
/// 赤道上、遮蔽十度的一座站。
fn station() -> GroundPoint {
GroundPoint::new(Geodetic::new(0.0, 0.0, 0.0), 10.0_f64.to_radians())
}
/// A finder with a one second step and a millisecond tolerance.
///
/// 一秒步长、毫秒容差的查找器。
fn finder() -> AccessFinder {
AccessFinder {
station: station(),
@ -49,6 +74,10 @@ fn finder() -> AccessFinder {
///
/// The reference closed form solves for the crossing times relative to an
/// initial position of minus one radian, so the track starts there.
///
/// 搜索开始时自站量起的轨道位置,单位为弧度。
///
/// 参考闭式是相对初始位置负一弧度求解穿越时刻,故轨迹从那里开始。
const INITIAL_ANGLE: f64 = -1.0;
/// Returns the satellite position for a reference orbit that passes over the
@ -58,12 +87,19 @@ const INITIAL_ANGLE: f64 = -1.0;
/// angular position at the given time. The angle is measured from the station
/// and increases at the mean motion, so the elevation is a function of that
/// angle alone.
///
/// 返回一条自该站正上方经过的参考轨道的卫星位置。
///
/// 轨道是圆形的且位于赤道面,故位置只依赖于给定时刻的角位置。该角自站量起并以平
/// 运动增大,故仰角只是该角的函数。
fn reference_position(seconds: f64) -> Vec3 {
let angle = INITIAL_ANGLE + MEAN_MOTION * seconds;
Vec3::new(ORBIT_RADIUS * angle.cos(), ORBIT_RADIUS * angle.sin(), 0.0)
}
/// Returns the samples of the reference orbit over a window around one pass.
///
/// 返回参考轨道在一次过境附近窗口上的样本。
fn reference_samples(step: f64, start: f64, end: f64) -> Vec<OrbitSample> {
let mut samples = Vec::new();
let mut seconds = start;
@ -78,16 +114,26 @@ fn reference_samples(step: f64, start: f64, end: f64) -> Vec<OrbitSample> {
///
/// This is `90 - mask - asin(Re cos(mask) / Rs)`, which for the reference
/// case is 14.9566 degrees.
///
/// 参考几何的覆盖区半角,单位为弧度。
///
/// 这是 `90 - mask - asin(Re cos(mask) / Rs)`,对参考用例为 14.9566 度。
const FOOTPRINT_HALF_ANGLE: f64 = 0.261_042_364_648_894_9;
/// The rise time of the reference pass from the start of the search, in
/// seconds.
///
/// 参考过境自搜索起点起的升起时刻,单位为秒。
const REFERENCE_RISE: f64 = 674.955_681_770_3;
/// The peak time of the reference pass, in seconds.
///
/// 参考过境的峰值时刻,单位为秒。
const REFERENCE_PEAK: f64 = 913.389_074_878_5;
/// The set time of the reference pass, in seconds.
///
/// 参考过境的降落时刻,单位为秒。
const REFERENCE_SET: f64 = 1_151.822_467_986_6;
#[test]
@ -165,6 +211,7 @@ fn the_peak_of_an_overhead_pass_is_a_right_angle() {
let pass = &summary.passes[0];
// The reference geometry passes directly over the station, so the
// greatest elevation is a right angle.
// 参考几何自该站正上方经过,故最大仰角是直角。
assert!(
(pass.peak_elevation - FRAC_PI_2).abs() < 0.01,
"peak elevation {}",
@ -192,6 +239,7 @@ fn the_duration_matches_the_geometry() {
let pass = &summary.passes[0];
// The orbital position spans twice the footprint half angle, so the
// duration is 2 theta_c / n.
// 轨道位置跨越覆盖区半角的两倍,故时长为 2 theta_c / n。
let expected = 2.0 * FOOTPRINT_HALF_ANGLE / MEAN_MOTION;
assert!(
(pass.duration() - expected).abs() < 1.0,
@ -207,6 +255,8 @@ fn the_refinement_is_independent_of_the_sample_step() {
// step falls. The residual difference between two steps is second order
// in the step, because the chord between two samples departs from the
// arc by that order.
// 穿越时刻通过样本之间插值卫星位置求得,故结果随步长减小而趋近真穿越。两个步长
// 之间的残余差是步长的二阶量,因为两样本之间的弦与弧的偏差为该阶。
let coarse = finder()
.find(&reference_samples(10.0, 0.0, 2_000.0))
.expect("valid");
@ -221,6 +271,7 @@ fn the_refinement_is_independent_of_the_sample_step() {
// A ten second step gives a crossing within about a tenth of a second
// of the one found with a one second step, because the chord error is
// second order.
// 十秒步长给出的穿越与一秒步长所得相差约十分之一秒,因为弦误差是二阶的。
assert!(
(coarse_pass.rise_seconds - fine_pass.rise_seconds).abs() < 0.1,
"rise {} against {}",
@ -249,6 +300,7 @@ fn the_tolerance_controls_the_crossing_accuracy() {
let loose_pass = loose.find(&samples).expect("valid").passes[0];
let tight_pass = tight.find(&samples).expect("valid").passes[0];
// The two agree to within the looser tolerance.
// 二者在较宽松的容差内一致。
assert!((loose_pass.rise_seconds - tight_pass.rise_seconds).abs() <= 1.0);
assert!((loose_pass.set_seconds - tight_pass.set_seconds).abs() <= 1.0);
}
@ -257,6 +309,7 @@ fn the_tolerance_controls_the_crossing_accuracy() {
fn a_full_orbit_finds_exactly_one_pass() {
// The reference geometry gives one overhead pass per revolution, so a
// window of one period contains one pass.
// 参考几何每圈给出一次过顶,故一个周期的窗口含一次过境。
let period = 2.0 * PI / MEAN_MOTION;
let samples = reference_samples(1.0, 0.0, period);
let summary = finder().find(&samples).expect("valid");
@ -272,6 +325,7 @@ fn the_summary_reports_the_access_fraction() {
assert!((fraction - expected).abs() < 1e-12);
// One overhead pass in a two thousand second window is about a quarter
// of the time.
// 两千秒窗口中的一次过顶约占四分之一时间。
assert!(fraction > 0.2 && fraction < 0.3, "fraction {fraction}");
}
@ -288,6 +342,7 @@ fn the_summary_finds_the_longest_and_highest_pass() {
fn a_pass_that_starts_before_the_window_is_clipped() {
// Starting the search after the rise means the pass is reported from
// the first sample rather than extrapolated backwards.
// 在升起之后开始搜索意味着过境自第一个样本起报告,而不向后外推。
let samples = reference_samples(1.0, 800.0, 1_400.0);
let summary = finder().find(&samples).expect("valid");
assert_eq!(summary.count(), 1);
@ -309,6 +364,7 @@ fn a_pass_that_ends_after_the_window_is_clipped() {
#[test]
fn a_window_with_no_pass_is_empty() {
// A window before the satellite comes into view contains no pass.
// 在卫星进入视野之前的窗口不含过境。
let samples = reference_samples(1.0, -2_000.0, -1_000.0);
let summary = finder().find(&samples).expect("valid");
assert_eq!(summary.count(), 0);
@ -322,6 +378,7 @@ fn a_window_with_no_pass_is_empty() {
fn the_gap_between_passes_is_reported() {
// Two orbits give two passes, and the gap between them is the period
// less the pass duration.
// 两个轨道给出两次过境,其间的间隙是周期减去过境时长。
let period = 2.0 * PI / MEAN_MOTION;
let samples = reference_samples(1.0, 0.0, 2.0 * period);
let summary = finder().find(&samples).expect("valid");
@ -331,6 +388,7 @@ fn the_gap_between_passes_is_reported() {
let expected = period - summary.passes[0].duration();
assert!((gap - expected).abs() < 2.0, "{gap} against {expected}");
// The largest gap is at least that one.
// 最大间隙至少是该值。
let longest = summary.longest_gap_seconds().expect("at least two passes");
assert!(longest >= gap - 1e-9);
}
@ -381,6 +439,7 @@ fn an_unordered_sample_sequence_is_rejected() {
let samples = reference_samples(1.0, 1_000.0, 0.0);
assert!(samples.is_empty());
// The ends run backwards.
// 两端是倒序的。
let backwards = [
OrbitSample::new(100.0, reference_position(100.0)),
OrbitSample::new(50.0, reference_position(50.0)),
@ -393,6 +452,9 @@ fn an_unordered_sample_sequence_is_rejected() {
// list, and a binary search over an unordered list returns a wrong index
// without failing, so the search would refine a crossing against the
// wrong pair of positions and report a pass that never happened.
// 两端有序但中间无序。这正是只检查两端会漏掉的情形:插值对该列表做二分查找,而
// 对无序列表的二分查找会返回错误下标而不报错,故搜索会针对错误的位置对细化穿越,
// 并报告一个从未发生过的过境。
let mut shuffled = reference_samples(60.0, 0.0, 1_200.0);
assert!(shuffled.len() > 6, "the reference track is long enough");
shuffled.swap(2, 4);
@ -418,6 +480,7 @@ fn an_invalid_position_is_rejected() {
fn the_finder_can_drive_a_propagator_directly() {
// The convenience entry point must agree with building the samples by
// hand.
// 该便利入口必须与手工构建样本一致。
let via_samples = finder()
.find(&reference_samples(1.0, 0.0, 2_000.0))
.expect("valid");
@ -495,6 +558,8 @@ fn the_propagator_entry_point_reaches_the_tolerance_regardless_of_step() {
// When the propagator is available the refinement evaluates the true
// position at each bisection point rather than interpolating, so the
// crossing is accurate to the tolerance however coarse the sampling.
// 当传播器可用时,细化在每个二分点求真实位置而非插值,故无论采样多粗,穿越都
// 精确到容差。
let coarse = AccessFinder {
sample_step_seconds: 30.0,
crossing_tolerance_seconds: 1e-6,
@ -529,6 +594,7 @@ fn the_propagator_entry_point_reaches_the_tolerance_regardless_of_step() {
second.set_seconds
);
// The crossing matches the closed form to the tolerance.
// 该穿越在容差内与闭式一致。
assert!(
(first.rise_seconds - REFERENCE_RISE).abs() < 1e-5,
"rise {} against {REFERENCE_RISE}",

View File

@ -1,4 +1,6 @@
//! Tests for the Walker constellation pattern.
//!
//! Walker 星座模式的测试。
use apsis_constellation::{Constellation, ConstellationErrorKind, Satellite, WalkerPattern};
use apsis_kepler::true_from_mean;
@ -8,6 +10,8 @@ const TOLERANCE: f64 = 1e-12;
/// A Walker 53 degree pattern with 24 satellites in 3 planes, as used by a
/// mid latitude broadband constellation.
///
/// 一个 53 度的 Walker 模式,3 个轨道面、24 颗卫星,如中纬度宽带星座所用。
fn broadband_pattern() -> WalkerPattern {
WalkerPattern {
total_satellites: 24,
@ -24,6 +28,7 @@ fn the_derived_counts_are_correct() {
assert_eq!(pattern.satellites_per_plane(), Some(8));
assert!((pattern.plane_spacing().expect("valid") - 2.0 * PI / 3.0).abs() < TOLERANCE);
// The phase offset is 1 * 360 / 24 degrees.
// 相位偏移为 1 * 360 / 24 度。
let expected = 2.0 * PI / 24.0;
assert!((pattern.phase_offset().expect("valid") - expected).abs() < TOLERANCE);
}
@ -87,6 +92,7 @@ fn the_phase_offset_appears_between_adjacent_planes() {
// The first satellite of each plane is advanced by the phase offset
// relative to the first satellite of the previous plane.
// 每个轨道面的第一颗卫星相对前一面第一颗卫星超前一个相位偏移。
let first_of_plane = |plane: usize| -> f64 {
constellation
.plane(plane)
@ -131,6 +137,7 @@ fn a_zero_phase_factor_gives_no_offset_between_planes() {
.mean_anomaly()
};
// Every plane starts at the same anomaly.
// 每个轨道面从相同的近点角开始。
assert!((first_of_plane(0) - first_of_plane(1)).abs() < 1e-9);
assert!((first_of_plane(1) - first_of_plane(2)).abs() < 1e-9);
}
@ -158,6 +165,7 @@ fn a_single_plane_pattern_is_accepted() {
assert_eq!(constellation.len(), 6);
assert_eq!(constellation.plane_count(), 1);
// All satellites share a plane, so they share a right ascension.
// 所有卫星共享一个轨道面,故共享一个赤经。
for satellite in constellation.satellites() {
assert_eq!(satellite.elements.right_ascension, 0.0);
}
@ -279,6 +287,7 @@ fn the_satellite_indices_are_consistent() {
assert_eq!(satellite.index, position);
// The satellites are emitted plane by plane, so the plane index is
// the quotient of the position and the plane population.
// 卫星逐面发射,故轨道面下标是位置除以每面卫星数之商。
assert_eq!(satellite.plane, position / 8);
assert_eq!(satellite.index_in_plane, position % 8);
}
@ -343,6 +352,7 @@ fn a_custom_constellation_groups_satellites_by_right_ascension() {
assert_eq!(constellation.pattern(), None);
// The first two share a plane.
// 前两颗共享一个轨道面。
assert_eq!(constellation.satellites()[0].plane, 0);
assert_eq!(constellation.satellites()[1].plane, 0);
assert_eq!(constellation.satellites()[2].plane, 1);
@ -381,6 +391,7 @@ fn an_out_of_range_plane_query_is_empty() {
fn the_satellite_type_is_copyable() {
// The type must remain cheap to pass around, since coverage and access
// analysis iterate over it repeatedly.
// 该类型必须保持传递开销小,因为覆盖与可见性分析会反复遍历它。
fn assert_copy<T: Copy>() {}
assert_copy::<Satellite>();
}

View File

@ -1,4 +1,6 @@
//! Tests for footprint geometry and coverage fractions.
//!
//! 覆盖区几何与覆盖比例的测试。
use apsis_coverage::{
Footprint, central_angle_between, footprint, footprint_area, footprint_central_angle,
@ -8,12 +10,18 @@ use apsis_math::Vec3;
use core::f64::consts::{FRAC_PI_2, PI};
/// The Earth equatorial radius, in kilometers.
///
/// 地球赤道半径,单位为千米。
const BODY_RADIUS: f64 = 6_378.137;
/// A low Earth orbit at 550 km altitude.
///
/// 550 km 高度的低轨。
const LOW_ORBIT: f64 = BODY_RADIUS + 550.0;
/// A geostationary radius.
///
/// 地球静止轨道半径。
const GEOSTATIONARY: f64 = 42_164.0;
const TOLERANCE: f64 = 1e-9;
@ -23,6 +31,8 @@ fn the_footprint_at_a_ten_degree_mask_matches_a_hand_computation() {
// A 550 km satellite at a ten degree mask has a central angle of
// 14.9566 degrees, a surface radius of 1664.965 km, and an area of
// 8 659 493.8 square kilometers.
// 十度遮蔽下 550 km 卫星的中心角为 14.9566 度,地表半径为 1664.965 km,面积为
// 8 659 493.8 平方千米。
let mask = 10.0_f64.to_radians();
let footprint = footprint(LOW_ORBIT, BODY_RADIUS, mask).expect("valid");
assert!((footprint.central_angle.to_degrees() - 14.956_635_124_957_29).abs() < 1e-9);
@ -56,6 +66,7 @@ fn the_area_grows_with_the_central_angle() {
fn a_footprint_can_never_cover_more_than_a_hemisphere() {
// The limit is a satellite at the surface with no mask, which sees half
// the sphere. Any real satellite sees less.
// 极限是地表上、无遮蔽的卫星,它看到半个球面。任何真实卫星看到的都更少。
for satellite_radius in [LOW_ORBIT, GEOSTATIONARY, 1.0e6] {
for mask_degrees in [0.0_f64, 10.0, 45.0] {
let fraction =
@ -73,6 +84,7 @@ fn a_footprint_can_never_cover_more_than_a_hemisphere() {
fn a_geostationary_satellite_sees_about_a_third_of_the_earth() {
// A geostationary satellite at a zero degree mask sees to about 81.3
// degrees of central angle, which is a fraction of about 0.42.
// 零度遮蔽的地球静止卫星看到约 81.3 度的中心角,比例约为 0.42。
let fraction = footprint_fraction(GEOSTATIONARY, BODY_RADIUS, 0.0).expect("valid");
assert!(fraction > 0.40 && fraction < 0.43, "fraction {fraction}");
}
@ -91,15 +103,18 @@ fn containment_is_inclusive_at_the_edge() {
#[test]
fn the_central_angle_between_two_surface_points_is_the_included_angle() {
// Two points on the equator ninety degrees apart.
// 赤道上相隔九十度的两个点。
let first = Vec3::new(BODY_RADIUS, 0.0, 0.0);
let second = Vec3::new(0.0, BODY_RADIUS, 0.0);
let angle = central_angle_between(first, second).expect("valid");
assert!((angle - FRAC_PI_2).abs() < TOLERANCE);
// A point with itself gives zero.
// 点与其自身给出零。
assert!(central_angle_between(first, first).expect("valid").abs() < TOLERANCE);
// Opposite points give a straight angle.
// 相对的点给出平角。
let opposite = Vec3::new(-BODY_RADIUS, 0.0, 0.0);
assert!((central_angle_between(first, opposite).expect("valid") - PI).abs() < TOLERANCE);
}
@ -107,6 +122,7 @@ fn the_central_angle_between_two_surface_points_is_the_included_angle() {
#[test]
fn the_central_angle_between_points_is_scale_invariant() {
// The angle depends only on the directions, not on the radii.
// 该角只依赖于方向,而不依赖于半径。
let near = Vec3::new(1.0, 0.5, 0.25);
let far = near * 1_000.0;
let other = Vec3::new(0.0, 1.0, 0.0);
@ -126,9 +142,11 @@ fn a_degenerate_vector_has_no_central_angle() {
fn point_in_footprint_uses_the_central_angle_from_the_sub_satellite_point() {
let footprint = Footprint::new(0.1, BODY_RADIUS);
// The sub-satellite point on the equator.
// 赤道上的星下点。
let sub_satellite = Vec3::new(BODY_RADIUS, 0.0, 0.0);
// A point within the footprint, about 0.05 radians away.
// 覆盖区内的一个点,约 0.05 弧度之外。
let inside = Vec3::new(
BODY_RADIUS * 0.05_f64.cos(),
BODY_RADIUS * 0.05_f64.sin(),
@ -140,6 +158,7 @@ fn point_in_footprint_uses_the_central_angle_from_the_sub_satellite_point() {
);
// A point well outside it, about a quarter radian away.
// 远在其外的一个点,约四分之一弧度之外。
let outside = Vec3::new(
BODY_RADIUS * 0.25_f64.cos(),
BODY_RADIUS * 0.25_f64.sin(),

View File

@ -3,21 +3,33 @@
//! The expected values are computed independently from the law of sines
//! applied to the triangle formed by the satellite, the ground point, and
//! the centre of the Earth.
//!
//! 地面覆盖计算中卫星几何的测试。
//!
//! 期望值由正弦定理独立计算,应用于卫星、地面点与地心构成的三角形。
use apsis_coverage::{earth_central_angle, elevation_from_central_angle, nadir_angle, slant_range};
use core::f64::consts::FRAC_PI_2;
/// The Earth equatorial radius, in kilometers.
///
/// 地球赤道半径,单位为千米。
const BODY_RADIUS: f64 = 6_378.137;
/// A low Earth orbit at 550 km altitude.
///
/// 550 km 高度的低轨。
const LOW_ORBIT: f64 = BODY_RADIUS + 550.0;
/// A medium Earth orbit at 20 200 km altitude, near the navigation
/// constellations.
///
/// 20 200 km 高度的中轨,接近导航星座。
const MEDIUM_ORBIT: f64 = BODY_RADIUS + 20_200.0;
/// A geostationary radius.
///
/// 地球静止轨道半径。
const GEOSTATIONARY: f64 = 42_164.0;
const TOLERANCE: f64 = 1e-9;
@ -26,6 +38,7 @@ const TOLERANCE: f64 = 1e-9;
fn the_angles_at_the_zenith_are_consistent() {
// Directly below the satellite the elevation is a right angle, the
// nadir angle is zero, and the central angle is zero.
// 卫星正下方处仰角为直角,天底角为零,中心角为零。
assert!(
nadir_angle(LOW_ORBIT, BODY_RADIUS, FRAC_PI_2)
.expect("valid")
@ -39,6 +52,7 @@ fn the_angles_at_the_zenith_are_consistent() {
< TOLERANCE
);
// The slant range is the altitude.
// 斜距即高度。
assert!(
(slant_range(LOW_ORBIT, BODY_RADIUS, FRAC_PI_2).expect("valid") - 550.0).abs() < TOLERANCE
);
@ -47,6 +61,7 @@ fn the_angles_at_the_zenith_are_consistent() {
#[test]
fn the_three_angles_are_complementary() {
// elevation + nadir + central = 90 degrees, for every geometry.
// 对每种几何,elevation + nadir + central = 90 度。
for satellite_radius in [LOW_ORBIT, MEDIUM_ORBIT, GEOSTATIONARY] {
for elevation_degrees in [0.0_f64, 5.0, 10.0, 30.0, 60.0, 89.0] {
let elevation = elevation_degrees.to_radians();
@ -67,6 +82,8 @@ fn the_geometry_matches_a_hand_computation() {
// A 550 km satellite at a ten degree mask. The nadir angle is
// asin(Re cos(el) / Rs) = 65.0434 degrees, the central angle is
// 14.9566 degrees, and the slant range is 1815.652 km.
// 十度遮蔽下的 550 km 卫星。天底角为 asin(Re cos(el) / Rs) = 65.0434 度,
// 中心角为 14.9566 度,斜距为 1815.652 km。
let elevation = 10.0_f64.to_radians();
assert!(
(nadir_angle(LOW_ORBIT, BODY_RADIUS, elevation)
@ -105,6 +122,7 @@ fn a_higher_satellite_sees_further() {
fn a_lower_elevation_mask_sees_further() {
// Relaxing the mask lets the satellite be seen closer to the horizon,
// which increases the central angle.
// 放宽遮蔽使卫星能被看到至更接近地平,这增大中心角。
let mut previous = 0.0;
for elevation_degrees in [30.0_f64, 20.0, 10.0, 5.0, 0.0] {
let central = earth_central_angle(LOW_ORBIT, BODY_RADIUS, elevation_degrees.to_radians())
@ -122,6 +140,7 @@ fn a_zero_degree_mask_is_the_horizon() {
// At a zero degree mask the central angle is the horizon angle
// acos(Re / Rs), which is where the line of sight is tangent to the
// surface.
// 零度遮蔽下中心角即地平角 acos(Re / Rs),那里视线与地表相切。
let central = earth_central_angle(LOW_ORBIT, BODY_RADIUS, 0.0).expect("valid");
let horizon = (BODY_RADIUS / LOW_ORBIT).acos();
assert!((central - horizon).abs() < 1e-9);
@ -130,6 +149,7 @@ fn a_zero_degree_mask_is_the_horizon() {
#[test]
fn the_slant_range_grows_as_the_elevation_falls() {
// A satellite near the horizon is further away than one overhead.
// 接近地平的卫星比过顶的更远。
let overhead = slant_range(LOW_ORBIT, BODY_RADIUS, FRAC_PI_2).expect("valid");
let low = slant_range(LOW_ORBIT, BODY_RADIUS, 5.0_f64.to_radians()).expect("valid");
assert!(low > overhead, "{low} against {overhead}");
@ -164,6 +184,7 @@ fn the_elevation_is_a_right_angle_when_the_central_angle_is_zero() {
fn a_satellite_inside_the_body_is_rejected() {
// A satellite below the surface has no geometry, so the functions
// report nothing rather than a negative angle.
// 地表以下的卫星没有几何,故这些函数报告空而非负角。
assert_eq!(nadir_angle(BODY_RADIUS - 1.0, BODY_RADIUS, 0.0), None);
assert_eq!(
earth_central_angle(BODY_RADIUS - 1.0, BODY_RADIUS, 0.0),
@ -193,6 +214,8 @@ fn a_grazing_geometry_is_handled_at_the_horizon() {
// At the horizon the nadir angle is the arc sine of the ratio of the
// radii, which is exactly one when the satellite is at the surface and
// less than one above it. The clamp keeps the arc sine defined.
// 在地平处天底角是两半径之比的反正弦,卫星在地表时它恰为一,在其上方时小于一。
// 该夹逼保持反正弦有定义。
let nadir = nadir_angle(LOW_ORBIT, BODY_RADIUS, 0.0).expect("valid");
assert!(nadir.is_finite());
let expected = (BODY_RADIUS / LOW_ORBIT).asin();

View File

@ -1,4 +1,6 @@
//! Tests for line of sight and visibility from a ground station.
//!
//! 自地面站的视线与可见性的测试。
use apsis_coverage::{
GroundPoint, Sensor, central_angle_to_station, elevation_angle, line_of_sight, local_up,
@ -9,9 +11,13 @@ use apsis_math::Vec3;
use core::f64::consts::{FRAC_PI_2, PI};
/// The Earth equatorial radius, in kilometers.
///
/// 地球赤道半径,单位为千米。
const BODY_RADIUS: f64 = 6_378.137;
/// A station at the equator on the prime meridian, at sea level.
///
/// 赤道上本初子午线处、海平面的一座站。
fn equatorial_station() -> GroundPoint {
GroundPoint::new(Geodetic::new(0.0, 0.0, 0.0), 0.0)
}
@ -30,6 +36,7 @@ fn a_station_on_the_equator_is_at_the_equatorial_radius() {
fn a_station_at_the_pole_is_at_the_polar_radius() {
// The polar radius is the semi-minor axis of the ellipsoid, which is
// about 21 km less than the equatorial radius.
// 极半径是椭球的半短轴,比赤道半径小约 21 km。
let station = GroundPoint::new(Geodetic::new(FRAC_PI_2, 0.0, 0.0), 0.0);
let radius = station.radius();
assert!((radius - 6_356.752).abs() < 0.01, "radius {radius}");
@ -58,6 +65,7 @@ fn the_local_up_at_the_pole_is_the_third_axis() {
fn a_satellite_overhead_is_at_the_zenith() {
let station = equatorial_station();
// Directly above the station along the local normal.
// 沿当地法线位于该站正上方。
let satellite = Vec3::new(BODY_RADIUS + 550.0, 0.0, 0.0);
let elevation = elevation_angle(satellite, &station).expect("valid");
assert!(
@ -73,6 +81,8 @@ fn a_satellite_at_the_horizon_has_zero_elevation() {
// sphere, at a central angle of about 22.98 degrees for a satellite at
// 550 km. At that geometry the elevation is zero and the satellite is
// at its greatest distance along the surface.
// 当地地平是视线与球面相切之处,对 550 km 的卫星中心角约为 22.98 度。在该几何
// 处仰角为零,卫星沿地表距站最远。
let central = (BODY_RADIUS / (BODY_RADIUS + 550.0)).acos();
let satellite = Vec3::new(
(BODY_RADIUS + 550.0) * central.cos(),
@ -87,6 +97,7 @@ fn a_satellite_at_the_horizon_has_zero_elevation() {
fn a_satellite_below_the_horizon_has_negative_elevation() {
let station = equatorial_station();
// On the far side of the Earth, the elevation is negative.
// 在地球远侧,仰角为负。
let satellite = Vec3::new(-(BODY_RADIUS + 550.0), 0.0, 0.0);
let elevation = elevation_angle(satellite, &station).expect("valid");
assert!(elevation < -1.0, "elevation {elevation}");
@ -97,6 +108,7 @@ fn the_elevation_is_symmetric_about_the_overhead_direction() {
let station = equatorial_station();
// Two satellites at the same angular distance from the overhead point,
// one to the east and one to the west, have the same elevation.
// 距过顶点的角距离相同、一个在东一个在西的两颗卫星具有相同的仰角。
let angle = 0.3_f64;
let east = Vec3::new(
(BODY_RADIUS + 550.0) * angle.cos(),
@ -119,17 +131,20 @@ fn visibility_respects_the_elevation_mask() {
let sensor = Sensor::omnidirectional(station);
// Overhead is well above the mask.
// 过顶处远高于遮蔽。
assert_eq!(
visible_from(Vec3::new(BODY_RADIUS + 550.0, 0.0, 0.0), &sensor),
Some(true)
);
// On the horizon the satellite is below a 0.2 radian mask, because the
// horizon direction is a right angle from the local up vector.
// 在地平处卫星低于 0.2 弧度的遮蔽,因为地平方向与当地朝上矢量成直角。
assert_eq!(
visible_from(Vec3::new(0.0, BODY_RADIUS + 550.0, 0.0), &sensor),
Some(false)
);
// On the far side is well below it.
// 在远侧远低于它。
assert_eq!(
visible_from(Vec3::new(-(BODY_RADIUS + 550.0), 0.0, 0.0), &sensor),
Some(false)
@ -141,6 +156,7 @@ fn a_zero_mask_admits_a_satellite_at_the_horizon() {
let sensor = Sensor::omnidirectional(equatorial_station());
// A satellite just inside the horizon direction has a small positive
// elevation and is visible with a zero mask.
// 恰在地平方向之内的卫星有小正仰角,在零遮蔽下可见。
let central = (BODY_RADIUS / (BODY_RADIUS + 550.0)).acos() * 0.999;
let satellite = Vec3::new(
(BODY_RADIUS + 550.0) * central.cos(),
@ -154,6 +170,7 @@ fn a_zero_mask_admits_a_satellite_at_the_horizon() {
fn a_directional_sensor_only_sees_its_forward_hemisphere() {
// The station points at the zenith, so it sees a satellite overhead but
// not one on the horizon behind it.
// 该站指向天顶,故它看到过顶的卫星,但看不到其后方地平的卫星。
let station = equatorial_station();
let sensor = Sensor::directional(station, Vec3::new(1.0, 0.0, 0.0));
@ -163,6 +180,7 @@ fn a_directional_sensor_only_sees_its_forward_hemisphere() {
);
// A satellite to the east is at a right angle to the pointing, so it is
// not in the forward hemisphere.
// 东侧的卫星与指向成直角,故不在前半球。
assert_eq!(
visible_from(Vec3::new(0.0, BODY_RADIUS + 550.0, 0.0), &sensor),
Some(false)
@ -172,6 +190,7 @@ fn a_directional_sensor_only_sees_its_forward_hemisphere() {
#[test]
fn a_directional_sensor_normalizes_its_pointing() {
// An unnormalized pointing vector must behave the same as a unit one.
// 未归一化的指向矢量必须与单位矢量表现相同。
let station = equatorial_station();
let unit = Sensor::directional(station, Vec3::new(1.0, 0.0, 0.0));
let scaled = Sensor::directional(station, Vec3::new(100.0, 0.0, 0.0));
@ -196,6 +215,7 @@ fn the_geodetic_helper_agrees_with_the_sensor_path() {
let mask = 0.15;
let satellite = {
// A point above the station.
// 该站上方的一个点。
let station = GroundPoint::new(position, mask);
station.ecef() * 1.09
};
@ -236,12 +256,14 @@ fn a_satellite_on_the_polar_axis_has_no_sub_satellite_point() {
#[test]
fn the_central_angle_to_a_station_follows_the_spherical_relation() {
// A satellite at the zenith of the station has a central angle of zero.
// 位于该站天顶的卫星中心角为零。
let station = equatorial_station();
let satellite = Vec3::new(BODY_RADIUS + 550.0, 0.0, 0.0);
let central = central_angle_to_station(satellite, &station, FRAC_PI_2).expect("valid");
assert!(central.abs() < 1e-9);
// At a ten degree elevation the central angle is the footprint angle.
// 十度仰角处中心角即覆盖区角。
let central =
central_angle_to_station(satellite, &station, 10.0_f64.to_radians()).expect("valid");
assert!((central.to_degrees() - 14.956_635_124_957_29).abs() < 1e-6);
@ -252,6 +274,7 @@ fn invalid_inputs_are_rejected() {
let station = equatorial_station();
assert_eq!(elevation_angle(Vec3::splat(f64::NAN), &station), None);
// A station with an out of range mask is not usable.
// 遮蔽越界的站不可用。
let bad = GroundPoint::new(Geodetic::new(0.0, 0.0, 0.0), PI);
assert!(!bad.is_valid());
assert_eq!(
@ -262,6 +285,7 @@ fn invalid_inputs_are_rejected() {
None
);
// A satellite at the same place as the station has no line of sight.
// 与站同处的卫星没有视线。
let coincident = station.ecef();
assert_eq!(line_of_sight(coincident, &station), None);
}

View File

@ -1,4 +1,6 @@
//! Tests for handover decisions.
//!
//! 切换决策的测试。
use apsis_handover::{
Candidate, HandoverDecision, HandoverErrorKind, HandoverPolicy, HandoverRecord,
@ -6,11 +8,15 @@ use apsis_handover::{
};
/// The default policy: minimum quality 0.2, margin 0.1, dwell 5 s.
///
/// 缺省策略:最低质量 0.2、余量 0.1、驻留 5 s。
fn policy() -> HandoverPolicy {
HandoverPolicy::default()
}
/// A candidate with the given identifier and quality.
///
/// 具有给定标识符与质量的候选。
fn candidate(satellite: usize, quality: f64) -> Candidate {
Candidate::new(satellite, quality, 100.0)
}
@ -29,6 +35,7 @@ fn a_policy_is_validated() {
assert!(HandoverPolicy::new(0.2, 0.0, 0.0).is_valid());
// A negative margin would make a handover worthwhile to a worse
// candidate.
// 负余量会使切换到更差候选也值得。
assert!(!HandoverPolicy::new(0.2, -0.1, 5.0).is_valid());
assert!(!HandoverPolicy::new(0.2, 0.1, -1.0).is_valid());
assert!(!HandoverPolicy::new(f64::NAN, 0.1, 5.0).is_valid());
@ -76,6 +83,7 @@ fn a_terminal_with_no_server_drops_when_nothing_is_good_enough() {
#[test]
fn a_good_current_server_is_kept() {
// The alternative is better, but by less than the margin.
// 备选更好,但幅度小于余量。
let candidates = [candidate(1, 0.80), candidate(2, 0.85)];
let decision = decide(&policy(), Some(1), &candidates).expect("valid");
assert_eq!(decision, HandoverDecision::Stay);
@ -84,6 +92,7 @@ fn a_good_current_server_is_kept() {
#[test]
fn a_clearly_better_candidate_causes_a_handover() {
// The alternative beats the current server by more than the margin.
// 备选超出当前服务星多于余量。
let candidates = [candidate(1, 0.50), candidate(2, 0.80)];
let decision = decide(&policy(), Some(1), &candidates).expect("valid");
assert_eq!(
@ -103,6 +112,7 @@ fn the_margin_is_the_threshold_for_a_handover() {
let policy = policy();
// Exactly at the margin is not enough, because the comparison is
// strict.
// 恰在余量处不够,因为该比较是严格的。
let at_margin = [candidate(1, 0.5), candidate(2, 0.6)];
assert_eq!(
decide(&policy, Some(1), &at_margin).expect("valid"),
@ -110,6 +120,7 @@ fn the_margin_is_the_threshold_for_a_handover() {
);
// Just above the margin is enough.
// 恰高于余量即可。
let above = [candidate(1, 0.5), candidate(2, 0.600_1)];
assert!(decide(&policy, Some(1), &above).expect("valid").is_change());
}
@ -151,6 +162,7 @@ fn a_weak_terminal_drops_when_no_candidate_meets_the_minimum() {
fn a_current_server_that_disappears_causes_a_handover() {
// The current server is no longer in the candidate list, which means
// its access window has ended.
// 当前服务星不再在候选列表中,这意味着它的可见性窗口已结束。
let candidates = [candidate(2, 0.6), candidate(3, 0.4)];
let decision = decide(&policy(), Some(1), &candidates).expect("valid");
assert_eq!(
@ -182,6 +194,7 @@ fn the_only_candidate_being_the_current_server_keeps_it() {
fn a_weak_current_server_with_no_alternative_drops() {
// The current server is below the minimum and is the only candidate, so
// there is nowhere to move and the service is lost.
// 当前服务星低于最低值且是唯一候选,故无处可迁,服务丢失。
let candidates = [candidate(1, 0.1)];
let decision = decide(&policy(), Some(1), &candidates).expect("valid");
assert_eq!(decision, HandoverDecision::Drop);
@ -192,11 +205,13 @@ fn the_dwell_time_refuses_an_opportunistic_handover() {
let candidates = [candidate(1, 0.5), candidate(2, 0.9)];
// The last handover was one second ago, which is inside the five second
// dwell time, so the better candidate is not taken.
// 上一次切换在一秒前,仍在五秒驻留时间内,故不取更好的候选。
let decision =
decide_with_history(&policy(), Some(1), Some(9.0), &candidates, 10.0).expect("valid");
assert_eq!(decision, HandoverDecision::Stay);
// After the dwell time the handover proceeds.
// 驻留时间之后切换进行。
let decision =
decide_with_history(&policy(), Some(1), Some(1.0), &candidates, 10.0).expect("valid");
assert!(decision.is_change());
@ -207,6 +222,7 @@ fn the_dwell_time_does_not_block_a_forced_handover() {
// The current server has fallen below the minimum, so the handover must
// happen even though the dwell time has not elapsed: refusing it would
// drop the link.
// 当前服务星已跌至最低值以下,故即便驻留时间未过也必须切换:拒绝它会掉链。
let candidates = [candidate(1, 0.1), candidate(2, 0.9)];
let decision =
decide_with_history(&policy(), Some(1), Some(9.9), &candidates, 10.0).expect("valid");
@ -296,6 +312,7 @@ fn a_tracker_acquires_and_then_hands_over() {
let mut tracker = HandoverTracker::new(policy());
// First step: acquire the best satellite.
// 第一步:获取最优卫星。
let decision = tracker
.step(0.0, &[candidate(1, 0.5), candidate(2, 0.8)])
.expect("valid");
@ -304,10 +321,12 @@ fn a_tracker_acquires_and_then_hands_over() {
assert_eq!(tracker.handover_count(), 0);
// The current server stays while it remains the best.
// 当前服务星在仍为最优期间保持不变。
let decision = tracker.step(1.0, &[candidate(2, 0.8)]).expect("valid");
assert_eq!(decision, HandoverDecision::Stay);
// A much better candidate after the dwell time causes a handover.
// 驻留时间之后好得多的候选引起一次切换。
let decision = tracker
.step(10.0, &[candidate(2, 0.3), candidate(3, 0.9)])
.expect("valid");
@ -321,6 +340,7 @@ fn a_tracker_acquires_and_then_hands_over() {
fn a_tracker_records_the_handover_history() {
let mut tracker = HandoverTracker::serving(policy(), 1);
// Force two handovers by dropping the current server below the minimum.
// 通过把当前服务星降到最低值以下强制两次切换。
tracker
.step(0.0, &[candidate(1, 0.1), candidate(2, 0.9)])
.expect("valid");
@ -335,6 +355,7 @@ fn a_tracker_records_the_handover_history() {
assert_eq!(history[1].from, 2);
assert_eq!(history[1].to, 3);
// The quality gain is the difference across each handover.
// 质量增益是每次切换处的差值。
assert!((history[0].quality_gain() - 0.8).abs() < 1e-12);
assert!((tracker.total_quality_gain() - 1.6).abs() < 1e-12);
}
@ -351,6 +372,7 @@ fn a_tracker_reports_the_handover_rate() {
// Two handovers a hundred seconds apart give a mean interval of a
// hundred seconds.
// 相隔一百秒的两次切换给出百秒的平均间隔。
assert_eq!(tracker.mean_interval_seconds(), Some(100.0));
assert_eq!(tracker.handover_rate_per_second(), Some(0.01));
}
@ -385,6 +407,7 @@ fn a_handover_record_reports_its_gain() {
let record = HandoverRecord::new(1, 2, 5.0, 0.3, 0.8);
assert!((record.quality_gain() - 0.5).abs() < 1e-12);
// A forced handover can show a loss.
// 强制切换可能显示为损失。
let forced = HandoverRecord::new(1, 2, 5.0, 0.9, 0.3);
assert!(forced.quality_gain() < 0.0);
}

View File

@ -3,14 +3,23 @@
//! The parallel form must produce exactly the matrix the sequential form
//! does, in the same row major order. Every test compares the two rather than
//! against a constant, so a content or ordering difference is caught.
//!
//! 批量可见性矩阵的测试。
//!
//! 并行形式必须产生与顺序形式完全相同的矩阵,且行主序相同。每个测试都比较二者
//! 而非与常量比较,故内容或次序差异会被捕获。
use apsis_isl::{visibility_matrix, visibility_matrix_parallel};
use apsis_math::Vec3;
/// The body radius used throughout, in kilometers.
///
/// 全程所用的天体半径,单位为千米。
const RADIUS: f64 = 6_378.137;
/// Returns a set of positions spread around a shell.
///
/// 返回分布在一个壳层上的一组位置。
fn shell(count: usize) -> Vec<Vec3> {
(0..count)
.map(|index| {
@ -33,12 +42,14 @@ fn a_single_position_gives_a_one_by_one_matrix() {
visibility_matrix(&positions, RADIUS)
);
// A satellite is visible to itself over a body radius below its own.
// 卫星对自身可见,只要天体半径低于它自身的半径。
assert!(visibility_matrix(&positions, RADIUS)[0]);
}
#[test]
fn a_shell_matches_the_sequential_matrix() {
// Long enough that the parallel path takes the multi thread branch.
// 长到足以使并行路径走多线程分支。
for count in [2_usize, 16, 64, 200] {
let positions = shell(count);
assert_eq!(
@ -60,6 +71,9 @@ fn the_matrix_is_row_major_and_symmetric() {
// row satellite to the column one. For a spherical body the geometry is
// symmetric, so the matrix is symmetric, which is what a wrong ordering
// of a row major layout would break.
// 行主序:位于 row * len + column 的元素是从第 row 颗卫星到第 column 颗的
// 可见性。对球形天体几何是对称的,故矩阵对称,而行主序布局的次序错误会破坏这
// 一点。
let positions = shell(12);
let matrix = visibility_matrix_parallel(&positions, RADIUS);
let len = positions.len();
@ -79,6 +93,7 @@ fn the_matrix_is_row_major_and_symmetric() {
fn the_matrix_matches_the_scalar_test() {
// Each entry must agree with calling the geometry directly, so a wrong
// pairing of row and column is caught even when the matrix is symmetric.
// 每个元素必须与直接调用几何一致,故即便矩阵对称,行列配错也会被捕获。
let positions = shell(10);
let matrix = visibility_matrix_parallel(&positions, RADIUS);
let len = positions.len();
@ -95,6 +110,7 @@ fn the_matrix_matches_the_scalar_test() {
fn an_unusable_body_radius_yields_all_false() {
// A non positive radius makes every test undefined, which the sequential
// form reports as false; the parallel form must do the same.
// 非正半径使每个判定都未定义,顺序形式报告为假;并行形式必须同样处理。
let positions = shell(8);
for radius in [0.0, -1.0] {
let matrix = visibility_matrix_parallel(&positions, radius);
@ -106,6 +122,7 @@ fn an_unusable_body_radius_yields_all_false() {
fn a_position_that_is_not_finite_yields_false_in_its_row() {
// A satellite with a non finite position cannot see anything. The rest
// of the matrix is unaffected.
// 位置非有限的卫星看不到任何东西。矩阵其余部分不受影响。
let mut positions = shell(6);
positions[2] = Vec3::new(f64::NAN, 0.0, 0.0);
let matrix = visibility_matrix_parallel(&positions, RADIUS);

View File

@ -3,6 +3,11 @@
//! For two satellites at the same radius separated by a central angle
//! `theta`, the closest approach of the chord to the centre is
//! `Rs cos(theta / 2)`. That closed form is used as the reference here.
//!
//! 星间链路几何的测试。
//!
//! 对同一半径、相隔中心角 `theta` 的两颗卫星,弦到中心的最接近距离为
//! `Rs cos(theta / 2)`。此处以该闭式作为参考。
use apsis_isl::{
LinkState, closest_approach_to_centre, link_geometry, mutual_visibility, sun_blocks_link,
@ -11,13 +16,19 @@ use apsis_math::Vec3;
use core::f64::consts::{FRAC_PI_2, PI};
/// The Earth equatorial radius, in kilometers.
///
/// 地球赤道半径,单位为千米。
const BODY_RADIUS: f64 = 6_378.137;
/// A 550 km circular orbit radius, in kilometers.
///
/// 550 km 圆轨道的半径,单位为千米。
const ORBIT_RADIUS: f64 = BODY_RADIUS + 550.0;
/// Returns a satellite on the equator at the given central angle from the
/// first axis.
///
/// 返回赤道上、距第一轴给定中心角的一颗卫星。
fn satellite_at(central_angle: f64) -> Vec3 {
Vec3::new(
ORBIT_RADIUS * central_angle.cos(),
@ -30,6 +41,7 @@ fn satellite_at(central_angle: f64) -> Vec3 {
fn the_closest_approach_matches_the_chord_geometry() {
// Two satellites at the same radius separated by a central angle have a
// closest approach of Rs cos(theta / 2).
// 同一半径、相隔中心角的两颗卫星其最接近距离为 Rs cos(theta / 2)。
for degrees in [0.0_f64, 10.0, 30.0, 60.0, 120.0, 180.0] {
let central = degrees.to_radians();
let first = satellite_at(0.0);
@ -54,6 +66,7 @@ fn the_closest_approach_is_the_distance_for_coincident_points() {
fn the_closest_approach_is_the_nearer_end_when_the_perpendicular_falls_outside() {
// Two points on the same side of the origin, so the foot of the
// perpendicular lies outside the segment.
// 两点在原点同一侧,故垂足落在该线段之外。
let first = Vec3::new(10.0, 0.0, 0.0);
let second = Vec3::new(20.0, 0.0, 0.0);
let closest = closest_approach_to_centre(first, second).expect("valid");
@ -90,18 +103,22 @@ fn satellites_on_opposite_sides_do_not() {
fn the_visibility_threshold_is_where_the_chord_grazes_the_surface() {
// The chord has a closest approach equal to the body radius when
// cos(theta / 2) = Re / Rs.
// 当 cos(theta / 2) = Re / Rs 时弦的最接近距离等于天体半径。
let threshold = 2.0 * (BODY_RADIUS / ORBIT_RADIUS).acos();
let first = satellite_at(0.0);
// Just inside the threshold the link is clear.
// 恰在阈值之内链路通畅。
let inside = satellite_at(threshold - 1e-6);
assert_eq!(mutual_visibility(first, inside, BODY_RADIUS), Some(true));
// Just outside it is blocked.
// 恰在其外被遮挡。
let outside = satellite_at(threshold + 1e-6);
assert_eq!(mutual_visibility(first, outside, BODY_RADIUS), Some(false));
// Exactly at the threshold the grazing link counts as clear.
// 恰在阈值处擦过的链路算作通畅。
let grazing = satellite_at(threshold);
assert_eq!(mutual_visibility(first, grazing, BODY_RADIUS), Some(true));
}
@ -112,6 +129,9 @@ fn a_higher_orbit_allows_a_wider_link() {
// angle between them is less than 2 acos(Re / Rs). For a 550 km orbit
// that limit is 45.97 degrees, and for a 20 200 km orbit it is 152.23
// degrees. A separation of 90 degrees therefore falls between the two.
// 同一半径的两颗卫星在它们之间的中心角小于 2 acos(Re / Rs) 时互相可见。对
// 550 km 轨道该极限为 45.97 度,对 20 200 km 轨道为 152.23 度。故 90 度的间隔
// 落在两者之间。
let central = FRAC_PI_2;
let low_radius = BODY_RADIUS + 550.0;
let high_radius = BODY_RADIUS + 20_200.0;
@ -142,14 +162,17 @@ fn the_link_geometry_reports_range_and_directions() {
let geometry = link_geometry(first, second, BODY_RADIUS).expect("valid");
// The range is the distance between the positions.
// 距离是两位置之间的距离。
assert!((geometry.range_km - first.distance(second)).abs() < 1e-12);
// The two directions are opposite unit vectors.
// 两个方向是相反的单位矢量。
assert!((geometry.direction_first_to_second.norm() - 1.0).abs() < 1e-12);
assert!((geometry.direction_second_to_first.norm() - 1.0).abs() < 1e-12);
assert!(
(geometry.direction_first_to_second + geometry.direction_second_to_first).norm() < 1e-12
);
// The direction points from the first to the second.
// 方向从第一颗指向第二颗。
let expected = (second - first).normalize().expect("non-zero separation");
assert!((geometry.direction_first_to_second - expected).norm() < 1e-12);
}
@ -199,6 +222,7 @@ fn invalid_link_arguments_are_rejected() {
fn a_link_along_a_radius_has_the_altitude_as_its_closest_approach() {
// Two satellites on the same radial line, one above the other. The
// lower one is the closest point of the segment.
// 同一径向线上的两颗卫星,一颗在另一颗之上。较低者是该线段的最接近点。
let low = Vec3::new(7_000.0, 0.0, 0.0);
let high = Vec3::new(8_000.0, 0.0, 0.0);
let closest = closest_approach_to_centre(low, high).expect("valid");
@ -214,10 +238,12 @@ fn the_sun_blocks_a_link_when_it_lies_along_it() {
// The Sun in the same direction as the link, seen from the first
// terminal.
// 从第一终端看,太阳与链路同方向。
let along = geometry.direction_first_to_second;
assert_eq!(sun_blocks_link(&geometry, along, field_of_view), Some(true));
// The Sun behind the link, seen from the second terminal.
// 从第二终端看,太阳在链路之后。
let behind = geometry.direction_second_to_first;
assert_eq!(
sun_blocks_link(&geometry, behind, field_of_view),
@ -225,6 +251,7 @@ fn the_sun_blocks_a_link_when_it_lies_along_it() {
);
// The Sun at right angles to the link.
// 太阳与链路成直角。
let transverse = Vec3::new(0.0, 0.0, 1.0);
assert_eq!(
sun_blocks_link(&geometry, transverse, field_of_view),
@ -242,9 +269,11 @@ fn a_zero_field_of_view_blocks_only_an_exact_alignment() {
.expect("valid");
// Exactly along the link, the angle is zero, which is within a zero
// field of view.
// 恰沿链路时夹角为零,落在零视场内。
let along = geometry.direction_first_to_second;
assert_eq!(sun_blocks_link(&geometry, along, 0.0), Some(true));
// A tiny offset is outside a zero field of view.
// 极小偏移就在零视场之外。
let offset = (along + Vec3::new(0.0, 0.0, 0.01))
.normalize()
.expect("valid");
@ -290,6 +319,9 @@ fn a_link_between_neighbouring_planes_is_clear() {
// which at the same radius would be blocked. Satellites that are near
// each other in time are much closer, which is what makes cross-plane
// links viable only between nearby satellites.
// 550 km 的 Walker 模式中相邻两面的两颗卫星,在赤道处相隔约 120 度赤经,同一
// 半径下这会被遮挡。在时间上彼此接近的卫星要近得多,这正是跨面链路只在邻近卫星
// 之间可行的原因。
let first = satellite_at(0.0);
let neighbour = satellite_at(5.0_f64.to_radians());
assert_eq!(mutual_visibility(first, neighbour, BODY_RADIUS), Some(true));

View File

@ -3,6 +3,11 @@
//! 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,
@ -10,6 +15,8 @@ use apsis_mesh::{
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 {
@ -21,6 +28,8 @@ fn ring(node_count: usize) -> 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) {
@ -32,6 +41,8 @@ fn line(node_count: usize) -> Topology {
}
/// A complete bidirectional graph.
///
/// 完全双向图。
fn complete(node_count: usize) -> Topology {
let mut topology = Topology::new(node_count);
for first in 0..node_count {
@ -47,6 +58,7 @@ fn complete(node_count: usize) -> Topology {
#[test]
fn reachability_follows_the_edge_direction() {
// A one way line: 0 reaches 1 and 2, but 2 reaches only itself.
// 单向线:0 可达 1 与 2,但 2 只可达自身。
let mut topology = Topology::new(3);
topology.add_edge(0, 1, 1.0).expect("valid");
topology.add_edge(1, 2, 1.0).expect("valid");
@ -73,6 +85,8 @@ 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));
@ -82,6 +96,7 @@ fn a_ring_has_two_independent_paths() {
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));
}
@ -92,6 +107,8 @@ fn a_complete_graph_has_the_full_degree() {
// 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));
}
@ -122,15 +139,18 @@ fn connectivity_rejects_an_out_of_range_node() {
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");
@ -147,6 +167,7 @@ fn the_strongly_connected_components_of_a_bidirectional_ring_is_one() {
#[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");
@ -161,6 +182,7 @@ fn a_one_way_line_has_one_component_per_node() {
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);
}
@ -219,11 +241,14 @@ fn the_critical_links_of_a_line_are_every_link() {
// 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);
@ -258,6 +283,7 @@ fn the_summary_reports_the_mesh_shape() {
#[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
@ -274,6 +300,7 @@ fn the_summary_of_a_split_mesh_reports_the_pieces() {
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());
}
@ -291,6 +318,8 @@ fn a_critical_link_type_is_constructible_directly() {
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);

View File

@ -1,4 +1,6 @@
//! Tests for shortest path routing.
//!
//! 最短路径路由的测试。
use apsis_routing::{
PathTree, Route, RouteStep, RoutingErrorKind, route_over_series, shortest_path,
@ -7,6 +9,8 @@ use apsis_routing::{
use apsis_topology::{Topology, TopologySample, TopologySeries};
/// A topology with the given directed edges and unit costs.
///
/// 具有给定有向边与单位代价的拓扑。
fn topology_with(node_count: usize, edges: &[(usize, usize)]) -> Topology {
let mut topology = Topology::new(node_count);
for &(from, to) in edges {
@ -23,6 +27,7 @@ fn a_direct_link_is_the_shortest_route() {
topology.add_edge(2, 1, 1.0).expect("valid");
// The direct link costs five, the two hop route costs two.
// 直接链路代价为五,两跳路由代价为二。
let route = shortest_path(&topology, 0, 1).expect("a route exists");
assert_eq!(route.nodes, vec![0, 2, 1]);
assert!((route.cost - 2.0).abs() < 1e-12);
@ -35,6 +40,7 @@ fn a_direct_link_is_the_shortest_route() {
fn a_longer_route_in_hops_can_be_cheaper_in_cost() {
let mut topology = Topology::new(4);
// A cheap path of three hops against an expensive direct link.
// 三条廉价跳的路径对一条昂贵的直接链路。
topology.add_edge(0, 1, 1.0).expect("valid");
topology.add_edge(1, 2, 1.0).expect("valid");
topology.add_edge(2, 3, 1.0).expect("valid");
@ -71,6 +77,7 @@ fn a_route_records_its_steps() {
#[test]
fn a_directed_topology_may_have_no_route() {
// The link only goes one way, so the reverse route does not exist.
// 该链路只有一个方向,故反向路由不存在。
let mut topology = Topology::new(2);
topology.add_edge(0, 1, 1.0).expect("valid");
assert!(shortest_path(&topology, 0, 1).is_ok());
@ -117,6 +124,7 @@ fn the_path_tree_covers_every_reachable_node() {
topology.add_edge(0, 1, 1.0).expect("valid");
topology.add_edge(1, 2, 1.0).expect("valid");
// Node 3 is isolated.
// 节点 3 是孤立的。
let tree = shortest_paths_from(&topology, 0).expect("valid");
assert_eq!(tree.source, 0);
assert_eq!(tree.distance(0), Some(0.0));
@ -141,6 +149,7 @@ fn the_path_tree_reconstructs_each_route() {
assert!((route.cost - 3.0).abs() < 1e-12);
// The route to the source is the source alone.
// 到源的路由只有源本身。
let self_route = tree.route_to(0, &topology).expect("a route exists");
assert_eq!(self_route.nodes, vec![0]);
assert_eq!(self_route.hop_count(), 0);
@ -161,6 +170,8 @@ fn a_cheaper_route_replaces_a_more_expensive_one() {
// The heap must settle the nearer node first even when it is discovered
// second, which is the case that a naive breadth first search gets
// wrong.
// 即便较近的节点是第二个被发现的,堆也必须先结算它,这正是朴素广度优先搜索会
// 弄错的情形。
let mut topology = Topology::new(4);
topology.add_edge(0, 1, 10.0).expect("valid");
topology.add_edge(0, 2, 1.0).expect("valid");
@ -219,6 +230,7 @@ fn the_route_type_is_constructible_directly() {
assert!((route.cost - 2.0).abs() < 1e-12);
assert_eq!(route.hop_count(), 1);
// An empty route has no endpoints.
// 空路由没有端点。
let empty = Route::new(Vec::new(), Vec::new());
assert_eq!(empty.source(), None);
assert_eq!(empty.destination(), None);
@ -229,6 +241,8 @@ fn a_route_over_a_series_uses_the_links_present_throughout() {
// The link 1 to 2 exists at both ends of the interval but is absent in
// the middle, so no route through it survives the interval. The
// two hop route via node 3 is present throughout.
// 链路 1 到 2 在区间两端存在但中间缺席,故没有经它的路由能存活整个区间。经
// 节点 3 的两跳路由则全程存在。
let series = TopologySeries::new(vec![
TopologySample::new(0.0, topology_with(4, &[(0, 1), (1, 2), (0, 3), (3, 2)])),
TopologySample::new(10.0, topology_with(4, &[(0, 1), (0, 3), (3, 2)])),
@ -240,6 +254,7 @@ fn a_route_over_a_series_uses_the_links_present_throughout() {
assert_eq!(leg.route.nodes, vec![0, 3, 2]);
assert!((leg.route.cost - 2.0).abs() < 1e-12);
// The leg is valid until the next change after the interval.
// 该路段有效直到区间之后的下一处变化。
assert_eq!(leg.start_seconds, 0.0);
assert!(leg.end_seconds.is_infinite());
}
@ -254,6 +269,7 @@ fn a_route_over_a_series_reports_its_validity() {
.expect("the times increase");
// A route planned over the first two samples is valid until the third.
// 在前两个样本上规划的路由有效到第三个。
let leg = route_over_series(&series, 0, 1, 0.0, 10.0).expect("a route exists");
assert_eq!(leg.end_seconds, 20.0);
assert!((leg.duration_seconds() - 20.0).abs() < 1e-12);
@ -263,6 +279,7 @@ fn a_route_over_a_series_reports_its_validity() {
fn a_route_over_a_series_fails_when_no_link_survives() {
// The only link is present at the start and absent at the end, so it
// does not survive the interval.
// 唯一的链路在起点存在、终点缺席,故它不能存活整个区间。
let series = TopologySeries::new(vec![
TopologySample::new(0.0, topology_with(2, &[(0, 1)])),
TopologySample::new(10.0, topology_with(2, &[])),
@ -307,6 +324,7 @@ fn a_route_over_a_series_validates_its_arguments() {
fn the_path_tree_type_is_public() {
// The type must be nameable by a caller that stores a routing table,
// and what it hands back must be the tree for the source asked for.
// 该类型必须能被存储路由表的调用方指名,且它所交回的必须是所请求源的那棵树。
fn takes_tree(_tree: &PathTree) {}
let topology = topology_with(2, &[(0, 1)]);
let tree = shortest_paths_from(&topology, 0).expect("valid");

View File

@ -1,8 +1,12 @@
//! Tests for the topology graph.
//!
//! 拓扑图的测试。
use apsis_topology::{Edge, Topology, TopologyErrorKind};
/// A four node ring, with links both ways.
///
/// 一个四节点环,链路双向。
fn ring() -> Topology {
let mut topology = Topology::new(4);
for node in 0..4 {
@ -60,6 +64,7 @@ fn an_out_of_range_node_is_rejected() {
TopologyErrorKind::NoSuchNode
);
// The topology is unchanged.
// 拓扑不变。
assert!(topology.is_empty());
}
@ -84,6 +89,7 @@ fn a_duplicate_edge_is_rejected() {
.expect_err("the edge already exists");
assert_eq!(error.kind(), TopologyErrorKind::DuplicateEdge);
// The original cost is retained.
// 原代价被保留。
assert_eq!(topology.cost(0, 1), Some(1.0));
assert_eq!(topology.edge_count(), 1);
}
@ -92,6 +98,7 @@ fn a_duplicate_edge_is_rejected() {
fn a_self_loop_is_allowed_and_counted() {
// A self loop is unusual in a satellite network but is not an error:
// a node may have a local interface, and the graph need not forbid it.
// 自环在卫星网中不常见但并非错误:节点可能有本地接口,而图无需禁止它。
let mut topology = Topology::new(1);
topology.add_edge(0, 0, 1.0).expect("valid");
assert!(topology.has_edge(0, 0));
@ -119,11 +126,14 @@ fn the_adjacency_lists_every_node() {
assert_eq!(adjacency.len(), 4);
// Each node of a ring has two neighbours in each direction; the
// bidirectional construction gives two outgoing edges.
// 环的每个节点在每个方向上有两个邻居;双向构建给出两条出发边。
for neighbours in &adjacency {
assert_eq!(neighbours.len(), 2);
}
// The order follows the insertion order: node 0 gains its edge to 1
// when node 0 is processed, and its edge to 3 when node 3 is.
// 其次序遵循插入次序:节点 0 在节点 0 被处理时获得其到 1 的边,在节点 3 被处理
// 时获得其到 3 的边。
assert_eq!(adjacency[0], vec![1, 3]);
assert_eq!(adjacency[1], vec![0, 2]);
}
@ -140,6 +150,7 @@ fn removing_a_link_splits_the_ring() {
let mut topology = ring();
// Removing one direction of each of two opposite links breaks the ring
// into two paths.
// 移除两条相对链路各自的一个方向,把环断成两条路径。
topology.remove_edge(0, 1).expect("the edge exists");
topology.remove_edge(1, 0).expect("the edge exists");
topology.remove_edge(2, 3).expect("the edge exists");
@ -152,12 +163,14 @@ fn removing_a_link_splits_the_ring() {
fn a_single_direction_is_enough_to_connect_components() {
// The component count treats edges as undirected, so a link that only
// carries traffic one way still joins two pieces of the graph.
// 分量计数把边视为无向,故只单向承载业务的链路仍连接图的两部分。
let mut topology = Topology::new(2);
topology.add_edge(0, 1, 1.0).expect("valid");
assert_eq!(topology.connected_components(), 1);
// But the reverse direction does not exist, so the node cannot be
// reached from the other side.
// 但反向不存在,故该节点无法从另一侧到达。
assert!(!topology.has_edge(1, 0));
}
@ -167,6 +180,7 @@ fn isolated_nodes_are_each_a_component() {
assert_eq!(topology.connected_components(), 4);
// A graph with no edges is still connected when it has at most one
// node, and is not when it has more.
// 无边图在至多一个节点时仍是连通的,节点更多时则不连通。
assert!(!topology.is_connected());
assert!(Topology::new(1).is_connected());
assert!(Topology::new(0).is_connected());
@ -195,12 +209,14 @@ fn a_failed_bidirectional_add_leaves_the_topology_unchanged() {
let mut topology = Topology::new(2);
// The first direction succeeds, but the second is a duplicate, so the
// whole call must fail and roll back.
// 第一个方向成功,但第二个是重复,故整个调用必须失败并回滚。
topology.add_edge(1, 0, 5.0).expect("valid");
let error = topology
.add_bidirectional_edge(0, 1, 2.0)
.expect_err("the reverse edge exists");
assert_eq!(error.kind(), TopologyErrorKind::DuplicateEdge);
// The forward direction that was added must have been removed.
// 所加入的正向必须已被移除。
assert!(!topology.has_edge(0, 1));
assert!(topology.has_edge(1, 0));
assert_eq!(topology.edge_count(), 1);

View File

@ -1,8 +1,12 @@
//! Tests for the topology time series.
//!
//! 拓扑时间序列的测试。
use apsis_topology::{Topology, TopologyErrorKind, TopologySample, TopologySeries};
/// A topology with the given directed edges, all of unit cost.
///
/// 具有给定有向边、全部单位代价的拓扑。
fn topology_with(node_count: usize, edges: &[(usize, usize)]) -> Topology {
let mut topology = Topology::new(node_count);
for &(from, to) in edges {
@ -12,6 +16,8 @@ fn topology_with(node_count: usize, edges: &[(usize, usize)]) -> Topology {
}
/// A series of three topologies at 0, 10, and 25 seconds.
///
/// 0、10 与 25 秒处三个拓扑构成的序列。
fn series() -> TopologySeries {
TopologySeries::new(vec![
TopologySample::new(0.0, topology_with(3, &[(0, 1)])),
@ -35,19 +41,23 @@ fn a_series_holds_its_samples() {
fn a_time_selects_the_sample_in_force() {
let series = series();
// At the exact times, the entry itself applies.
// 在精确时刻,该时刻的项本身适用。
assert_eq!(series.index_at(0.0), Some(0));
assert_eq!(series.index_at(10.0), Some(1));
assert_eq!(series.index_at(25.0), Some(2));
// Between entries, the earlier one applies.
// 在各项之间,较早者适用。
assert_eq!(series.index_at(5.0), Some(0));
assert_eq!(series.index_at(9.999), Some(0));
assert_eq!(series.index_at(10.001), Some(1));
assert_eq!(series.index_at(24.999), Some(1));
// A time after the last entry keeps the last topology.
// 最后一项之后的时间保持最后拓扑。
assert_eq!(series.index_at(1_000.0), Some(2));
// A time before the first entry uses the first topology.
// 首项之前的时间使用第一个拓扑。
assert_eq!(series.index_at(-50.0), Some(0));
}
@ -55,16 +65,19 @@ fn a_time_selects_the_sample_in_force() {
fn the_topology_in_force_reflects_the_changes() {
let series = series();
// At one second only the first link exists.
// 一秒时只有第一条链路存在。
let early = series.topology_at(1.0).expect("a topology exists");
assert!(early.has_edge(0, 1));
assert!(!early.has_edge(1, 2));
// At fifteen seconds the second link has been added.
// 十五秒时第二条链路已被加入。
let middle = series.topology_at(15.0).expect("a topology exists");
assert!(middle.has_edge(0, 1));
assert!(middle.has_edge(1, 2));
// At thirty seconds the topology has changed again.
// 三十秒时拓扑又已改变。
let late = series.topology_at(30.0).expect("a topology exists");
assert!(!late.has_edge(0, 1));
assert!(late.has_edge(0, 2));
@ -76,6 +89,7 @@ fn the_interval_of_a_sample_is_bounded_by_the_next() {
assert_eq!(series.interval_of(0), Some((0.0, 10.0)));
assert_eq!(series.interval_of(1), Some((10.0, 25.0)));
// The last interval is unbounded.
// 最后一个区间无界。
let (start, end) = series.interval_of(2).expect("the index exists");
assert_eq!(start, 25.0);
assert!(end.is_infinite());
@ -95,6 +109,8 @@ fn the_edge_changes_count_appearances_and_disappearances() {
// From the first to the second topology, the edge 1 to 2 appears, one
// change. From the second to the third, the edges 0 to 1 and 1 to 2
// disappear and 0 to 2 appears, three changes.
// 从第一个拓扑到第二个,边 1 到 2 出现,一次变化。从第二个到第三个,边 0 到 1
// 与 1 到 2 消失、0 到 2 出现,三次变化。
assert_eq!(series.edge_changes(), 4);
}
@ -134,6 +150,7 @@ fn unordered_times_are_rejected() {
// Equal times are also rejected, because the topology at that instant
// would be ambiguous.
// 相等的时间也被拒绝,因为该瞬时的拓扑会含糊。
let error = TopologySeries::new(vec![
TopologySample::new(10.0, Topology::new(1)),
TopologySample::new(10.0, Topology::new(1)),
@ -183,6 +200,7 @@ fn a_non_finite_query_time_is_rejected() {
fn a_series_with_negative_times_is_accepted() {
// The origin of the time axis is the caller's choice, so negative times
// are as valid as positive ones.
// 时间轴的原点由调用方选择,故负时间与正时间同样有效。
let series = TopologySeries::new(vec![
TopologySample::new(-100.0, topology_with(2, &[(0, 1)])),
TopologySample::new(-50.0, topology_with(2, &[])),
@ -215,6 +233,7 @@ fn the_samples_are_accessible() {
#[test]
fn a_change_at_the_boundary_is_immediate() {
// The topology changes exactly at the recorded time, not after it.
// 拓扑恰在所记录的时间处改变,而非在其后。
let series = series();
let before = series.topology_at(10.0 - 1e-9).expect("a topology exists");
let after = series.topology_at(10.0).expect("a topology exists");