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::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}