Skip to main content

sidereon_core/
carrier_phase.rs

1//! Carrier-phase combinations, cycle-slip detection, and Hatch smoothing.
2//!
3//! This module owns the language-independent modeling behind Sidereon'
4//! `CarrierPhase` API: phase-cycle conversion, geometry-free and
5//! Melbourne-Wubbena combinations, loss-of-lock/GF/MW/data-gap cycle-slip
6//! classification, and single/dual-frequency Hatch smoothing.
7
8use crate::combinations;
9use crate::constants::C_M_S;
10use crate::tolerances::FREQUENCY_DENOMINATOR_EPS_HZ;
11use crate::validate;
12
13/// Frequency separation below which wide-lane / narrow-lane denominators are
14/// treated as degenerate, hertz.
15pub use crate::tolerances::FREQUENCY_DENOMINATOR_EPS_HZ as FREQ_EPSILON_HZ;
16
17/// Default geometry-free cycle-slip threshold, meters.
18pub const DEFAULT_GF_THRESHOLD_M: f64 = 0.05;
19
20/// Default Melbourne-Wubbena cycle-slip threshold, wide-lane cycles.
21pub const DEFAULT_MW_THRESHOLD_CYCLES: f64 = 4.0;
22
23/// Default maximum gap between consecutive usable arc samples, seconds.
24pub const DEFAULT_MIN_ARC_GAP_S: f64 = 300.0;
25
26pub(crate) const MIN_HATCH_WINDOW_CAP: usize = 1;
27
28/// Default Hatch carrier-smoothing window cap, in epochs.
29///
30/// The smoothing window length is held to at most this many epochs so the code
31/// noise keeps averaging down while the carrier-phase ambiguity contribution
32/// stays bounded. This is the value every binding hardcodes and the core
33/// smoothing goldens run with (the `smooth_code` / `smooth_iono_free_code`
34/// tests pass `100`). Feeds the `hatch_window_cap` argument of
35/// [`smooth_code`] and [`smooth_iono_free_code`]; the floor enforced internally
36/// is [`MIN_HATCH_WINDOW_CAP`].
37pub const DEFAULT_HATCH_WINDOW_CAP: usize = 100;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub(crate) struct InvalidHatchWindowCap;
41
42/// Error produced by carrier-phase scalar combinations.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum CarrierPhaseError {
45    /// The denominator for a frequency combination is degenerate.
46    EqualFrequencies,
47    /// Cycle-to-meter conversion requires a positive carrier frequency.
48    InvalidFrequency,
49    /// Observation values must be finite and produce finite combinations.
50    InvalidObservation,
51    /// Cycle-slip and smoothing thresholds must be finite and in range.
52    InvalidThreshold,
53}
54
55impl core::fmt::Display for CarrierPhaseError {
56    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
57        match self {
58            Self::EqualFrequencies => write!(f, "equal carrier frequencies"),
59            Self::InvalidFrequency => write!(f, "carrier frequency must be positive"),
60            Self::InvalidObservation => write!(f, "carrier observations must be finite"),
61            Self::InvalidThreshold => write!(f, "carrier thresholds must be finite and sane"),
62        }
63    }
64}
65
66impl std::error::Error for CarrierPhaseError {}
67
68/// One epoch in a single-satellite carrier-phase arc.
69#[derive(Debug, Clone, Copy, PartialEq)]
70pub struct ArcEpoch {
71    /// Band-1 carrier phase, cycles.
72    pub phi1_cycles: Option<f64>,
73    /// Band-2 carrier phase, cycles.
74    pub phi2_cycles: Option<f64>,
75    /// Band-1 code pseudorange, meters.
76    pub p1_m: Option<f64>,
77    /// Band-2 code pseudorange, meters.
78    pub p2_m: Option<f64>,
79    /// Band-1 loss-of-lock indicator.
80    pub lli1: Option<i64>,
81    /// Band-2 loss-of-lock indicator.
82    pub lli2: Option<i64>,
83    /// Band-1 carrier frequency, hertz. `None` means the epoch is skipped.
84    pub f1_hz: Option<f64>,
85    /// Band-2 carrier frequency, hertz. `None` means the epoch is skipped.
86    pub f2_hz: Option<f64>,
87    /// Comparable epoch coordinate in seconds, when the caller can supply one.
88    pub gap_time_s: Option<f64>,
89}
90
91/// Options controlling cycle-slip classification.
92#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct CycleSlipOptions {
94    /// Geometry-free step threshold, meters.
95    pub gf_threshold_m: f64,
96    /// Melbourne-Wubbena step threshold, wide-lane cycles.
97    pub mw_threshold_cycles: f64,
98    /// Data-gap threshold, seconds.
99    pub min_arc_gap_s: f64,
100}
101
102impl Default for CycleSlipOptions {
103    fn default() -> Self {
104        Self {
105            gf_threshold_m: DEFAULT_GF_THRESHOLD_M,
106            mw_threshold_cycles: DEFAULT_MW_THRESHOLD_CYCLES,
107            min_arc_gap_s: DEFAULT_MIN_ARC_GAP_S,
108        }
109    }
110}
111
112/// Reason a carrier arc was split at an epoch.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum SlipReason {
115    /// Loss-of-lock indicator bit 0 was set on either band.
116    Lli,
117    /// Gap to the previous usable sample exceeded the configured threshold.
118    DataGap,
119    /// Geometry-free phase step exceeded the configured threshold.
120    GeometryFree,
121    /// Melbourne-Wubbena step exceeded the configured wide-lane-cycle threshold.
122    MelbourneWubbena,
123}
124
125/// Cycle-slip classification for one input epoch.
126#[derive(Debug, Clone, PartialEq)]
127pub struct SlipResult {
128    /// Whether any slip reason was flagged.
129    pub slip: bool,
130    /// Slip reasons in deterministic Sidereon API order.
131    pub reasons: Vec<SlipReason>,
132    /// Current geometry-free phase, meters, when computable.
133    pub gf_m: Option<f64>,
134    /// Current Melbourne-Wubbena combination, meters, when computable.
135    pub mw_m: Option<f64>,
136    /// Whether the epoch was skipped because a frequency was unavailable.
137    pub skipped: bool,
138}
139
140/// Hatch-smoothed single-frequency code output for one epoch.
141#[derive(Debug, Clone, Copy, PartialEq)]
142pub struct SmoothCodeResult {
143    /// Smoothed code pseudorange, meters, when computable.
144    pub p_smooth_m: Option<f64>,
145    /// Hatch window length used at this epoch.
146    pub window: usize,
147    /// True when a prior running window was reset by a slip at this epoch.
148    pub reset: bool,
149}
150
151/// Hatch-smoothed ionosphere-free code output for one epoch.
152#[derive(Debug, Clone, Copy, PartialEq)]
153pub struct IonoFreeSmoothResult {
154    /// Smoothed ionosphere-free code pseudorange, meters, when computable.
155    pub p_smooth_m: Option<f64>,
156    /// Instantaneous ionosphere-free code, meters.
157    pub p_if_m: Option<f64>,
158    /// Instantaneous ionosphere-free carrier phase, meters.
159    pub l_if_m: Option<f64>,
160    /// Hatch window length used at this epoch.
161    pub window: usize,
162    /// True when a prior running window was reset by a slip at this epoch.
163    pub reset: bool,
164}
165
166/// Carrier phase in meters, `L = c / f * phi`.
167pub fn phase_meters(phi_cycles: f64, f_hz: f64) -> Result<f64, CarrierPhaseError> {
168    let f_hz = validate_frequency(f_hz, "f_hz")?;
169    let phi_cycles = validate_observation(phi_cycles, "phi_cycles")?;
170    validate_observation(C_M_S / f_hz * phi_cycles, "phase_m")
171}
172
173/// Geometry-free phase combination `L_GF = L1 - L2`, meters.
174pub fn geometry_free(l1_m: f64, l2_m: f64) -> Result<f64, CarrierPhaseError> {
175    let l1_m = validate_observation(l1_m, "l1_m")?;
176    let l2_m = validate_observation(l2_m, "l2_m")?;
177    validate_observation(l1_m - l2_m, "geometry_free_m")
178}
179
180/// Wide-lane wavelength `lambda_WL = c / (f1 - f2)`, meters.
181pub fn wide_lane_wavelength(f1_hz: f64, f2_hz: f64) -> Result<f64, CarrierPhaseError> {
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).abs() < FREQUENCY_DENOMINATOR_EPS_HZ {
185        Err(CarrierPhaseError::EqualFrequencies)
186    } else {
187        Ok(C_M_S / (f1_hz - f2_hz))
188    }
189}
190
191/// Narrow-lane code `P_NL = (f1*P1 + f2*P2) / (f1 + f2)`, meters.
192pub fn narrow_lane_code(
193    p1_m: f64,
194    p2_m: f64,
195    f1_hz: f64,
196    f2_hz: f64,
197) -> Result<f64, CarrierPhaseError> {
198    let f1_hz = validate_frequency(f1_hz, "f1_hz")?;
199    let f2_hz = validate_frequency(f2_hz, "f2_hz")?;
200    let p1_m = validate_observation(p1_m, "p1_m")?;
201    let p2_m = validate_observation(p2_m, "p2_m")?;
202    if (f1_hz + f2_hz).abs() < FREQUENCY_DENOMINATOR_EPS_HZ {
203        Err(CarrierPhaseError::EqualFrequencies)
204    } else {
205        validate_observation((f1_hz * p1_m + f2_hz * p2_m) / (f1_hz + f2_hz), "p_nl_m")
206    }
207}
208
209/// Melbourne-Wubbena combination, meters.
210pub fn melbourne_wubbena(
211    phi1_cycles: f64,
212    phi2_cycles: f64,
213    p1_m: f64,
214    p2_m: f64,
215    f1_hz: f64,
216    f2_hz: f64,
217) -> Result<f64, CarrierPhaseError> {
218    let lambda_wl = wide_lane_wavelength(f1_hz, f2_hz)?;
219    let p_nl = narrow_lane_code(p1_m, p2_m, f1_hz, f2_hz)?;
220    let phi1_cycles = validate_observation(phi1_cycles, "phi1_cycles")?;
221    let phi2_cycles = validate_observation(phi2_cycles, "phi2_cycles")?;
222    let l_wl = lambda_wl * (phi1_cycles - phi2_cycles);
223    let l_wl = validate_observation(l_wl, "wide_lane_phase_m")?;
224    validate_observation(l_wl - p_nl, "melbourne_wubbena_m")
225}
226
227/// Code-minus-carrier diagnostic `CMC = P - L`, meters.
228pub fn code_minus_carrier(p_m: f64, phi_cycles: f64, f_hz: f64) -> Result<f64, CarrierPhaseError> {
229    let p_m = validate_observation(p_m, "p_m")?;
230    let l_m = phase_meters(phi_cycles, f_hz)?;
231    validate_observation(p_m - l_m, "code_minus_carrier_m")
232}
233
234/// Melbourne-Wubbena wide-lane ambiguity estimate in wide-lane cycles,
235/// `(MW combination) / lambda_WL`.
236pub fn wide_lane_cycles(
237    phi1_cycles: f64,
238    phi2_cycles: f64,
239    p1_m: f64,
240    p2_m: f64,
241    f1_hz: f64,
242    f2_hz: f64,
243) -> Result<f64, CarrierPhaseError> {
244    let mw_m = melbourne_wubbena(phi1_cycles, phi2_cycles, p1_m, p2_m, f1_hz, f2_hz)?;
245    let lambda_wl = wide_lane_wavelength(f1_hz, f2_hz)?;
246    validate_observation(mw_m / lambda_wl, "wide_lane_cycles")
247}
248
249fn validate_frequency(f_hz: f64, field: &'static str) -> Result<f64, CarrierPhaseError> {
250    validate::finite_positive(f_hz, field).map_err(|_| CarrierPhaseError::InvalidFrequency)
251}
252
253fn validate_observation(value: f64, field: &'static str) -> Result<f64, CarrierPhaseError> {
254    validate::finite(value, field).map_err(|_| CarrierPhaseError::InvalidObservation)
255}
256
257fn validate_optional_observation(
258    value: Option<f64>,
259    field: &'static str,
260) -> Result<(), CarrierPhaseError> {
261    match value {
262        Some(value) => validate_observation(value, field).map(|_| ()),
263        None => Ok(()),
264    }
265}
266
267fn validate_optional_frequency(
268    value: Option<f64>,
269    field: &'static str,
270) -> Result<(), CarrierPhaseError> {
271    match value {
272        Some(value) => validate_frequency(value, field).map(|_| ()),
273        None => Ok(()),
274    }
275}
276
277fn validate_options(options: CycleSlipOptions) -> Result<(), CarrierPhaseError> {
278    validate::finite_nonneg(options.gf_threshold_m, "gf_threshold_m")
279        .map_err(|_| CarrierPhaseError::InvalidThreshold)?;
280    validate::finite_nonneg(options.mw_threshold_cycles, "mw_threshold_cycles")
281        .map_err(|_| CarrierPhaseError::InvalidThreshold)?;
282    validate::finite_positive(options.min_arc_gap_s, "min_arc_gap_s")
283        .map_err(|_| CarrierPhaseError::InvalidThreshold)?;
284    Ok(())
285}
286
287fn validate_arc_epoch(ep: &ArcEpoch) -> Result<(), CarrierPhaseError> {
288    validate_optional_observation(ep.phi1_cycles, "phi1_cycles")?;
289    validate_optional_observation(ep.phi2_cycles, "phi2_cycles")?;
290    validate_optional_observation(ep.p1_m, "p1_m")?;
291    validate_optional_observation(ep.p2_m, "p2_m")?;
292    validate_optional_frequency(ep.f1_hz, "f1_hz")?;
293    validate_optional_frequency(ep.f2_hz, "f2_hz")?;
294    validate_optional_observation(ep.gap_time_s, "gap_time_s")
295}
296
297pub(crate) fn validate_hatch_window_cap(
298    hatch_window_cap: usize,
299) -> Result<usize, InvalidHatchWindowCap> {
300    if hatch_window_cap < MIN_HATCH_WINDOW_CAP {
301        Err(InvalidHatchWindowCap)
302    } else {
303        Ok(hatch_window_cap)
304    }
305}
306
307fn clamp_hatch_window_cap(hatch_window_cap: usize) -> usize {
308    validate_hatch_window_cap(hatch_window_cap).unwrap_or(MIN_HATCH_WINDOW_CAP)
309}
310
311/// Detect cycle slips on a time-ordered single-satellite arc.
312pub fn detect_cycle_slips(
313    arc: &[ArcEpoch],
314    options: CycleSlipOptions,
315) -> Result<Vec<SlipResult>, CarrierPhaseError> {
316    validate_options(options)?;
317    let mut results = Vec::with_capacity(arc.len());
318    let mut prev = None;
319
320    for ep in arc {
321        validate_arc_epoch(ep)?;
322        let result = classify_epoch(ep, prev, options)?;
323        if dual_frequency_reference_usable(&result) {
324            prev = Some(PreviousEpoch {
325                gf_m: result.gf_m,
326                mw_m: result.mw_m,
327                gap_time_s: ep.gap_time_s,
328            });
329        }
330        results.push(result);
331    }
332
333    Ok(results)
334}
335
336fn dual_frequency_reference_usable(result: &SlipResult) -> bool {
337    !result.skipped && (result.gf_m.is_some() || result.mw_m.is_some())
338}
339
340/// Single-frequency Hatch carrier-smoothed code on band 1.
341pub fn smooth_code(
342    arc: &[ArcEpoch],
343    options: CycleSlipOptions,
344    hatch_window_cap: usize,
345) -> Result<Vec<SmoothCodeResult>, CarrierPhaseError> {
346    let hatch_window_cap = clamp_hatch_window_cap(hatch_window_cap);
347    let slips = detect_band1_hatch_slips(arc, options)?;
348    let mut results = Vec::with_capacity(arc.len());
349    let mut state = None;
350
351    for (ep, slip) in arc.iter().zip(&slips) {
352        let (result, next_state) = hatch_epoch(ep, slip, state, hatch_window_cap);
353        validate_smooth_code_result(result)?;
354        results.push(result);
355        state = next_state;
356    }
357
358    Ok(results)
359}
360
361fn detect_band1_hatch_slips(
362    arc: &[ArcEpoch],
363    options: CycleSlipOptions,
364) -> Result<Vec<SlipResult>, CarrierPhaseError> {
365    validate_options(options)?;
366    let mut results = Vec::with_capacity(arc.len());
367    let mut prev_band1 = None;
368    let mut prev_dual = None;
369
370    for ep in arc {
371        validate_arc_epoch(ep)?;
372        let band1 = classify_single_frequency_hatch_epoch(ep, prev_band1, options);
373        let dual = if ep.f2_hz.is_some() {
374            Some(classify_epoch(ep, prev_dual, options)?)
375        } else {
376            None
377        };
378
379        let result = match dual.as_ref() {
380            Some(dual) if !band1.skipped => combine_band1_hatch_slip(&band1, dual),
381            _ => band1,
382        };
383
384        if band1_hatch_usable(ep) {
385            prev_band1 = Some(PreviousEpoch {
386                gf_m: None,
387                mw_m: None,
388                gap_time_s: ep.gap_time_s,
389            });
390        }
391        if let Some(dual) = dual {
392            if dual_frequency_reference_usable(&dual) {
393                prev_dual = Some(PreviousEpoch {
394                    gf_m: dual.gf_m,
395                    mw_m: dual.mw_m,
396                    gap_time_s: ep.gap_time_s,
397                });
398            }
399        }
400        results.push(result);
401    }
402
403    Ok(results)
404}
405
406fn combine_band1_hatch_slip(band1: &SlipResult, dual: &SlipResult) -> SlipResult {
407    let mut reasons = Vec::new();
408    if dual.reasons.contains(&SlipReason::Lli) || band1.reasons.contains(&SlipReason::Lli) {
409        reasons.push(SlipReason::Lli);
410    }
411    if band1.reasons.contains(&SlipReason::DataGap) {
412        reasons.push(SlipReason::DataGap);
413    }
414    if dual.reasons.contains(&SlipReason::GeometryFree) {
415        reasons.push(SlipReason::GeometryFree);
416    }
417    if dual.reasons.contains(&SlipReason::MelbourneWubbena) {
418        reasons.push(SlipReason::MelbourneWubbena);
419    }
420
421    SlipResult {
422        slip: !reasons.is_empty(),
423        reasons,
424        gf_m: dual.gf_m,
425        mw_m: dual.mw_m,
426        skipped: false,
427    }
428}
429
430/// Dual-frequency ionosphere-free Hatch carrier-smoothed code.
431pub fn smooth_iono_free_code(
432    arc: &[ArcEpoch],
433    options: CycleSlipOptions,
434    hatch_window_cap: usize,
435) -> Result<Vec<IonoFreeSmoothResult>, CarrierPhaseError> {
436    let hatch_window_cap = clamp_hatch_window_cap(hatch_window_cap);
437    let slips = detect_cycle_slips(arc, options)?;
438    let mut results = Vec::with_capacity(arc.len());
439    let mut state = None;
440
441    for (ep, slip) in arc.iter().zip(&slips) {
442        let (result, next_state) = iono_free_hatch_epoch(ep, slip, state, hatch_window_cap)?;
443        validate_iono_free_smooth_result(result)?;
444        results.push(result);
445        state = next_state;
446    }
447
448    Ok(results)
449}
450
451#[derive(Debug, Clone, Copy)]
452struct PreviousEpoch {
453    gf_m: Option<f64>,
454    mw_m: Option<f64>,
455    gap_time_s: Option<f64>,
456}
457
458fn classify_epoch(
459    ep: &ArcEpoch,
460    prev: Option<PreviousEpoch>,
461    options: CycleSlipOptions,
462) -> Result<SlipResult, CarrierPhaseError> {
463    let (Some(f1), Some(f2)) = (ep.f1_hz, ep.f2_hz) else {
464        return Ok(SlipResult {
465            slip: false,
466            reasons: Vec::new(),
467            gf_m: None,
468            mw_m: None,
469            skipped: true,
470        });
471    };
472
473    let gf = current_gf(ep.phi1_cycles, ep.phi2_cycles, f1, f2)?;
474    let mw = current_mw(ep.phi1_cycles, ep.phi2_cycles, ep.p1_m, ep.p2_m, f1, f2)?;
475
476    let mut reasons = Vec::new();
477    if loss_of_lock(ep) {
478        reasons.push(SlipReason::Lli);
479    }
480    if gap_reason(ep.gap_time_s, prev, options.min_arc_gap_s) {
481        reasons.push(SlipReason::DataGap);
482    }
483    if gf_reason(gf, prev, options.gf_threshold_m) {
484        reasons.push(SlipReason::GeometryFree);
485    }
486    if mw_reason(mw, prev, f1, f2, options.mw_threshold_cycles) {
487        reasons.push(SlipReason::MelbourneWubbena);
488    }
489
490    Ok(SlipResult {
491        slip: !reasons.is_empty(),
492        reasons,
493        gf_m: gf,
494        mw_m: mw,
495        skipped: false,
496    })
497}
498
499fn classify_single_frequency_hatch_epoch(
500    ep: &ArcEpoch,
501    prev: Option<PreviousEpoch>,
502    options: CycleSlipOptions,
503) -> SlipResult {
504    if !band1_hatch_usable(ep) {
505        return SlipResult {
506            slip: false,
507            reasons: Vec::new(),
508            gf_m: None,
509            mw_m: None,
510            skipped: true,
511        };
512    }
513
514    let mut reasons = Vec::new();
515    if lli_set(ep.lli1) {
516        reasons.push(SlipReason::Lli);
517    }
518    if gap_reason(ep.gap_time_s, prev, options.min_arc_gap_s) {
519        reasons.push(SlipReason::DataGap);
520    }
521
522    SlipResult {
523        slip: !reasons.is_empty(),
524        reasons,
525        gf_m: None,
526        mw_m: None,
527        skipped: false,
528    }
529}
530
531fn band1_hatch_usable(ep: &ArcEpoch) -> bool {
532    ep.f1_hz.is_some() && ep.p1_m.is_some() && ep.phi1_cycles.is_some()
533}
534
535fn current_gf(
536    phi1: Option<f64>,
537    phi2: Option<f64>,
538    f1: f64,
539    f2: f64,
540) -> Result<Option<f64>, CarrierPhaseError> {
541    let (Some(phi1), Some(phi2)) = (phi1, phi2) else {
542        return Ok(None);
543    };
544    let l1 = phase_meters(phi1, f1)?;
545    let l2 = phase_meters(phi2, f2)?;
546    geometry_free(l1, l2).map(Some)
547}
548
549fn current_mw(
550    phi1: Option<f64>,
551    phi2: Option<f64>,
552    p1: Option<f64>,
553    p2: Option<f64>,
554    f1: f64,
555    f2: f64,
556) -> Result<Option<f64>, CarrierPhaseError> {
557    let (Some(phi1), Some(phi2), Some(p1), Some(p2)) = (phi1, phi2, p1, p2) else {
558        return Ok(None);
559    };
560    melbourne_wubbena(phi1, phi2, p1, p2, f1, f2).map(Some)
561}
562
563fn gf_reason(gf: Option<f64>, prev: Option<PreviousEpoch>, threshold_m: f64) -> bool {
564    match (gf, prev.and_then(|p| p.gf_m)) {
565        (Some(gf), Some(prev_gf)) => (gf - prev_gf).abs() > threshold_m,
566        _ => false,
567    }
568}
569
570fn mw_reason(
571    mw: Option<f64>,
572    prev: Option<PreviousEpoch>,
573    f1: f64,
574    f2: f64,
575    threshold_cycles: f64,
576) -> bool {
577    let (Some(mw), Some(prev_mw), Ok(lambda_wl)) =
578        (mw, prev.and_then(|p| p.mw_m), wide_lane_wavelength(f1, f2))
579    else {
580        return false;
581    };
582    ((mw - prev_mw).abs() / lambda_wl.abs()) > threshold_cycles
583}
584
585fn gap_reason(time_s: Option<f64>, prev: Option<PreviousEpoch>, min_arc_gap_s: f64) -> bool {
586    match (time_s, prev.and_then(|p| p.gap_time_s)) {
587        (Some(time_s), Some(prev_time_s)) => (time_s - prev_time_s).abs() > min_arc_gap_s,
588        _ => false,
589    }
590}
591
592fn loss_of_lock(ep: &ArcEpoch) -> bool {
593    lli_set(ep.lli1) || lli_set(ep.lli2)
594}
595
596fn lli_set(lli: Option<i64>) -> bool {
597    lli.is_some_and(|value| (value & 1) == 1)
598}
599
600#[derive(Debug, Clone, Copy)]
601struct HatchState {
602    p_smooth_m: f64,
603    l1_m: f64,
604    window: usize,
605}
606
607fn hatch_epoch(
608    ep: &ArcEpoch,
609    slip: &SlipResult,
610    state: Option<HatchState>,
611    cap: usize,
612) -> (SmoothCodeResult, Option<HatchState>) {
613    if slip.skipped || !band1_hatch_usable(ep) {
614        return (
615            SmoothCodeResult {
616                p_smooth_m: None,
617                window: 0,
618                reset: false,
619            },
620            None,
621        );
622    }
623
624    let (Some(f1), Some(p1), Some(phi1)) = (ep.f1_hz, ep.p1_m, ep.phi1_cycles) else {
625        unreachable!("presence checked above")
626    };
627    match phase_meters(phi1, f1) {
628        Ok(l1) => do_hatch(p1, l1, slip.slip, state, cap),
629        Err(_) => (
630            SmoothCodeResult {
631                p_smooth_m: None,
632                window: 0,
633                reset: false,
634            },
635            None,
636        ),
637    }
638}
639
640fn do_hatch(
641    p1_m: f64,
642    l1_m: f64,
643    slip: bool,
644    state: Option<HatchState>,
645    cap: usize,
646) -> (SmoothCodeResult, Option<HatchState>) {
647    if slip || state.is_none() {
648        let result = SmoothCodeResult {
649            p_smooth_m: Some(p1_m),
650            window: 1,
651            reset: state.is_some() && slip,
652        };
653        let next = HatchState {
654            p_smooth_m: p1_m,
655            l1_m,
656            window: 1,
657        };
658        return (result, Some(next));
659    }
660
661    let state = state.expect("checked above");
662    let window = (state.window + 1).min(cap);
663    let n = window as f64;
664    let p_smooth_m = p1_m / n + (n - 1.0) / n * (state.p_smooth_m + (l1_m - state.l1_m));
665    let result = SmoothCodeResult {
666        p_smooth_m: Some(p_smooth_m),
667        window,
668        reset: false,
669    };
670    let next = HatchState {
671        p_smooth_m,
672        l1_m,
673        window,
674    };
675    (result, Some(next))
676}
677
678#[derive(Debug, Clone, Copy)]
679struct IonoFreeHatchState {
680    p_smooth_m: f64,
681    l_if_m: f64,
682    window: usize,
683}
684
685fn iono_free_hatch_epoch(
686    ep: &ArcEpoch,
687    slip: &SlipResult,
688    state: Option<IonoFreeHatchState>,
689    cap: usize,
690) -> Result<(IonoFreeSmoothResult, Option<IonoFreeHatchState>), CarrierPhaseError> {
691    let Some((p_if_m, l_if_m)) = current_iono_free_code_phase(ep)? else {
692        return Ok((
693            IonoFreeSmoothResult {
694                p_smooth_m: None,
695                p_if_m: None,
696                l_if_m: None,
697                window: 0,
698                reset: false,
699            },
700            None,
701        ));
702    };
703
704    Ok(do_iono_free_hatch(p_if_m, l_if_m, slip.slip, state, cap))
705}
706
707fn current_iono_free_code_phase(ep: &ArcEpoch) -> Result<Option<(f64, f64)>, CarrierPhaseError> {
708    let (Some(f1), Some(f2), Some(p1), Some(p2), Some(phi1), Some(phi2)) = (
709        ep.f1_hz,
710        ep.f2_hz,
711        ep.p1_m,
712        ep.p2_m,
713        ep.phi1_cycles,
714        ep.phi2_cycles,
715    ) else {
716        return Ok(None);
717    };
718
719    let p_if = combinations::ionosphere_free(p1, p2, f1, f2).map_err(map_combination_error)?;
720    let l_if = combinations::ionosphere_free_phase_cycles(phi1, phi2, f1, f2)
721        .map_err(map_combination_error)?;
722    Ok(Some((p_if, l_if)))
723}
724
725fn map_combination_error(error: combinations::IonosphereFreeError) -> CarrierPhaseError {
726    match error {
727        combinations::IonosphereFreeError::EqualFrequencies => CarrierPhaseError::EqualFrequencies,
728        combinations::IonosphereFreeError::InvalidFrequency => CarrierPhaseError::InvalidFrequency,
729        combinations::IonosphereFreeError::InvalidObservation => {
730            CarrierPhaseError::InvalidObservation
731        }
732        combinations::IonosphereFreeError::UnknownSystem(_)
733        | combinations::IonosphereFreeError::UnknownBand { .. } => {
734            CarrierPhaseError::InvalidFrequency
735        }
736    }
737}
738
739fn validate_smooth_code_result(result: SmoothCodeResult) -> Result<(), CarrierPhaseError> {
740    validate_optional_observation(result.p_smooth_m, "p_smooth_m")
741}
742
743fn validate_iono_free_smooth_result(result: IonoFreeSmoothResult) -> Result<(), CarrierPhaseError> {
744    validate_optional_observation(result.p_smooth_m, "p_smooth_m")?;
745    validate_optional_observation(result.p_if_m, "p_if_m")?;
746    validate_optional_observation(result.l_if_m, "l_if_m")
747}
748
749fn do_iono_free_hatch(
750    p_if_m: f64,
751    l_if_m: f64,
752    slip: bool,
753    state: Option<IonoFreeHatchState>,
754    cap: usize,
755) -> (IonoFreeSmoothResult, Option<IonoFreeHatchState>) {
756    if slip || state.is_none() {
757        let result = IonoFreeSmoothResult {
758            p_smooth_m: Some(p_if_m),
759            p_if_m: Some(p_if_m),
760            l_if_m: Some(l_if_m),
761            window: 1,
762            reset: state.is_some() && slip,
763        };
764        let next = IonoFreeHatchState {
765            p_smooth_m: p_if_m,
766            l_if_m,
767            window: 1,
768        };
769        return (result, Some(next));
770    }
771
772    let state = state.expect("checked above");
773    let window = (state.window + 1).min(cap);
774    let n = window as f64;
775    let p_smooth_m = p_if_m / n + (n - 1.0) / n * (state.p_smooth_m + (l_if_m - state.l_if_m));
776    let result = IonoFreeSmoothResult {
777        p_smooth_m: Some(p_smooth_m),
778        p_if_m: Some(p_if_m),
779        l_if_m: Some(l_if_m),
780        window,
781        reset: false,
782    };
783    let next = IonoFreeHatchState {
784        p_smooth_m,
785        l_if_m,
786        window,
787    };
788    (result, Some(next))
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    fn f(bits: u64) -> f64 {
796        f64::from_bits(bits)
797    }
798
799    fn b(value: Option<f64>) -> Option<u64> {
800        value.map(f64::to_bits)
801    }
802
803    fn oracle_arc() -> Vec<ArcEpoch> {
804        let rows = [
805            (
806                0_u64,
807                0x419ad7697cf35157,
808                0x4194f2cad78dd8ca,
809                0x4174689c023d70a4,
810                0x4174689bfd06a506,
811                0,
812                0,
813                Some(0x41d779c018000000),
814                Some(0x41d24aec20000000),
815            ),
816            (
817                1,
818                0x419ad771514355cd,
819                0x4194f2d0f16095a4,
820                0x417468a1f420c49c,
821                0x417468a1f5c5f3ee,
822                0,
823                0,
824                Some(0x41d779c018000000),
825                Some(0x41d24aec20000000),
826            ),
827            (
828                2,
829                0x419ad779344a2977,
830                0x4194f2d716aa7f95,
831                0x417468a7f9374bc6,
832                0x417468a7f499bdb8,
833                0,
834                0,
835                Some(0x41d779c018000000),
836                Some(0x41d24aec20000000),
837            ),
838            (
839                3,
840                0x419ad7812607cc59,
841                0x4194f2dd476b96a2,
842                0x417468adffe76c8b,
843                0x417468ae036d8781,
844                0,
845                0,
846                Some(0x41d779c018000000),
847                Some(0x41d24aec20000000),
848            ),
849            (
850                4,
851                0x419ad7893e7c3e72,
852                0x4194f2e383a3dac9,
853                0x417468b41a45a1cb,
854                0x417468b41574847e,
855                0,
856                0,
857                Some(0x41d779c018000000),
858                Some(0x41d24aec20000000),
859            ),
860            (
861                5,
862                0x419ad7914da77fc0,
863                0x4194f2e9cb534c08,
864                0x417468ba38a3d70a,
865                0x417468ba3ba4773d,
866                0,
867                0,
868                Some(0x41d779c018000000),
869                Some(0x41d24aec20000000),
870            ),
871            (
872                6,
873                0x419ad7996b899045,
874                0x4194f2f01e79ea62,
875                0x417468c06ad91687,
876                0x417468c066ca2c8c,
877                0,
878                1,
879                Some(0x41d779c018000000),
880                Some(0x41d24aec20000000),
881            ),
882            (
883                7,
884                0x419ad7a198226fff,
885                0x4194f2f67d17b5d4,
886                0x417468c6a06a7ef9,
887                0x417468c6a2bcaea7,
888                0,
889                0,
890                Some(0x41d779c018000000),
891                Some(0x41d24aec20000000),
892            ),
893            (
894                8,
895                0x419ad7a9d3721ef1,
896                0x4194f2fce72cae62,
897                0x417468cce5d2f1a9,
898                0x417468cce471c01e,
899                0,
900                0,
901                None,
902                Some(0x41d24aec20000000),
903            ),
904        ];
905
906        rows.into_iter()
907            .map(|(epoch, phi1, phi2, p1, p2, lli1, lli2, f1, f2)| ArcEpoch {
908                phi1_cycles: Some(f(phi1)),
909                phi2_cycles: Some(f(phi2)),
910                p1_m: Some(f(p1)),
911                p2_m: Some(f(p2)),
912                lli1: Some(lli1),
913                lli2: Some(lli2),
914                f1_hz: f1.map(f),
915                f2_hz: f2.map(f),
916                gap_time_s: Some(epoch as f64),
917            })
918            .collect()
919    }
920
921    #[test]
922    fn scalar_combinations_match_python_oracle_bits() {
923        let f1 = f(0x41d779c018000000);
924        let f2 = f(0x41d24aec20000000);
925
926        assert_eq!(
927            phase_meters(123_456_789.25, f1).unwrap().to_bits(),
928            0x4176679b5dbb7fd0
929        );
930        assert_eq!(
931            code_minus_carrier(23_000_010.25, 123_456_789.25, f1)
932                .unwrap()
933                .to_bits(),
934            0xc11e17ae6edff400
935        );
936        assert_eq!(
937            geometry_free(100.0, 60.0).unwrap().to_bits(),
938            0x4044000000000000
939        );
940        assert_eq!(
941            wide_lane_wavelength(f1, f2).unwrap().to_bits(),
942            0x3feb94d5e5a6844d
943        );
944        assert_eq!(
945            narrow_lane_code(10.0, 12.0, f1, f2).unwrap().to_bits(),
946            0x4025c077975b8fe2
947        );
948        assert_eq!(
949            melbourne_wubbena(5.0, 3.0, 10.0, 12.0, f1, f2)
950                .unwrap()
951                .to_bits(),
952            0xc0224ddcdaa6bf58
953        );
954    }
955
956    #[test]
957    fn invalid_frequency_modes_are_tagged() {
958        let f1 = f(0x41d779c018000000);
959        let f2 = f(0x41d24aec20000000);
960        assert_eq!(
961            phase_meters(100.0, 0.0),
962            Err(CarrierPhaseError::InvalidFrequency)
963        );
964        assert_eq!(
965            phase_meters(100.0, f64::NAN),
966            Err(CarrierPhaseError::InvalidFrequency)
967        );
968        assert_eq!(
969            phase_meters(100.0, f64::INFINITY),
970            Err(CarrierPhaseError::InvalidFrequency)
971        );
972        assert_eq!(
973            wide_lane_wavelength(f1, f1),
974            Err(CarrierPhaseError::EqualFrequencies)
975        );
976        assert_eq!(
977            wide_lane_wavelength(f1, f64::INFINITY),
978            Err(CarrierPhaseError::InvalidFrequency)
979        );
980        assert_eq!(
981            narrow_lane_code(10.0, 12.0, f1, -f2),
982            Err(CarrierPhaseError::InvalidFrequency)
983        );
984        assert_eq!(
985            melbourne_wubbena(1.0, 2.0, 3.0, 4.0, f1, f1),
986            Err(CarrierPhaseError::EqualFrequencies)
987        );
988    }
989
990    #[test]
991    fn invalid_observations_and_thresholds_are_tagged() {
992        let f1 = f(0x41d779c018000000);
993        let f2 = f(0x41d24aec20000000);
994        assert_eq!(
995            phase_meters(f64::NAN, f1),
996            Err(CarrierPhaseError::InvalidObservation)
997        );
998        assert_eq!(
999            geometry_free(f64::INFINITY, 1.0),
1000            Err(CarrierPhaseError::InvalidObservation)
1001        );
1002        assert_eq!(
1003            narrow_lane_code(10.0, f64::NAN, f1, f2),
1004            Err(CarrierPhaseError::InvalidObservation)
1005        );
1006        assert_eq!(
1007            melbourne_wubbena(f64::NAN, 2.0, 3.0, 4.0, f1, f2),
1008            Err(CarrierPhaseError::InvalidObservation)
1009        );
1010        assert_eq!(
1011            wide_lane_cycles(1.0, 2.0, f64::INFINITY, 4.0, f1, f2),
1012            Err(CarrierPhaseError::InvalidObservation)
1013        );
1014
1015        let options = CycleSlipOptions {
1016            gf_threshold_m: f64::NAN,
1017            ..CycleSlipOptions::default()
1018        };
1019        assert_eq!(
1020            detect_cycle_slips(&oracle_arc(), options),
1021            Err(CarrierPhaseError::InvalidThreshold)
1022        );
1023
1024        let mut arc = oracle_arc();
1025        arc[0].p1_m = Some(f64::NAN);
1026        assert_eq!(
1027            smooth_code(&arc, CycleSlipOptions::default(), 100),
1028            Err(CarrierPhaseError::InvalidObservation)
1029        );
1030    }
1031
1032    #[test]
1033    fn cycle_slip_classification_matches_python_oracle_bits() {
1034        let actual = detect_cycle_slips(&oracle_arc(), CycleSlipOptions::default())
1035            .expect("valid cycle-slip arc");
1036        let expected = [
1037            (
1038                false,
1039                vec![],
1040                Some(0xc0e07fd931e60e00),
1041                Some(0xc0f7618a9fb55c00),
1042                false,
1043            ),
1044            (
1045                false,
1046                vec![],
1047                Some(0xc0e07fd93c7f8a00),
1048                Some(0xc0f76189f4fdf000),
1049                false,
1050            ),
1051            (
1052                false,
1053                vec![],
1054                Some(0xc0e07fd947190400),
1055                Some(0xc0f7618b8b4d9c00),
1056                false,
1057            ),
1058            (
1059                false,
1060                vec![],
1061                Some(0xc0e07fd951b28000),
1062                Some(0xc0f76189d67f0600),
1063                false,
1064            ),
1065            (
1066                true,
1067                vec![SlipReason::GeometryFree, SlipReason::MelbourneWubbena],
1068                Some(0xc0e07fb4d2fb7200),
1069                Some(0xc0f76136a660be00),
1070                false,
1071            ),
1072            (
1073                false,
1074                vec![],
1075                Some(0xc0e07fb4dd94ec00),
1076                Some(0xc0f76136155f9c00),
1077                false,
1078            ),
1079            (
1080                true,
1081                vec![SlipReason::Lli],
1082                Some(0xc0e07fb4e82e6800),
1083                Some(0xc0f76137a3e94c00),
1084                false,
1085            ),
1086            (
1087                false,
1088                vec![],
1089                Some(0xc0e07fb4f2c7e200),
1090                Some(0xc0f761373e423d00),
1091                false,
1092            ),
1093            (false, vec![], None, None, true),
1094        ];
1095
1096        for (got, (slip, reasons, gf, mw, skipped)) in actual.iter().zip(expected) {
1097            assert_eq!(got.slip, slip);
1098            assert_eq!(got.reasons, reasons);
1099            assert_eq!(b(got.gf_m), gf);
1100            assert_eq!(b(got.mw_m), mw);
1101            assert_eq!(got.skipped, skipped);
1102        }
1103    }
1104
1105    #[test]
1106    fn data_gap_uses_previous_usable_epoch() {
1107        let mut arc = oracle_arc();
1108        arc.truncate(3);
1109        arc[1].f1_hz = None;
1110        arc[1].gap_time_s = Some(1_000.0);
1111        arc[2].gap_time_s = Some(30.0);
1112
1113        let actual =
1114            detect_cycle_slips(&arc, CycleSlipOptions::default()).expect("valid cycle-slip arc");
1115        assert!(actual[1].skipped);
1116        assert!(!actual[2].reasons.contains(&SlipReason::DataGap));
1117
1118        arc[2].gap_time_s = Some(301.0);
1119        let actual =
1120            detect_cycle_slips(&arc, CycleSlipOptions::default()).expect("valid cycle-slip arc");
1121        assert!(actual[2].reasons.contains(&SlipReason::DataGap));
1122    }
1123
1124    #[test]
1125    fn unusable_dual_frequency_row_does_not_hide_later_data_gap() {
1126        let mut arc = oracle_arc();
1127        arc.truncate(3);
1128        arc[0].gap_time_s = Some(0.0);
1129        arc[1].phi1_cycles = None;
1130        arc[1].phi2_cycles = None;
1131        arc[1].p1_m = None;
1132        arc[1].p2_m = None;
1133        arc[1].gap_time_s = Some(100.0);
1134        arc[2].gap_time_s = Some(350.0);
1135
1136        let actual =
1137            detect_cycle_slips(&arc, CycleSlipOptions::default()).expect("valid cycle-slip arc");
1138        assert!(!actual[1].skipped);
1139        assert_eq!(actual[1].gf_m, None);
1140        assert_eq!(actual[1].mw_m, None);
1141        assert!(actual[2].reasons.contains(&SlipReason::DataGap));
1142    }
1143
1144    #[test]
1145    fn hatch_smoothing_matches_python_oracle_bits() {
1146        let actual =
1147            smooth_code(&oracle_arc(), CycleSlipOptions::default(), 100).expect("valid smoothing");
1148        let expected = [
1149            (Some(0x4174689c023d70a4), 1, false),
1150            (Some(0x417468a1f6000000), 2, false),
1151            (Some(0x417468a7f7a06d39), 3, false),
1152            (Some(0x417468ae02b851eb), 4, false),
1153            (Some(0x417468b41a45a1cb), 1, true),
1154            (Some(0x417468ba3aac0831), 2, false),
1155            (Some(0x417468c06ad91687), 1, true),
1156            (Some(0x417468c6a20c49ba), 2, false),
1157            (None, 0, false),
1158        ];
1159
1160        for (got, (p_smooth, window, reset)) in actual.iter().zip(expected) {
1161            assert_eq!(b(got.p_smooth_m), p_smooth);
1162            assert_eq!(got.window, window);
1163            assert_eq!(got.reset, reset);
1164        }
1165    }
1166
1167    #[test]
1168    fn hatch_smoothing_accepts_l1_only_arc() {
1169        let arc: Vec<_> = [(0.0, 10.0, 100.0), (30.0, 12.0, 101.0), (60.0, 15.0, 103.0)]
1170            .into_iter()
1171            .map(|(gap_time_s, p1_m, phi1_cycles)| ArcEpoch {
1172                phi1_cycles: Some(phi1_cycles),
1173                phi2_cycles: None,
1174                p1_m: Some(p1_m),
1175                p2_m: None,
1176                lli1: Some(0),
1177                lli2: None,
1178                f1_hz: Some(C_M_S),
1179                f2_hz: None,
1180                gap_time_s: Some(gap_time_s),
1181            })
1182            .collect();
1183
1184        let actual =
1185            smooth_code(&arc, CycleSlipOptions::default(), 100).expect("valid L1-only smoothing");
1186        let expected = [
1187            (Some(10.0), 1, false),
1188            (Some(11.5), 2, false),
1189            (Some(14.0), 3, false),
1190        ];
1191
1192        for (got, (p_smooth_m, window, reset)) in actual.iter().zip(expected) {
1193            assert_eq!(got.p_smooth_m, p_smooth_m);
1194            assert_eq!(got.window, window);
1195            assert_eq!(got.reset, reset);
1196        }
1197    }
1198
1199    #[test]
1200    fn hatch_smoothing_keeps_band1_state_across_sparse_l2_arc() {
1201        let f1 = C_M_S;
1202        let f2 = C_M_S / 2.0;
1203        let rows = [
1204            (0.0, 10.0, 100.0, Some(10.0), Some(50.0), Some(f2)),
1205            (30.0, 12.0, 101.0, None, None, None),
1206            (60.0, 15.0, 103.0, Some(15.0), Some(51.5), Some(f2)),
1207        ];
1208        let arc: Vec<_> = rows
1209            .into_iter()
1210            .map(
1211                |(gap_time_s, p1_m, phi1_cycles, p2_m, phi2_cycles, f2_hz)| ArcEpoch {
1212                    phi1_cycles: Some(phi1_cycles),
1213                    phi2_cycles,
1214                    p1_m: Some(p1_m),
1215                    p2_m,
1216                    lli1: Some(0),
1217                    lli2: Some(0),
1218                    f1_hz: Some(f1),
1219                    f2_hz,
1220                    gap_time_s: Some(gap_time_s),
1221                },
1222            )
1223            .collect();
1224
1225        let actual =
1226            smooth_code(&arc, CycleSlipOptions::default(), 100).expect("valid mixed smoothing");
1227        let expected = [
1228            (Some(10.0), 1, false),
1229            (Some(11.5), 2, false),
1230            (Some(14.0), 3, false),
1231        ];
1232
1233        for (got, (p_smooth_m, window, reset)) in actual.iter().zip(expected) {
1234            assert_eq!(got.p_smooth_m, p_smooth_m);
1235            assert_eq!(got.window, window);
1236            assert_eq!(got.reset, reset);
1237        }
1238    }
1239
1240    #[test]
1241    fn hatch_window_cap_zero_clamps_to_minimum() {
1242        let arc = oracle_arc();
1243
1244        let single_zero =
1245            smooth_code(&arc, CycleSlipOptions::default(), 0).expect("valid smoothing");
1246        let single_one =
1247            smooth_code(&arc, CycleSlipOptions::default(), 1).expect("valid smoothing");
1248        assert_eq!(single_zero, single_one);
1249        for result in &single_zero {
1250            if let Some(p_smooth_m) = result.p_smooth_m {
1251                assert!(p_smooth_m.is_finite());
1252            }
1253            assert!(result.window <= MIN_HATCH_WINDOW_CAP);
1254        }
1255
1256        let if_zero =
1257            smooth_iono_free_code(&arc, CycleSlipOptions::default(), 0).expect("valid smoothing");
1258        let if_one =
1259            smooth_iono_free_code(&arc, CycleSlipOptions::default(), 1).expect("valid smoothing");
1260        assert_eq!(if_zero, if_one);
1261        for result in &if_zero {
1262            for value in [result.p_smooth_m, result.p_if_m, result.l_if_m]
1263                .into_iter()
1264                .flatten()
1265            {
1266                assert!(value.is_finite());
1267            }
1268            assert!(result.window <= MIN_HATCH_WINDOW_CAP);
1269        }
1270    }
1271
1272    #[test]
1273    fn ionosphere_free_hatch_smoothing_matches_python_oracle_bits() {
1274        let actual = smooth_iono_free_code(&oracle_arc(), CycleSlipOptions::default(), 100)
1275            .expect("valid smoothing");
1276        let expected = [
1277            (
1278                Some(0x4174689c0a4cab98),
1279                Some(0x4174689c0a4cab98),
1280                Some(0x41746197d93b3cb8),
1281                1,
1282                false,
1283            ),
1284            (
1285                Some(0x417468a1f8be0026),
1286                Some(0x417468a1f195bb1b),
1287                Some(0x4174619dced4d652),
1288                2,
1289                false,
1290            ),
1291            (
1292                Some(0x417468a7fbcfc0cc),
1293                Some(0x417468a80059a882),
1294                Some(0x417461a3cfa1a31e),
1295                3,
1296                false,
1297            ),
1298            (
1299                Some(0x417468ae0479118a),
1300                Some(0x417468adfa7503c6),
1301                Some(0x417461a9dba1a31e),
1302                4,
1303                false,
1304            ),
1305            (
1306                Some(0x417468b421b7b0f7),
1307                Some(0x417468b421b7b0f7),
1308                Some(0x417461b021565566),
1309                1,
1310                true,
1311            ),
1312            (
1313                Some(0x417468ba3c0eec2c),
1314                Some(0x417468ba33ffc0f9),
1315                Some(0x417461b643bcbbce),
1316                2,
1317                false,
1318            ),
1319            (
1320                Some(0x417468c0711ef759),
1321                Some(0x417468c0711ef759),
1322                Some(0x417461bc71565568),
1323                1,
1324                true,
1325            ),
1326            (
1327                Some(0x417468c6a35fe7ef),
1328                Some(0x417468c69cd40bb8),
1329                Some(0x417461c2aa232235),
1330                2,
1331                false,
1332            ),
1333            (None, None, None, 0, false),
1334        ];
1335
1336        for (got, (p_smooth, p_if, l_if, window, reset)) in actual.iter().zip(expected) {
1337            assert_eq!(b(got.p_smooth_m), p_smooth);
1338            assert_eq!(b(got.p_if_m), p_if);
1339            assert_eq!(b(got.l_if_m), l_if);
1340            assert_eq!(got.window, window);
1341            assert_eq!(got.reset, reset);
1342        }
1343    }
1344}