1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5
6use crate::constants::C_M_S;
7pub use crate::dop::{dop, Dop, DopError, LineOfSight};
8use crate::ephemeris::{BroadcastEphemeris, Sp3};
9use crate::id::GnssSystem;
10pub use crate::quality::{
11 spp_robust_fde_driver, FdeError, FdeOptions, FdeResult, FdeSppError, FdeSppOptions,
12};
13use crate::rinex::observations::{pseudoranges, ObsEpochTime, ObservationFile, SignalPolicy};
14use crate::rtcm::{self, MsmKind};
15pub use crate::spp::{
16 residual_rms, solve, solve_broadcast, solve_doppler_velocity, solve_spp_batch_parallel,
17 solve_spp_batch_serial, solve_with_doppler_velocity, solve_with_fallback, solve_with_policy,
18 solve_with_solver, BroadcastReason, Corrections, DopplerObservation, DopplerVelocityInputs,
19 EphemerisSource, FallbackError, FixSource, GalileoNequickCoeffs, KlobucharCoeffs, Observation,
20 ReceiverSolution, RejectedSat, RejectionReason, RobustConfig, SolutionMetadata, SolveInputs,
21 SolvePolicy, SolvePolicyError, SourcedSolution, SppDopplerSolution, SppError, SurfaceMet,
22 DEFAULT_HUBER_K, DEFAULT_ROBUST_MAX_OUTER, DEFAULT_ROBUST_OUTER_TOL_M,
23 DEFAULT_ROBUST_SCALE_FLOOR_M, ELEVATION_MASK_RAD, SIGMA0_M, TRANSMIT_TIME_ITERATIONS,
24};
25pub use crate::static_positioning::{
26 solve_static, StaticClockBias, StaticCovariance, StaticEpoch, StaticEpochInfluence,
27 StaticInfluenceStatus, StaticResidual, StaticSatelliteBatchInfluence, StaticSatelliteInfluence,
28 StaticSolution, StaticSolutionMetadata, StaticSolveError, StaticSolveOptions,
29};
30pub use crate::static_reference_station::{
31 solve_static_reference_station_rinex, StaticReferenceCarrierRinexOptions,
32 StaticReferenceCarrierSolution, StaticReferenceCodeSolution, StaticReferenceEpochDiagnostic,
33 StaticReferenceFixStatus, StaticReferenceModeError, StaticReferenceModeReport,
34 StaticReferenceModeStatus, StaticReferenceStationCovariance, StaticReferenceStationError,
35 StaticReferenceStationMode, StaticReferenceStationRinexOptions, StaticReferenceStationSolution,
36};
37use crate::{astro::time, Error as CoreError, GnssSatelliteId};
38
39pub type Solution = ReceiverSolution;
41
42pub type Error = SppError;
44
45#[derive(Debug, Clone, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum RinexSppError {
50 Observation(CoreError),
52 MissingApproxPosition,
55}
56
57impl fmt::Display for RinexSppError {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Self::Observation(error) => write!(f, "RINEX SPP observation assembly failed: {error}"),
61 Self::MissingApproxPosition => {
62 f.write_str("RINEX SPP assembly needs APPROX POSITION XYZ or an initial guess")
63 }
64 }
65 }
66}
67
68impl std::error::Error for RinexSppError {}
69
70impl From<CoreError> for RinexSppError {
71 fn from(error: CoreError) -> Self {
72 Self::Observation(error)
73 }
74}
75
76#[derive(Debug, Clone, PartialEq)]
85pub struct RinexSppBroadcastCorrections {
86 pub klobuchar: KlobucharCoeffs,
89 pub beidou_klobuchar: Option<KlobucharCoeffs>,
92 pub galileo_nequick: Option<GalileoNequickCoeffs>,
94 pub glonass_channels: BTreeMap<u8, i8>,
96}
97
98impl Default for RinexSppBroadcastCorrections {
99 fn default() -> Self {
100 Self {
101 klobuchar: zero_klobuchar(),
102 beidou_klobuchar: None,
103 galileo_nequick: None,
104 glonass_channels: BTreeMap::new(),
105 }
106 }
107}
108
109pub trait RinexSppAssemblySource {
116 fn rinex_spp_broadcast_corrections(&self) -> RinexSppBroadcastCorrections;
118}
119
120impl RinexSppAssemblySource for BroadcastEphemeris {
121 fn rinex_spp_broadcast_corrections(&self) -> RinexSppBroadcastCorrections {
122 let iono = self.iono_corrections();
123 let gps = iono.gps.map(klobuchar_from_alpha_beta);
124 RinexSppBroadcastCorrections {
125 klobuchar: gps.unwrap_or_else(zero_klobuchar),
126 beidou_klobuchar: iono.beidou.map(klobuchar_from_alpha_beta),
127 galileo_nequick: iono.galileo,
128 glonass_channels: self.glonass_frequency_channels(),
129 }
130 }
131}
132
133impl RinexSppAssemblySource for Sp3 {
134 fn rinex_spp_broadcast_corrections(&self) -> RinexSppBroadcastCorrections {
135 RinexSppBroadcastCorrections::default()
136 }
137}
138
139pub struct RinexSppSource<'a, E: EphemerisSource + ?Sized> {
146 ephemeris: &'a E,
147 broadcast: Option<&'a BroadcastEphemeris>,
148}
149
150impl<'a, E: EphemerisSource + ?Sized> RinexSppSource<'a, E> {
151 #[must_use]
153 pub const fn new(ephemeris: &'a E) -> Self {
154 Self {
155 ephemeris,
156 broadcast: None,
157 }
158 }
159
160 #[must_use]
163 pub const fn with_broadcast_context(
164 ephemeris: &'a E,
165 broadcast: &'a BroadcastEphemeris,
166 ) -> Self {
167 Self {
168 ephemeris,
169 broadcast: Some(broadcast),
170 }
171 }
172
173 #[must_use]
175 pub const fn ephemeris(&self) -> &'a E {
176 self.ephemeris
177 }
178
179 #[must_use]
181 pub const fn broadcast_context(&self) -> Option<&'a BroadcastEphemeris> {
182 self.broadcast
183 }
184}
185
186impl<E: EphemerisSource + ?Sized> EphemerisSource for RinexSppSource<'_, E> {
187 fn position_clock_at_j2000_s(
188 &self,
189 sat: GnssSatelliteId,
190 t_j2000_s: f64,
191 ) -> Option<([f64; 3], f64)> {
192 self.ephemeris.position_clock_at_j2000_s(sat, t_j2000_s)
193 }
194}
195
196impl<E: EphemerisSource + ?Sized> RinexSppAssemblySource for RinexSppSource<'_, E> {
197 fn rinex_spp_broadcast_corrections(&self) -> RinexSppBroadcastCorrections {
198 self.broadcast
199 .map(RinexSppAssemblySource::rinex_spp_broadcast_corrections)
200 .unwrap_or_default()
201 }
202}
203
204#[derive(Debug, Clone, PartialEq)]
206pub struct RinexSppOptions {
207 pub signal_policy: SignalPolicy,
209 pub corrections: Corrections,
211 pub initial_guess: Option<[f64; 4]>,
215 pub satellites: Option<BTreeSet<GnssSatelliteId>>,
218 pub met: SurfaceMet,
220 pub robust: Option<RobustConfig>,
222}
223
224impl RinexSppOptions {
225 #[must_use]
227 pub fn new(signal_policy: SignalPolicy) -> Self {
228 Self {
229 signal_policy,
230 corrections: Corrections::IONO_TROPO,
231 initial_guess: None,
232 satellites: None,
233 met: SurfaceMet::default(),
234 robust: None,
235 }
236 }
237
238 pub fn default_for(obs: &ObservationFile) -> Result<Self, RinexSppError> {
241 Ok(Self::new(SignalPolicy::default_for(obs.header().version)?))
242 }
243
244 #[must_use]
246 pub const fn with_corrections(mut self, corrections: Corrections) -> Self {
247 self.corrections = corrections;
248 self
249 }
250
251 #[must_use]
253 pub const fn with_initial_guess(mut self, initial_guess: [f64; 4]) -> Self {
254 self.initial_guess = Some(initial_guess);
255 self
256 }
257
258 #[must_use]
260 pub fn with_satellites<I>(mut self, satellites: I) -> Self
261 where
262 I: IntoIterator<Item = GnssSatelliteId>,
263 {
264 self.satellites = Some(satellites.into_iter().collect());
265 self
266 }
267
268 #[must_use]
270 pub const fn with_surface_met(mut self, met: SurfaceMet) -> Self {
271 self.met = met;
272 self
273 }
274
275 #[must_use]
277 pub const fn with_robust(mut self, robust: Option<RobustConfig>) -> Self {
278 self.robust = robust;
279 self
280 }
281}
282
283#[derive(Debug, Clone)]
285pub struct RinexSppEpochInputs {
286 pub epoch_index: usize,
288 pub epoch: ObsEpochTime,
290 pub inputs: SolveInputs,
292}
293
294#[derive(Debug, Clone)]
296pub struct RinexSppEpochSolution {
297 pub epoch_index: usize,
299 pub epoch: ObsEpochTime,
301 pub solution: Result<ReceiverSolution, SolvePolicyError>,
303}
304
305#[derive(Debug, Clone)]
307pub struct RtcmSppEpochInputs {
308 pub epoch_index: usize,
310 pub epoch: ObsEpochTime,
312 pub inputs: SolveInputs,
314}
315
316pub fn spp_inputs_from_rtcm_msm<S, F>(
321 messages: &[rtcm::MsmMessage],
322 source: &S,
323 options: &RinexSppOptions,
324 mut map_epoch: F,
325) -> Result<Vec<RtcmSppEpochInputs>, RinexSppError>
326where
327 S: RinexSppAssemblySource + ?Sized,
328 F: FnMut(GnssSystem, u32) -> Option<(f64, ObsEpochTime)>,
329{
330 if messages.is_empty() {
331 return Ok(Vec::new());
332 }
333
334 let initial_guess = options.initial_guess.unwrap_or([0.0; 4]);
335 let base_corrections = merged_broadcast_corrections_from_source(source);
336 let mut groups = Vec::<(GnssSystem, u32, Vec<usize>)>::new();
337 let mut group_index = BTreeMap::<(GnssSystem, u32), usize>::new();
338
339 for (index, message) in messages.iter().enumerate() {
340 let key = (message.system, message.header.epoch_time);
341 let slot = if let Some(index) = group_index.get(&key) {
342 *index
343 } else {
344 let slot = groups.len();
345 groups.push((key.0, key.1, Vec::new()));
346 group_index.insert(key, slot);
347 slot
348 };
349 groups[slot].2.push(index);
350 }
351
352 let mut out = Vec::new();
353 for (epoch_index, (system, epoch_time, group_indexes)) in groups.into_iter().enumerate() {
354 let Some((t_rx_j2000_s, epoch)) = map_epoch(system, epoch_time) else {
355 continue;
356 };
357 let Some(preferred_codes) = options.signal_policy.codes.get(&system) else {
358 continue;
359 };
360 let preferred_codes = preferred_codes
361 .iter()
362 .map(String::as_str)
363 .collect::<Vec<_>>();
364
365 let Some(message_kind) = group_indexes
366 .first()
367 .and_then(|index| messages.get(*index))
368 .map(|message| message.kind)
369 else {
370 continue;
371 };
372
373 let mut by_satellite = BTreeMap::<u8, Vec<&rtcm::MsmSignal>>::new();
374 let mut satellite_cache = BTreeMap::<u8, rtcm::MsmSatellite>::new();
375 for message in group_indexes
376 .iter()
377 .copied()
378 .filter_map(|index| messages.get(index))
379 {
380 for satellite in &message.satellites {
381 satellite_cache.insert(satellite.id, *satellite);
382 }
383 for signal in &message.signals {
384 by_satellite
385 .entry(signal.satellite_id)
386 .or_default()
387 .push(signal);
388 }
389 }
390
391 let mut observations = Vec::new();
392 for (satellite_id, signals) in by_satellite {
393 let Some(satellite) = satellite_cache.get(&satellite_id) else {
394 continue;
395 };
396 let Some(pseudorange_m) = rtcm_msm_pseudorange_m(
397 system,
398 message_kind,
399 *satellite,
400 &signals,
401 &preferred_codes,
402 ) else {
403 continue;
404 };
405 if let Ok(satellite_id) = GnssSatelliteId::new(system, satellite_id) {
406 observations.push(Observation {
407 satellite_id,
408 pseudorange_m,
409 });
410 }
411 }
412
413 if observations.is_empty() {
414 continue;
415 }
416
417 let t_rx_second_of_day_s =
418 time::second_of_day(epoch.hour.into(), epoch.minute.into(), epoch.second);
419 let day_of_year = time::day_of_year(
420 epoch.year,
421 i32::from(epoch.month),
422 i32::from(epoch.day),
423 epoch.hour.into(),
424 epoch.minute.into(),
425 epoch.second,
426 );
427
428 out.push(RtcmSppEpochInputs {
429 epoch_index,
430 epoch,
431 inputs: SolveInputs {
432 observations,
433 t_rx_j2000_s,
434 t_rx_second_of_day_s,
435 day_of_year,
436 initial_guess,
437 corrections: options.corrections,
438 klobuchar: base_corrections.klobuchar,
439 beidou_klobuchar: base_corrections.beidou_klobuchar,
440 galileo_nequick: base_corrections.galileo_nequick,
441 sbas_iono: None,
442 glonass_channels: base_corrections.glonass_channels.clone(),
443 met: options.met,
444 robust: options.robust,
445 },
446 });
447 }
448
449 Ok(out)
450}
451
452fn rtcm_msm_pseudorange_m(
453 system: GnssSystem,
454 kind: MsmKind,
455 satellite: rtcm::MsmSatellite,
456 signals: &[&rtcm::MsmSignal],
457 preferred_codes: &[&str],
458) -> Option<f64> {
459 if satellite.rough_range_ms == 255 {
460 return None;
461 }
462 let rough_ms =
463 f64::from(satellite.rough_range_ms) + f64::from(satellite.rough_range_mod1) / 1024.0;
464
465 if let Some(signal) = select_rtcm_signal(system, signals, preferred_codes) {
466 let fine_ms = match kind {
467 MsmKind::Msm4 => {
468 if signal.fine_pseudorange == -16_384 {
469 f64::NAN
470 } else {
471 f64::from(signal.fine_pseudorange) / 2_f64.powi(24)
472 }
473 }
474 MsmKind::Msm7 => f64::from(signal.fine_pseudorange) / 2_f64.powi(29),
475 };
476 if fine_ms.is_finite() {
477 return Some((rough_ms + fine_ms) * 1.0e-3 * C_M_S);
478 }
479 }
480
481 None
482}
483
484fn select_rtcm_signal<'a>(
485 system: GnssSystem,
486 signals: &'a [&'a rtcm::MsmSignal],
487 preferred_codes: &[&'a str],
488) -> Option<&'a rtcm::MsmSignal> {
489 if signals.is_empty() {
490 return None;
491 }
492
493 if preferred_codes.is_empty() {
494 return signals.iter().copied().next();
495 }
496
497 preferred_codes
498 .iter()
499 .find_map(|requested| {
500 let normalized = requested
501 .strip_prefix('C')
502 .or_else(|| requested.strip_prefix('L'))
503 .unwrap_or(requested);
504 signals.iter().copied().find(|signal| {
505 let Some(code) = rtcm::msm_signal_rinex_code(system, signal.signal_id) else {
506 return false;
507 };
508 code == *requested || code == normalized
509 })
510 })
511 .or_else(|| signals.iter().copied().next())
512}
513
514pub fn spp_inputs_from_rinex_obs<S>(
525 obs: &ObservationFile,
526 source: &S,
527 options: &RinexSppOptions,
528) -> Result<Vec<RinexSppEpochInputs>, RinexSppError>
529where
530 S: RinexSppAssemblySource + ?Sized,
531{
532 let initial_guess = initial_guess(obs, options)?;
533 let base_corrections = merged_broadcast_corrections(obs, source);
534 let mut out = Vec::new();
535
536 for (epoch_index, epoch) in obs.epochs().iter().enumerate() {
537 if epoch.flag > 1 {
538 continue;
539 }
540 let mut selected = pseudoranges(obs, epoch, &options.signal_policy)?;
541 if let Some(allowed) = &options.satellites {
542 selected.retain(|(sat, _)| allowed.contains(sat));
543 }
544 if selected.is_empty() {
545 continue;
546 }
547
548 let epoch_context = epoch_time_context(epoch.epoch);
549 let observations = selected
550 .into_iter()
551 .map(|(satellite_id, pseudorange_m)| Observation {
552 satellite_id,
553 pseudorange_m,
554 })
555 .collect();
556
557 out.push(RinexSppEpochInputs {
558 epoch_index,
559 epoch: epoch.epoch,
560 inputs: SolveInputs {
561 observations,
562 t_rx_j2000_s: epoch_context.t_rx_j2000_s,
563 t_rx_second_of_day_s: epoch_context.t_rx_second_of_day_s,
564 day_of_year: epoch_context.day_of_year,
565 initial_guess,
566 corrections: options.corrections,
567 klobuchar: base_corrections.klobuchar,
568 beidou_klobuchar: base_corrections.beidou_klobuchar,
569 galileo_nequick: base_corrections.galileo_nequick,
570 sbas_iono: None,
571 glonass_channels: base_corrections.glonass_channels.clone(),
572 met: options.met,
573 robust: options.robust,
574 },
575 });
576 }
577
578 Ok(out)
579}
580
581pub fn solve_spp_from_rinex_obs<S>(
587 source: &S,
588 obs: &ObservationFile,
589 options: &RinexSppOptions,
590 with_geodetic: bool,
591 policy: SolvePolicy,
592) -> Result<Vec<RinexSppEpochSolution>, RinexSppError>
593where
594 S: EphemerisSource + RinexSppAssemblySource,
595{
596 let epochs = spp_inputs_from_rinex_obs(obs, source, options)?;
597 let inputs = epochs
598 .iter()
599 .map(|epoch| epoch.inputs.clone())
600 .collect::<Vec<_>>();
601 let results = solve_spp_batch_serial(source, &inputs, with_geodetic, policy);
602 Ok(epochs
603 .into_iter()
604 .zip(results)
605 .map(|(epoch, solution)| RinexSppEpochSolution {
606 epoch_index: epoch.epoch_index,
607 epoch: epoch.epoch,
608 solution,
609 })
610 .collect())
611}
612
613fn klobuchar_from_alpha_beta(value: crate::ephemeris::KlobucharAlphaBeta) -> KlobucharCoeffs {
614 KlobucharCoeffs {
615 alpha: value.alpha,
616 beta: value.beta,
617 }
618}
619
620const fn zero_klobuchar() -> KlobucharCoeffs {
621 KlobucharCoeffs {
622 alpha: [0.0; 4],
623 beta: [0.0; 4],
624 }
625}
626
627fn initial_guess(
628 obs: &ObservationFile,
629 options: &RinexSppOptions,
630) -> Result<[f64; 4], RinexSppError> {
631 if let Some(initial_guess) = options.initial_guess {
632 return Ok(initial_guess);
633 }
634 let approx = obs
635 .header()
636 .approx_position_m
637 .ok_or(RinexSppError::MissingApproxPosition)?;
638 Ok([approx[0], approx[1], approx[2], 0.0])
639}
640
641fn merged_broadcast_corrections<S>(
642 obs: &ObservationFile,
643 source: &S,
644) -> RinexSppBroadcastCorrections
645where
646 S: RinexSppAssemblySource + ?Sized,
647{
648 let mut corrections = source.rinex_spp_broadcast_corrections();
649 corrections.glonass_channels.extend(
650 obs.header()
651 .glonass_slots
652 .iter()
653 .map(|(&slot, &channel)| (slot, channel)),
654 );
655 corrections
656}
657
658fn merged_broadcast_corrections_from_source<S>(source: &S) -> RinexSppBroadcastCorrections
659where
660 S: RinexSppAssemblySource + ?Sized,
661{
662 source.rinex_spp_broadcast_corrections()
663}
664
665struct EpochTimeContext {
666 t_rx_j2000_s: f64,
667 t_rx_second_of_day_s: f64,
668 day_of_year: f64,
669}
670
671fn epoch_time_context(epoch: ObsEpochTime) -> EpochTimeContext {
672 let year = epoch.year;
673 let month = i32::from(epoch.month);
674 let day = i32::from(epoch.day);
675 let hour = i32::from(epoch.hour);
676 let minute = i32::from(epoch.minute);
677 EpochTimeContext {
678 t_rx_j2000_s: time::j2000_seconds(year, month, day, hour, minute, epoch.second),
679 t_rx_second_of_day_s: time::second_of_day(hour, minute, epoch.second),
680 day_of_year: time::day_of_year(year, month, day, hour, minute, epoch.second),
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687 use crate::rinex_obs::SignalPolicy;
688 use crate::rtcm::{MsmHeader, MsmKind, MsmMessage, MsmSatellite, MsmSignal};
689
690 #[derive(Default)]
691 struct NoCorrections;
692
693 impl RinexSppAssemblySource for NoCorrections {
694 fn rinex_spp_broadcast_corrections(&self) -> RinexSppBroadcastCorrections {
695 RinexSppBroadcastCorrections::default()
696 }
697 }
698
699 fn synthetic_rtcm_messages() -> Vec<MsmMessage> {
700 vec![MsmMessage {
701 message_number: 1074,
702 system: GnssSystem::Gps,
703 kind: MsmKind::Msm4,
704 header: MsmHeader {
705 reference_station_id: 0,
706 epoch_time: 12_345,
707 multiple_message: false,
708 iods: 1,
709 reserved: 0,
710 clock_steering: 0,
711 external_clock: 0,
712 divergence_free_smoothing: false,
713 smoothing_interval: 0,
714 },
715 satellites: vec![MsmSatellite {
716 id: 1,
717 rough_range_ms: 100,
718 rough_range_mod1: 512,
719 extended_info: None,
720 rough_phase_range_rate_m_s: None,
721 }],
722 signals: vec![
723 MsmSignal {
724 satellite_id: 1,
725 signal_id: 1,
726 fine_pseudorange: 1 << 24,
727 lock_time_indicator: 0,
728 half_cycle_ambiguity: false,
729 cnr: 0,
730 fine_phase_range: 0,
731 fine_phase_range_rate: None,
732 },
733 MsmSignal {
734 satellite_id: 1,
735 signal_id: 2,
736 fine_pseudorange: 0,
737 lock_time_indicator: 0,
738 half_cycle_ambiguity: false,
739 cnr: 0,
740 fine_phase_range: 0,
741 fine_phase_range_rate: None,
742 },
743 ],
744 }]
745 }
746
747 #[test]
748 fn rtcm_msm_helper_assembles_single_epoch_with_signal_selection() {
749 let messages = synthetic_rtcm_messages();
750 let options = RinexSppOptions::new(SignalPolicy::default_for(3.03).expect("policy"));
751 let inputs =
752 spp_inputs_from_rtcm_msm(&messages, &NoCorrections, &options, |_system, _raw| {
753 Some((
754 1_234_567.0,
755 ObsEpochTime {
756 year: 2026,
757 month: 7,
758 day: 7,
759 hour: 0,
760 minute: 0,
761 second: 0.0,
762 },
763 ))
764 })
765 .expect("convert");
766
767 assert_eq!(inputs.len(), 1);
768 let epoch = &inputs[0];
769 assert_eq!(epoch.inputs.observations.len(), 1);
770 assert_eq!(epoch.inputs.observations[0].satellite_id.to_string(), "G01");
771 assert_eq!(
772 epoch.inputs.observations[0].pseudorange_m as i64,
773 30_129_142
774 );
775 }
776}