Solving the Problem
Each solver implements the OdeSolverMethod
trait, which provides a number of methods to solve the problem.
Solving the Problem
DiffSol has a few high-level solution functions on the OdeSolverMethod
trait that are the easiest way to solve your equations:
solve
- solve the problem from an initial state up to a specified time, returning the solution at all the internal timesteps used by the solver.solve_dense
- solve the problem from an initial state, returning the solution at aVec
of times provided by the user.solve_dense_sensitivities
- solve the forward sensitivity problem from an initial state, returning the solution at aVec
of times provided by the user.solve_adjoint
- solve the adjoint sensitivity problem from an initial state to a final time, returning the integration of the output function over time as well as its gradient with respect to the initial state.
The following example shows how to solve a simple ODE problem using the solve
method on the OdeSolverMethod
trait.
use diffsol::OdeBuilder; use nalgebra::DVector; use diffsol::{OdeSolverMethod, NalgebraLU}; type M = nalgebra::DMatrix<f64>; type LS = NalgebraLU<f64>; fn main() { let problem = OdeBuilder::<M>::new() .p(vec![1.0, 10.0]) .rhs_implicit( |x, p, _t, y| y[0] = p[0] * x[0] * (1.0 - x[0] / p[1]), |x, p, _t, v , y| y[0] = p[0] * v[0] * (1.0 - 2.0 * x[0] / p[1]), ) .init(|_p, _t| DVector::from_element(1, 0.1)) .build() .unwrap(); let mut solver = problem.bdf::<LS>().unwrap(); let (ys, ts) = solver.solve(10.0).unwrap(); }
solve_dense
will solve a problem from an initial state, returning the solution at a Vec
of times provided by the user.
use diffsol::OdeBuilder; use nalgebra::DVector; type M = nalgebra::DMatrix<f64>; use diffsol::{OdeSolverMethod, NalgebraLU}; type LS = NalgebraLU<f64>; fn main() { let problem = OdeBuilder::<M>::new() .p(vec![1.0, 10.0]) .rhs_implicit( |x, p, _t, y| y[0] = p[0] * x[0] * (1.0 - x[0] / p[1]), |x, p, _t, v , y| y[0] = p[0] * v[0] * (1.0 - 2.0 * x[0] / p[1]), ) .init(|_p, _t| DVector::from_element(1, 0.1)) .build() .unwrap(); let mut solver = problem.bdf::<LS>().unwrap(); let times = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]; let _soln = solver.solve_dense(×).unwrap(); }
Stepping the Solution
The fundamental method to step the solver through a solution is the step
method on the OdeSolverMethod
trait, which steps the solution forward in time by a single step, with a step size chosen by the solver in order to satisfy the error tolerances in the problem
struct. The step
method returns a Result
that contains the new state of the solution if the step was successful, or an error if the step failed.
use diffsol::OdeBuilder; use nalgebra::DVector; type M = nalgebra::DMatrix<f64>; use diffsol::{OdeSolverMethod, NalgebraLU}; type LS = NalgebraLU<f64>; fn main() { let problem = OdeBuilder::<M>::new() .p(vec![1.0, 10.0]) .rhs_implicit( |x, p, _t, y| y[0] = p[0] * x[0] * (1.0 - x[0] / p[1]), |x, p, _t, v , y| y[0] = p[0] * v[0] * (1.0 - 2.0 * x[0] / p[1]), ) .init(|_p, _t| DVector::from_element(1, 0.1)) .build() .unwrap(); let mut solver = problem.bdf::<LS>().unwrap(); while solver.state().t < 10.0 { if let Err(_) = solver.step() { break; } } }
The step
method will return an error if the solver fails to converge to the solution or if the step size becomes too small.
Often you will want to get the solution at a specific time \(t_o\). There are two ways to do this based on your particular needs, the most lightweight way is to step the solution forward
until you are beyond \(t_o\), and then interpolate the solution back to \(t_o\) using the interpolate
method on the OdeSolverMethod
trait.
use diffsol::OdeBuilder; use nalgebra::DVector; type M = nalgebra::DMatrix<f64>; use diffsol::{OdeSolverMethod, NalgebraLU}; type LS = NalgebraLU<f64>; fn main() { let problem = OdeBuilder::<M>::new() .p(vec![1.0, 10.0]) .rhs_implicit( |x, p, _t, y| y[0] = p[0] * x[0] * (1.0 - x[0] / p[1]), |x, p, _t, v , y| y[0] = p[0] * v[0] * (1.0 - 2.0 * x[0] / p[1]), ) .init(|_p, _t| DVector::from_element(1, 0.1)) .build() .unwrap(); let mut solver = problem.bdf::<LS>().unwrap(); let t_o = 10.0; while solver.state().t < t_o { solver.step().unwrap(); } let _soln = solver.interpolate(t_o).unwrap(); }
The second way is to use the set_stop_time
method on the OdeSolverMethod
trait to stop the solver at a specific time, this will override the internal time step so that the solver stops at the specified time.
Note that this can be less efficient if you wish to continue stepping forward after the specified time, as the solver will need to be re-initialised.
The enum returned by step
will indicate when the solver has stopped at the specified time.
Once the solver has stopped at the specified time, you can get the current state of the solution using the state
method on the solver, which returns an OdeSolverState
struct.
use diffsol::OdeBuilder; use nalgebra::DVector; type M = nalgebra::DMatrix<f64>; use diffsol::{OdeSolverMethod, OdeSolverStopReason, NalgebraLU}; type LS = NalgebraLU<f64>; fn main() { let problem = OdeBuilder::<M>::new() .p(vec![1.0, 10.0]) .rhs_implicit( |x, p, _t, y| y[0] = p[0] * x[0] * (1.0 - x[0] / p[1]), |x, p, _t, v , y| y[0] = p[0] * v[0] * (1.0 - 2.0 * x[0] / p[1]), ) .init(|_p, _t| DVector::from_element(1, 0.1)) .build() .unwrap(); let mut solver = problem.bdf::<LS>().unwrap(); solver.set_stop_time(10.0).unwrap(); loop { match solver.step() { Ok(OdeSolverStopReason::InternalTimestep) => continue, Ok(OdeSolverStopReason::TstopReached) => break, Ok(OdeSolverStopReason::RootFound(_)) => panic!("Root finding not used"), Err(e) => panic!("Solver failed to converge: {}", e), } } let _soln = &solver.state().y; }