1use crate::{
12 astro::{Aberration, AzElRange},
13 constants::SPEED_OF_LIGHT_KM_S,
14 ephemerides::{EphemerisError, EphemerisPhysicsSnafu},
15 errors::{AlmanacError, EphemerisSnafu, OrientationSnafu, PhysicsError},
16 frames::Frame,
17 math::angles::{between_0_360, between_pm_180},
18 prelude::Orbit,
19 structure::location::Location,
20};
21
22use super::Almanac;
23use crate::errors::AlmanacResult;
24
25use hifitime::TimeUnits;
26use log::warn;
27
28use snafu::ResultExt;
29
30impl Almanac {
31 pub fn azimuth_elevation_range_sez(
46 &self,
47 rx: Orbit,
48 tx: Orbit,
49 obstructing_body: Option<Frame>,
50 ab_corr: Option<Aberration>,
51 ) -> AlmanacResult<AzElRange> {
52 if tx.epoch != rx.epoch {
53 return Err(AlmanacError::Ephemeris {
54 action: "",
55 source: Box::new(EphemerisError::EphemerisPhysics {
56 action: "computing AER",
57 source: PhysicsError::EpochMismatch {
58 action: "computing AER",
59 epoch1: tx.epoch,
60 epoch2: rx.epoch,
61 },
62 }),
63 });
64 }
65
66 let mut obstructed_by = None;
67 if let Some(obstructing_body) = obstructing_body {
68 if self.line_of_sight_obstructed(tx, rx, obstructing_body, ab_corr)? {
69 obstructed_by = Some(obstructing_body);
70 }
71 }
72
73 let sez_dcm = tx
76 .dcm_from_topocentric_to_body_fixed(-1)
77 .context(EphemerisPhysicsSnafu { action: "" })
78 .context(EphemerisSnafu {
79 action: "computing SEZ DCM for AER",
80 })?;
81
82 let tx_sez = (sez_dcm.transpose() * tx)
83 .context(EphemerisPhysicsSnafu { action: "" })
84 .context(EphemerisSnafu {
85 action: "transforming transmitter to SEZ",
86 })?;
87
88 let rx_in_tx_frame = self.transform_to(rx, tx.frame, ab_corr)?;
90 let rx_sez = (sez_dcm.transpose() * rx_in_tx_frame)
92 .context(EphemerisPhysicsSnafu { action: "" })
93 .context(EphemerisSnafu {
94 action: "transforming received to SEZ",
95 })?;
96
97 let rx_in_tx_frame = self.transform_to(rx, tx.frame, ab_corr)?;
99
100 let rho_sez = rx_sez.radius_km - tx_sez.radius_km;
102 let rho_tx_frame = rx_in_tx_frame.radius_km - tx.radius_km;
105
106 let range_rate_km_s = rho_tx_frame.dot(&rx_in_tx_frame.velocity_km_s) / rho_tx_frame.norm();
108
109 let elevation_deg = between_pm_180((rho_sez.z / rho_sez.norm()).asin().to_degrees());
114 if (elevation_deg - 90.0).abs() < 1e-6 {
115 warn!("object nearly overhead (el = {elevation_deg:.6} deg), azimuth may be incorrect");
116 }
117 let azimuth_deg = between_0_360((rho_sez.y.atan2(-rho_sez.x)).to_degrees());
119
120 Ok(AzElRange {
121 epoch: tx.epoch,
122 azimuth_deg,
123 elevation_deg,
124 range_km: rho_sez.norm(),
125 range_rate_km_s,
126 obstructed_by,
127 light_time: (rho_sez.norm() / SPEED_OF_LIGHT_KM_S).seconds(),
128 })
129 }
130
131 pub fn azimuth_elevation_range_sez_from_location_id(
135 &self,
136 rx: Orbit,
137 location_id: i32,
138 obstructing_body: Option<Frame>,
139 ab_corr: Option<Aberration>,
140 ) -> AlmanacResult<AzElRange> {
141 match self.location_data.get_by_id(location_id) {
142 Ok(location) => self.azimuth_elevation_range_sez_from_location(
143 rx,
144 location,
145 obstructing_body,
146 ab_corr,
147 ),
148
149 Err(source) => Err(AlmanacError::TLDataSet {
150 action: "AER for location",
151 source,
152 }),
153 }
154 }
155
156 pub fn azimuth_elevation_range_sez_from_location_name(
160 &self,
161 rx: Orbit,
162 location_name: &str,
163 obstructing_body: Option<Frame>,
164 ab_corr: Option<Aberration>,
165 ) -> AlmanacResult<AzElRange> {
166 match self.location_data.get_by_name(location_name) {
167 Ok(location) => self.azimuth_elevation_range_sez_from_location(
168 rx,
169 location,
170 obstructing_body,
171 ab_corr,
172 ),
173
174 Err(source) => Err(AlmanacError::TLDataSet {
175 action: "AER for location",
176 source,
177 }),
178 }
179 }
180
181 pub fn azimuth_elevation_range_sez_from_location(
186 &self,
187 rx: Orbit,
188 location: Location,
189 obstructing_body: Option<Frame>,
190 ab_corr: Option<Aberration>,
191 ) -> AlmanacResult<AzElRange> {
192 let epoch = rx.epoch;
193 let from_frame =
195 self.frame_info(location.frame)
196 .map_err(|e| AlmanacError::GenericError {
197 err: format!("{e} when fetching {} frame data", location.frame),
198 })?;
199 let omega = self
200 .angular_velocity_wtr_j2000_rad_s(from_frame, epoch)
201 .context(OrientationSnafu {
202 action: "AER computation from location ID",
203 })?;
204 match Orbit::try_latlongalt_omega(
206 location.latitude_deg,
207 location.longitude_deg,
208 location.height_km,
209 omega,
210 epoch,
211 from_frame,
212 ) {
213 Ok(tx) => self
214 .azimuth_elevation_range_sez(rx, tx, obstructing_body, ab_corr)
215 .map(|mut aer| {
216 if location.elevation_mask_at_azimuth_deg(aer.azimuth_deg) >= aer.elevation_deg
218 {
219 aer.obstructed_by = Some(from_frame);
221 if !location.terrain_mask_ignored {
222 aer.range_km = f64::NAN;
223 aer.range_rate_km_s = f64::NAN;
224 aer.azimuth_deg = f64::NAN;
225 aer.elevation_deg = f64::NAN;
226 }
227 }
228 aer
230 }),
231 Err(source) => Err(AlmanacError::Ephemeris {
232 action: "AER from location: could not build transmitter state",
233 source: Box::new(EphemerisError::EphemerisPhysics {
234 action: "try_latlongalt_omega",
235 source,
236 }),
237 }),
238 }
239 }
240}
241
242#[cfg(test)]
243mod ut_aer {
244 use core::str::FromStr;
245 use std::path::Path;
246
247 use hifitime::Unit;
248
249 use crate::astro::orbit::Orbit;
250 use crate::astro::AzElRange;
251 use crate::constants::frames::{EARTH_ITRF93, EARTH_J2000, IAU_EARTH_FRAME};
252 use crate::constants::usual_planetary_constants::MEAN_EARTH_ANGULAR_VELOCITY_DEG_S;
253 use crate::math::cartesian::CartesianState;
254 use crate::prelude::{Almanac, Epoch};
255 use crate::structure::location::{Location, TerrainMask};
256 use crate::structure::LocationDataSet;
257
258 #[test]
259 fn verif_edge_case() {
260 let almanac = Almanac::new("../data/pck08.pca").unwrap();
261 let itrf93 = almanac.frame_info(EARTH_ITRF93).unwrap();
262
263 let latitude_deg = -7.906_635_7;
265 let longitude_deg = 345.5975;
266 let height_km = 56.0e-3;
267 let epoch = Epoch::from_gregorian_utc_at_midnight(2024, 1, 14);
268
269 let ground_station = Orbit::try_latlongalt(
270 latitude_deg,
271 longitude_deg,
272 height_km,
273 MEAN_EARTH_ANGULAR_VELOCITY_DEG_S,
274 epoch,
275 itrf93,
276 )
277 .unwrap();
278
279 let aer = almanac
280 .azimuth_elevation_range_sez(ground_station, ground_station, None, None)
281 .unwrap();
282
283 assert!(!aer.is_valid());
284 }
285
286 #[cfg(feature = "metaload")]
290 #[test]
291 fn gmat_verif() {
292 use crate::prelude::MetaAlmanac;
293 let latitude_deg = 40.427_222;
295 let longitude_deg = 4.250_556;
296 let height_km = 0.834_939;
297
298 let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../data/aer_regression.dhall");
299 let almanac = MetaAlmanac::new(path.to_str().unwrap())
300 .unwrap()
301 .process(false)
302 .unwrap();
303
304 let iau_earth = almanac.frame_info(IAU_EARTH_FRAME).unwrap();
305 let eme2k = almanac.frame_info(EARTH_J2000).unwrap();
306
307 let gmat_ranges_km = [
309 9.145_755_787_575_61e4,
310 9.996_505_560_799_869e4,
311 1.073_229_118_411_670_2e5,
312 1.145_516_751_191_464_7e5,
313 1.265_739_190_638_930_7e5,
314 ];
315
316 let regression_data = [
317 AzElRange {
318 epoch: Epoch::from_str("2023-11-16T13:35:30.231999909 UTC").unwrap(),
319 azimuth_deg: 133.59998745846255,
320 elevation_deg: 7.23756749931629,
321 range_km: 91457.2680164461,
322 range_rate_km_s: 2.198785823156608,
323 obstructed_by: None,
324 light_time: 305068608 * Unit::Nanosecond,
325 },
326 AzElRange {
327 epoch: Epoch::from_str("2023-11-16T14:41:30.231999930 UTC").unwrap(),
328 azimuth_deg: 145.20134040829316,
329 elevation_deg: 15.541883052027405,
330 range_km: 99963.52694785153,
331 range_rate_km_s: 2.1050771837046436,
332 obstructed_by: None,
333 light_time: 333442434 * Unit::Nanosecond,
334 },
335 AzElRange {
336 epoch: Epoch::from_str("2023-11-16T15:40:30.231999839 UTC").unwrap(),
337 azimuth_deg: 157.35605910179052,
338 elevation_deg: 21.262025972059224,
339 range_km: 107320.26696466877,
340 range_rate_km_s: 2.0559576546712433,
341 obstructed_by: None,
342 light_time: 357981877 * Unit::Nanosecond,
343 },
344 AzElRange {
345 epoch: Epoch::from_str("2023-11-16T16:39:30.232000062 UTC").unwrap(),
346 azimuth_deg: 171.0253271744456,
347 elevation_deg: 24.777800273900453,
348 range_km: 114548.0748997545,
349 range_rate_km_s: 2.0308909733778924,
350 obstructed_by: None,
351 light_time: 382091249 * Unit::Nanosecond,
352 },
353 AzElRange {
354 epoch: Epoch::from_str("2023-11-16T18:18:30.231999937 UTC").unwrap(),
355 azimuth_deg: 195.44253883914308,
356 elevation_deg: 24.63526601848747,
357 range_km: 126569.46572408297,
358 range_rate_km_s: 2.021336308601692,
359 obstructed_by: None,
360 light_time: 422190293 * Unit::Nanosecond,
361 },
362 ];
363
364 let states = [
365 CartesianState::new(
366 58643.769881020,
367 -61696.430010747,
368 -36178.742480219,
369 2.148654262,
370 -1.202488371,
371 -0.714016096,
372 Epoch::from_str("2023-11-16T13:35:30.231999909 UTC").unwrap(),
373 eme2k,
374 ),
375 CartesianState::new(
376 66932.786922851,
377 -66232.181345574,
378 -38873.607459037,
379 2.040554622,
380 -1.092315772,
381 -0.649375769,
382 Epoch::from_str("2023-11-16T14:41:30.231999930 UTC").unwrap(),
383 eme2k,
384 ),
385 CartesianState::new(
386 74004.678508956,
387 -69951.392953800,
388 -41085.743778595,
389 1.956605843,
390 -1.011238479,
391 -0.601766262,
392 Epoch::from_str("2023-11-16T15:40:30.231999839 UTC").unwrap(),
393 eme2k,
394 ),
395 CartesianState::new(
396 80796.571971532,
397 -73405.942333285,
398 -43142.412981359,
399 1.882014733,
400 -0.942231959,
401 -0.561216138,
402 Epoch::from_str("2023-11-16T16:39:30.232000062 UTC").unwrap(),
403 eme2k,
404 ),
405 CartesianState::new(
406 91643.443331668,
407 -78707.208988294,
408 -46302.221669744,
409 1.773134524,
410 -0.846263432,
411 -0.504774983,
412 Epoch::from_str("2023-11-16T18:18:30.231999937 UTC").unwrap(),
413 eme2k,
414 ),
415 ];
416
417 for (sno, state) in states.iter().copied().enumerate() {
418 let madrid = Orbit::try_latlongalt(
420 latitude_deg,
421 longitude_deg,
422 height_km,
423 MEAN_EARTH_ANGULAR_VELOCITY_DEG_S,
424 state.epoch,
425 iau_earth,
426 )
427 .unwrap();
428
429 let aer = almanac
430 .azimuth_elevation_range_sez(state, madrid, None, None)
431 .unwrap();
432
433 if sno == 0 {
434 assert_eq!(
435 format!("{aer}"),
436 format!(
437 "{}: az.: 133.599987 deg el.: 7.237567 deg range: 91457.268016 km range-rate: 2.198786 km/s obstruction: none",
438 state.epoch
439 )
440 );
441 }
442
443 let expect = gmat_ranges_km[sno];
444
445 assert!((aer.range_km - expect).abs() < 5.0);
449 assert_eq!(aer, regression_data[sno], "{sno} differ");
451 }
452
453 let states = states.map(|state| almanac.transform_to(state, EARTH_ITRF93, None).unwrap());
456
457 for (sno, state) in states.iter().copied().enumerate() {
458 let madrid = Orbit::try_latlongalt(
460 latitude_deg,
461 longitude_deg,
462 height_km,
463 MEAN_EARTH_ANGULAR_VELOCITY_DEG_S,
464 state.epoch,
465 iau_earth,
466 )
467 .unwrap();
468
469 let aer = almanac
470 .azimuth_elevation_range_sez(state, madrid, None, None)
471 .unwrap();
472
473 if sno == 0 {
474 assert_eq!(
475 format!("{aer}"),
476 format!(
477 "{}: az.: 133.599987 deg el.: 7.237567 deg range: 91457.268016 km range-rate: 2.198786 km/s obstruction: none",
478 state.epoch
479 )
480 );
481 }
482
483 let expect = gmat_ranges_km[sno];
484
485 assert!((aer.range_km - expect).abs() < 5.0);
489 assert!(
491 (aer.range_km - regression_data[sno].range_km).abs() < 1e-10,
492 "{sno}"
493 );
494 assert!(
495 (aer.range_rate_km_s - regression_data[sno].range_rate_km_s).abs() < 1e-10,
496 "{sno}"
497 );
498 assert!(
499 (aer.elevation_deg - regression_data[sno].elevation_deg).abs() < 1e-10,
500 "{sno}"
501 );
502 assert!(
503 (aer.azimuth_deg - regression_data[sno].azimuth_deg).abs() < 1e-10,
504 "{sno}"
505 );
506 }
507 }
508
509 #[cfg(feature = "metaload")]
519 #[test]
520 fn gmat_verif_location() {
521 use crate::prelude::MetaAlmanac;
522 let dsn_madrid = Location {
524 latitude_deg: 40.427_222,
525 longitude_deg: 4.250_556,
526 height_km: 0.834_939,
527 frame: EARTH_ITRF93.into(),
528 terrain_mask: vec![
530 TerrainMask {
531 azimuth_deg: 0.0,
532 elevation_mask_deg: 0.0,
533 },
534 TerrainMask {
535 azimuth_deg: 130.0,
536 elevation_mask_deg: 8.0,
537 },
538 TerrainMask {
539 azimuth_deg: 140.0,
540 elevation_mask_deg: 0.0,
541 },
542 ],
543 terrain_mask_ignored: true,
545 };
546
547 let mut loc_data = LocationDataSet::default();
549 loc_data
550 .push(dsn_madrid, Some(123), Some("DSN Madrid"))
551 .unwrap();
552
553 let path = Path::new(env!("CARGO_MANIFEST_DIR"));
554 let mut almanac =
555 MetaAlmanac::new(path.join("../data/aer_regression.dhall").to_str().unwrap())
556 .unwrap()
557 .process(false)
558 .unwrap()
559 .load("../data/pck08.pca")
560 .unwrap();
561 almanac.location_data = loc_data;
562
563 let eme2k = almanac.frame_info(EARTH_J2000).unwrap();
564 let gmat_ranges_km = [
567 9.145_755_787_575_61e4,
568 9.996_505_560_799_869e4,
569 1.073_229_118_411_670_2e5,
570 1.145_516_751_191_464_7e5,
571 1.265_739_190_638_930_7e5,
572 ];
573
574 let states = [
575 CartesianState::new(
576 58643.769881020,
577 -61696.430010747,
578 -36178.742480219,
579 2.148654262,
580 -1.202488371,
581 -0.714016096,
582 Epoch::from_str("2023-11-16T13:35:30.231999909 UTC").unwrap(),
583 eme2k,
584 ),
585 CartesianState::new(
586 66932.786922851,
587 -66232.181345574,
588 -38873.607459037,
589 2.040554622,
590 -1.092315772,
591 -0.649375769,
592 Epoch::from_str("2023-11-16T14:41:30.231999930 UTC").unwrap(),
593 eme2k,
594 ),
595 CartesianState::new(
596 74004.678508956,
597 -69951.392953800,
598 -41085.743778595,
599 1.956605843,
600 -1.011238479,
601 -0.601766262,
602 Epoch::from_str("2023-11-16T15:40:30.231999839 UTC").unwrap(),
603 eme2k,
604 ),
605 CartesianState::new(
606 80796.571971532,
607 -73405.942333285,
608 -43142.412981359,
609 1.882014733,
610 -0.942231959,
611 -0.561216138,
612 Epoch::from_str("2023-11-16T16:39:30.232000062 UTC").unwrap(),
613 eme2k,
614 ),
615 CartesianState::new(
616 91643.443331668,
617 -78707.208988294,
618 -46302.221669744,
619 1.773134524,
620 -0.846263432,
621 -0.504774983,
622 Epoch::from_str("2023-11-16T18:18:30.231999937 UTC").unwrap(),
623 eme2k,
624 ),
625 ];
626
627 for (sno, state) in states.iter().copied().enumerate() {
628 let aer_from_name = almanac
629 .azimuth_elevation_range_sez_from_location_name(state, "DSN Madrid", None, None)
630 .unwrap();
631
632 let expect = gmat_ranges_km[sno];
637 assert!(dbg!(aer_from_name.range_km - expect).abs() < 5.1);
638
639 let aer_from_id = almanac
641 .azimuth_elevation_range_sez_from_location_id(state, 123, None, None)
642 .unwrap();
643
644 assert_eq!(aer_from_id, aer_from_name);
645
646 if sno == 0 {
647 assert!(aer_from_id.is_obstructed(), "terrain should be in the way");
648 }
649 }
650 }
651}