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