1use std::collections::{BTreeMap, BTreeSet};
11
12use crate::ambiguity::{self, AmbiguityId, CycleSlipPolicy, NarrowLaneParams};
13use crate::carrier_phase::{
14 detect_cycle_slips, ArcEpoch, CarrierPhaseError, CycleSlipOptions, SlipReason,
15};
16use crate::combinations::{self, IonosphereFreeError};
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct DualFrequencyObservation {
21 pub satellite_id: String,
22 pub ambiguity_id: String,
23 pub p1_m: f64,
24 pub p2_m: f64,
25 pub phi1_cyc: f64,
26 pub phi2_cyc: f64,
27 pub f1_hz: f64,
28 pub f2_hz: f64,
29 pub lli1: Option<i64>,
30 pub lli2: Option<i64>,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub struct DualFrequencyEpoch {
36 pub gap_time_s: Option<f64>,
38 pub observations: Vec<DualFrequencyObservation>,
39}
40
41#[derive(Debug, Clone, PartialEq)]
43pub struct PreparedFloatObservation {
44 pub satellite_id: String,
45 pub ambiguity_id: String,
46 pub code_m: f64,
47 pub phase_m: f64,
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct PreparedFloatEpoch {
53 pub epoch_index: usize,
54 pub observations: Vec<PreparedFloatObservation>,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq)]
59pub struct WideLanePrepOptions {
60 pub min_epochs: usize,
61 pub tolerance_cycles: f64,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct PppSplitArc {
67 pub satellite_id: String,
68 pub ambiguity_id: String,
69 pub start_epoch_index: usize,
70 pub end_epoch_index: usize,
71 pub n_epochs: usize,
72}
73
74#[derive(Debug, Clone, PartialEq)]
76pub struct WideLanePrepResult {
77 pub epochs: Vec<PreparedFloatEpoch>,
78 pub wavelengths_m: BTreeMap<String, f64>,
79 pub offsets_m: BTreeMap<String, f64>,
80 pub wide_lane_cycles: BTreeMap<String, i64>,
81 pub dropped_sats: Vec<String>,
82 pub split_arcs: Vec<PppSplitArc>,
83}
84
85#[derive(Debug, Clone, PartialEq)]
87pub enum WideLanePrepError {
88 CycleSlipDetected {
89 satellite_id: String,
90 epoch_index: usize,
91 reasons: Vec<SlipReason>,
92 },
93 WideLaneFailed {
94 ambiguity_id: String,
95 reason: CarrierPhaseError,
96 },
97 TooFewWideLaneEpochs {
98 ambiguity_id: String,
99 count: usize,
100 minimum: usize,
101 },
102 WideLaneNotInteger {
103 ambiguity_id: String,
104 mean_cycles: f64,
105 fixed_cycles: i64,
106 },
107 MissingWideLaneAmbiguity(String),
108 InconsistentFrequencies(String),
109 IonosphereFreeFailed {
110 satellite_id: String,
111 reason: IonosphereFreeError,
112 },
113}
114
115#[derive(Debug, Clone, PartialEq)]
118pub struct FloatCycleSlipObservation {
119 pub satellite_id: String,
120 pub ambiguity_id: String,
121 pub raw: Option<DualFrequencyObservation>,
122}
123
124#[derive(Debug, Clone, PartialEq)]
126pub struct FloatCycleSlipEpoch {
127 pub gap_time_s: Option<f64>,
128 pub observations: Vec<FloatCycleSlipObservation>,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct FloatCycleSlipTaggedObservation {
134 pub satellite_id: String,
135 pub ambiguity_id: String,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct FloatCycleSlipTaggedEpoch {
141 pub observations: Vec<FloatCycleSlipTaggedObservation>,
142}
143pub fn prepare_widelane_fixed_epochs(
146 epochs: &[DualFrequencyEpoch],
147 wide_lane: WideLanePrepOptions,
148 cycle_slip_policy: CycleSlipPolicy,
149 cycle_slip_options: CycleSlipOptions,
150) -> Result<WideLanePrepResult, WideLanePrepError> {
151 let (prepared_dual_epochs, wide_lane_cycles, dropped_sats, split_arcs) =
152 wide_lane_ambiguities(epochs, wide_lane, cycle_slip_policy, cycle_slip_options)?;
153 let filtered_dual_epochs =
154 filter_dual_epochs_by_wide_lanes(&prepared_dual_epochs, &wide_lane_cycles);
155 let (if_epochs, wavelengths_m, offsets_m) =
156 ionosphere_free_narrow_lane_epochs(&filtered_dual_epochs, &wide_lane_cycles)?;
157 Ok(WideLanePrepResult {
158 epochs: if_epochs,
159 wavelengths_m,
160 offsets_m,
161 wide_lane_cycles,
162 dropped_sats,
163 split_arcs,
164 })
165}
166pub fn split_float_cycle_slip_epochs(
168 epochs: &[FloatCycleSlipEpoch],
169 cycle_slip_options: CycleSlipOptions,
170) -> Vec<FloatCycleSlipTaggedEpoch> {
171 let tags = float_cycle_slip_tags(epochs, cycle_slip_options);
172 epochs
173 .iter()
174 .enumerate()
175 .map(|(epoch_index, epoch)| {
176 let mut observations = epoch
177 .observations
178 .iter()
179 .map(|obs| {
180 let ambiguity_id = tags
181 .get(&(epoch_index, obs.satellite_id.clone()))
182 .map(|id| id.as_str().to_string())
183 .unwrap_or_else(|| obs.ambiguity_id.clone());
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
216type WideLanePrepPieces = (
217 Vec<PreparedDualFrequencyEpoch>,
218 BTreeMap<String, i64>,
219 Vec<String>,
220 Vec<PppSplitArc>,
221);
222
223type TaggedWideLaneArc = (
224 Vec<(usize, DualFrequencyObservation)>,
225 BTreeMap<String, i64>,
226 Option<PppSplitArc>,
227);
228
229type WideLaneArcPrepared = (
230 Vec<(usize, DualFrequencyObservation)>,
231 BTreeMap<String, i64>,
232 Vec<String>,
233 Vec<PppSplitArc>,
234);
235
236fn wide_lane_ambiguities(
237 epochs: &[DualFrequencyEpoch],
238 wide_lane: WideLanePrepOptions,
239 cycle_slip_policy: CycleSlipPolicy,
240 cycle_slip_options: CycleSlipOptions,
241) -> Result<WideLanePrepPieces, WideLanePrepError> {
242 let mut arcs = BTreeMap::<String, Vec<DualArcSample<'_>>>::new();
243 for (epoch_index, epoch) in epochs.iter().enumerate() {
244 for observation in &epoch.observations {
245 arcs.entry(observation.satellite_id.clone())
246 .or_default()
247 .push(DualArcSample {
248 epoch_index,
249 gap_time_s: epoch.gap_time_s,
250 observation,
251 });
252 }
253 }
254
255 let mut entries = Vec::new();
256 let mut cycles = BTreeMap::new();
257 let mut dropped = Vec::new();
258 let mut split_arcs = Vec::new();
259 for (satellite_id, mut arc) in arcs {
260 arc.sort_by_key(|sample| sample.epoch_index);
261 let (arc_entries, arc_cycles, arc_dropped, arc_splits) = prepare_wide_lane_arc(
262 &satellite_id,
263 &arc,
264 wide_lane,
265 cycle_slip_policy,
266 cycle_slip_options,
267 )?;
268 entries.extend(arc_entries);
269 cycles.extend(arc_cycles);
270 dropped.extend(arc_dropped);
271 split_arcs.extend(arc_splits);
272 }
273
274 dropped.sort();
275 dropped.dedup();
276 split_arcs.sort_by(|a, b| {
277 (a.satellite_id.as_str(), a.ambiguity_id.as_str())
278 .cmp(&(b.satellite_id.as_str(), b.ambiguity_id.as_str()))
279 });
280
281 Ok((
282 dual_epochs_from_entries(entries),
283 cycles,
284 dropped,
285 split_arcs,
286 ))
287}
288
289fn prepare_wide_lane_arc(
290 satellite_id: &str,
291 arc: &[DualArcSample<'_>],
292 wide_lane: WideLanePrepOptions,
293 cycle_slip_policy: CycleSlipPolicy,
294 cycle_slip_options: CycleSlipOptions,
295) -> Result<WideLaneArcPrepared, WideLanePrepError> {
296 let slips = cycle_slips_for_dual_arc(arc, cycle_slip_options);
297 match cycle_slip_policy {
298 CycleSlipPolicy::SplitArc if !slips.is_empty() => {
299 prepare_split_wide_lane_arc(satellite_id, arc, wide_lane, &slips)
300 }
301 _ if slips.is_empty() => {
302 let arc_id = AmbiguityId::new(satellite_id);
304 estimate_tagged_wide_lane_arc(&arc_id, &arc_id, arc, wide_lane, None).map(
305 |(entries, cycles, split_arc)| {
306 (entries, cycles, Vec::new(), split_arc.into_iter().collect())
307 },
308 )
309 }
310 CycleSlipPolicy::DropSatellite => Ok((
311 Vec::new(),
312 BTreeMap::new(),
313 vec![satellite_id.to_string()],
314 Vec::new(),
315 )),
316 CycleSlipPolicy::Error | CycleSlipPolicy::SplitArc => {
317 let slip = &slips[0];
318 Err(WideLanePrepError::CycleSlipDetected {
319 satellite_id: satellite_id.to_string(),
320 epoch_index: slip.epoch_index,
321 reasons: slip.reasons.clone(),
322 })
323 }
324 }
325}
326
327fn prepare_split_wide_lane_arc(
328 satellite_id: &str,
329 arc: &[DualArcSample<'_>],
330 wide_lane: WideLanePrepOptions,
331 slips: &[DualSlipEvent],
332) -> Result<WideLaneArcPrepared, WideLanePrepError> {
333 let slip_epochs = slips
334 .iter()
335 .map(|slip| slip.epoch_index)
336 .collect::<BTreeSet<_>>();
337 let segments = split_dual_arc(arc, &slip_epochs);
338 let mut entries = Vec::new();
339 let mut cycles = BTreeMap::new();
340 let dropped = Vec::new();
341 let mut split_arcs = Vec::new();
342
343 for (segment_idx, segment) in segments {
344 if segment.len() < wide_lane.min_epochs {
345 continue;
346 }
347 let ambiguity_id = split_ambiguity_id(satellite_id, segment_idx);
348 let split_arc = split_arc_metadata(satellite_id, &ambiguity_id, &segment);
349 let (arc_entries, arc_cycles, arc_split) = estimate_tagged_wide_lane_arc(
350 &ambiguity_id,
351 &ambiguity_id,
352 &segment,
353 wide_lane,
354 Some(split_arc),
355 )?;
356 entries.extend(arc_entries);
357 cycles.extend(arc_cycles);
358 split_arcs.extend(arc_split);
359 }
360
361 if cycles.is_empty() {
362 Ok((
363 Vec::new(),
364 BTreeMap::new(),
365 vec![satellite_id.to_string()],
366 split_arcs,
367 ))
368 } else {
369 Ok((entries, cycles, dropped, split_arcs))
370 }
371}
372
373fn estimate_tagged_wide_lane_arc(
374 error_id: &AmbiguityId,
375 ambiguity_id: &AmbiguityId,
376 arc: &[DualArcSample<'_>],
377 wide_lane: WideLanePrepOptions,
378 split_arc: Option<PppSplitArc>,
379) -> Result<TaggedWideLaneArc, WideLanePrepError> {
380 let fixed = estimate_wide_lane_integer(error_id, arc, wide_lane)?;
381 let entries = arc
382 .iter()
383 .map(|sample| {
384 let mut observation = sample.observation.clone();
385 observation.ambiguity_id = ambiguity_id.to_string();
386 (sample.epoch_index, observation)
387 })
388 .collect();
389 Ok((
390 entries,
391 BTreeMap::from([(ambiguity_id.to_string(), fixed)]),
392 split_arc,
393 ))
394}
395
396fn estimate_wide_lane_integer(
397 ambiguity_id: &AmbiguityId,
398 arc: &[DualArcSample<'_>],
399 wide_lane: WideLanePrepOptions,
400) -> Result<i64, WideLanePrepError> {
401 let mut cycles = Vec::with_capacity(arc.len());
402 for sample in arc {
403 let value = wide_lane_cycles(sample.observation).map_err(|reason| {
404 WideLanePrepError::WideLaneFailed {
405 ambiguity_id: ambiguity_id.to_string(),
406 reason,
407 }
408 })?;
409 cycles.push(value);
410 }
411
412 ambiguity::estimate_wide_lane_integer(&cycles, wide_lane.min_epochs, wide_lane.tolerance_cycles)
413 .map_err(|err| match err {
414 ambiguity::WideLaneEstimateError::TooFewEpochs { count, minimum } => {
415 WideLanePrepError::TooFewWideLaneEpochs {
416 ambiguity_id: ambiguity_id.to_string(),
417 count,
418 minimum,
419 }
420 }
421 ambiguity::WideLaneEstimateError::NotInteger {
422 mean_cycles,
423 fixed_cycles,
424 } => WideLanePrepError::WideLaneNotInteger {
425 ambiguity_id: ambiguity_id.to_string(),
426 mean_cycles,
427 fixed_cycles,
428 },
429 })
430}
431
432fn wide_lane_cycles(observation: &DualFrequencyObservation) -> Result<f64, CarrierPhaseError> {
433 crate::carrier_phase::wide_lane_cycles(
434 observation.phi1_cyc,
435 observation.phi2_cyc,
436 observation.p1_m,
437 observation.p2_m,
438 observation.f1_hz,
439 observation.f2_hz,
440 )
441}
442
443fn cycle_slips_for_dual_arc<'a>(
444 arc: &'a [DualArcSample<'a>],
445 options: CycleSlipOptions,
446) -> Vec<DualSlipEvent> {
447 let arc_epochs = arc
448 .iter()
449 .map(|sample| dual_arc_epoch(sample.observation, sample.gap_time_s))
450 .collect::<Vec<_>>();
451 let results = detect_cycle_slips(&arc_epochs, options).expect("validated cycle-slip arc");
452 arc.iter()
453 .zip(results)
454 .filter_map(|(sample, result)| {
455 if result.slip {
456 Some(DualSlipEvent {
457 epoch_index: sample.epoch_index,
458 reasons: result.reasons,
459 })
460 } else {
461 None
462 }
463 })
464 .collect()
465}
466
467fn dual_arc_epoch(observation: &DualFrequencyObservation, gap_time_s: Option<f64>) -> ArcEpoch {
468 ArcEpoch {
469 phi1_cycles: Some(observation.phi1_cyc),
470 phi2_cycles: Some(observation.phi2_cyc),
471 p1_m: Some(observation.p1_m),
472 p2_m: Some(observation.p2_m),
473 lli1: observation.lli1,
474 lli2: observation.lli2,
475 f1_hz: Some(observation.f1_hz),
476 f2_hz: Some(observation.f2_hz),
477 gap_time_s,
478 }
479}
480
481fn split_dual_arc<'a>(
482 arc: &'a [DualArcSample<'a>],
483 slip_epochs: &BTreeSet<usize>,
484) -> Vec<(usize, Vec<DualArcSample<'a>>)> {
485 let mut segments = Vec::new();
486 let mut current = Vec::new();
487 let mut current_idx = 1;
488 for sample in arc {
489 if slip_epochs.contains(&sample.epoch_index) {
490 if !current.is_empty() {
491 segments.push((current_idx, current));
492 }
493 current = vec![*sample];
494 current_idx += 1;
495 } else {
496 current.push(*sample);
497 }
498 }
499 if !current.is_empty() {
500 segments.push((current_idx, current));
501 }
502 segments
503}
504
505fn split_ambiguity_id(satellite_id: &str, segment_idx: usize) -> AmbiguityId {
506 AmbiguityId::new(format!("{satellite_id}#{segment_idx}"))
507}
508
509fn split_arc_metadata(
510 satellite_id: &str,
511 ambiguity_id: &AmbiguityId,
512 segment: &[DualArcSample<'_>],
513) -> PppSplitArc {
514 PppSplitArc {
515 satellite_id: satellite_id.to_string(),
516 ambiguity_id: ambiguity_id.to_string(),
517 start_epoch_index: segment.first().map(|s| s.epoch_index).unwrap_or(0),
518 end_epoch_index: segment.last().map(|s| s.epoch_index).unwrap_or(0),
519 n_epochs: segment.len(),
520 }
521}
522
523fn dual_epochs_from_entries(
524 entries: Vec<(usize, DualFrequencyObservation)>,
525) -> Vec<PreparedDualFrequencyEpoch> {
526 let mut by_epoch = BTreeMap::<usize, Vec<DualFrequencyObservation>>::new();
527 for (epoch_index, observation) in entries {
528 by_epoch.entry(epoch_index).or_default().push(observation);
529 }
530 by_epoch
531 .into_iter()
532 .map(|(epoch_index, mut observations)| {
533 observations.sort_by(|a, b| {
534 (a.satellite_id.as_str(), a.ambiguity_id.as_str())
535 .cmp(&(b.satellite_id.as_str(), b.ambiguity_id.as_str()))
536 });
537 PreparedDualFrequencyEpoch {
538 epoch_index,
539 observations,
540 }
541 })
542 .collect()
543}
544
545fn filter_dual_epochs_by_wide_lanes(
546 dual_epochs: &[PreparedDualFrequencyEpoch],
547 wide_lane_cycles: &BTreeMap<String, i64>,
548) -> Vec<PreparedDualFrequencyEpoch> {
549 let keep = wide_lane_cycles.keys().cloned().collect::<BTreeSet<_>>();
550 dual_epochs
551 .iter()
552 .filter_map(|epoch| {
553 let observations = epoch
554 .observations
555 .iter()
556 .filter(|observation| keep.contains(&observation.ambiguity_id))
557 .cloned()
558 .collect::<Vec<_>>();
559 if observations.is_empty() {
560 None
561 } else {
562 Some(PreparedDualFrequencyEpoch {
563 epoch_index: epoch.epoch_index,
564 observations,
565 })
566 }
567 })
568 .collect()
569}
570
571type IonosphereFreeNarrowLane = (
572 Vec<PreparedFloatEpoch>,
573 BTreeMap<String, f64>,
574 BTreeMap<String, f64>,
575);
576
577fn ionosphere_free_narrow_lane_epochs(
578 dual_epochs: &[PreparedDualFrequencyEpoch],
579 wide_lane_cycles: &BTreeMap<String, i64>,
580) -> Result<IonosphereFreeNarrowLane, WideLanePrepError> {
581 let params = narrow_lane_params(dual_epochs, wide_lane_cycles)?;
582 let if_epochs = ionosphere_free_epochs(dual_epochs)?;
583 let wavelengths_m = params
584 .iter()
585 .map(|(id, params)| (id.as_str().to_string(), params.wavelength_m))
586 .collect();
587 let offsets_m = params
588 .iter()
589 .map(|(id, params)| (id.as_str().to_string(), params.offset_m))
590 .collect();
591 Ok((if_epochs, wavelengths_m, offsets_m))
592}
593
594fn narrow_lane_params(
595 dual_epochs: &[PreparedDualFrequencyEpoch],
596 wide_lane_cycles: &BTreeMap<String, i64>,
597) -> Result<BTreeMap<AmbiguityId, NarrowLaneParams>, WideLanePrepError> {
598 let mut out = BTreeMap::new();
599 for observation in dual_epochs.iter().flat_map(|epoch| &epoch.observations) {
600 let ambiguity_id = AmbiguityId::new(observation.ambiguity_id.as_str());
601 let wide_lane = wide_lane_cycles
602 .get(ambiguity_id.as_str())
603 .copied()
604 .ok_or_else(|| {
605 WideLanePrepError::MissingWideLaneAmbiguity(ambiguity_id.as_str().to_string())
606 })?;
607 let params = narrow_lane_param(observation.f1_hz, observation.f2_hz, wide_lane as f64)?;
608 if let Some(prev) = out.get(&ambiguity_id) {
609 ensure_consistent_narrow_lane_params(&ambiguity_id, params, *prev)?;
610 } else {
611 out.insert(ambiguity_id, params);
612 }
613 }
614 Ok(out)
615}
616
617fn narrow_lane_param(
618 f1_hz: f64,
619 f2_hz: f64,
620 wide_lane_cycles: f64,
621) -> Result<NarrowLaneParams, WideLanePrepError> {
622 ambiguity::narrow_lane_params(f1_hz, f2_hz, wide_lane_cycles).map_err(|reason| {
623 WideLanePrepError::IonosphereFreeFailed {
624 satellite_id: String::new(),
625 reason,
626 }
627 })
628}
629
630fn ensure_consistent_narrow_lane_params(
631 ambiguity_id: &AmbiguityId,
632 params: NarrowLaneParams,
633 prev: NarrowLaneParams,
634) -> Result<(), WideLanePrepError> {
635 if ambiguity::frequencies_match(params.f1_hz, prev.f1_hz)
636 && ambiguity::frequencies_match(params.f2_hz, prev.f2_hz)
637 {
638 Ok(())
639 } else {
640 Err(WideLanePrepError::InconsistentFrequencies(
641 ambiguity_id.to_string(),
642 ))
643 }
644}
645
646fn ionosphere_free_epochs(
647 dual_epochs: &[PreparedDualFrequencyEpoch],
648) -> Result<Vec<PreparedFloatEpoch>, WideLanePrepError> {
649 dual_epochs
650 .iter()
651 .map(|epoch| {
652 Ok(PreparedFloatEpoch {
653 epoch_index: epoch.epoch_index,
654 observations: ionosphere_free_observations(&epoch.observations)?,
655 })
656 })
657 .collect()
658}
659
660fn ionosphere_free_observations(
661 observations: &[DualFrequencyObservation],
662) -> Result<Vec<PreparedFloatObservation>, WideLanePrepError> {
663 observations
664 .iter()
665 .map(|observation| {
666 let code_m = combinations::ionosphere_free(
667 observation.p1_m,
668 observation.p2_m,
669 observation.f1_hz,
670 observation.f2_hz,
671 )
672 .map_err(|reason| WideLanePrepError::IonosphereFreeFailed {
673 satellite_id: observation.satellite_id.clone(),
674 reason,
675 })?;
676 let phase_m = combinations::ionosphere_free_phase_cycles(
677 observation.phi1_cyc,
678 observation.phi2_cyc,
679 observation.f1_hz,
680 observation.f2_hz,
681 )
682 .map_err(|reason| WideLanePrepError::IonosphereFreeFailed {
683 satellite_id: observation.satellite_id.clone(),
684 reason,
685 })?;
686 Ok(PreparedFloatObservation {
687 satellite_id: observation.satellite_id.clone(),
688 ambiguity_id: observation.ambiguity_id.clone(),
689 code_m,
690 phase_m,
691 })
692 })
693 .collect()
694}
695
696#[derive(Clone, Copy)]
697struct FloatSlipSample<'a> {
698 epoch_index: usize,
699 gap_time_s: Option<f64>,
700 observation: &'a FloatCycleSlipObservation,
701}
702
703fn float_cycle_slip_tags(
704 epochs: &[FloatCycleSlipEpoch],
705 options: CycleSlipOptions,
706) -> BTreeMap<(usize, String), AmbiguityId> {
707 let mut arcs = BTreeMap::<String, Vec<FloatSlipSample<'_>>>::new();
708 for (epoch_index, epoch) in epochs.iter().enumerate() {
709 for observation in &epoch.observations {
710 arcs.entry(observation.satellite_id.clone())
711 .or_default()
712 .push(FloatSlipSample {
713 epoch_index,
714 gap_time_s: epoch.gap_time_s,
715 observation,
716 });
717 }
718 }
719
720 let mut tags = BTreeMap::new();
721 for (satellite_id, mut arc) in arcs {
722 arc.sort_by_key(|sample| sample.epoch_index);
723 tags.extend(float_arc_tags(&satellite_id, &arc, options));
724 }
725 tags
726}
727
728fn float_arc_tags(
729 satellite_id: &str,
730 arc: &[FloatSlipSample<'_>],
731 options: CycleSlipOptions,
732) -> BTreeMap<(usize, String), AmbiguityId> {
733 let carrier_phase_samples = float_carrier_phase_arc(arc);
734 if carrier_phase_samples.is_empty() {
735 return BTreeMap::new();
736 }
737 let arc_epochs = carrier_phase_samples
738 .iter()
739 .map(|(_, epoch)| *epoch)
740 .collect::<Vec<_>>();
741 let slip_epochs = detect_cycle_slips(&arc_epochs, options)
742 .expect("validated cycle-slip arc")
743 .into_iter()
744 .zip(carrier_phase_samples.iter())
745 .filter_map(|(result, (epoch_index, _))| result.slip.then_some(*epoch_index))
746 .collect::<BTreeSet<_>>();
747 if slip_epochs.is_empty() {
748 return BTreeMap::new();
749 }
750
751 let mut out = BTreeMap::new();
752 for (segment_idx, segment) in split_float_arc(arc, &slip_epochs) {
753 let ambiguity_id = split_ambiguity_id(satellite_id, segment_idx);
754 for sample in segment {
755 out.insert(
756 (sample.epoch_index, satellite_id.to_string()),
757 ambiguity_id.clone(),
758 );
759 }
760 }
761 out
762}
763
764fn float_carrier_phase_arc(arc: &[FloatSlipSample<'_>]) -> Vec<(usize, ArcEpoch)> {
765 arc.iter()
766 .filter_map(|sample| {
767 let raw = sample.observation.raw.as_ref()?;
768 Some((sample.epoch_index, dual_arc_epoch(raw, sample.gap_time_s)))
769 })
770 .collect()
771}
772
773fn split_float_arc<'a>(
774 arc: &'a [FloatSlipSample<'a>],
775 slip_epochs: &BTreeSet<usize>,
776) -> Vec<(usize, Vec<FloatSlipSample<'a>>)> {
777 let mut segments = Vec::new();
778 let mut current = Vec::new();
779 let mut current_idx = 1;
780 for sample in arc {
781 if slip_epochs.contains(&sample.epoch_index) {
782 if !current.is_empty() {
783 segments.push((current_idx, current));
784 }
785 current = vec![*sample];
786 current_idx += 1;
787 } else {
788 current.push(*sample);
789 }
790 }
791 if !current.is_empty() {
792 segments.push((current_idx, current));
793 }
794 segments
795}