add read me
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
"""Numerical cubature algorithms"""
|
||||
|
||||
from ._base import (
|
||||
Rule, FixedRule,
|
||||
NestedFixedRule,
|
||||
ProductNestedFixed,
|
||||
)
|
||||
from ._genz_malik import GenzMalikCubature
|
||||
from ._gauss_kronrod import GaussKronrodQuadrature
|
||||
from ._gauss_legendre import GaussLegendreQuadrature
|
||||
|
||||
__all__ = [s for s in dir() if not s.startswith('_')]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,518 @@
|
||||
from scipy._lib._array_api import array_namespace, xp_size
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
|
||||
class Rule:
|
||||
"""
|
||||
Base class for numerical integration algorithms (cubatures).
|
||||
|
||||
Finds an estimate for the integral of ``f`` over the region described by two arrays
|
||||
``a`` and ``b`` via `estimate`, and find an estimate for the error of this
|
||||
approximation via `estimate_error`.
|
||||
|
||||
If a subclass does not implement its own `estimate_error`, then it will use a
|
||||
default error estimate based on the difference between the estimate over the whole
|
||||
region and the sum of estimates over that region divided into ``2^ndim`` subregions.
|
||||
|
||||
See Also
|
||||
--------
|
||||
FixedRule
|
||||
|
||||
Examples
|
||||
--------
|
||||
In the following, a custom rule is created which uses 3D Genz-Malik cubature for
|
||||
the estimate of the integral, and the difference between this estimate and a less
|
||||
accurate estimate using 5-node Gauss-Legendre quadrature as an estimate for the
|
||||
error.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.integrate import cubature
|
||||
>>> from scipy.integrate._rules import (
|
||||
... Rule, ProductNestedFixed, GenzMalikCubature, GaussLegendreQuadrature
|
||||
... )
|
||||
>>> def f(x, r, alphas):
|
||||
... # f(x) = cos(2*pi*r + alpha @ x)
|
||||
... # Need to allow r and alphas to be arbitrary shape
|
||||
... npoints, ndim = x.shape[0], x.shape[-1]
|
||||
... alphas_reshaped = alphas[np.newaxis, :]
|
||||
... x_reshaped = x.reshape(npoints, *([1]*(len(alphas.shape) - 1)), ndim)
|
||||
... return np.cos(2*np.pi*r + np.sum(alphas_reshaped * x_reshaped, axis=-1))
|
||||
>>> genz = GenzMalikCubature(ndim=3)
|
||||
>>> gauss = GaussKronrodQuadrature(npoints=21)
|
||||
>>> # Gauss-Kronrod is 1D, so we find the 3D product rule:
|
||||
>>> gauss_3d = ProductNestedFixed([gauss, gauss, gauss])
|
||||
>>> class CustomRule(Rule):
|
||||
... def estimate(self, f, a, b, args=()):
|
||||
... return genz.estimate(f, a, b, args)
|
||||
... def estimate_error(self, f, a, b, args=()):
|
||||
... return np.abs(
|
||||
... genz.estimate(f, a, b, args)
|
||||
... - gauss_3d.estimate(f, a, b, args)
|
||||
... )
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> res = cubature(
|
||||
... f=f,
|
||||
... a=np.array([0, 0, 0]),
|
||||
... b=np.array([1, 1, 1]),
|
||||
... rule=CustomRule(),
|
||||
... args=(rng.random((2,)), rng.random((3, 2, 3)))
|
||||
... )
|
||||
>>> res.estimate
|
||||
array([[-0.95179502, 0.12444608],
|
||||
[-0.96247411, 0.60866385],
|
||||
[-0.97360014, 0.25515587]])
|
||||
"""
|
||||
|
||||
def estimate(self, f, a, b, args=()):
|
||||
r"""
|
||||
Calculate estimate of integral of `f` in rectangular region described by
|
||||
corners `a` and ``b``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f : callable
|
||||
Function to integrate. `f` must have the signature::
|
||||
f(x : ndarray, \*args) -> ndarray
|
||||
|
||||
`f` should accept arrays ``x`` of shape::
|
||||
(npoints, ndim)
|
||||
|
||||
and output arrays of shape::
|
||||
(npoints, output_dim_1, ..., output_dim_n)
|
||||
|
||||
In this case, `estimate` will return arrays of shape::
|
||||
(output_dim_1, ..., output_dim_n)
|
||||
a, b : ndarray
|
||||
Lower and upper limits of integration as rank-1 arrays specifying the left
|
||||
and right endpoints of the intervals being integrated over. Infinite limits
|
||||
are currently not supported.
|
||||
args : tuple, optional
|
||||
Additional positional args passed to ``f``, if any.
|
||||
|
||||
Returns
|
||||
-------
|
||||
est : ndarray
|
||||
Result of estimation. If `f` returns arrays of shape ``(npoints,
|
||||
output_dim_1, ..., output_dim_n)``, then `est` will be of shape
|
||||
``(output_dim_1, ..., output_dim_n)``.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def estimate_error(self, f, a, b, args=()):
|
||||
r"""
|
||||
Estimate the error of the approximation for the integral of `f` in rectangular
|
||||
region described by corners `a` and `b`.
|
||||
|
||||
If a subclass does not override this method, then a default error estimator is
|
||||
used. This estimates the error as ``|est - refined_est|`` where ``est`` is
|
||||
``estimate(f, a, b)`` and ``refined_est`` is the sum of
|
||||
``estimate(f, a_k, b_k)`` where ``a_k, b_k`` are the coordinates of each
|
||||
subregion of the region described by ``a`` and ``b``. In the 1D case, this
|
||||
is equivalent to comparing the integral over an entire interval ``[a, b]`` to
|
||||
the sum of the integrals over the left and right subintervals, ``[a, (a+b)/2]``
|
||||
and ``[(a+b)/2, b]``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f : callable
|
||||
Function to estimate error for. `f` must have the signature::
|
||||
f(x : ndarray, \*args) -> ndarray
|
||||
|
||||
`f` should accept arrays `x` of shape::
|
||||
(npoints, ndim)
|
||||
|
||||
and output arrays of shape::
|
||||
(npoints, output_dim_1, ..., output_dim_n)
|
||||
|
||||
In this case, `estimate` will return arrays of shape::
|
||||
(output_dim_1, ..., output_dim_n)
|
||||
a, b : ndarray
|
||||
Lower and upper limits of integration as rank-1 arrays specifying the left
|
||||
and right endpoints of the intervals being integrated over. Infinite limits
|
||||
are currently not supported.
|
||||
args : tuple, optional
|
||||
Additional positional args passed to `f`, if any.
|
||||
|
||||
Returns
|
||||
-------
|
||||
err_est : ndarray
|
||||
Result of error estimation. If `f` returns arrays of shape
|
||||
``(npoints, output_dim_1, ..., output_dim_n)``, then `est` will be
|
||||
of shape ``(output_dim_1, ..., output_dim_n)``.
|
||||
"""
|
||||
|
||||
est = self.estimate(f, a, b, args)
|
||||
refined_est = 0
|
||||
|
||||
for a_k, b_k in _split_subregion(a, b):
|
||||
refined_est += self.estimate(f, a_k, b_k, args)
|
||||
|
||||
return self.xp.abs(est - refined_est)
|
||||
|
||||
|
||||
class FixedRule(Rule):
|
||||
"""
|
||||
A rule implemented as the weighted sum of function evaluations at fixed nodes.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
nodes_and_weights : (ndarray, ndarray)
|
||||
A tuple ``(nodes, weights)`` of nodes at which to evaluate ``f`` and the
|
||||
corresponding weights. ``nodes`` should be of shape ``(num_nodes,)`` for 1D
|
||||
cubature rules (quadratures) and more generally for N-D cubature rules, it
|
||||
should be of shape ``(num_nodes, ndim)``. ``weights`` should be of shape
|
||||
``(num_nodes,)``. The nodes and weights should be for integrals over
|
||||
:math:`[-1, 1]^n`.
|
||||
|
||||
See Also
|
||||
--------
|
||||
GaussLegendreQuadrature, GaussKronrodQuadrature, GenzMalikCubature
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Implementing Simpson's 1/3 rule:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.integrate._rules import FixedRule
|
||||
>>> class SimpsonsQuad(FixedRule):
|
||||
... @property
|
||||
... def nodes_and_weights(self):
|
||||
... nodes = np.array([-1, 0, 1])
|
||||
... weights = np.array([1/3, 4/3, 1/3])
|
||||
... return (nodes, weights)
|
||||
>>> rule = SimpsonsQuad()
|
||||
>>> rule.estimate(
|
||||
... f=lambda x: x**2,
|
||||
... a=np.array([0]),
|
||||
... b=np.array([1]),
|
||||
... )
|
||||
[0.3333333]
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.xp = None
|
||||
|
||||
@property
|
||||
def nodes_and_weights(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def estimate(self, f, a, b, args=()):
|
||||
r"""
|
||||
Calculate estimate of integral of `f` in rectangular region described by
|
||||
corners `a` and `b` as ``sum(weights * f(nodes))``.
|
||||
|
||||
Nodes and weights will automatically be adjusted from calculating integrals over
|
||||
:math:`[-1, 1]^n` to :math:`[a, b]^n`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f : callable
|
||||
Function to integrate. `f` must have the signature::
|
||||
f(x : ndarray, \*args) -> ndarray
|
||||
|
||||
`f` should accept arrays `x` of shape::
|
||||
(npoints, ndim)
|
||||
|
||||
and output arrays of shape::
|
||||
(npoints, output_dim_1, ..., output_dim_n)
|
||||
|
||||
In this case, `estimate` will return arrays of shape::
|
||||
(output_dim_1, ..., output_dim_n)
|
||||
a, b : ndarray
|
||||
Lower and upper limits of integration as rank-1 arrays specifying the left
|
||||
and right endpoints of the intervals being integrated over. Infinite limits
|
||||
are currently not supported.
|
||||
args : tuple, optional
|
||||
Additional positional args passed to `f`, if any.
|
||||
|
||||
Returns
|
||||
-------
|
||||
est : ndarray
|
||||
Result of estimation. If `f` returns arrays of shape ``(npoints,
|
||||
output_dim_1, ..., output_dim_n)``, then `est` will be of shape
|
||||
``(output_dim_1, ..., output_dim_n)``.
|
||||
"""
|
||||
nodes, weights = self.nodes_and_weights
|
||||
|
||||
if self.xp is None:
|
||||
self.xp = array_namespace(nodes)
|
||||
|
||||
return _apply_fixed_rule(f, a, b, nodes, weights, args, self.xp)
|
||||
|
||||
|
||||
class NestedFixedRule(FixedRule):
|
||||
r"""
|
||||
A cubature rule with error estimate given by the difference between two underlying
|
||||
fixed rules.
|
||||
|
||||
If constructed as ``NestedFixedRule(higher, lower)``, this will use::
|
||||
|
||||
estimate(f, a, b) := higher.estimate(f, a, b)
|
||||
estimate_error(f, a, b) := \|higher.estimate(f, a, b) - lower.estimate(f, a, b)|
|
||||
|
||||
(where the absolute value is taken elementwise).
|
||||
|
||||
Attributes
|
||||
----------
|
||||
higher : Rule
|
||||
Higher accuracy rule.
|
||||
|
||||
lower : Rule
|
||||
Lower accuracy rule.
|
||||
|
||||
See Also
|
||||
--------
|
||||
GaussKronrodQuadrature
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> from scipy.integrate import cubature
|
||||
>>> from scipy.integrate._rules import (
|
||||
... GaussLegendreQuadrature, NestedFixedRule, ProductNestedFixed
|
||||
... )
|
||||
>>> higher = GaussLegendreQuadrature(10)
|
||||
>>> lower = GaussLegendreQuadrature(5)
|
||||
>>> rule = NestedFixedRule(
|
||||
... higher,
|
||||
... lower
|
||||
... )
|
||||
>>> rule_2d = ProductNestedFixed([rule, rule])
|
||||
"""
|
||||
|
||||
def __init__(self, higher, lower):
|
||||
self.higher = higher
|
||||
self.lower = lower
|
||||
self.xp = None
|
||||
|
||||
@property
|
||||
def nodes_and_weights(self):
|
||||
if self.higher is not None:
|
||||
return self.higher.nodes_and_weights
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def lower_nodes_and_weights(self):
|
||||
if self.lower is not None:
|
||||
return self.lower.nodes_and_weights
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def estimate_error(self, f, a, b, args=()):
|
||||
r"""
|
||||
Estimate the error of the approximation for the integral of `f` in rectangular
|
||||
region described by corners `a` and `b`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f : callable
|
||||
Function to estimate error for. `f` must have the signature::
|
||||
f(x : ndarray, \*args) -> ndarray
|
||||
|
||||
`f` should accept arrays `x` of shape::
|
||||
(npoints, ndim)
|
||||
|
||||
and output arrays of shape::
|
||||
(npoints, output_dim_1, ..., output_dim_n)
|
||||
|
||||
In this case, `estimate` will return arrays of shape::
|
||||
(output_dim_1, ..., output_dim_n)
|
||||
a, b : ndarray
|
||||
Lower and upper limits of integration as rank-1 arrays specifying the left
|
||||
and right endpoints of the intervals being integrated over. Infinite limits
|
||||
are currently not supported.
|
||||
args : tuple, optional
|
||||
Additional positional args passed to `f`, if any.
|
||||
|
||||
Returns
|
||||
-------
|
||||
err_est : ndarray
|
||||
Result of error estimation. If `f` returns arrays of shape
|
||||
``(npoints, output_dim_1, ..., output_dim_n)``, then `est` will be
|
||||
of shape ``(output_dim_1, ..., output_dim_n)``.
|
||||
"""
|
||||
|
||||
nodes, weights = self.nodes_and_weights
|
||||
lower_nodes, lower_weights = self.lower_nodes_and_weights
|
||||
|
||||
if self.xp is None:
|
||||
self.xp = array_namespace(nodes)
|
||||
|
||||
error_nodes = self.xp.concat([nodes, lower_nodes], axis=0)
|
||||
error_weights = self.xp.concat([weights, -lower_weights], axis=0)
|
||||
|
||||
return self.xp.abs(
|
||||
_apply_fixed_rule(f, a, b, error_nodes, error_weights, args, self.xp)
|
||||
)
|
||||
|
||||
|
||||
class ProductNestedFixed(NestedFixedRule):
|
||||
"""
|
||||
Find the n-dimensional cubature rule constructed from the Cartesian product of 1-D
|
||||
`NestedFixedRule` quadrature rules.
|
||||
|
||||
Given a list of N 1-dimensional quadrature rules which support error estimation
|
||||
using NestedFixedRule, this will find the N-dimensional cubature rule obtained by
|
||||
taking the Cartesian product of their nodes, and estimating the error by taking the
|
||||
difference with a lower-accuracy N-dimensional cubature rule obtained using the
|
||||
``.lower_nodes_and_weights`` rule in each of the base 1-dimensional rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
base_rules : list of NestedFixedRule
|
||||
List of base 1-dimensional `NestedFixedRule` quadrature rules.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
base_rules : list of NestedFixedRule
|
||||
List of base 1-dimensional `NestedFixedRule` qudarature rules.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Evaluate a 2D integral by taking the product of two 1D rules:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.integrate import cubature
|
||||
>>> from scipy.integrate._rules import (
|
||||
... ProductNestedFixed, GaussKronrodQuadrature
|
||||
... )
|
||||
>>> def f(x):
|
||||
... # f(x) = cos(x_1) + cos(x_2)
|
||||
... return np.sum(np.cos(x), axis=-1)
|
||||
>>> rule = ProductNestedFixed(
|
||||
... [GaussKronrodQuadrature(15), GaussKronrodQuadrature(15)]
|
||||
... ) # Use 15-point Gauss-Kronrod, which implements NestedFixedRule
|
||||
>>> a, b = np.array([0, 0]), np.array([1, 1])
|
||||
>>> rule.estimate(f, a, b) # True value 2*sin(1), approximately 1.6829
|
||||
np.float64(1.682941969615793)
|
||||
>>> rule.estimate_error(f, a, b)
|
||||
np.float64(2.220446049250313e-16)
|
||||
"""
|
||||
|
||||
def __init__(self, base_rules):
|
||||
for rule in base_rules:
|
||||
if not isinstance(rule, NestedFixedRule):
|
||||
raise ValueError("base rules for product need to be instance of"
|
||||
"NestedFixedRule")
|
||||
|
||||
self.base_rules = base_rules
|
||||
self.xp = None
|
||||
|
||||
@cached_property
|
||||
def nodes_and_weights(self):
|
||||
nodes = _cartesian_product(
|
||||
[rule.nodes_and_weights[0] for rule in self.base_rules]
|
||||
)
|
||||
|
||||
if self.xp is None:
|
||||
self.xp = array_namespace(nodes)
|
||||
|
||||
weights = self.xp.prod(
|
||||
_cartesian_product(
|
||||
[rule.nodes_and_weights[1] for rule in self.base_rules]
|
||||
),
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
return nodes, weights
|
||||
|
||||
@cached_property
|
||||
def lower_nodes_and_weights(self):
|
||||
nodes = _cartesian_product(
|
||||
[cubature.lower_nodes_and_weights[0] for cubature in self.base_rules]
|
||||
)
|
||||
|
||||
if self.xp is None:
|
||||
self.xp = array_namespace(nodes)
|
||||
|
||||
weights = self.xp.prod(
|
||||
_cartesian_product(
|
||||
[cubature.lower_nodes_and_weights[1] for cubature in self.base_rules]
|
||||
),
|
||||
axis=-1,
|
||||
)
|
||||
|
||||
return nodes, weights
|
||||
|
||||
|
||||
def _cartesian_product(arrays):
|
||||
xp = array_namespace(*arrays)
|
||||
|
||||
arrays_ix = xp.meshgrid(*arrays, indexing='ij')
|
||||
result = xp.reshape(xp.stack(arrays_ix, axis=-1), (-1, len(arrays)))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _split_subregion(a, b, xp, split_at=None):
|
||||
"""
|
||||
Given the coordinates of a region like a=[0, 0] and b=[1, 1], yield the coordinates
|
||||
of all subregions, which in this case would be::
|
||||
|
||||
([0, 0], [1/2, 1/2]),
|
||||
([0, 1/2], [1/2, 1]),
|
||||
([1/2, 0], [1, 1/2]),
|
||||
([1/2, 1/2], [1, 1])
|
||||
"""
|
||||
xp = array_namespace(a, b)
|
||||
|
||||
if split_at is None:
|
||||
split_at = (a + b) / 2
|
||||
|
||||
left = [xp.stack((a[i], split_at[i])) for i in range(a.shape[0])]
|
||||
right = [xp.stack((split_at[i], b[i])) for i in range(b.shape[0])]
|
||||
|
||||
a_sub = _cartesian_product(left)
|
||||
b_sub = _cartesian_product(right)
|
||||
|
||||
for i in range(a_sub.shape[0]):
|
||||
yield a_sub[i, ...], b_sub[i, ...]
|
||||
|
||||
|
||||
def _apply_fixed_rule(f, a, b, orig_nodes, orig_weights, args, xp):
|
||||
# Downcast nodes and weights to common dtype of a and b
|
||||
result_dtype = a.dtype
|
||||
orig_nodes = xp.astype(orig_nodes, result_dtype)
|
||||
orig_weights = xp.astype(orig_weights, result_dtype)
|
||||
|
||||
# Ensure orig_nodes are at least 2D, since 1D cubature methods can return arrays of
|
||||
# shape (npoints,) rather than (npoints, 1)
|
||||
if orig_nodes.ndim == 1:
|
||||
orig_nodes = orig_nodes[:, None]
|
||||
|
||||
rule_ndim = orig_nodes.shape[-1]
|
||||
|
||||
a_ndim = xp_size(a)
|
||||
b_ndim = xp_size(b)
|
||||
|
||||
if rule_ndim != a_ndim or rule_ndim != b_ndim:
|
||||
raise ValueError(f"rule and function are of incompatible dimension, nodes have"
|
||||
f"ndim {rule_ndim}, while limit of integration has ndim"
|
||||
f"a_ndim={a_ndim}, b_ndim={b_ndim}")
|
||||
|
||||
lengths = b - a
|
||||
|
||||
# The underlying rule is for the hypercube [-1, 1]^n.
|
||||
#
|
||||
# To handle arbitrary regions of integration, it's necessary to apply a linear
|
||||
# change of coordinates to map each interval [a[i], b[i]] to [-1, 1].
|
||||
nodes = (orig_nodes + 1) * (lengths * 0.5) + a
|
||||
|
||||
# Also need to multiply the weights by a scale factor equal to the determinant
|
||||
# of the Jacobian for this coordinate change.
|
||||
weight_scale_factor = xp.prod(lengths, dtype=result_dtype) / 2**rule_ndim
|
||||
weights = orig_weights * weight_scale_factor
|
||||
|
||||
f_nodes = f(nodes, *args)
|
||||
weights_reshaped = xp.reshape(weights, (-1, *([1] * (f_nodes.ndim - 1))))
|
||||
|
||||
# f(nodes) will have shape (num_nodes, output_dim_1, ..., output_dim_n)
|
||||
# Summing along the first axis means estimate will shape (output_dim_1, ...,
|
||||
# output_dim_n)
|
||||
est = xp.sum(weights_reshaped * f_nodes, axis=0, dtype=result_dtype)
|
||||
|
||||
return est
|
||||
@@ -0,0 +1,202 @@
|
||||
from scipy._lib._array_api import np_compat, array_namespace
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from ._base import NestedFixedRule
|
||||
from ._gauss_legendre import GaussLegendreQuadrature
|
||||
|
||||
|
||||
class GaussKronrodQuadrature(NestedFixedRule):
|
||||
"""
|
||||
Gauss-Kronrod quadrature.
|
||||
|
||||
Gauss-Kronrod rules consist of two quadrature rules, one higher-order and one
|
||||
lower-order. The higher-order rule is used as the estimate of the integral and the
|
||||
difference between them is used as an estimate for the error.
|
||||
|
||||
Gauss-Kronrod is a 1D rule. To use it for multidimensional integrals, it will be
|
||||
necessary to use ProductNestedFixed and multiple Gauss-Kronrod rules. See Examples.
|
||||
|
||||
For n-node Gauss-Kronrod, the lower-order rule has ``n//2`` nodes, which are the
|
||||
ordinary Gauss-Legendre nodes with corresponding weights. The higher-order rule has
|
||||
``n`` nodes, ``n//2`` of which are the same as the lower-order rule and the
|
||||
remaining nodes are the Kronrod extension of those nodes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
npoints : int
|
||||
Number of nodes for the higher-order rule.
|
||||
|
||||
xp : array_namespace, optional
|
||||
The namespace for the node and weight arrays. Default is None, where NumPy is
|
||||
used.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
lower : Rule
|
||||
Lower-order rule.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] R. Piessens, E. de Doncker, Quadpack: A Subroutine Package for Automatic
|
||||
Integration, files: dqk21.f, dqk15.f (1983).
|
||||
|
||||
Examples
|
||||
--------
|
||||
Evaluate a 1D integral. Note in this example that ``f`` returns an array, so the
|
||||
estimates will also be arrays, despite the fact that this is a 1D problem.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.integrate import cubature
|
||||
>>> from scipy.integrate._rules import GaussKronrodQuadrature
|
||||
>>> def f(x):
|
||||
... return np.cos(x)
|
||||
>>> rule = GaussKronrodQuadrature(21) # Use 21-point GaussKronrod
|
||||
>>> a, b = np.array([0]), np.array([1])
|
||||
>>> rule.estimate(f, a, b) # True value sin(1), approximately 0.84147
|
||||
array([0.84147098])
|
||||
>>> rule.estimate_error(f, a, b)
|
||||
array([1.11022302e-16])
|
||||
|
||||
Evaluate a 2D integral. Note that in this example ``f`` returns a float, so the
|
||||
estimates will also be floats.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.integrate import cubature
|
||||
>>> from scipy.integrate._rules import (
|
||||
... ProductNestedFixed, GaussKronrodQuadrature
|
||||
... )
|
||||
>>> def f(x):
|
||||
... # f(x) = cos(x_1) + cos(x_2)
|
||||
... return np.sum(np.cos(x), axis=-1)
|
||||
>>> rule = ProductNestedFixed(
|
||||
... [GaussKronrodQuadrature(15), GaussKronrodQuadrature(15)]
|
||||
... ) # Use 15-point Gauss-Kronrod
|
||||
>>> a, b = np.array([0, 0]), np.array([1, 1])
|
||||
>>> rule.estimate(f, a, b) # True value 2*sin(1), approximately 1.6829
|
||||
np.float64(1.682941969615793)
|
||||
>>> rule.estimate_error(f, a, b)
|
||||
np.float64(2.220446049250313e-16)
|
||||
"""
|
||||
|
||||
def __init__(self, npoints, xp=None):
|
||||
# TODO: nodes and weights are currently hard-coded for values 15 and 21, but in
|
||||
# the future it would be best to compute the Kronrod extension of the lower rule
|
||||
if npoints != 15 and npoints != 21:
|
||||
raise NotImplementedError("Gauss-Kronrod quadrature is currently only"
|
||||
"supported for 15 or 21 nodes")
|
||||
|
||||
self.npoints = npoints
|
||||
|
||||
if xp is None:
|
||||
xp = np_compat
|
||||
|
||||
self.xp = array_namespace(xp.empty(0))
|
||||
|
||||
self.gauss = GaussLegendreQuadrature(npoints//2, xp=self.xp)
|
||||
|
||||
@cached_property
|
||||
def nodes_and_weights(self):
|
||||
# These values are from QUADPACK's `dqk21.f` and `dqk15.f` (1983).
|
||||
if self.npoints == 21:
|
||||
nodes = self.xp.asarray(
|
||||
[
|
||||
0.995657163025808080735527280689003,
|
||||
0.973906528517171720077964012084452,
|
||||
0.930157491355708226001207180059508,
|
||||
0.865063366688984510732096688423493,
|
||||
0.780817726586416897063717578345042,
|
||||
0.679409568299024406234327365114874,
|
||||
0.562757134668604683339000099272694,
|
||||
0.433395394129247190799265943165784,
|
||||
0.294392862701460198131126603103866,
|
||||
0.148874338981631210884826001129720,
|
||||
0,
|
||||
-0.148874338981631210884826001129720,
|
||||
-0.294392862701460198131126603103866,
|
||||
-0.433395394129247190799265943165784,
|
||||
-0.562757134668604683339000099272694,
|
||||
-0.679409568299024406234327365114874,
|
||||
-0.780817726586416897063717578345042,
|
||||
-0.865063366688984510732096688423493,
|
||||
-0.930157491355708226001207180059508,
|
||||
-0.973906528517171720077964012084452,
|
||||
-0.995657163025808080735527280689003,
|
||||
],
|
||||
dtype=self.xp.float64,
|
||||
)
|
||||
|
||||
weights = self.xp.asarray(
|
||||
[
|
||||
0.011694638867371874278064396062192,
|
||||
0.032558162307964727478818972459390,
|
||||
0.054755896574351996031381300244580,
|
||||
0.075039674810919952767043140916190,
|
||||
0.093125454583697605535065465083366,
|
||||
0.109387158802297641899210590325805,
|
||||
0.123491976262065851077958109831074,
|
||||
0.134709217311473325928054001771707,
|
||||
0.142775938577060080797094273138717,
|
||||
0.147739104901338491374841515972068,
|
||||
0.149445554002916905664936468389821,
|
||||
0.147739104901338491374841515972068,
|
||||
0.142775938577060080797094273138717,
|
||||
0.134709217311473325928054001771707,
|
||||
0.123491976262065851077958109831074,
|
||||
0.109387158802297641899210590325805,
|
||||
0.093125454583697605535065465083366,
|
||||
0.075039674810919952767043140916190,
|
||||
0.054755896574351996031381300244580,
|
||||
0.032558162307964727478818972459390,
|
||||
0.011694638867371874278064396062192,
|
||||
],
|
||||
dtype=self.xp.float64,
|
||||
)
|
||||
elif self.npoints == 15:
|
||||
nodes = self.xp.asarray(
|
||||
[
|
||||
0.991455371120812639206854697526329,
|
||||
0.949107912342758524526189684047851,
|
||||
0.864864423359769072789712788640926,
|
||||
0.741531185599394439863864773280788,
|
||||
0.586087235467691130294144838258730,
|
||||
0.405845151377397166906606412076961,
|
||||
0.207784955007898467600689403773245,
|
||||
0.000000000000000000000000000000000,
|
||||
-0.207784955007898467600689403773245,
|
||||
-0.405845151377397166906606412076961,
|
||||
-0.586087235467691130294144838258730,
|
||||
-0.741531185599394439863864773280788,
|
||||
-0.864864423359769072789712788640926,
|
||||
-0.949107912342758524526189684047851,
|
||||
-0.991455371120812639206854697526329,
|
||||
],
|
||||
dtype=self.xp.float64,
|
||||
)
|
||||
|
||||
weights = self.xp.asarray(
|
||||
[
|
||||
0.022935322010529224963732008058970,
|
||||
0.063092092629978553290700663189204,
|
||||
0.104790010322250183839876322541518,
|
||||
0.140653259715525918745189590510238,
|
||||
0.169004726639267902826583426598550,
|
||||
0.190350578064785409913256402421014,
|
||||
0.204432940075298892414161999234649,
|
||||
0.209482141084727828012999174891714,
|
||||
0.204432940075298892414161999234649,
|
||||
0.190350578064785409913256402421014,
|
||||
0.169004726639267902826583426598550,
|
||||
0.140653259715525918745189590510238,
|
||||
0.104790010322250183839876322541518,
|
||||
0.063092092629978553290700663189204,
|
||||
0.022935322010529224963732008058970,
|
||||
],
|
||||
dtype=self.xp.float64,
|
||||
)
|
||||
|
||||
return nodes, weights
|
||||
|
||||
@property
|
||||
def lower_nodes_and_weights(self):
|
||||
return self.gauss.nodes_and_weights
|
||||
@@ -0,0 +1,62 @@
|
||||
from scipy._lib._array_api import array_namespace, np_compat
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from scipy.special import roots_legendre
|
||||
|
||||
from ._base import FixedRule
|
||||
|
||||
|
||||
class GaussLegendreQuadrature(FixedRule):
|
||||
"""
|
||||
Gauss-Legendre quadrature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
npoints : int
|
||||
Number of nodes for the higher-order rule.
|
||||
|
||||
xp : array_namespace, optional
|
||||
The namespace for the node and weight arrays. Default is None, where NumPy is
|
||||
used.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Evaluate a 1D integral. Note in this example that ``f`` returns an array, so the
|
||||
estimates will also be arrays.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.integrate import cubature
|
||||
>>> from scipy.integrate._rules import GaussLegendreQuadrature
|
||||
>>> def f(x):
|
||||
... return np.cos(x)
|
||||
>>> rule = GaussLegendreQuadrature(21) # Use 21-point GaussLegendre
|
||||
>>> a, b = np.array([0]), np.array([1])
|
||||
>>> rule.estimate(f, a, b) # True value sin(1), approximately 0.84147
|
||||
array([0.84147098])
|
||||
>>> rule.estimate_error(f, a, b)
|
||||
array([1.11022302e-16])
|
||||
"""
|
||||
|
||||
def __init__(self, npoints, xp=None):
|
||||
if npoints < 2:
|
||||
raise ValueError(
|
||||
"At least 2 nodes required for Gauss-Legendre cubature"
|
||||
)
|
||||
|
||||
self.npoints = npoints
|
||||
|
||||
if xp is None:
|
||||
xp = np_compat
|
||||
|
||||
self.xp = array_namespace(xp.empty(0))
|
||||
|
||||
@cached_property
|
||||
def nodes_and_weights(self):
|
||||
# TODO: current converting to/from numpy
|
||||
nodes, weights = roots_legendre(self.npoints)
|
||||
|
||||
return (
|
||||
self.xp.asarray(nodes, dtype=self.xp.float64),
|
||||
self.xp.asarray(weights, dtype=self.xp.float64)
|
||||
)
|
||||
@@ -0,0 +1,210 @@
|
||||
import math
|
||||
import itertools
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from scipy._lib._array_api import array_namespace, np_compat
|
||||
|
||||
from scipy.integrate._rules import NestedFixedRule
|
||||
|
||||
|
||||
class GenzMalikCubature(NestedFixedRule):
|
||||
"""
|
||||
Genz-Malik cubature.
|
||||
|
||||
Genz-Malik is only defined for integrals of dimension >= 2.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ndim : int
|
||||
The spatial dimension of the integrand.
|
||||
|
||||
xp : array_namespace, optional
|
||||
The namespace for the node and weight arrays. Default is None, where NumPy is
|
||||
used.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
higher : Cubature
|
||||
Higher-order rule.
|
||||
|
||||
lower : Cubature
|
||||
Lower-order rule.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] A.C. Genz, A.A. Malik, Remarks on algorithm 006: An adaptive algorithm for
|
||||
numerical integration over an N-dimensional rectangular region, Journal of
|
||||
Computational and Applied Mathematics, Volume 6, Issue 4, 1980, Pages 295-302,
|
||||
ISSN 0377-0427, https://doi.org/10.1016/0771-050X(80)90039-X.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Evaluate a 3D integral:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.integrate import cubature
|
||||
>>> from scipy.integrate._rules import GenzMalikCubature
|
||||
>>> def f(x):
|
||||
... # f(x) = cos(x_1) + cos(x_2) + cos(x_3)
|
||||
... return np.sum(np.cos(x), axis=-1)
|
||||
>>> rule = GenzMalikCubature(3) # Use 3D Genz-Malik
|
||||
>>> a, b = np.array([0, 0, 0]), np.array([1, 1, 1])
|
||||
>>> rule.estimate(f, a, b) # True value 3*sin(1), approximately 2.5244
|
||||
np.float64(2.5244129547230862)
|
||||
>>> rule.estimate_error(f, a, b)
|
||||
np.float64(1.378269656626685e-06)
|
||||
"""
|
||||
|
||||
def __init__(self, ndim, degree=7, lower_degree=5, xp=None):
|
||||
if ndim < 2:
|
||||
raise ValueError("Genz-Malik cubature is only defined for ndim >= 2")
|
||||
|
||||
if degree != 7 or lower_degree != 5:
|
||||
raise NotImplementedError("Genz-Malik cubature is currently only supported"
|
||||
"for degree=7, lower_degree=5")
|
||||
|
||||
self.ndim = ndim
|
||||
self.degree = degree
|
||||
self.lower_degree = lower_degree
|
||||
|
||||
if xp is None:
|
||||
xp = np_compat
|
||||
|
||||
self.xp = array_namespace(xp.empty(0))
|
||||
|
||||
@cached_property
|
||||
def nodes_and_weights(self):
|
||||
# TODO: Currently only support for degree 7 Genz-Malik cubature, should aim to
|
||||
# support arbitrary degree
|
||||
l_2 = math.sqrt(9/70)
|
||||
l_3 = math.sqrt(9/10)
|
||||
l_4 = math.sqrt(9/10)
|
||||
l_5 = math.sqrt(9/19)
|
||||
|
||||
its = itertools.chain(
|
||||
[(0,) * self.ndim],
|
||||
_distinct_permutations((l_2,) + (0,) * (self.ndim - 1)),
|
||||
_distinct_permutations((-l_2,) + (0,) * (self.ndim - 1)),
|
||||
_distinct_permutations((l_3,) + (0,) * (self.ndim - 1)),
|
||||
_distinct_permutations((-l_3,) + (0,) * (self.ndim - 1)),
|
||||
_distinct_permutations((l_4, l_4) + (0,) * (self.ndim - 2)),
|
||||
_distinct_permutations((l_4, -l_4) + (0,) * (self.ndim - 2)),
|
||||
_distinct_permutations((-l_4, -l_4) + (0,) * (self.ndim - 2)),
|
||||
itertools.product((l_5, -l_5), repeat=self.ndim),
|
||||
)
|
||||
|
||||
nodes_size = 1 + (2 * (self.ndim + 1) * self.ndim) + 2**self.ndim
|
||||
|
||||
nodes = self.xp.asarray(
|
||||
list(zip(*its)),
|
||||
dtype=self.xp.float64,
|
||||
)
|
||||
|
||||
nodes = self.xp.reshape(nodes, (self.ndim, nodes_size))
|
||||
|
||||
# It's convenient to generate the nodes as a sequence of evaluation points
|
||||
# as an array of shape (npoints, ndim), but nodes needs to have shape
|
||||
# (ndim, npoints)
|
||||
nodes = nodes.T
|
||||
|
||||
w_1 = (
|
||||
(2**self.ndim) * (12824 - 9120*self.ndim + (400 * self.ndim**2)) / 19683
|
||||
)
|
||||
w_2 = (2**self.ndim) * 980/6561
|
||||
w_3 = (2**self.ndim) * (1820 - 400 * self.ndim) / 19683
|
||||
w_4 = (2**self.ndim) * (200 / 19683)
|
||||
w_5 = 6859 / 19683
|
||||
|
||||
weights = self.xp.concat([
|
||||
self.xp.asarray([w_1] * 1, dtype=self.xp.float64),
|
||||
self.xp.asarray([w_2] * (2 * self.ndim), dtype=self.xp.float64),
|
||||
self.xp.asarray([w_3] * (2 * self.ndim), dtype=self.xp.float64),
|
||||
self.xp.asarray(
|
||||
[w_4] * (2 * (self.ndim - 1) * self.ndim),
|
||||
dtype=self.xp.float64,
|
||||
),
|
||||
self.xp.asarray([w_5] * (2**self.ndim), dtype=self.xp.float64),
|
||||
])
|
||||
|
||||
return nodes, weights
|
||||
|
||||
@cached_property
|
||||
def lower_nodes_and_weights(self):
|
||||
# TODO: Currently only support for the degree 5 lower rule, in the future it
|
||||
# would be worth supporting arbitrary degree
|
||||
|
||||
# Nodes are almost the same as the full rule, but there are no nodes
|
||||
# corresponding to l_5.
|
||||
l_2 = math.sqrt(9/70)
|
||||
l_3 = math.sqrt(9/10)
|
||||
l_4 = math.sqrt(9/10)
|
||||
|
||||
its = itertools.chain(
|
||||
[(0,) * self.ndim],
|
||||
_distinct_permutations((l_2,) + (0,) * (self.ndim - 1)),
|
||||
_distinct_permutations((-l_2,) + (0,) * (self.ndim - 1)),
|
||||
_distinct_permutations((l_3,) + (0,) * (self.ndim - 1)),
|
||||
_distinct_permutations((-l_3,) + (0,) * (self.ndim - 1)),
|
||||
_distinct_permutations((l_4, l_4) + (0,) * (self.ndim - 2)),
|
||||
_distinct_permutations((l_4, -l_4) + (0,) * (self.ndim - 2)),
|
||||
_distinct_permutations((-l_4, -l_4) + (0,) * (self.ndim - 2)),
|
||||
)
|
||||
|
||||
nodes_size = 1 + (2 * (self.ndim + 1) * self.ndim)
|
||||
|
||||
nodes = self.xp.asarray(list(zip(*its)), dtype=self.xp.float64)
|
||||
nodes = self.xp.reshape(nodes, (self.ndim, nodes_size))
|
||||
nodes = nodes.T
|
||||
|
||||
# Weights are different from those in the full rule.
|
||||
w_1 = (2**self.ndim) * (729 - 950*self.ndim + 50*self.ndim**2) / 729
|
||||
w_2 = (2**self.ndim) * (245 / 486)
|
||||
w_3 = (2**self.ndim) * (265 - 100*self.ndim) / 1458
|
||||
w_4 = (2**self.ndim) * (25 / 729)
|
||||
|
||||
weights = self.xp.concat([
|
||||
self.xp.asarray([w_1] * 1, dtype=self.xp.float64),
|
||||
self.xp.asarray([w_2] * (2 * self.ndim), dtype=self.xp.float64),
|
||||
self.xp.asarray([w_3] * (2 * self.ndim), dtype=self.xp.float64),
|
||||
self.xp.asarray(
|
||||
[w_4] * (2 * (self.ndim - 1) * self.ndim),
|
||||
dtype=self.xp.float64,
|
||||
),
|
||||
])
|
||||
|
||||
return nodes, weights
|
||||
|
||||
|
||||
def _distinct_permutations(iterable):
|
||||
"""
|
||||
Find the number of distinct permutations of elements of `iterable`.
|
||||
"""
|
||||
|
||||
# Algorithm: https://w.wiki/Qai
|
||||
|
||||
items = sorted(iterable)
|
||||
size = len(items)
|
||||
|
||||
while True:
|
||||
# Yield the permutation we have
|
||||
yield tuple(items)
|
||||
|
||||
# Find the largest index i such that A[i] < A[i + 1]
|
||||
for i in range(size - 2, -1, -1):
|
||||
if items[i] < items[i + 1]:
|
||||
break
|
||||
|
||||
# If no such index exists, this permutation is the last one
|
||||
else:
|
||||
return
|
||||
|
||||
# Find the largest index j greater than j such that A[i] < A[j]
|
||||
for j in range(size - 1, i, -1):
|
||||
if items[i] < items[j]:
|
||||
break
|
||||
|
||||
# Swap the value of A[i] with that of A[j], then reverse the
|
||||
# sequence from A[i + 1] to form the new permutation
|
||||
items[i], items[j] = items[j], items[i]
|
||||
items[i+1:] = items[:i-size:-1] # A[i + 1:][::-1]
|
||||
Reference in New Issue
Block a user