Skip to main content

hpr_sim/
integrator.rs

1//! Initial-value integrators: the adaptive Dormand–Prince 5(4) pair with dense output, and the
2//! classical fixed-step fourth-order Runge–Kutta method.
3//!
4//! [`Integrator::advance`] takes accepted steps from the current time toward a stop time and
5//! returns at the stop time, at the first events on the way, or when the system asks to stop. A
6//! stop time is always a step boundary, so a caller puts discontinuities there (burnout, a
7//! thrust-curve knot, a phase change) and never lets one fall inside a step
8//! ([Loft lesson L23][l23]: Loft let burnout fall inside steps). The system declares its events
9//! and sees each accepted step, with its dense output, through [`OdeSystem`]'s provided methods.
10//!
11//! [l23]: https://nrdptel.github.io/hpr-sim/decisions-and-roadmap.html#l23
12//!
13//! - **Dormand–Prince 5(4)** ([`Method::DormandPrince54`]): the pair of J. R. Dormand and
14//!   P. J. Prince, "A family of embedded Runge-Kutta formulae", *J. Comput. Appl. Math.* 6 (1980)
15//!   19–26, advancing with the fifth-order solution (local extrapolation). The coefficients, the
16//!   error norm, the PI step-size controller, the starting step and the fourth-order continuous
17//!   extension follow E. Hairer and G. Wanner's `DOPRI5` (version of 2004, BSD-2-Clause, pinned
18//!   as `hairer-dopri5` in `validation/refs.lock.toml`), which implements Hairer, Nørsett and
19//!   Wanner, *Solving Ordinary Differential Equations I*, 2nd ed., Springer, 1993, §II.4–II.6,
20//!   and §IV.2 of volume II. The port is noted in `THIRD-PARTY-NOTICES.md`.
21//! - **RK4** ([`Method::Rk4`]): Kutta's classical method (HNW I, §II.1, table 1.2) with a fixed
22//!   step and cubic Hermite dense output from the derivatives at both ends.
23//!
24//! Method, tests and limits: `docs/physics/integration.md`.
25
26use std::fmt;
27use std::ops::ControlFlow;
28
29use serde::{Deserialize, Serialize};
30use thiserror::Error;
31
32use crate::events::{Direction, EVENT_TIME_RESOLUTION_S, RootError, find_root};
33
34/// A first-order system `y' = f(t, y)` with `N` components, its events and its step observer.
35///
36/// Only [`derivative`](Self::derivative) is required. The event methods declare scalar functions
37/// `g_i(t, y)` whose crossings stop the integration (none by default), and
38/// [`accept_step`](Self::accept_step) sees every accepted step. One type carries all three so
39/// that a flight phase can record from, and stop on, its own state.
40pub trait OdeSystem<const N: usize> {
41    /// The error the derivative can return.
42    type Error;
43
44    /// The derivative `f(t, y)`.
45    ///
46    /// # Errors
47    ///
48    /// Whatever the system can't evaluate. In an adaptive step the integrator treats an error as a
49    /// rejection and retries with a shorter step, because a long step's stages can probe states
50    /// off the trajectory; the error is returned once the step can't shrink further, or at once
51    /// for the step's first stage and for RK4.
52    fn derivative(&mut self, t_s: f64, y: &[f64; N]) -> Result<[f64; N], Self::Error>;
53
54    /// Per-component weights `wᵢ` on the absolute tolerance, so that components in different
55    /// units share one [`Adaptive::absolute_tolerance`]: component `i` is held to
56    /// `wᵢ·atol + rtol·|yᵢ|`. Every weight must be finite and positive. All ones by default.
57    fn absolute_tolerance_weights(&self) -> [f64; N] {
58        [1.0; N]
59    }
60
61    /// The number of event functions (none by default).
62    fn event_count(&self) -> usize {
63        0
64    }
65
66    /// The direction of event `index`.
67    fn event_direction(&self, _index: usize) -> Direction {
68        Direction::Either
69    }
70
71    /// `g_index(t, y)`. A non-finite value stops the integration with
72    /// [`IntegrationError::EventNotFinite`].
73    fn event_value(&mut self, _index: usize, _t_s: f64, _y: &[f64; N]) -> f64 {
74        f64::NAN
75    }
76
77    /// Sees each accepted step, including one shortened to end at an event or a stop time.
78    /// Returning `Break` stops the integration at the end of this step with [`Advance::Stopped`]
79    /// (unless events fired there, which take precedence).
80    fn accept_step(&mut self, _step: &Step<N>) -> ControlFlow<()> {
81        ControlFlow::Continue(())
82    }
83}
84
85/// Settings for the adaptive Dormand–Prince 5(4) method.
86#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
87#[serde(default, deny_unknown_fields)]
88pub struct Adaptive {
89    /// The relative tolerance `rtol` on each component.
90    pub relative_tolerance: f64,
91    /// The absolute tolerance `atol`, in the state's units, scaled per component by
92    /// [`OdeSystem::absolute_tolerance_weights`].
93    pub absolute_tolerance: f64,
94    /// The first step, in seconds, or `None` for Hairer's starting-step estimate (HNW I, §II.4).
95    pub initial_step_s: Option<f64>,
96    /// The longest step, in seconds, or `None` for no limit. Events that cross and return within
97    /// one step go unseen, so this also bounds the event functions' shortest detectable excursion.
98    pub max_step_s: Option<f64>,
99}
100
101impl Default for Adaptive {
102    /// `rtol = atol = 1e-8`, a free first step and no step limit.
103    fn default() -> Self {
104        Self {
105            relative_tolerance: 1e-8,
106            absolute_tolerance: 1e-8,
107            initial_step_s: None,
108            max_step_s: None,
109        }
110    }
111}
112
113/// The integration method.
114#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
115#[serde(tag = "method", deny_unknown_fields)]
116#[non_exhaustive]
117pub enum Method {
118    /// Adaptive Dormand–Prince 5(4) with error control and fourth-order dense output.
119    #[serde(rename = "dopri5")]
120    DormandPrince54(Adaptive),
121    /// Classical RK4 with a fixed step, shortened only to land on a stop time or an event.
122    #[serde(rename = "rk4")]
123    Rk4 {
124        /// The step, in seconds.
125        step_s: f64,
126    },
127}
128
129impl Default for Method {
130    /// Dormand–Prince 5(4) with the default [`Adaptive`] settings.
131    fn default() -> Self {
132        Self::DormandPrince54(Adaptive::default())
133    }
134}
135
136impl Method {
137    /// Checks that tolerances and steps are finite and positive.
138    ///
139    /// # Errors
140    ///
141    /// [`SettingsError`] naming the first bad setting.
142    pub fn validate(&self) -> Result<(), SettingsError> {
143        match self {
144            Self::DormandPrince54(adaptive) => {
145                positive("relative tolerance", adaptive.relative_tolerance)?;
146                positive("absolute tolerance", adaptive.absolute_tolerance)?;
147                if let Some(h) = adaptive.initial_step_s {
148                    positive("initial step", h)?;
149                }
150                if let Some(h) = adaptive.max_step_s {
151                    positive("maximum step", h)?;
152                }
153                Ok(())
154            }
155            Self::Rk4 { step_s } => positive("RK4 step", *step_s),
156        }
157    }
158}
159
160fn positive(what: &'static str, value: f64) -> Result<(), SettingsError> {
161    if value.is_finite() && value > 0.0 {
162        Ok(())
163    } else {
164        Err(SettingsError::NotPositive { what, value })
165    }
166}
167
168/// A setting or initial value the integrator can't use.
169#[derive(Debug, Clone, Copy, PartialEq, Error)]
170#[non_exhaustive]
171pub enum SettingsError {
172    /// A tolerance, step or weight that isn't finite and positive.
173    #[error("the {what} must be finite and positive, not {value}")]
174    NotPositive {
175        /// What the value is.
176        what: &'static str,
177        /// The value.
178        value: f64,
179    },
180    /// A time or state component that isn't finite.
181    #[error("the {what} must be finite, not {value}")]
182    NotFinite {
183        /// What the value is.
184        what: &'static str,
185        /// The value.
186        value: f64,
187    },
188}
189
190/// Why an integration stopped short. The integrator stays at its last accepted step, and a later
191/// [`Integrator::advance`] may resume from there.
192#[derive(Debug, Clone, PartialEq, Error)]
193#[non_exhaustive]
194pub enum IntegrationError<E> {
195    /// The system's derivative failed.
196    #[error("the derivative failed at t = {t_s} s")]
197    Derivative {
198        /// The time of the failed evaluation.
199        t_s: f64,
200        /// The system's error.
201        source: E,
202    },
203    /// The adaptive step fell below the rounding of `t`: the solution changes faster than the
204    /// tolerances allow, or the system is stiff or singular there.
205    #[error("the step fell to {step_s} s at t = {t_s} s")]
206    StepTooSmall {
207        /// Where the integration stopped.
208        t_s: f64,
209        /// The rejected step.
210        step_s: f64,
211    },
212    /// The state, its derivative or the step stopped being finite and a shorter step didn't help.
213    #[error("the solution is not finite after t = {t_s} s")]
214    NotFinite {
215        /// The start of the failed step.
216        t_s: f64,
217    },
218    /// The integrator's step limit was reached. [`Integrator::set_step_limit`] can raise it.
219    #[error("the step limit ({limit}) was reached at t = {t_s} s")]
220    StepLimit {
221        /// Where the integration stopped.
222        t_s: f64,
223        /// The limit, counting accepted and rejected steps.
224        limit: u64,
225    },
226    /// An event function returned a non-finite value, or its crossing couldn't be located.
227    #[error("event {index} failed at t = {t_s} s")]
228    EventNotFinite {
229        /// The event's index.
230        index: usize,
231        /// Where it was evaluated.
232        t_s: f64,
233    },
234    /// A stop time before the current time, or not a number.
235    #[error("cannot integrate from t = {t_s} s to {t_stop_s} s")]
236    Backward {
237        /// The current time.
238        t_s: f64,
239        /// The requested stop time.
240        t_stop_s: f64,
241    },
242    /// A bad setting or tolerance weight.
243    #[error(transparent)]
244    Settings(#[from] SettingsError),
245}
246
247/// How [`Integrator::advance`] returned.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249#[non_exhaustive]
250pub enum Advance {
251    /// The integrator is at the stop time.
252    Reached,
253    /// The integrator is at the earliest event crossing; [`Integrator::fired_events`] lists every
254    /// event past its zero there.
255    Events,
256    /// [`OdeSystem::accept_step`] asked to stop; the integrator is at the end of that step.
257    Stopped,
258}
259
260/// Work counters since the integrator was built.
261#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
262pub struct Stats {
263    /// Derivative evaluations.
264    pub evaluations: u64,
265    /// Accepted steps, including steps shortened to end at an event.
266    pub accepted_steps: u64,
267    /// Rejected steps, including steps whose stages failed.
268    pub rejected_steps: u64,
269}
270
271/// One accepted step and its dense output.
272#[derive(Clone, PartialEq)]
273pub struct Step<const N: usize> {
274    start_s: f64,
275    end_s: f64,
276    /// The span of the dense-output polynomial: the step as taken, before an event shortened it.
277    span_s: f64,
278    start: [f64; N],
279    end: [f64; N],
280    dense: Dense<N>,
281}
282
283#[derive(Clone, PartialEq)]
284enum Dense<const N: usize> {
285    /// Hairer's `CONTD5` form: `y(θ) = y₀ + θ(Δ + (1−θ)(r₂ + θ(r₃ + (1−θ) r₄)))`.
286    Quartic {
287        delta: [f64; N],
288        r2: [f64; N],
289        r3: [f64; N],
290        r4: [f64; N],
291    },
292    /// Cubic Hermite in the same nested form, without `r₄`.
293    Cubic {
294        delta: [f64; N],
295        r2: [f64; N],
296        r3: [f64; N],
297    },
298}
299
300impl<const N: usize> fmt::Debug for Step<N> {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        f.debug_struct("Step")
303            .field("start_s", &self.start_s)
304            .field("end_s", &self.end_s)
305            .field("start", &self.start)
306            .field("end", &self.end)
307            .finish_non_exhaustive()
308    }
309}
310
311/// `Δ = y₁ − y₀`, `r₂ = h f₀ − Δ` and `r₃ = Δ − h f₁ − r₂`, shared by both dense outputs.
312fn hermite_terms<const N: usize>(
313    h: f64,
314    start: &[f64; N],
315    end: &[f64; N],
316    f0: &[f64; N],
317    f1: &[f64; N],
318) -> ([f64; N], [f64; N], [f64; N]) {
319    let mut delta = [0.0; N];
320    let mut r2 = [0.0; N];
321    let mut r3 = [0.0; N];
322    for i in 0..N {
323        delta[i] = end[i] - start[i];
324        r2[i] = h * f0[i] - delta[i];
325        r3[i] = delta[i] - h * f1[i] - r2[i];
326    }
327    (delta, r2, r3)
328}
329
330impl<const N: usize> Step<N> {
331    fn quartic(
332        start_s: f64,
333        end_s: f64,
334        start: [f64; N],
335        end: [f64; N],
336        k1: &[f64; N],
337        k7: &[f64; N],
338        r4: [f64; N],
339    ) -> Self {
340        let h = end_s - start_s;
341        let (delta, r2, r3) = hermite_terms(h, &start, &end, k1, k7);
342        Self {
343            start_s,
344            end_s,
345            span_s: h,
346            start,
347            end,
348            dense: Dense::Quartic { delta, r2, r3, r4 },
349        }
350    }
351
352    fn cubic(
353        start_s: f64,
354        end_s: f64,
355        start: [f64; N],
356        end: [f64; N],
357        f0: &[f64; N],
358        f1: &[f64; N],
359    ) -> Self {
360        let h = end_s - start_s;
361        let (delta, r2, r3) = hermite_terms(h, &start, &end, f0, f1);
362        Self {
363            start_s,
364            end_s,
365            span_s: h,
366            start,
367            end,
368            dense: Dense::Cubic { delta, r2, r3 },
369        }
370    }
371
372    /// The same step ending early at `t_s` in `[start_s, end_s]`, with the dense output's state
373    /// there.
374    fn truncated(&self, t_s: f64) -> Self {
375        let end = self.state_at(t_s);
376        Self {
377            end_s: t_s,
378            end,
379            ..self.clone()
380        }
381    }
382
383    /// The time the step starts, in seconds.
384    #[must_use]
385    pub fn start_s(&self) -> f64 {
386        self.start_s
387    }
388
389    /// The time the step ends, in seconds.
390    #[must_use]
391    pub fn end_s(&self) -> f64 {
392        self.end_s
393    }
394
395    /// The state at the start.
396    #[must_use]
397    pub fn start(&self) -> &[f64; N] {
398        &self.start
399    }
400
401    /// The state at the end.
402    #[must_use]
403    pub fn end(&self) -> &[f64; N] {
404        &self.end
405    }
406
407    /// The dense output at `t`, meant for `t` in `[start_s, end_s]`: the stored states at the two
408    /// ends, fourth order inside a Dormand–Prince step and third order inside an RK4 step.
409    #[must_use]
410    pub fn state_at(&self, t_s: f64) -> [f64; N] {
411        if t_s == self.end_s {
412            return self.end;
413        }
414        if self.span_s == 0.0 {
415            return self.start;
416        }
417        let theta = (t_s - self.start_s) / self.span_s;
418        let theta1 = 1.0 - theta;
419        let mut y = [0.0; N];
420        match &self.dense {
421            Dense::Quartic { delta, r2, r3, r4 } => {
422                for i in 0..N {
423                    y[i] = self.start[i]
424                        + theta * (delta[i] + theta1 * (r2[i] + theta * (r3[i] + theta1 * r4[i])));
425                }
426            }
427            Dense::Cubic { delta, r2, r3 } => {
428                for i in 0..N {
429                    y[i] = self.start[i] + theta * (delta[i] + theta1 * (r2[i] + theta * r3[i]));
430                }
431            }
432        }
433        y
434    }
435}
436
437/// The Dormand–Prince 5(4) tableau with Hairer's error and dense-output coefficients
438/// (`DOPRI5`, subroutine `CDOPRI`).
439mod dp {
440    pub const C2: f64 = 1.0 / 5.0;
441    pub const C3: f64 = 3.0 / 10.0;
442    pub const C4: f64 = 4.0 / 5.0;
443    pub const C5: f64 = 8.0 / 9.0;
444    pub const A21: f64 = 1.0 / 5.0;
445    pub const A31: f64 = 3.0 / 40.0;
446    pub const A32: f64 = 9.0 / 40.0;
447    pub const A41: f64 = 44.0 / 45.0;
448    pub const A42: f64 = -56.0 / 15.0;
449    pub const A43: f64 = 32.0 / 9.0;
450    pub const A51: f64 = 19372.0 / 6561.0;
451    pub const A52: f64 = -25360.0 / 2187.0;
452    pub const A53: f64 = 64448.0 / 6561.0;
453    pub const A54: f64 = -212.0 / 729.0;
454    pub const A61: f64 = 9017.0 / 3168.0;
455    pub const A62: f64 = -355.0 / 33.0;
456    pub const A63: f64 = 46732.0 / 5247.0;
457    pub const A64: f64 = 49.0 / 176.0;
458    pub const A65: f64 = -5103.0 / 18656.0;
459    /// The fifth-order weights, also the last row (FSAL).
460    pub const B1: f64 = 35.0 / 384.0;
461    pub const B3: f64 = 500.0 / 1113.0;
462    pub const B4: f64 = 125.0 / 192.0;
463    pub const B5: f64 = -2187.0 / 6784.0;
464    pub const B6: f64 = 11.0 / 84.0;
465    /// `b − b̂`, the fifth-order weights minus the embedded fourth-order ones.
466    pub const E1: f64 = 71.0 / 57600.0;
467    pub const E3: f64 = -71.0 / 16695.0;
468    pub const E4: f64 = 71.0 / 1920.0;
469    pub const E5: f64 = -17253.0 / 339200.0;
470    pub const E6: f64 = 22.0 / 525.0;
471    pub const E7: f64 = -1.0 / 40.0;
472    /// The continuous extension's last coefficient row.
473    pub const D1: f64 = -12715105075.0 / 11282082432.0;
474    pub const D3: f64 = 87487479700.0 / 32700410799.0;
475    pub const D4: f64 = -10690763975.0 / 1880347072.0;
476    pub const D5: f64 = 701980252875.0 / 199316789632.0;
477    pub const D6: f64 = -1453857185.0 / 822651844.0;
478    pub const D7: f64 = 69997945.0 / 29380423.0;
479}
480
481/// Hairer's step-size controller constants (`DOPRI5` defaults).
482const SAFETY: f64 = 0.9;
483/// The step may shrink by at most a factor of 5 (`1/FAC1`, `FAC1 = 0.2`)...
484const MAX_SHRINK: f64 = 5.0;
485/// ...and grow by at most 10 (`1/FAC2`, `FAC2 = 10`), as a divisor `0.1`.
486const MIN_DIVISOR: f64 = 0.1;
487/// The PI controller's `β` (HNW II, §IV.2).
488const BETA: f64 = 0.04;
489/// The error exponent `1/5 − 0.75 β`.
490const EXPONENT: f64 = 0.2 - BETA * 0.75;
491/// `DOPRI5`'s rounding unit.
492const ROUNDING: f64 = 2.3e-16;
493
494/// Whether a step `h` at `t` is too short to change `t` meaningfully (`DOPRI5`:
495/// `0.1|h| ≤ |t|·uround`), with `|t|` floored at one second so that it also applies near `t = 0`.
496fn negligible_step(t: f64, h: f64) -> bool {
497    0.1 * h <= t.abs().max(1.0) * ROUNDING
498}
499
500/// The default step limit.
501pub const DEFAULT_STEP_LIMIT: u64 = 1_000_000;
502
503/// A stateful integrator for an `N`-component system.
504#[derive(Debug, Clone)]
505pub struct Integrator<const N: usize> {
506    method: Method,
507    t_s: f64,
508    y: [f64; N],
509    /// The next step to try (adaptive); zero until the first step.
510    next_step_s: f64,
511    /// The PI controller's previous error.
512    previous_error: f64,
513    stats: Stats,
514    step_limit: u64,
515    g_start: Vec<f64>,
516    g_end: Vec<f64>,
517    fired: Vec<usize>,
518}
519
520impl<const N: usize> Integrator<N> {
521    /// An integrator at `(t0, y0)`.
522    ///
523    /// # Errors
524    ///
525    /// [`SettingsError`] for a bad method setting or a non-finite `t0` or `y0`.
526    pub fn new(method: Method, t0_s: f64, y0: [f64; N]) -> Result<Self, SettingsError> {
527        method.validate()?;
528        let mut integrator = Self {
529            method,
530            t_s: 0.0,
531            y: [0.0; N],
532            next_step_s: 0.0,
533            previous_error: 1e-4,
534            stats: Stats::default(),
535            step_limit: DEFAULT_STEP_LIMIT,
536            g_start: Vec::new(),
537            g_end: Vec::new(),
538            fired: Vec::new(),
539        };
540        integrator.reset(t0_s, y0)?;
541        Ok(integrator)
542    }
543
544    /// Limits the steps, accepted and rejected, over the integrator's life
545    /// ([`DEFAULT_STEP_LIMIT`] otherwise).
546    #[must_use]
547    pub fn with_step_limit(mut self, steps: u64) -> Self {
548        self.step_limit = steps;
549        self
550    }
551
552    /// Changes the step limit, for example to continue after [`IntegrationError::StepLimit`].
553    pub fn set_step_limit(&mut self, steps: u64) {
554        self.step_limit = steps;
555    }
556
557    /// Moves the integrator to a new time and state, as after an impulse or a projection such as
558    /// renormalizing a quaternion. The step-size estimate carries over.
559    ///
560    /// # Errors
561    ///
562    /// [`SettingsError::NotFinite`] for a non-finite time or state; the integrator is unchanged.
563    pub fn reset(&mut self, t_s: f64, y: [f64; N]) -> Result<(), SettingsError> {
564        if !t_s.is_finite() {
565            return Err(SettingsError::NotFinite {
566                what: "time",
567                value: t_s,
568            });
569        }
570        if let Some(bad) = y.iter().find(|v| !v.is_finite()) {
571            return Err(SettingsError::NotFinite {
572                what: "state",
573                value: *bad,
574            });
575        }
576        self.t_s = t_s;
577        self.y = y;
578        self.fired.clear();
579        Ok(())
580    }
581
582    /// The method.
583    #[must_use]
584    pub fn method(&self) -> Method {
585        self.method
586    }
587
588    /// The current time, in seconds.
589    #[must_use]
590    pub fn time_s(&self) -> f64 {
591        self.t_s
592    }
593
594    /// The current state.
595    #[must_use]
596    pub fn state(&self) -> &[f64; N] {
597        &self.y
598    }
599
600    /// Work done so far.
601    #[must_use]
602    pub fn stats(&self) -> Stats {
603        self.stats
604    }
605
606    /// The step the adaptive method will try next, once it has taken one.
607    #[must_use]
608    pub fn next_step_s(&self) -> Option<f64> {
609        (self.next_step_s > 0.0).then_some(self.next_step_s)
610    }
611
612    /// The events that fired at the current time, ascending, after [`Advance::Events`]; empty
613    /// otherwise.
614    #[must_use]
615    pub fn fired_events(&self) -> &[usize] {
616        &self.fired
617    }
618
619    /// Integrates toward `t_stop_s` (which may be infinite), stopping at the first events.
620    ///
621    /// - The last step is shortened, or stretched by up to 1%, to end exactly at `t_stop_s`. A stop
622    ///   within the rounding of `t` counts as reached without a step.
623    /// - Events are checked at the end of every accepted step. The earliest crossing is located on
624    ///   the step's dense output; the integrator stops there with the dense output's state, just
625    ///   past the crossing (within [`EVENT_TIME_RESOLUTION_S`]), and reports every event that is
626    ///   past its zero at that state.
627    /// - A call that starts on an event it returned doesn't return it again: the event's function
628    ///   is already past zero there, and a crossing must start strictly on the near side.
629    /// - [`OdeSystem::accept_step`] sees every accepted step and may stop the integration.
630    /// - The derivative is evaluated afresh at the start of each call, so a caller can change the
631    ///   system (a phase) between calls.
632    ///
633    /// # Errors
634    ///
635    /// [`IntegrationError`]: a failed derivative, a non-finite solution or event, a step that
636    /// can't be made small enough, the step limit, or a stop time in the past. The integrator is
637    /// left at its last accepted step.
638    pub fn advance<S>(
639        &mut self,
640        system: &mut S,
641        t_stop_s: f64,
642    ) -> Result<Advance, IntegrationError<S::Error>>
643    where
644        S: OdeSystem<N> + ?Sized,
645    {
646        self.fired.clear();
647        if t_stop_s.is_nan() || t_stop_s < self.t_s {
648            return Err(IntegrationError::Backward {
649                t_s: self.t_s,
650                t_stop_s,
651            });
652        }
653        if negligible_step(self.t_s, t_stop_s - self.t_s) {
654            self.t_s = t_stop_s;
655            return Ok(Advance::Reached);
656        }
657        let weights = system.absolute_tolerance_weights();
658        for w in weights {
659            positive("absolute tolerance weight", w)?;
660        }
661        self.g_start.clear();
662        for index in 0..system.event_count() {
663            let g = system.event_value(index, self.t_s, &self.y);
664            if !g.is_finite() {
665                return Err(IntegrationError::EventNotFinite {
666                    index,
667                    t_s: self.t_s,
668                });
669            }
670            self.g_start.push(g);
671        }
672        match self.method {
673            Method::DormandPrince54(adaptive) => {
674                self.advance_dp(system, &adaptive, &weights, t_stop_s)
675            }
676            Method::Rk4 { step_s } => self.advance_rk4(system, step_s, t_stop_s),
677        }
678    }
679
680    fn evaluate<S: OdeSystem<N> + ?Sized>(
681        &mut self,
682        system: &mut S,
683        t_s: f64,
684        y: &[f64; N],
685    ) -> Result<[f64; N], IntegrationError<S::Error>> {
686        self.stats.evaluations += 1;
687        system
688            .derivative(t_s, y)
689            .map_err(|source| IntegrationError::Derivative { t_s, source })
690    }
691
692    fn check_step_limit<X>(&self) -> Result<(), IntegrationError<X>> {
693        if self.stats.accepted_steps + self.stats.rejected_steps >= self.step_limit {
694            Err(IntegrationError::StepLimit {
695                t_s: self.t_s,
696                limit: self.step_limit,
697            })
698        } else {
699            Ok(())
700        }
701    }
702
703    /// Stages 2 to 7 and the fifth-order solution of a Dormand–Prince step of `h` from `(t, y)`,
704    /// with `k7` evaluated at `t1`.
705    fn dp_stages<S: OdeSystem<N> + ?Sized>(
706        &mut self,
707        system: &mut S,
708        t: f64,
709        y: &[f64; N],
710        k1: &[f64; N],
711        h: f64,
712        t1: f64,
713    ) -> Result<DpStages<N>, IntegrationError<S::Error>> {
714        use dp::*;
715        let k2 = self.evaluate(system, t + C2 * h, &lin(y, h, &[(A21, k1)]))?;
716        let k3 = self.evaluate(system, t + C3 * h, &lin(y, h, &[(A31, k1), (A32, &k2)]))?;
717        let k4 = self.evaluate(
718            system,
719            t + C4 * h,
720            &lin(y, h, &[(A41, k1), (A42, &k2), (A43, &k3)]),
721        )?;
722        let k5 = self.evaluate(
723            system,
724            t + C5 * h,
725            &lin(y, h, &[(A51, k1), (A52, &k2), (A53, &k3), (A54, &k4)]),
726        )?;
727        let k6 = self.evaluate(
728            system,
729            t1,
730            &lin(
731                y,
732                h,
733                &[(A61, k1), (A62, &k2), (A63, &k3), (A64, &k4), (A65, &k5)],
734            ),
735        )?;
736        let y1 = lin(
737            y,
738            h,
739            &[(B1, k1), (B3, &k3), (B4, &k4), (B5, &k5), (B6, &k6)],
740        );
741        let k7 = self.evaluate(system, t1, &y1)?;
742        Ok(DpStages {
743            k3,
744            k4,
745            k5,
746            k6,
747            k7,
748            y1,
749        })
750    }
751
752    fn advance_dp<S: OdeSystem<N> + ?Sized>(
753        &mut self,
754        system: &mut S,
755        adaptive: &Adaptive,
756        weights: &[f64; N],
757        t_stop_s: f64,
758    ) -> Result<Advance, IntegrationError<S::Error>> {
759        use dp::*;
760        let rtol = adaptive.relative_tolerance;
761        let atol = adaptive.absolute_tolerance;
762        let max_step = adaptive.max_step_s.unwrap_or(f64::INFINITY);
763        let scale = |i: usize, a: f64, b: f64| weights[i] * atol + rtol * a.abs().max(b.abs());
764
765        let (t0, y0) = (self.t_s, self.y);
766        let mut k1 = self.evaluate(system, t0, &y0)?;
767        let mut h = if self.next_step_s > 0.0 {
768            self.next_step_s
769        } else if let Some(h0) = adaptive.initial_step_s {
770            h0
771        } else {
772            self.starting_step(system, &k1, weights, rtol, atol, t_stop_s, max_step)?
773        };
774        h = h.min(max_step);
775        let mut rejected = false;
776        // Why the last step was rejected, if not for its error estimate: reported instead of
777        // `StepTooSmall` if the step can't shrink further.
778        let mut failure: Option<IntegrationError<S::Error>> = None;
779        loop {
780            self.check_step_limit()?;
781            let t = self.t_s;
782            let y = self.y;
783            if !h.is_finite() || h <= 0.0 {
784                return Err(failure.unwrap_or(IntegrationError::NotFinite { t_s: t }));
785            }
786            let proposed = h;
787            // Stretch up to 1% to land on the stop, but not past the longest step (beyond the
788            // rounding that equal steps accumulate).
789            let last = t + 1.01 * h >= t_stop_s && t_stop_s - t <= max_step * (1.0 + 1e-9);
790            if last {
791                h = t_stop_s - t;
792            }
793            if negligible_step(t, h) {
794                if last {
795                    // The stop is within the rounding of `t`.
796                    self.t_s = t_stop_s;
797                    return Ok(Advance::Reached);
798                }
799                return Err(failure.unwrap_or(IntegrationError::StepTooSmall { t_s: t, step_s: h }));
800            }
801            let t1 = if last { t_stop_s } else { t + h };
802            let stages = match self.dp_stages(system, t, &y, &k1, h, t1) {
803                Ok(stages) => stages,
804                Err(error) => {
805                    // A long step's stages can leave the trajectory; retry shorter.
806                    self.stats.rejected_steps += 1;
807                    failure = Some(error);
808                    rejected = true;
809                    h /= MAX_SHRINK;
810                    continue;
811                }
812            };
813
814            let mut sum = 0.0;
815            for i in 0..N {
816                let e = h
817                    * (E1 * k1[i]
818                        + E3 * stages.k3[i]
819                        + E4 * stages.k4[i]
820                        + E5 * stages.k5[i]
821                        + E6 * stages.k6[i]
822                        + E7 * stages.k7[i]);
823                sum += (e / scale(i, y[i], stages.y1[i])).powi(2);
824            }
825            let error = (sum / N.max(1) as f64).sqrt();
826            if !error.is_finite() {
827                self.stats.rejected_steps += 1;
828                failure = Some(IntegrationError::NotFinite { t_s: t });
829                rejected = true;
830                h /= MAX_SHRINK;
831                continue;
832            }
833            let fac11 = error.powf(EXPONENT);
834            if error > 1.0 {
835                self.stats.rejected_steps += 1;
836                failure = None;
837                rejected = true;
838                h /= (fac11 / SAFETY).min(MAX_SHRINK);
839                continue;
840            }
841
842            // Accepted.
843            let fac =
844                (fac11 / self.previous_error.powf(BETA) / SAFETY).clamp(MIN_DIVISOR, MAX_SHRINK);
845            let mut h_new = (h / fac).min(max_step);
846            if rejected {
847                h_new = h_new.min(h);
848            }
849            // A step shortened to land on the stop time says little about the next one.
850            let carry = if last {
851                h_new.max(proposed.min(max_step))
852            } else {
853                h_new
854            };
855            let r4 = lin(
856                &[0.0; N],
857                h,
858                &[
859                    (D1, &k1),
860                    (D3, &stages.k3),
861                    (D4, &stages.k4),
862                    (D5, &stages.k5),
863                    (D6, &stages.k6),
864                    (D7, &stages.k7),
865                ],
866            );
867            let step = Step::quartic(t, t1, y, stages.y1, &k1, &stages.k7, r4);
868            let located = self.locate_event(system, &step)?;
869            self.previous_error = error.max(1e-4);
870            self.next_step_s = carry;
871            if let Some(outcome) = self.commit(system, step, located)? {
872                return Ok(outcome);
873            }
874            if last {
875                return Ok(Advance::Reached);
876            }
877            k1 = stages.k7;
878            rejected = false;
879            failure = None;
880            h = h_new;
881        }
882    }
883
884    /// Moves to the end of an accepted step, or to the event located in it, and tells the system.
885    /// Returns the outcome if the integration stops here.
886    fn commit<S: OdeSystem<N> + ?Sized>(
887        &mut self,
888        system: &mut S,
889        step: Step<N>,
890        event: Option<(usize, f64)>,
891    ) -> Result<Option<Advance>, IntegrationError<S::Error>> {
892        let Some((first, t_event)) = event else {
893            self.stats.accepted_steps += 1;
894            self.t_s = step.end_s;
895            self.y = step.end;
896            std::mem::swap(&mut self.g_start, &mut self.g_end);
897            return Ok(system
898                .accept_step(&step)
899                .is_break()
900                .then_some(Advance::Stopped));
901        };
902        let shortened = step.truncated(t_event);
903        self.fired.clear();
904        for index in 0..self.g_start.len() {
905            let g = system.event_value(index, t_event, &shortened.end);
906            if !g.is_finite() {
907                self.fired.clear();
908                return Err(IntegrationError::EventNotFinite {
909                    index,
910                    t_s: t_event,
911                });
912            }
913            if system
914                .event_direction(index)
915                .crosses(self.g_start[index], g)
916            {
917                self.fired.push(index);
918            }
919        }
920        // The earliest event is past zero at its far-side bracket end by construction; include it
921        // even if an event function that isn't a pure function of `(t, y)` says otherwise.
922        if let Err(position) = self.fired.binary_search(&first) {
923            self.fired.insert(position, first);
924        }
925        self.stats.accepted_steps += 1;
926        self.t_s = t_event;
927        self.y = shortened.end;
928        // Events take precedence over a request to stop.
929        let _ = system.accept_step(&shortened);
930        Ok(Some(Advance::Events))
931    }
932
933    /// Hairer's starting step (`DOPRI5` function `HINIT`; HNW I, §II.4): a first guess from the
934    /// ratio of `‖y₀‖` to `‖f₀‖`, refined with a second-derivative estimate from one Euler step.
935    #[expect(
936        clippy::too_many_arguments,
937        reason = "private helper sharing `advance_dp`'s validated inputs"
938    )]
939    fn starting_step<S: OdeSystem<N> + ?Sized>(
940        &mut self,
941        system: &mut S,
942        f0: &[f64; N],
943        weights: &[f64; N],
944        rtol: f64,
945        atol: f64,
946        t_stop_s: f64,
947        max_step: f64,
948    ) -> Result<f64, IntegrationError<S::Error>> {
949        let (mut dnf, mut dny) = (0.0, 0.0);
950        for i in 0..N {
951            let sk = weights[i] * atol + rtol * self.y[i].abs();
952            dnf += (f0[i] / sk).powi(2);
953            dny += (self.y[i] / sk).powi(2);
954        }
955        let mut h = if dnf <= 1e-10 || dny <= 1e-10 {
956            1e-6
957        } else {
958            (dny / dnf).sqrt() * 0.01
959        };
960        h = h.min(max_step).min(t_stop_s - self.t_s);
961        let y1 = lin(&self.y, h, &[(1.0, f0)]);
962        let f1 = self.evaluate(system, self.t_s + h, &y1)?;
963        let mut der2 = 0.0;
964        for i in 0..N {
965            let sk = weights[i] * atol + rtol * self.y[i].abs();
966            der2 += ((f1[i] - f0[i]) / sk).powi(2);
967        }
968        let der2 = der2.sqrt() / h;
969        let der12 = der2.abs().max(dnf.sqrt());
970        let h1 = if der12 <= 1e-15 {
971            (h * 1e-3).max(1e-6)
972        } else {
973            (0.01 / der12).powf(0.2)
974        };
975        let h = (100.0 * h).min(h1).min(max_step);
976        if h.is_finite() && h > 0.0 {
977            Ok(h)
978        } else {
979            Err(IntegrationError::NotFinite { t_s: self.t_s })
980        }
981    }
982
983    fn advance_rk4<S: OdeSystem<N> + ?Sized>(
984        &mut self,
985        system: &mut S,
986        step_s: f64,
987        t_stop_s: f64,
988    ) -> Result<Advance, IntegrationError<S::Error>> {
989        let (t0, y0) = (self.t_s, self.y);
990        let mut k1 = self.evaluate(system, t0, &y0)?;
991        loop {
992            self.check_step_limit()?;
993            let t = self.t_s;
994            let y = self.y;
995            let last = t + 1.01 * step_s >= t_stop_s;
996            let (h, t1) = if last {
997                (t_stop_s - t, t_stop_s)
998            } else {
999                (step_s, t + step_s)
1000            };
1001            if negligible_step(t, h) {
1002                if last {
1003                    self.t_s = t_stop_s;
1004                    return Ok(Advance::Reached);
1005                }
1006                return Err(IntegrationError::StepTooSmall { t_s: t, step_s: h });
1007            }
1008            let y1 = self.rk4_step(system, t, &y, &k1, h, t1)?;
1009            let f1 = self.evaluate(system, t1, &y1)?;
1010            if y1.iter().chain(&f1).any(|v| !v.is_finite()) {
1011                return Err(IntegrationError::NotFinite { t_s: t });
1012            }
1013            let step = Step::cubic(t, t1, y, y1, &k1, &f1);
1014            let located = self.locate_event(system, &step)?;
1015            if let Some(outcome) = self.commit(system, step, located)? {
1016                return Ok(outcome);
1017            }
1018            if last {
1019                return Ok(Advance::Reached);
1020            }
1021            k1 = f1;
1022        }
1023    }
1024
1025    /// One classical RK4 step (HNW I, table 1.2): weights 1/6, 2/6, 2/6, 1/6 at 0, ½, ½, 1.
1026    fn rk4_step<S: OdeSystem<N> + ?Sized>(
1027        &mut self,
1028        system: &mut S,
1029        t: f64,
1030        y: &[f64; N],
1031        k1: &[f64; N],
1032        h: f64,
1033        t1: f64,
1034    ) -> Result<[f64; N], IntegrationError<S::Error>> {
1035        let k2 = self.evaluate(system, t + 0.5 * h, &lin(y, h, &[(0.5, k1)]))?;
1036        let k3 = self.evaluate(system, t + 0.5 * h, &lin(y, h, &[(0.5, &k2)]))?;
1037        let k4 = self.evaluate(system, t1, &lin(y, h, &[(1.0, &k3)]))?;
1038        Ok(lin(
1039            y,
1040            h,
1041            &[
1042                (1.0 / 6.0, k1),
1043                (2.0 / 6.0, &k2),
1044                (2.0 / 6.0, &k3),
1045                (1.0 / 6.0, &k4),
1046            ],
1047        ))
1048    }
1049
1050    /// Evaluates every event at the step's end into `g_end` and returns the earliest crossing
1051    /// (the lowest index on a tie) and its time, located on the dense output.
1052    fn locate_event<S: OdeSystem<N> + ?Sized>(
1053        &mut self,
1054        system: &mut S,
1055        step: &Step<N>,
1056    ) -> Result<Option<(usize, f64)>, IntegrationError<S::Error>> {
1057        self.g_end.clear();
1058        let mut earliest: Option<(usize, f64)> = None;
1059        for index in 0..self.g_start.len() {
1060            let g1 = system.event_value(index, step.end_s, &step.end);
1061            if !g1.is_finite() {
1062                return Err(IntegrationError::EventNotFinite {
1063                    index,
1064                    t_s: step.end_s,
1065                });
1066            }
1067            self.g_end.push(g1);
1068            let g0 = self.g_start[index];
1069            if !system.event_direction(index).crosses(g0, g1) {
1070                continue;
1071            }
1072            let root = find_root(
1073                |t| system.event_value(index, t, &step.state_at(t)),
1074                step.start_s,
1075                step.end_s,
1076                g0,
1077                g1,
1078                EVENT_TIME_RESOLUTION_S,
1079            )
1080            .map_err(|error| IntegrationError::EventNotFinite {
1081                index,
1082                t_s: match error {
1083                    RootError::NotFinite { x } => x,
1084                    _ => step.end_s,
1085                },
1086            })?;
1087            if earliest.is_none_or(|(_, t)| root < t) {
1088                earliest = Some((index, root));
1089            }
1090        }
1091        Ok(earliest)
1092    }
1093}
1094
1095/// Stages kept from a Dormand–Prince step.
1096struct DpStages<const N: usize> {
1097    k3: [f64; N],
1098    k4: [f64; N],
1099    k5: [f64; N],
1100    k6: [f64; N],
1101    k7: [f64; N],
1102    y1: [f64; N],
1103}
1104
1105/// `y + h Σ aⱼ kⱼ`.
1106#[inline]
1107fn lin<const N: usize>(y: &[f64; N], h: f64, terms: &[(f64, &[f64; N])]) -> [f64; N] {
1108    let mut out = *y;
1109    for (a, k) in terms {
1110        let ha = h * a;
1111        for i in 0..N {
1112            out[i] += ha * k[i];
1113        }
1114    }
1115    out
1116}
1117
1118#[cfg(test)]
1119mod tests;