use super::*;
use crate::atmosphere::ionosphere::{
galileo_nequick_g_native, klobuchar_native, GalileoNequickCoeffs,
};
use crate::constants::SECONDS_PER_HOUR;
const AZ_HIGH: GalileoNequickCoeffs = GalileoNequickCoeffs {
ai0: 236.831641,
ai1: -0.39362878,
ai2: 0.00402826613,
};
const AZ_MEDIUM: GalileoNequickCoeffs = GalileoNequickCoeffs {
ai0: 121.129893,
ai1: 0.351254133,
ai2: 0.0134635348,
};
const AZ_LOW: GalileoNequickCoeffs = GalileoNequickCoeffs {
ai0: 2.580271,
ai1: 0.127628236,
ai2: 0.0252748384,
};
const GALILEO_E1_HZ: f64 = 1_575_420_000.0;
#[test]
fn modip_grid_matches_reference() {
let vectors = [
(297.65954, 82.49429, 76.28407),
(307.19404, 5.25218, 19.52877),
(355.75034, 40.42916, 47.85769),
(40.19439, -2.99591, -23.31631),
(166.66933, -77.83835, -71.81130),
(141.13283, 39.13517, 46.48742),
(204.54366, 19.80135, 33.05457),
(115.88525, -31.80197, -51.37982),
];
for (lon, lat, expected) in vectors {
let got = modip_degree(lon, lat);
assert!(
(got - expected).abs() < 1.0e-5,
"modip(lon={lon}, lat={lat}) = {got}, expected {expected}"
);
}
}
#[test]
fn effective_ionisation_level_matches_reference() {
let vectors = [
(AZ_HIGH, 76.284073, 230.245562),
(AZ_MEDIUM, 76.284073, 226.272795),
(AZ_LOW, 76.284073, 159.397123),
(AZ_HIGH, 19.528774, 230.680826),
(AZ_MEDIUM, 19.528774, 133.124084),
(AZ_LOW, 19.528774, 14.711835),
(AZ_HIGH, -71.811295, 285.871846),
(AZ_MEDIUM, -71.811295, 165.335471),
(AZ_LOW, -71.811295, 123.753978),
];
for (coeffs, modip, expected) in vectors {
let got = effective_ionisation_level_sfu(&coeffs, modip);
assert!(
(got - expected).abs() < 1.0e-6,
"Az(modip={modip}) = {got}, expected {expected}"
);
}
}
#[test]
fn ray_perigee_geometry_matches_reference() {
let station = Position::new(297.659539798, 82.494293510, 0.078107446 * 1.0e3);
let satellite = Position::new(241.529931024, 54.445029416, 20370.730845002 * 1.0e3);
let geometry = RayGeometry::new(&station, &satellite).expect("valid ray");
assert!(!geometry.is_vertical);
assert!((geometry.perigee_radius_km - 4169.486317342).abs() < 1.0e-5);
assert!((geometry.azimuth_sin - (-0.164640718)).abs() < 1.0e-5);
assert!((geometry.azimuth_cos - 0.986353605).abs() < 1.0e-5);
assert!((geometry.perigee_lat.deg - 43.550617197).abs() < 1.0e-5);
assert!((geometry.perigee_lon.deg - 405.289045373).abs() < 1.0e-5);
}
struct Bench {
coeffs: GalileoNequickCoeffs,
ray: NequickGRayEval,
stec: f64,
}
#[allow(clippy::too_many_arguments)]
fn bench(
coeffs: GalileoNequickCoeffs,
month: u8,
utc: f64,
s_lon: f64,
s_lat: f64,
s_h: f64,
v_lon: f64,
v_lat: f64,
v_h: f64,
stec: f64,
) -> Bench {
Bench {
coeffs,
ray: NequickGRayEval {
month,
utc_hours: utc,
station_lon_deg: s_lon,
station_lat_deg: s_lat,
station_height_m: s_h,
satellite_lon_deg: v_lon,
satellite_lat_deg: v_lat,
satellite_height_m: v_h,
},
stec,
}
}
fn reference_benchmarks() -> Vec<Bench> {
vec![
bench(
AZ_HIGH,
4,
0.0,
297.66,
82.49,
78.11,
8.23,
54.29,
20281546.18,
20.40224,
),
bench(
AZ_HIGH,
4,
4.0,
297.66,
82.49,
78.11,
-85.72,
53.69,
20544786.65,
18.77379,
),
bench(
AZ_HIGH,
4,
16.0,
297.66,
82.49,
78.11,
-70.26,
50.63,
20043030.82,
23.92627,
),
bench(
AZ_HIGH,
4,
4.0,
307.19,
5.25,
-25.76,
-18.13,
14.17,
20267783.18,
95.52199,
),
bench(
AZ_HIGH,
4,
20.0,
307.19,
5.25,
-25.76,
10.94,
44.72,
20450566.19,
336.73204,
),
bench(
AZ_MEDIUM,
4,
0.0,
40.19,
-3.00,
-23.32,
76.65,
-41.43,
20157673.93,
18.26001,
),
bench(
AZ_MEDIUM,
4,
8.0,
40.19,
-3.00,
-23.32,
89.22,
-40.56,
20055109.63,
101.24016,
),
bench(
AZ_MEDIUM,
4,
0.0,
115.89,
-31.80,
12.78,
119.90,
-8.76,
19941513.27,
24.84680,
),
bench(
AZ_MEDIUM,
4,
12.0,
115.89,
-31.80,
12.78,
133.47,
-24.87,
19975574.41,
13.63080,
),
bench(
AZ_LOW,
4,
0.0,
141.13,
39.14,
117.00,
165.14,
-13.93,
20181976.50,
36.44498,
),
bench(
AZ_LOW,
4,
12.0,
141.13,
39.14,
117.00,
115.63,
-1.28,
20165065.92,
11.05962,
),
bench(
AZ_LOW,
4,
0.0,
204.54,
19.80,
3754.69,
-144.16,
-15.44,
20007317.84,
72.82501,
),
bench(
AZ_LOW,
4,
16.0,
204.54,
19.80,
3754.69,
-167.50,
-43.24,
20095343.11,
3.11261,
),
]
}
#[test]
fn slant_tec_matches_reference_benchmarks() {
for b in reference_benchmarks() {
let got = nequick_g_stec_tecu(&b.coeffs, &b.ray).expect("valid NeQuick-G ray");
assert!(
(got - b.stec).abs() < 1.0e-4,
"STEC for ray {:?} = {got}, expected {}",
b.ray,
b.stec
);
}
}
#[test]
fn delay_is_tec_scaled_by_dispersive_factor() {
let b = &reference_benchmarks()[0];
let stec = nequick_g_stec_tecu(&b.coeffs, &b.ray).expect("valid ray");
let delay = nequick_g_delay_m(&b.coeffs, &b.ray, GALILEO_E1_HZ).expect("valid ray");
let expected = stec * (40.3e16 / (GALILEO_E1_HZ * GALILEO_E1_HZ));
assert!((delay - expected).abs() < 1.0e-9);
assert!(delay > 0.0 && delay < 100.0);
}
#[test]
fn satellite_at_receiver_gives_zero_tec() {
let coeffs = GalileoNequickCoeffs {
ai0: 0.0,
ai1: 0.0,
ai2: 0.0,
};
let ray = NequickGRayEval {
month: 1,
utc_hours: 0.0,
station_lon_deg: 0.0,
station_lat_deg: 0.0,
station_height_m: 0.0,
satellite_lon_deg: 0.0,
satellite_lat_deg: 0.0,
satellite_height_m: 0.0,
};
let stec = nequick_g_stec_tecu(&coeffs, &ray).expect("valid ray");
assert!(stec.abs() < 1.0e-10, "co-located STEC = {stec}");
}
#[test]
fn sub_surface_ray_is_rejected() {
let coeffs = GalileoNequickCoeffs {
ai0: 0.0,
ai1: 0.0,
ai2: 0.0,
};
let ray = NequickGRayEval {
month: 1,
utc_hours: 0.0,
station_lon_deg: 0.0,
station_lat_deg: 0.0,
station_height_m: -3_000_000.0,
satellite_lon_deg: 0.0,
satellite_lat_deg: 90.0,
satellite_height_m: 2_000_000.0,
};
assert!(nequick_g_stec_tecu(&coeffs, &ray).is_err());
}
#[test]
fn rejects_invalid_inputs() {
let coeffs = AZ_HIGH;
let base = reference_benchmarks()[0].ray;
let bad_month = NequickGRayEval { month: 13, ..base };
assert!(nequick_g_stec_tecu(&coeffs, &bad_month).is_err());
let bad_utc = NequickGRayEval {
utc_hours: 25.0,
..base
};
assert!(nequick_g_stec_tecu(&coeffs, &bad_utc).is_err());
let bad_lat = NequickGRayEval {
station_lat_deg: 91.0,
..base
};
assert!(nequick_g_stec_tecu(&coeffs, &bad_lat).is_err());
assert!(nequick_g_delay_m(&coeffs, &base, -1.0).is_err());
}
#[test]
fn galileo_single_frequency_path_uses_nequick_g_not_klobuchar() {
let b = &reference_benchmarks()[0];
let nequick_delay = nequick_g_delay_m(&b.coeffs, &b.ray, GALILEO_E1_HZ).expect("valid ray");
let nequick_stec = nequick_g_stec_tecu(&b.coeffs, &b.ray).expect("valid ray");
assert!((nequick_stec - b.stec).abs() < 1.0e-4);
assert!(nequick_delay > 0.0);
let galileo_compact = galileo_nequick_g_native(
&b.coeffs,
super::super::GalileoNequickEval {
lat_deg: b.ray.station_lat_deg,
lon_deg: b.ray.station_lon_deg - 360.0,
el_deg: 40.0,
t_gal_s: SECONDS_PER_HOUR * b.ray.utc_hours,
day_of_year: 101.0,
frequency_hz: GALILEO_E1_HZ,
},
)
.expect("valid compact Galileo eval");
let klobuchar = klobuchar_native(
&super::super::KlobucharParams {
alpha: [1.0e-8, 0.0, 0.0, 0.0],
beta: [90_000.0, 0.0, 0.0, 0.0],
},
b.ray.station_lat_deg,
b.ray.station_lon_deg - 360.0,
0.0,
40.0,
SECONDS_PER_HOUR * b.ray.utc_hours,
GALILEO_E1_HZ,
)
.expect("valid Klobuchar eval");
assert!(galileo_compact > 0.0);
assert!(
(nequick_delay - klobuchar).abs() > 1.0e-3,
"NeQuick-G delay {nequick_delay} must differ from Klobuchar {klobuchar}"
);
}