1use std::collections::{BTreeMap, BTreeSet};
10
11use crate::constants::C_M_S;
12use crate::frequencies;
13pub use crate::frequencies::{CarrierBand, CarrierFrequency, CarrierPair};
14use crate::validate;
15use crate::GnssSystem;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum IonosphereFreeError {
20 UnknownSystem(char),
22 UnknownBand { system: char, band: String },
24 EqualFrequencies,
26 InvalidFrequency,
28 InvalidObservation,
30}
31
32impl core::fmt::Display for IonosphereFreeError {
33 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34 match self {
35 Self::UnknownSystem(system) => {
36 write!(f, "unknown ionosphere-free constellation {system}")
37 }
38 Self::UnknownBand { system, band } => {
39 write!(f, "unknown carrier band {band} for constellation {system}")
40 }
41 Self::EqualFrequencies => write!(f, "equal carrier frequencies"),
42 Self::InvalidFrequency => write!(f, "carrier frequencies must be positive"),
43 Self::InvalidObservation => write!(f, "observations must be finite"),
44 }
45 }
46}
47
48impl std::error::Error for IonosphereFreeError {}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
52pub enum PseudorangeDropReason {
53 MissingBand1,
55 MissingBand2,
57 DuplicateObservation,
59 UnknownSystem,
61}
62
63pub type PseudorangeObservation = (String, f64);
65
66pub type CombinedPseudoranges = Vec<PseudorangeObservation>;
68
69pub type DroppedPseudoranges = Vec<(String, PseudorangeDropReason)>;
71
72pub type PseudorangeCombinationResult =
74 Result<(CombinedPseudoranges, DroppedPseudoranges), IonosphereFreeError>;
75
76pub const fn carrier_frequencies() -> [CarrierFrequency; 6] {
78 frequencies::iono_free_carrier_frequencies()
79}
80
81pub fn carrier_frequency_hz(system: GnssSystem, band: CarrierBand) -> Option<f64> {
83 frequencies::frequency_hz(system, band)
84}
85
86pub fn frequency_hz(system: char, band: &str) -> Result<f64, IonosphereFreeError> {
88 let Some(system_id) = GnssSystem::from_letter(system) else {
89 return Err(IonosphereFreeError::UnknownSystem(system));
90 };
91 let Some(carrier_band) = CarrierBand::from_iono_free_name(band) else {
92 return Err(IonosphereFreeError::UnknownBand {
93 system,
94 band: band.to_owned(),
95 });
96 };
97 carrier_frequency_hz(system_id, carrier_band).ok_or_else(|| IonosphereFreeError::UnknownBand {
98 system,
99 band: band.to_owned(),
100 })
101}
102
103pub fn default_pair(system: char) -> Result<CarrierPair, IonosphereFreeError> {
105 match GnssSystem::from_letter(system).and_then(frequencies::default_iono_free_pair) {
106 Some(pair) => Ok(pair),
107 None => Err(IonosphereFreeError::UnknownSystem(system)),
108 }
109}
110
111pub fn gamma(f1_hz: f64, f2_hz: f64) -> Result<f64, IonosphereFreeError> {
113 let (f1_hz, f2_hz) = validate_distinct_frequencies(f1_hz, f2_hz)?;
114 let f1sq = finite_frequency_product(f1_hz * f1_hz)?;
115 let f2sq = finite_frequency_product(f2_hz * f2_hz)?;
116 let denominator = finite_frequency_product(f1sq - f2sq)?;
117 if denominator == 0.0 {
118 return Err(IonosphereFreeError::EqualFrequencies);
119 }
120 let gamma = f1sq / denominator;
121 validate::finite(gamma, "gamma").map_err(|_| IonosphereFreeError::InvalidFrequency)
122}
123
124pub fn noise_amplification(f1_hz: f64, f2_hz: f64) -> Result<f64, IonosphereFreeError> {
126 let g = gamma(f1_hz, f2_hz)?;
127 let amplification = (g * g + (g - 1.0) * (g - 1.0)).sqrt();
128 validate::finite(amplification, "noise_amplification")
129 .map_err(|_| IonosphereFreeError::InvalidFrequency)
130}
131
132pub fn ionosphere_free(
134 obs1_m: f64,
135 obs2_m: f64,
136 f1_hz: f64,
137 f2_hz: f64,
138) -> Result<f64, IonosphereFreeError> {
139 let (f1_hz, f2_hz) = validate_distinct_frequencies(f1_hz, f2_hz)?;
140 let obs1_m = validate_observation(obs1_m, "obs1_m")?;
141 let obs2_m = validate_observation(obs2_m, "obs2_m")?;
142 validate_observation(
143 ionosphere_free_unchecked(obs1_m, obs2_m, f1_hz, f2_hz),
144 "ionosphere_free_m",
145 )
146}
147
148pub fn ionosphere_free_phase_m(
150 phase1_m: f64,
151 phase2_m: f64,
152 f1_hz: f64,
153 f2_hz: f64,
154) -> Result<f64, IonosphereFreeError> {
155 ionosphere_free(phase1_m, phase2_m, f1_hz, f2_hz)
156}
157
158pub fn ionosphere_free_phase_cycles(
160 phi1_cycles: f64,
161 phi2_cycles: f64,
162 f1_hz: f64,
163 f2_hz: f64,
164) -> Result<f64, IonosphereFreeError> {
165 let (f1_hz, f2_hz) = validate_distinct_frequencies(f1_hz, f2_hz)?;
166 let phi1_cycles = validate_observation(phi1_cycles, "phi1_cycles")?;
167 let phi2_cycles = validate_observation(phi2_cycles, "phi2_cycles")?;
168 let phase1_m = C_M_S / f1_hz * phi1_cycles;
169 let phase2_m = C_M_S / f2_hz * phi2_cycles;
170 let phase1_m = validate_observation(phase1_m, "phase1_m")?;
171 let phase2_m = validate_observation(phase2_m, "phase2_m")?;
172 validate_observation(
173 ionosphere_free_unchecked(phase1_m, phase2_m, f1_hz, f2_hz),
174 "ionosphere_free_phase_m",
175 )
176}
177
178fn validate_distinct_frequencies(
179 f1_hz: f64,
180 f2_hz: f64,
181) -> Result<(f64, f64), IonosphereFreeError> {
182 let f1_hz = validate_frequency(f1_hz, "f1_hz")?;
183 let f2_hz = validate_frequency(f2_hz, "f2_hz")?;
184 if f1_hz == f2_hz {
185 Err(IonosphereFreeError::EqualFrequencies)
186 } else {
187 Ok((f1_hz, f2_hz))
188 }
189}
190
191fn validate_frequency(f_hz: f64, field: &'static str) -> Result<f64, IonosphereFreeError> {
192 validate::finite_positive(f_hz, field).map_err(|_| IonosphereFreeError::InvalidFrequency)
193}
194
195fn finite_frequency_product(value: f64) -> Result<f64, IonosphereFreeError> {
196 validate::finite(value, "frequency_product").map_err(|_| IonosphereFreeError::InvalidFrequency)
197}
198
199fn validate_observation(value: f64, field: &'static str) -> Result<f64, IonosphereFreeError> {
200 validate::finite(value, field).map_err(|_| IonosphereFreeError::InvalidObservation)
201}
202
203fn gamma_unchecked(f1_hz: f64, f2_hz: f64) -> f64 {
204 let f1sq = f1_hz * f1_hz;
205 let f2sq = f2_hz * f2_hz;
206 f1sq / (f1sq - f2sq)
207}
208
209fn ionosphere_free_unchecked(obs1_m: f64, obs2_m: f64, f1_hz: f64, f2_hz: f64) -> f64 {
210 let g = gamma_unchecked(f1_hz, f2_hz);
211 g * obs1_m - (g - 1.0) * obs2_m
212}
213
214pub fn ionosphere_free_pseudoranges(
221 band1: &[PseudorangeObservation],
222 band2: &[PseudorangeObservation],
223 overrides: &[(char, String, String)],
224) -> PseudorangeCombinationResult {
225 let (m1, dups1) = map_and_duplicates(band1)?;
226 let (m2, dups2) = map_and_duplicates(band2)?;
227
228 let dups = dups1.union(&dups2).cloned().collect::<BTreeSet<_>>();
229 let ids1 = m1.keys().cloned().collect::<BTreeSet<_>>();
230 let ids2 = m2.keys().cloned().collect::<BTreeSet<_>>();
231
232 let mut combined = Vec::new();
233 let mut dropped = Vec::new();
234
235 for sat in ids1.intersection(&ids2) {
236 if dups.contains(sat) {
237 continue;
238 }
239 match combine_satellite(sat, m1[sat], m2[sat], overrides) {
240 Ok(range_m) => combined.push((sat.clone(), range_m)),
241 Err(IonosphereFreeError::UnknownSystem(_))
242 | Err(IonosphereFreeError::UnknownBand { .. }) => {
243 dropped.push((sat.clone(), PseudorangeDropReason::UnknownSystem))
244 }
245 Err(error) => return Err(error),
246 }
247 }
248
249 for sat in ids1.difference(&ids2) {
250 if !dups.contains(sat) {
251 dropped.push((sat.clone(), PseudorangeDropReason::MissingBand2));
252 }
253 }
254
255 for sat in ids2.difference(&ids1) {
256 if !dups.contains(sat) {
257 dropped.push((sat.clone(), PseudorangeDropReason::MissingBand1));
258 }
259 }
260
261 for sat in dups {
262 dropped.push((sat, PseudorangeDropReason::DuplicateObservation));
263 }
264
265 dropped.sort();
266 Ok((combined, dropped))
267}
268
269fn map_and_duplicates(
270 observations: &[(String, f64)],
271) -> Result<(BTreeMap<String, f64>, BTreeSet<String>), IonosphereFreeError> {
272 let mut counts = BTreeMap::<String, usize>::new();
273 let mut map = BTreeMap::<String, f64>::new();
274 for (sat, range_m) in observations {
275 let range_m = validate_observation(*range_m, "pseudorange_m")?;
276 *counts.entry(sat.clone()).or_insert(0) += 1;
277 map.insert(sat.clone(), range_m);
278 }
279 let dups = counts
280 .into_iter()
281 .filter_map(|(sat, count)| (count > 1).then_some(sat))
282 .collect();
283 Ok((map, dups))
284}
285
286fn combine_satellite(
287 sat: &str,
288 pr1_m: f64,
289 pr2_m: f64,
290 overrides: &[(char, String, String)],
291) -> Result<f64, IonosphereFreeError> {
292 let system = sat
293 .chars()
294 .next()
295 .ok_or(IonosphereFreeError::UnknownSystem('\0'))?;
296 let pair = pair_for(system, overrides)?;
297 let f1 = frequency_hz(system, pair.band1.name())?;
298 let f2 = frequency_hz(system, pair.band2.name())?;
299 ionosphere_free(pr1_m, pr2_m, f1, f2)
300}
301
302fn pair_for(
303 system: char,
304 overrides: &[(char, String, String)],
305) -> Result<CarrierPair, IonosphereFreeError> {
306 if let Some((_system, band1, band2)) = overrides.iter().find(|(s, _, _)| *s == system) {
307 let Some(band1) = CarrierBand::from_iono_free_name(band1) else {
308 return Err(IonosphereFreeError::UnknownBand {
309 system,
310 band: band1.clone(),
311 });
312 };
313 let Some(band2) = CarrierBand::from_iono_free_name(band2) else {
314 return Err(IonosphereFreeError::UnknownBand {
315 system,
316 band: band2.clone(),
317 });
318 };
319 Ok(CarrierPair::new(band1, band2))
320 } else {
321 default_pair(system)
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::constants::{F_B1I_HZ, F_B3I_HZ, F_E1_HZ, F_E5A_HZ, F_L1_HZ, F_L2_HZ};
329
330 struct OracleCase {
331 f1_hz: u64,
332 f2_hz: u64,
333 pr1_m: u64,
334 pr2_m: u64,
335 gamma: u64,
336 noise: u64,
337 iono_free_m: u64,
338 phi1_cycles: u64,
339 phi2_cycles: u64,
340 phase1_m: u64,
341 phase2_m: u64,
342 phase_if_m: u64,
343 phase_if_cycles_m: u64,
344 }
345
346 fn f(bits: u64) -> f64 {
347 f64::from_bits(bits)
348 }
349
350 #[test]
351 fn frequency_table_matches_standard_carriers() {
352 assert_eq!(frequency_hz('G', "l1"), Ok(F_L1_HZ));
353 assert_eq!(frequency_hz('G', "l2"), Ok(F_L2_HZ));
354 assert_eq!(frequency_hz('E', "e1"), Ok(F_E1_HZ));
355 assert_eq!(frequency_hz('E', "e5a"), Ok(F_E5A_HZ));
356 assert_eq!(frequency_hz('C', "b1i"), Ok(F_B1I_HZ));
357 assert_eq!(frequency_hz('C', "b3i"), Ok(F_B3I_HZ));
358 assert_eq!(carrier_frequencies().len(), 6);
359 }
360
361 #[test]
362 fn frequency_lookup_classifies_system_and_band_errors() {
363 assert_eq!(
364 frequency_hz('X', "l1"),
365 Err(IonosphereFreeError::UnknownSystem('X'))
366 );
367 assert_eq!(
368 frequency_hz('G', "bad"),
369 Err(IonosphereFreeError::UnknownBand {
370 system: 'G',
371 band: "bad".to_owned(),
372 })
373 );
374 assert_eq!(frequency_hz('G', "l1"), Ok(F_L1_HZ));
375 }
376
377 #[test]
378 fn default_pairs_match_supported_systems() {
379 assert_eq!(
380 default_pair('G'),
381 Ok(CarrierPair::new(CarrierBand::L1, CarrierBand::L2))
382 );
383 assert_eq!(
384 default_pair('E'),
385 Ok(CarrierPair::new(CarrierBand::E1, CarrierBand::E5a))
386 );
387 assert_eq!(
388 default_pair('C'),
389 Ok(CarrierPair::new(CarrierBand::B1i, CarrierBand::B3i))
390 );
391 assert_eq!(
392 default_pair('X'),
393 Err(IonosphereFreeError::UnknownSystem('X'))
394 );
395 }
396
397 #[test]
398 fn scipy_oracle_cases_are_zero_ulp() {
399 let cases = [
400 OracleCase {
401 f1_hz: 0x41d779c018000000,
402 f2_hz: 0x41d24aec20000000,
403 pr1_m: 0x4175ef3c40772a36,
404 pr2_m: 0x4175ef3c6a2bcbb5,
405 gamma: 0x40045da686c28e3c,
406 noise: 0x4007d3777c503ebc,
407 iono_free_m: 0x4175ef3c00000000,
408 phi1_cycles: 0x419cd8990a6a993b,
409 phi2_cycles: 0x419682ad3bea73b9,
410 phase1_m: 0x4175f4f80ddd7ecd,
411 phase2_m: 0x4175fd37d057d184,
412 phase_if_m: 0x4175e837d93b3cba,
413 phase_if_cycles_m: 0x4175e837d93b3cba,
414 },
415 OracleCase {
416 f1_hz: 0x41d779c018000000,
417 f2_hz: 0x41d187ccf4000000,
418 pr1_m: 0x41775d7280ee546c,
419 pr2_m: 0x41775d72e7354588,
420 gamma: 0x400215b7b8bf1d8d,
421 noise: 0x4004b4e6a9e28198,
422 iono_free_m: 0x41775d71ffffffff,
423 phi1_cycles: 0x419eb9b5c28924dd,
424 phi2_cycles: 0x4196fa6edb5f553e,
425 phase1_m: 0x4177632debd8c260,
426 phase2_m: 0x41776c09259441ae,
427 phase_if_m: 0x41775803d8d388ae,
428 phase_if_cycles_m: 0x41775803d8d388ae,
429 },
430 OracleCase {
431 f1_hz: 0x41d7431dc4000000,
432 f2_hz: 0x41d2e70510000000,
433 pr1_m: 0x41753821627b0be3,
434 pr2_m: 0x417538219525d0a5,
435 gamma: 0x40078ca90724ddf1,
436 noise: 0x400c384adb005afd,
437 iono_free_m: 0x4175382100000000,
438 phi1_cycles: 0x419ba72a23131776,
439 phi2_cycles: 0x419680982c673023,
440 phase1_m: 0x41753deaa1cd1743,
441 phase2_m: 0x417545a9733bf939,
442 phase_if_m: 0x41752edcaa262834,
443 phase_if_cycles_m: 0x41752edcaa262834,
444 },
445 ];
446
447 for case in cases {
448 let f1 = f(case.f1_hz);
449 let f2 = f(case.f2_hz);
450 assert_eq!(gamma(f1, f2).unwrap().to_bits(), case.gamma);
451 assert_eq!(noise_amplification(f1, f2).unwrap().to_bits(), case.noise);
452 assert_eq!(
453 ionosphere_free(f(case.pr1_m), f(case.pr2_m), f1, f2)
454 .unwrap()
455 .to_bits(),
456 case.iono_free_m
457 );
458 assert_eq!(
459 ionosphere_free_phase_m(f(case.phase1_m), f(case.phase2_m), f1, f2)
460 .unwrap()
461 .to_bits(),
462 case.phase_if_m
463 );
464 assert_eq!(
465 ionosphere_free_phase_cycles(f(case.phi1_cycles), f(case.phi2_cycles), f1, f2)
466 .unwrap()
467 .to_bits(),
468 case.phase_if_cycles_m
469 );
470 }
471 }
472
473 #[test]
474 fn invalid_frequency_modes_are_tagged() {
475 assert_eq!(
476 gamma(F_L1_HZ, F_L1_HZ),
477 Err(IonosphereFreeError::EqualFrequencies)
478 );
479 assert_eq!(
480 gamma(f64::NAN, F_L2_HZ),
481 Err(IonosphereFreeError::InvalidFrequency)
482 );
483 assert_eq!(
484 gamma(f64::MAX, F_L2_HZ),
485 Err(IonosphereFreeError::InvalidFrequency)
486 );
487 assert_eq!(
488 noise_amplification(F_L1_HZ, f64::INFINITY),
489 Err(IonosphereFreeError::InvalidFrequency)
490 );
491 assert_eq!(
492 ionosphere_free(1.0, 2.0, F_L1_HZ, 0.0),
493 Err(IonosphereFreeError::InvalidFrequency)
494 );
495 assert_eq!(
496 ionosphere_free_phase_cycles(1.0, 2.0, F_L1_HZ, F_L1_HZ),
497 Err(IonosphereFreeError::EqualFrequencies)
498 );
499 assert_eq!(
500 ionosphere_free_phase_cycles(1.0, 2.0, 0.0, F_L2_HZ),
501 Err(IonosphereFreeError::InvalidFrequency)
502 );
503 assert_eq!(
504 ionosphere_free_phase_cycles(1.0, 2.0, -F_L1_HZ, F_L2_HZ),
505 Err(IonosphereFreeError::InvalidFrequency)
506 );
507 }
508
509 #[test]
510 fn invalid_observations_are_rejected() {
511 assert_eq!(
512 ionosphere_free(f64::NAN, 2.0, F_L1_HZ, F_L2_HZ),
513 Err(IonosphereFreeError::InvalidObservation)
514 );
515 assert_eq!(
516 ionosphere_free_phase_m(1.0, f64::INFINITY, F_L1_HZ, F_L2_HZ),
517 Err(IonosphereFreeError::InvalidObservation)
518 );
519 assert_eq!(
520 ionosphere_free_phase_cycles(f64::NAN, 2.0, F_L1_HZ, F_L2_HZ),
521 Err(IonosphereFreeError::InvalidObservation)
522 );
523 assert_eq!(
524 ionosphere_free(f64::MAX, -f64::MAX, F_L1_HZ, F_L2_HZ),
525 Err(IonosphereFreeError::InvalidObservation)
526 );
527 }
528
529 #[test]
530 fn pseudorange_pairing_reports_missing_unknown_and_duplicate_satellites() {
531 let band1 = vec![
532 ("G01".to_string(), 23_000_000.0),
533 ("G01".to_string(), 23_000_010.0),
534 ("G02".to_string(), 22_000_000.0),
535 ("G03".to_string(), 21_000_000.0),
536 ("X01".to_string(), 20_000_000.0),
537 ];
538 let band2 = vec![
539 ("G01".to_string(), 23_000_000.0),
540 ("G02".to_string(), 22_000_000.0),
541 ("G04".to_string(), 24_000_000.0),
542 ("X01".to_string(), 20_000_000.0),
543 ];
544
545 let (combined, dropped) =
546 ionosphere_free_pseudoranges(&band1, &band2, &[]).expect("valid pseudoranges");
547
548 assert_eq!(combined.len(), 1);
549 assert_eq!(combined[0].0, "G02");
550 assert_eq!(
551 dropped,
552 vec![
553 (
554 "G01".to_string(),
555 PseudorangeDropReason::DuplicateObservation
556 ),
557 ("G03".to_string(), PseudorangeDropReason::MissingBand2),
558 ("G04".to_string(), PseudorangeDropReason::MissingBand1),
559 ("X01".to_string(), PseudorangeDropReason::UnknownSystem),
560 ]
561 );
562 }
563
564 #[test]
565 fn pseudorange_pairing_rejects_non_finite_values() {
566 let band1 = vec![("G01".to_string(), f64::NAN)];
567 let band2 = vec![("G01".to_string(), 23_000_000.0)];
568
569 assert_eq!(
570 ionosphere_free_pseudoranges(&band1, &band2, &[]),
571 Err(IonosphereFreeError::InvalidObservation)
572 );
573 }
574}