Struct EclipseLocator

Source
pub struct EclipseLocator {
    pub light_source: Frame,
    pub shadow_bodies: Vec<Frame>,
}

Fields§

§light_source: Frame§shadow_bodies: Vec<Frame>

Implementations§

Source§

impl EclipseLocator

Source

pub fn cislunar(almanac: Arc<Almanac>) -> Self

Creates a new typical eclipse locator. The light source is the Sun, and the shadow bodies are the Earth and the Moon.

Examples found in repository?
examples/03_geo_analysis/stationkeeping.rs (line 121)
28fn main() -> Result<(), Box<dyn Error>> {
29    pel::init();
30    // Set up the dynamics like in the orbit raise.
31    let almanac = Arc::new(MetaAlmanac::latest().map_err(Box::new)?);
32    let epoch = Epoch::from_gregorian_utc_hms(2024, 2, 29, 12, 13, 14);
33
34    // Define the GEO orbit, and we're just going to maintain it very tightly.
35    let earth_j2000 = almanac.frame_from_uid(EARTH_J2000)?;
36    let orbit = Orbit::try_keplerian(42164.0, 1e-5, 0., 163.0, 75.0, 0.0, epoch, earth_j2000)?;
37    println!("{orbit:x}");
38
39    let sc = Spacecraft::builder()
40        .orbit(orbit)
41        .mass(Mass::from_dry_and_prop_masses(1000.0, 1000.0)) // 1000 kg of dry mass and prop, totalling 2.0 tons
42        .srp(SRPData::from_area(3.0 * 6.0)) // Assuming 1 kW/m^2 or 18 kW, giving a margin of 4.35 kW for on-propulsion consumption
43        .thruster(Thruster {
44            // "NEXT-STEP" row in Table 2
45            isp_s: 4435.0,
46            thrust_N: 0.472,
47        })
48        .mode(GuidanceMode::Thrust) // Start thrusting immediately.
49        .build();
50
51    // Set up the spacecraft dynamics like in the orbit raise example.
52
53    let prop_time = 30.0 * Unit::Day;
54
55    // Define the guidance law -- we're just using a Ruggiero controller as demonstrated in AAS-2004-5089.
56    let objectives = &[
57        Objective::within_tolerance(StateParameter::SMA, 42_164.0, 5.0), // 5 km
58        Objective::within_tolerance(StateParameter::Eccentricity, 0.001, 5e-5),
59        Objective::within_tolerance(StateParameter::Inclination, 0.05, 1e-2),
60    ];
61
62    let ruggiero_ctrl = Ruggiero::from_max_eclipse(objectives, sc, 0.2)?;
63    println!("{ruggiero_ctrl}");
64
65    let mut orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN]);
66
67    let mut jgm3_meta = MetaFile {
68        uri: "http://public-data.nyxspace.com/nyx/models/JGM3.cof.gz".to_string(),
69        crc32: Some(0xF446F027), // Specifying the CRC32 avoids redownloading it if it's cached.
70    };
71    jgm3_meta.process(true)?;
72
73    let harmonics = Harmonics::from_stor(
74        almanac.frame_from_uid(IAU_EARTH_FRAME)?,
75        HarmonicsMem::from_cof(&jgm3_meta.uri, 8, 8, true)?,
76    );
77    orbital_dyn.accel_models.push(harmonics);
78
79    let srp_dyn = SolarPressure::default(EARTH_J2000, almanac.clone())?;
80    let sc_dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn)
81        .with_guidance_law(ruggiero_ctrl.clone());
82
83    println!("{sc_dynamics}");
84
85    // Finally, let's use the Monte Carlo framework built into Nyx to propagate spacecraft.
86
87    // Let's start by defining the dispersion.
88    // The MultivariateNormal structure allows us to define the dispersions in any of the orbital parameters, but these are applied directly in the Cartesian state space.
89    // Note that additional validation on the MVN is in progress -- https://github.com/nyx-space/nyx/issues/339.
90    let mc_rv = MvnSpacecraft::new(
91        sc,
92        vec![StateDispersion::zero_mean(StateParameter::SMA, 3.0)],
93    )?;
94
95    let my_mc = MonteCarlo::new(
96        sc, // Nominal state
97        mc_rv,
98        "03_geo_sk".to_string(), // Scenario name
99        None, // No specific seed specified, so one will be drawn from the computer's entropy.
100    );
101
102    // Build the propagator setup.
103    let setup = Propagator::rk89(
104        sc_dynamics.clone(),
105        IntegratorOptions::builder()
106            .min_step(10.0_f64.seconds())
107            .error_ctrl(ErrorControl::RSSCartesianStep)
108            .build(),
109    );
110
111    let num_runs = 25;
112    let rslts = my_mc.run_until_epoch(setup, almanac.clone(), sc.epoch() + prop_time, num_runs);
113
114    assert_eq!(rslts.runs.len(), num_runs);
115
116    // For all of the resulting trajectories, we'll want to compute the percentage of penumbra and umbra.
117
118    rslts.to_parquet(
119        "03_geo_sk.parquet",
120        Some(vec![
121            &EclipseLocator::cislunar(almanac.clone()).to_penumbra_event()
122        ]),
123        ExportCfg::default(),
124        almanac,
125    )?;
126
127    Ok(())
128}
More examples
Hide additional examples
examples/03_geo_analysis/raise.rs (line 135)
27fn main() -> Result<(), Box<dyn Error>> {
28    pel::init();
29
30    // Dynamics models require planetary constants and ephemerides to be defined.
31    // Let's start by grabbing those by using ANISE's latest MetaAlmanac.
32    // This will automatically download the DE440s planetary ephemeris,
33    // the daily-updated Earth Orientation Parameters, the high fidelity Moon orientation
34    // parameters (for the Moon Mean Earth and Moon Principal Axes frames), and the PCK11
35    // planetary constants kernels.
36    // For details, refer to https://github.com/nyx-space/anise/blob/master/data/latest.dhall.
37    // Note that we place the Almanac into an Arc so we can clone it cheaply and provide read-only
38    // references to many functions.
39    let almanac = Arc::new(MetaAlmanac::latest().map_err(Box::new)?);
40    // Fetch the EME2000 frame from the Almabac
41    let eme2k = almanac.frame_from_uid(EARTH_J2000).unwrap();
42    // Define the orbit epoch
43    let epoch = Epoch::from_gregorian_utc_hms(2024, 2, 29, 12, 13, 14);
44
45    // Build the spacecraft itself.
46    // Using slide 6 of https://aerospace.org/sites/default/files/2018-11/Davis-Mayberry_HPSEP_11212018.pdf
47    // for the "next gen" SEP characteristics.
48
49    // GTO start
50    let orbit = Orbit::keplerian(24505.9, 0.725, 7.05, 0.0, 0.0, 0.0, epoch, eme2k);
51
52    let sc = Spacecraft::builder()
53        .orbit(orbit)
54        .mass(Mass::from_dry_and_prop_masses(1000.0, 1000.0)) // 1000 kg of dry mass and prop, totalling 2.0 tons
55        .srp(SRPData::from_area(3.0 * 6.0)) // Assuming 1 kW/m^2 or 18 kW, giving a margin of 4.35 kW for on-propulsion consumption
56        .thruster(Thruster {
57            // "NEXT-STEP" row in Table 2
58            isp_s: 4435.0,
59            thrust_N: 0.472,
60        })
61        .mode(GuidanceMode::Thrust) // Start thrusting immediately.
62        .build();
63
64    let prop_time = 180.0 * Unit::Day;
65
66    // Define the guidance law -- we're just using a Ruggiero controller as demonstrated in AAS-2004-5089.
67    let objectives = &[
68        Objective::within_tolerance(StateParameter::SMA, 42_165.0, 20.0),
69        Objective::within_tolerance(StateParameter::Eccentricity, 0.001, 5e-5),
70        Objective::within_tolerance(StateParameter::Inclination, 0.05, 1e-2),
71    ];
72
73    // Ensure that we only thrust if we have more than 20% illumination.
74    let ruggiero_ctrl = Ruggiero::from_max_eclipse(objectives, sc, 0.2).unwrap();
75    println!("{ruggiero_ctrl}");
76
77    // Define the high fidelity dynamics
78
79    // Set up the spacecraft dynamics.
80
81    // Specify that the orbital dynamics must account for the graviational pull of the Moon and the Sun.
82    // The gravity of the Earth will also be accounted for since the spaceraft in an Earth orbit.
83    let mut orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN]);
84
85    // We want to include the spherical harmonics, so let's download the gravitational data from the Nyx Cloud.
86    // We're using the JGM3 model here, which is the default in GMAT.
87    let mut jgm3_meta = MetaFile {
88        uri: "http://public-data.nyxspace.com/nyx/models/JGM3.cof.gz".to_string(),
89        crc32: Some(0xF446F027), // Specifying the CRC32 avoids redownloading it if it's cached.
90    };
91    // And let's download it if we don't have it yet.
92    jgm3_meta.process(true)?;
93
94    // Build the spherical harmonics.
95    // The harmonics must be computed in the body fixed frame.
96    // We're using the long term prediction of the Earth centered Earth fixed frame, IAU Earth.
97    let harmonics = Harmonics::from_stor(
98        almanac.frame_from_uid(IAU_EARTH_FRAME)?,
99        HarmonicsMem::from_cof(&jgm3_meta.uri, 8, 8, true).unwrap(),
100    );
101
102    // Include the spherical harmonics into the orbital dynamics.
103    orbital_dyn.accel_models.push(harmonics);
104
105    // We define the solar radiation pressure, using the default solar flux and accounting only
106    // for the eclipsing caused by the Earth.
107    let srp_dyn = SolarPressure::default(EARTH_J2000, almanac.clone())?;
108
109    // Finalize setting up the dynamics, specifying the force models (orbital_dyn) separately from the
110    // acceleration models (SRP in this case). Use `from_models` to specify multiple accel models.
111    let sc_dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn)
112        .with_guidance_law(ruggiero_ctrl.clone());
113
114    println!("{:x}", orbit);
115
116    // We specify a minimum step in the propagator because the Ruggiero control would otherwise drive this step very low.
117    let (final_state, traj) = Propagator::rk89(
118        sc_dynamics.clone(),
119        IntegratorOptions::builder()
120            .min_step(10.0_f64.seconds())
121            .error_ctrl(ErrorControl::RSSCartesianStep)
122            .build(),
123    )
124    .with(sc, almanac.clone())
125    .for_duration_with_traj(prop_time)?;
126
127    let prop_usage = sc.mass.prop_mass_kg - final_state.mass.prop_mass_kg;
128    println!("{:x}", final_state.orbit);
129    println!("prop usage: {:.3} kg", prop_usage);
130
131    // Finally, export the results for analysis, including the penumbra percentage throughout the orbit raise.
132    traj.to_parquet(
133        "./03_geo_raise.parquet",
134        Some(vec![
135            &EclipseLocator::cislunar(almanac.clone()).to_penumbra_event()
136        ]),
137        ExportCfg::default(),
138        almanac,
139    )?;
140
141    for status_line in ruggiero_ctrl.status(&final_state) {
142        println!("{status_line}");
143    }
144
145    ruggiero_ctrl
146        .achieved(&final_state)
147        .expect("objective not achieved");
148
149    Ok(())
150}
examples/02_jwst_covar_monte_carlo/main.rs (line 153)
26fn main() -> Result<(), Box<dyn Error>> {
27    pel::init();
28    // Dynamics models require planetary constants and ephemerides to be defined.
29    // Let's start by grabbing those by using ANISE's latest MetaAlmanac.
30    // For details, refer to https://github.com/nyx-space/anise/blob/master/data/latest.dhall.
31
32    // Download the regularly update of the James Webb Space Telescope reconstucted (or definitive) ephemeris.
33    // Refer to https://naif.jpl.nasa.gov/pub/naif/JWST/kernels/spk/aareadme.txt for details.
34    let mut latest_jwst_ephem = MetaFile {
35        uri: "https://naif.jpl.nasa.gov/pub/naif/JWST/kernels/spk/jwst_rec.bsp".to_string(),
36        crc32: None,
37    };
38    latest_jwst_ephem.process(true)?;
39
40    // Load this ephem in the general Almanac we're using for this analysis.
41    let almanac = Arc::new(
42        MetaAlmanac::latest()
43            .map_err(Box::new)?
44            .load_from_metafile(latest_jwst_ephem, true)?,
45    );
46
47    // By loading this ephemeris file in the ANISE GUI or ANISE CLI, we can find the NAIF ID of the JWST
48    // in the BSP. We need this ID in order to query the ephemeris.
49    const JWST_NAIF_ID: i32 = -170;
50    // Let's build a frame in the J2000 orientation centered on the JWST.
51    const JWST_J2000: Frame = Frame::from_ephem_j2000(JWST_NAIF_ID);
52
53    // Since the ephemeris file is updated regularly, we'll just grab the latest state in the ephem.
54    let (earliest_epoch, latest_epoch) = almanac.spk_domain(JWST_NAIF_ID)?;
55    println!("JWST defined from {earliest_epoch} to {latest_epoch}");
56    // Fetch the state, printing it in the Earth J2000 frame.
57    let jwst_orbit = almanac.transform(JWST_J2000, EARTH_J2000, latest_epoch, None)?;
58    println!("{jwst_orbit:x}");
59
60    // Build the spacecraft
61    // SRP area assumed to be the full sunshield and mass if 6200.0 kg, c.f. https://webb.nasa.gov/content/about/faqs/facts.html
62    // SRP Coefficient of reflectivity assumed to be that of Kapton, i.e. 2 - 0.44 = 1.56, table 1 from https://amostech.com/TechnicalPapers/2018/Poster/Bengtson.pdf
63    let jwst = Spacecraft::builder()
64        .orbit(jwst_orbit)
65        .srp(SRPData {
66            area_m2: 21.197 * 14.162,
67            coeff_reflectivity: 1.56,
68        })
69        .mass(Mass::from_dry_mass(6200.0))
70        .build();
71
72    // Build up the spacecraft uncertainty builder.
73    // We can use the spacecraft uncertainty structure to build this up.
74    // We start by specifying the nominal state (as defined above), then the uncertainty in position and velocity
75    // in the RIC frame. We could also specify the Cr, Cd, and mass uncertainties, but these aren't accounted for until
76    // Nyx can also estimate the deviation of the spacecraft parameters.
77    let jwst_uncertainty = SpacecraftUncertainty::builder()
78        .nominal(jwst)
79        .frame(LocalFrame::RIC)
80        .x_km(0.5)
81        .y_km(0.3)
82        .z_km(1.5)
83        .vx_km_s(1e-4)
84        .vy_km_s(0.6e-3)
85        .vz_km_s(3e-3)
86        .build();
87
88    println!("{jwst_uncertainty}");
89
90    // Build the Kalman filter estimate.
91    // Note that we could have used the KfEstimate structure directly (as seen throughout the OD integration tests)
92    // but this approach requires quite a bit more boilerplate code.
93    let jwst_estimate = jwst_uncertainty.to_estimate()?;
94
95    // Set up the spacecraft dynamics.
96    // We'll use the point masses of the Earth, Sun, Jupiter (barycenter, because it's in the DE440), and the Moon.
97    // We'll also enable solar radiation pressure since the James Webb has a huge and highly reflective sun shield.
98
99    let orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN, JUPITER_BARYCENTER]);
100    let srp_dyn = SolarPressure::new(vec![EARTH_J2000, MOON_J2000], almanac.clone())?;
101
102    // Finalize setting up the dynamics.
103    let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn);
104
105    // Build the propagator set up to use for the whole analysis.
106    let setup = Propagator::default(dynamics);
107
108    // All of the analysis will use this duration.
109    let prediction_duration = 6.5 * Unit::Day;
110
111    // === Covariance mapping ===
112    // For the covariance mapping / prediction, we'll use the common orbit determination approach.
113    // This is done by setting up a spacecraft OD process, and predicting for the analysis duration.
114
115    let ckf = KF::no_snc(jwst_estimate);
116
117    // Build the propagation instance for the OD process.
118    let prop = setup.with(jwst.with_stm(), almanac.clone());
119    let mut odp = SpacecraftODProcess::ckf(prop, ckf, BTreeMap::new(), None, almanac.clone());
120
121    // Define the prediction step, i.e. how often we want to know the covariance.
122    let step = 1_i64.minutes();
123    // Finally, predict, and export the trajectory with covariance to a parquet file.
124    odp.predict_for(step, prediction_duration)?;
125    odp.to_parquet(
126        &TrackingDataArc::default(),
127        "./02_jwst_covar_map.parquet",
128        ExportCfg::default(),
129    )?;
130
131    // === Monte Carlo framework ===
132    // Nyx comes with a complete multi-threaded Monte Carlo frame. It's blazing fast.
133
134    let my_mc = MonteCarlo::new(
135        jwst, // Nominal state
136        jwst_estimate.to_random_variable()?,
137        "02_jwst".to_string(), // Scenario name
138        None, // No specific seed specified, so one will be drawn from the computer's entropy.
139    );
140
141    let num_runs = 5_000;
142    let rslts = my_mc.run_until_epoch(
143        setup,
144        almanac.clone(),
145        jwst.epoch() + prediction_duration,
146        num_runs,
147    );
148
149    assert_eq!(rslts.runs.len(), num_runs);
150    // Finally, export these results, computing the eclipse percentage for all of these results.
151
152    // For all of the resulting trajectories, we'll want to compute the percentage of penumbra and umbra.
153    let eclipse_loc = EclipseLocator::cislunar(almanac.clone());
154    let umbra_event = eclipse_loc.to_umbra_event();
155    let penumbra_event = eclipse_loc.to_penumbra_event();
156
157    rslts.to_parquet(
158        "02_jwst_monte_carlo.parquet",
159        Some(vec![&umbra_event, &penumbra_event]),
160        ExportCfg::default(),
161        almanac,
162    )?;
163
164    Ok(())
165}
examples/03_geo_analysis/drift.rs (line 155)
26fn main() -> Result<(), Box<dyn Error>> {
27    pel::init();
28    // Dynamics models require planetary constants and ephemerides to be defined.
29    // Let's start by grabbing those by using ANISE's latest MetaAlmanac.
30    // This will automatically download the DE440s planetary ephemeris,
31    // the daily-updated Earth Orientation Parameters, the high fidelity Moon orientation
32    // parameters (for the Moon Mean Earth and Moon Principal Axes frames), and the PCK11
33    // planetary constants kernels.
34    // For details, refer to https://github.com/nyx-space/anise/blob/master/data/latest.dhall.
35    // Note that we place the Almanac into an Arc so we can clone it cheaply and provide read-only
36    // references to many functions.
37    let almanac = Arc::new(MetaAlmanac::latest().map_err(Box::new)?);
38    // Define the orbit epoch
39    let epoch = Epoch::from_gregorian_utc_hms(2024, 2, 29, 12, 13, 14);
40
41    // Define the orbit.
42    // First we need to fetch the Earth J2000 from information from the Almanac.
43    // This allows the frame to include the gravitational parameters and the shape of the Earth,
44    // defined as a tri-axial ellipoid. Note that this shape can be changed manually or in the Almanac
45    // by loading a different set of planetary constants.
46    let earth_j2000 = almanac.frame_from_uid(EARTH_J2000)?;
47
48    // Placing this GEO bird just above Colorado.
49    // In theory, the eccentricity is zero, but in practice, it's about 1e-5 to 1e-6 at best.
50    let orbit = Orbit::try_keplerian(42164.0, 1e-5, 0., 163.0, 75.0, 0.0, epoch, earth_j2000)?;
51    // Print in in Keplerian form.
52    println!("{orbit:x}");
53
54    let state_bf = almanac.transform_to(orbit, IAU_EARTH_FRAME, None)?;
55    let (orig_lat_deg, orig_long_deg, orig_alt_km) = state_bf.latlongalt()?;
56
57    // Nyx is used for high fidelity propagation, not Keplerian propagation as above.
58    // Nyx only propagates Spacecraft at the moment, which allows it to account for acceleration
59    // models such as solar radiation pressure.
60
61    // Let's build a cubesat sized spacecraft, with an SRP area of 10 cm^2 and a mass of 9.6 kg.
62    let sc = Spacecraft::builder()
63        .orbit(orbit)
64        .mass(Mass::from_dry_mass(9.60))
65        .srp(SRPData {
66            area_m2: 10e-4,
67            coeff_reflectivity: 1.1,
68        })
69        .build();
70    println!("{sc:x}");
71
72    // Set up the spacecraft dynamics.
73
74    // Specify that the orbital dynamics must account for the graviational pull of the Moon and the Sun.
75    // The gravity of the Earth will also be accounted for since the spaceraft in an Earth orbit.
76    let mut orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN]);
77
78    // We want to include the spherical harmonics, so let's download the gravitational data from the Nyx Cloud.
79    // We're using the JGM3 model here, which is the default in GMAT.
80    let mut jgm3_meta = MetaFile {
81        uri: "http://public-data.nyxspace.com/nyx/models/JGM3.cof.gz".to_string(),
82        crc32: Some(0xF446F027), // Specifying the CRC32 avoids redownloading it if it's cached.
83    };
84    // And let's download it if we don't have it yet.
85    jgm3_meta.process(true)?;
86
87    // Build the spherical harmonics.
88    // The harmonics must be computed in the body fixed frame.
89    // We're using the long term prediction of the Earth centered Earth fixed frame, IAU Earth.
90    let harmonics_21x21 = Harmonics::from_stor(
91        almanac.frame_from_uid(IAU_EARTH_FRAME)?,
92        HarmonicsMem::from_cof(&jgm3_meta.uri, 21, 21, true).unwrap(),
93    );
94
95    // Include the spherical harmonics into the orbital dynamics.
96    orbital_dyn.accel_models.push(harmonics_21x21);
97
98    // We define the solar radiation pressure, using the default solar flux and accounting only
99    // for the eclipsing caused by the Earth and Moon.
100    let srp_dyn = SolarPressure::new(vec![EARTH_J2000, MOON_J2000], almanac.clone())?;
101
102    // Finalize setting up the dynamics, specifying the force models (orbital_dyn) separately from the
103    // acceleration models (SRP in this case). Use `from_models` to specify multiple accel models.
104    let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn);
105
106    println!("{dynamics}");
107
108    // Finally, let's propagate this orbit to the same epoch as above.
109    // The first returned value is the spacecraft state at the final epoch.
110    // The second value is the full trajectory where the step size is variable step used by the propagator.
111    let (future_sc, trajectory) = Propagator::default(dynamics)
112        .with(sc, almanac.clone())
113        .until_epoch_with_traj(epoch + Unit::Century * 0.03)?;
114
115    println!("=== High fidelity propagation ===");
116    println!(
117        "SMA changed by {:.3} km",
118        orbit.sma_km()? - future_sc.orbit.sma_km()?
119    );
120    println!(
121        "ECC changed by {:.6}",
122        orbit.ecc()? - future_sc.orbit.ecc()?
123    );
124    println!(
125        "INC changed by {:.3e} deg",
126        orbit.inc_deg()? - future_sc.orbit.inc_deg()?
127    );
128    println!(
129        "RAAN changed by {:.3} deg",
130        orbit.raan_deg()? - future_sc.orbit.raan_deg()?
131    );
132    println!(
133        "AOP changed by {:.3} deg",
134        orbit.aop_deg()? - future_sc.orbit.aop_deg()?
135    );
136    println!(
137        "TA changed by {:.3} deg",
138        orbit.ta_deg()? - future_sc.orbit.ta_deg()?
139    );
140
141    // We also have access to the full trajectory throughout the propagation.
142    println!("{trajectory}");
143
144    println!("Spacecraft params after 3 years without active control:\n{future_sc:x}");
145
146    // With the trajectory, let's build a few data products.
147
148    // 1. Export the trajectory as a parquet file, which includes the Keplerian orbital elements.
149
150    let analysis_step = Unit::Minute * 5;
151
152    trajectory.to_parquet(
153        "./03_geo_hf_prop.parquet",
154        Some(vec![
155            &EclipseLocator::cislunar(almanac.clone()).to_penumbra_event()
156        ]),
157        ExportCfg::builder().step(analysis_step).build(),
158        almanac.clone(),
159    )?;
160
161    // 2. Compute the latitude, longitude, and altitude throughout the trajectory by rotating the spacecraft position into the Earth body fixed frame.
162
163    // We iterate over the trajectory, grabbing a state every two minutes.
164    let mut offset_s = vec![];
165    let mut epoch_str = vec![];
166    let mut longitude_deg = vec![];
167    let mut latitude_deg = vec![];
168    let mut altitude_km = vec![];
169
170    for state in trajectory.every(analysis_step) {
171        // Convert the GEO bird state into the body fixed frame, and keep track of its latitude, longitude, and altitude.
172        // These define the GEO stationkeeping box.
173
174        let this_epoch = state.epoch();
175
176        offset_s.push((this_epoch - orbit.epoch).to_seconds());
177        epoch_str.push(this_epoch.to_isoformat());
178
179        let state_bf = almanac.transform_to(state.orbit, IAU_EARTH_FRAME, None)?;
180        let (lat_deg, long_deg, alt_km) = state_bf.latlongalt()?;
181        longitude_deg.push(long_deg);
182        latitude_deg.push(lat_deg);
183        altitude_km.push(alt_km);
184    }
185
186    println!(
187        "Longitude changed by {:.3} deg -- Box is 0.1 deg E-W",
188        orig_long_deg - longitude_deg.last().unwrap()
189    );
190
191    println!(
192        "Latitude changed by {:.3} deg -- Box is 0.05 deg N-S",
193        orig_lat_deg - latitude_deg.last().unwrap()
194    );
195
196    println!(
197        "Altitude changed by {:.3} km -- Box is 30 km",
198        orig_alt_km - altitude_km.last().unwrap()
199    );
200
201    // Build the station keeping data frame.
202    let mut sk_df = df!(
203        "Offset (s)" => offset_s.clone(),
204        "Epoch (UTC)" => epoch_str.clone(),
205        "Longitude E-W (deg)" => longitude_deg,
206        "Latitude N-S (deg)" => latitude_deg,
207        "Altitude (km)" => altitude_km,
208
209    )?;
210
211    // Create a file to write the Parquet to
212    let file = File::create("./03_geo_lla.parquet").expect("Could not create file");
213
214    // Create a ParquetWriter and write the DataFrame to the file
215    ParquetWriter::new(file).finish(&mut sk_df)?;
216
217    Ok(())
218}
Source

pub fn compute( &self, observer: Orbit, almanac: Arc<Almanac>, ) -> AlmanacResult<Occultation>

Compute the visibility/eclipse between an observer and an observed state

Source

pub fn to_umbra_event(&self) -> UmbraEvent

Creates an umbra event from this eclipse locator. Evaluation of the event, returns 0.0 for umbra, 1.0 for visibility (no shadow) and some value in between for penumbra

Examples found in repository?
examples/02_jwst_covar_monte_carlo/main.rs (line 154)
26fn main() -> Result<(), Box<dyn Error>> {
27    pel::init();
28    // Dynamics models require planetary constants and ephemerides to be defined.
29    // Let's start by grabbing those by using ANISE's latest MetaAlmanac.
30    // For details, refer to https://github.com/nyx-space/anise/blob/master/data/latest.dhall.
31
32    // Download the regularly update of the James Webb Space Telescope reconstucted (or definitive) ephemeris.
33    // Refer to https://naif.jpl.nasa.gov/pub/naif/JWST/kernels/spk/aareadme.txt for details.
34    let mut latest_jwst_ephem = MetaFile {
35        uri: "https://naif.jpl.nasa.gov/pub/naif/JWST/kernels/spk/jwst_rec.bsp".to_string(),
36        crc32: None,
37    };
38    latest_jwst_ephem.process(true)?;
39
40    // Load this ephem in the general Almanac we're using for this analysis.
41    let almanac = Arc::new(
42        MetaAlmanac::latest()
43            .map_err(Box::new)?
44            .load_from_metafile(latest_jwst_ephem, true)?,
45    );
46
47    // By loading this ephemeris file in the ANISE GUI or ANISE CLI, we can find the NAIF ID of the JWST
48    // in the BSP. We need this ID in order to query the ephemeris.
49    const JWST_NAIF_ID: i32 = -170;
50    // Let's build a frame in the J2000 orientation centered on the JWST.
51    const JWST_J2000: Frame = Frame::from_ephem_j2000(JWST_NAIF_ID);
52
53    // Since the ephemeris file is updated regularly, we'll just grab the latest state in the ephem.
54    let (earliest_epoch, latest_epoch) = almanac.spk_domain(JWST_NAIF_ID)?;
55    println!("JWST defined from {earliest_epoch} to {latest_epoch}");
56    // Fetch the state, printing it in the Earth J2000 frame.
57    let jwst_orbit = almanac.transform(JWST_J2000, EARTH_J2000, latest_epoch, None)?;
58    println!("{jwst_orbit:x}");
59
60    // Build the spacecraft
61    // SRP area assumed to be the full sunshield and mass if 6200.0 kg, c.f. https://webb.nasa.gov/content/about/faqs/facts.html
62    // SRP Coefficient of reflectivity assumed to be that of Kapton, i.e. 2 - 0.44 = 1.56, table 1 from https://amostech.com/TechnicalPapers/2018/Poster/Bengtson.pdf
63    let jwst = Spacecraft::builder()
64        .orbit(jwst_orbit)
65        .srp(SRPData {
66            area_m2: 21.197 * 14.162,
67            coeff_reflectivity: 1.56,
68        })
69        .mass(Mass::from_dry_mass(6200.0))
70        .build();
71
72    // Build up the spacecraft uncertainty builder.
73    // We can use the spacecraft uncertainty structure to build this up.
74    // We start by specifying the nominal state (as defined above), then the uncertainty in position and velocity
75    // in the RIC frame. We could also specify the Cr, Cd, and mass uncertainties, but these aren't accounted for until
76    // Nyx can also estimate the deviation of the spacecraft parameters.
77    let jwst_uncertainty = SpacecraftUncertainty::builder()
78        .nominal(jwst)
79        .frame(LocalFrame::RIC)
80        .x_km(0.5)
81        .y_km(0.3)
82        .z_km(1.5)
83        .vx_km_s(1e-4)
84        .vy_km_s(0.6e-3)
85        .vz_km_s(3e-3)
86        .build();
87
88    println!("{jwst_uncertainty}");
89
90    // Build the Kalman filter estimate.
91    // Note that we could have used the KfEstimate structure directly (as seen throughout the OD integration tests)
92    // but this approach requires quite a bit more boilerplate code.
93    let jwst_estimate = jwst_uncertainty.to_estimate()?;
94
95    // Set up the spacecraft dynamics.
96    // We'll use the point masses of the Earth, Sun, Jupiter (barycenter, because it's in the DE440), and the Moon.
97    // We'll also enable solar radiation pressure since the James Webb has a huge and highly reflective sun shield.
98
99    let orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN, JUPITER_BARYCENTER]);
100    let srp_dyn = SolarPressure::new(vec![EARTH_J2000, MOON_J2000], almanac.clone())?;
101
102    // Finalize setting up the dynamics.
103    let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn);
104
105    // Build the propagator set up to use for the whole analysis.
106    let setup = Propagator::default(dynamics);
107
108    // All of the analysis will use this duration.
109    let prediction_duration = 6.5 * Unit::Day;
110
111    // === Covariance mapping ===
112    // For the covariance mapping / prediction, we'll use the common orbit determination approach.
113    // This is done by setting up a spacecraft OD process, and predicting for the analysis duration.
114
115    let ckf = KF::no_snc(jwst_estimate);
116
117    // Build the propagation instance for the OD process.
118    let prop = setup.with(jwst.with_stm(), almanac.clone());
119    let mut odp = SpacecraftODProcess::ckf(prop, ckf, BTreeMap::new(), None, almanac.clone());
120
121    // Define the prediction step, i.e. how often we want to know the covariance.
122    let step = 1_i64.minutes();
123    // Finally, predict, and export the trajectory with covariance to a parquet file.
124    odp.predict_for(step, prediction_duration)?;
125    odp.to_parquet(
126        &TrackingDataArc::default(),
127        "./02_jwst_covar_map.parquet",
128        ExportCfg::default(),
129    )?;
130
131    // === Monte Carlo framework ===
132    // Nyx comes with a complete multi-threaded Monte Carlo frame. It's blazing fast.
133
134    let my_mc = MonteCarlo::new(
135        jwst, // Nominal state
136        jwst_estimate.to_random_variable()?,
137        "02_jwst".to_string(), // Scenario name
138        None, // No specific seed specified, so one will be drawn from the computer's entropy.
139    );
140
141    let num_runs = 5_000;
142    let rslts = my_mc.run_until_epoch(
143        setup,
144        almanac.clone(),
145        jwst.epoch() + prediction_duration,
146        num_runs,
147    );
148
149    assert_eq!(rslts.runs.len(), num_runs);
150    // Finally, export these results, computing the eclipse percentage for all of these results.
151
152    // For all of the resulting trajectories, we'll want to compute the percentage of penumbra and umbra.
153    let eclipse_loc = EclipseLocator::cislunar(almanac.clone());
154    let umbra_event = eclipse_loc.to_umbra_event();
155    let penumbra_event = eclipse_loc.to_penumbra_event();
156
157    rslts.to_parquet(
158        "02_jwst_monte_carlo.parquet",
159        Some(vec![&umbra_event, &penumbra_event]),
160        ExportCfg::default(),
161        almanac,
162    )?;
163
164    Ok(())
165}
Source

pub fn to_penumbra_event(&self) -> PenumbraEvent

Creates a penumbra event from this eclipse locator

Examples found in repository?
examples/03_geo_analysis/stationkeeping.rs (line 121)
28fn main() -> Result<(), Box<dyn Error>> {
29    pel::init();
30    // Set up the dynamics like in the orbit raise.
31    let almanac = Arc::new(MetaAlmanac::latest().map_err(Box::new)?);
32    let epoch = Epoch::from_gregorian_utc_hms(2024, 2, 29, 12, 13, 14);
33
34    // Define the GEO orbit, and we're just going to maintain it very tightly.
35    let earth_j2000 = almanac.frame_from_uid(EARTH_J2000)?;
36    let orbit = Orbit::try_keplerian(42164.0, 1e-5, 0., 163.0, 75.0, 0.0, epoch, earth_j2000)?;
37    println!("{orbit:x}");
38
39    let sc = Spacecraft::builder()
40        .orbit(orbit)
41        .mass(Mass::from_dry_and_prop_masses(1000.0, 1000.0)) // 1000 kg of dry mass and prop, totalling 2.0 tons
42        .srp(SRPData::from_area(3.0 * 6.0)) // Assuming 1 kW/m^2 or 18 kW, giving a margin of 4.35 kW for on-propulsion consumption
43        .thruster(Thruster {
44            // "NEXT-STEP" row in Table 2
45            isp_s: 4435.0,
46            thrust_N: 0.472,
47        })
48        .mode(GuidanceMode::Thrust) // Start thrusting immediately.
49        .build();
50
51    // Set up the spacecraft dynamics like in the orbit raise example.
52
53    let prop_time = 30.0 * Unit::Day;
54
55    // Define the guidance law -- we're just using a Ruggiero controller as demonstrated in AAS-2004-5089.
56    let objectives = &[
57        Objective::within_tolerance(StateParameter::SMA, 42_164.0, 5.0), // 5 km
58        Objective::within_tolerance(StateParameter::Eccentricity, 0.001, 5e-5),
59        Objective::within_tolerance(StateParameter::Inclination, 0.05, 1e-2),
60    ];
61
62    let ruggiero_ctrl = Ruggiero::from_max_eclipse(objectives, sc, 0.2)?;
63    println!("{ruggiero_ctrl}");
64
65    let mut orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN]);
66
67    let mut jgm3_meta = MetaFile {
68        uri: "http://public-data.nyxspace.com/nyx/models/JGM3.cof.gz".to_string(),
69        crc32: Some(0xF446F027), // Specifying the CRC32 avoids redownloading it if it's cached.
70    };
71    jgm3_meta.process(true)?;
72
73    let harmonics = Harmonics::from_stor(
74        almanac.frame_from_uid(IAU_EARTH_FRAME)?,
75        HarmonicsMem::from_cof(&jgm3_meta.uri, 8, 8, true)?,
76    );
77    orbital_dyn.accel_models.push(harmonics);
78
79    let srp_dyn = SolarPressure::default(EARTH_J2000, almanac.clone())?;
80    let sc_dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn)
81        .with_guidance_law(ruggiero_ctrl.clone());
82
83    println!("{sc_dynamics}");
84
85    // Finally, let's use the Monte Carlo framework built into Nyx to propagate spacecraft.
86
87    // Let's start by defining the dispersion.
88    // The MultivariateNormal structure allows us to define the dispersions in any of the orbital parameters, but these are applied directly in the Cartesian state space.
89    // Note that additional validation on the MVN is in progress -- https://github.com/nyx-space/nyx/issues/339.
90    let mc_rv = MvnSpacecraft::new(
91        sc,
92        vec![StateDispersion::zero_mean(StateParameter::SMA, 3.0)],
93    )?;
94
95    let my_mc = MonteCarlo::new(
96        sc, // Nominal state
97        mc_rv,
98        "03_geo_sk".to_string(), // Scenario name
99        None, // No specific seed specified, so one will be drawn from the computer's entropy.
100    );
101
102    // Build the propagator setup.
103    let setup = Propagator::rk89(
104        sc_dynamics.clone(),
105        IntegratorOptions::builder()
106            .min_step(10.0_f64.seconds())
107            .error_ctrl(ErrorControl::RSSCartesianStep)
108            .build(),
109    );
110
111    let num_runs = 25;
112    let rslts = my_mc.run_until_epoch(setup, almanac.clone(), sc.epoch() + prop_time, num_runs);
113
114    assert_eq!(rslts.runs.len(), num_runs);
115
116    // For all of the resulting trajectories, we'll want to compute the percentage of penumbra and umbra.
117
118    rslts.to_parquet(
119        "03_geo_sk.parquet",
120        Some(vec![
121            &EclipseLocator::cislunar(almanac.clone()).to_penumbra_event()
122        ]),
123        ExportCfg::default(),
124        almanac,
125    )?;
126
127    Ok(())
128}
More examples
Hide additional examples
examples/03_geo_analysis/raise.rs (line 135)
27fn main() -> Result<(), Box<dyn Error>> {
28    pel::init();
29
30    // Dynamics models require planetary constants and ephemerides to be defined.
31    // Let's start by grabbing those by using ANISE's latest MetaAlmanac.
32    // This will automatically download the DE440s planetary ephemeris,
33    // the daily-updated Earth Orientation Parameters, the high fidelity Moon orientation
34    // parameters (for the Moon Mean Earth and Moon Principal Axes frames), and the PCK11
35    // planetary constants kernels.
36    // For details, refer to https://github.com/nyx-space/anise/blob/master/data/latest.dhall.
37    // Note that we place the Almanac into an Arc so we can clone it cheaply and provide read-only
38    // references to many functions.
39    let almanac = Arc::new(MetaAlmanac::latest().map_err(Box::new)?);
40    // Fetch the EME2000 frame from the Almabac
41    let eme2k = almanac.frame_from_uid(EARTH_J2000).unwrap();
42    // Define the orbit epoch
43    let epoch = Epoch::from_gregorian_utc_hms(2024, 2, 29, 12, 13, 14);
44
45    // Build the spacecraft itself.
46    // Using slide 6 of https://aerospace.org/sites/default/files/2018-11/Davis-Mayberry_HPSEP_11212018.pdf
47    // for the "next gen" SEP characteristics.
48
49    // GTO start
50    let orbit = Orbit::keplerian(24505.9, 0.725, 7.05, 0.0, 0.0, 0.0, epoch, eme2k);
51
52    let sc = Spacecraft::builder()
53        .orbit(orbit)
54        .mass(Mass::from_dry_and_prop_masses(1000.0, 1000.0)) // 1000 kg of dry mass and prop, totalling 2.0 tons
55        .srp(SRPData::from_area(3.0 * 6.0)) // Assuming 1 kW/m^2 or 18 kW, giving a margin of 4.35 kW for on-propulsion consumption
56        .thruster(Thruster {
57            // "NEXT-STEP" row in Table 2
58            isp_s: 4435.0,
59            thrust_N: 0.472,
60        })
61        .mode(GuidanceMode::Thrust) // Start thrusting immediately.
62        .build();
63
64    let prop_time = 180.0 * Unit::Day;
65
66    // Define the guidance law -- we're just using a Ruggiero controller as demonstrated in AAS-2004-5089.
67    let objectives = &[
68        Objective::within_tolerance(StateParameter::SMA, 42_165.0, 20.0),
69        Objective::within_tolerance(StateParameter::Eccentricity, 0.001, 5e-5),
70        Objective::within_tolerance(StateParameter::Inclination, 0.05, 1e-2),
71    ];
72
73    // Ensure that we only thrust if we have more than 20% illumination.
74    let ruggiero_ctrl = Ruggiero::from_max_eclipse(objectives, sc, 0.2).unwrap();
75    println!("{ruggiero_ctrl}");
76
77    // Define the high fidelity dynamics
78
79    // Set up the spacecraft dynamics.
80
81    // Specify that the orbital dynamics must account for the graviational pull of the Moon and the Sun.
82    // The gravity of the Earth will also be accounted for since the spaceraft in an Earth orbit.
83    let mut orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN]);
84
85    // We want to include the spherical harmonics, so let's download the gravitational data from the Nyx Cloud.
86    // We're using the JGM3 model here, which is the default in GMAT.
87    let mut jgm3_meta = MetaFile {
88        uri: "http://public-data.nyxspace.com/nyx/models/JGM3.cof.gz".to_string(),
89        crc32: Some(0xF446F027), // Specifying the CRC32 avoids redownloading it if it's cached.
90    };
91    // And let's download it if we don't have it yet.
92    jgm3_meta.process(true)?;
93
94    // Build the spherical harmonics.
95    // The harmonics must be computed in the body fixed frame.
96    // We're using the long term prediction of the Earth centered Earth fixed frame, IAU Earth.
97    let harmonics = Harmonics::from_stor(
98        almanac.frame_from_uid(IAU_EARTH_FRAME)?,
99        HarmonicsMem::from_cof(&jgm3_meta.uri, 8, 8, true).unwrap(),
100    );
101
102    // Include the spherical harmonics into the orbital dynamics.
103    orbital_dyn.accel_models.push(harmonics);
104
105    // We define the solar radiation pressure, using the default solar flux and accounting only
106    // for the eclipsing caused by the Earth.
107    let srp_dyn = SolarPressure::default(EARTH_J2000, almanac.clone())?;
108
109    // Finalize setting up the dynamics, specifying the force models (orbital_dyn) separately from the
110    // acceleration models (SRP in this case). Use `from_models` to specify multiple accel models.
111    let sc_dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn)
112        .with_guidance_law(ruggiero_ctrl.clone());
113
114    println!("{:x}", orbit);
115
116    // We specify a minimum step in the propagator because the Ruggiero control would otherwise drive this step very low.
117    let (final_state, traj) = Propagator::rk89(
118        sc_dynamics.clone(),
119        IntegratorOptions::builder()
120            .min_step(10.0_f64.seconds())
121            .error_ctrl(ErrorControl::RSSCartesianStep)
122            .build(),
123    )
124    .with(sc, almanac.clone())
125    .for_duration_with_traj(prop_time)?;
126
127    let prop_usage = sc.mass.prop_mass_kg - final_state.mass.prop_mass_kg;
128    println!("{:x}", final_state.orbit);
129    println!("prop usage: {:.3} kg", prop_usage);
130
131    // Finally, export the results for analysis, including the penumbra percentage throughout the orbit raise.
132    traj.to_parquet(
133        "./03_geo_raise.parquet",
134        Some(vec![
135            &EclipseLocator::cislunar(almanac.clone()).to_penumbra_event()
136        ]),
137        ExportCfg::default(),
138        almanac,
139    )?;
140
141    for status_line in ruggiero_ctrl.status(&final_state) {
142        println!("{status_line}");
143    }
144
145    ruggiero_ctrl
146        .achieved(&final_state)
147        .expect("objective not achieved");
148
149    Ok(())
150}
examples/02_jwst_covar_monte_carlo/main.rs (line 155)
26fn main() -> Result<(), Box<dyn Error>> {
27    pel::init();
28    // Dynamics models require planetary constants and ephemerides to be defined.
29    // Let's start by grabbing those by using ANISE's latest MetaAlmanac.
30    // For details, refer to https://github.com/nyx-space/anise/blob/master/data/latest.dhall.
31
32    // Download the regularly update of the James Webb Space Telescope reconstucted (or definitive) ephemeris.
33    // Refer to https://naif.jpl.nasa.gov/pub/naif/JWST/kernels/spk/aareadme.txt for details.
34    let mut latest_jwst_ephem = MetaFile {
35        uri: "https://naif.jpl.nasa.gov/pub/naif/JWST/kernels/spk/jwst_rec.bsp".to_string(),
36        crc32: None,
37    };
38    latest_jwst_ephem.process(true)?;
39
40    // Load this ephem in the general Almanac we're using for this analysis.
41    let almanac = Arc::new(
42        MetaAlmanac::latest()
43            .map_err(Box::new)?
44            .load_from_metafile(latest_jwst_ephem, true)?,
45    );
46
47    // By loading this ephemeris file in the ANISE GUI or ANISE CLI, we can find the NAIF ID of the JWST
48    // in the BSP. We need this ID in order to query the ephemeris.
49    const JWST_NAIF_ID: i32 = -170;
50    // Let's build a frame in the J2000 orientation centered on the JWST.
51    const JWST_J2000: Frame = Frame::from_ephem_j2000(JWST_NAIF_ID);
52
53    // Since the ephemeris file is updated regularly, we'll just grab the latest state in the ephem.
54    let (earliest_epoch, latest_epoch) = almanac.spk_domain(JWST_NAIF_ID)?;
55    println!("JWST defined from {earliest_epoch} to {latest_epoch}");
56    // Fetch the state, printing it in the Earth J2000 frame.
57    let jwst_orbit = almanac.transform(JWST_J2000, EARTH_J2000, latest_epoch, None)?;
58    println!("{jwst_orbit:x}");
59
60    // Build the spacecraft
61    // SRP area assumed to be the full sunshield and mass if 6200.0 kg, c.f. https://webb.nasa.gov/content/about/faqs/facts.html
62    // SRP Coefficient of reflectivity assumed to be that of Kapton, i.e. 2 - 0.44 = 1.56, table 1 from https://amostech.com/TechnicalPapers/2018/Poster/Bengtson.pdf
63    let jwst = Spacecraft::builder()
64        .orbit(jwst_orbit)
65        .srp(SRPData {
66            area_m2: 21.197 * 14.162,
67            coeff_reflectivity: 1.56,
68        })
69        .mass(Mass::from_dry_mass(6200.0))
70        .build();
71
72    // Build up the spacecraft uncertainty builder.
73    // We can use the spacecraft uncertainty structure to build this up.
74    // We start by specifying the nominal state (as defined above), then the uncertainty in position and velocity
75    // in the RIC frame. We could also specify the Cr, Cd, and mass uncertainties, but these aren't accounted for until
76    // Nyx can also estimate the deviation of the spacecraft parameters.
77    let jwst_uncertainty = SpacecraftUncertainty::builder()
78        .nominal(jwst)
79        .frame(LocalFrame::RIC)
80        .x_km(0.5)
81        .y_km(0.3)
82        .z_km(1.5)
83        .vx_km_s(1e-4)
84        .vy_km_s(0.6e-3)
85        .vz_km_s(3e-3)
86        .build();
87
88    println!("{jwst_uncertainty}");
89
90    // Build the Kalman filter estimate.
91    // Note that we could have used the KfEstimate structure directly (as seen throughout the OD integration tests)
92    // but this approach requires quite a bit more boilerplate code.
93    let jwst_estimate = jwst_uncertainty.to_estimate()?;
94
95    // Set up the spacecraft dynamics.
96    // We'll use the point masses of the Earth, Sun, Jupiter (barycenter, because it's in the DE440), and the Moon.
97    // We'll also enable solar radiation pressure since the James Webb has a huge and highly reflective sun shield.
98
99    let orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN, JUPITER_BARYCENTER]);
100    let srp_dyn = SolarPressure::new(vec![EARTH_J2000, MOON_J2000], almanac.clone())?;
101
102    // Finalize setting up the dynamics.
103    let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn);
104
105    // Build the propagator set up to use for the whole analysis.
106    let setup = Propagator::default(dynamics);
107
108    // All of the analysis will use this duration.
109    let prediction_duration = 6.5 * Unit::Day;
110
111    // === Covariance mapping ===
112    // For the covariance mapping / prediction, we'll use the common orbit determination approach.
113    // This is done by setting up a spacecraft OD process, and predicting for the analysis duration.
114
115    let ckf = KF::no_snc(jwst_estimate);
116
117    // Build the propagation instance for the OD process.
118    let prop = setup.with(jwst.with_stm(), almanac.clone());
119    let mut odp = SpacecraftODProcess::ckf(prop, ckf, BTreeMap::new(), None, almanac.clone());
120
121    // Define the prediction step, i.e. how often we want to know the covariance.
122    let step = 1_i64.minutes();
123    // Finally, predict, and export the trajectory with covariance to a parquet file.
124    odp.predict_for(step, prediction_duration)?;
125    odp.to_parquet(
126        &TrackingDataArc::default(),
127        "./02_jwst_covar_map.parquet",
128        ExportCfg::default(),
129    )?;
130
131    // === Monte Carlo framework ===
132    // Nyx comes with a complete multi-threaded Monte Carlo frame. It's blazing fast.
133
134    let my_mc = MonteCarlo::new(
135        jwst, // Nominal state
136        jwst_estimate.to_random_variable()?,
137        "02_jwst".to_string(), // Scenario name
138        None, // No specific seed specified, so one will be drawn from the computer's entropy.
139    );
140
141    let num_runs = 5_000;
142    let rslts = my_mc.run_until_epoch(
143        setup,
144        almanac.clone(),
145        jwst.epoch() + prediction_duration,
146        num_runs,
147    );
148
149    assert_eq!(rslts.runs.len(), num_runs);
150    // Finally, export these results, computing the eclipse percentage for all of these results.
151
152    // For all of the resulting trajectories, we'll want to compute the percentage of penumbra and umbra.
153    let eclipse_loc = EclipseLocator::cislunar(almanac.clone());
154    let umbra_event = eclipse_loc.to_umbra_event();
155    let penumbra_event = eclipse_loc.to_penumbra_event();
156
157    rslts.to_parquet(
158        "02_jwst_monte_carlo.parquet",
159        Some(vec![&umbra_event, &penumbra_event]),
160        ExportCfg::default(),
161        almanac,
162    )?;
163
164    Ok(())
165}
examples/03_geo_analysis/drift.rs (line 155)
26fn main() -> Result<(), Box<dyn Error>> {
27    pel::init();
28    // Dynamics models require planetary constants and ephemerides to be defined.
29    // Let's start by grabbing those by using ANISE's latest MetaAlmanac.
30    // This will automatically download the DE440s planetary ephemeris,
31    // the daily-updated Earth Orientation Parameters, the high fidelity Moon orientation
32    // parameters (for the Moon Mean Earth and Moon Principal Axes frames), and the PCK11
33    // planetary constants kernels.
34    // For details, refer to https://github.com/nyx-space/anise/blob/master/data/latest.dhall.
35    // Note that we place the Almanac into an Arc so we can clone it cheaply and provide read-only
36    // references to many functions.
37    let almanac = Arc::new(MetaAlmanac::latest().map_err(Box::new)?);
38    // Define the orbit epoch
39    let epoch = Epoch::from_gregorian_utc_hms(2024, 2, 29, 12, 13, 14);
40
41    // Define the orbit.
42    // First we need to fetch the Earth J2000 from information from the Almanac.
43    // This allows the frame to include the gravitational parameters and the shape of the Earth,
44    // defined as a tri-axial ellipoid. Note that this shape can be changed manually or in the Almanac
45    // by loading a different set of planetary constants.
46    let earth_j2000 = almanac.frame_from_uid(EARTH_J2000)?;
47
48    // Placing this GEO bird just above Colorado.
49    // In theory, the eccentricity is zero, but in practice, it's about 1e-5 to 1e-6 at best.
50    let orbit = Orbit::try_keplerian(42164.0, 1e-5, 0., 163.0, 75.0, 0.0, epoch, earth_j2000)?;
51    // Print in in Keplerian form.
52    println!("{orbit:x}");
53
54    let state_bf = almanac.transform_to(orbit, IAU_EARTH_FRAME, None)?;
55    let (orig_lat_deg, orig_long_deg, orig_alt_km) = state_bf.latlongalt()?;
56
57    // Nyx is used for high fidelity propagation, not Keplerian propagation as above.
58    // Nyx only propagates Spacecraft at the moment, which allows it to account for acceleration
59    // models such as solar radiation pressure.
60
61    // Let's build a cubesat sized spacecraft, with an SRP area of 10 cm^2 and a mass of 9.6 kg.
62    let sc = Spacecraft::builder()
63        .orbit(orbit)
64        .mass(Mass::from_dry_mass(9.60))
65        .srp(SRPData {
66            area_m2: 10e-4,
67            coeff_reflectivity: 1.1,
68        })
69        .build();
70    println!("{sc:x}");
71
72    // Set up the spacecraft dynamics.
73
74    // Specify that the orbital dynamics must account for the graviational pull of the Moon and the Sun.
75    // The gravity of the Earth will also be accounted for since the spaceraft in an Earth orbit.
76    let mut orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN]);
77
78    // We want to include the spherical harmonics, so let's download the gravitational data from the Nyx Cloud.
79    // We're using the JGM3 model here, which is the default in GMAT.
80    let mut jgm3_meta = MetaFile {
81        uri: "http://public-data.nyxspace.com/nyx/models/JGM3.cof.gz".to_string(),
82        crc32: Some(0xF446F027), // Specifying the CRC32 avoids redownloading it if it's cached.
83    };
84    // And let's download it if we don't have it yet.
85    jgm3_meta.process(true)?;
86
87    // Build the spherical harmonics.
88    // The harmonics must be computed in the body fixed frame.
89    // We're using the long term prediction of the Earth centered Earth fixed frame, IAU Earth.
90    let harmonics_21x21 = Harmonics::from_stor(
91        almanac.frame_from_uid(IAU_EARTH_FRAME)?,
92        HarmonicsMem::from_cof(&jgm3_meta.uri, 21, 21, true).unwrap(),
93    );
94
95    // Include the spherical harmonics into the orbital dynamics.
96    orbital_dyn.accel_models.push(harmonics_21x21);
97
98    // We define the solar radiation pressure, using the default solar flux and accounting only
99    // for the eclipsing caused by the Earth and Moon.
100    let srp_dyn = SolarPressure::new(vec![EARTH_J2000, MOON_J2000], almanac.clone())?;
101
102    // Finalize setting up the dynamics, specifying the force models (orbital_dyn) separately from the
103    // acceleration models (SRP in this case). Use `from_models` to specify multiple accel models.
104    let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn);
105
106    println!("{dynamics}");
107
108    // Finally, let's propagate this orbit to the same epoch as above.
109    // The first returned value is the spacecraft state at the final epoch.
110    // The second value is the full trajectory where the step size is variable step used by the propagator.
111    let (future_sc, trajectory) = Propagator::default(dynamics)
112        .with(sc, almanac.clone())
113        .until_epoch_with_traj(epoch + Unit::Century * 0.03)?;
114
115    println!("=== High fidelity propagation ===");
116    println!(
117        "SMA changed by {:.3} km",
118        orbit.sma_km()? - future_sc.orbit.sma_km()?
119    );
120    println!(
121        "ECC changed by {:.6}",
122        orbit.ecc()? - future_sc.orbit.ecc()?
123    );
124    println!(
125        "INC changed by {:.3e} deg",
126        orbit.inc_deg()? - future_sc.orbit.inc_deg()?
127    );
128    println!(
129        "RAAN changed by {:.3} deg",
130        orbit.raan_deg()? - future_sc.orbit.raan_deg()?
131    );
132    println!(
133        "AOP changed by {:.3} deg",
134        orbit.aop_deg()? - future_sc.orbit.aop_deg()?
135    );
136    println!(
137        "TA changed by {:.3} deg",
138        orbit.ta_deg()? - future_sc.orbit.ta_deg()?
139    );
140
141    // We also have access to the full trajectory throughout the propagation.
142    println!("{trajectory}");
143
144    println!("Spacecraft params after 3 years without active control:\n{future_sc:x}");
145
146    // With the trajectory, let's build a few data products.
147
148    // 1. Export the trajectory as a parquet file, which includes the Keplerian orbital elements.
149
150    let analysis_step = Unit::Minute * 5;
151
152    trajectory.to_parquet(
153        "./03_geo_hf_prop.parquet",
154        Some(vec![
155            &EclipseLocator::cislunar(almanac.clone()).to_penumbra_event()
156        ]),
157        ExportCfg::builder().step(analysis_step).build(),
158        almanac.clone(),
159    )?;
160
161    // 2. Compute the latitude, longitude, and altitude throughout the trajectory by rotating the spacecraft position into the Earth body fixed frame.
162
163    // We iterate over the trajectory, grabbing a state every two minutes.
164    let mut offset_s = vec![];
165    let mut epoch_str = vec![];
166    let mut longitude_deg = vec![];
167    let mut latitude_deg = vec![];
168    let mut altitude_km = vec![];
169
170    for state in trajectory.every(analysis_step) {
171        // Convert the GEO bird state into the body fixed frame, and keep track of its latitude, longitude, and altitude.
172        // These define the GEO stationkeeping box.
173
174        let this_epoch = state.epoch();
175
176        offset_s.push((this_epoch - orbit.epoch).to_seconds());
177        epoch_str.push(this_epoch.to_isoformat());
178
179        let state_bf = almanac.transform_to(state.orbit, IAU_EARTH_FRAME, None)?;
180        let (lat_deg, long_deg, alt_km) = state_bf.latlongalt()?;
181        longitude_deg.push(long_deg);
182        latitude_deg.push(lat_deg);
183        altitude_km.push(alt_km);
184    }
185
186    println!(
187        "Longitude changed by {:.3} deg -- Box is 0.1 deg E-W",
188        orig_long_deg - longitude_deg.last().unwrap()
189    );
190
191    println!(
192        "Latitude changed by {:.3} deg -- Box is 0.05 deg N-S",
193        orig_lat_deg - latitude_deg.last().unwrap()
194    );
195
196    println!(
197        "Altitude changed by {:.3} km -- Box is 30 km",
198        orig_alt_km - altitude_km.last().unwrap()
199    );
200
201    // Build the station keeping data frame.
202    let mut sk_df = df!(
203        "Offset (s)" => offset_s.clone(),
204        "Epoch (UTC)" => epoch_str.clone(),
205        "Longitude E-W (deg)" => longitude_deg,
206        "Latitude N-S (deg)" => latitude_deg,
207        "Altitude (km)" => altitude_km,
208
209    )?;
210
211    // Create a file to write the Parquet to
212    let file = File::create("./03_geo_lla.parquet").expect("Could not create file");
213
214    // Create a ParquetWriter and write the DataFrame to the file
215    ParquetWriter::new(file).finish(&mut sk_df)?;
216
217    Ok(())
218}

Trait Implementations§

Source§

impl Clone for EclipseLocator

Source§

fn clone(&self) -> EclipseLocator

Returns a copy of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Display for EclipseLocator

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dst: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dst. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
§

impl<SS, SP> SupersetOf<SS> for SP
where SS: SubsetOf<SP>,

§

fn to_subset(&self) -> Option<SS>

The inverse inclusion map: attempts to construct self from the equivalent element of its superset. Read more
§

fn is_in_subset(&self) -> bool

Checks if self is actually part of its subset T (and can be converted to it).
§

fn to_subset_unchecked(&self) -> SS

Use with care! Same as self.to_subset but without any property checks. Always succeeds.
§

fn from_subset(element: &SS) -> SP

The inclusion map: converts self to the equivalent element of its superset.
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

impl<T> Allocation for T
where T: RefUnwindSafe + Send + Sync,

§

impl<T> ErasedDestructor for T
where T: 'static,

§

impl<T> MaybeSendSync for T