1use crate::astro::angles::normalize_geodetic_lon_rad;
7use crate::astro::frames::transforms::itrs_to_geodetic_compute;
8use crate::astro::math::vec3::{dot3, norm3, sub3};
9use crate::astro::time::model::{Instant, JulianDateSplit, TimeScale};
10
11use crate::ambiguity::{self, AmbiguityId, NarrowLaneParams};
12use crate::carrier_phase::{
13 detect_cycle_slips, validate_hatch_window_cap, ArcEpoch, CarrierPhaseError, CycleSlipOptions,
14 SlipReason,
15};
16use crate::combinations::{self, IonosphereFreeError};
17use crate::constants::{DEG_TO_RAD, KM_TO_M, RAD_TO_DEG};
18use crate::tropo::{tropo_slant, Met};
19use crate::validate;
20use crate::Wgs84Geodetic;
21use std::cmp::Ordering;
22use std::collections::{BTreeMap, BTreeSet};
23
24#[derive(Debug, Clone, PartialEq)]
26pub struct Observation {
27 pub satellite_id: String,
28 pub ambiguity_id: String,
29 pub code_m: f64,
30 pub phase_m: f64,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub struct CodeSmoothingObservation {
36 pub satellite_id: String,
37 pub ambiguity_id: String,
38 pub code_m: f64,
39 pub phase_m: f64,
40 pub lli: Option<i64>,
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct CodeSmoothingEpoch {
46 pub base_observations: Vec<CodeSmoothingObservation>,
47 pub rover_observations: Vec<CodeSmoothingObservation>,
48}
49
50pub type CycleSlipObservation = CodeSmoothingObservation;
52
53pub type CycleSlipEpoch = CodeSmoothingEpoch;
55
56#[derive(Debug, Clone, PartialEq)]
59pub struct DualObservation {
60 pub ambiguity_id: String,
61 pub p1_m: f64,
62 pub p2_m: f64,
63 pub phi1_cycles: f64,
64 pub phi2_cycles: f64,
65 pub f1_hz: f64,
66 pub f2_hz: f64,
67}
68
69#[derive(Debug, Clone, PartialEq)]
71pub struct DualSatelliteObservation {
72 pub satellite_id: String,
73 pub base: DualObservation,
74 pub rover: DualObservation,
75}
76
77#[derive(Debug, Clone, PartialEq)]
80pub struct DualEpoch {
81 pub observations: Vec<DualSatelliteObservation>,
82}
83
84#[derive(Debug, Clone, PartialEq)]
86pub struct DualCycleSlipObservation {
87 pub satellite_id: String,
88 pub ambiguity_id: String,
89 pub p1_m: f64,
90 pub p2_m: f64,
91 pub phi1_cycles: f64,
92 pub phi2_cycles: f64,
93 pub f1_hz: f64,
94 pub f2_hz: f64,
95 pub lli1: Option<i64>,
96 pub lli2: Option<i64>,
97}
98
99#[derive(Debug, Clone, PartialEq)]
101pub struct DualCycleSlipEpoch {
102 pub epoch_sort_key: String,
104 pub gap_time_s: Option<f64>,
106 pub base_observations: Vec<DualCycleSlipObservation>,
107 pub rover_observations: Vec<DualCycleSlipObservation>,
108}
109
110#[derive(Debug, Clone, PartialEq)]
113pub struct DualIonosphereFreeObservation {
114 pub ambiguity_id: String,
115 pub p1_m: f64,
116 pub p2_m: f64,
117 pub phi1_cycles: f64,
118 pub phi2_cycles: f64,
119 pub f1_hz: f64,
120 pub f2_hz: f64,
121 pub tropo_m: f64,
122}
123
124#[derive(Debug, Clone, PartialEq)]
126pub struct DualIonosphereFreeSatelliteObservation {
127 pub satellite_id: String,
128 pub base: DualIonosphereFreeObservation,
129 pub rover: DualIonosphereFreeObservation,
130}
131
132#[derive(Debug, Clone, PartialEq)]
134pub struct DualIonosphereFreeEpoch {
135 pub observations: Vec<DualIonosphereFreeSatelliteObservation>,
136}
137
138#[derive(Debug, Clone, PartialEq)]
144pub struct DualIonosphereFreeSetupEpoch {
145 pub jd_whole: f64,
146 pub jd_fraction: f64,
147 pub observations: Vec<DualSatelliteObservation>,
148 pub base_satellite_positions_m: BTreeMap<String, [f64; 3]>,
149 pub rover_satellite_positions_m: BTreeMap<String, [f64; 3]>,
150}
151
152#[derive(Debug, Clone, PartialEq)]
154pub struct IonosphereFreeBaselineEpoch {
155 pub epoch_index: usize,
156 pub satellite_ids: Vec<String>,
157 pub base_observations: Vec<Observation>,
158 pub rover_observations: Vec<Observation>,
159}
160
161#[derive(Debug, Clone, PartialEq)]
163pub struct IonosphereFreeBaselineResult {
164 pub epochs: Vec<IonosphereFreeBaselineEpoch>,
165 pub wavelengths_m: BTreeMap<String, f64>,
166 pub offsets_m: BTreeMap<String, f64>,
167}
168
169#[derive(Debug, Clone, PartialEq)]
171pub struct BaselineReferenceEpoch {
172 pub available_satellite_ids: Vec<String>,
174 pub satellite_positions_m: BTreeMap<String, [f64; 3]>,
176}
177
178#[derive(Debug, Clone, PartialEq)]
180pub struct ElevationMaskEpoch {
181 pub satellite_positions_m: BTreeMap<String, [f64; 3]>,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct ElevationMaskEpochResult {
188 pub kept_satellite_ids: Vec<String>,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct ElevationMaskResult {
195 pub epochs: Vec<ElevationMaskEpochResult>,
196 pub masked_satellite_ids: Vec<String>,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq)]
202pub struct WideLaneOptions {
203 pub min_epochs: usize,
204 pub tolerance_cycles: f64,
205 pub skip_short_fragments: bool,
208}
209
210#[derive(Debug, Clone, PartialEq)]
212pub enum WideLaneError {
213 InvalidInput {
214 field: &'static str,
215 reason: &'static str,
216 },
217 ReferenceSatelliteMissing(String),
218 WideLaneFailed {
219 satellite_id: String,
220 reason: CarrierPhaseError,
221 },
222 TooFewWideLaneEpochs {
223 ambiguity_id: String,
224 count: usize,
225 minimum: usize,
226 },
227 WideLaneNotInteger {
228 ambiguity_id: String,
229 mean_cycles: f64,
230 fixed_cycles: i64,
231 },
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub enum IonosphereFreeBaselineError {
237 InvalidInput {
238 field: &'static str,
239 reason: &'static str,
240 },
241 NoEpochs,
242 InconsistentFrequencies(String),
243 NarrowLaneFailed(IonosphereFreeError),
244 IonosphereFreeFailed {
245 satellite_id: String,
246 reason: IonosphereFreeError,
247 },
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum CodeSmoothingError {
253 InvalidWindowCap,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
258pub enum CycleSlipReceiver {
259 Base,
260 Rover,
261}
262
263pub use crate::ambiguity::CycleSlipPolicy;
264
265#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct CycleSlipSplitArc {
268 pub receiver: CycleSlipReceiver,
269 pub satellite_id: String,
270 pub ambiguity_id: String,
271 pub start_epoch_index: usize,
272 pub end_epoch_index: usize,
273 pub n_epochs: usize,
274}
275
276#[derive(Debug, Clone, PartialEq)]
278pub struct CycleSlipPrepResult {
279 pub epochs: Vec<CycleSlipEpoch>,
280 pub dropped_sats: Vec<String>,
281 pub split_arcs: Vec<CycleSlipSplitArc>,
282}
283
284#[derive(Debug, Clone, PartialEq)]
286pub struct DualCycleSlipPrepResult {
287 pub epochs: Vec<DualCycleSlipEpoch>,
288 pub dropped_sats: Vec<String>,
289 pub split_arcs: Vec<CycleSlipSplitArc>,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
294pub enum CycleSlipPrepError {
295 InvalidInput {
296 field: &'static str,
297 reason: &'static str,
298 },
299 CycleSlipDetected {
300 receiver: CycleSlipReceiver,
301 satellite_id: String,
302 epoch_index: usize,
303 reasons: Vec<SlipReason>,
304 },
305}
306
307#[derive(Debug, Clone, Default, PartialEq, Eq)]
309pub enum ReferenceSelection {
310 #[default]
312 Auto,
313 Satellite(String),
315 PerSystem(BTreeMap<String, String>),
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
321pub enum ReferenceReport {
322 Satellite(String),
323 PerSystem(BTreeMap<String, String>),
324}
325
326#[derive(Debug, Clone, Default, PartialEq, Eq)]
328pub enum BaselineReferenceSelection {
329 #[default]
331 Auto,
332 Satellite(String),
334 PerSystem(BTreeMap<String, String>),
336}
337
338#[derive(Debug, Clone, PartialEq)]
340pub struct DoubleDifference {
341 pub satellite_id: String,
342 pub reference_satellite_id: String,
343 pub ambiguity_id: String,
344 pub code_m: f64,
345 pub phase_m: f64,
346}
347
348#[derive(Debug, Clone, PartialEq)]
350pub struct DoubleDifferenceResult {
351 pub reference_satellite_id: ReferenceReport,
352 pub double_differences: Vec<DoubleDifference>,
353 pub dropped_sats: Vec<String>,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq)]
358pub enum DoubleDifferenceError {
359 InvalidInput {
360 field: &'static str,
361 reason: &'static str,
362 },
363 DuplicateObservation(String),
364 TooFewCommonSatellites {
365 count: usize,
366 minimum: usize,
367 },
368 NoCommonReferenceSatellite(String),
369 MissingSatellitePosition(String),
370 ReferenceSatelliteMissing(String),
371 ReferenceSatelliteSingleSystem(String),
372 ReferenceSatelliteMissingSystem(String),
373 InvalidReferenceOption,
374}
375
376#[derive(Debug, Clone)]
377struct SingleDifference {
378 satellite_id: String,
379 ambiguity_id: AmbiguityId,
380 code_m: f64,
381 phase_m: f64,
382}
383
384#[derive(Debug, Clone, Copy)]
385struct CodeSmoothingState {
386 p_smooth_m: f64,
387 phase_m: f64,
388 window: usize,
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
392struct CycleSlipEvent {
393 receiver: CycleSlipReceiver,
394 satellite_id: String,
395 epoch_index: usize,
396 reasons: Vec<SlipReason>,
397}
398
399#[derive(Debug, Clone)]
400struct DualSingleDifference {
401 satellite_id: String,
402 ambiguity_id: AmbiguityId,
403 wide_lane_cycles: f64,
404}
405
406#[derive(Debug, Clone)]
407struct WideLaneSample {
408 ambiguity_id: AmbiguityId,
409 cycles: f64,
410}
411
412#[derive(Debug, Clone, Copy)]
413struct DualTropoReceiver {
414 position_m: [f64; 3],
415 geodetic: Wgs84Geodetic,
416}
417
418#[derive(Debug, Clone, Copy)]
419struct DualTropoConfig {
420 base: DualTropoReceiver,
421 rover: DualTropoReceiver,
422}
423
424pub fn double_differences(
426 base_observations: &[Observation],
427 rover_observations: &[Observation],
428 reference: ReferenceSelection,
429) -> Result<DoubleDifferenceResult, DoubleDifferenceError> {
430 let base = observations_by_satellite(base_observations)?;
431 let rover = observations_by_satellite(rover_observations)?;
432 let (common, dropped_sats) = common_observations(&base, &rover)?;
433 let refs = reference_satellites(&common, &reference)?;
434 ensure_non_reference_satellites(&common, &refs)?;
435 let ref_set = refs.values().cloned().collect::<BTreeSet<_>>();
436
437 let mut ref_data = BTreeMap::new();
438 for (system, reference_sat) in &refs {
439 let ref_base = base.get(reference_sat).expect("reference base observation");
440 let ref_rover = rover
441 .get(reference_sat)
442 .expect("reference rover observation");
443 ref_data.insert(
444 system.clone(),
445 SingleDifference {
446 satellite_id: reference_sat.clone(),
447 ambiguity_id: single_difference_ambiguity_id(reference_sat, ref_base, ref_rover),
448 code_m: finite_double_difference_value(
449 ref_rover.code_m - ref_base.code_m,
450 "rtk single difference code_m",
451 )?,
452 phase_m: finite_double_difference_value(
453 ref_rover.phase_m - ref_base.phase_m,
454 "rtk single difference phase_m",
455 )?,
456 },
457 );
458 }
459
460 let double_differences = common
461 .iter()
462 .filter(|sat| !ref_set.contains(*sat))
463 .map(|sat| {
464 let system = satellite_system(sat);
465 let reference = ref_data
466 .get(&system)
467 .expect("reference for satellite system");
468 let base_obs = base.get(sat).expect("base observation");
469 let rover_obs = rover.get(sat).expect("rover observation");
470 let sat_sd_id = single_difference_ambiguity_id(sat, base_obs, rover_obs);
471 let code_m = finite_double_difference_value(
472 rover_obs.code_m - base_obs.code_m - reference.code_m,
473 "rtk double difference code_m",
474 )?;
475 let phase_m = finite_double_difference_value(
476 rover_obs.phase_m - base_obs.phase_m - reference.phase_m,
477 "rtk double difference phase_m",
478 )?;
479
480 Ok(DoubleDifference {
481 satellite_id: sat.clone(),
482 reference_satellite_id: reference.satellite_id.clone(),
483 ambiguity_id: double_difference_ambiguity_id(sat, &sat_sd_id, reference)
484 .into_string(),
485 code_m,
486 phase_m,
487 })
488 })
489 .collect::<Result<Vec<_>, _>>()?;
490
491 Ok(DoubleDifferenceResult {
492 reference_satellite_id: reference_report(refs),
493 double_differences,
494 dropped_sats,
495 })
496}
497
498pub fn hatch_smooth_baseline_code_epochs(
503 epochs: &[CodeSmoothingEpoch],
504 hatch_window_cap: usize,
505) -> Result<Vec<CodeSmoothingEpoch>, CodeSmoothingError> {
506 let hatch_window_cap = validate_hatch_window_cap(hatch_window_cap)
507 .map_err(|_| CodeSmoothingError::InvalidWindowCap)?;
508
509 let mut smoothed = epochs.to_vec();
510 smooth_receiver_code_epochs(&mut smoothed, Receiver::Base, hatch_window_cap);
511 smooth_receiver_code_epochs(&mut smoothed, Receiver::Rover, hatch_window_cap);
512 Ok(smoothed)
513}
514
515pub fn prepare_cycle_slip_baseline_epochs(
520 epochs: &[CycleSlipEpoch],
521 policy: CycleSlipPolicy,
522) -> Result<CycleSlipPrepResult, CycleSlipPrepError> {
523 validate_cycle_slip_baseline_epochs(epochs)?;
524 let slips = cycle_slip_events(epochs);
525
526 let (prepared, dropped_sats, split_arcs) = match (policy, slips.as_slice()) {
527 (_, []) => (epochs.to_vec(), Vec::new(), Vec::new()),
528 (CycleSlipPolicy::Error, [slip, ..]) => {
529 return Err(CycleSlipPrepError::CycleSlipDetected {
530 receiver: slip.receiver,
531 satellite_id: slip.satellite_id.clone(),
532 epoch_index: slip.epoch_index,
533 reasons: slip.reasons.clone(),
534 });
535 }
536 (CycleSlipPolicy::DropSatellite, slips) => {
537 let dropped_sats = dropped_cycle_slip_sats(slips);
538 (
539 drop_cycle_slip_satellites(epochs, &dropped_sats),
540 dropped_sats,
541 Vec::new(),
542 )
543 }
544 (CycleSlipPolicy::SplitArc, slips) => {
545 let split_sides = cycle_slip_split_sides(slips);
546 let split_epochs = split_cycle_slip_arcs(epochs, &split_sides, slips);
547 let split_arcs = cycle_slip_split_metadata(&split_epochs, &split_sides);
548 (split_epochs, Vec::new(), split_arcs)
549 }
550 };
551
552 Ok(CycleSlipPrepResult {
553 epochs: segment_reacquired_arcs(prepared),
554 dropped_sats,
555 split_arcs,
556 })
557}
558
559pub fn prepare_dual_cycle_slip_baseline_epochs(
565 epochs: &[DualCycleSlipEpoch],
566 policy: CycleSlipPolicy,
567 options: CycleSlipOptions,
568) -> Result<DualCycleSlipPrepResult, CycleSlipPrepError> {
569 validate_cycle_slip_options(options)?;
570 validate_dual_cycle_slip_baseline_epochs(epochs)?;
571 let slips = dual_cycle_slip_events(epochs, options)?;
572
573 let (prepared, dropped_sats, split_arcs) = match (policy, slips.as_slice()) {
574 (_, []) => (epochs.to_vec(), Vec::new(), Vec::new()),
575 (CycleSlipPolicy::Error, [slip, ..]) => {
576 return Err(CycleSlipPrepError::CycleSlipDetected {
577 receiver: slip.receiver,
578 satellite_id: slip.satellite_id.clone(),
579 epoch_index: slip.epoch_index,
580 reasons: slip.reasons.clone(),
581 });
582 }
583 (CycleSlipPolicy::DropSatellite, slips) => {
584 let dropped_sats = dropped_cycle_slip_sats(slips);
585 (
586 drop_dual_cycle_slip_satellites(epochs, &dropped_sats),
587 dropped_sats,
588 Vec::new(),
589 )
590 }
591 (CycleSlipPolicy::SplitArc, slips) => {
592 let split_sides = cycle_slip_split_sides(slips);
593 let split_epochs = split_dual_cycle_slip_arcs(epochs, &split_sides, slips);
594 let split_arcs = dual_cycle_slip_split_metadata(&split_epochs, &split_sides);
595 (split_epochs, Vec::new(), split_arcs)
596 }
597 };
598
599 Ok(DualCycleSlipPrepResult {
600 epochs: segment_reacquired_dual_arcs(prepared),
601 dropped_sats,
602 split_arcs,
603 })
604}
605
606pub fn baseline_reference_satellites(
613 base_m: [f64; 3],
614 epochs: &[BaselineReferenceEpoch],
615 selection: BaselineReferenceSelection,
616) -> Result<BTreeMap<String, String>, DoubleDifferenceError> {
617 validate_rtk_receiver_position(base_m)?;
618 validate_baseline_reference_positions(base_m, epochs)?;
619
620 let all_sats = baseline_all_satellites(epochs);
621 let systems = all_sats
622 .iter()
623 .map(|sat| satellite_system(sat))
624 .collect::<BTreeSet<_>>();
625 let common_by_system = baseline_common_by_system(epochs);
626
627 match selection {
628 BaselineReferenceSelection::Auto => systems
629 .into_iter()
630 .map(|system| {
631 let common = common_by_system.get(&system).cloned().unwrap_or_default();
632 if common.is_empty() {
633 Err(DoubleDifferenceError::NoCommonReferenceSatellite(system))
634 } else {
635 let system_epochs = baseline_epochs_for_system(epochs, &system);
636 let reference = highest_elevation_reference(base_m, &system_epochs, &common)?;
637 Ok((system, reference))
638 }
639 })
640 .collect(),
641 BaselineReferenceSelection::Satellite(sat) => {
642 let systems = systems.into_iter().collect::<Vec<_>>();
643 match systems.as_slice() {
644 [system] => {
645 if common_by_system
646 .get(system)
647 .is_some_and(|sats| sats.contains(&sat))
648 {
649 Ok(BTreeMap::from([(system.clone(), sat)]))
650 } else {
651 Err(DoubleDifferenceError::ReferenceSatelliteMissing(sat))
652 }
653 }
654 _ => Err(DoubleDifferenceError::ReferenceSatelliteSingleSystem(sat)),
655 }
656 }
657 BaselineReferenceSelection::PerSystem(refs) => {
658 let mut out = BTreeMap::new();
659 for system in systems {
660 let Some(sat) = refs.get(&system) else {
661 return Err(DoubleDifferenceError::ReferenceSatelliteMissingSystem(
662 system,
663 ));
664 };
665 if common_by_system
666 .get(&system)
667 .is_some_and(|sats| sats.contains(sat))
668 {
669 out.insert(system, sat.clone());
670 } else {
671 return Err(DoubleDifferenceError::ReferenceSatelliteMissing(
672 sat.clone(),
673 ));
674 }
675 }
676 Ok(out)
677 }
678 }
679}
680
681fn validate_baseline_reference_positions(
682 base_m: [f64; 3],
683 epochs: &[BaselineReferenceEpoch],
684) -> Result<(), DoubleDifferenceError> {
685 for epoch in epochs {
686 for sat in &epoch.available_satellite_ids {
687 let sat_pos = *baseline_satellite_position(epoch, sat)?;
688 validate_rtk_satellite_geometry(base_m, sat_pos)?;
689 }
690 }
691 Ok(())
692}
693
694fn baseline_satellite_position<'a>(
695 epoch: &'a BaselineReferenceEpoch,
696 sat: &str,
697) -> Result<&'a [f64; 3], DoubleDifferenceError> {
698 validate::present(
699 epoch.satellite_positions_m.get(sat),
700 "satellite_positions_m",
701 )
702 .map_err(|_| DoubleDifferenceError::MissingSatellitePosition(sat.to_string()))
703}
704
705fn validate_rtk_receiver_position(base_m: [f64; 3]) -> Result<(), DoubleDifferenceError> {
706 validate::finite_vec3(base_m, "rtk base position_m")
707 .map_err(double_difference_invalid_input)?;
708 let norm = norm3(base_m);
709 if !norm.is_finite() {
710 return Err(invalid_double_difference_input(
711 "rtk base position_m",
712 "out of range",
713 ));
714 }
715 if norm <= 0.0 {
716 return Err(invalid_double_difference_input(
717 "rtk base position_m",
718 "degenerate geometry",
719 ));
720 }
721 Ok(())
722}
723
724fn validate_rtk_satellite_geometry(
725 base_m: [f64; 3],
726 sat_pos_m: [f64; 3],
727) -> Result<(), DoubleDifferenceError> {
728 validate::finite_vec3(sat_pos_m, "rtk satellite position_m")
729 .map_err(double_difference_invalid_input)?;
730 rtk_line_of_sight(base_m, sat_pos_m).map(|_| ())
731}
732
733pub fn apply_elevation_mask(
739 base_m: [f64; 3],
740 epochs: &[ElevationMaskEpoch],
741 mask_deg: f64,
742) -> Result<ElevationMaskResult, DoubleDifferenceError> {
743 validate_rtk_receiver_position(base_m)?;
744 let mask_deg = validate::finite_in_range(mask_deg, -90.0, 90.0, "rtk elevation mask_deg")
745 .map_err(double_difference_invalid_input)?;
746 let min_sin = (mask_deg * DEG_TO_RAD).sin();
747 let up = local_up(base_m);
748 let mut masked = BTreeSet::new();
749 let mut results = Vec::with_capacity(epochs.len());
750
751 for epoch in epochs {
752 let mut kept = Vec::new();
753 for (sat, sat_pos) in &epoch.satellite_positions_m {
754 if elevation_score_with_up(base_m, up, *sat_pos)? >= min_sin {
755 kept.push(sat.clone());
756 } else {
757 masked.insert(sat.clone());
758 }
759 }
760 results.push(ElevationMaskEpochResult {
761 kept_satellite_ids: kept,
762 });
763 }
764
765 Ok(ElevationMaskResult {
766 epochs: results,
767 masked_satellite_ids: masked.into_iter().collect(),
768 })
769}
770
771pub fn estimate_wide_lane_ambiguities(
774 epochs: &[DualEpoch],
775 reference_satellite_id: &str,
776 options: WideLaneOptions,
777) -> Result<BTreeMap<String, i64>, WideLaneError> {
778 validate_wide_lane_options(options)?;
779 validate_wide_lane_epochs(epochs)?;
780 let mut samples = BTreeMap::<AmbiguityId, Vec<f64>>::new();
781
782 for epoch in epochs {
783 let reference = epoch
784 .observations
785 .iter()
786 .find(|obs| obs.satellite_id == reference_satellite_id)
787 .ok_or_else(|| {
788 WideLaneError::ReferenceSatelliteMissing(reference_satellite_id.to_string())
789 })?;
790 let reference = dual_single_difference(reference)?;
791
792 for observation in epoch
793 .observations
794 .iter()
795 .filter(|obs| obs.satellite_id != reference_satellite_id)
796 {
797 let sample = dual_wide_lane_double_difference(observation, &reference)?;
798 samples
799 .entry(sample.ambiguity_id)
800 .or_default()
801 .push(sample.cycles);
802 }
803 }
804
805 let mut fixed = BTreeMap::new();
806 for (ambiguity_id, cycles) in samples {
807 match estimate_wide_lane_integer(ambiguity_id.as_str(), &cycles, options) {
808 Ok(value) => {
809 fixed.insert(ambiguity_id.into_string(), value);
810 }
811 Err(WideLaneError::TooFewWideLaneEpochs { .. }) if options.skip_short_fragments => {}
812 Err(err) => return Err(err),
813 }
814 }
815 Ok(fixed)
816}
817
818pub fn build_ionosphere_free_baseline_epochs(
821 epochs: &[DualIonosphereFreeEpoch],
822 reference_satellite_id: &str,
823 wide_lane_cycles: &BTreeMap<String, i64>,
824) -> Result<IonosphereFreeBaselineResult, IonosphereFreeBaselineError> {
825 validate_ionosphere_free_epochs(epochs)?;
826 let params = dual_narrow_lane_params(epochs, reference_satellite_id, wide_lane_cycles)?;
827 let mut if_epochs = Vec::new();
828
829 for (epoch_index, epoch) in epochs.iter().enumerate() {
830 let keep_sats =
831 dual_ionosphere_free_keep_sats(epoch, reference_satellite_id, wide_lane_cycles);
832 if keep_sats.len() < 2 {
833 continue;
834 }
835
836 if_epochs.push(IonosphereFreeBaselineEpoch {
837 epoch_index,
838 base_observations: dual_ionosphere_free_observations(
839 epoch,
840 &keep_sats,
841 Receiver::Base,
842 )?,
843 rover_observations: dual_ionosphere_free_observations(
844 epoch,
845 &keep_sats,
846 Receiver::Rover,
847 )?,
848 satellite_ids: keep_sats,
849 });
850 }
851
852 if if_epochs.is_empty() {
853 return Err(IonosphereFreeBaselineError::NoEpochs);
854 }
855
856 Ok(IonosphereFreeBaselineResult {
857 epochs: if_epochs,
858 wavelengths_m: params
859 .iter()
860 .map(|(id, param)| (id.as_str().to_string(), param.wavelength_m))
861 .collect(),
862 offsets_m: params
863 .into_iter()
864 .map(|(id, param)| (id.into_string(), param.offset_m))
865 .collect(),
866 })
867}
868
869pub fn prepare_ionosphere_free_baseline_epochs(
877 base_m: [f64; 3],
878 initial_baseline_m: [f64; 3],
879 epochs: &[DualIonosphereFreeSetupEpoch],
880 reference_satellite_id: &str,
881 wide_lane_cycles: &BTreeMap<String, i64>,
882 apply_troposphere: bool,
883) -> Result<IonosphereFreeBaselineResult, IonosphereFreeBaselineError> {
884 validate_ionosphere_free_setup_epochs(base_m, initial_baseline_m, epochs, apply_troposphere)?;
885 let tropo = apply_troposphere
886 .then(|| dual_tropo_config(base_m, initial_baseline_m))
887 .transpose()?;
888 let if_epochs = epochs
889 .iter()
890 .map(|epoch| dual_setup_ionosphere_free_epoch(epoch, tropo.as_ref()))
891 .collect::<Result<Vec<_>, _>>()?;
892
893 build_ionosphere_free_baseline_epochs(&if_epochs, reference_satellite_id, wide_lane_cycles)
894}
895
896fn observations_by_satellite(
897 observations: &[Observation],
898) -> Result<BTreeMap<String, Observation>, DoubleDifferenceError> {
899 let mut by_sat = BTreeMap::new();
900 for observation in observations {
901 validate_double_difference_observation(observation)?;
902 if by_sat
903 .insert(observation.satellite_id.clone(), observation.clone())
904 .is_some()
905 {
906 return Err(DoubleDifferenceError::DuplicateObservation(
907 observation.satellite_id.clone(),
908 ));
909 }
910 }
911 Ok(by_sat)
912}
913
914fn validate_double_difference_observation(
915 observation: &Observation,
916) -> Result<(), DoubleDifferenceError> {
917 validate::finite(observation.code_m, "rtk observation code_m")
918 .map_err(double_difference_invalid_input)?;
919 validate::finite(observation.phase_m, "rtk observation phase_m")
920 .map_err(double_difference_invalid_input)?;
921 Ok(())
922}
923
924fn finite_double_difference_value(
925 value: f64,
926 field: &'static str,
927) -> Result<f64, DoubleDifferenceError> {
928 validate::finite(value, field).map_err(double_difference_invalid_input)
929}
930
931fn double_difference_invalid_input(error: validate::FieldError) -> DoubleDifferenceError {
932 DoubleDifferenceError::InvalidInput {
933 field: error.field(),
934 reason: error.reason(),
935 }
936}
937
938fn invalid_double_difference_input(
939 field: &'static str,
940 reason: &'static str,
941) -> DoubleDifferenceError {
942 DoubleDifferenceError::InvalidInput { field, reason }
943}
944
945fn common_observations(
946 base: &BTreeMap<String, Observation>,
947 rover: &BTreeMap<String, Observation>,
948) -> Result<(Vec<String>, Vec<String>), DoubleDifferenceError> {
949 let base_sats = base.keys().cloned().collect::<BTreeSet<_>>();
950 let rover_sats = rover.keys().cloned().collect::<BTreeSet<_>>();
951 let common = base_sats
952 .intersection(&rover_sats)
953 .cloned()
954 .collect::<Vec<_>>();
955
956 if common.len() < 2 {
957 return Err(DoubleDifferenceError::TooFewCommonSatellites {
958 count: common.len(),
959 minimum: 2,
960 });
961 }
962
963 let common_set = common.iter().cloned().collect::<BTreeSet<_>>();
964 let dropped = base_sats
965 .union(&rover_sats)
966 .filter(|sat| !common_set.contains(*sat))
967 .cloned()
968 .collect();
969 Ok((common, dropped))
970}
971
972fn reference_satellites(
973 common: &[String],
974 selection: &ReferenceSelection,
975) -> Result<BTreeMap<String, String>, DoubleDifferenceError> {
976 let mut common_by_system = BTreeMap::<String, Vec<String>>::new();
977 for sat in common {
978 common_by_system
979 .entry(satellite_system(sat))
980 .or_default()
981 .push(sat.clone());
982 }
983 let systems = common_by_system.keys().cloned().collect::<Vec<_>>();
984
985 match selection {
986 ReferenceSelection::Auto => Ok(common_by_system
987 .into_iter()
988 .map(|(system, sats)| (system, sats[0].clone()))
989 .collect()),
990 ReferenceSelection::Satellite(sat) => match systems.as_slice() {
991 [system] => {
992 if common_by_system
993 .get(system)
994 .is_some_and(|sats| sats.contains(sat))
995 {
996 Ok(BTreeMap::from([(system.clone(), sat.clone())]))
997 } else {
998 Err(DoubleDifferenceError::ReferenceSatelliteMissing(
999 sat.clone(),
1000 ))
1001 }
1002 }
1003 _ => Err(DoubleDifferenceError::ReferenceSatelliteSingleSystem(
1004 sat.clone(),
1005 )),
1006 },
1007 ReferenceSelection::PerSystem(refs) => {
1008 let mut out = BTreeMap::new();
1009 for system in systems {
1010 let Some(sat) = refs.get(&system) else {
1011 return Err(DoubleDifferenceError::ReferenceSatelliteMissingSystem(
1012 system,
1013 ));
1014 };
1015 if common_by_system
1016 .get(&system)
1017 .is_some_and(|sats| sats.contains(sat))
1018 {
1019 out.insert(system, sat.clone());
1020 } else {
1021 return Err(DoubleDifferenceError::ReferenceSatelliteMissing(
1022 sat.clone(),
1023 ));
1024 }
1025 }
1026 Ok(out)
1027 }
1028 }
1029}
1030
1031fn ensure_non_reference_satellites(
1032 common: &[String],
1033 refs: &BTreeMap<String, String>,
1034) -> Result<(), DoubleDifferenceError> {
1035 for (system, reference_sat) in refs {
1036 let common_count = common
1037 .iter()
1038 .filter(|sat| satellite_system(sat) == system.as_str())
1039 .count();
1040 let non_reference_count = common
1041 .iter()
1042 .filter(|sat| {
1043 satellite_system(sat) == system.as_str() && sat.as_str() != reference_sat.as_str()
1044 })
1045 .count();
1046 if non_reference_count == 0 {
1047 return Err(DoubleDifferenceError::TooFewCommonSatellites {
1048 count: common_count,
1049 minimum: 2,
1050 });
1051 }
1052 }
1053 Ok(())
1054}
1055
1056fn baseline_all_satellites(epochs: &[BaselineReferenceEpoch]) -> Vec<String> {
1057 epochs
1058 .iter()
1059 .flat_map(|epoch| epoch.available_satellite_ids.iter().cloned())
1060 .collect::<BTreeSet<_>>()
1061 .into_iter()
1062 .collect()
1063}
1064
1065fn baseline_common_by_system(
1066 epochs: &[BaselineReferenceEpoch],
1067) -> BTreeMap<String, BTreeSet<String>> {
1068 let mut common_by_system = BTreeMap::<String, BTreeSet<String>>::new();
1069
1070 for epoch in epochs {
1071 let mut sats_by_system = BTreeMap::<String, BTreeSet<String>>::new();
1072 for sat in &epoch.available_satellite_ids {
1073 sats_by_system
1074 .entry(satellite_system(sat))
1075 .or_default()
1076 .insert(sat.clone());
1077 }
1078
1079 for (system, sats) in sats_by_system {
1080 common_by_system
1081 .entry(system)
1082 .and_modify(|common| {
1083 *common = common.intersection(&sats).cloned().collect();
1084 })
1085 .or_insert(sats);
1086 }
1087 }
1088
1089 common_by_system
1090}
1091
1092fn baseline_epochs_for_system<'a>(
1093 epochs: &'a [BaselineReferenceEpoch],
1094 system: &str,
1095) -> Vec<&'a BaselineReferenceEpoch> {
1096 epochs
1097 .iter()
1098 .filter(|epoch| {
1099 epoch
1100 .available_satellite_ids
1101 .iter()
1102 .any(|sat| satellite_system(sat) == system)
1103 })
1104 .collect()
1105}
1106
1107fn highest_elevation_reference(
1108 base_m: [f64; 3],
1109 epochs: &[&BaselineReferenceEpoch],
1110 common: &BTreeSet<String>,
1111) -> Result<String, DoubleDifferenceError> {
1112 let mut scores = common
1113 .iter()
1114 .map(|sat| average_elevation_score(base_m, epochs, sat).map(|score| (sat.clone(), score)))
1115 .collect::<Result<Vec<_>, _>>()?;
1116
1117 scores.sort_by(|(sat_a, score_a), (sat_b, score_b)| {
1118 score_b
1119 .partial_cmp(score_a)
1120 .unwrap_or(Ordering::Equal)
1121 .then_with(|| sat_a.cmp(sat_b))
1122 });
1123
1124 Ok(scores[0].0.clone())
1125}
1126
1127fn average_elevation_score(
1128 base_m: [f64; 3],
1129 epochs: &[&BaselineReferenceEpoch],
1130 sat: &str,
1131) -> Result<f64, DoubleDifferenceError> {
1132 let up = local_up(base_m);
1133 let mut sum = 0.0;
1134
1135 for epoch in epochs {
1136 let sat_pos = *baseline_satellite_position(epoch, sat)?;
1137 sum += elevation_score_with_up(base_m, up, sat_pos)?;
1138 }
1139
1140 Ok(sum / epochs.len() as f64)
1141}
1142
1143fn elevation_score_with_up(
1144 base_m: [f64; 3],
1145 up: [f64; 3],
1146 sat_pos_m: [f64; 3],
1147) -> Result<f64, DoubleDifferenceError> {
1148 validate::finite_vec3(up, "rtk local up").map_err(double_difference_invalid_input)?;
1149 let (los, n) = rtk_line_of_sight(base_m, sat_pos_m)?;
1150 let inv = 1.0 / n;
1151 let los = [los[0] * inv, los[1] * inv, los[2] * inv];
1152 let score = dot3(los, up);
1153 validate_elevation_score(score)
1154}
1155
1156fn rtk_line_of_sight(
1157 base_m: [f64; 3],
1158 sat_pos_m: [f64; 3],
1159) -> Result<([f64; 3], f64), DoubleDifferenceError> {
1160 validate::finite_vec3(base_m, "rtk base position_m")
1161 .map_err(double_difference_invalid_input)?;
1162 validate::finite_vec3(sat_pos_m, "rtk satellite position_m")
1163 .map_err(double_difference_invalid_input)?;
1164 let los = sub3(sat_pos_m, base_m);
1165 validate::finite_vec3(los, "rtk line of sight_m").map_err(double_difference_invalid_input)?;
1166 let n = norm3(los);
1167 if !n.is_finite() {
1168 return Err(invalid_double_difference_input(
1169 "rtk line of sight range_m",
1170 "out of range",
1171 ));
1172 }
1173 if n <= 0.0 {
1174 return Err(invalid_double_difference_input(
1175 "rtk line of sight_m",
1176 "degenerate geometry",
1177 ));
1178 }
1179 Ok((los, n))
1180}
1181
1182fn validate_elevation_score(score: f64) -> Result<f64, DoubleDifferenceError> {
1183 validate::finite(score, "rtk elevation score").map_err(double_difference_invalid_input)?;
1184 if !(-1.0 - 1.0e-12..=1.0 + 1.0e-12).contains(&score) {
1185 return Err(invalid_double_difference_input(
1186 "rtk elevation score",
1187 "out of range",
1188 ));
1189 }
1190 Ok(score.clamp(-1.0, 1.0))
1191}
1192
1193fn local_up(base_m: [f64; 3]) -> [f64; 3] {
1194 crate::estimation::substrate::frames::local_up(
1195 crate::estimation::recipe::FrameRecipe::GeocentricUpRtkReference,
1196 base_m,
1197 )
1198}
1199
1200fn validate_wide_lane_options(options: WideLaneOptions) -> Result<(), WideLaneError> {
1201 if options.min_epochs == 0 {
1202 return Err(invalid_wide_lane_input(
1203 "rtk wide lane min_epochs",
1204 "not positive",
1205 ));
1206 }
1207 validate::finite_positive(options.tolerance_cycles, "rtk wide lane tolerance_cycles")
1208 .map_err(wide_lane_invalid_input)?;
1209 Ok(())
1210}
1211
1212fn validate_wide_lane_epochs(epochs: &[DualEpoch]) -> Result<(), WideLaneError> {
1213 for epoch in epochs {
1214 for observation in &epoch.observations {
1215 validate_wide_lane_observation(&observation.base)?;
1216 validate_wide_lane_observation(&observation.rover)?;
1217 }
1218 }
1219 Ok(())
1220}
1221
1222fn validate_wide_lane_observation(observation: &DualObservation) -> Result<(), WideLaneError> {
1223 validate::finite(observation.p1_m, "rtk wide lane p1_m").map_err(wide_lane_invalid_input)?;
1224 validate::finite(observation.p2_m, "rtk wide lane p2_m").map_err(wide_lane_invalid_input)?;
1225 validate::finite(observation.phi1_cycles, "rtk wide lane phi1_cycles")
1226 .map_err(wide_lane_invalid_input)?;
1227 validate::finite(observation.phi2_cycles, "rtk wide lane phi2_cycles")
1228 .map_err(wide_lane_invalid_input)?;
1229 validate::finite_positive(observation.f1_hz, "rtk wide lane f1_hz")
1230 .map_err(wide_lane_invalid_input)?;
1231 validate::finite_positive(observation.f2_hz, "rtk wide lane f2_hz")
1232 .map_err(wide_lane_invalid_input)?;
1233 Ok(())
1234}
1235
1236fn finite_wide_lane_value(value: f64, field: &'static str) -> Result<f64, WideLaneError> {
1237 validate::finite(value, field).map_err(wide_lane_invalid_input)
1238}
1239
1240fn wide_lane_invalid_input(error: validate::FieldError) -> WideLaneError {
1241 WideLaneError::InvalidInput {
1242 field: error.field(),
1243 reason: error.reason(),
1244 }
1245}
1246
1247fn invalid_wide_lane_input(field: &'static str, reason: &'static str) -> WideLaneError {
1248 WideLaneError::InvalidInput { field, reason }
1249}
1250
1251fn dual_single_difference(
1252 observation: &DualSatelliteObservation,
1253) -> Result<DualSingleDifference, WideLaneError> {
1254 let rover_wide_lane = dual_observation_wide_lane_cycles(&observation.rover).map_err(|err| {
1255 WideLaneError::WideLaneFailed {
1256 satellite_id: observation.satellite_id.clone(),
1257 reason: err,
1258 }
1259 })?;
1260 let base_wide_lane = dual_observation_wide_lane_cycles(&observation.base).map_err(|err| {
1261 WideLaneError::WideLaneFailed {
1262 satellite_id: observation.satellite_id.clone(),
1263 reason: err,
1264 }
1265 })?;
1266
1267 let wide_lane_cycles = finite_wide_lane_value(
1268 rover_wide_lane - base_wide_lane,
1269 "rtk wide lane single difference cycles",
1270 )?;
1271
1272 Ok(DualSingleDifference {
1273 satellite_id: observation.satellite_id.clone(),
1274 ambiguity_id: single_difference_dual_ambiguity_id(
1275 &observation.satellite_id,
1276 &observation.base,
1277 &observation.rover,
1278 ),
1279 wide_lane_cycles,
1280 })
1281}
1282
1283fn dual_wide_lane_double_difference(
1284 observation: &DualSatelliteObservation,
1285 reference: &DualSingleDifference,
1286) -> Result<WideLaneSample, WideLaneError> {
1287 let sd = dual_single_difference(observation)?;
1288 let cycles = finite_wide_lane_value(
1289 sd.wide_lane_cycles - reference.wide_lane_cycles,
1290 "rtk wide lane double difference cycles",
1291 )?;
1292 Ok(WideLaneSample {
1293 ambiguity_id: double_difference_dual_ambiguity_id(
1294 &observation.satellite_id,
1295 &sd.ambiguity_id,
1296 reference,
1297 ),
1298 cycles,
1299 })
1300}
1301
1302fn dual_observation_wide_lane_cycles(
1303 observation: &DualObservation,
1304) -> Result<f64, CarrierPhaseError> {
1305 crate::carrier_phase::wide_lane_cycles(
1306 observation.phi1_cycles,
1307 observation.phi2_cycles,
1308 observation.p1_m,
1309 observation.p2_m,
1310 observation.f1_hz,
1311 observation.f2_hz,
1312 )
1313}
1314
1315fn estimate_wide_lane_integer(
1316 ambiguity_id: &str,
1317 cycles: &[f64],
1318 options: WideLaneOptions,
1319) -> Result<i64, WideLaneError> {
1320 ambiguity::estimate_wide_lane_integer(cycles, options.min_epochs, options.tolerance_cycles)
1321 .map_err(|err| match err {
1322 ambiguity::WideLaneEstimateError::TooFewEpochs { count, minimum } => {
1323 WideLaneError::TooFewWideLaneEpochs {
1324 ambiguity_id: ambiguity_id.to_string(),
1325 count,
1326 minimum,
1327 }
1328 }
1329 ambiguity::WideLaneEstimateError::NotInteger {
1330 mean_cycles,
1331 fixed_cycles,
1332 } => WideLaneError::WideLaneNotInteger {
1333 ambiguity_id: ambiguity_id.to_string(),
1334 mean_cycles,
1335 fixed_cycles,
1336 },
1337 })
1338}
1339
1340fn validate_ionosphere_free_epochs(
1341 epochs: &[DualIonosphereFreeEpoch],
1342) -> Result<(), IonosphereFreeBaselineError> {
1343 for epoch in epochs {
1344 for observation in &epoch.observations {
1345 validate_ionosphere_free_observation(&observation.base)?;
1346 validate_ionosphere_free_observation(&observation.rover)?;
1347 }
1348 }
1349 Ok(())
1350}
1351
1352fn validate_ionosphere_free_observation(
1353 observation: &DualIonosphereFreeObservation,
1354) -> Result<(), IonosphereFreeBaselineError> {
1355 validate::finite(observation.p1_m, "rtk if p1_m").map_err(ionosphere_free_invalid_input)?;
1356 validate::finite(observation.p2_m, "rtk if p2_m").map_err(ionosphere_free_invalid_input)?;
1357 validate::finite(observation.phi1_cycles, "rtk if phi1_cycles")
1358 .map_err(ionosphere_free_invalid_input)?;
1359 validate::finite(observation.phi2_cycles, "rtk if phi2_cycles")
1360 .map_err(ionosphere_free_invalid_input)?;
1361 validate::finite_positive(observation.f1_hz, "rtk if f1_hz")
1362 .map_err(ionosphere_free_invalid_input)?;
1363 validate::finite_positive(observation.f2_hz, "rtk if f2_hz")
1364 .map_err(ionosphere_free_invalid_input)?;
1365 validate::finite(observation.tropo_m, "rtk if tropo_m")
1366 .map_err(ionosphere_free_invalid_input)?;
1367 Ok(())
1368}
1369
1370fn validate_ionosphere_free_setup_epochs(
1371 base_m: [f64; 3],
1372 initial_baseline_m: [f64; 3],
1373 epochs: &[DualIonosphereFreeSetupEpoch],
1374 apply_troposphere: bool,
1375) -> Result<(), IonosphereFreeBaselineError> {
1376 validate_tropo_receiver_position(base_m, "rtk tropo base position_m")?;
1377 validate::finite_vec3(initial_baseline_m, "rtk tropo initial_baseline_m")
1378 .map_err(ionosphere_free_invalid_input)?;
1379 let rover_m = [
1380 base_m[0] + initial_baseline_m[0],
1381 base_m[1] + initial_baseline_m[1],
1382 base_m[2] + initial_baseline_m[2],
1383 ];
1384 validate_tropo_receiver_position(rover_m, "rtk tropo rover position_m")?;
1385
1386 for epoch in epochs {
1387 validate::finite(epoch.jd_whole, "rtk if setup jd_whole")
1388 .map_err(ionosphere_free_invalid_input)?;
1389 validate::finite_in_range(epoch.jd_fraction, -1.0, 1.0, "rtk if setup jd_fraction")
1390 .map_err(ionosphere_free_invalid_input)?;
1391 for observation in &epoch.observations {
1392 validate_setup_dual_observation(&observation.base)?;
1393 validate_setup_dual_observation(&observation.rover)?;
1394 if apply_troposphere {
1395 let base_sat = *validate::present(
1396 epoch
1397 .base_satellite_positions_m
1398 .get(&observation.satellite_id),
1399 "rtk tropo base satellite position_m",
1400 )
1401 .map_err(ionosphere_free_invalid_input)?;
1402 let rover_sat = *validate::present(
1403 epoch
1404 .rover_satellite_positions_m
1405 .get(&observation.satellite_id),
1406 "rtk tropo rover satellite position_m",
1407 )
1408 .map_err(ionosphere_free_invalid_input)?;
1409 validate_tropo_satellite_geometry(
1410 base_m,
1411 base_sat,
1412 "rtk tropo base satellite position_m",
1413 )?;
1414 validate_tropo_satellite_geometry(
1415 rover_m,
1416 rover_sat,
1417 "rtk tropo rover satellite position_m",
1418 )?;
1419 }
1420 }
1421 }
1422 Ok(())
1423}
1424
1425fn validate_setup_dual_observation(
1426 observation: &DualObservation,
1427) -> Result<(), IonosphereFreeBaselineError> {
1428 validate::finite(observation.p1_m, "rtk if setup p1_m")
1429 .map_err(ionosphere_free_invalid_input)?;
1430 validate::finite(observation.p2_m, "rtk if setup p2_m")
1431 .map_err(ionosphere_free_invalid_input)?;
1432 validate::finite(observation.phi1_cycles, "rtk if setup phi1_cycles")
1433 .map_err(ionosphere_free_invalid_input)?;
1434 validate::finite(observation.phi2_cycles, "rtk if setup phi2_cycles")
1435 .map_err(ionosphere_free_invalid_input)?;
1436 validate::finite_positive(observation.f1_hz, "rtk if setup f1_hz")
1437 .map_err(ionosphere_free_invalid_input)?;
1438 validate::finite_positive(observation.f2_hz, "rtk if setup f2_hz")
1439 .map_err(ionosphere_free_invalid_input)?;
1440 Ok(())
1441}
1442
1443fn validate_tropo_receiver_position(
1444 position_m: [f64; 3],
1445 field: &'static str,
1446) -> Result<(), IonosphereFreeBaselineError> {
1447 validate::finite_vec3(position_m, field).map_err(ionosphere_free_invalid_input)?;
1448 let norm = norm3(position_m);
1449 if !norm.is_finite() {
1450 return Err(invalid_ionosphere_free_input(field, "out of range"));
1451 }
1452 if norm <= 0.0 {
1453 return Err(invalid_ionosphere_free_input(field, "degenerate geometry"));
1454 }
1455 Ok(())
1456}
1457
1458fn validate_tropo_satellite_geometry(
1459 receiver_m: [f64; 3],
1460 sat_pos_m: [f64; 3],
1461 field: &'static str,
1462) -> Result<(), IonosphereFreeBaselineError> {
1463 validate::finite_vec3(sat_pos_m, field).map_err(ionosphere_free_invalid_input)?;
1464 let los = sub3(sat_pos_m, receiver_m);
1465 validate::finite_vec3(los, "rtk tropo line of sight_m")
1466 .map_err(ionosphere_free_invalid_input)?;
1467 let range = norm3(los);
1468 if !range.is_finite() {
1469 return Err(invalid_ionosphere_free_input(
1470 "rtk tropo line of sight range_m",
1471 "out of range",
1472 ));
1473 }
1474 if range <= 0.0 {
1475 return Err(invalid_ionosphere_free_input(
1476 "rtk tropo line of sight_m",
1477 "degenerate geometry",
1478 ));
1479 }
1480 Ok(())
1481}
1482
1483fn finite_ionosphere_free_value(
1484 value: f64,
1485 field: &'static str,
1486) -> Result<f64, IonosphereFreeBaselineError> {
1487 validate::finite(value, field).map_err(ionosphere_free_invalid_input)
1488}
1489
1490fn ionosphere_free_invalid_input(error: validate::FieldError) -> IonosphereFreeBaselineError {
1491 IonosphereFreeBaselineError::InvalidInput {
1492 field: error.field(),
1493 reason: error.reason(),
1494 }
1495}
1496
1497fn invalid_ionosphere_free_input(
1498 field: &'static str,
1499 reason: &'static str,
1500) -> IonosphereFreeBaselineError {
1501 IonosphereFreeBaselineError::InvalidInput { field, reason }
1502}
1503
1504fn dual_narrow_lane_params(
1505 epochs: &[DualIonosphereFreeEpoch],
1506 reference_satellite_id: &str,
1507 wide_lane_cycles: &BTreeMap<String, i64>,
1508) -> Result<BTreeMap<AmbiguityId, NarrowLaneParams>, IonosphereFreeBaselineError> {
1509 let mut params = BTreeMap::new();
1510 for epoch in epochs {
1511 let Some(ref_sd) = dual_if_single_difference_ambiguity(epoch, reference_satellite_id)
1512 else {
1513 continue;
1514 };
1515 for sat in dual_if_epoch_common_sats(epoch)
1516 .into_iter()
1517 .filter(|sat| sat != reference_satellite_id)
1518 {
1519 let Some(ambiguity_id) = dual_if_wide_lane_ambiguity_id(epoch, &sat, &ref_sd) else {
1520 continue;
1521 };
1522 let Some(&wide_lane) = wide_lane_cycles.get(ambiguity_id.as_str()) else {
1523 continue;
1524 };
1525 let param = dual_narrow_lane_param_from_epoch(
1526 epoch,
1527 &sat,
1528 reference_satellite_id,
1529 ambiguity_id.as_str(),
1530 wide_lane as f64,
1531 )?;
1532 ensure_consistent_dual_narrow_lane_params(
1533 ambiguity_id.as_str(),
1534 param,
1535 params.get(&ambiguity_id).copied(),
1536 )?;
1537 params.entry(ambiguity_id).or_insert(param);
1538 }
1539 }
1540 Ok(params)
1541}
1542
1543#[derive(Debug, Clone, Copy)]
1544enum Receiver {
1545 Base,
1546 Rover,
1547}
1548
1549fn dual_narrow_lane_param_from_epoch(
1550 epoch: &DualIonosphereFreeEpoch,
1551 sat: &str,
1552 reference_satellite_id: &str,
1553 ambiguity_id: &str,
1554 wide_lane_cycles: f64,
1555) -> Result<NarrowLaneParams, IonosphereFreeBaselineError> {
1556 let sat_obs = dual_if_satellite(epoch, sat).expect("satellite from epoch common set");
1557 let ref_obs =
1558 dual_if_satellite(epoch, reference_satellite_id).expect("reference from epoch common set");
1559 ensure_same_dual_frequencies(
1560 ambiguity_id,
1561 [&sat_obs.base, &sat_obs.rover, &ref_obs.base, &ref_obs.rover],
1562 )?;
1563 dual_narrow_lane_param(sat_obs.base.f1_hz, sat_obs.base.f2_hz, wide_lane_cycles)
1564}
1565
1566fn ensure_same_dual_frequencies(
1567 ambiguity_id: &str,
1568 observations: [&DualIonosphereFreeObservation; 4],
1569) -> Result<(), IonosphereFreeBaselineError> {
1570 let first = observations[0];
1571 if observations[1..].iter().all(|obs| {
1572 ambiguity::frequencies_match(obs.f1_hz, first.f1_hz)
1573 && ambiguity::frequencies_match(obs.f2_hz, first.f2_hz)
1574 }) {
1575 Ok(())
1576 } else {
1577 Err(IonosphereFreeBaselineError::InconsistentFrequencies(
1578 ambiguity_id.to_string(),
1579 ))
1580 }
1581}
1582
1583fn dual_narrow_lane_param(
1584 f1_hz: f64,
1585 f2_hz: f64,
1586 wide_lane_cycles: f64,
1587) -> Result<NarrowLaneParams, IonosphereFreeBaselineError> {
1588 ambiguity::narrow_lane_params(f1_hz, f2_hz, wide_lane_cycles)
1589 .map_err(IonosphereFreeBaselineError::NarrowLaneFailed)
1590}
1591
1592fn ensure_consistent_dual_narrow_lane_params(
1593 ambiguity_id: &str,
1594 params: NarrowLaneParams,
1595 prev: Option<NarrowLaneParams>,
1596) -> Result<(), IonosphereFreeBaselineError> {
1597 if prev.is_none_or(|prev| {
1598 ambiguity::frequencies_match(params.f1_hz, prev.f1_hz)
1599 && ambiguity::frequencies_match(params.f2_hz, prev.f2_hz)
1600 }) {
1601 Ok(())
1602 } else {
1603 Err(IonosphereFreeBaselineError::InconsistentFrequencies(
1604 ambiguity_id.to_string(),
1605 ))
1606 }
1607}
1608
1609fn dual_ionosphere_free_keep_sats(
1610 epoch: &DualIonosphereFreeEpoch,
1611 reference_satellite_id: &str,
1612 wide_lane_cycles: &BTreeMap<String, i64>,
1613) -> Vec<String> {
1614 let common = dual_if_epoch_common_sats(epoch);
1615 if !common.iter().any(|sat| sat == reference_satellite_id) {
1616 return Vec::new();
1617 }
1618 let Some(ref_sd) = dual_if_single_difference_ambiguity(epoch, reference_satellite_id) else {
1619 return Vec::new();
1620 };
1621 let kept_nonrefs = common
1622 .into_iter()
1623 .filter(|sat| sat != reference_satellite_id)
1624 .filter(|sat| {
1625 dual_if_wide_lane_ambiguity_id(epoch, sat, &ref_sd)
1626 .is_some_and(|id| wide_lane_cycles.contains_key(id.as_str()))
1627 })
1628 .collect::<Vec<_>>();
1629 if kept_nonrefs.is_empty() {
1630 Vec::new()
1631 } else {
1632 let mut out = Vec::with_capacity(kept_nonrefs.len() + 1);
1633 out.push(reference_satellite_id.to_string());
1634 out.extend(kept_nonrefs);
1635 out
1636 }
1637}
1638
1639fn dual_ionosphere_free_observations(
1640 epoch: &DualIonosphereFreeEpoch,
1641 keep_sats: &[String],
1642 receiver: Receiver,
1643) -> Result<Vec<Observation>, IonosphereFreeBaselineError> {
1644 let keep = keep_sats.iter().collect::<BTreeSet<_>>();
1645 let mut out = Vec::new();
1646 for sat_obs in &epoch.observations {
1647 if keep.contains(&sat_obs.satellite_id) {
1648 let obs = match receiver {
1649 Receiver::Base => &sat_obs.base,
1650 Receiver::Rover => &sat_obs.rover,
1651 };
1652 out.push(dual_ionosphere_free_observation(
1653 &sat_obs.satellite_id,
1654 obs,
1655 )?);
1656 }
1657 }
1658 Ok(out)
1659}
1660
1661fn dual_ionosphere_free_observation(
1662 satellite_id: &str,
1663 obs: &DualIonosphereFreeObservation,
1664) -> Result<Observation, IonosphereFreeBaselineError> {
1665 let code_m = combinations::ionosphere_free(obs.p1_m, obs.p2_m, obs.f1_hz, obs.f2_hz).map_err(
1666 |reason| IonosphereFreeBaselineError::IonosphereFreeFailed {
1667 satellite_id: satellite_id.to_string(),
1668 reason,
1669 },
1670 )?;
1671 let phase_m = combinations::ionosphere_free_phase_cycles(
1672 obs.phi1_cycles,
1673 obs.phi2_cycles,
1674 obs.f1_hz,
1675 obs.f2_hz,
1676 )
1677 .map_err(|reason| IonosphereFreeBaselineError::IonosphereFreeFailed {
1678 satellite_id: satellite_id.to_string(),
1679 reason,
1680 })?;
1681 let code_m = finite_ionosphere_free_value(code_m - obs.tropo_m, "rtk if code_m")?;
1682 let phase_m = finite_ionosphere_free_value(phase_m - obs.tropo_m, "rtk if phase_m")?;
1683 Ok(Observation {
1684 satellite_id: satellite_id.to_string(),
1685 ambiguity_id: obs.ambiguity_id.clone(),
1686 code_m,
1687 phase_m,
1688 })
1689}
1690
1691fn dual_setup_ionosphere_free_epoch(
1692 epoch: &DualIonosphereFreeSetupEpoch,
1693 tropo: Option<&DualTropoConfig>,
1694) -> Result<DualIonosphereFreeEpoch, IonosphereFreeBaselineError> {
1695 let observations = epoch
1696 .observations
1697 .iter()
1698 .map(|obs| {
1699 let base_tropo_m = dual_slant_tropo_m(
1700 tropo,
1701 Receiver::Base,
1702 epoch
1703 .base_satellite_positions_m
1704 .get(&obs.satellite_id)
1705 .copied(),
1706 epoch.jd_whole,
1707 epoch.jd_fraction,
1708 )?;
1709 let rover_tropo_m = dual_slant_tropo_m(
1710 tropo,
1711 Receiver::Rover,
1712 epoch
1713 .rover_satellite_positions_m
1714 .get(&obs.satellite_id)
1715 .copied(),
1716 epoch.jd_whole,
1717 epoch.jd_fraction,
1718 )?;
1719
1720 Ok(DualIonosphereFreeSatelliteObservation {
1721 satellite_id: obs.satellite_id.clone(),
1722 base: dual_if_observation_from_dual(&obs.base, base_tropo_m),
1723 rover: dual_if_observation_from_dual(&obs.rover, rover_tropo_m),
1724 })
1725 })
1726 .collect::<Result<Vec<_>, IonosphereFreeBaselineError>>()?;
1727 Ok(DualIonosphereFreeEpoch { observations })
1728}
1729
1730fn dual_if_observation_from_dual(
1731 obs: &DualObservation,
1732 tropo_m: f64,
1733) -> DualIonosphereFreeObservation {
1734 DualIonosphereFreeObservation {
1735 ambiguity_id: obs.ambiguity_id.clone(),
1736 p1_m: obs.p1_m,
1737 p2_m: obs.p2_m,
1738 phi1_cycles: obs.phi1_cycles,
1739 phi2_cycles: obs.phi2_cycles,
1740 f1_hz: obs.f1_hz,
1741 f2_hz: obs.f2_hz,
1742 tropo_m,
1743 }
1744}
1745
1746fn dual_slant_tropo_m(
1747 tropo: Option<&DualTropoConfig>,
1748 receiver: Receiver,
1749 sat_pos_m: Option<[f64; 3]>,
1750 jd_whole: f64,
1751 jd_fraction: f64,
1752) -> Result<f64, IonosphereFreeBaselineError> {
1753 let (Some(tropo), Some(sat_pos_m)) = (tropo, sat_pos_m) else {
1754 return Ok(0.0);
1755 };
1756 let receiver = match receiver {
1757 Receiver::Base => tropo.base,
1758 Receiver::Rover => tropo.rover,
1759 };
1760 let elevation_rad = geodetic_elevation_rad(receiver.geodetic, receiver.position_m, sat_pos_m);
1761 let met = Met::standard(receiver.geodetic.height_m, 0.0)
1762 .map_err(map_ionosphere_free_setup_tropo_error)?;
1763 let split = JulianDateSplit::new(jd_whole, jd_fraction)
1764 .map_err(map_ionosphere_free_setup_julian_split_error)?;
1765 let epoch = Instant::from_julian_date(TimeScale::Gpst, split);
1766 tropo_slant(elevation_rad, receiver.geodetic, met, epoch)
1767 .map_err(map_ionosphere_free_setup_tropo_error)
1768}
1769
1770fn map_ionosphere_free_setup_julian_split_error(
1771 error: crate::astro::time::model::TimeModelError,
1772) -> IonosphereFreeBaselineError {
1773 let crate::astro::time::model::TimeModelError::InvalidInput { field, reason } = error;
1774 let field = match field {
1775 "jd_whole" => "rtk if setup jd_whole",
1776 "fraction" => "rtk if setup jd_fraction",
1777 _ => "rtk if setup JulianDateSplit",
1778 };
1779 invalid_ionosphere_free_input(field, reason)
1780}
1781
1782fn map_ionosphere_free_setup_tropo_error(
1783 error: crate::error::Error,
1784) -> IonosphereFreeBaselineError {
1785 match error {
1786 crate::error::Error::InvalidInput(message) => {
1787 let (field, reason) = map_ionosphere_free_setup_tropo_message(&message);
1788 invalid_ionosphere_free_input(field, reason)
1789 }
1790 _ => invalid_ionosphere_free_input("rtk if setup tropo", "invalid input"),
1791 }
1792}
1793
1794fn map_ionosphere_free_setup_tropo_message(message: &str) -> (&'static str, &'static str) {
1795 match message {
1796 "height_m not finite" => ("rtk tropo receiver height_m", "not finite"),
1797 "relative_humidity not finite" => ("rtk tropo relative_humidity", "not finite"),
1798 "relative_humidity out of range" => ("rtk tropo relative_humidity", "out of range"),
1799 "elevation_rad not finite" => ("rtk tropo elevation_rad", "not finite"),
1800 "elevation_rad out of range" => ("rtk tropo elevation_rad", "out of range"),
1801 "receiver.lat_rad not finite" => ("rtk tropo receiver.lat_rad", "not finite"),
1802 "receiver.lat_rad out of range" => ("rtk tropo receiver.lat_rad", "out of range"),
1803 "receiver.lon_rad not finite" => ("rtk tropo receiver.lon_rad", "not finite"),
1804 "receiver.lon_rad out of range" => ("rtk tropo receiver.lon_rad", "out of range"),
1805 "receiver.height_m not finite" => ("rtk tropo receiver.height_m", "not finite"),
1806 "receiver.height_m out of range" => ("rtk tropo receiver.height_m", "out of range"),
1807 "pressure_hpa not finite" => ("rtk tropo pressure_hpa", "not finite"),
1808 "pressure_hpa not positive" => ("rtk tropo pressure_hpa", "not positive"),
1809 "temperature_k not finite" => ("rtk tropo temperature_k", "not finite"),
1810 "temperature_k not positive" => ("rtk tropo temperature_k", "not positive"),
1811 "epoch.jd_whole not finite" => ("rtk if setup jd_whole", "not finite"),
1812 "epoch.fraction not finite" => ("rtk if setup jd_fraction", "not finite"),
1813 "epoch.fraction out of range" => ("rtk if setup jd_fraction", "out of range"),
1814 _ => ("rtk if setup tropo", "invalid input"),
1815 }
1816}
1817
1818fn dual_tropo_config(
1819 base_m: [f64; 3],
1820 initial_baseline_m: [f64; 3],
1821) -> Result<DualTropoConfig, IonosphereFreeBaselineError> {
1822 let rover_m = [
1823 base_m[0] + initial_baseline_m[0],
1824 base_m[1] + initial_baseline_m[1],
1825 base_m[2] + initial_baseline_m[2],
1826 ];
1827 Ok(DualTropoConfig {
1828 base: DualTropoReceiver {
1829 position_m: base_m,
1830 geodetic: receiver_geodetic_for_rtk_tropo(base_m, "rtk tropo base position_m")?,
1831 },
1832 rover: DualTropoReceiver {
1833 position_m: rover_m,
1834 geodetic: receiver_geodetic_for_rtk_tropo(rover_m, "rtk tropo rover position_m")?,
1835 },
1836 })
1837}
1838
1839fn receiver_geodetic_for_rtk_tropo(
1840 position_m: [f64; 3],
1841 field: &'static str,
1842) -> Result<Wgs84Geodetic, IonosphereFreeBaselineError> {
1843 let (lat_deg, lon_deg, height_km) = itrs_to_geodetic_compute(
1844 position_m[0] / KM_TO_M,
1845 position_m[1] / KM_TO_M,
1846 position_m[2] / KM_TO_M,
1847 )
1848 .map_err(|_| invalid_ionosphere_free_input(field, "invalid geodetic"))?;
1849 Wgs84Geodetic::new(
1850 lat_deg * DEG_TO_RAD,
1851 normalize_geodetic_lon_rad(lon_deg * DEG_TO_RAD),
1852 (height_km * KM_TO_M).max(0.0),
1853 )
1854 .map_err(|_| invalid_ionosphere_free_input(field, "invalid geodetic"))
1855}
1856
1857fn geodetic_elevation_rad(
1858 geodetic: Wgs84Geodetic,
1859 receiver_m: [f64; 3],
1860 sat_pos_m: [f64; 3],
1861) -> f64 {
1862 let dx = sat_pos_m[0] - receiver_m[0];
1863 let dy = sat_pos_m[1] - receiver_m[1];
1864 let dz = sat_pos_m[2] - receiver_m[2];
1865 let range = (dx * dx + dy * dy + dz * dz).sqrt();
1866
1867 if range <= 0.0 {
1868 0.0
1869 } else {
1870 let lat = geodetic.lat_rad;
1871 let lon = geodetic.lon_rad;
1872 let u = lat.cos() * lon.cos() * dx + lat.cos() * lon.sin() * dy + lat.sin() * dz;
1873 let elevation_deg = (u / range).clamp(-1.0, 1.0).asin() * RAD_TO_DEG;
1874 elevation_deg * DEG_TO_RAD
1875 }
1876}
1877
1878fn dual_if_satellite<'a>(
1879 epoch: &'a DualIonosphereFreeEpoch,
1880 sat: &str,
1881) -> Option<&'a DualIonosphereFreeSatelliteObservation> {
1882 epoch
1883 .observations
1884 .iter()
1885 .find(|obs| obs.satellite_id == sat)
1886}
1887
1888fn dual_if_epoch_common_sats(epoch: &DualIonosphereFreeEpoch) -> Vec<String> {
1889 epoch
1890 .observations
1891 .iter()
1892 .map(|obs| obs.satellite_id.clone())
1893 .collect::<BTreeSet<_>>()
1894 .into_iter()
1895 .collect()
1896}
1897
1898fn dual_if_single_difference_ambiguity(
1899 epoch: &DualIonosphereFreeEpoch,
1900 sat: &str,
1901) -> Option<DualSingleDifference> {
1902 let obs = dual_if_satellite(epoch, sat)?;
1903 Some(DualSingleDifference {
1904 satellite_id: sat.to_string(),
1905 ambiguity_id: single_difference_if_ambiguity_id(sat, &obs.base, &obs.rover),
1906 wide_lane_cycles: 0.0,
1907 })
1908}
1909
1910fn dual_if_wide_lane_ambiguity_id(
1911 epoch: &DualIonosphereFreeEpoch,
1912 sat: &str,
1913 ref_sd: &DualSingleDifference,
1914) -> Option<AmbiguityId> {
1915 let obs = dual_if_satellite(epoch, sat)?;
1916 let sat_sd_id = single_difference_if_ambiguity_id(sat, &obs.base, &obs.rover);
1917 Some(double_difference_dual_ambiguity_id(sat, &sat_sd_id, ref_sd))
1918}
1919
1920fn single_difference_if_ambiguity_id(
1921 sat: &str,
1922 base_obs: &DualIonosphereFreeObservation,
1923 rover_obs: &DualIonosphereFreeObservation,
1924) -> AmbiguityId {
1925 let token = match (
1926 base_obs.ambiguity_id.as_str(),
1927 rover_obs.ambiguity_id.as_str(),
1928 ) {
1929 (base_id, rover_id) if base_id == sat && rover_id == sat => sat.to_string(),
1930 (base_id, rover_id) if base_id == sat => rover_id.to_string(),
1931 (base_id, rover_id) if rover_id == sat => base_id.to_string(),
1932 (base_id, rover_id) if base_id == rover_id => base_id.to_string(),
1933 (base_id, rover_id) => format!("{sat}:base={base_id},rover={rover_id}"),
1934 };
1935 AmbiguityId::new(token)
1936}
1937
1938fn reference_report(refs: BTreeMap<String, String>) -> ReferenceReport {
1939 if refs.len() == 1 {
1940 ReferenceReport::Satellite(refs.into_values().next().expect("single reference"))
1941 } else {
1942 ReferenceReport::PerSystem(refs)
1943 }
1944}
1945
1946fn smooth_receiver_code_epochs(
1947 epochs: &mut [CodeSmoothingEpoch],
1948 receiver: Receiver,
1949 hatch_window_cap: usize,
1950) {
1951 let mut states = BTreeMap::<String, CodeSmoothingState>::new();
1952
1953 for epoch in epochs {
1954 let observations = match receiver {
1955 Receiver::Base => &mut epoch.base_observations,
1956 Receiver::Rover => &mut epoch.rover_observations,
1957 };
1958 observations.sort_by(|a, b| a.satellite_id.cmp(&b.satellite_id));
1959
1960 for observation in observations {
1961 let state = states.get(&observation.ambiguity_id).copied();
1962 let (code_m, next_state) =
1963 smooth_observation_code(observation, state, hatch_window_cap);
1964 observation.code_m = code_m;
1965 states.insert(observation.ambiguity_id.clone(), next_state);
1966 }
1967 }
1968}
1969
1970fn smooth_observation_code(
1971 observation: &CodeSmoothingObservation,
1972 state: Option<CodeSmoothingState>,
1973 hatch_window_cap: usize,
1974) -> (f64, CodeSmoothingState) {
1975 if state.is_none() || rtk_lli_set(observation.lli) {
1976 return (
1977 observation.code_m,
1978 CodeSmoothingState {
1979 p_smooth_m: observation.code_m,
1980 phase_m: observation.phase_m,
1981 window: 1,
1982 },
1983 );
1984 }
1985
1986 let state = state.expect("checked above");
1987 let window = (state.window + 1).min(hatch_window_cap);
1988 let n = window as f64;
1989 let p_smooth_m = observation.code_m / n
1990 + (n - 1.0) / n * (state.p_smooth_m + (observation.phase_m - state.phase_m));
1991 (
1992 p_smooth_m,
1993 CodeSmoothingState {
1994 p_smooth_m,
1995 phase_m: observation.phase_m,
1996 window,
1997 },
1998 )
1999}
2000
2001fn rtk_lli_set(lli: Option<i64>) -> bool {
2002 lli.is_some_and(|value| (value & 1) == 1)
2003}
2004
2005fn validate_cycle_slip_baseline_epochs(
2006 epochs: &[CycleSlipEpoch],
2007) -> Result<(), CycleSlipPrepError> {
2008 for epoch in epochs {
2009 for observation in &epoch.base_observations {
2010 validate_cycle_slip_observation(observation)?;
2011 }
2012 for observation in &epoch.rover_observations {
2013 validate_cycle_slip_observation(observation)?;
2014 }
2015 }
2016 Ok(())
2017}
2018
2019fn validate_cycle_slip_observation(
2020 observation: &CycleSlipObservation,
2021) -> Result<(), CycleSlipPrepError> {
2022 validate::finite(observation.code_m, "rtk cycle slip code_m")
2023 .map_err(cycle_slip_invalid_input)?;
2024 validate::finite(observation.phase_m, "rtk cycle slip phase_m")
2025 .map_err(cycle_slip_invalid_input)?;
2026 Ok(())
2027}
2028
2029fn validate_cycle_slip_options(options: CycleSlipOptions) -> Result<(), CycleSlipPrepError> {
2030 validate::finite_positive(options.gf_threshold_m, "rtk cycle slip gf_threshold_m")
2031 .map_err(cycle_slip_invalid_input)?;
2032 validate::finite_positive(
2033 options.mw_threshold_cycles,
2034 "rtk cycle slip mw_threshold_cycles",
2035 )
2036 .map_err(cycle_slip_invalid_input)?;
2037 validate::finite_positive(options.min_arc_gap_s, "rtk cycle slip min_arc_gap_s")
2038 .map_err(cycle_slip_invalid_input)?;
2039 Ok(())
2040}
2041
2042fn validate_dual_cycle_slip_baseline_epochs(
2043 epochs: &[DualCycleSlipEpoch],
2044) -> Result<(), CycleSlipPrepError> {
2045 for epoch in epochs {
2046 if let Some(gap_time_s) = epoch.gap_time_s {
2047 validate::finite(gap_time_s, "rtk cycle slip gap_time_s")
2048 .map_err(cycle_slip_invalid_input)?;
2049 }
2050 for observation in &epoch.base_observations {
2051 validate_dual_cycle_slip_observation(observation)?;
2052 }
2053 for observation in &epoch.rover_observations {
2054 validate_dual_cycle_slip_observation(observation)?;
2055 }
2056 }
2057 Ok(())
2058}
2059
2060fn validate_dual_cycle_slip_observation(
2061 observation: &DualCycleSlipObservation,
2062) -> Result<(), CycleSlipPrepError> {
2063 validate::finite(observation.p1_m, "rtk cycle slip p1_m").map_err(cycle_slip_invalid_input)?;
2064 validate::finite(observation.p2_m, "rtk cycle slip p2_m").map_err(cycle_slip_invalid_input)?;
2065 validate::finite(observation.phi1_cycles, "rtk cycle slip phi1_cycles")
2066 .map_err(cycle_slip_invalid_input)?;
2067 validate::finite(observation.phi2_cycles, "rtk cycle slip phi2_cycles")
2068 .map_err(cycle_slip_invalid_input)?;
2069 validate::finite_positive(observation.f1_hz, "rtk cycle slip f1_hz")
2070 .map_err(cycle_slip_invalid_input)?;
2071 validate::finite_positive(observation.f2_hz, "rtk cycle slip f2_hz")
2072 .map_err(cycle_slip_invalid_input)?;
2073 if (observation.f1_hz - observation.f2_hz).abs() < crate::carrier_phase::FREQ_EPSILON_HZ {
2074 return Err(invalid_cycle_slip_input(
2075 "rtk cycle slip frequencies_hz",
2076 "degenerate frequencies",
2077 ));
2078 }
2079 Ok(())
2080}
2081
2082fn cycle_slip_invalid_input(error: validate::FieldError) -> CycleSlipPrepError {
2083 CycleSlipPrepError::InvalidInput {
2084 field: error.field(),
2085 reason: error.reason(),
2086 }
2087}
2088
2089fn invalid_cycle_slip_input(field: &'static str, reason: &'static str) -> CycleSlipPrepError {
2090 CycleSlipPrepError::InvalidInput { field, reason }
2091}
2092
2093fn cycle_slip_events(epochs: &[CycleSlipEpoch]) -> Vec<CycleSlipEvent> {
2094 let mut events = Vec::new();
2095 for (epoch_index, epoch) in epochs.iter().enumerate() {
2096 cycle_slip_events_for_receiver(
2097 CycleSlipReceiver::Base,
2098 epoch_index,
2099 &epoch.base_observations,
2100 &mut events,
2101 );
2102 cycle_slip_events_for_receiver(
2103 CycleSlipReceiver::Rover,
2104 epoch_index,
2105 &epoch.rover_observations,
2106 &mut events,
2107 );
2108 }
2109 events
2110}
2111
2112fn cycle_slip_events_for_receiver(
2113 receiver: CycleSlipReceiver,
2114 epoch_index: usize,
2115 observations: &[CycleSlipObservation],
2116 events: &mut Vec<CycleSlipEvent>,
2117) {
2118 let mut observations = observations.iter().collect::<Vec<_>>();
2119 observations.sort_by(|a, b| a.satellite_id.cmp(&b.satellite_id));
2120
2121 for obs in observations {
2122 if rtk_lli_set(obs.lli) {
2123 events.push(CycleSlipEvent {
2124 receiver,
2125 satellite_id: obs.satellite_id.clone(),
2126 epoch_index,
2127 reasons: vec![SlipReason::Lli],
2128 });
2129 }
2130 }
2131}
2132
2133fn dual_cycle_slip_events(
2134 epochs: &[DualCycleSlipEpoch],
2135 options: CycleSlipOptions,
2136) -> Result<Vec<CycleSlipEvent>, CycleSlipPrepError> {
2137 let mut events = Vec::new();
2138 dual_cycle_slip_events_for_receiver(CycleSlipReceiver::Base, epochs, options, &mut events)?;
2139 dual_cycle_slip_events_for_receiver(CycleSlipReceiver::Rover, epochs, options, &mut events)?;
2140 Ok(events)
2141}
2142
2143#[derive(Clone, Copy)]
2144struct DualCycleSlipSample<'a> {
2145 epoch_index: usize,
2146 epoch_sort_key: &'a str,
2147 gap_time_s: Option<f64>,
2148 observation: &'a DualCycleSlipObservation,
2149}
2150
2151fn dual_cycle_slip_events_for_receiver(
2152 receiver: CycleSlipReceiver,
2153 epochs: &[DualCycleSlipEpoch],
2154 options: CycleSlipOptions,
2155 events: &mut Vec<CycleSlipEvent>,
2156) -> Result<(), CycleSlipPrepError> {
2157 let mut arcs = BTreeMap::<String, Vec<DualCycleSlipSample<'_>>>::new();
2158
2159 for (epoch_index, epoch) in epochs.iter().enumerate() {
2160 let observations = match receiver {
2161 CycleSlipReceiver::Base => &epoch.base_observations,
2162 CycleSlipReceiver::Rover => &epoch.rover_observations,
2163 };
2164
2165 for observation in observations {
2166 arcs.entry(observation.satellite_id.clone())
2167 .or_default()
2168 .push(DualCycleSlipSample {
2169 epoch_index,
2170 epoch_sort_key: &epoch.epoch_sort_key,
2171 gap_time_s: epoch.gap_time_s,
2172 observation,
2173 });
2174 }
2175 }
2176
2177 for (satellite_id, mut samples) in arcs {
2178 samples.sort_by(|a, b| a.epoch_sort_key.cmp(b.epoch_sort_key));
2179 let arc = samples
2180 .iter()
2181 .map(|sample| dual_arc_epoch(sample.observation, sample.gap_time_s))
2182 .collect::<Vec<_>>();
2183 let results = detect_cycle_slips(&arc, options).map_err(cycle_slip_detector_error)?;
2184
2185 for (sample, result) in samples.iter().zip(results) {
2186 if result.slip {
2187 events.push(CycleSlipEvent {
2188 receiver,
2189 satellite_id: satellite_id.clone(),
2190 epoch_index: sample.epoch_index,
2191 reasons: result.reasons,
2192 });
2193 }
2194 }
2195 }
2196 Ok(())
2197}
2198
2199fn cycle_slip_detector_error(error: CarrierPhaseError) -> CycleSlipPrepError {
2200 let (field, reason) = match error {
2201 CarrierPhaseError::EqualFrequencies => {
2202 ("rtk cycle slip frequencies_hz", "degenerate frequencies")
2203 }
2204 CarrierPhaseError::InvalidFrequency => ("rtk cycle slip frequency_hz", "not positive"),
2205 CarrierPhaseError::InvalidObservation => ("rtk cycle slip observation", "not finite"),
2206 CarrierPhaseError::InvalidThreshold => ("rtk cycle slip threshold", "invalid"),
2207 };
2208 invalid_cycle_slip_input(field, reason)
2209}
2210
2211fn dual_arc_epoch(observation: &DualCycleSlipObservation, gap_time_s: Option<f64>) -> ArcEpoch {
2212 ArcEpoch {
2213 phi1_cycles: Some(observation.phi1_cycles),
2214 phi2_cycles: Some(observation.phi2_cycles),
2215 p1_m: Some(observation.p1_m),
2216 p2_m: Some(observation.p2_m),
2217 lli1: observation.lli1,
2218 lli2: observation.lli2,
2219 f1_hz: Some(observation.f1_hz),
2220 f2_hz: Some(observation.f2_hz),
2221 gap_time_s,
2222 }
2223}
2224
2225fn dropped_cycle_slip_sats(slips: &[CycleSlipEvent]) -> Vec<String> {
2226 slips
2227 .iter()
2228 .map(|slip| slip.satellite_id.clone())
2229 .collect::<BTreeSet<_>>()
2230 .into_iter()
2231 .collect()
2232}
2233
2234fn drop_cycle_slip_satellites(
2235 epochs: &[CycleSlipEpoch],
2236 dropped_sats: &[String],
2237) -> Vec<CycleSlipEpoch> {
2238 let dropped = dropped_sats.iter().collect::<BTreeSet<_>>();
2239 epochs
2240 .iter()
2241 .map(|epoch| CycleSlipEpoch {
2242 base_observations: epoch
2243 .base_observations
2244 .iter()
2245 .filter(|obs| !dropped.contains(&obs.satellite_id))
2246 .cloned()
2247 .collect(),
2248 rover_observations: epoch
2249 .rover_observations
2250 .iter()
2251 .filter(|obs| !dropped.contains(&obs.satellite_id))
2252 .cloned()
2253 .collect(),
2254 })
2255 .collect()
2256}
2257
2258fn drop_dual_cycle_slip_satellites(
2259 epochs: &[DualCycleSlipEpoch],
2260 dropped_sats: &[String],
2261) -> Vec<DualCycleSlipEpoch> {
2262 let dropped = dropped_sats.iter().collect::<BTreeSet<_>>();
2263 epochs
2264 .iter()
2265 .map(|epoch| DualCycleSlipEpoch {
2266 epoch_sort_key: epoch.epoch_sort_key.clone(),
2267 gap_time_s: epoch.gap_time_s,
2268 base_observations: epoch
2269 .base_observations
2270 .iter()
2271 .filter(|obs| !dropped.contains(&obs.satellite_id))
2272 .cloned()
2273 .collect(),
2274 rover_observations: epoch
2275 .rover_observations
2276 .iter()
2277 .filter(|obs| !dropped.contains(&obs.satellite_id))
2278 .cloned()
2279 .collect(),
2280 })
2281 .collect()
2282}
2283
2284fn cycle_slip_split_sides(slips: &[CycleSlipEvent]) -> BTreeSet<(CycleSlipReceiver, String)> {
2285 slips
2286 .iter()
2287 .map(|slip| (slip.receiver, slip.satellite_id.clone()))
2288 .collect()
2289}
2290
2291fn split_cycle_slip_arcs(
2292 epochs: &[CycleSlipEpoch],
2293 split_sides: &BTreeSet<(CycleSlipReceiver, String)>,
2294 slips: &[CycleSlipEvent],
2295) -> Vec<CycleSlipEpoch> {
2296 let slip_epochs = slips
2297 .iter()
2298 .map(|slip| (slip.receiver, slip.satellite_id.clone(), slip.epoch_index))
2299 .collect::<BTreeSet<_>>();
2300 let mut split_epochs = epochs.to_vec();
2301 let mut segments = BTreeMap::<(CycleSlipReceiver, String), usize>::new();
2302
2303 for (epoch_index, epoch) in split_epochs.iter_mut().enumerate() {
2304 split_receiver_cycle_slip_arcs(
2305 CycleSlipReceiver::Base,
2306 epoch_index,
2307 &mut epoch.base_observations,
2308 split_sides,
2309 &slip_epochs,
2310 &mut segments,
2311 );
2312 split_receiver_cycle_slip_arcs(
2313 CycleSlipReceiver::Rover,
2314 epoch_index,
2315 &mut epoch.rover_observations,
2316 split_sides,
2317 &slip_epochs,
2318 &mut segments,
2319 );
2320 }
2321
2322 split_epochs
2323}
2324
2325fn split_dual_cycle_slip_arcs(
2326 epochs: &[DualCycleSlipEpoch],
2327 split_sides: &BTreeSet<(CycleSlipReceiver, String)>,
2328 slips: &[CycleSlipEvent],
2329) -> Vec<DualCycleSlipEpoch> {
2330 let slip_epochs = slips
2331 .iter()
2332 .map(|slip| (slip.receiver, slip.satellite_id.clone(), slip.epoch_index))
2333 .collect::<BTreeSet<_>>();
2334 let mut split_epochs = epochs.to_vec();
2335 let mut segments = BTreeMap::<(CycleSlipReceiver, String), usize>::new();
2336
2337 for (epoch_index, epoch) in split_epochs.iter_mut().enumerate() {
2338 split_dual_receiver_cycle_slip_arcs(
2339 CycleSlipReceiver::Base,
2340 epoch_index,
2341 &mut epoch.base_observations,
2342 split_sides,
2343 &slip_epochs,
2344 &mut segments,
2345 );
2346 split_dual_receiver_cycle_slip_arcs(
2347 CycleSlipReceiver::Rover,
2348 epoch_index,
2349 &mut epoch.rover_observations,
2350 split_sides,
2351 &slip_epochs,
2352 &mut segments,
2353 );
2354 }
2355
2356 split_epochs
2357}
2358
2359fn split_receiver_cycle_slip_arcs(
2360 receiver: CycleSlipReceiver,
2361 epoch_index: usize,
2362 observations: &mut [CycleSlipObservation],
2363 split_sides: &BTreeSet<(CycleSlipReceiver, String)>,
2364 slip_epochs: &BTreeSet<(CycleSlipReceiver, String, usize)>,
2365 segments: &mut BTreeMap<(CycleSlipReceiver, String), usize>,
2366) {
2367 observations.sort_by(|a, b| a.satellite_id.cmp(&b.satellite_id));
2368
2369 for obs in observations {
2370 let key = (receiver, obs.satellite_id.clone());
2371 if split_sides.contains(&key) {
2372 let current_segment = segments.get(&key).copied().unwrap_or(1);
2373 let segment =
2374 if slip_epochs.contains(&(receiver, obs.satellite_id.clone(), epoch_index)) {
2375 current_segment + 1
2376 } else {
2377 current_segment
2378 };
2379 obs.ambiguity_id = split_side_ambiguity_id_core(&obs.satellite_id, receiver, segment);
2380 segments.insert(key, segment);
2381 }
2382 }
2383}
2384
2385fn split_dual_receiver_cycle_slip_arcs(
2386 receiver: CycleSlipReceiver,
2387 epoch_index: usize,
2388 observations: &mut [DualCycleSlipObservation],
2389 split_sides: &BTreeSet<(CycleSlipReceiver, String)>,
2390 slip_epochs: &BTreeSet<(CycleSlipReceiver, String, usize)>,
2391 segments: &mut BTreeMap<(CycleSlipReceiver, String), usize>,
2392) {
2393 observations.sort_by(|a, b| a.satellite_id.cmp(&b.satellite_id));
2394
2395 for obs in observations {
2396 let key = (receiver, obs.satellite_id.clone());
2397 if split_sides.contains(&key) {
2398 let current_segment = segments.get(&key).copied().unwrap_or(1);
2399 let segment =
2400 if slip_epochs.contains(&(receiver, obs.satellite_id.clone(), epoch_index)) {
2401 current_segment + 1
2402 } else {
2403 current_segment
2404 };
2405 obs.ambiguity_id = split_side_ambiguity_id_core(&obs.satellite_id, receiver, segment);
2406 segments.insert(key, segment);
2407 }
2408 }
2409}
2410
2411fn cycle_slip_split_metadata(
2412 epochs: &[CycleSlipEpoch],
2413 split_sides: &BTreeSet<(CycleSlipReceiver, String)>,
2414) -> Vec<CycleSlipSplitArc> {
2415 let mut grouped = BTreeMap::<(CycleSlipReceiver, String, String), Vec<usize>>::new();
2416
2417 for (epoch_index, epoch) in epochs.iter().enumerate() {
2418 split_metadata_entries(
2419 CycleSlipReceiver::Base,
2420 epoch_index,
2421 &epoch.base_observations,
2422 split_sides,
2423 &mut grouped,
2424 );
2425 split_metadata_entries(
2426 CycleSlipReceiver::Rover,
2427 epoch_index,
2428 &epoch.rover_observations,
2429 split_sides,
2430 &mut grouped,
2431 );
2432 }
2433
2434 grouped
2435 .into_iter()
2436 .map(|((receiver, satellite_id, ambiguity_id), epoch_indices)| {
2437 let start_epoch_index = *epoch_indices
2438 .first()
2439 .expect("metadata has at least one epoch");
2440 let end_epoch_index = *epoch_indices
2441 .last()
2442 .expect("metadata has at least one epoch");
2443 CycleSlipSplitArc {
2444 receiver,
2445 satellite_id,
2446 ambiguity_id,
2447 start_epoch_index,
2448 end_epoch_index,
2449 n_epochs: epoch_indices.len(),
2450 }
2451 })
2452 .collect()
2453}
2454
2455fn dual_cycle_slip_split_metadata(
2456 epochs: &[DualCycleSlipEpoch],
2457 split_sides: &BTreeSet<(CycleSlipReceiver, String)>,
2458) -> Vec<CycleSlipSplitArc> {
2459 let mut grouped = BTreeMap::<(CycleSlipReceiver, String, String), Vec<usize>>::new();
2460
2461 for (epoch_index, epoch) in epochs.iter().enumerate() {
2462 dual_split_metadata_entries(
2463 CycleSlipReceiver::Base,
2464 epoch_index,
2465 &epoch.base_observations,
2466 split_sides,
2467 &mut grouped,
2468 );
2469 dual_split_metadata_entries(
2470 CycleSlipReceiver::Rover,
2471 epoch_index,
2472 &epoch.rover_observations,
2473 split_sides,
2474 &mut grouped,
2475 );
2476 }
2477
2478 grouped
2479 .into_iter()
2480 .map(|((receiver, satellite_id, ambiguity_id), epoch_indices)| {
2481 let start_epoch_index = *epoch_indices
2482 .first()
2483 .expect("metadata has at least one epoch");
2484 let end_epoch_index = *epoch_indices
2485 .last()
2486 .expect("metadata has at least one epoch");
2487 CycleSlipSplitArc {
2488 receiver,
2489 satellite_id,
2490 ambiguity_id,
2491 start_epoch_index,
2492 end_epoch_index,
2493 n_epochs: epoch_indices.len(),
2494 }
2495 })
2496 .collect()
2497}
2498
2499fn split_metadata_entries(
2500 receiver: CycleSlipReceiver,
2501 epoch_index: usize,
2502 observations: &[CycleSlipObservation],
2503 split_sides: &BTreeSet<(CycleSlipReceiver, String)>,
2504 grouped: &mut BTreeMap<(CycleSlipReceiver, String, String), Vec<usize>>,
2505) {
2506 let mut observations = observations.iter().collect::<Vec<_>>();
2507 observations.sort_by(|a, b| a.satellite_id.cmp(&b.satellite_id));
2508
2509 for obs in observations {
2510 if split_sides.contains(&(receiver, obs.satellite_id.clone())) {
2511 grouped
2512 .entry((receiver, obs.satellite_id.clone(), obs.ambiguity_id.clone()))
2513 .or_default()
2514 .push(epoch_index);
2515 }
2516 }
2517}
2518
2519fn dual_split_metadata_entries(
2520 receiver: CycleSlipReceiver,
2521 epoch_index: usize,
2522 observations: &[DualCycleSlipObservation],
2523 split_sides: &BTreeSet<(CycleSlipReceiver, String)>,
2524 grouped: &mut BTreeMap<(CycleSlipReceiver, String, String), Vec<usize>>,
2525) {
2526 let mut observations = observations.iter().collect::<Vec<_>>();
2527 observations.sort_by(|a, b| a.satellite_id.cmp(&b.satellite_id));
2528
2529 for obs in observations {
2530 if split_sides.contains(&(receiver, obs.satellite_id.clone())) {
2531 grouped
2532 .entry((receiver, obs.satellite_id.clone(), obs.ambiguity_id.clone()))
2533 .or_default()
2534 .push(epoch_index);
2535 }
2536 }
2537}
2538
2539fn segment_reacquired_arcs(mut epochs: Vec<CycleSlipEpoch>) -> Vec<CycleSlipEpoch> {
2540 segment_receiver_reacquisitions(&mut epochs, CycleSlipReceiver::Base);
2541 segment_receiver_reacquisitions(&mut epochs, CycleSlipReceiver::Rover);
2542 epochs
2543}
2544
2545fn segment_receiver_reacquisitions(epochs: &mut [CycleSlipEpoch], receiver: CycleSlipReceiver) {
2546 let mut present_last = BTreeSet::<String>::new();
2547 let mut arcs = BTreeMap::<String, usize>::new();
2548
2549 for epoch in epochs {
2550 let observations = match receiver {
2551 CycleSlipReceiver::Base => &mut epoch.base_observations,
2552 CycleSlipReceiver::Rover => &mut epoch.rover_observations,
2553 };
2554 observations.sort_by(|a, b| a.satellite_id.cmp(&b.satellite_id));
2555 let present = observations
2556 .iter()
2557 .map(|obs| obs.satellite_id.clone())
2558 .collect::<BTreeSet<_>>();
2559
2560 for obs in observations {
2561 let reacquired =
2562 arcs.contains_key(&obs.satellite_id) && !present_last.contains(&obs.satellite_id);
2563 let arc = arcs.get(&obs.satellite_id).copied().unwrap_or(0) + usize::from(reacquired);
2564 arcs.insert(obs.satellite_id.clone(), arc);
2565
2566 if arc > 0 {
2567 obs.ambiguity_id = reacquired_ambiguity_id_core(&obs.ambiguity_id, arc);
2568 }
2569 }
2570
2571 present_last = present;
2572 }
2573}
2574
2575fn segment_reacquired_dual_arcs(mut epochs: Vec<DualCycleSlipEpoch>) -> Vec<DualCycleSlipEpoch> {
2576 segment_dual_receiver_reacquisitions(&mut epochs, CycleSlipReceiver::Base);
2577 segment_dual_receiver_reacquisitions(&mut epochs, CycleSlipReceiver::Rover);
2578 epochs
2579}
2580
2581fn segment_dual_receiver_reacquisitions(
2582 epochs: &mut [DualCycleSlipEpoch],
2583 receiver: CycleSlipReceiver,
2584) {
2585 let mut present_last = BTreeSet::<String>::new();
2586 let mut arcs = BTreeMap::<String, usize>::new();
2587
2588 for epoch in epochs {
2589 let observations = match receiver {
2590 CycleSlipReceiver::Base => &mut epoch.base_observations,
2591 CycleSlipReceiver::Rover => &mut epoch.rover_observations,
2592 };
2593 observations.sort_by(|a, b| a.satellite_id.cmp(&b.satellite_id));
2594 let present = observations
2595 .iter()
2596 .map(|obs| obs.satellite_id.clone())
2597 .collect::<BTreeSet<_>>();
2598
2599 for obs in observations {
2600 let reacquired =
2601 arcs.contains_key(&obs.satellite_id) && !present_last.contains(&obs.satellite_id);
2602 let arc = arcs.get(&obs.satellite_id).copied().unwrap_or(0) + usize::from(reacquired);
2603 arcs.insert(obs.satellite_id.clone(), arc);
2604
2605 if arc > 0 {
2606 obs.ambiguity_id = reacquired_ambiguity_id_core(&obs.ambiguity_id, arc);
2607 }
2608 }
2609
2610 present_last = present;
2611 }
2612}
2613
2614fn split_side_ambiguity_id_core(
2615 satellite_id: &str,
2616 receiver: CycleSlipReceiver,
2617 segment: usize,
2618) -> String {
2619 format!(
2620 "{satellite_id}@{}#{segment}",
2621 cycle_slip_receiver_tag(receiver)
2622 )
2623}
2624
2625fn reacquired_ambiguity_id_core(ambiguity_id: &str, arc: usize) -> String {
2626 let base = strip_reacquired_suffix(ambiguity_id);
2627 format!("{base}~ra{arc}")
2628}
2629
2630fn strip_reacquired_suffix(ambiguity_id: &str) -> &str {
2631 if let Some(index) = ambiguity_id.rfind("~ra") {
2632 let suffix = &ambiguity_id[index + 3..];
2633 if !suffix.is_empty() && suffix.chars().all(|ch| ch.is_ascii_digit()) {
2634 return &ambiguity_id[..index];
2635 }
2636 }
2637 ambiguity_id
2638}
2639
2640fn cycle_slip_receiver_tag(receiver: CycleSlipReceiver) -> &'static str {
2641 match receiver {
2642 CycleSlipReceiver::Base => "base",
2643 CycleSlipReceiver::Rover => "rover",
2644 }
2645}
2646
2647fn satellite_system(satellite_id: &str) -> String {
2648 crate::id::constellation_letter(satellite_id).to_string()
2649}
2650
2651pub(crate) fn sd_ambiguity_token(sat: &str, base_id: &str, rover_id: &str) -> String {
2659 match (base_id, rover_id) {
2660 (base_id, rover_id) if base_id == sat && rover_id == sat => sat.to_string(),
2661 (base_id, rover_id) if base_id == sat => rover_id.to_string(),
2662 (base_id, rover_id) if rover_id == sat => base_id.to_string(),
2663 (base_id, rover_id) if base_id == rover_id => base_id.to_string(),
2664 (base_id, rover_id) => format!("{sat}:base={base_id},rover={rover_id}"),
2665 }
2666}
2667
2668pub(crate) fn dd_ambiguity_token(
2674 sat: &str,
2675 sat_sd_id: &str,
2676 ref_sd_id: &str,
2677 ref_sat: &str,
2678) -> String {
2679 if sat_sd_id == sat && ref_sd_id == ref_sat {
2680 sat.to_string()
2681 } else {
2682 format!("{sat_sd_id}|ref={ref_sd_id}")
2683 }
2684}
2685
2686fn single_difference_ambiguity_id(
2687 sat: &str,
2688 base_obs: &Observation,
2689 rover_obs: &Observation,
2690) -> AmbiguityId {
2691 AmbiguityId::new(sd_ambiguity_token(
2692 sat,
2693 base_obs.ambiguity_id.as_str(),
2694 rover_obs.ambiguity_id.as_str(),
2695 ))
2696}
2697
2698fn single_difference_dual_ambiguity_id(
2699 sat: &str,
2700 base_obs: &DualObservation,
2701 rover_obs: &DualObservation,
2702) -> AmbiguityId {
2703 AmbiguityId::new(sd_ambiguity_token(
2704 sat,
2705 base_obs.ambiguity_id.as_str(),
2706 rover_obs.ambiguity_id.as_str(),
2707 ))
2708}
2709
2710fn double_difference_ambiguity_id(
2711 sat: &str,
2712 sat_sd_id: &AmbiguityId,
2713 ref_sd: &SingleDifference,
2714) -> AmbiguityId {
2715 AmbiguityId::new(dd_ambiguity_token(
2716 sat,
2717 sat_sd_id.as_str(),
2718 ref_sd.ambiguity_id.as_str(),
2719 &ref_sd.satellite_id,
2720 ))
2721}
2722
2723fn double_difference_dual_ambiguity_id(
2724 sat: &str,
2725 sat_sd_id: &AmbiguityId,
2726 ref_sd: &DualSingleDifference,
2727) -> AmbiguityId {
2728 AmbiguityId::new(dd_ambiguity_token(
2729 sat,
2730 sat_sd_id.as_str(),
2731 ref_sd.ambiguity_id.as_str(),
2732 &ref_sd.satellite_id,
2733 ))
2734}
2735
2736#[cfg(test)]
2737mod tests {
2738 use super::*;
2739
2740 fn gps_l1_hz() -> f64 {
2741 crate::frequencies::frequency_hz(
2742 crate::GnssSystem::Gps,
2743 crate::frequencies::CarrierBand::L1,
2744 )
2745 .expect("canonical GPS L1 carrier exists")
2746 }
2747
2748 fn gps_l2_hz() -> f64 {
2749 crate::frequencies::frequency_hz(
2750 crate::GnssSystem::Gps,
2751 crate::frequencies::CarrierBand::L2,
2752 )
2753 .expect("canonical GPS L2 carrier exists")
2754 }
2755
2756 fn obs(sat: &str, code_m: f64, phase_m: f64) -> Observation {
2757 Observation {
2758 satellite_id: sat.to_string(),
2759 ambiguity_id: sat.to_string(),
2760 code_m,
2761 phase_m,
2762 }
2763 }
2764
2765 fn dual_observation(ambiguity_id: &str, wide_lane_phase_cycles: f64) -> DualObservation {
2766 DualObservation {
2767 ambiguity_id: ambiguity_id.to_string(),
2768 p1_m: 0.0,
2769 p2_m: 0.0,
2770 phi1_cycles: wide_lane_phase_cycles,
2771 phi2_cycles: 0.0,
2772 f1_hz: gps_l1_hz(),
2773 f2_hz: gps_l2_hz(),
2774 }
2775 }
2776
2777 fn dual_pair(sat: &str, base_wide_lane: f64, rover_wide_lane: f64) -> DualSatelliteObservation {
2778 DualSatelliteObservation {
2779 satellite_id: sat.to_string(),
2780 base: dual_observation(sat, base_wide_lane),
2781 rover: dual_observation(sat, rover_wide_lane),
2782 }
2783 }
2784
2785 fn split_dual_pair(
2786 sat: &str,
2787 rover_id: &str,
2788 base_wide_lane: f64,
2789 rover_wide_lane: f64,
2790 ) -> DualSatelliteObservation {
2791 DualSatelliteObservation {
2792 satellite_id: sat.to_string(),
2793 base: dual_observation(sat, base_wide_lane),
2794 rover: dual_observation(rover_id, rover_wide_lane),
2795 }
2796 }
2797
2798 fn if_observation(
2799 ambiguity_id: &str,
2800 p1_m: f64,
2801 p2_m: f64,
2802 phi1_cycles: f64,
2803 phi2_cycles: f64,
2804 tropo_m: f64,
2805 ) -> DualIonosphereFreeObservation {
2806 DualIonosphereFreeObservation {
2807 ambiguity_id: ambiguity_id.to_string(),
2808 p1_m,
2809 p2_m,
2810 phi1_cycles,
2811 phi2_cycles,
2812 f1_hz: gps_l1_hz(),
2813 f2_hz: gps_l2_hz(),
2814 tropo_m,
2815 }
2816 }
2817
2818 fn if_pair(
2819 sat: &str,
2820 base_code: f64,
2821 rover_code: f64,
2822 base_phase_cycles: f64,
2823 rover_phase_cycles: f64,
2824 ) -> DualIonosphereFreeSatelliteObservation {
2825 DualIonosphereFreeSatelliteObservation {
2826 satellite_id: sat.to_string(),
2827 base: if_observation(
2828 sat,
2829 base_code,
2830 base_code + 2.0,
2831 base_phase_cycles,
2832 base_phase_cycles - 4.0,
2833 0.25,
2834 ),
2835 rover: if_observation(
2836 sat,
2837 rover_code,
2838 rover_code + 2.5,
2839 rover_phase_cycles,
2840 rover_phase_cycles - 3.0,
2841 0.5,
2842 ),
2843 }
2844 }
2845
2846 fn arc_obs(sat: &str, ambiguity_id: &str, code_m: f64, phase_m: f64) -> Observation {
2847 Observation {
2848 satellite_id: sat.to_string(),
2849 ambiguity_id: ambiguity_id.to_string(),
2850 code_m,
2851 phase_m,
2852 }
2853 }
2854
2855 fn smooth_obs(
2856 sat: &str,
2857 ambiguity_id: &str,
2858 code_m: f64,
2859 phase_m: f64,
2860 lli: Option<i64>,
2861 ) -> CodeSmoothingObservation {
2862 CodeSmoothingObservation {
2863 satellite_id: sat.to_string(),
2864 ambiguity_id: ambiguity_id.to_string(),
2865 code_m,
2866 phase_m,
2867 lli,
2868 }
2869 }
2870
2871 fn dual_slip_obs(
2872 sat: &str,
2873 ambiguity_id: &str,
2874 phi1_cycles: f64,
2875 phi2_cycles: f64,
2876 lli1: Option<i64>,
2877 lli2: Option<i64>,
2878 ) -> DualCycleSlipObservation {
2879 DualCycleSlipObservation {
2880 satellite_id: sat.to_string(),
2881 ambiguity_id: ambiguity_id.to_string(),
2882 p1_m: 20.0,
2883 p2_m: 21.0,
2884 phi1_cycles,
2885 phi2_cycles,
2886 f1_hz: gps_l1_hz(),
2887 f2_hz: gps_l2_hz(),
2888 lli1,
2889 lli2,
2890 }
2891 }
2892
2893 fn dual_slip_epoch(
2894 epoch_sort_key: &str,
2895 gap_time_s: f64,
2896 base_observations: Vec<DualCycleSlipObservation>,
2897 rover_observations: Vec<DualCycleSlipObservation>,
2898 ) -> DualCycleSlipEpoch {
2899 DualCycleSlipEpoch {
2900 epoch_sort_key: epoch_sort_key.to_string(),
2901 gap_time_s: Some(gap_time_s),
2902 base_observations,
2903 rover_observations,
2904 }
2905 }
2906
2907 fn code_bits(observations: &[CodeSmoothingObservation]) -> Vec<(&str, u64)> {
2908 observations
2909 .iter()
2910 .map(|obs| (obs.satellite_id.as_str(), obs.code_m.to_bits()))
2911 .collect()
2912 }
2913
2914 fn ambiguity_id<'a>(
2915 observations: &'a [CycleSlipObservation],
2916 satellite_id: &str,
2917 ) -> Option<&'a str> {
2918 observations
2919 .iter()
2920 .find(|obs| obs.satellite_id == satellite_id)
2921 .map(|obs| obs.ambiguity_id.as_str())
2922 }
2923
2924 fn dual_ambiguity_id<'a>(
2925 observations: &'a [DualCycleSlipObservation],
2926 satellite_id: &str,
2927 ) -> Option<&'a str> {
2928 observations
2929 .iter()
2930 .find(|obs| obs.satellite_id == satellite_id)
2931 .map(|obs| obs.ambiguity_id.as_str())
2932 }
2933
2934 fn baseline_reference_epoch(entries: &[(&str, [f64; 3])]) -> BaselineReferenceEpoch {
2935 BaselineReferenceEpoch {
2936 available_satellite_ids: entries.iter().map(|(sat, _)| sat.to_string()).collect(),
2937 satellite_positions_m: entries
2938 .iter()
2939 .map(|(sat, pos)| (sat.to_string(), *pos))
2940 .collect(),
2941 }
2942 }
2943
2944 #[test]
2945 fn double_differences_cancel_receiver_and_common_terms() {
2946 let sats = ["G01", "G02", "G03", "G04"];
2947 let reference = "G01";
2948 let base_clock_m = 125.0;
2949 let rover_clock_m = -42.0;
2950 let base_ranges = BTreeMap::from([
2951 ("G01", 20_000.0),
2952 ("G02", 21_000.0),
2953 ("G03", 22_500.0),
2954 ("G04", 23_100.0),
2955 ]);
2956 let rover_ranges = BTreeMap::from([
2957 ("G01", 20_010.0),
2958 ("G02", 21_025.0),
2959 ("G03", 22_480.0),
2960 ("G04", 23_150.0),
2961 ]);
2962 let common_errors =
2963 BTreeMap::from([("G01", 3.25), ("G02", -12.0), ("G03", 8.5), ("G04", 1.0)]);
2964 let base_ambiguities =
2965 BTreeMap::from([("G01", 2.0), ("G02", -3.0), ("G03", 7.0), ("G04", 11.0)]);
2966 let rover_ambiguities =
2967 BTreeMap::from([("G01", 5.0), ("G02", 4.0), ("G03", 1.0), ("G04", 19.0)]);
2968
2969 let base = sats
2970 .iter()
2971 .map(|sat| {
2972 obs(
2973 sat,
2974 base_ranges[sat] + base_clock_m + common_errors[sat],
2975 base_ranges[sat] + base_clock_m + common_errors[sat] + base_ambiguities[sat],
2976 )
2977 })
2978 .collect::<Vec<_>>();
2979 let rover = sats
2980 .iter()
2981 .map(|sat| {
2982 obs(
2983 sat,
2984 rover_ranges[sat] + rover_clock_m + common_errors[sat],
2985 rover_ranges[sat] + rover_clock_m + common_errors[sat] + rover_ambiguities[sat],
2986 )
2987 })
2988 .collect::<Vec<_>>();
2989
2990 let result = double_differences(
2991 &base,
2992 &rover,
2993 ReferenceSelection::Satellite(reference.to_string()),
2994 )
2995 .unwrap();
2996
2997 assert_eq!(
2998 result.reference_satellite_id,
2999 ReferenceReport::Satellite(reference.to_string())
3000 );
3001 assert!(result.dropped_sats.is_empty());
3002
3003 let by_sat = result
3004 .double_differences
3005 .iter()
3006 .map(|dd| (dd.satellite_id.as_str(), dd))
3007 .collect::<BTreeMap<_, _>>();
3008
3009 for sat in sats.into_iter().filter(|sat| *sat != reference) {
3010 let expected_code = rover_ranges[sat]
3011 - base_ranges[sat]
3012 - (rover_ranges[reference] - base_ranges[reference]);
3013 let expected_phase = expected_code + (rover_ambiguities[sat] - base_ambiguities[sat])
3014 - (rover_ambiguities[reference] - base_ambiguities[reference]);
3015 let dd = by_sat[sat];
3016 assert_eq!(dd.reference_satellite_id, reference);
3017 assert_eq!(dd.code_m, expected_code);
3018 assert_eq!(dd.phase_m, expected_phase);
3019 }
3020 }
3021
3022 #[test]
3023 fn auto_reference_reports_dropped_satellites() {
3024 let base = vec![
3025 obs("G02", 210.0, 211.0),
3026 obs("G01", 100.0, 101.0),
3027 obs("G09", 900.0, 901.0),
3028 ];
3029 let rover = vec![
3030 obs("G02", 230.0, 233.0),
3031 obs("G01", 105.0, 108.0),
3032 obs("G10", 1000.0, 1001.0),
3033 ];
3034
3035 let result = double_differences(&base, &rover, ReferenceSelection::Auto).unwrap();
3036
3037 assert_eq!(
3038 result.reference_satellite_id,
3039 ReferenceReport::Satellite("G01".to_string())
3040 );
3041 assert_eq!(result.dropped_sats, ["G09".to_string(), "G10".to_string()]);
3042 assert_eq!(
3043 result.double_differences,
3044 vec![DoubleDifference {
3045 satellite_id: "G02".to_string(),
3046 reference_satellite_id: "G01".to_string(),
3047 ambiguity_id: "G02".to_string(),
3048 code_m: 15.0,
3049 phase_m: 15.0,
3050 }]
3051 );
3052 }
3053
3054 #[test]
3055 fn explicit_arc_ids_feed_double_difference_ambiguity_id() {
3056 let base = vec![obs("G01", 100.0, 101.0), obs("G02", 210.0, 211.0)];
3057 let rover = vec![
3058 arc_obs("G01", "G01#2", 105.0, 108.0),
3059 arc_obs("G02", "G02#2", 230.0, 233.0),
3060 ];
3061
3062 let result = double_differences(
3063 &base,
3064 &rover,
3065 ReferenceSelection::Satellite("G01".to_string()),
3066 )
3067 .unwrap();
3068
3069 assert_eq!(result.double_differences[0].ambiguity_id, "G02#2|ref=G01#2");
3070 }
3071
3072 #[test]
3073 fn multi_system_uses_reference_per_system() {
3074 let base = vec![
3075 obs("G01", 10.0, 11.0),
3076 obs("G02", 20.0, 21.0),
3077 obs("E11", 30.0, 31.0),
3078 obs("E19", 40.0, 41.0),
3079 ];
3080 let rover = vec![
3081 obs("G01", 12.0, 14.0),
3082 obs("G02", 24.0, 27.0),
3083 obs("E11", 33.0, 35.0),
3084 obs("E19", 46.0, 49.0),
3085 ];
3086 let refs = BTreeMap::from([
3087 ("E".to_string(), "E11".to_string()),
3088 ("G".to_string(), "G01".to_string()),
3089 ]);
3090
3091 let result =
3092 double_differences(&base, &rover, ReferenceSelection::PerSystem(refs.clone())).unwrap();
3093
3094 assert_eq!(
3095 result.reference_satellite_id,
3096 ReferenceReport::PerSystem(refs)
3097 );
3098 assert_eq!(
3099 result
3100 .double_differences
3101 .iter()
3102 .map(|dd| (&dd.satellite_id, &dd.reference_satellite_id))
3103 .collect::<Vec<_>>(),
3104 vec![
3105 (&"E19".to_string(), &"E11".to_string()),
3106 (&"G02".to_string(), &"G01".to_string()),
3107 ]
3108 );
3109 }
3110
3111 #[test]
3112 fn multi_system_requires_non_reference_satellite_per_system() {
3113 let base = vec![obs("G01", 10.0, 11.0), obs("E01", 30.0, 31.0)];
3114 let rover = vec![obs("G01", 12.0, 14.0), obs("E01", 33.0, 35.0)];
3115 let expected = Err(DoubleDifferenceError::TooFewCommonSatellites {
3116 count: 1,
3117 minimum: 2,
3118 });
3119
3120 assert_eq!(
3121 double_differences(&base, &rover, ReferenceSelection::Auto),
3122 expected
3123 );
3124 assert_eq!(
3125 double_differences(
3126 &base,
3127 &rover,
3128 ReferenceSelection::PerSystem(BTreeMap::from([
3129 ("E".to_string(), "E01".to_string()),
3130 ("G".to_string(), "G01".to_string()),
3131 ])),
3132 ),
3133 expected
3134 );
3135 }
3136
3137 #[test]
3138 fn baseline_auto_reference_uses_highest_average_elevation_per_system() {
3139 let base = [10.0, 0.0, 0.0];
3140 let epochs = vec![
3141 baseline_reference_epoch(&[
3142 ("G01", [20.0, 0.0, 0.0]),
3143 ("G02", [20.0, 0.0, 0.0]),
3144 ("G03", [20.0, 0.0, 0.0]),
3145 ("E01", [10.0, 10.0, 0.0]),
3146 ("E02", [20.0, 0.0, 0.0]),
3147 ]),
3148 baseline_reference_epoch(&[
3149 ("G01", [10.0, 10.0, 0.0]),
3150 ("G03", [20.0, 0.0, 0.0]),
3151 ("E01", [10.0, 10.0, 0.0]),
3152 ("E02", [20.0, 0.0, 0.0]),
3153 ]),
3154 ];
3155
3156 let refs =
3157 baseline_reference_satellites(base, &epochs, BaselineReferenceSelection::Auto).unwrap();
3158
3159 assert_eq!(
3160 refs,
3161 BTreeMap::from([
3162 ("E".to_string(), "E02".to_string()),
3163 ("G".to_string(), "G03".to_string()),
3164 ])
3165 );
3166
3167 let g_epochs = baseline_epochs_for_system(&epochs, "G");
3168 assert_eq!(
3169 average_elevation_score(base, &g_epochs, "G01")
3170 .unwrap()
3171 .to_bits(),
3172 0x3fe0_0000_0000_0000
3173 );
3174 assert_eq!(
3175 average_elevation_score(base, &g_epochs, "G03")
3176 .unwrap()
3177 .to_bits(),
3178 0x3ff0_0000_0000_0000
3179 );
3180 }
3181
3182 #[test]
3183 fn baseline_auto_reference_ties_by_satellite_id() {
3184 let base = [10.0, 0.0, 0.0];
3185 let epochs = vec![baseline_reference_epoch(&[
3186 ("G01", [20.0, 0.0, 0.0]),
3187 ("G02", [20.0, 0.0, 0.0]),
3188 ])];
3189
3190 let refs =
3191 baseline_reference_satellites(base, &epochs, BaselineReferenceSelection::Auto).unwrap();
3192
3193 assert_eq!(refs, BTreeMap::from([("G".to_string(), "G01".to_string())]));
3194 }
3195
3196 #[test]
3197 fn baseline_reference_errors_when_available_satellite_position_missing() {
3198 let mut epoch = baseline_reference_epoch(&[("G01", [20.0, 0.0, 0.0])]);
3199 epoch.available_satellite_ids.push("G02".to_string());
3200
3201 assert_eq!(
3202 baseline_reference_satellites(
3203 [10.0, 0.0, 0.0],
3204 &[epoch],
3205 BaselineReferenceSelection::Auto
3206 ),
3207 Err(DoubleDifferenceError::MissingSatellitePosition(
3208 "G02".to_string()
3209 ))
3210 );
3211 }
3212
3213 #[test]
3214 fn elevation_mask_keeps_epoch_satellites_above_threshold() {
3215 let base = [10.0, 0.0, 0.0];
3216 let up = local_up(base);
3217
3218 assert_eq!(
3219 elevation_score_with_up(base, up, [20.0, 0.0, 0.0])
3220 .unwrap()
3221 .to_bits(),
3222 0x3ff0_0000_0000_0000
3223 );
3224 assert_eq!(
3225 elevation_score_with_up(base, up, [10.0, 10.0, 0.0])
3226 .unwrap()
3227 .to_bits(),
3228 0x0000_0000_0000_0000
3229 );
3230 assert_eq!(
3231 elevation_score_with_up(base, up, [0.0, 0.0, 0.0])
3232 .unwrap()
3233 .to_bits(),
3234 0xbff0_0000_0000_0000
3235 );
3236
3237 let epochs = vec![
3238 ElevationMaskEpoch {
3239 satellite_positions_m: BTreeMap::from([
3240 ("G01".to_string(), [20.0, 0.0, 0.0]),
3241 ("G02".to_string(), [10.0, 10.0, 0.0]),
3242 ("G03".to_string(), [0.0, 0.0, 0.0]),
3243 ]),
3244 },
3245 ElevationMaskEpoch {
3246 satellite_positions_m: BTreeMap::from([
3247 ("G01".to_string(), [20.0, 0.0, 0.0]),
3248 ("G02".to_string(), [20.0, 0.0, 0.0]),
3249 ("G04".to_string(), [0.0, 0.0, 0.0]),
3250 ]),
3251 },
3252 ];
3253
3254 let result = apply_elevation_mask(base, &epochs, 30.0).unwrap();
3255
3256 assert_eq!(
3257 result.epochs,
3258 vec![
3259 ElevationMaskEpochResult {
3260 kept_satellite_ids: vec!["G01".to_string()],
3261 },
3262 ElevationMaskEpochResult {
3263 kept_satellite_ids: vec!["G01".to_string(), "G02".to_string()],
3264 },
3265 ]
3266 );
3267 assert_eq!(
3268 result.masked_satellite_ids,
3269 vec!["G02".to_string(), "G03".to_string(), "G04".to_string()]
3270 );
3271 }
3272
3273 #[test]
3274 fn baseline_reference_rejects_invalid_geometry() {
3275 let epochs = vec![baseline_reference_epoch(&[
3276 ("G01", [20.0, 0.0, 0.0]),
3277 ("G02", [30.0, 0.0, 0.0]),
3278 ])];
3279
3280 assert_eq!(
3281 baseline_reference_satellites(
3282 [f64::NAN, 0.0, 0.0],
3283 &epochs,
3284 BaselineReferenceSelection::Auto,
3285 ),
3286 Err(DoubleDifferenceError::InvalidInput {
3287 field: "rtk base position_m",
3288 reason: "not finite",
3289 })
3290 );
3291 assert_eq!(
3292 baseline_reference_satellites(
3293 [0.0, 0.0, 0.0],
3294 &epochs,
3295 BaselineReferenceSelection::Auto,
3296 ),
3297 Err(DoubleDifferenceError::InvalidInput {
3298 field: "rtk base position_m",
3299 reason: "degenerate geometry",
3300 })
3301 );
3302
3303 let invalid_sat = vec![baseline_reference_epoch(&[
3304 ("G01", [20.0, 0.0, 0.0]),
3305 ("G02", [f64::INFINITY, 0.0, 0.0]),
3306 ])];
3307 assert_eq!(
3308 baseline_reference_satellites(
3309 [10.0, 0.0, 0.0],
3310 &invalid_sat,
3311 BaselineReferenceSelection::Auto,
3312 ),
3313 Err(DoubleDifferenceError::InvalidInput {
3314 field: "rtk satellite position_m",
3315 reason: "not finite",
3316 })
3317 );
3318
3319 let coincident_sat = vec![baseline_reference_epoch(&[
3320 ("G01", [10.0, 0.0, 0.0]),
3321 ("G02", [20.0, 0.0, 0.0]),
3322 ])];
3323 assert_eq!(
3324 baseline_reference_satellites(
3325 [10.0, 0.0, 0.0],
3326 &coincident_sat,
3327 BaselineReferenceSelection::Auto,
3328 ),
3329 Err(DoubleDifferenceError::InvalidInput {
3330 field: "rtk line of sight_m",
3331 reason: "degenerate geometry",
3332 })
3333 );
3334 }
3335
3336 #[test]
3337 fn elevation_mask_rejects_invalid_geometry_and_mask() {
3338 let epochs = vec![ElevationMaskEpoch {
3339 satellite_positions_m: BTreeMap::from([("G01".to_string(), [20.0, 0.0, 0.0])]),
3340 }];
3341
3342 assert_eq!(
3343 apply_elevation_mask([10.0, 0.0, 0.0], &epochs, f64::NAN),
3344 Err(DoubleDifferenceError::InvalidInput {
3345 field: "rtk elevation mask_deg",
3346 reason: "not finite",
3347 })
3348 );
3349 assert_eq!(
3350 apply_elevation_mask([10.0, 0.0, 0.0], &epochs, 91.0),
3351 Err(DoubleDifferenceError::InvalidInput {
3352 field: "rtk elevation mask_deg",
3353 reason: "out of range",
3354 })
3355 );
3356
3357 let coincident_sat = vec![ElevationMaskEpoch {
3358 satellite_positions_m: BTreeMap::from([("G01".to_string(), [10.0, 0.0, 0.0])]),
3359 }];
3360 assert_eq!(
3361 apply_elevation_mask([10.0, 0.0, 0.0], &coincident_sat, 0.0),
3362 Err(DoubleDifferenceError::InvalidInput {
3363 field: "rtk line of sight_m",
3364 reason: "degenerate geometry",
3365 })
3366 );
3367 }
3368
3369 #[test]
3370 fn code_smoothing_has_frozen_bits_and_receiver_state() {
3371 let epochs = vec![
3372 CodeSmoothingEpoch {
3373 base_observations: vec![
3374 smooth_obs("G02", "G02", 100.0, 10.0, None),
3375 smooth_obs("G01", "G01", 200.0, 20.0, None),
3376 ],
3377 rover_observations: vec![
3378 smooth_obs("G03", "G03", 600.0, 60.0, None),
3379 smooth_obs("G02", "G02", 500.0, 50.0, None),
3380 ],
3381 },
3382 CodeSmoothingEpoch {
3383 base_observations: vec![
3384 smooth_obs("G01", "G01", 201.0, 21.0, Some(1)),
3385 smooth_obs("G02", "G02", 102.0, 11.0, None),
3386 ],
3387 rover_observations: vec![smooth_obs("G02", "G02", 504.0, 51.0, None)],
3388 },
3389 CodeSmoothingEpoch {
3390 base_observations: vec![
3391 smooth_obs("G02", "G02", 103.0, 13.0, None),
3392 smooth_obs("G01", "G01", 202.0, 22.0, None),
3393 ],
3394 rover_observations: vec![
3395 smooth_obs("G03", "G03", 604.0, 62.0, None),
3396 smooth_obs("G02", "G02", 506.0, 53.0, None),
3397 ],
3398 },
3399 ];
3400
3401 let result = hatch_smooth_baseline_code_epochs(&epochs, 2).unwrap();
3402
3403 assert_eq!(
3404 code_bits(&result[0].base_observations),
3405 vec![
3406 ("G01", 0x4069_0000_0000_0000),
3407 ("G02", 0x4059_0000_0000_0000),
3408 ]
3409 );
3410 assert_eq!(
3411 code_bits(&result[1].base_observations),
3412 vec![
3413 ("G01", 0x4069_2000_0000_0000),
3414 ("G02", 0x4059_6000_0000_0000),
3415 ]
3416 );
3417 assert_eq!(
3418 code_bits(&result[2].base_observations),
3419 vec![
3420 ("G01", 0x4069_4000_0000_0000),
3421 ("G02", 0x4059_d000_0000_0000),
3422 ]
3423 );
3424 assert_eq!(
3425 code_bits(&result[0].rover_observations),
3426 vec![
3427 ("G02", 0x407f_4000_0000_0000),
3428 ("G03", 0x4082_c000_0000_0000),
3429 ]
3430 );
3431 assert_eq!(
3432 code_bits(&result[1].rover_observations),
3433 vec![("G02", 0x407f_6800_0000_0000)]
3434 );
3435 assert_eq!(
3436 code_bits(&result[2].rover_observations),
3437 vec![
3438 ("G02", 0x407f_9400_0000_0000),
3439 ("G03", 0x4082_d800_0000_0000),
3440 ]
3441 );
3442 assert_eq!(result[1].base_observations[0].lli, Some(1));
3443 assert_eq!(
3444 hatch_smooth_baseline_code_epochs(&epochs, 0),
3445 Err(CodeSmoothingError::InvalidWindowCap)
3446 );
3447 }
3448
3449 #[test]
3450 fn cycle_slip_prep_pins_policy_and_reacquisition_behavior() {
3451 let epochs = vec![
3452 CycleSlipEpoch {
3453 base_observations: vec![
3454 smooth_obs("G02", "G02", 100.0, 10.0, None),
3455 smooth_obs("G01", "G01", 200.0, 20.0, None),
3456 ],
3457 rover_observations: vec![
3458 smooth_obs("G02", "G02", 101.0, 11.0, None),
3459 smooth_obs("G01", "G01", 201.0, 21.0, None),
3460 ],
3461 },
3462 CycleSlipEpoch {
3463 base_observations: vec![
3464 smooth_obs("G01", "G01", 210.0, 30.0, None),
3465 smooth_obs("G02", "G02", 110.0, 15.0, None),
3466 ],
3467 rover_observations: vec![
3468 smooth_obs("G01", "G01", 211.0, 31.0, None),
3469 smooth_obs("G02", "G02", 111.0, 16.0, Some(1)),
3470 ],
3471 },
3472 CycleSlipEpoch {
3473 base_observations: vec![smooth_obs("G01", "G01", 220.0, 40.0, None)],
3474 rover_observations: vec![smooth_obs("G01", "G01", 221.0, 41.0, None)],
3475 },
3476 CycleSlipEpoch {
3477 base_observations: vec![
3478 smooth_obs("G02", "G02", 130.0, 18.0, None),
3479 smooth_obs("G01", "G01", 230.0, 50.0, None),
3480 ],
3481 rover_observations: vec![
3482 smooth_obs("G02", "G02", 131.0, 19.0, None),
3483 smooth_obs("G01", "G01", 231.0, 51.0, None),
3484 ],
3485 },
3486 ];
3487
3488 assert_eq!(
3489 prepare_cycle_slip_baseline_epochs(&epochs, CycleSlipPolicy::Error),
3490 Err(CycleSlipPrepError::CycleSlipDetected {
3491 receiver: CycleSlipReceiver::Rover,
3492 satellite_id: "G02".to_string(),
3493 epoch_index: 1,
3494 reasons: vec![SlipReason::Lli],
3495 })
3496 );
3497
3498 let dropped =
3499 prepare_cycle_slip_baseline_epochs(&epochs, CycleSlipPolicy::DropSatellite).unwrap();
3500 assert_eq!(dropped.dropped_sats, vec!["G02".to_string()]);
3501 assert!(dropped.split_arcs.is_empty());
3502 assert!(dropped
3503 .epochs
3504 .iter()
3505 .all(
3506 |epoch| ambiguity_id(&epoch.base_observations, "G02").is_none()
3507 && ambiguity_id(&epoch.rover_observations, "G02").is_none()
3508 ));
3509
3510 let split = prepare_cycle_slip_baseline_epochs(&epochs, CycleSlipPolicy::SplitArc).unwrap();
3511 assert!(split.dropped_sats.is_empty());
3512 assert_eq!(
3513 split.split_arcs,
3514 vec![
3515 CycleSlipSplitArc {
3516 receiver: CycleSlipReceiver::Rover,
3517 satellite_id: "G02".to_string(),
3518 ambiguity_id: "G02@rover#1".to_string(),
3519 start_epoch_index: 0,
3520 end_epoch_index: 0,
3521 n_epochs: 1,
3522 },
3523 CycleSlipSplitArc {
3524 receiver: CycleSlipReceiver::Rover,
3525 satellite_id: "G02".to_string(),
3526 ambiguity_id: "G02@rover#2".to_string(),
3527 start_epoch_index: 1,
3528 end_epoch_index: 3,
3529 n_epochs: 2,
3530 },
3531 ]
3532 );
3533 assert_eq!(
3534 ambiguity_id(&split.epochs[0].rover_observations, "G02"),
3535 Some("G02@rover#1")
3536 );
3537 assert_eq!(
3538 ambiguity_id(&split.epochs[1].rover_observations, "G02"),
3539 Some("G02@rover#2")
3540 );
3541 assert_eq!(
3542 ambiguity_id(&split.epochs[2].rover_observations, "G02"),
3543 None
3544 );
3545 assert_eq!(
3546 ambiguity_id(&split.epochs[3].rover_observations, "G02"),
3547 Some("G02@rover#2~ra1")
3548 );
3549 assert_eq!(
3550 ambiguity_id(&split.epochs[3].base_observations, "G02"),
3551 Some("G02~ra1")
3552 );
3553 }
3554
3555 #[test]
3556 fn cycle_slip_prep_rejects_non_finite_arc_data() {
3557 assert_eq!(
3558 prepare_cycle_slip_baseline_epochs(
3559 &[CycleSlipEpoch {
3560 base_observations: vec![smooth_obs("G01", "G01", f64::NAN, 10.0, None)],
3561 rover_observations: Vec::new(),
3562 }],
3563 CycleSlipPolicy::DropSatellite,
3564 ),
3565 Err(CycleSlipPrepError::InvalidInput {
3566 field: "rtk cycle slip code_m",
3567 reason: "not finite",
3568 })
3569 );
3570 assert_eq!(
3571 prepare_cycle_slip_baseline_epochs(
3572 &[CycleSlipEpoch {
3573 base_observations: Vec::new(),
3574 rover_observations: vec![smooth_obs("G01", "G01", 100.0, f64::INFINITY, None,)],
3575 }],
3576 CycleSlipPolicy::DropSatellite,
3577 ),
3578 Err(CycleSlipPrepError::InvalidInput {
3579 field: "rtk cycle slip phase_m",
3580 reason: "not finite",
3581 })
3582 );
3583 }
3584
3585 #[test]
3586 fn dual_cycle_slip_prep_rejects_invalid_arc_data_and_options() {
3587 let epochs = vec![dual_slip_epoch(
3588 "0",
3589 0.0,
3590 vec![dual_slip_obs("G01", "G01", 10.0, 8.0, None, None)],
3591 Vec::new(),
3592 )];
3593
3594 assert_eq!(
3595 prepare_dual_cycle_slip_baseline_epochs(
3596 &epochs,
3597 CycleSlipPolicy::DropSatellite,
3598 CycleSlipOptions {
3599 gf_threshold_m: f64::NAN,
3600 ..CycleSlipOptions::default()
3601 },
3602 ),
3603 Err(CycleSlipPrepError::InvalidInput {
3604 field: "rtk cycle slip gf_threshold_m",
3605 reason: "not finite",
3606 })
3607 );
3608 assert_eq!(
3609 prepare_dual_cycle_slip_baseline_epochs(
3610 &epochs,
3611 CycleSlipPolicy::DropSatellite,
3612 CycleSlipOptions {
3613 min_arc_gap_s: 0.0,
3614 ..CycleSlipOptions::default()
3615 },
3616 ),
3617 Err(CycleSlipPrepError::InvalidInput {
3618 field: "rtk cycle slip min_arc_gap_s",
3619 reason: "not positive",
3620 })
3621 );
3622
3623 let mut bad_gap = epochs.clone();
3624 bad_gap[0].gap_time_s = Some(f64::INFINITY);
3625 assert_eq!(
3626 prepare_dual_cycle_slip_baseline_epochs(
3627 &bad_gap,
3628 CycleSlipPolicy::DropSatellite,
3629 CycleSlipOptions::default(),
3630 ),
3631 Err(CycleSlipPrepError::InvalidInput {
3632 field: "rtk cycle slip gap_time_s",
3633 reason: "not finite",
3634 })
3635 );
3636
3637 let mut bad_phase = epochs.clone();
3638 bad_phase[0].base_observations[0].phi1_cycles = f64::NAN;
3639 assert_eq!(
3640 prepare_dual_cycle_slip_baseline_epochs(
3641 &bad_phase,
3642 CycleSlipPolicy::DropSatellite,
3643 CycleSlipOptions::default(),
3644 ),
3645 Err(CycleSlipPrepError::InvalidInput {
3646 field: "rtk cycle slip phi1_cycles",
3647 reason: "not finite",
3648 })
3649 );
3650
3651 let mut equal_frequency = epochs;
3652 equal_frequency[0].base_observations[0].f2_hz =
3653 equal_frequency[0].base_observations[0].f1_hz;
3654 assert_eq!(
3655 prepare_dual_cycle_slip_baseline_epochs(
3656 &equal_frequency,
3657 CycleSlipPolicy::DropSatellite,
3658 CycleSlipOptions::default(),
3659 ),
3660 Err(CycleSlipPrepError::InvalidInput {
3661 field: "rtk cycle slip frequencies_hz",
3662 reason: "degenerate frequencies",
3663 })
3664 );
3665
3666 let overflow_epoch = dual_slip_epoch(
3667 "0",
3668 0.0,
3669 vec![dual_slip_obs("G01", "G01", f64::MAX, -f64::MAX, None, None)],
3670 Vec::new(),
3671 );
3672 assert_eq!(
3673 prepare_dual_cycle_slip_baseline_epochs(
3674 &[overflow_epoch],
3675 CycleSlipPolicy::DropSatellite,
3676 CycleSlipOptions::default(),
3677 ),
3678 Err(CycleSlipPrepError::InvalidInput {
3679 field: "rtk cycle slip observation",
3680 reason: "not finite",
3681 })
3682 );
3683 }
3684
3685 #[test]
3686 fn dual_cycle_slip_prep_pins_policy_and_reacquisition_behavior() {
3687 let epochs = vec![
3688 dual_slip_epoch(
3689 "0",
3690 0.0,
3691 vec![
3692 dual_slip_obs("G02", "G02", 10.0, 8.0, None, None),
3693 dual_slip_obs("G01", "G01", 20.0, 18.0, None, None),
3694 ],
3695 vec![
3696 dual_slip_obs("G02", "G02", 11.0, 9.0, None, None),
3697 dual_slip_obs("G01", "G01", 21.0, 19.0, None, None),
3698 ],
3699 ),
3700 dual_slip_epoch(
3701 "1",
3702 1.0,
3703 vec![
3704 dual_slip_obs("G01", "G01", 20.0, 18.0, None, None),
3705 dual_slip_obs("G02", "G02", 10.0, 8.0, None, None),
3706 ],
3707 vec![
3708 dual_slip_obs("G01", "G01", 21.0, 19.0, None, None),
3709 dual_slip_obs("G02", "G02", 11.0, 9.0, Some(1), None),
3710 ],
3711 ),
3712 dual_slip_epoch(
3713 "2",
3714 2.0,
3715 vec![dual_slip_obs("G01", "G01", 20.0, 18.0, None, None)],
3716 vec![dual_slip_obs("G01", "G01", 21.0, 19.0, None, None)],
3717 ),
3718 dual_slip_epoch(
3719 "3",
3720 3.0,
3721 vec![
3722 dual_slip_obs("G02", "G02", 10.0, 8.0, None, None),
3723 dual_slip_obs("G01", "G01", 20.0, 18.0, None, None),
3724 ],
3725 vec![
3726 dual_slip_obs("G02", "G02", 11.0, 9.0, None, None),
3727 dual_slip_obs("G01", "G01", 21.0, 19.0, None, None),
3728 ],
3729 ),
3730 ];
3731 let options = CycleSlipOptions {
3732 gf_threshold_m: 1.0e9,
3733 mw_threshold_cycles: 1.0e9,
3734 min_arc_gap_s: 300.0,
3735 };
3736
3737 assert_eq!(
3738 prepare_dual_cycle_slip_baseline_epochs(&epochs, CycleSlipPolicy::Error, options),
3739 Err(CycleSlipPrepError::CycleSlipDetected {
3740 receiver: CycleSlipReceiver::Rover,
3741 satellite_id: "G02".to_string(),
3742 epoch_index: 1,
3743 reasons: vec![SlipReason::Lli],
3744 })
3745 );
3746
3747 let dropped = prepare_dual_cycle_slip_baseline_epochs(
3748 &epochs,
3749 CycleSlipPolicy::DropSatellite,
3750 options,
3751 )
3752 .unwrap();
3753 assert_eq!(dropped.dropped_sats, vec!["G02".to_string()]);
3754 assert!(dropped.split_arcs.is_empty());
3755 assert!(dropped.epochs.iter().all(|epoch| dual_ambiguity_id(
3756 &epoch.base_observations,
3757 "G02"
3758 )
3759 .is_none()
3760 && dual_ambiguity_id(&epoch.rover_observations, "G02").is_none()));
3761
3762 let split =
3763 prepare_dual_cycle_slip_baseline_epochs(&epochs, CycleSlipPolicy::SplitArc, options)
3764 .unwrap();
3765 assert!(split.dropped_sats.is_empty());
3766 assert_eq!(
3767 split.split_arcs,
3768 vec![
3769 CycleSlipSplitArc {
3770 receiver: CycleSlipReceiver::Rover,
3771 satellite_id: "G02".to_string(),
3772 ambiguity_id: "G02@rover#1".to_string(),
3773 start_epoch_index: 0,
3774 end_epoch_index: 0,
3775 n_epochs: 1,
3776 },
3777 CycleSlipSplitArc {
3778 receiver: CycleSlipReceiver::Rover,
3779 satellite_id: "G02".to_string(),
3780 ambiguity_id: "G02@rover#2".to_string(),
3781 start_epoch_index: 1,
3782 end_epoch_index: 3,
3783 n_epochs: 2,
3784 },
3785 ]
3786 );
3787 assert_eq!(
3788 dual_ambiguity_id(&split.epochs[0].rover_observations, "G02"),
3789 Some("G02@rover#1")
3790 );
3791 assert_eq!(
3792 dual_ambiguity_id(&split.epochs[1].rover_observations, "G02"),
3793 Some("G02@rover#2")
3794 );
3795 assert_eq!(
3796 dual_ambiguity_id(&split.epochs[2].rover_observations, "G02"),
3797 None
3798 );
3799 assert_eq!(
3800 dual_ambiguity_id(&split.epochs[3].rover_observations, "G02"),
3801 Some("G02@rover#2~ra1")
3802 );
3803 assert_eq!(
3804 dual_ambiguity_id(&split.epochs[3].base_observations, "G02"),
3805 Some("G02~ra1")
3806 );
3807 }
3808
3809 #[test]
3810 fn dual_cycle_slip_prep_uses_threshold_and_gap_classification() {
3811 let epochs = vec![
3812 dual_slip_epoch(
3813 "0",
3814 0.0,
3815 vec![dual_slip_obs("G03", "G03", 10.0, 8.0, None, None)],
3816 vec![dual_slip_obs("G03", "G03", 11.0, 9.0, None, None)],
3817 ),
3818 dual_slip_epoch(
3819 "1",
3820 20.0,
3821 vec![dual_slip_obs("G03", "G03", 11.0, 8.0, None, None)],
3822 vec![dual_slip_obs("G03", "G03", 11.0, 9.0, None, None)],
3823 ),
3824 ];
3825 let options = CycleSlipOptions {
3826 gf_threshold_m: 0.05,
3827 mw_threshold_cycles: 0.5,
3828 min_arc_gap_s: 10.0,
3829 };
3830
3831 assert_eq!(
3832 prepare_dual_cycle_slip_baseline_epochs(&epochs, CycleSlipPolicy::Error, options),
3833 Err(CycleSlipPrepError::CycleSlipDetected {
3834 receiver: CycleSlipReceiver::Base,
3835 satellite_id: "G03".to_string(),
3836 epoch_index: 1,
3837 reasons: vec![
3838 SlipReason::DataGap,
3839 SlipReason::GeometryFree,
3840 SlipReason::MelbourneWubbena,
3841 ],
3842 })
3843 );
3844 }
3845
3846 #[test]
3847 fn baseline_reference_errors_match_public_tags() {
3848 let base = [10.0, 0.0, 0.0];
3849 let multi = vec![baseline_reference_epoch(&[
3850 ("G01", [20.0, 0.0, 0.0]),
3851 ("G02", [20.0, 0.0, 0.0]),
3852 ("E01", [20.0, 0.0, 0.0]),
3853 ("E02", [20.0, 0.0, 0.0]),
3854 ])];
3855 assert_eq!(
3856 baseline_reference_satellites(
3857 base,
3858 &multi,
3859 BaselineReferenceSelection::Satellite("G01".to_string()),
3860 ),
3861 Err(DoubleDifferenceError::ReferenceSatelliteSingleSystem(
3862 "G01".to_string()
3863 ))
3864 );
3865 assert_eq!(
3866 baseline_reference_satellites(
3867 base,
3868 &multi,
3869 BaselineReferenceSelection::PerSystem(BTreeMap::from([(
3870 "G".to_string(),
3871 "G01".to_string(),
3872 )])),
3873 ),
3874 Err(DoubleDifferenceError::ReferenceSatelliteMissingSystem(
3875 "E".to_string()
3876 ))
3877 );
3878
3879 let split = vec![
3880 baseline_reference_epoch(&[("G01", [20.0, 0.0, 0.0])]),
3881 baseline_reference_epoch(&[("G02", [20.0, 0.0, 0.0])]),
3882 ];
3883 assert_eq!(
3884 baseline_reference_satellites(base, &split, BaselineReferenceSelection::Auto),
3885 Err(DoubleDifferenceError::NoCommonReferenceSatellite(
3886 "G".to_string()
3887 ))
3888 );
3889 }
3890
3891 #[test]
3892 fn errors_are_tagged() {
3893 assert_eq!(
3894 double_differences(
3895 &[obs("G01", 1.0, 2.0)],
3896 &[obs("G01", 1.0, 2.0)],
3897 ReferenceSelection::Auto
3898 ),
3899 Err(DoubleDifferenceError::TooFewCommonSatellites {
3900 count: 1,
3901 minimum: 2,
3902 })
3903 );
3904 assert_eq!(
3905 double_differences(
3906 &[obs("G01", 1.0, 2.0), obs("G01", 3.0, 4.0)],
3907 &[obs("G01", 1.0, 2.0), obs("G02", 3.0, 4.0)],
3908 ReferenceSelection::Auto,
3909 ),
3910 Err(DoubleDifferenceError::DuplicateObservation(
3911 "G01".to_string()
3912 ))
3913 );
3914 assert_eq!(
3915 double_differences(
3916 &[obs("G01", 1.0, 2.0), obs("G02", 3.0, 4.0)],
3917 &[obs("G01", 1.0, 2.0), obs("G02", 3.0, 4.0)],
3918 ReferenceSelection::Satellite("G99".to_string()),
3919 ),
3920 Err(DoubleDifferenceError::ReferenceSatelliteMissing(
3921 "G99".to_string()
3922 ))
3923 );
3924 }
3925
3926 #[test]
3927 fn double_differences_reject_non_finite_observations() {
3928 let rover = vec![obs("G01", 10.0, 11.0), obs("G02", 20.0, 21.0)];
3929
3930 assert_eq!(
3931 double_differences(
3932 &[obs("G01", 1.0, 2.0), obs("G02", f64::NAN, 4.0)],
3933 &rover,
3934 ReferenceSelection::Auto,
3935 ),
3936 Err(DoubleDifferenceError::InvalidInput {
3937 field: "rtk observation code_m",
3938 reason: "not finite",
3939 })
3940 );
3941
3942 assert_eq!(
3943 double_differences(
3944 &[obs("G01", 1.0, 2.0), obs("G02", 3.0, 4.0)],
3945 &[obs("G01", 10.0, f64::INFINITY), obs("G02", 20.0, 21.0)],
3946 ReferenceSelection::Auto,
3947 ),
3948 Err(DoubleDifferenceError::InvalidInput {
3949 field: "rtk observation phase_m",
3950 reason: "not finite",
3951 })
3952 );
3953 }
3954
3955 #[test]
3956 fn estimates_dual_frequency_wide_lane_integers() {
3957 let epochs = vec![
3958 DualEpoch {
3959 observations: vec![
3960 dual_pair("G01", 0.0, 1.0),
3961 dual_pair("G02", 0.0, 4.0),
3962 dual_pair("G03", 0.0, -1.0),
3963 ],
3964 },
3965 DualEpoch {
3966 observations: vec![
3967 dual_pair("G01", 0.0, 2.0),
3968 dual_pair("G02", 0.0, 5.0),
3969 dual_pair("G03", 0.0, 0.0),
3970 ],
3971 },
3972 ];
3973
3974 let fixed = estimate_wide_lane_ambiguities(
3975 &epochs,
3976 "G01",
3977 WideLaneOptions {
3978 min_epochs: 2,
3979 tolerance_cycles: 1.0e-9,
3980 skip_short_fragments: false,
3981 },
3982 )
3983 .unwrap();
3984
3985 assert_eq!(
3986 fixed,
3987 BTreeMap::from([("G02".to_string(), 3), ("G03".to_string(), -2)])
3988 );
3989 }
3990
3991 #[test]
3992 fn split_arc_wide_lane_skips_short_fragments() {
3993 let epochs = vec![
3994 DualEpoch {
3995 observations: vec![
3996 dual_pair("G01", 0.0, 1.0),
3997 split_dual_pair("G02", "G02@rover#1", 0.0, 4.0),
3998 dual_pair("G03", 0.0, -1.0),
3999 ],
4000 },
4001 DualEpoch {
4002 observations: vec![dual_pair("G01", 0.0, 2.0), dual_pair("G03", 0.0, 0.0)],
4003 },
4004 ];
4005
4006 let options = WideLaneOptions {
4007 min_epochs: 2,
4008 tolerance_cycles: 1.0e-9,
4009 skip_short_fragments: true,
4010 };
4011 let fixed = estimate_wide_lane_ambiguities(&epochs, "G01", options).unwrap();
4012 assert_eq!(fixed, BTreeMap::from([("G03".to_string(), -2)]));
4013
4014 let err = estimate_wide_lane_ambiguities(
4015 &epochs,
4016 "G01",
4017 WideLaneOptions {
4018 skip_short_fragments: false,
4019 ..options
4020 },
4021 )
4022 .unwrap_err();
4023 assert_eq!(
4024 err,
4025 WideLaneError::TooFewWideLaneEpochs {
4026 ambiguity_id: "G02@rover#1|ref=G01".to_string(),
4027 count: 1,
4028 minimum: 2,
4029 }
4030 );
4031 }
4032
4033 #[test]
4034 fn wide_lane_rejects_invalid_inputs_and_options() {
4035 let epochs = vec![DualEpoch {
4036 observations: vec![dual_pair("G01", 0.0, 1.0), dual_pair("G02", 0.0, 4.0)],
4037 }];
4038
4039 assert_eq!(
4040 estimate_wide_lane_ambiguities(
4041 &epochs,
4042 "G01",
4043 WideLaneOptions {
4044 min_epochs: 0,
4045 tolerance_cycles: 1.0e-9,
4046 skip_short_fragments: false,
4047 },
4048 ),
4049 Err(WideLaneError::InvalidInput {
4050 field: "rtk wide lane min_epochs",
4051 reason: "not positive",
4052 })
4053 );
4054 assert_eq!(
4055 estimate_wide_lane_ambiguities(
4056 &epochs,
4057 "G01",
4058 WideLaneOptions {
4059 min_epochs: 1,
4060 tolerance_cycles: f64::NAN,
4061 skip_short_fragments: false,
4062 },
4063 ),
4064 Err(WideLaneError::InvalidInput {
4065 field: "rtk wide lane tolerance_cycles",
4066 reason: "not finite",
4067 })
4068 );
4069
4070 let mut bad_epoch = epochs;
4071 bad_epoch[0].observations[1].rover.p1_m = f64::INFINITY;
4072 assert_eq!(
4073 estimate_wide_lane_ambiguities(
4074 &bad_epoch,
4075 "G01",
4076 WideLaneOptions {
4077 min_epochs: 1,
4078 tolerance_cycles: 1.0e-9,
4079 skip_short_fragments: false,
4080 },
4081 ),
4082 Err(WideLaneError::InvalidInput {
4083 field: "rtk wide lane p1_m",
4084 reason: "not finite",
4085 })
4086 );
4087 }
4088
4089 #[test]
4090 fn builds_ionosphere_free_epochs_and_narrow_lane_params() {
4091 let epochs = vec![
4092 DualIonosphereFreeEpoch {
4093 observations: vec![
4094 if_pair(
4095 "G01",
4096 20_000_000.0,
4097 20_000_020.0,
4098 105_100_000.0,
4099 105_100_040.0,
4100 ),
4101 if_pair(
4102 "G02",
4103 21_000_000.0,
4104 21_000_035.0,
4105 110_200_000.0,
4106 110_200_090.0,
4107 ),
4108 if_pair(
4109 "G03",
4110 22_000_000.0,
4111 22_000_055.0,
4112 115_300_000.0,
4113 115_300_120.0,
4114 ),
4115 ],
4116 },
4117 DualIonosphereFreeEpoch {
4118 observations: vec![
4119 if_pair(
4120 "G01",
4121 20_000_100.0,
4122 20_000_120.0,
4123 105_100_500.0,
4124 105_100_540.0,
4125 ),
4126 if_pair(
4127 "G02",
4128 21_000_100.0,
4129 21_000_135.0,
4130 110_200_500.0,
4131 110_200_590.0,
4132 ),
4133 if_pair(
4134 "G03",
4135 22_000_100.0,
4136 22_000_155.0,
4137 115_300_500.0,
4138 115_300_620.0,
4139 ),
4140 ],
4141 },
4142 ];
4143 let wide_lanes = BTreeMap::from([("G02".to_string(), 3), ("G03".to_string(), -5)]);
4144
4145 let result = build_ionosphere_free_baseline_epochs(&epochs, "G01", &wide_lanes).unwrap();
4146
4147 assert_eq!(
4148 result
4149 .wavelengths_m
4150 .iter()
4151 .map(|(id, value)| (id.as_str(), value.to_bits()))
4152 .collect::<Vec<_>>(),
4153 [("G02", 0x3fbb614bed5136b9), ("G03", 0x3fbb614bed5136b9),]
4154 );
4155 assert_eq!(
4156 result
4157 .offsets_m
4158 .iter()
4159 .map(|(id, value)| (id.as_str(), value.to_bits()))
4160 .collect::<Vec<_>>(),
4161 [("G02", 0x3ff21e814dfd4618), ("G03", 0xbffe32d781fb74d4),]
4162 );
4163 assert_eq!(result.epochs.len(), 2);
4164 assert_eq!(
4165 result
4166 .epochs
4167 .iter()
4168 .map(|epoch| (
4169 epoch.epoch_index,
4170 epoch.satellite_ids.clone(),
4171 epoch
4172 .base_observations
4173 .iter()
4174 .map(|obs| (
4175 obs.satellite_id.as_str(),
4176 obs.ambiguity_id.as_str(),
4177 obs.code_m.to_bits(),
4178 obs.phase_m.to_bits()
4179 ))
4180 .collect::<Vec<_>>(),
4181 epoch
4182 .rover_observations
4183 .iter()
4184 .map(|obs| (
4185 obs.satellite_id.as_str(),
4186 obs.ambiguity_id.as_str(),
4187 obs.code_m.to_bits(),
4188 obs.phase_m.to_bits()
4189 ))
4190 .collect::<Vec<_>>()
4191 ))
4192 .collect::<Vec<_>>(),
4193 vec![
4194 (
4195 0,
4196 vec!["G01".to_string(), "G02".to_string(), "G03".to_string()],
4197 vec![
4198 ("G01", "G01", 0x417312cfca8965e4, 0x416570ac29af7848),
4199 ("G02", "G02", 0x417406f3ca8965e4, 0x41667b02f0ff8bd8),
4200 ("G03", "G03", 0x4174fb17ca8965e4, 0x41678559b84f9f64),
4201 ],
4202 vec![
4203 ("G01", "G01", 0x417312d0fa2bbf5f, 0x416570ac9e819db4),
4204 ("G02", "G02", 0x417406f5ea2bbf5e, 0x41667b0410f1cbd0),
4205 ("G03", "G03", 0x4174fb1b2a2bbf5e, 0x4167855b3eeebc18),
4206 ],
4207 ),
4208 (
4209 1,
4210 vec!["G01".to_string(), "G02".to_string(), "G03".to_string()],
4211 vec![
4212 ("G01", "G01", 0x417312d60a8965e4, 0x416570b2d8f081bc),
4213 ("G02", "G02", 0x417406fa0a8965e4, 0x41667b09a0409544),
4214 ("G03", "G03", 0x4174fb1e0a8965e4, 0x416785606790a8d4),
4215 ],
4216 vec![
4217 ("G01", "G01", 0x417312d73a2bbf5f, 0x416570b34dc2a728),
4218 ("G02", "G02", 0x417406fc2a2bbf5e, 0x41667b0ac032d540),
4219 ("G03", "G03", 0x4174fb216a2bbf5e, 0x41678561ee2fc588),
4220 ],
4221 ),
4222 ]
4223 );
4224 }
4225
4226 #[test]
4227 fn ionosphere_free_builders_reject_invalid_tropo_and_setup_inputs() {
4228 let mut epochs = vec![DualIonosphereFreeEpoch {
4229 observations: vec![
4230 if_pair(
4231 "G01",
4232 20_000_000.0,
4233 20_000_020.0,
4234 105_100_000.0,
4235 105_100_040.0,
4236 ),
4237 if_pair(
4238 "G02",
4239 21_000_000.0,
4240 21_000_035.0,
4241 110_200_000.0,
4242 110_200_090.0,
4243 ),
4244 ],
4245 }];
4246 epochs[0].observations[1].rover.tropo_m = f64::NAN;
4247 assert_eq!(
4248 build_ionosphere_free_baseline_epochs(
4249 &epochs,
4250 "G01",
4251 &BTreeMap::from([("G02".to_string(), 3)]),
4252 ),
4253 Err(IonosphereFreeBaselineError::InvalidInput {
4254 field: "rtk if tropo_m",
4255 reason: "not finite",
4256 })
4257 );
4258
4259 let setup_epochs = vec![DualIonosphereFreeSetupEpoch {
4260 jd_whole: 2_460_100.5,
4261 jd_fraction: 2.0,
4262 observations: vec![dual_pair("G01", 0.0, 1.0), dual_pair("G02", 0.0, 4.0)],
4263 base_satellite_positions_m: BTreeMap::from([
4264 ("G01".to_string(), [20.0, 0.0, 0.0]),
4265 ("G02".to_string(), [30.0, 0.0, 0.0]),
4266 ]),
4267 rover_satellite_positions_m: BTreeMap::from([
4268 ("G01".to_string(), [20.0, 0.0, 0.0]),
4269 ("G02".to_string(), [30.0, 0.0, 0.0]),
4270 ]),
4271 }];
4272 assert_eq!(
4273 prepare_ionosphere_free_baseline_epochs(
4274 [10.0, 0.0, 0.0],
4275 [0.0, 0.0, 0.0],
4276 &setup_epochs,
4277 "G01",
4278 &BTreeMap::from([("G02".to_string(), 3)]),
4279 false,
4280 ),
4281 Err(IonosphereFreeBaselineError::InvalidInput {
4282 field: "rtk if setup jd_fraction",
4283 reason: "out of range",
4284 })
4285 );
4286
4287 let tropo_epochs = vec![DualIonosphereFreeSetupEpoch {
4288 jd_whole: 2_460_100.5,
4289 jd_fraction: 0.0,
4290 observations: vec![dual_pair("G01", 0.0, 1.0), dual_pair("G02", 0.0, 4.0)],
4291 base_satellite_positions_m: BTreeMap::from([
4292 ("G01".to_string(), [10.0, 0.0, 0.0]),
4293 ("G02".to_string(), [30.0, 0.0, 0.0]),
4294 ]),
4295 rover_satellite_positions_m: BTreeMap::from([
4296 ("G01".to_string(), [20.0, 0.0, 0.0]),
4297 ("G02".to_string(), [30.0, 0.0, 0.0]),
4298 ]),
4299 }];
4300 assert_eq!(
4301 prepare_ionosphere_free_baseline_epochs(
4302 [10.0, 0.0, 0.0],
4303 [0.0, 0.0, 0.0],
4304 &tropo_epochs,
4305 "G01",
4306 &BTreeMap::from([("G02".to_string(), 3)]),
4307 true,
4308 ),
4309 Err(IonosphereFreeBaselineError::InvalidInput {
4310 field: "rtk tropo line of sight_m",
4311 reason: "degenerate geometry",
4312 })
4313 );
4314 }
4315
4316 #[test]
4317 fn ionosphere_free_setup_rejects_invalid_julian_split_without_panic() {
4318 let setup_epochs = vec![DualIonosphereFreeSetupEpoch {
4319 jd_whole: f64::NAN,
4320 jd_fraction: 0.0,
4321 observations: vec![dual_pair("G01", 0.0, 1.0), dual_pair("G02", 0.0, 4.0)],
4322 base_satellite_positions_m: BTreeMap::from([
4323 (
4324 "G01".to_string(),
4325 [20_200_000.0, 14_000_000.0, 21_700_000.0],
4326 ),
4327 (
4328 "G02".to_string(),
4329 [21_200_000.0, 13_000_000.0, 20_700_000.0],
4330 ),
4331 ]),
4332 rover_satellite_positions_m: BTreeMap::from([
4333 (
4334 "G01".to_string(),
4335 [20_200_100.0, 14_000_000.0, 21_700_000.0],
4336 ),
4337 (
4338 "G02".to_string(),
4339 [21_200_100.0, 13_000_000.0, 20_700_000.0],
4340 ),
4341 ]),
4342 }];
4343
4344 let result = std::panic::catch_unwind(|| {
4345 prepare_ionosphere_free_baseline_epochs(
4346 [6_378_137.0, 0.0, 0.0],
4347 [1.0, 0.0, 0.0],
4348 &setup_epochs,
4349 "G01",
4350 &BTreeMap::from([("G02".to_string(), 3)]),
4351 true,
4352 )
4353 });
4354
4355 assert!(result.is_ok(), "invalid Julian split must not panic");
4356 assert_eq!(
4357 result.expect("invalid Julian split should not unwind"),
4358 Err(IonosphereFreeBaselineError::InvalidInput {
4359 field: "rtk if setup jd_whole",
4360 reason: "not finite",
4361 })
4362 );
4363 }
4364
4365 #[test]
4366 fn ionosphere_free_setup_rejects_nonfinite_tropo_receiver_without_panic() {
4367 let setup_epochs = vec![DualIonosphereFreeSetupEpoch {
4368 jd_whole: 2_460_100.5,
4369 jd_fraction: 0.0,
4370 observations: vec![dual_pair("G01", 0.0, 1.0), dual_pair("G02", 0.0, 4.0)],
4371 base_satellite_positions_m: BTreeMap::from([
4372 (
4373 "G01".to_string(),
4374 [20_200_000.0, 14_000_000.0, 21_700_000.0],
4375 ),
4376 (
4377 "G02".to_string(),
4378 [21_200_000.0, 13_000_000.0, 20_700_000.0],
4379 ),
4380 ]),
4381 rover_satellite_positions_m: BTreeMap::from([
4382 (
4383 "G01".to_string(),
4384 [20_200_100.0, 14_000_000.0, 21_700_000.0],
4385 ),
4386 (
4387 "G02".to_string(),
4388 [21_200_100.0, 13_000_000.0, 20_700_000.0],
4389 ),
4390 ]),
4391 }];
4392
4393 let result = std::panic::catch_unwind(|| {
4394 prepare_ionosphere_free_baseline_epochs(
4395 [f64::NAN, 0.0, 0.0],
4396 [1.0, 0.0, 0.0],
4397 &setup_epochs,
4398 "G01",
4399 &BTreeMap::from([("G02".to_string(), 3)]),
4400 true,
4401 )
4402 });
4403
4404 assert!(result.is_ok(), "non-finite tropo receiver must not panic");
4405 assert_eq!(
4406 result.expect("non-finite tropo receiver should not unwind"),
4407 Err(IonosphereFreeBaselineError::InvalidInput {
4408 field: "rtk tropo base position_m",
4409 reason: "not finite",
4410 })
4411 );
4412 }
4413
4414 #[test]
4415 fn ionosphere_free_setup_handles_antimeridian_tropo_receiver_without_panic() {
4416 let setup_epochs = vec![DualIonosphereFreeSetupEpoch {
4417 jd_whole: 2_460_100.5,
4418 jd_fraction: 0.0,
4419 observations: vec![dual_pair("G01", 0.0, 1.0), dual_pair("G02", 0.0, 4.0)],
4420 base_satellite_positions_m: BTreeMap::from([
4421 (
4422 "G01".to_string(),
4423 [-20_200_000.0, -14_000_000.0, 21_700_000.0],
4424 ),
4425 (
4426 "G02".to_string(),
4427 [-21_200_000.0, -13_000_000.0, 20_700_000.0],
4428 ),
4429 ]),
4430 rover_satellite_positions_m: BTreeMap::from([
4431 (
4432 "G01".to_string(),
4433 [-20_200_100.0, -14_000_000.0, 21_700_000.0],
4434 ),
4435 (
4436 "G02".to_string(),
4437 [-21_200_100.0, -13_000_000.0, 20_700_000.0],
4438 ),
4439 ]),
4440 }];
4441
4442 let result = std::panic::catch_unwind(|| {
4443 prepare_ionosphere_free_baseline_epochs(
4444 [-6_378_137.0, -0.0, 0.0],
4445 [1.0, 0.0, 0.0],
4446 &setup_epochs,
4447 "G01",
4448 &BTreeMap::from([("G02".to_string(), 3)]),
4449 true,
4450 )
4451 });
4452
4453 assert!(result.is_ok(), "antimeridian tropo receiver must not panic");
4454 result
4455 .expect("antimeridian tropo receiver should not unwind")
4456 .expect("antimeridian tropo receiver should prepare IF epochs");
4457 }
4458
4459 #[test]
4460 fn ionosphere_free_epoch_builder_skips_missing_wide_lane_fragments() {
4461 let epochs = vec![DualIonosphereFreeEpoch {
4462 observations: vec![
4463 if_pair(
4464 "G01",
4465 20_000_000.0,
4466 20_000_020.0,
4467 105_100_000.0,
4468 105_100_040.0,
4469 ),
4470 if_pair(
4471 "G02",
4472 21_000_000.0,
4473 21_000_035.0,
4474 110_200_000.0,
4475 110_200_090.0,
4476 ),
4477 if_pair(
4478 "G03",
4479 22_000_000.0,
4480 22_000_055.0,
4481 115_300_000.0,
4482 115_300_120.0,
4483 ),
4484 ],
4485 }];
4486 let wide_lanes = BTreeMap::from([("G02".to_string(), 3)]);
4487
4488 let result = build_ionosphere_free_baseline_epochs(&epochs, "G01", &wide_lanes).unwrap();
4489
4490 assert_eq!(result.epochs[0].satellite_ids, ["G01", "G02"]);
4491 assert_eq!(
4492 result.wavelengths_m.keys().collect::<Vec<_>>(),
4493 [&"G02".to_string()]
4494 );
4495 }
4496
4497 #[test]
4498 fn wide_lane_errors_on_equal_frequencies() {
4499 let mut bad = dual_pair("G02", 0.0, 4.0);
4500 bad.base.f2_hz = gps_l1_hz();
4501 let epochs = vec![DualEpoch {
4502 observations: vec![dual_pair("G01", 0.0, 1.0), bad],
4503 }];
4504
4505 let err = estimate_wide_lane_ambiguities(
4506 &epochs,
4507 "G01",
4508 WideLaneOptions {
4509 min_epochs: 1,
4510 tolerance_cycles: 0.5,
4511 skip_short_fragments: false,
4512 },
4513 )
4514 .unwrap_err();
4515
4516 assert_eq!(
4517 err,
4518 WideLaneError::WideLaneFailed {
4519 satellite_id: "G02".to_string(),
4520 reason: CarrierPhaseError::EqualFrequencies,
4521 }
4522 );
4523 }
4524}