Source code for assetlife.quadratures

from collections.abc import Callable
from typing import TypeAlias

import numpy as np
import optype.numpy as onp

from assetlife.typing import CoercibleFloat64_ND, Float64_ND

__all__ = [
    "laguerre_quadrature",
    "legendre_quadrature",
    "unweighted_laguerre_quadrature",
]

ST: TypeAlias = int | float
NumpyST: TypeAlias = np.floating | np.uint


def _control_bounds(*bounds: CoercibleFloat64_ND) -> None:
    for bound in bounds:
        if np.any(bound < 0):
            raise ValueError("Bound values of the integral can't be lower than 0")


[docs] def legendre_quadrature( func: Callable[[CoercibleFloat64_ND], Float64_ND], lower_bound: CoercibleFloat64_ND, upper_bound: CoercibleFloat64_ND, args: tuple[object, ...] = (), deg: int = 10, ) -> np.float64 | onp.ArrayND[np.float64]: r"""Numerical integration of :math:`f(x)` over the interval :math:`[a,b]` Parameters ---------- func : Callable A function of the form `y = func(x, a, b, c, ...)` taking floats or ndarrays as inputs and returning a np.float64 or an ndarray. `a, b, c, ...` are extra arguments that must be passed in the `args` parameter. lower_bound : float or ndarray The lower bound of the integration. upper_bound : float or ndarray The upper bound of the integration. Can't be `np.inf`. args : any Extra arguments used in the function call. deg : int, default is 10. Number of sample points and weights for the quadrature Notes ----- The function computes if `y = func(x, a, b, c, ...)` has a compatible shape with `lower_bound` and `upper_bound` for `x.shape == np.broadcast_shapes(lower_bound, upper_bound)`. Otherwise, adapt `lower_bound`, `upper_bound` or change `func` implementation. Returns ------- out : np.float64 or np.ndarray The output shape corresponds to a broadcast between `a`, `b` and `*args`. """ lower_bound = np.float64(lower_bound) # (*a.shape,) upper_bound = np.float64(upper_bound) # (*b.shape,) _control_bounds(lower_bound, upper_bound) lower_bound, upper_bound, *_ = np.broadcast_arrays( lower_bound, upper_bound, *(np.asarray(arg) for arg in args) ) # (*shape,) if np.any(upper_bound == np.inf): raise ValueError("Bound values of Legendre quadrature must be finite") if np.any(lower_bound > upper_bound): raise ValueError("Bound values a must be lower than values of b") x, w = np.polynomial.legendre.leggauss(deg) # (deg,) x = np.expand_dims( x, axis=tuple(range(1, max(2, lower_bound.ndim + 1))) ) # (deg, 1, ..., 1) w = np.expand_dims( w, axis=tuple(range(1, max(2, lower_bound.ndim + 1))) ) # (deg, 1, ..., 1) p = (upper_bound - lower_bound) / 2 # (*shape,) m = (lower_bound + upper_bound) / 2 # (*shape,) u = p * x + m # (deg, *shape) v = p * w # (deg, *shape) fvalues = func(u) # (deg, *shape) return np.sum(v * fvalues, axis=0).reshape(lower_bound.shape) # (*shape,)
[docs] def laguerre_quadrature( func: Callable[[CoercibleFloat64_ND], Float64_ND], lower_bound: CoercibleFloat64_ND, args: tuple[object, ...] = (), deg: int = 10, ) -> np.float64 | onp.ArrayND[np.float64]: r"""Numerical integration of :math:`f(x) * exp(-x)` over the interval :math:`[a, \infty]`. Parameters ---------- func : Callable A function of the form `y = func(x, a, b, c, ...)` taking floats or ndarrays as inputs and returning a np.float64 or an ndarray. `a, b, c, ...` are extra arguments that must be passed in the `args` parameter. lower_bound : float or ndarray The lower bound of the integration. args : float or ndarray Extra arguments used in the function call. deg : int, default is 10. Number of sample points and weights for the quadrature Notes ----- The function computes if `y = func(x, a, b, c, ...)` has a compatible shape with `lower_bound`. Otherwise, adapt `lower_bound` or change `func` implementation. Returns ------- out : np.float64 or np.ndarray """ lower_bound = np.float64(lower_bound) _control_bounds(lower_bound) lower_bound, *_ = np.broadcast_arrays( lower_bound, *(np.asarray(arg) for arg in args) ) # (*shape,) x, w = np.polynomial.laguerre.laggauss(deg) # (deg,) x = np.expand_dims( x, axis=tuple(range(1, max(2, lower_bound.ndim + 1))) ) # (deg, 1, ..., 1) w = np.expand_dims( w, axis=tuple(range(1, max(2, lower_bound.ndim + 1))) ) # (deg, 1, ..., 1) fvalues = func(x + lower_bound) # (deg, *shape) exp_a = np.where( np.exp(-np.float64(lower_bound)) == 0, 1.0, np.exp(-np.float64(lower_bound)) ) # (*shape,) return np.sum(w * fvalues * exp_a, axis=0).reshape(lower_bound.shape) # (*shape,)
[docs] def unweighted_laguerre_quadrature( func: Callable[[CoercibleFloat64_ND], Float64_ND], lower_bound: CoercibleFloat64_ND, args: tuple[object, ...] = (), deg: int = 10, ) -> np.float64 | onp.ArrayND[np.float64]: r"""Numerical integration of :math:`f(x)` over the interval :math:`[a, \infty]` Parameters ---------- func : Callable A function of the form `y = func(x, a, b, c, ...)` taking floats or ndarrays as inputs and returning a np.float64 or an ndarray. `a, b, c, ...` are extra arguments that must be passed in the `args` parameter. lower_bound : float or ndarray The lower bound of the integration. args : float or ndarray Extra arguments used in the function call. deg : int, default is 10. Number of sample points and weights for the quadrature Notes ----- The function computes if `y = func(x, a, b, c, ...)` has a compatible shape with `lower_bound`. Otherwise, adapt `lower_bound` or change `func` implementation. Returns ------- out : np.float64 or np.ndarray """ lower_bound = np.float64(lower_bound) _control_bounds(lower_bound) lower_bound, *_ = np.broadcast_arrays( lower_bound, *(np.asarray(arg) for arg in args) ) # (*shape,) x, w = np.polynomial.laguerre.laggauss(deg) # (deg,) x = np.expand_dims( x, axis=tuple(range(1, max(2, lower_bound.ndim + 1))) ) # (deg, 1, ..., 1) w = np.expand_dims( w, axis=tuple(range(1, max(2, lower_bound.ndim + 1))) ) # (deg, 1, ..., 1) fvalues = func(x + lower_bound) # (deg, *shape) return np.sum(w * fvalues * np.exp(x), axis=0).reshape( lower_bound.shape ) # (*shape,)