rkf45/lib.rs
1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{Error, format_err};
6
7/// Runge-Kutta Fehlberg 4(5) parameters
8///
9/// These are readily available in literature, e.g.
10/// <https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta%E2%80%93Fehlberg_method>.
11mod rkf45_params {
12 /// Number of stages
13 pub const NUM_STAGES: usize = 6;
14
15 /// Runge-Kutta matrix
16 pub static A: [[f64; NUM_STAGES - 1]; NUM_STAGES - 1] = [
17 [1.0 / 4.0, 0.0, 0.0, 0.0, 0.0],
18 [3.0 / 32.0, 9.0 / 32.0, 0.0, 0.0, 0.0],
19 [1932.0 / 2197.0, -7200.0 / 2197.0, 7296.0 / 2197.0, 0.0, 0.0],
20 [439.0 / 216.0, -8.0, 3680.0 / 513.0, -845.0 / 4104.0, 0.0],
21 [-8.0 / 27.0, 2.0, -3544.0 / 2565.0, 1859.0 / 4104.0, -11.0 / 40.0],
22 ];
23
24 /// 4th-order weights
25 pub static B4: [f64; NUM_STAGES] =
26 [25.0 / 216.0, 0.0, 1408.0 / 2565.0, 2197.0 / 4104.0, -1.0 / 5.0, 0.0];
27
28 /// 5th-order weights
29 pub static B5: [f64; NUM_STAGES] =
30 [16.0 / 135.0, 0.0, 6656.0 / 12825.0, 28561.0 / 56430.0, -9.0 / 50.0, 2.0 / 55.0];
31
32 /// Nodes
33 pub static C: [f64; NUM_STAGES - 1] = [1.0 / 4.0, 3.0 / 8.0, 12.0 / 13.0, 1.0, 1.0 / 2.0];
34}
35
36/// Parameters for adaptive time-stepping logic.
37///
38/// These incorporate a variety of heuristics and are mostly described in Press and Teukolsky's
39/// [Adaptive Stepsize Runge-Kutta Integration, Computers in Physics 6, 188
40/// (1992)](https://doi.org/10.1063/1.4823060).
41mod adaptive_stepping_params {
42 /// Safety factor to use when modifying time steps. This makes a revised time step slightly
43 /// smaller than required by formal analysis, providing margin for inaccuracy in the error
44 /// estimate.
45 pub static SAFETY_FACTOR: f64 = 0.9;
46
47 /// Only refine/coarsen the time step when the ratio of estimate error to desired error
48 /// crosses these thresholds. Doing so prevents excessive recalculation of time steps.
49 pub static ERROR_RATIO_REFINING_THRESHOLD: f64 = 1.1;
50 pub static ERROR_RATIO_COARSENING_THRESHOLD: f64 = 0.5;
51
52 /// RKF45's local truncation (single-step) error can be written as
53 /// E(dt) = C*dt^5 + O(dt^6).
54 ///
55 /// Suppose the desired error D is fixed with respect to dt. Then the optimal step size
56 /// approximately satisfies
57 /// C*dt_opt^5 = D ==> dt_opt = (D / C)^(1/5).
58 /// If we have attempted a time step of size dt, then we can approximate
59 /// C = E(dt) / dt^5.
60 /// This yields
61 /// dt_opt = dt * (D / E(dt))^(1/5) = (E(dt) / D)^(-1/5).
62 ///
63 /// Suppose instead that D is proprotional to dt, so D(dt) = D1*dt. Then the optimal step size
64 /// satisfies
65 /// C*dt_opt^5 = D1*dt_opt ==> C = (D1 / C)^(1/4).
66 /// If we have attempted a time step of size dt, with estimated error E(dt) and desired error
67 /// D(dt), then we observe that
68 /// D(dt) / E(dt) = (D1*dt) / (C*dt^5)
69 /// ==> D1 / C = dt^4 (D(dt) / E(dt)).
70 /// Using this relationship above yields
71 /// dt_opt = dt * (D(dt) / E(dt))^(1/4) = (E(dt) / D(dt))^(-1/4).
72 ///
73 /// Following the guidance of Press and Teukolsky, rather than inspect the form of the desired
74 /// error, we simply use the more conservative exponent depending on the situation. When
75 /// refining, we will have E/D > 1. Raising to the smaller exponent (-0.25) will yield a
76 /// smaller time step. Converseley, when coarsening we have E/D < 1, and the larger exponent
77 /// (-0.20) will yield a smaller timestep.
78 pub static REFINING_EXPONENT: f64 = -0.25;
79 pub static COARSENING_EXPONENT: f64 = -0.20;
80
81 /// Bounds for the factor used when refining/coarsening the time step, to avoid too much of
82 /// a change at once.
83 pub static MIN_REFINING_FACTOR: f64 = 0.2;
84 pub static MAX_COARSENING_FACTOR: f64 = 5.0;
85}
86
87/// Vector operations to implement on `[f64]` for convenience.
88///
89/// All methods involving another [f64] expect the two slices to be of equal length. The caller
90/// is responsible for ensuring this. (Practically speaking, the slices involved are from `Vec`s
91/// created using the length of `rkf45_adaptive`'s input `y`.)
92trait VectorOperations {
93 /// Scale by a scalar.
94 fn scale(&mut self, a: f64);
95
96 /// Copy to this vector from another one. Slice lengths must be equal.
97 fn copy_from(&mut self, x: &Self);
98
99 /// Add a vector to this one. Slice lengths must be equal.
100 fn add(&mut self, x: &Self);
101
102 /// Add a scalar times another vector. Slice lengths must be equal.
103 fn add_ax(&mut self, a: f64, x: &Self);
104
105 /// Subtract a vector from this one. Slice lengths must be equal.
106 fn subtract(&mut self, x: &Self);
107
108 /// Sets all elements to zero.
109 fn to_zeros(&mut self);
110}
111
112impl VectorOperations for [f64] {
113 fn scale(&mut self, a: f64) {
114 self.iter_mut().for_each(|p| *p = *p * a);
115 }
116
117 fn copy_from(&mut self, x: &Self) {
118 self.iter_mut().zip(x.iter()).for_each(|(p, q)| *p = *q);
119 }
120
121 fn add(&mut self, x: &Self) {
122 self.iter_mut().zip(x.iter()).for_each(|(p, q)| *p += *q);
123 }
124
125 fn add_ax(&mut self, a: f64, x: &Self) {
126 self.iter_mut().zip(x.iter()).for_each(|(p, q)| *p += a * *q);
127 }
128
129 fn subtract(&mut self, x: &Self) {
130 self.iter_mut().zip(x.iter()).for_each(|(p, q)| *p -= *q);
131 }
132
133 fn to_zeros(&mut self) {
134 self.iter_mut().for_each(|p| *p = 0.0)
135 }
136}
137
138/// Takes a single time step using RKF45.
139///
140/// Args:
141/// - `y`: Slice containing solution values at the beginning of the time step. This will be
142/// updated in-place.
143/// - `dydt`: Function that evaluates the time derivative of `y`.
144/// - `tn`: Time value at the beginning of the step.
145/// - `dt`: Length of the step.
146///
147/// Returns:
148/// - Vector estimating elementwise numerical error over this single step, and the maximum ratio of
149/// estimated error to error bound.
150fn rkf45_step(
151 y: &mut [f64],
152 dydt: &impl Fn(f64, &[f64]) -> Vec<f64>,
153 tn: f64,
154 dt: f64,
155 error_control: &ErrorControlOptions,
156) -> (Vec<f64>, f64) {
157 // Work array reused by several loops below.
158 let mut work = vec![0.0; y.len()];
159
160 let mut k = Vec::with_capacity(6);
161 k.push(dydt(tn, y));
162
163 use rkf45_params as params;
164 for i in 1..params::NUM_STAGES {
165 work.to_zeros();
166 for j in 0..i {
167 work.add_ax(params::A[i - 1][j], &k[j]);
168 }
169 // Effectively, k[i] = dydt(tn + C[i-1] * dt, y + dt * work).
170 work.scale(dt);
171 work.add(&y);
172 k.push(dydt(tn + params::C[i - 1] * dt, &work));
173 }
174
175 // The 5th-order solution will be used for error-estimation.
176 let mut y_5th_order = y.to_vec();
177 work.to_zeros();
178 for i in 0..params::B5.len() {
179 work.add_ax(params::B5[i], &k[i]);
180 }
181 y_5th_order.add_ax(dt, &work);
182
183 // y is updated with the 4th-order solution.
184 work.to_zeros();
185 for i in 0..params::B4.len() {
186 work.add_ax(params::B4[i], &k[i]);
187 }
188 y.add_ax(dt, &work);
189
190 let mut error_estimate = y_5th_order;
191 error_estimate.subtract(&y);
192 error_estimate.iter_mut().for_each(|x| *x = (*x).abs());
193
194 let mut max_error_ratio = 0.0;
195 for i in 0..y.len() {
196 // We use the fact that k[0] stores dydt(tn, yn).
197 let error_bound = error_control.absolute_magnitude
198 + error_control.relative_magnitude
199 * (error_control.function_scale * y[i].abs()
200 + error_control.derivative_scale * dt * k[0][i].abs());
201 max_error_ratio = f64::max(max_error_ratio, error_estimate[i] / error_bound);
202 }
203
204 (error_estimate, max_error_ratio)
205}
206
207/// Options to configure adaptive time-stepping.
208pub struct AdaptiveOdeSolverOptions {
209 /// Initial time for the solution.
210 pub t_initial: f64,
211 /// Final time for the solution.
212 pub t_final: f64,
213 /// Length of first attempted time step.
214 pub dt_initial: f64,
215 /// Parameters that determine the error bounds used to accept or reject time steps.
216 pub error_control: ErrorControlOptions,
217}
218
219/// Options for computing desired error when performing adaptive time-stepping.
220///
221/// `rkf45_adaptive` compares its estimate to a desired error, which is computed as
222/// ```
223/// D = max(absolute_magnitude + relative_magnitude * (function_scale * y[i] + derivative_scale * dt * dydt[i]))
224/// ```
225/// where `[i]` denotes the `i`th component, and the max is taken over all components.
226///
227/// This form supports a variety of ways of controlling the size of the desired error
228/// relative to `y` itself or to its increments.
229pub struct ErrorControlOptions {
230 /// Magnitude of the absolute component of desired error. Even if relative error is the primary
231 /// feature of interest, this must be set to a nonzero value as a safety measure in case
232 /// both y and dydt are near zero.
233 pub absolute_magnitude: f64,
234 /// Magnitude of the relative component of desired error.
235 pub relative_magnitude: f64,
236 /// Contribution of `y` to the relative component of desired error.
237 pub function_scale: f64,
238 /// Contribution of `y`'s increment (`dt * dydt`) to the relative component of desired error.
239 pub derivative_scale: f64,
240}
241
242impl ErrorControlOptions {
243 /// A simple error control option that sets the desired error to
244 /// ```
245 /// D = max(scale * (1 + y[i] + dt * dydt[i]))
246 /// ```
247 /// This has a lower bound of `scale`, but it grows proportional to `y` or `dydt` as either one
248 /// becomes large.
249 pub fn simple(scale: f64) -> ErrorControlOptions {
250 ErrorControlOptions {
251 absolute_magnitude: scale,
252 relative_magnitude: scale,
253 function_scale: 1.0,
254 derivative_scale: 1.0,
255 }
256 }
257}
258
259/// Solves an initial value problem using RKF45 with adaptive time-stepping.
260///
261/// The method of error control and time step refinement are described in
262/// Press and Teukolsky's [Adaptive Stepsize Runge-Kutta Integration, Computers in Physics 6,
263/// 188 (1992)](https://doi.org/10.1063/1.4823060).
264///
265/// Args:
266/// - `y`: Slice containing the initial value of the solution. This will be updated in-place.
267/// - `dydt`: Function that evaluates the time derivative of `y`.
268/// - `options`: Specifies parameters for the solver.
269///
270/// Returns:
271/// - Vector estimating total elementwise numerical error incurred by integration.
272pub fn rkf45_adaptive(
273 y: &mut [f64],
274 dydt: &impl Fn(f64, &[f64]) -> Vec<f64>,
275 options: &AdaptiveOdeSolverOptions,
276) -> Result<Vec<f64>, Error> {
277 macro_rules! validate_input {
278 ($x:expr) => {
279 if !($x) {
280 return Err(format_err!("Failed input validation: {}", stringify!($x)));
281 }
282 };
283 }
284
285 validate_input!(options.t_final > options.t_initial);
286 validate_input!(options.dt_initial > 0.0);
287 validate_input!(options.error_control.absolute_magnitude > 0.0);
288 validate_input!(options.error_control.relative_magnitude >= 0.0);
289 validate_input!(options.error_control.function_scale >= 0.0);
290 validate_input!(options.error_control.derivative_scale >= 0.0);
291
292 let mut t = options.t_initial;
293 let mut dt = options.dt_initial;
294 let mut total_error = vec![0.0; y.len()];
295
296 let mut work = y.to_vec();
297 while t < options.t_final {
298 if t + dt > options.t_final {
299 dt = options.t_final - t;
300 }
301
302 let (error_estimate, max_error_ratio) =
303 rkf45_step(&mut work, dydt, t, dt, &options.error_control);
304 use adaptive_stepping_params as params;
305 if max_error_ratio < params::ERROR_RATIO_REFINING_THRESHOLD {
306 // Accept the step, and update time.
307 y.copy_from(&work);
308 total_error.add(&error_estimate);
309 t += dt;
310 if max_error_ratio < params::ERROR_RATIO_COARSENING_THRESHOLD {
311 // Error was particularly small, so increase step size.
312 let factor = f64::min(
313 params::SAFETY_FACTOR * max_error_ratio.powf(params::COARSENING_EXPONENT),
314 params::MAX_COARSENING_FACTOR,
315 );
316 dt *= factor;
317 }
318 } else {
319 // Throw out this time stemp. Revert the work array, and reduce dt.
320 work.copy_from(y);
321 let factor = f64::max(
322 params::SAFETY_FACTOR * max_error_ratio.powf(params::REFINING_EXPONENT),
323 params::MIN_REFINING_FACTOR,
324 );
325 dt *= factor;
326 }
327 }
328
329 Ok(total_error)
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use std::f64::consts::PI;
336 use test_util::{assert_gt, assert_lt};
337
338 // rkf45_step requires ErrorControlOptions as an input. But when performing convergence
339 // tests on that function, the options are meaningless. This is named to document that fact.
340 static MEANINGLESS_OPTIONS: ErrorControlOptions = ErrorControlOptions {
341 absolute_magnitude: 1e-8,
342 relative_magnitude: 1e-8,
343 function_scale: 1.0,
344 derivative_scale: 1.0,
345 };
346
347 // Test rkf45_step on a first-order problem:
348 // y' = lambda*y, y(0)=1.
349 // The exact solution is y(t)=exp(lambda*t).
350 //
351 // rkf45_step should be accurate up to O(dt^5) over a single time step (local truncation
352 // error). Its error estimate should be accurate up to O(dt). We test both expectations
353 // by the way the numerical errors decrease as dt is refined.
354 #[fuchsia::test]
355 fn test_first_order_problem_rkf45_step() {
356 let lambda = -0.1;
357 let dydt = |_t: f64, y: &[f64]| -> Vec<f64> { vec![lambda * y[0]] };
358 let y_true = |t: f64| -> f64 { f64::exp(lambda * t) };
359
360 // Record the actual error values, and the errors in estimated error values (i.e.
361 // how close the estimated numerical error is to the actual numerical error) over
362 // succesively-halved time steps.
363 let mut actual_errors = Vec::new();
364 let mut errors_in_estimated_error = Vec::new();
365 for dt in &[1.0, 0.5, 0.25] {
366 let mut y = [1.0];
367 let estimated_error = rkf45_step(&mut y, &dydt, 0.0, *dt, &MEANINGLESS_OPTIONS).0[0];
368 let actual_error = (y_true(*dt) - y[0]).abs();
369 errors_in_estimated_error.push((estimated_error - actual_error).abs());
370 actual_errors.push(actual_error);
371 }
372
373 // Each time dt is halved, actual_errors should shrink by roughly 1/32, and
374 // errors_in_estimated_error should shrink by roughly 1/2. The factor of 1.10 allows a 10%
375 // margin versus the expected convergence rate.
376 assert_lt!(actual_errors[1], actual_errors[0] / 32.0 * 1.10);
377 assert_lt!(actual_errors[2], actual_errors[1] / 32.0 * 1.10);
378 assert_lt!(errors_in_estimated_error[1], errors_in_estimated_error[0] / 2.0 * 1.10);
379 assert_lt!(errors_in_estimated_error[2], errors_in_estimated_error[1] / 2.0 * 1.10);
380 }
381
382 // Test rkf45_adaptive on the same first-order problem as above.
383 //
384 // This checks that we integrate to t_final with the requested degree of accuracy.
385 #[fuchsia::test]
386 fn test_first_order_problem_rkf45_adaptive() -> Result<(), Error> {
387 let lambda = -0.1;
388 let dydt = |_t: f64, y: &[f64]| -> Vec<f64> { vec![lambda * y[0]] };
389 let y_true = |t: f64| -> f64 { f64::exp(lambda * t) };
390
391 let options = AdaptiveOdeSolverOptions {
392 t_initial: 0.0,
393 t_final: 3.0,
394 dt_initial: 0.1,
395 error_control: ErrorControlOptions::simple(1e-6),
396 };
397 let mut y = [1.0];
398 rkf45_adaptive(&mut y, &dydt, &options)?;
399 let actual_error = (y_true(options.t_final) - y[0]).abs();
400
401 // Error should be near the size specified, but not smaller than it.
402 assert_lt!(actual_error, 1e-5);
403 assert_gt!(actual_error, 1e-7);
404
405 Ok(())
406 }
407
408 // Test rkf45_step for a second-order problem:
409 // y'' = -y; y(0)=1, y'(0)=0.
410 // The exact solution is y(t)=cos(t).
411 //
412 // To apply the ODE solver, we translate this to the first-order system
413 // y[0]' = y[1]
414 // y[1]' = -y[0].
415 // with initial conditions y[0](0)=1, y[1](0)=0.
416 //
417 // This test follows the same methodology as test_first_order_problem_rkf45_step;
418 // see it for detailed documentation.
419 #[fuchsia::test]
420 fn test_second_order_problem_rkf45_step() {
421 let dydt = |_t: f64, y: &[f64]| -> Vec<f64> { vec![y[1], -y[0]] };
422 let y_true = |t: f64| -> f64 { f64::cos(t) };
423
424 let mut actual_errors = Vec::new();
425 let mut errors_in_estimated_error = Vec::new();
426 for dt in &[PI / 4.0, PI / 8.0, PI / 16.0] {
427 let mut y = [1.0, 0.0];
428 let estimated_error = rkf45_step(&mut y, &dydt, 0.0, *dt, &MEANINGLESS_OPTIONS).0[0];
429 let actual_error = (y_true(*dt) - y[0]).abs();
430 errors_in_estimated_error.push((estimated_error - actual_error).abs());
431 actual_errors.push(actual_error);
432 }
433
434 assert_lt!(actual_errors[1], actual_errors[0] / 32.0 * 1.10);
435 assert_lt!(actual_errors[2], actual_errors[1] / 32.0 * 1.10);
436 assert_lt!(errors_in_estimated_error[1], errors_in_estimated_error[0] / 2.0 * 1.10);
437 assert_lt!(errors_in_estimated_error[2], errors_in_estimated_error[1] / 2.0 * 1.10);
438 }
439
440 // Test rkf45_adaptive for the same second-order problem as above.
441 #[fuchsia::test]
442 fn test_second_order_problem_rkf45_adaptive() -> Result<(), Error> {
443 let dydt = |_t: f64, y: &[f64]| -> Vec<f64> { vec![y[1], -y[0]] };
444 let y_true = |t: f64| -> f64 { f64::cos(t) };
445
446 let options = AdaptiveOdeSolverOptions {
447 t_initial: 0.0,
448 t_final: 2.0 * PI,
449 dt_initial: PI / 4.0,
450 error_control: ErrorControlOptions::simple(1e-6),
451 };
452 let mut y = [1.0, 0.0];
453 rkf45_adaptive(&mut y, &dydt, &options)?;
454 let actual_error = (y_true(options.t_final) - y[0]).abs();
455
456 // Error should be near the size specified, but not smaller than it.
457 assert_lt!(actual_error, 1e-4);
458 assert_gt!(actual_error, 1e-7);
459
460 Ok(())
461 }
462
463 // Test rkf45_step for a third-order time-variant problem.
464 //
465 // This is a contrived problem with solution
466 // y(t) = cos(alpha*t^2).
467 // As a third-order scalar equation,
468 // y''' + 4*alpha^2*t^2*y' + 12*alpha^2*t*y = 0; y(0)=1, y'(0)=0, y''(0)=0.
469 // As a first-order system,
470 // y[0]' = y[1]
471 // y[1]' = y[2].
472 // y[2]' = -12*alpha^2*t*y[0] - 4*alpha^2*t^2*y[1]
473 // with initial conditions y[0](0)=1, y[1](0)=0, y[2](0)=0.
474 //
475 // This test follows the same methodology as test_first_order_problem_rkf45_step;
476 // see it for detailed documentation.
477 #[fuchsia::test]
478 fn test_third_order_problem_rkf45_step() {
479 let alpha = 0.1;
480 let square = |x: f64| -> f64 { x * x };
481 let dydt = |t: f64, y: &[f64]| -> Vec<f64> {
482 vec![y[1], y[2], -12.0 * square(alpha) * t * y[0] - 4.0 * square(alpha * t) * y[1]]
483 };
484 let y_true = |t: f64| -> f64 { f64::cos(alpha * square(t)) };
485
486 let mut actual_errors = Vec::new();
487 let mut errors_in_estimated_error = Vec::new();
488 for dt in &[0.25, 0.125, 0.0625] {
489 let mut y = [1.0, 0.0, 0.0];
490 let estimated_error = rkf45_step(&mut y, &dydt, 0.0, *dt, &MEANINGLESS_OPTIONS).0[0];
491 let actual_error = (y_true(*dt) - y[0]).abs();
492 errors_in_estimated_error.push((estimated_error - actual_error).abs());
493 actual_errors.push(actual_error);
494 }
495
496 assert_lt!(actual_errors[1], actual_errors[0] / 32.0 * 1.10);
497 assert_lt!(actual_errors[2], actual_errors[1] / 32.0 * 1.10);
498 assert_lt!(errors_in_estimated_error[1], errors_in_estimated_error[0] / 2.0 * 1.10);
499 assert_lt!(errors_in_estimated_error[2], errors_in_estimated_error[1] / 2.0 * 1.10);
500 }
501 // This tests rkf45_adaptive for the same third-order problem as above.
502 #[fuchsia::test]
503 fn test_third_order_problem_rkf45_adaptive() -> Result<(), Error> {
504 let alpha = 0.1;
505 let square = |x: f64| -> f64 { x * x };
506 let dydt = |t: f64, y: &[f64]| -> Vec<f64> {
507 vec![y[1], y[2], -12.0 * square(alpha) * t * y[0] - 4.0 * square(alpha * t) * y[1]]
508 };
509 let y_true = |t: f64| -> f64 { f64::cos(alpha * square(t)) };
510
511 let options = AdaptiveOdeSolverOptions {
512 t_initial: 0.0,
513 t_final: 4.0,
514 dt_initial: 0.25,
515 error_control: ErrorControlOptions::simple(1e-6),
516 };
517 let mut y = [1.0, 0.0, 0.0];
518 rkf45_adaptive(&mut y, &dydt, &options)?;
519 let actual_error = (y_true(options.t_final) - y[0]).abs();
520
521 // Error should be near the size specified, but not smaller than it.
522 assert_lt!(actual_error, 1e-4);
523 assert_gt!(actual_error, 1e-7);
524
525 Ok(())
526 }
527
528 // Test rkf45_adaptive on a problem with multiple time scales.
529 //
530 // This is the canonical use case for adaptive time-stepping. We have a problem with two time
531 // scales, one that is much faster than the other. Our t_final is 1, and we suggest a single
532 // step of length 1, which would be plenty for the slow time scale. However, the fast time
533 // scale requires much smaller time steps, and the integrator must detect this.
534 #[fuchsia::test]
535 fn test_multiple_time_scales() -> Result<(), Error> {
536 let lambda1 = 10.0;
537 let lambda2 = 0.001;
538 let dydt = |_t: f64, y: &[f64]| -> Vec<f64> { vec![-lambda1 * y[0], -lambda2 * y[1]] };
539 let y_true = |t: f64| -> Vec<f64> { vec![f64::exp(-lambda1 * t), f64::exp(-lambda2 * t)] };
540
541 let options = AdaptiveOdeSolverOptions {
542 t_initial: 0.0,
543 t_final: 1.0,
544 dt_initial: 1.0,
545 error_control: ErrorControlOptions::simple(1e-6),
546 };
547 let mut y = [1.0, 1.0];
548 rkf45_adaptive(&mut y, &dydt, &options)?;
549 let mut actual_error = y_true(options.t_final);
550 actual_error.iter_mut().zip(y.iter()).for_each(|(p, q)| *p = (*p - *q).abs());
551
552 // Our requested error scale bounds the error in both components above. The second component
553 // has a much smaller error than our requested tolerance because it varies so slowly relative
554 // to the first, which governs the time step selection. Hence, we don't test actual_error[1]
555 // for a lower bound.
556 assert_lt!(actual_error[0], 1e-5);
557 assert_lt!(actual_error[1], 1e-5);
558 assert_gt!(actual_error[0], 1e-7);
559
560 Ok(())
561 }
562
563 // Test that rkf45_advance returns an error when passed invalid options.
564 #[fuchsia::test]
565 fn test_error_checks() {
566 let dydt = |_t: f64, _y: &[f64]| -> Vec<f64> { vec![0.0] };
567
568 assert!(
569 rkf45_adaptive(
570 &mut [1.0],
571 &dydt,
572 &AdaptiveOdeSolverOptions {
573 t_initial: 2.0, // Greater than t_final
574 t_final: 1.0,
575 dt_initial: 0.1,
576 error_control: ErrorControlOptions::simple(1e-8),
577 }
578 )
579 .is_err()
580 );
581
582 assert!(
583 rkf45_adaptive(
584 &mut [1.0],
585 &dydt,
586 &AdaptiveOdeSolverOptions {
587 t_initial: 1.0,
588 t_final: 2.0,
589 dt_initial: -0.1, // Negative
590 error_control: ErrorControlOptions {
591 absolute_magnitude: 1e-8,
592 relative_magnitude: 1e-8,
593 function_scale: 1.0,
594 derivative_scale: 1.0,
595 }
596 }
597 )
598 .is_err()
599 );
600
601 assert!(
602 rkf45_adaptive(
603 &mut [1.0],
604 &dydt,
605 &AdaptiveOdeSolverOptions {
606 t_initial: 1.0,
607 t_final: 2.0,
608 dt_initial: 0.1,
609 error_control: ErrorControlOptions {
610 absolute_magnitude: -1e-8, // Negative
611 relative_magnitude: 1e-8,
612 function_scale: 1.0,
613 derivative_scale: 1.0,
614 }
615 }
616 )
617 .is_err()
618 );
619
620 assert!(
621 rkf45_adaptive(
622 &mut [1.0],
623 &dydt,
624 &AdaptiveOdeSolverOptions {
625 t_initial: 1.0,
626 t_final: 2.0,
627 dt_initial: 0.1,
628 error_control: ErrorControlOptions {
629 absolute_magnitude: 1e-8,
630 relative_magnitude: -1e-8, // Negative
631 function_scale: 1.0,
632 derivative_scale: 1.0,
633 }
634 }
635 )
636 .is_err()
637 );
638
639 assert!(
640 rkf45_adaptive(
641 &mut [1.0],
642 &dydt,
643 &AdaptiveOdeSolverOptions {
644 t_initial: 1.0,
645 t_final: 2.0,
646 dt_initial: 0.1,
647 error_control: ErrorControlOptions {
648 absolute_magnitude: 1e-8,
649 relative_magnitude: 1e-8,
650 function_scale: -1.0, // Negative
651 derivative_scale: 1.0,
652 }
653 }
654 )
655 .is_err()
656 );
657
658 assert!(
659 rkf45_adaptive(
660 &mut [1.0],
661 &dydt,
662 &AdaptiveOdeSolverOptions {
663 t_initial: 1.0,
664 t_final: 2.0,
665 dt_initial: 0.1,
666 error_control: ErrorControlOptions {
667 absolute_magnitude: 1e-8,
668 relative_magnitude: 1e-8,
669 function_scale: 1.0,
670 derivative_scale: -1.0, // Negative
671 }
672 }
673 )
674 .is_err()
675 );
676
677 assert!(
678 rkf45_adaptive(
679 &mut [1.0],
680 &dydt,
681 &AdaptiveOdeSolverOptions {
682 t_initial: 1.0,
683 t_final: 2.0,
684 dt_initial: 0.1,
685 error_control: ErrorControlOptions {
686 absolute_magnitude: 0.0, // Must be positive
687 relative_magnitude: 1e-8,
688 function_scale: 1.0,
689 derivative_scale: 1.0,
690 }
691 }
692 )
693 .is_err()
694 );
695 }
696}