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