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