Skip to main content

nyx_space/cosmic/
spacecraft.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use anise::analysis::prelude::OrbitalElement;
20use anise::astro::PhysicsResult;
21use anise::constants::frames::EARTH_J2000;
22pub use anise::prelude::Orbit;
23
24pub use anise::structure::spacecraft::{DragData, Mass, SRPData};
25use der::{Decode, Encode, Enumerated, Reader};
26use nalgebra::Vector3;
27use serde::{Deserialize, Serialize};
28use snafu::ResultExt;
29use typed_builder::TypedBuilder;
30
31use super::{AstroPhysicsSnafu, BPlane, State};
32use crate::cosmic::AstroAnalysisSnafu;
33use crate::dynamics::guidance::Thruster;
34use crate::dynamics::DynamicsError;
35use crate::errors::{StateAstroSnafu, StateError};
36use crate::io::ConfigRepr;
37use crate::linalg::{Const, DimName, OMatrix, OVector};
38use crate::md::StateParameter;
39use crate::time::Epoch;
40use crate::utils::{cartesian_to_spherical, spherical_to_cartesian};
41
42use std::default::Default;
43use std::fmt;
44use std::ops::Add;
45
46#[cfg(feature = "python")]
47use pyo3::prelude::*;
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default, Enumerated)]
50#[cfg_attr(feature = "python", pyclass)]
51#[repr(u8)]
52pub enum GuidanceMode {
53    /// Guidance is turned off and Guidance Law may switch mode to Thrust for next call
54    #[default]
55    Coast = 0,
56    /// Guidance is turned on and Guidance Law may switch mode to Coast for next call
57    Thrust = 1,
58    /// Guidance is turned off and Guidance Law may not change its mode (will need to be done externally to the guidance law).
59    Inhibit = 2,
60}
61
62impl From<f64> for GuidanceMode {
63    fn from(value: f64) -> Self {
64        if value >= 1.0 {
65            Self::Thrust
66        } else if value < 0.0 {
67            Self::Inhibit
68        } else {
69            Self::Coast
70        }
71    }
72}
73
74impl From<GuidanceMode> for f64 {
75    fn from(mode: GuidanceMode) -> f64 {
76        match mode {
77            GuidanceMode::Coast => 0.0,
78            GuidanceMode::Thrust => 1.0,
79            GuidanceMode::Inhibit => -1.0,
80        }
81    }
82}
83
84/// A spacecraft state, composed of its orbit, its masses (dry, prop, extra, all in kg), its SRP configuration, its drag configuration, its thruster configuration, and its guidance mode.
85///
86/// Optionally, the spacecraft state can also store the state transition matrix from the start of the propagation until the current time (i.e. trajectory STM, not step-size STM).
87#[derive(Clone, Copy, Debug, Serialize, Deserialize, TypedBuilder)]
88#[cfg_attr(feature = "python", pyclass)]
89pub struct Spacecraft {
90    /// Initial orbit of the vehicle
91    pub orbit: Orbit,
92    /// Dry, propellant, and extra masses
93    #[builder(default)]
94    pub mass: Mass,
95    /// Solar Radiation Pressure configuration for this spacecraft
96    #[builder(default)]
97    #[serde(default)]
98    pub srp: SRPData,
99    #[builder(default)]
100    #[serde(default)]
101    pub drag: DragData,
102    #[builder(default, setter(strip_option))]
103    pub thruster: Option<Thruster>,
104    /// Any extra information or extension that is needed for specific guidance laws
105    #[builder(default)]
106    #[serde(default)]
107    pub mode: GuidanceMode,
108    /// Optionally stores the state transition matrix from the start of the propagation until the current time (i.e. trajectory STM, not step-size STM)
109    /// STM is contains position and velocity, Cr, Cd, prop mass
110    #[builder(default, setter(strip_option))]
111    #[serde(skip)]
112    pub stm: Option<OMatrix<f64, Const<9>, Const<9>>>,
113}
114
115impl Default for Spacecraft {
116    fn default() -> Self {
117        Self {
118            orbit: Orbit::zero(EARTH_J2000),
119            mass: Mass::default(),
120            srp: SRPData::default(),
121            drag: DragData::default(),
122            thruster: None,
123            mode: GuidanceMode::default(),
124            stm: None,
125        }
126    }
127}
128
129impl From<Orbit> for Spacecraft {
130    fn from(orbit: Orbit) -> Self {
131        Self::builder().orbit(orbit).build()
132    }
133}
134
135impl Spacecraft {
136    /// Initialize a spacecraft state from all of its parameters
137    pub fn new(
138        orbit: Orbit,
139        dry_mass_kg: f64,
140        prop_mass_kg: f64,
141        srp_area_m2: f64,
142        drag_area_m2: f64,
143        coeff_reflectivity: f64,
144        coeff_drag: f64,
145    ) -> Self {
146        Self {
147            orbit,
148            mass: Mass::from_dry_and_prop_masses(dry_mass_kg, prop_mass_kg),
149            srp: SRPData {
150                area_m2: srp_area_m2,
151                coeff_reflectivity,
152            },
153            drag: DragData {
154                area_m2: drag_area_m2,
155                coeff_drag,
156            },
157            ..Default::default()
158        }
159    }
160
161    /// Initialize a spacecraft state from only a thruster and mass. Use this when designing guidance laws while ignoring drag and SRP.
162    pub fn from_thruster(
163        orbit: Orbit,
164        dry_mass_kg: f64,
165        prop_mass_kg: f64,
166        thruster: Thruster,
167        mode: GuidanceMode,
168    ) -> Self {
169        Self {
170            orbit,
171            mass: Mass::from_dry_and_prop_masses(dry_mass_kg, prop_mass_kg),
172            thruster: Some(thruster),
173            mode,
174            ..Default::default()
175        }
176    }
177
178    /// Initialize a spacecraft state from the SRP default 1.8 for coefficient of reflectivity (prop mass and drag parameters nullified!)
179    pub fn from_srp_defaults(orbit: Orbit, dry_mass_kg: f64, srp_area_m2: f64) -> Self {
180        Self {
181            orbit,
182            mass: Mass::from_dry_mass(dry_mass_kg),
183            srp: SRPData::from_area(srp_area_m2),
184            ..Default::default()
185        }
186    }
187
188    /// Initialize a spacecraft state from the SRP default 1.8 for coefficient of drag (prop mass and SRP parameters nullified!)
189    pub fn from_drag_defaults(orbit: Orbit, dry_mass_kg: f64, drag_area_m2: f64) -> Self {
190        Self {
191            orbit,
192            mass: Mass::from_dry_mass(dry_mass_kg),
193            drag: DragData::from_area(drag_area_m2),
194            ..Default::default()
195        }
196    }
197
198    pub fn with_dv_km_s(mut self, dv_km_s: Vector3<f64>) -> Self {
199        self.orbit.apply_dv_km_s(dv_km_s);
200        self
201    }
202
203    /// Returns a copy of the state with a new dry mass
204    pub fn with_dry_mass(mut self, dry_mass_kg: f64) -> Self {
205        self.mass.dry_mass_kg = dry_mass_kg;
206        self
207    }
208
209    /// Returns a copy of the state with a new prop mass
210    pub fn with_prop_mass(mut self, prop_mass_kg: f64) -> Self {
211        self.mass.prop_mass_kg = prop_mass_kg;
212        self
213    }
214
215    /// Returns a copy of the state with a new SRP area and CR
216    pub fn with_srp(mut self, srp_area_m2: f64, coeff_reflectivity: f64) -> Self {
217        self.srp = SRPData {
218            area_m2: srp_area_m2,
219            coeff_reflectivity,
220        };
221
222        self
223    }
224
225    /// Returns a copy of the state with a new SRP area
226    pub fn with_srp_area(mut self, srp_area_m2: f64) -> Self {
227        self.srp.area_m2 = srp_area_m2;
228        self
229    }
230
231    /// Returns a copy of the state with a new coefficient of reflectivity
232    pub fn with_cr(mut self, coeff_reflectivity: f64) -> Self {
233        self.srp.coeff_reflectivity = coeff_reflectivity;
234        self
235    }
236
237    /// Returns a copy of the state with a new drag area and CD
238    pub fn with_drag(mut self, drag_area_m2: f64, coeff_drag: f64) -> Self {
239        self.drag = DragData {
240            area_m2: drag_area_m2,
241            coeff_drag,
242        };
243        self
244    }
245
246    /// Returns a copy of the state with a new SRP area
247    pub fn with_drag_area(mut self, drag_area_m2: f64) -> Self {
248        self.drag.area_m2 = drag_area_m2;
249        self
250    }
251
252    /// Returns a copy of the state with a new coefficient of drag
253    pub fn with_cd(mut self, coeff_drag: f64) -> Self {
254        self.drag.coeff_drag = coeff_drag;
255        self
256    }
257
258    /// Returns a copy of the state with a new orbit
259    pub fn with_orbit(mut self, orbit: Orbit) -> Self {
260        self.orbit = orbit;
261        self
262    }
263
264    /// Sets the STM of this state of identity, which also enables computation of the STM for spacecraft navigation
265    pub fn enable_stm(&mut self) {
266        self.stm = Some(OMatrix::<f64, Const<9>, Const<9>>::identity());
267    }
268
269    /// Returns the total mass in kilograms
270    pub fn mass_kg(&self) -> f64 {
271        self.mass.total_mass_kg()
272    }
273
274    /// Returns a copy of the state with the provided guidance mode
275    pub fn with_guidance_mode(mut self, mode: GuidanceMode) -> Self {
276        self.mode = mode;
277        self
278    }
279
280    pub fn mode(&self) -> GuidanceMode {
281        self.mode
282    }
283
284    pub fn mut_mode(&mut self, mode: GuidanceMode) {
285        self.mode = mode;
286    }
287}
288
289#[cfg_attr(feature = "python", pymethods)]
290impl Spacecraft {
291    /// Returns the root sum square error between this spacecraft and the other, in kilometers for the position, kilometers per second in velocity, and kilograms in prop
292    pub fn rss(&self, other: &Self) -> PhysicsResult<(f64, f64, f64)> {
293        let rss_p_km = self.orbit.rss_radius_km(&other.orbit)?;
294        let rss_v_km_s = self.orbit.rss_velocity_km_s(&other.orbit)?;
295        let rss_prop_kg = (self.mass.prop_mass_kg - other.mass.prop_mass_kg)
296            .powi(2)
297            .sqrt();
298
299        Ok((rss_p_km, rss_v_km_s, rss_prop_kg))
300    }
301}
302
303impl PartialEq for Spacecraft {
304    fn eq(&self, other: &Self) -> bool {
305        let mass_tol = 1e-6; // milligram
306        self.orbit.eq_within(&other.orbit, 1e-9, 1e-12)
307            && (self.mass - other.mass).abs().total_mass_kg() < mass_tol
308            && self.srp == other.srp
309            && self.drag == other.drag
310    }
311}
312
313#[allow(clippy::format_in_format_args)]
314impl fmt::Display for Spacecraft {
315    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316        let mass_prec = f.precision().unwrap_or(3);
317        let orbit_prec = f.precision().unwrap_or(6);
318        write!(
319            f,
320            "total mass = {} kg @  {}  {:?}",
321            format!("{:.*}", mass_prec, self.mass.total_mass_kg()),
322            format!("{:.*}", orbit_prec, self.orbit),
323            self.mode,
324        )
325    }
326}
327
328#[allow(clippy::format_in_format_args)]
329impl fmt::LowerExp for Spacecraft {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        let mass_prec = f.precision().unwrap_or(3);
332        let orbit_prec = f.precision().unwrap_or(6);
333        write!(
334            f,
335            "total mass = {} kg @  {}  {:?}",
336            format!("{:.*e}", mass_prec, self.mass.total_mass_kg()),
337            format!("{:.*e}", orbit_prec, self.orbit),
338            self.mode,
339        )
340    }
341}
342
343#[allow(clippy::format_in_format_args)]
344impl fmt::LowerHex for Spacecraft {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        let mass_prec = f.precision().unwrap_or(3);
347        let orbit_prec = f.precision().unwrap_or(6);
348        write!(
349            f,
350            "total mass = {} kg @  {}  {:?}",
351            format!("{:.*}", mass_prec, self.mass.total_mass_kg()),
352            format!("{:.*x}", orbit_prec, self.orbit),
353            self.mode,
354        )
355    }
356}
357
358#[allow(clippy::format_in_format_args)]
359impl fmt::UpperHex for Spacecraft {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        let mass_prec = f.precision().unwrap_or(3);
362        let orbit_prec = f.precision().unwrap_or(6);
363        write!(
364            f,
365            "total mass = {} kg @  {}  {:?}",
366            format!("{:.*e}", mass_prec, self.mass.total_mass_kg()),
367            format!("{:.*X}", orbit_prec, self.orbit),
368            self.mode,
369        )
370    }
371}
372
373impl State for Spacecraft {
374    type Size = Const<9>;
375    type VecLength = Const<90>;
376
377    /// Copies the current state but sets the STM to identity
378    fn with_stm(mut self) -> Self {
379        self.enable_stm();
380        self
381    }
382
383    fn reset_stm(&mut self) {
384        self.stm = Some(OMatrix::<f64, Const<9>, Const<9>>::identity());
385    }
386
387    fn zeros() -> Self {
388        Self::default()
389    }
390
391    /// The vector is organized as such:
392    /// [X, Y, Z, Vx, Vy, Vz, Cr, Cd, Fuel mass, STM(9x9)]
393    fn to_vector(&self) -> OVector<f64, Const<90>> {
394        let mut vector = OVector::<f64, Const<90>>::zeros();
395        // Set the orbit state info
396        for (i, val) in self.orbit.radius_km.iter().enumerate() {
397            // Place the orbit state first, then skip three (Cr, Cd, Fuel), then copy orbit STM
398            vector[i] = *val;
399        }
400        for (i, val) in self.orbit.velocity_km_s.iter().enumerate() {
401            // Place the orbit state first, then skip three (Cr, Cd, Fuel), then copy orbit STM
402            vector[i + 3] = *val;
403        }
404        // Set the spacecraft parameters
405        vector[6] = self.srp.coeff_reflectivity;
406        vector[7] = self.drag.coeff_drag;
407        vector[8] = self.mass.prop_mass_kg;
408        // Add the STM to the vector
409        if let Some(stm) = self.stm {
410            for (idx, stm_val) in stm.as_slice().iter().enumerate() {
411                vector[idx + Self::Size::dim()] = *stm_val;
412            }
413        }
414        vector
415    }
416
417    /// Vector is expected to be organized as such:
418    /// [X, Y, Z, Vx, Vy, Vz, Cr, Cd, Fuel mass, STM(9x9)]
419    fn set(&mut self, epoch: Epoch, vector: &OVector<f64, Const<90>>) {
420        let sc_state =
421            OVector::<f64, Self::Size>::from_column_slice(&vector.as_slice()[..Self::Size::dim()]);
422
423        if self.stm.is_some() {
424            let sc_full_stm = OMatrix::<f64, Self::Size, Self::Size>::from_column_slice(
425                &vector.as_slice()[Self::Size::dim()..],
426            );
427
428            self.stm = Some(sc_full_stm);
429        }
430
431        let radius_km = sc_state.fixed_rows::<3>(0).into_owned();
432        let vel_km_s = sc_state.fixed_rows::<3>(3).into_owned();
433        self.orbit.epoch = epoch;
434        self.orbit.radius_km = radius_km;
435        self.orbit.velocity_km_s = vel_km_s;
436        self.srp.coeff_reflectivity = sc_state[6].clamp(0.0, 2.0);
437        self.drag.coeff_drag = sc_state[7];
438        self.mass.prop_mass_kg = sc_state[8];
439    }
440
441    /// diag(STM) = [X,Y,Z,Vx,Vy,Vz,Cr,Cd,Fuel]
442    /// WARNING: Currently the STM assumes that the prop mass is constant at ALL TIMES!
443    fn stm(&self) -> Result<OMatrix<f64, Self::Size, Self::Size>, DynamicsError> {
444        match self.stm {
445            Some(stm) => Ok(stm),
446            None => Err(DynamicsError::StateTransitionMatrixUnset),
447        }
448    }
449
450    fn epoch(&self) -> Epoch {
451        self.orbit.epoch
452    }
453
454    fn set_epoch(&mut self, epoch: Epoch) {
455        self.orbit.epoch = epoch
456    }
457
458    fn add(self, other: OVector<f64, Self::Size>) -> Self {
459        self + other
460    }
461
462    fn value(&self, param: StateParameter) -> Result<f64, StateError> {
463        match param {
464            StateParameter::Cd() => Ok(self.drag.coeff_drag),
465            StateParameter::Cr() => Ok(self.srp.coeff_reflectivity),
466            StateParameter::DryMass() => Ok(self.mass.dry_mass_kg),
467            StateParameter::PropMass() => Ok(self.mass.prop_mass_kg),
468            StateParameter::TotalMass() => Ok(self.mass.total_mass_kg()),
469            StateParameter::Isp() => match self.thruster {
470                Some(thruster) => Ok(thruster.isp_s),
471                None => Err(StateError::NoThrusterAvail),
472            },
473            StateParameter::Thrust() => match self.thruster {
474                Some(thruster) => Ok(thruster.thrust_N),
475                None => Err(StateError::NoThrusterAvail),
476            },
477            StateParameter::GuidanceMode() => Ok(self.mode.into()),
478            StateParameter::Element(e) => e
479                .evaluate(self.orbit)
480                .context(AstroAnalysisSnafu)
481                .context(StateAstroSnafu { param }),
482            StateParameter::BdotR() => Ok(BPlane::new(self.orbit)
483                .context(StateAstroSnafu { param })?
484                .b_r_km
485                .real()),
486            StateParameter::BdotT() => Ok(BPlane::new(self.orbit)
487                .context(StateAstroSnafu { param })?
488                .b_t_km
489                .real()),
490            StateParameter::BLTOF() => Ok(BPlane::new(self.orbit)
491                .context(StateAstroSnafu { param })?
492                .ltof_s
493                .real()),
494            _ => Err(StateError::Unavailable { param }),
495        }
496    }
497
498    fn set_value(&mut self, param: StateParameter, val: f64) -> Result<(), StateError> {
499        match param {
500            StateParameter::Cd() => self.drag.coeff_drag = val,
501            StateParameter::Cr() => self.srp.coeff_reflectivity = val,
502            StateParameter::PropMass() => self.mass.prop_mass_kg = val,
503            StateParameter::DryMass() => self.mass.dry_mass_kg = val,
504            StateParameter::Isp() => match self.thruster {
505                Some(ref mut thruster) => thruster.isp_s = val,
506                None => return Err(StateError::NoThrusterAvail),
507            },
508            StateParameter::Thrust() => match self.thruster {
509                Some(ref mut thruster) => thruster.thrust_N = val,
510                None => return Err(StateError::NoThrusterAvail),
511            },
512            StateParameter::Element(el) => {
513                match el {
514                    OrbitalElement::AoP => self
515                        .orbit
516                        .set_aop_deg(val)
517                        .context(AstroPhysicsSnafu)
518                        .context(StateAstroSnafu { param })?,
519                    OrbitalElement::Eccentricity => self
520                        .orbit
521                        .set_ecc(val)
522                        .context(AstroPhysicsSnafu)
523                        .context(StateAstroSnafu { param })?,
524                    OrbitalElement::Inclination => self
525                        .orbit
526                        .set_inc_deg(val)
527                        .context(AstroPhysicsSnafu)
528                        .context(StateAstroSnafu { param })?,
529                    OrbitalElement::RAAN => self
530                        .orbit
531                        .set_raan_deg(val)
532                        .context(AstroPhysicsSnafu)
533                        .context(StateAstroSnafu { param })?,
534                    OrbitalElement::SemiMajorAxis => self
535                        .orbit
536                        .set_sma_km(val)
537                        .context(AstroPhysicsSnafu)
538                        .context(StateAstroSnafu { param })?,
539                    OrbitalElement::TrueAnomaly => self
540                        .orbit
541                        .set_ta_deg(val)
542                        .context(AstroPhysicsSnafu)
543                        .context(StateAstroSnafu { param })?,
544                    OrbitalElement::X => self.orbit.radius_km.x = val,
545                    OrbitalElement::Y => self.orbit.radius_km.y = val,
546                    OrbitalElement::Z => self.orbit.radius_km.z = val,
547                    OrbitalElement::Rmag => {
548                        // Convert the position to spherical coordinates
549                        let (_, θ, φ) = cartesian_to_spherical(&self.orbit.radius_km);
550                        // Convert back to cartesian after setting the new range value
551                        self.orbit.radius_km = spherical_to_cartesian(val, θ, φ);
552                    }
553                    OrbitalElement::VX => self.orbit.velocity_km_s.x = val,
554                    OrbitalElement::VY => self.orbit.velocity_km_s.y = val,
555                    OrbitalElement::VZ => self.orbit.velocity_km_s.z = val,
556                    OrbitalElement::Vmag => {
557                        // Convert the velocity to spherical coordinates
558                        let (_, θ, φ) = cartesian_to_spherical(&self.orbit.velocity_km_s);
559                        // Convert back to cartesian after setting the new range value
560                        self.orbit.velocity_km_s = spherical_to_cartesian(val, θ, φ);
561                    }
562                    _ => return Err(StateError::ReadOnly { param }),
563                }
564            }
565            _ => return Err(StateError::ReadOnly { param }),
566        }
567        Ok(())
568    }
569
570    fn unset_stm(&mut self) {
571        self.stm = None;
572    }
573
574    fn orbit(&self) -> Orbit {
575        self.orbit
576    }
577
578    fn set_orbit(&mut self, orbit: Orbit) {
579        self.orbit = orbit;
580    }
581}
582
583impl Add<OVector<f64, Const<6>>> for Spacecraft {
584    type Output = Self;
585
586    /// Adds the provided state deviation to this orbit
587    fn add(mut self, other: OVector<f64, Const<6>>) -> Self {
588        let radius_km = other.fixed_rows::<3>(0).into_owned();
589        let vel_km_s = other.fixed_rows::<3>(3).into_owned();
590
591        self.orbit.radius_km += radius_km;
592        self.orbit.velocity_km_s += vel_km_s;
593
594        self
595    }
596}
597
598impl Add<OVector<f64, Const<9>>> for Spacecraft {
599    type Output = Self;
600
601    /// Adds the provided state deviation to this orbit
602    fn add(mut self, other: OVector<f64, Const<9>>) -> Self {
603        let radius_km = other.fixed_rows::<3>(0).into_owned();
604        let vel_km_s = other.fixed_rows::<3>(3).into_owned();
605
606        self.orbit.radius_km += radius_km;
607        self.orbit.velocity_km_s += vel_km_s;
608        self.srp.coeff_reflectivity = (self.srp.coeff_reflectivity + other[6]).clamp(0.0, 2.0);
609        self.drag.coeff_drag += other[7];
610        self.mass.prop_mass_kg += other[8];
611
612        self
613    }
614}
615
616impl<'a> Decode<'a> for Spacecraft {
617    fn decode<R: Reader<'a>>(decoder: &mut R) -> der::Result<Self> {
618        let orbit = decoder.decode()?;
619        let mass = decoder.decode()?;
620        let srp = decoder.decode()?;
621        let drag = decoder.decode()?;
622        let mode = decoder.decode()?;
623        // Decode the thruster last, checking the presence flag
624        let thruster = if decoder.decode::<bool>()? {
625            Some(decoder.decode()?)
626        } else {
627            None
628        };
629
630        Ok(Spacecraft {
631            orbit,
632            mass,
633            srp,
634            drag,
635            thruster,
636            mode,
637            stm: None,
638        })
639    }
640}
641
642impl Encode for Spacecraft {
643    fn encoded_len(&self) -> der::Result<der::Length> {
644        self.orbit.encoded_len()?
645            + self.mass.encoded_len()?
646            + self.srp.encoded_len()?
647            + self.drag.encoded_len()?
648            + self.mode.encoded_len()?
649            + self.thruster.is_some().encoded_len()?
650            + self.thruster.encoded_len()?
651    }
652
653    fn encode(&self, encoder: &mut impl der::Writer) -> der::Result<()> {
654        self.orbit.encode(encoder)?;
655        self.mass.encode(encoder)?;
656        self.srp.encode(encoder)?;
657        self.drag.encode(encoder)?;
658        self.mode.encode(encoder)?;
659        if let Some(thruster) = self.thruster {
660            true.encode(encoder)?;
661            thruster.encode(encoder)?;
662        } else {
663            false.encode(encoder)?
664        };
665
666        Ok(())
667    }
668}
669
670impl ConfigRepr for Spacecraft {}
671
672#[test]
673fn test_serde() {
674    use serde_yml;
675    use std::str::FromStr;
676
677    use anise::constants::frames::EARTH_J2000;
678
679    let orbit = Orbit::new(
680        -9042.862234,
681        18536.333069,
682        6999.957069,
683        -3.288789,
684        -2.226285,
685        1.646738,
686        Epoch::from_str("2018-09-15T00:15:53.098 UTC").unwrap(),
687        EARTH_J2000,
688    );
689
690    let sc = Spacecraft::new(orbit, 500.0, 159.0, 2.0, 2.0, 1.8, 2.2);
691
692    let serialized_sc = serde_yml::to_string(&sc).unwrap();
693    println!("{serialized_sc}");
694
695    let deser_sc: Spacecraft = serde_yml::from_str(&serialized_sc).unwrap();
696
697    assert_eq!(sc, deser_sc);
698
699    // Check that we can omit the thruster info entirely.
700    let s = r#"
701orbit:
702    radius_km:
703        - -9042.862234
704        - 18536.333069
705        - 6999.957069
706    velocity_km_s:
707        - -3.288789
708        - -2.226285
709        - 1.646738
710    epoch: 2018-09-15T00:15:53.098000000 UTC
711    frame:
712      ephemeris_id: 399
713      orientation_id: 1
714      mu_km3_s2: null
715      shape: null
716mass:
717    dry_mass_kg: 500.0
718    prop_mass_kg: 159.0
719    extra_mass_kg: 0.0
720srp:
721    area_m2: 2.0
722    coeff_reflectivity: 1.8
723drag:
724    area_m2: 2.0
725    coeff_drag: 2.2
726    "#;
727
728    let deser_sc: Spacecraft = serde_yml::from_str(s).unwrap();
729    assert_eq!(sc, deser_sc);
730
731    // Check that we can specify a thruster info entirely.
732    let s = r#"
733orbit:
734    radius_km:
735    - -9042.862234
736    - 18536.333069
737    - 6999.957069
738    velocity_km_s:
739    - -3.288789
740    - -2.226285
741    - 1.646738
742    epoch: 2018-09-15T00:15:53.098000000 UTC
743    frame:
744        ephemeris_id: 399
745        orientation_id: 1
746        mu_km3_s2: null
747        shape: null
748mass:
749    dry_mass_kg: 500.0
750    prop_mass_kg: 159.0
751    extra_mass_kg: 0.0
752srp:
753    area_m2: 2.0
754    coeff_reflectivity: 1.8
755drag:
756    area_m2: 2.0
757    coeff_drag: 2.2
758thruster:
759    thrust_N: 1e-5
760    isp_s: 300.5
761    "#;
762
763    let mut sc_thruster = sc;
764    sc_thruster.thruster = Some(Thruster {
765        isp_s: 300.5,
766        thrust_N: 1e-5,
767    });
768    let deser_sc: Spacecraft = serde_yml::from_str(s).unwrap();
769    assert_eq!(sc_thruster, deser_sc);
770
771    // Tests the minimum definition which will set all of the defaults too
772    let s = r#"
773orbit:
774    radius_km:
775    - -9042.862234
776    - 18536.333069
777    - 6999.957069
778    velocity_km_s:
779    - -3.288789
780    - -2.226285
781    - 1.646738
782    epoch: 2018-09-15T00:15:53.098000000 UTC
783    frame:
784        ephemeris_id: 399
785        orientation_id: 1
786        mu_km3_s2: null
787        shape: null
788mass:
789    dry_mass_kg: 500.0
790    prop_mass_kg: 159.0
791    extra_mass_kg: 0.0
792"#;
793
794    let deser_sc: Spacecraft = serde_yml::from_str(s).unwrap();
795
796    let sc = Spacecraft::new(orbit, 500.0, 159.0, 0.0, 0.0, 1.8, 2.2);
797    assert_eq!(sc, deser_sc);
798}