Skip to content

Commit 3aefe89

Browse files
Fixed range rate computation & add GMAT validation
1 parent ab2d70e commit 3aefe89

File tree

6 files changed

+272
-14
lines changed

6 files changed

+272
-14
lines changed

anise/src/almanac/aer.rs

+10-8
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ use crate::{
1616
frames::Frame,
1717
math::angles::{between_0_360, between_pm_180},
1818
prelude::Orbit,
19-
time::uuid_from_epoch,
2019
};
2120

2221
use super::Almanac;
@@ -81,10 +80,9 @@ impl Almanac {
8180
}
8281

8382
// Compute the SEZ DCM
84-
let from = uuid_from_epoch(tx.frame.orientation_id, rx.epoch);
8583
// SEZ DCM is topo to fixed
8684
let sez_dcm = tx
87-
.dcm_from_topocentric_to_body_fixed(from)
85+
.dcm_from_topocentric_to_body_fixed(-1)
8886
.context(EphemerisPhysicsSnafu { action: "" })
8987
.context(EphemerisSnafu {
9088
action: "computing SEZ DCM for AER",
@@ -96,7 +94,7 @@ impl Almanac {
9694
action: "transforming transmitter to SEZ",
9795
})?;
9896

99-
// Convert the receiver into the transmitter frame.
97+
// Convert the receiver into the body fixed transmitter frame.
10098
let rx_in_tx_frame = self.transform_to(rx, tx.frame, ab_corr)?;
10199
// Convert into SEZ frame
102100
let rx_sez = (sez_dcm.transpose() * rx_in_tx_frame)
@@ -105,12 +103,16 @@ impl Almanac {
105103
action: "transforming received to SEZ",
106104
})?;
107105

108-
// Compute the range ρ.
106+
// Convert transmitter and receiver into the transmitter frame
107+
let rx_in_tx_frame = self.transform_to(rx, tx.frame, ab_corr)?;
108+
109+
// Compute the range ρ in the SEZ frame for az/el
109110
let rho_sez = rx_sez.radius_km - tx_sez.radius_km;
111+
// And in the inertial frame for range and range-rate
112+
let rho_tx_frame = rx_in_tx_frame.radius_km - tx.radius_km;
110113

111114
// Compute the range-rate \dot ρ
112-
let range_rate_km_s =
113-
rho_sez.dot(&(rx_sez.velocity_km_s - tx_sez.velocity_km_s)) / rho_sez.norm();
115+
let range_rate_km_s = rho_tx_frame.dot(&rx_in_tx_frame.velocity_km_s) / rho_tx_frame.norm();
114116

115117
// Finally, compute the elevation (math is the same as declination)
116118
// Source: Vallado, section 4.4.3
@@ -127,7 +129,7 @@ impl Almanac {
127129
epoch: tx.epoch,
128130
azimuth_deg,
129131
elevation_deg,
130-
range_km: rho_sez.norm(),
132+
range_km: rho_tx_frame.norm(),
131133
range_rate_km_s,
132134
obstructed_by,
133135
light_time: (rho_sez.norm() / SPEED_OF_LIGHT_KM_S).seconds(),

anise/src/astro/orbit.rs

+37-5
Original file line numberDiff line numberDiff line change
@@ -320,20 +320,52 @@ impl Orbit {
320320
#[allow(clippy::too_many_arguments)]
321321
#[cfg_attr(feature = "python", pymethods)]
322322
impl Orbit {
323+
/// Builds the rotation matrix that rotates from the topocentric frame (SEZ) into the body fixed frame of this state.
324+
///
325+
/// # Frame warnings
326+
/// + If the state is NOT in a body fixed frame (i.e. ITRF93), then this computation is INVALID.
327+
/// + (Usually) no time derivative can be computed: the orbit is expected to be a body fixed frame where the `at_epoch` function will fail. Exceptions for Moon body fixed frames.
328+
///
329+
/// # UNUSED Arguments
330+
/// + `from`: ID of this new frame. Only used to set the "from" frame of the DCM. -- No longer used since 0.5.3
331+
///
332+
/// # Source
333+
/// From the GMAT MathSpec, page 30 section 2.6.9 and from `Calculate_RFT` in `TopocentricAxes.cpp`, this returns the
334+
/// rotation matrix from the topocentric frame (SEZ) to body fixed frame.
335+
/// In the GMAT MathSpec notation, R_{IF} is the DCM from body fixed to inertial. Similarly, R{FT} is from topocentric
336+
/// to body fixed.
337+
pub fn dcm_from_topocentric_to_body_fixed(&self, _from: NaifId) -> PhysicsResult<DCM> {
338+
let rot_mat_dt = if let Ok(pre) = self.at_epoch(self.epoch - Unit::Second * 1) {
339+
if let Ok(post) = self.at_epoch(self.epoch + Unit::Second * 1) {
340+
let dcm_pre = pre.dcm3x3_from_topocentric_to_body_fixed()?;
341+
let dcm_post = post.dcm3x3_from_topocentric_to_body_fixed()?;
342+
Some(0.5 * dcm_post.rot_mat - 0.5 * dcm_pre.rot_mat)
343+
} else {
344+
None
345+
}
346+
} else {
347+
None
348+
};
349+
350+
Ok(DCM {
351+
rot_mat: self.dcm3x3_from_topocentric_to_body_fixed()?.rot_mat,
352+
rot_mat_dt,
353+
from: uuid_from_epoch(self.frame.orientation_id, self.epoch),
354+
to: self.frame.orientation_id,
355+
})
356+
}
357+
323358
/// Builds the rotation matrix that rotates from the topocentric frame (SEZ) into the body fixed frame of this state.
324359
///
325360
/// # Frame warning
326361
/// If the state is NOT in a body fixed frame (i.e. ITRF93), then this computation is INVALID.
327362
///
328-
/// # Arguments
329-
/// + `from`: ID of this new frame, must be unique if it'll be added to the Almanac. Only used to set the "from" frame of the DCM.
330-
///
331363
/// # Source
332364
/// From the GMAT MathSpec, page 30 section 2.6.9 and from `Calculate_RFT` in `TopocentricAxes.cpp`, this returns the
333365
/// rotation matrix from the topocentric frame (SEZ) to body fixed frame.
334366
/// In the GMAT MathSpec notation, R_{IF} is the DCM from body fixed to inertial. Similarly, R{FT} is from topocentric
335367
/// to body fixed.
336-
pub fn dcm_from_topocentric_to_body_fixed(&self, from: NaifId) -> PhysicsResult<DCM> {
368+
pub fn dcm3x3_from_topocentric_to_body_fixed(&self) -> PhysicsResult<DCM> {
337369
if (self.radius_km.x.powi(2) + self.radius_km.y.powi(2)).sqrt() < 1e-3 {
338370
warn!("SEZ frame ill-defined when close to the poles");
339371
}
@@ -357,7 +389,7 @@ impl Orbit {
357389
Ok(DCM {
358390
rot_mat,
359391
rot_mat_dt: None,
360-
from,
392+
from: uuid_from_epoch(self.frame.orientation_id, self.epoch),
361393
to: self.frame.orientation_id,
362394
})
363395
}

anise/src/frames/frame.rs

+9
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ impl Frame {
9191
self.shape = Some(shape);
9292
self
9393
}
94+
95+
/// Returns a copy of this frame with the graviational parameter and the shape information from this frame.
96+
/// Use this to prevent astrodynamical computations.
97+
///
98+
/// :rtype: None
99+
pub fn stripped(mut self) -> Self {
100+
self.strip();
101+
self
102+
}
94103
}
95104

96105
#[cfg(feature = "python")]

anise/tests/astro/aer.rs

+214
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
use anise::{
2+
constants::{
3+
frames::{EME2000, IAU_EARTH_FRAME},
4+
usual_planetary_constants::MEAN_EARTH_ANGULAR_VELOCITY_DEG_S,
5+
},
6+
prelude::{Almanac, Orbit},
7+
};
8+
use core::str::FromStr;
9+
use hifitime::Epoch;
10+
11+
// Define location of DSN DSS-65 in Madrid, Spain
12+
const DSS65_LATITUDE_DEG: f64 = 40.427_222;
13+
const DSS65_LONGITUDE_DEG: f64 = 4.250_556;
14+
const DSS65_HEIGHT_KM: f64 = 0.834_939;
15+
16+
/// Validation test for azimuth, elevation, range, and range-rate computation.
17+
/// Importantly, we only test for range-rate here; I forget the set up used for DSS65 in GMAT, and could not export the az/el data.
18+
#[test]
19+
fn validate_aer_vs_gmat_cislunar1() {
20+
let almanac = Almanac::default()
21+
.load("../data/earth_latest_high_prec.bpc")
22+
.unwrap()
23+
.load("../data/pck11.pca")
24+
.unwrap()
25+
.load("../data/de430.bsp")
26+
.unwrap();
27+
28+
let eme2k = almanac.frame_from_uid(EME2000).unwrap();
29+
30+
let states = &[
31+
Orbit::new(
32+
58643.769540,
33+
-61696.435624,
34+
-36178.745722,
35+
2.148654,
36+
-1.202489,
37+
-0.714016,
38+
Epoch::from_str("2023-11-16T13:35:30.231999909 UTC").unwrap(),
39+
eme2k,
40+
),
41+
Orbit::new(
42+
66932.786920,
43+
-66232.188134,
44+
-38873.611383,
45+
2.040555,
46+
-1.092316,
47+
-0.649376,
48+
Epoch::from_str("2023-11-16T14:41:30.231999930 UTC").unwrap(),
49+
eme2k,
50+
),
51+
Orbit::new(
52+
74004.678872,
53+
-69951.400821,
54+
-41085.748329,
55+
1.956606,
56+
-1.011239,
57+
-0.601766,
58+
Epoch::from_str("2023-11-16T15:40:30.231999839 UTC").unwrap(),
59+
eme2k,
60+
),
61+
Orbit::new(
62+
80796.572756,
63+
-73405.951304,
64+
-43142.418173,
65+
1.882015,
66+
-0.942232,
67+
-0.561216,
68+
Epoch::from_str("2023-11-16T16:39:30.232000062 UTC").unwrap(),
69+
eme2k,
70+
),
71+
Orbit::new(
72+
91643.444941,
73+
-78707.219860,
74+
-46302.227968,
75+
1.773135,
76+
-0.846264,
77+
-0.504775,
78+
Epoch::from_str("2023-11-16T18:18:30.231999937 UTC").unwrap(),
79+
eme2k,
80+
),
81+
];
82+
83+
let observations = &[
84+
(91457.558, 2.199),
85+
(99965.056, 2.105),
86+
(107322.912, 2.056),
87+
(114551.675, 2.031),
88+
(126573.919, 2.021),
89+
];
90+
91+
for (rx, (range_km, range_rate_km_s)) in
92+
states.iter().copied().zip(observations.iter().copied())
93+
{
94+
// Rebuild the ground stations
95+
let tx = Orbit::try_latlongalt(
96+
DSS65_LATITUDE_DEG,
97+
DSS65_LONGITUDE_DEG,
98+
DSS65_HEIGHT_KM,
99+
MEAN_EARTH_ANGULAR_VELOCITY_DEG_S,
100+
rx.epoch,
101+
almanac.frame_from_uid(IAU_EARTH_FRAME).unwrap(),
102+
)
103+
.unwrap();
104+
105+
let aer = almanac
106+
.azimuth_elevation_range_sez(rx, tx, None, None)
107+
.unwrap();
108+
109+
dbg!(aer.range_km - range_km);
110+
assert!(
111+
(aer.range_rate_km_s - range_rate_km_s).abs() < 1e-3,
112+
"more than 1 m/s error!"
113+
);
114+
}
115+
}
116+
117+
#[test]
118+
fn validate_aer_vs_gmat_cislunar2() {
119+
let almanac = Almanac::default()
120+
.load("../data/earth_latest_high_prec.bpc")
121+
.unwrap()
122+
.load("../data/pck11.pca")
123+
.unwrap()
124+
.load("../data/de430.bsp")
125+
.unwrap();
126+
127+
let eme2k = almanac.frame_from_uid(EME2000).unwrap();
128+
129+
let states = &[
130+
Orbit::new(
131+
102114.297454,
132+
13933.746232,
133+
-671.117990,
134+
2.193540,
135+
0.906982,
136+
0.333105,
137+
Epoch::from_str("2022-11-29T14:39:28.000000216 TAI").unwrap(),
138+
eme2k,
139+
),
140+
Orbit::new(
141+
110278.148176,
142+
17379.224108,
143+
608.602854,
144+
2.062036,
145+
0.887598,
146+
0.333149,
147+
Epoch::from_str("2022-11-29T15:43:28.000000160 TAI").unwrap(),
148+
eme2k,
149+
),
150+
Orbit::new(
151+
117388.586896,
152+
20490.340765,
153+
1786.240391,
154+
1.957486,
155+
0.870185,
156+
0.332038,
157+
Epoch::from_str("2022-11-29T16:42:28.000000384 TAI").unwrap(),
158+
eme2k,
159+
),
160+
Orbit::new(
161+
124151.820782,
162+
23540.835319,
163+
2958.593254,
164+
1.865399,
165+
0.853363,
166+
0.330212,
167+
Epoch::from_str("2022-11-29T17:41:28.000000293 TAI").unwrap(),
168+
eme2k,
169+
),
170+
Orbit::new(
171+
131247.969145,
172+
26834.012939,
173+
4241.579371,
174+
1.775455,
175+
0.835578,
176+
0.327653,
177+
Epoch::from_str("2022-11-29T18:46:28.000000433 TAI").unwrap(),
178+
eme2k,
179+
),
180+
];
181+
182+
let observations = &[
183+
(102060.177, 1.957),
184+
(109389.490, 1.868),
185+
(115907.202, 1.820),
186+
(122305.708, 1.799),
187+
(129320.821, 1.802),
188+
];
189+
190+
for (rx, (range_km, range_rate_km_s)) in
191+
states.iter().copied().zip(observations.iter().copied())
192+
{
193+
// Rebuild the ground stations
194+
let tx = Orbit::try_latlongalt(
195+
DSS65_LATITUDE_DEG,
196+
DSS65_LONGITUDE_DEG,
197+
DSS65_HEIGHT_KM,
198+
MEAN_EARTH_ANGULAR_VELOCITY_DEG_S,
199+
rx.epoch,
200+
almanac.frame_from_uid(IAU_EARTH_FRAME).unwrap(),
201+
)
202+
.unwrap();
203+
204+
let aer = almanac
205+
.azimuth_elevation_range_sez(rx, tx, None, None)
206+
.unwrap();
207+
208+
dbg!(aer.range_km - range_km);
209+
assert!(
210+
(aer.range_rate_km_s - range_rate_km_s).abs() < 1e-3,
211+
"more than 1 m/s error!"
212+
);
213+
}
214+
}

anise/tests/astro/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
mod aer;
12
mod orbit;

anise/tests/orientations/validation.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ fn validate_bpc_to_iau_rotations() {
666666
dcm.rot_mat - rot_mat
667667
);
668668

669-
// Check the derivative with a slightly tighet constraint
669+
// Check the derivative with a slightly tighter constraint
670670
assert!(
671671
(dcm.rot_mat_dt.unwrap() - spice_dcm.rot_mat_dt.unwrap()).norm()
672672
< DCM_EPSILON * 0.1,

0 commit comments

Comments
 (0)