1use std::collections::{BTreeMap, BTreeSet};
11
12use crate::ambiguity::{self, AmbiguityId, CycleSlipPolicy, NarrowLaneParams};
13use crate::carrier_phase::{
14 detect_cycle_slips, wide_lane_wavelength, ArcEpoch, CarrierPhaseError, CycleSlipOptions,
15 SlipReason, SlipResult,
16};
17use crate::combinations::{self, IonosphereFreeError};
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct DualFrequencyObservation {
22 pub satellite_id: String,
23 pub ambiguity_id: String,
24 pub p1_m: f64,
25 pub p2_m: f64,
26 pub phi1_cyc: f64,
27 pub phi2_cyc: f64,
28 pub f1_hz: f64,
29 pub f2_hz: f64,
30 pub lli1: Option<i64>,
31 pub lli2: Option<i64>,
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct DualFrequencyEpoch {
37 pub gap_time_s: Option<f64>,
39 pub observations: Vec<DualFrequencyObservation>,
40}
41
42#[derive(Debug, Clone, PartialEq)]
44pub struct PreparedFloatObservation {
45 pub satellite_id: String,
46 pub ambiguity_id: String,
47 pub code_m: f64,
48 pub phase_m: f64,
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub struct PreparedFloatEpoch {
54 pub epoch_index: usize,
55 pub observations: Vec<PreparedFloatObservation>,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct WideLanePrepOptions {
61 pub min_epochs: usize,
62 pub tolerance_cycles: f64,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct PppSplitArc {
68 pub satellite_id: String,
69 pub ambiguity_id: String,
70 pub start_epoch_index: usize,
71 pub end_epoch_index: usize,
72 pub n_epochs: usize,
73}
74
75#[derive(Debug, Clone, PartialEq)]
77pub struct WideLanePrepResult {
78 pub epochs: Vec<PreparedFloatEpoch>,
79 pub wavelengths_m: BTreeMap<String, f64>,
80 pub offsets_m: BTreeMap<String, f64>,
81 pub wide_lane_cycles: BTreeMap<String, i64>,
82 pub dropped_sats: Vec<String>,
83 pub split_arcs: Vec<PppSplitArc>,
84}
85
86#[derive(Debug, Clone, PartialEq)]
88pub enum WideLanePrepError {
89 CycleSlipDetected {
90 satellite_id: String,
91 epoch_index: usize,
92 reasons: Vec<SlipReason>,
93 },
94 WideLaneFailed {
95 ambiguity_id: String,
96 reason: CarrierPhaseError,
97 },
98 TooFewWideLaneEpochs {
99 ambiguity_id: String,
100 count: usize,
101 minimum: usize,
102 },
103 WideLaneNotInteger {
104 ambiguity_id: String,
105 mean_cycles: f64,
106 fixed_cycles: i64,
107 },
108 MissingWideLaneAmbiguity(String),
109 InconsistentFrequencies(String),
110 IonosphereFreeFailed {
111 satellite_id: String,
112 reason: IonosphereFreeError,
113 },
114}
115
116#[derive(Debug, Clone, PartialEq)]
119pub struct FloatCycleSlipObservation {
120 pub satellite_id: String,
121 pub ambiguity_id: String,
122 pub raw: Option<DualFrequencyObservation>,
123}
124
125#[derive(Debug, Clone, PartialEq)]
127pub struct FloatCycleSlipEpoch {
128 pub gap_time_s: Option<f64>,
129 pub observations: Vec<FloatCycleSlipObservation>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct FloatCycleSlipTaggedObservation {
135 pub satellite_id: String,
136 pub ambiguity_id: String,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct FloatCycleSlipTaggedEpoch {
142 pub observations: Vec<FloatCycleSlipTaggedObservation>,
143}
144pub fn prepare_widelane_fixed_epochs(
147 epochs: &[DualFrequencyEpoch],
148 wide_lane: WideLanePrepOptions,
149 cycle_slip_policy: CycleSlipPolicy,
150 cycle_slip_options: CycleSlipOptions,
151) -> Result<WideLanePrepResult, WideLanePrepError> {
152 let (prepared_dual_epochs, wide_lane_cycles, dropped_sats, split_arcs) =
153 wide_lane_ambiguities(epochs, wide_lane, cycle_slip_policy, cycle_slip_options)?;
154 let filtered_dual_epochs =
155 filter_dual_epochs_by_wide_lanes(&prepared_dual_epochs, &wide_lane_cycles);
156 let (if_epochs, wavelengths_m, offsets_m) =
157 ionosphere_free_narrow_lane_epochs(&filtered_dual_epochs, &wide_lane_cycles)?;
158 Ok(WideLanePrepResult {
159 epochs: if_epochs,
160 wavelengths_m,
161 offsets_m,
162 wide_lane_cycles,
163 dropped_sats,
164 split_arcs,
165 })
166}
167pub fn split_float_cycle_slip_epochs(
169 epochs: &[FloatCycleSlipEpoch],
170 cycle_slip_options: CycleSlipOptions,
171) -> Vec<FloatCycleSlipTaggedEpoch> {
172 let tags = float_cycle_slip_tags(epochs, cycle_slip_options);
173 epochs
174 .iter()
175 .enumerate()
176 .map(|(epoch_index, epoch)| {
177 let mut observations = epoch
178 .observations
179 .iter()
180 .map(|obs| {
181 let ambiguity_id = tags
182 .get(&(epoch_index, obs.satellite_id.clone()))
183 .map_or_else(|| obs.ambiguity_id.clone(), |id| id.as_str().to_string());
184 FloatCycleSlipTaggedObservation {
185 satellite_id: obs.satellite_id.clone(),
186 ambiguity_id,
187 }
188 })
189 .collect::<Vec<_>>();
190 observations.sort_by(|a, b| {
191 (a.satellite_id.as_str(), a.ambiguity_id.as_str())
192 .cmp(&(b.satellite_id.as_str(), b.ambiguity_id.as_str()))
193 });
194 FloatCycleSlipTaggedEpoch { observations }
195 })
196 .collect()
197}
198#[derive(Clone, Copy)]
199struct DualArcSample<'a> {
200 epoch_index: usize,
201 gap_time_s: Option<f64>,
202 observation: &'a DualFrequencyObservation,
203}
204
205#[derive(Clone)]
206struct PreparedDualFrequencyEpoch {
207 epoch_index: usize,
208 observations: Vec<DualFrequencyObservation>,
209}
210
211struct DualSlipEvent {
212 epoch_index: usize,
213 reasons: Vec<SlipReason>,
214}
215
216#[derive(Clone)]
217struct PendingGfMwSlip {
218 epoch_index: usize,
219 reasons: Vec<SlipReason>,
220 reference: SlipReference,
221}
222
223#[derive(Clone, Copy)]
224struct SlipReference {
225 gf_m: Option<f64>,
226 mw_m: Option<f64>,
227 f1_hz: Option<f64>,
228 f2_hz: Option<f64>,
229}
230
231type WideLanePrepPieces = (
232 Vec<PreparedDualFrequencyEpoch>,
233 BTreeMap<String, i64>,
234 Vec<String>,
235 Vec<PppSplitArc>,
236);
237
238type TaggedWideLaneArc = (
239 Vec<(usize, DualFrequencyObservation)>,
240 BTreeMap<String, i64>,
241 Option<PppSplitArc>,
242);
243
244type WideLaneArcPrepared = (
245 Vec<(usize, DualFrequencyObservation)>,
246 BTreeMap<String, i64>,
247 Vec<String>,
248 Vec<PppSplitArc>,
249);
250
251fn wide_lane_ambiguities(
252 epochs: &[DualFrequencyEpoch],
253 wide_lane: WideLanePrepOptions,
254 cycle_slip_policy: CycleSlipPolicy,
255 cycle_slip_options: CycleSlipOptions,
256) -> Result<WideLanePrepPieces, WideLanePrepError> {
257 let mut arcs = BTreeMap::<String, Vec<DualArcSample<'_>>>::new();
258 for (epoch_index, epoch) in epochs.iter().enumerate() {
259 for observation in &epoch.observations {
260 arcs.entry(observation.satellite_id.clone())
261 .or_default()
262 .push(DualArcSample {
263 epoch_index,
264 gap_time_s: epoch.gap_time_s,
265 observation,
266 });
267 }
268 }
269
270 let mut entries = Vec::new();
271 let mut cycles = BTreeMap::new();
272 let mut dropped = Vec::new();
273 let mut split_arcs = Vec::new();
274 for (satellite_id, mut arc) in arcs {
275 arc.sort_by_key(|sample| sample.epoch_index);
276 let (arc_entries, arc_cycles, arc_dropped, arc_splits) = prepare_wide_lane_arc(
277 &satellite_id,
278 &arc,
279 wide_lane,
280 cycle_slip_policy,
281 cycle_slip_options,
282 )?;
283 entries.extend(arc_entries);
284 cycles.extend(arc_cycles);
285 dropped.extend(arc_dropped);
286 split_arcs.extend(arc_splits);
287 }
288
289 dropped.sort();
290 dropped.dedup();
291 split_arcs.sort_by(|a, b| {
292 (a.satellite_id.as_str(), a.ambiguity_id.as_str())
293 .cmp(&(b.satellite_id.as_str(), b.ambiguity_id.as_str()))
294 });
295
296 Ok((
297 dual_epochs_from_entries(entries),
298 cycles,
299 dropped,
300 split_arcs,
301 ))
302}
303
304fn prepare_wide_lane_arc(
305 satellite_id: &str,
306 arc: &[DualArcSample<'_>],
307 wide_lane: WideLanePrepOptions,
308 cycle_slip_policy: CycleSlipPolicy,
309 cycle_slip_options: CycleSlipOptions,
310) -> Result<WideLaneArcPrepared, WideLanePrepError> {
311 let slips = cycle_slips_for_dual_arc(arc, cycle_slip_options);
312 match cycle_slip_policy {
313 CycleSlipPolicy::SplitArc if !slips.is_empty() => {
314 prepare_split_wide_lane_arc(satellite_id, arc, wide_lane, &slips)
315 }
316 _ if slips.is_empty() => {
317 let arc_id = AmbiguityId::new(satellite_id);
319 estimate_tagged_wide_lane_arc(&arc_id, &arc_id, arc, wide_lane, None).map(
320 |(entries, cycles, split_arc)| {
321 (entries, cycles, Vec::new(), split_arc.into_iter().collect())
322 },
323 )
324 }
325 CycleSlipPolicy::DropSatellite => Ok((
326 Vec::new(),
327 BTreeMap::new(),
328 vec![satellite_id.to_string()],
329 Vec::new(),
330 )),
331 CycleSlipPolicy::Error | CycleSlipPolicy::SplitArc => {
332 let slip = &slips[0];
333 Err(WideLanePrepError::CycleSlipDetected {
334 satellite_id: satellite_id.to_string(),
335 epoch_index: slip.epoch_index,
336 reasons: slip.reasons.clone(),
337 })
338 }
339 }
340}
341
342fn prepare_split_wide_lane_arc(
343 satellite_id: &str,
344 arc: &[DualArcSample<'_>],
345 wide_lane: WideLanePrepOptions,
346 slips: &[DualSlipEvent],
347) -> Result<WideLaneArcPrepared, WideLanePrepError> {
348 let slip_epochs = slips
349 .iter()
350 .map(|slip| slip.epoch_index)
351 .collect::<BTreeSet<_>>();
352 let segments = split_dual_arc(arc, &slip_epochs);
353 let mut entries = Vec::new();
354 let mut cycles = BTreeMap::new();
355 let dropped = Vec::new();
356 let mut split_arcs = Vec::new();
357
358 for (segment_idx, segment) in segments {
359 if segment.len() < wide_lane.min_epochs {
360 continue;
361 }
362 let ambiguity_id = split_ambiguity_id(satellite_id, segment_idx);
363 let split_arc = split_arc_metadata(satellite_id, &ambiguity_id, &segment);
364 let (arc_entries, arc_cycles, arc_split) = estimate_tagged_wide_lane_arc(
365 &ambiguity_id,
366 &ambiguity_id,
367 &segment,
368 wide_lane,
369 Some(split_arc),
370 )?;
371 entries.extend(arc_entries);
372 cycles.extend(arc_cycles);
373 split_arcs.extend(arc_split);
374 }
375
376 if cycles.is_empty() {
377 Ok((
378 Vec::new(),
379 BTreeMap::new(),
380 vec![satellite_id.to_string()],
381 split_arcs,
382 ))
383 } else {
384 Ok((entries, cycles, dropped, split_arcs))
385 }
386}
387
388fn estimate_tagged_wide_lane_arc(
389 error_id: &AmbiguityId,
390 ambiguity_id: &AmbiguityId,
391 arc: &[DualArcSample<'_>],
392 wide_lane: WideLanePrepOptions,
393 split_arc: Option<PppSplitArc>,
394) -> Result<TaggedWideLaneArc, WideLanePrepError> {
395 let fixed = estimate_wide_lane_integer(error_id, arc, wide_lane)?;
396 let entries = arc
397 .iter()
398 .map(|sample| {
399 let mut observation = sample.observation.clone();
400 observation.ambiguity_id = ambiguity_id.to_string();
401 (sample.epoch_index, observation)
402 })
403 .collect();
404 Ok((
405 entries,
406 BTreeMap::from([(ambiguity_id.to_string(), fixed)]),
407 split_arc,
408 ))
409}
410
411fn estimate_wide_lane_integer(
412 ambiguity_id: &AmbiguityId,
413 arc: &[DualArcSample<'_>],
414 wide_lane: WideLanePrepOptions,
415) -> Result<i64, WideLanePrepError> {
416 let mut cycles = Vec::with_capacity(arc.len());
417 for sample in arc {
418 let value = wide_lane_cycles(sample.observation).map_err(|reason| {
419 WideLanePrepError::WideLaneFailed {
420 ambiguity_id: ambiguity_id.to_string(),
421 reason,
422 }
423 })?;
424 cycles.push(value);
425 }
426
427 ambiguity::estimate_wide_lane_integer(&cycles, wide_lane.min_epochs, wide_lane.tolerance_cycles)
428 .map_err(|err| match err {
429 ambiguity::WideLaneEstimateError::TooFewEpochs { count, minimum } => {
430 WideLanePrepError::TooFewWideLaneEpochs {
431 ambiguity_id: ambiguity_id.to_string(),
432 count,
433 minimum,
434 }
435 }
436 ambiguity::WideLaneEstimateError::NotInteger {
437 mean_cycles,
438 fixed_cycles,
439 } => WideLanePrepError::WideLaneNotInteger {
440 ambiguity_id: ambiguity_id.to_string(),
441 mean_cycles,
442 fixed_cycles,
443 },
444 })
445}
446
447fn wide_lane_cycles(observation: &DualFrequencyObservation) -> Result<f64, CarrierPhaseError> {
448 crate::carrier_phase::wide_lane_cycles(
449 observation.phi1_cyc,
450 observation.phi2_cyc,
451 observation.p1_m,
452 observation.p2_m,
453 observation.f1_hz,
454 observation.f2_hz,
455 )
456}
457
458fn cycle_slips_for_dual_arc<'a>(
459 arc: &'a [DualArcSample<'a>],
460 options: CycleSlipOptions,
461) -> Vec<DualSlipEvent> {
462 let arc_epochs = arc
463 .iter()
464 .map(|sample| {
465 (
466 sample.epoch_index,
467 dual_arc_epoch(sample.observation, sample.gap_time_s),
468 )
469 })
470 .collect::<Vec<_>>();
471 ppp_cycle_slip_events(&arc_epochs, options)
472}
473
474fn ppp_cycle_slip_events(
475 arc: &[(usize, ArcEpoch)],
476 options: CycleSlipOptions,
477) -> Vec<DualSlipEvent> {
478 let arc_epochs = arc.iter().map(|(_, epoch)| *epoch).collect::<Vec<_>>();
479 let results = detect_cycle_slips(&arc_epochs, options).expect("validated cycle-slip arc");
480 let mut events = Vec::new();
481 let mut reference = None;
482 let mut pending = None;
483
484 for ((epoch_index, epoch), result) in arc.iter().zip(results) {
485 if !ppp_slip_reference_usable(&result) {
486 continue;
487 }
488
489 if let Some(candidate) = pending.take() {
490 let immediate = immediate_split_reasons(&result.reasons);
491 if !immediate.is_empty() {
492 events.push(DualSlipEvent {
493 epoch_index: *epoch_index,
494 reasons: result.reasons.clone(),
495 });
496 reference = Some(SlipReference::from_result(&result, epoch));
497 continue;
498 }
499 if confirms_gf_mw_slip(&candidate, &result, epoch, options) {
500 events.push(DualSlipEvent {
501 epoch_index: candidate.epoch_index,
502 reasons: candidate.reasons,
503 });
504 reference = Some(SlipReference::from_result(&result, epoch));
505 continue;
506 }
507 reference = Some(SlipReference::from_result(&result, epoch));
508 continue;
509 }
510
511 let immediate = immediate_split_reasons(&result.reasons);
512 if !immediate.is_empty() {
513 events.push(DualSlipEvent {
514 epoch_index: *epoch_index,
515 reasons: result.reasons.clone(),
516 });
517 reference = Some(SlipReference::from_result(&result, epoch));
518 continue;
519 }
520
521 let gf_mw = gf_mw_reasons(&result.reasons);
522 if !gf_mw.is_empty() {
523 if let Some(reference) = reference {
524 pending = Some(PendingGfMwSlip {
525 epoch_index: *epoch_index,
526 reasons: gf_mw,
527 reference,
528 });
529 } else {
530 reference = Some(SlipReference::from_result(&result, epoch));
531 }
532 continue;
533 }
534
535 reference = Some(SlipReference::from_result(&result, epoch));
536 }
537
538 if let Some(candidate) = pending {
539 events.push(DualSlipEvent {
540 epoch_index: candidate.epoch_index,
541 reasons: candidate.reasons,
542 });
543 }
544
545 events
546}
547
548fn ppp_slip_reference_usable(result: &SlipResult) -> bool {
549 !result.skipped && (result.gf_m.is_some() || result.mw_m.is_some())
550}
551
552fn immediate_split_reasons(reasons: &[SlipReason]) -> Vec<SlipReason> {
553 reasons
554 .iter()
555 .copied()
556 .filter(|reason| matches!(reason, SlipReason::Lli | SlipReason::DataGap))
557 .collect()
558}
559
560fn gf_mw_reasons(reasons: &[SlipReason]) -> Vec<SlipReason> {
561 reasons
562 .iter()
563 .copied()
564 .filter(|reason| {
565 matches!(
566 reason,
567 SlipReason::GeometryFree | SlipReason::MelbourneWubbena
568 )
569 })
570 .collect()
571}
572
573fn confirms_gf_mw_slip(
574 pending: &PendingGfMwSlip,
575 result: &SlipResult,
576 epoch: &ArcEpoch,
577 options: CycleSlipOptions,
578) -> bool {
579 pending.reasons.iter().any(|reason| match reason {
580 SlipReason::GeometryFree => {
581 geometry_free_crosses(result.gf_m, pending.reference.gf_m, options.gf_threshold_m)
582 }
583 SlipReason::MelbourneWubbena => melbourne_wubbena_crosses(
584 result.mw_m,
585 pending.reference.mw_m,
586 epoch
587 .f1_hz
588 .or(pending.reference.f1_hz)
589 .zip(epoch.f2_hz.or(pending.reference.f2_hz)),
590 options.mw_threshold_cycles,
591 ),
592 SlipReason::Lli | SlipReason::DataGap => false,
593 })
594}
595
596fn geometry_free_crosses(
597 current_m: Option<f64>,
598 reference_m: Option<f64>,
599 threshold_m: f64,
600) -> bool {
601 match (current_m, reference_m) {
602 (Some(current_m), Some(reference_m)) => (current_m - reference_m).abs() > threshold_m,
603 _ => false,
604 }
605}
606
607fn melbourne_wubbena_crosses(
608 current_m: Option<f64>,
609 reference_m: Option<f64>,
610 frequencies_hz: Option<(f64, f64)>,
611 threshold_cycles: f64,
612) -> bool {
613 let (Some(current_m), Some(reference_m), Some((f1_hz, f2_hz))) =
614 (current_m, reference_m, frequencies_hz)
615 else {
616 return false;
617 };
618 let Ok(lambda_wl) = wide_lane_wavelength(f1_hz, f2_hz) else {
619 return false;
620 };
621 ((current_m - reference_m).abs() / lambda_wl.abs()) > threshold_cycles
622}
623
624impl SlipReference {
625 fn from_result(result: &SlipResult, epoch: &ArcEpoch) -> Self {
626 Self {
627 gf_m: result.gf_m,
628 mw_m: result.mw_m,
629 f1_hz: epoch.f1_hz,
630 f2_hz: epoch.f2_hz,
631 }
632 }
633}
634
635fn dual_arc_epoch(observation: &DualFrequencyObservation, gap_time_s: Option<f64>) -> ArcEpoch {
636 ArcEpoch {
637 phi1_cycles: Some(observation.phi1_cyc),
638 phi2_cycles: Some(observation.phi2_cyc),
639 p1_m: Some(observation.p1_m),
640 p2_m: Some(observation.p2_m),
641 lli1: observation.lli1,
642 lli2: observation.lli2,
643 f1_hz: Some(observation.f1_hz),
644 f2_hz: Some(observation.f2_hz),
645 gap_time_s,
646 }
647}
648
649fn split_dual_arc<'a>(
650 arc: &'a [DualArcSample<'a>],
651 slip_epochs: &BTreeSet<usize>,
652) -> Vec<(usize, Vec<DualArcSample<'a>>)> {
653 let mut segments = Vec::new();
654 let mut current = Vec::new();
655 let mut current_idx = 1;
656 for sample in arc {
657 if slip_epochs.contains(&sample.epoch_index) {
658 if !current.is_empty() {
659 segments.push((current_idx, current));
660 }
661 current = vec![*sample];
662 current_idx += 1;
663 } else {
664 current.push(*sample);
665 }
666 }
667 if !current.is_empty() {
668 segments.push((current_idx, current));
669 }
670 segments
671}
672
673fn split_ambiguity_id(satellite_id: &str, segment_idx: usize) -> AmbiguityId {
674 AmbiguityId::new(format!("{satellite_id}#{segment_idx}"))
675}
676
677fn split_arc_metadata(
678 satellite_id: &str,
679 ambiguity_id: &AmbiguityId,
680 segment: &[DualArcSample<'_>],
681) -> PppSplitArc {
682 PppSplitArc {
683 satellite_id: satellite_id.to_string(),
684 ambiguity_id: ambiguity_id.to_string(),
685 start_epoch_index: segment.first().map(|s| s.epoch_index).unwrap_or(0),
686 end_epoch_index: segment.last().map(|s| s.epoch_index).unwrap_or(0),
687 n_epochs: segment.len(),
688 }
689}
690
691fn dual_epochs_from_entries(
692 entries: Vec<(usize, DualFrequencyObservation)>,
693) -> Vec<PreparedDualFrequencyEpoch> {
694 let mut by_epoch = BTreeMap::<usize, Vec<DualFrequencyObservation>>::new();
695 for (epoch_index, observation) in entries {
696 by_epoch.entry(epoch_index).or_default().push(observation);
697 }
698 by_epoch
699 .into_iter()
700 .map(|(epoch_index, mut observations)| {
701 observations.sort_by(|a, b| {
702 (a.satellite_id.as_str(), a.ambiguity_id.as_str())
703 .cmp(&(b.satellite_id.as_str(), b.ambiguity_id.as_str()))
704 });
705 PreparedDualFrequencyEpoch {
706 epoch_index,
707 observations,
708 }
709 })
710 .collect()
711}
712
713fn filter_dual_epochs_by_wide_lanes(
714 dual_epochs: &[PreparedDualFrequencyEpoch],
715 wide_lane_cycles: &BTreeMap<String, i64>,
716) -> Vec<PreparedDualFrequencyEpoch> {
717 let keep = wide_lane_cycles.keys().cloned().collect::<BTreeSet<_>>();
718 dual_epochs
719 .iter()
720 .filter_map(|epoch| {
721 let observations = epoch
722 .observations
723 .iter()
724 .filter(|observation| keep.contains(&observation.ambiguity_id))
725 .cloned()
726 .collect::<Vec<_>>();
727 if observations.is_empty() {
728 None
729 } else {
730 Some(PreparedDualFrequencyEpoch {
731 epoch_index: epoch.epoch_index,
732 observations,
733 })
734 }
735 })
736 .collect()
737}
738
739type IonosphereFreeNarrowLane = (
740 Vec<PreparedFloatEpoch>,
741 BTreeMap<String, f64>,
742 BTreeMap<String, f64>,
743);
744
745fn ionosphere_free_narrow_lane_epochs(
746 dual_epochs: &[PreparedDualFrequencyEpoch],
747 wide_lane_cycles: &BTreeMap<String, i64>,
748) -> Result<IonosphereFreeNarrowLane, WideLanePrepError> {
749 let params = narrow_lane_params(dual_epochs, wide_lane_cycles)?;
750 let if_epochs = ionosphere_free_epochs(dual_epochs)?;
751 let wavelengths_m = params
752 .iter()
753 .map(|(id, params)| (id.as_str().to_string(), params.wavelength_m))
754 .collect();
755 let offsets_m = params
756 .iter()
757 .map(|(id, params)| (id.as_str().to_string(), params.offset_m))
758 .collect();
759 Ok((if_epochs, wavelengths_m, offsets_m))
760}
761
762fn narrow_lane_params(
763 dual_epochs: &[PreparedDualFrequencyEpoch],
764 wide_lane_cycles: &BTreeMap<String, i64>,
765) -> Result<BTreeMap<AmbiguityId, NarrowLaneParams>, WideLanePrepError> {
766 let mut out = BTreeMap::new();
767 for observation in dual_epochs.iter().flat_map(|epoch| &epoch.observations) {
768 let ambiguity_id = AmbiguityId::new(observation.ambiguity_id.as_str());
769 let wide_lane = wide_lane_cycles
770 .get(ambiguity_id.as_str())
771 .copied()
772 .ok_or_else(|| {
773 WideLanePrepError::MissingWideLaneAmbiguity(ambiguity_id.as_str().to_string())
774 })?;
775 let params = narrow_lane_param(observation.f1_hz, observation.f2_hz, wide_lane as f64)?;
776 if let Some(prev) = out.get(&ambiguity_id) {
777 ensure_consistent_narrow_lane_params(&ambiguity_id, params, *prev)?;
778 } else {
779 out.insert(ambiguity_id, params);
780 }
781 }
782 Ok(out)
783}
784
785fn narrow_lane_param(
786 f1_hz: f64,
787 f2_hz: f64,
788 wide_lane_cycles: f64,
789) -> Result<NarrowLaneParams, WideLanePrepError> {
790 ambiguity::narrow_lane_params(f1_hz, f2_hz, wide_lane_cycles).map_err(|reason| {
791 WideLanePrepError::IonosphereFreeFailed {
792 satellite_id: String::new(),
793 reason,
794 }
795 })
796}
797
798fn ensure_consistent_narrow_lane_params(
799 ambiguity_id: &AmbiguityId,
800 params: NarrowLaneParams,
801 prev: NarrowLaneParams,
802) -> Result<(), WideLanePrepError> {
803 if ambiguity::frequencies_match(params.f1_hz, prev.f1_hz)
804 && ambiguity::frequencies_match(params.f2_hz, prev.f2_hz)
805 {
806 Ok(())
807 } else {
808 Err(WideLanePrepError::InconsistentFrequencies(
809 ambiguity_id.to_string(),
810 ))
811 }
812}
813
814fn ionosphere_free_epochs(
815 dual_epochs: &[PreparedDualFrequencyEpoch],
816) -> Result<Vec<PreparedFloatEpoch>, WideLanePrepError> {
817 dual_epochs
818 .iter()
819 .map(|epoch| {
820 Ok(PreparedFloatEpoch {
821 epoch_index: epoch.epoch_index,
822 observations: ionosphere_free_observations(&epoch.observations)?,
823 })
824 })
825 .collect()
826}
827
828fn ionosphere_free_observations(
829 observations: &[DualFrequencyObservation],
830) -> Result<Vec<PreparedFloatObservation>, WideLanePrepError> {
831 observations
832 .iter()
833 .map(|observation| {
834 let code_m = combinations::ionosphere_free(
835 observation.p1_m,
836 observation.p2_m,
837 observation.f1_hz,
838 observation.f2_hz,
839 )
840 .map_err(|reason| WideLanePrepError::IonosphereFreeFailed {
841 satellite_id: observation.satellite_id.clone(),
842 reason,
843 })?;
844 let phase_m = combinations::ionosphere_free_phase_cycles(
845 observation.phi1_cyc,
846 observation.phi2_cyc,
847 observation.f1_hz,
848 observation.f2_hz,
849 )
850 .map_err(|reason| WideLanePrepError::IonosphereFreeFailed {
851 satellite_id: observation.satellite_id.clone(),
852 reason,
853 })?;
854 Ok(PreparedFloatObservation {
855 satellite_id: observation.satellite_id.clone(),
856 ambiguity_id: observation.ambiguity_id.clone(),
857 code_m,
858 phase_m,
859 })
860 })
861 .collect()
862}
863
864#[derive(Clone, Copy)]
865struct FloatSlipSample<'a> {
866 epoch_index: usize,
867 gap_time_s: Option<f64>,
868 observation: &'a FloatCycleSlipObservation,
869}
870
871fn float_cycle_slip_tags(
872 epochs: &[FloatCycleSlipEpoch],
873 options: CycleSlipOptions,
874) -> BTreeMap<(usize, String), AmbiguityId> {
875 let mut arcs = BTreeMap::<String, Vec<FloatSlipSample<'_>>>::new();
876 for (epoch_index, epoch) in epochs.iter().enumerate() {
877 for observation in &epoch.observations {
878 arcs.entry(observation.satellite_id.clone())
879 .or_default()
880 .push(FloatSlipSample {
881 epoch_index,
882 gap_time_s: epoch.gap_time_s,
883 observation,
884 });
885 }
886 }
887
888 let mut tags = BTreeMap::new();
889 for (satellite_id, mut arc) in arcs {
890 arc.sort_by_key(|sample| sample.epoch_index);
891 tags.extend(float_arc_tags(&satellite_id, &arc, options));
892 }
893 tags
894}
895
896fn float_arc_tags(
897 satellite_id: &str,
898 arc: &[FloatSlipSample<'_>],
899 options: CycleSlipOptions,
900) -> BTreeMap<(usize, String), AmbiguityId> {
901 let carrier_phase_samples = float_carrier_phase_arc(arc);
902 if carrier_phase_samples.is_empty() {
903 return BTreeMap::new();
904 }
905 let slip_epochs = ppp_cycle_slip_events(&carrier_phase_samples, options)
906 .into_iter()
907 .map(|event| event.epoch_index)
908 .collect::<BTreeSet<_>>();
909 if slip_epochs.is_empty() {
910 return BTreeMap::new();
911 }
912
913 let mut out = BTreeMap::new();
914 for (segment_idx, segment) in split_float_arc(arc, &slip_epochs) {
915 let ambiguity_id = split_ambiguity_id(satellite_id, segment_idx);
916 for sample in segment {
917 out.insert(
918 (sample.epoch_index, satellite_id.to_string()),
919 ambiguity_id.clone(),
920 );
921 }
922 }
923 out
924}
925
926fn float_carrier_phase_arc(arc: &[FloatSlipSample<'_>]) -> Vec<(usize, ArcEpoch)> {
927 arc.iter()
928 .filter_map(|sample| {
929 let raw = sample.observation.raw.as_ref()?;
930 Some((sample.epoch_index, dual_arc_epoch(raw, sample.gap_time_s)))
931 })
932 .collect()
933}
934
935fn split_float_arc<'a>(
936 arc: &'a [FloatSlipSample<'a>],
937 slip_epochs: &BTreeSet<usize>,
938) -> Vec<(usize, Vec<FloatSlipSample<'a>>)> {
939 let mut segments = Vec::new();
940 let mut current = Vec::new();
941 let mut current_idx = 1;
942 for sample in arc {
943 if slip_epochs.contains(&sample.epoch_index) {
944 if !current.is_empty() {
945 segments.push((current_idx, current));
946 }
947 current = vec![*sample];
948 current_idx += 1;
949 } else {
950 current.push(*sample);
951 }
952 }
953 if !current.is_empty() {
954 segments.push((current_idx, current));
955 }
956 segments
957}