1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum ObservabilityTier {
6 RankDeficient,
9 ZeroRedundancy,
11 Weak,
14 Nominal,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct GeometryQuality {
21 pub tier: ObservabilityTier,
24 pub redundancy: i32,
26 pub rank: usize,
28 pub condition_number: f64,
31 pub gdop: f64,
33 pub raim_checkable: bool,
35 pub covariance_validated: bool,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct GeometryQualityThresholds {
51 pub cond_cutoff: f64,
54 pub gdop_cutoff: f64,
56}
57
58impl Default for GeometryQualityThresholds {
59 fn default() -> Self {
60 Self {
61 cond_cutoff: 1.0e8,
62 gdop_cutoff: 10.0,
63 }
64 }
65}
66
67pub fn classify(
76 rank: usize,
77 n_params: usize,
78 redundancy: i32,
79 condition_number: f64,
80 gdop: f64,
81 has_valid_prior: bool,
82 thresholds: GeometryQualityThresholds,
83) -> GeometryQuality {
84 let (tier, raim_checkable, covariance_validated) = if rank < n_params {
85 (ObservabilityTier::RankDeficient, false, false)
86 } else if redundancy == 0 {
87 (ObservabilityTier::ZeroRedundancy, false, has_valid_prior)
88 } else if redundancy >= 1
89 && (exceeds_cutoff(condition_number, thresholds.cond_cutoff)
90 || exceeds_cutoff(gdop, thresholds.gdop_cutoff))
91 {
92 (ObservabilityTier::Weak, true, true)
93 } else {
94 let raim_checkable = redundancy >= 1;
95 (
96 ObservabilityTier::Nominal,
97 raim_checkable,
98 raim_checkable || has_valid_prior,
99 )
100 };
101
102 GeometryQuality {
103 tier,
104 redundancy,
105 rank,
106 condition_number,
107 gdop,
108 raim_checkable,
109 covariance_validated,
110 }
111}
112
113fn exceeds_cutoff(value: f64, cutoff: f64) -> bool {
114 !value.is_finite() || !cutoff.is_finite() || value > cutoff
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
124
125 fn thresholds() -> GeometryQualityThresholds {
126 GeometryQualityThresholds {
127 cond_cutoff: 100.0,
128 gdop_cutoff: 10.0,
129 }
130 }
131
132 #[test]
133 fn zero_redundancy_without_prior_is_not_validated() {
134 let quality = classify(4, 4, 0, 3.0, 2.0, false, thresholds());
135
136 assert_eq!(
137 quality,
138 GeometryQuality {
139 tier: ObservabilityTier::ZeroRedundancy,
140 redundancy: 0,
141 rank: 4,
142 condition_number: 3.0,
143 gdop: 2.0,
144 raim_checkable: false,
145 covariance_validated: false,
146 }
147 );
148 }
149
150 #[test]
151 fn zero_redundancy_with_prior_is_validated() {
152 let quality = classify(4, 4, 0, 3.0, 2.0, true, thresholds());
153
154 assert_eq!(quality.tier, ObservabilityTier::ZeroRedundancy);
155 assert!(!quality.raim_checkable);
156 assert!(quality.covariance_validated);
157 }
158
159 #[test]
160 fn rank_deficient_disables_raim_and_covariance_validation() {
161 let quality = classify(3, 4, 2, 2.0e12, 30.0, true, thresholds());
162
163 assert_eq!(quality.tier, ObservabilityTier::RankDeficient);
164 assert_eq!(quality.rank, 3);
165 assert_eq!(quality.redundancy, 2);
166 assert!(!quality.raim_checkable);
167 assert!(!quality.covariance_validated);
168 }
169
170 #[test]
171 fn weak_when_condition_number_exceeds_cutoff() {
172 let quality = classify(4, 4, 1, 100.0 + 1.0e-9, 2.0, false, thresholds());
173
174 assert_eq!(quality.tier, ObservabilityTier::Weak);
175 assert!(quality.raim_checkable);
176 assert!(quality.covariance_validated);
177 }
178
179 #[test]
180 fn weak_when_gdop_exceeds_cutoff() {
181 let quality = classify(4, 4, 1, 3.0, 10.0 + 1.0e-12, false, thresholds());
182
183 assert_eq!(quality.tier, ObservabilityTier::Weak);
184 assert!(quality.raim_checkable);
185 assert!(quality.covariance_validated);
186 }
187
188 #[test]
189 fn nominal_with_full_rank_and_positive_redundancy() {
190 let quality = classify(4, 4, 2, 20.0, 4.0, false, thresholds());
191
192 assert_eq!(
193 quality,
194 GeometryQuality {
195 tier: ObservabilityTier::Nominal,
196 redundancy: 2,
197 rank: 4,
198 condition_number: 20.0,
199 gdop: 4.0,
200 raim_checkable: true,
201 covariance_validated: true,
202 }
203 );
204 }
205
206 #[test]
207 fn condition_cutoff_boundary_is_strict() {
208 let at_cutoff = classify(4, 4, 1, 100.0, 2.0, false, thresholds());
209 let below_cutoff = classify(4, 4, 1, 100.0 - 1.0e-9, 2.0, false, thresholds());
210 let above_cutoff = classify(4, 4, 1, 100.0 + 1.0e-9, 2.0, false, thresholds());
211
212 assert_eq!(at_cutoff.tier, ObservabilityTier::Nominal);
213 assert_eq!(below_cutoff.tier, ObservabilityTier::Nominal);
214 assert_eq!(above_cutoff.tier, ObservabilityTier::Weak);
215 }
216
217 #[test]
218 fn gdop_cutoff_boundary_is_strict() {
219 let at_cutoff = classify(4, 4, 1, 3.0, 10.0, false, thresholds());
220 let below_cutoff = classify(4, 4, 1, 3.0, 10.0 - 1.0e-12, false, thresholds());
221 let above_cutoff = classify(4, 4, 1, 3.0, 10.0 + 1.0e-12, false, thresholds());
222
223 assert_eq!(at_cutoff.tier, ObservabilityTier::Nominal);
224 assert_eq!(below_cutoff.tier, ObservabilityTier::Nominal);
225 assert_eq!(above_cutoff.tier, ObservabilityTier::Weak);
226 }
227}