Source code for mathematicskit.integrators.bvp

"""Two-point boundary-value problems by collocation.

:func:`collocation_bvp` wraps :func:`scipy.integrate.solve_bvp`, a 4th-order
collocation method (3-stage Lobatto IIIA, equivalently a cubic
C1-continuous spline satisfying the ODE at each interval's endpoints and
midpoint) with residual-based mesh refinement (Kierzenka & Shampine 2001).
Unlike an initial-value integrator, the whole solution is found at once:
the collocation conditions and boundary conditions form one nonlinear
system solved by damped Newton iterations, so a nonlinear BVP may have
several solutions, and which one is found depends on the initial guess.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass

import numpy as np
from numpy.typing import ArrayLike, NDArray
from scipy.integrate import solve_bvp

from mathematicskit.integrators.fixed_step import RHSFunc

__all__ = ["BVPResult", "collocation_bvp"]

#: Boundary-condition residual ``bc(state_a, state_b, params) -> ndarray``,
#: zero when the solution's endpoint states satisfy the conditions.
BCFunc = Callable[[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]], ArrayLike]


[docs] @dataclass class BVPResult: """Solution of a two-point boundary-value problem (see :func:`collocation_bvp`).""" x: NDArray[np.float64] """ndarray, shape (n_nodes,): Final (refined) mesh.""" y: NDArray[np.float64] """ndarray, shape (n_nodes, dim): Solution at the mesh nodes.""" sol: Callable[[ArrayLike], NDArray[np.float64]] """callable: ``sol(x) -> ndarray (len(x), dim)``, the continuous cubic-spline solution evaluated anywhere in the interval.""" success: bool """bool: Whether the requested tolerance was met.""" message: str """str: Solver termination message.""" max_rms_residual: float """float: Largest per-interval RMS relative residual of the collocation spline."""
[docs] def collocation_bvp( rhs: RHSFunc, bc: BCFunc, x: ArrayLike, state_guess: ArrayLike, params: NDArray[np.float64], tol: float = 1e-6, max_nodes: int = 10_000, ) -> BVPResult: """Solve ``state' = rhs(state, x, params)`` subject to ``bc(state(a), state(b)) = 0``. Parameters ---------- rhs : callable Right-hand side ``rhs(state, x, params) -> ndarray (dim,)``, in the same convention as the initial-value integrators (an ``@njit`` function or a plain Python one). bc : callable Boundary residual ``bc(state_a, state_b, params) -> ndarray (dim,)``. x : array_like of float, shape (m,) Initial mesh, strictly increasing, from ``a`` to ``b``. state_guess : array_like of float, shape (m, dim) Initial guess for the solution at each mesh node. params : ndarray of float Parameter vector passed through to `rhs` and `bc`. tol : float, default=1e-6 Target relative collocation residual. max_nodes : int, default=10000 Mesh-refinement cap. Returns ------- BVPResult The solution; check ``success`` before trusting it. Examples -------- ``y'' = -y`` with ``y(0) = 0``, ``y(pi/2) = 1`` has solution ``sin x``: >>> import numpy as np >>> def rhs(state, x, params): ... return np.array([state[1], -state[0]]) >>> def bc(ya, yb, params): ... return np.array([ya[0], yb[0] - 1.0]) >>> x = np.linspace(0.0, np.pi / 2, 11) >>> res = collocation_bvp(rhs, bc, x, np.zeros((11, 2)), np.zeros(0)) >>> res.success, bool(np.allclose(res.sol([0.5])[0, 0], np.sin(0.5), atol=1e-6)) (True, True) """ def fun(xs: NDArray[np.float64], ys: NDArray[np.float64]) -> NDArray[np.float64]: rows = np.ascontiguousarray(ys.T) return np.column_stack([rhs(rows[i], xs[i], params) for i in range(xs.size)]) sol = solve_bvp( fun, lambda ya, yb: np.asarray(bc(ya, yb, params), dtype=np.float64), np.asarray(x, dtype=np.float64), np.asarray(state_guess, dtype=np.float64).T, tol=tol, max_nodes=max_nodes, ) return BVPResult( x=sol.x, y=sol.y.T, sol=lambda xq: sol.sol(np.atleast_1d(np.asarray(xq, dtype=np.float64))).T, success=bool(sol.success), message=sol.message, max_rms_residual=float(np.max(sol.rms_residuals)), )