1use 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 #[default]
55 Coast = 0,
56 Thrust = 1,
58 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#[derive(Clone, Copy, Debug, Serialize, Deserialize, TypedBuilder)]
88#[cfg_attr(feature = "python", pyclass)]
89pub struct Spacecraft {
90 pub orbit: Orbit,
92 #[builder(default)]
94 pub mass: Mass,
95 #[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 #[builder(default)]
106 #[serde(default)]
107 pub mode: GuidanceMode,
108 #[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 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 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 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 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 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 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 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 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 pub fn with_cr(mut self, coeff_reflectivity: f64) -> Self {
233 self.srp.coeff_reflectivity = coeff_reflectivity;
234 self
235 }
236
237 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 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 pub fn with_cd(mut self, coeff_drag: f64) -> Self {
254 self.drag.coeff_drag = coeff_drag;
255 self
256 }
257
258 pub fn with_orbit(mut self, orbit: Orbit) -> Self {
260 self.orbit = orbit;
261 self
262 }
263
264 pub fn enable_stm(&mut self) {
266 self.stm = Some(OMatrix::<f64, Const<9>, Const<9>>::identity());
267 }
268
269 pub fn mass_kg(&self) -> f64 {
271 self.mass.total_mass_kg()
272 }
273
274 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 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; 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 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 fn to_vector(&self) -> OVector<f64, Const<90>> {
394 let mut vector = OVector::<f64, Const<90>>::zeros();
395 for (i, val) in self.orbit.radius_km.iter().enumerate() {
397 vector[i] = *val;
399 }
400 for (i, val) in self.orbit.velocity_km_s.iter().enumerate() {
401 vector[i + 3] = *val;
403 }
404 vector[6] = self.srp.coeff_reflectivity;
406 vector[7] = self.drag.coeff_drag;
407 vector[8] = self.mass.prop_mass_kg;
408 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 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 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 let (_, θ, φ) = cartesian_to_spherical(&self.orbit.radius_km);
550 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 let (_, θ, φ) = cartesian_to_spherical(&self.orbit.velocity_km_s);
559 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 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 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 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 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 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 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}