Skip to main content

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}