pub trait ConfigRepr:
Debug
+ Sized
+ Serialize
+ DeserializeOwned {
// Provided methods
fn load<P>(path: P) -> Result<Self, ConfigError>
where P: AsRef<Path> { ... }
fn load_many<P>(path: P) -> Result<Vec<Self>, ConfigError>
where P: AsRef<Path> { ... }
fn load_named<P>(path: P) -> Result<BTreeMap<String, Self>, ConfigError>
where P: AsRef<Path> { ... }
fn loads_many(data: &str) -> Result<Vec<Self>, ConfigError> { ... }
fn loads_named(data: &str) -> Result<BTreeMap<String, Self>, ConfigError> { ... }
}
Provided Methods§
Sourcefn load<P>(path: P) -> Result<Self, ConfigError>
fn load<P>(path: P) -> Result<Self, ConfigError>
Builds the configuration representation from the path to a yaml
Sourcefn load_many<P>(path: P) -> Result<Vec<Self>, ConfigError>
fn load_many<P>(path: P) -> Result<Vec<Self>, ConfigError>
Builds a sequence of “Selves” from the provided path to a yaml
Sourcefn load_named<P>(path: P) -> Result<BTreeMap<String, Self>, ConfigError>
fn load_named<P>(path: P) -> Result<BTreeMap<String, Self>, ConfigError>
Builds a map of names to “selves” from the provided path to a yaml
Examples found in repository?
examples/04_lro_od/main.rs (line 199)
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 data_folder: PathBuf = [env!("CARGO_MANIFEST_DIR"), "examples", "04_lro_od"]
45 .iter()
46 .collect();
47
48 let meta = data_folder.join("lro-dynamics.dhall");
49
50 // Load this ephem in the general Almanac we're using for this analysis.
51 let mut almanac = MetaAlmanac::new(meta.to_string_lossy().to_string())
52 .map_err(Box::new)?
53 .process(true)
54 .map_err(Box::new)?;
55
56 let mut moon_pc = almanac.planetary_data.get_by_id(MOON)?;
57 moon_pc.mu_km3_s2 = 4902.74987;
58 almanac.planetary_data.set_by_id(MOON, moon_pc)?;
59
60 let mut earth_pc = almanac.planetary_data.get_by_id(EARTH)?;
61 earth_pc.mu_km3_s2 = 398600.436;
62 almanac.planetary_data.set_by_id(EARTH, earth_pc)?;
63
64 // Save this new kernel for reuse.
65 // In an operational context, this would be part of the "Lock" process, and should not change throughout the mission.
66 almanac
67 .planetary_data
68 .save_as(&data_folder.join("lro-specific.pca"), true)?;
69
70 // Lock the almanac (an Arc is a read only structure).
71 let almanac = Arc::new(almanac);
72
73 // Orbit determination requires a Trajectory structure, which can be saved as parquet file.
74 // In our case, the trajectory comes from the BSP file, so we need to build a Trajectory from the almanac directly.
75 // To query the Almanac, we need to build the LRO frame in the J2000 orientation in our case.
76 // Inspecting the LRO BSP in the ANISE GUI shows us that NASA has assigned ID -85 to LRO.
77 let lro_frame = Frame::from_ephem_j2000(-85);
78
79 // To build the trajectory we need to provide a spacecraft template.
80 let sc_template = Spacecraft::builder()
81 .mass(Mass::from_dry_and_prop_masses(1018.0, 900.0)) // Launch masses
82 .srp(SRPData {
83 // SRP configuration is arbitrary, but we will be estimating it anyway.
84 area_m2: 3.9 * 2.7,
85 coeff_reflectivity: 0.96,
86 })
87 .orbit(Orbit::zero(MOON_J2000)) // Setting a zero orbit here because it's just a template
88 .build();
89 // Now we can build the trajectory from the BSP file.
90 // We'll arbitrarily set the tracking arc to 24 hours with a five second time step.
91 let traj_as_flown = Traj::from_bsp(
92 lro_frame,
93 MOON_J2000,
94 almanac.clone(),
95 sc_template,
96 5.seconds(),
97 Some(Epoch::from_str("2024-01-01 00:00:00 UTC")?),
98 Some(Epoch::from_str("2024-01-02 00:00:00 UTC")?),
99 Aberration::LT,
100 Some("LRO".to_string()),
101 )?;
102
103 println!("{traj_as_flown}");
104
105 // ====================== //
106 // === MODEL MATCHING === //
107 // ====================== //
108
109 // Set up the spacecraft dynamics.
110
111 // Specify that the orbital dynamics must account for the graviational pull of the Earth and the Sun.
112 // The gravity of the Moon will also be accounted for since the spaceraft in a lunar orbit.
113 let mut orbital_dyn = OrbitalDynamics::point_masses(vec![EARTH, SUN, JUPITER_BARYCENTER]);
114
115 // We want to include the spherical harmonics, so let's download the gravitational data from the Nyx Cloud.
116 // We're using the GRAIL JGGRX model.
117 let mut jggrx_meta = MetaFile {
118 uri: "http://public-data.nyxspace.com/nyx/models/Luna_jggrx_1500e_sha.tab.gz".to_string(),
119 crc32: Some(0x6bcacda8), // Specifying the CRC32 avoids redownloading it if it's cached.
120 };
121 // And let's download it if we don't have it yet.
122 jggrx_meta.process(true)?;
123
124 // Build the spherical harmonics.
125 // The harmonics must be computed in the body fixed frame.
126 // We're using the long term prediction of the Moon principal axes frame.
127 let moon_pa_frame = MOON_PA_FRAME.with_orient(31008);
128 let sph_harmonics = Harmonics::from_stor(
129 almanac.frame_from_uid(moon_pa_frame)?,
130 HarmonicsMem::from_shadr(&jggrx_meta.uri, 80, 80, true)?,
131 );
132
133 // Include the spherical harmonics into the orbital dynamics.
134 orbital_dyn.accel_models.push(sph_harmonics);
135
136 // We define the solar radiation pressure, using the default solar flux and accounting only
137 // for the eclipsing caused by the Earth and Moon.
138 // Note that by default, enabling the SolarPressure model will also enable the estimation of the coefficient of reflectivity.
139 let srp_dyn = SolarPressure::new(vec![EARTH_J2000, MOON_J2000], almanac.clone())?;
140
141 // Finalize setting up the dynamics, specifying the force models (orbital_dyn) separately from the
142 // acceleration models (SRP in this case). Use `from_models` to specify multiple accel models.
143 let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn);
144
145 println!("{dynamics}");
146
147 // Now we can build the propagator.
148 let setup = Propagator::default_dp78(dynamics.clone());
149
150 // For reference, let's build the trajectory with Nyx's models from that LRO state.
151 let (sim_final, traj_as_sim) = setup
152 .with(*traj_as_flown.first(), almanac.clone())
153 .until_epoch_with_traj(traj_as_flown.last().epoch())?;
154
155 println!("SIM INIT: {:x}", traj_as_flown.first());
156 println!("SIM FINAL: {sim_final:x}");
157 // Compute RIC difference between SIM and LRO ephem
158 let sim_lro_delta = sim_final
159 .orbit
160 .ric_difference(&traj_as_flown.last().orbit)?;
161 println!("{traj_as_sim}");
162 println!(
163 "SIM v LRO - RIC Position (m): {:.3}",
164 sim_lro_delta.radius_km * 1e3
165 );
166 println!(
167 "SIM v LRO - RIC Velocity (m/s): {:.3}",
168 sim_lro_delta.velocity_km_s * 1e3
169 );
170
171 traj_as_sim.ric_diff_to_parquet(
172 &traj_as_flown,
173 "./04_lro_sim_truth_error.parquet",
174 ExportCfg::default(),
175 )?;
176
177 // ==================== //
178 // === OD SIMULATOR === //
179 // ==================== //
180
181 // 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
182 // and the truth LRO state.
183
184 // Therefore, we will actually run an estimation from a dispersed LRO state.
185 // The sc_seed is the true LRO state from the BSP.
186 let sc_seed = *traj_as_flown.first();
187
188 // Load the Deep Space Network ground stations.
189 // Nyx allows you to build these at runtime but it's pretty static so we can just load them from YAML.
190 let ground_station_file: PathBuf = [
191 env!("CARGO_MANIFEST_DIR"),
192 "examples",
193 "04_lro_od",
194 "dsn-network.yaml",
195 ]
196 .iter()
197 .collect();
198
199 let devices = GroundStation::load_named(ground_station_file)?;
200
201 let mut proc_devices = devices.clone();
202
203 // Increase the noise in the devices to accept more measurements.
204 for gs in proc_devices.values_mut() {
205 if let Some(noise) = &mut gs
206 .stochastic_noises
207 .as_mut()
208 .unwrap()
209 .get_mut(&MeasurementType::Range)
210 {
211 *noise.white_noise.as_mut().unwrap() *= 3.0;
212 }
213 }
214
215 // Typical OD software requires that you specify your own tracking schedule or you'll have overlapping measurements.
216 // Nyx can build a tracking schedule for you based on the first station with access.
217 let trkconfg_yaml: PathBuf = [
218 env!("CARGO_MANIFEST_DIR"),
219 "examples",
220 "04_lro_od",
221 "tracking-cfg.yaml",
222 ]
223 .iter()
224 .collect();
225
226 let configs: BTreeMap<String, TrkConfig> = TrkConfig::load_named(trkconfg_yaml)?;
227
228 // Build the tracking arc simulation to generate a "standard measurement".
229 let mut trk = TrackingArcSim::<Spacecraft, GroundStation>::with_seed(
230 devices.clone(),
231 traj_as_flown.clone(),
232 configs,
233 123, // Set a seed for reproducibility
234 )?;
235
236 trk.build_schedule(almanac.clone())?;
237 let arc = trk.generate_measurements(almanac.clone())?;
238 // Save the simulated tracking data
239 arc.to_parquet_simple("./04_lro_simulated_tracking.parquet")?;
240
241 // We'll note that in our case, we have continuous coverage of LRO when the vehicle is not behind the Moon.
242 println!("{arc}");
243
244 // Now that we have simulated measurements, we'll run the orbit determination.
245
246 // ===================== //
247 // === OD ESTIMATION === //
248 // ===================== //
249
250 let sc = SpacecraftUncertainty::builder()
251 .nominal(sc_seed)
252 .frame(LocalFrame::RIC)
253 .x_km(0.5)
254 .y_km(0.5)
255 .z_km(0.5)
256 .vx_km_s(5e-3)
257 .vy_km_s(5e-3)
258 .vz_km_s(5e-3)
259 .build();
260
261 // Build the filter initial estimate, which we will reuse in the filter.
262 let mut initial_estimate = sc.to_estimate()?;
263 initial_estimate.covar *= 3.0;
264
265 println!("== FILTER STATE ==\n{sc_seed:x}\n{initial_estimate}");
266
267 // Build the SNC in the Moon J2000 frame, specified as a velocity noise over time.
268 let process_noise = ProcessNoise3D::from_velocity_km_s(
269 &[1e-10, 1e-10, 1e-10],
270 1 * Unit::Hour,
271 10 * Unit::Minute,
272 None,
273 );
274
275 println!("{process_noise}");
276
277 // We'll set up the OD process to reject measurements whose residuals are move than 3 sigmas away from what we expect.
278 let odp = SpacecraftKalmanOD::new(
279 setup,
280 KalmanVariant::ReferenceUpdate,
281 Some(ResidRejectCrit::default()),
282 proc_devices,
283 almanac.clone(),
284 )
285 .with_process_noise(process_noise);
286
287 let od_sol = odp.process_arc(initial_estimate, &arc)?;
288
289 let final_est = od_sol.estimates.last().unwrap();
290
291 println!("{final_est}");
292
293 let ric_err = traj_as_flown
294 .at(final_est.epoch())?
295 .orbit
296 .ric_difference(&final_est.orbital_state())?;
297 println!("== RIC at end ==");
298 println!("RIC Position (m): {:.3}", ric_err.radius_km * 1e3);
299 println!("RIC Velocity (m/s): {:.3}", ric_err.velocity_km_s * 1e3);
300
301 println!(
302 "Num residuals rejected: #{}",
303 od_sol.rejected_residuals().len()
304 );
305 println!(
306 "Percentage within +/-3: {}",
307 od_sol.residual_ratio_within_threshold(3.0).unwrap()
308 );
309 println!("Ratios normal? {}", od_sol.is_normal(None).unwrap());
310
311 od_sol.to_parquet("./04_lro_od_results.parquet", ExportCfg::default())?;
312
313 // In our case, we have the truth trajectory from NASA.
314 // So we can compute the RIC state difference between the real LRO ephem and what we've just estimated.
315 // Export the OD trajectory first.
316 let od_trajectory = od_sol.to_traj()?;
317 // Build the RIC difference.
318 od_trajectory.ric_diff_to_parquet(
319 &traj_as_flown,
320 "./04_lro_od_truth_error.parquet",
321 ExportCfg::default(),
322 )?;
323
324 Ok(())
325}
Sourcefn loads_many(data: &str) -> Result<Vec<Self>, ConfigError>
fn loads_many(data: &str) -> Result<Vec<Self>, ConfigError>
Builds a sequence of “Selves” from the provided string of a yaml
Sourcefn loads_named(data: &str) -> Result<BTreeMap<String, Self>, ConfigError>
fn loads_named(data: &str) -> Result<BTreeMap<String, Self>, ConfigError>
Builds a sequence of “Selves” from the provided string of a yaml
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.