nyx_space/propagators/event.rs
1/*
2 Nyx, blazing fast astrodynamics
3 Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU Affero General Public License as published
7 by the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU Affero General Public License for more details.
14
15 You should have received a copy of the GNU Affero General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use super::PropagationError;
20use crate::dynamics::Dynamics;
21use crate::linalg::allocator::Allocator;
22use crate::linalg::DefaultAllocator;
23use crate::md::trajectory::{Interpolatable, Traj};
24use crate::propagators::{PropAlmanacSnafu, PropAnalysisSnafu, TrajectoryEventSnafu};
25use crate::time::{Duration, Epoch};
26use crate::State;
27use anise::analysis::event::Event;
28use anise::analysis::{brent_solver, AnalysisError};
29use anise::frames::Frame;
30use log::info;
31use rayon::iter::ParallelBridge;
32use rayon::prelude::ParallelIterator;
33use snafu::ResultExt;
34use std::f64;
35use std::sync::mpsc::channel;
36
37use super::PropInstance;
38
39impl<D: Dynamics> PropInstance<'_, D>
40where
41 DefaultAllocator: Allocator<<D::StateType as State>::Size>
42 + Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>
43 + Allocator<<D::StateType as State>::VecLength>,
44{
45 /// Propagates the dynamics until the specified event has occurred one, or until `max_duration` is reached. Refer to [until_nth_event] for details.
46 pub fn until_event(
47 &mut self,
48 max_duration: Duration,
49 event: &Event,
50 event_frame: Option<Frame>,
51 ) -> Result<(D::StateType, Traj<D::StateType>), PropagationError>
52 where
53 <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
54 D::StateType: Interpolatable,
55 {
56 self.until_nth_event(max_duration, event, event_frame, 1)
57 }
58
59 /// Propagates the dynamics until the specified event has occurred `trigger` times, or until `max_duration` is reached.
60 ///
61 /// This method monitors the provided `event` during propagation. Once the event condition is met
62 /// `trigger` number of times (e.g., set `trigger` to 1 for the first occurrence), the propagation stops
63 /// at the end of that integration step.
64 ///
65 /// A root-finding algorithm (Brent's method) is then used to locate the exact time of the event
66 /// within the final integration step. The returned state corresponds to this precise event time,
67 /// interpolated from the trajectory.
68 ///
69 /// # Arguments
70 ///
71 /// * `max_duration` - The maximum duration to propagate if the event is not triggered the requested number of times.
72 /// * `event` - The event definition (scalar expression and condition) to monitor.
73 /// * `trigger` - The 1-based index of the event occurrence to stop at (e.g. 1 for the first crossing, 2 for the second).
74 ///
75 /// # Returns
76 ///
77 /// A tuple containing:
78 /// 1. The interpolated state exactly at the moment the $n$-th event occurred.
79 /// 2. The full trajectory recorded up to the end of the propagation step where the event occurred.
80 ///
81 /// # Errors
82 ///
83 /// * `PropagationError::NthEventError`: Returned if `max_duration` is reached before the event was triggered `trigger` times.
84 /// * `PropagationError::TrajectoryEvent`: Returned if the interpolation of the event state fails.
85 /// * `PropagationError::Analysis`: Returned if the event evaluation fails during the search.
86 pub fn until_nth_event(
87 &mut self,
88 max_duration: Duration,
89 event: &Event,
90 event_frame: Option<Frame>,
91 trigger: usize,
92 ) -> Result<(D::StateType, Traj<D::StateType>), PropagationError>
93 where
94 <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
95 D::StateType: Interpolatable,
96 {
97 info!("Propagating until {event} or {max_duration}");
98
99 let mut crossing_counts = 0;
100 let closure_almanac = self.almanac.clone();
101 let orbit = if let Some(observer_frame) = event_frame {
102 self.almanac
103 .transform_to(self.state.orbit(), observer_frame, None)
104 .context(PropAlmanacSnafu)?
105 } else {
106 self.state.orbit()
107 };
108
109 let mut y_prev = event
110 .eval(orbit, &self.almanac)
111 .context(PropAnalysisSnafu)?;
112
113 let enough_crossings = |next_state: D::StateType| -> Result<bool, PropagationError> {
114 let orbit = if let Some(observer_frame) = event_frame {
115 closure_almanac
116 .transform_to(next_state.orbit(), observer_frame, None)
117 .context(PropAlmanacSnafu)?
118 } else {
119 next_state.orbit()
120 };
121
122 let y_next = event
123 .eval(orbit, &closure_almanac)
124 .context(PropAnalysisSnafu)?;
125
126 let delta = (y_next - y_prev).abs();
127
128 if event.scalar.is_angle() {
129 // Atan2 is a triangular signal so a bracket exists only if y_prev is negative and y_next is positive.
130 // Anything else is a fluke, and we can quickly speed through the whole trajectory.
131 if y_prev.signum() != y_next.signum() && delta < 180.0 {
132 crossing_counts += 1;
133 }
134 } else {
135 // Previous step accepted, check if there was a zero crossing.
136 if y_prev * y_next < 0.0 {
137 crossing_counts += 1;
138 }
139 }
140 y_prev = y_next;
141
142 Ok(crossing_counts >= trigger)
143 };
144
145 // Build the trajectory during the propagation.
146 let end_state;
147 let mut traj = Traj::new();
148 let start_state = self.state;
149
150 let rx = {
151 // Channels that have a single state for the propagator
152 let (tx, rx) = channel();
153 // Propagate the dynamics
154 end_state = self.propagate(max_duration, Some(tx), Some(enough_crossings))?;
155 // Note that the end state is also sent on the channel before the return of this function.
156 rx
157 };
158
159 traj.states = rx.into_iter().par_bridge().collect();
160 // Push the start state -- will be reordered in the finalize call.
161 // For some reason, this must happen at the end -- can't figure out why.
162 traj.states.push(start_state);
163
164 traj.finalize();
165
166 // We pushed at least one item to this trajectory, so the following unwrap will always succeed.
167 let last_traj_state = traj.states.last().cloned().unwrap();
168 if end_state == last_traj_state {
169 return Err(PropagationError::NthEventError {
170 nth: trigger,
171 found: crossing_counts,
172 });
173 }
174
175 // Add the final state to the trajectory so we can search it in full.
176 traj.states.push(end_state);
177
178 // We have one bracket to search.
179 let traj_at = |epoch: Epoch| -> Result<f64, AnalysisError> {
180 let state = traj
181 .at(epoch)
182 .map_err(|e| AnalysisError::GenericAnalysisError {
183 err: format!("{e}"),
184 })?;
185
186 event.eval(state.orbit(), &self.almanac)
187 };
188
189 // Else Brent solver between these states
190
191 let event_epoch = brent_solver(traj_at, event, last_traj_state.epoch(), end_state.epoch())
192 .context(PropAnalysisSnafu)?;
193
194 Ok((traj.at(event_epoch).context(TrajectoryEventSnafu)?, traj))
195 }
196}