Skip to main content

03_geo_drift/
drift.rs

1#![doc = include_str!("./README.md")]
2extern crate log;
3extern crate nyx_space as nyx;
4extern crate pretty_env_logger as pel;
5
6use anise::{
7    almanac::metaload::MetaFile,
8    constants::{
9        celestial_objects::{MOON, SUN},
10        frames::{EARTH_J2000, IAU_EARTH_FRAME, MOON_J2000},
11    },
12};
13use hifitime::{Epoch, Unit};
14use nyx::{
15    Spacecraft, State,
16    cosmic::{Mass, MetaAlmanac, Orbit, SRPData},
17    dynamics::{GravityField, OrbitalDynamics, SolarPressure, SpacecraftDynamics},
18    io::{ExportCfg, gravity::GravityFieldData},
19    propagators::Propagator,
20};
21use polars::{df, prelude::ParquetWriter};
22
23use std::fs::File;
24use std::{error::Error, sync::Arc};
25
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_info(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 = GravityField::new(
91        GravityFieldData::from_cof(
92            &jgm3_meta.uri,
93            21,
94            21,
95            true,
96            almanac.frame_info(IAU_EARTH_FRAME)?,
97        )
98        .unwrap(),
99    );
100
101    // Include the spherical harmonics into the orbital dynamics.
102    orbital_dyn.accel_models.push(harmonics_21x21);
103
104    // We define the solar radiation pressure, using the default solar flux and accounting only
105    // for the eclipsing caused by the Earth and Moon.
106    let srp_dyn = SolarPressure::new(vec![EARTH_J2000, MOON_J2000], &almanac)?;
107
108    // Finalize setting up the dynamics, specifying the force models (orbital_dyn) separately from the
109    // acceleration models (SRP in this case). Use `from_models` to specify multiple accel models.
110    let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn);
111
112    println!("{dynamics}");
113
114    // Finally, let's propagate this orbit to the same epoch as above.
115    // The first returned value is the spacecraft state at the final epoch.
116    // The second value is the full trajectory where the step size is variable step used by the propagator.
117    let (future_sc, trajectory) = Propagator::default(dynamics)
118        .with(sc, almanac.clone())
119        .until_epoch_with_traj(epoch + Unit::Century * 0.03)?;
120
121    println!("=== High fidelity propagation ===");
122    println!(
123        "SMA changed by {:.3} km",
124        orbit.sma_km()? - future_sc.orbit.sma_km()?
125    );
126    println!(
127        "ECC changed by {:.6}",
128        orbit.ecc()? - future_sc.orbit.ecc()?
129    );
130    println!(
131        "INC changed by {:.3e} deg",
132        orbit.inc_deg()? - future_sc.orbit.inc_deg()?
133    );
134    println!(
135        "RAAN changed by {:.3} deg",
136        orbit.raan_deg()? - future_sc.orbit.raan_deg()?
137    );
138    println!(
139        "AOP changed by {:.3} deg",
140        orbit.aop_deg()? - future_sc.orbit.aop_deg()?
141    );
142    println!(
143        "TA changed by {:.3} deg",
144        orbit.ta_deg()? - future_sc.orbit.ta_deg()?
145    );
146
147    // We also have access to the full trajectory throughout the propagation.
148    println!("{trajectory}");
149
150    println!("Spacecraft params after 3 years without active control:\n{future_sc:x}");
151
152    // With the trajectory, let's build a few data products.
153
154    // 1. Export the trajectory as a parquet file, which includes the Keplerian orbital elements.
155
156    let analysis_step = Unit::Minute * 5;
157
158    trajectory.to_parquet(
159        "./03_geo_hf_prop.parquet",
160        ExportCfg::builder().step(analysis_step).build(),
161    )?;
162
163    // 2. Compute the latitude, longitude, and altitude throughout the trajectory by rotating the spacecraft position into the Earth body fixed frame.
164
165    // We iterate over the trajectory, grabbing a state every two minutes.
166    let mut offset_s = vec![];
167    let mut epoch_str = vec![];
168    let mut longitude_deg = vec![];
169    let mut latitude_deg = vec![];
170    let mut altitude_km = vec![];
171
172    for state in trajectory.every(analysis_step) {
173        // Convert the GEO bird state into the body fixed frame, and keep track of its latitude, longitude, and altitude.
174        // These define the GEO stationkeeping box.
175
176        let this_epoch = state.epoch();
177
178        offset_s.push((this_epoch - orbit.epoch).to_seconds());
179        epoch_str.push(this_epoch.to_isoformat());
180
181        let state_bf = almanac.transform_to(state.orbit, IAU_EARTH_FRAME, None)?;
182        let (lat_deg, long_deg, alt_km) = state_bf.latlongalt()?;
183        longitude_deg.push(long_deg);
184        latitude_deg.push(lat_deg);
185        altitude_km.push(alt_km);
186    }
187
188    println!(
189        "Longitude changed by {:.3} deg -- Box is 0.1 deg E-W",
190        orig_long_deg - longitude_deg.last().unwrap()
191    );
192
193    println!(
194        "Latitude changed by {:.3} deg -- Box is 0.05 deg N-S",
195        orig_lat_deg - latitude_deg.last().unwrap()
196    );
197
198    println!(
199        "Altitude changed by {:.3} km -- Box is 30 km",
200        orig_alt_km - altitude_km.last().unwrap()
201    );
202
203    // Build the station keeping data frame.
204    let mut sk_df = df!(
205        "Offset (s)" => offset_s.clone(),
206        "Epoch (UTC)" => epoch_str.clone(),
207        "Longitude E-W (deg)" => longitude_deg,
208        "Latitude N-S (deg)" => latitude_deg,
209        "Altitude (km)" => altitude_km,
210
211    )?;
212
213    // Create a file to write the Parquet to
214    let file = File::create("./03_geo_lla.parquet").expect("Could not create file");
215
216    // Create a ParquetWriter and write the DataFrame to the file
217    ParquetWriter::new(file).finish(&mut sk_df)?;
218
219    Ok(())
220}