Skip to main content

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