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

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety", so this trait is not object safe.

Implementors§