|
| 1 | +extern crate log; |
| 2 | +extern crate nyx_space as nyx; |
| 3 | +extern crate pretty_env_logger as pel; |
| 4 | + |
| 5 | +use anise::{ |
| 6 | + almanac::metaload::MetaFile, |
| 7 | + constants::{ |
| 8 | + celestial_objects::{MOON, SUN}, |
| 9 | + frames::{EARTH_J2000, IAU_EARTH_FRAME, MOON_J2000}, |
| 10 | + }, |
| 11 | +}; |
| 12 | +use hifitime::{Epoch, Unit}; |
| 13 | +use nyx::{ |
| 14 | + cosmic::{eclipse::EclipseLocator, MetaAlmanac, Orbit, SrpConfig}, |
| 15 | + dynamics::{Harmonics, OrbitalDynamics, SolarPressure, SpacecraftDynamics}, |
| 16 | + io::{gravity::HarmonicsMem, ExportCfg}, |
| 17 | + propagators::Propagator, |
| 18 | + Spacecraft, State, |
| 19 | +}; |
| 20 | +use polars::{df, prelude::ParquetWriter}; |
| 21 | + |
| 22 | +use std::fs::File; |
| 23 | +use std::{error::Error, sync::Arc}; |
| 24 | + |
| 25 | +fn main() -> Result<(), Box<dyn Error>> { |
| 26 | + pel::init(); |
| 27 | + // Dynamics models require planetary constants and ephemerides to be defined. |
| 28 | + // Let's start by grabbing those by using ANISE's latest MetaAlmanac. |
| 29 | + // This will automatically download the DE440s planetary ephemeris, |
| 30 | + // the daily-updated Earth Orientation Parameters, the high fidelity Moon orientation |
| 31 | + // parameters (for the Moon Mean Earth and Moon Principal Axes frames), and the PCK11 |
| 32 | + // planetary constants kernels. |
| 33 | + // For details, refer to https://github.com/nyx-space/anise/blob/master/data/latest.dhall. |
| 34 | + // Note that we place the Almanac into an Arc so we can clone it cheaply and provide read-only |
| 35 | + // references to many functions. |
| 36 | + let almanac = Arc::new(MetaAlmanac::latest().map_err(Box::new)?); |
| 37 | + // Define the orbit epoch |
| 38 | + let epoch = Epoch::from_gregorian_utc_hms(2024, 2, 29, 12, 13, 14); |
| 39 | + |
| 40 | + // Define the orbit. |
| 41 | + // First we need to fetch the Earth J2000 from information from the Almanac. |
| 42 | + // This allows the frame to include the gravitational parameters and the shape of the Earth, |
| 43 | + // defined as a tri-axial ellipoid. Note that this shape can be changed manually or in the Almanac |
| 44 | + // by loading a different set of planetary constants. |
| 45 | + let earth_j2000 = almanac.frame_from_uid(EARTH_J2000)?; |
| 46 | + |
| 47 | + // Placing this GEO bird just above Colorado. |
| 48 | + // In theory, the eccentricity is zero, but in practice, it's about 1e-5 to 1e-6 at best. |
| 49 | + let orbit = Orbit::try_keplerian(42164.0, 1e-5, 0., 163.0, 75.0, 0.0, epoch, earth_j2000)?; |
| 50 | + // Print in in Keplerian form. |
| 51 | + println!("{orbit:x}"); |
| 52 | + |
| 53 | + let state_bf = almanac.transform_to(orbit, IAU_EARTH_FRAME, None)?; |
| 54 | + let (orig_lat_deg, orig_long_deg, orig_alt_km) = state_bf.latlongalt()?; |
| 55 | + |
| 56 | + // Nyx is used for high fidelity propagation, not Keplerian propagation as above. |
| 57 | + // Nyx only propagates Spacecraft at the moment, which allows it to account for acceleration |
| 58 | + // models such as solar radiation pressure. |
| 59 | + |
| 60 | + // Let's build a cubesat sized spacecraft, with an SRP area of 10 cm^2 and a mass of 9.6 kg. |
| 61 | + let sc = Spacecraft::builder() |
| 62 | + .orbit(orbit) |
| 63 | + .dry_mass_kg(9.60) |
| 64 | + .srp(SrpConfig { |
| 65 | + area_m2: 10e-4, |
| 66 | + cr: 1.1, |
| 67 | + }) |
| 68 | + .build(); |
| 69 | + println!("{sc:x}"); |
| 70 | + |
| 71 | + // Set up the spacecraft dynamics. |
| 72 | + |
| 73 | + // Specify that the orbital dynamics must account for the graviational pull of the Moon and the Sun. |
| 74 | + // The gravity of the Earth will also be accounted for since the spaceraft in an Earth orbit. |
| 75 | + let mut orbital_dyn = OrbitalDynamics::point_masses(vec![MOON, SUN]); |
| 76 | + |
| 77 | + // We want to include the spherical harmonics, so let's download the gravitational data from the Nyx Cloud. |
| 78 | + // We're using the JGM3 model here, which is the default in GMAT. |
| 79 | + let mut jgm3_meta = MetaFile { |
| 80 | + uri: "http://public-data.nyxspace.com/nyx/models/JGM3.cof.gz".to_string(), |
| 81 | + crc32: Some(0xF446F027), // Specifying the CRC32 avoids redownloading it if it's cached. |
| 82 | + }; |
| 83 | + // And let's download it if we don't have it yet. |
| 84 | + jgm3_meta.process()?; |
| 85 | + |
| 86 | + // Build the spherical harmonics. |
| 87 | + // The harmonics must be computed in the body fixed frame. |
| 88 | + // We're using the long term prediction of the Earth centered Earth fixed frame, IAU Earth. |
| 89 | + let harmonics_21x21 = Harmonics::from_stor( |
| 90 | + almanac.frame_from_uid(IAU_EARTH_FRAME)?, |
| 91 | + HarmonicsMem::from_cof(&jgm3_meta.uri, 21, 21, true).unwrap(), |
| 92 | + ); |
| 93 | + |
| 94 | + // Include the spherical harmonics into the orbital dynamics. |
| 95 | + orbital_dyn.accel_models.push(harmonics_21x21); |
| 96 | + |
| 97 | + // We define the solar radiation pressure, using the default solar flux and accounting only |
| 98 | + // for the eclipsing caused by the Earth and Moon. |
| 99 | + let srp_dyn = SolarPressure::new(vec![EARTH_J2000, MOON_J2000], almanac.clone())?; |
| 100 | + |
| 101 | + // Finalize setting up the dynamics, specifying the force models (orbital_dyn) separately from the |
| 102 | + // acceleration models (SRP in this case). Use `from_models` to specify multiple accel models. |
| 103 | + let dynamics = SpacecraftDynamics::from_model(orbital_dyn, srp_dyn); |
| 104 | + |
| 105 | + println!("{dynamics}"); |
| 106 | + |
| 107 | + // Finally, let's propagate this orbit to the same epoch as above. |
| 108 | + // The first returned value is the spacecraft state at the final epoch. |
| 109 | + // The second value is the full trajectory where the step size is variable step used by the propagator. |
| 110 | + let (future_sc, trajectory) = Propagator::default(dynamics) |
| 111 | + .with(sc, almanac.clone()) |
| 112 | + .until_epoch_with_traj(epoch + Unit::Century * 0.03)?; |
| 113 | + |
| 114 | + println!("=== High fidelity propagation ==="); |
| 115 | + println!( |
| 116 | + "SMA changed by {:.3} km", |
| 117 | + orbit.sma_km()? - future_sc.orbit.sma_km()? |
| 118 | + ); |
| 119 | + println!( |
| 120 | + "ECC changed by {:.6}", |
| 121 | + orbit.ecc()? - future_sc.orbit.ecc()? |
| 122 | + ); |
| 123 | + println!( |
| 124 | + "INC changed by {:.3e} deg", |
| 125 | + orbit.inc_deg()? - future_sc.orbit.inc_deg()? |
| 126 | + ); |
| 127 | + println!( |
| 128 | + "RAAN changed by {:.3} deg", |
| 129 | + orbit.raan_deg()? - future_sc.orbit.raan_deg()? |
| 130 | + ); |
| 131 | + println!( |
| 132 | + "AOP changed by {:.3} deg", |
| 133 | + orbit.aop_deg()? - future_sc.orbit.aop_deg()? |
| 134 | + ); |
| 135 | + println!( |
| 136 | + "TA changed by {:.3} deg", |
| 137 | + orbit.ta_deg()? - future_sc.orbit.ta_deg()? |
| 138 | + ); |
| 139 | + |
| 140 | + // We also have access to the full trajectory throughout the propagation. |
| 141 | + println!("{trajectory}"); |
| 142 | + |
| 143 | + println!("Spacecraft params after 3 years without active control:\n{future_sc:x}"); |
| 144 | + |
| 145 | + // With the trajectory, let's build a few data products. |
| 146 | + |
| 147 | + // 1. Export the trajectory as a parquet file, which includes the Keplerian orbital elements. |
| 148 | + |
| 149 | + let analysis_step = Unit::Minute * 5; |
| 150 | + |
| 151 | + trajectory.to_parquet( |
| 152 | + "./03_geo_hf_prop.parquet", |
| 153 | + Some(vec![ |
| 154 | + &EclipseLocator::cislunar(almanac.clone()).to_penumbra_event() |
| 155 | + ]), |
| 156 | + ExportCfg::builder().step(analysis_step).build(), |
| 157 | + almanac.clone(), |
| 158 | + )?; |
| 159 | + |
| 160 | + // 2. Compute the latitude, longitude, and altitude throughout the trajectory by rotating the spacecraft position into the Earth body fixed frame. |
| 161 | + |
| 162 | + // We iterate over the trajectory, grabbing a state every two minutes. |
| 163 | + let mut offset_s = vec![]; |
| 164 | + let mut epoch_str = vec![]; |
| 165 | + let mut longitude_deg = vec![]; |
| 166 | + let mut latitude_deg = vec![]; |
| 167 | + let mut altitude_km = vec![]; |
| 168 | + |
| 169 | + for state in trajectory.every(analysis_step) { |
| 170 | + // Convert the GEO bird state into the body fixed frame, and keep track of its latitude, longitude, and altitude. |
| 171 | + // These define the GEO stationkeeping box. |
| 172 | + |
| 173 | + let this_epoch = state.epoch(); |
| 174 | + |
| 175 | + offset_s.push((this_epoch - orbit.epoch).to_seconds()); |
| 176 | + epoch_str.push(this_epoch.to_isoformat()); |
| 177 | + |
| 178 | + let state_bf = almanac.transform_to(state.orbit, IAU_EARTH_FRAME, None)?; |
| 179 | + let (lat_deg, long_deg, alt_km) = state_bf.latlongalt()?; |
| 180 | + longitude_deg.push(long_deg); |
| 181 | + latitude_deg.push(lat_deg); |
| 182 | + altitude_km.push(alt_km); |
| 183 | + } |
| 184 | + |
| 185 | + println!( |
| 186 | + "Longitude changed by {:.3} deg -- Box is 0.1 deg E-W", |
| 187 | + orig_long_deg - longitude_deg.last().unwrap() |
| 188 | + ); |
| 189 | + |
| 190 | + println!( |
| 191 | + "Latitude changed by {:.3} deg -- Box is 0.05 deg N-S", |
| 192 | + orig_lat_deg - latitude_deg.last().unwrap() |
| 193 | + ); |
| 194 | + |
| 195 | + println!( |
| 196 | + "Altitude changed by {:.3} km -- Box is 30 km", |
| 197 | + orig_alt_km - altitude_km.last().unwrap() |
| 198 | + ); |
| 199 | + |
| 200 | + // Build the station keeping data frame. |
| 201 | + let mut sk_df = df!( |
| 202 | + "Offset (s)" => offset_s.clone(), |
| 203 | + "Epoch (UTC)" => epoch_str.clone(), |
| 204 | + "Longitude E-W (deg)" => longitude_deg, |
| 205 | + "Latitude N-S (deg)" => latitude_deg, |
| 206 | + "Altitude (km)" => altitude_km, |
| 207 | + |
| 208 | + )?; |
| 209 | + |
| 210 | + // Create a file to write the Parquet to |
| 211 | + let file = File::create("./03_geo_lla.parquet").expect("Could not create file"); |
| 212 | + |
| 213 | + // Create a ParquetWriter and write the DataFrame to the file |
| 214 | + ParquetWriter::new(file).finish(&mut sk_df)?; |
| 215 | + |
| 216 | + Ok(()) |
| 217 | +} |
0 commit comments