From 4ffe215860b510e32b7956d8ec8efb4644aa3903 Mon Sep 17 00:00:00 2001 From: ApsisRS contributors Date: Fri, 2 Oct 2026 01:59:43 +0800 Subject: [PATCH] docs: bilingual comments for the network tests --- crates/network/apsis-access/tests/batch.rs | 19 ++++++ crates/network/apsis-access/tests/finder.rs | 66 +++++++++++++++++++ .../apsis-constellation/tests/walker.rs | 11 ++++ .../network/apsis-coverage/tests/footprint.rs | 19 ++++++ .../network/apsis-coverage/tests/geometry.rs | 23 +++++++ .../apsis-coverage/tests/visibility.rs | 24 +++++++ .../network/apsis-handover/tests/decision.rs | 23 +++++++ crates/network/apsis-isl/tests/batch.rs | 17 +++++ crates/network/apsis-isl/tests/geometry.rs | 32 +++++++++ .../network/apsis-mesh/tests/connectivity.rs | 29 ++++++++ .../network/apsis-routing/tests/dijkstra.rs | 18 +++++ crates/network/apsis-topology/tests/graph.rs | 16 +++++ crates/network/apsis-topology/tests/series.rs | 19 ++++++ 13 files changed, 316 insertions(+) diff --git a/crates/network/apsis-access/tests/batch.rs b/crates/network/apsis-access/tests/batch.rs index 02e6d1d..015ce87 100644 --- a/crates/network/apsis-access/tests/batch.rs +++ b/crates/network/apsis-access/tests/batch.rs @@ -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 { (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 { // 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> = (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![ diff --git a/crates/network/apsis-access/tests/finder.rs b/crates/network/apsis-access/tests/finder.rs index 4e0eebe..bc91dcc 100644 --- a/crates/network/apsis-access/tests/finder.rs +++ b/crates/network/apsis-access/tests/finder.rs @@ -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 { let mut samples = Vec::new(); let mut seconds = start; @@ -78,16 +114,26 @@ fn reference_samples(step: f64, start: f64, end: f64) -> Vec { /// /// 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}", diff --git a/crates/network/apsis-constellation/tests/walker.rs b/crates/network/apsis-constellation/tests/walker.rs index 1bd6f52..d2b864a 100644 --- a/crates/network/apsis-constellation/tests/walker.rs +++ b/crates/network/apsis-constellation/tests/walker.rs @@ -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() {} assert_copy::(); } diff --git a/crates/network/apsis-coverage/tests/footprint.rs b/crates/network/apsis-coverage/tests/footprint.rs index d4e8cd9..cc70175 100644 --- a/crates/network/apsis-coverage/tests/footprint.rs +++ b/crates/network/apsis-coverage/tests/footprint.rs @@ -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(), diff --git a/crates/network/apsis-coverage/tests/geometry.rs b/crates/network/apsis-coverage/tests/geometry.rs index 9dad599..4c9db25 100644 --- a/crates/network/apsis-coverage/tests/geometry.rs +++ b/crates/network/apsis-coverage/tests/geometry.rs @@ -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(); diff --git a/crates/network/apsis-coverage/tests/visibility.rs b/crates/network/apsis-coverage/tests/visibility.rs index 095728c..5bc1160 100644 --- a/crates/network/apsis-coverage/tests/visibility.rs +++ b/crates/network/apsis-coverage/tests/visibility.rs @@ -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); } diff --git a/crates/network/apsis-handover/tests/decision.rs b/crates/network/apsis-handover/tests/decision.rs index aa84467..c579bde 100644 --- a/crates/network/apsis-handover/tests/decision.rs +++ b/crates/network/apsis-handover/tests/decision.rs @@ -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); } diff --git a/crates/network/apsis-isl/tests/batch.rs b/crates/network/apsis-isl/tests/batch.rs index 67f0ad5..373f3d2 100644 --- a/crates/network/apsis-isl/tests/batch.rs +++ b/crates/network/apsis-isl/tests/batch.rs @@ -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 { (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); diff --git a/crates/network/apsis-isl/tests/geometry.rs b/crates/network/apsis-isl/tests/geometry.rs index 00bcc5f..639ec1f 100644 --- a/crates/network/apsis-isl/tests/geometry.rs +++ b/crates/network/apsis-isl/tests/geometry.rs @@ -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)); diff --git a/crates/network/apsis-mesh/tests/connectivity.rs b/crates/network/apsis-mesh/tests/connectivity.rs index 9041056..e0cc4a8 100644 --- a/crates/network/apsis-mesh/tests/connectivity.rs +++ b/crates/network/apsis-mesh/tests/connectivity.rs @@ -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); diff --git a/crates/network/apsis-routing/tests/dijkstra.rs b/crates/network/apsis-routing/tests/dijkstra.rs index 7708715..ac9bda8 100644 --- a/crates/network/apsis-routing/tests/dijkstra.rs +++ b/crates/network/apsis-routing/tests/dijkstra.rs @@ -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"); diff --git a/crates/network/apsis-topology/tests/graph.rs b/crates/network/apsis-topology/tests/graph.rs index e379e00..f6b3251 100644 --- a/crates/network/apsis-topology/tests/graph.rs +++ b/crates/network/apsis-topology/tests/graph.rs @@ -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); diff --git a/crates/network/apsis-topology/tests/series.rs b/crates/network/apsis-topology/tests/series.rs index 2d61b90..ef0c92b 100644 --- a/crates/network/apsis-topology/tests/series.rs +++ b/crates/network/apsis-topology/tests/series.rs @@ -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");