From 9bb725b9c49202f0fd0017cc9289146a09aa0e3a Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 23 Jun 2023 22:57:46 -0600 Subject: [PATCH 01/16] Add mean to true anomaly computation and initialization --- src/cosmic/orbit.rs | 201 ++++++++++++++++++++++++++++++++++++++++++ tests/cosmic/orbit.rs | 21 ++++- 2 files changed, 221 insertions(+), 1 deletion(-) diff --git a/src/cosmic/orbit.rs b/src/cosmic/orbit.rs index 55901faa..e6b86121 100644 --- a/src/cosmic/orbit.rs +++ b/src/cosmic/orbit.rs @@ -37,6 +37,7 @@ use crate::NyxError; use approx::{abs_diff_eq, relative_eq}; use serde::{Deserialize, Serialize}; use std::f64::consts::PI; +use std::f64::consts::TAU; use std::f64::EPSILON; use std::fmt; use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; @@ -55,6 +56,7 @@ use std::collections::HashMap; /// If an orbit has an eccentricity below the following value, it is considered circular (only affects warning messages) pub const ECC_EPSILON: f64 = 1e-11; +pub const MA_EPSILON: f64 = 1e-16; pub fn assert_orbit_eq_or_abs(left: &Orbit, right: &Orbit, epsilon: f64, msg: &str) { if !(left.to_cartesian_vec() == right.to_cartesian_vec()) @@ -380,6 +382,37 @@ impl Orbit { ) } + /// Initializes a new orbit from the Keplerian orbital elements using the mean anomaly instead of the true anomaly. + /// + /// # Implementation notes + /// This function starts by converting the mean anomaly to true anomaly, and then it initializes the orbit + /// using the keplerian(..) method. + /// The conversion is from GMAT's MeanToTrueAnomaly function, transliterated originally by Claude and GPT4 with human adjustments. + pub fn keplerian_mean_anomaly( + sma_km: f64, + ecc: f64, + inc_deg: f64, + raan_deg: f64, + aop_deg: f64, + ma_deg: f64, + epoch: Epoch, + frame: Frame, + ) -> Result { + // Start by computing the true anomaly + let ta_rad = compute_mean_to_true_anomaly(ma_deg.to_radians(), ecc, MA_EPSILON)?; + + Ok(Self::keplerian( + sma_km, + ecc, + inc_deg, + raan_deg, + aop_deg, + ta_rad.to_degrees(), + epoch, + frame, + )) + } + /// Creates a new Orbit from the geodetic latitude (φ), longitude (λ) and height with respect to the ellipsoid of the frame. /// /// **Units:** degrees, degrees, km @@ -2200,6 +2233,174 @@ impl Configurable for Orbit { } } +/// Computes the true anomaly from the given mean anomaly for an orbit. +/// +/// The computation process varies depending on whether the orbit is elliptical (eccentricity less than or equal to 1) +/// or hyperbolic (eccentricity greater than 1). In each case, the method uses an iterative algorithm to find a +/// sufficiently accurate approximation of the true anomaly. +/// +/// # Arguments +/// +/// * `ma_radians` - The mean anomaly in radians. +/// * `ecc` - The eccentricity of the orbit. +/// * `tol` - The tolerance for accuracy in the resulting true anomaly. +/// +/// # Returns +/// +/// * Ok(f64) - The calculated true anomaly in radians, if the computation was successful. +/// * Err(NyxError) - An error that occurred during the computation. Possible errors include: +/// - MaxIterReached: The iterative process did not converge within 1000 iterations. +/// - MathDomain: A mathematical error occurred during computation, such as division by zero. +/// +/// # Remarks +/// +/// This function uses GTDS MathSpec Equations 3-180, 3-181, and 3-186 for the iterative computation process. +/// +/// If a numerical error occurs during computation, the function may return a MathDomain error. In the case of a +/// non-converging iterative process, the function will return a MaxIterReached error after 1000 iterations. +/// +/// # Examples +/// +/// ``` +/// let ma_radians = 0.5; +/// let ecc = 0.1; +/// let tol = 1e-6; +/// let result = compute_mean_to_true_anomaly(ma_radians, ecc, tol); +/// ``` +fn compute_mean_to_true_anomaly(ma_radians: f64, ecc: f64, tol: f64) -> Result { + let rm = ma_radians; + if ecc <= 1.0 { + // Elliptical orbit + let mut e2 = rm + ecc * rm.sin(); // GTDS MathSpec Equation 3-182 + + let mut iter = 0; + + loop { + iter += 1; + if iter > 1000 { + return Err(NyxError::MaxIterReached(format!("{iter}"))); + } + + // GTDS MathSpec Equation 3-180 Note: a little difference here is that it uses Cos(E) instead of Cos(E-0.5*f) + let normalized_anomaly = 1.0 - ecc * e2.cos(); + + if normalized_anomaly.abs() < MA_EPSILON { + return Err(NyxError::MathDomain(format!( + "normalizer too small {normalized_anomaly}" + ))); + } + + // GTDS MathSpec Equation 3-181 + let e1 = e2 - (e2 - ecc * e2.sin() - rm) / normalized_anomaly; + + if (e2 - e1).abs() < tol { + break; + } + + e2 = e1; + } + + let mut e = e2; + + if e < 0.0 { + e += TAU; + } + + let c = (e - PI).abs(); + + let mut ta = if c >= 1.0e-08 { + let normalized_anomaly = 1.0 - ecc; + + if (normalized_anomaly).abs() < MA_EPSILON { + return Err(NyxError::MathDomain(format!( + "normalized anomaly too small {normalized_anomaly}" + ))); + } + + let eccentricity_ratio = (1.0 + ecc) / normalized_anomaly; // temp2 = (1+ecc)/(1-ecc) + + if eccentricity_ratio < 0.0 { + return Err(NyxError::MathDomain(format!( + "eccentric ratio too small {eccentricity_ratio}" + ))); + } + + let f = eccentricity_ratio.sqrt(); + let g = (e / 2.0).tan(); + // tan(TA/2) = Sqrt[(1+ecc)/(1-ecc)] * tan(E/2) + 2.0 * (f * g).atan() + } else { + e + }; + + if ta < 0.0 { + ta += TAU; + } + Ok(ta) + } else { + //--------------------------------------------------------- + // hyperbolic orbit + //--------------------------------------------------------- + + // For hyperbolic orbit, anomaly is nolonger to be an angle so we cannot use mod of 2*PI to mean anomaly. + // We need to keep its original value for calculation. + //if (rm > PI) // incorrect + // rm = rm - TWO_PI; // incorrect + + //f2 = ecc * Sinh(rm) - rm; // incorrect + //f2 = rm / 2; // incorrect // GTDS MathSpec Equation 3-186 + let mut f2: f64 = 0.0; // This is the correct initial value for hyperbolic eccentric anomaly. + let mut iter = 0; + + loop { + iter += 1; + if iter > 1000 { + return Err(NyxError::MaxIterReached(format!("{iter}"))); + } + + let normalizer = ecc * f2.cosh() - 1.0; + + if normalizer.abs() < MA_EPSILON { + return Err(NyxError::MathDomain(format!( + "normalizer too small {normalizer}" + ))); + } + + let f1 = f2 - (ecc * f2.sinh() - f2 - rm) / normalizer; // GTDS MathSpec Equation 3-186 + if (f2 - f1).abs() < tol { + break; + } + f2 = f1; + } + + let f = f2; + let normalized_anomaly = ecc - 1.0; + + if normalized_anomaly.abs() < MA_EPSILON { + return Err(NyxError::MathDomain(format!( + "eccentric ratio too small {normalized_anomaly}" + ))); + } + + let eccentricity_ratio = (ecc + 1.0) / normalized_anomaly; // temp2 = (ecc+1)/(ecc-1) + + if eccentricity_ratio < 0.0 { + return Err(NyxError::MathDomain(format!( + "eccentric ratio too small {eccentricity_ratio}" + ))); + } + + let e = eccentricity_ratio.sqrt(); + let g = (f / 2.0).tanh(); + let mut ta = 2.0 * (e * g).atan(); // tan(TA/2) = Sqrt[(ecc+1)/(ecc-1)] * Tanh(F/2) where: F is hyperbolic centric anomaly + + if ta < 0.0 { + ta += TAU; + } + Ok(ta) + } +} + #[test] fn test_serde() { use super::Cosm; diff --git a/tests/cosmic/orbit.rs b/tests/cosmic/orbit.rs index 7b6fc6da..7f1b2c9d 100644 --- a/tests/cosmic/orbit.rs +++ b/tests/cosmic/orbit.rs @@ -4,6 +4,7 @@ extern crate pretty_env_logger as pel; use approx::relative_eq; use nyx::cosmic::{Cosm, Frame, Orbit}; use nyx::time::{Epoch, Unit}; +use nyx::utils::rss_orbit_errors; macro_rules! f64_eq { ($x:expr, $val:expr, $msg:expr) => { @@ -100,7 +101,14 @@ fn state_def_circ_inc() { "semi parameter" ); - let kep = Orbit::keplerian(8_191.93, 0.2, 12.85, 306.614, 314.19, -99.887_7, dt, eme2k); + let sma_km = 8_191.93; + let ecc = 0.2; + let inc_deg = 12.85; + let raan_deg = 306.614; + let aop_deg = 314.19; + let ta_deg = -99.887_7; + + let kep = Orbit::keplerian(sma_km, ecc, inc_deg, raan_deg, aop_deg, ta_deg, dt, eme2k); f64_eq!(kep.ta_deg(), 260.1123, "ta"); // Test that DCMs are valid @@ -115,6 +123,17 @@ fn state_def_circ_inc() { let dcm = kep.dcm_from_traj_frame(Frame::RIC).unwrap(); assert!(((dcm * dcm.transpose()).determinant() - 1.0).abs() < 1e-12); assert!(((dcm.transpose() * dcm).determinant() - 1.0).abs() < 1e-12); + + // Test initialization from mean anomaly + let ma_deg = kep.ma_deg(); + let kep_from_ma = + Orbit::keplerian_mean_anomaly(sma_km, ecc, inc_deg, raan_deg, aop_deg, ma_deg, dt, eme2k) + .unwrap(); + + let (pos_rss_km, vel_rss_km_s) = rss_orbit_errors(&kep_from_ma, &kep); + + assert!(dbg!(pos_rss_km) < 1e-10); + assert!(dbg!(vel_rss_km_s) < 1e-14); } #[test] From d95ed18bd6472257738cac4526383cc857e31864 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Fri, 23 Jun 2023 23:42:26 -0600 Subject: [PATCH 02/16] Trying to add this to Python --- src/cosmic/orbit.rs | 47 ++++++++++++++++++++ src/python/mission_design/mod.rs | 54 +++++++++++++++++++++++ tests/orbit_determination/resid_reject.rs | 6 +-- tests/orbit_determination/robust.rs | 4 +- tests/orbit_determination/spacecraft.rs | 2 +- tests/orbit_determination/two_body.rs | 2 +- tests/python/test_mission_design.py | 47 +++++++++++++++++++- 7 files changed, 154 insertions(+), 8 deletions(-) diff --git a/src/cosmic/orbit.rs b/src/cosmic/orbit.rs index e6b86121..6582abce 100644 --- a/src/cosmic/orbit.rs +++ b/src/cosmic/orbit.rs @@ -1556,6 +1556,28 @@ impl Orbit { } } + /// Adjusts the true anomaly of this orbit using the mean anomaly. + /// + /// # Astrodynamics note + /// This is akin to a two body propagation. + pub fn orbit_at_epoch(&self, new_epoch: Epoch) -> Result { + let m0_rad = self.ma_deg().to_radians(); + let mt_rad = m0_rad + + (self.frame.gm() / self.sma_km().powi(3)).sqrt() + * (new_epoch - self.epoch).to_seconds(); + + Self::keplerian_mean_anomaly( + self.sma_km(), + self.ecc(), + self.inc_deg(), + self.raan_deg(), + self.aop_deg(), + mt_rad.to_degrees(), + new_epoch, + self.frame, + ) + } + /// Prints this orbit in Cartesian form #[cfg(feature = "python")] fn __repr__(&self) -> String { @@ -1675,6 +1697,31 @@ impl Orbit { ) } + #[cfg(feature = "python")] + #[classmethod] + fn from_keplerian_mean_anomaly( + _cls: &PyType, + sma_km: f64, + ecc: f64, + inc_deg: f64, + raan_deg: f64, + aop_deg: f64, + ma_deg: f64, + epoch: Epoch, + frame: PyRef, + ) -> Result { + Self::keplerian_mean_anomaly( + sma_km, + ecc, + inc_deg, + raan_deg, + aop_deg, + ma_deg, + epoch, + frame.inner, + ) + } + /// Creates a new Orbit from the provided semi-major axis altitude in kilometers #[cfg(feature = "python")] #[classmethod] diff --git a/src/python/mission_design/mod.rs b/src/python/mission_design/mod.rs index 5e4c2cc9..1654c509 100644 --- a/src/python/mission_design/mod.rs +++ b/src/python/mission_design/mod.rs @@ -48,6 +48,7 @@ pub(crate) fn register_md(py: Python<'_>, parent_module: &PyModule) -> PyResult< sm.add_class::()?; sm.add_class::()?; sm.add_function(wrap_pyfunction!(propagate, sm)?)?; + sm.add_function(wrap_pyfunction!(two_body, sm)?)?; py_run!( py, @@ -153,3 +154,56 @@ fn propagate( ))) } } + +#[pyfunction] +fn two_body( + orbits: Vec, + new_epochs: Option>, + durations: Option>, +) -> Vec { + let epochs: Vec = if (new_orbits.is_some() && durations.is_some()) + || (new_orbit.is_none() && durations.is_none()) + { + return Err(NyxError::ConfigError(ConfigError::InvalidConfig( + "Either duration or epoch must be provided for a propagation to happen, but not both" + .to_string(), + ))); + } else if let Some(new_epochs) = new_epochs { + if new_epochs.len() == 1 { + vec![new_epochs[0]; orbits.len()] + } else if new_epochs.len() == orbits.len() { + new_epochs + } else { + return Err(NyxError::ConfigError(ConfigError::InvalidConfig( + "New epochs must be either of the same size as the orbits vector or contain one item exactly" + .to_string(), + ))); + }; + } else if let Some(durations) = durations { + if durations.len() == 1 { + orbits.iter().map(|orbit| orbit + durations[0]).collect() + } else if durations.len() == orbits.len() { + durations + } else { + return; + Err(NyxError::ConfigError(ConfigError::InvalidConfig( + "Durations must be either of the same size as the orbits vector or contain one item exactly" + .to_string(), + ))) + }; + }; + + orbits + .par_iter() + .zip(epochsepochs.par_iter()) + .map(|(orbit, &epoch)| orbit.orbit_at_epoch(epoch)) + .collect::>>() + .into_iter() + .map(|result| match result { + Ok(orbit) => orbit, + Err(e) => { + eprintln!("Error: {:?}", error); + } + }) + .collect::>() +} diff --git a/tests/orbit_determination/resid_reject.rs b/tests/orbit_determination/resid_reject.rs index da1874a4..aa2c9a4c 100644 --- a/tests/orbit_determination/resid_reject.rs +++ b/tests/orbit_determination/resid_reject.rs @@ -40,7 +40,7 @@ fn traj(epoch: Epoch) -> Traj { Bodies::JupiterBarycenter, Bodies::SaturnBarycenter, ]; - let orbital_dyn = OrbitalDynamics::point_masses(&bodies, cosm.clone()); + let orbital_dyn = OrbitalDynamics::point_masses(&bodies, cosm); let truth_setup = Propagator::dp78(orbital_dyn, PropOpts::with_max_step(step_size)); let (_, traj) = truth_setup .with(initial_state) @@ -192,7 +192,7 @@ fn od_resid_reject_all_ckf_two_way( min_accepted: 0, // Start the preprocessing filter at the first measurement because we try to filter everything out in this test num_sigmas: 3.0, }), - cosm.clone(), + cosm, ); // TODO: Fix the deserialization of the measurements such that they also deserialize the integration time. @@ -257,7 +257,7 @@ fn od_resid_reject_default_ckf_two_way( let kf = KF::new(initial_estimate, process_noise, measurement_noise); - let mut odp = ODProcess::ckf(prop_est, kf, Some(FltResid::default()), cosm.clone()); + let mut odp = ODProcess::ckf(prop_est, kf, Some(FltResid::default()), cosm); // TODO: Fix the deserialization of the measurements such that they also deserialize the integration time. // Without it, we're stuck having to rebuild them from scratch. diff --git a/tests/orbit_determination/robust.rs b/tests/orbit_determination/robust.rs index fd684348..55cb38be 100644 --- a/tests/orbit_determination/robust.rs +++ b/tests/orbit_determination/robust.rs @@ -149,7 +149,7 @@ fn od_robust_test_ekf_realistic_one_way() { let trig = EkfTrigger::new(ekf_num_meas, ekf_disable_time); - let mut odp = ODProcess::ekf(prop_est, kf, trig, None, cosm.clone()); + let mut odp = ODProcess::ekf(prop_est, kf, trig, None, cosm); // Let's filter and iterate on the initial subset of the arc to refine the initial estimate let subset = arc.filter_by_offset(..3.hours()); @@ -368,7 +368,7 @@ fn od_robust_test_ekf_realistic_two_way() { let trig = EkfTrigger::new(ekf_num_meas, ekf_disable_time); - let mut odp = ODProcess::ekf(prop_est, kf, trig, None, cosm.clone()); + let mut odp = ODProcess::ekf(prop_est, kf, trig, None, cosm); // TODO: Fix the deserialization of the measurements such that they also deserialize the integration time. // Without it, we're stuck having to rebuild them from scratch. diff --git a/tests/orbit_determination/spacecraft.rs b/tests/orbit_determination/spacecraft.rs index 434329c0..8e2843a1 100644 --- a/tests/orbit_determination/spacecraft.rs +++ b/tests/orbit_determination/spacecraft.rs @@ -167,7 +167,7 @@ fn od_val_sc_mb_srp_reals_duals_models() { let ckf = KF::no_snc(initial_estimate, measurement_noise); - let mut odp = ODProcess::ckf(prop_est, ckf, None, cosm.clone()); + let mut odp = ODProcess::ckf(prop_est, ckf, None, cosm); odp.process_arc::(&arc).unwrap(); diff --git a/tests/orbit_determination/two_body.rs b/tests/orbit_determination/two_body.rs index be254636..33dde995 100644 --- a/tests/orbit_determination/two_body.rs +++ b/tests/orbit_determination/two_body.rs @@ -441,7 +441,7 @@ fn od_tb_val_ckf_fixed_step_perfect_stations() { let ckf = KF::no_snc(initial_estimate, measurement_noise); - let mut odp = ODProcess::ckf(prop_est, ckf, None, cosm.clone()); + let mut odp = ODProcess::ckf(prop_est, ckf, None, cosm); odp.process_arc::(&arc).unwrap(); diff --git a/tests/python/test_mission_design.py b/tests/python/test_mission_design.py index 57482e8c..18c0be51 100644 --- a/tests/python/test_mission_design.py +++ b/tests/python/test_mission_design.py @@ -5,12 +5,14 @@ from nyx_space.cosmic import Spacecraft, Orbit, SrpConfig, Cosm from nyx_space.mission_design import ( propagate, + two_body, StateParameter, Event, SpacecraftDynamics, TrajectoryLoader, ) from nyx_space.time import Duration, Unit, Epoch, TimeSeries +from nyx_space.monte_carlo import generate_orbits, StateParameter def test_propagate(): @@ -167,7 +169,50 @@ def test_build_spacecraft(): assert dc_orbit.__eq__(sc_orbit) +def test_two_body(): + # Build a demo orbit + cosm = Cosm.de438() + eme2k = cosm.frame("EME2000") + + assert eme2k.gm() == 398600.435392 # SPICE data, not GMAT data (398600.4415) + assert eme2k.is_geoid() + assert eme2k.equatorial_radius() == 6378.1363 + + e = Epoch.system_now() + + orbit = Orbit.from_keplerian_altitude( + 400, + ecc=1e-4, + inc_deg=30.5, + raan_deg=35.0, + aop_deg=65.0, + ta_deg=590, + epoch=e, + frame=eme2k, + ) + + orbits = generate_orbits( + orbit, + [ + (StateParameter("SMA"), 0.05), + (StateParameter.Eccentricity, 0.1), + (StateParameter.Inclination, 0.1), + ], + 100, + kind="prct", + ) + + # And propagate in parallel using a single duration + proped_orbits = two_body(orbits, durations=[Unit.Day*531.5]) + assert len(proped_orbits) == len(orbits) + + # And propagate in parallel using many epochs + ts = TimeSeries(e, e + Unit.Day * 100, step=Unit.Day*1, inclusive=True) + epochs = [e for e in ts][:100] + proped_orbits = two_body(orbits, new_epochs=epochs) + assert len(proped_orbits) == len(orbits) if __name__ == "__main__": # test_propagate() - test_build_spacecraft() + # test_build_spacecraft() + test_two_body() From 6f2612bb2c49c95b122a938ee73ffd3904c351ea Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 24 Jun 2023 08:28:37 -0600 Subject: [PATCH 03/16] Fix two body call from Python --- src/cosmic/orbit.rs | 2 +- src/python/mission_design/mod.rs | 73 +++++++++++++++++------------ tests/python/test_mission_design.py | 10 ++-- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/src/cosmic/orbit.rs b/src/cosmic/orbit.rs index 6582abce..cbd22383 100644 --- a/src/cosmic/orbit.rs +++ b/src/cosmic/orbit.rs @@ -1560,7 +1560,7 @@ impl Orbit { /// /// # Astrodynamics note /// This is akin to a two body propagation. - pub fn orbit_at_epoch(&self, new_epoch: Epoch) -> Result { + pub fn at_epoch(&self, new_epoch: Epoch) -> Result { let m0_rad = self.ma_deg().to_radians(); let mt_rad = m0_rad + (self.frame.gm() / self.sma_km().powi(3)).sqrt() diff --git a/src/python/mission_design/mod.rs b/src/python/mission_design/mod.rs index 1654c509..4c6bf900 100644 --- a/src/python/mission_design/mod.rs +++ b/src/python/mission_design/mod.rs @@ -20,13 +20,13 @@ use crate::io::trajectory_data::TrajectoryLoader; use crate::io::{ConfigError, ExportCfg}; use crate::md::prelude::{PropOpts, Propagator, SpacecraftDynamics}; use crate::md::{Event, StateParameter}; - use crate::propagators::{ CashKarp45, Dormand45, Dormand78, Fehlberg45, RK2Fixed, RK4Fixed, Verner56, }; -use crate::{NyxError, Spacecraft}; +use crate::{NyxError, Orbit, Spacecraft}; use hifitime::{Duration, Epoch, Unit}; use pyo3::{prelude::*, py_run}; +use rayon::prelude::*; pub(crate) use self::orbit_trajectory::OrbitTraj; pub(crate) use self::sc_trajectory::SpacecraftTraj; @@ -160,50 +160,63 @@ fn two_body( orbits: Vec, new_epochs: Option>, durations: Option>, -) -> Vec { - let epochs: Vec = if (new_orbits.is_some() && durations.is_some()) - || (new_orbit.is_none() && durations.is_none()) +) -> Result, NyxError> { + let rslt_epochs: Result, NyxError> = if (new_epochs.is_some() && durations.is_some()) + || (new_epochs.is_none() && durations.is_none()) { - return Err(NyxError::ConfigError(ConfigError::InvalidConfig( + Err(NyxError::ConfigError(ConfigError::InvalidConfig( "Either duration or epoch must be provided for a propagation to happen, but not both" .to_string(), - ))); + ))) } else if let Some(new_epochs) = new_epochs { if new_epochs.len() == 1 { - vec![new_epochs[0]; orbits.len()] + Ok(vec![new_epochs[0]; orbits.len()]) } else if new_epochs.len() == orbits.len() { - new_epochs + Ok(new_epochs) } else { - return Err(NyxError::ConfigError(ConfigError::InvalidConfig( - "New epochs must be either of the same size as the orbits vector or contain one item exactly" - .to_string(), - ))); - }; + Err(NyxError::ConfigError(ConfigError::InvalidConfig(format!( + "Expecting either one or {} items in epochs vector", + orbits.len() + )))) + } } else if let Some(durations) = durations { if durations.len() == 1 { - orbits.iter().map(|orbit| orbit + durations[0]).collect() + Ok(orbits + .iter() + .map(|orbit| orbit.epoch + durations[0]) + .collect()) } else if durations.len() == orbits.len() { - durations + Ok(durations + .iter() + .zip(&orbits) + .map(|(duration, orbit)| orbit.epoch + *duration) + .collect()) } else { - return; - Err(NyxError::ConfigError(ConfigError::InvalidConfig( - "Durations must be either of the same size as the orbits vector or contain one item exactly" - .to_string(), - ))) - }; + Err(NyxError::ConfigError(ConfigError::InvalidConfig(format!( + "Expecting either one or {} items in durations vector", + orbits.len() + )))) + } + } else { + Err(NyxError::CustomError( + "you have entered unreachable code, please report a bug".to_string(), + )) }; - orbits - .par_iter() - .zip(epochsepochs.par_iter()) - .map(|(orbit, &epoch)| orbit.orbit_at_epoch(epoch)) + let epochs = rslt_epochs?; + + Ok((orbits, epochs) + .into_par_iter() + .map(|(orbit, epoch)| orbit.at_epoch(epoch)) .collect::>>() .into_iter() - .map(|result| match result { - Ok(orbit) => orbit, + .filter(|result| match result { + Ok(_) => true, Err(e) => { - eprintln!("Error: {:?}", error); + println!("Error: {e:?}"); + false } }) - .collect::>() + .map(|s| s.unwrap()) + .collect::>()) } diff --git a/tests/python/test_mission_design.py b/tests/python/test_mission_design.py index 18c0be51..1f7d59dd 100644 --- a/tests/python/test_mission_design.py +++ b/tests/python/test_mission_design.py @@ -1,6 +1,7 @@ import logging from pathlib import Path import pickle +from timeit import timeit from nyx_space.cosmic import Spacecraft, Orbit, SrpConfig, Cosm from nyx_space.mission_design import ( @@ -198,7 +199,7 @@ def test_two_body(): (StateParameter.Eccentricity, 0.1), (StateParameter.Inclination, 0.1), ], - 100, + 1000, kind="prct", ) @@ -207,11 +208,14 @@ def test_two_body(): assert len(proped_orbits) == len(orbits) # And propagate in parallel using many epochs - ts = TimeSeries(e, e + Unit.Day * 100, step=Unit.Day*1, inclusive=True) - epochs = [e for e in ts][:100] + ts = TimeSeries(e, e + Unit.Day * 1000, step=Unit.Day*1, inclusive=False) + epochs = [e for e in ts] proped_orbits = two_body(orbits, new_epochs=epochs) assert len(proped_orbits) == len(orbits) + timing = timeit(lambda: two_body(orbits, new_epochs=epochs), number=1) + print(f"two body propagation of {len(orbits)} orbits in {timing} s") + if __name__ == "__main__": # test_propagate() # test_build_spacecraft() From 1742b1737b5a676c3ae6c4b27569de119ef6bd51 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 24 Jun 2023 09:07:35 -0600 Subject: [PATCH 04/16] Add azimuth computation to ground stations --- src/od/ground_station.rs | 22 ++++++++++++------- .../orbit_determination/ground_station.rs | 9 ++++++++ tests/propagation/events.rs | 2 +- tests/python/test_mission_design.py | 2 -- tests/python/test_orbit_determination.py | 10 +++++++-- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/od/ground_station.rs b/src/od/ground_station.rs index 50b8e39d..c74e5ef4 100644 --- a/src/od/ground_station.rs +++ b/src/od/ground_station.rs @@ -150,9 +150,9 @@ impl GroundStation { } } - /// Computes the elevation of the provided object seen from this ground station. + /// Computes the azimuth and elevation of the provided object seen from this ground station, both in degrees. /// Also returns the ground station's orbit in the frame of the receiver - pub fn elevation_of(&self, rx: Orbit, cosm: &Cosm) -> (f64, Orbit, Orbit) { + pub fn azimuth_elevation_of(&self, rx: Orbit, cosm: &Cosm) -> (f64, f64, Orbit, Orbit) { // Start by converting the receiver spacecraft into the ground station frame. let rx_gs_frame = cosm.frame_chg(&rx, self.frame); @@ -170,10 +170,16 @@ impl GroundStation { let rho_sez = rx_sez - tx_sez; // Finally, compute the elevation (math is the same as declination) - let elevation = rho_sez.declination_deg(); + let elevation_deg = rho_sez.declination_deg(); + let azimuth_deg = (rho_sez.y_km / rho_sez.x_km).tan().to_degrees(); // Return elevation in degrees and rx/tx in the inertial frame of the spacecraft - (elevation, rx, cosm.frame_chg(&tx_gs_frame, rx.frame)) + ( + azimuth_deg, + elevation_deg, + rx, + cosm.frame_chg(&tx_gs_frame, rx.frame), + ) } /// Return this ground station as an orbit in its current frame @@ -267,8 +273,8 @@ impl TrackingDeviceSim for GroundStation { let rx_0 = traj.at(epoch - integration_time)?; let rx_1 = traj.at(epoch)?; - let (elevation_0, rx_0, tx_0) = self.elevation_of(rx_0, &cosm); - let (elevation_1, rx_1, tx_1) = self.elevation_of(rx_1, &cosm); + let (_, elevation_0, rx_0, tx_0) = self.azimuth_elevation_of(rx_0, &cosm); + let (_, elevation_1, rx_1, tx_1) = self.azimuth_elevation_of(rx_1, &cosm); if elevation_0 < self.elevation_mask_deg || elevation_1 < self.elevation_mask_deg { debug!( @@ -308,7 +314,7 @@ impl TrackingDeviceSim for GroundStation { rng: Option<&mut Pcg64Mcg>, cosm: Arc, ) -> Result, NyxError> { - let (elevation, rx, tx) = self.elevation_of(rx, &cosm); + let (_, elevation, rx, tx) = self.azimuth_elevation_of(rx, &cosm); if elevation >= self.elevation_mask_deg { // Only update the noises if the measurement is valid. @@ -359,7 +365,7 @@ impl TrackingDeviceSim for GroundStation { rng: Option<&mut Pcg64Mcg>, cosm: Arc, ) -> Result, NyxError> { - let (elevation, rx, tx) = self.elevation_of(rx.orbit, &cosm); + let (_, elevation, rx, tx) = self.azimuth_elevation_of(rx.orbit, &cosm); if elevation >= self.elevation_mask_deg { // Only update the noises if the measurement is valid. diff --git a/src/python/orbit_determination/ground_station.rs b/src/python/orbit_determination/ground_station.rs index 77acba3f..0481a4b8 100644 --- a/src/python/orbit_determination/ground_station.rs +++ b/src/python/orbit_determination/ground_station.rs @@ -25,6 +25,8 @@ pub use crate::od::simulator::TrkConfig; use crate::NyxError; pub use crate::{io::ConfigError, od::prelude::GroundStation}; +use crate::python::cosmic::Cosm as CosmPy; + use pyo3::prelude::*; use pyo3::types::PyType; @@ -58,6 +60,13 @@ impl GroundStation { } } + /// Computes the azimuth and elevation of the provided object seen from this ground station, both in degrees. + fn compute_azimuth_elevation(&self, receiver: Orbit, cosm: &CosmPy) -> (f64, f64) { + let (az_deg, el_deg, _, _) = self.azimuth_elevation_of(receiver, &cosm.inner); + + (az_deg, el_deg) + } + // Manual getter/setters -- waiting on https://github.com/PyO3/pyo3/pull/2786 #[getter] diff --git a/tests/propagation/events.rs b/tests/propagation/events.rs index 7ef0543c..52fe0e40 100644 --- a/tests/propagation/events.rs +++ b/tests/propagation/events.rs @@ -69,7 +69,7 @@ fn event_tracker_true_anomaly() { } // Compute the elevation - let (elevation, _, _) = gc.elevation_of(state, &cosm); + let (elevation, _, _, _) = gc.azimuth_elevation_of(state, &cosm); if elevation > max_el { max_el = elevation; max_dt = state.epoch(); diff --git a/tests/python/test_mission_design.py b/tests/python/test_mission_design.py index 1f7d59dd..22dbacb0 100644 --- a/tests/python/test_mission_design.py +++ b/tests/python/test_mission_design.py @@ -217,6 +217,4 @@ def test_two_body(): print(f"two body propagation of {len(orbits)} orbits in {timing} s") if __name__ == "__main__": - # test_propagate() - # test_build_spacecraft() test_two_body() diff --git a/tests/python/test_orbit_determination.py b/tests/python/test_orbit_determination.py index b922f72d..1f453cfd 100644 --- a/tests/python/test_orbit_determination.py +++ b/tests/python/test_orbit_determination.py @@ -15,7 +15,7 @@ ExportCfg, ) from nyx_space.mission_design import TrajectoryLoader, SpacecraftDynamics, propagate -from nyx_space.cosmic import Spacecraft +from nyx_space.cosmic import Spacecraft, Cosm from nyx_space.time import Unit from nyx_space.plots.od import ( plot_covar, @@ -203,6 +203,12 @@ def test_one_way_msr(): assert abs(range_km - 18097.562811514355) < 0.1 assert abs(doppler_km_s - -0.2498238312640348) < 0.1 + # Azimuth and elevation + cosm = Cosm.de438() + az_deg, el_deg = devices[0].compute_azimuth_elevation(end_sc.orbit, cosm) + + assert abs(az_deg - 172.38393292690975) < 1e-15 + assert abs(el_deg - 27.904687635388676) < 1e-15 def test_pure_prediction(): # Initialize logging @@ -267,4 +273,4 @@ def test_pure_prediction(): if __name__ == "__main__": - test_pure_prediction() + test_one_way_msr() From f8731c92ab30e1f4b1835505cdfd600dbb4b13cc Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 24 Jun 2023 16:45:21 -0600 Subject: [PATCH 05/16] Fix azimuth --- .github/workflows/python.yml | 5 --- .github/workflows/tests.yaml | 7 ++--- pyproject.toml | 2 +- src/od/ground_station.rs | 10 +++++- tests/python/test_orbit_determination.py | 39 +++++++++++++++++++++--- 5 files changed, 48 insertions(+), 15 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 69f01a8e..049a05d8 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,8 +1,3 @@ -# This file is autogenerated by maturin v0.14.16 -# To update, run -# -# maturin generate-ci --pytest -o .github/workflows/python.yml github -# name: Python on: push: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index fd31d119..1cf0dcbd 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -135,13 +135,12 @@ jobs: - name: Generate coverage report env: RUSTFLAGS: "-Cinstrument-coverage" - LLVM_PROFILE_FILE: "nyx_space-%p-%m.profraw" - # grcov . -s . --binary-path ./target/debug/ -t html --branch --ignore-not-existing -o ./target/debug/coverage/ # Export as HTML + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" run: | cargo test --lib - grcov . --binary-path ./target/debug/ -t lcov -s . > lcov-lib.txt + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-lib.txt cargo test -- cosmic mission_design orbit_determination propulsion test_monte_carlo_epoch - grcov . --binary-path ./target/debug/ -t lcov -s . > lcov-integ.txt + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-integ.txt - name: Upload coverage report uses: codecov/codecov-action@v3 diff --git a/pyproject.toml b/pyproject.toml index 424c8b5d..5ce4a2c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=0.14,<0.15"] +requires = ["maturin>=1.1,<1.2"] build-backend = "maturin" [project] diff --git a/src/od/ground_station.rs b/src/od/ground_station.rs index c74e5ef4..efe9e565 100644 --- a/src/od/ground_station.rs +++ b/src/od/ground_station.rs @@ -23,6 +23,7 @@ use crate::cosmic::{Cosm, Frame, Orbit}; use crate::io::{frame_from_str, frame_to_str, ConfigRepr, Configurable}; use crate::md::prelude::Traj; use crate::time::Epoch; +use crate::utils::between_0_360; use crate::{NyxError, Spacecraft}; use hifitime::Duration; use rand_pcg::Pcg64Mcg; @@ -170,8 +171,15 @@ impl GroundStation { let rho_sez = rx_sez - tx_sez; // Finally, compute the elevation (math is the same as declination) + // Source: Vallado, section 4.4.3 + // Only the sine is needed as per Vallado, and the formula is the same as the declination + // because we're in the SEZ frame. let elevation_deg = rho_sez.declination_deg(); - let azimuth_deg = (rho_sez.y_km / rho_sez.x_km).tan().to_degrees(); + if (elevation_deg - 90.0).abs() < 1e-6 { + warn!("object nearly overhead (el = {elevation_deg} deg), azimuth may be incorrect"); + } + // For the elevation, we need to perform a quadrant check because it's measured from 0 to 360 degrees. + let azimuth_deg = between_0_360((-rho_sez.y_km.atan2(rho_sez.x_km)).to_degrees()); // Return elevation in degrees and rx/tx in the inertial frame of the spacecraft ( diff --git a/tests/python/test_orbit_determination.py b/tests/python/test_orbit_determination.py index 1f453cfd..6a2238bf 100644 --- a/tests/python/test_orbit_determination.py +++ b/tests/python/test_orbit_determination.py @@ -16,7 +16,7 @@ ) from nyx_space.mission_design import TrajectoryLoader, SpacecraftDynamics, propagate from nyx_space.cosmic import Spacecraft, Cosm -from nyx_space.time import Unit +from nyx_space.time import Unit, TimeSeries from nyx_space.plots.od import ( plot_covar, plot_estimates, @@ -194,9 +194,40 @@ def test_one_way_msr(): # One way measurement - end_sc, _ = propagate(sc, dynamics["hifi"], sc.orbit.period() * 1.1) + end_sc, traj = propagate(sc, dynamics["hifi"], sc.orbit.period() * 1.1) print(end_sc) + print(traj) + # Let's build a dataframe of the range, doppler, azimuth, and elevation as seen from a ground station that sees the spacecraft a bunch + gs = devices[1] + print(f"Using {gs}") + cosm = Cosm.de438() + data = {"epoch": [], "range (km)": [], "doppler (km/s)": [], "azimuth (deg)": [], "elevation (deg)": []} + # Start by building a time series + ts = TimeSeries(traj.first().epoch, traj.last().epoch, step=Unit.Minute*30, inclusive=True) + # And iterate over it + for epoch in ts: + orbit = traj.at(epoch).orbit + try: + range_km, doppler_km_s = gs.measure(orbit) + except: + # Spacecraft is not visible then, nothing to store + pass + else: + # Also grab the azimuth and elevation angles + az_deg, el_deg = gs.compute_azimuth_elevation(orbit, cosm) + # And push to the data dictionary + data["epoch"] += [str(epoch)] + data["azimuth (deg)"] += [az_deg] + data["elevation (deg)"] += [el_deg] + data["range (km)"] += [range_km] + data["doppler (km/s)"] += [doppler_km_s] + # And convert to a data frame + df = pd.DataFrame(data, columns=data.keys()) + assert len(df) == 8 + print(df.describe()) + + # Test values range_km, doppler_km_s = devices[0].measure(end_sc.orbit) print(range_km, doppler_km_s) @@ -204,10 +235,10 @@ def test_one_way_msr(): assert abs(doppler_km_s - -0.2498238312640348) < 0.1 # Azimuth and elevation - cosm = Cosm.de438() + az_deg, el_deg = devices[0].compute_azimuth_elevation(end_sc.orbit, cosm) - assert abs(az_deg - 172.38393292690975) < 1e-15 + assert abs(az_deg - 128.66181520071825) < 1e-15 assert abs(el_deg - 27.904687635388676) < 1e-15 def test_pure_prediction(): From a8844dc1f166d6483bd120b4948ffd948e1865c7 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sat, 24 Jun 2023 17:55:46 -0600 Subject: [PATCH 06/16] Trying to download the dist file --- .github/workflows/python.yml | 22 +++++++++++++++++++++- src/python/mission_design/mod.rs | 3 +++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 049a05d8..d806ed83 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -38,9 +38,14 @@ jobs: name: wheels path: dist + - name: Download wheels + uses: actions/download-artifact@v3 + with: + name: wheels + path: dist + - name: pytest x86_64 if: ${{ matrix.target == 'x86_64' }} - shell: bash run: | set -e pip install nyx_space --find-links dist --force-reinstall @@ -91,6 +96,13 @@ jobs: with: name: wheels path: dist + + - name: Download wheels + uses: actions/download-artifact@v3 + with: + name: wheels + path: dist + - name: pytest if: ${{ !startsWith(matrix.target, 'aarch64') }} shell: bash @@ -116,11 +128,19 @@ jobs: target: ${{ matrix.target }} args: --release --out dist --find-interpreter -F python sccache: 'true' + - name: Upload wheels uses: actions/upload-artifact@v3 with: name: wheels path: dist + + - name: Download wheels + uses: actions/download-artifact@v3 + with: + name: wheels + path: dist + - name: pytest if: ${{ !startsWith(matrix.target, 'aarch64') }} shell: bash diff --git a/src/python/mission_design/mod.rs b/src/python/mission_design/mod.rs index 4c6bf900..a14064a4 100644 --- a/src/python/mission_design/mod.rs +++ b/src/python/mission_design/mod.rs @@ -155,6 +155,9 @@ fn propagate( } } +/// Performs a two body propagation around the central body of each orbit in the `orbits` list either for the duration in the list or until the epochs in the list. +/// New epoch and duration parameters may either be exactly one item or N items, where N is the number of orbits. +/// This computation will happen in parallel on all CPUs. It uses the `at_epoch` function of `Orbit`. #[pyfunction] fn two_body( orbits: Vec, From f1f4bb49d96f62e0ba8c5cf838537503f00c410b Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sun, 25 Jun 2023 08:59:40 -0600 Subject: [PATCH 07/16] Up msrv --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1cf0dcbd..00a850c2 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -37,7 +37,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] rust: - - { version: "1.67", name: MSRV } + - { version: "1.70", name: MSRV } - { version: stable, name: stable } runs-on: ${{ matrix.os }} From e5d2e070b6b3bf08833ea5ef8e47d8354f117729 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sun, 25 Jun 2023 15:36:22 +0000 Subject: [PATCH 08/16] Trying to force no download --- .github/workflows/python.yml | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index d806ed83..62992ed0 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -38,17 +38,11 @@ jobs: name: wheels path: dist - - name: Download wheels - uses: actions/download-artifact@v3 - with: - name: wheels - path: dist - - name: pytest x86_64 if: ${{ matrix.target == 'x86_64' }} run: | set -e - pip install nyx_space --find-links dist --force-reinstall + pip install nyx_space --use-wheel --no-index --find-links dist --force-reinstall pip install pytest numpy pandas plotly pyarrow scipy pytest @@ -65,7 +59,7 @@ jobs: pip3 install -U pip pytest plotly run: | set -e - pip3 install nyx_space --find-links dist --force-reinstall + pip3 install nyx_space --use-wheel --no-index --find-links dist --force-reinstall pytest - name: Upload python tests HTMLs @@ -97,18 +91,12 @@ jobs: name: wheels path: dist - - name: Download wheels - uses: actions/download-artifact@v3 - with: - name: wheels - path: dist - - name: pytest if: ${{ !startsWith(matrix.target, 'aarch64') }} shell: bash run: | set -e - pip install nyx_space --find-links dist --force-reinstall + pip install nyx_space --use-wheel --no-index --find-links dist --force-reinstall pip install pytest numpy pandas plotly pyarrow pytest @@ -134,19 +122,13 @@ jobs: with: name: wheels path: dist - - - name: Download wheels - uses: actions/download-artifact@v3 - with: - name: wheels - path: dist - name: pytest if: ${{ !startsWith(matrix.target, 'aarch64') }} shell: bash run: | set -e - pip install nyx_space --find-links dist --force-reinstall + pip install nyx_space --use-wheel --no-index --find-links dist --force-reinstall pip install pytest numpy pandas plotly pyarrow pytest From c7baf2b05c38f2f023120e3772a54f37cb97c600 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Sun, 25 Jun 2023 20:42:16 +0000 Subject: [PATCH 09/16] Remove incorrect flag in pip install --- .github/workflows/python.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 62992ed0..1f87059b 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -42,7 +42,8 @@ jobs: if: ${{ matrix.target == 'x86_64' }} run: | set -e - pip install nyx_space --use-wheel --no-index --find-links dist --force-reinstall + ls -lh dist + pip install nyx_space --find-links dist --force-reinstall pip install pytest numpy pandas plotly pyarrow scipy pytest @@ -58,8 +59,9 @@ jobs: apt-get install -y --no-install-recommends python3 python3-pip python3-numpy python3-dev python3-pandas python-pyarrow python3-scipy pip3 install -U pip pytest plotly run: | + ls -lh dist set -e - pip3 install nyx_space --use-wheel --no-index --find-links dist --force-reinstall + pip3 install nyx_space --find-links dist --force-reinstall pytest - name: Upload python tests HTMLs @@ -96,7 +98,7 @@ jobs: shell: bash run: | set -e - pip install nyx_space --use-wheel --no-index --find-links dist --force-reinstall + pip install nyx_space --find-links dist --force-reinstall pip install pytest numpy pandas plotly pyarrow pytest @@ -128,7 +130,8 @@ jobs: shell: bash run: | set -e - pip install nyx_space --use-wheel --no-index --find-links dist --force-reinstall + ls -lh dist + pip install nyx_space --find-links dist --force-reinstall pip install pytest numpy pandas plotly pyarrow pytest From 168847e0e57b0f7b5d9891a91641f5abd13f4190 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Mon, 26 Jun 2023 01:34:15 +0000 Subject: [PATCH 10/16] Trying to install the exact wheel --- .github/workflows/python.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 1f87059b..62a3e987 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: [x86_64, x86, aarch64] + target: [x86_64, aarch64] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -43,12 +43,12 @@ jobs: run: | set -e ls -lh dist - pip install nyx_space --find-links dist --force-reinstall + pip install --find-links dist --force-reinstall `find dist -name nyx_space* | head -n 1` pip install pytest numpy pandas plotly pyarrow scipy pytest - - name: pytest - if: ${{ !startsWith(matrix.target, 'x86') && matrix.target != 'ppc64' && matrix.target != 'aarch64' }} + - name: pytest aarch64 + if: ${{ matrix.target == 'aarch64' }} uses: uraimo/run-on-arch-action@v2.5.0 with: arch: ${{ matrix.target }} @@ -61,7 +61,7 @@ jobs: run: | ls -lh dist set -e - pip3 install nyx_space --find-links dist --force-reinstall + pip install --find-links dist --force-reinstall `find dist -name nyx_space* | head -n 1` pytest - name: Upload python tests HTMLs @@ -94,11 +94,11 @@ jobs: path: dist - name: pytest - if: ${{ !startsWith(matrix.target, 'aarch64') }} shell: bash run: | set -e - pip install nyx_space --find-links dist --force-reinstall + ls -lh dist + pip install --find-links dist --force-reinstall `find dist -name nyx_space* | head -n 1` pip install pytest numpy pandas plotly pyarrow pytest @@ -131,7 +131,7 @@ jobs: run: | set -e ls -lh dist - pip install nyx_space --find-links dist --force-reinstall + pip install --find-links dist --force-reinstall `find dist -name nyx_space* | head -n 1` pip install pytest numpy pandas plotly pyarrow pytest From 6300e6cc636fa4c11cb68111a16e0e6428f99ad3 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Mon, 26 Jun 2023 01:37:47 +0000 Subject: [PATCH 11/16] Separate coverage tests --- .github/workflows/tests.yaml | 44 +++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 00a850c2..6a3ec8da 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -132,21 +132,59 @@ jobs: rustup component add llvm-tools-preview cargo install grcov - - name: Generate coverage report + - name: Generate coverage report for unit tests env: RUSTFLAGS: "-Cinstrument-coverage" LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" run: | cargo test --lib grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-lib.txt + + - name: Generate coverage report for cosmic integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | cargo test -- cosmic mission_design orbit_determination propulsion test_monte_carlo_epoch - grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-integ.txt + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-cosmic.txt + + - name: Generate coverage report for mission_design integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test -- cosmic mission_design orbit_determination propulsion test_monte_carlo_epoch + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-mission_design.txt + + - name: Generate coverage report for OD integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test -- orbit_determination + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-od.txt + + - name: Generate coverage report for propulsion integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test -- propulsion + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-prop.txt + + - name: Generate coverage report for monte carlo integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test -- test_monte_carlo_epoch + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-mc.txt - name: Upload coverage report uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./lcov-lib.txt, ./lcov-integ.txt + files: ./lcov-*.txt release: name: Release From 974acc3b66d6f7677c9299a360762c48462cf99d Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Mon, 26 Jun 2023 03:14:59 +0000 Subject: [PATCH 12/16] Moving most coverage test to daily runs --- .github/workflows/daily.yaml | 87 +++++++++++++++++++++ .github/workflows/python.yml | 8 +- .github/workflows/{tests.yaml => rust.yaml} | 44 +---------- src/cosmic/orbit.rs | 9 --- tests/python/test_orbit_determination.py | 4 +- 5 files changed, 95 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/daily.yaml rename .github/workflows/{tests.yaml => rust.yaml} (67%) diff --git a/.github/workflows/daily.yaml b/.github/workflows/daily.yaml new file mode 100644 index 00000000..98503693 --- /dev/null +++ b/.github/workflows/daily.yaml @@ -0,0 +1,87 @@ +name: Daily Workflow + +on: + schedule: + - cron: '0 0 * * *' # Run at midnight every day + +jobs: + full-coverage: + name: Unit test and integration test coverage analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt, clippy + + - name: Install cargo-grcov + run: | + rustup component add llvm-tools-preview + cargo install grcov + + - name: Generate coverage report for unit tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test --lib + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-lib.txt + + - name: Generate coverage report for doc tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test --doc + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-lib.txt + + - name: Generate coverage report for cosmic integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test -- cosmic mission_design orbit_determination propulsion test_monte_carlo_epoch + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-cosmic.txt + + - name: Generate coverage report for mission_design integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test -- cosmic mission_design orbit_determination propulsion test_monte_carlo_epoch + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-mission_design.txt + + - name: Generate coverage report for OD integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test -- orbit_determination + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-od.txt + + - name: Generate coverage report for propulsion integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test -- propulsion + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-prop.txt + + - name: Generate coverage report for monte carlo integr. tests + env: + RUSTFLAGS: "-Cinstrument-coverage" + LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" + run: | + cargo test -- test_monte_carlo_epoch + grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-mc.txt + + - name: Upload coverage report + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./lcov-*.txt \ No newline at end of file diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 62a3e987..0adc4872 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -43,7 +43,7 @@ jobs: run: | set -e ls -lh dist - pip install --find-links dist --force-reinstall `find dist -name nyx_space* | head -n 1` + pip install --find-links dist --force-reinstall nyx_space pip install pytest numpy pandas plotly pyarrow scipy pytest @@ -61,7 +61,7 @@ jobs: run: | ls -lh dist set -e - pip install --find-links dist --force-reinstall `find dist -name nyx_space* | head -n 1` + pip install --find-links dist --force-reinstall nyx_space pytest - name: Upload python tests HTMLs @@ -98,7 +98,7 @@ jobs: run: | set -e ls -lh dist - pip install --find-links dist --force-reinstall `find dist -name nyx_space* | head -n 1` + pip install --find-links dist --force-reinstall nyx_space pip install pytest numpy pandas plotly pyarrow pytest @@ -131,7 +131,7 @@ jobs: run: | set -e ls -lh dist - pip install --find-links dist --force-reinstall `find dist -name nyx_space* | head -n 1` + pip install --find-links dist --force-reinstall nyx_space pip install pytest numpy pandas plotly pyarrow pytest diff --git a/.github/workflows/tests.yaml b/.github/workflows/rust.yaml similarity index 67% rename from .github/workflows/tests.yaml rename to .github/workflows/rust.yaml index 6a3ec8da..c5c7edfc 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/rust.yaml @@ -112,8 +112,8 @@ jobs: - name: Audit code run: cargo audit - coverage: - name: Coverage + ut-coverage: + name: Coverage (unit tests only) runs-on: ubuntu-latest needs: [tests] @@ -139,46 +139,6 @@ jobs: run: | cargo test --lib grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-lib.txt - - - name: Generate coverage report for cosmic integr. tests - env: - RUSTFLAGS: "-Cinstrument-coverage" - LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" - run: | - cargo test -- cosmic mission_design orbit_determination propulsion test_monte_carlo_epoch - grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-cosmic.txt - - - name: Generate coverage report for mission_design integr. tests - env: - RUSTFLAGS: "-Cinstrument-coverage" - LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" - run: | - cargo test -- cosmic mission_design orbit_determination propulsion test_monte_carlo_epoch - grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-mission_design.txt - - - name: Generate coverage report for OD integr. tests - env: - RUSTFLAGS: "-Cinstrument-coverage" - LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" - run: | - cargo test -- orbit_determination - grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-od.txt - - - name: Generate coverage report for propulsion integr. tests - env: - RUSTFLAGS: "-Cinstrument-coverage" - LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" - run: | - cargo test -- propulsion - grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-prop.txt - - - name: Generate coverage report for monte carlo integr. tests - env: - RUSTFLAGS: "-Cinstrument-coverage" - LLVM_PROFILE_FILE: "target/coverage/nyx_space-%p-%m.profraw" - run: | - cargo test -- test_monte_carlo_epoch - grcov . --binary-path ./target/debug/ -t lcov -s . --keep-only 'src/*' > lcov-mc.txt - name: Upload coverage report uses: codecov/codecov-action@v3 diff --git a/src/cosmic/orbit.rs b/src/cosmic/orbit.rs index cbd22383..4807ca59 100644 --- a/src/cosmic/orbit.rs +++ b/src/cosmic/orbit.rs @@ -2305,15 +2305,6 @@ impl Configurable for Orbit { /// /// If a numerical error occurs during computation, the function may return a MathDomain error. In the case of a /// non-converging iterative process, the function will return a MaxIterReached error after 1000 iterations. -/// -/// # Examples -/// -/// ``` -/// let ma_radians = 0.5; -/// let ecc = 0.1; -/// let tol = 1e-6; -/// let result = compute_mean_to_true_anomaly(ma_radians, ecc, tol); -/// ``` fn compute_mean_to_true_anomaly(ma_radians: f64, ecc: f64, tol: f64) -> Result { let rm = ma_radians; if ecc <= 1.0 { diff --git a/tests/python/test_orbit_determination.py b/tests/python/test_orbit_determination.py index 6a2238bf..0a053f4e 100644 --- a/tests/python/test_orbit_determination.py +++ b/tests/python/test_orbit_determination.py @@ -238,8 +238,8 @@ def test_one_way_msr(): az_deg, el_deg = devices[0].compute_azimuth_elevation(end_sc.orbit, cosm) - assert abs(az_deg - 128.66181520071825) < 1e-15 - assert abs(el_deg - 27.904687635388676) < 1e-15 + assert abs(az_deg - 128.66181520071825) < 1e-10 + assert abs(el_deg - 27.904687635388676) < 1e-10 def test_pure_prediction(): # Initialize logging From 3e378943e6d912f952f72571bab17e073e1ef58a Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Mon, 26 Jun 2023 04:24:49 +0000 Subject: [PATCH 13/16] God this pip bug is so annoying --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5ce4a2c3..b91aa4c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] -requires = ["maturin>=1.1,<1.2"] +# requires = ["maturin>=1.1,<1.2"] +requires = ["maturin>=0.14,<0.15"] build-backend = "maturin" [project] From 6695084f733fef36970f09a77cee883b8bbd517c Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Mon, 26 Jun 2023 13:19:16 +0000 Subject: [PATCH 14/16] Add vv to pip install --- .github/workflows/python.yml | 12 ++++++------ pyproject.toml | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 0adc4872..7b6de05d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: [x86_64, aarch64] + target: [x86_64, x86] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -43,12 +43,12 @@ jobs: run: | set -e ls -lh dist - pip install --find-links dist --force-reinstall nyx_space + pip install nyx_space --find-links dist --force-reinstall -vv pip install pytest numpy pandas plotly pyarrow scipy pytest - - name: pytest aarch64 - if: ${{ matrix.target == 'aarch64' }} + - name: pytest x86 + if: ${{ matrix.target == 'x86' }} uses: uraimo/run-on-arch-action@v2.5.0 with: arch: ${{ matrix.target }} @@ -59,9 +59,9 @@ jobs: apt-get install -y --no-install-recommends python3 python3-pip python3-numpy python3-dev python3-pandas python-pyarrow python3-scipy pip3 install -U pip pytest plotly run: | - ls -lh dist set -e - pip install --find-links dist --force-reinstall nyx_space + ls -lh dist + pip install nyx_space --find-links dist --force-reinstall -vv pytest - name: Upload python tests HTMLs diff --git a/pyproject.toml b/pyproject.toml index b91aa4c9..5ce4a2c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,5 @@ [build-system] -# requires = ["maturin>=1.1,<1.2"] -requires = ["maturin>=0.14,<0.15"] +requires = ["maturin>=1.1,<1.2"] build-backend = "maturin" [project] From 61fcd3350e1244ce050d572b483063036715f655 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Mon, 26 Jun 2023 20:25:14 +0000 Subject: [PATCH 15/16] Trying to upgrade to python 3.11 --- .github/workflows/python.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 7b6de05d..ef8325d4 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Build wheels uses: PyO3/maturin-action@v1 @@ -43,7 +43,7 @@ jobs: run: | set -e ls -lh dist - pip install nyx_space --find-links dist --force-reinstall -vv + pip install nyx_space --find-links dist --force-reinstall -v --no-cache-dir pip install pytest numpy pandas plotly pyarrow scipy pytest @@ -61,7 +61,7 @@ jobs: run: | set -e ls -lh dist - pip install nyx_space --find-links dist --force-reinstall -vv + pip install nyx_space --find-links dist --force-reinstall -v --no-cache-dir pytest - name: Upload python tests HTMLs @@ -79,7 +79,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" architecture: ${{ matrix.target }} - name: Build wheels uses: PyO3/maturin-action@v1 @@ -111,7 +111,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Build wheels uses: PyO3/maturin-action@v1 with: From 174d0a68587b1eea2b687fe6300300e5bdad8424 Mon Sep 17 00:00:00 2001 From: Christopher Rabotin Date: Mon, 26 Jun 2023 21:39:16 +0000 Subject: [PATCH 16/16] Maybe changing the name of the package will do --- .github/workflows/python.yml | 19 +------------------ Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index ef8325d4..31ef6976 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: [x86_64, x86] + target: [x86_64] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -47,23 +47,6 @@ jobs: pip install pytest numpy pandas plotly pyarrow scipy pytest - - name: pytest x86 - if: ${{ matrix.target == 'x86' }} - uses: uraimo/run-on-arch-action@v2.5.0 - with: - arch: ${{ matrix.target }} - distro: ubuntu22.04 - githubToken: ${{ github.token }} - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip python3-numpy python3-dev python3-pandas python-pyarrow python3-scipy - pip3 install -U pip pytest plotly - run: | - set -e - ls -lh dist - pip install nyx_space --find-links dist --force-reinstall -v --no-cache-dir - pytest - - name: Upload python tests HTMLs uses: actions/upload-artifact@v3 with: diff --git a/Cargo.toml b/Cargo.toml index c12aeb90..5c66e34f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "nyx-space" build = "build.rs" -version = "2.0.0-alpha.2" +version = "2.0.0-beta-dev" edition = "2021" authors = ["Christopher Rabotin "] description = "A high-fidelity space mission toolkit, with orbit propagation, estimation and some systems engineering"