Skip to content

Commit

Permalink
Merge pull request #155 from nyx-space/154-pandas-may-fail-to-convert…
Browse files Browse the repository at this point in the history
…-hifitime-epochs-into-datetime

Fix #154
  • Loading branch information
ChristopherRabotin committed May 25, 2023
2 parents 9724045 + 8d8fb5e commit c9e51e4
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 66 deletions.
49 changes: 2 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# nyx
[Nyx](https://nyxspace.com) is a high fidelity, fast, reliable and **[validated]([https://nyxspace.com/MathSpec/](https://nyxspace.com/nyxspace/MathSpec/))** astrodynamics toolkit library written in Rust. It provides convenient interfaces to both Rust and Python.
[Nyx](https://nyxspace.com) is a high fidelity, fast, reliable and **[validated]([https://nyxspace.com/MathSpec/](https://nyxspace.com/nyxspace/MathSpec/))** astrodynamics toolkit library written in Rust and available in Python.

The target audience is mission designers, astrodynamics engineers/hobbyists, and GNC engineers. The rationale for using Rust is to allow for very fast computations, guaranteed thread safety,
and portability to all platforms supported by [Rust](https://forge.rust-lang.org/platform-support.html).
Expand Down Expand Up @@ -50,49 +50,4 @@ The [AGPLv3 LICENSE](https://nyxspace.com/license/) is enforced.
- [x] Frame rotations

# Who am I?
An astrodynamics engineer with a heavy background in software. I currently work for Rocket Lab USA on the GNC of the Blue Ghost lunar lander.

# Examples
Refer to the [showcase](https://nyxspace.com/showcase/).

# Python todo

The "[maturin](https://crates.io/crates/maturin)" python package is used to build the python bindings.

```sh
pip install maturin
```

Build the python bindings using the following command.
```sh
maturin build --cargo-extra-args="--features python"
```

This creates a wheel file in `./target/wheels/` which can be installed using `pip install <filename.whl>`.

For development mode, the following command may be used that automatically installs the python module

```sh
maturin develop --cargo-extra-args="--features python"
```

## Python code example

This minimal example runs the scenario defined in `data/simple-scenario.toml` using the Python bindings.

```py
from nyx_space import md
from nyx_space import io
from nyx_space import cosmic

# Initialize the cosm which stores the ephemeris
cosm = cosmic.Cosm.de438()

with open('data/simple-scenario.toml', 'r') as f:
scen_data = f.read()

scenario = io.ScenarioSerde.from_toml_str(scen_data)

md.MDProcess.execute_all_in_scenario(scenario, cosm)

```
An astrodynamics engineer with a heavy background in software. I currently work for Rocket Lab USA on the GNC of the Blue Ghost lunar lander. -- Find me on [LinkedIn](https://www.linkedin.com/in/chrisrabotin/).
43 changes: 36 additions & 7 deletions python/nyx_space/plots/od.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,13 @@ def plot_estimates(
orig_tim_col = df[col_name]

# Build a Python datetime column
time_col = pd.to_datetime(orig_tim_col)
pd_ok_epochs = []
for epoch in orig_tim_col:
epoch = epoch.replace("UTC", "").strip()
if '.' not in epoch:
epoch += ".0"
pd_ok_epochs += [epoch]
time_col = pd.to_datetime(pd_ok_epochs)
x_title = "Epoch {}".format(time_col_name[-3:])

# Check that the requested covariance frame exists
Expand All @@ -115,9 +121,14 @@ def plot_estimates(
"cz_dot_z_dot": "Covariance VzVz",
}.items():
# Create a new column with the transformed covariance. (e.g. "Covariance VzVz (RIC) 1.0-sigma sqrt")
cov_col_name = f"{covar_col} ({cov_frame}) {cov_sigma}-sigma {cov_fmt}"
# Transform the current column
df[cov_col_name] = eval(f"np.{cov_fmt}")(df[f"{covar_col} ({cov_frame})"])
if cov_fmt is None:
cov_col_name = f"{covar_col} ({cov_frame}) {cov_sigma}-sigma"
# No transformation here
df[cov_col_name] = df[f"{covar_col} ({cov_frame})"]
else:
cov_col_name = f"{covar_col} ({cov_frame}) {cov_sigma}-sigma {cov_fmt}"
# Transform the current column
df[cov_col_name] = eval(f"np.{cov_fmt}")(df[f"{covar_col} ({cov_frame})"])
covar[f"{covar_var}"] = cov_col_name

plt_df = df
Expand Down Expand Up @@ -315,7 +326,13 @@ def plot_covar(
orig_tim_col = df[col_name]

# Build a Python datetime column
time_col = pd.to_datetime(orig_tim_col)
pd_ok_epochs = []
for epoch in orig_tim_col:
epoch = epoch.replace("UTC", "").strip()
if '.' not in epoch:
epoch += ".0"
pd_ok_epochs += [epoch]
time_col = pd.to_datetime(pd_ok_epochs)
x_title = "Epoch {}".format(time_col_name[-3:])

# Check that the requested covariance frame exists
Expand Down Expand Up @@ -494,7 +511,13 @@ def plot_measurements(
orig_tim_col = df[col_name]

# Build a Python datetime column
time_col = pd.to_datetime(orig_tim_col)
pd_ok_epochs = []
for epoch in orig_tim_col:
epoch = epoch.replace("UTC", "").strip()
if '.' not in epoch:
epoch += ".0"
pd_ok_epochs += [epoch]
time_col = pd.to_datetime(pd_ok_epochs)
x_title = "Epoch {}".format(time_col_name[-3:])

# Diff the epochs of the measurements to find when there is a start and end.
Expand Down Expand Up @@ -585,7 +608,13 @@ def plot_residuals(
orig_tim_col = df[col_name]

# Build a Python datetime column
time_col = pd.to_datetime(orig_tim_col)
pd_ok_epochs = []
for epoch in orig_tim_col:
epoch = epoch.replace("UTC", "").strip()
if '.' not in epoch:
epoch += ".0"
pd_ok_epochs += [epoch]
time_col = pd.to_datetime(pd_ok_epochs)
x_title = "Epoch {}".format(time_col_name[-3:])

plt_any = False
Expand Down
8 changes: 7 additions & 1 deletion python/nyx_space/plots/traj.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,13 @@ def plot_orbit_elements(
dfs = [dfs]

for df in dfs:
df["Epoch"] = pd.to_datetime(df["Epoch:Gregorian UTC"])
pd_ok_epochs = []
for epoch in df["Epoch:Gregorian UTC"]:
epoch = epoch.replace("UTC", "").strip()
if '.' not in epoch:
epoch += ".0"
pd_ok_epochs += [epoch]
df["Epoch"] = pd.to_datetime(pd_ok_epochs)

if not isinstance(names, list):
names = [names]
Expand Down
18 changes: 14 additions & 4 deletions src/od/process/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,10 +631,15 @@ where
}

/// Continuously predicts the trajectory until the provided end epoch, with covariance mapping at each step. In other words, this performs a time update.
pub fn predict_until(&mut self, max_step: Duration, end_epoch: Epoch) -> Result<(), NyxError> {
pub fn predict_until(
&mut self,
step: Duration,
fixed_step: bool,
end_epoch: Epoch,
) -> Result<(), NyxError> {
let prop_time = end_epoch - self.kf.previous_estimate().epoch();
info!("Propagating for {prop_time} seconds and mapping covariance",);
self.prop.set_step(max_step, false);
self.prop.set_step(step, fixed_step);

loop {
let mut epoch = self.prop.state.epoch();
Expand Down Expand Up @@ -671,10 +676,15 @@ where
}

/// Continuously predicts the trajectory for the provided duration, with covariance mapping at each step. In other words, this performs a time update.
pub fn predict_for(&mut self, max_step: Duration, duration: Duration) -> Result<(), NyxError> {
pub fn predict_for(
&mut self,
step: Duration,
fixed_step: bool,
duration: Duration,
) -> Result<(), NyxError> {
let end_epoch = self.kf.previous_estimate().epoch() + duration;

self.predict_until(max_step, end_epoch)
self.predict_until(step, fixed_step, end_epoch)
}

/// Builds the navigation trajectory for the estimated state only
Expand Down
18 changes: 12 additions & 6 deletions src/python/orbit_determination/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use super::{estimate::OrbitEstimate, GroundStation};
/// You must also provide an export path and optionally and export configuration to export the results to a Parquet file.
#[pyfunction]
#[pyo3(
text_signature = "(dynamics, spacecraft, initial_estimate, measurement_noise, arc, export_path, export_cfg, ekf_num_meas=None, ekf_disable_time=None, resid_crit=None, predict_until=None, predict_for=None, predict_step=None)"
text_signature = "(dynamics, spacecraft, initial_estimate, measurement_noise, arc, export_path, export_cfg, ekf_num_meas=None, ekf_disable_time=None, resid_crit=None, predict_until=None, predict_for=None, predict_step=None, fixed_step=False)"
)]
pub(crate) fn process_tracking_arc(
dynamics: SpacecraftDynamics,
Expand All @@ -53,6 +53,7 @@ pub(crate) fn process_tracking_arc(
predict_until: Option<Epoch>,
predict_for: Option<Duration>,
predict_step: Option<Duration>,
fixed_step: Option<bool>,
) -> Result<String, NyxError> {
// TODO: Return a navigation trajectory or use a class that mimics the better ODProcess -- https://github.com/nyx-space/nyx/issues/134
let msr_noise = Matrix2::from_iterator(measurement_noise);
Expand Down Expand Up @@ -87,11 +88,13 @@ pub(crate) fn process_tracking_arc(
if let Some(epoch) = predict_until {
let max_step =
predict_step.ok_or_else(|| NyxError::CustomError("predict_step unset".to_string()))?;
odp.predict_until(max_step, epoch).unwrap();
odp.predict_until(max_step, fixed_step.unwrap_or_else(|| false), epoch)
.unwrap();
} else if let Some(duration) = predict_for {
let max_step =
predict_step.ok_or_else(|| NyxError::CustomError("predict_step unset".to_string()))?;
odp.predict_for(max_step, duration).unwrap();
odp.predict_for(max_step, fixed_step.unwrap_or_else(|| false), duration)
.unwrap();
}

let maybe = odp.to_parquet(
Expand All @@ -109,7 +112,7 @@ pub(crate) fn process_tracking_arc(
/// You must also provide an export path and optionally and export configuration to export the results to a Parquet file.
#[pyfunction]
#[pyo3(
text_signature = "(dynamics, spacecraft, initial_estimate, step, export_path, export_cfg, predict_until=None, predict_for=None)"
text_signature = "(dynamics, spacecraft, initial_estimate, step, export_path, export_cfg, predict_until=None, predict_for=None, fixed_step=False)"
)]
pub(crate) fn predictor(
dynamics: SpacecraftDynamics,
Expand All @@ -120,6 +123,7 @@ pub(crate) fn predictor(
export_cfg: Option<ExportCfg>,
predict_until: Option<Epoch>,
predict_for: Option<Duration>,
fixed_step: Option<bool>,
) -> Result<String, NyxError> {
// TODO: Return a navigation trajectory or use a class that mimics the better ODProcess -- https://github.com/nyx-space/nyx/issues/134
let msr_noise = Matrix2::from_iterator(vec![1e-10, 0.0, 0.0, 1e-10]);
Expand All @@ -143,9 +147,11 @@ pub(crate) fn predictor(
let mut odp = ODProcess::ckf(prop_est, kf, None, Cosm::de438());

if let Some(epoch) = predict_until {
odp.predict_until(step, epoch).unwrap();
odp.predict_until(step, fixed_step.unwrap_or_else(|| false), epoch)
.unwrap();
} else if let Some(duration) = predict_for {
odp.predict_for(step, duration).unwrap();
odp.predict_for(step, fixed_step.unwrap_or_else(|| false), duration)
.unwrap();
}

let maybe = odp.to_parquet(
Expand Down
2 changes: 1 addition & 1 deletion tests/orbit_determination/two_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ fn od_tb_ckf_map_covar() {
KF<Orbit, nalgebra::Const<3>, nalgebra::Const<2>>,
> = ODProcess::ckf(prop_est, ckf, None, cosm);

odp.predict_for(30.seconds(), duration).unwrap();
odp.predict_for(30.seconds(), false, duration).unwrap();

// Check that the covariance inflated (we don't get the norm of the estimate because it's zero without any truth data)
let estimates = odp.estimates;
Expand Down

0 comments on commit c9e51e4

Please sign in to comment.