test_cubature.py 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376
  1. import math
  2. import scipy
  3. import itertools
  4. import pytest
  5. from scipy._lib._array_api import (
  6. array_namespace,
  7. xp_assert_close,
  8. xp_size,
  9. np_compat,
  10. is_array_api_strict,
  11. make_xp_test_case,
  12. )
  13. from scipy.integrate import cubature
  14. from scipy.integrate._cubature import _InfiniteLimitsTransform
  15. from scipy.integrate._rules import (
  16. Rule, FixedRule,
  17. NestedFixedRule,
  18. GaussLegendreQuadrature, GaussKronrodQuadrature,
  19. GenzMalikCubature,
  20. )
  21. skip_xp_backends = pytest.mark.skip_xp_backends
  22. boolean_index_skip_reason = 'JAX/Dask arrays do not support boolean assignment.'
  23. # The integrands ``genz_malik_1980_*`` come from the paper:
  24. # A.C. Genz, A.A. Malik, Remarks on algorithm 006: An adaptive algorithm for
  25. # numerical integration over an N-dimensional rectangular region, Journal of
  26. # Computational and Applied Mathematics, Volume 6, Issue 4, 1980, Pages 295-302,
  27. # ISSN 0377-0427, https://doi.org/10.1016/0771-050X(80)90039-X.
  28. def basic_1d_integrand(x, n, xp):
  29. x_reshaped = xp.reshape(x, (-1, 1, 1))
  30. n_reshaped = xp.reshape(n, (1, -1, 1))
  31. return x_reshaped**n_reshaped
  32. def basic_1d_integrand_exact(n, xp):
  33. # Exact only for integration over interval [0, 2].
  34. return xp.reshape(2**(n+1)/(n+1), (-1, 1))
  35. def basic_nd_integrand(x, n, xp):
  36. return xp.reshape(xp.sum(x, axis=-1), (-1, 1))**xp.reshape(n, (1, -1))
  37. def basic_nd_integrand_exact(n, xp):
  38. # Exact only for integration over interval [0, 2].
  39. return (-2**(3+n) + 4**(2+n))/((1+n)*(2+n))
  40. def genz_malik_1980_f_1(x, r, alphas, xp):
  41. r"""
  42. .. math:: f_1(\mathbf x) = \cos\left(2\pi r + \sum^n_{i = 1}\alpha_i x_i\right)
  43. .. code-block:: mathematica
  44. genzMalik1980f1[x_List, r_, alphas_List] := Cos[2*Pi*r + Total[x*alphas]]
  45. """
  46. npoints, ndim = x.shape[0], x.shape[-1]
  47. alphas_reshaped = alphas[None, ...]
  48. x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
  49. return xp.cos(2*math.pi*r + xp.sum(alphas_reshaped * x_reshaped, axis=-1))
  50. def genz_malik_1980_f_1_exact(a, b, r, alphas, xp):
  51. ndim = xp_size(a)
  52. a = xp.reshape(a, (*([1]*(len(alphas.shape) - 1)), ndim))
  53. b = xp.reshape(b, (*([1]*(len(alphas.shape) - 1)), ndim))
  54. return (
  55. (-2)**ndim
  56. * 1/xp.prod(alphas, axis=-1)
  57. * xp.cos(2*math.pi*r + xp.sum(alphas * (a+b) * 0.5, axis=-1))
  58. * xp.prod(xp.sin(alphas * (a-b)/2), axis=-1)
  59. )
  60. def genz_malik_1980_f_1_random_args(rng, shape, xp):
  61. r = xp.asarray(rng.random(shape[:-1]))
  62. alphas = xp.asarray(rng.random(shape))
  63. difficulty = 9
  64. normalisation_factors = xp.sum(alphas, axis=-1)[..., None]
  65. alphas = difficulty * alphas / normalisation_factors
  66. return (r, alphas)
  67. def genz_malik_1980_f_2(x, alphas, betas, xp):
  68. r"""
  69. .. math:: f_2(\mathbf x) = \prod^n_{i = 1} (\alpha_i^2 + (x_i - \beta_i)^2)^{-1}
  70. .. code-block:: mathematica
  71. genzMalik1980f2[x_List, alphas_List, betas_List] :=
  72. 1/Times @@ ((alphas^2 + (x - betas)^2))
  73. """
  74. npoints, ndim = x.shape[0], x.shape[-1]
  75. alphas_reshaped = alphas[None, ...]
  76. betas_reshaped = betas[None, ...]
  77. x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
  78. return 1/xp.prod(alphas_reshaped**2 + (x_reshaped-betas_reshaped)**2, axis=-1)
  79. def genz_malik_1980_f_2_exact(a, b, alphas, betas, xp):
  80. ndim = xp_size(a)
  81. a = xp.reshape(a, (*([1]*(len(alphas.shape) - 1)), ndim))
  82. b = xp.reshape(b, (*([1]*(len(alphas.shape) - 1)), ndim))
  83. return (
  84. (-1)**ndim * 1/xp.prod(alphas, axis=-1)
  85. * xp.prod(
  86. xp.atan((a - betas)/alphas) - xp.atan((b - betas)/alphas),
  87. axis=-1,
  88. )
  89. )
  90. def genz_malik_1980_f_2_random_args(rng, shape, xp):
  91. ndim = shape[-1]
  92. alphas = xp.asarray(rng.random(shape))
  93. betas = xp.asarray(rng.random(shape))
  94. difficulty = 25.0
  95. products = xp.prod(alphas**xp.asarray(-2.0), axis=-1)
  96. normalisation_factors = (products**xp.asarray(1 / (2*ndim)))[..., None]
  97. alphas = alphas * normalisation_factors * math.pow(difficulty, 1 / (2*ndim))
  98. # Adjust alphas from distribution used in Genz and Malik 1980 since denominator
  99. # is very small for high dimensions.
  100. alphas *= 10
  101. return alphas, betas
  102. def genz_malik_1980_f_3(x, alphas, xp):
  103. r"""
  104. .. math:: f_3(\mathbf x) = \exp\left(\sum^n_{i = 1} \alpha_i x_i\right)
  105. .. code-block:: mathematica
  106. genzMalik1980f3[x_List, alphas_List] := Exp[Dot[x, alphas]]
  107. """
  108. npoints, ndim = x.shape[0], x.shape[-1]
  109. alphas_reshaped = alphas[None, ...]
  110. x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
  111. return xp.exp(xp.sum(alphas_reshaped * x_reshaped, axis=-1))
  112. def genz_malik_1980_f_3_exact(a, b, alphas, xp):
  113. ndim = xp_size(a)
  114. a = xp.reshape(a, (*([1]*(len(alphas.shape) - 1)), ndim))
  115. b = xp.reshape(b, (*([1]*(len(alphas.shape) - 1)), ndim))
  116. return (
  117. (-1)**ndim * 1/xp.prod(alphas, axis=-1)
  118. * xp.prod(xp.exp(alphas * a) - xp.exp(alphas * b), axis=-1)
  119. )
  120. def genz_malik_1980_f_3_random_args(rng, shape, xp):
  121. alphas = xp.asarray(rng.random(shape))
  122. normalisation_factors = xp.sum(alphas, axis=-1)[..., None]
  123. difficulty = 12.0
  124. alphas = difficulty * alphas / normalisation_factors
  125. return (alphas,)
  126. def genz_malik_1980_f_4(x, alphas, xp):
  127. r"""
  128. .. math:: f_4(\mathbf x) = \left(1 + \sum^n_{i = 1} \alpha_i x_i\right)^{-n-1}
  129. .. code-block:: mathematica
  130. genzMalik1980f4[x_List, alphas_List] :=
  131. (1 + Dot[x, alphas])^(-Length[alphas] - 1)
  132. """
  133. npoints, ndim = x.shape[0], x.shape[-1]
  134. alphas_reshaped = alphas[None, ...]
  135. x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
  136. return (1 + xp.sum(alphas_reshaped * x_reshaped, axis=-1))**(-ndim-1)
  137. def genz_malik_1980_f_4_exact(a, b, alphas, xp):
  138. ndim = xp_size(a)
  139. def F(x):
  140. x_reshaped = xp.reshape(x, (*([1]*(len(alphas.shape) - 1)), ndim))
  141. return (
  142. (-1)**ndim/xp.prod(alphas, axis=-1)
  143. / math.factorial(ndim)
  144. / (1 + xp.sum(alphas * x_reshaped, axis=-1))
  145. )
  146. return _eval_indefinite_integral(F, a, b, xp)
  147. def _eval_indefinite_integral(F, a, b, xp):
  148. """
  149. Calculates a definite integral from points `a` to `b` by summing up over the corners
  150. of the corresponding hyperrectangle.
  151. """
  152. ndim = xp_size(a)
  153. points = xp.stack([a, b], axis=0)
  154. out = 0
  155. for ind in itertools.product(range(2), repeat=ndim):
  156. selected_points = xp.asarray(
  157. [float(points[i, j]) for i, j in zip(ind, range(ndim))]
  158. )
  159. out += pow(-1, sum(ind) + ndim) * F(selected_points)
  160. return out
  161. def genz_malik_1980_f_4_random_args(rng, shape, xp):
  162. ndim = shape[-1]
  163. alphas = xp.asarray(rng.random(shape))
  164. normalisation_factors = xp.sum(alphas, axis=-1)[..., None]
  165. difficulty = 14.0
  166. alphas = (difficulty / ndim) * alphas / normalisation_factors
  167. return (alphas,)
  168. def genz_malik_1980_f_5(x, alphas, betas, xp):
  169. r"""
  170. .. math::
  171. f_5(\mathbf x) = \exp\left(-\sum^n_{i = 1} \alpha^2_i (x_i - \beta_i)^2\right)
  172. .. code-block:: mathematica
  173. genzMalik1980f5[x_List, alphas_List, betas_List] :=
  174. Exp[-Total[alphas^2 * (x - betas)^2]]
  175. """
  176. npoints, ndim = x.shape[0], x.shape[-1]
  177. alphas_reshaped = alphas[None, ...]
  178. betas_reshaped = betas[None, ...]
  179. x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
  180. return xp.exp(
  181. -xp.sum(alphas_reshaped**2 * (x_reshaped - betas_reshaped)**2, axis=-1)
  182. )
  183. def genz_malik_1980_f_5_exact(a, b, alphas, betas, xp):
  184. ndim = xp_size(a)
  185. a = xp.reshape(a, (*([1]*(len(alphas.shape) - 1)), ndim))
  186. b = xp.reshape(b, (*([1]*(len(alphas.shape) - 1)), ndim))
  187. return (
  188. (1/2)**ndim
  189. * 1/xp.prod(alphas, axis=-1)
  190. * (math.pi**(ndim/2))
  191. * xp.prod(
  192. scipy.special.erf(alphas * (betas - a))
  193. + scipy.special.erf(alphas * (b - betas)),
  194. axis=-1,
  195. )
  196. )
  197. def genz_malik_1980_f_5_random_args(rng, shape, xp):
  198. alphas = xp.asarray(rng.random(shape))
  199. betas = xp.asarray(rng.random(shape))
  200. difficulty = 21.0
  201. normalisation_factors = xp.sqrt(xp.sum(alphas**xp.asarray(2.0), axis=-1))[..., None]
  202. alphas = alphas / normalisation_factors * math.sqrt(difficulty)
  203. return alphas, betas
  204. def f_gaussian(x, alphas, xp):
  205. r"""
  206. .. math::
  207. f(\mathbf x) = \exp\left(-\sum^n_{i = 1} (\alpha_i x_i)^2 \right)
  208. """
  209. npoints, ndim = x.shape[0], x.shape[-1]
  210. alphas_reshaped = alphas[None, ...]
  211. x_reshaped = xp.reshape(x, (npoints, *([1]*(len(alphas.shape) - 1)), ndim))
  212. return xp.exp(-xp.sum((alphas_reshaped * x_reshaped)**2, axis=-1))
  213. def f_gaussian_exact(a, b, alphas, xp):
  214. # Exact only when `a` and `b` are one of:
  215. # (-oo, oo), or
  216. # (0, oo), or
  217. # (-oo, 0)
  218. # `alphas` can be arbitrary.
  219. ndim = xp_size(a)
  220. double_infinite_count = 0
  221. semi_infinite_count = 0
  222. for i in range(ndim):
  223. if xp.isinf(a[i]) and xp.isinf(b[i]): # doubly-infinite
  224. double_infinite_count += 1
  225. elif xp.isinf(a[i]) != xp.isinf(b[i]): # exclusive or, so semi-infinite
  226. semi_infinite_count += 1
  227. return (math.sqrt(math.pi) ** ndim) / (
  228. 2**semi_infinite_count * xp.prod(alphas, axis=-1)
  229. )
  230. def f_gaussian_random_args(rng, shape, xp):
  231. alphas = xp.asarray(rng.random(shape))
  232. # If alphas are very close to 0 this makes the problem very difficult due to large
  233. # values of ``f``.
  234. alphas *= 100
  235. return (alphas,)
  236. def f_modified_gaussian(x_arr, n, xp):
  237. r"""
  238. .. math::
  239. f(x, y, z, w) = x^n \sqrt{y} \exp(-y-z^2-w^2)
  240. """
  241. x, y, z, w = x_arr[:, 0], x_arr[:, 1], x_arr[:, 2], x_arr[:, 3]
  242. res = (x ** n[:, None]) * xp.sqrt(y) * xp.exp(-y-z**2-w**2)
  243. return res.T
  244. def f_modified_gaussian_exact(a, b, n, xp):
  245. # Exact only for the limits
  246. # a = (0, 0, -oo, -oo)
  247. # b = (1, oo, oo, oo)
  248. # but defined here as a function to match the format of the other integrands.
  249. return 1/(2 + 2*n) * math.pi ** (3/2)
  250. def f_with_problematic_points(x_arr, points, xp):
  251. """
  252. This emulates a function with a list of singularities given by `points`.
  253. If no `x_arr` are one of the `points`, then this function returns 1.
  254. """
  255. for point in points:
  256. if xp.any(x_arr == point):
  257. raise ValueError("called with a problematic point")
  258. return xp.ones(x_arr.shape[0])
  259. @make_xp_test_case(cubature)
  260. class TestCubature:
  261. """
  262. Tests related to the interface of `cubature`.
  263. """
  264. @pytest.mark.parametrize("rule_str", [
  265. "gauss-kronrod",
  266. "genz-malik",
  267. "gk21",
  268. "gk15",
  269. ])
  270. def test_pass_str(self, rule_str, xp):
  271. n = xp.arange(5, dtype=xp.float64)
  272. a = xp.asarray([0, 0], dtype=xp.float64)
  273. b = xp.asarray([2, 2], dtype=xp.float64)
  274. res = cubature(basic_nd_integrand, a, b, rule=rule_str, args=(n, xp))
  275. xp_assert_close(
  276. res.estimate,
  277. basic_nd_integrand_exact(n, xp),
  278. rtol=1e-8,
  279. atol=0,
  280. )
  281. def test_pass_array_like_not_array(self):
  282. n = np_compat.arange(5, dtype=np_compat.float64)
  283. a = [0]
  284. b = [2]
  285. res = cubature(
  286. basic_1d_integrand,
  287. a,
  288. b,
  289. args=(n, np_compat)
  290. )
  291. xp_assert_close(
  292. res.estimate,
  293. basic_1d_integrand_exact(n, np_compat),
  294. rtol=1e-8,
  295. atol=0,
  296. )
  297. def test_stops_after_max_subdivisions(self, xp):
  298. a = xp.asarray([0])
  299. b = xp.asarray([1])
  300. rule = BadErrorRule()
  301. res = cubature(
  302. basic_1d_integrand, # Any function would suffice
  303. a,
  304. b,
  305. rule=rule,
  306. max_subdivisions=10,
  307. args=(xp.arange(5, dtype=xp.float64), xp),
  308. )
  309. assert res.subdivisions == 10
  310. assert res.status == "not_converged"
  311. def test_a_and_b_must_be_1d(self, xp):
  312. a = xp.asarray([[0]], dtype=xp.float64)
  313. b = xp.asarray([[1]], dtype=xp.float64)
  314. with pytest.raises(Exception, match="`a` and `b` must be 1D arrays"):
  315. cubature(basic_1d_integrand, a, b, args=(xp,))
  316. def test_a_and_b_must_be_nonempty(self, xp):
  317. a = xp.asarray([])
  318. b = xp.asarray([])
  319. with pytest.raises(Exception, match="`a` and `b` must be nonempty"):
  320. cubature(basic_1d_integrand, a, b, args=(xp,))
  321. def test_zero_width_limits(self, xp):
  322. n = xp.arange(5, dtype=xp.float64)
  323. a = xp.asarray([0], dtype=xp.float64)
  324. b = xp.asarray([0], dtype=xp.float64)
  325. res = cubature(
  326. basic_1d_integrand,
  327. a,
  328. b,
  329. args=(n, xp),
  330. )
  331. xp_assert_close(
  332. res.estimate,
  333. xp.asarray([[0], [0], [0], [0], [0]], dtype=xp.float64),
  334. rtol=1e-8,
  335. atol=0,
  336. )
  337. def test_limits_other_way_around(self, xp):
  338. n = xp.arange(5, dtype=xp.float64)
  339. a = xp.asarray([2], dtype=xp.float64)
  340. b = xp.asarray([0], dtype=xp.float64)
  341. res = cubature(
  342. basic_1d_integrand,
  343. a,
  344. b,
  345. args=(n, xp),
  346. )
  347. xp_assert_close(
  348. res.estimate,
  349. -basic_1d_integrand_exact(n, xp),
  350. rtol=1e-8,
  351. atol=0,
  352. )
  353. def test_result_dtype_promoted_correctly(self, xp):
  354. result_dtype = cubature(
  355. basic_1d_integrand,
  356. xp.asarray([0], dtype=xp.float64),
  357. xp.asarray([1], dtype=xp.float64),
  358. points=[],
  359. args=(xp.asarray([1], dtype=xp.float64), xp),
  360. ).estimate.dtype
  361. assert result_dtype == xp.float64
  362. result_dtype = cubature(
  363. basic_1d_integrand,
  364. xp.asarray([0], dtype=xp.float32),
  365. xp.asarray([1], dtype=xp.float32),
  366. points=[],
  367. args=(xp.asarray([1], dtype=xp.float32), xp),
  368. ).estimate.dtype
  369. assert result_dtype == xp.float32
  370. result_dtype = cubature(
  371. basic_1d_integrand,
  372. xp.asarray([0], dtype=xp.float32),
  373. xp.asarray([1], dtype=xp.float64),
  374. points=[],
  375. args=(xp.asarray([1], dtype=xp.float32), xp),
  376. ).estimate.dtype
  377. assert result_dtype == xp.float64
  378. @make_xp_test_case(cubature)
  379. @pytest.mark.parametrize("rtol", [1e-4])
  380. @pytest.mark.parametrize("atol", [1e-5])
  381. @pytest.mark.parametrize("rule", [
  382. "gk15",
  383. "gk21",
  384. "genz-malik",
  385. ])
  386. class TestCubatureProblems:
  387. """
  388. Tests that `cubature` gives the correct answer.
  389. """
  390. @skip_xp_backends("dask.array",
  391. reason="Dask hangs/takes a long time for some test cases")
  392. @pytest.mark.parametrize("problem", [
  393. # -- f1 --
  394. (
  395. # Function to integrate, like `f(x, *args)`
  396. genz_malik_1980_f_1,
  397. # Exact solution, like `exact(a, b, *args)`
  398. genz_malik_1980_f_1_exact,
  399. # Coordinates of `a`
  400. [0],
  401. # Coordinates of `b`
  402. [10],
  403. # Arguments to pass to `f` and `exact`
  404. (
  405. 1/4,
  406. [5],
  407. )
  408. ),
  409. (
  410. genz_malik_1980_f_1,
  411. genz_malik_1980_f_1_exact,
  412. [0, 0],
  413. [1, 1],
  414. (
  415. 1/4,
  416. [2, 4],
  417. ),
  418. ),
  419. (
  420. genz_malik_1980_f_1,
  421. genz_malik_1980_f_1_exact,
  422. [0, 0],
  423. [5, 5],
  424. (
  425. 1/2,
  426. [2, 4],
  427. )
  428. ),
  429. (
  430. genz_malik_1980_f_1,
  431. genz_malik_1980_f_1_exact,
  432. [0, 0, 0],
  433. [5, 5, 5],
  434. (
  435. 1/2,
  436. [1, 1, 1],
  437. )
  438. ),
  439. # -- f2 --
  440. (
  441. genz_malik_1980_f_2,
  442. genz_malik_1980_f_2_exact,
  443. [-1],
  444. [1],
  445. (
  446. [5],
  447. [4],
  448. )
  449. ),
  450. (
  451. genz_malik_1980_f_2,
  452. genz_malik_1980_f_2_exact,
  453. [0, 0],
  454. [10, 50],
  455. (
  456. [-3, 3],
  457. [-2, 2],
  458. ),
  459. ),
  460. (
  461. genz_malik_1980_f_2,
  462. genz_malik_1980_f_2_exact,
  463. [0, 0, 0],
  464. [1, 1, 1],
  465. (
  466. [1, 1, 1],
  467. [1, 1, 1],
  468. )
  469. ),
  470. (
  471. genz_malik_1980_f_2,
  472. genz_malik_1980_f_2_exact,
  473. [0, 0, 0],
  474. [1, 1, 1],
  475. (
  476. [2, 3, 4],
  477. [2, 3, 4],
  478. )
  479. ),
  480. (
  481. genz_malik_1980_f_2,
  482. genz_malik_1980_f_2_exact,
  483. [-1, -1, -1],
  484. [1, 1, 1],
  485. (
  486. [1, 1, 1],
  487. [2, 2, 2],
  488. )
  489. ),
  490. (
  491. genz_malik_1980_f_2,
  492. genz_malik_1980_f_2_exact,
  493. [-1, -1, -1, -1],
  494. [1, 1, 1, 1],
  495. (
  496. [1, 1, 1, 1],
  497. [1, 1, 1, 1],
  498. )
  499. ),
  500. # -- f3 --
  501. (
  502. genz_malik_1980_f_3,
  503. genz_malik_1980_f_3_exact,
  504. [-1],
  505. [1],
  506. (
  507. [1/2],
  508. ),
  509. ),
  510. (
  511. genz_malik_1980_f_3,
  512. genz_malik_1980_f_3_exact,
  513. [0, -1],
  514. [1, 1],
  515. (
  516. [5, 5],
  517. ),
  518. ),
  519. (
  520. genz_malik_1980_f_3,
  521. genz_malik_1980_f_3_exact,
  522. [-1, -1, -1],
  523. [1, 1, 1],
  524. (
  525. [1, 1, 1],
  526. ),
  527. ),
  528. # -- f4 --
  529. (
  530. genz_malik_1980_f_4,
  531. genz_malik_1980_f_4_exact,
  532. [0],
  533. [2],
  534. (
  535. [1],
  536. ),
  537. ),
  538. (
  539. genz_malik_1980_f_4,
  540. genz_malik_1980_f_4_exact,
  541. [0, 0],
  542. [2, 1],
  543. ([1, 1],),
  544. ),
  545. (
  546. genz_malik_1980_f_4,
  547. genz_malik_1980_f_4_exact,
  548. [0, 0, 0],
  549. [1, 1, 1],
  550. ([1, 1, 1],),
  551. ),
  552. # -- f5 --
  553. (
  554. genz_malik_1980_f_5,
  555. genz_malik_1980_f_5_exact,
  556. [-1],
  557. [1],
  558. (
  559. [-2],
  560. [2],
  561. ),
  562. ),
  563. (
  564. genz_malik_1980_f_5,
  565. genz_malik_1980_f_5_exact,
  566. [-1, -1],
  567. [1, 1],
  568. (
  569. [2, 3],
  570. [4, 5],
  571. ),
  572. ),
  573. (
  574. genz_malik_1980_f_5,
  575. genz_malik_1980_f_5_exact,
  576. [-1, -1],
  577. [1, 1],
  578. (
  579. [-1, 1],
  580. [0, 0],
  581. ),
  582. ),
  583. (
  584. genz_malik_1980_f_5,
  585. genz_malik_1980_f_5_exact,
  586. [-1, -1, -1],
  587. [1, 1, 1],
  588. (
  589. [1, 1, 1],
  590. [1, 1, 1],
  591. ),
  592. ),
  593. ])
  594. def test_scalar_output(self, problem, rule, rtol, atol, xp):
  595. f, exact, a, b, args = problem
  596. a = xp.asarray(a, dtype=xp.float64)
  597. b = xp.asarray(b, dtype=xp.float64)
  598. args = tuple(xp.asarray(arg, dtype=xp.float64) for arg in args)
  599. ndim = xp_size(a)
  600. if rule == "genz-malik" and ndim < 2:
  601. pytest.skip("Genz-Malik cubature does not support 1D integrals")
  602. res = cubature(
  603. f,
  604. a,
  605. b,
  606. rule=rule,
  607. rtol=rtol,
  608. atol=atol,
  609. args=(*args, xp),
  610. )
  611. assert res.status == "converged"
  612. est = res.estimate
  613. exact_sol = exact(a, b, *args, xp)
  614. xp_assert_close(
  615. est,
  616. exact_sol,
  617. rtol=rtol,
  618. atol=atol,
  619. err_msg=f"estimate_error={res.error}, subdivisions={res.subdivisions}",
  620. )
  621. @skip_xp_backends("dask.array",
  622. reason="Dask hangs/takes a long time for some test cases")
  623. @pytest.mark.parametrize("problem", [
  624. (
  625. # Function to integrate, like `f(x, *args)`
  626. genz_malik_1980_f_1,
  627. # Exact solution, like `exact(a, b, *args)`
  628. genz_malik_1980_f_1_exact,
  629. # Function that generates random args of a certain shape.
  630. genz_malik_1980_f_1_random_args,
  631. ),
  632. (
  633. genz_malik_1980_f_2,
  634. genz_malik_1980_f_2_exact,
  635. genz_malik_1980_f_2_random_args,
  636. ),
  637. (
  638. genz_malik_1980_f_3,
  639. genz_malik_1980_f_3_exact,
  640. genz_malik_1980_f_3_random_args
  641. ),
  642. (
  643. genz_malik_1980_f_4,
  644. genz_malik_1980_f_4_exact,
  645. genz_malik_1980_f_4_random_args
  646. ),
  647. (
  648. genz_malik_1980_f_5,
  649. genz_malik_1980_f_5_exact,
  650. genz_malik_1980_f_5_random_args,
  651. ),
  652. ])
  653. @pytest.mark.parametrize("shape", [
  654. (2,),
  655. (3,),
  656. (4,),
  657. (1, 2),
  658. (1, 3),
  659. (1, 4),
  660. (3, 2),
  661. (3, 4, 2),
  662. (2, 1, 3),
  663. ])
  664. def test_array_output(self, problem, rule, shape, rtol, atol, xp):
  665. rng = np_compat.random.default_rng(1)
  666. ndim = shape[-1]
  667. if rule == "genz-malik" and ndim < 2:
  668. pytest.skip("Genz-Malik cubature does not support 1D integrals")
  669. if rule == "genz-malik" and ndim >= 5:
  670. pytest.mark.slow("Gauss-Kronrod is slow in >= 5 dim")
  671. f, exact, random_args = problem
  672. args = random_args(rng, shape, xp)
  673. a = xp.asarray([0] * ndim, dtype=xp.float64)
  674. b = xp.asarray([1] * ndim, dtype=xp.float64)
  675. res = cubature(
  676. f,
  677. a,
  678. b,
  679. rule=rule,
  680. rtol=rtol,
  681. atol=atol,
  682. args=(*args, xp),
  683. )
  684. est = res.estimate
  685. exact_sol = exact(a, b, *args, xp)
  686. xp_assert_close(
  687. est,
  688. exact_sol,
  689. rtol=rtol,
  690. atol=atol,
  691. err_msg=f"estimate_error={res.error}, subdivisions={res.subdivisions}",
  692. )
  693. err_msg = (f"estimate_error={res.error}, "
  694. f"subdivisions= {res.subdivisions}, "
  695. f"true_error={xp.abs(res.estimate - exact_sol)}")
  696. assert res.status == "converged", err_msg
  697. assert res.estimate.shape == shape[:-1]
  698. @pytest.mark.parametrize("problem", [
  699. (
  700. # Function to integrate
  701. lambda x, xp: x,
  702. # Exact value
  703. [50.0],
  704. # Coordinates of `a`
  705. [0],
  706. # Coordinates of `b`
  707. [10],
  708. # Points by which to split up the initial region
  709. None,
  710. ),
  711. (
  712. lambda x, xp: xp.sin(x)/x,
  713. [2.551496047169878], # si(1) + si(2),
  714. [-1],
  715. [2],
  716. [
  717. [0.0],
  718. ],
  719. ),
  720. (
  721. lambda x, xp: xp.ones((x.shape[0], 1)),
  722. [1.0],
  723. [0, 0, 0],
  724. [1, 1, 1],
  725. [
  726. [0.5, 0.5, 0.5],
  727. ],
  728. ),
  729. (
  730. lambda x, xp: xp.ones((x.shape[0], 1)),
  731. [1.0],
  732. [0, 0, 0],
  733. [1, 1, 1],
  734. [
  735. [0.25, 0.25, 0.25],
  736. [0.5, 0.5, 0.5],
  737. ],
  738. ),
  739. (
  740. lambda x, xp: xp.ones((x.shape[0], 1)),
  741. [1.0],
  742. [0, 0, 0],
  743. [1, 1, 1],
  744. [
  745. [0.1, 0.25, 0.5],
  746. [0.25, 0.25, 0.25],
  747. [0.5, 0.5, 0.5],
  748. ],
  749. )
  750. ])
  751. def test_break_points(self, problem, rule, rtol, atol, xp):
  752. f, exact, a, b, points = problem
  753. a = xp.asarray(a, dtype=xp.float64)
  754. b = xp.asarray(b, dtype=xp.float64)
  755. exact = xp.asarray(exact, dtype=xp.float64)
  756. if points is not None:
  757. points = [xp.asarray(point, dtype=xp.float64) for point in points]
  758. ndim = xp_size(a)
  759. if rule == "genz-malik" and ndim < 2:
  760. pytest.skip("Genz-Malik cubature does not support 1D integrals")
  761. if rule == "genz-malik" and ndim >= 5:
  762. pytest.mark.slow("Gauss-Kronrod is slow in >= 5 dim")
  763. res = cubature(
  764. f,
  765. a,
  766. b,
  767. rule=rule,
  768. rtol=rtol,
  769. atol=atol,
  770. points=points,
  771. args=(xp,),
  772. )
  773. xp_assert_close(
  774. res.estimate,
  775. exact,
  776. rtol=rtol,
  777. atol=atol,
  778. err_msg=f"estimate_error={res.error}, subdivisions={res.subdivisions}",
  779. check_dtype=False,
  780. )
  781. err_msg = (f"estimate_error={res.error}, "
  782. f"subdivisions= {res.subdivisions}, "
  783. f"true_error={xp.abs(res.estimate - exact)}")
  784. assert res.status == "converged", err_msg
  785. @pytest.mark.skip_xp_backends('jax.numpy', reason=boolean_index_skip_reason)
  786. @pytest.mark.skip_xp_backends('dask.array', reason=boolean_index_skip_reason)
  787. @pytest.mark.parametrize("problem", [
  788. (
  789. # Function to integrate
  790. f_gaussian,
  791. # Exact solution
  792. f_gaussian_exact,
  793. # Arguments passed to f
  794. f_gaussian_random_args,
  795. (1, 1),
  796. # Limits, have to match the shape of the arguments
  797. [-math.inf], # a
  798. [math.inf], # b
  799. ),
  800. (
  801. f_gaussian,
  802. f_gaussian_exact,
  803. f_gaussian_random_args,
  804. (2, 2),
  805. [-math.inf, -math.inf],
  806. [math.inf, math.inf],
  807. ),
  808. (
  809. f_gaussian,
  810. f_gaussian_exact,
  811. f_gaussian_random_args,
  812. (1, 1),
  813. [0],
  814. [math.inf],
  815. ),
  816. (
  817. f_gaussian,
  818. f_gaussian_exact,
  819. f_gaussian_random_args,
  820. (1, 1),
  821. [-math.inf],
  822. [0],
  823. ),
  824. (
  825. f_gaussian,
  826. f_gaussian_exact,
  827. f_gaussian_random_args,
  828. (2, 2),
  829. [0, 0],
  830. [math.inf, math.inf],
  831. ),
  832. (
  833. f_gaussian,
  834. f_gaussian_exact,
  835. f_gaussian_random_args,
  836. (2, 2),
  837. [0, -math.inf],
  838. [math.inf, math.inf],
  839. ),
  840. (
  841. f_gaussian,
  842. f_gaussian_exact,
  843. f_gaussian_random_args,
  844. (1, 4),
  845. [0, 0, -math.inf, -math.inf],
  846. [math.inf, math.inf, math.inf, math.inf],
  847. ),
  848. (
  849. f_gaussian,
  850. f_gaussian_exact,
  851. f_gaussian_random_args,
  852. (1, 4),
  853. [-math.inf, -math.inf, -math.inf, -math.inf],
  854. [0, 0, math.inf, math.inf],
  855. ),
  856. (
  857. lambda x, xp: 1/xp.prod(x, axis=-1)**2,
  858. # Exact only for the below limits, not for general `a` and `b`.
  859. lambda a, b, xp: xp.asarray(1/6, dtype=xp.float64),
  860. # Arguments
  861. lambda rng, shape, xp: tuple(),
  862. tuple(),
  863. [1, -math.inf, 3],
  864. [math.inf, -2, math.inf],
  865. ),
  866. # This particular problem can be slow
  867. pytest.param(
  868. (
  869. # f(x, y, z, w) = x^n * sqrt(y) * exp(-y-z**2-w**2) for n in [0,1,2,3]
  870. f_modified_gaussian,
  871. # This exact solution is for the below limits, not in general
  872. f_modified_gaussian_exact,
  873. # Constant arguments
  874. lambda rng, shape, xp: (xp.asarray([0, 1, 2, 3, 4], dtype=xp.float64),),
  875. tuple(),
  876. [0, 0, -math.inf, -math.inf],
  877. [1, math.inf, math.inf, math.inf]
  878. ),
  879. marks=pytest.mark.xslow,
  880. ),
  881. ])
  882. def test_infinite_limits(self, problem, rule, rtol, atol, xp):
  883. rng = np_compat.random.default_rng(1)
  884. f, exact, random_args_func, random_args_shape, a, b = problem
  885. a = xp.asarray(a, dtype=xp.float64)
  886. b = xp.asarray(b, dtype=xp.float64)
  887. args = random_args_func(rng, random_args_shape, xp)
  888. ndim = xp_size(a)
  889. if rule == "genz-malik" and ndim < 2:
  890. pytest.skip("Genz-Malik cubature does not support 1D integrals")
  891. if rule == "genz-malik" and ndim >= 4:
  892. pytest.mark.slow("Genz-Malik is slow in >= 5 dim")
  893. if rule == "genz-malik" and ndim >= 4 and is_array_api_strict(xp):
  894. pytest.mark.xslow("Genz-Malik very slow for array_api_strict in >= 4 dim")
  895. res = cubature(
  896. f,
  897. a,
  898. b,
  899. rule=rule,
  900. rtol=rtol,
  901. atol=atol,
  902. args=(*args, xp),
  903. )
  904. assert res.status == "converged"
  905. xp_assert_close(
  906. res.estimate,
  907. exact(a, b, *args, xp),
  908. rtol=rtol,
  909. atol=atol,
  910. err_msg=f"error_estimate={res.error}, subdivisions={res.subdivisions}",
  911. check_0d=False,
  912. )
  913. @pytest.mark.skip_xp_backends('jax.numpy', reason=boolean_index_skip_reason)
  914. @pytest.mark.skip_xp_backends('dask.array', reason=boolean_index_skip_reason)
  915. @pytest.mark.parametrize("problem", [
  916. (
  917. # Function to integrate
  918. lambda x, xp: (xp.sin(x) / x)**8,
  919. # Exact value
  920. [151/315 * math.pi],
  921. # Limits
  922. [-math.inf],
  923. [math.inf],
  924. # Breakpoints
  925. [[0]],
  926. ),
  927. (
  928. # Function to integrate
  929. lambda x, xp: (xp.sin(x[:, 0]) / x[:, 0])**8,
  930. # Exact value
  931. 151/315 * math.pi,
  932. # Limits
  933. [-math.inf, 0],
  934. [math.inf, 1],
  935. # Breakpoints
  936. [[0, 0.5]],
  937. )
  938. ])
  939. def test_infinite_limits_and_break_points(self, problem, rule, rtol, atol, xp):
  940. f, exact, a, b, points = problem
  941. a = xp.asarray(a, dtype=xp.float64)
  942. b = xp.asarray(b, dtype=xp.float64)
  943. exact = xp.asarray(exact, dtype=xp.float64)
  944. ndim = xp_size(a)
  945. if rule == "genz-malik" and ndim < 2:
  946. pytest.skip("Genz-Malik cubature does not support 1D integrals")
  947. if points is not None:
  948. points = [xp.asarray(point, dtype=xp.float64) for point in points]
  949. res = cubature(
  950. f,
  951. a,
  952. b,
  953. rule=rule,
  954. rtol=rtol,
  955. atol=atol,
  956. points=points,
  957. args=(xp,),
  958. )
  959. assert res.status == "converged"
  960. xp_assert_close(
  961. res.estimate,
  962. exact,
  963. rtol=rtol,
  964. atol=atol,
  965. err_msg=f"error_estimate={res.error}, subdivisions={res.subdivisions}",
  966. check_0d=False,
  967. )
  968. class TestRules:
  969. """
  970. Tests related to the general Rule interface (currently private).
  971. """
  972. @pytest.mark.parametrize("problem", [
  973. (
  974. # 2D problem, 1D rule
  975. [0, 0],
  976. [1, 1],
  977. GaussKronrodQuadrature,
  978. (21,),
  979. ),
  980. (
  981. # 1D problem, 2D rule
  982. [0],
  983. [1],
  984. GenzMalikCubature,
  985. (2,),
  986. )
  987. ])
  988. def test_incompatible_dimension_raises_error(self, problem, xp):
  989. a, b, quadrature, quadrature_args = problem
  990. rule = quadrature(*quadrature_args, xp=xp)
  991. a = xp.asarray(a, dtype=xp.float64)
  992. b = xp.asarray(b, dtype=xp.float64)
  993. with pytest.raises(Exception, match="incompatible dimension"):
  994. rule.estimate(basic_1d_integrand, a, b, args=(xp,))
  995. def test_estimate_with_base_classes_raise_error(self, xp):
  996. a = xp.asarray([0])
  997. b = xp.asarray([1])
  998. for base_class in [Rule(), FixedRule()]:
  999. with pytest.raises(Exception):
  1000. base_class.estimate(basic_1d_integrand, a, b, args=(xp,))
  1001. class TestRulesQuadrature:
  1002. """
  1003. Tests underlying quadrature rules (ndim == 1).
  1004. """
  1005. @pytest.mark.parametrize(("rule", "rule_args"), [
  1006. (GaussLegendreQuadrature, (3,)),
  1007. (GaussLegendreQuadrature, (5,)),
  1008. (GaussLegendreQuadrature, (10,)),
  1009. (GaussKronrodQuadrature, (15,)),
  1010. (GaussKronrodQuadrature, (21,)),
  1011. ])
  1012. def test_base_1d_quadratures_simple(self, rule, rule_args, xp):
  1013. quadrature = rule(*rule_args, xp=xp)
  1014. n = xp.arange(5, dtype=xp.float64)
  1015. def f(x):
  1016. x_reshaped = xp.reshape(x, (-1, 1, 1))
  1017. n_reshaped = xp.reshape(n, (1, -1, 1))
  1018. return x_reshaped**n_reshaped
  1019. a = xp.asarray([0], dtype=xp.float64)
  1020. b = xp.asarray([2], dtype=xp.float64)
  1021. exact = xp.reshape(2**(n+1)/(n+1), (-1, 1))
  1022. estimate = quadrature.estimate(f, a, b)
  1023. xp_assert_close(
  1024. estimate,
  1025. exact,
  1026. rtol=1e-8,
  1027. atol=0,
  1028. )
  1029. @pytest.mark.parametrize(("rule_pair", "rule_pair_args"), [
  1030. ((GaussLegendreQuadrature, GaussLegendreQuadrature), (10, 5)),
  1031. ])
  1032. def test_base_1d_quadratures_error_from_difference(self, rule_pair, rule_pair_args,
  1033. xp):
  1034. n = xp.arange(5, dtype=xp.float64)
  1035. a = xp.asarray([0], dtype=xp.float64)
  1036. b = xp.asarray([2], dtype=xp.float64)
  1037. higher = rule_pair[0](rule_pair_args[0], xp=xp)
  1038. lower = rule_pair[1](rule_pair_args[1], xp=xp)
  1039. rule = NestedFixedRule(higher, lower)
  1040. res = cubature(
  1041. basic_1d_integrand,
  1042. a, b,
  1043. rule=rule,
  1044. rtol=1e-8,
  1045. args=(n, xp),
  1046. )
  1047. xp_assert_close(
  1048. res.estimate,
  1049. basic_1d_integrand_exact(n, xp),
  1050. rtol=1e-8,
  1051. atol=0,
  1052. )
  1053. @pytest.mark.parametrize("quadrature", [
  1054. GaussLegendreQuadrature
  1055. ])
  1056. def test_one_point_fixed_quad_impossible(self, quadrature, xp):
  1057. with pytest.raises(Exception):
  1058. quadrature(1, xp=xp)
  1059. class TestRulesCubature:
  1060. """
  1061. Tests underlying cubature rules (ndim >= 2).
  1062. """
  1063. @pytest.mark.parametrize("ndim", range(2, 11))
  1064. def test_genz_malik_func_evaluations(self, ndim, xp):
  1065. """
  1066. Tests that the number of function evaluations required for Genz-Malik cubature
  1067. matches the number in Genz and Malik 1980.
  1068. """
  1069. nodes, _ = GenzMalikCubature(ndim, xp=xp).nodes_and_weights
  1070. assert nodes.shape[0] == (2**ndim) + 2*ndim**2 + 2*ndim + 1
  1071. def test_genz_malik_1d_raises_error(self, xp):
  1072. with pytest.raises(Exception, match="only defined for ndim >= 2"):
  1073. GenzMalikCubature(1, xp=xp)
  1074. @pytest.mark.skip_xp_backends('jax.numpy', reason=boolean_index_skip_reason)
  1075. @pytest.mark.skip_xp_backends('dask.array', reason=boolean_index_skip_reason)
  1076. class TestTransformations:
  1077. @pytest.mark.parametrize(("a", "b", "points"), [
  1078. (
  1079. [0, 1, -math.inf],
  1080. [1, math.inf, math.inf],
  1081. [
  1082. [1, 1, 1],
  1083. [0.5, 10, 10],
  1084. ]
  1085. )
  1086. ])
  1087. def test_infinite_limits_maintains_points(self, a, b, points, xp):
  1088. """
  1089. Test that break points are correctly mapped under the _InfiniteLimitsTransform
  1090. transformation.
  1091. """
  1092. points = [xp.asarray(p, dtype=xp.float64) for p in points]
  1093. f_transformed = _InfiniteLimitsTransform(
  1094. # Bind `points` and `xp` argument in f
  1095. lambda x: f_with_problematic_points(x, points, xp),
  1096. xp.asarray(a, dtype=xp.float64),
  1097. xp.asarray(b, dtype=xp.float64),
  1098. xp=xp,
  1099. )
  1100. for point in points:
  1101. transformed_point = f_transformed.inv(xp.reshape(point, (1, -1)))
  1102. with pytest.raises(Exception, match="called with a problematic point"):
  1103. f_transformed(transformed_point)
  1104. class BadErrorRule(Rule):
  1105. """
  1106. A rule with fake high error so that cubature will keep on subdividing.
  1107. """
  1108. def estimate(self, f, a, b, args=()):
  1109. xp = array_namespace(a, b)
  1110. underlying = GaussLegendreQuadrature(10, xp=xp)
  1111. return underlying.estimate(f, a, b, args)
  1112. def estimate_error(self, f, a, b, args=()):
  1113. xp = array_namespace(a, b)
  1114. return xp.asarray(1e6, dtype=xp.float64)