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