1use std::collections::HashMap;
6use std::fmt;
7
8use lox_bodies::{DynOrigin, Origin};
9use lox_core::units::AngularRate;
10
11#[cfg(feature = "imaging")]
12use crate::imaging::OpticalPayload;
13#[cfg(feature = "imaging")]
14use crate::imaging::SarPayload;
15#[cfg(feature = "imaging")]
16use crate::imaging::analysis::PayloadAccessor;
17use lox_frames::rotations::TryRotation;
18use lox_frames::{DynFrame, ReferenceFrame};
19use lox_time::Time;
20use lox_time::intervals::TimeInterval;
21use lox_time::time_scales::{DynTimeScale, Tai};
22use rayon::prelude::*;
23
24#[cfg(feature = "comms")]
25use lox_comms::system::CommunicationSystem;
26
27use crate::visibility::ElevationMask;
28use lox_orbits::constellations::{ConstellationPropagator, DynConstellation};
29use lox_orbits::ground::DynGroundLocation;
30use lox_orbits::orbits::{Ensemble, KeplerianOrbit};
31use lox_orbits::propagators::j2::DynJ2Propagator;
32use lox_orbits::propagators::j4::DynJ4Propagator;
33use lox_orbits::propagators::numerical::DynNumericalPropagator;
34use lox_orbits::propagators::semi_analytical::DynVallado;
35use lox_orbits::propagators::{OrbitSource, PropagateError};
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct AssetId(String);
41
42impl AssetId {
43 pub fn new(id: impl Into<String>) -> Self {
45 Self(id.into())
46 }
47
48 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52}
53
54impl fmt::Display for AssetId {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{}", self.0)
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub struct ConstellationId(String);
64
65impl ConstellationId {
66 pub fn new(id: impl Into<String>) -> Self {
68 Self(id.into())
69 }
70
71 pub fn as_str(&self) -> &str {
73 &self.0
74 }
75}
76
77impl fmt::Display for ConstellationId {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(f, "{}", self.0)
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
86pub struct NetworkId(String);
87
88impl NetworkId {
89 pub fn new(id: impl Into<String>) -> Self {
91 Self(id.into())
92 }
93
94 pub fn as_str(&self) -> &str {
96 &self.0
97 }
98}
99
100impl fmt::Display for NetworkId {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "{}", self.0)
103 }
104}
105
106#[derive(Debug, Clone)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub struct GroundStation {
110 id: AssetId,
111 location: DynGroundLocation,
112 mask: ElevationMask,
113 body_fixed_frame: DynFrame,
114 network: Option<NetworkId>,
115 #[cfg(feature = "comms")]
116 communication_systems: Vec<CommunicationSystem>,
117}
118
119impl GroundStation {
120 pub fn new(id: impl Into<String>, location: DynGroundLocation, mask: ElevationMask) -> Self {
122 let body_fixed_frame = DynFrame::Iau(location.origin());
123 Self {
124 id: AssetId::new(id),
125 location,
126 mask,
127 body_fixed_frame,
128 network: None,
129 #[cfg(feature = "comms")]
130 communication_systems: Vec::new(),
131 }
132 }
133
134 pub fn with_body_fixed_frame(mut self, frame: impl Into<DynFrame>) -> Self {
136 self.body_fixed_frame = frame.into();
137 self
138 }
139
140 pub fn with_network_id(mut self, id: impl Into<String>) -> Self {
142 self.network = Some(NetworkId(id.into()));
143 self
144 }
145
146 #[cfg(feature = "comms")]
148 pub fn with_communication_system(mut self, system: CommunicationSystem) -> Self {
149 self.communication_systems.push(system);
150 self
151 }
152
153 pub fn id(&self) -> &AssetId {
155 &self.id
156 }
157
158 pub fn location(&self) -> &DynGroundLocation {
160 &self.location
161 }
162
163 pub fn mask(&self) -> &ElevationMask {
165 &self.mask
166 }
167
168 pub fn network_id(&self) -> Option<&NetworkId> {
170 self.network.as_ref()
171 }
172
173 pub fn body_fixed_frame(&self) -> DynFrame {
175 self.body_fixed_frame
176 }
177
178 #[cfg(feature = "comms")]
180 pub fn communication_systems(&self) -> &[CommunicationSystem] {
181 &self.communication_systems
182 }
183}
184
185#[derive(Debug, Clone)]
187#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
188pub struct Spacecraft {
189 id: AssetId,
190 orbit: OrbitSource,
191 max_slew_rate: Option<AngularRate>,
192 constellation: Option<ConstellationId>,
193 #[cfg(feature = "imaging")]
194 optical_payload: Option<OpticalPayload>,
195 #[cfg(feature = "imaging")]
196 sar_payload: Option<SarPayload>,
197 #[cfg(feature = "comms")]
198 communication_systems: Vec<CommunicationSystem>,
199}
200
201impl Spacecraft {
202 pub fn new(id: impl Into<String>, orbit: OrbitSource) -> Self {
204 Self {
205 id: AssetId::new(id),
206 orbit,
207 max_slew_rate: None,
208 constellation: None,
209 #[cfg(feature = "imaging")]
210 optical_payload: None,
211 #[cfg(feature = "imaging")]
212 sar_payload: None,
213 #[cfg(feature = "comms")]
214 communication_systems: Vec::new(),
215 }
216 }
217
218 pub fn with_max_slew_rate(mut self, rate: AngularRate) -> Self {
220 self.max_slew_rate = Some(rate);
221 self
222 }
223
224 pub fn with_constellation_id(mut self, id: impl Into<String>) -> Self {
226 self.constellation = Some(ConstellationId(id.into()));
227 self
228 }
229
230 #[cfg(feature = "imaging")]
232 pub fn with_optical_payload(mut self, payload: OpticalPayload) -> Self {
233 self.optical_payload = Some(payload);
234 self
235 }
236
237 #[cfg(feature = "imaging")]
239 pub fn with_sar_payload(mut self, payload: SarPayload) -> Self {
240 self.sar_payload = Some(payload);
241 self
242 }
243
244 #[cfg(feature = "comms")]
246 pub fn with_communication_system(mut self, system: CommunicationSystem) -> Self {
247 self.communication_systems.push(system);
248 self
249 }
250
251 pub fn id(&self) -> &AssetId {
253 &self.id
254 }
255
256 pub fn orbit(&self) -> &OrbitSource {
258 &self.orbit
259 }
260
261 pub fn constellation_id(&self) -> Option<&ConstellationId> {
263 self.constellation.as_ref()
264 }
265
266 pub fn max_slew_rate(&self) -> Option<AngularRate> {
268 self.max_slew_rate
269 }
270
271 #[cfg(feature = "imaging")]
273 pub fn optical_payload(&self) -> Option<OpticalPayload> {
274 self.optical_payload
275 }
276
277 #[cfg(feature = "imaging")]
279 pub fn sar_payload(&self) -> Option<SarPayload> {
280 self.sar_payload
281 }
282
283 #[cfg(feature = "comms")]
285 pub fn communication_systems(&self) -> &[CommunicationSystem] {
286 &self.communication_systems
287 }
288}
289
290#[derive(Debug, Clone)]
296#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
297pub struct Scenario<O: Origin, R: ReferenceFrame> {
298 interval: TimeInterval<Tai>,
299 origin: O,
300 frame: R,
301 ground_stations: Vec<GroundStation>,
302 spacecraft: Vec<Spacecraft>,
303 constellations: Vec<DynConstellation>,
304}
305
306pub type DynScenario = Scenario<DynOrigin, DynFrame>;
308
309#[derive(Debug, thiserror::Error)]
311pub enum ConstellationConvertError {
312 #[error("failed to create Keplerian orbit: {0}")]
314 KeplerianOrbit(String),
315 #[error("failed to convert to Cartesian orbit: {0}")]
317 CartesianConversion(String),
318 #[error("failed to create propagator: {0}")]
320 Propagator(String),
321}
322
323#[derive(Debug, thiserror::Error)]
325pub enum ScenarioPropagateError {
326 #[error("propagation failed for spacecraft \"{0}\": {1}")]
328 Propagate(AssetId, PropagateError),
329 #[error("frame transformation failed for spacecraft \"{0}\": {1}")]
331 FrameTransformation(AssetId, String),
332}
333
334impl<O: Origin + Copy + Send + Sync, R: ReferenceFrame + Copy + Send + Sync> Scenario<O, R> {
335 pub fn new(start_time: Time<Tai>, end_time: Time<Tai>, origin: O, frame: R) -> Self {
337 let interval = TimeInterval::new(start_time, end_time);
338 Self::with_interval(interval, origin, frame)
339 }
340
341 pub fn with_interval(interval: TimeInterval<Tai>, origin: O, frame: R) -> Self {
343 Self {
344 interval,
345 origin,
346 frame,
347 ground_stations: Vec::new(),
348 spacecraft: Vec::new(),
349 constellations: Vec::new(),
350 }
351 }
352
353 pub fn with_spacecraft(mut self, spacecraft: &[Spacecraft]) -> Self {
355 self.spacecraft = spacecraft.into();
356 self
357 }
358
359 pub fn with_ground_stations(mut self, ground_stations: &[GroundStation]) -> Self {
361 self.ground_stations = ground_stations.into();
362 self
363 }
364
365 pub fn interval(&self) -> &TimeInterval<Tai> {
367 &self.interval
368 }
369
370 pub fn origin(&self) -> O {
372 self.origin
373 }
374
375 pub fn frame(&self) -> R {
377 self.frame
378 }
379
380 pub fn with_constellation(
383 mut self,
384 constellation: DynConstellation,
385 ) -> Result<Self, ConstellationConvertError> {
386 let epoch = constellation.epoch();
387 let origin = constellation.origin();
388 let frame = constellation.frame();
389 let propagator_kind = constellation.propagator();
390 let name = constellation.name().to_string();
391
392 for sat in constellation.satellites() {
393 let keplerian_orbit =
394 KeplerianOrbit::try_from_keplerian(sat.elements, epoch, origin, frame)
395 .map_err(|e| ConstellationConvertError::KeplerianOrbit(e.to_string()))?;
396 let cartesian_orbit = keplerian_orbit
397 .try_to_cartesian()
398 .map_err(|e| ConstellationConvertError::CartesianConversion(e.to_string()))?;
399
400 let orbit_source = match propagator_kind {
401 ConstellationPropagator::Vallado => {
402 let v = DynVallado::try_new(cartesian_orbit)
403 .map_err(|e| ConstellationConvertError::Propagator(e.to_string()))?;
404 OrbitSource::Vallado(v)
405 }
406 ConstellationPropagator::Numerical => {
407 let n = DynNumericalPropagator::try_new(cartesian_orbit)
408 .map_err(|e| ConstellationConvertError::Propagator(e.to_string()))?;
409 OrbitSource::Numerical(n)
410 }
411 ConstellationPropagator::J2 => {
412 let p = DynJ2Propagator::try_new(cartesian_orbit)
413 .map_err(|e| ConstellationConvertError::Propagator(e.to_string()))?;
414 OrbitSource::J2(p)
415 }
416 ConstellationPropagator::J2Osc => {
417 let p = DynJ2Propagator::try_new(cartesian_orbit)
418 .map(|p| p.with_osculating(true))
419 .map_err(|e| ConstellationConvertError::Propagator(e.to_string()))?;
420 OrbitSource::J2(p)
421 }
422 ConstellationPropagator::J4 => {
423 let p = DynJ4Propagator::try_new(cartesian_orbit)
424 .map_err(|e| ConstellationConvertError::Propagator(e.to_string()))?;
425 OrbitSource::J4(p)
426 }
427 ConstellationPropagator::J4Osc => {
428 let p = DynJ4Propagator::try_new(cartesian_orbit)
429 .map(|p| p.with_osculating(true))
430 .map_err(|e| ConstellationConvertError::Propagator(e.to_string()))?;
431 OrbitSource::J4(p)
432 }
433 };
434
435 let sc_id = format!("{} P{} S{}", name, sat.plane + 1, sat.index_in_plane + 1);
436 let sc = Spacecraft::new(sc_id, orbit_source).with_constellation_id(&name);
437 self.spacecraft.push(sc);
438 }
439
440 self.constellations.push(constellation);
441 Ok(self)
442 }
443
444 pub fn constellations(&self) -> &[DynConstellation] {
446 &self.constellations
447 }
448
449 pub fn ground_stations(&self) -> &[GroundStation] {
451 &self.ground_stations
452 }
453
454 pub fn spacecraft(&self) -> &[Spacecraft] {
456 &self.spacecraft
457 }
458
459 pub fn propagate<P>(
468 &self,
469 provider: &P,
470 ) -> Result<Ensemble<AssetId, Tai, O, R>, ScenarioPropagateError>
471 where
472 R: Into<DynFrame>,
473 P: TryRotation<DynFrame, R, DynTimeScale> + Send + Sync,
474 P::Error: std::fmt::Display,
475 {
476 let dyn_interval = TimeInterval::new(
477 self.interval.start().into_dyn(),
478 self.interval.end().into_dyn(),
479 );
480 let origin = self.origin;
481 let frame = self.frame;
482 let entries: Result<HashMap<_, _>, _> = self
483 .spacecraft
484 .par_iter()
485 .map(|sc| {
486 let traj = sc
487 .orbit
488 .propagate(dyn_interval)
489 .map_err(|e| ScenarioPropagateError::Propagate(sc.id.clone(), e))?;
490 let rotated = traj.into_frame(frame, provider).map_err(|e| {
493 ScenarioPropagateError::FrameTransformation(sc.id.clone(), e.to_string())
494 })?;
495 let (epoch, _origin, frame, data) = rotated.into_parts();
497 let typed = lox_orbits::orbits::Trajectory::from_parts(
498 epoch.with_scale(Tai),
499 origin,
500 frame,
501 data,
502 );
503 Ok((sc.id.clone(), typed))
504 })
505 .collect();
506 Ok(Ensemble::new(entries?))
507 }
508
509 pub fn filter_by_constellations(&self, constellations: &[ConstellationId]) -> Self {
511 let spacecraft = self
512 .spacecraft
513 .clone()
514 .into_iter()
515 .filter(|s| s.constellation.is_some())
516 .filter(|s| constellations.contains(s.constellation.as_ref().unwrap()))
517 .collect();
518 Scenario {
519 spacecraft,
520 ..self.clone()
521 }
522 }
523
524 pub fn filter_by_networks(&self, networks: &[NetworkId]) -> Self {
526 let ground_stations = self
527 .ground_stations
528 .clone()
529 .into_iter()
530 .filter(|s| s.network.is_some())
531 .filter(|s| networks.contains(s.network.as_ref().unwrap()))
532 .collect();
533 Scenario {
534 ground_stations,
535 ..self.clone()
536 }
537 }
538}
539
540#[cfg(feature = "imaging")]
541impl PayloadAccessor<OpticalPayload> for Spacecraft {
542 fn extract(&self) -> Option<OpticalPayload> {
543 self.optical_payload
544 }
545}
546
547#[cfg(feature = "imaging")]
548impl PayloadAccessor<SarPayload> for Spacecraft {
549 fn extract(&self) -> Option<SarPayload> {
550 self.sar_payload
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use lox_core::coords::LonLatAlt;
558 use lox_frames::DynFrame;
559 use lox_orbits::ground::GroundLocation;
560 use lox_time::deltas::TimeDelta;
561
562 fn dummy_location() -> DynGroundLocation {
563 let coords = LonLatAlt::from_degrees(-4.3676, 40.4527, 0.0).unwrap();
564 GroundLocation::try_new(coords, DynOrigin::Earth).unwrap()
565 }
566
567 fn dummy_mask() -> ElevationMask {
568 ElevationMask::with_fixed_elevation(0.0)
569 }
570
571 #[test]
574 fn test_asset_id() {
575 let id = AssetId::new("station-1");
576 assert_eq!(id.as_str(), "station-1");
577 assert_eq!(format!("{id}"), "station-1");
578 assert_eq!(id, AssetId::new("station-1"));
579 assert_ne!(id, AssetId::new("station-2"));
580 }
581
582 #[test]
583 fn test_constellation_id() {
584 let id = ConstellationId::new("oneweb");
585 assert_eq!(id.as_str(), "oneweb");
586 assert_eq!(format!("{id}"), "oneweb");
587 }
588
589 #[test]
590 fn test_network_id() {
591 let id = NetworkId::new("estrack");
592 assert_eq!(id.as_str(), "estrack");
593 assert_eq!(format!("{id}"), "estrack");
594 }
595
596 #[test]
599 fn test_ground_station_new() {
600 let loc = dummy_location();
601 let mask = dummy_mask();
602 let gs = GroundStation::new("gs1", loc, mask);
603 assert_eq!(gs.id().as_str(), "gs1");
604 assert_eq!(gs.body_fixed_frame(), DynFrame::Iau(DynOrigin::Earth));
605 }
606
607 #[test]
608 fn test_ground_station_with_body_fixed_frame() {
609 let gs = GroundStation::new("gs1", dummy_location(), dummy_mask())
610 .with_body_fixed_frame(DynFrame::Itrf);
611 assert_eq!(gs.body_fixed_frame(), DynFrame::Itrf);
612 }
613
614 #[test]
615 fn test_ground_station_network_id_none_by_default() {
616 let gs = GroundStation::new("gs1", dummy_location(), dummy_mask());
617 assert!(gs.network_id().is_none());
618 }
619
620 #[test]
621 fn test_ground_station_with_network_id() {
622 let gs =
623 GroundStation::new("gs1", dummy_location(), dummy_mask()).with_network_id("estrack");
624 assert_eq!(gs.network_id(), Some(&NetworkId::new("estrack")));
625 let start = Time::j2000(Tai);
627 let end = start + TimeDelta::from_seconds(86400);
628 let scenario = DynScenario::new(start, end, DynOrigin::Earth, DynFrame::Icrf)
629 .with_ground_stations(&[gs]);
630 let filtered = scenario.filter_by_networks(&[NetworkId::new("estrack")]);
631 assert_eq!(filtered.ground_stations().len(), 1);
632 }
633
634 #[test]
635 fn test_ground_station_location_getter() {
636 let loc = dummy_location();
637 let gs = GroundStation::new("gs1", loc.clone(), dummy_mask());
638 let _ = gs.location(); }
640
641 #[test]
642 fn test_ground_station_mask_getter() {
643 let mask = ElevationMask::with_fixed_elevation(0.1);
644 let gs = GroundStation::new("gs1", dummy_location(), mask.clone());
645 assert_eq!(gs.mask().min_elevation(0.0), 0.1);
646 }
647
648 #[test]
651 fn test_spacecraft_new() {
652 let traj = lox_orbits::orbits::DynTrajectory::from_csv_dyn(
653 &lox_test_utils::read_data_file("trajectory_lunar.csv"),
654 DynOrigin::Earth,
655 DynFrame::Icrf,
656 )
657 .unwrap();
658 let sc = Spacecraft::new("sc1", OrbitSource::Trajectory(traj));
659 assert_eq!(sc.id().as_str(), "sc1");
660 assert!(sc.max_slew_rate().is_none());
661 assert!(sc.constellation_id().is_none());
662 }
663
664 #[test]
665 fn test_spacecraft_with_max_slew_rate() {
666 let traj = lox_orbits::orbits::DynTrajectory::from_csv_dyn(
667 &lox_test_utils::read_data_file("trajectory_lunar.csv"),
668 DynOrigin::Earth,
669 DynFrame::Icrf,
670 )
671 .unwrap();
672 let rate = AngularRate::degrees_per_second(5.0);
673 let sc = Spacecraft::new("sc1", OrbitSource::Trajectory(traj)).with_max_slew_rate(rate);
674 assert!(sc.max_slew_rate().is_some());
675 }
676
677 #[test]
678 fn test_spacecraft_with_constellation_id() {
679 let traj = lox_orbits::orbits::DynTrajectory::from_csv_dyn(
680 &lox_test_utils::read_data_file("trajectory_lunar.csv"),
681 DynOrigin::Earth,
682 DynFrame::Icrf,
683 )
684 .unwrap();
685 let sc =
686 Spacecraft::new("sc1", OrbitSource::Trajectory(traj)).with_constellation_id("oneweb");
687 assert_eq!(sc.constellation_id(), Some(&ConstellationId::new("oneweb")));
688 }
689
690 #[test]
691 fn test_spacecraft_orbit_getter() {
692 let traj = lox_orbits::orbits::DynTrajectory::from_csv_dyn(
693 &lox_test_utils::read_data_file("trajectory_lunar.csv"),
694 DynOrigin::Earth,
695 DynFrame::Icrf,
696 )
697 .unwrap();
698 let sc = Spacecraft::new("sc1", OrbitSource::Trajectory(traj));
699 assert!(matches!(sc.orbit(), OrbitSource::Trajectory(_)));
700 }
701
702 #[test]
705 fn test_scenario_construction() {
706 let start = Time::j2000(Tai);
707 let end = start + TimeDelta::from_seconds(86400);
708 let scenario = DynScenario::new(start, end, DynOrigin::Earth, DynFrame::Icrf);
709 assert_eq!(scenario.origin(), DynOrigin::Earth);
710 assert_eq!(scenario.frame(), DynFrame::Icrf);
711 assert!(scenario.spacecraft().is_empty());
712 assert!(scenario.ground_stations().is_empty());
713 }
714
715 #[test]
716 fn test_scenario_with_assets() {
717 let start = Time::j2000(Tai);
718 let end = start + TimeDelta::from_seconds(86400);
719 let gs = GroundStation::new("gs1", dummy_location(), dummy_mask());
720 let traj = lox_orbits::orbits::DynTrajectory::from_csv_dyn(
721 &lox_test_utils::read_data_file("trajectory_lunar.csv"),
722 DynOrigin::Earth,
723 DynFrame::Icrf,
724 )
725 .unwrap();
726 let sc = Spacecraft::new("sc1", OrbitSource::Trajectory(traj));
727 let scenario = DynScenario::new(start, end, DynOrigin::Earth, DynFrame::Icrf)
728 .with_ground_stations(&[gs])
729 .with_spacecraft(&[sc]);
730 assert_eq!(scenario.ground_stations().len(), 1);
731 assert_eq!(scenario.spacecraft().len(), 1);
732 }
733
734 #[test]
735 fn test_scenario_filter_by_constellations() {
736 let start = Time::j2000(Tai);
737 let end = start + TimeDelta::from_seconds(86400);
738 let traj = lox_orbits::orbits::DynTrajectory::from_csv_dyn(
739 &lox_test_utils::read_data_file("trajectory_lunar.csv"),
740 DynOrigin::Earth,
741 DynFrame::Icrf,
742 )
743 .unwrap();
744 let sc1 = Spacecraft::new("sc1", OrbitSource::Trajectory(traj.clone()))
745 .with_constellation_id("oneweb");
746 let sc2 = Spacecraft::new("sc2", OrbitSource::Trajectory(traj));
747 let scenario = DynScenario::new(start, end, DynOrigin::Earth, DynFrame::Icrf)
748 .with_spacecraft(&[sc1, sc2]);
749 let filtered = scenario.filter_by_constellations(&[ConstellationId::new("oneweb")]);
750 assert_eq!(filtered.spacecraft().len(), 1);
751 assert_eq!(filtered.spacecraft()[0].id().as_str(), "sc1");
752 }
753
754 #[test]
755 fn test_scenario_filter_by_networks() {
756 let start = Time::j2000(Tai);
757 let end = start + TimeDelta::from_seconds(86400);
758 let gs1 =
759 GroundStation::new("gs1", dummy_location(), dummy_mask()).with_network_id("estrack");
760 let gs2 = GroundStation::new("gs2", dummy_location(), dummy_mask());
761 let scenario = DynScenario::new(start, end, DynOrigin::Earth, DynFrame::Icrf)
762 .with_ground_stations(&[gs1, gs2]);
763 let filtered = scenario.filter_by_networks(&[NetworkId::new("estrack")]);
764 assert_eq!(filtered.ground_stations().len(), 1);
765 assert_eq!(filtered.ground_stations()[0].id().as_str(), "gs1");
766 }
767
768 #[test]
769 fn test_scenario_with_constellation() {
770 use lox_core::units::{AngleUnits, DistanceUnits};
771 use lox_orbits::constellations::WalkerDeltaBuilder;
772
773 let start = Time::j2000(Tai);
774 let end = start + TimeDelta::from_seconds(86400);
775 let scenario = DynScenario::new(start, end, DynOrigin::Earth, DynFrame::Icrf);
776
777 let constellation = WalkerDeltaBuilder::new(6, 3)
778 .with_semi_major_axis(7000.0_f64.km(), 0.0)
779 .with_inclination(53.0_f64.deg())
780 .build_constellation("test", start, DynOrigin::Earth, DynFrame::Icrf)
781 .unwrap()
782 .into_dyn();
783
784 let scenario = scenario.with_constellation(constellation).unwrap();
785 assert_eq!(scenario.spacecraft().len(), 6);
786 assert_eq!(scenario.constellations().len(), 1);
787 assert_eq!(scenario.constellations()[0].name(), "test");
788 assert!(scenario.spacecraft()[0].id().as_str().contains("test"));
790 }
791
792 #[test]
793 fn test_scenario_interval() {
794 let start = Time::j2000(Tai);
795 let end = start + TimeDelta::from_seconds(86400);
796 let scenario = DynScenario::new(start, end, DynOrigin::Earth, DynFrame::Icrf);
797 assert_eq!(scenario.interval().start(), start);
798 assert_eq!(scenario.interval().end(), end);
799 }
800
801 #[test]
802 fn test_scenario_propagate() {
803 let traj = lox_orbits::orbits::DynTrajectory::from_csv_dyn(
804 &lox_test_utils::read_data_file("trajectory_lunar.csv"),
805 DynOrigin::Earth,
806 DynFrame::Icrf,
807 )
808 .unwrap();
809 let start = traj.start_time().to_scale(Tai);
810 let end = traj.end_time().to_scale(Tai);
811 let sc = Spacecraft::new("sc1", OrbitSource::Trajectory(traj));
812 let scenario =
813 DynScenario::new(start, end, DynOrigin::Earth, DynFrame::Icrf).with_spacecraft(&[sc]);
814 let ensemble = scenario
815 .propagate(&lox_frames::providers::DefaultRotationProvider)
816 .unwrap();
817 assert_eq!(ensemble.len(), 1);
818 assert!(ensemble.get(&AssetId::new("sc1")).is_some());
819 }
820}