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, wide_lane_wavelength, ArcEpoch, CarrierPhaseError, CycleSlipOptions,
15    SlipReason, SlipResult,
16};
17use crate::combinations::{self, IonosphereFreeError};
18
19/// Raw dual-frequency PPP observation used by wide-lane/narrow-lane prep.
20#[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/// One raw dual-frequency PPP epoch.
35#[derive(Debug, Clone, PartialEq)]
36pub struct DualFrequencyEpoch {
37    /// Comparable epoch coordinate in seconds for data-gap cycle-slip checks.
38    pub gap_time_s: Option<f64>,
39    pub observations: Vec<DualFrequencyObservation>,
40}
41
42/// One ionosphere-free PPP observation emitted by dual-frequency prep.
43#[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/// One prepared ionosphere-free epoch.
52#[derive(Debug, Clone, PartialEq)]
53pub struct PreparedFloatEpoch {
54    pub epoch_index: usize,
55    pub observations: Vec<PreparedFloatObservation>,
56}
57
58/// Wide-lane and narrow-lane prep controls.
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct WideLanePrepOptions {
61    pub min_epochs: usize,
62    pub tolerance_cycles: f64,
63}
64
65/// Public split-arc metadata for PPP ambiguity segmentation.
66#[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/// Prepared dual-frequency PPP arc for the fixed wide-lane/narrow-lane path.
76#[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/// Error from PPP wide-lane/narrow-lane prep.
87#[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/// One float PPP observation with optional raw dual-frequency fields for
117/// cycle-slip ambiguity-tagging.
118#[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/// One float PPP epoch for cycle-slip ambiguity-tagging.
126#[derive(Debug, Clone, PartialEq)]
127pub struct FloatCycleSlipEpoch {
128    pub gap_time_s: Option<f64>,
129    pub observations: Vec<FloatCycleSlipObservation>,
130}
131
132/// One tagged float PPP observation returned by cycle-slip prep.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct FloatCycleSlipTaggedObservation {
135    pub satellite_id: String,
136    pub ambiguity_id: String,
137}
138
139/// One tagged float PPP epoch returned by cycle-slip prep.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct FloatCycleSlipTaggedEpoch {
142    pub observations: Vec<FloatCycleSlipTaggedObservation>,
143}
144/// Prepare raw dual-frequency PPP observations for the wide-lane then
145/// narrow-lane fixed ambiguity path.
146pub 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}
167/// Rewrite float PPP ambiguity ids at detected dual-frequency cycle slips.
168pub 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            // An unslipped arc's ambiguity id is the bare satellite token.
318            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}