_quadrature.py 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370
  1. import numpy as np
  2. import math
  3. import warnings
  4. from collections import namedtuple
  5. from collections.abc import Callable
  6. from scipy.special import roots_legendre
  7. from scipy.special import gammaln, logsumexp
  8. from scipy._lib._util import _rng_spawn
  9. from scipy._lib._array_api import (_asarray, array_namespace, xp_result_type, xp_copy,
  10. xp_capabilities, xp_promote, xp_swapaxes, is_numpy)
  11. import scipy._lib.array_api_extra as xpx
  12. __all__ = ['fixed_quad', 'romb',
  13. 'trapezoid', 'simpson',
  14. 'cumulative_trapezoid', 'newton_cotes',
  15. 'qmc_quad', 'cumulative_simpson']
  16. @xp_capabilities(skip_backends=[('jax.numpy',
  17. "JAX arrays do not support item assignment")])
  18. def trapezoid(y, x=None, dx=1.0, axis=-1):
  19. r"""
  20. Integrate along the given axis using the composite trapezoidal rule.
  21. If `x` is provided, the integration happens in sequence along its
  22. elements - they are not sorted.
  23. Integrate `y` (`x`) along each 1d slice on the given axis, compute
  24. :math:`\int y(x) dx`.
  25. When `x` is specified, this integrates along the parametric curve,
  26. computing :math:`\int_t y(t) dt =
  27. \int_t y(t) \left.\frac{dx}{dt}\right|_{x=x(t)} dt`.
  28. Parameters
  29. ----------
  30. y : array_like
  31. Input array to integrate.
  32. x : array_like, optional
  33. The sample points corresponding to the `y` values. If `x` is None,
  34. the sample points are assumed to be evenly spaced `dx` apart. The
  35. default is None.
  36. dx : scalar, optional
  37. The spacing between sample points when `x` is None. The default is 1.
  38. axis : int, optional
  39. The axis along which to integrate. The default is the last axis.
  40. Returns
  41. -------
  42. trapezoid : float or ndarray
  43. Definite integral of `y` = n-dimensional array as approximated along
  44. a single axis by the trapezoidal rule. If `y` is a 1-dimensional array,
  45. then the result is a float. If `n` is greater than 1, then the result
  46. is an `n`-1 dimensional array.
  47. See Also
  48. --------
  49. cumulative_trapezoid, simpson, romb
  50. Notes
  51. -----
  52. Image [2]_ illustrates trapezoidal rule -- y-axis locations of points
  53. will be taken from `y` array, by default x-axis distances between
  54. points will be 1.0, alternatively they can be provided with `x` array
  55. or with `dx` scalar. Return value will be equal to combined area under
  56. the red lines.
  57. References
  58. ----------
  59. .. [1] Wikipedia page: https://en.wikipedia.org/wiki/Trapezoidal_rule
  60. .. [2] Illustration image:
  61. https://en.wikipedia.org/wiki/File:Composite_trapezoidal_rule_illustration.png
  62. Examples
  63. --------
  64. Use the trapezoidal rule on evenly spaced points:
  65. >>> import numpy as np
  66. >>> from scipy import integrate
  67. >>> integrate.trapezoid([1, 2, 3])
  68. 4.0
  69. The spacing between sample points can be selected by either the
  70. ``x`` or ``dx`` arguments:
  71. >>> integrate.trapezoid([1, 2, 3], x=[4, 6, 8])
  72. 8.0
  73. >>> integrate.trapezoid([1, 2, 3], dx=2)
  74. 8.0
  75. Using a decreasing ``x`` corresponds to integrating in reverse:
  76. >>> integrate.trapezoid([1, 2, 3], x=[8, 6, 4])
  77. -8.0
  78. More generally ``x`` is used to integrate along a parametric curve. We can
  79. estimate the integral :math:`\int_0^1 x^2 = 1/3` using:
  80. >>> x = np.linspace(0, 1, num=50)
  81. >>> y = x**2
  82. >>> integrate.trapezoid(y, x)
  83. 0.33340274885464394
  84. Or estimate the area of a circle, noting we repeat the sample which closes
  85. the curve:
  86. >>> theta = np.linspace(0, 2 * np.pi, num=1000, endpoint=True)
  87. >>> integrate.trapezoid(np.cos(theta), x=np.sin(theta))
  88. 3.141571941375841
  89. ``trapezoid`` can be applied along a specified axis to do multiple
  90. computations in one call:
  91. >>> a = np.arange(6).reshape(2, 3)
  92. >>> a
  93. array([[0, 1, 2],
  94. [3, 4, 5]])
  95. >>> integrate.trapezoid(a, axis=0)
  96. array([1.5, 2.5, 3.5])
  97. >>> integrate.trapezoid(a, axis=1)
  98. array([2., 8.])
  99. """
  100. xp = array_namespace(y)
  101. y = _asarray(y, xp=xp, subok=True)
  102. # Cannot just use the broadcasted arrays that are returned
  103. # because trapezoid does not follow normal broadcasting rules
  104. # cf. https://github.com/scipy/scipy/pull/21524#issuecomment-2354105942
  105. result_dtype = xp_result_type(y, force_floating=True, xp=xp)
  106. nd = y.ndim
  107. slice1 = [slice(None)]*nd
  108. slice2 = [slice(None)]*nd
  109. slice1[axis] = slice(1, None)
  110. slice2[axis] = slice(None, -1)
  111. if x is None:
  112. d = dx
  113. else:
  114. x = _asarray(x, xp=xp, subok=True)
  115. if x.ndim == 1:
  116. d = x[1:] - x[:-1]
  117. # make d broadcastable to y
  118. slice3 = [None] * nd
  119. slice3[axis] = slice(None)
  120. d = d[tuple(slice3)]
  121. else:
  122. # if x is n-D it should be broadcastable to y
  123. x = xp.broadcast_to(x, y.shape)
  124. d = x[tuple(slice1)] - x[tuple(slice2)]
  125. try:
  126. ret = xp.sum(
  127. d * (y[tuple(slice1)] + y[tuple(slice2)]) / 2.0,
  128. axis=axis, dtype=result_dtype
  129. )
  130. except ValueError:
  131. # Operations didn't work, cast to ndarray
  132. d = xp.asarray(d)
  133. y = xp.asarray(y)
  134. ret = xp.sum(
  135. d * (y[tuple(slice1)] + y[tuple(slice2)]) / 2.0,
  136. axis=axis, dtype=result_dtype
  137. )
  138. return ret
  139. def _cached_roots_legendre(n):
  140. """
  141. Cache roots_legendre results to speed up calls of the fixed_quad
  142. function.
  143. """
  144. if n in _cached_roots_legendre.cache:
  145. return _cached_roots_legendre.cache[n]
  146. _cached_roots_legendre.cache[n] = roots_legendre(n)
  147. return _cached_roots_legendre.cache[n]
  148. _cached_roots_legendre.cache = dict()
  149. @xp_capabilities(np_only=True)
  150. def fixed_quad(func, a, b, args=(), n=5):
  151. """
  152. Compute a definite integral using fixed-order Gaussian quadrature.
  153. Integrate `func` from `a` to `b` using Gaussian quadrature of
  154. order `n`.
  155. Parameters
  156. ----------
  157. func : callable
  158. A Python function or method to integrate (must accept vector inputs).
  159. If integrating a vector-valued function, the returned array must have
  160. shape ``(..., len(x))``.
  161. a : float
  162. Lower limit of integration.
  163. b : float
  164. Upper limit of integration.
  165. args : tuple, optional
  166. Extra arguments to pass to function, if any.
  167. n : int, optional
  168. Order of quadrature integration. Default is 5.
  169. Returns
  170. -------
  171. val : float
  172. Gaussian quadrature approximation to the integral
  173. none : None
  174. Statically returned value of None
  175. See Also
  176. --------
  177. quad : adaptive quadrature using QUADPACK
  178. dblquad : double integrals
  179. tplquad : triple integrals
  180. romb : integrators for sampled data
  181. simpson : integrators for sampled data
  182. cumulative_trapezoid : cumulative integration for sampled data
  183. Examples
  184. --------
  185. >>> from scipy import integrate
  186. >>> import numpy as np
  187. >>> f = lambda x: x**8
  188. >>> integrate.fixed_quad(f, 0.0, 1.0, n=4)
  189. (0.1110884353741496, None)
  190. >>> integrate.fixed_quad(f, 0.0, 1.0, n=5)
  191. (0.11111111111111102, None)
  192. >>> print(1/9.0) # analytical result
  193. 0.1111111111111111
  194. >>> integrate.fixed_quad(np.cos, 0.0, np.pi/2, n=4)
  195. (0.9999999771971152, None)
  196. >>> integrate.fixed_quad(np.cos, 0.0, np.pi/2, n=5)
  197. (1.000000000039565, None)
  198. >>> np.sin(np.pi/2)-np.sin(0) # analytical result
  199. 1.0
  200. """
  201. x, w = _cached_roots_legendre(n)
  202. x = np.real(x)
  203. if np.isinf(a) or np.isinf(b):
  204. raise ValueError("Gaussian quadrature is only available for "
  205. "finite limits.")
  206. y = (b-a)*(x+1)/2.0 + a
  207. return (b-a)/2.0 * np.sum(w*func(y, *args), axis=-1), None
  208. def tupleset(t, i, value):
  209. l = list(t)
  210. l[i] = value
  211. return tuple(l)
  212. @xp_capabilities()
  213. def cumulative_trapezoid(y, x=None, dx=1.0, axis=-1, initial=None):
  214. """
  215. Cumulatively integrate y(x) using the composite trapezoidal rule.
  216. Parameters
  217. ----------
  218. y : array_like
  219. Values to integrate.
  220. x : array_like, optional
  221. The coordinate to integrate along. If None (default), use spacing `dx`
  222. between consecutive elements in `y`.
  223. dx : float, optional
  224. Spacing between elements of `y`. Only used if `x` is None.
  225. axis : int, optional
  226. Specifies the axis to cumulate. Default is -1 (last axis).
  227. initial : scalar, optional
  228. If given, insert this value at the beginning of the returned result.
  229. 0 or None are the only values accepted. Default is None, which means
  230. `res` has one element less than `y` along the axis of integration.
  231. Returns
  232. -------
  233. res : ndarray
  234. The result of cumulative integration of `y` along `axis`.
  235. If `initial` is None, the shape is such that the axis of integration
  236. has one less value than `y`. If `initial` is given, the shape is equal
  237. to that of `y`.
  238. See Also
  239. --------
  240. numpy.cumsum, numpy.cumprod
  241. cumulative_simpson : cumulative integration using Simpson's 1/3 rule
  242. quad : adaptive quadrature using QUADPACK
  243. fixed_quad : fixed-order Gaussian quadrature
  244. dblquad : double integrals
  245. tplquad : triple integrals
  246. romb : integrators for sampled data
  247. Examples
  248. --------
  249. >>> from scipy import integrate
  250. >>> import numpy as np
  251. >>> import matplotlib.pyplot as plt
  252. >>> x = np.linspace(-2, 2, num=20)
  253. >>> y = x
  254. >>> y_int = integrate.cumulative_trapezoid(y, x, initial=0)
  255. >>> plt.plot(x, y_int, 'ro', x, y[0] + 0.5 * x**2, 'b-')
  256. >>> plt.show()
  257. """
  258. xp = array_namespace(y)
  259. y = xp.asarray(y)
  260. if y.shape[axis] == 0:
  261. raise ValueError("At least one point is required along `axis`.")
  262. if x is None:
  263. d = dx
  264. else:
  265. x = xp.asarray(x)
  266. if x.ndim == 1:
  267. d = xp.diff(x)
  268. # reshape to correct shape
  269. shape = [1] * y.ndim
  270. shape[axis] = -1
  271. d = xp.reshape(d, tuple(shape))
  272. elif len(x.shape) != len(y.shape):
  273. raise ValueError("If given, shape of x must be 1-D or the "
  274. "same as y.")
  275. else:
  276. d = xp.diff(x, axis=axis)
  277. if d.shape[axis] != y.shape[axis] - 1:
  278. raise ValueError("If given, length of x along axis must be the "
  279. "same as y.")
  280. nd = len(y.shape)
  281. slice1 = tupleset((slice(None),)*nd, axis, slice(1, None))
  282. slice2 = tupleset((slice(None),)*nd, axis, slice(None, -1))
  283. res = xp.cumulative_sum(d * (y[slice1] + y[slice2]) / 2.0, axis=axis)
  284. if initial is not None:
  285. if initial != 0:
  286. raise ValueError("`initial` must be `None` or `0`.")
  287. if not np.isscalar(initial):
  288. raise ValueError("`initial` parameter should be a scalar.")
  289. shape = list(res.shape)
  290. shape[axis] = 1
  291. res = xp.concat((xp.full(tuple(shape), initial, dtype=res.dtype), res),
  292. axis=axis)
  293. return res
  294. def _basic_simpson(y, start, stop, x, dx, axis):
  295. nd = len(y.shape)
  296. if start is None:
  297. start = 0
  298. step = 2
  299. slice_all = (slice(None),)*nd
  300. slice0 = tupleset(slice_all, axis, slice(start, stop, step))
  301. slice1 = tupleset(slice_all, axis, slice(start+1, stop+1, step))
  302. slice2 = tupleset(slice_all, axis, slice(start+2, stop+2, step))
  303. if x is None: # Even-spaced Simpson's rule.
  304. result = np.sum(y[slice0] + 4.0*y[slice1] + y[slice2], axis=axis)
  305. result *= dx / 3.0
  306. else:
  307. # Account for possibly different spacings.
  308. # Simpson's rule changes a bit.
  309. h = np.diff(x, axis=axis)
  310. sl0 = tupleset(slice_all, axis, slice(start, stop, step))
  311. sl1 = tupleset(slice_all, axis, slice(start+1, stop+1, step))
  312. h0 = h[sl0].astype(float, copy=False)
  313. h1 = h[sl1].astype(float, copy=False)
  314. hsum = h0 + h1
  315. hprod = h0 * h1
  316. h0divh1 = np.true_divide(h0, h1, out=np.zeros_like(h0), where=h1 != 0)
  317. tmp = hsum/6.0 * (y[slice0] *
  318. (2.0 - np.true_divide(1.0, h0divh1,
  319. out=np.zeros_like(h0divh1),
  320. where=h0divh1 != 0)) +
  321. y[slice1] * (hsum *
  322. np.true_divide(hsum, hprod,
  323. out=np.zeros_like(hsum),
  324. where=hprod != 0)) +
  325. y[slice2] * (2.0 - h0divh1))
  326. result = np.sum(tmp, axis=axis)
  327. return result
  328. @xp_capabilities(np_only=True)
  329. def simpson(y, x=None, *, dx=1.0, axis=-1):
  330. """
  331. Integrate y(x) using samples along the given axis and the composite
  332. Simpson's rule. If x is None, spacing of dx is assumed.
  333. Parameters
  334. ----------
  335. y : array_like
  336. Array to be integrated.
  337. x : array_like, optional
  338. If given, the points at which `y` is sampled.
  339. dx : float, optional
  340. Spacing of integration points along axis of `x`. Only used when
  341. `x` is None. Default is 1.
  342. axis : int, optional
  343. Axis along which to integrate. Default is the last axis.
  344. Returns
  345. -------
  346. float
  347. The estimated integral computed with the composite Simpson's rule.
  348. See Also
  349. --------
  350. quad : adaptive quadrature using QUADPACK
  351. fixed_quad : fixed-order Gaussian quadrature
  352. dblquad : double integrals
  353. tplquad : triple integrals
  354. romb : integrators for sampled data
  355. cumulative_trapezoid : cumulative integration for sampled data
  356. cumulative_simpson : cumulative integration using Simpson's 1/3 rule
  357. Notes
  358. -----
  359. For an odd number of samples that are equally spaced the result is
  360. exact if the function is a polynomial of order 3 or less. If
  361. the samples are not equally spaced, then the result is exact only
  362. if the function is a polynomial of order 2 or less.
  363. References
  364. ----------
  365. .. [1] Cartwright, Kenneth V. Simpson's Rule Cumulative Integration with
  366. MS Excel and Irregularly-spaced Data. Journal of Mathematical
  367. Sciences and Mathematics Education. 12 (2): 1-9
  368. Examples
  369. --------
  370. >>> from scipy import integrate
  371. >>> import numpy as np
  372. >>> x = np.arange(0, 10)
  373. >>> y = np.arange(0, 10)
  374. >>> integrate.simpson(y, x=x)
  375. 40.5
  376. >>> y = np.power(x, 3)
  377. >>> integrate.simpson(y, x=x)
  378. 1640.5
  379. >>> integrate.quad(lambda x: x**3, 0, 9)[0]
  380. 1640.25
  381. """
  382. y = np.asarray(y)
  383. nd = len(y.shape)
  384. N = y.shape[axis]
  385. last_dx = dx
  386. returnshape = 0
  387. if x is not None:
  388. x = np.asarray(x)
  389. if len(x.shape) == 1:
  390. shapex = [1] * nd
  391. shapex[axis] = x.shape[0]
  392. saveshape = x.shape
  393. returnshape = 1
  394. x = x.reshape(tuple(shapex))
  395. elif len(x.shape) != len(y.shape):
  396. raise ValueError("If given, shape of x must be 1-D or the "
  397. "same as y.")
  398. if x.shape[axis] != N:
  399. raise ValueError("If given, length of x along axis must be the "
  400. "same as y.")
  401. if N % 2 == 0:
  402. val = 0.0
  403. result = 0.0
  404. slice_all = (slice(None),) * nd
  405. if N == 2:
  406. # need at least 3 points in integration axis to form parabolic
  407. # segment. If there are two points then any of 'avg', 'first',
  408. # 'last' should give the same result.
  409. slice1 = tupleset(slice_all, axis, -1)
  410. slice2 = tupleset(slice_all, axis, -2)
  411. if x is not None:
  412. last_dx = x[slice1] - x[slice2]
  413. val += 0.5 * last_dx * (y[slice1] + y[slice2])
  414. else:
  415. # use Simpson's rule on first intervals
  416. result = _basic_simpson(y, 0, N-3, x, dx, axis)
  417. slice1 = tupleset(slice_all, axis, -1)
  418. slice2 = tupleset(slice_all, axis, -2)
  419. slice3 = tupleset(slice_all, axis, -3)
  420. h = np.asarray([dx, dx], dtype=np.float64)
  421. if x is not None:
  422. # grab the last two spacings from the appropriate axis
  423. hm2 = tupleset(slice_all, axis, slice(-2, -1, 1))
  424. hm1 = tupleset(slice_all, axis, slice(-1, None, 1))
  425. diffs = np.float64(np.diff(x, axis=axis))
  426. h = [np.squeeze(diffs[hm2], axis=axis),
  427. np.squeeze(diffs[hm1], axis=axis)]
  428. # This is the correction for the last interval according to
  429. # Cartwright.
  430. # However, I used the equations given at
  431. # https://en.wikipedia.org/wiki/Simpson%27s_rule#Composite_Simpson's_rule_for_irregularly_spaced_data
  432. # A footnote on Wikipedia says:
  433. # Cartwright 2017, Equation 8. The equation in Cartwright is
  434. # calculating the first interval whereas the equations in the
  435. # Wikipedia article are adjusting for the last integral. If the
  436. # proper algebraic substitutions are made, the equation results in
  437. # the values shown.
  438. num = 2 * h[1] ** 2 + 3 * h[0] * h[1]
  439. den = 6 * (h[1] + h[0])
  440. alpha = np.true_divide(
  441. num,
  442. den,
  443. out=np.zeros_like(den),
  444. where=den != 0
  445. )
  446. num = h[1] ** 2 + 3.0 * h[0] * h[1]
  447. den = 6 * h[0]
  448. beta = np.true_divide(
  449. num,
  450. den,
  451. out=np.zeros_like(den),
  452. where=den != 0
  453. )
  454. num = 1 * h[1] ** 3
  455. den = 6 * h[0] * (h[0] + h[1])
  456. eta = np.true_divide(
  457. num,
  458. den,
  459. out=np.zeros_like(den),
  460. where=den != 0
  461. )
  462. result += alpha*y[slice1] + beta*y[slice2] - eta*y[slice3]
  463. result += val
  464. else:
  465. result = _basic_simpson(y, 0, N-2, x, dx, axis)
  466. if returnshape:
  467. x = x.reshape(saveshape)
  468. return result
  469. def _cumulatively_sum_simpson_integrals(
  470. y: np.ndarray,
  471. dx: np.ndarray,
  472. integration_func: Callable[[np.ndarray, np.ndarray], np.ndarray],
  473. xp
  474. ) -> np.ndarray:
  475. """Calculate cumulative sum of Simpson integrals.
  476. Takes as input the integration function to be used.
  477. The integration_func is assumed to return the cumulative sum using
  478. composite Simpson's rule. Assumes the axis of summation is -1.
  479. """
  480. sub_integrals_h1 = integration_func(y, dx)
  481. sub_integrals_h2 = xp.flip(
  482. integration_func(xp.flip(y, axis=-1), xp.flip(dx, axis=-1)),
  483. axis=-1
  484. )
  485. shape = list(sub_integrals_h1.shape)
  486. shape[-1] += 1
  487. sub_integrals = xp.empty(shape, dtype=xp.result_type(y, dx))
  488. sub_integrals[..., :-1:2] = sub_integrals_h1[..., ::2]
  489. sub_integrals[..., 1::2] = sub_integrals_h2[..., ::2]
  490. # Integral over last subinterval can only be calculated from
  491. # formula for h2
  492. sub_integrals[..., -1] = sub_integrals_h2[..., -1]
  493. res = xp.cumulative_sum(sub_integrals, axis=-1)
  494. return res
  495. def _cumulative_simpson_equal_intervals(y: np.ndarray, dx: np.ndarray) -> np.ndarray:
  496. """Calculate the Simpson integrals for all h1 intervals assuming equal interval
  497. widths. The function can also be used to calculate the integral for all
  498. h2 intervals by reversing the inputs, `y` and `dx`.
  499. """
  500. d = dx[..., :-1]
  501. f1 = y[..., :-2]
  502. f2 = y[..., 1:-1]
  503. f3 = y[..., 2:]
  504. # Calculate integral over the subintervals (eqn (10) of Reference [2])
  505. return d / 3 * (5 * f1 / 4 + 2 * f2 - f3 / 4)
  506. def _cumulative_simpson_unequal_intervals(y: np.ndarray, dx: np.ndarray) -> np.ndarray:
  507. """Calculate the Simpson integrals for all h1 intervals assuming unequal interval
  508. widths. The function can also be used to calculate the integral for all
  509. h2 intervals by reversing the inputs, `y` and `dx`.
  510. """
  511. x21 = dx[..., :-1]
  512. x32 = dx[..., 1:]
  513. f1 = y[..., :-2]
  514. f2 = y[..., 1:-1]
  515. f3 = y[..., 2:]
  516. x31 = x21 + x32
  517. x21_x31 = x21/x31
  518. x21_x32 = x21/x32
  519. x21x21_x31x32 = x21_x31 * x21_x32
  520. # Calculate integral over the subintervals (eqn (8) of Reference [2])
  521. coeff1 = 3 - x21_x31
  522. coeff2 = 3 + x21x21_x31x32 + x21_x31
  523. coeff3 = -x21x21_x31x32
  524. return x21/6 * (coeff1*f1 + coeff2*f2 + coeff3*f3)
  525. @xp_capabilities(allow_dask_compute=1,
  526. skip_backends=[("jax.numpy", "item assignment")])
  527. def cumulative_simpson(y, *, x=None, dx=1.0, axis=-1, initial=None):
  528. r"""
  529. Cumulatively integrate y(x) using the composite Simpson's 1/3 rule.
  530. The integral of the samples at every point is calculated by assuming a
  531. quadratic relationship between each point and the two adjacent points.
  532. Parameters
  533. ----------
  534. y : array_like
  535. Values to integrate. Requires at least one point along `axis`. If two or fewer
  536. points are provided along `axis`, Simpson's integration is not possible and the
  537. result is calculated with `cumulative_trapezoid`.
  538. x : array_like, optional
  539. The coordinate to integrate along. Must have the same shape as `y` or
  540. must be 1D with the same length as `y` along `axis`. `x` must also be
  541. strictly increasing along `axis`.
  542. If `x` is None (default), integration is performed using spacing `dx`
  543. between consecutive elements in `y`.
  544. dx : scalar or array_like, optional
  545. Spacing between elements of `y`. Only used if `x` is None. Can either
  546. be a float, or an array with the same shape as `y`, but of length one along
  547. `axis`. Default is 1.0.
  548. axis : int, optional
  549. Specifies the axis to integrate along. Default is -1 (last axis).
  550. initial : scalar or array_like, optional
  551. If given, insert this value at the beginning of the returned result,
  552. and add it to the rest of the result. Default is None, which means no
  553. value at ``x[0]`` is returned and `res` has one element less than `y`
  554. along the axis of integration. Can either be a float, or an array with
  555. the same shape as `y`, but of length one along `axis`.
  556. Returns
  557. -------
  558. res : ndarray
  559. The result of cumulative integration of `y` along `axis`.
  560. If `initial` is None, the shape is such that the axis of integration
  561. has one less value than `y`. If `initial` is given, the shape is equal
  562. to that of `y`.
  563. See Also
  564. --------
  565. numpy.cumsum
  566. cumulative_trapezoid : cumulative integration using the composite
  567. trapezoidal rule
  568. simpson : integrator for sampled data using the Composite Simpson's Rule
  569. Notes
  570. -----
  571. .. versionadded:: 1.12.0
  572. The composite Simpson's 1/3 method can be used to approximate the definite
  573. integral of a sampled input function :math:`y(x)` [1]_. The method assumes
  574. a quadratic relationship over the interval containing any three consecutive
  575. sampled points.
  576. Consider three consecutive points:
  577. :math:`(x_1, y_1), (x_2, y_2), (x_3, y_3)`.
  578. Assuming a quadratic relationship over the three points, the integral over
  579. the subinterval between :math:`x_1` and :math:`x_2` is given by formula
  580. (8) of [2]_:
  581. .. math::
  582. \int_{x_1}^{x_2} y(x) dx\ &= \frac{x_2-x_1}{6}\left[\
  583. \left\{3-\frac{x_2-x_1}{x_3-x_1}\right\} y_1 + \
  584. \left\{3 + \frac{(x_2-x_1)^2}{(x_3-x_2)(x_3-x_1)} + \
  585. \frac{x_2-x_1}{x_3-x_1}\right\} y_2\\
  586. - \frac{(x_2-x_1)^2}{(x_3-x_2)(x_3-x_1)} y_3\right]
  587. The integral between :math:`x_2` and :math:`x_3` is given by swapping
  588. appearances of :math:`x_1` and :math:`x_3`. The integral is estimated
  589. separately for each subinterval and then cumulatively summed to obtain
  590. the final result.
  591. For samples that are equally spaced, the result is exact if the function
  592. is a polynomial of order three or less [1]_ and the number of subintervals
  593. is even. Otherwise, the integral is exact for polynomials of order two or
  594. less.
  595. References
  596. ----------
  597. .. [1] Wikipedia page: https://en.wikipedia.org/wiki/Simpson's_rule
  598. .. [2] Cartwright, Kenneth V. Simpson's Rule Cumulative Integration with
  599. MS Excel and Irregularly-spaced Data. Journal of Mathematical
  600. Sciences and Mathematics Education. 12 (2): 1-9
  601. Examples
  602. --------
  603. >>> from scipy import integrate
  604. >>> import numpy as np
  605. >>> import matplotlib.pyplot as plt
  606. >>> x = np.linspace(-2, 2, num=20)
  607. >>> y = x**2
  608. >>> y_int = integrate.cumulative_simpson(y, x=x, initial=0)
  609. >>> fig, ax = plt.subplots()
  610. >>> ax.plot(x, y_int, 'ro', x, x**3/3 - (x[0])**3/3, 'b-')
  611. >>> ax.grid()
  612. >>> plt.show()
  613. The output of `cumulative_simpson` is similar to that of iteratively
  614. calling `simpson` with successively higher upper limits of integration, but
  615. not identical.
  616. >>> def cumulative_simpson_reference(y, x):
  617. ... return np.asarray([integrate.simpson(y[:i], x=x[:i])
  618. ... for i in range(2, len(y) + 1)])
  619. >>>
  620. >>> rng = np.random.default_rng(354673834679465)
  621. >>> x, y = rng.random(size=(2, 10))
  622. >>> x.sort()
  623. >>>
  624. >>> res = integrate.cumulative_simpson(y, x=x)
  625. >>> ref = cumulative_simpson_reference(y, x)
  626. >>> equal = np.abs(res - ref) < 1e-15
  627. >>> equal # not equal when `simpson` has even number of subintervals
  628. array([False, True, False, True, False, True, False, True, True])
  629. This is expected: because `cumulative_simpson` has access to more
  630. information than `simpson`, it can typically produce more accurate
  631. estimates of the underlying integral over subintervals.
  632. """
  633. xp = array_namespace(y)
  634. y = xp_promote(y, force_floating=True, xp=xp)
  635. # validate `axis` and standardize to work along the last axis
  636. original_y = y
  637. original_shape = y.shape
  638. try:
  639. y = xp_swapaxes(y, axis, -1, xp)
  640. except IndexError as e:
  641. message = f"`axis={axis}` is not valid for `y` with `y.ndim={y.ndim}`."
  642. raise ValueError(message) from e
  643. if y.shape[-1] < 3:
  644. res = cumulative_trapezoid(original_y, x, dx=dx, axis=axis, initial=None)
  645. res = xp_swapaxes(res, axis, -1, xp)
  646. elif x is not None:
  647. x = xp_promote(x, force_floating=True, xp=xp)
  648. message = ("If given, shape of `x` must be the same as `y` or 1-D with "
  649. "the same length as `y` along `axis`.")
  650. if not (x.shape == original_shape
  651. or (x.ndim == 1 and x.shape[0] == original_shape[axis])):
  652. raise ValueError(message)
  653. x = xp.broadcast_to(x, y.shape) if x.ndim == 1 else xp_swapaxes(x, axis, -1, xp)
  654. dx = xp.diff(x, axis=-1)
  655. if xp.any(dx <= 0):
  656. raise ValueError("Input x must be strictly increasing.")
  657. res = _cumulatively_sum_simpson_integrals(
  658. y, dx, _cumulative_simpson_unequal_intervals, xp
  659. )
  660. else:
  661. dx = xp_promote(xp.asarray(dx), force_floating=True, xp=xp)
  662. final_dx_shape = tupleset(original_shape, axis, original_shape[axis] - 1)
  663. alt_input_dx_shape = tupleset(original_shape, axis, 1)
  664. message = ("If provided, `dx` must either be a scalar or have the same "
  665. "shape as `y` but with only 1 point along `axis`.")
  666. if not (dx.ndim == 0 or dx.shape == alt_input_dx_shape):
  667. raise ValueError(message)
  668. dx = xp.broadcast_to(dx, final_dx_shape)
  669. dx = xp_swapaxes(dx, axis, -1, xp)
  670. res = _cumulatively_sum_simpson_integrals(
  671. y, dx, _cumulative_simpson_equal_intervals, xp
  672. )
  673. if initial is not None:
  674. initial = xp_promote(initial, force_floating=True, xp=xp)
  675. alt_initial_input_shape = tupleset(original_shape, axis, 1)
  676. message = ("If provided, `initial` must either be a scalar or have the "
  677. "same shape as `y` but with only 1 point along `axis`.")
  678. if not (initial.ndim == 0 or initial.shape == alt_initial_input_shape):
  679. raise ValueError(message)
  680. initial = xp.broadcast_to(initial, alt_initial_input_shape)
  681. initial = xp_swapaxes(initial, axis, -1, xp)
  682. res += initial
  683. res = xp.concat((initial, res), axis=-1)
  684. res = xp_swapaxes(res, -1, axis, xp)
  685. return res
  686. @xp_capabilities()
  687. def romb(y, dx=1.0, axis=-1, show=False):
  688. """
  689. Romberg integration using samples of a function.
  690. Parameters
  691. ----------
  692. y : array_like
  693. A vector of ``2**k + 1`` equally-spaced samples of a function.
  694. dx : float, optional
  695. The sample spacing. Default is 1.
  696. axis : int, optional
  697. The axis along which to integrate. Default is -1 (last axis).
  698. show : bool, optional
  699. When `y` is a single 1-D array, then if this argument is True
  700. print the table showing Richardson extrapolation from the
  701. samples. Default is False.
  702. Returns
  703. -------
  704. romb : ndarray
  705. The integrated result for `axis`.
  706. See Also
  707. --------
  708. quad : adaptive quadrature using QUADPACK
  709. fixed_quad : fixed-order Gaussian quadrature
  710. dblquad : double integrals
  711. tplquad : triple integrals
  712. simpson : integrators for sampled data
  713. cumulative_trapezoid : cumulative integration for sampled data
  714. Examples
  715. --------
  716. >>> from scipy import integrate
  717. >>> import numpy as np
  718. >>> x = np.arange(10, 14.25, 0.25)
  719. >>> y = np.arange(3, 12)
  720. >>> integrate.romb(y)
  721. 56.0
  722. >>> y = np.sin(np.power(x, 2.5))
  723. >>> integrate.romb(y)
  724. -0.742561336672229
  725. >>> integrate.romb(y, show=True)
  726. Richardson Extrapolation Table for Romberg Integration
  727. ======================================================
  728. -0.81576
  729. 4.63862 6.45674
  730. -1.10581 -3.02062 -3.65245
  731. -2.57379 -3.06311 -3.06595 -3.05664
  732. -1.34093 -0.92997 -0.78776 -0.75160 -0.74256
  733. ======================================================
  734. -0.742561336672229 # may vary
  735. >>> integrate.romb([[1, 2, 3], [4, 5, 6]], show=True)
  736. *** Printing table only supported for integrals of a single data set.
  737. array([ 4., 10.])
  738. """
  739. xp = array_namespace(y)
  740. y = xp.asarray(y)
  741. nd = len(y.shape)
  742. Nsamps = y.shape[axis]
  743. Ninterv = Nsamps-1
  744. n = 1
  745. k = 0
  746. while n < Ninterv:
  747. n <<= 1
  748. k += 1
  749. if n != Ninterv:
  750. raise ValueError("Number of samples must be one plus a "
  751. "non-negative power of 2.")
  752. R = {}
  753. slice_all = (slice(None),) * nd
  754. slice0 = tupleset(slice_all, axis, 0)
  755. slicem1 = tupleset(slice_all, axis, -1)
  756. h = Ninterv * xp.asarray(dx, dtype=xp.float64)
  757. R[(0, 0)] = (y[slice0] + y[slicem1])/2.0*h
  758. slice_R = slice_all
  759. start = stop = step = Ninterv
  760. for i in range(1, k+1):
  761. start >>= 1
  762. slice_R = tupleset(slice_R, axis, slice(start, stop, step))
  763. step >>= 1
  764. R[(i, 0)] = 0.5*(R[(i-1, 0)] + h*xp.sum(y[slice_R], axis=axis))
  765. for j in range(1, i+1):
  766. prev = R[(i, j-1)]
  767. R[(i, j)] = prev + (prev-R[(i-1, j-1)]) / ((1 << (2*j))-1)
  768. h /= 2.0
  769. if show:
  770. if not np.isscalar(R[(0, 0)]):
  771. print("*** Printing table only supported for integrals" +
  772. " of a single data set.")
  773. else:
  774. try:
  775. precis = show[0]
  776. except (TypeError, IndexError):
  777. precis = 5
  778. try:
  779. width = show[1]
  780. except (TypeError, IndexError):
  781. width = 8
  782. formstr = f"%{width}.{precis}f"
  783. title = "Richardson Extrapolation Table for Romberg Integration"
  784. print(title, "=" * len(title), sep="\n", end="\n")
  785. for i in range(k+1):
  786. for j in range(i+1):
  787. print(formstr % R[(i, j)], end=" ")
  788. print()
  789. print("=" * len(title))
  790. return R[(k, k)]
  791. # Coefficients for Newton-Cotes quadrature
  792. #
  793. # These are the points being used
  794. # to construct the local interpolating polynomial
  795. # a are the weights for Newton-Cotes integration
  796. # B is the error coefficient.
  797. # error in these coefficients grows as N gets larger.
  798. # or as samples are closer and closer together
  799. # You can use maxima to find these rational coefficients
  800. # for equally spaced data using the commands
  801. # a(i,N) := (integrate(product(r-j,j,0,i-1) * product(r-j,j,i+1,N),r,0,N)
  802. # / ((N-i)! * i!) * (-1)^(N-i));
  803. # Be(N) := N^(N+2)/(N+2)! * (N/(N+3) - sum((i/N)^(N+2)*a(i,N),i,0,N));
  804. # Bo(N) := N^(N+1)/(N+1)! * (N/(N+2) - sum((i/N)^(N+1)*a(i,N),i,0,N));
  805. # B(N) := (if (mod(N,2)=0) then Be(N) else Bo(N));
  806. #
  807. # pre-computed for equally-spaced weights
  808. #
  809. # num_a, den_a, int_a, num_B, den_B = _builtincoeffs[N]
  810. #
  811. # a = num_a*array(int_a)/den_a
  812. # B = num_B*1.0 / den_B
  813. #
  814. # integrate(f(x),x,x_0,x_N) = dx*sum(a*f(x_i)) + B*(dx)^(2k+3) f^(2k+2)(x*)
  815. # where k = N // 2
  816. #
  817. _builtincoeffs = {
  818. 1: (1,2,[1,1],-1,12),
  819. 2: (1,3,[1,4,1],-1,90),
  820. 3: (3,8,[1,3,3,1],-3,80),
  821. 4: (2,45,[7,32,12,32,7],-8,945),
  822. 5: (5,288,[19,75,50,50,75,19],-275,12096),
  823. 6: (1,140,[41,216,27,272,27,216,41],-9,1400),
  824. 7: (7,17280,[751,3577,1323,2989,2989,1323,3577,751],-8183,518400),
  825. 8: (4,14175,[989,5888,-928,10496,-4540,10496,-928,5888,989],
  826. -2368,467775),
  827. 9: (9,89600,[2857,15741,1080,19344,5778,5778,19344,1080,
  828. 15741,2857], -4671, 394240),
  829. 10: (5,299376,[16067,106300,-48525,272400,-260550,427368,
  830. -260550,272400,-48525,106300,16067],
  831. -673175, 163459296),
  832. 11: (11,87091200,[2171465,13486539,-3237113, 25226685,-9595542,
  833. 15493566,15493566,-9595542,25226685,-3237113,
  834. 13486539,2171465], -2224234463, 237758976000),
  835. 12: (1, 5255250, [1364651,9903168,-7587864,35725120,-51491295,
  836. 87516288,-87797136,87516288,-51491295,35725120,
  837. -7587864,9903168,1364651], -3012, 875875),
  838. 13: (13, 402361344000,[8181904909, 56280729661, -31268252574,
  839. 156074417954,-151659573325,206683437987,
  840. -43111992612,-43111992612,206683437987,
  841. -151659573325,156074417954,-31268252574,
  842. 56280729661,8181904909], -2639651053,
  843. 344881152000),
  844. 14: (7, 2501928000, [90241897,710986864,-770720657,3501442784,
  845. -6625093363,12630121616,-16802270373,19534438464,
  846. -16802270373,12630121616,-6625093363,3501442784,
  847. -770720657,710986864,90241897], -3740727473,
  848. 1275983280000)
  849. }
  850. @xp_capabilities(np_only=True)
  851. def newton_cotes(rn, equal=0):
  852. r"""
  853. Return weights and error coefficient for Newton-Cotes integration.
  854. Suppose we have :math:`(N+1)` samples of :math:`f` at the positions
  855. :math:`x_0, x_1, ..., x_N`. Then an :math:`N`-point Newton-Cotes formula
  856. for the integral between :math:`x_0` and :math:`x_N` is:
  857. :math:`\int_{x_0}^{x_N} f(x)dx = \Delta x \sum_{i=0}^{N} a_i f(x_i)
  858. + B_N (\Delta x)^{N+2} f^{N+1} (\xi)`
  859. where :math:`\xi \in [x_0,x_N]`
  860. and :math:`\Delta x = \frac{x_N-x_0}{N}` is the average samples spacing.
  861. If the samples are equally-spaced and N is even, then the error
  862. term is :math:`B_N (\Delta x)^{N+3} f^{N+2}(\xi)`.
  863. Parameters
  864. ----------
  865. rn : int
  866. The integer order for equally-spaced data or the relative positions of
  867. the samples with the first sample at 0 and the last at N, where N+1 is
  868. the length of `rn`. N is the order of the Newton-Cotes integration.
  869. equal : int, optional
  870. Set to 1 to enforce equally spaced data.
  871. Returns
  872. -------
  873. an : ndarray
  874. 1-D array of weights to apply to the function at the provided sample
  875. positions.
  876. B : float
  877. Error coefficient.
  878. Notes
  879. -----
  880. Normally, the Newton-Cotes rules are used on smaller integration
  881. regions and a composite rule is used to return the total integral.
  882. Examples
  883. --------
  884. Compute the integral of sin(x) in [0, :math:`\pi`]:
  885. >>> from scipy.integrate import newton_cotes
  886. >>> import numpy as np
  887. >>> def f(x):
  888. ... return np.sin(x)
  889. >>> a = 0
  890. >>> b = np.pi
  891. >>> exact = 2
  892. >>> for N in [2, 4, 6, 8, 10]:
  893. ... x = np.linspace(a, b, N + 1)
  894. ... an, B = newton_cotes(N, 1)
  895. ... dx = (b - a) / N
  896. ... quad = dx * np.sum(an * f(x))
  897. ... error = abs(quad - exact)
  898. ... print('{:2d} {:10.9f} {:.5e}'.format(N, quad, error))
  899. ...
  900. 2 2.094395102 9.43951e-02
  901. 4 1.998570732 1.42927e-03
  902. 6 2.000017814 1.78136e-05
  903. 8 1.999999835 1.64725e-07
  904. 10 2.000000001 1.14677e-09
  905. """
  906. try:
  907. N = len(rn)-1
  908. if equal:
  909. rn = np.arange(N+1)
  910. elif np.all(np.diff(rn) == 1):
  911. equal = 1
  912. except Exception:
  913. N = rn
  914. rn = np.arange(N+1)
  915. equal = 1
  916. if equal and N in _builtincoeffs:
  917. na, da, vi, nb, db = _builtincoeffs[N]
  918. an = na * np.array(vi, dtype=float) / da
  919. return an, float(nb)/db
  920. if (rn[0] != 0) or (rn[-1] != N):
  921. raise ValueError("The sample positions must start at 0"
  922. " and end at N")
  923. yi = rn / float(N)
  924. ti = 2 * yi - 1
  925. nvec = np.arange(N+1)
  926. C = ti ** nvec[:, np.newaxis]
  927. Cinv = np.linalg.inv(C)
  928. # improve precision of result
  929. for i in range(2):
  930. Cinv = 2*Cinv - Cinv.dot(C).dot(Cinv)
  931. vec = 2.0 / (nvec[::2]+1)
  932. ai = Cinv[:, ::2].dot(vec) * (N / 2.)
  933. if (N % 2 == 0) and equal:
  934. BN = N/(N+3.)
  935. power = N+2
  936. else:
  937. BN = N/(N+2.)
  938. power = N+1
  939. BN = BN - np.dot(yi**power, ai)
  940. p1 = power+1
  941. fac = power*math.log(N) - gammaln(p1)
  942. fac = math.exp(fac)
  943. return ai, BN*fac
  944. def _qmc_quad_iv(func, a, b, n_points, n_estimates, qrng, log, xp):
  945. # lazy import to avoid issues with partially-initialized submodule
  946. if not hasattr(qmc_quad, 'qmc'):
  947. from scipy import stats
  948. qmc_quad.stats = stats
  949. else:
  950. stats = qmc_quad.stats
  951. if not callable(func):
  952. message = "`func` must be callable."
  953. raise TypeError(message)
  954. # a, b will be modified, so copy. Oh well if it's copied twice.
  955. a, b = xp_promote(a, b, broadcast=True, force_floating=True, xp=xp)
  956. a = xpx.atleast_nd(a, ndim=1, xp=xp)
  957. b = xpx.atleast_nd(b, ndim=1, xp=xp)
  958. a, b = xp.broadcast_arrays(a, b)
  959. a, b = xp_copy(a), xp_copy(b)
  960. dim = a.shape[0]
  961. try:
  962. func((a + b) / 2)
  963. except Exception as e:
  964. message = ("`func` must evaluate the integrand at points within "
  965. "the integration range; e.g. `func( (a + b) / 2)` "
  966. "must return the integrand at the centroid of the "
  967. "integration volume.")
  968. raise ValueError(message) from e
  969. try:
  970. func(xp.stack([a, b]).T)
  971. vfunc = func
  972. except Exception as e:
  973. if not is_numpy(xp):
  974. message = ("Exception encountered when attempting vectorized call to "
  975. f"`func`: {e}. When using array library {xp}, `func` must "
  976. "accept two-dimensional array `x` with shape `(a.shape[0], "
  977. "n_points)` and return an array of the integrand value at "
  978. "each of the `n_points`.")
  979. raise ValueError(message)
  980. message = ("Exception encountered when attempting vectorized call to "
  981. f"`func`: {e}. For better performance, `func` should "
  982. "accept two-dimensional array `x` with shape `(len(a), "
  983. "n_points)` and return an array of the integrand value at "
  984. "each of the `n_points`.")
  985. warnings.warn(message, stacklevel=3)
  986. def vfunc(x):
  987. return np.apply_along_axis(func, axis=-1, arr=x)
  988. n_points_int = int(n_points)
  989. if n_points != n_points_int:
  990. message = "`n_points` must be an integer."
  991. raise TypeError(message)
  992. n_estimates_int = int(n_estimates)
  993. if n_estimates != n_estimates_int:
  994. message = "`n_estimates` must be an integer."
  995. raise TypeError(message)
  996. if qrng is None:
  997. qrng = stats.qmc.Halton(dim)
  998. elif not isinstance(qrng, stats.qmc.QMCEngine):
  999. message = "`qrng` must be an instance of scipy.stats.qmc.QMCEngine."
  1000. raise TypeError(message)
  1001. if qrng.d != a.shape[0]:
  1002. message = ("`qrng` must be initialized with dimensionality equal to "
  1003. "the number of variables in `a`, i.e., "
  1004. "`qrng.random().shape[-1]` must equal `a.shape[0]`.")
  1005. raise ValueError(message)
  1006. rng_seed = getattr(qrng, 'rng_seed', None)
  1007. rng = stats._qmc.check_random_state(rng_seed)
  1008. if log not in {True, False}:
  1009. message = "`log` must be boolean (`True` or `False`)."
  1010. raise TypeError(message)
  1011. return (vfunc, a, b, n_points_int, n_estimates_int, qrng, rng, log, stats)
  1012. QMCQuadResult = namedtuple('QMCQuadResult', ['integral', 'standard_error'])
  1013. @xp_capabilities(skip_backends=[("dask.array",
  1014. "Dask arrays are confused about their shape")],
  1015. jax_jit=False)
  1016. def qmc_quad(func, a, b, *, n_estimates=8, n_points=1024, qrng=None,
  1017. log=False):
  1018. """
  1019. Compute an integral in N-dimensions using Quasi-Monte Carlo quadrature.
  1020. Parameters
  1021. ----------
  1022. func : callable
  1023. The integrand. Must accept a single argument ``x``, an array which
  1024. specifies the point(s) at which to evaluate the scalar-valued
  1025. integrand, and return the value(s) of the integrand.
  1026. For efficiency, the function should be vectorized to accept an array of
  1027. shape ``(d, n_points)``, where ``d`` is the number of variables (i.e.
  1028. the dimensionality of the function domain) and `n_points` is the number
  1029. of quadrature points, and return an array of shape ``(n_points,)``,
  1030. the integrand at each quadrature point.
  1031. a, b : array-like
  1032. One-dimensional arrays specifying the lower and upper integration
  1033. limits, respectively, of each of the ``d`` variables.
  1034. n_estimates, n_points : int, optional
  1035. `n_estimates` (default: 8) statistically independent QMC samples, each
  1036. of `n_points` (default: 1024) points, will be generated by `qrng`.
  1037. The total number of points at which the integrand `func` will be
  1038. evaluated is ``n_points * n_estimates``. See Notes for details.
  1039. qrng : `~scipy.stats.qmc.QMCEngine`, optional
  1040. An instance of the QMCEngine from which to sample QMC points.
  1041. The QMCEngine must be initialized to a number of dimensions ``d``
  1042. corresponding with the number of variables ``x1, ..., xd`` passed to
  1043. `func`.
  1044. The provided QMCEngine is used to produce the first integral estimate.
  1045. If `n_estimates` is greater than one, additional QMCEngines are
  1046. spawned from the first (with scrambling enabled, if it is an option.)
  1047. If a QMCEngine is not provided, the default `scipy.stats.qmc.Halton`
  1048. will be initialized with the number of dimensions determine from
  1049. the length of `a`.
  1050. log : boolean, default: False
  1051. When set to True, `func` returns the log of the integrand, and
  1052. the result object contains the log of the integral.
  1053. Returns
  1054. -------
  1055. result : object
  1056. A result object with attributes:
  1057. integral : float
  1058. The estimate of the integral.
  1059. standard_error :
  1060. The error estimate. See Notes for interpretation.
  1061. Notes
  1062. -----
  1063. Values of the integrand at each of the `n_points` points of a QMC sample
  1064. are used to produce an estimate of the integral. This estimate is drawn
  1065. from a population of possible estimates of the integral, the value of
  1066. which we obtain depends on the particular points at which the integral
  1067. was evaluated. We perform this process `n_estimates` times, each time
  1068. evaluating the integrand at different scrambled QMC points, effectively
  1069. drawing i.i.d. random samples from the population of integral estimates.
  1070. The sample mean :math:`m` of these integral estimates is an
  1071. unbiased estimator of the true value of the integral, and the standard
  1072. error of the mean :math:`s` of these estimates may be used to generate
  1073. confidence intervals using the t distribution with ``n_estimates - 1``
  1074. degrees of freedom. Perhaps counter-intuitively, increasing `n_points`
  1075. while keeping the total number of function evaluation points
  1076. ``n_points * n_estimates`` fixed tends to reduce the actual error, whereas
  1077. increasing `n_estimates` tends to decrease the error estimate.
  1078. Examples
  1079. --------
  1080. QMC quadrature is particularly useful for computing integrals in higher
  1081. dimensions. An example integrand is the probability density function
  1082. of a multivariate normal distribution.
  1083. >>> import numpy as np
  1084. >>> from scipy import stats
  1085. >>> dim = 8
  1086. >>> mean = np.zeros(dim)
  1087. >>> cov = np.eye(dim)
  1088. >>> def func(x):
  1089. ... # `multivariate_normal` expects the _last_ axis to correspond with
  1090. ... # the dimensionality of the space, so `x` must be transposed
  1091. ... return stats.multivariate_normal.pdf(x.T, mean, cov)
  1092. To compute the integral over the unit hypercube:
  1093. >>> from scipy.integrate import qmc_quad
  1094. >>> a = np.zeros(dim)
  1095. >>> b = np.ones(dim)
  1096. >>> rng = np.random.default_rng()
  1097. >>> qrng = stats.qmc.Halton(d=dim, seed=rng)
  1098. >>> n_estimates = 8
  1099. >>> res = qmc_quad(func, a, b, n_estimates=n_estimates, qrng=qrng)
  1100. >>> res.integral, res.standard_error
  1101. (0.00018429555666024108, 1.0389431116001344e-07)
  1102. A two-sided, 99% confidence interval for the integral may be estimated
  1103. as:
  1104. >>> t = stats.t(df=n_estimates-1, loc=res.integral,
  1105. ... scale=res.standard_error)
  1106. >>> t.interval(0.99)
  1107. (0.0001839319802536469, 0.00018465913306683527)
  1108. Indeed, the value reported by `scipy.stats.multivariate_normal` is
  1109. within this range.
  1110. >>> stats.multivariate_normal.cdf(b, mean, cov, lower_limit=a)
  1111. 0.00018430867675187443
  1112. """
  1113. xp = array_namespace(a, b)
  1114. args = _qmc_quad_iv(func, a, b, n_points, n_estimates, qrng, log, xp)
  1115. func, a, b, n_points, n_estimates, qrng, rng, log, stats = args
  1116. def sum_product(integrands, dA, log=False):
  1117. if log:
  1118. return logsumexp(integrands) + math.log(dA)
  1119. else:
  1120. return xp.sum(integrands * dA)
  1121. def mean(estimates, log=False):
  1122. if log:
  1123. return logsumexp(estimates) - math.log(n_estimates)
  1124. else:
  1125. return xp.mean(estimates)
  1126. def std(estimates, m=None, ddof=0, log=False):
  1127. m = m or mean(estimates, log)
  1128. if log:
  1129. estimates, m = xp.broadcast_arrays(estimates, m)
  1130. temp = xp.stack((estimates, m + xp.pi * 1j))
  1131. diff = logsumexp(temp, axis=0)
  1132. return xp.real(0.5 * (logsumexp(2 * diff)
  1133. - math.log(n_estimates - ddof)))
  1134. else:
  1135. return xp.std(estimates, correction=ddof)
  1136. def sem(estimates, m=None, s=None, log=False):
  1137. m = m or mean(estimates, log)
  1138. s = s or std(estimates, m, ddof=1, log=log)
  1139. if log:
  1140. return s - 0.5*math.log(n_estimates)
  1141. else:
  1142. return s / math.sqrt(n_estimates)
  1143. # The sign of the integral depends on the order of the limits. Fix this by
  1144. # ensuring that lower bounds are indeed lower and setting sign of resulting
  1145. # integral manually
  1146. if xp.any(a == b):
  1147. message = ("A lower limit was equal to an upper limit, so the value "
  1148. "of the integral is zero by definition.")
  1149. warnings.warn(message, stacklevel=2)
  1150. zero = xp.asarray(-xp.inf if log else 0, dtype=a.dtype)
  1151. return QMCQuadResult(zero, xp.asarray(0., dtype=a.dtype))
  1152. i_swap = b < a
  1153. sign = (-1)**(xp.count_nonzero(i_swap, axis=-1)) # odd # of swaps -> negative
  1154. sign = xp.astype(sign, a.dtype)
  1155. # a[i_swap], b[i_swap] = b[i_swap], a[i_swap]
  1156. a_iswap = a[i_swap]
  1157. b_iswap = b[i_swap]
  1158. a = xpx.at(a)[i_swap].set(b_iswap)
  1159. b = xpx.at(b)[i_swap].set(a_iswap)
  1160. A = xp.prod(b - a)
  1161. dA = A / n_points
  1162. estimates = xp.zeros(n_estimates, dtype=a.dtype)
  1163. rngs = _rng_spawn(qrng.rng, n_estimates)
  1164. for i in range(n_estimates):
  1165. # Generate integral estimate
  1166. sample = xp.asarray(qrng.random(n_points), dtype=a.dtype)
  1167. # The rationale for transposing is that this allows users to easily
  1168. # unpack `x` into separate variables, if desired. This is consistent
  1169. # with the `xx` array passed into the `scipy.integrate.nquad` `func`.
  1170. x = (sample * (b - a) + a).T # (n_dim, n_points)
  1171. integrands = func(x)
  1172. estimates = xpx.at(estimates)[i].set(sum_product(integrands, dA, log))
  1173. # Get a new, independently-scrambled QRNG for next time
  1174. qrng = type(qrng)(seed=rngs[i], **qrng._init_quad)
  1175. integral = mean(estimates, log)
  1176. standard_error = sem(estimates, m=integral, log=log)
  1177. integral = integral + xp.pi*1j if (log and sign < 0) else integral*sign
  1178. return QMCQuadResult(integral, standard_error)