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
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub(crate) struct InvalidHatchWindowCap;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CarrierPhaseError {
34 EqualFrequencies,
36 InvalidFrequency,
38 InvalidObservation,
40 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#[derive(Debug, Clone, Copy, PartialEq)]
59pub struct ArcEpoch {
60 pub phi1_cycles: Option<f64>,
62 pub phi2_cycles: Option<f64>,
64 pub p1_m: Option<f64>,
66 pub p2_m: Option<f64>,
68 pub lli1: Option<i64>,
70 pub lli2: Option<i64>,
72 pub f1_hz: Option<f64>,
74 pub f2_hz: Option<f64>,
76 pub gap_time_s: Option<f64>,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq)]
82pub struct CycleSlipOptions {
83 pub gf_threshold_m: f64,
85 pub mw_threshold_cycles: f64,
87 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum SlipReason {
104 Lli,
106 DataGap,
108 GeometryFree,
110 MelbourneWubbena,
112}
113
114#[derive(Debug, Clone, PartialEq)]
116pub struct SlipResult {
117 pub slip: bool,
119 pub reasons: Vec<SlipReason>,
121 pub gf_m: Option<f64>,
123 pub mw_m: Option<f64>,
125 pub skipped: bool,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq)]
131pub struct SmoothCodeResult {
132 pub p_smooth_m: Option<f64>,
134 pub window: usize,
136 pub reset: bool,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq)]
142pub struct IonoFreeSmoothResult {
143 pub p_smooth_m: Option<f64>,
145 pub p_if_m: Option<f64>,
147 pub l_if_m: Option<f64>,
149 pub window: usize,
151 pub reset: bool,
153}
154
155pub 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
162pub 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
169pub 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
180pub 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
198pub 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
216pub 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
223pub 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
300pub 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
329pub 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
419pub 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}