Skip to main content

sidereon_core/
frequencies.rs

1//! Canonical GNSS carrier-frequency table.
2//!
3//! All GNSS carrier lookup paths route through this module so signal frequency
4//! policy cannot drift between combinations, RINEX observation decoding, SPP,
5//! and ionosphere models.
6
7use crate::constants::{C_M_S, F_B1I_HZ, F_B3I_HZ, F_E1_HZ, F_E5A_HZ, F_L1_HZ, F_L2_HZ};
8use crate::validate;
9use crate::GnssSystem;
10
11const F_E6_HZ: f64 = 1_278_750_000.0;
12const F_E5B_HZ: f64 = 1_207_140_000.0;
13const F_E5_HZ: f64 = 1_191_795_000.0;
14const F_GLONASS_G1_BASE_HZ: f64 = 1_602_000_000.0;
15const F_GLONASS_G1_STEP_HZ: f64 = 562_500.0;
16const F_GLONASS_G2_BASE_HZ: f64 = 1_246_000_000.0;
17const F_GLONASS_G2_STEP_HZ: f64 = 437_500.0;
18
19/// GNSS carrier band.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub enum CarrierBand {
22    /// GPS/QZSS L1.
23    L1,
24    /// GPS/QZSS L2.
25    L2,
26    /// GPS/QZSS L5.
27    L5,
28    /// Galileo E1.
29    E1,
30    /// Galileo E5a.
31    E5a,
32    /// Galileo E5b.
33    E5b,
34    /// Galileo E5 AltBOC.
35    E5,
36    /// Galileo E6.
37    E6,
38    /// BeiDou B1C.
39    B1c,
40    /// BeiDou B1I.
41    B1i,
42    /// BeiDou B2a.
43    B2a,
44    /// BeiDou B2b.
45    B2b,
46    /// BeiDou B2.
47    B2,
48    /// BeiDou B3I.
49    B3i,
50    /// GLONASS G1 FDMA.
51    G1,
52    /// GLONASS G2 FDMA.
53    G2,
54}
55
56impl CarrierBand {
57    /// Parse a lower-case carrier-band token.
58    pub fn from_name(name: &str) -> Option<Self> {
59        match name {
60            "l1" => Some(Self::L1),
61            "l2" => Some(Self::L2),
62            "l5" => Some(Self::L5),
63            "e1" => Some(Self::E1),
64            "e5a" => Some(Self::E5a),
65            "e5b" => Some(Self::E5b),
66            "e5" => Some(Self::E5),
67            "e6" => Some(Self::E6),
68            "b1c" => Some(Self::B1c),
69            "b1i" => Some(Self::B1i),
70            "b2a" => Some(Self::B2a),
71            "b2b" => Some(Self::B2b),
72            "b2" => Some(Self::B2),
73            "b3i" => Some(Self::B3i),
74            "g1" => Some(Self::G1),
75            "g2" => Some(Self::G2),
76            _ => None,
77        }
78    }
79
80    /// Parse only the carrier-band tokens supported by the ionosphere-free API.
81    pub fn from_iono_free_name(name: &str) -> Option<Self> {
82        match name {
83            "l1" => Some(Self::L1),
84            "l2" => Some(Self::L2),
85            "e1" => Some(Self::E1),
86            "e5a" => Some(Self::E5a),
87            "b1i" => Some(Self::B1i),
88            "b3i" => Some(Self::B3i),
89            _ => None,
90        }
91    }
92
93    /// The canonical lower-case band token.
94    pub const fn name(self) -> &'static str {
95        match self {
96            Self::L1 => "l1",
97            Self::L2 => "l2",
98            Self::L5 => "l5",
99            Self::E1 => "e1",
100            Self::E5a => "e5a",
101            Self::E5b => "e5b",
102            Self::E5 => "e5",
103            Self::E6 => "e6",
104            Self::B1c => "b1c",
105            Self::B1i => "b1i",
106            Self::B2a => "b2a",
107            Self::B2b => "b2b",
108            Self::B2 => "b2",
109            Self::B3i => "b3i",
110            Self::G1 => "g1",
111            Self::G2 => "g2",
112        }
113    }
114}
115
116/// A standard two-carrier ionosphere-free pair.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct CarrierPair {
119    /// First carrier band in the affine combination.
120    pub band1: CarrierBand,
121    /// Second carrier band in the affine combination.
122    pub band2: CarrierBand,
123}
124
125impl CarrierPair {
126    /// Construct a pair from two carrier bands.
127    pub const fn new(band1: CarrierBand, band2: CarrierBand) -> Self {
128        Self { band1, band2 }
129    }
130}
131
132/// One fixed-frequency carrier-table entry.
133#[derive(Debug, Clone, Copy, PartialEq)]
134pub struct CarrierFrequency {
135    /// GNSS constellation.
136    pub system: GnssSystem,
137    /// Carrier band.
138    pub band: CarrierBand,
139    /// Carrier frequency in hertz.
140    pub frequency_hz: f64,
141}
142
143/// Fixed-frequency carrier entries. GLONASS FDMA carriers are channel-derived
144/// through [`rinex_band_frequency_hz`] and therefore do not appear here.
145pub const fn fixed_carrier_frequencies() -> [CarrierFrequency; 17] {
146    [
147        CarrierFrequency {
148            system: GnssSystem::Gps,
149            band: CarrierBand::L1,
150            frequency_hz: F_L1_HZ,
151        },
152        CarrierFrequency {
153            system: GnssSystem::Gps,
154            band: CarrierBand::L2,
155            frequency_hz: F_L2_HZ,
156        },
157        CarrierFrequency {
158            system: GnssSystem::Gps,
159            band: CarrierBand::L5,
160            frequency_hz: F_E5A_HZ,
161        },
162        CarrierFrequency {
163            system: GnssSystem::Qzss,
164            band: CarrierBand::L1,
165            frequency_hz: F_L1_HZ,
166        },
167        CarrierFrequency {
168            system: GnssSystem::Qzss,
169            band: CarrierBand::L2,
170            frequency_hz: F_L2_HZ,
171        },
172        CarrierFrequency {
173            system: GnssSystem::Qzss,
174            band: CarrierBand::L5,
175            frequency_hz: F_E5A_HZ,
176        },
177        CarrierFrequency {
178            system: GnssSystem::Galileo,
179            band: CarrierBand::E1,
180            frequency_hz: F_E1_HZ,
181        },
182        CarrierFrequency {
183            system: GnssSystem::Galileo,
184            band: CarrierBand::E5a,
185            frequency_hz: F_E5A_HZ,
186        },
187        CarrierFrequency {
188            system: GnssSystem::Galileo,
189            band: CarrierBand::E6,
190            frequency_hz: F_E6_HZ,
191        },
192        CarrierFrequency {
193            system: GnssSystem::Galileo,
194            band: CarrierBand::E5b,
195            frequency_hz: F_E5B_HZ,
196        },
197        CarrierFrequency {
198            system: GnssSystem::Galileo,
199            band: CarrierBand::E5,
200            frequency_hz: F_E5_HZ,
201        },
202        CarrierFrequency {
203            system: GnssSystem::BeiDou,
204            band: CarrierBand::B1c,
205            frequency_hz: F_L1_HZ,
206        },
207        CarrierFrequency {
208            system: GnssSystem::BeiDou,
209            band: CarrierBand::B1i,
210            frequency_hz: F_B1I_HZ,
211        },
212        CarrierFrequency {
213            system: GnssSystem::BeiDou,
214            band: CarrierBand::B2a,
215            frequency_hz: F_E5A_HZ,
216        },
217        CarrierFrequency {
218            system: GnssSystem::BeiDou,
219            band: CarrierBand::B3i,
220            frequency_hz: F_B3I_HZ,
221        },
222        CarrierFrequency {
223            system: GnssSystem::BeiDou,
224            band: CarrierBand::B2b,
225            frequency_hz: F_E5B_HZ,
226        },
227        CarrierFrequency {
228            system: GnssSystem::BeiDou,
229            band: CarrierBand::B2,
230            frequency_hz: F_E5_HZ,
231        },
232    ]
233}
234
235/// Carrier entries used by the current ionosphere-free public API.
236pub const fn iono_free_carrier_frequencies() -> [CarrierFrequency; 6] {
237    [
238        CarrierFrequency {
239            system: GnssSystem::Gps,
240            band: CarrierBand::L1,
241            frequency_hz: F_L1_HZ,
242        },
243        CarrierFrequency {
244            system: GnssSystem::Gps,
245            band: CarrierBand::L2,
246            frequency_hz: F_L2_HZ,
247        },
248        CarrierFrequency {
249            system: GnssSystem::Galileo,
250            band: CarrierBand::E1,
251            frequency_hz: F_E1_HZ,
252        },
253        CarrierFrequency {
254            system: GnssSystem::Galileo,
255            band: CarrierBand::E5a,
256            frequency_hz: F_E5A_HZ,
257        },
258        CarrierFrequency {
259            system: GnssSystem::BeiDou,
260            band: CarrierBand::B1i,
261            frequency_hz: F_B1I_HZ,
262        },
263        CarrierFrequency {
264            system: GnssSystem::BeiDou,
265            band: CarrierBand::B3i,
266            frequency_hz: F_B3I_HZ,
267        },
268    ]
269}
270
271/// Carrier frequency in hertz for a constellation and canonical carrier band.
272pub const fn frequency_hz(system: GnssSystem, band: CarrierBand) -> Option<f64> {
273    match (system, band) {
274        (GnssSystem::Gps, CarrierBand::L1) => Some(F_L1_HZ),
275        (GnssSystem::Gps, CarrierBand::L2) => Some(F_L2_HZ),
276        (GnssSystem::Gps, CarrierBand::L5) => Some(F_E5A_HZ),
277        (GnssSystem::Qzss, CarrierBand::L1) => Some(F_L1_HZ),
278        (GnssSystem::Qzss, CarrierBand::L2) => Some(F_L2_HZ),
279        (GnssSystem::Qzss, CarrierBand::L5) => Some(F_E5A_HZ),
280        (GnssSystem::Galileo, CarrierBand::E1) => Some(F_E1_HZ),
281        (GnssSystem::Galileo, CarrierBand::E5a) => Some(F_E5A_HZ),
282        (GnssSystem::Galileo, CarrierBand::E6) => Some(F_E6_HZ),
283        (GnssSystem::Galileo, CarrierBand::E5b) => Some(F_E5B_HZ),
284        (GnssSystem::Galileo, CarrierBand::E5) => Some(F_E5_HZ),
285        (GnssSystem::BeiDou, CarrierBand::B1c) => Some(F_L1_HZ),
286        (GnssSystem::BeiDou, CarrierBand::B1i) => Some(F_B1I_HZ),
287        (GnssSystem::BeiDou, CarrierBand::B2a) => Some(F_E5A_HZ),
288        (GnssSystem::BeiDou, CarrierBand::B3i) => Some(F_B3I_HZ),
289        (GnssSystem::BeiDou, CarrierBand::B2b) => Some(F_E5B_HZ),
290        (GnssSystem::BeiDou, CarrierBand::B2) => Some(F_E5_HZ),
291        _ => None,
292    }
293}
294
295/// Carrier wavelength in meters for a constellation and canonical carrier band.
296pub fn wavelength_m(system: GnssSystem, band: CarrierBand) -> Option<f64> {
297    frequency_hz(system, band).and_then(wavelength_for_frequency)
298}
299
300/// RINEX observation band frequency in hertz for a system and band digit.
301///
302/// GLONASS G1/G2 carriers require the FDMA channel number from the observation
303/// file's `GLONASS SLOT / FRQ #` records.
304pub fn rinex_band_frequency_hz(
305    system: GnssSystem,
306    band: char,
307    glonass_channel: Option<i8>,
308) -> Option<f64> {
309    rinex_signal_frequency_hz(system, band, None, None, glonass_channel)
310}
311
312/// RINEX observation-code frequency in hertz for a system and full code.
313///
314/// BeiDou's band labels changed across RINEX 3 minor versions: in RINEX 3.02
315/// `C1I`/`L1I` are B1I (1561.098 MHz), while in RINEX 3.03 and later band 1 is
316/// B1C (1575.42 MHz). Use this helper when the observation code and file
317/// version are available instead of reducing the code to a band digit first.
318pub fn rinex_observation_frequency_hz(
319    system: GnssSystem,
320    code: &str,
321    rinex_version: f64,
322    glonass_channel: Option<i8>,
323) -> Option<f64> {
324    let mut chars = code.chars();
325    let _kind = chars.next()?;
326    let band = chars.next()?;
327    let tracking = chars.next();
328    rinex_signal_frequency_hz(system, band, tracking, Some(rinex_version), glonass_channel)
329}
330
331fn rinex_signal_frequency_hz(
332    system: GnssSystem,
333    band: char,
334    tracking: Option<char>,
335    rinex_version: Option<f64>,
336    glonass_channel: Option<i8>,
337) -> Option<f64> {
338    let frequency_hz = match (system, band, glonass_channel) {
339        (GnssSystem::Gps, '1', _) => frequency_hz(system, CarrierBand::L1),
340        (GnssSystem::Gps, '2', _) => frequency_hz(system, CarrierBand::L2),
341        (GnssSystem::Gps, '5', _) => frequency_hz(system, CarrierBand::L5),
342        (GnssSystem::Qzss, '1', _) => frequency_hz(system, CarrierBand::L1),
343        (GnssSystem::Qzss, '2', _) => frequency_hz(system, CarrierBand::L2),
344        (GnssSystem::Qzss, '5', _) => frequency_hz(system, CarrierBand::L5),
345        (GnssSystem::Galileo, '1', _) => frequency_hz(system, CarrierBand::E1),
346        (GnssSystem::Galileo, '5', _) => frequency_hz(system, CarrierBand::E5a),
347        (GnssSystem::Galileo, '6', _) => frequency_hz(system, CarrierBand::E6),
348        (GnssSystem::Galileo, '7', _) => frequency_hz(system, CarrierBand::E5b),
349        (GnssSystem::Galileo, '8', _) => frequency_hz(system, CarrierBand::E5),
350        (GnssSystem::BeiDou, _, _) => {
351            frequency_hz(system, rinex_beidou_band(band, tracking, rinex_version)?)
352        }
353        (GnssSystem::Glonass, '1', Some(channel)) => {
354            Some(F_GLONASS_G1_BASE_HZ + f64::from(channel) * F_GLONASS_G1_STEP_HZ)
355        }
356        (GnssSystem::Glonass, '2', Some(channel)) => {
357            Some(F_GLONASS_G2_BASE_HZ + f64::from(channel) * F_GLONASS_G2_STEP_HZ)
358        }
359        _ => None,
360    }?;
361    valid_frequency_hz(frequency_hz)
362}
363
364fn rinex_beidou_band(
365    band: char,
366    tracking: Option<char>,
367    rinex_version: Option<f64>,
368) -> Option<CarrierBand> {
369    match band {
370        '1' if tracking == Some('I') && rinex_version.is_some_and(is_rinex_302) => {
371            Some(CarrierBand::B1i)
372        }
373        '1' => Some(CarrierBand::B1c),
374        '2' => Some(CarrierBand::B1i),
375        '5' => Some(CarrierBand::B2a),
376        '6' => Some(CarrierBand::B3i),
377        '7' => Some(CarrierBand::B2b),
378        '8' => Some(CarrierBand::B2),
379        _ => None,
380    }
381}
382
383fn is_rinex_302(version: f64) -> bool {
384    (3.015..3.025).contains(&version)
385}
386
387/// RINEX observation band wavelength in meters for a system and band digit.
388pub fn rinex_band_wavelength_m(
389    system: GnssSystem,
390    band: char,
391    glonass_channel: Option<i8>,
392) -> Option<f64> {
393    rinex_band_frequency_hz(system, band, glonass_channel).and_then(wavelength_for_frequency)
394}
395
396/// RINEX observation-code wavelength in meters for a system and full code.
397pub fn rinex_observation_wavelength_m(
398    system: GnssSystem,
399    code: &str,
400    rinex_version: f64,
401    glonass_channel: Option<i8>,
402) -> Option<f64> {
403    rinex_observation_frequency_hz(system, code, rinex_version, glonass_channel)
404        .and_then(wavelength_for_frequency)
405}
406
407fn valid_frequency_hz(frequency_hz: f64) -> Option<f64> {
408    validate::finite_positive(frequency_hz, "frequency_hz").ok()
409}
410
411pub(crate) fn wavelength_for_frequency(frequency_hz: f64) -> Option<f64> {
412    valid_frequency_hz(frequency_hz).map(|frequency_hz| C_M_S / frequency_hz)
413}
414
415/// Standard dual-frequency ionosphere-free carrier pair for a constellation.
416pub const fn default_iono_free_pair(system: GnssSystem) -> Option<CarrierPair> {
417    match system {
418        GnssSystem::Gps => Some(CarrierPair::new(CarrierBand::L1, CarrierBand::L2)),
419        GnssSystem::Galileo => Some(CarrierPair::new(CarrierBand::E1, CarrierBand::E5a)),
420        GnssSystem::BeiDou => Some(CarrierPair::new(CarrierBand::B1i, CarrierBand::B3i)),
421        _ => None,
422    }
423}
424
425/// GLONASS G1 FDMA carrier frequency in hertz for an FDMA channel number `k`.
426///
427/// GLONASS is frequency-division multiplexed, so it has no single fixed
428/// single-frequency carrier and is therefore absent from
429/// [`default_spp_frequency_hz`]. The SPP ionosphere-scaling policy resolves the
430/// GLONASS carrier per satellite from its broadcast / observation FDMA channel
431/// instead: `1602.0 MHz + k * 562.5 kHz`, the same table as
432/// [`rinex_band_frequency_hz`] uses for G1. This is the carrier the broadcast
433/// Klobuchar L1 delay is scaled to by `(f_L1 / f_k)^2` for a GLONASS satellite.
434pub const fn glonass_g1_frequency_hz(channel: i8) -> f64 {
435    F_GLONASS_G1_BASE_HZ + (channel as f64) * F_GLONASS_G1_STEP_HZ
436}
437
438/// Single-frequency carrier used by the SPP ionosphere-scaling policy.
439pub const fn default_spp_carrier(system: GnssSystem) -> Option<CarrierBand> {
440    match system {
441        GnssSystem::Gps => Some(CarrierBand::L1),
442        GnssSystem::Galileo => Some(CarrierBand::E1),
443        GnssSystem::BeiDou => Some(CarrierBand::B1i),
444        _ => None,
445    }
446}
447
448/// Single-frequency carrier frequency used by the SPP ionosphere-scaling policy.
449pub const fn default_spp_frequency_hz(system: GnssSystem) -> Option<f64> {
450    match default_spp_carrier(system) {
451        Some(band) => frequency_hz(system, band),
452        None => None,
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn iono_free_table_matches_supported_pairs() {
462        assert_eq!(
463            default_iono_free_pair(GnssSystem::Gps),
464            Some(CarrierPair::new(CarrierBand::L1, CarrierBand::L2))
465        );
466        assert_eq!(
467            default_iono_free_pair(GnssSystem::Galileo),
468            Some(CarrierPair::new(CarrierBand::E1, CarrierBand::E5a))
469        );
470        assert_eq!(
471            default_iono_free_pair(GnssSystem::BeiDou),
472            Some(CarrierPair::new(CarrierBand::B1i, CarrierBand::B3i))
473        );
474        assert_eq!(default_iono_free_pair(GnssSystem::Glonass), None);
475        assert_eq!(iono_free_carrier_frequencies().len(), 6);
476    }
477
478    #[test]
479    fn rinex_band_table_matches_existing_frequency_bits() {
480        let cases: [(GnssSystem, char, Option<i8>, f64); 16] = [
481            (GnssSystem::Gps, '1', None, 1_575_420_000.0),
482            (GnssSystem::Gps, '2', None, 1_227_600_000.0),
483            (GnssSystem::Gps, '5', None, 1_176_450_000.0),
484            (GnssSystem::Qzss, '1', None, 1_575_420_000.0),
485            (GnssSystem::Qzss, '2', None, 1_227_600_000.0),
486            (GnssSystem::Qzss, '5', None, 1_176_450_000.0),
487            (GnssSystem::Galileo, '6', None, 1_278_750_000.0),
488            (GnssSystem::Galileo, '7', None, 1_207_140_000.0),
489            (GnssSystem::Galileo, '8', None, 1_191_795_000.0),
490            (GnssSystem::BeiDou, '1', None, 1_575_420_000.0),
491            (GnssSystem::BeiDou, '2', None, 1_561_098_000.0),
492            (GnssSystem::BeiDou, '6', None, 1_268_520_000.0),
493            (GnssSystem::BeiDou, '7', None, 1_207_140_000.0),
494            (GnssSystem::BeiDou, '8', None, 1_191_795_000.0),
495            (GnssSystem::Glonass, '1', Some(1), 1_602_562_500.0),
496            (GnssSystem::Glonass, '2', Some(1), 1_246_437_500.0),
497        ];
498        for (system, band, channel, expected) in cases {
499            assert_eq!(
500                rinex_band_frequency_hz(system, band, channel).map(f64::to_bits),
501                Some(expected.to_bits())
502            );
503        }
504        assert_eq!(
505            rinex_band_frequency_hz(GnssSystem::Glonass, '1', None),
506            None
507        );
508    }
509
510    #[test]
511    fn rinex_observation_code_resolves_beidou_302_b1i() {
512        for code in ["C1I", "L1I"] {
513            assert_eq!(
514                rinex_observation_frequency_hz(GnssSystem::BeiDou, code, 3.02, None)
515                    .map(f64::to_bits),
516                Some(F_B1I_HZ.to_bits())
517            );
518        }
519        assert_eq!(
520            rinex_observation_frequency_hz(GnssSystem::BeiDou, "L1X", 3.03, None).map(f64::to_bits),
521            Some(F_L1_HZ.to_bits())
522        );
523    }
524
525    #[test]
526    fn wavelength_helpers_use_c_over_frequency() {
527        let wavelength = wavelength_m(GnssSystem::Gps, CarrierBand::L1).unwrap();
528        assert_eq!(wavelength.to_bits(), (C_M_S / F_L1_HZ).to_bits());
529
530        let glonass_wavelength = rinex_band_wavelength_m(GnssSystem::Glonass, '1', Some(-7))
531            .expect("GLONASS G1 channel");
532        let frequency = 1_602_000_000.0 + f64::from(-7) * 562_500.0;
533        assert_eq!(glonass_wavelength.to_bits(), (C_M_S / frequency).to_bits());
534    }
535
536    #[test]
537    fn glonass_g1_spp_carrier_matches_rinex_band_table() {
538        for channel in [-7_i8, -1, 0, 1, 6] {
539            assert_eq!(
540                glonass_g1_frequency_hz(channel).to_bits(),
541                rinex_band_frequency_hz(GnssSystem::Glonass, '1', Some(channel))
542                    .expect("GLONASS G1 channel frequency")
543                    .to_bits()
544            );
545        }
546    }
547
548    #[test]
549    fn frequency_validation_rejects_invalid_runtime_values() {
550        assert_eq!(valid_frequency_hz(f64::NAN), None);
551        assert_eq!(valid_frequency_hz(f64::INFINITY), None);
552        assert_eq!(valid_frequency_hz(0.0), None);
553        assert_eq!(valid_frequency_hz(-1.0), None);
554        assert_eq!(wavelength_for_frequency(f64::NAN), None);
555    }
556}