1use crate::astro::covariance::{positive_semidefinite, symmetric};
13use crate::astro::math::special::erf;
14use crate::astro::math::vec3;
15use crate::validate;
16use std::f64::consts::PI;
17
18const ZERO_REL_SPEED_EPS_KM_S: f64 = 1.0e-12;
20const COLLISION_COURSE_EPS_KM: f64 = 1.0e-12;
23const COLLISION_TRIAL_AXIS_THRESHOLD: f64 = 0.9;
26const SIGMA_FLOOR_KM: f64 = 1.0e-12;
29const EIGENVECTOR_EPS: f64 = 1.0e-30;
32
33const ALFANO_SIMPSON_INTERVALS: usize = 200;
36const FOSTER_NUMERICAL_RADIAL_STEPS: usize = 20;
38const FOSTER_NUMERICAL_ANGULAR_STEPS: usize = 40;
40
41#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct EncounterFrame {
47 pub x_hat: [f64; 3],
48 pub y_hat: [f64; 3],
49 pub z_hat: [f64; 3],
50 pub relative_position_km: [f64; 3],
51 pub relative_velocity_km_s: [f64; 3],
52 pub miss_km: f64,
53 pub relative_speed_km_s: f64,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum PcMethod {
59 FosterEqualArea,
61 FosterNumerical,
63 Alfano2005,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
70pub struct ConjunctionState {
71 pub position_km: [f64; 3],
72 pub velocity_km_s: [f64; 3],
73 pub covariance_km2: [[f64; 3]; 3],
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct CollisionPc {
79 pub pc: f64,
80 pub miss_km: f64,
81 pub relative_speed_km_s: f64,
82 pub sigma_x_km: f64,
83 pub sigma_z_km: f64,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum ConjunctionError {
89 NonFinite { field: &'static str },
91 NotPositive { field: &'static str },
93 UndefinedFrame,
95}
96
97impl core::fmt::Display for ConjunctionError {
98 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99 match self {
100 Self::NonFinite { field } => write!(f, "{field} must be finite"),
101 Self::NotPositive { field } => write!(f, "{field} must be positive"),
102 Self::UndefinedFrame => write!(f, "encounter frame is undefined"),
103 }
104 }
105}
106
107impl std::error::Error for ConjunctionError {}
108
109pub fn encounter_frame(
115 r1: [f64; 3],
116 v1: [f64; 3],
117 r2: [f64; 3],
118 v2: [f64; 3],
119) -> Result<EncounterFrame, ConjunctionError> {
120 validate::finite_vec3(r1, "object1.position_km").map_err(map_conjunction_field_error)?;
121 validate::finite_vec3(v1, "object1.velocity_km_s").map_err(map_conjunction_field_error)?;
122 validate::finite_vec3(r2, "object2.position_km").map_err(map_conjunction_field_error)?;
123 validate::finite_vec3(v2, "object2.velocity_km_s").map_err(map_conjunction_field_error)?;
124 let frame =
125 encounter_frame_unchecked(r1, v1, r2, v2).ok_or(ConjunctionError::UndefinedFrame)?;
126 validate_encounter_frame(&frame)?;
127 Ok(frame)
128}
129
130fn encounter_frame_unchecked(
131 r1: [f64; 3],
132 v1: [f64; 3],
133 r2: [f64; 3],
134 v2: [f64; 3],
135) -> Option<EncounterFrame> {
136 let dr = vec3::sub3(r2, r1);
137 let dv = vec3::sub3(v2, v1);
138
139 let rel_speed = vec3::norm3(dv);
140 if rel_speed < ZERO_REL_SPEED_EPS_KM_S {
141 return None;
142 }
143
144 let y_hat = [dv[0] / rel_speed, dv[1] / rel_speed, dv[2] / rel_speed];
148
149 let dr_dot_y = vec3::dot3(dr, y_hat);
151 let dr_ortho = vec3::sub3(dr, vec3::scale3(y_hat, dr_dot_y));
152 let miss_ortho = vec3::norm3(dr_ortho);
153
154 let x_hat = if miss_ortho < COLLISION_COURSE_EPS_KM {
155 let trial = if y_hat[0].abs() < COLLISION_TRIAL_AXIS_THRESHOLD {
158 [1.0, 0.0, 0.0]
159 } else {
160 [0.0, 1.0, 0.0]
161 };
162 let cr = vec3::cross3(y_hat, trial);
163 let n = vec3::norm3(cr);
164 [cr[0] / n, cr[1] / n, cr[2] / n]
165 } else {
166 [
168 dr_ortho[0] / miss_ortho,
169 dr_ortho[1] / miss_ortho,
170 dr_ortho[2] / miss_ortho,
171 ]
172 };
173
174 let z_hat = vec3::cross3(y_hat, x_hat);
175
176 Some(EncounterFrame {
177 x_hat,
178 y_hat,
179 z_hat,
180 relative_position_km: dr,
181 relative_velocity_km_s: dv,
182 miss_km: miss_ortho,
183 relative_speed_km_s: rel_speed,
184 })
185}
186
187pub fn encounter_plane_covariance(
192 frame: &EncounterFrame,
193 cov: &[[f64; 3]; 3],
194) -> Result<[[f64; 2]; 2], ConjunctionError> {
195 validate_encounter_frame(frame)?;
196 validate_covariance(cov, "covariance_km2")?;
197 let projected = encounter_plane_covariance_unchecked(frame, cov);
198 validate_plane_covariance(&projected, "encounter_plane_covariance")?;
199 Ok(projected)
200}
201
202fn encounter_plane_covariance_unchecked(
203 frame: &EncounterFrame,
204 cov: &[[f64; 3]; 3],
205) -> [[f64; 2]; 2] {
206 let r = [frame.x_hat, frame.z_hat];
207
208 let mut m = [[0.0_f64; 3]; 2];
210 for (i, m_row) in m.iter_mut().enumerate() {
211 for (j, m_ij) in m_row.iter_mut().enumerate() {
212 let mut s = 0.0_f64;
213 s += r[i][0] * cov[0][j];
214 s += r[i][1] * cov[1][j];
215 s += r[i][2] * cov[2][j];
216 *m_ij = s;
217 }
218 }
219
220 let mut out = [[0.0_f64; 2]; 2];
222 for (i, out_row) in out.iter_mut().enumerate() {
223 for (jj, out_ij) in out_row.iter_mut().enumerate() {
224 let mut s = 0.0_f64;
225 s += m[i][0] * r[jj][0];
226 s += m[i][1] * r[jj][1];
227 s += m[i][2] * r[jj][2];
228 *out_ij = s;
229 }
230 }
231
232 out
233}
234
235pub fn collision_probability(
240 object1: &ConjunctionState,
241 object2: &ConjunctionState,
242 hard_body_radius_km: f64,
243 method: PcMethod,
244) -> Result<CollisionPc, ConjunctionError> {
245 validate_conjunction_state(
246 object1,
247 "object1.position_km",
248 "object1.velocity_km_s",
249 "object1.covariance_km2",
250 )?;
251 validate_conjunction_state(
252 object2,
253 "object2.position_km",
254 "object2.velocity_km_s",
255 "object2.covariance_km2",
256 )?;
257 validate::finite_positive(hard_body_radius_km, "hard_body_radius_km")
258 .map_err(map_conjunction_field_error)?;
259
260 let frame = encounter_frame_unchecked(
261 object1.position_km,
262 object1.velocity_km_s,
263 object2.position_km,
264 object2.velocity_km_s,
265 )
266 .ok_or(ConjunctionError::UndefinedFrame)?;
267 validate_encounter_frame(&frame)?;
268 let combined = add_cov(&object1.covariance_km2, &object2.covariance_km2);
269 validate_covariance(&combined, "combined_covariance_km2")?;
270 let c_enc = encounter_plane_covariance_unchecked(&frame, &combined);
271 validate_plane_covariance(&c_enc, "encounter_plane_covariance")?;
272 let (sigma_x, sigma_z, xm, zm) = principal_components(&frame, &c_enc);
273 validate_principal_components(sigma_x, sigma_z, xm, zm)?;
274
275 let pc = match method {
276 PcMethod::FosterEqualArea => {
277 foster_equal_area(sigma_x, sigma_z, xm, zm, hard_body_radius_km)
278 }
279 PcMethod::FosterNumerical => {
280 foster_numerical(sigma_x, sigma_z, xm, zm, hard_body_radius_km)
281 }
282 PcMethod::Alfano2005 => alfano_2005(sigma_x, sigma_z, xm, zm, hard_body_radius_km),
283 };
284 let pc = validate::finite(pc, "collision probability").map_err(map_conjunction_field_error)?;
285
286 Ok(CollisionPc {
287 pc: if pc < 0.0 { 0.0 } else { pc },
288 miss_km: frame.miss_km,
289 relative_speed_km_s: frame.relative_speed_km_s,
290 sigma_x_km: sigma_x,
291 sigma_z_km: sigma_z,
292 })
293}
294
295fn validate_conjunction_state(
296 state: &ConjunctionState,
297 position_field: &'static str,
298 velocity_field: &'static str,
299 covariance_field: &'static str,
300) -> Result<(), ConjunctionError> {
301 validate::finite_vec3(state.position_km, position_field)
302 .map_err(map_conjunction_field_error)?;
303 validate::finite_vec3(state.velocity_km_s, velocity_field)
304 .map_err(map_conjunction_field_error)?;
305 validate_covariance(&state.covariance_km2, covariance_field)?;
306 Ok(())
307}
308
309fn validate_encounter_frame(frame: &EncounterFrame) -> Result<(), ConjunctionError> {
310 validate::finite_vec3(frame.x_hat, "frame.x_hat").map_err(map_conjunction_field_error)?;
311 validate::finite_vec3(frame.y_hat, "frame.y_hat").map_err(map_conjunction_field_error)?;
312 validate::finite_vec3(frame.z_hat, "frame.z_hat").map_err(map_conjunction_field_error)?;
313 validate::finite_vec3(frame.relative_position_km, "frame.relative_position_km")
314 .map_err(map_conjunction_field_error)?;
315 validate::finite_vec3(frame.relative_velocity_km_s, "frame.relative_velocity_km_s")
316 .map_err(map_conjunction_field_error)?;
317 validate::finite(frame.miss_km, "frame.miss_km").map_err(map_conjunction_field_error)?;
318 validate::finite(frame.relative_speed_km_s, "frame.relative_speed_km_s")
319 .map_err(map_conjunction_field_error)?;
320 Ok(())
321}
322
323fn validate_plane_covariance(
324 cov: &[[f64; 2]; 2],
325 field: &'static str,
326) -> Result<(), ConjunctionError> {
327 for row in cov {
328 validate::finite_slice(row, field).map_err(map_conjunction_field_error)?;
329 }
330 Ok(())
331}
332
333fn validate_principal_components(
334 sigma_x: f64,
335 sigma_z: f64,
336 xm: f64,
337 zm: f64,
338) -> Result<(), ConjunctionError> {
339 validate::finite(sigma_x, "sigma_x_km").map_err(map_conjunction_field_error)?;
340 validate::finite(sigma_z, "sigma_z_km").map_err(map_conjunction_field_error)?;
341 validate::finite(xm, "miss_x_km").map_err(map_conjunction_field_error)?;
342 validate::finite(zm, "miss_z_km").map_err(map_conjunction_field_error)?;
343 Ok(())
344}
345
346fn validate_covariance(cov: &[[f64; 3]; 3], field: &'static str) -> Result<(), ConjunctionError> {
347 for row in cov {
348 validate::finite_slice(row, field).map_err(map_conjunction_field_error)?;
349 }
350 if !symmetric(cov) || !positive_semidefinite(cov) {
351 return Err(ConjunctionError::NotPositive { field });
352 }
353 Ok(())
354}
355
356fn map_conjunction_field_error(error: validate::FieldError) -> ConjunctionError {
357 match error {
358 validate::FieldError::NonFinite { field } => ConjunctionError::NonFinite { field },
359 validate::FieldError::NotPositive { field }
360 | validate::FieldError::Negative { field }
361 | validate::FieldError::OutOfRange { field, .. } => ConjunctionError::NotPositive { field },
362 validate::FieldError::Missing { field }
363 | validate::FieldError::FloatParse { field, .. }
364 | validate::FieldError::IntParse { field, .. }
365 | validate::FieldError::InvalidCivilDate { field, .. }
366 | validate::FieldError::InvalidCivilTime { field, .. } => {
367 ConjunctionError::NonFinite { field }
368 }
369 }
370}
371
372fn add_cov(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
373 let mut out = [[0.0_f64; 3]; 3];
374 for i in 0..3 {
375 for j in 0..3 {
376 out[i][j] = a[i][j] + b[i][j];
377 }
378 }
379 out
380}
381
382fn principal_components(frame: &EncounterFrame, c_enc: &[[f64; 2]; 2]) -> (f64, f64, f64, f64) {
385 let a = c_enc[0][0];
386 let b = c_enc[0][1];
387 let c = c_enc[1][0];
388 let d = c_enc[1][1];
389
390 let trace = a + d;
391 let det = a * d - b * c;
392 let disc = (trace * trace / 4.0 - det).max(0.0).sqrt();
393
394 let l1 = trace / 2.0 + disc;
395 let l2 = trace / 2.0 - disc;
396
397 let sigma_x = l1.max(0.0).sqrt();
398 let sigma_z = l2.max(0.0).sqrt();
399
400 let (vx, vy) = if b.abs() > EIGENVECTOR_EPS {
401 normalize_2d(l1 - d, c)
402 } else if c.abs() > EIGENVECTOR_EPS {
403 normalize_2d(c, l1 - a)
404 } else {
405 (1.0, 0.0)
406 };
407
408 let theta = vy.atan2(vx);
409 let xm = frame.miss_km * theta.cos();
410 let zm = -frame.miss_km * theta.sin();
411
412 (sigma_x, sigma_z, xm, zm)
413}
414
415fn normalize_2d(x: f64, y: f64) -> (f64, f64) {
416 let m = (x * x + y * y).sqrt();
417 if m > EIGENVECTOR_EPS {
418 (x / m, y / m)
419 } else {
420 (1.0, 0.0)
421 }
422}
423
424fn foster_equal_area(sigma_x: f64, sigma_z: f64, xm: f64, zm: f64, hbr: f64) -> f64 {
426 if sigma_x > SIGMA_FLOOR_KM && sigma_z > SIGMA_FLOOR_KM {
427 let hsq = (PI / 4.0).sqrt() * hbr;
428 let sqrt2 = 2.0_f64.sqrt();
429 0.25 * (erf((xm + hsq) / (sqrt2 * sigma_x)) - erf((xm - hsq) / (sqrt2 * sigma_x)))
430 * (erf((zm + hsq) / (sqrt2 * sigma_z)) - erf((zm - hsq) / (sqrt2 * sigma_z)))
431 } else {
432 0.0
433 }
434}
435
436fn alfano_2005(sigma_x: f64, sigma_z: f64, xm: f64, zm: f64, hbr: f64) -> f64 {
440 if sigma_x > SIGMA_FLOOR_KM && sigma_z > SIGMA_FLOOR_KM {
441 let n = ALFANO_SIMPSON_INTERVALS;
442 let h = 2.0 * hbr / n as f64;
443 let sqrt2 = 2.0_f64.sqrt();
444 let sqrt_2pi = (2.0 * PI).sqrt();
445
446 let integrand = |i: usize| -> f64 {
447 let x = -hbr + i as f64 * h;
448 let y_top_sq = hbr * hbr - x * x;
449 if y_top_sq <= 0.0 {
450 0.0
451 } else {
452 let y_top = y_top_sq.sqrt();
453 let arg = (x - xm) * (x - xm) / (2.0 * sigma_x * sigma_x);
454 let exp_term = (-arg).exp();
455 let erf_top = erf((y_top - zm) / (sigma_z * sqrt2));
456 let erf_bot = erf((-y_top - zm) / (sigma_z * sqrt2));
457 exp_term * (erf_top - erf_bot)
458 }
459 };
460
461 let mut simpson_sum = 0.0_f64;
462 for i in 0..=n {
463 let weight = if i == 0 || i == n {
464 1.0
465 } else if i % 2 == 1 {
466 4.0
467 } else {
468 2.0
469 };
470 simpson_sum += weight * integrand(i);
471 }
472
473 let prefactor = 1.0 / (2.0 * sigma_x * sqrt_2pi);
474 prefactor * (h / 3.0) * simpson_sum
475 } else {
476 0.0
477 }
478}
479
480fn foster_numerical(sigma_x: f64, sigma_z: f64, xm: f64, zm: f64, hbr: f64) -> f64 {
482 if sigma_x > SIGMA_FLOOR_KM && sigma_z > SIGMA_FLOOR_KM {
483 let steps_r = FOSTER_NUMERICAL_RADIAL_STEPS;
484 let steps_theta = FOSTER_NUMERICAL_ANGULAR_STEPS;
485 let dr = hbr / steps_r as f64;
486 let dtheta = 2.0 * PI / steps_theta as f64;
487
488 let mut acc = 0.0_f64;
489 for i in 0..steps_r {
490 for j in 0..steps_theta {
491 let r = (i as f64 + 0.5) * dr;
492 let theta = (j as f64 + 0.5) * dtheta;
493 let x = r * theta.cos();
494 let z = r * theta.sin();
495
496 let arg = (x - xm).powf(2.0) / (2.0 * sigma_x * sigma_x)
499 + (z - zm).powf(2.0) / (2.0 * sigma_z * sigma_z);
500 let f = (-arg).exp() / (2.0 * PI * sigma_x * sigma_z);
501 acc += f * r * dr * dtheta;
502 }
503 }
504 acc
505 } else {
506 0.0
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 const OMITRON_OBJ1: ConjunctionState = ConjunctionState {
516 position_km: [378.39559, 4305.721887, 5752.767554],
517 velocity_km_s: [2.360800244, 5.580331936, -4.322349039],
518 covariance_km2: [
519 [44.5757544811362, 81.6751751052616, -67.8687662707124],
520 [81.6751751052616, 158.453402956163, -128.616921644857],
521 [-67.8687662707124, -128.616921644858, 105.490542562701],
522 ],
523 };
524 const OMITRON_OBJ2: ConjunctionState = ConjunctionState {
525 position_km: [374.5180598, 4307.560983, 5751.130418],
526 velocity_km_s: [-5.388125081, -3.946827739, 3.322820358],
527 covariance_km2: [
528 [2.31067077720423, 1.69905293875632, -1.4170164577661],
529 [1.69905293875632, 1.24957388457206, -1.04174164279599],
530 [-1.4170164577661, -1.04174164279599, 0.869260558223714],
531 ],
532 };
533 const OMITRON_HBR_KM: f64 = 0.020;
534
535 fn omitron_pc(method: PcMethod, hbr: f64) -> CollisionPc {
536 collision_probability(&OMITRON_OBJ1, &OMITRON_OBJ2, hbr, method)
537 .expect("omitron frame is well defined")
538 }
539
540 #[test]
541 fn collision_probability_matches_cara_published_value() {
542 let result = omitron_pc(PcMethod::FosterEqualArea, OMITRON_HBR_KM);
547 assert!((result.pc - 2.706_015_734_901_11e-5).abs() < 1.0e-9);
548 }
549
550 #[test]
551 fn collision_probability_has_frozen_bits() {
552 let ea = omitron_pc(PcMethod::FosterEqualArea, OMITRON_HBR_KM);
558 assert_eq!(ea.pc.to_bits(), 0x3efc_5fe7_e374_e761);
559 assert_eq!(ea.miss_km.to_bits(), 0x4012_5f76_bb63_5a77);
560 assert_eq!(ea.relative_speed_km_s.to_bits(), 0x402c_ee85_c45b_2fca);
561 assert_eq!(ea.sigma_x_km.to_bits(), 0x400b_2f8b_338c_261f);
562 assert_eq!(ea.sigma_z_km.to_bits(), 0x3fe9_dc26_3a96_e94c);
563
564 let num = omitron_pc(PcMethod::FosterNumerical, OMITRON_HBR_KM);
565 assert_eq!(num.pc.to_bits(), 0x3efc_5fed_5966_2e48);
566
567 let alf = omitron_pc(PcMethod::Alfano2005, OMITRON_HBR_KM);
568 assert_eq!(alf.pc.to_bits(), 0x3efc_5edd_3b73_ec59);
569
570 assert_eq!(
571 omitron_pc(PcMethod::Alfano2005, 0.010).pc.to_bits(),
572 0x3edc_5f31_c28e_87bb
573 );
574 assert_eq!(
575 omitron_pc(PcMethod::Alfano2005, 0.040).pc.to_bits(),
576 0x3f1c_5d8b_338e_291a
577 );
578 }
579
580 #[test]
581 fn methods_agree_for_well_conditioned_geometry() {
582 let ea = omitron_pc(PcMethod::FosterEqualArea, OMITRON_HBR_KM).pc;
583 let num = omitron_pc(PcMethod::FosterNumerical, OMITRON_HBR_KM).pc;
584 let alf = omitron_pc(PcMethod::Alfano2005, OMITRON_HBR_KM).pc;
585
586 assert!((num - ea).abs() < ea * 0.01);
587 assert!((alf - ea).abs() < ea * 0.01);
588 assert!((alf - num).abs() < num * 0.01);
589 assert!(alf > 0.0);
590 }
591
592 #[test]
593 fn larger_hard_body_radius_increases_pc() {
594 for method in [
595 PcMethod::FosterEqualArea,
596 PcMethod::FosterNumerical,
597 PcMethod::Alfano2005,
598 ] {
599 let small = omitron_pc(method, 0.010).pc;
600 let large = omitron_pc(method, 0.040).pc;
601 assert!(large > small);
602 }
603 }
604
605 #[test]
606 fn swapping_objects_preserves_pc() {
607 let forward = omitron_pc(PcMethod::FosterEqualArea, OMITRON_HBR_KM).pc;
608 let swapped = collision_probability(
609 &OMITRON_OBJ2,
610 &OMITRON_OBJ1,
611 OMITRON_HBR_KM,
612 PcMethod::FosterEqualArea,
613 )
614 .unwrap()
615 .pc;
616 assert!((forward - swapped).abs() < 1.0e-15);
617 }
618
619 #[test]
620 fn zero_relative_velocity_has_no_frame() {
621 let cov = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
622 assert!(encounter_frame(
623 [7000.0, 0.0, 0.0],
624 [0.0, 7.5, 0.0],
625 [7000.01, 0.0, 0.0],
626 [0.0, 7.5, 0.0],
627 )
628 .is_err_and(|err| err == ConjunctionError::UndefinedFrame));
629 let obj1 = ConjunctionState {
630 position_km: [7000.0, 0.0, 0.0],
631 velocity_km_s: [0.0, 7.5, 0.0],
632 covariance_km2: cov,
633 };
634 let obj2 = ConjunctionState {
635 position_km: [7000.01, 0.0, 0.0],
636 velocity_km_s: [0.0, 7.5, 0.0],
637 covariance_km2: cov,
638 };
639 assert_eq!(
640 collision_probability(&obj1, &obj2, 0.015, PcMethod::FosterEqualArea),
641 Err(ConjunctionError::UndefinedFrame)
642 );
643 }
644
645 #[test]
646 fn non_finite_state_inputs_are_rejected() {
647 let mut object = OMITRON_OBJ1;
648 object.position_km[0] = f64::NAN;
649 assert_eq!(
650 collision_probability(
651 &object,
652 &OMITRON_OBJ2,
653 OMITRON_HBR_KM,
654 PcMethod::FosterEqualArea,
655 ),
656 Err(ConjunctionError::NonFinite {
657 field: "object1.position_km"
658 })
659 );
660
661 assert_eq!(
662 encounter_frame(
663 [f64::INFINITY, 0.0, 0.0],
664 [0.0, 7.5, 0.0],
665 [7000.01, 0.0, 0.0],
666 [0.0, -7.5, 0.0],
667 ),
668 Err(ConjunctionError::NonFinite {
669 field: "object1.position_km"
670 })
671 );
672 }
673
674 #[test]
675 fn non_finite_encounter_frame_outputs_are_rejected() {
676 assert!(matches!(
677 encounter_frame(
678 [0.0, 0.0, 0.0],
679 [0.0, 0.0, 0.0],
680 [f64::MAX, 0.0, 0.0],
681 [0.0, f64::MAX, 0.0],
682 ),
683 Err(ConjunctionError::NonFinite {
684 field: "frame.x_hat"
685 | "frame.y_hat"
686 | "frame.z_hat"
687 | "frame.relative_position_km"
688 | "frame.relative_velocity_km_s"
689 | "frame.miss_km"
690 | "frame.relative_speed_km_s"
691 })
692 ));
693 }
694
695 #[test]
696 fn invalid_covariance_shape_is_rejected_before_pc() {
697 for covariance_km2 in [
698 [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
699 [[1.0, 2.0, 0.0], [2.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
700 [[1.0, 0.25, 0.0], [0.20, 1.0, 0.0], [0.0, 0.0, 1.0]],
701 ] {
702 let object = ConjunctionState {
703 covariance_km2,
704 ..OMITRON_OBJ1
705 };
706
707 assert_eq!(
708 collision_probability(
709 &object,
710 &OMITRON_OBJ2,
711 OMITRON_HBR_KM,
712 PcMethod::FosterEqualArea,
713 ),
714 Err(ConjunctionError::NotPositive {
715 field: "object1.covariance_km2"
716 })
717 );
718 }
719 }
720
721 #[test]
722 fn covariance_and_hbr_inputs_are_rejected() {
723 let mut object = OMITRON_OBJ2;
724 object.covariance_km2[1][2] = f64::INFINITY;
725 assert_eq!(
726 collision_probability(
727 &OMITRON_OBJ1,
728 &object,
729 OMITRON_HBR_KM,
730 PcMethod::FosterEqualArea,
731 ),
732 Err(ConjunctionError::NonFinite {
733 field: "object2.covariance_km2"
734 })
735 );
736
737 assert_eq!(
738 collision_probability(&OMITRON_OBJ1, &OMITRON_OBJ2, 0.0, PcMethod::FosterEqualArea),
739 Err(ConjunctionError::NotPositive {
740 field: "hard_body_radius_km"
741 })
742 );
743
744 let frame = encounter_frame(
745 [7000.0, 0.0, 0.0],
746 [0.0, 7.5, 0.0],
747 [7000.1, 0.0, 0.0],
748 [0.0, -7.5, 0.0],
749 )
750 .unwrap();
751 let cov = [[1.0, 0.0, 0.0], [0.0, f64::NAN, 0.0], [0.0, 0.0, 3.0]];
752 assert_eq!(
753 encounter_plane_covariance(&frame, &cov),
754 Err(ConjunctionError::NonFinite {
755 field: "covariance_km2"
756 })
757 );
758 }
759
760 #[test]
761 fn head_on_frame_matches_reference_bits() {
762 let frame = encounter_frame(
763 [7000.0, 0.0, 0.0],
764 [0.0, 7.5, 0.0],
765 [7000.1, 0.0, 0.0],
766 [0.0, -7.5, 0.0],
767 )
768 .unwrap();
769
770 assert_eq!(frame.x_hat[0].to_bits(), 1.0_f64.to_bits());
771 assert_eq!(frame.x_hat[1].to_bits(), 0.0_f64.to_bits());
772 assert_eq!(frame.x_hat[2].to_bits(), 0.0_f64.to_bits());
773 assert_eq!(frame.y_hat[0].to_bits(), 0.0_f64.to_bits());
774 assert_eq!(frame.y_hat[1].to_bits(), (-1.0_f64).to_bits());
775 assert_eq!(frame.y_hat[2].to_bits(), 0.0_f64.to_bits());
776 assert_eq!(frame.z_hat[0].to_bits(), (-0.0_f64).to_bits());
777 assert_eq!(frame.z_hat[1].to_bits(), 0.0_f64.to_bits());
778 assert_eq!(frame.z_hat[2].to_bits(), 1.0_f64.to_bits());
779 assert_eq!(frame.miss_km.to_bits(), 0x3fb9_9999_999a_0000);
780 assert_eq!(frame.relative_speed_km_s.to_bits(), 0x402e_0000_0000_0000);
781 }
782
783 #[test]
784 fn encounter_plane_covariance_projects_onto_xz() {
785 let frame = encounter_frame(
786 [7000.0, 0.0, 0.0],
787 [0.0, 7.5, 0.0],
788 [7000.1, 0.0, 0.0],
789 [0.0, -7.5, 0.0],
790 )
791 .unwrap();
792 let cov = [[1.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 3.0]];
793 let c_enc = encounter_plane_covariance(&frame, &cov).unwrap();
794 assert_eq!(c_enc, [[1.0, 0.0], [0.0, 3.0]]);
795 }
796}