diff --git a/CHANGELOG.md b/CHANGELOG.md index 684096bb44b397bb6c5b43ea9b966545b39304d3..beb020fd007033de4716377d42f24deb5c64fd4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## Release notes -# 6.0.5 (in preparation) +# 6.0.6 (in preparation) + +### Features: + - Add external ode-solver interface with [Sundials CVODE + library](http://computation.llnl.gov/projects/sundials-suite-nonlinear-differential-algebraic-equation-solvers/cvode). + +## Release notes +# 6.0.5 ### Features: - Added an ODE solver library that can solve transient and nonlinear processes diff --git a/Documentation/images/external-ode-solver-concept.pdf b/Documentation/images/external-ode-solver-concept.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f0c5a2fabe1c05abc754c7a4f34a7f2b0e9ea82d Binary files /dev/null and b/Documentation/images/external-ode-solver-concept.pdf differ diff --git a/Documentation/images/external-ode-solver-concept.png b/Documentation/images/external-ode-solver-concept.png new file mode 100644 index 0000000000000000000000000000000000000000..94ad2056a868cb801bce815af3d2f9aeb628529a Binary files /dev/null and b/Documentation/images/external-ode-solver-concept.png differ diff --git a/Documentation/images/external-ode-solver-concept.tex b/Documentation/images/external-ode-solver-concept.tex new file mode 100644 index 0000000000000000000000000000000000000000..586cd078ffff575aa101e7f1ca74f2b648c570d8 --- /dev/null +++ b/Documentation/images/external-ode-solver-concept.tex @@ -0,0 +1,143 @@ +%\documentclass[landscape,pagesize,DIV=14]{scrartcl} +\documentclass[border=2mm]{standalone} + +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} + +\usepackage{amsmath} + +\usepackage{tikz} +\usetikzlibrary{graphs}%,graphdrawing} +\usetikzlibrary{positioning} +\usetikzlibrary{calc} + +\newcommand{\code}[1]{\texttt{#1}} +\newcommand{\Owns}[1]{{\color{red}#1}} + +\definecolor{mycontour}{HTML}{43709A} +\definecolor{myfill}{HTML}{E2EBF2} +\newlength\separation +\setlength{\separation}{1ex} + +\begin{document} + +\begin{tikzpicture}[ + very thick, + align=left, anchor=west, + %node distance=\separation, + node distance=7\separation and 0pt, + color=mycontour, + text=black, + every node/.style={ + inner ysep=\separation, + inner xsep=1.5\separation, + rectangle, + rounded corners, + draw=mycontour, + fill=myfill + } +] + \node (odesolver) at (0, 0) { + \code{ODESolver<NumEquations>} + \\ agnostic to specific ODE solver backend + \\ method signatures use \code{Eigen::Map} types + \\ $\implies$ vector size information implicitly provided, checked at compile time + }; + + \node (concreteodesolver) [below right=of odesolver.south west] { + \code{ConcreteODESolver<CVodeSolver, NumEquations>} + \\ interfaces with a specific library + \\ method signatures use \code{Eigen::Map} types + }; + + \node (cvodesolver) [below right=of concreteodesolver.south west] { + \code{CVodeSolver} + \\ method signatures use \code{double*} + \\ no template parameters + }; + + \node (cvodesolverimpl) [below right=of cvodesolver.south west] { + \code{CVodeSolverImpl} + \\ method signatures use \code{double*} + \\ implementation of the pimpl idiom + \\ external library headers only have to be included in that file \\ + \quad where this class is defined + }; + + \draw[->] let \p1=(odesolver.south west), \p2=(concreteodesolver.north west) + in (\x1+1.75\separation,\y1-.25\separation) -- + node [pos=0.5, right=1.75\separation] { + dynamic polymorphism + } + (\x2+1.75\separation,\y2+.25\separation); + + \draw[->] let \p1=(concreteodesolver.south west), \p2=(cvodesolver.north west) + in (\x1+1.75\separation,\y1-.25\separation) -- + node [pos=0.5, right=1.75\separation] { + pass vectors on as \code{double*}, thereby no need for templates anymore + } + (\x2+1.75\separation,\y2+.25\separation); + + \draw[->] let \p1=(cvodesolver.south west), \p2=(cvodesolverimpl.north west) + in (\x1+1.75\separation,\y1-.25\separation) -- + node [pos=0.5, right=1.75\separation] { + pimpl idiom: only forward calls + } + (\x2+1.75\separation,\y2+.25\separation); + + + + \node (fcthandles) [right=50ex of cvodesolverimpl.east] { + \code{FunctionHandles::call(double t,} + \\ \code{~~~~double const*const y, double *const ydot)} + }; + + \node (fcthandlesimpl) [above left=16\separation and 0pt of fcthandles.north east] { + \code{FunctionHandlesImpl<N>::call(double t,} + \\ \code{~~~~double const*const y, double *const ydot)} + \\ \code{FunctionHandlesImpl<N>} has a member \code{\_f}, + \\ \quad which is a user-supplied \code{std::function<>}. + }; + + \node (fct) [above left=16\separation and 0pt of fcthandlesimpl.north east] { + call \code{\_f} with arguments \code{double t}, + \\ \quad\code{Eigen::Map<> const\& y} and \code{Eigen::Map<>\& ydot} + \\ \code{\_f} is the function the user sets with + \\ \quad\code{ODESolver::setFunction()} + }; + + \draw[->] let \p1=(fcthandles.north east), \p2=(fcthandlesimpl.south east) + in (\x1-1.75\separation,\y1+.25\separation) -- + node [pos=0.5, left=1.75\separation] { + dynamic polymorphism + } + (\x2-1.75\separation,\y2-.25\separation); + + \draw[->] let \p1=(fcthandlesimpl.north east), \p2=(fct.south east) + in (\x1-1.75\separation,\y1+.25\separation) -- + node [pos=0.5, left=1.75\separation] { + call \code{\_f} wrapping \code{double*} into \code{Eigen::Map<>} + } + (\x2-1.75\separation,\y2-.25\separation); + + \path[-] (cvodesolverimpl.east) edge[dotted] + node [pos=0.5, above=\separation, solid] { + \code{CVodeSolverImpl} has a member + \\ of type \code{FunctionHandles} + \\ whose \code{call()} method is called + \\ in order to compute $\dot y = f(t,y)$. + } + (fcthandles.west); + + \path let \p1=(current bounding box.west), \p2=(current bounding box.east) in + node (ogs6) [above=2ex of current bounding box.north, minimum width=\x2-\x1] + { \textbf{OGS6 side} }; + + \path let \p1=(current bounding box.west), \p2=(current bounding box.east) in + node (external) [below=2ex of current bounding box.south, minimum width=\x2-\x1] + { \textbf{external library side} }; + + +\end{tikzpicture} + +\end{document} diff --git a/Documentation/mainpage.dox b/Documentation/mainpage.dox index 6087b9782f9cf763124c4d15ea3e4f066b902a29..063eaccb9d130382ac0e33f448f147b7822e1842 100644 --- a/Documentation/mainpage.dox +++ b/Documentation/mainpage.dox @@ -14,5 +14,6 @@ * \section internal_modules Internal Modules * * * \subpage ODESolver + * * \subpage ExternalODESolverInterface * */ diff --git a/MathLib/CMakeLists.txt b/MathLib/CMakeLists.txt index a97ed510448c9db74449c9ee6a996f16f6ff5daf..bf640b887abedb211dfcf22c823f7264f1fba27c 100644 --- a/MathLib/CMakeLists.txt +++ b/MathLib/CMakeLists.txt @@ -23,24 +23,27 @@ set(SOURCES ${SOURCES} ${SOURCES_LINALG_SOLVERS}) GET_SOURCE_FILES(SOURCES_LINALG_PRECOND LinAlg/Preconditioner) set(SOURCES ${SOURCES} ${SOURCES_LINALG_PRECOND}) +GET_SOURCE_FILES(SOURCES_ODE ODE) +set(SOURCES ${SOURCES} ${SOURCES_ODE}) + if(OGS_USE_EIGEN) - GET_SOURCE_FILES(SOURCES_LINALG_EIGEN LinAlg/Eigen) - set(SOURCES ${SOURCES} ${SOURCES_LINALG_EIGEN}) + GET_SOURCE_FILES(SOURCES_LINALG_EIGEN LinAlg/Eigen) + set(SOURCES ${SOURCES} ${SOURCES_LINALG_EIGEN}) endif() if(OGS_USE_LIS) - GET_SOURCE_FILES(SOURCES_LINALG_LIS LinAlg/Lis) - set(SOURCES ${SOURCES} ${SOURCES_LINALG_LIS}) + GET_SOURCE_FILES(SOURCES_LINALG_LIS LinAlg/Lis) + set(SOURCES ${SOURCES} ${SOURCES_LINALG_LIS}) endif() if(OGS_USE_EIGEN AND OGS_USE_LIS) - GET_SOURCE_FILES(SOURCES_LINALG_EIGENLIS LinAlg/EigenLis) - set(SOURCES ${SOURCES} ${SOURCES_LINALG_EIGENLIS}) + GET_SOURCE_FILES(SOURCES_LINALG_EIGENLIS LinAlg/EigenLis) + set(SOURCES ${SOURCES} ${SOURCES_LINALG_EIGENLIS}) endif() if(OGS_USE_PETSC) - GET_SOURCE_FILES(SOURCES_LINALG_PETSC LinAlg/PETSc) - set(SOURCES ${SOURCES} ${SOURCES_LINALG_PETSC}) + GET_SOURCE_FILES(SOURCES_LINALG_PETSC LinAlg/PETSc) + set(SOURCES ${SOURCES} ${SOURCES_LINALG_PETSC}) endif() if(METIS_FOUND) @@ -64,6 +67,10 @@ target_link_libraries(MathLib AssemblerLib ) +if (CVODE_FOUND) + target_link_libraries(MathLib ${CVODE_LIBRARIES}) +endif() + if(METIS_FOUND) target_link_libraries(MathLib ${METIS_LIBRARIES}) endif() @@ -81,7 +88,7 @@ if (OGS_USE_PETSC) endif() if(TARGET Boost) - add_dependencies(MathLib Boost) + add_dependencies(MathLib Boost) endif() if(TARGET Eigen) add_dependencies(MathLib Eigen) diff --git a/MathLib/ODE/CVodeSolver.cpp b/MathLib/ODE/CVodeSolver.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5132cd5c6e12193e8f7be7ea35668b703f71f1eb --- /dev/null +++ b/MathLib/ODE/CVodeSolver.cpp @@ -0,0 +1,367 @@ +/** + * \copyright + * Copyright (c) 2012-2016, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#ifdef CVODE_FOUND + +#include "CVodeSolver.h" + +#include <cassert> +#include <logog/include/logog.hpp> + +#include <cvode/cvode.h> /* prototypes for CVODE fcts., consts. */ +#include <nvector/nvector_serial.h> /* serial N_Vector types, fcts., macros */ +#include <cvode/cvode_dense.h> /* prototype for CVDense */ +#include <sundials/sundials_dense.h> /* definitions DlsMat DENSE_ELEM */ +#include <sundials/sundials_types.h> /* definition of type realtype */ + +#include "BaseLib/ConfigTree.h" + +//! \addtogroup ExternalODESolverInterface +//! @{ + +/*! Checks Sundials error flags and aborts the program in case of error. + * + * \param f_name name of the function that returned the \c error_flag + * \param error_flag the error flag to be checked + */ +void check_error(std::string const& f_name, int const error_flag) +{ + if (error_flag != CV_SUCCESS) + { + ERR("CVodeSolver: %s failed with error flag %d.", f_name.c_str(), + error_flag); + std::abort(); + } +} + +//! Prints some statistics about an ODE solver run. +void printStats(void* cvode_mem) +{ + long int nst = 0, nfe = 0, nsetups = 0, nje = 0, nfeLS = 0, nni = 0, + ncfn = 0, netf = 0, nge = 0; + + check_error("CVodeGetNumSteps", CVodeGetNumSteps(cvode_mem, &nst)); + check_error("CVodeGetNumRhsEvals", CVodeGetNumRhsEvals(cvode_mem, &nfe)); + check_error("CVodeGetNumLinSolvSetups", + CVodeGetNumLinSolvSetups(cvode_mem, &nsetups)); + check_error("CVodeGetNumErrTestFails", + CVodeGetNumErrTestFails(cvode_mem, &netf)); + check_error("CVodeGetNumNonlinSolvIters", + CVodeGetNumNonlinSolvIters(cvode_mem, &nni)); + check_error("CVodeGetNumNonlinSolvConvFails", + CVodeGetNumNonlinSolvConvFails(cvode_mem, &ncfn)); + check_error("CVDlsGetNumJacEvals", CVDlsGetNumJacEvals(cvode_mem, &nje)); + check_error("CVDlsGetNumRhsEvals", CVDlsGetNumRhsEvals(cvode_mem, &nfeLS)); + check_error("CVodeGetNumGEvals", CVodeGetNumGEvals(cvode_mem, &nge)); + + DBUG("Sundials CVode solver. Statistics:"); + DBUG("nst = %-6ld nfe = %-6ld nsetups = %-6ld nfeLS = %-6ld nje = %ld", + nst, nfe, nsetups, nfeLS, nje); + DBUG("nni = %-6ld ncfn = %-6ld netf = %-6ld nge = %ld\n", nni, ncfn, + netf, nge); +} + +//! @} + +namespace MathLib +{ +namespace ODE +{ +//! \addtogroup ExternalODESolverInterface +//! @{ + +/*! This class provides concrete access to Sundials' CVode solver. + * + * This class is the implementation part in the pimpl idiom used by the + * CVodeSolver class. Therefore all if this classes methods are only forwarded + * from CVodeSolver. + */ +class CVodeSolverImpl final +{ + static_assert(std::is_same<realtype, double>::value, + "CVode's realtype is not the same as double"); + +public: + CVodeSolverImpl(BaseLib::ConfigTree const& config, + unsigned const num_equations); + + void setFunction(std::unique_ptr<detail::FunctionHandles>&& f); + + void preSolve(); + bool solve(const double t_end); + + double const* getSolution() const { return NV_DATA_S(_y); } + double getTime() const { return _t; } + void getYDot(const double t, double const* const y, double* const y_dot); + void setTolerance(const double* abstol, const double reltol); + void setTolerance(const double abstol, const double reltol); + void setIC(const double t0, double const* const y0); + + ~CVodeSolverImpl(); + +private: + N_Vector _y = nullptr; //!< The solution vector. + + realtype _t; //! current time + + N_Vector _abstol = nullptr; //!< Array of absolute tolerances. + realtype _reltol; //!< Relative tolerance + + unsigned _num_equations; //!< Number of equations in the ODE system. + void* _cvode_mem; //!< CVode's internal memory + + //! Function handles that compute \f$\partial \dot y/\partial y\f$ + //! and \f$\dot y\f$. + std::unique_ptr<detail::FunctionHandles> _f; + + //! The multistep method used for solving the ODE. + int _linear_multistep_method = CV_ADAMS; + + //! Either solve via fixed-point iteration or via Newton-Raphson method. + int _nonlinear_solver_iteration = CV_FUNCTIONAL; +}; + +//! @} + +CVodeSolverImpl::CVodeSolverImpl(const BaseLib::ConfigTree& config, + const unsigned num_equations) +{ + if (auto const param = + config.getConfParamOptional<std::string>("linear_multistep_method")) + { + DBUG("setting linear multistep method (config: %s)", param->c_str()); + + if (*param == "Adams") + { + _linear_multistep_method = CV_ADAMS; + } + else if (*param == "BDF") + { + _linear_multistep_method = CV_BDF; + } + else + { + ERR("unknown linear multistep method: %s", param->c_str()); + std::abort(); + } + } + + if (auto const param = config.getConfParamOptional<std::string>( + "nonlinear_solver_iteration")) + { + DBUG("setting nonlinear solver iteration (config: %s)", param->c_str()); + + if (*param == "Functional") + { + _nonlinear_solver_iteration = CV_FUNCTIONAL; + } + else if (*param == "Newton") + { + _nonlinear_solver_iteration = CV_NEWTON; + } + else + { + ERR("unknown nonlinear solver iteration: %s", param->c_str()); + std::abort(); + } + } + + _y = N_VNew_Serial(num_equations); + _abstol = N_VNew_Serial(num_equations); + _num_equations = num_equations; + + _cvode_mem = + CVodeCreate(_linear_multistep_method, _nonlinear_solver_iteration); + + if (_cvode_mem == nullptr || _y == nullptr || _abstol == nullptr) + { + ERR("couldn't allocate storage for CVode solver."); + std::abort(); + } + + auto f_wrapped = [](const realtype t, const N_Vector y, N_Vector ydot, + void* function_handles) -> int + { + bool successful = + static_cast<detail::FunctionHandles*>(function_handles) + ->call(t, NV_DATA_S(y), NV_DATA_S(ydot)); + return successful ? 0 : 1; + }; + + check_error("CVodeInit", CVodeInit(_cvode_mem, f_wrapped, 0.0, _y)); +} + +void CVodeSolverImpl::setTolerance(const double* abstol, const double reltol) +{ + for (unsigned i = 0; i < _num_equations; ++i) + { + NV_Ith_S(_abstol, i) = abstol[i]; + } + + _reltol = reltol; +} + +void CVodeSolverImpl::setTolerance(const double abstol, const double reltol) +{ + for (unsigned i = 0; i < _num_equations; ++i) + { + NV_Ith_S(_abstol, i) = abstol; + } + + _reltol = reltol; +} + +void CVodeSolverImpl::setFunction(std::unique_ptr<detail::FunctionHandles>&& f) +{ + _f = std::move(f); + assert(_num_equations == _f->getNumEquations()); +} + +void CVodeSolverImpl::setIC(const double t0, double const* const y0) +{ + for (unsigned i = 0; i < _num_equations; ++i) + { + NV_Ith_S(_y, i) = y0[i]; + } + + _t = t0; +} + +void CVodeSolverImpl::preSolve() +{ + assert(_f != nullptr && "ode function handle was not provided"); + + // sets initial conditions + check_error("CVodeReInit", CVodeReInit(_cvode_mem, _t, _y)); + + check_error("CVodeSetUserData", + CVodeSetUserData(_cvode_mem, static_cast<void*>(_f.get()))); + + /* Call CVodeSVtolerances to specify the scalar relative tolerance + * and vector absolute tolerances */ + check_error("CVodeSVtolerances", + CVodeSVtolerances(_cvode_mem, _reltol, _abstol)); + + /* Call CVDense to specify the CVDENSE dense linear solver */ + check_error("CVDense", CVDense(_cvode_mem, _num_equations)); + + if (_f->hasJacobian()) + { + auto df_wrapped = [](const long N, const realtype t, const N_Vector y, + const N_Vector ydot, const DlsMat jac, + void* function_handles, N_Vector /*tmp1*/, + N_Vector /*tmp2*/, N_Vector /*tmp3*/ + ) -> int + { + (void)N; // prevent warnings during non-debug build + auto* fh = static_cast<detail::FunctionHandles*>(function_handles); + assert(N == fh->getNumEquations()); + + // Caution: by calling the DENSE_COL() macro we assume that matrices + // are stored contiguously in memory! + // See also the header files sundials_direct.h and cvode_direct.h in + // the Sundials source code. The comments about the macro DENSE_COL + // in those files indicate that matrices are stored column-wise. + bool successful = fh->callJacobian(t, NV_DATA_S(y), NV_DATA_S(ydot), + DENSE_COL(jac, 0)); + return successful ? 0 : 1; + }; + + check_error("CVDlsSetDenseJacFn", + CVDlsSetDenseJacFn(_cvode_mem, df_wrapped)); + } +} + +bool CVodeSolverImpl::solve(const double t_end) +{ + realtype t_reached; + check_error("CVode solve", + CVode(_cvode_mem, t_end, _y, &t_reached, CV_NORMAL)); + _t = t_reached; + + // check_error asserts that t_end == t_reached and that solving the ODE + // went fine. Otherwise the program will be aborted. Therefore, we don't + // have to check manually for errors here and can always savely return true. + return true; +} + +void CVodeSolverImpl::getYDot(const double t, double const* const y, + double* const y_dot) +{ + assert(_f != nullptr); + _f->call(t, y, y_dot); +} + +CVodeSolverImpl::~CVodeSolverImpl() +{ + printStats(_cvode_mem); + + N_VDestroy_Serial(_y); + N_VDestroy_Serial(_abstol); + CVodeFree(&_cvode_mem); +} + +CVodeSolver::CVodeSolver(BaseLib::ConfigTree const& config, + unsigned const num_equations) + : _impl{new CVodeSolverImpl{config, num_equations}} +{ +} + +void CVodeSolver::setTolerance(const double* abstol, const double reltol) +{ + _impl->setTolerance(abstol, reltol); +} + +void CVodeSolver::setTolerance(const double abstol, const double reltol) +{ + _impl->setTolerance(abstol, reltol); +} + +void CVodeSolver::setFunction(std::unique_ptr<detail::FunctionHandles>&& f) +{ + _impl->setFunction(std::move(f)); +} + +void CVodeSolver::setIC(const double t0, double const* const y0) +{ + _impl->setIC(t0, y0); +} + +void CVodeSolver::preSolve() +{ + _impl->preSolve(); +} + +bool CVodeSolver::solve(const double t_end) +{ + return _impl->solve(t_end); +} + +double const* CVodeSolver::getSolution() const +{ + return _impl->getSolution(); +} + +void CVodeSolver::getYDot(const double t, double const* const y, + double* const y_dot) const +{ + _impl->getYDot(t, y, y_dot); +} + +double CVodeSolver::getTime() const +{ + return _impl->getTime(); +} + +CVodeSolver::~CVodeSolver() = default; + +} // namespace ODE +} // namespace MathLib + +#endif // CVODE_FOUND diff --git a/MathLib/ODE/CVodeSolver.h b/MathLib/ODE/CVodeSolver.h new file mode 100644 index 0000000000000000000000000000000000000000..c6f77b74500e0d548b6dbedb54580d57e6627581 --- /dev/null +++ b/MathLib/ODE/CVodeSolver.h @@ -0,0 +1,81 @@ +/** + * \copyright + * Copyright (c) 2012-2016, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#ifndef MATHLIB_CVODESOLVER_H +#define MATHLIB_CVODESOLVER_H + +#include <memory> + +#include "ODESolverTypes.h" +#include "FunctionHandles.h" + +namespace BaseLib +{ +class ConfigTree; +} + +namespace MathLib +{ +namespace ODE +{ +//! \addtogroup ExternalODESolverInterface +//! @{ + +class CVodeSolverImpl; + +/*! ODE solver interfacing with Sundials' CVode. + * + * All methods of this class have the same semantics as methods from the + * ODESolver interface with the same name. + * + * There is no implicit information about vector and matrix sizes in this class + * anymore, rather this class only handles raw pointers. But this is exactly + * what is necessary when using C libraries like Sundials. + * + * \note + * This class is for internal use only. Therefore all members of this class are + * protected, namely because ConcreteODESolver derives from this class. + */ +class CVodeSolver +{ +protected: + //! Construct from the given \c config with storage allocated for the given + //! \c num_equations. + CVodeSolver(BaseLib::ConfigTree const& config, + unsigned const num_equations); + + void setTolerance(double const* const abstol, const double reltol); + void setTolerance(const double abstol, const double reltol); + + void setFunction(std::unique_ptr<detail::FunctionHandles>&& f); + + void setIC(const double t0, double const* const y0); + + void preSolve(); + bool solve(const double t_end); + + double const* getSolution() const; + double getTime() const; + void getYDot(const double t, + double const* const y, + double* const y_dot) const; + + ~CVodeSolver(); + +private: + //! pimpl idiom. + std::unique_ptr<CVodeSolverImpl> _impl; +}; + +//! @}} + +} // namespace ODE +} // namespace MathLib + +#endif // MATHLIB_CVODESOLVER_H diff --git a/MathLib/ODE/ConcreteODESolver.h b/MathLib/ODE/ConcreteODESolver.h new file mode 100644 index 0000000000000000000000000000000000000000..169c02d9cba25e23d62a75afe10443656b99ebe8 --- /dev/null +++ b/MathLib/ODE/ConcreteODESolver.h @@ -0,0 +1,123 @@ +/** + * \copyright + * Copyright (c) 2012-2016, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#ifndef MATHLIB_ODE_CONCRETEODESOLVER_H +#define MATHLIB_ODE_CONCRETEODESOLVER_H + +#include <memory> + +#include "FunctionHandles.h" +#include "ODESolver.h" + +namespace BaseLib +{ +class ConfigTree; +} + +namespace MathLib +{ +namespace ODE +{ +template <unsigned NumEquations> +std::unique_ptr<ODESolver<NumEquations>> createODESolver( + BaseLib::ConfigTree const& config); + +//! \addtogroup ExternalODESolverInterface +//! @{ + +/*! ODE solver with a bounds-safe interface using a concrete implementation. + * + * This class makes contact between the abstract \c ODESolver interface and a + * certain solver \c Implementation. + * + * The interface of this class inherits the array bounds checking from \c + * ODESolver. + * Its methods forward calls to the \c Implementation erasing array bounds info + * by passing \c std::array and Eigen::Matrix arguments as raw pointers. + * + * Thus the \c Implementation does not need template to be templated + * which makes it possible for \c Implementation to use the pimpl idiom whereby + * the headers of external libraries only have to be included in the final cpp + * file. Thereby our namespaces do not get polluted with symbols from external + * libraries. + * + * \tparam NumEquations the number of equations in the ODE system. + * \tparam Implementation a concrete ODE solver implementation used as a + * backend. + */ +template <typename Implementation, unsigned NumEquations> +class ConcreteODESolver final : public ODESolver<NumEquations>, + private Implementation +{ +public: + void setFunction(Function<NumEquations> f, + JacobianFunction<NumEquations> df) override + { + Implementation::setFunction( + std::unique_ptr<detail::FunctionHandlesImpl<NumEquations>>{ + new detail::FunctionHandlesImpl<NumEquations>{f, df}}); + } + + void setTolerance(const std::array<double, NumEquations>& abstol, + const double reltol) override + { + Implementation::setTolerance(abstol.data(), reltol); + } + + void setTolerance(const double abstol, const double reltol) override + { + Implementation::setTolerance(abstol, reltol); + } + + void setIC(const double t0, + std::initializer_list<double> const& y0) override + { + assert(y0.size() == NumEquations); + Implementation::setIC(t0, y0.begin()); + } + + void setIC(const double t0, + Eigen::Matrix<double, NumEquations, 1, Eigen::ColMajor> const& + y0) override + { + Implementation::setIC(t0, y0.data()); + } + + void preSolve() override { Implementation::preSolve(); } + bool solve(const double t) override { return Implementation::solve(t); } + MappedConstVector<NumEquations> getSolution() const override + { + return MappedConstVector<NumEquations>{Implementation::getSolution()}; + } + double getTime() const override { return Implementation::getTime(); } + Eigen::Matrix<double, NumEquations, 1, Eigen::ColMajor> getYDot( + const double t, const MappedConstVector<NumEquations>& y) const override + { + Eigen::Matrix<double, NumEquations, 1, Eigen::ColMajor> y_dot; + Implementation::getYDot(t, y.data(), y_dot.data()); + return y_dot; + } + +private: + //! Instances of this class shall only be constructed by createODESolver(). + ConcreteODESolver(BaseLib::ConfigTree const& config) + : Implementation{config, NumEquations} + { + } + + friend std::unique_ptr<ODESolver<NumEquations>> + createODESolver<NumEquations>(BaseLib::ConfigTree const& config); +}; + +//! @} + +} // namespace ODE +} // namespace MathLib + +#endif // MATHLIB_ODE_CONCRETEODESOLVER_H diff --git a/MathLib/ODE/FunctionHandles.h b/MathLib/ODE/FunctionHandles.h new file mode 100644 index 0000000000000000000000000000000000000000..2e1bf60bd65f4bd77d7f494cb552b0321c1b0d8c --- /dev/null +++ b/MathLib/ODE/FunctionHandles.h @@ -0,0 +1,115 @@ +/** + * \copyright + * Copyright (c) 2012-2016, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#ifndef MATHLIB_ODE_HANDLES_H +#define MATHLIB_ODE_HANDLES_H + +#include "ODESolverTypes.h" + +namespace MathLib +{ +namespace ODE +{ +namespace detail +{ +//! \addtogroup ExternalODESolverInterface +//! @{ + +/*! Interface providing acces to functions computing \f$\dot y\f$ and its + * Jacobian to code interfacing with external ODE solver libraries. + * + * \note + * The methods of this class accept raw pointers as arguments, not Eigen::Matrix + * types. + */ +class FunctionHandles +{ +public: + //! Calls a function computing \f$\dot y\f$. + //! \returns true or false indicating whether the function succeeded. + virtual bool call(const double t, double const* const y, + double* const ydot) = 0; + + //! Calls a function computing \f$\mathtt{jac} := \partial \dot y/\partial + //! y\f$. + //! \returns true or false indicating whether the function succeeded. + virtual bool callJacobian(const double t, + double const* const y, + double* const ydot, + double* const jac) = 0; + + //! Tells whether a Jacobian function has been set. + virtual bool hasJacobian() const = 0; + + //! Returns the number of equations in the ODE system. + virtual unsigned getNumEquations() const = 0; + + virtual ~FunctionHandles() = default; +}; + +//! Function handles for an ODE system of \c N equations. +template <unsigned N> +struct FunctionHandlesImpl final : public FunctionHandles +{ + FunctionHandlesImpl(Function<N>& f, JacobianFunction<N>& df) : f(f), df(df) + { + } + + /*! Calls the stored function \c f computing \f$\dot y\f$. + * + * The raw pointers passed to this method are wrapped in some Eigen::Map + * objects before being passed to \c f. Thereby the information about the + * size of the vectors is restored. No memory is copied for that. + * + * \returns true or false indicating whether the function succeeded. + */ + bool call(const double t, const double* const y, + double* const ydot) override + { + if (f) + { + MappedVector<N> ydot_mapped{ydot}; + return f(t, MappedConstVector<N>{y}, ydot_mapped); + } + return false; + } + + /*! Calls the stored function computing + * \f$\mathtt{jac} := \partial \dot y/\partial y\f$. + * + * \returns true or false indicating whether the function succeeded. + * \see call() + */ + bool callJacobian(const double t, const double* const y, double* const ydot, + double* const jac) override + { + if (df) + { + MappedMatrix<N, N> jac_mapped{jac}; + return df(t, + MappedConstVector<N>{y}, + MappedConstVector<N>{ydot}, + jac_mapped); + } + return false; + } + + bool hasJacobian() const override { return df != nullptr; } + unsigned getNumEquations() const override { return N; } + Function<N> f; + JacobianFunction<N> df; +}; + +//! @} + +} // namespace detail +} // namespace ODE +} // namespace MathLib + +#endif // MATHLIB_ODE_HANDLES_H diff --git a/MathLib/ODE/ODESolver.dox b/MathLib/ODE/ODESolver.dox new file mode 100644 index 0000000000000000000000000000000000000000..5e8112c208d3ce75d30ec8bc357135c1cff4f045 --- /dev/null +++ b/MathLib/ODE/ODESolver.dox @@ -0,0 +1,26 @@ +/*! \defgroup ExternalODESolverInterface Interface to external ODE Solver Libraries + +The purpose of these classes and functions is to provide a general interface to +external ODE solver libraries. The interface is designed s.t. it can be used +without having to know which particular external library will be used to solve +the ODE. Furthermore all user-side (or OGS6-side) computations are done using +Eigen types. These types are convenient to use and provide bounds checking, even +at compile-time. + +The inheritance and cooperation between the classes involved when solving an ODE +is depicted in the image below. The user side is at the top, the external +library side at the bottom. Sundials' CVode solver has been chosen as an example +since it is the first external solver we use. + +The path on the left side is essentially the way data coming from the user takes +towards the ODE solver backend. On that way Eigen types are stripped to raw +pointers (<tt>double*</tt>). + +The path on the right side is the way data coming from the ODE solver backend +takes towards the functions provided by the user. On that way raw pointers are +wrapped into some <tt>Eigen::Map<></tt>s. + +\image html external-ode-solver-concept.png "Inheritance/Cooperation between different classes, and data flow when computing y_dot = f(t,y)." +\image latex external-ode-solver-concept.pdf "Inheritance/Cooperation between different classes, and data flow when computing y_dot = f(t,y)." + +*/ diff --git a/MathLib/ODE/ODESolver.h b/MathLib/ODE/ODESolver.h new file mode 100644 index 0000000000000000000000000000000000000000..e951a1f625914725eb1e32c48a5439347a3bb040 --- /dev/null +++ b/MathLib/ODE/ODESolver.h @@ -0,0 +1,140 @@ +/** + * \copyright + * Copyright (c) 2012-2016, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#ifndef MATHLIB_ODESOLVER_H +#define MATHLIB_ODESOLVER_H + +#include <array> + +#include "ODESolverTypes.h" + +namespace MathLib +{ +namespace ODE +{ +//! \addtogroup ExternalODESolverInterface +//! @{ + +/** + * ODE solver Interface. + * + * This class provides an abstract interface for ODE solvers. + * It provides type-safe and array-bounds checked access to external + * ODE solver libraries. However, it is agnostic to the specific solver used. + * + * The ODEs solved using this class are first-order ODEs that are given in + * explicit form, i.e. \f[ \dot y = f(t, y). \f] + * + * \tparam NumEquations number of equations in the ODE system. + */ +template <unsigned NumEquations> +class ODESolver +{ +public: + /*! Sets functions that compute \f$\dot y\f$ and + * the Jacobian \f$\partial \dot y/\partial y\f$. + * + * If no Jacobian function shall be set, \c nullptr can be passed fo \c df. + * + * \remark + * solve() cannot be directly called after this method, rather preSolve() + * has to be called first! + */ + virtual void setFunction(Function<NumEquations> f, + JacobianFunction<NumEquations> df) = 0; + + /*! Sets the tolerances for the ODE solver. + * + * \param abstol absolute tolerance, one value for all equations. + * \param reltol relative tolerance. + * + * \remark + * solve() cannot be directly called after this method, rather preSolve() + * has to be called first! + */ + virtual void setTolerance(const double abstol, const double reltol) = 0; + + /*! Sets the tolerances for the ODE solver. + * + * \param abstol absolute tolerance, one value each equation. + * \param reltol relative tolerance. + * + * \remark + * solve() cannot be directly called after this method, rather preSolve() + * has to be called first! + */ + virtual void setTolerance(const std::array<double, NumEquations>& abstol, + const double reltol) = 0; + + /*! Sets the conditions. + * + * \param t0 initial time. + * \param y0 initial values. + * + * \remark + * solve() cannot be directly called after this method, rather preSolve() + * has to be called first! + */ + virtual void setIC(const double t0, + std::initializer_list<double> const& y0) = 0; + + //! \overload + virtual void setIC( + const double t0, + Eigen::Matrix<double, NumEquations, 1, Eigen::ColMajor> const& y0) = 0; + + /*! Finishes setting up the ODE solver, makes it ready to solve the provided + * ODE. + * + * This method applies settings to the ODE solver, hence it has to be called + * after calling setters. + * + * \note + * preSolve() has to be called once before calling solve, it is not + * necessary + * to call it after each setter. + */ + virtual void preSolve() = 0; + + /*! Solves the ODE from the set initial condition to time \c t. + * + * \returns true or false indicating whether solving succeeded. + * + * \pre preSolve() has to be called before this method. + */ + virtual bool solve(const double t) = 0; + + //! Returns the number of equations. + virtual unsigned getNumEquations() const { return NumEquations; } + //! Returns the solution vector \c y + virtual MappedConstVector<NumEquations> getSolution() const = 0; + + /*! Returns the time that the solver has reached. + * + * The return value should be equal to the time \c t passed to solve() if + * everything went fine. + */ + virtual double getTime() const = 0; + + /*! Computes \f$ \dot y = f(t,y) \f$. + * + * This method is provided for convenience only. + */ + virtual Eigen::Matrix<double, NumEquations, 1, Eigen::ColMajor> getYDot( + const double t, const MappedConstVector<NumEquations>& y) const = 0; + + virtual ~ODESolver() = default; +}; + +//! @} + +} // namespace ODE +} // namespace MathLib + +#endif // MATHLIB_ODESOLVER_H diff --git a/MathLib/ODE/ODESolverBuilder.h b/MathLib/ODE/ODESolverBuilder.h new file mode 100644 index 0000000000000000000000000000000000000000..88212814c972e98642030191d7a7904546b5a31b --- /dev/null +++ b/MathLib/ODE/ODESolverBuilder.h @@ -0,0 +1,59 @@ +/** + * \copyright + * Copyright (c) 2012-2016, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#ifndef MATHLIB_ODE_ODESOLVERBUILDER_H +#define MATHLIB_ODE_ODESOLVERBUILDER_H + +#include <logog/include/logog.hpp> + +#include "ODESolver.h" +#include "ConcreteODESolver.h" + +#ifdef CVODE_FOUND +#include "CVodeSolver.h" +#endif + +namespace BaseLib +{ +class ConfigTree; +} + +namespace MathLib +{ +namespace ODE +{ +//! \addtogroup ExternalODESolverInterface +//! @{ + +/*! Creates a new ODESolver instance from the given \c config. + * + * \tparam NumEquations the number of equations in the ODE system to be solved. + */ +template <unsigned NumEquations> +std::unique_ptr<ODESolver<NumEquations>> createODESolver( + BaseLib::ConfigTree const& config) +{ +#ifdef CVODE_FOUND + return std::unique_ptr<ODESolver<NumEquations>>( + new ConcreteODESolver<CVodeSolver, NumEquations>(config)); +#endif + (void)config; // Unused parameter warning if no library is available. + + ERR( + "No ODE solver could be created. Maybe it is because you did not build" + " OGS6 with support for any external ODE solver library."); + std::abort(); +} + +//! @} + +} // namespace ODE +} // namespace MathLib + +#endif // MATHLIB_ODE_ODESOLVERBUILDER_H diff --git a/MathLib/ODE/ODESolverTypes.h b/MathLib/ODE/ODESolverTypes.h new file mode 100644 index 0000000000000000000000000000000000000000..2fbfbc9530dd159000931e5a79c967244faa8113 --- /dev/null +++ b/MathLib/ODE/ODESolverTypes.h @@ -0,0 +1,74 @@ +/** + * \copyright + * Copyright (c) 2012-2016, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#ifndef MATHLIB_ODE_ODESOLVERTYPES_H +#define MATHLIB_ODE_ODESOLVERTYPES_H + +#include <functional> +#include <Eigen/Core> + +namespace MathLib +{ +namespace ODE +{ +//! \addtogroup ExternalODESolverInterface +//! @{ + +/*! This type can be used like an \f$N \times M\f$ Eigen::Matrix, but it + * does not manage the storage for its entries on its own. + * + * \tparam N number of rows + * \tparam M number of columns + * + * \see https://eigen.tuxfamily.org/dox/classEigen_1_1Map.html + */ +template <int N, int M> +using MappedMatrix = Eigen::Map<Eigen::Matrix<double, N, M, Eigen::ColMajor>>; + +//! Behaves like a \c const Eigen::Matrix. \see MappedMatrix +template <int N, int M> +using MappedConstMatrix = + Eigen::Map<const Eigen::Matrix<double, N, M, Eigen::ColMajor>>; + +//! \see MappedMatrix +template <int N> +using MappedVector = MappedMatrix<N, 1>; + +//! \see MappedConstMatrix +template <int N> +using MappedConstVector = MappedConstMatrix<N, 1>; + +/*! A function computing \c ydot as a function of \c t and \c y. + * + * The function returns true or false indecating whether it succeeded. + * + * \tparam N the number of equations in the ODE system. + */ +template <unsigned N> +using Function = std::function<bool( + const double t, MappedConstVector<N> const& y, MappedVector<N>& ydot)>; + +/*! A function computing \f$\mathtt{jac} := \partial \dot y/\partial y\f$. + * + * The function returns true or false indecating whether it succeeded. + * + * \tparam N the number of equations in the ODE system. + */ +template <unsigned N> +using JacobianFunction = std::function<bool(const double t, + MappedConstVector<N> const& y, + MappedConstVector<N> const& ydot, + MappedMatrix<N, N>& jac)>; + +//! @} + +} // namespace ODE +} // namespace MathLib + +#endif // MATHLIB_ODE_ODESOLVERTYPES_H diff --git a/Tests/MathLib/TestODESolver.cpp b/Tests/MathLib/TestODESolver.cpp new file mode 100644 index 0000000000000000000000000000000000000000..29b79a3413a34cf0b504fe87f4926db25ff7bc29 --- /dev/null +++ b/Tests/MathLib/TestODESolver.cpp @@ -0,0 +1,294 @@ +/** + * \copyright + * Copyright (c) 2012-2016, OpenGeoSys Community (http://www.opengeosys.org) + * Distributed under a Modified BSD License. + * See accompanying file LICENSE.txt or + * http://www.opengeosys.org/project/license + * + */ + +#include <gtest/gtest.h> +#include <logog/include/logog.hpp> + +#include "BaseLib/ConfigTree.h" +#include "MathLib/ODE/ODESolverBuilder.h" + +const double abs_tol = 1e-8; +const double rel_tol = 1e-8; + +bool f(const double, + MathLib::ODE::MappedConstVector<1> const& y, + MathLib::ODE::MappedVector<1>& ydot) +{ + if (y[0] <= 0.0) return false; + + ydot[0] = -15.0 * y[0]; + return true; +} + +bool df(const double /*t*/, + MathLib::ODE::MappedConstVector<1> const& y, + MathLib::ODE::MappedConstVector<1> const& /*ydot*/, + MathLib::ODE::MappedMatrix<1, 1>& jac) +{ + if (y[0] <= 0.0) return false; + + jac(0, 0) = -15.0; + return true; +} + +struct ExtraData +{ + double value = 12.5; +}; + +bool f_extra(const double, + MathLib::ODE::MappedConstVector<1> const& y, + MathLib::ODE::MappedVector<1>& ydot, + ExtraData& data) +{ + if (y[0] <= 0.0) return false; + + ydot[0] = -data.value * y[0]; + return true; +} + +bool any_ode_solver_libs_available() +{ +#ifdef CVODE_FOUND + return true; +#else + return false; +#endif // CVODE_FOUND +} + +template <unsigned NumEquations> +std::unique_ptr<MathLib::ODE::ODESolver<NumEquations>> make_ode_solver( + boost::property_tree::ptree const& conf) +{ + // Make sure testrunner does not crash if we haven't built with support for + // any external ODE solver lib. + if (!any_ode_solver_libs_available()) + { + ERR( + "I cannot create any ODE solver. This test therefore might be " + "skipped."); + return nullptr; + } + + BaseLib::ConfigTree config(conf, "", BaseLib::ConfigTree::onerror, + BaseLib::ConfigTree::onwarning); + return MathLib::ODE::createODESolver<NumEquations>(config); +} + +// There is no definition of this function in order to prevent passing temporary +// property trees! There will be linker errors if you do so anyway. +template <unsigned NumEquations> +std::unique_ptr<MathLib::ODE::ODESolver<NumEquations>> make_ode_solver( + boost::property_tree::ptree&&); + +void check(const double time_reached, const double y, const double y_dot, + const double time, const double y_ana, const double y_dot_ana) +{ + DBUG("t: %14.7g, y: %14.7g, diff: %14.7g, y_dot: %14.7g, diff: %14.7g", + time_reached, y, y - y_ana, y_dot, y_dot - y_dot_ana); + (void)y_dot_ana; // Avoid unused variable warning when DBUG output is + // disabled. + + EXPECT_NEAR(time, time_reached, std::numeric_limits<double>::epsilon()); + + auto const abs_err = std::abs(y - y_ana); + auto const rel_err = abs_err / std::abs(y_ana); + EXPECT_GT(10.0 * abs_tol, abs_err); + + // relative errors are not checked. + // EXPECT_GT(10.0*rel_tol, rel_err); + + // make sure that the relative error of the derivative is not much bigger + // than the one of the solution y. + auto const dot_rel_err = std::abs(y_dot / y_ana - 1.0); + EXPECT_LT(10.0 * rel_err, dot_rel_err); +} + +TEST(MathLibCVodeTest, Exponential) +{ + // initial values + const double y0 = 1.0; + const double t0 = 0.0; + + auto tree = boost::property_tree::ptree{}; + auto ode_solver = make_ode_solver<1>(tree); + + ASSERT_EQ(any_ode_solver_libs_available(), !!ode_solver); + // Don't run the test if the ODE solver could not be constructed. + if (!ode_solver) return; + + ode_solver->setFunction(f, nullptr); + ode_solver->setTolerance(abs_tol, rel_tol); + + ode_solver->setIC(t0, {y0}); + + ode_solver->preSolve(); + + const double dt = 1e-1; + + for (unsigned i = 1; i <= 10; ++i) + { + const double time = dt * i; + + ASSERT_TRUE(ode_solver->solve(time)); + + auto const y = ode_solver->getSolution(); + auto const time_reached = ode_solver->getTime(); + auto const y_dot = ode_solver->getYDot(time_reached, y); + + auto const y_ana = exp(-15.0 * time); + auto const y_dot_ana = -15.0 * exp(-15.0 * time); + + check(time_reached, y[0], y_dot[0], time, y_ana, y_dot_ana); + } +} + +TEST(MathLibCVodeTest, ExponentialExtraData) +{ + // initial values + const double y0 = 1.0; + const double t0 = 0.0; + + auto tree = boost::property_tree::ptree{}; + auto ode_solver = make_ode_solver<1>(tree); + + ASSERT_EQ(any_ode_solver_libs_available(), !!ode_solver); + // Don't run the test if the ODE solver could not be constructed. + if (!ode_solver) return; + + ExtraData data; + auto f_lambda = [&](double t, + MathLib::ODE::MappedConstVector<1> const& y, + MathLib::ODE::MappedVector<1>& ydot) + { + return f_extra(t, y, ydot, data); + }; + + ode_solver->setFunction(f_lambda, nullptr); + + ode_solver->setTolerance(abs_tol, rel_tol); + ode_solver->setIC(t0, {y0}); + ode_solver->preSolve(); + + const double dt = 1e-1; + + for (unsigned i = 1; i <= 10; ++i) + { + const double time = dt * i; + + ASSERT_TRUE(ode_solver->solve(time)); + + auto const y = ode_solver->getSolution(); + auto const time_reached = ode_solver->getTime(); + auto const y_dot = ode_solver->getYDot(time_reached, y); + + auto const y_ana = exp(-data.value * time); + auto const y_dot_ana = -data.value * exp(-data.value * time); + + check(time_reached, y[0], y_dot[0], time, y_ana, y_dot_ana); + } + + ode_solver->setFunction(f_lambda, nullptr); + ode_solver->preSolve(); + for (unsigned i = 11; i <= 15; ++i) + { + const double time = dt * i; + + ASSERT_TRUE(ode_solver->solve(time)); + + auto const y = ode_solver->getSolution(); + auto const time_reached = ode_solver->getTime(); + auto const y_dot = ode_solver->getYDot(time_reached, y); + + auto const y_ana = exp(-data.value * time); + auto const y_dot_ana = -data.value * exp(-data.value * time); + + check(time_reached, y[0], y_dot[0], time, y_ana, y_dot_ana); + } +} + +TEST(MathLibCVodeTest, ExponentialWithJacobian) +{ + // initial values + const double y0 = 1.0; + const double t0 = 0.0; + + auto tree = boost::property_tree::ptree{}; + auto ode_solver = make_ode_solver<1>(tree); + + ASSERT_EQ(any_ode_solver_libs_available(), !!ode_solver); + // Don't run the test if the ODE solver could not be constructed. + if (!ode_solver) return; + + ode_solver->setFunction(f, df); + ode_solver->setTolerance(abs_tol, rel_tol); + + ode_solver->setIC(t0, {y0}); + + ode_solver->preSolve(); + + const double dt = 1e-1; + + for (unsigned i = 1; i <= 10; ++i) + { + const double time = dt * i; + + ASSERT_TRUE(ode_solver->solve(time)); + + auto const y = ode_solver->getSolution(); + auto const time_reached = ode_solver->getTime(); + auto const y_dot = ode_solver->getYDot(time_reached, y); + + auto const y_ana = exp(-15.0 * time); + auto const y_dot_ana = -15.0 * exp(-15.0 * time); + + check(time_reached, y[0], y_dot[0], time, y_ana, y_dot_ana); + } +} + +TEST(MathLibCVodeTest, ExponentialWithJacobianNewton) +{ + // initial values + const double y0 = 1.0; + const double t0 = 0.0; + + boost::property_tree::ptree tree; + tree.put("linear_multistep_method", "BDF"); + tree.put("nonlinear_solver_iteration", "Newton"); + auto ode_solver = make_ode_solver<1>(tree); + + ASSERT_EQ(any_ode_solver_libs_available(), !!ode_solver); + // Don't run the test if the ODE solver could not be constructed. + if (!ode_solver) return; + + ode_solver->setFunction(f, df); + ode_solver->setTolerance(abs_tol, rel_tol); + + ode_solver->setIC(t0, {y0}); + + ode_solver->preSolve(); + + const double dt = 1e-1; + + for (unsigned i = 1; i <= 10; ++i) + { + const double time = dt * i; + + ASSERT_TRUE(ode_solver->solve(time)); + + auto const y = ode_solver->getSolution(); + auto const time_reached = ode_solver->getTime(); + auto const y_dot = ode_solver->getYDot(time_reached, y); + + auto const y_ana = exp(-15.0 * time); + auto const y_dot_ana = -15.0 * exp(-15.0 * time); + + check(time_reached, y[0], y_dot[0], time, y_ana, y_dot_ana); + } +} diff --git a/scripts/cmake/Find.cmake b/scripts/cmake/Find.cmake index af7836aa528a1e99ef57ac16d94127cf311642a6..84d163bdf6d2695f7b34232def9002c375b72c20 100644 --- a/scripts/cmake/Find.cmake +++ b/scripts/cmake/Find.cmake @@ -146,3 +146,9 @@ if(Shapelib_FOUND) elseif(OGS_BUILD_GUI) message(FATAL_ERROR "Shapelib not found but it is required for OGS_BUILD_GUI!") endif() + +## Sundials cvode ode-solver library +find_package(CVODE) +if(CVODE_FOUND) + add_definitions(-DCVODE_FOUND) +endif() # CVODE_FOUND diff --git a/scripts/cmake/cmake/FindCVODE.cmake b/scripts/cmake/cmake/FindCVODE.cmake new file mode 100644 index 0000000000000000000000000000000000000000..78d4b50965400c75d07848a46eac42d83e288067 --- /dev/null +++ b/scripts/cmake/cmake/FindCVODE.cmake @@ -0,0 +1,45 @@ +# Tries to find Sundials CVODE. +# +# This module will define the following variables: +# CVODE_INCLUDE_DIRS - Location of the CVODE includes +# CVODE_FOUND - true if CVODE was found on the system +# CVODE_LIBRARIES - Required libraries for all requested components +# +# This module accepts the following environment or CMake vars +# CVODE_ROOT - Install location to search for + +include(FindPackageHandleStandardArgs) + +if(NOT "$ENV{CVODE_ROOT}" STREQUAL "" OR NOT "${CVODE_ROOT}" STREQUAL "") + list(APPEND CMAKE_INCLUDE_PATH "$ENV{CVODE_ROOT}" "${CVODE_ROOT}") + list(APPEND CMAKE_LIBRARY_PATH "$ENV{CVODE_ROOT}" "${CVODE_ROOT}") +endif() + +find_path(CVODE_INCLUDE_DIRS sundials_types.h + ENV CVODE_ROOT + PATH_SUFFIXES include include/sundials +) + +find_library(CVODE_LIBRARY + NAMES sundials_cvode + ENV CVODE_ROOT + PATH_SUFFIXES lib Lib +) + +find_library(CVODE_NVECSERIAL + NAMES sundials_nvecserial + ENV CVODE_ROOT + PATH_SUFFIXES lib Lib +) + +find_package_handle_standard_args(CVODE DEFAULT_MSG + CVODE_LIBRARY + CVODE_NVECSERIAL + CVODE_INCLUDE_DIRS +) + +if(CVODE_FOUND) + set(CVODE_LIBRARIES sundials_cvode sundials_nvecserial) +endif() + +mark_as_advanced(CVODE_INCLUDE_DIRS CVODE_LIBRARY CVODE_NVECSERIAL)