Skip to main content

Estimate

Trait Estimate 

Source
pub trait Estimate<T: State>
where Self: Clone + PartialEq + Sized + Display, DefaultAllocator: Allocator<<T as State>::Size> + Allocator<<T as State>::Size, <T as State>::Size> + Allocator<<T as State>::VecLength>,
{
Show 14 methods // Required methods fn zeros(state: T) -> Self; fn state_deviation(&self) -> OVector<f64, <T as State>::Size>; fn nominal_state(&self) -> T; fn covar(&self) -> OMatrix<f64, <T as State>::Size, <T as State>::Size>; fn predicted_covar( &self, ) -> OMatrix<f64, <T as State>::Size, <T as State>::Size>; fn set_state_deviation( &mut self, new_state: OVector<f64, <T as State>::Size>, ); fn set_covar( &mut self, new_covar: OMatrix<f64, <T as State>::Size, <T as State>::Size>, ); fn predicted(&self) -> bool; fn stm(&self) -> &OMatrix<f64, <T as State>::Size, <T as State>::Size>; // Provided methods fn epoch(&self) -> Epoch { ... } fn set_epoch(&mut self, dt: Epoch) { ... } fn state(&self) -> T { ... } fn within_sigma(&self, sigma: f64) -> bool { ... } fn within_3sigma(&self) -> bool { ... }
}
Expand description

Stores an Estimate, as the result of a time_update or measurement_update.

Required Methods§

Source

fn zeros(state: T) -> Self

An empty estimate. This is useful if wanting to store an estimate outside the scope of a filtering loop.

Source

fn state_deviation(&self) -> OVector<f64, <T as State>::Size>

The state deviation as computed by the filter.

Source

fn nominal_state(&self) -> T

The nominal state as reported by the filter dynamics

Source

fn covar(&self) -> OMatrix<f64, <T as State>::Size, <T as State>::Size>

The Covariance of this estimate. Will return the predicted covariance if this is a time update/prediction.

Source

fn predicted_covar( &self, ) -> OMatrix<f64, <T as State>::Size, <T as State>::Size>

The predicted covariance of this estimate from the time update

Source

fn set_state_deviation(&mut self, new_state: OVector<f64, <T as State>::Size>)

Sets the state deviation.

Source

fn set_covar( &mut self, new_covar: OMatrix<f64, <T as State>::Size, <T as State>::Size>, )

Sets the Covariance of this estimate

Source

fn predicted(&self) -> bool

Whether or not this is a predicted estimate from a time update, or an estimate from a measurement

Source

fn stm(&self) -> &OMatrix<f64, <T as State>::Size, <T as State>::Size>

The STM used to compute this Estimate

Provided Methods§

Source

fn epoch(&self) -> Epoch

Epoch of this Estimate

Examples found in repository?
nyx-core/examples/05_cislunar_spacecraft_link_od/main.rs (line 252)
34fn main() -> Result<(), Box<dyn Error>> {
35    pel::init();
36
37    // ====================== //
38    // === ALMANAC SET UP === //
39    // ====================== //
40
41    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
42
43    let out = manifest_dir.join("data/04_output/");
44
45    let almanac = Arc::new(
46        Almanac::new(
47            &manifest_dir
48                .join("data/01_planetary/pck08.pca")
49                .to_string_lossy(),
50        )
51        .unwrap()
52        .load(
53            &manifest_dir
54                .join("data/01_planetary/de440s.bsp")
55                .to_string_lossy(),
56        )
57        .unwrap(),
58    );
59
60    let eme2k = almanac.frame_info(EARTH_J2000).unwrap();
61    let moon_iau = almanac.frame_info(IAU_MOON_FRAME).unwrap();
62
63    let epoch = Epoch::from_gregorian_tai(2021, 5, 29, 19, 51, 16, 852_000);
64    let nrho = Orbit::cartesian(
65        166_473.631_302_239_7,
66        -274_715.487_253_382_7,
67        -211_233.210_176_686_7,
68        0.933_451_604_520_018_4,
69        0.436_775_046_841_900_9,
70        -0.082_211_021_250_348_95,
71        epoch,
72        eme2k,
73    );
74
75    let tx_nrho_sc = Spacecraft::from(nrho);
76
77    let state_luna = almanac.transform_to(nrho, MOON_J2000, None).unwrap();
78    println!("Start state (dynamics: Earth, Moon, Sun gravity):\n{state_luna}");
79
80    let bodies = vec![EARTH, SUN];
81    let dynamics = SpacecraftDynamics::new(OrbitalDynamics::point_masses(bodies));
82
83    let setup = Propagator::rk89(
84        dynamics,
85        IntegratorOptions::builder().max_step(0.5.minutes()).build(),
86    );
87
88    /* == Propagate the NRHO vehicle == */
89    let prop_time = 1.1 * state_luna.period().unwrap();
90
91    let (nrho_final, mut tx_traj) = setup
92        .with(tx_nrho_sc, almanac.clone())
93        .for_duration_with_traj(prop_time)
94        .unwrap();
95
96    tx_traj.name = Some("NRHO Tx SC".to_string());
97
98    println!("{tx_traj}");
99
100    /* == Propagate an LLO vehicle == */
101    let llo_orbit =
102        Orbit::try_keplerian_altitude(110.0, 1e-4, 90.0, 0.0, 0.0, 0.0, epoch, moon_iau).unwrap();
103
104    let llo_sc = Spacecraft::builder().orbit(llo_orbit).build();
105
106    let (_, llo_traj) = setup
107        .with(llo_sc, almanac.clone())
108        .until_epoch_with_traj(nrho_final.epoch())
109        .unwrap();
110
111    // Export the subset of the first two hours.
112    llo_traj
113        .clone()
114        .filter_by_offset(..2.hours())
115        .to_parquet_simple(out.join("05_caps_llo_truth.pq"))?;
116
117    /* == Setup the interlink == */
118
119    let mut measurement_types = IndexSet::new();
120    measurement_types.insert(MeasurementType::Range);
121    measurement_types.insert(MeasurementType::Doppler);
122
123    let mut stochastics = IndexMap::new();
124
125    let sa45_csac_allan_dev = 1e-11;
126
127    stochastics.insert(
128        MeasurementType::Range,
129        StochasticNoise::from_hardware_range_km(
130            sa45_csac_allan_dev,
131            10.0.seconds(),
132            link_specific::ChipRate::StandardT4B(),
133            link_specific::SN0::Average(),
134        ),
135    );
136
137    stochastics.insert(
138        MeasurementType::Doppler,
139        StochasticNoise::from_hardware_doppler_km_s(
140            sa45_csac_allan_dev,
141            10.0.seconds(),
142            link_specific::CarrierFreq::SBand(),
143            link_specific::CN0::Average(),
144        ),
145    );
146
147    let interlink = InterlinkTxSpacecraft {
148        traj: tx_traj,
149        measurement_types,
150        integration_time: None,
151        timestamp_noise_s: None,
152        ab_corr: Aberration::LT,
153        stochastic_noises: Some(stochastics),
154    };
155
156    // Devices are the transmitter, which is our NRHO vehicle.
157    let mut devices = BTreeMap::new();
158    devices.insert("NRHO Tx SC".to_string(), interlink);
159
160    let mut configs = BTreeMap::new();
161    configs.insert(
162        "NRHO Tx SC".to_string(),
163        TrkConfig::builder()
164            .strands(vec![Strand {
165                start: epoch,
166                end: nrho_final.epoch(),
167            }])
168            .build(),
169    );
170
171    let mut trk_sim =
172        TrackingArcSim::with_seed(devices.clone(), llo_traj.clone(), configs, 0).unwrap();
173    println!("{trk_sim}");
174
175    let trk_data = trk_sim.generate_measurements(&almanac).unwrap();
176    println!("{trk_data}");
177
178    trk_data
179        .to_parquet_simple(out.clone().join("nrho_interlink_msr.pq"))
180        .unwrap();
181
182    // Run a truth OD where we estimate the LLO position
183    let llo_uncertainty = SpacecraftUncertainty::builder()
184        .nominal(llo_sc)
185        .x_km(1.0)
186        .y_km(1.0)
187        .z_km(1.0)
188        .vx_km_s(1e-3)
189        .vy_km_s(1e-3)
190        .vz_km_s(1e-3)
191        .build();
192
193    let mut proc_devices = devices.clone();
194
195    // Define the initial estimate, randomized, seed for reproducibility
196    let mut initial_estimate = llo_uncertainty.to_estimate_randomized(Some(0)).unwrap();
197    // Inflate the covariance -- https://github.com/nyx-space/nyx/issues/339
198    initial_estimate.covar *= 2.5;
199
200    // Increase the noise in the devices to accept more measurements.
201
202    for link in proc_devices.values_mut() {
203        for noise in &mut link.stochastic_noises.as_mut().unwrap().values_mut() {
204            *noise.white_noise.as_mut().unwrap() *= 3.0;
205        }
206    }
207
208    let init_err = initial_estimate
209        .orbital_state()
210        .ric_difference(&llo_orbit)
211        .unwrap();
212
213    println!("initial estimate:\n{initial_estimate}");
214    println!("RIC errors = {init_err}",);
215
216    let odp = InterlinkKalmanOD::new(
217        setup.clone(),
218        KalmanVariant::ReferenceUpdate,
219        Some(SigmaRejection::default()),
220        proc_devices,
221        almanac.clone(),
222    );
223
224    // Shrink the data to process.
225    let arc = trk_data.filter_by_offset(..2.hours());
226
227    let od_sol = odp.process_arc(initial_estimate, &arc).unwrap();
228
229    println!("{od_sol}");
230
231    od_sol
232        .to_parquet(
233            out.join("05_caps_interlink_od_sol.pq"),
234            ExportCfg::default(),
235        )
236        .unwrap();
237
238    let od_traj = od_sol.to_traj().unwrap();
239
240    od_traj
241        .ric_diff_to_parquet(
242            &llo_traj,
243            out.join("05_caps_interlink_llo_est_error.pq"),
244            ExportCfg::default(),
245        )
246        .unwrap();
247
248    let final_est = od_sol.estimates.last().unwrap();
249    assert!(final_est.within_3sigma(), "should be within 3 sigma");
250
251    println!("ESTIMATE\n{final_est:x}\n");
252    let truth = llo_traj.at(final_est.epoch()).unwrap();
253    println!("TRUTH\n{truth:x}");
254
255    let final_err = truth
256        .orbit
257        .ric_difference(&final_est.orbital_state())
258        .unwrap();
259    println!("ERROR {final_err}");
260
261    // Build the residuals versus reference plot.
262    let rvr_sol = odp
263        .process_arc(initial_estimate, &arc.resid_vs_ref_check())
264        .unwrap();
265
266    rvr_sol
267        .to_parquet(
268            out.join("05_caps_interlink_resid_v_ref.pq"),
269            ExportCfg::default(),
270        )
271        .unwrap();
272
273    let final_rvr = rvr_sol.estimates.last().unwrap();
274
275    println!("RMAG error {:.3} m", final_err.rmag_km() * 1e3);
276    println!(
277        "Pure prop error {:.3} m",
278        final_rvr
279            .orbital_state()
280            .ric_difference(&final_est.orbital_state())
281            .unwrap()
282            .rmag_km()
283            * 1e3
284    );
285
286    Ok(())
287}
More examples
Hide additional examples
nyx-core/examples/06_lunar_orbit_determination/main.rs (line 214)
35fn main() -> Result<(), Box<dyn Error>> {
36    pel::init();
37
38    // ====================== //
39    // === ALMANAC SET UP === //
40    // ====================== //
41
42    // Dynamics models require planetary constants and ephemerides to be defined.
43    // Let's start by grabbing those by using ANISE's MetaAlmanac.
44
45    let data_folder: PathBuf = [
46        env!("CARGO_MANIFEST_DIR"),
47        "examples",
48        "06_lunar_orbit_determination",
49    ]
50    .iter()
51    .collect();
52
53    let meta = data_folder.join("metaalmanac.dhall");
54
55    // Load this ephem in the general Almanac we're using for this analysis.
56    let almanac = MetaAlmanac::new(meta.to_string_lossy().as_ref())
57        .map_err(Box::new)?
58        .process(true)
59        .map_err(Box::new)?;
60
61    // Lock the almanac (an Arc is a read only structure).
62    let almanac = Arc::new(almanac);
63
64    // Build a nominal trajectory
65    // TODO: Switch this to a sequence once the OD over a spacecraft sequence is implemented.
66
67    let epoch = Epoch::from_gregorian_utc_at_noon(2024, 2, 29);
68    let moon_j2000 = almanac.frame_info(MOON_J2000)?;
69
70    // To build the trajectory we need to provide a spacecraft template.
71    let orbiter = Spacecraft::builder()
72        .mass(Mass::from_dry_and_prop_masses(1018.0, 900.0))
73        .srp(SRPData {
74            area_m2: 3.9 * 2.7,
75            coeff_reflectivity: 0.96,
76        })
77        .orbit(Orbit::try_keplerian_altitude(
78            150.0, 0.00212, 33.6, 45.0, 45.0, 0.0, epoch, moon_j2000,
79        )?) // Setting a zero orbit here because it's just a template
80        .build();
81
82    // ========================== //
83    // === BUILD NOMINAL TRAJ === //
84    // ========================== //
85
86    // Set up the spacecraft dynamics.
87
88    // Specify that the orbital dynamics must account for the graviational pull of the Earth and the Sun.
89    // The gravity of the Moon will also be accounted for since the spaceraft in a lunar orbit.
90    let mut orbital_dyn = OrbitalDynamics::point_masses(vec![EARTH, SUN, JUPITER_BARYCENTER]);
91
92    // We want to include the spherical harmonics, so let's download the gravitational data from the Nyx Cloud.
93    // We're using the GRAIL JGGRX model.
94    let mut jggrx_meta = MetaFile {
95        uri: "http://public-data.nyxspace.com/nyx/models/Luna_jggrx_1500e_sha.tab.gz".to_string(),
96        crc32: Some(0x6bcacda8), // Specifying the CRC32 avoids redownloading it if it's cached.
97    };
98    // And let's download it if we don't have it yet.
99    jggrx_meta.process(true)?;
100
101    // Build the spherical harmonics.
102    // The harmonics must be computed in the body fixed frame.
103    // We're using the long term prediction of the Moon principal axes frame.
104    let moon_pa_frame = MOON_PA_FRAME.with_orient(31008);
105    let sph_harmonics = GravityField::new(GravityFieldData::from_shadr(
106        &jggrx_meta.uri,
107        80,
108        80,
109        almanac.frame_info(moon_pa_frame)?,
110    )?);
111
112    // Include the spherical harmonics into the orbital dynamics.
113    orbital_dyn.accel_models.push(sph_harmonics);
114
115    // We define the solar radiation pressure, using the default solar flux and accounting only
116    // for the eclipsing caused by the Earth and Moon.
117    // Note that by default, enabling the SolarPressure model will also enable the estimation of the coefficient of reflectivity.
118    let srp_dyn = SolarPressure::new(vec![MOON_J2000], &almanac)?;
119
120    // Finalize setting up the dynamics, specifying the force models (orbital_dyn) separately from the
121    // acceleration models (SRP in this case). Use `from_models` to specify multiple accel models.
122    let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn);
123
124    println!("{dynamics}");
125
126    let setup = Propagator::rk89(dynamics.clone(), IntegratorOptions::default());
127
128    let truth_traj = setup
129        .with(orbiter, almanac.clone())
130        .for_duration_with_traj(Unit::Day * 2)?
131        .1;
132
133    // ==================== //
134    // === OD SIMULATOR === //
135    // ==================== //
136
137    // Load the Deep Space Network ground stations.
138    // Nyx allows you to build these at runtime but it's pretty static so we can just load them from YAML.
139    let ground_station_file = data_folder.join("dsn-network.yaml");
140    let devices = GroundStation::load_named(ground_station_file)?;
141
142    let proc_devices = devices.clone();
143
144    // Typical OD software requires that you specify your own tracking schedule or you'll have overlapping measurements.
145    // Nyx can build a tracking schedule for you based on the first station with access.
146    let configs: BTreeMap<String, TrkConfig> =
147        TrkConfig::load_named(data_folder.join("tracking-cfg.yaml"))?;
148
149    // Build the tracking arc simulation to generate a "standard measurement".
150    let mut trk = TrackingArcSim::<Spacecraft, GroundStation>::with_seed(
151        devices.clone(),
152        truth_traj.clone(),
153        configs,
154        123, // Set a seed for reproducibility
155    )?;
156
157    trk.build_schedule(&almanac)?;
158    let arc = trk.generate_measurements(&almanac)?;
159    // Save the simulated tracking data
160    arc.to_parquet_simple("./data/04_output/06_lunar_simulated_tracking.parquet")?;
161
162    // We'll note that in our case, we have continuous coverage of LRO when the vehicle is not behind the Moon.
163    println!("{arc}");
164
165    // Now that we have simulated measurements, we'll run the orbit determination.
166
167    // ===================== //
168    // === OD ESTIMATION === //
169    // ===================== //
170
171    let sc = SpacecraftUncertainty::builder()
172        .nominal(orbiter)
173        .frame(LocalFrame::RIC)
174        .x_km(0.5)
175        .y_km(0.5)
176        .z_km(0.5)
177        .vx_km_s(5e-3)
178        .vy_km_s(5e-3)
179        .vz_km_s(5e-3)
180        .build();
181
182    // Build the filter initial estimate, which we will reuse in the filter.
183    let initial_estimate = sc.to_estimate()?;
184
185    println!("== FILTER STATE ==\n{orbiter:x}\n{initial_estimate}");
186
187    // Build the SNC in the Moon J2000 frame, specified as a velocity noise over time.
188    let process_noise = ProcessNoise3D::from_velocity_km_s(
189        &[1e-14, 1e-14, 1e-14],
190        1 * Unit::Hour,
191        10 * Unit::Minute,
192        None,
193    );
194
195    println!("{process_noise}");
196
197    // We'll set up the OD process to reject measurements whose residuals are move than 3 sigmas away from what we expect.
198    let odp = SpacecraftKalmanScalarOD::new(
199        setup,
200        KalmanVariant::ReferenceUpdate,
201        Some(SigmaRejection::default()),
202        proc_devices,
203        almanac.clone(),
204    )
205    .with_process_noise(process_noise);
206
207    let od_sol = odp.process_arc(initial_estimate, &arc)?;
208
209    let final_est = od_sol.estimates.last().unwrap();
210
211    println!("{final_est}");
212
213    let ric_err = truth_traj
214        .at(final_est.epoch())?
215        .orbit
216        .ric_difference(&final_est.orbital_state())?;
217    println!("== RIC at end ==");
218    println!("RIC Position (m): {:.3}", ric_err.radius_km * 1e3);
219    println!("RIC Velocity (m/s): {:.3}", ric_err.velocity_km_s * 1e3);
220
221    println!(
222        "Num residuals rejected: #{}",
223        od_sol.rejected_residuals().len()
224    );
225    println!(
226        "Percentage within +/-3: {}",
227        od_sol.residual_ratio_within_threshold(3.0).unwrap()
228    );
229    println!("Whitened residuals normal? {}", od_sol.is_normal(None)?);
230    println!("NIS consistency: {}", od_sol.nis_consistency(None)?);
231
232    od_sol.to_parquet(
233        "./data/04_output/06_lunar_od_results.parquet",
234        ExportCfg::default(),
235    )?;
236
237    let od_trajectory = od_sol.to_traj()?;
238    // Build the RIC difference.
239    od_trajectory.ric_diff_to_parquet(
240        &truth_traj,
241        "./data/04_output/06_lunar_od_truth_error.parquet",
242        ExportCfg::default(),
243    )?;
244
245    Ok(())
246}
nyx-core/examples/04_lro_od/main.rs (line 273)
34fn main() -> Result<(), Box<dyn Error>> {
35    pel::init();
36
37    // ====================== //
38    // === ALMANAC SET UP === //
39    // ====================== //
40
41    // Dynamics models require planetary constants and ephemerides to be defined.
42    // Let's start by grabbing those by using ANISE's MetaAlmanac.
43
44    let output_folder: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../data", "04_output"]
45        .iter()
46        .collect();
47
48    let data_folder: PathBuf = [env!("CARGO_MANIFEST_DIR"), "examples", "04_lro_od"]
49        .iter()
50        .collect();
51
52    let meta = data_folder.join("lro-dynamics.dhall");
53
54    // Load this ephem in the general Almanac we're using for this analysis.
55    let almanac = Arc::new(
56        MetaAlmanac::new(meta.to_string_lossy().as_ref())
57            .map_err(Box::new)?
58            .process(true)
59            .map_err(Box::new)?,
60    );
61
62    // Orbit determination requires a Trajectory structure, which can be saved as parquet file.
63    // In our case, the trajectory comes from the BSP file, so we need to build a Trajectory from the almanac directly.
64    // To query the Almanac, we need to build the LRO frame in the J2000 orientation in our case.
65    // Inspecting the LRO BSP in the ANISE GUI shows us that NASA has assigned ID -85 to LRO.
66    let lro_frame = Frame::from_ephem_j2000(-85);
67
68    // To build the trajectory we need to provide a spacecraft template.
69    let sc_template = Spacecraft::builder()
70        .mass(Mass::from_dry_and_prop_masses(1018.0, 900.0)) // Launch masses
71        .srp(SRPData {
72            // SRP configuration is arbitrary, but we will be estimating it anyway.
73            area_m2: 3.9 * 2.7,
74            coeff_reflectivity: 0.96,
75        })
76        .orbit(Orbit::zero(MOON_J2000)) // Setting a zero orbit here because it's just a template
77        .build();
78    // Now we can build the trajectory from the BSP file.
79    // We'll arbitrarily set the tracking arc to 24 hours with a five second time step.
80    let traj_as_flown = Traj::from_bsp(
81        lro_frame,
82        MOON_J2000,
83        &almanac,
84        sc_template,
85        5.seconds(),
86        Some(Epoch::from_str("2024-01-01 01:00:00 UTC")?),
87        Some(Epoch::from_str("2024-01-02 01:00:00 UTC")?),
88        None,
89        Some("LRO".to_string()),
90    )?;
91
92    println!("{traj_as_flown}");
93
94    // ====================== //
95    // === MODEL MATCHING === //
96    // ====================== //
97
98    // Set up the spacecraft dynamics.
99
100    // Specify that the orbital dynamics must account for the graviational pull of the Earth and the Sun.
101    // The gravity of the Moon will also be accounted for since the spaceraft in a lunar orbit.
102    let mut orbital_dyn = OrbitalDynamics::point_masses(vec![EARTH, SUN, JUPITER_BARYCENTER]);
103
104    // We want to include the spherical harmonics, so let's download the gravitational data from the Nyx Cloud.
105    // We're using the GRAIL JGGRX model.
106    let mut jggrx_meta = MetaFile {
107        uri: "http://public-data.nyxspace.com/nyx/models/Luna_jggrx_1500e_sha.tab.gz".to_string(),
108        crc32: Some(0x6bcacda8), // Specifying the CRC32 avoids redownloading it if it's cached.
109    };
110    // And let's download it if we don't have it yet.
111    jggrx_meta.process(true)?;
112
113    // Build the spherical harmonics.
114    // The harmonics must be computed in the body fixed frame.
115    // We're using the long term prediction of the Moon principal axes frame.
116    let moon_pa_frame = MOON_PA_FRAME.with_orient(31008);
117    let sph_harmonics = GravityField::new(GravityFieldData::from_shadr(
118        &jggrx_meta.uri,
119        81,
120        81,
121        almanac.frame_info(moon_pa_frame)?,
122    )?);
123
124    // Include the spherical harmonics into the orbital dynamics.
125    orbital_dyn.accel_models.push(sph_harmonics);
126
127    // We define the solar radiation pressure, using the default solar flux and accounting only
128    // for the eclipsing caused by the Earth and Moon.
129    // Note that by default, enabling the SolarPressure model will also enable the estimation of the coefficient of reflectivity.
130    let srp_dyn = SolarPressure::new(vec![EARTH_J2000, MOON_J2000], &almanac)?;
131
132    // Finalize setting up the dynamics, specifying the force models (orbital_dyn) separately from the
133    // acceleration models (SRP in this case). Use `from_models` to specify multiple accel models.
134    let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn);
135
136    println!("{dynamics}");
137
138    // Now we can build the propagator.
139    let setup = Propagator::default_dp78(dynamics.clone());
140
141    // For reference, let's build the trajectory with Nyx's models from that LRO state.
142    let (sim_final, traj_as_sim) = setup
143        .with(*traj_as_flown.first(), almanac.clone())
144        .until_epoch_with_traj(traj_as_flown.last().epoch())?;
145
146    println!("SIM INIT:  {:x}", traj_as_flown.first());
147    println!("SIM FINAL: {sim_final:x}");
148    // Compute RIC difference between SIM and LRO ephem
149    let sim_lro_delta = sim_final
150        .orbit
151        .ric_difference(&traj_as_flown.last().orbit)?;
152    println!("{traj_as_sim}");
153    println!(
154        "SIM v LRO - RIC Position (m): {:.3}",
155        sim_lro_delta.radius_km * 1e3
156    );
157    println!(
158        "SIM v LRO - RIC Velocity (m/s): {:.3}",
159        sim_lro_delta.velocity_km_s * 1e3
160    );
161
162    traj_as_sim.ric_diff_to_parquet(
163        &traj_as_flown,
164        output_folder.join("./04_lro_sim_truth_error.parquet"),
165        ExportCfg::default(),
166    )?;
167
168    // ==================== //
169    // === OD SIMULATOR === //
170    // ==================== //
171
172    // After quite some time trying to exactly match the model, we still end up with an oscillatory difference on the order of 150 meters between the propagated state
173    // and the truth LRO state.
174
175    // Therefore, we will actually run an estimation from a dispersed LRO state.
176    // The sc_seed is the true LRO state from the BSP.
177    let sc_seed = *traj_as_flown.first();
178
179    // Load the Deep Space Network ground stations.
180    // Nyx allows you to build these at runtime but it's pretty static so we can just load them from YAML.
181    let ground_station_file: PathBuf = [
182        env!("CARGO_MANIFEST_DIR"),
183        "examples",
184        "04_lro_od",
185        "dsn-network.yaml",
186    ]
187    .iter()
188    .collect();
189
190    let devices = GroundStation::load_named(ground_station_file)?;
191
192    let proc_devices = devices.clone();
193
194    // Typical OD software requires that you specify your own tracking schedule or you'll have overlapping measurements.
195    // Nyx can build a tracking schedule for you based on the first station with access.
196    let trkconfg_yaml: PathBuf = [
197        env!("CARGO_MANIFEST_DIR"),
198        "examples",
199        "04_lro_od",
200        "tracking-cfg.yaml",
201    ]
202    .iter()
203    .collect();
204
205    let configs: BTreeMap<String, TrkConfig> = TrkConfig::load_named(trkconfg_yaml)?;
206
207    // Build the tracking arc simulation to generate a "standard measurement".
208    let mut trk = TrackingArcSim::<Spacecraft, GroundStation>::with_seed(
209        devices.clone(),
210        traj_as_flown.clone(),
211        configs,
212        123, // Set a seed for reproducibility
213    )?;
214
215    trk.build_schedule(&almanac)?;
216    let arc = trk.generate_measurements(&almanac)?;
217    // Save the simulated tracking data
218    arc.to_parquet_simple(output_folder.join("04_lro_simulated_tracking.parquet"))?;
219
220    // We'll note that in our case, we have continuous coverage of LRO when the vehicle is not behind the Moon.
221    println!("{arc}");
222
223    // Now that we have simulated measurements, we'll run the orbit determination.
224
225    // ===================== //
226    // === OD ESTIMATION === //
227    // ===================== //
228
229    let sc = SpacecraftUncertainty::builder()
230        .nominal(sc_seed)
231        .frame(LocalFrame::RIC)
232        .x_km(0.5)
233        .y_km(0.5)
234        .z_km(0.5)
235        .vx_km_s(5e-3)
236        .vy_km_s(5e-3)
237        .vz_km_s(5e-3)
238        .build();
239
240    // Build the filter initial estimate, which we will reuse in the filter.
241    let mut initial_estimate = sc.to_estimate()?;
242    initial_estimate.covar *= 1.5;
243
244    println!("== FILTER STATE ==\n{sc_seed:x}\n{initial_estimate}");
245
246    // Build the SNC in the Moon J2000 frame, specified as a velocity noise over time.
247    let process_noise = ProcessNoise3D::from_velocity_km_s(
248        &[5e-13, 5e-13, 5e-13],
249        1 * Unit::Hour,
250        10 * Unit::Minute,
251        None,
252    );
253
254    println!("{process_noise}");
255
256    // We'll set up the OD process to reject measurements whose residuals are move than 3 sigmas away from what we expect.
257    let odp = SpacecraftKalmanOD::new(
258        setup,
259        KalmanVariant::ReferenceUpdate,
260        Some(SigmaRejection::default()),
261        proc_devices,
262        almanac.clone(),
263    )
264    .with_process_noise(process_noise);
265
266    let od_sol = odp.process_arc(initial_estimate, &arc)?;
267
268    let final_est = od_sol.estimates.last().unwrap();
269
270    println!("{final_est}");
271
272    let ric_err = traj_as_flown
273        .at(final_est.epoch())?
274        .orbit
275        .ric_difference(&final_est.orbital_state())?;
276    println!("== RIC at end ==");
277    println!("RIC Position (m): {:.3}", ric_err.radius_km * 1e3);
278    println!("RIC Velocity (m/s): {:.3}", ric_err.velocity_km_s * 1e3);
279
280    println!(
281        "Num residuals rejected: #{}",
282        od_sol.rejected_residuals().len()
283    );
284    println!(
285        "Percentage within +/-3: {}",
286        od_sol.residual_ratio_within_threshold(3.0).unwrap()
287    );
288    println!("Ratios normal? {}", od_sol.is_normal(None).unwrap());
289    od_sol.nis_consistency(None)?.log();
290
291    od_sol.to_parquet(
292        output_folder.join("04_lro_od_results.parquet"),
293        ExportCfg::default(),
294    )?;
295
296    // Create the ephemeris
297    let ephem = od_sol.to_ephemeris("LRO rebuilt".to_string());
298    let ephem_start = ephem.start_epoch().unwrap();
299    let ephem_end = ephem.end_epoch().unwrap();
300    // Check that the covariance is PSD throughout the ephemeris by interpolating it.
301    for epoch in TimeSeries::inclusive(ephem_start, ephem_end, Unit::Minute * 5) {
302        ephem
303            .covar_at(
304                epoch,
305                anise::ephemerides::ephemeris::LocalFrame::RIC,
306                &almanac,
307            )
308            .unwrap_or_else(|e| panic!("covar not PSD at {epoch}: {e}"));
309    }
310    // Export as BSP!
311    ephem
312        .write_spice_bsp(
313            -85,
314            output_folder.join("04_lro_rebuilt.bsp").to_str().unwrap(),
315            None,
316        )
317        .expect("could not built BSP");
318    let new_almanac = Almanac::default()
319        .load(output_folder.join("04_lro_rebuilt.bsp").to_str().unwrap())
320        .unwrap();
321    new_almanac.describe(None, None, None, None, None, None, None, None);
322    let (spk_start, spk_end) = new_almanac.spk_domain(-85).unwrap();
323
324    assert!((ephem_start - spk_start).abs() < Unit::Microsecond * 1);
325    assert!((ephem_end - spk_end).abs() < Unit::Microsecond * 1);
326
327    // In our case, we have the truth trajectory from NASA.
328    // So we can compute the RIC state difference between the real LRO ephem and what we've just estimated.
329    // Export the OD trajectory first.
330    let od_trajectory = od_sol.to_traj()?;
331    // Build the RIC difference.
332    od_trajectory.ric_diff_to_parquet(
333        &traj_as_flown,
334        output_folder.join("04_lro_od_truth_error.parquet"),
335        ExportCfg::default(),
336    )?;
337
338    Ok(())
339}
Source

fn set_epoch(&mut self, dt: Epoch)

Source

fn state(&self) -> T

The estimated state

Source

fn within_sigma(&self, sigma: f64) -> bool

Returns whether this estimate is within some bound The 68-95-99.7 rule is a good way to assess whether the filter is operating normally

Source

fn within_3sigma(&self) -> bool

Returns whether this estimate is within 3 sigma, which represent 99.7% for a Normal distribution

Examples found in repository?
nyx-core/examples/05_cislunar_spacecraft_link_od/main.rs (line 249)
34fn main() -> Result<(), Box<dyn Error>> {
35    pel::init();
36
37    // ====================== //
38    // === ALMANAC SET UP === //
39    // ====================== //
40
41    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
42
43    let out = manifest_dir.join("data/04_output/");
44
45    let almanac = Arc::new(
46        Almanac::new(
47            &manifest_dir
48                .join("data/01_planetary/pck08.pca")
49                .to_string_lossy(),
50        )
51        .unwrap()
52        .load(
53            &manifest_dir
54                .join("data/01_planetary/de440s.bsp")
55                .to_string_lossy(),
56        )
57        .unwrap(),
58    );
59
60    let eme2k = almanac.frame_info(EARTH_J2000).unwrap();
61    let moon_iau = almanac.frame_info(IAU_MOON_FRAME).unwrap();
62
63    let epoch = Epoch::from_gregorian_tai(2021, 5, 29, 19, 51, 16, 852_000);
64    let nrho = Orbit::cartesian(
65        166_473.631_302_239_7,
66        -274_715.487_253_382_7,
67        -211_233.210_176_686_7,
68        0.933_451_604_520_018_4,
69        0.436_775_046_841_900_9,
70        -0.082_211_021_250_348_95,
71        epoch,
72        eme2k,
73    );
74
75    let tx_nrho_sc = Spacecraft::from(nrho);
76
77    let state_luna = almanac.transform_to(nrho, MOON_J2000, None).unwrap();
78    println!("Start state (dynamics: Earth, Moon, Sun gravity):\n{state_luna}");
79
80    let bodies = vec![EARTH, SUN];
81    let dynamics = SpacecraftDynamics::new(OrbitalDynamics::point_masses(bodies));
82
83    let setup = Propagator::rk89(
84        dynamics,
85        IntegratorOptions::builder().max_step(0.5.minutes()).build(),
86    );
87
88    /* == Propagate the NRHO vehicle == */
89    let prop_time = 1.1 * state_luna.period().unwrap();
90
91    let (nrho_final, mut tx_traj) = setup
92        .with(tx_nrho_sc, almanac.clone())
93        .for_duration_with_traj(prop_time)
94        .unwrap();
95
96    tx_traj.name = Some("NRHO Tx SC".to_string());
97
98    println!("{tx_traj}");
99
100    /* == Propagate an LLO vehicle == */
101    let llo_orbit =
102        Orbit::try_keplerian_altitude(110.0, 1e-4, 90.0, 0.0, 0.0, 0.0, epoch, moon_iau).unwrap();
103
104    let llo_sc = Spacecraft::builder().orbit(llo_orbit).build();
105
106    let (_, llo_traj) = setup
107        .with(llo_sc, almanac.clone())
108        .until_epoch_with_traj(nrho_final.epoch())
109        .unwrap();
110
111    // Export the subset of the first two hours.
112    llo_traj
113        .clone()
114        .filter_by_offset(..2.hours())
115        .to_parquet_simple(out.join("05_caps_llo_truth.pq"))?;
116
117    /* == Setup the interlink == */
118
119    let mut measurement_types = IndexSet::new();
120    measurement_types.insert(MeasurementType::Range);
121    measurement_types.insert(MeasurementType::Doppler);
122
123    let mut stochastics = IndexMap::new();
124
125    let sa45_csac_allan_dev = 1e-11;
126
127    stochastics.insert(
128        MeasurementType::Range,
129        StochasticNoise::from_hardware_range_km(
130            sa45_csac_allan_dev,
131            10.0.seconds(),
132            link_specific::ChipRate::StandardT4B(),
133            link_specific::SN0::Average(),
134        ),
135    );
136
137    stochastics.insert(
138        MeasurementType::Doppler,
139        StochasticNoise::from_hardware_doppler_km_s(
140            sa45_csac_allan_dev,
141            10.0.seconds(),
142            link_specific::CarrierFreq::SBand(),
143            link_specific::CN0::Average(),
144        ),
145    );
146
147    let interlink = InterlinkTxSpacecraft {
148        traj: tx_traj,
149        measurement_types,
150        integration_time: None,
151        timestamp_noise_s: None,
152        ab_corr: Aberration::LT,
153        stochastic_noises: Some(stochastics),
154    };
155
156    // Devices are the transmitter, which is our NRHO vehicle.
157    let mut devices = BTreeMap::new();
158    devices.insert("NRHO Tx SC".to_string(), interlink);
159
160    let mut configs = BTreeMap::new();
161    configs.insert(
162        "NRHO Tx SC".to_string(),
163        TrkConfig::builder()
164            .strands(vec![Strand {
165                start: epoch,
166                end: nrho_final.epoch(),
167            }])
168            .build(),
169    );
170
171    let mut trk_sim =
172        TrackingArcSim::with_seed(devices.clone(), llo_traj.clone(), configs, 0).unwrap();
173    println!("{trk_sim}");
174
175    let trk_data = trk_sim.generate_measurements(&almanac).unwrap();
176    println!("{trk_data}");
177
178    trk_data
179        .to_parquet_simple(out.clone().join("nrho_interlink_msr.pq"))
180        .unwrap();
181
182    // Run a truth OD where we estimate the LLO position
183    let llo_uncertainty = SpacecraftUncertainty::builder()
184        .nominal(llo_sc)
185        .x_km(1.0)
186        .y_km(1.0)
187        .z_km(1.0)
188        .vx_km_s(1e-3)
189        .vy_km_s(1e-3)
190        .vz_km_s(1e-3)
191        .build();
192
193    let mut proc_devices = devices.clone();
194
195    // Define the initial estimate, randomized, seed for reproducibility
196    let mut initial_estimate = llo_uncertainty.to_estimate_randomized(Some(0)).unwrap();
197    // Inflate the covariance -- https://github.com/nyx-space/nyx/issues/339
198    initial_estimate.covar *= 2.5;
199
200    // Increase the noise in the devices to accept more measurements.
201
202    for link in proc_devices.values_mut() {
203        for noise in &mut link.stochastic_noises.as_mut().unwrap().values_mut() {
204            *noise.white_noise.as_mut().unwrap() *= 3.0;
205        }
206    }
207
208    let init_err = initial_estimate
209        .orbital_state()
210        .ric_difference(&llo_orbit)
211        .unwrap();
212
213    println!("initial estimate:\n{initial_estimate}");
214    println!("RIC errors = {init_err}",);
215
216    let odp = InterlinkKalmanOD::new(
217        setup.clone(),
218        KalmanVariant::ReferenceUpdate,
219        Some(SigmaRejection::default()),
220        proc_devices,
221        almanac.clone(),
222    );
223
224    // Shrink the data to process.
225    let arc = trk_data.filter_by_offset(..2.hours());
226
227    let od_sol = odp.process_arc(initial_estimate, &arc).unwrap();
228
229    println!("{od_sol}");
230
231    od_sol
232        .to_parquet(
233            out.join("05_caps_interlink_od_sol.pq"),
234            ExportCfg::default(),
235        )
236        .unwrap();
237
238    let od_traj = od_sol.to_traj().unwrap();
239
240    od_traj
241        .ric_diff_to_parquet(
242            &llo_traj,
243            out.join("05_caps_interlink_llo_est_error.pq"),
244            ExportCfg::default(),
245        )
246        .unwrap();
247
248    let final_est = od_sol.estimates.last().unwrap();
249    assert!(final_est.within_3sigma(), "should be within 3 sigma");
250
251    println!("ESTIMATE\n{final_est:x}\n");
252    let truth = llo_traj.at(final_est.epoch()).unwrap();
253    println!("TRUTH\n{truth:x}");
254
255    let final_err = truth
256        .orbit
257        .ric_difference(&final_est.orbital_state())
258        .unwrap();
259    println!("ERROR {final_err}");
260
261    // Build the residuals versus reference plot.
262    let rvr_sol = odp
263        .process_arc(initial_estimate, &arc.resid_vs_ref_check())
264        .unwrap();
265
266    rvr_sol
267        .to_parquet(
268            out.join("05_caps_interlink_resid_v_ref.pq"),
269            ExportCfg::default(),
270        )
271        .unwrap();
272
273    let final_rvr = rvr_sol.estimates.last().unwrap();
274
275    println!("RMAG error {:.3} m", final_err.rmag_km() * 1e3);
276    println!(
277        "Pure prop error {:.3} m",
278        final_rvr
279            .orbital_state()
280            .ric_difference(&final_est.orbital_state())
281            .unwrap()
282            .rmag_km()
283            * 1e3
284    );
285
286    Ok(())
287}

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§