//! Tests for the satellite geometry of a ground coverage calculation. //! //! 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. const LOW_ORBIT: f64 = BODY_RADIUS + 550.0; /// A medium Earth orbit at 20 200 km altitude, near the navigation /// constellations. const MEDIUM_ORBIT: f64 = BODY_RADIUS + 20_200.0; /// A geostationary radius. const GEOSTATIONARY: f64 = 42_164.0; const TOLERANCE: f64 = 1e-9; #[test] 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") .abs() < TOLERANCE ); assert!( earth_central_angle(LOW_ORBIT, BODY_RADIUS, FRAC_PI_2) .expect("valid") .abs() < TOLERANCE ); // The slant range is the altitude. assert!( (slant_range(LOW_ORBIT, BODY_RADIUS, FRAC_PI_2).expect("valid") - 550.0).abs() < TOLERANCE ); } #[test] fn the_three_angles_are_complementary() { // elevation + nadir + central = 90 degrees, for every geometry. 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(); let nadir = nadir_angle(satellite_radius, BODY_RADIUS, elevation).expect("valid"); let central = earth_central_angle(satellite_radius, BODY_RADIUS, elevation).expect("valid"); let sum = elevation + nadir + central; assert!( (sum - FRAC_PI_2).abs() < 1e-9, "radius {satellite_radius}, elevation {elevation_degrees}: sum {sum}" ); } } } #[test] 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. let elevation = 10.0_f64.to_radians(); assert!( (nadir_angle(LOW_ORBIT, BODY_RADIUS, elevation) .expect("valid") .to_degrees() - 65.043_364_875_042_71) .abs() < 1e-9 ); assert!( (earth_central_angle(LOW_ORBIT, BODY_RADIUS, elevation) .expect("valid") .to_degrees() - 14.956_635_124_957_29) .abs() < 1e-9 ); assert!( (slant_range(LOW_ORBIT, BODY_RADIUS, elevation).expect("valid") - 1_815.652_169_368_375_8) .abs() < 1e-9 ); } #[test] fn a_higher_satellite_sees_further() { let elevation = 10.0_f64.to_radians(); let low = earth_central_angle(LOW_ORBIT, BODY_RADIUS, elevation).expect("valid"); let medium = earth_central_angle(MEDIUM_ORBIT, BODY_RADIUS, elevation).expect("valid"); let high = earth_central_angle(GEOSTATIONARY, BODY_RADIUS, elevation).expect("valid"); assert!(low < medium, "{low} against {medium}"); assert!(medium < high, "{medium} against {high}"); } #[test] 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()) .expect("valid"); assert!( central > previous, "elevation {elevation_degrees}: {central} against {previous}" ); previous = central; } } #[test] 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. 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); } #[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}"); assert!(overhead < 600.0); assert!(low > 1_000.0); } #[test] fn the_elevation_inverts_the_central_angle() { for satellite_radius in [LOW_ORBIT, MEDIUM_ORBIT, GEOSTATIONARY] { for elevation_degrees in [0.0_f64, 10.0, 30.0, 70.0] { let elevation = elevation_degrees.to_radians(); let central = earth_central_angle(satellite_radius, BODY_RADIUS, elevation).expect("valid"); let recovered = elevation_from_central_angle(satellite_radius, BODY_RADIUS, central) .expect("valid"); assert!( (recovered - elevation).abs() < 1e-9, "radius {satellite_radius}, elevation {elevation_degrees}: {recovered}" ); } } } #[test] fn the_elevation_is_a_right_angle_when_the_central_angle_is_zero() { let elevation = elevation_from_central_angle(LOW_ORBIT, BODY_RADIUS, 0.0).expect("valid"); assert!((elevation - FRAC_PI_2).abs() < TOLERANCE); } #[test] 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), None ); assert_eq!(slant_range(BODY_RADIUS - 1.0, BODY_RADIUS, 0.0), None); } #[test] fn non_finite_arguments_are_rejected() { assert_eq!(nadir_angle(f64::NAN, BODY_RADIUS, 0.0), None); assert_eq!(nadir_angle(LOW_ORBIT, f64::NAN, 0.0), None); assert_eq!(nadir_angle(LOW_ORBIT, BODY_RADIUS, f64::NAN), None); assert_eq!( earth_central_angle(LOW_ORBIT, BODY_RADIUS, f64::INFINITY), None ); assert_eq!(slant_range(LOW_ORBIT, 0.0, 0.0), None); assert_eq!( elevation_from_central_angle(LOW_ORBIT, BODY_RADIUS, f64::NAN), None ); } #[test] 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(); assert!((nadir - expected).abs() < 1e-9); }