1use std::collections::{BTreeMap, BTreeSet};
9
10use crate::astro::math::vec3;
11use crate::dgnss::{apply_corrections, pseudorange_corrections, solve_position, CodeObservation};
12use crate::dop::rotate_covariance_ecef_to_enu_m2;
13use crate::frame::{itrf_to_geodetic, ItrfPositionM, Wgs84Geodetic};
14use crate::observables::ObservableEphemerisSource;
15use crate::positioning::{
16 spp_inputs_from_rinex_obs, EphemerisSource, RinexSppAssemblySource, RinexSppEpochInputs,
17 RinexSppOptions, StaticEpoch, StaticSolution, StaticSolveOptions,
18};
19use crate::rinex::observations::{ObsEpochTime, RinexObs};
20use crate::rtk_filter::{
21 build_rinex_rtk_arc, IntegerStatus, RtkRinexArcOptions, RtkStaticArcConfig,
22 RtkStaticArcSolution,
23};
24use crate::spp::{Corrections, Observation};
25use crate::validate;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum StaticReferenceStationMode {
30 CodeDgnss,
32 CarrierFloat,
34 CarrierFixed,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum StaticReferenceFixStatus {
41 CodeDgnss,
43 CarrierFloat,
45 CarrierFixed,
47}
48
49impl From<StaticReferenceFixStatus> for crate::fusion::GnssFixStatus {
50 fn from(value: StaticReferenceFixStatus) -> Self {
51 match value {
52 StaticReferenceFixStatus::CodeDgnss => Self::Single,
53 StaticReferenceFixStatus::CarrierFloat => Self::Float,
54 StaticReferenceFixStatus::CarrierFixed => Self::Fixed,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum StaticReferenceModeStatus {
62 Solved,
64 Failed,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq)]
70pub struct StaticReferenceStationCovariance {
71 pub position_ecef_m2: [[f64; 3]; 3],
73 pub position_enu_m2: [[f64; 3]; 3],
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub struct StaticReferenceEpochDiagnostic {
80 pub mode: StaticReferenceStationMode,
82 pub epoch_index: usize,
84 pub used_satellites: Vec<String>,
86 pub rejected_satellite_count: usize,
88 pub code_residual_rms_m: Option<f64>,
90 pub phase_residual_rms_m: Option<f64>,
92 pub residual_rms_m: Option<f64>,
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub struct StaticReferenceModeReport {
99 pub mode: StaticReferenceStationMode,
101 pub status: StaticReferenceModeStatus,
103 pub used_epochs: usize,
105 pub skipped_epochs: usize,
107 pub used_measurements: usize,
109 pub error: Option<StaticReferenceModeError>,
111}
112
113#[derive(Debug, Clone, PartialEq)]
115pub enum StaticReferenceModeError {
116 RinexAssembly {
118 side: &'static str,
120 reason: String,
122 },
123 NoMatchedCodeEpochs,
125 CodeDgnss {
127 reason: String,
129 },
130 StaticSolve {
132 reason: String,
134 },
135 CarrierArc {
137 reason: String,
139 },
140 CarrierSolve {
142 reason: String,
144 },
145 Frame {
147 field: &'static str,
149 reason: String,
151 },
152 CorrectedObservation {
154 reason: String,
156 },
157 InvalidCorrectedSatelliteId {
159 satellite_id: String,
161 },
162}
163
164impl core::fmt::Display for StaticReferenceModeError {
165 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
166 match self {
167 Self::RinexAssembly { side, reason } => {
168 write!(f, "{side} RINEX assembly failed: {reason}")
169 }
170 Self::NoMatchedCodeEpochs => f.write_str("no matched epochs"),
171 Self::CodeDgnss { reason } => {
172 write!(f, "code-DGNSS failed: {reason}")
173 }
174 Self::StaticSolve { reason } => {
175 write!(f, "static code solve failed: {reason}")
176 }
177 Self::CarrierArc { reason } => {
178 write!(f, "carrier RTK arc failed: {reason}")
179 }
180 Self::CarrierSolve { reason } => {
181 write!(f, "carrier RTK solve failed: {reason}")
182 }
183 Self::Frame { field, reason } => {
184 write!(f, "{field} conversion failed: {reason}")
185 }
186 Self::CorrectedObservation { reason } => {
187 write!(f, "corrected observation failed: {reason}")
188 }
189 Self::InvalidCorrectedSatelliteId { satellite_id } => {
190 write!(f, "invalid corrected satellite id {satellite_id}")
191 }
192 }
193 }
194}
195
196impl std::error::Error for StaticReferenceModeError {}
197
198#[derive(Debug, Clone, PartialEq)]
200pub struct StaticReferenceCodeSolution {
201 pub position: ItrfPositionM,
203 pub geodetic: Option<Wgs84Geodetic>,
205 pub covariance: StaticReferenceStationCovariance,
207 pub static_solution: Option<StaticSolution>,
209 pub baseline_vector_m: [f64; 3],
211 pub baseline_m: f64,
213 pub diagnostics: Vec<StaticReferenceEpochDiagnostic>,
215}
216
217#[derive(Debug, Clone, PartialEq)]
219pub struct StaticReferenceCarrierSolution {
220 pub position: ItrfPositionM,
222 pub geodetic: Option<Wgs84Geodetic>,
224 pub covariance: StaticReferenceStationCovariance,
226 pub baseline_vector_m: [f64; 3],
228 pub baseline_m: f64,
230 pub integer_status: IntegerStatus,
232 pub integer_ratio: Option<f64>,
234 pub rtk_solution: RtkStaticArcSolution,
236 pub diagnostics: Vec<StaticReferenceEpochDiagnostic>,
238}
239
240#[derive(Debug, Clone, PartialEq)]
242pub struct StaticReferenceStationSolution {
243 pub mode: StaticReferenceStationMode,
245 pub fix_status: StaticReferenceFixStatus,
247 pub position: ItrfPositionM,
249 pub geodetic: Option<Wgs84Geodetic>,
251 pub covariance: StaticReferenceStationCovariance,
253 pub baseline_vector_m: [f64; 3],
255 pub baseline_m: f64,
257 pub code_solution: Option<StaticReferenceCodeSolution>,
259 pub carrier_solution: Option<StaticReferenceCarrierSolution>,
261 pub mode_reports: Vec<StaticReferenceModeReport>,
263 pub diagnostics: Vec<StaticReferenceEpochDiagnostic>,
265}
266
267#[derive(Debug, Clone, PartialEq)]
269pub struct StaticReferenceCarrierRinexOptions {
270 pub arc_options: RtkRinexArcOptions,
272 pub static_config: RtkStaticArcConfig,
275}
276
277#[derive(Debug, Clone, PartialEq)]
279pub struct StaticReferenceStationRinexOptions {
280 pub code_options: Option<RinexSppOptions>,
282 pub carrier_options: Option<StaticReferenceCarrierRinexOptions>,
284 pub with_geodetic: bool,
286}
287
288impl StaticReferenceStationRinexOptions {
289 #[must_use]
291 pub const fn code_and_carrier(
292 code_options: RinexSppOptions,
293 carrier_options: StaticReferenceCarrierRinexOptions,
294 with_geodetic: bool,
295 ) -> Self {
296 Self {
297 code_options: Some(code_options),
298 carrier_options: Some(carrier_options),
299 with_geodetic,
300 }
301 }
302
303 #[must_use]
305 pub const fn code_only(code_options: RinexSppOptions, with_geodetic: bool) -> Self {
306 Self {
307 code_options: Some(code_options),
308 carrier_options: None,
309 with_geodetic,
310 }
311 }
312
313 #[must_use]
315 pub const fn carrier_only(
316 carrier_options: StaticReferenceCarrierRinexOptions,
317 with_geodetic: bool,
318 ) -> Self {
319 Self {
320 code_options: None,
321 carrier_options: Some(carrier_options),
322 with_geodetic,
323 }
324 }
325}
326
327#[derive(Debug, Clone, PartialEq)]
329pub enum StaticReferenceStationError {
330 InvalidInput {
332 field: &'static str,
334 reason: &'static str,
336 },
337 NoEnabledModes,
339 AllModesFailed {
341 mode_reports: Vec<StaticReferenceModeReport>,
343 },
344}
345
346impl core::fmt::Display for StaticReferenceStationError {
347 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
348 match self {
349 Self::InvalidInput { field, reason } => {
350 write!(
351 f,
352 "invalid static reference-station input {field}: {reason}"
353 )
354 }
355 Self::NoEnabledModes => {
356 f.write_str("static reference-station solve has no enabled modes")
357 }
358 Self::AllModesFailed { mode_reports } => {
359 f.write_str("all static reference-station modes failed")?;
360 for report in mode_reports {
361 if let Some(error) = &report.error {
362 write!(f, "; {}: {error}", mode_label(report.mode))?;
363 } else {
364 write!(f, "; {}: failed", mode_label(report.mode))?;
365 }
366 }
367 Ok(())
368 }
369 }
370 }
371}
372
373impl std::error::Error for StaticReferenceStationError {}
374
375pub fn solve_static_reference_station_rinex<S>(
383 source: &S,
384 reference_obs: &RinexObs,
385 rover_obs: &RinexObs,
386 reference_position_m: [f64; 3],
387 options: &StaticReferenceStationRinexOptions,
388) -> Result<StaticReferenceStationSolution, StaticReferenceStationError>
389where
390 S: EphemerisSource + ObservableEphemerisSource + RinexSppAssemblySource,
391{
392 validate::finite_vec3(reference_position_m, "reference_position_m")
393 .map_err(static_reference_input_error)?;
394
395 if options.code_options.is_none() && options.carrier_options.is_none() {
396 return Err(StaticReferenceStationError::NoEnabledModes);
397 }
398
399 let mut reports = Vec::new();
400 let code = match &options.code_options {
401 Some(code_options) => match solve_code_dgnss_static(
402 source,
403 reference_obs,
404 rover_obs,
405 reference_position_m,
406 code_options,
407 options.with_geodetic,
408 ) {
409 Ok(solution) => {
410 reports.push(StaticReferenceModeReport {
411 mode: StaticReferenceStationMode::CodeDgnss,
412 status: StaticReferenceModeStatus::Solved,
413 used_epochs: solution.diagnostics.len(),
414 skipped_epochs: 0,
415 used_measurements: solution
416 .diagnostics
417 .iter()
418 .map(|row| row.used_satellites.len())
419 .sum(),
420 error: None,
421 });
422 Some(solution)
423 }
424 Err(error) => {
425 reports.push(failed_report(StaticReferenceStationMode::CodeDgnss, error));
426 None
427 }
428 },
429 None => None,
430 };
431
432 let carrier = match &options.carrier_options {
433 Some(carrier_options) => match solve_carrier_static(
434 source,
435 reference_obs,
436 rover_obs,
437 reference_position_m,
438 carrier_options,
439 options.with_geodetic,
440 ) {
441 Ok((solution, skipped_epochs)) => {
442 reports.push(StaticReferenceModeReport {
443 mode: solution_mode_from_carrier(&solution),
444 status: StaticReferenceModeStatus::Solved,
445 used_epochs: solution.diagnostics.len(),
446 skipped_epochs,
447 used_measurements: carrier_used_measurements(&solution),
448 error: None,
449 });
450 Some(solution)
451 }
452 Err(error) => {
453 reports.push(failed_report(
454 StaticReferenceStationMode::CarrierFixed,
455 error,
456 ));
457 None
458 }
459 },
460 None => None,
461 };
462
463 let selected = select_solution(reference_position_m, code, carrier, reports)?;
464 Ok(selected)
465}
466
467fn solve_code_dgnss_static<S>(
468 source: &S,
469 reference_obs: &RinexObs,
470 rover_obs: &RinexObs,
471 reference_position_m: [f64; 3],
472 code_options: &RinexSppOptions,
473 with_geodetic: bool,
474) -> Result<StaticReferenceCodeSolution, StaticReferenceModeError>
475where
476 S: EphemerisSource + ObservableEphemerisSource + RinexSppAssemblySource,
477{
478 let reference_epochs =
479 spp_inputs_from_rinex_obs(reference_obs, source, code_options).map_err(|error| {
480 StaticReferenceModeError::RinexAssembly {
481 side: "reference",
482 reason: error.to_string(),
483 }
484 })?;
485 let rover_epochs =
486 spp_inputs_from_rinex_obs(rover_obs, source, code_options).map_err(|error| {
487 StaticReferenceModeError::RinexAssembly {
488 side: "rover",
489 reason: error.to_string(),
490 }
491 })?;
492 let matched = matched_code_epochs(&reference_epochs, &rover_epochs);
493 if matched.is_empty() {
494 return Err(StaticReferenceModeError::NoMatchedCodeEpochs);
495 }
496
497 if matched.len() == 1 {
498 return solve_single_code_epoch(source, reference_position_m, matched[0], with_geodetic);
499 }
500
501 let static_options = StaticSolveOptions::from_solve_inputs(&matched[0].1.inputs, with_geodetic);
502 let mut static_epochs = Vec::with_capacity(matched.len());
503 for (reference_epoch, rover_epoch) in matched {
504 let corrected = corrected_rover_observations(
505 source,
506 reference_position_m,
507 reference_epoch,
508 rover_epoch,
509 )?;
510 let mut inputs = rover_epoch.inputs.clone();
511 inputs.observations = corrected;
512 inputs.corrections = Corrections::NONE;
513 let mut static_epoch = StaticEpoch::from_solve_inputs(inputs);
514 static_epoch.weights = Some(vec![0.5; static_epoch.measurements.len()]);
515 static_epochs.push(static_epoch);
516 }
517
518 let static_solution = crate::static_positioning::solve_static_without_influence(
519 source,
520 &static_epochs,
521 static_options,
522 )
523 .map_err(|error| StaticReferenceModeError::StaticSolve {
524 reason: error.to_string(),
525 })?;
526 let position = static_solution.position;
527 let covariance = StaticReferenceStationCovariance {
528 position_ecef_m2: static_solution.covariance.position_ecef_m2,
529 position_enu_m2: static_solution.covariance.position_enu_m2,
530 };
531 let baseline_vector_m = vec3::sub3(position.as_array(), reference_position_m);
532 let baseline_m = vec3::norm3(baseline_vector_m);
533 let diagnostics = code_static_diagnostics(&static_solution);
534
535 Ok(StaticReferenceCodeSolution {
536 position,
537 geodetic: static_solution.geodetic,
538 covariance,
539 static_solution: Some(static_solution),
540 baseline_vector_m,
541 baseline_m,
542 diagnostics,
543 })
544}
545
546fn solve_single_code_epoch<S>(
547 source: &S,
548 reference_position_m: [f64; 3],
549 matched: (&RinexSppEpochInputs, &RinexSppEpochInputs),
550 with_geodetic: bool,
551) -> Result<StaticReferenceCodeSolution, StaticReferenceModeError>
552where
553 S: EphemerisSource + ObservableEphemerisSource,
554{
555 let (reference_epoch, rover_epoch) = matched;
556 let reference_codes = code_observations(&reference_epoch.inputs.observations);
557 let rover_codes = code_observations(&rover_epoch.inputs.observations);
558 let solution = solve_position(
559 source,
560 reference_position_m,
561 &reference_codes,
562 &rover_codes,
563 rover_epoch.inputs.clone(),
564 with_geodetic,
565 )
566 .map_err(|error| StaticReferenceModeError::CodeDgnss {
567 reason: error.to_string(),
568 })?;
569 let covariance = StaticReferenceStationCovariance {
570 position_ecef_m2: solution.solution.position_covariance.ecef_m2,
571 position_enu_m2: solution.solution.position_covariance.enu_m2,
572 };
573 let position = solution.solution.position;
574 let diagnostics = vec![StaticReferenceEpochDiagnostic {
575 mode: StaticReferenceStationMode::CodeDgnss,
576 epoch_index: 0,
577 used_satellites: solution
578 .solution
579 .used_sats
580 .iter()
581 .map(ToString::to_string)
582 .collect(),
583 rejected_satellite_count: solution.solution.rejected_sats.len(),
584 code_residual_rms_m: Some(solution.solution.residual_rms_m()),
585 phase_residual_rms_m: None,
586 residual_rms_m: Some(solution.solution.residual_rms_m()),
587 }];
588
589 Ok(StaticReferenceCodeSolution {
590 position,
591 geodetic: solution.solution.geodetic,
592 covariance,
593 static_solution: None,
594 baseline_vector_m: solution.baseline_vector_m,
595 baseline_m: solution.baseline_m,
596 diagnostics,
597 })
598}
599
600fn solve_carrier_static<S>(
601 source: &S,
602 reference_obs: &RinexObs,
603 rover_obs: &RinexObs,
604 reference_position_m: [f64; 3],
605 carrier_options: &StaticReferenceCarrierRinexOptions,
606 with_geodetic: bool,
607) -> Result<(StaticReferenceCarrierSolution, usize), StaticReferenceModeError>
608where
609 S: ObservableEphemerisSource,
610{
611 let arc = build_rinex_rtk_arc(
612 source,
613 reference_obs,
614 rover_obs,
615 &carrier_options.arc_options,
616 )
617 .map_err(|error| StaticReferenceModeError::CarrierArc {
618 reason: error.to_string(),
619 })?;
620 let mut config = carrier_options.static_config.clone();
621 config.arc.base_m = reference_position_m;
622 config.arc.wavelengths_m = arc.wavelengths_m.clone();
623 config.arc.offsets_m = arc.offsets_m.clone();
624 let rtk_solution =
625 crate::rtk_filter::solve_static_rtk_arc(&arc.epochs, &config).map_err(|error| {
626 StaticReferenceModeError::CarrierSolve {
627 reason: error.to_string(),
628 }
629 })?;
630 let fixed = &rtk_solution.fixed_solution.fixed_solution;
631 let (baseline_vector_m, covariance_ecef_m2, diagnostics) =
632 if fixed.search.integer_status == IntegerStatus::Fixed {
633 (
634 fixed.baseline_m,
635 fixed.baseline_covariance_m2,
636 carrier_diagnostics(StaticReferenceStationMode::CarrierFixed, &fixed.residuals),
637 )
638 } else {
639 (
640 rtk_solution.float_solution.baseline_m,
641 rtk_solution.float_solution.baseline_covariance_m2,
642 carrier_diagnostics(
643 StaticReferenceStationMode::CarrierFloat,
644 &rtk_solution.float_solution.residuals,
645 ),
646 )
647 };
648 let position_m = vec3::add3(reference_position_m, baseline_vector_m);
649 let position =
650 ItrfPositionM::new(position_m[0], position_m[1], position_m[2]).map_err(|error| {
651 StaticReferenceModeError::Frame {
652 field: "position",
653 reason: error.to_string(),
654 }
655 })?;
656 let covariance = covariance_from_ecef(position, covariance_ecef_m2)?;
657 let geodetic = if with_geodetic {
658 Some(
659 itrf_to_geodetic(position).map_err(|error| StaticReferenceModeError::Frame {
660 field: "geodetic",
661 reason: error.to_string(),
662 })?,
663 )
664 } else {
665 None
666 };
667
668 Ok((
669 StaticReferenceCarrierSolution {
670 position,
671 geodetic,
672 covariance,
673 baseline_vector_m,
674 baseline_m: vec3::norm3(baseline_vector_m),
675 integer_status: fixed.search.integer_status,
676 integer_ratio: fixed.search.integer_ratio,
677 rtk_solution,
678 diagnostics,
679 },
680 arc.skipped_epoch_count,
681 ))
682}
683
684fn select_solution(
685 reference_position_m: [f64; 3],
686 code: Option<StaticReferenceCodeSolution>,
687 carrier: Option<StaticReferenceCarrierSolution>,
688 reports: Vec<StaticReferenceModeReport>,
689) -> Result<StaticReferenceStationSolution, StaticReferenceStationError> {
690 if code.is_none() && carrier.is_none() {
691 return Err(StaticReferenceStationError::AllModesFailed {
692 mode_reports: reports,
693 });
694 }
695
696 let (mode, position, geodetic, covariance, baseline_vector_m, mut baseline_m, diagnostics) =
697 match (carrier.as_ref(), code.as_ref()) {
698 (Some(carrier), _)
699 if solution_mode_from_carrier(carrier)
700 == StaticReferenceStationMode::CarrierFixed =>
701 {
702 (
703 StaticReferenceStationMode::CarrierFixed,
704 carrier.position,
705 carrier.geodetic,
706 carrier.covariance,
707 carrier.baseline_vector_m,
708 carrier.baseline_m,
709 carrier.diagnostics.clone(),
710 )
711 }
712 (_, Some(code)) => (
713 StaticReferenceStationMode::CodeDgnss,
714 code.position,
715 code.geodetic,
716 code.covariance,
717 code.baseline_vector_m,
718 code.baseline_m,
719 code.diagnostics.clone(),
720 ),
721 (Some(carrier), None) => (
722 StaticReferenceStationMode::CarrierFloat,
723 carrier.position,
724 carrier.geodetic,
725 carrier.covariance,
726 carrier.baseline_vector_m,
727 carrier.baseline_m,
728 carrier.diagnostics.clone(),
729 ),
730 (None, None) => unreachable!("handled above"),
731 };
732 let baseline_vector_m = if baseline_vector_m.iter().all(|value| value.is_finite()) {
733 baseline_vector_m
734 } else {
735 let fallback = vec3::sub3(position.as_array(), reference_position_m);
736 baseline_m = vec3::norm3(fallback);
737 fallback
738 };
739
740 Ok(StaticReferenceStationSolution {
741 mode,
742 fix_status: fix_status_from_mode(mode),
743 position,
744 geodetic,
745 covariance,
746 baseline_vector_m,
747 baseline_m,
748 code_solution: code,
749 carrier_solution: carrier,
750 mode_reports: reports,
751 diagnostics,
752 })
753}
754
755fn matched_code_epochs<'a>(
756 reference_epochs: &'a [RinexSppEpochInputs],
757 rover_epochs: &'a [RinexSppEpochInputs],
758) -> Vec<(&'a RinexSppEpochInputs, &'a RinexSppEpochInputs)> {
759 let rover_by_epoch = rover_epochs
760 .iter()
761 .map(|epoch| (epoch_key(epoch.epoch), epoch))
762 .collect::<BTreeMap<_, _>>();
763 reference_epochs
764 .iter()
765 .filter_map(|reference_epoch| {
766 rover_by_epoch
767 .get(&epoch_key(reference_epoch.epoch))
768 .map(|&rover_epoch| (reference_epoch, rover_epoch))
769 })
770 .collect()
771}
772
773fn corrected_rover_observations<S>(
774 source: &S,
775 reference_position_m: [f64; 3],
776 reference_epoch: &RinexSppEpochInputs,
777 rover_epoch: &RinexSppEpochInputs,
778) -> Result<Vec<Observation>, StaticReferenceModeError>
779where
780 S: ObservableEphemerisSource,
781{
782 let corrections = pseudorange_corrections(
783 source,
784 reference_position_m,
785 &code_observations(&reference_epoch.inputs.observations),
786 reference_epoch.inputs.t_rx_j2000_s,
787 )
788 .map_err(|error| StaticReferenceModeError::CodeDgnss {
789 reason: error.to_string(),
790 })?;
791 let corrected = apply_corrections(
792 &code_observations(&rover_epoch.inputs.observations),
793 &corrections,
794 )
795 .map_err(|error| StaticReferenceModeError::CorrectedObservation {
796 reason: error.to_string(),
797 })?;
798 corrected
799 .corrected
800 .into_iter()
801 .map(|obs| {
802 obs.satellite_id
803 .parse()
804 .map(|satellite_id| Observation {
805 satellite_id,
806 pseudorange_m: obs.pseudorange_m,
807 })
808 .map_err(|_| StaticReferenceModeError::InvalidCorrectedSatelliteId {
809 satellite_id: obs.satellite_id,
810 })
811 })
812 .collect()
813}
814
815fn code_observations(observations: &[Observation]) -> Vec<CodeObservation> {
816 observations
817 .iter()
818 .map(|obs| CodeObservation::new(obs.satellite_id.to_string(), obs.pseudorange_m))
819 .collect()
820}
821
822fn code_static_diagnostics(solution: &StaticSolution) -> Vec<StaticReferenceEpochDiagnostic> {
823 let mut residuals_by_epoch = BTreeMap::<usize, Vec<f64>>::new();
824 for residual in &solution.residuals_m {
825 residuals_by_epoch
826 .entry(residual.epoch_index)
827 .or_default()
828 .push(residual.residual_m);
829 }
830
831 solution
832 .used_sats
833 .iter()
834 .enumerate()
835 .map(|(epoch_index, sats)| {
836 let residuals = residuals_by_epoch
837 .get(&epoch_index)
838 .map(Vec::as_slice)
839 .unwrap_or(&[]);
840 let rms = residuals_rms(residuals.iter().copied());
841 StaticReferenceEpochDiagnostic {
842 mode: StaticReferenceStationMode::CodeDgnss,
843 epoch_index,
844 used_satellites: sats.iter().map(ToString::to_string).collect(),
845 rejected_satellite_count: solution
846 .rejected_sats
847 .get(epoch_index)
848 .map_or(0, Vec::len),
849 code_residual_rms_m: rms,
850 phase_residual_rms_m: None,
851 residual_rms_m: rms,
852 }
853 })
854 .collect()
855}
856
857fn carrier_diagnostics(
858 mode: StaticReferenceStationMode,
859 residuals: &[crate::rtk_filter::FloatResidual],
860) -> Vec<StaticReferenceEpochDiagnostic> {
861 #[derive(Default)]
862 struct Accum {
863 sats: BTreeSet<String>,
864 code: Vec<f64>,
865 phase: Vec<f64>,
866 }
867
868 let mut by_epoch = BTreeMap::<usize, Accum>::new();
869 for residual in residuals {
870 let entry = by_epoch.entry(residual.epoch_index).or_default();
871 entry.sats.insert(residual.satellite_id.clone());
872 entry.code.push(residual.code_m);
873 entry.phase.push(residual.phase_m);
874 }
875
876 by_epoch
877 .into_iter()
878 .map(|(epoch_index, accum)| {
879 let total = accum
880 .code
881 .iter()
882 .chain(accum.phase.iter())
883 .copied()
884 .collect::<Vec<_>>();
885 StaticReferenceEpochDiagnostic {
886 mode,
887 epoch_index,
888 used_satellites: accum.sats.into_iter().collect(),
889 rejected_satellite_count: 0,
890 code_residual_rms_m: residuals_rms(accum.code),
891 phase_residual_rms_m: residuals_rms(accum.phase),
892 residual_rms_m: residuals_rms(total),
893 }
894 })
895 .collect()
896}
897
898fn covariance_from_ecef(
899 position: ItrfPositionM,
900 position_ecef_m2: [[f64; 3]; 3],
901) -> Result<StaticReferenceStationCovariance, StaticReferenceModeError> {
902 let geodetic = itrf_to_geodetic(position).map_err(|error| StaticReferenceModeError::Frame {
903 field: "geodetic",
904 reason: error.to_string(),
905 })?;
906 let position_enu_m2 =
907 rotate_covariance_ecef_to_enu_m2(position_ecef_m2, geodetic).map_err(|error| {
908 StaticReferenceModeError::Frame {
909 field: "covariance",
910 reason: error.to_string(),
911 }
912 })?;
913 Ok(StaticReferenceStationCovariance {
914 position_ecef_m2,
915 position_enu_m2,
916 })
917}
918
919fn solution_mode_from_carrier(
920 solution: &StaticReferenceCarrierSolution,
921) -> StaticReferenceStationMode {
922 if solution.integer_status == IntegerStatus::Fixed {
923 StaticReferenceStationMode::CarrierFixed
924 } else {
925 StaticReferenceStationMode::CarrierFloat
926 }
927}
928
929fn carrier_used_measurements(solution: &StaticReferenceCarrierSolution) -> usize {
930 if solution.integer_status == IntegerStatus::Fixed {
931 solution
932 .rtk_solution
933 .fixed_solution
934 .fixed_solution
935 .n_observations
936 } else {
937 solution.rtk_solution.float_solution.n_observations
938 }
939}
940
941fn fix_status_from_mode(mode: StaticReferenceStationMode) -> StaticReferenceFixStatus {
942 match mode {
943 StaticReferenceStationMode::CodeDgnss => StaticReferenceFixStatus::CodeDgnss,
944 StaticReferenceStationMode::CarrierFloat => StaticReferenceFixStatus::CarrierFloat,
945 StaticReferenceStationMode::CarrierFixed => StaticReferenceFixStatus::CarrierFixed,
946 }
947}
948
949fn mode_label(mode: StaticReferenceStationMode) -> &'static str {
950 match mode {
951 StaticReferenceStationMode::CodeDgnss => "code-DGNSS",
952 StaticReferenceStationMode::CarrierFloat => "carrier-float",
953 StaticReferenceStationMode::CarrierFixed => "carrier-fixed",
954 }
955}
956
957fn residuals_rms(values: impl IntoIterator<Item = f64>) -> Option<f64> {
958 let mut sum = 0.0;
959 let mut count = 0usize;
960 for value in values {
961 sum += value * value;
962 count += 1;
963 }
964 (count > 0).then(|| (sum / count as f64).sqrt())
965}
966
967fn failed_report(
968 mode: StaticReferenceStationMode,
969 error: StaticReferenceModeError,
970) -> StaticReferenceModeReport {
971 StaticReferenceModeReport {
972 mode,
973 status: StaticReferenceModeStatus::Failed,
974 used_epochs: 0,
975 skipped_epochs: 0,
976 used_measurements: 0,
977 error: Some(error),
978 }
979}
980
981fn epoch_key(epoch: ObsEpochTime) -> (i32, u8, u8, u8, u8, u64) {
982 (
983 epoch.year,
984 epoch.month,
985 epoch.day,
986 epoch.hour,
987 epoch.minute,
988 epoch.second.to_bits(),
989 )
990}
991
992fn static_reference_input_error(error: validate::FieldError) -> StaticReferenceStationError {
993 StaticReferenceStationError::InvalidInput {
994 field: error.field(),
995 reason: error.reason(),
996 }
997}