Skip to content

Commit b03b1cb

Browse files
Merge pull request #398 from nyx-space/feat/gh-396-moar-serde
Orbit determination constant biases + measurement ambiguity and moduli
2 parents 0f9ed48 + 84e4f43 commit b03b1cb

File tree

27 files changed

+732
-152
lines changed

27 files changed

+732
-152
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "nyx-space"
33
build = "build.rs"
4-
version = "2.0.0"
4+
version = "2.0.1"
55
edition = "2021"
66
authors = ["Christopher Rabotin <[email protected]>"]
77
description = "A high-fidelity space mission toolkit, with orbit propagation, estimation and some systems engineering"

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ For Python projects, get started by installing the library via `pip`: `pip insta
6262

6363
Nyx is provided under the [AGPLv3 License](./LICENSE). By using this software, you assume responsibility for adhering to the license. Refer to [the pricing page](https://nyxspace.com/pricing/?utm_source=readme-price) for an FAQ on the AGPLv3 license. Notably, any software that incorporates, links to, or depends on Nyx must also be released under the AGPLv3 license, even if you distribute an unmodified version of Nyx.
6464

65+
# Versioning
66+
67+
Nyx mostly adheres to SemVer. New patch versions should be rare. Updated dependencies trigger a new minor version. _However_ new fields in structures and new behavior may also be added with minor releases, but the public facing initializers and functions should not significantly change (but may still change).
68+
69+
Major releases are for dramatic changes.
70+
6571

6672
[cratesio-image]: https://img.shields.io/crates/v/nyx-space.svg
6773
[cratesio]: https://crates.io/crates/nyx-space

examples/04_lro_od/plot_od_rslt.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
@click.command
1111
@click.option("-p", "--path", type=str, default="./04_lro_od_results.parquet")
12-
def main(path: str):
12+
@click.option("-f", "--full", type=bool, default=True)
13+
def main(path: str, full: bool):
1314
df = pl.read_parquet(path)
1415

1516
df = (
@@ -123,6 +124,9 @@ def main(path: str):
123124
y=["Sigma Vx (RIC) (km/s)", "Sigma Vy (RIC) (km/s)", "Sigma Vz (RIC) (km/s)"],
124125
).show()
125126

127+
if not full:
128+
return
129+
126130
# Load the RIC diff.
127131
for fname, errname in [
128132
("04_lro_od_truth_error", "OD vs Flown"),

src/cosmic/orbitdual.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ impl OrbitDual {
476476
param: StateParameter::MeanAnomaly,
477477
})
478478
} else if self.ecc()?.real() > 1.0 {
479-
info!("computing the hyperbolic anomaly");
479+
debug!("computing the hyperbolic anomaly");
480480
// From GMAT's TrueToHyperbolicAnomaly
481481
Ok(OrbitPartial {
482482
dual: ((self.ta_deg()?.dual.to_radians().sin()

src/dynamics/guidance/ruggiero.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818

1919
use anise::prelude::Almanac;
20+
use serde::{Deserialize, Serialize};
2021
use snafu::ResultExt;
2122

2223
use super::{
@@ -32,7 +33,7 @@ use std::fmt;
3233
use std::sync::Arc;
3334

3435
/// Ruggiero defines the closed loop guidance law from IEPC 2011-102
35-
#[derive(Copy, Clone, Default)]
36+
#[derive(Copy, Clone, Default, Serialize, Deserialize)]
3637
pub struct Ruggiero {
3738
/// Stores the objectives
3839
pub objectives: [Option<Objective>; 5],

src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub mod cosmic;
4141
/// Utility functions shared by different modules, and which may be useful to engineers.
4242
pub mod utils;
4343

44-
mod errors;
44+
pub mod errors;
4545
/// Nyx will (almost) never panic and functions which may fail will return an error.
4646
pub use self::errors::NyxError;
4747

src/mc/dispersion.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
*/
1818

1919
use crate::md::StateParameter;
20+
use serde::{Deserialize, Serialize};
2021
use typed_builder::TypedBuilder;
2122

2223
/// A dispersions configuration, allows specifying min/max bounds (by default, they are not set)
23-
#[derive(Copy, Clone, TypedBuilder)]
24+
#[derive(Copy, Clone, TypedBuilder, Serialize, Deserialize)]
2425
pub struct StateDispersion {
2526
pub param: StateParameter,
2627
#[builder(default, setter(strip_option))]

src/md/events/details.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ use crate::md::prelude::{Interpolatable, Traj};
2525
use crate::md::EventEvaluator;
2626
use crate::time::Duration;
2727
use core::fmt;
28+
use serde::{Deserialize, Serialize};
2829
use std::sync::Arc;
2930

3031
/// Enumerates the possible edges of an event in a trajectory.
3132
///
3233
/// `EventEdge` is used to describe the nature of a trajectory event, particularly in terms of its temporal dynamics relative to a specified condition or threshold. This enum helps in distinguishing whether the event is occurring at a rising edge, a falling edge, or if the edge is unclear due to insufficient data or ambiguous conditions.
33-
#[derive(Copy, Clone, Debug, PartialEq)]
34+
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
3435
pub enum EventEdge {
3536
/// Represents a rising edge of the event. This indicates that the event is transitioning from a lower to a higher evaluation of the event. For example, in the context of elevation, a rising edge would indicate an increase in elevation from a lower angle.
3637
Rising,
@@ -73,14 +74,14 @@ where
7374
{
7475
/// Generates detailed information about an event at a specific epoch in a trajectory.
7576
///
76-
/// This takes an `Epoch` as an input and returns a `Result<Self, NyxError>`.
77+
/// This takes an `Epoch` as an input and returns a `Result<Self, EventError>`.
7778
/// It is designed to determine the state of a trajectory at a given epoch, evaluate the specific event at that state, and ascertain the nature of the event (rising, falling, or unclear).
7879
/// The initialization intelligently determines the edge type of the event by comparing the event's value at the current, previous, and next epochs.
7980
/// It ensures robust event characterization in trajectories.
8081
///
8182
/// # Returns
8283
/// - `Ok(EventDetails<S>)` if the state at the given epoch can be determined and the event details are successfully evaluated.
83-
/// - `Err(NyxError)` if there is an error in retrieving the state at the specified epoch.
84+
/// - `Err(EventError)` if there is an error in retrieving the state at the specified epoch.
8485
///
8586
pub fn new<E: EventEvaluator<S>>(
8687
state: S,

src/md/events/mod.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use crate::time::{Duration, Unit};
2727
use crate::State;
2828
use anise::prelude::{Almanac, Frame};
2929
use anise::structure::planetocentric::ellipsoid::Ellipsoid;
30+
use serde::{Deserialize, Serialize};
3031

3132
use std::default::Default;
3233
use std::fmt;
@@ -59,15 +60,14 @@ where
5960
}
6061

6162
/// Defines a state parameter event finder
62-
#[derive(Clone, Debug)]
63-
63+
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
6464
pub struct Event {
6565
/// The state parameter
6666
pub parameter: StateParameter,
6767
/// The desired self.desired_value, must be in the same units as the state parameter
6868
pub desired_value: f64,
69-
/// The time precision after which the solver will report that it cannot find any more precise
70-
pub epoch_precision: Unit,
69+
/// The duration precision after which the solver will report that it cannot find any more precise
70+
pub epoch_precision: Duration,
7171
/// The precision on the desired value
7272
pub value_precision: f64,
7373
/// An optional frame in which to search this -- it IS recommended to convert the whole trajectory instead of searching in a given frame!
@@ -133,12 +133,12 @@ impl Event {
133133
parameter: StateParameter,
134134
desired_value: f64,
135135
value_precision: f64,
136-
epoch_precision: Unit,
136+
unit_precision: Unit,
137137
) -> Self {
138138
Self {
139139
parameter,
140140
desired_value,
141-
epoch_precision,
141+
epoch_precision: 1 * unit_precision,
142142
value_precision,
143143
obs_frame: None,
144144
}
@@ -166,7 +166,7 @@ impl Event {
166166
Self {
167167
parameter,
168168
desired_value,
169-
epoch_precision: Unit::Millisecond,
169+
epoch_precision: Unit::Millisecond * 1,
170170
value_precision: 1e-3,
171171
obs_frame: Some(target_frame),
172172
}
@@ -179,7 +179,7 @@ impl Default for Event {
179179
parameter: StateParameter::Periapsis,
180180
desired_value: 0.0,
181181
value_precision: 1e-3,
182-
epoch_precision: Unit::Second,
182+
epoch_precision: Unit::Second * 1,
183183
obs_frame: None,
184184
}
185185
}

src/md/objective.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818

1919
use super::StateParameter;
2020
use crate::{errors::StateError, Spacecraft, State};
21+
use serde::{Deserialize, Serialize};
2122
use std::fmt;
2223

2324
/// Defines a state parameter event finder
24-
#[derive(Copy, Clone, Debug)]
25+
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
2526
pub struct Objective {
2627
/// The state parameter to target
2728
pub parameter: StateParameter,

src/od/filter/kalman.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,14 @@ where
300300
// Compute the prefit ratio for the automatic rejection.
301301
// The measurement covariance is the square of the measurement itself.
302302
// So we compute its Cholesky decomposition to return to the non squared values.
303-
let r_k_chol = s_k.clone().cholesky().ok_or(ODError::SingularNoiseRk)?.l();
303+
let r_k_chol = match s_k.clone().cholesky() {
304+
Some(r_k_clone) => r_k_clone.l(),
305+
None => {
306+
// In very rare case, when there isn't enough noise in the measurements,
307+
// the inverting of S_k fails. If so, we revert back to the nominal Kalman derivation.
308+
r_k.clone().cholesky().ok_or(ODError::SingularNoiseRk)?.l()
309+
}
310+
};
304311

305312
// Compute the ratio as the average of each component of the prefit over the square root of the measurement
306313
// matrix r_k. Refer to ODTK MathSpec equation 4.10.

src/od/ground_station/mod.rs

+29-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use indexmap::{IndexMap, IndexSet};
2424
use snafu::ensure;
2525

2626
use super::msr::MeasurementType;
27-
use super::noise::StochasticNoise;
27+
use super::noise::{GaussMarkov, StochasticNoise};
2828
use super::{ODAlmanacSnafu, ODError, ODTrajSnafu, TrackingDevice};
2929
use crate::io::ConfigRepr;
3030
use crate::od::NoiseNotConfiguredSnafu;
@@ -119,6 +119,34 @@ impl GroundStation {
119119
self
120120
}
121121

122+
/// Returns a copy of this ground station with the measurement type noises' constant bias set to the provided value.
123+
pub fn with_msr_bias_constant(
124+
mut self,
125+
msr_type: MeasurementType,
126+
bias_constant: f64,
127+
) -> Result<Self, ODError> {
128+
if self.stochastic_noises.is_none() {
129+
self.stochastic_noises = Some(IndexMap::new());
130+
}
131+
132+
let stochastics = self.stochastic_noises.as_mut().unwrap();
133+
134+
let this_noise = stochastics
135+
.get_mut(&msr_type)
136+
.ok_or(ODError::NoiseNotConfigured {
137+
kind: format!("{msr_type:?}"),
138+
})
139+
.unwrap();
140+
141+
if this_noise.bias.is_none() {
142+
this_noise.bias = Some(GaussMarkov::ZERO);
143+
}
144+
145+
this_noise.bias.unwrap().constant = Some(bias_constant);
146+
147+
Ok(self)
148+
}
149+
122150
/// Computes the azimuth and elevation of the provided object seen from this ground station, both in degrees.
123151
/// This is a shortcut to almanac.azimuth_elevation_range_sez.
124152
pub fn azimuth_elevation_of(

src/od/ground_station/trk_device.rs

+16
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,20 @@ impl TrackingDevice<Spacecraft> for GroundStation {
174174
})?
175175
.covariance(epoch))
176176
}
177+
178+
fn measurement_bias(&self, msr_type: MeasurementType, _epoch: Epoch) -> Result<f64, ODError> {
179+
let stochastics = self.stochastic_noises.as_ref().unwrap();
180+
181+
if let Some(gm) = stochastics
182+
.get(&msr_type)
183+
.ok_or(ODError::NoiseNotConfigured {
184+
kind: format!("{msr_type:?}"),
185+
})?
186+
.bias
187+
{
188+
Ok(gm.constant.unwrap_or(0.0))
189+
} else {
190+
Ok(0.0)
191+
}
192+
}
177193
}

src/od/msr/measurement.rs

+19-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ use nalgebra::{allocator::Allocator, DefaultAllocator, DimName, OVector};
2323
use std::fmt;
2424

2525
/// A type-agnostic simultaneous measurement storage structure. Allows storing any number of simultaneous measurement of a given taker.
26-
#[derive(Clone, Debug, PartialEq)]
26+
///
27+
/// Note that two measurements are considered equal if the tracker and epoch match exactly, and if both have the same measurement types,
28+
/// and those measurements are equal to within 1e-10 (this allows for some leeway in TDM producers).
29+
#[derive(Clone, Debug)]
2730
pub struct Measurement {
2831
/// Tracker alias which made this measurement
2932
pub tracker: String,
@@ -58,6 +61,7 @@ impl Measurement {
5861
where
5962
DefaultAllocator: Allocator<S>,
6063
{
64+
// Consider adding a modulo modifier here, any bias should be configured by each ground station.
6165
let mut obs = OVector::zeros();
6266
for (i, t) in types.iter().enumerate() {
6367
if let Some(msr_value) = self.data.get(t) {
@@ -91,3 +95,17 @@ impl fmt::Display for Measurement {
9195
write!(f, "{} measured {} on {}", self.tracker, msrs, self.epoch)
9296
}
9397
}
98+
99+
impl PartialEq for Measurement {
100+
fn eq(&self, other: &Self) -> bool {
101+
self.tracker == other.tracker
102+
&& self.epoch == other.epoch
103+
&& self.data.iter().all(|(key, &value)| {
104+
if let Some(&other_value) = other.data.get(key) {
105+
(value - other_value).abs() < 1e-10
106+
} else {
107+
false
108+
}
109+
})
110+
}
111+
}

0 commit comments

Comments
 (0)