| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376 |
- 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,
- make_xp_test_case,
- )
- from scipy.integrate import cubature
- from scipy.integrate._cubature import _InfiniteLimitsTransform
- from scipy.integrate._rules import (
- Rule, FixedRule,
- NestedFixedRule,
- GaussLegendreQuadrature, GaussKronrodQuadrature,
- GenzMalikCubature,
- )
- skip_xp_backends = pytest.mark.skip_xp_backends
- boolean_index_skip_reason = 'JAX/Dask arrays do not support boolean assignment.'
- # 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))
- return (
- (-1)**ndim * 1/xp.prod(alphas, axis=-1)
- * xp.prod(
- xp.atan((a - betas)/alphas) - xp.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(
- [float(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])
- @make_xp_test_case(cubature)
- 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,
- )
- def test_pass_array_like_not_array(self):
- n = np_compat.arange(5, dtype=np_compat.float64)
- a = [0]
- b = [2]
- res = cubature(
- basic_1d_integrand,
- a,
- b,
- args=(n, np_compat)
- )
- xp_assert_close(
- res.estimate,
- basic_1d_integrand_exact(n, np_compat),
- 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
- @make_xp_test_case(cubature)
- @pytest.mark.parametrize("rtol", [1e-4])
- @pytest.mark.parametrize("atol", [1e-5])
- @pytest.mark.parametrize("rule", [
- "gk15",
- "gk21",
- "genz-malik",
- ])
- class TestCubatureProblems:
- """
- Tests that `cubature` gives the correct answer.
- """
- @skip_xp_backends("dask.array",
- reason="Dask hangs/takes a long time for some test cases")
- @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}",
- )
- @skip_xp_backends("dask.array",
- reason="Dask hangs/takes a long time for some test cases")
- @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
- @pytest.mark.skip_xp_backends('jax.numpy', reason=boolean_index_skip_reason)
- @pytest.mark.skip_xp_backends('dask.array', reason=boolean_index_skip_reason)
- @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,
- )
- @pytest.mark.skip_xp_backends('jax.numpy', reason=boolean_index_skip_reason)
- @pytest.mark.skip_xp_backends('dask.array', reason=boolean_index_skip_reason)
- @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,
- )
- 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,))
- 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)
- 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)
- @pytest.mark.skip_xp_backends('jax.numpy', reason=boolean_index_skip_reason)
- @pytest.mark.skip_xp_backends('dask.array', reason=boolean_index_skip_reason)
- 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.
- """
- 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),
- xp.asarray(a, dtype=xp.float64),
- xp.asarray(b, dtype=xp.float64),
- xp=xp,
- )
- for point in points:
- transformed_point = f_transformed.inv(xp.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)
|