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