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::dynamics::{Dynamics, DynamicsAlmanacSnafu};
21use crate::linalg::allocator::Allocator;
22use crate::linalg::{DefaultAllocator, OVector};
23use crate::md::trajectory::{Interpolatable, Traj};
24use crate::time::{Duration, Epoch, Unit};
25use crate::State;
26use anise::almanac::Almanac;
27use anise::errors::MathError;
28use log::{debug, info, warn};
29use rayon::iter::ParallelBridge;
30use rayon::prelude::ParallelIterator;
31use snafu::ResultExt;
32use std::f64;
33use std::sync::mpsc::{channel, Sender};
34use std::sync::Arc;
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.clone())
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            if integration_frame != self.state.orbit().frame {
121                original_frame = Some(self.state.orbit().frame);
122                let mut new_orbit = self
123                    .almanac
124                    .transform_to(self.state.orbit(), integration_frame, None)
125                    .context(DynamicsAlmanacSnafu {
126                        action: "transforming state into desired integration frame",
127                    })
128                    .context(DynamicsSnafu)?;
129                // If the integration frame has parameters, we set them here.
130                if let Some(mu_km3_s2) = integration_frame.mu_km3_s2 {
131                    new_orbit.frame.mu_km3_s2 = Some(mu_km3_s2);
132                }
133                // If the integration frame has parameters, we set them here.
134                if let Some(shape) = integration_frame.shape {
135                    new_orbit.frame.shape = Some(shape);
136                }
137                if self.log_progress {
138                    info!("State transformed to the integration frame {integration_frame}");
139                }
140                self.state.set_orbit(new_orbit);
141            }
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                            debug!("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                    if let Err(e) = chan.send(self.state) {
191                        warn!("{e} when sending on channel")
192                    }
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                    if condition(self.state)? {
245                        // Stopping condition triggered. We don't send
246                        // the new state on the channel for the caller to know that the exact
247                        // condition they are seeking is between the last state on the channel
248                        // and the state we're returning
249
250                        return Ok(self.state);
251                    }
252                }
253
254                // Publish to channel if provided
255                if let Some(ref chan) = maybe_tx_chan {
256                    if let Err(e) = chan.send(self.state) {
257                        warn!("{e} when sending on channel")
258                    }
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    /// Known bug #190: Cannot generate a valid trajectory when propagating backward
331    pub fn until_epoch_with_traj(
332        &mut self,
333        end_time: Epoch,
334    ) -> Result<(D::StateType, Traj<D::StateType>), PropagationError>
335    where
336        <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
337        D::StateType: Interpolatable,
338    {
339        let duration: Duration = end_time - self.state.epoch();
340        self.for_duration_with_traj(duration)
341    }
342
343    /// Take a single propagator step and emit the result on the TX channel (if enabled)
344    pub fn single_step(&mut self) -> Result<(), PropagationError> {
345        let (t, state_vec) = self.derive()?;
346        self.state.set(self.state.epoch() + t, &state_vec);
347        self.state = self
348            .prop
349            .dynamics
350            .finally(self.state, self.almanac.clone())
351            .context(DynamicsSnafu)?;
352        Ok(())
353    }
354
355    /// This method integrates whichever function is provided as `d_xdt`. Everything passed to this function is in **seconds**.
356    ///
357    /// This function returns the step sized used (as a Duration) and the new state as y_{n+1} = y_n + \frac{dy_n}{dt}.
358    /// To get the integration details, check `self.latest_details`.
359    fn derive(
360        &mut self,
361    ) -> Result<(Duration, OVector<f64, <D::StateType as State>::VecLength>), PropagationError>
362    {
363        let state_vec = &self.state.to_vector();
364        let state_ctx = &self.state;
365        // Reset the number of attempts used (we don't reset the error because it's set before it's read)
366        self.details.attempts = 1;
367        // Convert the step size to seconds -- it's mutable because we may change it below
368        let mut step_size_s = self.step_size.to_seconds();
369        loop {
370            let ki = self
371                .prop
372                .dynamics
373                .eom(0.0, state_vec, state_ctx, self.almanac.clone())
374                .context(DynamicsSnafu)?;
375            self.k[0] = ki;
376            let mut a_idx: usize = 0;
377            for i in 0..(self.prop.method.stages() - 1) {
378                // Let's compute the c_i by summing the relevant items from the list of coefficients.
379                // \sum_{j=1}^{i-1} a_ij  ∀ i ∈ [2, s]
380                let mut ci: f64 = 0.0;
381                // The wi stores the a_{s1} * k_1 + a_{s2} * k_2 + ... + a_{s, s-1} * k_{s-1} +
382                let mut wi = OVector::<f64, <D::StateType as State>::VecLength>::from_element(0.0);
383                for kj in &self.k[0..i + 1] {
384                    let a_ij = self.prop.method.a_coeffs()[a_idx];
385                    ci += a_ij;
386                    wi += a_ij * kj;
387                    a_idx += 1;
388                }
389
390                let ki = self
391                    .prop
392                    .dynamics
393                    .eom(
394                        ci * step_size_s,
395                        &(state_vec + step_size_s * wi),
396                        state_ctx,
397                        self.almanac.clone(),
398                    )
399                    .context(DynamicsSnafu)?;
400                self.k[i + 1] = ki;
401            }
402            // Compute the next state and the error
403            let mut next_state = state_vec.clone();
404            // State error estimation from https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#Adaptive_Runge%E2%80%93Kutta_methods
405            // This is consistent with GMAT https://github.com/ChristopherRabotin/GMAT/blob/37201a6290e7f7b941bc98ee973a527a5857104b/src/base/propagator/RungeKutta.cpp#L537
406            let mut error_est =
407                OVector::<f64, <D::StateType as State>::VecLength>::from_element(0.0);
408            for (i, ki) in self.k.iter().enumerate() {
409                let b_i = self.prop.method.b_coeffs()[i];
410                if !self.fixed_step {
411                    let b_i_star = self.prop.method.b_coeffs()[i + self.prop.method.stages()];
412                    error_est += step_size_s * (b_i - b_i_star) * ki;
413                }
414                next_state += step_size_s * b_i * ki;
415            }
416
417            if self.fixed_step {
418                // Using a fixed step, no adaptive step necessary
419                self.details.step = self.step_size;
420                return Ok(((self.details.step), next_state));
421            } else {
422                // Compute the error estimate.
423                self.details.error =
424                    self.prop
425                        .opts
426                        .error_ctrl
427                        .estimate(&error_est, &next_state, state_vec);
428
429                if self.details.error <= self.prop.opts.tolerance
430                    || step_size_s <= self.prop.opts.min_step.to_seconds()
431                    || self.details.attempts >= self.prop.opts.attempts
432                {
433                    if next_state.iter().any(|x| x.is_nan()) {
434                        return Err(PropagationError::PropMathError {
435                            source: MathError::DomainError {
436                                value: f64::NAN,
437                                msg: "try another integration method, or decrease step size; part of state vector is",
438                            },
439                        });
440                    }
441                    if self.details.attempts >= self.prop.opts.attempts {
442                        warn!(
443                            "Could not further decrease step size: maximum number of attempts reached ({})",
444                            self.details.attempts
445                        );
446                    }
447
448                    self.details.step = step_size_s * Unit::Second;
449                    if self.details.error < self.prop.opts.tolerance {
450                        // Let's increase the step size for the next iteration.
451                        // Error is less than tolerance, let's attempt to increase the step for the next iteration.
452                        let proposed_step = 0.9
453                            * step_size_s
454                            * (self.prop.opts.tolerance / self.details.error)
455                                .powf(1.0 / f64::from(self.prop.method.order()));
456
457                        step_size_s =
458                            if proposed_step.abs() > self.prop.opts.max_step.to_seconds().abs() {
459                                self.prop.opts.max_step.to_seconds() * proposed_step.signum()
460                            } else {
461                                proposed_step
462                            };
463                    }
464                    // In all cases, let's update the step size to whatever was the adapted step size
465                    self.step_size = step_size_s * Unit::Second;
466                    if self.step_size.abs() < self.prop.opts.min_step {
467                        // Custom signum in case the step size becomes zero.
468                        let signum = if self.step_size.is_negative() {
469                            -1.0
470                        } else {
471                            1.0
472                        };
473                        self.step_size = self.prop.opts.min_step * signum;
474                    }
475                    return Ok((self.details.step, next_state));
476                } else {
477                    // Error is too high and we aren't using the smallest step, and we haven't hit the max number of attempts.
478                    // So let's adapt the step size.
479                    self.details.attempts += 1;
480                    let proposed_step_s = 0.9
481                        * step_size_s
482                        * (self.prop.opts.tolerance / self.details.error)
483                            .powf(1.0 / f64::from(self.prop.method.order() - 1));
484
485                    step_size_s = if proposed_step_s < self.prop.opts.min_step.to_seconds() {
486                        self.prop.opts.min_step.to_seconds()
487                    } else {
488                        proposed_step_s
489                    };
490                    // Note that we don't set self.step_size, that will be updated right before we return
491                }
492            }
493        }
494    }
495
496    /// Copy the details of the latest integration step.
497    pub fn latest_details(&self) -> IntegrationDetails {
498        self.details
499    }
500}