164 lines
6.7 KiB
Rust
164 lines
6.7 KiB
Rust
//! The reference-trajectory format, and reading it.
|
|
//!
|
|
//! A fixture is one propagated arc from another tool, with enough of that tool's setup
|
|
//! recorded alongside it to tell whether the comparison is fair. The format is this
|
|
//! crate's to define — a generator conforms to it, not the other way round — so it is
|
|
//! specified here and the loader is the specification's executable half.
|
|
//!
|
|
//! # Matching is the whole difficulty
|
|
//!
|
|
//! A disagreement between two propagators is only evidence of anything if they were asked
|
|
//! the same question. Every one of these has to agree before a number means anything, and
|
|
//! each has bitten someone:
|
|
//!
|
|
//! - **The gravity field, by name and truncation.** Not "EGM96 to degree 70" but the same
|
|
//! coefficient file: this crate found a 180x180 table that was not EGM96 and differed in
|
|
//! the fourth digit, which is 1.6 m/day of along-track at a low altitude.
|
|
//! - **The tide convention.** Tide-free against zero-tide moves `C(2,0)` alone, and that
|
|
//! term dominates everything.
|
|
//! - **The reference radius and `mu` the coefficients are defined against**, which are not
|
|
//! the body's WGS84 figures. This crate uses EGM96's own 6378136.3 m.
|
|
//! - **The third-body ephemeris**, by source and not only by body. DE440 against DE430 is
|
|
//! a real difference at the meter level over days.
|
|
//! - **Earth orientation.** Whether EOP corrections are applied at all, and from which
|
|
//! series, changes where the body-fixed frame points, which changes the gravity
|
|
//! gradient and the drag wind.
|
|
//! - **The atmosphere**, including which solar and geomagnetic drivers were read, since a
|
|
//! density model given different weather is a different model.
|
|
//! - **The drag and radiation coefficients and areas**, which tools variously take as
|
|
//! `Cd` and area separately or as the product.
|
|
//! - **The integrator and its tolerances**, since a comparison at 1 m has to be made
|
|
//! between two arcs each converged well inside that.
|
|
//!
|
|
//! The configuration block below is deliberately free-form text per entry rather than a
|
|
//! typed mirror of this crate's `Dynamics`: it records what the *other* tool was told, in
|
|
//! that tool's own vocabulary, so that a mismatch is visible rather than silently
|
|
//! translated away.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// One state along a reference arc.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct ReferenceState {
|
|
/// Seconds of uniform time after the arc's epoch.
|
|
pub elapsed: f64,
|
|
/// Position in the arc's frame, m.
|
|
pub position: [f64; 3],
|
|
/// Velocity in the arc's frame, m/s.
|
|
pub velocity: [f64; 3],
|
|
}
|
|
|
|
/// What produced a reference arc, in the producing tool's own terms.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct ReferenceSetup {
|
|
/// The tool, for example `orekit`.
|
|
pub tool: String,
|
|
/// Its version, exactly as the tool reports it.
|
|
pub version: String,
|
|
/// The date the fixture was generated, `YYYY-MM-DD`.
|
|
pub generated: String,
|
|
/// The force model as the tool was configured, one entry per force, in that tool's
|
|
/// vocabulary rather than translated into this crate's.
|
|
pub forces: Vec<String>,
|
|
/// The integrator and its tolerances, likewise verbatim.
|
|
pub integrator: String,
|
|
/// Anything else that had to be matched: Earth orientation, tide convention,
|
|
/// ephemeris source, and so on.
|
|
pub notes: Vec<String>,
|
|
}
|
|
|
|
/// One reference arc.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct ReferenceArc {
|
|
/// A short name, which is also the fixture's file stem.
|
|
pub name: String,
|
|
/// What produced it.
|
|
pub setup: ReferenceSetup,
|
|
/// Epoch of the first state, ISO 8601 with its time scale.
|
|
pub epoch: String,
|
|
/// The frame every state is expressed in, for example `EME2000`.
|
|
pub frame: String,
|
|
/// The states, earliest first, the first being the initial condition.
|
|
pub states: Vec<ReferenceState>,
|
|
}
|
|
|
|
/// Why a fixture could not be used.
|
|
#[derive(Debug)]
|
|
pub enum FixtureError {
|
|
/// The file could not be read.
|
|
Unreadable(std::io::Error),
|
|
/// The file is not the format.
|
|
Malformed(serde_json::Error),
|
|
/// The file parses but says something impossible.
|
|
Invalid(String),
|
|
}
|
|
|
|
impl std::fmt::Display for FixtureError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Unreadable(error) => write!(f, "could not read the fixture: {error}"),
|
|
Self::Malformed(error) => write!(f, "not a reference arc: {error}"),
|
|
Self::Invalid(why) => write!(f, "unusable reference arc: {why}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ReferenceArc {
|
|
/// Reads an arc, checking it says something usable.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// If the file is missing, is not the format, or is internally inconsistent.
|
|
pub fn load(path: &std::path::Path) -> Result<Self, FixtureError> {
|
|
let text = std::fs::read_to_string(path).map_err(FixtureError::Unreadable)?;
|
|
let arc: Self = serde_json::from_str(&text).map_err(FixtureError::Malformed)?;
|
|
arc.check()?;
|
|
Ok(arc)
|
|
}
|
|
|
|
/// The checks a fixture has to pass before it is worth comparing against.
|
|
///
|
|
/// Not style: each of these would otherwise show up as a physics disagreement much
|
|
/// later. An arc whose times run backwards, or which records no setup, produces a
|
|
/// number that looks like a result.
|
|
fn check(&self) -> Result<(), FixtureError> {
|
|
let invalid = |why: String| Err(FixtureError::Invalid(why));
|
|
if self.states.len() < 2 {
|
|
return invalid(format!("{}: {} states", self.name, self.states.len()));
|
|
}
|
|
if self.states[0].elapsed != 0.0 {
|
|
return invalid(format!(
|
|
"{}: starts at {} s rather than at its own epoch",
|
|
self.name, self.states[0].elapsed
|
|
));
|
|
}
|
|
if self.setup.forces.is_empty() {
|
|
return invalid(format!("{}: records no force model", self.name));
|
|
}
|
|
for pair in self.states.windows(2) {
|
|
if pair[1].elapsed <= pair[0].elapsed {
|
|
return invalid(format!(
|
|
"{}: times are not increasing, {} then {}",
|
|
self.name, pair[0].elapsed, pair[1].elapsed
|
|
));
|
|
}
|
|
}
|
|
for state in &self.states {
|
|
let finite = state
|
|
.position
|
|
.iter()
|
|
.chain(&state.velocity)
|
|
.all(|x| x.is_finite());
|
|
if !finite {
|
|
return invalid(format!("{}: a state is not finite", self.name));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// How long the arc covers, in seconds.
|
|
pub fn duration(&self) -> f64 {
|
|
self.states.last().map(|s| s.elapsed).unwrap_or(0.0)
|
|
}
|
|
}
|