1390 lines
36 KiB
Python
1390 lines
36 KiB
Python
import math
|
|
import scipy
|
|
import itertools
|
|
|
|
import pytest
|
|
|
|
from scipy._lib._array_api import (
|
|
array_namespace,
|
|
xp_assert_close,
|
|
xp_size,
|
|
np_compat,
|
|
is_array_api_strict,
|
|
)
|
|
from scipy.conftest import array_api_compatible
|
|
|
|
from scipy.integrate import cubature
|
|
|
|
from scipy.integrate._rules import (
|
|
Rule, FixedRule,
|
|
NestedFixedRule,
|
|
GaussLegendreQuadrature, GaussKronrodQuadrature,
|
|
GenzMalikCubature,
|
|
)
|
|
|
|
from scipy.integrate._cubature import _InfiniteLimitsTransform
|
|
|
|
pytestmark = [pytest.mark.usefixtures("skip_xp_backends"),]
|
|
skip_xp_backends = pytest.mark.skip_xp_backends
|
|
|
|
# The integrands ``genz_malik_1980_*`` come from the paper:
|
|
# 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.
|
|
|
|
|
|
def basic_1d_integrand(x, n, xp):
|
|
x_reshaped = xp.reshape(x, (-1, 1, 1))
|
|
n_reshaped = xp.reshape(n, (1, -1, 1))
|
|
|
|
return x_reshaped**n_reshaped
|
|
|
|
|
|
def basic_1d_integrand_exact(n, xp):
|
|
# Exact only for integration over interval [0, 2].
|
|
return xp.reshape(2**(n+1)/(n+1), (-1, 1))
|
|
|
|
|
|
def basic_nd_integrand(x, n, xp):
|
|
return xp.reshape(xp.sum(x, axis=-1), (-1, 1))**xp.reshape(n, (1, -1))
|
|
|
|
|
|
def basic_nd_integrand_exact(n, xp):
|
|
# Exact only for integration over interval [0, 2].
|
|
return (-2**(3+n) + 4**(2+n))/((1+n)*(2+n))
|
|
|
|
|
|
def genz_malik_1980_f_1(x, r, alphas, xp):
|
|
r"""
|
|
.. math:: f_1(\mathbf x) = \cos\left(2\pi r + \sum^n_{i = 1}\alpha_i x_i\right)
|
|
|
|
.. code-block:: mathematica
|
|
|
|
genzMalik1980f1[x_List, r_, alphas_List] := Cos[2*Pi*r + Total[x*alphas]]
|
|
"""
|
|
|
|
npoints, ndim = x.shape[0], x.shape[-1]
|
|
|
|
alphas_reshaped = alphas[None, ...]
|
|
x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return xp.cos(2*math.pi*r + xp.sum(alphas_reshaped * x_reshaped, axis=-1))
|
|
|
|
|
|
def genz_malik_1980_f_1_exact(a, b, r, alphas, xp):
|
|
ndim = xp_size(a)
|
|
a = xp.reshape(a, (*([1]*(len(alphas.shape) - 1)), ndim))
|
|
b = xp.reshape(b, (*([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return (
|
|
(-2)**ndim
|
|
* 1/xp.prod(alphas, axis=-1)
|
|
* xp.cos(2*math.pi*r + xp.sum(alphas * (a+b) * 0.5, axis=-1))
|
|
* xp.prod(xp.sin(alphas * (a-b)/2), axis=-1)
|
|
)
|
|
|
|
|
|
def genz_malik_1980_f_1_random_args(rng, shape, xp):
|
|
r = xp.asarray(rng.random(shape[:-1]))
|
|
alphas = xp.asarray(rng.random(shape))
|
|
|
|
difficulty = 9
|
|
normalisation_factors = xp.sum(alphas, axis=-1)[..., None]
|
|
alphas = difficulty * alphas / normalisation_factors
|
|
|
|
return (r, alphas)
|
|
|
|
|
|
def genz_malik_1980_f_2(x, alphas, betas, xp):
|
|
r"""
|
|
.. math:: f_2(\mathbf x) = \prod^n_{i = 1} (\alpha_i^2 + (x_i - \beta_i)^2)^{-1}
|
|
|
|
.. code-block:: mathematica
|
|
|
|
genzMalik1980f2[x_List, alphas_List, betas_List] :=
|
|
1/Times @@ ((alphas^2 + (x - betas)^2))
|
|
"""
|
|
npoints, ndim = x.shape[0], x.shape[-1]
|
|
|
|
alphas_reshaped = alphas[None, ...]
|
|
betas_reshaped = betas[None, ...]
|
|
|
|
x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return 1/xp.prod(alphas_reshaped**2 + (x_reshaped-betas_reshaped)**2, axis=-1)
|
|
|
|
|
|
def genz_malik_1980_f_2_exact(a, b, alphas, betas, xp):
|
|
ndim = xp_size(a)
|
|
a = xp.reshape(a, (*([1]*(len(alphas.shape) - 1)), ndim))
|
|
b = xp.reshape(b, (*([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
# `xp` is the unwrapped namespace, so `.atan` won't work for `xp = np` and np<2.
|
|
xp_test = array_namespace(a)
|
|
|
|
return (
|
|
(-1)**ndim * 1/xp.prod(alphas, axis=-1)
|
|
* xp.prod(
|
|
xp_test.atan((a - betas)/alphas) - xp_test.atan((b - betas)/alphas),
|
|
axis=-1,
|
|
)
|
|
)
|
|
|
|
|
|
def genz_malik_1980_f_2_random_args(rng, shape, xp):
|
|
ndim = shape[-1]
|
|
alphas = xp.asarray(rng.random(shape))
|
|
betas = xp.asarray(rng.random(shape))
|
|
|
|
difficulty = 25.0
|
|
products = xp.prod(alphas**xp.asarray(-2.0), axis=-1)
|
|
normalisation_factors = (products**xp.asarray(1 / (2*ndim)))[..., None]
|
|
alphas = alphas * normalisation_factors * math.pow(difficulty, 1 / (2*ndim))
|
|
|
|
# Adjust alphas from distribution used in Genz and Malik 1980 since denominator
|
|
# is very small for high dimensions.
|
|
alphas *= 10
|
|
|
|
return alphas, betas
|
|
|
|
|
|
def genz_malik_1980_f_3(x, alphas, xp):
|
|
r"""
|
|
.. math:: f_3(\mathbf x) = \exp\left(\sum^n_{i = 1} \alpha_i x_i\right)
|
|
|
|
.. code-block:: mathematica
|
|
|
|
genzMalik1980f3[x_List, alphas_List] := Exp[Dot[x, alphas]]
|
|
"""
|
|
|
|
npoints, ndim = x.shape[0], x.shape[-1]
|
|
|
|
alphas_reshaped = alphas[None, ...]
|
|
x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return xp.exp(xp.sum(alphas_reshaped * x_reshaped, axis=-1))
|
|
|
|
|
|
def genz_malik_1980_f_3_exact(a, b, alphas, xp):
|
|
ndim = xp_size(a)
|
|
a = xp.reshape(a, (*([1]*(len(alphas.shape) - 1)), ndim))
|
|
b = xp.reshape(b, (*([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return (
|
|
(-1)**ndim * 1/xp.prod(alphas, axis=-1)
|
|
* xp.prod(xp.exp(alphas * a) - xp.exp(alphas * b), axis=-1)
|
|
)
|
|
|
|
|
|
def genz_malik_1980_f_3_random_args(rng, shape, xp):
|
|
alphas = xp.asarray(rng.random(shape))
|
|
normalisation_factors = xp.sum(alphas, axis=-1)[..., None]
|
|
difficulty = 12.0
|
|
alphas = difficulty * alphas / normalisation_factors
|
|
|
|
return (alphas,)
|
|
|
|
|
|
def genz_malik_1980_f_4(x, alphas, xp):
|
|
r"""
|
|
.. math:: f_4(\mathbf x) = \left(1 + \sum^n_{i = 1} \alpha_i x_i\right)^{-n-1}
|
|
|
|
.. code-block:: mathematica
|
|
genzMalik1980f4[x_List, alphas_List] :=
|
|
(1 + Dot[x, alphas])^(-Length[alphas] - 1)
|
|
"""
|
|
|
|
npoints, ndim = x.shape[0], x.shape[-1]
|
|
|
|
alphas_reshaped = alphas[None, ...]
|
|
x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return (1 + xp.sum(alphas_reshaped * x_reshaped, axis=-1))**(-ndim-1)
|
|
|
|
|
|
def genz_malik_1980_f_4_exact(a, b, alphas, xp):
|
|
ndim = xp_size(a)
|
|
|
|
def F(x):
|
|
x_reshaped = xp.reshape(x, (*([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return (
|
|
(-1)**ndim/xp.prod(alphas, axis=-1)
|
|
/ math.factorial(ndim)
|
|
/ (1 + xp.sum(alphas * x_reshaped, axis=-1))
|
|
)
|
|
|
|
return _eval_indefinite_integral(F, a, b, xp)
|
|
|
|
|
|
def _eval_indefinite_integral(F, a, b, xp):
|
|
"""
|
|
Calculates a definite integral from points `a` to `b` by summing up over the corners
|
|
of the corresponding hyperrectangle.
|
|
"""
|
|
|
|
ndim = xp_size(a)
|
|
points = xp.stack([a, b], axis=0)
|
|
|
|
out = 0
|
|
for ind in itertools.product(range(2), repeat=ndim):
|
|
selected_points = xp.asarray([points[i, j] for i, j in zip(ind, range(ndim))])
|
|
out += pow(-1, sum(ind) + ndim) * F(selected_points)
|
|
|
|
return out
|
|
|
|
|
|
def genz_malik_1980_f_4_random_args(rng, shape, xp):
|
|
ndim = shape[-1]
|
|
|
|
alphas = xp.asarray(rng.random(shape))
|
|
normalisation_factors = xp.sum(alphas, axis=-1)[..., None]
|
|
difficulty = 14.0
|
|
alphas = (difficulty / ndim) * alphas / normalisation_factors
|
|
|
|
return (alphas,)
|
|
|
|
|
|
def genz_malik_1980_f_5(x, alphas, betas, xp):
|
|
r"""
|
|
.. math::
|
|
|
|
f_5(\mathbf x) = \exp\left(-\sum^n_{i = 1} \alpha^2_i (x_i - \beta_i)^2\right)
|
|
|
|
.. code-block:: mathematica
|
|
|
|
genzMalik1980f5[x_List, alphas_List, betas_List] :=
|
|
Exp[-Total[alphas^2 * (x - betas)^2]]
|
|
"""
|
|
|
|
npoints, ndim = x.shape[0], x.shape[-1]
|
|
|
|
alphas_reshaped = alphas[None, ...]
|
|
betas_reshaped = betas[None, ...]
|
|
|
|
x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return xp.exp(
|
|
-xp.sum(alphas_reshaped**2 * (x_reshaped - betas_reshaped)**2, axis=-1)
|
|
)
|
|
|
|
|
|
def genz_malik_1980_f_5_exact(a, b, alphas, betas, xp):
|
|
ndim = xp_size(a)
|
|
a = xp.reshape(a, (*([1]*(len(alphas.shape) - 1)), ndim))
|
|
b = xp.reshape(b, (*([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return (
|
|
(1/2)**ndim
|
|
* 1/xp.prod(alphas, axis=-1)
|
|
* (math.pi**(ndim/2))
|
|
* xp.prod(
|
|
scipy.special.erf(alphas * (betas - a))
|
|
+ scipy.special.erf(alphas * (b - betas)),
|
|
axis=-1,
|
|
)
|
|
)
|
|
|
|
|
|
def genz_malik_1980_f_5_random_args(rng, shape, xp):
|
|
alphas = xp.asarray(rng.random(shape))
|
|
betas = xp.asarray(rng.random(shape))
|
|
|
|
difficulty = 21.0
|
|
normalisation_factors = xp.sqrt(xp.sum(alphas**xp.asarray(2.0), axis=-1))[..., None]
|
|
alphas = alphas / normalisation_factors * math.sqrt(difficulty)
|
|
|
|
return alphas, betas
|
|
|
|
|
|
def f_gaussian(x, alphas, xp):
|
|
r"""
|
|
.. math::
|
|
|
|
f(\mathbf x) = \exp\left(-\sum^n_{i = 1} (\alpha_i x_i)^2 \right)
|
|
"""
|
|
npoints, ndim = x.shape[0], x.shape[-1]
|
|
alphas_reshaped = alphas[None, ...]
|
|
x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
|
|
|
|
return xp.exp(-xp.sum((alphas_reshaped * x_reshaped)**2, axis=-1))
|
|
|
|
|
|
def f_gaussian_exact(a, b, alphas, xp):
|
|
# Exact only when `a` and `b` are one of:
|
|
# (-oo, oo), or
|
|
# (0, oo), or
|
|
# (-oo, 0)
|
|
# `alphas` can be arbitrary.
|
|
|
|
ndim = xp_size(a)
|
|
double_infinite_count = 0
|
|
semi_infinite_count = 0
|
|
|
|
for i in range(ndim):
|
|
if xp.isinf(a[i]) and xp.isinf(b[i]): # doubly-infinite
|
|
double_infinite_count += 1
|
|
elif xp.isinf(a[i]) != xp.isinf(b[i]): # exclusive or, so semi-infinite
|
|
semi_infinite_count += 1
|
|
|
|
return (math.sqrt(math.pi) ** ndim) / (
|
|
2**semi_infinite_count * xp.prod(alphas, axis=-1)
|
|
)
|
|
|
|
|
|
def f_gaussian_random_args(rng, shape, xp):
|
|
alphas = xp.asarray(rng.random(shape))
|
|
|
|
# If alphas are very close to 0 this makes the problem very difficult due to large
|
|
# values of ``f``.
|
|
alphas *= 100
|
|
|
|
return (alphas,)
|
|
|
|
|
|
def f_modified_gaussian(x_arr, n, xp):
|
|
r"""
|
|
.. math::
|
|
|
|
f(x, y, z, w) = x^n \sqrt{y} \exp(-y-z^2-w^2)
|
|
"""
|
|
x, y, z, w = x_arr[:, 0], x_arr[:, 1], x_arr[:, 2], x_arr[:, 3]
|
|
res = (x ** n[:, None]) * xp.sqrt(y) * xp.exp(-y-z**2-w**2)
|
|
|
|
return res.T
|
|
|
|
|
|
def f_modified_gaussian_exact(a, b, n, xp):
|
|
# Exact only for the limits
|
|
# a = (0, 0, -oo, -oo)
|
|
# b = (1, oo, oo, oo)
|
|
# but defined here as a function to match the format of the other integrands.
|
|
return 1/(2 + 2*n) * math.pi ** (3/2)
|
|
|
|
|
|
def f_with_problematic_points(x_arr, points, xp):
|
|
"""
|
|
This emulates a function with a list of singularities given by `points`.
|
|
|
|
If no `x_arr` are one of the `points`, then this function returns 1.
|
|
"""
|
|
|
|
for point in points:
|
|
if xp.any(x_arr == point):
|
|
raise ValueError("called with a problematic point")
|
|
|
|
return xp.ones(x_arr.shape[0])
|
|
|
|
|
|
@array_api_compatible
|
|
class TestCubature:
|
|
"""
|
|
Tests related to the interface of `cubature`.
|
|
"""
|
|
|
|
@pytest.mark.parametrize("rule_str", [
|
|
"gauss-kronrod",
|
|
"genz-malik",
|
|
"gk21",
|
|
"gk15",
|
|
])
|
|
def test_pass_str(self, rule_str, xp):
|
|
n = xp.arange(5, dtype=xp.float64)
|
|
a = xp.asarray([0, 0], dtype=xp.float64)
|
|
b = xp.asarray([2, 2], dtype=xp.float64)
|
|
|
|
res = cubature(basic_nd_integrand, a, b, rule=rule_str, args=(n, xp))
|
|
|
|
xp_assert_close(
|
|
res.estimate,
|
|
basic_nd_integrand_exact(n, xp),
|
|
rtol=1e-8,
|
|
atol=0,
|
|
)
|
|
|
|
@skip_xp_backends(np_only=True,
|
|
reason='array-likes only supported for NumPy backend')
|
|
def test_pass_array_like_not_array(self, xp):
|
|
n = np_compat.arange(5, dtype=np_compat.float64)
|
|
a = [0]
|
|
b = [2]
|
|
|
|
res = cubature(
|
|
basic_1d_integrand,
|
|
a,
|
|
b,
|
|
args=(n, xp)
|
|
)
|
|
|
|
xp_assert_close(
|
|
res.estimate,
|
|
basic_1d_integrand_exact(n, xp),
|
|
rtol=1e-8,
|
|
atol=0,
|
|
)
|
|
|
|
def test_stops_after_max_subdivisions(self, xp):
|
|
a = xp.asarray([0])
|
|
b = xp.asarray([1])
|
|
rule = BadErrorRule()
|
|
|
|
res = cubature(
|
|
basic_1d_integrand, # Any function would suffice
|
|
a,
|
|
b,
|
|
rule=rule,
|
|
max_subdivisions=10,
|
|
args=(xp.arange(5, dtype=xp.float64), xp),
|
|
)
|
|
|
|
assert res.subdivisions == 10
|
|
assert res.status == "not_converged"
|
|
|
|
def test_a_and_b_must_be_1d(self, xp):
|
|
a = xp.asarray([[0]], dtype=xp.float64)
|
|
b = xp.asarray([[1]], dtype=xp.float64)
|
|
|
|
with pytest.raises(Exception, match="`a` and `b` must be 1D arrays"):
|
|
cubature(basic_1d_integrand, a, b, args=(xp,))
|
|
|
|
def test_a_and_b_must_be_nonempty(self, xp):
|
|
a = xp.asarray([])
|
|
b = xp.asarray([])
|
|
|
|
with pytest.raises(Exception, match="`a` and `b` must be nonempty"):
|
|
cubature(basic_1d_integrand, a, b, args=(xp,))
|
|
|
|
def test_zero_width_limits(self, xp):
|
|
n = xp.arange(5, dtype=xp.float64)
|
|
|
|
a = xp.asarray([0], dtype=xp.float64)
|
|
b = xp.asarray([0], dtype=xp.float64)
|
|
|
|
res = cubature(
|
|
basic_1d_integrand,
|
|
a,
|
|
b,
|
|
args=(n, xp),
|
|
)
|
|
|
|
xp_assert_close(
|
|
res.estimate,
|
|
xp.asarray([[0], [0], [0], [0], [0]], dtype=xp.float64),
|
|
rtol=1e-8,
|
|
atol=0,
|
|
)
|
|
|
|
def test_limits_other_way_around(self, xp):
|
|
n = xp.arange(5, dtype=xp.float64)
|
|
|
|
a = xp.asarray([2], dtype=xp.float64)
|
|
b = xp.asarray([0], dtype=xp.float64)
|
|
|
|
res = cubature(
|
|
basic_1d_integrand,
|
|
a,
|
|
b,
|
|
args=(n, xp),
|
|
)
|
|
|
|
xp_assert_close(
|
|
res.estimate,
|
|
-basic_1d_integrand_exact(n, xp),
|
|
rtol=1e-8,
|
|
atol=0,
|
|
)
|
|
|
|
def test_result_dtype_promoted_correctly(self, xp):
|
|
result_dtype = cubature(
|
|
basic_1d_integrand,
|
|
xp.asarray([0], dtype=xp.float64),
|
|
xp.asarray([1], dtype=xp.float64),
|
|
points=[],
|
|
args=(xp.asarray([1], dtype=xp.float64), xp),
|
|
).estimate.dtype
|
|
|
|
assert result_dtype == xp.float64
|
|
|
|
result_dtype = cubature(
|
|
basic_1d_integrand,
|
|
xp.asarray([0], dtype=xp.float32),
|
|
xp.asarray([1], dtype=xp.float32),
|
|
points=[],
|
|
args=(xp.asarray([1], dtype=xp.float32), xp),
|
|
).estimate.dtype
|
|
|
|
assert result_dtype == xp.float32
|
|
|
|
result_dtype = cubature(
|
|
basic_1d_integrand,
|
|
xp.asarray([0], dtype=xp.float32),
|
|
xp.asarray([1], dtype=xp.float64),
|
|
points=[],
|
|
args=(xp.asarray([1], dtype=xp.float32), xp),
|
|
).estimate.dtype
|
|
|
|
assert result_dtype == xp.float64
|
|
|
|
|
|
@pytest.mark.parametrize("rtol", [1e-4])
|
|
@pytest.mark.parametrize("atol", [1e-5])
|
|
@pytest.mark.parametrize("rule", [
|
|
"gk15",
|
|
"gk21",
|
|
"genz-malik",
|
|
])
|
|
@array_api_compatible
|
|
class TestCubatureProblems:
|
|
"""
|
|
Tests that `cubature` gives the correct answer.
|
|
"""
|
|
|
|
@pytest.mark.parametrize("problem", [
|
|
# -- f1 --
|
|
(
|
|
# Function to integrate, like `f(x, *args)`
|
|
genz_malik_1980_f_1,
|
|
|
|
# Exact solution, like `exact(a, b, *args)`
|
|
genz_malik_1980_f_1_exact,
|
|
|
|
# Coordinates of `a`
|
|
[0],
|
|
|
|
# Coordinates of `b`
|
|
[10],
|
|
|
|
# Arguments to pass to `f` and `exact`
|
|
(
|
|
1/4,
|
|
[5],
|
|
)
|
|
),
|
|
(
|
|
genz_malik_1980_f_1,
|
|
genz_malik_1980_f_1_exact,
|
|
[0, 0],
|
|
[1, 1],
|
|
(
|
|
1/4,
|
|
[2, 4],
|
|
),
|
|
),
|
|
(
|
|
genz_malik_1980_f_1,
|
|
genz_malik_1980_f_1_exact,
|
|
[0, 0],
|
|
[5, 5],
|
|
(
|
|
1/2,
|
|
[2, 4],
|
|
)
|
|
),
|
|
(
|
|
genz_malik_1980_f_1,
|
|
genz_malik_1980_f_1_exact,
|
|
[0, 0, 0],
|
|
[5, 5, 5],
|
|
(
|
|
1/2,
|
|
[1, 1, 1],
|
|
)
|
|
),
|
|
|
|
# -- f2 --
|
|
(
|
|
genz_malik_1980_f_2,
|
|
genz_malik_1980_f_2_exact,
|
|
[-1],
|
|
[1],
|
|
(
|
|
[5],
|
|
[4],
|
|
)
|
|
),
|
|
(
|
|
genz_malik_1980_f_2,
|
|
genz_malik_1980_f_2_exact,
|
|
|
|
[0, 0],
|
|
[10, 50],
|
|
(
|
|
[-3, 3],
|
|
[-2, 2],
|
|
),
|
|
),
|
|
(
|
|
genz_malik_1980_f_2,
|
|
genz_malik_1980_f_2_exact,
|
|
[0, 0, 0],
|
|
[1, 1, 1],
|
|
(
|
|
[1, 1, 1],
|
|
[1, 1, 1],
|
|
)
|
|
),
|
|
(
|
|
genz_malik_1980_f_2,
|
|
genz_malik_1980_f_2_exact,
|
|
[0, 0, 0],
|
|
[1, 1, 1],
|
|
(
|
|
[2, 3, 4],
|
|
[2, 3, 4],
|
|
)
|
|
),
|
|
(
|
|
genz_malik_1980_f_2,
|
|
genz_malik_1980_f_2_exact,
|
|
[-1, -1, -1],
|
|
[1, 1, 1],
|
|
(
|
|
[1, 1, 1],
|
|
[2, 2, 2],
|
|
)
|
|
),
|
|
(
|
|
genz_malik_1980_f_2,
|
|
genz_malik_1980_f_2_exact,
|
|
[-1, -1, -1, -1],
|
|
[1, 1, 1, 1],
|
|
(
|
|
[1, 1, 1, 1],
|
|
[1, 1, 1, 1],
|
|
)
|
|
),
|
|
|
|
# -- f3 --
|
|
(
|
|
genz_malik_1980_f_3,
|
|
genz_malik_1980_f_3_exact,
|
|
[-1],
|
|
[1],
|
|
(
|
|
[1/2],
|
|
),
|
|
),
|
|
(
|
|
genz_malik_1980_f_3,
|
|
genz_malik_1980_f_3_exact,
|
|
[0, -1],
|
|
[1, 1],
|
|
(
|
|
[5, 5],
|
|
),
|
|
),
|
|
(
|
|
genz_malik_1980_f_3,
|
|
genz_malik_1980_f_3_exact,
|
|
[-1, -1, -1],
|
|
[1, 1, 1],
|
|
(
|
|
[1, 1, 1],
|
|
),
|
|
),
|
|
|
|
# -- f4 --
|
|
(
|
|
genz_malik_1980_f_4,
|
|
genz_malik_1980_f_4_exact,
|
|
[0],
|
|
[2],
|
|
(
|
|
[1],
|
|
),
|
|
),
|
|
(
|
|
genz_malik_1980_f_4,
|
|
genz_malik_1980_f_4_exact,
|
|
[0, 0],
|
|
[2, 1],
|
|
([1, 1],),
|
|
),
|
|
(
|
|
genz_malik_1980_f_4,
|
|
genz_malik_1980_f_4_exact,
|
|
[0, 0, 0],
|
|
[1, 1, 1],
|
|
([1, 1, 1],),
|
|
),
|
|
|
|
# -- f5 --
|
|
(
|
|
genz_malik_1980_f_5,
|
|
genz_malik_1980_f_5_exact,
|
|
[-1],
|
|
[1],
|
|
(
|
|
[-2],
|
|
[2],
|
|
),
|
|
),
|
|
(
|
|
genz_malik_1980_f_5,
|
|
genz_malik_1980_f_5_exact,
|
|
[-1, -1],
|
|
[1, 1],
|
|
(
|
|
[2, 3],
|
|
[4, 5],
|
|
),
|
|
),
|
|
(
|
|
genz_malik_1980_f_5,
|
|
genz_malik_1980_f_5_exact,
|
|
[-1, -1],
|
|
[1, 1],
|
|
(
|
|
[-1, 1],
|
|
[0, 0],
|
|
),
|
|
),
|
|
(
|
|
genz_malik_1980_f_5,
|
|
genz_malik_1980_f_5_exact,
|
|
[-1, -1, -1],
|
|
[1, 1, 1],
|
|
(
|
|
[1, 1, 1],
|
|
[1, 1, 1],
|
|
),
|
|
),
|
|
])
|
|
def test_scalar_output(self, problem, rule, rtol, atol, xp):
|
|
f, exact, a, b, args = problem
|
|
|
|
a = xp.asarray(a, dtype=xp.float64)
|
|
b = xp.asarray(b, dtype=xp.float64)
|
|
args = tuple(xp.asarray(arg, dtype=xp.float64) for arg in args)
|
|
|
|
ndim = xp_size(a)
|
|
|
|
if rule == "genz-malik" and ndim < 2:
|
|
pytest.skip("Genz-Malik cubature does not support 1D integrals")
|
|
|
|
res = cubature(
|
|
f,
|
|
a,
|
|
b,
|
|
rule=rule,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
args=(*args, xp),
|
|
)
|
|
|
|
assert res.status == "converged"
|
|
|
|
est = res.estimate
|
|
exact_sol = exact(a, b, *args, xp)
|
|
|
|
xp_assert_close(
|
|
est,
|
|
exact_sol,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
err_msg=f"estimate_error={res.error}, subdivisions={res.subdivisions}",
|
|
)
|
|
|
|
@pytest.mark.parametrize("problem", [
|
|
(
|
|
# Function to integrate, like `f(x, *args)`
|
|
genz_malik_1980_f_1,
|
|
|
|
# Exact solution, like `exact(a, b, *args)`
|
|
genz_malik_1980_f_1_exact,
|
|
|
|
# Function that generates random args of a certain shape.
|
|
genz_malik_1980_f_1_random_args,
|
|
),
|
|
(
|
|
genz_malik_1980_f_2,
|
|
genz_malik_1980_f_2_exact,
|
|
genz_malik_1980_f_2_random_args,
|
|
),
|
|
(
|
|
genz_malik_1980_f_3,
|
|
genz_malik_1980_f_3_exact,
|
|
genz_malik_1980_f_3_random_args
|
|
),
|
|
(
|
|
genz_malik_1980_f_4,
|
|
genz_malik_1980_f_4_exact,
|
|
genz_malik_1980_f_4_random_args
|
|
),
|
|
(
|
|
genz_malik_1980_f_5,
|
|
genz_malik_1980_f_5_exact,
|
|
genz_malik_1980_f_5_random_args,
|
|
),
|
|
])
|
|
@pytest.mark.parametrize("shape", [
|
|
(2,),
|
|
(3,),
|
|
(4,),
|
|
(1, 2),
|
|
(1, 3),
|
|
(1, 4),
|
|
(3, 2),
|
|
(3, 4, 2),
|
|
(2, 1, 3),
|
|
])
|
|
def test_array_output(self, problem, rule, shape, rtol, atol, xp):
|
|
rng = np_compat.random.default_rng(1)
|
|
ndim = shape[-1]
|
|
|
|
if rule == "genz-malik" and ndim < 2:
|
|
pytest.skip("Genz-Malik cubature does not support 1D integrals")
|
|
|
|
if rule == "genz-malik" and ndim >= 5:
|
|
pytest.mark.slow("Gauss-Kronrod is slow in >= 5 dim")
|
|
|
|
f, exact, random_args = problem
|
|
args = random_args(rng, shape, xp)
|
|
|
|
a = xp.asarray([0] * ndim, dtype=xp.float64)
|
|
b = xp.asarray([1] * ndim, dtype=xp.float64)
|
|
|
|
res = cubature(
|
|
f,
|
|
a,
|
|
b,
|
|
rule=rule,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
args=(*args, xp),
|
|
)
|
|
|
|
est = res.estimate
|
|
exact_sol = exact(a, b, *args, xp)
|
|
|
|
xp_assert_close(
|
|
est,
|
|
exact_sol,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
err_msg=f"estimate_error={res.error}, subdivisions={res.subdivisions}",
|
|
)
|
|
|
|
err_msg = (f"estimate_error={res.error}, "
|
|
f"subdivisions= {res.subdivisions}, "
|
|
f"true_error={xp.abs(res.estimate - exact_sol)}")
|
|
assert res.status == "converged", err_msg
|
|
|
|
assert res.estimate.shape == shape[:-1]
|
|
|
|
@pytest.mark.parametrize("problem", [
|
|
(
|
|
# Function to integrate
|
|
lambda x, xp: x,
|
|
|
|
# Exact value
|
|
[50.0],
|
|
|
|
# Coordinates of `a`
|
|
[0],
|
|
|
|
# Coordinates of `b`
|
|
[10],
|
|
|
|
# Points by which to split up the initial region
|
|
None,
|
|
),
|
|
(
|
|
lambda x, xp: xp.sin(x)/x,
|
|
[2.551496047169878], # si(1) + si(2),
|
|
[-1],
|
|
[2],
|
|
[
|
|
[0.0],
|
|
],
|
|
),
|
|
(
|
|
lambda x, xp: xp.ones((x.shape[0], 1)),
|
|
[1.0],
|
|
[0, 0, 0],
|
|
[1, 1, 1],
|
|
[
|
|
[0.5, 0.5, 0.5],
|
|
],
|
|
),
|
|
(
|
|
lambda x, xp: xp.ones((x.shape[0], 1)),
|
|
[1.0],
|
|
[0, 0, 0],
|
|
[1, 1, 1],
|
|
[
|
|
[0.25, 0.25, 0.25],
|
|
[0.5, 0.5, 0.5],
|
|
],
|
|
),
|
|
(
|
|
lambda x, xp: xp.ones((x.shape[0], 1)),
|
|
[1.0],
|
|
[0, 0, 0],
|
|
[1, 1, 1],
|
|
[
|
|
[0.1, 0.25, 0.5],
|
|
[0.25, 0.25, 0.25],
|
|
[0.5, 0.5, 0.5],
|
|
],
|
|
)
|
|
])
|
|
def test_break_points(self, problem, rule, rtol, atol, xp):
|
|
f, exact, a, b, points = problem
|
|
|
|
a = xp.asarray(a, dtype=xp.float64)
|
|
b = xp.asarray(b, dtype=xp.float64)
|
|
exact = xp.asarray(exact, dtype=xp.float64)
|
|
|
|
if points is not None:
|
|
points = [xp.asarray(point, dtype=xp.float64) for point in points]
|
|
|
|
ndim = xp_size(a)
|
|
|
|
if rule == "genz-malik" and ndim < 2:
|
|
pytest.skip("Genz-Malik cubature does not support 1D integrals")
|
|
|
|
if rule == "genz-malik" and ndim >= 5:
|
|
pytest.mark.slow("Gauss-Kronrod is slow in >= 5 dim")
|
|
|
|
res = cubature(
|
|
f,
|
|
a,
|
|
b,
|
|
rule=rule,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
points=points,
|
|
args=(xp,),
|
|
)
|
|
|
|
xp_assert_close(
|
|
res.estimate,
|
|
exact,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
err_msg=f"estimate_error={res.error}, subdivisions={res.subdivisions}",
|
|
check_dtype=False,
|
|
)
|
|
|
|
err_msg = (f"estimate_error={res.error}, "
|
|
f"subdivisions= {res.subdivisions}, "
|
|
f"true_error={xp.abs(res.estimate - exact)}")
|
|
assert res.status == "converged", err_msg
|
|
|
|
@skip_xp_backends(
|
|
"jax.numpy",
|
|
reasons=["transforms make use of indexing assignment"],
|
|
)
|
|
@pytest.mark.parametrize("problem", [
|
|
(
|
|
# Function to integrate
|
|
f_gaussian,
|
|
|
|
# Exact solution
|
|
f_gaussian_exact,
|
|
|
|
# Arguments passed to f
|
|
f_gaussian_random_args,
|
|
(1, 1),
|
|
|
|
# Limits, have to match the shape of the arguments
|
|
[-math.inf], # a
|
|
[math.inf], # b
|
|
),
|
|
(
|
|
f_gaussian,
|
|
f_gaussian_exact,
|
|
f_gaussian_random_args,
|
|
(2, 2),
|
|
[-math.inf, -math.inf],
|
|
[math.inf, math.inf],
|
|
),
|
|
(
|
|
f_gaussian,
|
|
f_gaussian_exact,
|
|
f_gaussian_random_args,
|
|
(1, 1),
|
|
[0],
|
|
[math.inf],
|
|
),
|
|
(
|
|
f_gaussian,
|
|
f_gaussian_exact,
|
|
f_gaussian_random_args,
|
|
(1, 1),
|
|
[-math.inf],
|
|
[0],
|
|
),
|
|
(
|
|
f_gaussian,
|
|
f_gaussian_exact,
|
|
f_gaussian_random_args,
|
|
(2, 2),
|
|
[0, 0],
|
|
[math.inf, math.inf],
|
|
),
|
|
(
|
|
f_gaussian,
|
|
f_gaussian_exact,
|
|
f_gaussian_random_args,
|
|
(2, 2),
|
|
[0, -math.inf],
|
|
[math.inf, math.inf],
|
|
),
|
|
(
|
|
f_gaussian,
|
|
f_gaussian_exact,
|
|
f_gaussian_random_args,
|
|
(1, 4),
|
|
[0, 0, -math.inf, -math.inf],
|
|
[math.inf, math.inf, math.inf, math.inf],
|
|
),
|
|
(
|
|
f_gaussian,
|
|
f_gaussian_exact,
|
|
f_gaussian_random_args,
|
|
(1, 4),
|
|
[-math.inf, -math.inf, -math.inf, -math.inf],
|
|
[0, 0, math.inf, math.inf],
|
|
),
|
|
(
|
|
lambda x, xp: 1/xp.prod(x, axis=-1)**2,
|
|
|
|
# Exact only for the below limits, not for general `a` and `b`.
|
|
lambda a, b, xp: xp.asarray(1/6, dtype=xp.float64),
|
|
|
|
# Arguments
|
|
lambda rng, shape, xp: tuple(),
|
|
tuple(),
|
|
|
|
[1, -math.inf, 3],
|
|
[math.inf, -2, math.inf],
|
|
),
|
|
|
|
# This particular problem can be slow
|
|
pytest.param(
|
|
(
|
|
# f(x, y, z, w) = x^n * sqrt(y) * exp(-y-z**2-w**2) for n in [0,1,2,3]
|
|
f_modified_gaussian,
|
|
|
|
# This exact solution is for the below limits, not in general
|
|
f_modified_gaussian_exact,
|
|
|
|
# Constant arguments
|
|
lambda rng, shape, xp: (xp.asarray([0, 1, 2, 3, 4], dtype=xp.float64),),
|
|
tuple(),
|
|
|
|
[0, 0, -math.inf, -math.inf],
|
|
[1, math.inf, math.inf, math.inf]
|
|
),
|
|
|
|
marks=pytest.mark.xslow,
|
|
),
|
|
])
|
|
def test_infinite_limits(self, problem, rule, rtol, atol, xp):
|
|
rng = np_compat.random.default_rng(1)
|
|
f, exact, random_args_func, random_args_shape, a, b = problem
|
|
|
|
a = xp.asarray(a, dtype=xp.float64)
|
|
b = xp.asarray(b, dtype=xp.float64)
|
|
args = random_args_func(rng, random_args_shape, xp)
|
|
|
|
ndim = xp_size(a)
|
|
|
|
if rule == "genz-malik" and ndim < 2:
|
|
pytest.skip("Genz-Malik cubature does not support 1D integrals")
|
|
|
|
if rule == "genz-malik" and ndim >= 4:
|
|
pytest.mark.slow("Genz-Malik is slow in >= 5 dim")
|
|
|
|
if rule == "genz-malik" and ndim >= 4 and is_array_api_strict(xp):
|
|
pytest.mark.xslow("Genz-Malik very slow for array_api_strict in >= 4 dim")
|
|
|
|
res = cubature(
|
|
f,
|
|
a,
|
|
b,
|
|
rule=rule,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
args=(*args, xp),
|
|
)
|
|
|
|
assert res.status == "converged"
|
|
|
|
xp_assert_close(
|
|
res.estimate,
|
|
exact(a, b, *args, xp),
|
|
rtol=rtol,
|
|
atol=atol,
|
|
err_msg=f"error_estimate={res.error}, subdivisions={res.subdivisions}",
|
|
check_0d=False,
|
|
)
|
|
|
|
@skip_xp_backends(
|
|
"jax.numpy",
|
|
reasons=["transforms make use of indexing assignment"],
|
|
)
|
|
@pytest.mark.parametrize("problem", [
|
|
(
|
|
# Function to integrate
|
|
lambda x, xp: (xp.sin(x) / x)**8,
|
|
|
|
# Exact value
|
|
[151/315 * math.pi],
|
|
|
|
# Limits
|
|
[-math.inf],
|
|
[math.inf],
|
|
|
|
# Breakpoints
|
|
[[0]],
|
|
|
|
),
|
|
(
|
|
# Function to integrate
|
|
lambda x, xp: (xp.sin(x[:, 0]) / x[:, 0])**8,
|
|
|
|
# Exact value
|
|
151/315 * math.pi,
|
|
|
|
# Limits
|
|
[-math.inf, 0],
|
|
[math.inf, 1],
|
|
|
|
# Breakpoints
|
|
[[0, 0.5]],
|
|
|
|
)
|
|
])
|
|
def test_infinite_limits_and_break_points(self, problem, rule, rtol, atol, xp):
|
|
f, exact, a, b, points = problem
|
|
|
|
a = xp.asarray(a, dtype=xp.float64)
|
|
b = xp.asarray(b, dtype=xp.float64)
|
|
exact = xp.asarray(exact, dtype=xp.float64)
|
|
|
|
ndim = xp_size(a)
|
|
|
|
if rule == "genz-malik" and ndim < 2:
|
|
pytest.skip("Genz-Malik cubature does not support 1D integrals")
|
|
|
|
if points is not None:
|
|
points = [xp.asarray(point, dtype=xp.float64) for point in points]
|
|
|
|
res = cubature(
|
|
f,
|
|
a,
|
|
b,
|
|
rule=rule,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
points=points,
|
|
args=(xp,),
|
|
)
|
|
|
|
assert res.status == "converged"
|
|
|
|
xp_assert_close(
|
|
res.estimate,
|
|
exact,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
err_msg=f"error_estimate={res.error}, subdivisions={res.subdivisions}",
|
|
check_0d=False,
|
|
)
|
|
|
|
|
|
@array_api_compatible
|
|
class TestRules:
|
|
"""
|
|
Tests related to the general Rule interface (currently private).
|
|
"""
|
|
|
|
@pytest.mark.parametrize("problem", [
|
|
(
|
|
# 2D problem, 1D rule
|
|
[0, 0],
|
|
[1, 1],
|
|
GaussKronrodQuadrature,
|
|
(21,),
|
|
),
|
|
(
|
|
# 1D problem, 2D rule
|
|
[0],
|
|
[1],
|
|
GenzMalikCubature,
|
|
(2,),
|
|
)
|
|
])
|
|
def test_incompatible_dimension_raises_error(self, problem, xp):
|
|
a, b, quadrature, quadrature_args = problem
|
|
rule = quadrature(*quadrature_args, xp=xp)
|
|
|
|
a = xp.asarray(a, dtype=xp.float64)
|
|
b = xp.asarray(b, dtype=xp.float64)
|
|
|
|
with pytest.raises(Exception, match="incompatible dimension"):
|
|
rule.estimate(basic_1d_integrand, a, b, args=(xp,))
|
|
|
|
def test_estimate_with_base_classes_raise_error(self, xp):
|
|
a = xp.asarray([0])
|
|
b = xp.asarray([1])
|
|
|
|
for base_class in [Rule(), FixedRule()]:
|
|
with pytest.raises(Exception):
|
|
base_class.estimate(basic_1d_integrand, a, b, args=(xp,))
|
|
|
|
|
|
@array_api_compatible
|
|
class TestRulesQuadrature:
|
|
"""
|
|
Tests underlying quadrature rules (ndim == 1).
|
|
"""
|
|
|
|
@pytest.mark.parametrize(("rule", "rule_args"), [
|
|
(GaussLegendreQuadrature, (3,)),
|
|
(GaussLegendreQuadrature, (5,)),
|
|
(GaussLegendreQuadrature, (10,)),
|
|
(GaussKronrodQuadrature, (15,)),
|
|
(GaussKronrodQuadrature, (21,)),
|
|
])
|
|
def test_base_1d_quadratures_simple(self, rule, rule_args, xp):
|
|
quadrature = rule(*rule_args, xp=xp)
|
|
|
|
n = xp.arange(5, dtype=xp.float64)
|
|
|
|
def f(x):
|
|
x_reshaped = xp.reshape(x, (-1, 1, 1))
|
|
n_reshaped = xp.reshape(n, (1, -1, 1))
|
|
|
|
return x_reshaped**n_reshaped
|
|
|
|
a = xp.asarray([0], dtype=xp.float64)
|
|
b = xp.asarray([2], dtype=xp.float64)
|
|
|
|
exact = xp.reshape(2**(n+1)/(n+1), (-1, 1))
|
|
estimate = quadrature.estimate(f, a, b)
|
|
|
|
xp_assert_close(
|
|
estimate,
|
|
exact,
|
|
rtol=1e-8,
|
|
atol=0,
|
|
)
|
|
|
|
@pytest.mark.parametrize(("rule_pair", "rule_pair_args"), [
|
|
((GaussLegendreQuadrature, GaussLegendreQuadrature), (10, 5)),
|
|
])
|
|
def test_base_1d_quadratures_error_from_difference(self, rule_pair, rule_pair_args,
|
|
xp):
|
|
n = xp.arange(5, dtype=xp.float64)
|
|
a = xp.asarray([0], dtype=xp.float64)
|
|
b = xp.asarray([2], dtype=xp.float64)
|
|
|
|
higher = rule_pair[0](rule_pair_args[0], xp=xp)
|
|
lower = rule_pair[1](rule_pair_args[1], xp=xp)
|
|
|
|
rule = NestedFixedRule(higher, lower)
|
|
res = cubature(
|
|
basic_1d_integrand,
|
|
a, b,
|
|
rule=rule,
|
|
rtol=1e-8,
|
|
args=(n, xp),
|
|
)
|
|
|
|
xp_assert_close(
|
|
res.estimate,
|
|
basic_1d_integrand_exact(n, xp),
|
|
rtol=1e-8,
|
|
atol=0,
|
|
)
|
|
|
|
@pytest.mark.parametrize("quadrature", [
|
|
GaussLegendreQuadrature
|
|
])
|
|
def test_one_point_fixed_quad_impossible(self, quadrature, xp):
|
|
with pytest.raises(Exception):
|
|
quadrature(1, xp=xp)
|
|
|
|
|
|
@array_api_compatible
|
|
class TestRulesCubature:
|
|
"""
|
|
Tests underlying cubature rules (ndim >= 2).
|
|
"""
|
|
|
|
@pytest.mark.parametrize("ndim", range(2, 11))
|
|
def test_genz_malik_func_evaluations(self, ndim, xp):
|
|
"""
|
|
Tests that the number of function evaluations required for Genz-Malik cubature
|
|
matches the number in Genz and Malik 1980.
|
|
"""
|
|
|
|
nodes, _ = GenzMalikCubature(ndim, xp=xp).nodes_and_weights
|
|
|
|
assert nodes.shape[0] == (2**ndim) + 2*ndim**2 + 2*ndim + 1
|
|
|
|
def test_genz_malik_1d_raises_error(self, xp):
|
|
with pytest.raises(Exception, match="only defined for ndim >= 2"):
|
|
GenzMalikCubature(1, xp=xp)
|
|
|
|
|
|
@array_api_compatible
|
|
@skip_xp_backends(
|
|
"jax.numpy",
|
|
reasons=["transforms make use of indexing assignment"],
|
|
)
|
|
class TestTransformations:
|
|
@pytest.mark.parametrize(("a", "b", "points"), [
|
|
(
|
|
[0, 1, -math.inf],
|
|
[1, math.inf, math.inf],
|
|
[
|
|
[1, 1, 1],
|
|
[0.5, 10, 10],
|
|
]
|
|
)
|
|
])
|
|
def test_infinite_limits_maintains_points(self, a, b, points, xp):
|
|
"""
|
|
Test that break points are correctly mapped under the _InfiniteLimitsTransform
|
|
transformation.
|
|
"""
|
|
|
|
xp_compat = array_namespace(xp.empty(0))
|
|
points = [xp.asarray(p, dtype=xp.float64) for p in points]
|
|
|
|
f_transformed = _InfiniteLimitsTransform(
|
|
# Bind `points` and `xp` argument in f
|
|
lambda x: f_with_problematic_points(x, points, xp_compat),
|
|
xp.asarray(a, dtype=xp_compat.float64),
|
|
xp.asarray(b, dtype=xp_compat.float64),
|
|
xp=xp_compat,
|
|
)
|
|
|
|
for point in points:
|
|
transformed_point = f_transformed.inv(xp_compat.reshape(point, (1, -1)))
|
|
|
|
with pytest.raises(Exception, match="called with a problematic point"):
|
|
f_transformed(transformed_point)
|
|
|
|
|
|
class BadErrorRule(Rule):
|
|
"""
|
|
A rule with fake high error so that cubature will keep on subdividing.
|
|
"""
|
|
|
|
def estimate(self, f, a, b, args=()):
|
|
xp = array_namespace(a, b)
|
|
underlying = GaussLegendreQuadrature(10, xp=xp)
|
|
|
|
return underlying.estimate(f, a, b, args)
|
|
|
|
def estimate_error(self, f, a, b, args=()):
|
|
xp = array_namespace(a, b)
|
|
return xp.asarray(1e6, dtype=xp.float64)
|