1use crate::combinations;
9use crate::constants::C_M_S;
10use crate::tolerances::FREQUENCY_DENOMINATOR_EPS_HZ;
11use crate::validate;
12
13pub use crate::tolerances::FREQUENCY_DENOMINATOR_EPS_HZ as FREQ_EPSILON_HZ;
16
17pub const DEFAULT_GF_THRESHOLD_M: f64 = 0.05;
19
20pub const DEFAULT_MW_THRESHOLD_CYCLES: f64 = 4.0;
22
23pub const DEFAULT_MIN_ARC_GAP_S: f64 = 300.0;
25
26pub(crate) const MIN_HATCH_WINDOW_CAP: usize = 1;
27
28pub const DEFAULT_HATCH_WINDOW_CAP: usize = 100;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub(crate) struct InvalidHatchWindowCap;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum CarrierPhaseError {
45 EqualFrequencies,
47 InvalidFrequency,
49 InvalidObservation,
51 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#[derive(Debug, Clone, Copy, PartialEq)]
70pub struct ArcEpoch {
71 pub phi1_cycles: Option<f64>,
73 pub phi2_cycles: Option<f64>,
75 pub p1_m: Option<f64>,
77 pub p2_m: Option<f64>,
79 pub lli1: Option<i64>,
81 pub lli2: Option<i64>,
83 pub f1_hz: Option<f64>,
85 pub f2_hz: Option<f64>,
87 pub gap_time_s: Option<f64>,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct CycleSlipOptions {
94 pub gf_threshold_m: f64,
96 pub mw_threshold_cycles: f64,
98 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum SlipReason {
115 Lli,
117 DataGap,
119 GeometryFree,
121 MelbourneWubbena,
123}
124
125#[derive(Debug, Clone, PartialEq)]
127pub struct SlipResult {
128 pub slip: bool,
130 pub reasons: Vec<SlipReason>,
132 pub gf_m: Option<f64>,
134 pub mw_m: Option<f64>,
136 pub skipped: bool,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq)]
142pub struct SmoothCodeResult {
143 pub p_smooth_m: Option<f64>,
145 pub window: usize,
147 pub reset: bool,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq)]
153pub struct IonoFreeSmoothResult {
154 pub p_smooth_m: Option<f64>,
156 pub p_if_m: Option<f64>,
158 pub l_if_m: Option<f64>,
160 pub window: usize,
162 pub reset: bool,
164}
165
166pub 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
173pub 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
180pub 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
191pub 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
209pub 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
227pub 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
234pub 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
311pub 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
340pub 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
430pub 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}