use anise::analysis::prelude::OrbitalElement;
use anise::astro::PhysicsResult;
use anise::constants::frames::EARTH_J2000;
pub use anise::prelude::Orbit;
pub use anise::structure::spacecraft::{DragData, Mass, SRPData};
use der::{Decode, Encode, Enumerated, Reader};
use nalgebra::Vector3;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use typed_builder::TypedBuilder;
use super::{AstroPhysicsSnafu, BPlane, State};
use crate::cosmic::AstroAnalysisSnafu;
use crate::dynamics::DynamicsError;
use crate::dynamics::guidance::{LocalFrame, Thruster, plane_angles_from_unit_vector};
use crate::errors::{StateAstroSnafu, StateError};
use crate::io::ConfigRepr;
use crate::linalg::{Const, DimName, OMatrix, OVector};
use crate::md::StateParameter;
use crate::time::Epoch;
use crate::utils::{cartesian_to_spherical, spherical_to_cartesian};
use std::default::Default;
use std::fmt;
use std::ops::Add;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default, Enumerated)]
#[cfg_attr(feature = "python", pyclass(from_py_object))]
#[repr(u8)]
pub enum GuidanceMode {
#[default]
Coast = 0,
Thrust = 1,
Inhibit = 2,
}
impl From<f64> for GuidanceMode {
fn from(value: f64) -> Self {
if value >= 1.0 {
Self::Thrust
} else if value < 0.0 {
Self::Inhibit
} else {
Self::Coast
}
}
}
impl From<GuidanceMode> for f64 {
fn from(mode: GuidanceMode) -> f64 {
match mode {
GuidanceMode::Coast => 0.0,
GuidanceMode::Thrust => 1.0,
GuidanceMode::Inhibit => -1.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "python", pyclass(from_py_object))]
pub struct ThrustDirection {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl From<Vector3<f64>> for ThrustDirection {
fn from(vector: Vector3<f64>) -> Self {
Self {
x: vector[0] / vector.norm(),
y: vector[1] / vector.norm(),
z: vector[2] / vector.norm(),
}
}
}
impl From<ThrustDirection> for Vector3<f64> {
fn from(direction: ThrustDirection) -> Self {
Vector3::new(direction.x, direction.y, direction.z)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, TypedBuilder)]
#[cfg_attr(feature = "python", pyclass(from_py_object))]
pub struct Spacecraft {
pub orbit: Orbit,
#[builder(default)]
pub mass: Mass,
#[builder(default)]
#[serde(default)]
pub srp: SRPData,
#[builder(default)]
#[serde(default)]
pub drag: DragData,
#[builder(default, setter(strip_option))]
pub thruster: Option<Thruster>,
#[builder(default)]
#[serde(default)]
pub mode: GuidanceMode,
#[builder(default, setter(strip_option))]
#[serde(default)]
pub thrust_direction: Option<ThrustDirection>,
#[builder(default, setter(strip_option))]
#[serde(skip)]
pub stm: Option<OMatrix<f64, Const<9>, Const<9>>>,
}
impl Default for Spacecraft {
fn default() -> Self {
Self {
orbit: Orbit::zero(EARTH_J2000),
mass: Mass::default(),
srp: SRPData::default(),
drag: DragData::default(),
thruster: None,
mode: GuidanceMode::default(),
thrust_direction: None,
stm: None,
}
}
}
impl From<Orbit> for Spacecraft {
fn from(orbit: Orbit) -> Self {
Self::builder().orbit(orbit).build()
}
}
impl Spacecraft {
pub fn new(
orbit: Orbit,
dry_mass_kg: f64,
prop_mass_kg: f64,
srp_area_m2: f64,
drag_area_m2: f64,
coeff_reflectivity: f64,
coeff_drag: f64,
) -> Self {
Self {
orbit,
mass: Mass::from_dry_and_prop_masses(dry_mass_kg, prop_mass_kg),
srp: SRPData {
area_m2: srp_area_m2,
coeff_reflectivity,
},
drag: DragData {
area_m2: drag_area_m2,
coeff_drag,
},
..Default::default()
}
}
pub fn from_thruster(
orbit: Orbit,
dry_mass_kg: f64,
prop_mass_kg: f64,
thruster: Thruster,
mode: GuidanceMode,
) -> Self {
Self {
orbit,
mass: Mass::from_dry_and_prop_masses(dry_mass_kg, prop_mass_kg),
thruster: Some(thruster),
mode,
..Default::default()
}
}
pub fn from_srp_defaults(orbit: Orbit, dry_mass_kg: f64, srp_area_m2: f64) -> Self {
Self {
orbit,
mass: Mass::from_dry_mass(dry_mass_kg),
srp: SRPData::from_area(srp_area_m2),
..Default::default()
}
}
pub fn from_drag_defaults(orbit: Orbit, dry_mass_kg: f64, drag_area_m2: f64) -> Self {
Self {
orbit,
mass: Mass::from_dry_mass(dry_mass_kg),
drag: DragData::from_area(drag_area_m2),
..Default::default()
}
}
pub fn with_dv_km_s(mut self, dv_km_s: Vector3<f64>) -> Self {
self.orbit.apply_dv_km_s(dv_km_s);
self
}
pub fn with_dry_mass(mut self, dry_mass_kg: f64) -> Self {
self.mass.dry_mass_kg = dry_mass_kg;
self
}
pub fn with_prop_mass(mut self, prop_mass_kg: f64) -> Self {
self.mass.prop_mass_kg = prop_mass_kg;
self
}
pub fn with_srp(mut self, srp_area_m2: f64, coeff_reflectivity: f64) -> Self {
self.srp = SRPData {
area_m2: srp_area_m2,
coeff_reflectivity,
};
self
}
pub fn with_srp_area(mut self, srp_area_m2: f64) -> Self {
self.srp.area_m2 = srp_area_m2;
self
}
pub fn with_cr(mut self, coeff_reflectivity: f64) -> Self {
self.srp.coeff_reflectivity = coeff_reflectivity;
self
}
pub fn with_drag(mut self, drag_area_m2: f64, coeff_drag: f64) -> Self {
self.drag = DragData {
area_m2: drag_area_m2,
coeff_drag,
};
self
}
pub fn with_drag_area(mut self, drag_area_m2: f64) -> Self {
self.drag.area_m2 = drag_area_m2;
self
}
pub fn with_cd(mut self, coeff_drag: f64) -> Self {
self.drag.coeff_drag = coeff_drag;
self
}
pub fn with_orbit(mut self, orbit: Orbit) -> Self {
self.orbit = orbit;
self
}
pub fn enable_stm(&mut self) {
self.stm = Some(OMatrix::<f64, Const<9>, Const<9>>::identity());
}
pub fn mass_kg(&self) -> f64 {
self.mass.total_mass_kg()
}
pub fn with_guidance_mode(mut self, mode: GuidanceMode) -> Self {
self.mode = mode;
self
}
pub fn mode(&self) -> GuidanceMode {
self.mode
}
pub fn mut_mode(&mut self, mode: GuidanceMode) {
self.mode = mode;
}
pub fn thrust_direction(&self) -> Option<Vector3<f64>> {
self.thrust_direction.map(Into::into)
}
pub fn mut_thrust_direction(&mut self, direction: Option<Vector3<f64>>) {
self.thrust_direction = direction.map(Into::into);
}
pub fn thrust_angles_deg(&self, frame: LocalFrame) -> PhysicsResult<Option<(f64, f64)>> {
if let Some(thrust_dir_inertial) = self.thrust_direction() {
let dcm_local_to_inertial = self.orbit.dcm_to_inertial(frame)?;
let thrust_dir_local = dcm_local_to_inertial.transpose() * thrust_dir_inertial;
let (in_plane_rad, out_of_plane_rad) = plane_angles_from_unit_vector(thrust_dir_local);
Ok(Some((
in_plane_rad.to_degrees(),
out_of_plane_rad.to_degrees(),
)))
} else {
Ok(None)
}
}
}
#[cfg_attr(feature = "python", pymethods)]
impl Spacecraft {
pub fn rss(&self, other: &Self) -> PhysicsResult<(f64, f64, f64)> {
let rss_p_km = self.orbit.rss_radius_km(&other.orbit)?;
let rss_v_km_s = self.orbit.rss_velocity_km_s(&other.orbit)?;
let rss_prop_kg = (self.mass.prop_mass_kg - other.mass.prop_mass_kg)
.powi(2)
.sqrt();
Ok((rss_p_km, rss_v_km_s, rss_prop_kg))
}
}
impl PartialEq for Spacecraft {
fn eq(&self, other: &Self) -> bool {
let mass_tol = 1e-6; self.orbit.eq_within(&other.orbit, 1e-9, 1e-12)
&& (self.mass - other.mass).abs().total_mass_kg() < mass_tol
&& self.srp == other.srp
&& self.drag == other.drag
}
}
#[allow(clippy::format_in_format_args)]
impl fmt::Display for Spacecraft {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mass_prec = f.precision().unwrap_or(3);
let orbit_prec = f.precision().unwrap_or(6);
write!(
f,
"total mass = {} kg @ {} {:?}",
format!("{:.*}", mass_prec, self.mass.total_mass_kg()),
format!("{:.*}", orbit_prec, self.orbit),
self.mode,
)
}
}
#[allow(clippy::format_in_format_args)]
impl fmt::LowerExp for Spacecraft {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mass_prec = f.precision().unwrap_or(3);
let orbit_prec = f.precision().unwrap_or(6);
write!(
f,
"total mass = {} kg @ {} {:?}",
format!("{:.*e}", mass_prec, self.mass.total_mass_kg()),
format!("{:.*e}", orbit_prec, self.orbit),
self.mode,
)
}
}
#[allow(clippy::format_in_format_args)]
impl fmt::LowerHex for Spacecraft {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mass_prec = f.precision().unwrap_or(3);
let orbit_prec = f.precision().unwrap_or(6);
write!(
f,
"total mass = {} kg @ {} {:?}",
format!("{:.*}", mass_prec, self.mass.total_mass_kg()),
format!("{:.*x}", orbit_prec, self.orbit),
self.mode,
)
}
}
#[allow(clippy::format_in_format_args)]
impl fmt::UpperHex for Spacecraft {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mass_prec = f.precision().unwrap_or(3);
let orbit_prec = f.precision().unwrap_or(6);
write!(
f,
"total mass = {} kg @ {} {:?}",
format!("{:.*e}", mass_prec, self.mass.total_mass_kg()),
format!("{:.*X}", orbit_prec, self.orbit),
self.mode,
)
}
}
impl State for Spacecraft {
type Size = Const<9>;
type VecLength = Const<90>;
fn with_stm(mut self) -> Self {
self.enable_stm();
self
}
fn reset_stm(&mut self) {
self.stm = Some(OMatrix::<f64, Const<9>, Const<9>>::identity());
}
fn zeros() -> Self {
Self::default()
}
fn to_vector(&self) -> OVector<f64, Const<90>> {
let mut vector = OVector::<f64, Const<90>>::zeros();
for (i, val) in self.orbit.radius_km.iter().enumerate() {
vector[i] = *val;
}
for (i, val) in self.orbit.velocity_km_s.iter().enumerate() {
vector[i + 3] = *val;
}
vector[6] = self.srp.coeff_reflectivity;
vector[7] = self.drag.coeff_drag;
vector[8] = self.mass.prop_mass_kg;
if let Some(stm) = self.stm {
for (idx, stm_val) in stm.as_slice().iter().enumerate() {
vector[idx + Self::Size::dim()] = *stm_val;
}
}
vector
}
fn set(&mut self, epoch: Epoch, vector: &OVector<f64, Const<90>>) {
let sc_state =
OVector::<f64, Self::Size>::from_column_slice(&vector.as_slice()[..Self::Size::dim()]);
if self.stm.is_some() {
let sc_full_stm = OMatrix::<f64, Self::Size, Self::Size>::from_column_slice(
&vector.as_slice()[Self::Size::dim()..],
);
self.stm = Some(sc_full_stm);
}
let radius_km = sc_state.fixed_rows::<3>(0).into_owned();
let vel_km_s = sc_state.fixed_rows::<3>(3).into_owned();
self.orbit.epoch = epoch;
self.orbit.radius_km = radius_km;
self.orbit.velocity_km_s = vel_km_s;
self.srp.coeff_reflectivity = sc_state[6].clamp(0.0, 2.0);
self.drag.coeff_drag = sc_state[7];
self.mass.prop_mass_kg = sc_state[8];
}
fn stm(&self) -> Result<OMatrix<f64, Self::Size, Self::Size>, DynamicsError> {
match self.stm {
Some(stm) => Ok(stm),
None => Err(DynamicsError::StateTransitionMatrixUnset),
}
}
fn epoch(&self) -> Epoch {
self.orbit.epoch
}
fn set_epoch(&mut self, epoch: Epoch) {
self.orbit.epoch = epoch
}
fn add(self, other: OVector<f64, Self::Size>) -> Self {
self + other
}
fn value(&self, param: StateParameter) -> Result<f64, StateError> {
match param {
StateParameter::Cd() => Ok(self.drag.coeff_drag),
StateParameter::Cr() => Ok(self.srp.coeff_reflectivity),
StateParameter::DryMass() => Ok(self.mass.dry_mass_kg),
StateParameter::PropMass() => Ok(self.mass.prop_mass_kg),
StateParameter::TotalMass() => Ok(self.mass.total_mass_kg()),
StateParameter::Isp() => match self.thruster {
Some(thruster) => Ok(thruster.isp_s),
None => Err(StateError::NoThrusterAvail),
},
StateParameter::ThrustX() => self
.thrust_direction()
.map(|direction| direction[0])
.ok_or(StateError::Unavailable { param }),
StateParameter::ThrustY() => self
.thrust_direction()
.map(|direction| direction[1])
.ok_or(StateError::Unavailable { param }),
StateParameter::ThrustZ() => self
.thrust_direction()
.map(|direction| direction[2])
.ok_or(StateError::Unavailable { param }),
StateParameter::ThrustInPlane(frame) => self
.thrust_angles_deg(frame)
.context(AstroPhysicsSnafu)
.context(StateAstroSnafu { param })?
.map(|(in_plane_deg, _)| in_plane_deg)
.ok_or(StateError::Unavailable { param }),
StateParameter::ThrustOutOfPlane(frame) => self
.thrust_angles_deg(frame)
.context(AstroPhysicsSnafu)
.context(StateAstroSnafu { param })?
.map(|(_, out_of_plane_deg)| out_of_plane_deg)
.ok_or(StateError::Unavailable { param }),
StateParameter::Thrust() => match self.thruster {
Some(thruster) => Ok(thruster.thrust_N),
None => Err(StateError::NoThrusterAvail),
},
StateParameter::GuidanceMode() => Ok(self.mode.into()),
StateParameter::Element(e) => e
.evaluate(self.orbit)
.context(AstroAnalysisSnafu)
.context(StateAstroSnafu { param }),
StateParameter::BdotR() => Ok(BPlane::new(self.orbit)
.context(StateAstroSnafu { param })?
.b_r_km
.real()),
StateParameter::BdotT() => Ok(BPlane::new(self.orbit)
.context(StateAstroSnafu { param })?
.b_t_km
.real()),
StateParameter::BLTOF() => Ok(BPlane::new(self.orbit)
.context(StateAstroSnafu { param })?
.ltof_s
.real()),
_ => Err(StateError::Unavailable { param }),
}
}
fn set_value(&mut self, param: StateParameter, val: f64) -> Result<(), StateError> {
match param {
StateParameter::Cd() => self.drag.coeff_drag = val,
StateParameter::Cr() => self.srp.coeff_reflectivity = val,
StateParameter::PropMass() => self.mass.prop_mass_kg = val,
StateParameter::DryMass() => self.mass.dry_mass_kg = val,
StateParameter::Isp() => match self.thruster {
Some(ref mut thruster) => thruster.isp_s = val,
None => return Err(StateError::NoThrusterAvail),
},
StateParameter::ThrustX() => {
let mut direction = self.thrust_direction();
if direction.is_none() {
direction = Some(Vector3::zeros());
}
if let Some(mut direction) = direction {
direction[0] = val;
self.mut_thrust_direction(Some(direction));
}
}
StateParameter::ThrustY() => {
let mut direction = self.thrust_direction();
if direction.is_none() {
direction = Some(Vector3::zeros());
}
if let Some(mut direction) = direction {
direction[1] = val;
self.mut_thrust_direction(Some(direction));
}
}
StateParameter::ThrustZ() => {
let mut direction = self.thrust_direction();
if direction.is_none() {
direction = Some(Vector3::zeros());
}
if let Some(mut direction) = direction {
direction[2] = val;
self.mut_thrust_direction(Some(direction));
}
}
StateParameter::ThrustInPlane(_) | StateParameter::ThrustOutOfPlane(_) => {
return Err(StateError::ReadOnly { param });
}
StateParameter::Thrust() => match self.thruster {
Some(ref mut thruster) => thruster.thrust_N = val,
None => return Err(StateError::NoThrusterAvail),
},
StateParameter::Element(el) => {
match el {
OrbitalElement::AoP => self
.orbit
.set_aop_deg(val)
.context(AstroPhysicsSnafu)
.context(StateAstroSnafu { param })?,
OrbitalElement::Eccentricity => self
.orbit
.set_ecc(val)
.context(AstroPhysicsSnafu)
.context(StateAstroSnafu { param })?,
OrbitalElement::Inclination => self
.orbit
.set_inc_deg(val)
.context(AstroPhysicsSnafu)
.context(StateAstroSnafu { param })?,
OrbitalElement::RAAN => self
.orbit
.set_raan_deg(val)
.context(AstroPhysicsSnafu)
.context(StateAstroSnafu { param })?,
OrbitalElement::SemiMajorAxis => self
.orbit
.set_sma_km(val)
.context(AstroPhysicsSnafu)
.context(StateAstroSnafu { param })?,
OrbitalElement::TrueAnomaly => self
.orbit
.set_ta_deg(val)
.context(AstroPhysicsSnafu)
.context(StateAstroSnafu { param })?,
OrbitalElement::X => self.orbit.radius_km.x = val,
OrbitalElement::Y => self.orbit.radius_km.y = val,
OrbitalElement::Z => self.orbit.radius_km.z = val,
OrbitalElement::Rmag => {
let (_, θ, φ) = cartesian_to_spherical(&self.orbit.radius_km);
self.orbit.radius_km = spherical_to_cartesian(val, θ, φ);
}
OrbitalElement::VX => self.orbit.velocity_km_s.x = val,
OrbitalElement::VY => self.orbit.velocity_km_s.y = val,
OrbitalElement::VZ => self.orbit.velocity_km_s.z = val,
OrbitalElement::Vmag => {
let (_, θ, φ) = cartesian_to_spherical(&self.orbit.velocity_km_s);
self.orbit.velocity_km_s = spherical_to_cartesian(val, θ, φ);
}
_ => return Err(StateError::ReadOnly { param }),
}
}
_ => return Err(StateError::ReadOnly { param }),
}
Ok(())
}
fn unset_stm(&mut self) {
self.stm = None;
}
fn orbit(&self) -> Orbit {
self.orbit
}
fn set_orbit(&mut self, orbit: Orbit) {
self.orbit = orbit;
}
}
impl Add<OVector<f64, Const<6>>> for Spacecraft {
type Output = Self;
fn add(mut self, other: OVector<f64, Const<6>>) -> Self {
let radius_km = other.fixed_rows::<3>(0).into_owned();
let vel_km_s = other.fixed_rows::<3>(3).into_owned();
self.orbit.radius_km += radius_km;
self.orbit.velocity_km_s += vel_km_s;
self
}
}
impl Add<OVector<f64, Const<9>>> for Spacecraft {
type Output = Self;
fn add(mut self, other: OVector<f64, Const<9>>) -> Self {
let radius_km = other.fixed_rows::<3>(0).into_owned();
let vel_km_s = other.fixed_rows::<3>(3).into_owned();
self.orbit.radius_km += radius_km;
self.orbit.velocity_km_s += vel_km_s;
self.srp.coeff_reflectivity = (self.srp.coeff_reflectivity + other[6]).clamp(0.0, 2.0);
self.drag.coeff_drag += other[7];
self.mass.prop_mass_kg += other[8];
self
}
}
impl<'a> Decode<'a> for Spacecraft {
fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
let orbit = decoder.decode()?;
let mass = decoder.decode()?;
let srp = decoder.decode()?;
let drag = decoder.decode()?;
let mode = decoder.decode()?;
let thruster = if decoder.decode::<bool>()? {
Some(decoder.decode()?)
} else {
None
};
Ok(Spacecraft {
orbit,
mass,
srp,
drag,
thruster,
mode,
thrust_direction: None,
stm: None,
})
}
}
impl Encode for Spacecraft {
fn encoded_len(&self) -> der::Result<der::Length> {
self.orbit.encoded_len()?
+ self.mass.encoded_len()?
+ self.srp.encoded_len()?
+ self.drag.encoded_len()?
+ self.mode.encoded_len()?
+ self.thruster.is_some().encoded_len()?
+ self.thruster.encoded_len()?
}
fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
self.orbit.encode(encoder)?;
self.mass.encode(encoder)?;
self.srp.encode(encoder)?;
self.drag.encode(encoder)?;
self.mode.encode(encoder)?;
if let Some(thruster) = self.thruster {
true.encode(encoder)?;
thruster.encode(encoder)?;
} else {
false.encode(encoder)?
};
Ok(())
}
}
impl ConfigRepr for Spacecraft {}
#[test]
fn test_serde() {
use serde_yml;
use std::str::FromStr;
use anise::constants::frames::EARTH_J2000;
let orbit = Orbit::new(
-9042.862234,
18536.333069,
6999.957069,
-3.288789,
-2.226285,
1.646738,
Epoch::from_str("2018-09-15T00:15:53.098 UTC").unwrap(),
EARTH_J2000,
);
let sc = Spacecraft::new(orbit, 500.0, 159.0, 2.0, 2.0, 1.8, 2.2);
let serialized_sc = serde_yml::to_string(&sc).unwrap();
println!("{serialized_sc}");
let deser_sc: Spacecraft = serde_yml::from_str(&serialized_sc).unwrap();
assert_eq!(sc, deser_sc);
let s = r#"
orbit:
radius_km:
- -9042.862234
- 18536.333069
- 6999.957069
velocity_km_s:
- -3.288789
- -2.226285
- 1.646738
epoch: 2018-09-15T00:15:53.098000000 UTC
frame:
ephemeris_id: 399
orientation_id: 1
force_inertial: false
mu_km3_s2: null
shape: null
frozen_epoch: null
mass:
dry_mass_kg: 500.0
prop_mass_kg: 159.0
extra_mass_kg: 0.0
srp:
area_m2: 2.0
coeff_reflectivity: 1.8
drag:
area_m2: 2.0
coeff_drag: 2.2
"#;
let deser_sc: Spacecraft = serde_yml::from_str(s).unwrap();
assert_eq!(sc, deser_sc);
let s = r#"
orbit:
radius_km:
- -9042.862234
- 18536.333069
- 6999.957069
velocity_km_s:
- -3.288789
- -2.226285
- 1.646738
epoch: 2018-09-15T00:15:53.098000000 UTC
frame:
ephemeris_id: 399
orientation_id: 1
force_inertial: false
mu_km3_s2: null
shape: null
frozen_epoch: null
mass:
dry_mass_kg: 500.0
prop_mass_kg: 159.0
extra_mass_kg: 0.0
srp:
area_m2: 2.0
coeff_reflectivity: 1.8
drag:
area_m2: 2.0
coeff_drag: 2.2
thruster:
thrust_N: 1e-5
isp_s: 300.5
"#;
let mut sc_thruster = sc;
sc_thruster.thruster = Some(Thruster {
isp_s: 300.5,
thrust_N: 1e-5,
});
let deser_sc: Spacecraft = serde_yml::from_str(s).unwrap();
assert_eq!(sc_thruster, deser_sc);
let s = r#"
orbit:
radius_km:
- -9042.862234
- 18536.333069
- 6999.957069
velocity_km_s:
- -3.288789
- -2.226285
- 1.646738
epoch: 2018-09-15T00:15:53.098000000 UTC
frame:
ephemeris_id: 399
orientation_id: 1
force_inertial: false
mu_km3_s2: null
shape: null
frozen_epoch: null
mass:
dry_mass_kg: 500.0
prop_mass_kg: 159.0
extra_mass_kg: 0.0
"#;
let deser_sc: Spacecraft = serde_yml::from_str(s).unwrap();
let sc = Spacecraft::new(orbit, 500.0, 159.0, 0.0, 0.0, 1.8, 2.2);
assert_eq!(sc, deser_sc);
}