1use super::drag::{drag_and_partials, drag_force};
2use super::point_gravity::{point_gravity, point_gravity_and_partials};
3use super::settings::PropSettings;
4
5use crate::earthgravity;
6use crate::lpephem;
7use crate::ode;
8use crate::ode::ODEError;
9use crate::ode::ODEResult;
10use crate::ode::RKAdaptive;
11use crate::orbitprop::Precomputed;
12use crate::{Duration, Instant};
13use lpephem::sun::shadowfunc;
14
15use anyhow::{Context, Result};
16
17use crate::types::*;
18
19use crate::consts;
20use crate::orbitprop::SatProperties;
21use num_traits::identities::Zero;
22
23use thiserror::Error;
24
25use nalgebra as na;
26
27use serde::{Deserialize, Serialize};
28
29#[derive(Debug, Serialize, Deserialize, Clone)]
30pub struct PropagationResult<const T: usize> {
31 pub time_start: Instant,
32 pub state_start: Matrix<6, T>,
33 pub time_end: Instant,
34 pub state_end: Matrix<6, T>,
35 pub accepted_steps: u32,
36 pub rejected_steps: u32,
37 pub num_eval: u32,
38 pub odesol: Option<ode::ODESolution<Matrix<6, T>>>,
39}
40
41impl<const T: usize> PropagationResult<T> {
42 pub fn interp(&self, time: &Instant) -> Result<Matrix<6, T>> {
43 interp_propresult(self, time)
44 }
45}
46
47pub type StateType<const C: usize> = na::SMatrix<f64, 6, C>;
48
49pub type SimpleState = StateType<1>;
51
52pub type CovState = StateType<7>;
54
55#[derive(Debug, Error)]
56pub enum PropagationError {
57 #[error("Invalid number of columns: {c}")]
58 InvalidStateColumns { c: usize },
59 #[error("No Dense Output in Solution")]
60 NoDenseOutputInSolution,
61 #[error("ODE Error: {0}")]
62 ODEError(ode::ODEError),
63}
64
65pub fn propagate<const C: usize>(
191 state: &StateType<C>,
192 start: &Instant,
193 stop: &Instant,
194 settings: &PropSettings,
195 satprops: Option<&dyn SatProperties>,
196) -> Result<PropagationResult<C>> {
197 let x_end: f64 = (*stop - *start).as_seconds();
199
200 let odesettings = crate::ode::RKAdaptiveSettings {
201 abserror: settings.abs_error,
202 relerror: settings.rel_error,
203 dense_output: settings.enable_interp,
204 ..Default::default()
205 };
206
207 let interp: &Precomputed = {
209 if let Some(sinterp) = &settings.precomputed {
210 if stop > start {
211 if (*start >= sinterp.start) && (*stop <= sinterp.stop) {
212 sinterp
213 } else {
214 &Precomputed::new(start, stop)
215 .context("Cannot compute precomputed interpolation data for propagation")?
216 }
217 } else if (*stop >= sinterp.start) && (*start <= sinterp.stop) {
218 sinterp
219 } else {
220 &Precomputed::new(start, stop)
221 .context("Cannot compute precomputed interpolation data for propagation")?
222 }
223 } else {
224 &Precomputed::new(start, stop)
225 .context("Cannot compute precomputed interpolation dat for propagation")?
226 }
227 };
228
229 let ydot = |x: f64, y: &Matrix<6, C>| -> ODEResult<Matrix<6, C>> {
230 let time: Instant = *start + Duration::from_seconds(x);
232
233 let pos_gcrf: na::Vector3<f64> = y.fixed_view::<3, 1>(0, 0).into();
235 let vel_gcrf: na::Vector3<f64> = y.fixed_view::<3, 1>(3, 0).into();
236
237 let (qgcrf2itrf, sun_gcrf, moon_gcrf) = match interp.interp(&time) {
239 Ok(v) => v,
240 Err(e) => return Err(ODEError::YDotError(e.to_string())),
241 };
242 let qitrf2gcrf = qgcrf2itrf.conjugate();
243
244 let pos_itrf = qgcrf2itrf * pos_gcrf;
246
247 const fn is_one<const C2: usize>() -> bool {
248 C2 == 1
249 }
250 const fn is_seven<const C2: usize>() -> bool {
251 C2 == 7
252 }
253
254 if is_one::<C>() {
256 let mut accel = Vector3::zeros();
257
258 let gravity_itrf =
260 earthgravity::jgm3().accel(&pos_itrf, settings.gravity_order as usize);
261
262 accel += qitrf2gcrf * gravity_itrf;
264
265 accel += point_gravity(&pos_gcrf, &moon_gcrf, crate::consts::MU_MOON);
267
268 accel += point_gravity(&pos_gcrf, &sun_gcrf, crate::consts::MU_SUN);
270
271 if let Some(props) = satprops {
273 let ss = y.fixed_view::<6, 1>(0, 0);
274
275 let solarpressure = -shadowfunc(&sun_gcrf, &pos_gcrf)
277 * props.cr_a_over_m(&time, &ss.into())
278 * 4.56e-6
279 * sun_gcrf
280 / sun_gcrf.norm();
281 accel += solarpressure;
282
283 if pos_gcrf.norm() < 700.0e3 + crate::consts::EARTH_RADIUS {
285 let cd_a_over_m = props.cd_a_over_m(&time, &ss.into());
286
287 if cd_a_over_m > 1e-6 {
288 accel += drag_force(
289 &pos_gcrf,
290 &pos_itrf,
291 &vel_gcrf,
292 &time,
293 cd_a_over_m,
294 settings.use_spaceweather,
295 );
296 }
297 }
298 } let mut dy = Matrix::<6, C>::zeros();
301 dy.fixed_view_mut::<3, 1>(0, 0).copy_from(&vel_gcrf);
303
304 dy.fixed_view_mut::<3, 1>(3, 0).copy_from(&accel);
306
307 Ok(dy)
308 }
309 else if is_seven::<C>() {
311 let (gravity_accel, gravity_partials) =
314 earthgravity::jgm3().accel_and_partials(&pos_itrf, settings.gravity_order as usize);
315 let (sun_accel, sun_partials) =
316 point_gravity_and_partials(&pos_gcrf, &sun_gcrf, consts::MU_SUN);
317 let (moon_accel, moon_partials) =
318 point_gravity_and_partials(&pos_gcrf, &moon_gcrf, consts::MU_MOON);
319
320 let mut accel = qitrf2gcrf * gravity_accel + sun_accel + moon_accel;
321
322 let mut dfdy: StateType<6> = StateType::<6>::zeros();
324 dfdy.fixed_view_mut::<3, 3>(0, 3)
325 .copy_from(&na::Matrix3::<f64>::identity());
326
327 let ritrf2gcrf = qitrf2gcrf.to_rotation_matrix();
328 let mut dadr = ritrf2gcrf * gravity_partials * ritrf2gcrf.transpose()
331 + sun_partials
332 + moon_partials;
333
334 if let Some(props) = satprops {
336 let ss = y.fixed_view::<6, 1>(0, 0);
339
340 let solarpressure = -shadowfunc(&sun_gcrf, &pos_gcrf)
344 * props.cr_a_over_m(&time, &ss.into())
345 * 4.56e-6
346 * sun_gcrf
347 / sun_gcrf.norm();
348 accel += solarpressure;
349
350 if pos_gcrf.norm() < 700.0e3 + crate::consts::EARTH_RADIUS {
352 let cd_a_over_m = props.cd_a_over_m(&time, &ss.into());
353 if cd_a_over_m > 1e-6 {
354 let (drag_accel, ddragaccel_dr, ddragaccel_dv) = drag_and_partials(
355 &pos_gcrf,
356 &qgcrf2itrf,
357 &vel_gcrf,
358 &time,
359 cd_a_over_m,
360 settings.use_spaceweather,
361 );
362
363 accel += drag_accel;
365
366 dadr += ddragaccel_dr;
369
370 dfdy.fixed_view_mut::<3, 3>(3, 3).copy_from(&ddragaccel_dv);
373 }
374 }
375 }
376 dfdy.fixed_view_mut::<3, 3>(3, 0).copy_from(&dadr);
377
378 let dphi: na::Matrix<f64, na::Const<6>, na::Const<6>, na::ArrayStorage<f64, 6, 6>> =
380 dfdy * y.fixed_view::<6, 6>(0, 1);
381
382 let mut dy = Matrix::<6, C>::zero();
383 dy.fixed_view_mut::<3, 1>(0, 0).copy_from(&vel_gcrf);
384 dy.fixed_view_mut::<3, 1>(3, 0).copy_from(&accel);
385 dy.fixed_view_mut::<6, 6>(0, 1).copy_from(&dphi);
386 Ok(dy)
387 } else {
388 ODEError::YDotError(PropagationError::InvalidStateColumns { c: C }.to_string()).into()
389 }
390 };
391
392 match settings.enable_interp {
393 false => {
394 let res = match crate::ode::solvers::RKV98NoInterp::integrate(
395 0.0,
396 x_end,
397 state,
398 ydot,
399 &odesettings,
400 ) {
401 Ok(res) => res,
402 Err(e) => return Err(PropagationError::ODEError(e).into()),
403 };
404
405 Ok(PropagationResult {
406 time_start: *start,
407 state_start: *state,
408 time_end: *stop,
409 state_end: res.y,
410 accepted_steps: res.naccept as u32,
411 rejected_steps: res.nreject as u32,
412 num_eval: res.nevals as u32,
413 odesol: Some(res),
414 })
415 }
416 true => {
417 let res = crate::ode::solvers::RKV98::integrate(0.0, x_end, state, ydot, &odesettings)?;
418 Ok(PropagationResult {
419 time_start: *start,
420 state_start: *state,
421 time_end: *stop,
422 state_end: res.y,
423 accepted_steps: res.naccept as u32,
424 rejected_steps: res.nreject as u32,
425 num_eval: res.nevals as u32,
426 odesol: Some(res),
427 })
428 }
429 }
430}
431
432pub fn interp_propresult<const C: usize>(
433 res: &PropagationResult<C>,
434 time: &Instant,
435) -> Result<StateType<C>> {
436 if let Some(sol) = &res.odesol {
437 if sol.dense.is_some() {
438 let x = (time - res.time_start).as_seconds();
439 let y = crate::ode::solvers::RKV98::interpolate(x, sol)?;
440 Ok(y)
441 } else {
442 Err(PropagationError::NoDenseOutputInSolution.into())
443 }
444 } else {
445 Err(PropagationError::NoDenseOutputInSolution.into())
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use crate::{consts, orbitprop::SatPropertiesStatic};
453 use std::f64::consts::PI;
454
455 use std::fs::File;
456
457 use crate::Duration;
458 use std::io::{self, BufRead};
459
460 #[test]
461 fn test_short_propagate() -> Result<()> {
462 let starttime = Instant::from_datetime(2015, 3, 20, 0, 0, 0.0)?;
463 let stoptime = starttime + Duration::from_seconds(0.1);
464
465 let mut state: SimpleState = SimpleState::zeros();
466
467 state[0] = consts::GEO_R;
468 state[4] = (consts::MU_EARTH / consts::GEO_R).sqrt();
469
470 let settings = PropSettings {
471 abs_error: 1.0e-9,
472 rel_error: 1.0e-14,
473 gravity_order: 4,
474 ..Default::default()
475 };
476
477 let _res1 = propagate(&state, &starttime, &stoptime, &settings, None)?;
478
479 Ok(())
480 }
481
482 #[test]
483 fn test_propagate() -> Result<()> {
484 let starttime = Instant::from_datetime(2015, 3, 20, 0, 0, 0.0)?;
485 let stoptime = starttime + Duration::from_days(0.25);
486
487 let mut state: SimpleState = SimpleState::zeros();
488
489 state[0] = consts::GEO_R;
490 state[4] = (consts::MU_EARTH / consts::GEO_R).sqrt();
491
492 let mut settings = PropSettings {
493 abs_error: 1.0e-9,
494 rel_error: 1.0e-14,
495 gravity_order: 4,
496 ..Default::default()
497 };
498 settings.precompute_terms(&starttime, &stoptime)?;
499
500 let res1 = propagate(&state, &starttime, &stoptime, &settings, None)?;
501 let res2 = propagate(&res1.state_end, &stoptime, &starttime, &settings, None)?;
503 for ix in 0..6_usize {
505 assert!((res2.state_end[ix] - state[ix]).abs() < 1.0)
506 }
507
508 Ok(())
509 }
510
511 #[test]
512 fn test_interp() -> Result<()> {
513 let starttime = Instant::from_datetime(2015, 3, 20, 0, 0, 0.0)?;
514 let stoptime = starttime + Duration::from_days(1.0);
515
516 let mut state: SimpleState = SimpleState::zeros();
517
518 state[0] = consts::GEO_R;
519 state[4] = (consts::MU_EARTH / consts::GEO_R).sqrt();
520
521 let settings = PropSettings {
522 abs_error: 1.0e-9,
523 rel_error: 1.0e-14,
524 gravity_order: 4,
525 ..Default::default()
526 };
527
528 let res = propagate(&state, &starttime, &stoptime, &settings, None)?;
530 let res2 = propagate(&res.state_end, &stoptime, &starttime, &settings, None)?;
532 for ix in 0..6_usize {
534 assert!((state[ix] - res2.state_end[ix]).abs() < 1.0e-3);
535 }
536
537 let newtime = starttime + Duration::from_days(0.45);
539 let interp = res.interp(&newtime)?;
540 let interp2 = res2.interp(&newtime)?;
541 for ix in 0..6_usize {
542 assert!((interp[ix] - interp2[ix]).abs() < 1e-3);
543 }
544 Ok(())
545 }
546
547 #[test]
548 fn test_state_transition() -> Result<()> {
549 let starttime = Instant::from_datetime(2015, 3, 20, 0, 0, 0.0)?;
558 let stoptime = starttime + Duration::from_days(0.5);
559
560 let mut state: CovState = CovState::zeros();
561
562 let theta = PI / 6.0;
563 state[0] = consts::GEO_R * theta.cos();
564 state[2] = consts::GEO_R * theta.sin();
565 state[4] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.cos();
566 state[5] = (consts::MU_EARTH / consts::GEO_R).sqrt() * theta.sin();
567 state
568 .fixed_view_mut::<6, 6>(0, 1)
569 .copy_from(&na::Matrix6::<f64>::identity());
570
571 let settings = PropSettings {
572 abs_error: 1.0e-9,
573 rel_error: 1.0e-14,
574 gravity_order: 4,
575 ..Default::default()
576 };
577
578 let dstate = na::vector![6.0, -10.0, 120.5, 0.1, 0.2, -0.3];
580
581 let res = propagate(&state, &starttime, &stoptime, &settings, None)?;
583
584 let res2 = propagate(
586 &(state.fixed_view::<6, 1>(0, 0) + dstate),
587 &starttime,
588 &stoptime,
589 &settings,
590 None,
591 )?;
592
593 let dstate_prop = res2.state_end - res.state_end.fixed_view::<6, 1>(0, 0);
596
597 let dstate_phi = res.state_end.fixed_view::<6, 6>(0, 1) * dstate;
599 for ix in 0..6_usize {
600 assert!((dstate_prop[ix] - dstate_phi[ix]).abs() / dstate_prop[ix] < 1e-3);
601 }
602
603 Ok(())
604 }
605
606 #[test]
607 fn test_state_transition_drag() -> Result<()> {
608 let starttime = Instant::from_datetime(2015, 3, 20, 0, 0, 0.0)?;
618 let stoptime = starttime + crate::Duration::from_days(0.2);
619
620 let mut state: CovState = CovState::zeros();
621
622 let pgcrf = na::vector![3059573.85713792, 5855177.98848048, -7191.45042671];
623 let vgcrf = na::vector![916.08123489, -468.22498656, 7700.48460839];
624
625 state.fixed_view_mut::<3, 1>(0, 0).copy_from(&pgcrf);
627 state.fixed_view_mut::<3, 1>(3, 0).copy_from(&vgcrf);
628 state
629 .fixed_view_mut::<6, 6>(0, 1)
630 .copy_from(&na::Matrix6::<f64>::identity());
631
632 let settings = PropSettings {
633 abs_error: 1.0e-9,
634 rel_error: 1.0e-14,
635 gravity_order: 4,
636 ..Default::default()
637 };
638
639 let satprops: SatPropertiesStatic = SatPropertiesStatic::new(2.0 * 0.3 * 0.1 / 5.0, 0.0);
640
641 let dstate = na::vector![2.0, -4.0, 20.5, 0.05, 0.02, -0.01];
643
644 let res = propagate(&state, &starttime, &stoptime, &settings, Some(&satprops))?;
647
648 let res2 = propagate(
650 &(state.fixed_view::<6, 1>(0, 0) + dstate),
651 &starttime,
652 &stoptime,
653 &settings,
654 Some(&satprops),
655 )?;
656
657 let dstate_prop = res2.state_end - res.state_end.fixed_view::<6, 1>(0, 0);
660
661 let dstate_phi = res.state_end.fixed_view::<6, 6>(0, 1) * dstate;
662
663 for ix in 0..6_usize {
665 assert!((dstate_prop[ix] - dstate_phi[ix]).abs() / dstate_prop[ix] < 0.1);
666 }
667
668 Ok(())
669 }
670
671 #[test]
672 fn test_gps() -> Result<()> {
673 let testvecfile = crate::utils::test::get_testvec_dir()
674 .unwrap()
675 .join("orbitprop")
676 .join("ESA0OPSFIN_20233640000_01D_05M_ORB.SP3");
677
678 if !testvecfile.is_file() {
679 anyhow::bail!(
680 "Required GPS SP3 File: \"{}\" does not exist.
681 Clone test vectors from:
682 <https://storage.googleapis.com/satkit-testvecs/>
683 or using python script in satkit repo: `python/test/download_testvecs.py`
684 or set \"SATKIT_TESTVEC_ROOT\" to point to directory",
685 testvecfile.to_string_lossy()
686 );
687 }
688 let file: File = File::open(testvecfile.clone())?;
689
690 let times: Vec<crate::Instant> = io::BufReader::new(file)
691 .lines()
692 .filter(|x: &Result<String, io::Error>| x.as_ref().unwrap().starts_with('*'))
693 .map(|rline| -> Result<crate::Instant> {
694 let line = rline.unwrap();
695 let lvals: Vec<&str> = line.split_whitespace().collect();
696 let year: i32 = lvals[1].parse()?;
697 let mon: i32 = lvals[2].parse()?;
698 let day: i32 = lvals[3].parse()?;
699 let hour: i32 = lvals[4].parse()?;
700 let min: i32 = lvals[5].parse()?;
701 let sec: f64 = lvals[6].parse()?;
702 Instant::from_datetime(year, mon, day, hour, min, sec)
703 })
704 .collect::<Result<Vec<crate::Instant>, _>>()?;
705
706 let file: File = File::open(testvecfile)?;
707
708 let satnum: usize = 20;
709 let satstr = format!("PG{}", satnum);
710 let pitrf: Vec<na::Vector3<f64>> = io::BufReader::new(file)
711 .lines()
712 .filter(|x| {
713 let rline = &x.as_ref().unwrap()[0..4];
714 rline == satstr
715 })
716 .map(|rline| -> Result<na::Vector3<f64>> {
717 let line = rline.unwrap();
718 let lvals: Vec<&str> = line.split_whitespace().collect();
719 let px: f64 = lvals[1].parse()?;
720 let py: f64 = lvals[2].parse()?;
721 let pz: f64 = lvals[3].parse()?;
722 Ok(na::vector![px, py, pz] * 1.0e3)
723 })
724 .collect::<Result<Vec<na::Vector3<f64>>>>()?;
725
726 assert!(times.len() == pitrf.len());
727 let pgcrf: Vec<na::Vector3<f64>> = pitrf
728 .iter()
729 .enumerate()
730 .map(|(idx, p)| {
731 let q = crate::frametransform::qitrf2gcrf(×[idx]);
732 q * p
733 })
734 .collect();
735
736 let v0 = na::vector![
737 2.47130562e+03,
738 2.94682753e+03,
739 -5.34172176e+02,
740 2.32565692e-02
741 ];
742
743 let state0 = na::vector![pgcrf[0][0], pgcrf[0][1], pgcrf[0][2], v0[0], v0[1], v0[2]];
744 let satprops: SatPropertiesStatic = SatPropertiesStatic::new(0.0, v0[3]);
745
746 let settings = PropSettings {
747 enable_interp: true,
748 ..Default::default()
749 };
750
751 let res = propagate(
752 &state0,
753 ×[0],
754 ×[times.len() - 1],
755 &settings,
756 Some(&satprops),
757 )?;
758
759 for iv in 0..(pgcrf.len()) {
762 let interp_state = res.interp(×[iv])?;
763 for ix in 0..3 {
764 assert!((pgcrf[iv][ix] - interp_state[ix]).abs() < 8.0);
765 }
766 }
767
768 Ok(())
769 }
770}