Skip to content

Commit 05db159

Browse files
Finish LRO example
1 parent a45165c commit 05db159

17 files changed

+121
-55
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ exclude = [
2727
"rustfmt.toml",
2828
"tests/GMAT_scripts/*",
2929
"*.png",
30+
"*.pca",
3031
]
3132

3233
[badges]

examples/04_lro_od/README.md

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@
44

55
In this example, you'll learn how to use an "as-flown" (_definitive_) SPICE BSP ephemeris file to simulate orbit determination measurements from ground stations. Then, you'll learn how to set up an orbit determination process in Nyx with high fidelity Moon dynamics and estimate the state of LRO. Finally, you'll learn how to compare two ephemerides in the Radial, In-track, Cross-track (RIC) frame.
66

7+
**Jump to [results](#results)**
8+
79
To run this example, just execute:
810
```sh
911
RUST_LOG=info cargo run --example 04_lro_od --release
1012
```
1113

1214
Building in `release` mode will make the computation significantly faster. Specifying `RUST_LOG=info` will allow you to see all of the information messages happening in ANISE and Nyx throughout the execution of the program.
1315

14-
Throughout this analysis, we'll be focusing on an arbitrarily chosen period of two days.
16+
Throughout this analysis, we'll be focusing on an arbitrarily chosen period of one day started on 2024-01-01 at midnight UTC.
1517

1618
# Preliminary analysis: matching dynamical models
1719

@@ -88,4 +90,116 @@ shape: (9, 2)
8890

8991
![New Lunar GM Pos error](./plots/sim-new-pc-ric-pos-err.png)
9092

91-
![New Lunar GM Vel error](./plots/sim-new-pc-ric-vel-err.png)
93+
![New Lunar GM Vel error](./plots/sim-new-pc-ric-vel-err.png)
94+
95+
# Orbit determination set up
96+
97+
## Ground network
98+
99+
For this example, we simulate measurements from three of the Deep Space Network ground stations: Canberra, Australia; Madrid, Spain; and Goldstone, CA, USA. Nyx allows configuration of ground stations using a YAML input file, cf. [`dsn-network`](./dsn-network.yaml): these are configured as unbiased white noise ground stations where the standard deviation of the white noise is taken directly from the JPL DESCANSO series. The stochastic modeling in Nyx supports first order Gauss Markov processes and biased white noise.
100+
101+
In this simulation, we are generating geometric one-way range and Doppler measurements. Nyx supports all of the aberration computations provided by ANISE (and validated against SPICE).
102+
103+
## Tracking schedule
104+
105+
To prepare for a mission, flight dynamics engineers must simulate a tracking schedule and determine, through trial and error, how much tracking is required throughout the different orbital regimes of the mission. Unlike most orbit determination software, Nyx provides a "schedule generator" for simulation. Refer to [`tracking-cfg.yaml`](./tracking-cfg.yaml) for the tracking configuration. In short, this feature allows engineers to configure the following key inputs to a schedule:
106+
107+
- how to deal with overlapping measurements: greedy, eager, or overlap.
108+
- Greedy: when two stations overlap, the one which was previously tracking will continue until the vehicle is no longer in sight
109+
- Earger: when two stations overlap, the new tracker will start tracking instead of the previous one
110+
- Overlap: both stations may track and generate tracking data at the same time.
111+
- how many minimum samples are needed for this pass to be included: this prevents very short passes;
112+
- are the measurements taken exactly at a round number of seconds;
113+
- what is the sampling rate of this ground station.
114+
115+
The tracking scheduler will start by finding the exact times when the vehicle comes in view, using the embedded event finder on an elevation event.
116+
117+
```log
118+
INFO nyx_space::md::events::search > Searching for DSS-13 Goldstone (lat.: 35.2472 deg long.: 243.2050 deg alt.: 1071.149 m) [Earth IAU_EARTH (μ = 398600.435436096 km^3/s^2)] with initial heuristic of 14 min 24 s
119+
INFO nyx_space::md::events::search > Event DSS-13 Goldstone (lat.: 35.2472 deg long.: 243.2050 deg alt.: 1071.149 m) [Earth IAU_EARTH (μ = 398600.435436096 km^3/s^2)] found 2 times from 2024-01-01T05:49:54.697010810 UTC until 2024-01-01T18:08:53.555326604 UTC
120+
INFO nyx_space::od::simulator::arc > Built 1 tracking strands for DSS-13 Goldstone
121+
INFO nyx_space::od::simulator::arc > Building schedule for DSS-34 Canberra
122+
INFO nyx_space::md::trajectory::sc_traj > Converted trajectory from Moon J2000 (μ = 4902.74987 km^3/s^2, radius = 1737.4 km) to Earth IAU_EARTH (μ = 398600.435436096 km^3/s^2) in 220 ms: Trajectory in Earth IAU_EARTH (μ = 398600.436 km^3/s^2, eq. radius = 6378.1366 km, polar radius = 6356.7519 km, f = 0.0033528131084554717) from 2024-01-01T00:00:00 UTC to 2024-01-02T00:00:00 UTC (1 day, or 86400.000 s) [17281 states]
123+
INFO nyx_space::md::events::search > Searching for DSS-34 Canberra (lat.: -35.3983 deg long.: 148.9819 deg alt.: 691.750 m) [Earth IAU_EARTH (μ = 398600.435436096 km^3/s^2)] with initial heuristic of 14 min 24 s
124+
INFO nyx_space::md::events::search > Event DSS-34 Canberra (lat.: -35.3983 deg long.: 148.9819 deg alt.: 691.750 m) [Earth IAU_EARTH (μ = 398600.435436096 km^3/s^2)] found 2 times from 2024-01-01T13:19:29.424409217 UTC until 2024-01-01T23:47:35.676704956 UTC
125+
INFO nyx_space::od::simulator::arc > Built 1 tracking strands for DSS-34 Canberra
126+
INFO nyx_space::od::simulator::arc > Building schedule for DSS-65 Madrid
127+
INFO nyx_space::md::trajectory::sc_traj > Converted trajectory from Moon J2000 (μ = 4902.74987 km^3/s^2, radius = 1737.4 km) to Earth IAU_EARTH (μ = 398600.435436096 km^3/s^2) in 220 ms: Trajectory in Earth IAU_EARTH (μ = 398600.436 km^3/s^2, eq. radius = 6378.1366 km, polar radius = 6356.7519 km, f = 0.0033528131084554717) from 2024-01-01T00:00:00 UTC to 2024-01-02T00:00:00 UTC (1 day, or 86400.000 s) [17281 states]
128+
INFO nyx_space::md::events::search > Searching for DSS-65 Madrid (lat.: 40.4272 deg long.: 4.2506 deg alt.: 834.939 m) [Earth IAU_EARTH (μ = 398600.435436096 km^3/s^2)] with initial heuristic of 14 min 24 s
129+
INFO nyx_space::md::events::search > Event DSS-65 Madrid (lat.: 40.4272 deg long.: 4.2506 deg alt.: 834.939 m) [Earth IAU_EARTH (μ = 398600.435436096 km^3/s^2)] found 2 times from 2024-01-01T09:59:19.297494580 UTC until 2024-01-01T22:19:56.653993644 UTC
130+
INFO nyx_space::od::simulator::arc > Built 2 tracking strands for DSS-65 Madrid
131+
INFO nyx_space::od::simulator::arc > DSS-65 Madrid configured as Greedy, so DSS-13 Goldstone now starts on 2024-01-01T10:00:20 UTC
132+
INFO nyx_space::od::simulator::arc > DSS-13 Goldstone configured as Greedy, so DSS-34 Canberra now starts on 2024-01-01T18:09:50 UTC
133+
INFO nyx_space::od::simulator::arc > DSS-34 Canberra now hands off to DSS-65 Madrid on 2024-01-01T22:19:00 UTC because it's configured as Eager
134+
INFO nyx_space::od::simulator::arc > Simulated 319 measurements for DSS-13 Goldstone for 1 tracking strands in 13 ms
135+
INFO nyx_space::od::simulator::arc > Simulated 185 measurements for DSS-34 Canberra for 1 tracking strands in 7 ms
136+
INFO nyx_space::od::simulator::arc > Simulated 490 measurements for DSS-65 Madrid for 2 tracking strands in 18 ms
137+
INFO nyx_space::od::msr::arc > Serialized 994 measurements from {"DSS-34 Canberra", "DSS-13 Goldstone", "DSS-65 Madrid"} to ./04_lro_simulated_tracking.parquet
138+
994 measurements from {"DSS-13 Goldstone", "DSS-65 Madrid", "DSS-34 Canberra"}
139+
```
140+
141+
## Tracking arc
142+
143+
In this simulation, we use the official ephemeris to generate simulated measurements. In other words, we don't simulate a new ephemeris. **This serves as a validation of the orbit estimation in low lunar orbits,** the most dynamical of the cislunar orbits.
144+
145+
Nyx will not generate measurements if the vehicle is obstructed by a celestial object, in this case, the obstruction is the presence of the Moon itself.
146+
147+
![Schedule](./plots/msr-schedule.png)
148+
149+
![Range (km)](./plots/msr-range.png)
150+
151+
![Doppler (km/s)](./plots/msr-doppler.png)
152+
153+
## Filter set up
154+
155+
The OD filter uses the same dynamics as those used in the [model matching](#preliminary-analysis-matching-dynamical-models) section above.
156+
157+
However, as seen in the model matching section, there remains a difference in the modeling of the orbital dynamics between Nyx and the published LRO ephemeris. This causes an oscillation of roughly 250 meters of range in the RIC frame. In the LRO orbit determination paper, the authors mention that GTDS is a batch least squares estimator, whereas this example uses a Kalman filter. To account for the modeling difference, we bump up the state noise compensation of the filter to `1e-11` km/s^2, or approximately 0.6 cm/s over a 10 minute span. This causes the covariance to increase when there are no measurements to accomodate for the small but accumulating modeling differences.
158+
159+
The filter is configured with the default automatic residual rejection of Nyx whereby any residual ratio greater than 4 sigmas causes the measurement to be rejected.
160+
161+
# Results
162+
163+
**Nyx provides a good estimation of the orbit of the Lunar Reconnaissance Orbiter, matching the LRO team's desired uncertainty of 800 meters when tracking the vehicle,** despite modeling and filtering differences between GTDS and Nyx. As discussed in the [filter setup](#filter-set-up) section just above, we've had to increase the state noise compensation to account for [oscillating modeling differences]((#preliminary-analysis-matching-dynamical-models)) between Nyx and the LRO definitive ephemeris.
164+
165+
![RIC Covar Position](./plots/covar-ric-pos.png)
166+
167+
![RIC Covar Velocity](./plots/covar-ric-vel.png)
168+
169+
## Residual ratios
170+
171+
One of the key metrics to determine whether an OD result is correct is to look at the residual ratios. Some orbit determination software, like ODTK (as specified in their MathSpec), treats all measurements as scalars. This could be problematic because the order in which the measurements are processed at each time step will lead to variations in the covariance. For example, if there is an excellent range measurement and an average Doppler measurement for that same time step, then the state covariance will decrease and that greater model filter confidence may cause the Doppler measurement to be rejected. _Instead_, Nyx treats all simultaneous measurements simultaneously, ensuring that the order in which each measurement is processed is not a concern. This mathematically correct approach leads to the loss of the sign of the residual because the ratio is computed as `y_k^T * R_k^{-1} * y_k`, where `y_k` is prefit residual (using the nomenclature from the [Nyx Kalman filter MathSpec](https://nyxspace.com/nyxspace/MathSpec/orbit_determination/kalman/?utm_source=github-lro-rr)). **Therefore, the goodness test of the residual ratios is Chi Square test** with a degree of freedom of 2 (range and Doppler).
172+
173+
We can see that nearly all of the residuals are within the Chi Square distribution, although the tail-end of 4 sigmas is larger than it should be. This is entirely due to the models not matching perfectly, as can be seen in the plot of the residuals rejected over time: the further we are from the start of the OD track, the more measurements are rejected.
174+
175+
![Chi-Squared](./plots/resid-chi-square.png)
176+
177+
![Rejection](./plots/resid-rejections.png)
178+
179+
![Per tracker](./plots/resid-per-tracker.png)
180+
181+
## Measurement residuals
182+
183+
Another key metric is whether the residuals fit well within the expected measurement noise. As in ODTK, Nyx varies the measurement noise with the state covariance.
184+
185+
### Range residuals
186+
187+
The following plots show an auto-scaled and a zoomed-in version of the range residuals over time because the state covariance rises with the state noise compensation, causing the auto-scaling to hide the fun details, namely that the oscillatory nature of the modeling difference shows up in the prefit residuals but that the filter adequately corrects its own knowledge with each accepted measurement. The values on this plot is in km as indicated by the legend.
188+
189+
![Range resid auto](./plots/range-resid.png)
190+
191+
![Range resid zoom](./plots/range-resid-zoom.png)
192+
193+
### Doppler residuals
194+
195+
The Doppler residuals look great with the automatic zoom because the modeling differences are small but accumulating velocity errors. The values on this plot is in km/s as indicated by the legend.
196+
197+
![Doopler resid](./plots/doppler-resid.png)
198+
199+
## Verification
200+
201+
In our case, we have the true definitive ephemeris of LRO. Hence, we can compare the difference between the orbit determination results in Nyx and the definitive ephemeris in the RIC frame. **The OD results are slightly better than a pure propagation of the orbit with the modeling error.** The couple of singularities in the RIC plot are common interpolation artifacts which crop up especially when computing the RIC differences, and are not actual state differences.
202+
203+
![RIC OD vs truth pos err](./plots/od-vs-truth-ric-pos-err.png)
204+
205+
![RIC OD vs truth vel err](./plots/od-vs-truth-ric-vel-err.png)

examples/04_lro_od/main.rs

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ use nyx::{
2828
propagators::Propagator,
2929
Orbit, Spacecraft, State,
3030
};
31-
use rand::SeedableRng;
32-
use rand_distr::Distribution;
33-
use rand_pcg::Pcg64Mcg;
3431

3532
use std::{collections::BTreeMap, error::Error, path::PathBuf, str::FromStr, sync::Arc};
3633

@@ -79,8 +76,6 @@ fn main() -> Result<(), Box<dyn Error>> {
7976
// Inspecting the LRO BSP in the ANISE GUI shows us that NASA has assigned ID -85 to LRO.
8077
let lro_frame = Frame::from_ephem_j2000(-85);
8178

82-
// We also use a different gravitational parameter for the Moon to have a better estimation
83-
8479
// To build the trajectory we need to provide a spacecraft template.
8580
let sc_template = Spacecraft::builder()
8681
.dry_mass_kg(1018.0) // Launch masses
@@ -101,7 +96,7 @@ fn main() -> Result<(), Box<dyn Error>> {
10196
sc_template,
10297
5.seconds(),
10398
Some(Epoch::from_str("2024-01-01 00:00:00 UTC")?),
104-
Some(Epoch::from_str("2024-01-03 00:00:00 UTC")?),
99+
Some(Epoch::from_str("2024-01-02 00:00:00 UTC")?),
105100
Aberration::LT,
106101
Some("LRO".to_string()),
107102
)?;
@@ -192,38 +187,6 @@ fn main() -> Result<(), Box<dyn Error>> {
192187
// The sc_seed is the true LRO state from the BSP.
193188
let sc_seed = *traj_as_flown.first();
194189

195-
// Build an uncertainty using the LRO OD paper stats of 18 meters in the Radial direction
196-
let sc = SpacecraftUncertainty::builder()
197-
.nominal(sc_seed)
198-
.frame(LocalFrame::RIC)
199-
.x_km(0.01)
200-
.y_km(0.01)
201-
.z_km(0.01)
202-
.vx_km_s(0.01e-3)
203-
.vy_km_s(0.01e-3)
204-
.vz_km_s(0.01e-3)
205-
.build();
206-
207-
println!("== UNCERTAINTY ==\n{sc}");
208-
209-
// Build the filter initial estimate, which we will reuse in the filter.
210-
let sc_estimate = sc.to_estimate()?;
211-
212-
// Now let's sample from this distribution by building the multivariate random variable from this estimate ...
213-
let state_rv = sc_estimate.to_random_variable()?;
214-
// ... and sampling from this distribution.
215-
// This ensures that the trajectory from which we generate the states is not identical to the initial state of the filter.
216-
let sim_state = state_rv.sample(&mut Pcg64Mcg::from_entropy());
217-
218-
let setup = Propagator::default_dp78(dynamics);
219-
let (od_final_state, od_truth_traj) = setup
220-
.with(sim_state.state, almanac.clone())
221-
.until_epoch_with_traj(traj_as_flown.last().epoch())?;
222-
223-
println!("OD INIT: {:x}", sim_state.state);
224-
println!("OD FINAL: {od_final_state:x}");
225-
println!("{od_truth_traj}");
226-
227190
// Load the Deep Space Network ground stations.
228191
// Nyx allows you to build these at runtime but it's pretty static so we can just load them from YAML.
229192
let ground_station_file: PathBuf = [
@@ -286,29 +249,17 @@ fn main() -> Result<(), Box<dyn Error>> {
286249
let initial_estimate = sc.to_estimate()?;
287250

288251
println!("== FILTER STATE ==\n{sc_seed:x}\n{initial_estimate}");
289-
println!(
290-
"== SIM TRUTH ==\n{:x}\n{}",
291-
sim_state.state, sim_state.state
292-
);
293-
294-
let ric_err = sim_state
295-
.state
296-
.orbit
297-
.ric_difference(&initial_estimate.state().orbit)?;
298-
println!("== RIC at start ==");
299-
println!("RIC Position (m): {}", ric_err.radius_km * 1e3);
300-
println!("RIC Velocity (m/s): {}", ric_err.velocity_km_s * 1e3);
301252

302253
let kf = KF::new(
303254
// Increase the initial covariance to account for larger deviation.
304255
initial_estimate,
305256
// Until https://github.com/nyx-space/nyx/issues/351, we need to specify the SNC in the acceleration of the Moon J2000 frame.
306-
SNC3::from_diagonal(10 * Unit::Minute, &[5e-11, 5e-11, 5e-11]),
257+
SNC3::from_diagonal(10 * Unit::Minute, &[1e-11, 1e-11, 1e-11]),
307258
);
308259

309260
// We'll set up the OD process to reject measurements whose residuals are mover than 4 sigmas away from what we expect.
310261
let mut odp = ODProcess::ckf(
311-
setup.with(sc_estimate.state().with_stm(), almanac.clone()),
262+
setup.with(initial_estimate.state().with_stm(), almanac.clone()),
312263
kf,
313264
Some(ResidRejectCrit::default()),
314265
almanac.clone(),

examples/04_lro_od/plot_od_rslt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
fig = go.Figure()
4848
fig.add_trace(go.Scatter(x=x_chi, y=y_chi * scale_factor, mode="lines", name="Scaled Chi-Squared"))
49-
fig.add_trace(go.Histogram(x=df["Residual ratio"], nbinsx=100, name="Residuals"))
49+
fig.add_trace(go.Histogram(x=df["Residual ratio"], nbinsx=100, name="Residual ratios"))
5050
fig.show()
5151

5252
px.histogram(
263 KB
Loading
289 KB
Loading
194 KB
Loading
126 KB
Loading
185 KB
Loading
76.9 KB
Loading

0 commit comments

Comments
 (0)