sidereon_core/
ambiguity.rs1use core::fmt;
17
18use crate::combinations::{self, IonosphereFreeError};
19use crate::constants::C_M_S;
20use crate::tolerances::FREQUENCY_MATCH_EPS_HZ;
21
22#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub(crate) struct AmbiguityId(String);
36
37impl AmbiguityId {
38 pub(crate) fn new(token: impl Into<String>) -> Self {
40 Self(token.into())
41 }
42
43 pub(crate) fn as_str(&self) -> &str {
45 &self.0
46 }
47
48 pub(crate) fn into_string(self) -> String {
50 self.0
51 }
52
53 pub(crate) fn assign(&mut self, token: &str) {
57 self.0.clear();
58 self.0.push_str(token);
59 }
60
61 pub(crate) fn clear(&mut self) {
64 self.0.clear();
65 }
66
67 pub(crate) fn push_str(&mut self, fragment: &str) {
70 self.0.push_str(fragment);
71 }
72}
73
74impl fmt::Display for AmbiguityId {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(&self.0)
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum CycleSlipPolicy {
83 Error,
85 DropSatellite,
87 SplitArc,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq)]
93pub(crate) struct NarrowLaneParams {
94 pub(crate) wavelength_m: f64,
95 pub(crate) offset_m: f64,
96 pub(crate) f1_hz: f64,
97 pub(crate) f2_hz: f64,
98}
99
100pub(crate) fn narrow_lane_params(
103 f1_hz: f64,
104 f2_hz: f64,
105 wide_lane_cycles: f64,
106) -> Result<NarrowLaneParams, IonosphereFreeError> {
107 let gamma = combinations::gamma(f1_hz, f2_hz)?;
108 let beta = gamma - 1.0;
109 let lambda2 = C_M_S / f2_hz;
110 Ok(NarrowLaneParams {
111 wavelength_m: C_M_S / (f1_hz + f2_hz),
112 offset_m: beta * lambda2 * wide_lane_cycles,
113 f1_hz,
114 f2_hz,
115 })
116}
117
118#[derive(Debug, Clone, Copy, PartialEq)]
120pub(crate) enum WideLaneEstimateError {
121 TooFewEpochs { count: usize, minimum: usize },
123 NotInteger { mean_cycles: f64, fixed_cycles: i64 },
125}
126
127pub(crate) fn estimate_wide_lane_integer(
130 cycles: &[f64],
131 min_epochs: usize,
132 tolerance_cycles: f64,
133) -> Result<i64, WideLaneEstimateError> {
134 if cycles.len() < min_epochs {
135 return Err(WideLaneEstimateError::TooFewEpochs {
136 count: cycles.len(),
137 minimum: min_epochs,
138 });
139 }
140
141 let mut sum = 0.0;
142 for &cycle in cycles {
143 sum += cycle;
144 }
145 let mean = sum / cycles.len() as f64;
146 let fixed = mean.round() as i64;
147
148 if (mean - fixed as f64).abs() <= tolerance_cycles {
149 Ok(fixed)
150 } else {
151 Err(WideLaneEstimateError::NotInteger {
152 mean_cycles: mean,
153 fixed_cycles: fixed,
154 })
155 }
156}
157
158pub(crate) fn frequencies_match(a: f64, b: f64) -> bool {
160 (a - b).abs() <= FREQUENCY_MATCH_EPS_HZ
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn narrow_lane_params_match_reference_bits() {
169 let f1 = f64::from_bits(0x41d779c018000000);
170 let f2 = f64::from_bits(0x41d24aec20000000);
171 let params = narrow_lane_params(f1, f2, 3.0).unwrap();
172 assert_eq!(params.wavelength_m.to_bits(), 0x3fbb614bed5136b9);
173 assert_eq!(params.offset_m.to_bits(), 0x3ff21e814dfd4618);
174 }
175
176 #[test]
177 fn wide_lane_estimate_rounds_and_gates() {
178 assert_eq!(
179 estimate_wide_lane_integer(&[2.95, 3.02, 3.0], 2, 0.25),
180 Ok(3)
181 );
182 assert_eq!(
183 estimate_wide_lane_integer(&[3.0], 2, 0.25),
184 Err(WideLaneEstimateError::TooFewEpochs {
185 count: 1,
186 minimum: 2,
187 })
188 );
189 assert!(matches!(
190 estimate_wide_lane_integer(&[3.4, 3.5, 3.6], 2, 0.05),
191 Err(WideLaneEstimateError::NotInteger { .. })
192 ));
193 }
194
195 #[test]
196 fn ambiguity_id_preserves_token_bytes() {
197 let id = AmbiguityId::new("G01:base=G01~ra1,rover=G01");
198 assert_eq!(id.as_str(), "G01:base=G01~ra1,rover=G01");
200 assert_eq!(id.to_string(), "G01:base=G01~ra1,rover=G01");
201 assert_eq!(id.clone().into_string(), "G01:base=G01~ra1,rover=G01");
203 assert!(AmbiguityId::new("G01") < AmbiguityId::new("G02"));
205 assert_eq!(AmbiguityId::new("E12"), AmbiguityId::new("E12"));
206 }
207
208 #[test]
209 fn ambiguity_id_in_place_assign_matches_owned_construction() {
210 let mut reused = AmbiguityId::default();
214 reused.assign("G07");
215 assert_eq!(reused, AmbiguityId::new("G07"));
216
217 reused.assign("G07:base=G07~ra1,rover=G07");
220 assert_eq!(reused.as_str(), "G07:base=G07~ra1,rover=G07");
221 reused.assign("E05");
222 assert_eq!(reused.as_str(), "E05");
223
224 let mut composed = AmbiguityId::default();
225 composed.clear();
226 composed.push_str("G07");
227 composed.push_str("|ref=");
228 composed.push_str("G04");
229 assert_eq!(composed, AmbiguityId::new("G07|ref=G04"));
230 }
231
232 #[test]
233 fn frequency_match_uses_named_tolerance() {
234 assert!(frequencies_match(1.0e9, 1.0e9 + 5.0e-7));
235 assert!(!frequencies_match(1.0e9, 1.0e9 + 1.0e-3));
236 }
237}