1use hifitime::{Duration, Unit};
12use log::error;
13
14use crate::{
15 astro::{Aberration, Occultation},
16 constants::{frames::SUN_J2000, orientations::J2000},
17 ephemerides::EphemerisPhysicsSnafu,
18 errors::{AlmanacError, EphemerisSnafu, OrientationSnafu},
19 frames::Frame,
20 prelude::Orbit,
21};
22
23use super::Almanac;
24use crate::errors::AlmanacResult;
25
26use core::f64::consts::PI;
27use snafu::ResultExt;
28
29impl Almanac {
30 pub fn line_of_sight_obstructed(
59 &self,
60 observer: Orbit,
61 observed: Orbit,
62 mut obstructing_body: Frame,
63 ab_corr: Option<Aberration>,
64 ) -> AlmanacResult<bool> {
65 if observer == observed {
66 return Ok(false);
67 }
68
69 if obstructing_body.mean_equatorial_radius_km().is_err() {
70 obstructing_body =
71 self.frame_info(obstructing_body)
72 .map_err(|e| AlmanacError::GenericError {
73 err: format!("{e} when fetching frame data for {obstructing_body}"),
74 })?;
75 }
76
77 let ob_mean_eq_radius_km = obstructing_body
78 .mean_equatorial_radius_km()
79 .context(EphemerisPhysicsSnafu {
80 action: "fetching mean equatorial radius of obstructing body",
81 })
82 .context(EphemerisSnafu {
83 action: "computing line of sight",
84 })?;
85
86 let r1 = self
88 .transform_to(observed, obstructing_body, ab_corr)?
89 .radius_km;
90 let r2 = self
91 .transform_to(observer, obstructing_body, ab_corr)?
92 .radius_km;
93
94 let r1sq = r1.dot(&r1);
95 let r2sq = r2.dot(&r2);
96 let r1dotr2 = r1.dot(&r2);
97
98 let tau = (r1sq - r1dotr2) / (r1sq + r2sq - 2.0 * r1dotr2);
99 if !(0.0..=1.0).contains(&tau)
100 || (1.0 - tau) * r1sq + r1dotr2 * tau > ob_mean_eq_radius_km.powi(2)
101 {
102 Ok(false)
103 } else {
104 Ok(true)
105 }
106 }
107
108 pub fn occultation(
115 &self,
116 mut back_frame: Frame,
117 mut front_frame: Frame,
118 mut observer: Orbit,
119 ab_corr: Option<Aberration>,
120 ) -> AlmanacResult<Occultation> {
121 if back_frame.mean_equatorial_radius_km().is_err() {
122 back_frame = self
123 .frame_info(back_frame)
124 .map_err(|e| AlmanacError::GenericError {
125 err: format!("{e} when fetching {back_frame:e} frame data"),
126 })?;
127 }
128
129 if front_frame.mean_equatorial_radius_km().is_err() {
130 front_frame = self
131 .frame_info(front_frame)
132 .map_err(|e| AlmanacError::GenericError {
133 err: format!("{e} when fetching {front_frame:e} frame data"),
134 })?;
135 }
136
137 let bobj_mean_eq_radius_km = back_frame
138 .mean_equatorial_radius_km()
139 .context(EphemerisPhysicsSnafu {
140 action: "fetching mean equatorial radius of back frame",
141 })
142 .context(EphemerisSnafu {
143 action: "computing occultation state",
144 })?;
145
146 let epoch = observer.epoch;
147
148 if bobj_mean_eq_radius_km < f64::EPSILON {
150 let observed = -self.transform_to(observer, back_frame, ab_corr)?;
151 let percentage =
152 if self.line_of_sight_obstructed(observer, observed, front_frame, ab_corr)? {
153 100.0
154 } else {
155 0.0
156 };
157 return Ok(Occultation {
158 epoch,
159 percentage,
160 back_frame,
161 front_frame,
162 });
163 }
164
165 observer = self
171 .rotate_to(observer, observer.frame.with_orient(J2000))
172 .context(OrientationSnafu {
173 action: "computing eclipse state",
174 })?;
175 let r_eb = self
176 .transform_to(observer, front_frame.with_orient(J2000), ab_corr)?
177 .radius_km;
178
179 let r_ls = -self
181 .transform_to(observer, back_frame.with_orient(J2000), ab_corr)?
182 .radius_km;
183
184 let r_ls_prime = if bobj_mean_eq_radius_km >= r_ls.norm() {
186 core::f64::consts::FRAC_PI_2
187 } else {
188 (bobj_mean_eq_radius_km / r_ls.norm()).asin()
189 };
190
191 let fobj_mean_eq_radius_km = front_frame
192 .mean_equatorial_radius_km()
193 .context(EphemerisPhysicsSnafu {
194 action: "fetching mean equatorial radius of front object",
195 })
196 .context(EphemerisSnafu {
197 action: "computing eclipse state",
198 })?;
199
200 let r_fobj_prime = if fobj_mean_eq_radius_km >= r_eb.norm() {
201 core::f64::consts::FRAC_PI_2
202 } else {
203 (fobj_mean_eq_radius_km / r_eb.norm()).asin()
204 };
205
206 let d_prime = (-(r_ls.dot(&r_eb)) / (r_eb.norm() * r_ls.norm())).acos();
208
209 let percentage = compute_occultation_percentage(d_prime, r_ls_prime, r_fobj_prime)?;
210
211 Ok(Occultation {
212 epoch,
213 percentage,
214 back_frame,
215 front_frame,
216 })
217 }
218
219 pub fn solar_eclipsing(
229 &self,
230 eclipsing_frame: Frame,
231 observer: Orbit,
232 ab_corr: Option<Aberration>,
233 ) -> AlmanacResult<Occultation> {
234 self.occultation(SUN_J2000, eclipsing_frame, observer, ab_corr)
235 }
236
237 pub fn beta_angle_deg(&self, state: Orbit, ab_corr: Option<Aberration>) -> AlmanacResult<f64> {
247 let u_sun = self.sun_unit_vector(state.epoch, state.frame, ab_corr)?;
248 let u_hvec = state.h_hat().map_err(|e| AlmanacError::GenericError {
249 err: format!("{e}"),
250 })?;
251
252 Ok(u_hvec.dot(&u_sun).asin().to_degrees())
253 }
254
255 pub fn local_solar_time(
257 &self,
258 state: Orbit,
259 ab_corr: Option<Aberration>,
260 ) -> AlmanacResult<Duration> {
261 let u_sun = self.sun_unit_vector(state.epoch, state.frame, ab_corr)?;
262 let u_hvec = state.h_hat().map_err(|e| AlmanacError::GenericError {
263 err: format!("{e}"),
264 })?;
265
266 let u = u_sun.cross(&u_hvec);
267 let v = u_hvec.cross(&u);
268
269 let sin_theta = v.dot(&state.r_hat());
270 let cos_theta = u.dot(&state.r_hat());
271
272 let theta_rad = sin_theta.atan2(cos_theta);
273 let lst_h = (theta_rad / PI * 12.0 + 6.0).rem_euclid(24.0);
274
275 Ok(Unit::Hour * lst_h)
276 }
277
278 pub fn ltan(&self, orbit: Orbit, ab_corr: Option<Aberration>) -> AlmanacResult<Duration> {
288 let sun_state = self.transform(SUN_J2000, orbit.frame, orbit.epoch, ab_corr)?;
289 let ra_sun_deg = sun_state.right_ascension_deg();
290 let raan_orbit_deg = orbit.raan_deg().map_err(|e| AlmanacError::GenericError {
291 err: format!("{e}"),
292 })?;
293 let ltan = (12.0 + (raan_orbit_deg - ra_sun_deg) / 15.0).rem_euclid(24.0);
294 Ok(Unit::Hour * ltan)
295 }
296
297 pub fn ltdn(&self, orbit: Orbit, ab_corr: Option<Aberration>) -> AlmanacResult<Duration> {
306 let ltan_h = self.ltan(orbit, ab_corr)?.to_unit(Unit::Hour);
307 let ltdn_h = (ltan_h + 12.0).rem_euclid(24.0);
308 Ok(Unit::Hour * ltdn_h)
309 }
310}
311
312fn compute_occultation_percentage(
314 d_prime: f64,
315 r_ls_prime: f64,
316 r_fobj_prime: f64,
317) -> AlmanacResult<f64> {
318 if d_prime - r_ls_prime > r_fobj_prime {
319 Ok(0.0)
323 } else if r_fobj_prime > d_prime + r_ls_prime {
324 Ok(100.0)
326 } else if (r_ls_prime - r_fobj_prime).abs() < d_prime && d_prime < r_ls_prime + r_fobj_prime {
327 let d1 = (d_prime.powi(2) - r_ls_prime.powi(2) + r_fobj_prime.powi(2)) / (2.0 * d_prime);
336 let d2 = (d_prime.powi(2) + r_ls_prime.powi(2) - r_fobj_prime.powi(2)) / (2.0 * d_prime);
337
338 let shadow_area = circ_seg_area(r_fobj_prime, d1) + circ_seg_area(r_ls_prime, d2);
339 if shadow_area.is_nan() {
340 error!(
341 "Shadow area is NaN! Please file a bug with initial states, eclipsing bodies, etc."
342 );
343 return Ok(100.0);
344 }
345 let nominal_area = core::f64::consts::PI * r_ls_prime.powi(2);
347 Ok(100.0 * shadow_area / nominal_area)
349 } else {
350 Ok(100.0 * r_fobj_prime.powi(2) / r_ls_prime.powi(2))
353 }
354}
355
356fn circ_seg_area(r: f64, d: f64) -> f64 {
358 r.powi(2) * (d / r).acos() - d * (r.powi(2) - d.powi(2)).sqrt()
359}
360
361#[cfg(test)]
362mod ut_los {
363 use crate::constants::frames::{EARTH_J2000, MOON_J2000};
364
365 use super::*;
366 use crate::math::Vector3;
367 use hifitime::Epoch;
368 use rstest::*;
369
370 #[fixture]
371 pub fn almanac() -> Almanac {
372 use std::path::PathBuf;
373
374 let manifest_dir =
375 PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or(".".to_string()));
376
377 Almanac::new(
378 &manifest_dir
379 .clone()
380 .join("../data/de440s.bsp")
381 .to_string_lossy(),
382 )
383 .unwrap()
384 .load(
385 &manifest_dir
386 .clone()
387 .join("../data/pck08.pca")
388 .to_string_lossy(),
389 )
390 .unwrap()
391 }
392
393 #[rstest]
394 fn los_edge_case(almanac: Almanac) {
395 let eme2k = almanac.frame_info(EARTH_J2000).unwrap();
396 let luna = almanac.frame_info(MOON_J2000).unwrap();
397
398 let dt1 = Epoch::from_gregorian_tai_hms(2020, 1, 1, 6, 7, 40);
399 let dt2 = Epoch::from_gregorian_tai_hms(2020, 1, 1, 6, 7, 50);
400 let dt3 = Epoch::from_gregorian_tai_hms(2020, 1, 1, 6, 8, 0);
401
402 let xmtr1 = Orbit::new(
403 397_477.494_485,
404 -57_258.902_156,
405 -62_857.909_437,
406 0.230_482,
407 2.331_362,
408 0.615_501,
409 dt1,
410 eme2k,
411 );
412 let rcvr1 = Orbit::new(
413 338_335.467_589,
414 -55_439.526_977,
415 -13_327.354_273,
416 0.197_141,
417 0.944_261,
418 0.337_407,
419 dt1,
420 eme2k,
421 );
422 let xmtr2 = Orbit::new(
423 397_479.756_900,
424 -57_235.586_465,
425 -62_851.758_851,
426 0.222_000,
427 2.331_768,
428 0.614_614,
429 dt2,
430 eme2k,
431 );
432 let rcvr2 = Orbit::new(
433 338_337.438_860,
434 -55_430.084_340,
435 -13_323.980_229,
436 0.197_113,
437 0.944_266,
438 0.337_402,
439 dt2,
440 eme2k,
441 );
442 let xmtr3 = Orbit::new(
443 397_481.934_480,
444 -57_212.266_970,
445 -62_845.617_185,
446 0.213_516,
447 2.332_122,
448 0.613_717,
449 dt3,
450 eme2k,
451 );
452 let rcvr3 = Orbit::new(
453 338_339.409_858,
454 -55_420.641_651,
455 -13_320.606_228,
456 0.197_086,
457 0.944_272,
458 0.337_398,
459 dt3,
460 eme2k,
461 );
462
463 assert_eq!(
464 almanac.line_of_sight_obstructed(xmtr1, rcvr1, luna, None),
465 Ok(true)
466 );
467 assert_eq!(
468 almanac.line_of_sight_obstructed(xmtr2, rcvr2, luna, None),
469 Ok(true)
470 );
471 assert_eq!(
472 almanac.line_of_sight_obstructed(xmtr3, rcvr3, luna, None),
473 Ok(true)
474 );
475
476 assert_eq!(
478 almanac.line_of_sight_obstructed(rcvr1, xmtr1, luna, None),
479 Ok(true)
480 );
481 assert_eq!(
482 almanac.line_of_sight_obstructed(rcvr2, xmtr2, luna, None),
483 Ok(true)
484 );
485 assert_eq!(
486 almanac.line_of_sight_obstructed(rcvr3, xmtr3, luna, None),
487 Ok(true)
488 );
489 }
490
491 #[rstest]
492 fn los_earth_eclipse(almanac: Almanac) {
493 let eme2k = almanac.frame_info(EARTH_J2000).unwrap();
494
495 let dt = Epoch::from_gregorian_tai_at_midnight(2020, 1, 1);
496
497 let sma = eme2k.mean_equatorial_radius_km().unwrap() + 300.0;
498
499 let sc1 = Orbit::keplerian(sma, 0.001, 0.1, 90.0, 75.0, 0.0, dt, eme2k);
500 let sc2 = Orbit::keplerian(sma + 1.0, 0.001, 0.1, 90.0, 75.0, 0.0, dt, eme2k);
501 let sc3 = Orbit::keplerian(sma, 0.001, 0.1, 90.0, 75.0, 180.0, dt, eme2k);
502
503 assert_eq!(
505 almanac.line_of_sight_obstructed(sc1, sc3, eme2k, None),
506 Ok(true)
507 );
508
509 assert_eq!(
510 almanac.line_of_sight_obstructed(sc2, sc1, eme2k, None),
511 Ok(false)
512 );
513
514 assert_eq!(
516 almanac.line_of_sight_obstructed(sc1, sc2, eme2k, None),
517 Ok(false)
518 );
519 }
520
521 #[test]
522 fn test_compute_occultation() {
523 let d1 = 200.0;
529 let r1 = 100.0; let d2 = 200.0;
531 let r2 = 100.0; let sep = PI / 3.0;
534
535 let r_obs_to_front = Vector3::new(d1, 0.0, 0.0);
536 let r_back_to_obs = Vector3::new(-d2 * sep.cos(), -d2 * sep.sin(), 0.0);
537
538 let r_ls = r_back_to_obs;
539 let r_eb = r_obs_to_front;
540
541 let bobj_mean_eq_radius_km = r2;
542 let fobj_mean_eq_radius_km = r1;
543
544 let r_ls_prime = if bobj_mean_eq_radius_km >= r_ls.norm() {
546 core::f64::consts::FRAC_PI_2
547 } else {
548 (bobj_mean_eq_radius_km / r_ls.norm()).asin()
549 };
550
551 let r_fobj_prime = if fobj_mean_eq_radius_km >= r_eb.norm() {
552 core::f64::consts::FRAC_PI_2
553 } else {
554 (fobj_mean_eq_radius_km / r_eb.norm()).asin()
555 };
556
557 let d_prime = (-(r_ls.dot(&r_eb)) / (r_eb.norm() * r_ls.norm())).acos();
559
560 let pct = compute_occultation_percentage(d_prime, r_ls_prime, r_fobj_prime).unwrap();
561
562 println!("External Tangency Percentage: {}", pct);
563 assert!(pct <= 0.001);
564
565 let d_inside = 0.0005; let r_front_small = 0.001; let r_obs_to_front_small = Vector3::new(d_inside, 0.0, 0.0);
573 let r_back_to_obs_far = Vector3::new(-10000.0 * sep.cos(), -10000.0 * sep.sin(), 0.0); let r_back_small = 10.0;
575
576 let r_ls = r_back_to_obs_far;
577 let r_eb = r_obs_to_front_small;
578
579 let bobj_mean_eq_radius_km = r_back_small;
580 let fobj_mean_eq_radius_km = r_front_small;
581
582 let r_ls_prime = if bobj_mean_eq_radius_km >= r_ls.norm() {
583 core::f64::consts::FRAC_PI_2
584 } else {
585 (bobj_mean_eq_radius_km / r_ls.norm()).asin()
586 };
587
588 let r_fobj_prime = if fobj_mean_eq_radius_km >= r_eb.norm() {
589 core::f64::consts::FRAC_PI_2
590 } else {
591 (fobj_mean_eq_radius_km / r_eb.norm()).asin()
592 };
593
594 let d_prime = (-(r_ls.dot(&r_eb)) / (r_eb.norm() * r_ls.norm())).acos();
596
597 let pct_inside = compute_occultation_percentage(d_prime, r_ls_prime, r_fobj_prime).unwrap();
598
599 println!("Inside Small Body Percentage: {}", pct_inside);
600 assert!(pct_inside >= 99.999);
601 }
602}