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::astro::PhysicsResult;
20use anise::constants::frames::EARTH_J2000;
21pub use anise::prelude::Orbit;
22
23pub use anise::structure::spacecraft::{DragData, Mass, SRPData};
24use nalgebra::Vector3;
25use serde::{Deserialize, Serialize};
26use snafu::ResultExt;
27use typed_builder::TypedBuilder;
28
29use super::{AstroPhysicsSnafu, BPlane, State};
30use crate::dynamics::guidance::Thruster;
31use crate::dynamics::DynamicsError;
32use crate::errors::{StateAstroSnafu, StateError};
33use crate::io::ConfigRepr;
34use crate::linalg::{Const, DimName, OMatrix, OVector};
35use crate::md::StateParameter;
36use crate::time::Epoch;
37use crate::utils::{cartesian_to_spherical, spherical_to_cartesian};
38
39use std::default::Default;
40use std::fmt;
41use std::ops::Add;
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
44
45pub enum GuidanceMode {
46    /// Guidance is turned off and Guidance Law may switch mode to Thrust for next call
47    Coast,
48    /// Guidance is turned on and Guidance Law may switch mode to Coast for next call
49    Thrust,
50    /// Guidance is turned off and Guidance Law may not change its mode (will need to be done externally to the guidance law).
51    Inhibit,
52}
53
54impl Default for GuidanceMode {
55    fn default() -> Self {
56        Self::Coast
57    }
58}
59
60impl From<f64> for GuidanceMode {
61    fn from(value: f64) -> Self {
62        if value >= 1.0 {
63            Self::Thrust
64        } else if value < 0.0 {
65            Self::Inhibit
66        } else {
67            Self::Coast
68        }
69    }
70}
71
72impl From<GuidanceMode> for f64 {
73    fn from(mode: GuidanceMode) -> f64 {
74        match mode {
75            GuidanceMode::Coast => 0.0,
76            GuidanceMode::Thrust => 1.0,
77            GuidanceMode::Inhibit => -1.0,
78        }
79    }
80}
81
82/// 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.
83///
84/// 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).
85#[derive(Clone, Copy, Debug, Serialize, Deserialize, TypedBuilder)]
86pub struct Spacecraft {
87    /// Initial orbit of the vehicle
88    pub orbit: Orbit,
89    /// Dry, propellant, and extra masses
90    #[builder(default)]
91    pub mass: Mass,
92    /// Solar Radiation Pressure configuration for this spacecraft
93    #[builder(default)]
94    #[serde(default)]
95    pub srp: SRPData,
96    #[builder(default)]
97    #[serde(default)]
98    pub drag: DragData,
99    #[builder(default, setter(strip_option))]
100    pub thruster: Option<Thruster>,
101    /// Any extra information or extension that is needed for specific guidance laws
102    #[builder(default)]
103    #[serde(default)]
104    pub mode: GuidanceMode,
105    /// Optionally stores the state transition matrix from the start of the propagation until the current time (i.e. trajectory STM, not step-size STM)
106    /// STM is contains position and velocity, Cr, Cd, prop mass
107    #[builder(default, setter(strip_option))]
108    #[serde(skip)]
109    pub stm: Option<OMatrix<f64, Const<9>, Const<9>>>,
110}
111
112impl Default for Spacecraft {
113    fn default() -> Self {
114        Self {
115            orbit: Orbit::zero(EARTH_J2000),
116            mass: Mass::default(),
117            srp: SRPData::default(),
118            drag: DragData::default(),
119            thruster: None,
120            mode: GuidanceMode::default(),
121            stm: None,
122        }
123    }
124}
125
126impl From<Orbit> for Spacecraft {
127    fn from(orbit: Orbit) -> Self {
128        Self::builder().orbit(orbit).build()
129    }
130}
131
132impl Spacecraft {
133    /// Initialize a spacecraft state from all of its parameters
134    pub fn new(
135        orbit: Orbit,
136        dry_mass_kg: f64,
137        prop_mass_kg: f64,
138        srp_area_m2: f64,
139        drag_area_m2: f64,
140        coeff_reflectivity: f64,
141        coeff_drag: f64,
142    ) -> Self {
143        Self {
144            orbit,
145            mass: Mass::from_dry_and_prop_masses(dry_mass_kg, prop_mass_kg),
146            srp: SRPData {
147                area_m2: srp_area_m2,
148                coeff_reflectivity,
149            },
150            drag: DragData {
151                area_m2: drag_area_m2,
152                coeff_drag,
153            },
154            ..Default::default()
155        }
156    }
157
158    /// Initialize a spacecraft state from only a thruster and mass. Use this when designing guidance laws while ignoring drag and SRP.
159    pub fn from_thruster(
160        orbit: Orbit,
161        dry_mass_kg: f64,
162        prop_mass_kg: f64,
163        thruster: Thruster,
164        mode: GuidanceMode,
165    ) -> Self {
166        Self {
167            orbit,
168            mass: Mass::from_dry_and_prop_masses(dry_mass_kg, prop_mass_kg),
169            thruster: Some(thruster),
170            mode,
171            ..Default::default()
172        }
173    }
174
175    /// Initialize a spacecraft state from the SRP default 1.8 for coefficient of reflectivity (prop mass and drag parameters nullified!)
176    pub fn from_srp_defaults(orbit: Orbit, dry_mass_kg: f64, srp_area_m2: f64) -> Self {
177        Self {
178            orbit,
179            mass: Mass::from_dry_mass(dry_mass_kg),
180            srp: SRPData::from_area(srp_area_m2),
181            ..Default::default()
182        }
183    }
184
185    /// Initialize a spacecraft state from the SRP default 1.8 for coefficient of drag (prop mass and SRP parameters nullified!)
186    pub fn from_drag_defaults(orbit: Orbit, dry_mass_kg: f64, drag_area_m2: f64) -> Self {
187        Self {
188            orbit,
189            mass: Mass::from_dry_mass(dry_mass_kg),
190            drag: DragData::from_area(drag_area_m2),
191            ..Default::default()
192        }
193    }
194
195    pub fn with_dv_km_s(mut self, dv_km_s: Vector3<f64>) -> Self {
196        self.orbit.apply_dv_km_s(dv_km_s);
197        self
198    }
199
200    /// Returns a copy of the state with a new dry mass
201    pub fn with_dry_mass(mut self, dry_mass_kg: f64) -> Self {
202        self.mass.dry_mass_kg = dry_mass_kg;
203        self
204    }
205
206    /// Returns a copy of the state with a new prop mass
207    pub fn with_prop_mass(mut self, prop_mass_kg: f64) -> Self {
208        self.mass.prop_mass_kg = prop_mass_kg;
209        self
210    }
211
212    /// Returns a copy of the state with a new SRP area and CR
213    pub fn with_srp(mut self, srp_area_m2: f64, coeff_reflectivity: f64) -> Self {
214        self.srp = SRPData {
215            area_m2: srp_area_m2,
216            coeff_reflectivity,
217        };
218
219        self
220    }
221
222    /// Returns a copy of the state with a new SRP area
223    pub fn with_srp_area(mut self, srp_area_m2: f64) -> Self {
224        self.srp.area_m2 = srp_area_m2;
225        self
226    }
227
228    /// Returns a copy of the state with a new coefficient of reflectivity
229    pub fn with_cr(mut self, coeff_reflectivity: f64) -> Self {
230        self.srp.coeff_reflectivity = coeff_reflectivity;
231        self
232    }
233
234    /// Returns a copy of the state with a new drag area and CD
235    pub fn with_drag(mut self, drag_area_m2: f64, coeff_drag: f64) -> Self {
236        self.drag = DragData {
237            area_m2: drag_area_m2,
238            coeff_drag,
239        };
240        self
241    }
242
243    /// Returns a copy of the state with a new SRP area
244    pub fn with_drag_area(mut self, drag_area_m2: f64) -> Self {
245        self.drag.area_m2 = drag_area_m2;
246        self
247    }
248
249    /// Returns a copy of the state with a new coefficient of drag
250    pub fn with_cd(mut self, coeff_drag: f64) -> Self {
251        self.drag.coeff_drag = coeff_drag;
252        self
253    }
254
255    /// Returns a copy of the state with a new orbit
256    pub fn with_orbit(mut self, orbit: Orbit) -> Self {
257        self.orbit = orbit;
258        self
259    }
260
261    /// 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
262    pub fn rss(&self, other: &Self) -> PhysicsResult<(f64, f64, f64)> {
263        let rss_p_km = self.orbit.rss_radius_km(&other.orbit)?;
264        let rss_v_km_s = self.orbit.rss_velocity_km_s(&other.orbit)?;
265        let rss_prop_kg = (self.mass.prop_mass_kg - other.mass.prop_mass_kg)
266            .powi(2)
267            .sqrt();
268
269        Ok((rss_p_km, rss_v_km_s, rss_prop_kg))
270    }
271
272    /// Sets the STM of this state of identity, which also enables computation of the STM for spacecraft navigation
273    pub fn enable_stm(&mut self) {
274        self.stm = Some(OMatrix::<f64, Const<9>, Const<9>>::identity());
275    }
276
277    /// Copies the current state but sets the STM to identity
278    pub fn with_stm(mut self) -> Self {
279        self.enable_stm();
280        self
281    }
282
283    /// Returns the total mass in kilograms
284    pub fn mass_kg(&self) -> f64 {
285        self.mass.total_mass_kg()
286    }
287
288    /// Returns a copy of the state with the provided guidance mode
289    pub fn with_guidance_mode(mut self, mode: GuidanceMode) -> Self {
290        self.mode = mode;
291        self
292    }
293
294    pub fn mode(&self) -> GuidanceMode {
295        self.mode
296    }
297
298    pub fn mut_mode(&mut self, mode: GuidanceMode) {
299        self.mode = mode;
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    fn reset_stm(&mut self) {
378        self.stm = Some(OMatrix::<f64, Const<9>, Const<9>>::identity());
379    }
380
381    fn zeros() -> Self {
382        Self::default()
383    }
384
385    /// The vector is organized as such:
386    /// [X, Y, Z, Vx, Vy, Vz, Cr, Cd, Fuel mass, STM(9x9)]
387    fn to_vector(&self) -> OVector<f64, Const<90>> {
388        let mut vector = OVector::<f64, Const<90>>::zeros();
389        // Set the orbit state info
390        for (i, val) in self.orbit.radius_km.iter().enumerate() {
391            // Place the orbit state first, then skip three (Cr, Cd, Fuel), then copy orbit STM
392            vector[i] = *val;
393        }
394        for (i, val) in self.orbit.velocity_km_s.iter().enumerate() {
395            // Place the orbit state first, then skip three (Cr, Cd, Fuel), then copy orbit STM
396            vector[i + 3] = *val;
397        }
398        // Set the spacecraft parameters
399        vector[6] = self.srp.coeff_reflectivity;
400        vector[7] = self.drag.coeff_drag;
401        vector[8] = self.mass.prop_mass_kg;
402        // Add the STM to the vector
403        if let Some(stm) = self.stm {
404            for (idx, stm_val) in stm.as_slice().iter().enumerate() {
405                vector[idx + Self::Size::dim()] = *stm_val;
406            }
407        }
408        vector
409    }
410
411    /// Vector is expected to be organized as such:
412    /// [X, Y, Z, Vx, Vy, Vz, Cr, Cd, Fuel mass, STM(9x9)]
413    fn set(&mut self, epoch: Epoch, vector: &OVector<f64, Const<90>>) {
414        let sc_state =
415            OVector::<f64, Self::Size>::from_column_slice(&vector.as_slice()[..Self::Size::dim()]);
416
417        if self.stm.is_some() {
418            let sc_full_stm = OMatrix::<f64, Self::Size, Self::Size>::from_column_slice(
419                &vector.as_slice()[Self::Size::dim()..],
420            );
421
422            self.stm = Some(sc_full_stm);
423        }
424
425        let radius_km = sc_state.fixed_rows::<3>(0).into_owned();
426        let vel_km_s = sc_state.fixed_rows::<3>(3).into_owned();
427        self.orbit.epoch = epoch;
428        self.orbit.radius_km = radius_km;
429        self.orbit.velocity_km_s = vel_km_s;
430        self.srp.coeff_reflectivity = sc_state[6].clamp(0.0, 2.0);
431        self.drag.coeff_drag = sc_state[7];
432        self.mass.prop_mass_kg = sc_state[8];
433    }
434
435    /// diag(STM) = [X,Y,Z,Vx,Vy,Vz,Cr,Cd,Fuel]
436    /// WARNING: Currently the STM assumes that the prop mass is constant at ALL TIMES!
437    fn stm(&self) -> Result<OMatrix<f64, Self::Size, Self::Size>, DynamicsError> {
438        match self.stm {
439            Some(stm) => Ok(stm),
440            None => Err(DynamicsError::StateTransitionMatrixUnset),
441        }
442    }
443
444    fn epoch(&self) -> Epoch {
445        self.orbit.epoch
446    }
447
448    fn set_epoch(&mut self, epoch: Epoch) {
449        self.orbit.epoch = epoch
450    }
451
452    fn add(self, other: OVector<f64, Self::Size>) -> Self {
453        self + other
454    }
455
456    fn value(&self, param: StateParameter) -> Result<f64, StateError> {
457        match param {
458            StateParameter::Cd => Ok(self.drag.coeff_drag),
459            StateParameter::Cr => Ok(self.srp.coeff_reflectivity),
460            StateParameter::DryMass => Ok(self.mass.dry_mass_kg),
461            StateParameter::PropMass => Ok(self.mass.prop_mass_kg),
462            StateParameter::TotalMass => Ok(self.mass.total_mass_kg()),
463            StateParameter::Isp => match self.thruster {
464                Some(thruster) => Ok(thruster.isp_s),
465                None => Err(StateError::NoThrusterAvail),
466            },
467            StateParameter::Thrust => match self.thruster {
468                Some(thruster) => Ok(thruster.thrust_N),
469                None => Err(StateError::NoThrusterAvail),
470            },
471            StateParameter::GuidanceMode => Ok(self.mode.into()),
472            StateParameter::ApoapsisRadius => self
473                .orbit
474                .apoapsis_km()
475                .context(AstroPhysicsSnafu)
476                .context(StateAstroSnafu { param }),
477            StateParameter::AoL => self
478                .orbit
479                .aol_deg()
480                .context(AstroPhysicsSnafu)
481                .context(StateAstroSnafu { param }),
482            StateParameter::AoP => self
483                .orbit
484                .aop_deg()
485                .context(AstroPhysicsSnafu)
486                .context(StateAstroSnafu { param }),
487            StateParameter::BdotR => Ok(BPlane::new(self.orbit)
488                .context(StateAstroSnafu { param })?
489                .b_r
490                .real()),
491            StateParameter::BdotT => Ok(BPlane::new(self.orbit)
492                .context(StateAstroSnafu { param })?
493                .b_t
494                .real()),
495            StateParameter::BLTOF => Ok(BPlane::new(self.orbit)
496                .context(StateAstroSnafu { param })?
497                .ltof_s
498                .real()),
499            StateParameter::C3 => self
500                .orbit
501                .c3_km2_s2()
502                .context(AstroPhysicsSnafu)
503                .context(StateAstroSnafu { param }),
504            StateParameter::Declination => Ok(self.orbit.declination_deg()),
505            StateParameter::EccentricAnomaly => self
506                .orbit
507                .ea_deg()
508                .context(AstroPhysicsSnafu)
509                .context(StateAstroSnafu { param }),
510            StateParameter::Eccentricity => self
511                .orbit
512                .ecc()
513                .context(AstroPhysicsSnafu)
514                .context(StateAstroSnafu { param }),
515            StateParameter::Energy => self
516                .orbit
517                .energy_km2_s2()
518                .context(AstroPhysicsSnafu)
519                .context(StateAstroSnafu { param }),
520            StateParameter::FlightPathAngle => self
521                .orbit
522                .fpa_deg()
523                .context(AstroPhysicsSnafu)
524                .context(StateAstroSnafu { param }),
525            StateParameter::Height => self
526                .orbit
527                .height_km()
528                .context(AstroPhysicsSnafu)
529                .context(StateAstroSnafu { param }),
530            StateParameter::Latitude => self
531                .orbit
532                .latitude_deg()
533                .context(AstroPhysicsSnafu)
534                .context(StateAstroSnafu { param }),
535            StateParameter::Longitude => Ok(self.orbit.longitude_deg()),
536            StateParameter::Hmag => self
537                .orbit
538                .hmag()
539                .context(AstroPhysicsSnafu)
540                .context(StateAstroSnafu { param }),
541            StateParameter::HX => self
542                .orbit
543                .hx()
544                .context(AstroPhysicsSnafu)
545                .context(StateAstroSnafu { param }),
546            StateParameter::HY => self
547                .orbit
548                .hy()
549                .context(AstroPhysicsSnafu)
550                .context(StateAstroSnafu { param }),
551            StateParameter::HZ => self
552                .orbit
553                .hz()
554                .context(AstroPhysicsSnafu)
555                .context(StateAstroSnafu { param }),
556            StateParameter::HyperbolicAnomaly => self
557                .orbit
558                .hyperbolic_anomaly_deg()
559                .context(AstroPhysicsSnafu)
560                .context(StateAstroSnafu { param }),
561            StateParameter::Inclination => self
562                .orbit
563                .inc_deg()
564                .context(AstroPhysicsSnafu)
565                .context(StateAstroSnafu { param }),
566            StateParameter::MeanAnomaly => self
567                .orbit
568                .ma_deg()
569                .context(AstroPhysicsSnafu)
570                .context(StateAstroSnafu { param }),
571            StateParameter::PeriapsisRadius => self
572                .orbit
573                .periapsis_km()
574                .context(AstroPhysicsSnafu)
575                .context(StateAstroSnafu { param }),
576            StateParameter::Period => Ok(self
577                .orbit
578                .period()
579                .context(AstroPhysicsSnafu)
580                .context(StateAstroSnafu { param })?
581                .to_seconds()),
582            StateParameter::RightAscension => Ok(self.orbit.right_ascension_deg()),
583            StateParameter::RAAN => self
584                .orbit
585                .raan_deg()
586                .context(AstroPhysicsSnafu)
587                .context(StateAstroSnafu { param }),
588            StateParameter::Rmag => Ok(self.orbit.rmag_km()),
589            StateParameter::SemiMinorAxis => self
590                .orbit
591                .semi_minor_axis_km()
592                .context(AstroPhysicsSnafu)
593                .context(StateAstroSnafu { param }),
594            StateParameter::SemiParameter => self
595                .orbit
596                .semi_parameter_km()
597                .context(AstroPhysicsSnafu)
598                .context(StateAstroSnafu { param }),
599            StateParameter::SMA => self
600                .orbit
601                .sma_km()
602                .context(AstroPhysicsSnafu)
603                .context(StateAstroSnafu { param }),
604            StateParameter::TrueAnomaly => self
605                .orbit
606                .ta_deg()
607                .context(AstroPhysicsSnafu)
608                .context(StateAstroSnafu { param }),
609            StateParameter::TrueLongitude => self
610                .orbit
611                .tlong_deg()
612                .context(AstroPhysicsSnafu)
613                .context(StateAstroSnafu { param }),
614            StateParameter::VelocityDeclination => Ok(self.orbit.velocity_declination_deg()),
615            StateParameter::Vmag => Ok(self.orbit.vmag_km_s()),
616            StateParameter::X => Ok(self.orbit.radius_km.x),
617            StateParameter::Y => Ok(self.orbit.radius_km.y),
618            StateParameter::Z => Ok(self.orbit.radius_km.z),
619            StateParameter::VX => Ok(self.orbit.velocity_km_s.x),
620            StateParameter::VY => Ok(self.orbit.velocity_km_s.y),
621            StateParameter::VZ => Ok(self.orbit.velocity_km_s.z),
622            _ => Err(StateError::Unavailable { param }),
623        }
624    }
625
626    fn set_value(&mut self, param: StateParameter, val: f64) -> Result<(), StateError> {
627        match param {
628            StateParameter::Cd => self.drag.coeff_drag = val,
629            StateParameter::Cr => self.srp.coeff_reflectivity = val,
630            StateParameter::PropMass => self.mass.prop_mass_kg = val,
631            StateParameter::DryMass => self.mass.dry_mass_kg = val,
632            StateParameter::Isp => match self.thruster {
633                Some(ref mut thruster) => thruster.isp_s = val,
634                None => return Err(StateError::NoThrusterAvail),
635            },
636            StateParameter::Thrust => match self.thruster {
637                Some(ref mut thruster) => thruster.thrust_N = val,
638                None => return Err(StateError::NoThrusterAvail),
639            },
640            StateParameter::AoP => self
641                .orbit
642                .set_aop_deg(val)
643                .context(AstroPhysicsSnafu)
644                .context(StateAstroSnafu { param })?,
645            StateParameter::Eccentricity => self
646                .orbit
647                .set_ecc(val)
648                .context(AstroPhysicsSnafu)
649                .context(StateAstroSnafu { param })?,
650            StateParameter::Inclination => self
651                .orbit
652                .set_inc_deg(val)
653                .context(AstroPhysicsSnafu)
654                .context(StateAstroSnafu { param })?,
655            StateParameter::RAAN => self
656                .orbit
657                .set_raan_deg(val)
658                .context(AstroPhysicsSnafu)
659                .context(StateAstroSnafu { param })?,
660            StateParameter::SMA => self
661                .orbit
662                .set_sma_km(val)
663                .context(AstroPhysicsSnafu)
664                .context(StateAstroSnafu { param })?,
665            StateParameter::TrueAnomaly => self
666                .orbit
667                .set_ta_deg(val)
668                .context(AstroPhysicsSnafu)
669                .context(StateAstroSnafu { param })?,
670            StateParameter::X => self.orbit.radius_km.x = val,
671            StateParameter::Y => self.orbit.radius_km.y = val,
672            StateParameter::Z => self.orbit.radius_km.z = val,
673            StateParameter::Rmag => {
674                // Convert the position to spherical coordinates
675                let (_, θ, φ) = cartesian_to_spherical(&self.orbit.radius_km);
676                // Convert back to cartesian after setting the new range value
677                self.orbit.radius_km = spherical_to_cartesian(val, θ, φ);
678            }
679            StateParameter::VX => self.orbit.velocity_km_s.x = val,
680            StateParameter::VY => self.orbit.velocity_km_s.y = val,
681            StateParameter::VZ => self.orbit.velocity_km_s.z = val,
682            StateParameter::Vmag => {
683                // Convert the velocity to spherical coordinates
684                let (_, θ, φ) = cartesian_to_spherical(&self.orbit.velocity_km_s);
685                // Convert back to cartesian after setting the new range value
686                self.orbit.velocity_km_s = spherical_to_cartesian(val, θ, φ);
687            }
688            _ => return Err(StateError::ReadOnly { param }),
689        }
690        Ok(())
691    }
692
693    fn unset_stm(&mut self) {
694        self.stm = None;
695    }
696
697    fn orbit(&self) -> Orbit {
698        self.orbit
699    }
700
701    fn set_orbit(&mut self, orbit: Orbit) {
702        self.orbit = orbit;
703    }
704}
705
706impl Add<OVector<f64, Const<6>>> for Spacecraft {
707    type Output = Self;
708
709    /// Adds the provided state deviation to this orbit
710    fn add(mut self, other: OVector<f64, Const<6>>) -> Self {
711        let radius_km = other.fixed_rows::<3>(0).into_owned();
712        let vel_km_s = other.fixed_rows::<3>(3).into_owned();
713
714        self.orbit.radius_km += radius_km;
715        self.orbit.velocity_km_s += vel_km_s;
716
717        self
718    }
719}
720
721impl Add<OVector<f64, Const<9>>> for Spacecraft {
722    type Output = Self;
723
724    /// Adds the provided state deviation to this orbit
725    fn add(mut self, other: OVector<f64, Const<9>>) -> Self {
726        let radius_km = other.fixed_rows::<3>(0).into_owned();
727        let vel_km_s = other.fixed_rows::<3>(3).into_owned();
728
729        self.orbit.radius_km += radius_km;
730        self.orbit.velocity_km_s += vel_km_s;
731        self.srp.coeff_reflectivity = (self.srp.coeff_reflectivity + other[6]).clamp(0.0, 2.0);
732        self.drag.coeff_drag += other[7];
733        self.mass.prop_mass_kg += other[8];
734
735        self
736    }
737}
738
739impl ConfigRepr for Spacecraft {}
740
741#[test]
742fn test_serde() {
743    use serde_yml;
744    use std::str::FromStr;
745
746    use anise::constants::frames::EARTH_J2000;
747
748    let orbit = Orbit::new(
749        -9042.862234,
750        18536.333069,
751        6999.957069,
752        -3.288789,
753        -2.226285,
754        1.646738,
755        Epoch::from_str("2018-09-15T00:15:53.098 UTC").unwrap(),
756        EARTH_J2000,
757    );
758
759    let sc = Spacecraft::new(orbit, 500.0, 159.0, 2.0, 2.0, 1.8, 2.2);
760
761    let serialized_sc = serde_yml::to_string(&sc).unwrap();
762    println!("{}", serialized_sc);
763
764    let deser_sc: Spacecraft = serde_yml::from_str(&serialized_sc).unwrap();
765
766    assert_eq!(sc, deser_sc);
767
768    // Check that we can omit the thruster info entirely.
769    let s = r#"
770orbit:
771    radius_km:
772        - -9042.862234
773        - 18536.333069
774        - 6999.957069
775    velocity_km_s:
776        - -3.288789
777        - -2.226285
778        - 1.646738
779    epoch: 2018-09-15T00:15:53.098000000 UTC
780    frame:
781      ephemeris_id: 399
782      orientation_id: 1
783      mu_km3_s2: null
784      shape: null
785mass:
786    dry_mass_kg: 500.0
787    prop_mass_kg: 159.0
788    extra_mass_kg: 0.0
789srp:
790    area_m2: 2.0
791    coeff_reflectivity: 1.8
792drag:
793    area_m2: 2.0
794    coeff_drag: 2.2
795    "#;
796
797    let deser_sc: Spacecraft = serde_yml::from_str(s).unwrap();
798    assert_eq!(sc, deser_sc);
799
800    // Check that we can specify a thruster info entirely.
801    let s = r#"
802orbit:
803    radius_km:
804    - -9042.862234
805    - 18536.333069
806    - 6999.957069
807    velocity_km_s:
808    - -3.288789
809    - -2.226285
810    - 1.646738
811    epoch: 2018-09-15T00:15:53.098000000 UTC
812    frame:
813        ephemeris_id: 399
814        orientation_id: 1
815        mu_km3_s2: null
816        shape: null
817mass:
818    dry_mass_kg: 500.0
819    prop_mass_kg: 159.0
820    extra_mass_kg: 0.0
821srp:
822    area_m2: 2.0
823    coeff_reflectivity: 1.8
824drag:
825    area_m2: 2.0
826    coeff_drag: 2.2
827thruster:
828    thrust_N: 1e-5
829    isp_s: 300.5
830    "#;
831
832    let mut sc_thruster = sc;
833    sc_thruster.thruster = Some(Thruster {
834        isp_s: 300.5,
835        thrust_N: 1e-5,
836    });
837    let deser_sc: Spacecraft = serde_yml::from_str(s).unwrap();
838    assert_eq!(sc_thruster, deser_sc);
839
840    // Tests the minimum definition which will set all of the defaults too
841    let s = r#"
842orbit:
843    radius_km:
844    - -9042.862234
845    - 18536.333069
846    - 6999.957069
847    velocity_km_s:
848    - -3.288789
849    - -2.226285
850    - 1.646738
851    epoch: 2018-09-15T00:15:53.098000000 UTC
852    frame:
853        ephemeris_id: 399
854        orientation_id: 1
855        mu_km3_s2: null
856        shape: null
857mass:
858    dry_mass_kg: 500.0
859    prop_mass_kg: 159.0
860    extra_mass_kg: 0.0
861"#;
862
863    let deser_sc: Spacecraft = serde_yml::from_str(s).unwrap();
864
865    let sc = Spacecraft::new(orbit, 500.0, 159.0, 0.0, 0.0, 1.8, 2.2);
866    assert_eq!(sc, deser_sc);
867}