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
45pub enum GuidanceMode {
46 Coast,
48 Thrust,
50 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#[derive(Clone, Copy, Debug, Serialize, Deserialize, TypedBuilder)]
86pub struct Spacecraft {
87 pub orbit: Orbit,
89 #[builder(default)]
91 pub mass: Mass,
92 #[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 #[builder(default)]
103 #[serde(default)]
104 pub mode: GuidanceMode,
105 #[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 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 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 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 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 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 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 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 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 pub fn with_cr(mut self, coeff_reflectivity: f64) -> Self {
230 self.srp.coeff_reflectivity = coeff_reflectivity;
231 self
232 }
233
234 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 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 pub fn with_cd(mut self, coeff_drag: f64) -> Self {
251 self.drag.coeff_drag = coeff_drag;
252 self
253 }
254
255 pub fn with_orbit(mut self, orbit: Orbit) -> Self {
257 self.orbit = orbit;
258 self
259 }
260
261 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 pub fn enable_stm(&mut self) {
274 self.stm = Some(OMatrix::<f64, Const<9>, Const<9>>::identity());
275 }
276
277 pub fn with_stm(mut self) -> Self {
279 self.enable_stm();
280 self
281 }
282
283 pub fn mass_kg(&self) -> f64 {
285 self.mass.total_mass_kg()
286 }
287
288 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; 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 fn to_vector(&self) -> OVector<f64, Const<90>> {
388 let mut vector = OVector::<f64, Const<90>>::zeros();
389 for (i, val) in self.orbit.radius_km.iter().enumerate() {
391 vector[i] = *val;
393 }
394 for (i, val) in self.orbit.velocity_km_s.iter().enumerate() {
395 vector[i + 3] = *val;
397 }
398 vector[6] = self.srp.coeff_reflectivity;
400 vector[7] = self.drag.coeff_drag;
401 vector[8] = self.mass.prop_mass_kg;
402 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 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 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 let (_, θ, φ) = cartesian_to_spherical(&self.orbit.radius_km);
676 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 let (_, θ, φ) = cartesian_to_spherical(&self.orbit.velocity_km_s);
685 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 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 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 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 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 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}