Skip to main content

sidereon_core/precise_positioning/
prep.rs

1//! Wide-lane/narrow-lane and cycle-slip preparation for static PPP.
2//!
3//! Owns the dual-frequency ambiguity prep that runs ahead of the float/fixed
4//! solve: wide-lane integer estimation, ionosphere-free narrow-lane epoch
5//! construction, and cycle-slip arc segmentation (both the dual-frequency
6//! wide-lane path and the float ambiguity-id tagging path). These steps depend
7//! only on the raw observations, not on the least-squares solve, so they form a
8//! self-contained leaf that the parent module re-exports.
9
10use 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/// Raw dual-frequency PPP observation used by wide-lane/narrow-lane prep.
19#[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/// One raw dual-frequency PPP epoch.
34#[derive(Debug, Clone, PartialEq)]
35pub struct DualFrequencyEpoch {
36    /// Comparable epoch coordinate in seconds for data-gap cycle-slip checks.
37    pub gap_time_s: Option<f64>,
38    pub observations: Vec<DualFrequencyObservation>,
39}
40
41/// One ionosphere-free PPP observation emitted by dual-frequency prep.
42#[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/// One prepared ionosphere-free epoch.
51#[derive(Debug, Clone, PartialEq)]
52pub struct PreparedFloatEpoch {
53    pub epoch_index: usize,
54    pub observations: Vec<PreparedFloatObservation>,
55}
56
57/// Wide-lane and narrow-lane prep controls.
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub struct WideLanePrepOptions {
60    pub min_epochs: usize,
61    pub tolerance_cycles: f64,
62}
63
64/// Public split-arc metadata for PPP ambiguity segmentation.
65#[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/// Prepared dual-frequency PPP arc for the fixed wide-lane/narrow-lane path.
75#[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/// Error from PPP wide-lane/narrow-lane prep.
86#[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/// One float PPP observation with optional raw dual-frequency fields for
116/// cycle-slip ambiguity-tagging.
117#[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/// One float PPP epoch for cycle-slip ambiguity-tagging.
125#[derive(Debug, Clone, PartialEq)]
126pub struct FloatCycleSlipEpoch {
127    pub gap_time_s: Option<f64>,
128    pub observations: Vec<FloatCycleSlipObservation>,
129}
130
131/// One tagged float PPP observation returned by cycle-slip prep.
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct FloatCycleSlipTaggedObservation {
134    pub satellite_id: String,
135    pub ambiguity_id: String,
136}
137
138/// One tagged float PPP epoch returned by cycle-slip prep.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct FloatCycleSlipTaggedEpoch {
141    pub observations: Vec<FloatCycleSlipTaggedObservation>,
142}
143/// Prepare raw dual-frequency PPP observations for the wide-lane then
144/// narrow-lane fixed ambiguity path.
145pub 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}
166/// Rewrite float PPP ambiguity ids at detected dual-frequency cycle slips.
167pub 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            // An unslipped arc's ambiguity id is the bare satellite token.
302            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}