ode_solvers/
dopri5.rs

1//! Explicit Runge-Kutta method with Dormand-Prince coefficients of order 5(4) and dense output of order 4.
2
3use crate::butcher_tableau::dopri54;
4use crate::continuous_output_model::ContinuousOutputModel;
5use crate::controller::Controller;
6use crate::dop_shared::*;
7use nalgebra::{allocator::Allocator, DefaultAllocator, Dim, MatrixSum, OVector, U1};
8
9trait DefaultController<T: FloatNumber> {
10    fn default(x: T, x_end: T) -> Self;
11}
12
13impl<T: FloatNumber> DefaultController<T> for Controller<T> {
14    fn default(x: T, x_end: T) -> Self {
15        let alpha = T::from(0.2 - 0.04 * 0.75).unwrap();
16        Controller::new(
17            alpha,
18            T::from(0.04).unwrap(),
19            T::from(10.0).unwrap(),
20            T::from(0.2).unwrap(),
21            x_end - x,
22            T::from(0.9).unwrap(),
23            sign(T::one(), x_end - x),
24        )
25    }
26}
27
28/// Structure containing the parameters for the numerical integration.
29pub struct Dopri5<T, V, F>
30where
31    T: FloatNumber,
32    F: System<T, V>,
33{
34    f: F,
35    x: T,
36    x_old: T,
37    x_end: T,
38    xd: T,
39    dx: T,
40    y: V,
41    rtol: T,
42    atol: T,
43    results: SolverResult<T, V>,
44    uround: T,
45    h: T,
46    h_old: T,
47    n_max: u32,
48    n_stiff: u32,
49    controller: Controller<T>,
50    out_type: OutputType,
51    rcont: [V; 5],
52    stats: Stats,
53}
54
55impl<T, D: Dim, F> Dopri5<T, OVector<T, D>, F>
56where
57    f64: From<T>,
58    T: FloatNumber,
59    F: System<T, OVector<T, D>>,
60    OVector<T, D>: std::ops::Mul<T, Output = OVector<T, D>>,
61    DefaultAllocator: Allocator<D>,
62{
63    /// Default initializer for the structure
64    ///
65    /// # Arguments
66    ///
67    /// * `f`       - Structure implementing the [`System`] trait
68    /// * `x`       - Initial value of the independent variable (usually time)
69    /// * `x_end`   - Final value of the independent variable
70    /// * `dx`      - Increment in the dense output. This argument has no effect if the output type is Sparse
71    /// * `y`       - Initial value of the dependent variable(s)
72    /// * `rtol`    - Relative tolerance used in the computation of the adaptive step size
73    /// * `atol`    - Absolute tolerance used in the computation of the adaptive step size
74    ///
75    pub fn new(f: F, x: T, x_end: T, dx: T, y: OVector<T, D>, rtol: T, atol: T) -> Self {
76        let (rows, cols) = y.shape_generic();
77        Self {
78            f,
79            x,
80            xd: x,
81            dx,
82            x_old: x,
83            x_end,
84            y,
85            rtol,
86            atol,
87            results: SolverResult::default(),
88            uround: T::epsilon(),
89            h: T::zero(),
90            h_old: T::zero(),
91            n_max: 100000,
92            n_stiff: 1000,
93            controller: Controller::default(x, x_end),
94            out_type: OutputType::Dense,
95            rcont: [
96                OVector::zeros_generic(rows, cols),
97                OVector::zeros_generic(rows, cols),
98                OVector::zeros_generic(rows, cols),
99                OVector::zeros_generic(rows, cols),
100                OVector::zeros_generic(rows, cols),
101            ],
102            stats: Stats::new(),
103        }
104    }
105
106    /// Advanced initializer for the structure.
107    ///
108    /// # Arguments
109    ///
110    /// * `f`       - Structure implementing the [`System`] trait
111    /// * `x`       - Initial value of the independent variable (usually time)
112    /// * `x_end`   - Final value of the independent variable
113    /// * `dx`      - Increment in the dense output. This argument has no effect if the output type is Sparse
114    /// * `y`       - Initial value of the dependent variable(s)
115    /// * `rtol`    - Relative tolerance used in the computation of the adaptive step size
116    /// * `atol`    - Absolute tolerance used in the computation of the adaptive step size
117    /// * `safety_factor`   - Safety factor used in the computation of the adaptive step size
118    /// * `beta`    - Value of the beta coefficient of the PI controller. Default is 0.04
119    /// * `fac_min` - Minimum factor between two successive steps. Default is 0.2
120    /// * `fac_max` - Maximum factor between two successive steps. Default is 10.0
121    /// * `h_max`   - Maximum step size. Default is `x_end-x`
122    /// * `h`       - Initial value of the step size. If h = 0.0, the initial value of h is computed automatically
123    /// * `n_max`   - Maximum number of iterations. Default is 100000
124    /// * `n_stiff` - Stiffness is tested when the number of iterations is a multiple of n_stiff. Default is 1000
125    /// * `out_type`    - Type of the output. Must be a variant of the OutputType enum. Default is Dense
126    ///
127    #[allow(clippy::too_many_arguments)]
128    pub fn from_param(
129        f: F,
130        x: T,
131        x_end: T,
132        dx: T,
133        y: OVector<T, D>,
134        rtol: T,
135        atol: T,
136        safety_factor: T,
137        beta: T,
138        fac_min: T,
139        fac_max: T,
140        h_max: T,
141        h: T,
142        n_max: u32,
143        n_stiff: u32,
144        out_type: OutputType,
145    ) -> Self {
146        let alpha = T::from(0.2).unwrap() - beta * T::from(0.75).unwrap();
147        let (rows, cols) = y.shape_generic();
148        Self {
149            f,
150            x,
151            xd: x,
152            x_old: T::zero(),
153            x_end,
154            dx,
155            y,
156            rtol,
157            atol,
158            results: SolverResult::default(),
159            uround: T::from(f64::EPSILON).unwrap(),
160            h,
161            h_old: T::zero(),
162            n_max,
163            n_stiff,
164            controller: Controller::new(
165                alpha,
166                beta,
167                fac_max,
168                fac_min,
169                h_max,
170                safety_factor,
171                sign(T::one(), x_end - x),
172            ),
173            out_type,
174            rcont: [
175                OVector::zeros_generic(rows, cols),
176                OVector::zeros_generic(rows, cols),
177                OVector::zeros_generic(rows, cols),
178                OVector::zeros_generic(rows, cols),
179                OVector::zeros_generic(rows, cols),
180            ],
181            stats: Stats::new(),
182        }
183    }
184
185    /// Computes the initial step size
186    fn hinit(&self) -> T {
187        let (rows, cols) = self.y.shape_generic();
188        let mut f0 = OVector::zeros_generic(rows, cols);
189        self.f.system(self.x, &self.y, &mut f0);
190        let posneg = sign(T::from(1.0).unwrap(), self.x_end - self.x);
191
192        // Compute the norm of y0 and f0
193        let dim = rows.value();
194        let mut d0 = T::zero();
195        let mut d1 = T::zero();
196        for i in 0..dim {
197            let y_i = T::from(self.y[i]).unwrap();
198            let sci = self.atol + y_i.abs() * self.rtol;
199            d0 += (y_i / sci) * (y_i / sci);
200            let f0_i = T::from(f0[i]).unwrap();
201            d1 += (f0_i / sci) * (f0_i / sci);
202        }
203
204        // Compute h0
205        let tol = T::from(1.0E-10).unwrap();
206        let mut h0 = if d0 < tol || d1 < tol {
207            T::from(1.0E-6).unwrap()
208        } else {
209            T::from(0.01).unwrap() * (d0 / d1).sqrt()
210        };
211
212        h0 = h0.min(self.controller.h_max());
213        h0 = sign(h0, posneg);
214
215        let y1 = &self.y + &f0 * h0;
216        let mut f1 = OVector::zeros_generic(rows, cols);
217        self.f.system(self.x + h0, &y1, &mut f1);
218
219        // Compute the norm of f1-f0 divided by h0
220        let mut d2 = T::zero();
221        for i in 0..dim {
222            let f0_i = f0[i];
223            let f1_i = f1[i];
224            let y_i = self.y[i];
225            let sci = self.atol + y_i.abs() * self.rtol;
226            d2 += ((f1_i - f0_i) / sci) * ((f1_i - f0_i) / sci);
227        }
228        d2 = d2.sqrt() / h0;
229
230        let h1 = if d1.sqrt().max(d2.abs()) <= T::from(1.0E-15).unwrap() {
231            T::from(1.0E-6_f64)
232                .unwrap()
233                .max(h0.abs() * T::from(1.0E-3).unwrap())
234        } else {
235            (T::from(0.01).unwrap() / (d1.sqrt().max(d2))).powf(T::one() / T::from(5.0).unwrap())
236        };
237
238        sign(
239            (T::from(100.0).unwrap() * h0.abs()).min(h1.min(self.controller.h_max())),
240            posneg,
241        )
242    }
243
244    /// Integrates the system and builds a continuous output model.
245    pub fn integrate_with_continuous_output_model(
246        &mut self,
247        continuous_output: &mut ContinuousOutputModel<T, OVector<T, D>>,
248    ) -> Result<Stats, IntegrationError> {
249        self.out_type = OutputType::Continuous;
250        self.integrate_core(Some(continuous_output))
251    }
252
253    /// Integrates the system.
254    pub fn integrate(&mut self) -> Result<Stats, IntegrationError> {
255        if self.out_type == OutputType::Continuous {
256            panic!("Please use `integrate_with_continuous_output_model` to compute a continuous output model.");
257        }
258        self.integrate_core(None)
259    }
260
261    /// Core integration method.
262    fn integrate_core(
263        &mut self,
264        mut continuous_output_model: Option<&mut ContinuousOutputModel<T, OVector<T, D>>>,
265    ) -> Result<Stats, IntegrationError> {
266        // Initialization
267        let (rows, cols) = self.y.shape_generic();
268        self.x_old = self.x;
269        let mut n_step = 0;
270        let mut last = false;
271        let mut h_new = T::zero();
272        let dim = rows.value();
273        let mut non_stiff = 0;
274        let mut iasti = 0;
275        let posneg = sign(T::one(), self.x_end - self.x);
276
277        // Initialize the lower bound of the continuous output model if one is present
278        if let Some(output) = continuous_output_model.as_deref_mut() {
279            output.set_lower_bound(self.x);
280        }
281
282        if self.h == T::zero() {
283            self.h = self.hinit();
284            self.stats.num_eval += 2;
285        }
286        self.h_old = self.h;
287
288        // Save initial values
289        if self.out_type == OutputType::Sparse {
290            self.results.push(self.x, self.y.clone());
291        }
292
293        let mut k = vec![OVector::zeros_generic(rows, cols); 7];
294        self.f.system(self.x, &self.y, &mut k[0]);
295        self.stats.num_eval += 1;
296
297        // Main loop
298        while !last {
299            // Check if step number is within allowed range
300            if n_step > self.n_max {
301                self.h_old = self.h;
302                return Err(IntegrationError::MaxNumStepReached {
303                    x: f64::from(self.x),
304                    n_step,
305                });
306            }
307
308            // Check for step size underflow
309            if T::from(0.1).unwrap() * self.h.abs() <= self.uround * self.x.abs() {
310                self.h_old = self.h;
311                return Err(IntegrationError::StepSizeUnderflow {
312                    x: f64::from(self.x),
313                });
314            }
315
316            // Check if it's the last iteration
317            if (self.x + T::from(1.01).unwrap() * self.h - self.x_end) * posneg > T::zero() {
318                self.h = self.x_end - self.x;
319                last = true;
320            }
321            n_step += 1;
322
323            let h = self.h;
324            // 6 Stages
325            let mut y_next = OVector::zeros_generic(rows, cols);
326            let mut y_stiff = OVector::zeros_generic(rows, cols);
327            for s in 1..7 {
328                y_next = self.y.clone();
329                for (j, k_value) in k.iter().enumerate().take(s) {
330                    y_next += k_value * h * dopri54::a(s + 1, j + 1);
331                }
332                self.f
333                    .system(self.x + self.h * dopri54::c::<T>(s + 1), &y_next, &mut k[s]);
334                if s == 5 {
335                    y_stiff = y_next.clone();
336                }
337            }
338            k[1] = k[6].clone();
339            self.stats.num_eval += 6;
340
341            // Prepare dense/continuous output
342            if self.out_type == OutputType::Dense || self.out_type == OutputType::Continuous {
343                self.rcont[4] = (&k[0] * dopri54::d::<T>(1)
344                    + &k[2] * dopri54::d::<T>(3)
345                    + &k[3] * dopri54::d::<T>(4)
346                    + &k[4] * dopri54::d::<T>(5)
347                    + &k[5] * dopri54::d::<T>(6)
348                    + &k[1] * dopri54::d::<T>(7))
349                    * h;
350            }
351
352            // Compute error estimate
353            k[3] = (&k[0] * dopri54::e::<T>(1)
354                + &k[2] * dopri54::e::<T>(3)
355                + &k[3] * dopri54::e::<T>(4)
356                + &k[4] * dopri54::e::<T>(5)
357                + &k[5] * dopri54::e::<T>(6)
358                + &k[1] * dopri54::e::<T>(7))
359                * h;
360
361            // Compute error
362            let mut err = T::zero();
363            for i in 0..dim {
364                let y_i = T::from(self.y[i]).unwrap();
365                let y_next_i = T::from(y_next[i]).unwrap();
366                let sc_i: T = self.atol + y_i.abs().max(y_next_i.abs()) * self.rtol;
367                let err_est_i = T::from(k[3][i]).unwrap();
368                err += (err_est_i / sc_i) * (err_est_i / sc_i);
369            }
370            err = (err / T::from(dim).unwrap()).sqrt();
371
372            // Step size control
373            if self.controller.accept(err, self.h, &mut h_new) {
374                self.stats.accepted_steps += 1;
375
376                // Stiffness detection
377                if self.stats.accepted_steps % self.n_stiff == 0 || iasti > 0 {
378                    let num = T::from((&k[1] - &k[5]).dot(&(&k[1] - &k[5]))).unwrap();
379                    let den = T::from((&y_next - &y_stiff).dot(&(&y_next - &y_stiff))).unwrap();
380                    let h_lamb = if den > T::zero() {
381                        self.h * (num / den).sqrt()
382                    } else {
383                        T::zero()
384                    };
385
386                    if h_lamb > T::from(3.25).unwrap() {
387                        iasti += 1;
388                        non_stiff = 0;
389                        if iasti == 15 {
390                            self.h_old = self.h;
391                            return Err(IntegrationError::StiffnessDetected {
392                                x: f64::from(self.x),
393                            });
394                        }
395                    } else {
396                        non_stiff += 1;
397                        if non_stiff == 6 {
398                            iasti = 0;
399                        }
400                    }
401                }
402
403                // Prepare dense/continuous output
404                if self.out_type == OutputType::Dense || self.out_type == OutputType::Continuous {
405                    let h = self.h;
406
407                    let ydiff = &y_next - &self.y;
408                    let bspl = &k[0] * h - &ydiff;
409                    self.rcont[0] = self.y.clone();
410                    self.rcont[1] = ydiff.clone();
411                    self.rcont[2] = bspl.clone();
412                    self.rcont[3] = -&k[1] * h + ydiff - bspl;
413                }
414
415                k[0] = k[1].clone();
416                self.y = y_next.clone();
417                self.x_old = self.x;
418                self.x += self.h;
419                self.h_old = self.h;
420
421                self.solution_output(y_next, &mut continuous_output_model, &k[0]);
422
423                if self
424                    .f
425                    .solout(self.x, self.results.get().1.last().unwrap(), &k[0])
426                {
427                    last = true;
428                }
429
430                // Normal exit
431                if last {
432                    self.h_old = posneg * h_new;
433                    return Ok(self.stats);
434                }
435            } else {
436                last = false;
437                if self.stats.accepted_steps >= 1 {
438                    self.stats.rejected_steps += 1;
439                }
440            }
441            self.h = h_new;
442        }
443        Ok(self.stats)
444    }
445
446    fn solution_output(
447        &mut self,
448        y_next: OVector<T, D>,
449        continuous_output_model: &mut Option<&mut ContinuousOutputModel<T, OVector<T, D>>>,
450        dy: &OVector<T, D>,
451    ) {
452        if self.out_type == OutputType::Dense {
453            while self.xd.abs() <= self.x.abs() {
454                if self.x_old.abs() <= self.xd.abs() && self.x.abs() >= self.xd.abs() {
455                    let theta = (self.xd - self.x_old) / self.h_old;
456                    let theta1 = T::one() - theta;
457                    let y_out = self.compute_y_out(theta, theta1);
458                    self.results.push(self.xd, y_out);
459                    self.xd += self.dx;
460                }
461
462                if self
463                    .f
464                    .solout(self.xd, self.results.get().1.last().unwrap(), dy)
465                {
466                    break;
467                }
468            }
469
470            // Ensure the last point is added if it's within floating point error of x_end.
471            if (self.xd - self.x_end).abs() < T::from(1e-9).unwrap() {
472                let theta = (self.x_end - self.x_old) / self.h_old;
473                let theta1 = T::one() - theta;
474                let y_out = self.compute_y_out(theta, theta1);
475                self.results.push(self.x_end, y_out);
476                self.xd += self.dx;
477            }
478        } else {
479            self.results.push(self.x, y_next)
480        }
481
482        // Update the continuous output model if one is present
483        if let Some(output) = continuous_output_model.as_deref_mut() {
484            output.add_interval(self.x.abs(), self.rcont.to_vec(), self.h_old);
485        }
486    }
487
488    /// Computes the value of y for given theta and theta1 values.
489    fn compute_y_out(&mut self, theta: T, theta1: T) -> MatrixSum<T, D, U1, D, U1> {
490        &self.rcont[0]
491            + (&self.rcont[1]
492                + (&self.rcont[2] + (&self.rcont[3] + &self.rcont[4] * theta1) * theta) * theta1)
493                * theta
494    }
495
496    /// Getter for the independent variable's output.
497    pub fn x_out(&self) -> &Vec<T> {
498        self.results.get().0
499    }
500
501    /// Getter for the dependent variables' output.
502    pub fn y_out(&self) -> &Vec<OVector<T, D>> {
503        self.results.get().1
504    }
505
506    /// Getter for the results type, a pair of independent and dependent variables
507    pub fn results(&self) -> &SolverResult<T, OVector<T, D>> {
508        &self.results
509    }
510}
511
512impl<T, D: Dim, F> From<Dopri5<T, OVector<T, D>, F>> for SolverResult<T, OVector<T, D>>
513where
514    T: FloatNumber,
515    F: System<T, OVector<T, D>>,
516    DefaultAllocator: Allocator<D>,
517{
518    fn from(val: Dopri5<T, OVector<T, D>, F>) -> Self {
519        val.results
520    }
521}
522
523#[cfg(test)]
524mod tests {
525    use super::*;
526    use crate::{OVector, System, Vector1};
527    use nalgebra::{allocator::Allocator, DefaultAllocator, Dim};
528
529    // Same as Test3 from rk4.rs, but aborts after x is equal to or greater than 0.5
530    struct Test1 {}
531    impl<D: Dim> System<f64, OVector<f64, D>> for Test1
532    where
533        DefaultAllocator: Allocator<D>,
534    {
535        fn system(&self, x: f64, y: &OVector<f64, D>, dy: &mut OVector<f64, D>) {
536            dy[0] = (5. * x * x - y[0]) / (x + y[0]).exp();
537        }
538
539        fn solout(&mut self, x: f64, _y: &OVector<f64, D>, _dy: &OVector<f64, D>) -> bool {
540            x >= 0.5
541        }
542    }
543
544    #[test]
545    fn test_integrate_test1_svector() {
546        let system = Test1 {};
547        let mut stepper = Dopri5::new(system, 0., 1., 0.1, Vector1::new(1.), 1e-12, 1e-6);
548        let _ = stepper.integrate();
549
550        let x = stepper.x_out();
551        assert!((*x.last().unwrap() - 0.5).abs() < 1.0E-9);
552
553        let out = stepper.y_out();
554        assert!((&out[5][0] - 0.9130611474392001).abs() < 1.0E-9);
555    }
556
557    #[test]
558    #[should_panic]
559    fn test_integrate_when_continuous_output_type_panic() {
560        let system = Test1 {};
561        let mut stepper = Dopri5::from_param(
562            system,
563            0.,
564            1.,
565            0.1,
566            Vector1::new(1.),
567            1e-12,
568            1e-6,
569            0.1,
570            0.2,
571            0.3,
572            2.0,
573            5.0,
574            0.1,
575            10000,
576            1000,
577            OutputType::Continuous,
578        );
579        let _ = stepper.integrate();
580    }
581}