apsis-rs/crates/network/apsis-constellation/tests/walker.rs

398 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Tests for the Walker constellation pattern.
//!
//! Walker 星座模式的测试。
use apsis_constellation::{Constellation, ConstellationErrorKind, Satellite, WalkerPattern};
use apsis_kepler::true_from_mean;
use core::f64::consts::PI;
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,
planes: 3,
phase_factor: 1,
inclination: 53.0_f64.to_radians(),
semi_major_axis: 6_878.137,
}
}
#[test]
fn the_derived_counts_are_correct() {
let pattern = broadband_pattern();
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);
}
#[test]
fn instantiation_produces_the_right_number_of_satellites() {
let constellation = Constellation::walker(broadband_pattern(), 60_000.0).expect("valid");
assert_eq!(constellation.len(), 24);
assert_eq!(constellation.plane_count(), 3);
assert_eq!(constellation.plane_populations(), vec![8, 8, 8]);
assert!(!constellation.is_empty());
}
#[test]
fn every_satellite_is_circular_and_at_the_same_radius() {
let constellation = Constellation::walker(broadband_pattern(), 60_000.0).expect("valid");
for satellite in constellation.satellites() {
assert_eq!(satellite.elements.eccentricity, 0.0);
assert!((satellite.elements.semi_major_axis - 6_878.137).abs() < TOLERANCE);
assert!((satellite.elements.inclination - 53.0_f64.to_radians()).abs() < TOLERANCE);
}
}
#[test]
fn the_planes_are_evenly_spaced_in_right_ascension() {
let constellation = Constellation::walker(broadband_pattern(), 60_000.0).expect("valid");
let expected = [0.0, 2.0 * PI / 3.0, 4.0 * PI / 3.0];
for (plane, expected_right_ascension) in expected.iter().enumerate() {
for satellite in constellation.plane(plane) {
assert!(
(satellite.elements.right_ascension - expected_right_ascension).abs() < 1e-9,
"plane {plane}: {} against {expected_right_ascension}",
satellite.elements.right_ascension
);
}
}
}
#[test]
fn satellites_within_a_plane_are_evenly_spaced_in_anomaly() {
let constellation = Constellation::walker(broadband_pattern(), 60_000.0).expect("valid");
for plane in 0..3 {
let satellites = constellation.plane(plane);
let mut anomalies: Vec<f64> = satellites
.iter()
.map(|satellite| satellite.elements.mean_anomaly())
.collect();
anomalies.sort_by(|a, b| a.partial_cmp(b).expect("finite"));
for pair in anomalies.windows(2) {
let spacing = pair[1] - pair[0];
assert!((spacing - 2.0 * PI / 8.0).abs() < 1e-9, "spacing {spacing}");
}
}
}
#[test]
fn the_phase_offset_appears_between_adjacent_planes() {
let pattern = broadband_pattern();
let constellation = Constellation::walker(pattern, 60_000.0).expect("valid");
let offset = pattern.phase_offset().expect("valid");
// 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)
.iter()
.min_by(|a, b| {
a.index_in_plane
.partial_cmp(&b.index_in_plane)
.expect("indices are integers")
})
.expect("the plane is not empty")
.elements
.mean_anomaly()
};
let first = first_of_plane(0);
let second = first_of_plane(1);
let third = first_of_plane(2);
assert!(
(second - first - offset).abs() < 1e-9,
"{second} against {first}"
);
assert!(
(third - second - offset).abs() < 1e-9,
"{third} against {second}"
);
}
#[test]
fn a_zero_phase_factor_gives_no_offset_between_planes() {
let pattern = WalkerPattern {
phase_factor: 0,
..broadband_pattern()
};
let constellation = Constellation::walker(pattern, 0.0).expect("valid");
assert_eq!(constellation.len(), 24);
let first_of_plane = |plane: usize| -> f64 {
constellation
.plane(plane)
.iter()
.find(|satellite| satellite.index_in_plane == 0)
.expect("the plane is not empty")
.elements
.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);
}
#[test]
fn the_true_anomaly_matches_the_mean_anomaly_for_a_circular_orbit() {
let constellation = Constellation::walker(broadband_pattern(), 0.0).expect("valid");
for satellite in constellation.satellites() {
let mean = satellite.elements.mean_anomaly();
let expected = true_from_mean(mean, 0.0);
assert!((satellite.elements.true_anomaly - expected).abs() < 1e-12);
}
}
#[test]
fn a_single_plane_pattern_is_accepted() {
let pattern = WalkerPattern {
total_satellites: 6,
planes: 1,
phase_factor: 0,
inclination: 0.0,
semi_major_axis: 7_000.0,
};
let constellation = Constellation::walker(pattern, 0.0).expect("valid");
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);
}
}
#[test]
fn a_pattern_with_one_satellite_per_plane_is_accepted() {
let pattern = WalkerPattern {
total_satellites: 4,
planes: 4,
phase_factor: 3,
inclination: 1.0,
semi_major_axis: 7_000.0,
};
let constellation = Constellation::walker(pattern, 0.0).expect("valid");
assert_eq!(constellation.len(), 4);
assert_eq!(constellation.plane_count(), 4);
assert_eq!(constellation.plane_populations(), vec![1, 1, 1, 1]);
}
#[test]
fn an_uneven_split_is_rejected() {
let pattern = WalkerPattern {
total_satellites: 25,
planes: 3,
..broadband_pattern()
};
assert_eq!(pattern.satellites_per_plane(), None);
assert!(!pattern.is_valid());
let error = pattern
.instantiate()
.expect_err("25 is not a multiple of 3");
assert_eq!(error.kind(), ConstellationErrorKind::UnevenPlanes);
}
#[test]
fn a_zero_satellite_count_is_rejected() {
let pattern = WalkerPattern {
total_satellites: 0,
..broadband_pattern()
};
let error = pattern.instantiate().expect_err("no satellites");
assert_eq!(error.kind(), ConstellationErrorKind::NoSatellites);
}
#[test]
fn a_zero_plane_count_is_rejected() {
let pattern = WalkerPattern {
planes: 0,
..broadband_pattern()
};
let error = pattern.instantiate().expect_err("no planes");
assert_eq!(error.kind(), ConstellationErrorKind::NoPlanes);
}
#[test]
fn an_out_of_range_phase_factor_is_rejected() {
let pattern = WalkerPattern {
phase_factor: 5,
..broadband_pattern()
};
assert!(!pattern.is_valid());
let error = pattern
.instantiate()
.expect_err("phase factor exceeds the plane count");
assert_eq!(error.kind(), ConstellationErrorKind::InvalidOrbit);
}
#[test]
fn an_invalid_inclination_is_rejected() {
let pattern = WalkerPattern {
inclination: PI + 0.1,
..broadband_pattern()
};
assert!(!pattern.is_valid());
let error = pattern
.instantiate()
.expect_err("inclination is out of range");
assert_eq!(error.kind(), ConstellationErrorKind::InvalidOrbit);
}
#[test]
fn a_non_positive_semi_major_axis_is_rejected() {
let pattern = WalkerPattern {
semi_major_axis: 0.0,
..broadband_pattern()
};
assert!(!pattern.is_valid());
assert_eq!(
pattern.instantiate().expect_err("zero radius").kind(),
ConstellationErrorKind::InvalidOrbit
);
}
#[test]
fn a_non_finite_reference_time_is_rejected() {
let error =
Constellation::walker(broadband_pattern(), f64::NAN).expect_err("the time is not finite");
assert_eq!(error.kind(), ConstellationErrorKind::InvalidOrbit);
}
#[test]
fn the_reference_time_is_retained() {
let constellation = Constellation::walker(broadband_pattern(), 60_123.5).expect("valid");
assert_eq!(constellation.reference_mjd(), 60_123.5);
}
#[test]
fn the_pattern_is_retained() {
let pattern = broadband_pattern();
let constellation = Constellation::walker(pattern, 0.0).expect("valid");
assert_eq!(constellation.pattern(), Some(pattern));
}
#[test]
fn the_satellite_indices_are_consistent() {
let constellation = Constellation::walker(broadband_pattern(), 0.0).expect("valid");
for (position, satellite) in constellation.satellites().iter().enumerate() {
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);
}
}
#[test]
fn the_altitude_is_measured_above_the_given_radius() {
let constellation = Constellation::walker(broadband_pattern(), 0.0).expect("valid");
let altitude = constellation
.altitude_above(6_378.137)
.expect("the constellation is not empty");
assert!((altitude - 500.0).abs() < 1e-9, "altitude {altitude}");
}
#[test]
fn the_notation_reports_the_walker_fields() {
let pattern = WalkerPattern {
total_satellites: 24,
planes: 3,
phase_factor: 1,
inclination: 53.0_f64.to_radians(),
semi_major_axis: 6_878.137,
};
let notation = pattern.notation();
assert!(notation.starts_with("53"), "notation {notation}");
assert!(notation.contains("24/3/1"), "notation {notation}");
}
#[test]
fn a_custom_constellation_groups_satellites_by_right_ascension() {
use apsis_kepler::KeplerianElements;
let elements = [
KeplerianElements {
semi_major_axis: 7_000.0,
eccentricity: 0.001,
inclination: 0.9,
right_ascension: 0.0,
argument_of_perigee: 0.0,
true_anomaly: 0.0,
},
KeplerianElements {
semi_major_axis: 7_000.0,
eccentricity: 0.001,
inclination: 0.9,
right_ascension: 0.0,
argument_of_perigee: 0.0,
true_anomaly: PI,
},
KeplerianElements {
semi_major_axis: 7_000.0,
eccentricity: 0.001,
inclination: 0.9,
right_ascension: 2.0,
argument_of_perigee: 0.0,
true_anomaly: 0.5,
},
];
let constellation = Constellation::from_satellites(&elements, 0.0).expect("valid");
assert_eq!(constellation.len(), 3);
assert_eq!(constellation.plane_count(), 2);
assert_eq!(constellation.plane_populations(), vec![2, 1]);
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);
assert_eq!(constellation.satellites()[0].index_in_plane, 0);
assert_eq!(constellation.satellites()[1].index_in_plane, 1);
}
#[test]
fn an_empty_custom_constellation_is_rejected() {
let error = Constellation::from_satellites(&[], 0.0).expect_err("no satellites");
assert_eq!(error.kind(), ConstellationErrorKind::NoSatellites);
}
#[test]
fn a_custom_constellation_rejects_invalid_elements() {
use apsis_kepler::KeplerianElements;
let elements = [KeplerianElements {
semi_major_axis: -1.0,
eccentricity: 0.0,
inclination: 0.0,
right_ascension: 0.0,
argument_of_perigee: 0.0,
true_anomaly: 0.0,
}];
let error = Constellation::from_satellites(&elements, 0.0).expect_err("negative radius");
assert_eq!(error.kind(), ConstellationErrorKind::InvalidOrbit);
}
#[test]
fn an_out_of_range_plane_query_is_empty() {
let constellation = Constellation::walker(broadband_pattern(), 0.0).expect("valid");
assert!(constellation.plane(99).is_empty());
}
#[test]
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>();
}