nyx_space/propagators/
instance.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::{DynamicsSnafu, IntegrationDetails, PropagationError, Propagator};
20use crate::dynamics::{Dynamics, DynamicsAlmanacSnafu};
21use crate::linalg::allocator::Allocator;
22use crate::linalg::{DefaultAllocator, OVector};
23use crate::md::trajectory::{Interpolatable, Traj};
24use crate::md::EventEvaluator;
25use crate::propagators::TrajectoryEventSnafu;
26use crate::time::{Duration, Epoch, Unit};
27use crate::State;
28use anise::almanac::Almanac;
29use anise::errors::MathError;
30use log::{debug, info, warn};
31use rayon::iter::ParallelBridge;
32use rayon::prelude::ParallelIterator;
33use snafu::ResultExt;
34use std::f64;
35use std::sync::mpsc::{channel, Sender};
36use std::sync::Arc;
37#[cfg(not(target_arch = "wasm32"))]
38use std::time::Instant;
39
40/// A Propagator allows propagating a set of dynamics forward or backward in time.
41/// It is an EventTracker, without any event tracking. It includes the options, the integrator
42/// details of the previous step, and the set of coefficients used for the monomorphic instance.
43pub struct PropInstance<'a, D: Dynamics>
44where
45    DefaultAllocator: Allocator<<D::StateType as State>::Size>
46        + Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>
47        + Allocator<<D::StateType as State>::VecLength>,
48{
49    /// The state of this propagator instance
50    pub state: D::StateType,
51    /// The propagator setup (kind, stages, etc.)
52    pub prop: &'a Propagator<D>,
53    /// Stores the details of the previous integration step
54    pub details: IntegrationDetails,
55    /// Should progress reports be logged
56    pub log_progress: bool,
57    pub(crate) almanac: Arc<Almanac>,
58    pub(crate) step_size: Duration, // Stores the adapted step for the _next_ call
59    pub(crate) fixed_step: bool,
60    // Allows us to do pre-allocation of the ki vectors
61    pub(crate) k: Vec<OVector<f64, <D::StateType as State>::VecLength>>,
62}
63
64impl<D: Dynamics> PropInstance<'_, D>
65where
66    DefaultAllocator: Allocator<<D::StateType as State>::Size>
67        + Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>
68        + Allocator<<D::StateType as State>::VecLength>,
69{
70    /// Sets this instance to not log progress
71    pub fn quiet(mut self) -> Self {
72        self.log_progress = false;
73        self
74    }
75
76    /// Sets this instance to log progress
77    pub fn verbose(mut self) -> Self {
78        self.log_progress = true;
79        self
80    }
81
82    /// Allows setting the step size of the propagator
83    pub fn set_step(&mut self, step_size: Duration, fixed: bool) {
84        self.step_size = step_size;
85        self.fixed_step = fixed;
86    }
87
88    #[allow(clippy::erasing_op)]
89    fn for_duration_channel_option(
90        &mut self,
91        duration: Duration,
92        maybe_tx_chan: Option<Sender<D::StateType>>,
93    ) -> Result<D::StateType, PropagationError> {
94        if duration == 0 * Unit::Second {
95            return Ok(self.state);
96        }
97        let stop_time = self.state.epoch() + duration;
98
99        if self.log_progress {
100            // Prevent the print spam for orbit determination cases
101            info!("Propagating for {duration} until {stop_time}");
102        }
103        // Call `finally` on the current state to set anything up
104        self.state = self
105            .prop
106            .dynamics
107            .finally(self.state, self.almanac.clone())
108            .context(DynamicsSnafu)?;
109
110        let backprop = duration.is_negative();
111        if backprop {
112            self.step_size = -self.step_size; // Invert the step size
113        }
114
115        // Transform the state if needed
116        let mut original_frame = None;
117        if let Some(integration_frame) = self.prop.opts.integration_frame {
118            if integration_frame != self.state.orbit().frame {
119                original_frame = Some(self.state.orbit().frame);
120                let mut new_orbit = self
121                    .almanac
122                    .transform_to(self.state.orbit(), integration_frame, None)
123                    .context(DynamicsAlmanacSnafu {
124                        action: "transforming state into desired integration frame",
125                    })
126                    .context(DynamicsSnafu)?;
127                // If the integration frame has parameters, we set them here.
128                if let Some(mu_km3_s2) = integration_frame.mu_km3_s2 {
129                    new_orbit.frame.mu_km3_s2 = Some(mu_km3_s2);
130                }
131                // If the integration frame has parameters, we set them here.
132                if let Some(shape) = integration_frame.shape {
133                    new_orbit.frame.shape = Some(shape);
134                }
135                if self.log_progress {
136                    info!("State transformed to the integration frame {integration_frame}");
137                }
138                self.state.set_orbit(new_orbit);
139            }
140        }
141
142        #[cfg(not(target_arch = "wasm32"))]
143        let tick = Instant::now();
144        #[cfg(not(target_arch = "wasm32"))]
145        let mut prev_tick = Instant::now();
146
147        loop {
148            let epoch = self.state.epoch();
149            if (!backprop && epoch + self.step_size > stop_time)
150                || (backprop && epoch + self.step_size <= stop_time)
151            {
152                if stop_time == epoch {
153                    // No propagation necessary
154                    #[cfg(not(target_arch = "wasm32"))]
155                    {
156                        if self.log_progress {
157                            let tock: Duration = tick.elapsed().into();
158                            debug!("Done in {tock}");
159                        }
160                    }
161
162                    // Rotate back if needed
163                    if let Some(original_frame) = original_frame {
164                        let new_orbit = self
165                            .almanac
166                            .transform_to(self.state.orbit(), original_frame, None)
167                            .context(DynamicsAlmanacSnafu {
168                                action: "transforming state from desired integration frame",
169                            })
170                            .context(DynamicsSnafu)?;
171                        self.state.set_orbit(new_orbit);
172                    }
173
174                    return Ok(self.state);
175                }
176                // Take one final step of exactly the needed duration until the stop time
177                let prev_step_size = self.step_size;
178                let prev_step_kind = self.fixed_step;
179                self.set_step(stop_time - epoch, true);
180
181                self.single_step()?;
182
183                // Publish to channel if provided
184                if let Some(ref chan) = maybe_tx_chan {
185                    if let Err(e) = chan.send(self.state) {
186                        warn!("{e} when sending on channel")
187                    }
188                }
189
190                // Restore the step size for subsequent calls
191                self.set_step(prev_step_size, prev_step_kind);
192
193                if backprop {
194                    self.step_size = -self.step_size; // Restore to a positive step size
195                }
196
197                #[cfg(not(target_arch = "wasm32"))]
198                {
199                    if self.log_progress {
200                        let tock: Duration = tick.elapsed().into();
201                        info!("\t... done in {tock}");
202                    }
203                }
204
205                // Rotate back if needed
206                if let Some(original_frame) = original_frame {
207                    let new_orbit = self
208                        .almanac
209                        .transform_to(self.state.orbit(), original_frame, None)
210                        .context(DynamicsAlmanacSnafu {
211                            action: "transforming state from desired integration frame",
212                        })
213                        .context(DynamicsSnafu)?;
214                    self.state.set_orbit(new_orbit);
215                }
216
217                return Ok(self.state);
218            } else {
219                #[cfg(not(target_arch = "wasm32"))]
220                {
221                    if self.log_progress {
222                        let tock: Duration = prev_tick.elapsed().into();
223                        if tock.to_unit(Unit::Minute) > 1.0 {
224                            // Report status every minute
225                            let cur_epoch = self.state.epoch();
226                            let dur_to_go = (stop_time - cur_epoch).floor(Unit::Second * 1);
227                            info!(
228                                "\t... current epoch {}, remaining {} (step size = {})",
229                                cur_epoch, dur_to_go, self.details.step
230                            );
231                            prev_tick = Instant::now();
232                        }
233                    }
234                }
235                self.single_step()?;
236                // Publish to channel if provided
237                if let Some(ref chan) = maybe_tx_chan {
238                    if let Err(e) = chan.send(self.state) {
239                        warn!("{e} when sending on channel")
240                    }
241                }
242            }
243        }
244    }
245
246    /// This method propagates the provided Dynamics for the provided duration.
247    pub fn for_duration(&mut self, duration: Duration) -> Result<D::StateType, PropagationError> {
248        self.for_duration_channel_option(duration, None)
249    }
250
251    /// This method propagates the provided Dynamics for the provided duration and publishes each state on the channel.
252    pub fn for_duration_with_channel(
253        &mut self,
254        duration: Duration,
255        tx_chan: Sender<D::StateType>,
256    ) -> Result<D::StateType, PropagationError> {
257        self.for_duration_channel_option(duration, Some(tx_chan))
258    }
259
260    /// Propagates the provided Dynamics until the provided epoch. Returns the end state.
261    pub fn until_epoch(&mut self, end_time: Epoch) -> Result<D::StateType, PropagationError> {
262        let duration: Duration = end_time - self.state.epoch();
263        self.for_duration(duration)
264    }
265
266    /// Propagates the provided Dynamics until the provided epoch and publishes states on the provided channel. Returns the end state.
267    pub fn until_epoch_with_channel(
268        &mut self,
269        end_time: Epoch,
270        tx_chan: Sender<D::StateType>,
271    ) -> Result<D::StateType, PropagationError> {
272        let duration: Duration = end_time - self.state.epoch();
273        self.for_duration_with_channel(duration, tx_chan)
274    }
275
276    /// Propagates the provided Dynamics for the provided duration and generate the trajectory of these dynamics on its own thread.
277    /// Returns the end state and the trajectory.
278    #[allow(clippy::map_clone)]
279    pub fn for_duration_with_traj(
280        &mut self,
281        duration: Duration,
282    ) -> Result<(D::StateType, Traj<D::StateType>), PropagationError>
283    where
284        <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
285        D::StateType: Interpolatable,
286    {
287        let end_state;
288        let mut traj = Traj::new();
289        let start_state = self.state;
290
291        let rx = {
292            // Channels that have a single state for the propagator
293            let (tx, rx) = channel();
294            // Propagate the dynamics
295            // Note that the end state is also sent on the channel before the return of this function.
296            end_state = self.for_duration_with_channel(duration, tx)?;
297            rx
298        };
299
300        traj.states = rx.into_iter().par_bridge().collect();
301        // Push the start state -- will be reordered in the finalize call.
302        // For some reason, this must happen at the end -- can't figure out why.
303        traj.states.push(start_state);
304
305        traj.finalize();
306
307        Ok((end_state, traj))
308    }
309
310    /// Propagates the provided Dynamics until the provided epoch and generate the trajectory of these dynamics on its own thread.
311    /// Returns the end state and the trajectory.
312    /// Known bug #190: Cannot generate a valid trajectory when propagating backward
313    pub fn until_epoch_with_traj(
314        &mut self,
315        end_time: Epoch,
316    ) -> Result<(D::StateType, Traj<D::StateType>), PropagationError>
317    where
318        <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
319        D::StateType: Interpolatable,
320    {
321        let duration: Duration = end_time - self.state.epoch();
322        self.for_duration_with_traj(duration)
323    }
324
325    /// Propagate until a specific event is found once.
326    /// Returns the state found and the trajectory until `max_duration`
327    pub fn until_event<F: EventEvaluator<D::StateType>>(
328        &mut self,
329        max_duration: Duration,
330        event: &F,
331    ) -> Result<(D::StateType, Traj<D::StateType>), PropagationError>
332    where
333        <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
334        D::StateType: Interpolatable,
335    {
336        self.until_nth_event(max_duration, event, 0)
337    }
338
339    /// Propagate until a specific event is found `trigger` times.
340    /// Returns the state found and the trajectory until `max_duration`
341    pub fn until_nth_event<F: EventEvaluator<D::StateType>>(
342        &mut self,
343        max_duration: Duration,
344        event: &F,
345        trigger: usize,
346    ) -> Result<(D::StateType, Traj<D::StateType>), PropagationError>
347    where
348        <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
349        D::StateType: Interpolatable,
350    {
351        info!("Searching for {event}");
352
353        let (_, traj) = self.for_duration_with_traj(max_duration)?;
354        // Now, find the requested event
355        let events = traj
356            .find(event, None, self.almanac.clone())
357            .context(TrajectoryEventSnafu)?;
358        match events.get(trigger) {
359            Some(event_state) => Ok((event_state.state, traj)),
360            None => Err(PropagationError::NthEventError {
361                nth: trigger,
362                found: events.len(),
363            }),
364        }
365    }
366
367    /// Take a single propagator step and emit the result on the TX channel (if enabled)
368    pub fn single_step(&mut self) -> Result<(), PropagationError> {
369        let (t, state_vec) = self.derive()?;
370        self.state.set(self.state.epoch() + t, &state_vec);
371        self.state = self
372            .prop
373            .dynamics
374            .finally(self.state, self.almanac.clone())
375            .context(DynamicsSnafu)?;
376        Ok(())
377    }
378
379    /// This method integrates whichever function is provided as `d_xdt`. Everything passed to this function is in **seconds**.
380    ///
381    /// This function returns the step sized used (as a Duration) and the new state as y_{n+1} = y_n + \frac{dy_n}{dt}.
382    /// To get the integration details, check `self.latest_details`.
383    fn derive(
384        &mut self,
385    ) -> Result<(Duration, OVector<f64, <D::StateType as State>::VecLength>), PropagationError>
386    {
387        let state_vec = &self.state.to_vector();
388        let state_ctx = &self.state;
389        // Reset the number of attempts used (we don't reset the error because it's set before it's read)
390        self.details.attempts = 1;
391        // Convert the step size to seconds -- it's mutable because we may change it below
392        let mut step_size_s = self.step_size.to_seconds();
393        loop {
394            let ki = self
395                .prop
396                .dynamics
397                .eom(0.0, state_vec, state_ctx, self.almanac.clone())
398                .context(DynamicsSnafu)?;
399            self.k[0] = ki;
400            let mut a_idx: usize = 0;
401            for i in 0..(self.prop.method.stages() - 1) {
402                // Let's compute the c_i by summing the relevant items from the list of coefficients.
403                // \sum_{j=1}^{i-1} a_ij  ∀ i ∈ [2, s]
404                let mut ci: f64 = 0.0;
405                // The wi stores the a_{s1} * k_1 + a_{s2} * k_2 + ... + a_{s, s-1} * k_{s-1} +
406                let mut wi = OVector::<f64, <D::StateType as State>::VecLength>::from_element(0.0);
407                for kj in &self.k[0..i + 1] {
408                    let a_ij = self.prop.method.a_coeffs()[a_idx];
409                    ci += a_ij;
410                    wi += a_ij * kj;
411                    a_idx += 1;
412                }
413
414                let ki = self
415                    .prop
416                    .dynamics
417                    .eom(
418                        ci * step_size_s,
419                        &(state_vec + step_size_s * wi),
420                        state_ctx,
421                        self.almanac.clone(),
422                    )
423                    .context(DynamicsSnafu)?;
424                self.k[i + 1] = ki;
425            }
426            // Compute the next state and the error
427            let mut next_state = state_vec.clone();
428            // State error estimation from https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#Adaptive_Runge%E2%80%93Kutta_methods
429            // This is consistent with GMAT https://github.com/ChristopherRabotin/GMAT/blob/37201a6290e7f7b941bc98ee973a527a5857104b/src/base/propagator/RungeKutta.cpp#L537
430            let mut error_est =
431                OVector::<f64, <D::StateType as State>::VecLength>::from_element(0.0);
432            for (i, ki) in self.k.iter().enumerate() {
433                let b_i = self.prop.method.b_coeffs()[i];
434                if !self.fixed_step {
435                    let b_i_star = self.prop.method.b_coeffs()[i + self.prop.method.stages()];
436                    error_est += step_size_s * (b_i - b_i_star) * ki;
437                }
438                next_state += step_size_s * b_i * ki;
439            }
440
441            if self.fixed_step {
442                // Using a fixed step, no adaptive step necessary
443                self.details.step = self.step_size;
444                return Ok(((self.details.step), next_state));
445            } else {
446                // Compute the error estimate.
447                self.details.error =
448                    self.prop
449                        .opts
450                        .error_ctrl
451                        .estimate(&error_est, &next_state, state_vec);
452
453                if self.details.error <= self.prop.opts.tolerance
454                    || step_size_s <= self.prop.opts.min_step.to_seconds()
455                    || self.details.attempts >= self.prop.opts.attempts
456                {
457                    if next_state.iter().any(|x| x.is_nan()) {
458                        return Err(PropagationError::PropMathError {
459                            source: MathError::DomainError {
460                                value: f64::NAN,
461                                msg: "try another integration method, or decrease step size; part of state vector is",
462                            },
463                        });
464                    }
465                    if self.details.attempts >= self.prop.opts.attempts {
466                        warn!(
467                            "Could not further decrease step size: maximum number of attempts reached ({})",
468                            self.details.attempts
469                        );
470                    }
471
472                    self.details.step = step_size_s * Unit::Second;
473                    if self.details.error < self.prop.opts.tolerance {
474                        // Let's increase the step size for the next iteration.
475                        // Error is less than tolerance, let's attempt to increase the step for the next iteration.
476                        let proposed_step = 0.9
477                            * step_size_s
478                            * (self.prop.opts.tolerance / self.details.error)
479                                .powf(1.0 / f64::from(self.prop.method.order()));
480
481                        step_size_s =
482                            if proposed_step.abs() > self.prop.opts.max_step.to_seconds().abs() {
483                                self.prop.opts.max_step.to_seconds() * proposed_step.signum()
484                            } else {
485                                proposed_step
486                            };
487                    }
488                    // In all cases, let's update the step size to whatever was the adapted step size
489                    self.step_size = step_size_s * Unit::Second;
490                    if self.step_size.abs() < self.prop.opts.min_step {
491                        // Custom signum in case the step size becomes zero.
492                        let signum = if self.step_size.is_negative() {
493                            -1.0
494                        } else {
495                            1.0
496                        };
497                        self.step_size = self.prop.opts.min_step * signum;
498                    }
499                    return Ok((self.details.step, next_state));
500                } else {
501                    // Error is too high and we aren't using the smallest step, and we haven't hit the max number of attempts.
502                    // So let's adapt the step size.
503                    self.details.attempts += 1;
504                    let proposed_step_s = 0.9
505                        * step_size_s
506                        * (self.prop.opts.tolerance / self.details.error)
507                            .powf(1.0 / f64::from(self.prop.method.order() - 1));
508
509                    step_size_s = if proposed_step_s < self.prop.opts.min_step.to_seconds() {
510                        self.prop.opts.min_step.to_seconds()
511                    } else {
512                        proposed_step_s
513                    };
514                    // Note that we don't set self.step_size, that will be updated right before we return
515                }
516            }
517        }
518    }
519
520    /// Copy the details of the latest integration step.
521    pub fn latest_details(&self) -> IntegrationDetails {
522        self.details
523    }
524}