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::State;
21use crate::dynamics::Dynamics;
22use crate::linalg::DefaultAllocator;
23use crate::linalg::allocator::Allocator;
24use crate::md::trajectory::{Interpolatable, Traj};
25use crate::propagators::{PropAlmanacSnafu, PropAnalysisSnafu, TrajectoryEventSnafu};
26use crate::time::{Duration, Epoch};
27use anise::analysis::event::Event;
28use anise::analysis::{AnalysisError, brent_solver};
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#[cfg(not(target_arch = "wasm32"))]
37use std::time::Instant;
38
39use super::PropInstance;
40
41impl<D: Dynamics> PropInstance<'_, D>
42where
43 DefaultAllocator: Allocator<<D::StateType as State>::Size>
44 + Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>
45 + Allocator<<D::StateType as State>::VecLength>,
46{
47 /// Propagates the dynamics until the specified event has occurred one, or until `max_duration` is reached. Refer to [until_nth_event] for details.
48 pub fn until_event(
49 &mut self,
50 max_duration: Duration,
51 event: &Event,
52 event_frame: Option<Frame>,
53 ) -> Result<(D::StateType, Traj<D::StateType>), PropagationError>
54 where
55 <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
56 D::StateType: Interpolatable,
57 {
58 self.until_nth_event(max_duration, event, event_frame, 1)
59 }
60
61 /// Propagates the dynamics until the specified event has occurred `trigger` times, or until `max_duration` is reached.
62 ///
63 /// This method monitors the provided `event` during propagation. Once the event condition is met
64 /// `trigger` number of times (e.g., set `trigger` to 1 for the first occurrence), the propagation stops
65 /// at the end of that integration step.
66 ///
67 /// A root-finding algorithm (Brent's method) is then used to locate the exact time of the event
68 /// within the final integration step. The returned state corresponds to this precise event time,
69 /// interpolated from the trajectory.
70 ///
71 /// # Arguments
72 ///
73 /// * `max_duration` - The maximum duration to propagate if the event is not triggered the requested number of times.
74 /// * `event` - The event definition (scalar expression and condition) to monitor.
75 /// * `trigger` - The 1-based index of the event occurrence to stop at (e.g. 1 for the first crossing, 2 for the second).
76 ///
77 /// # Returns
78 ///
79 /// A tuple containing:
80 /// 1. The interpolated state exactly at the moment the $n$-th event occurred.
81 /// 2. The full trajectory recorded up to the end of the propagation step where the event occurred.
82 ///
83 /// # Errors
84 ///
85 /// * `PropagationError::NthEventError`: Returned if `max_duration` is reached before the event was triggered `trigger` times.
86 /// * `PropagationError::TrajectoryEvent`: Returned if the interpolation of the event state fails.
87 /// * `PropagationError::Analysis`: Returned if the event evaluation fails during the search.
88 pub fn until_nth_event(
89 &mut self,
90 max_duration: Duration,
91 event: &Event,
92 event_frame: Option<Frame>,
93 trigger: usize,
94 ) -> Result<(D::StateType, Traj<D::StateType>), PropagationError>
95 where
96 <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
97 D::StateType: Interpolatable,
98 {
99 info!("Propagating until {event} or {max_duration}");
100
101 let mut crossing_counts = 0;
102 let closure_almanac = self.almanac.clone();
103 let orbit = if let Some(observer_frame) = event_frame {
104 self.almanac
105 .transform_to(self.state.orbit(), observer_frame, None)
106 .context(PropAlmanacSnafu)?
107 } else {
108 self.state.orbit()
109 };
110
111 let mut y_prev = event
112 .eval(orbit, &self.almanac)
113 .context(PropAnalysisSnafu)?;
114
115 let enough_crossings = |next_state: D::StateType| -> Result<bool, PropagationError> {
116 let orbit = if let Some(observer_frame) = event_frame {
117 closure_almanac
118 .transform_to(next_state.orbit(), observer_frame, None)
119 .context(PropAlmanacSnafu)?
120 } else {
121 next_state.orbit()
122 };
123
124 let y_next = event
125 .eval(orbit, &closure_almanac)
126 .context(PropAnalysisSnafu)?;
127
128 let delta = (y_next - y_prev).abs();
129
130 if event.scalar.is_angle() {
131 // Atan2 is a triangular signal so a bracket exists only if y_prev is negative and y_next is positive.
132 // Anything else is a fluke, and we can quickly speed through the whole trajectory.
133 if y_prev.signum() != y_next.signum() && delta < 180.0 {
134 crossing_counts += 1;
135 }
136 } else {
137 // Previous step accepted, check if there was a zero crossing.
138 if y_prev * y_next < 0.0 {
139 crossing_counts += 1;
140 }
141 }
142 y_prev = y_next;
143
144 Ok(crossing_counts >= trigger)
145 };
146
147 // Build the trajectory during the propagation.
148 let end_state;
149 let mut traj = Traj::new();
150 let start_state = self.state;
151
152 // Keep our own tick because it's only available to the propagate function.
153 #[cfg(not(target_arch = "wasm32"))]
154 let tick = Instant::now();
155
156 let rx = {
157 // Channels that have a single state for the propagator
158 let (tx, rx) = channel();
159 // Propagate the dynamics
160 end_state = self.propagate(max_duration, Some(tx), Some(enough_crossings))?;
161 // Note that the end state is also sent on the channel before the return of this function.
162 rx
163 };
164
165 traj.states = rx.into_iter().par_bridge().collect();
166 // Push the start state -- will be reordered in the finalize call.
167 // For some reason, this must happen at the end -- can't figure out why.
168 traj.states.push(start_state);
169
170 traj.finalize();
171
172 // We pushed at least one item to this trajectory, so the following unwrap will always succeed.
173 let last_traj_state = traj.states.last().cloned().unwrap();
174 if end_state == last_traj_state {
175 return Err(PropagationError::NthEventError {
176 nth: trigger,
177 found: crossing_counts,
178 });
179 }
180
181 // Add the final state to the trajectory so we can search it in full.
182 traj.states.push(end_state);
183
184 // We have one bracket to search.
185 let traj_at = |epoch: Epoch| -> Result<f64, AnalysisError> {
186 let state = traj
187 .at(epoch)
188 .map_err(|e| AnalysisError::GenericAnalysisError {
189 err: format!("{e}"),
190 })?;
191
192 event.eval(state.orbit(), &self.almanac)
193 };
194
195 // Else Brent solver between these states
196 let event_epoch = brent_solver(traj_at, event, last_traj_state.epoch(), end_state.epoch())
197 .context(PropAnalysisSnafu)?;
198
199 #[cfg(not(target_arch = "wasm32"))]
200 {
201 if self.log_progress {
202 let tock: Duration = tick.elapsed().into();
203 if trigger > 1 {
204 info!("\t... event #{trigger} found in {tock}");
205 } else {
206 info!("\t... event found in {tock}");
207 }
208 }
209 }
210 Ok((traj.at(event_epoch).context(TrajectoryEventSnafu)?, traj))
211 }
212}