1use crate::quality;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PrimitiveError {
22 InvalidInput {
24 field: &'static str,
26 reason: &'static str,
28 },
29}
30
31impl core::fmt::Display for PrimitiveError {
32 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33 match self {
34 Self::InvalidInput { field, reason } => {
35 write!(f, "invalid input {field}: {reason}")
36 }
37 }
38 }
39}
40
41impl std::error::Error for PrimitiveError {}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct AlphaBetaState {
46 pub level: f64,
48 pub rate: f64,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct AlphaBetaGains {
55 pub alpha: f64,
57 pub beta: f64,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct AlphaBetaStep {
64 pub predicted: AlphaBetaState,
66 pub updated: AlphaBetaState,
68 pub innovation: f64,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq)]
74pub struct ScalarKalmanGains {
75 pub position_gain: f64,
77 pub rate_gain: f64,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct NisGate {
84 pub nis: f64,
86 pub threshold: f64,
88 pub in_gate: bool,
90 pub dof: usize,
92}
93
94pub const MAD_GAUSSIAN_CONSISTENCY: f64 = 1.482_602_218_505_602;
96
97pub fn alpha_beta_steady_state_gains(
113 tracking_index: f64,
114) -> Result<AlphaBetaGains, PrimitiveError> {
115 validate_finite_positive(tracking_index, "tracking_index")?;
116 let alpha = alpha_beta_steady_state_alpha(tracking_index)?;
117 let beta_sq = tracking_index * (1.0 - alpha);
118 let beta = beta_sq.sqrt();
119 Ok(AlphaBetaGains { alpha, beta })
120}
121
122pub fn alpha_beta_predict(
128 state: AlphaBetaState,
129 dt: f64,
130) -> Result<AlphaBetaState, PrimitiveError> {
131 validate_finite_positive(dt, "dt")?;
132 Ok(AlphaBetaState {
133 level: state.level + dt * state.rate,
134 rate: state.rate,
135 })
136}
137
138pub fn alpha_beta_apply_measurement(
145 predicted: AlphaBetaState,
146 measurement: f64,
147 dt: f64,
148 gains: AlphaBetaGains,
149) -> Result<AlphaBetaState, PrimitiveError> {
150 validate_finite(predicted.level, "predicted_level")?;
151 validate_finite(predicted.rate, "predicted_rate")?;
152 validate_finite(measurement, "measurement")?;
153 validate_finite_positive(gains.alpha, "alpha")?;
154 validate_finite_nonnegative(gains.beta, "beta")?;
155 if gains.alpha > 1.0 {
156 return Err(invalid_input("alpha", "must be <= 1"));
157 }
158 validate_finite_positive(dt, "dt")?;
159
160 let innovation = measurement - predicted.level;
161 Ok(AlphaBetaState {
162 level: predicted.level + gains.alpha * innovation,
163 rate: predicted.rate + (gains.beta / dt) * innovation,
164 })
165}
166
167pub fn alpha_beta_filter_step(
169 state: AlphaBetaState,
170 measurement: f64,
171 dt: f64,
172 gains: AlphaBetaGains,
173) -> Result<AlphaBetaStep, PrimitiveError> {
174 let predicted = alpha_beta_predict(state, dt)?;
175 let updated = alpha_beta_apply_measurement(predicted, measurement, dt, gains)?;
176 let innovation = measurement - predicted.level;
177 Ok(AlphaBetaStep {
178 predicted,
179 updated,
180 innovation,
181 })
182}
183
184pub fn kalman_cv_steady_state_gains(
197 tracking_index: f64,
198 dt: f64,
199 measurement_variance: f64,
200) -> Result<ScalarKalmanGains, PrimitiveError> {
201 validate_finite_positive(tracking_index, "tracking_index")?;
202 validate_finite_positive(dt, "dt")?;
203 validate_finite_positive(measurement_variance, "measurement_variance")?;
204
205 let q = tracking_index * measurement_variance / dt.powi(3);
206 let q11 = q * dt;
207 let q10 = q * dt * dt / 2.0;
208 let q00 = q * dt * dt * dt / 3.0;
209
210 let mut p00 = measurement_variance;
211 let mut p01 = 0.0;
212 let mut p11 = measurement_variance;
213 for _ in 0..5_000 {
214 let p00_pred = p00 + 2.0 * dt * p01 + dt * dt * p11 + q00;
215 let p01_pred = p01 + dt * p11 + q10;
216 let p11_pred = p11 + q11;
217
218 let s = p00_pred + measurement_variance;
219 validate_finite_positive(s, "measurement_gate_denominator")?;
220
221 let k0 = p00_pred / s;
222 let k1 = p01_pred / s;
223
224 let p00_next = (1.0 - k0) * p00_pred;
225 let p01_next = p01_pred - k1 * p00_pred;
226 let p11_next = p11_pred - k1 * p01_pred;
227
228 let delta = (p00_next - p00)
229 .abs()
230 .max((p01_next - p01).abs())
231 .max((p11_next - p11).abs());
232 p00 = p00_next;
233 p01 = p01_next;
234 p11 = p11_next;
235
236 if delta <= 1.0e-15 * measurement_variance {
237 break;
238 }
239 }
240
241 let p00_pred = p00 + 2.0 * dt * p01 + dt * dt * p11 + q00;
242 let p01_pred = p01 + dt * p11 + q10;
243 let s = p00_pred + measurement_variance;
244 let position_gain = p00_pred / s;
245 let rate_gain = p01_pred / s;
246 Ok(ScalarKalmanGains {
247 position_gain,
248 rate_gain,
249 })
250}
251
252pub fn normalized_innovation(
254 innovation: f64,
255 innovation_variance: f64,
256) -> Result<f64, PrimitiveError> {
257 validate_finite(innovation, "innovation")?;
258 validate_finite_positive(innovation_variance, "innovation_variance")?;
259 Ok(innovation / innovation_variance.sqrt())
260}
261
262pub fn nis_statistic(innovation: f64, innovation_variance: f64) -> Result<f64, PrimitiveError> {
264 let normalized = normalized_innovation(innovation, innovation_variance)?;
265 Ok(normalized * normalized)
266}
267
268pub fn nis_expected_value(dof: usize) -> Result<f64, PrimitiveError> {
270 if dof == 0 {
271 return Err(invalid_input("dof", "must be positive"));
272 }
273 Ok(dof as f64)
274}
275
276pub fn nis_gate_threshold(dof: usize, confidence: f64) -> Result<f64, PrimitiveError> {
278 if dof == 0 {
279 return Err(invalid_input("dof", "must be positive"));
280 }
281 validate_probability(confidence, "confidence")?;
282 quality::chi2_inv(confidence, dof).map_err(|_| invalid_input("confidence", "no chi2 inverse"))
283}
284
285pub fn nis_gate_test(
287 innovation: f64,
288 innovation_variance: f64,
289 dof: usize,
290 confidence: f64,
291) -> Result<NisGate, PrimitiveError> {
292 let nis = nis_statistic(innovation, innovation_variance)?;
293 let threshold = nis_gate_threshold(dof, confidence)?;
294 Ok(NisGate {
295 nis,
296 threshold,
297 in_gate: nis <= threshold,
298 dof,
299 })
300}
301
302pub fn ewma_update(previous: f64, sample: f64, alpha: f64) -> Result<f64, PrimitiveError> {
304 validate_finite(previous, "previous")?;
305 validate_finite(sample, "sample")?;
306 validate_fraction_or_one(alpha, "alpha")?;
307 Ok(previous + alpha * (sample - previous))
308}
309
310pub fn ewma_update_power_of_two(
316 previous: f64,
317 sample: f64,
318 shift: u32,
319) -> Result<f64, PrimitiveError> {
320 validate_finite(previous, "previous")?;
321 validate_finite(sample, "sample")?;
322 if shift > 52 {
323 return Err(invalid_input("shift", "must be <= 52"));
324 }
325 let denominator = 1u64 << shift;
326 let denominator = denominator as f64;
327 let gain = 1.0 / denominator;
328 if (denominator - 1.0).abs() < f64::EPSILON {
329 Ok(sample)
330 } else {
331 Ok(previous + gain * (sample - previous))
332 }
333}
334
335pub fn mad_spread(values: &[f64], scale_floor: f64) -> Result<f64, PrimitiveError> {
337 validate_finite_nonnegative(scale_floor, "scale_floor")?;
338 let median_all = median(values)?;
339 let mut deviations = values
340 .iter()
341 .map(|value| (value - median_all).abs())
342 .collect::<Vec<_>>();
343 if deviations.is_empty() {
344 return Err(invalid_input("values", "must not be empty"));
345 }
346 deviations.sort_by(|a, b| a.total_cmp(b));
347 let n = deviations.len();
348 let mad = if n % 2 == 1 {
349 deviations[n / 2]
350 } else {
351 (deviations[n / 2 - 1] + deviations[n / 2]) * 0.5
352 };
353 let scaled = MAD_GAUSSIAN_CONSISTENCY * mad;
354 if scaled > scale_floor {
355 Ok(scaled)
356 } else {
357 Ok(scale_floor)
358 }
359}
360
361pub fn cfar_ca_multiplier_from_pfa(
366 searched_cells: usize,
367 false_alarm_probability: f64,
368) -> Result<f64, PrimitiveError> {
369 if searched_cells == 0 {
370 return Err(invalid_input("searched_cells", "must be positive"));
371 }
372 validate_probability(false_alarm_probability, "false_alarm_probability")?;
373 let n = searched_cells as f64;
374 let inv = 1.0 / false_alarm_probability.powf(1.0 / n);
375 Ok(n * (inv - 1.0))
376}
377
378pub fn cfar_ca_pfa_from_multiplier(
380 searched_cells: usize,
381 multiplier: f64,
382) -> Result<f64, PrimitiveError> {
383 if searched_cells == 0 {
384 return Err(invalid_input("searched_cells", "must be positive"));
385 }
386 validate_finite_positive(multiplier, "multiplier")?;
387 let n = searched_cells as f64;
388 let one_plus = 1.0 + multiplier / n;
389 if !one_plus.is_finite() {
390 return Err(invalid_input("multiplier", "not finite"));
391 }
392 Ok(one_plus.powf(-n))
393}
394
395pub fn cfar_ca_threshold(
397 searched_cells: usize,
398 false_alarm_probability: f64,
399 noise_level: f64,
400) -> Result<f64, PrimitiveError> {
401 validate_finite_positive(noise_level, "noise_level")?;
402 let multiplier = cfar_ca_multiplier_from_pfa(searched_cells, false_alarm_probability)?;
403 Ok(noise_level * multiplier)
404}
405
406pub fn cfar_ca_false_alarm_probability(
408 searched_cells: usize,
409 threshold: f64,
410 noise_level: f64,
411) -> Result<f64, PrimitiveError> {
412 validate_finite_positive(noise_level, "noise_level")?;
413 validate_finite_positive(threshold, "threshold")?;
414 let multiplier = threshold / noise_level;
415 cfar_ca_pfa_from_multiplier(searched_cells, multiplier)
416}
417
418fn alpha_beta_steady_state_alpha(tracking_index: f64) -> Result<f64, PrimitiveError> {
419 let mut roots = Vec::new();
420 let start = f64::from_bits(1);
421 let end = 1.0 - start;
422 let steps = 16_384usize;
423
424 let mut previous_alpha = start;
425 let mut previous_value = alpha_beta_equation(tracking_index, previous_alpha);
426 if previous_value == 0.0 {
427 return Ok(previous_alpha);
428 }
429
430 for idx in 1..=steps {
431 let current_alpha = start + (end - start) * idx as f64 / steps as f64;
432 let current_value = alpha_beta_equation(tracking_index, current_alpha);
433 if current_value == 0.0 {
434 roots.push(current_alpha);
435 }
436 if previous_value * current_value < 0.0 {
437 let root = bisect_alpha_root(tracking_index, previous_alpha, current_alpha);
438 roots.push(root);
439 }
440 previous_alpha = current_alpha;
441 previous_value = current_value;
442 }
443
444 let alpha = roots.last().copied().ok_or_else(|| {
445 invalid_input(
446 "tracking_index",
447 "no valid steady-state alpha root in (0,1)",
448 )
449 })?;
450 Ok(alpha)
451}
452
453fn alpha_beta_equation(tracking_index: f64, alpha: f64) -> f64 {
454 let one_minus_alpha = 1.0 - alpha;
455 let spread = (tracking_index * one_minus_alpha).sqrt();
456 (2.0 - alpha) * spread - (alpha * alpha + tracking_index * one_minus_alpha / 6.0)
457}
458
459fn bisect_alpha_root(tracking_index: f64, mut left: f64, mut right: f64) -> f64 {
460 let mut f_left = alpha_beta_equation(tracking_index, left);
461 let mut f_right = alpha_beta_equation(tracking_index, right);
462 for _ in 0..80 {
463 let mid = 0.5 * (left + right);
464 let f_mid = alpha_beta_equation(tracking_index, mid);
465 if f_left * f_mid <= 0.0 {
466 right = mid;
467 f_right = f_mid;
468 } else {
469 left = mid;
470 f_left = f_mid;
471 }
472 if f_left == 0.0 || f_right == 0.0 {
473 return 0.5 * (left + right);
474 }
475 }
476 0.5 * (left + right)
477}
478
479fn median(values: &[f64]) -> Result<f64, PrimitiveError> {
480 if values.is_empty() {
481 return Err(invalid_input("values", "must not be empty"));
482 }
483 for value in values {
484 validate_finite(*value, "values")?;
485 }
486 let mut sorted = values.to_vec();
487 sorted.sort_by(|a, b| a.total_cmp(b));
488 let n = sorted.len();
489 Ok(if n % 2 == 1 {
490 sorted[n / 2]
491 } else {
492 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
493 })
494}
495
496fn validate_finite(value: f64, field: &'static str) -> Result<(), PrimitiveError> {
497 if value.is_finite() {
498 Ok(())
499 } else {
500 Err(invalid_input(field, "not finite"))
501 }
502}
503
504fn validate_finite_positive(value: f64, field: &'static str) -> Result<(), PrimitiveError> {
505 validate_finite(value, field)?;
506 if value > 0.0 {
507 Ok(())
508 } else {
509 Err(invalid_input(field, "must be positive"))
510 }
511}
512
513fn validate_finite_nonnegative(value: f64, field: &'static str) -> Result<(), PrimitiveError> {
514 validate_finite(value, field)?;
515 if value >= 0.0 {
516 Ok(())
517 } else {
518 Err(invalid_input(field, "must be non-negative"))
519 }
520}
521
522fn validate_fraction_or_one(value: f64, field: &'static str) -> Result<(), PrimitiveError> {
523 validate_finite(value, field)?;
524 if (0.0..=1.0).contains(&value) {
525 Ok(())
526 } else {
527 Err(invalid_input(field, "must be in [0,1]"))
528 }
529}
530
531fn validate_probability(value: f64, field: &'static str) -> Result<(), PrimitiveError> {
532 validate_finite(value, field)?;
533 if (0.0..1.0).contains(&value) {
534 Ok(())
535 } else {
536 Err(invalid_input(field, "must be in (0,1)"))
537 }
538}
539
540const fn invalid_input(field: &'static str, reason: &'static str) -> PrimitiveError {
541 PrimitiveError::InvalidInput { field, reason }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
554
555 const KALMAN_TOLERANCE: f64 = 1.0e-12;
556
557 #[test]
558 fn alpha_beta_steady_state_matches_reference_and_kalman() {
559 let gains = alpha_beta_steady_state_gains(4.0).expect("alpha-beta gains");
560 assert!((gains.alpha - 0.864_145_399_682_717_8).abs() < KALMAN_TOLERANCE);
561 assert!((gains.beta - 0.737_169_180_900_238_8).abs() < KALMAN_TOLERANCE);
562
563 let kalman =
564 kalman_cv_steady_state_gains(4.0, 1.0, 1.0).expect("kalman steady-state gains");
565 assert!((kalman.position_gain - gains.alpha).abs() < KALMAN_TOLERANCE);
566 assert!((kalman.rate_gain - gains.beta).abs() < KALMAN_TOLERANCE);
567 assert!((kalman.position_gain * 1.0 - gains.alpha).abs() < KALMAN_TOLERANCE);
568 }
569
570 #[test]
571 fn alpha_beta_step_response_settles_to_the_new_level() {
572 let gains = alpha_beta_steady_state_gains(4.0).expect("alpha-beta gains");
576 let step_level = 10.0;
577 let dt = 1.0;
578 let mut state = AlphaBetaState {
579 level: 0.0,
580 rate: 0.0,
581 };
582 let mut last_innovation = f64::INFINITY;
583 for _ in 0..300 {
584 let step = alpha_beta_filter_step(state, step_level, dt, gains).expect("step");
585 state = step.updated;
586 last_innovation = step.innovation;
587 }
588 assert!(
589 (state.level - step_level).abs() < 1e-9,
590 "level did not settle: {}",
591 state.level
592 );
593 assert!(
594 state.rate.abs() < 1e-9,
595 "rate did not settle: {}",
596 state.rate
597 );
598 assert!(
599 last_innovation.abs() < 1e-9,
600 "innovation did not decay: {}",
601 last_innovation
602 );
603 }
604
605 #[test]
606 fn alpha_beta_predict_and_update_are_dt_aware() {
607 let state = AlphaBetaState {
608 level: 5.0,
609 rate: 2.0,
610 };
611 let gains = AlphaBetaGains {
612 alpha: 0.6,
613 beta: 0.8,
614 };
615 let step = alpha_beta_filter_step(state, 8.0, 2.0, gains).expect("step");
616
617 assert_eq!(step.predicted.level, 9.0);
618 assert_eq!(step.predicted.rate, 2.0);
619 assert_eq!(step.innovation, -1.0);
620 assert_eq!(step.updated.level, 8.4);
621 assert_eq!(step.updated.rate, 1.6);
622 }
623
624 #[test]
625 fn nis_gate_threshold_and_expectation() {
626 let gate = nis_gate_test(1.0, 1.0, 1, 0.95).expect("nis gate");
627 assert!((gate.threshold - 3.841_458_820_694_124).abs() < 1.0e-12);
628 assert_eq!(gate.dof, 1);
629 assert!(gate.in_gate);
630 assert_eq!(nis_expected_value(3).expect("dof"), 3.0);
631 }
632
633 #[test]
634 fn mad_gives_expected_gaussian_scaling_and_ewma_power_of_two_matches_alpha_form() {
635 const Q75: f64 = 0.674_489_750_196_081_7;
636 let sigma_est =
637 mad_spread(&[-2.0 * Q75, -Q75, 0.0, Q75, 2.0 * Q75], 1.0e-12).expect("mad spread");
638 assert!((sigma_est - 1.0).abs() < 1.0e-12);
639 assert!((mad_spread(&[0.0, 0.0, 0.0], 1.2).expect("floored mad") - 1.2).abs() < 1.0e-12);
640
641 let previous = 16.0;
642 let sample = 2.0;
643 let alpha = 1.0 / 16.0;
644 let ewma_alpha = ewma_update(previous, sample, alpha).expect("ewma alpha");
645 let ewma_pow2 = ewma_update_power_of_two(previous, sample, 4).expect("ewma pow2");
646 let ewma_int = ((16.0_f64 - 1.0) * previous + sample) / 16.0;
647 assert!((ewma_alpha - ewma_pow2).abs() < 1.0e-15);
648 assert!((ewma_pow2 - ewma_int).abs() < 1.0e-15);
649 assert!((ewma_alpha - 15.125).abs() < 1.0e-12);
650 }
651
652 #[test]
653 fn cfar_threshold_and_probability_roundtrip() {
654 let noise = 5.0;
655 let pfa = 1.0e-3;
656 let searched_cells = 4usize;
657 let multiplier = cfar_ca_multiplier_from_pfa(searched_cells, pfa).expect("cfar multiplier");
658 let threshold = cfar_ca_threshold(searched_cells, pfa, noise).expect("cfar threshold");
659 let pfa_back = cfar_ca_false_alarm_probability(searched_cells, threshold, noise)
660 .expect("cfar pfa back");
661 assert!((multiplier - 18.493_653_007_613_965).abs() < 1.0e-12);
662 assert_eq!(threshold, noise * multiplier);
663 assert!((pfa_back - pfa).abs() < 1.0e-12);
664 }
665}