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