package owl-ode-odepack

  1. Overview
  2. Docs
Owl's ODE solvers, interface with ODEPACK

Install

Dune Dependency

Authors

Maintainers

Sources

owl-ode-v0.2.0.tbz
sha256=465ef6c59925fa58df395d67fbfe10454c97c48664e4b10061df1b16f0cca050
sha512=ac528b01b3dd7d8de8bfd2e42979625db52fcd0b3d849dd61f18fd2467ed383e156afe1e0c546ffa2ba37a826484077bd7432ca051a2657ded84376a432a6573

README.md.html

OwlDE - Ordinary Differential Equation Solvers

Please refer to the Project Page for details.

You can run the current examples with dune exec examples/van_der_pol.exe, dune exec examples/damped.exe.

The documentation is accessible at ocaml.xyz/owl_ode/owl-ode and ocaml.xyz/apidoc/ode.html.

Tutorial

Overview

Consider the problem of integrating a linear dymaical system that evolves according to

dx/dt = f(x,t) = Ax      x(t0) = x0,

where x is the state of the system, dx/dt is the time derivative of the state, and t is time. Our system A is the matrix [[1,-1; 2,-3]] and the system's initial state x0 is at [[-1]; [1]]. We want to integrate for 2 seconds with a step size of 1 millisecond. Here is how you would solve this problem using OwlDE:

(* f(x,t) *)
let f x t = 
   let a = [|[|1.; -1.|];
             [|2.; -3.|]|]
           |> Owl.Mat.of_arrays in
   Owl.Mat.(a *@ x)

(* temporal specification:
   construct a record using the constructor T1 and 
   includes information of start time, duration, 
   and step size.*)
let tspec = Owl_ode.Types.(T1 {t0 = 0.; duration = 2.; dt=1E-3})

(* initial state of the system *)
let x0 = Mat.of_array [|-1.; 1.|] 2 1

(* putting everything together *)
let ts, xs = Owl_ode.odeint (module Owl_ode.Native.D.RK4) f x0 tspec () 

(* or equivalently *)
let ts, xs = Owl_ode.odeint Owl_ode.Native.D.rk4 f x0 tspec ()

The results of odeint in this example are two matrices xs and ts, which contain the value of the state x at each time t. More specifically, column 0 of the matrix xs contains x(t0), while column 2000 contains x(t0 +. duration).

Here, we integrated the dynamical system with Native.D.RK4, a fixed-step, double-precision Runge-Kutta solver.

In Owl Ode, We support a number of natively-implemented double-precision solvers in Native.D and single-precision ones in Native.S.

The simple example above illustrates the basic components of defining and solving an ode problem using Owl Ode. The main function Owl_ode.odeint takes as its arguments:

  • a solver module of type Solver,

  • a function f that evolves the state,

  • an initial state x0, and

  • temporal spsecification tspec.

The solver module constrains the the type of x0 and that of function f . For example, the solvers in Owl_ode.Native, assume that the states are matrices (i.e. x:mat is a matrix) and f:mat->float->mat returns the time derivative of x at time t.

We have provided a number of single and double-precision symplectic solvers in Owl_ode.Symplectic. For symplectic ode problems, the state of the system is a tuple (x,p):mat * mat, where x and p are the position and momentum coordinates of the system and f:(mat,mat)->float->mat is a forcing function defined with at state (x,p) and time t. For a detailed example on how to call symplectic solvers, see example/damped.ml.

Sundials Cvode

We have implemented a thin wrapper over Sundials Cvode (via sundialsml's own wrapper). To use Cvode, one can use

  • Owl_ode_sundials.Owl_Cvode or

  • Owl_ode_sundials.Owl_Cvode_Stiff.

Currently, we only support double-precision Sundials solvers. To use Sundials in Owl Ode, one needs to install Sundials and sundialsml (see sundialsml for instructions).

Automatic inference of state dimensionality

All the provided solvers automatically infer the dimensionality of the state from the initial state. Consider Native solvers, for which the state of the system is a matrix. The initial state can be a row vector, a column vector, or a matrix, so long as it is consistent with that of f. If the initial state x0 is a row vector with dimensions 1xN and we integrate the system for T time steps, the time and states will be stacked vertically in the output (i.e. ts will have dimensions Tx1 and and xs will have dimensions TxN). On the contrary, if the initial state x0 is a column vector with dimensions, the results will be stacked horizontally (i.e. ts will have dimensions 1xT and xs will have dimensions NxT).

We also support temporal integration of matrices. That is, cases in which the state x is a matrix of dimensions of dimensions NxM. By default, in the output, we flatten and stack the states vertically (i.e., ts has dimensions Tx1 and xs has dimensions TxNM. We have a helper function Native.D.to_state_array which can be used to "unflatten" xs into an array of matrices.

Custom Solvers

We can define new solver module by creating a module of type Solver. For example, to create a custom Cvode solver that has a relative tolerance of 1E-7 as opposed to the default 1E-4, we can define and use custom_cvode as follows:

let custom_cvode = Owl_ode_sundials.cvode ~stiff:false ~relative_tol:1E-7 ~abs_tol:1E-4 
(* usage *)
let ts, xs = Owl_ode.odeint custom_cvode f x0 tspec ()

Here, we use the cvode function construct a solver module Custom_Owl_Cvode. This function is conveniently defined in src/sundials/owl_ode_sundials.ml. It takes the parameters (stiff, relative_tol, and abs_tol) and returns a solver module of type

val custom_cvode : (module Solver with 
                     type state = Mat.mat
                     and type f = Mat.mat -> float -> Mat.mat
                     and type step_output = Mat.mat * float
                     and type solve_output = Mat.mat * Mat.mat)

Similar helper functions like cvode have been also defined for native and symplectic solvers.

Supported Solvers

Native

  • Euler

  • Midpoint

  • RK4

  • RK23

  • RK45

example usage: Owl_ode.Native.D.Euler (or Owl_ode.Native.D.euler), Owl_ode.Native.S.Euler (or Owl_ode.Native.S.euler)

Symplectic

  • Symplectic_Euler

  • PseudoLeapFrog

  • LeapFrog

  • Ruth3

  • Ruth4

example usage: Owl_ode.Native.D.Symplectic_Euler, Owl_ode.Symplectic.S.Symplectic_Euler

Sundials

  • Owl_Cvode (Adams)

  • Owl_Cvode_Stiff (BDF)

example usage: Owl_ode_sundials.Owl_Cvode

We only support double-precisions Sundials solvers.

NOTES

The main idea is develop a uniform interface to integrate ODE solvers (and in the future finite element methods) into Owl. Currently there are three options available, providing incompatible underlying representations:

Of course such an interface could provide additional purely OCaml functionalities, like robust native implementations of

  • [x] standard fixed-step ode solvers, like Euler, Midpoint, Runge-Kutta 4

  • [x] standard adaptive solvers, say rk2(3), and rk4(5) or Tsit5 (in progress, missing Tsit5)

  • [x] symplectic ode solvers, like Störmer-Verlet, Forest-Ruth or Yoshida

  • [ ] sundialsml interface (already partially implemented)

  • [x] odepack interface (already partially implemented, currently not fully configurable/controllable)

and implementations leveraging Owl's specific capabilities, like an implementation of the Taylor integrator built upon Algodiff. Albeit relatively old and standard, a good starting point could be the two references from TaylorSeries.jl, namely:

  • W. Tucker, Validated numerics: A short introduction to rigorous computations, Princeton University Press (2011).

  • A. Haro, Automatic differentiation methods in computational dynamical systems: Invariant manifolds and normal forms of vector fields at fixed points, preprint.

Some important points to address for this are:

  • [X] provide a uniform type safe interface, capable of accepting pluggable new engines and dealing with the different sets of configuration options of each of them (maybe extensible types or GADTs can help in this regard more than Functors?)

  • [X] full Owl types interoperability

  • [X] ease of use (compared to JuliaDiffEq and Scipy)

  • [ ] make the native implementations robust (right now they are naive OCaml implementations)

  • ...

It would be interesting to design an interface that allows to implement the Neural ODE idea in a natural way also in Owl.

Further comments

We could provide two interfaces, one takes a stepper function and performs just a step, and can be iterated manually (like odeint in the current sundials implementation, or the integrators in the current ocaml implementation), and a lower level one mimicking sundials and odepack, that only performs each integration step separately.

We currently cannot have implicit methods for the lack of vector-valued root finding functions. We should add implementations for those, and then introduce some implicit methods (e.g. the implicit Störmer-Verlet is much more robust and works nicely for non-separable Hamiltonians). At least we can use Sundials for now :-)

It would also be nice to provide a function that takes the pair (t, y) and returns the interpolated function.

We should make the integrators more robust and with better failure modes, we could take inspiration from the very readable scipy implementation [https://github.com/scipy/scipy/blob/v1.2.0/scipy/integrate/_ivp/rk.py#L15].

Contributing

We use ocamlformat to format out code. Our preferred ocamlformat setup is specified in .ocamlformat. With dune, it is super simple to reformat the entire code base. Once you have ocamlformat installed, all you have to do in the project directory is do

dune build @fmt
dune promote
OCaml

Innovation. Community. Security.