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