1use 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 #[default]
48 Coast,
49 Thrust,
51 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#[derive(Clone, Copy, Debug, Serialize, Deserialize, TypedBuilder)]
81pub struct Spacecraft {
82 pub orbit: Orbit,
84 #[builder(default)]
86 pub mass: Mass,
87 #[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 #[builder(default)]
98 #[serde(default)]
99 pub mode: GuidanceMode,
100 #[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 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 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 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 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 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 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 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 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 pub fn with_cr(mut self, coeff_reflectivity: f64) -> Self {
225 self.srp.coeff_reflectivity = coeff_reflectivity;
226 self
227 }
228
229 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 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 pub fn with_cd(mut self, coeff_drag: f64) -> Self {
246 self.drag.coeff_drag = coeff_drag;
247 self
248 }
249
250 pub fn with_orbit(mut self, orbit: Orbit) -> Self {
252 self.orbit = orbit;
253 self
254 }
255
256 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 pub fn enable_stm(&mut self) {
269 self.stm = Some(OMatrix::<f64, Const<9>, Const<9>>::identity());
270 }
271
272 pub fn mass_kg(&self) -> f64 {
274 self.mass.total_mass_kg()
275 }
276
277 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; 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 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 fn to_vector(&self) -> OVector<f64, Const<90>> {
383 let mut vector = OVector::<f64, Const<90>>::zeros();
384 for (i, val) in self.orbit.radius_km.iter().enumerate() {
386 vector[i] = *val;
388 }
389 for (i, val) in self.orbit.velocity_km_s.iter().enumerate() {
390 vector[i + 3] = *val;
392 }
393 vector[6] = self.srp.coeff_reflectivity;
395 vector[7] = self.drag.coeff_drag;
396 vector[8] = self.mass.prop_mass_kg;
397 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 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 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 let (_, θ, φ) = cartesian_to_spherical(&self.orbit.radius_km);
671 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 let (_, θ, φ) = cartesian_to_spherical(&self.orbit.velocity_km_s);
680 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 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 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 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 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 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}