sidereon_core/integrity/
mod.rs1use crate::astro::math::linear::invert_symmetric_pd;
8pub use crate::astro::math::special::{erfc_inv, normal_q, normal_q_inv};
9use crate::dop::ecef_to_enu_rotation;
10use crate::frame::Wgs84Geodetic;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct ErrorEllipse2 {
18 pub confidence: f64,
20 pub chi_square_scale: f64,
23 pub semi_major: f64,
25 pub semi_minor: f64,
27 pub orientation_rad: f64,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum IntegrityError {
34 NonFinite,
36 NotPositiveSemidefinite,
38 InvalidProbability {
40 reason: &'static str,
42 },
43 InvalidInput {
45 field: &'static str,
47 reason: &'static str,
49 },
50 Singular,
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub(crate) struct GainMatrix {
57 pub(crate) enu_rows: [Vec<f64>; 3],
58}
59
60pub fn error_ellipse_2x2(
67 covariance: [[f64; 2]; 2],
68 confidence: f64,
69) -> Result<ErrorEllipse2, IntegrityError> {
70 validate_probability(confidence)?;
71 let scale = -2.0 * (1.0 - confidence).ln();
72 error_ellipse_2x2_scaled(covariance, scale, confidence)
73}
74
75pub fn error_ellipse_2x2_unit(covariance: [[f64; 2]; 2]) -> Result<ErrorEllipse2, IntegrityError> {
81 error_ellipse_2x2_scaled(covariance, 1.0, 1.0 - (-0.5_f64).exp())
82}
83
84pub fn metric_sigma(gain_row: &[f64], sigmas_m: &[f64]) -> f64 {
86 metric_cross(gain_row, gain_row, sigmas_m).sqrt()
87}
88
89pub fn metric_cross(gain_row_a: &[f64], gain_row_b: &[f64], sigmas_m: &[f64]) -> f64 {
91 gain_row_a
92 .iter()
93 .zip(gain_row_b)
94 .zip(sigmas_m)
95 .map(|((&a, &b), &sigma)| a * b * sigma * sigma)
96 .sum()
97}
98
99pub(crate) fn gain_matrix_enu_from_design_rows(
100 design_rows: &[Vec<f64>],
101 weights: &[f64],
102 receiver: Wgs84Geodetic,
103 n_state: usize,
104) -> Result<GainMatrix, IntegrityError> {
105 if weights.len() != design_rows.len() {
106 return Err(invalid_input("weights", "length must match rows"));
107 }
108 if n_state < 3 {
109 return Err(invalid_input("n_state", "must include position columns"));
110 }
111 if weights.iter().filter(|&&weight| weight > 0.0).count() < n_state {
112 return Err(IntegrityError::Singular);
113 }
114
115 let mut normal = vec![vec![0.0_f64; n_state]; n_state];
116 for (row, &weight) in design_rows.iter().zip(weights) {
117 if row.len() != n_state {
118 return Err(invalid_input("rows", "length must match state dimension"));
119 }
120 if !weight.is_finite() || weight < 0.0 {
121 return Err(invalid_input("weights", "not finite non-negative"));
122 }
123 if row.iter().any(|value| !value.is_finite()) {
124 return Err(invalid_input("rows", "not finite"));
125 }
126 if weight > 0.0 {
127 for i in 0..n_state {
128 for j in 0..n_state {
129 normal[i][j] += row[i] * weight * row[j];
130 }
131 }
132 }
133 }
134
135 let inverse = invert_symmetric_pd(&normal).ok_or(IntegrityError::Singular)?;
136 let mut ecef_rows = [
137 vec![0.0; design_rows.len()],
138 vec![0.0; design_rows.len()],
139 vec![0.0; design_rows.len()],
140 ];
141 for (measurement_idx, (design, &weight)) in design_rows.iter().zip(weights).enumerate() {
142 if weight == 0.0 {
143 continue;
144 }
145 for state_idx in 0..3 {
146 let mut value = 0.0;
147 for col in 0..n_state {
148 value += inverse[state_idx][col] * design[col] * weight;
149 }
150 ecef_rows[state_idx][measurement_idx] = value;
151 }
152 }
153
154 let r = ecef_to_enu_rotation(receiver.lat_rad, receiver.lon_rad);
155 let mut enu_rows = [
156 vec![0.0; design_rows.len()],
157 vec![0.0; design_rows.len()],
158 vec![0.0; design_rows.len()],
159 ];
160 for coord in 0..3 {
161 for measurement_idx in 0..design_rows.len() {
162 enu_rows[coord][measurement_idx] = r[coord][0] * ecef_rows[0][measurement_idx]
163 + r[coord][1] * ecef_rows[1][measurement_idx]
164 + r[coord][2] * ecef_rows[2][measurement_idx];
165 }
166 }
167
168 Ok(GainMatrix { enu_rows })
169}
170
171fn error_ellipse_2x2_scaled(
172 covariance: [[f64; 2]; 2],
173 chi_square_scale: f64,
174 confidence: f64,
175) -> Result<ErrorEllipse2, IntegrityError> {
176 for row in covariance {
177 if row.iter().any(|value| !value.is_finite()) {
178 return Err(IntegrityError::NonFinite);
179 }
180 }
181
182 let a = covariance[0][0];
183 let b = 0.5 * (covariance[0][1] + covariance[1][0]);
184 let c = covariance[1][1];
185 let half_delta = 0.5 * (a - c);
186 let center = 0.5 * (a + c);
187 let root = (half_delta * half_delta + b * b).sqrt();
188 let lambda_major = center + root;
189 let lambda_minor = center - root;
190 if !lambda_major.is_finite() || !lambda_minor.is_finite() || lambda_minor < -1.0e-12 {
191 return Err(IntegrityError::NotPositiveSemidefinite);
192 }
193
194 let semi_major = (lambda_major.max(0.0) * chi_square_scale).sqrt();
195 let semi_minor = (lambda_minor.max(0.0) * chi_square_scale).sqrt();
196 let orientation_rad = if root == 0.0 {
197 0.0
198 } else {
199 0.5 * (2.0 * b).atan2(a - c)
200 };
201 Ok(ErrorEllipse2 {
202 confidence,
203 chi_square_scale,
204 semi_major,
205 semi_minor,
206 orientation_rad,
207 })
208}
209
210fn validate_probability(value: f64) -> Result<(), IntegrityError> {
211 if !value.is_finite() {
212 return Err(IntegrityError::InvalidProbability {
213 reason: "not finite",
214 });
215 }
216 if !(0.0..1.0).contains(&value) {
217 return Err(IntegrityError::InvalidProbability {
218 reason: "out of range",
219 });
220 }
221 Ok(())
222}
223
224fn invalid_input(field: &'static str, reason: &'static str) -> IntegrityError {
225 IntegrityError::InvalidInput { field, reason }
226}