test_cdflib.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. """
  2. Test cdflib functions versus mpmath, if available.
  3. The following functions still need tests:
  4. - ncfdtridfn
  5. - ncfdtridfd
  6. - ncfdtrinc
  7. - nbdtrik
  8. - nbdtrin
  9. - nctdtridf
  10. - nctdtrinc
  11. """
  12. import itertools
  13. import numpy as np
  14. from numpy.testing import assert_equal, assert_allclose, assert_array_equal
  15. import pytest
  16. import scipy.special as sp
  17. from scipy.special._testutils import (
  18. MissingModule, check_version, FuncData)
  19. from scipy.special._mptestutils import (
  20. Arg, IntArg, get_args, mpf2float, assert_mpmath_equal)
  21. try:
  22. import mpmath
  23. except ImportError:
  24. mpmath = MissingModule('mpmath')
  25. class ProbArg:
  26. """Generate a set of probabilities on [0, 1]."""
  27. def __init__(self):
  28. # Include the endpoints for compatibility with Arg et. al.
  29. self.a = 0
  30. self.b = 1
  31. def values(self, n):
  32. """Return an array containing approximately n numbers."""
  33. m = max(1, n//3)
  34. v1 = np.logspace(-30, np.log10(0.3), m)
  35. v2 = np.linspace(0.3, 0.7, m + 1, endpoint=False)[1:]
  36. v3 = 1 - np.logspace(np.log10(0.3), -15, m)
  37. v = np.r_[v1, v2, v3]
  38. return np.unique(v)
  39. class EndpointFilter:
  40. def __init__(self, a, b, rtol, atol):
  41. self.a = a
  42. self.b = b
  43. self.rtol = rtol
  44. self.atol = atol
  45. def __call__(self, x):
  46. mask1 = np.abs(x - self.a) < self.rtol*np.abs(self.a) + self.atol
  47. mask2 = np.abs(x - self.b) < self.rtol*np.abs(self.b) + self.atol
  48. return np.where(mask1 | mask2, False, True)
  49. class _CDFData:
  50. def __init__(self, spfunc, mpfunc, index, argspec, spfunc_first=True,
  51. dps=20, n=5000, rtol=None, atol=None,
  52. endpt_rtol=None, endpt_atol=None):
  53. self.spfunc = spfunc
  54. self.mpfunc = mpfunc
  55. self.index = index
  56. self.argspec = argspec
  57. self.spfunc_first = spfunc_first
  58. self.dps = dps
  59. self.n = n
  60. self.rtol = rtol
  61. self.atol = atol
  62. if not isinstance(argspec, list):
  63. self.endpt_rtol = None
  64. self.endpt_atol = None
  65. elif endpt_rtol is not None or endpt_atol is not None:
  66. if isinstance(endpt_rtol, list):
  67. self.endpt_rtol = endpt_rtol
  68. else:
  69. self.endpt_rtol = [endpt_rtol]*len(self.argspec)
  70. if isinstance(endpt_atol, list):
  71. self.endpt_atol = endpt_atol
  72. else:
  73. self.endpt_atol = [endpt_atol]*len(self.argspec)
  74. else:
  75. self.endpt_rtol = None
  76. self.endpt_atol = None
  77. def idmap(self, *args):
  78. if self.spfunc_first:
  79. res = self.spfunc(*args)
  80. if np.isnan(res):
  81. return np.nan
  82. args = list(args)
  83. args[self.index] = res
  84. with mpmath.workdps(self.dps):
  85. res = self.mpfunc(*tuple(args))
  86. # Imaginary parts are spurious
  87. res = mpf2float(res.real)
  88. else:
  89. with mpmath.workdps(self.dps):
  90. res = self.mpfunc(*args)
  91. res = mpf2float(res.real)
  92. args = list(args)
  93. args[self.index] = res
  94. res = self.spfunc(*tuple(args))
  95. return res
  96. def get_param_filter(self):
  97. if self.endpt_rtol is None and self.endpt_atol is None:
  98. return None
  99. filters = []
  100. for rtol, atol, spec in zip(self.endpt_rtol, self.endpt_atol, self.argspec):
  101. if rtol is None and atol is None:
  102. filters.append(None)
  103. continue
  104. elif rtol is None:
  105. rtol = 0.0
  106. elif atol is None:
  107. atol = 0.0
  108. filters.append(EndpointFilter(spec.a, spec.b, rtol, atol))
  109. return filters
  110. def check(self):
  111. # Generate values for the arguments
  112. args = get_args(self.argspec, self.n)
  113. param_filter = self.get_param_filter()
  114. param_columns = tuple(range(args.shape[1]))
  115. result_columns = args.shape[1]
  116. args = np.hstack((args, args[:, self.index].reshape(args.shape[0], 1)))
  117. FuncData(self.idmap, args,
  118. param_columns=param_columns, result_columns=result_columns,
  119. rtol=self.rtol, atol=self.atol, vectorized=False,
  120. param_filter=param_filter).check()
  121. def _assert_inverts(*a, **kw):
  122. d = _CDFData(*a, **kw)
  123. d.check()
  124. def _binomial_cdf(k, n, p):
  125. k, n, p = mpmath.mpf(k), mpmath.mpf(n), mpmath.mpf(p)
  126. if k <= 0:
  127. return mpmath.mpf(0)
  128. elif k >= n:
  129. return mpmath.mpf(1)
  130. onemp = mpmath.fsub(1, p, exact=True)
  131. return mpmath.betainc(n - k, k + 1, x2=onemp, regularized=True)
  132. def _f_cdf(dfn, dfd, x):
  133. if x < 0:
  134. return mpmath.mpf(0)
  135. dfn, dfd, x = mpmath.mpf(dfn), mpmath.mpf(dfd), mpmath.mpf(x)
  136. ub = dfn*x/(dfn*x + dfd)
  137. res = mpmath.betainc(dfn/2, dfd/2, x2=ub, regularized=True)
  138. return res
  139. def _student_t_cdf(df, t, dps=None):
  140. if dps is None:
  141. dps = mpmath.mp.dps
  142. with mpmath.workdps(dps):
  143. df, t = mpmath.mpf(df), mpmath.mpf(t)
  144. fac = mpmath.hyp2f1(0.5, 0.5*(df + 1), 1.5, -t**2/df)
  145. fac *= t*mpmath.gamma(0.5*(df + 1))
  146. fac /= mpmath.sqrt(mpmath.pi*df)*mpmath.gamma(0.5*df)
  147. return 0.5 + fac
  148. def _noncentral_chi_pdf(t, df, nc):
  149. res = mpmath.besseli(df/2 - 1, mpmath.sqrt(nc*t))
  150. res *= mpmath.exp(-(t + nc)/2)*(t/nc)**(df/4 - 1/2)/2
  151. return res
  152. def _noncentral_chi_cdf(x, df, nc, dps=None):
  153. if dps is None:
  154. dps = mpmath.mp.dps
  155. x, df, nc = mpmath.mpf(x), mpmath.mpf(df), mpmath.mpf(nc)
  156. with mpmath.workdps(dps):
  157. res = mpmath.quad(lambda t: _noncentral_chi_pdf(t, df, nc), [0, x])
  158. return res
  159. def _tukey_lmbda_quantile(p, lmbda):
  160. # For lmbda != 0
  161. return (p**lmbda - (1 - p)**lmbda)/lmbda
  162. @pytest.mark.slow
  163. @check_version(mpmath, '0.19')
  164. class TestCDFlib:
  165. @pytest.mark.xfail(run=False)
  166. def test_bdtrik(self):
  167. _assert_inverts(
  168. sp.bdtrik,
  169. _binomial_cdf,
  170. 0, [ProbArg(), IntArg(1, 1000), ProbArg()],
  171. rtol=1e-4)
  172. def test_bdtrin(self):
  173. _assert_inverts(
  174. sp.bdtrin,
  175. _binomial_cdf,
  176. 1, [IntArg(1, 1000), ProbArg(), ProbArg()],
  177. rtol=1e-4, endpt_atol=[None, None, 1e-6])
  178. def test_btdtria(self):
  179. _assert_inverts(
  180. sp.btdtria,
  181. lambda a, b, x: mpmath.betainc(a, b, x2=x, regularized=True),
  182. 0, [ProbArg(), Arg(0, 3e2, inclusive_a=False),
  183. Arg(0, 1, inclusive_a=False, inclusive_b=False)],
  184. rtol=1e-12)
  185. def test_btdtrib(self):
  186. # Use small values of a or mpmath doesn't converge
  187. _assert_inverts(
  188. sp.btdtrib,
  189. lambda a, b, x: mpmath.betainc(a, b, x2=x, regularized=True),
  190. 1,
  191. [Arg(0, 1e2, inclusive_a=False), ProbArg(),
  192. Arg(0, 1, inclusive_a=False, inclusive_b=False)],
  193. rtol=1e-7,
  194. endpt_atol=[None, 1e-18, 1e-15])
  195. @pytest.mark.xfail(run=False)
  196. def test_fdtridfd(self):
  197. _assert_inverts(
  198. sp.fdtridfd,
  199. _f_cdf,
  200. 1,
  201. [IntArg(1, 100), ProbArg(), Arg(0, 100, inclusive_a=False)],
  202. rtol=1e-7)
  203. def test_gdtria(self):
  204. _assert_inverts(
  205. sp.gdtria,
  206. lambda a, b, x: mpmath.gammainc(b, b=a*x, regularized=True),
  207. 0,
  208. [ProbArg(), Arg(0, 1e3, inclusive_a=False),
  209. Arg(0, 1e4, inclusive_a=False)],
  210. rtol=1e-12,
  211. endpt_atol=[None, 1e-7, 1e-10])
  212. def test_gdtrib(self):
  213. # Use small values of a and x or mpmath doesn't converge
  214. _assert_inverts(
  215. sp.gdtrib,
  216. lambda a, b, x: mpmath.gammainc(b, b=a*x, regularized=True),
  217. 1,
  218. [Arg(0, 1e2, inclusive_a=False), ProbArg(),
  219. Arg(0, 1e3, inclusive_a=False)],
  220. rtol=1e-5)
  221. def test_gdtrix(self):
  222. _assert_inverts(
  223. sp.gdtrix,
  224. lambda a, b, x: mpmath.gammainc(b, b=a*x, regularized=True),
  225. 2,
  226. [Arg(0, 1e3, inclusive_a=False), Arg(0, 1e3, inclusive_a=False),
  227. ProbArg()],
  228. rtol=1e-7,
  229. endpt_atol=[None, 1e-7, 1e-10])
  230. # Overall nrdtrimn and nrdtrisd are not performing well with infeasible/edge
  231. # combinations of sigma and x, hence restricted the domains to still use the
  232. # testing machinery, also see gh-20069
  233. # nrdtrimn signature: p, sd, x
  234. # nrdtrisd signature: mn, p, x
  235. def test_nrdtrimn(self):
  236. _assert_inverts(
  237. sp.nrdtrimn,
  238. lambda x, y, z: mpmath.ncdf(z, x, y),
  239. 0,
  240. [ProbArg(), # CDF value p
  241. Arg(0.1, np.inf, inclusive_a=False, inclusive_b=False), # sigma
  242. Arg(-1e10, 1e10)], # x
  243. rtol=1e-5)
  244. def test_nrdtrisd(self):
  245. _assert_inverts(
  246. sp.nrdtrisd,
  247. lambda x, y, z: mpmath.ncdf(z, x, y),
  248. 1,
  249. [Arg(-np.inf, 10, inclusive_a=False, inclusive_b=False), # mn
  250. ProbArg(), # CDF value p
  251. Arg(10, 1e100)], # x
  252. rtol=1e-5)
  253. def test_stdtr(self):
  254. # Ideally the left endpoint for Arg() should be 0.
  255. assert_mpmath_equal(
  256. sp.stdtr,
  257. _student_t_cdf,
  258. [IntArg(1, 100), Arg(1e-10, np.inf)], rtol=1e-7)
  259. @pytest.mark.xfail(run=False)
  260. def test_stdtridf(self):
  261. _assert_inverts(
  262. sp.stdtridf,
  263. _student_t_cdf,
  264. 0, [ProbArg(), Arg()], rtol=1e-7)
  265. def test_stdtrit(self):
  266. _assert_inverts(
  267. sp.stdtrit,
  268. _student_t_cdf,
  269. 1, [IntArg(1, 100), ProbArg()], rtol=1e-7,
  270. endpt_atol=[None, 1e-10])
  271. def test_chdtriv(self):
  272. _assert_inverts(
  273. sp.chdtriv,
  274. lambda v, x: mpmath.gammainc(v/2, b=x/2, regularized=True),
  275. 0, [ProbArg(), IntArg(1, 100)], rtol=1e-4)
  276. @pytest.mark.xfail(run=False)
  277. def test_chndtridf(self):
  278. # Use a larger atol since mpmath is doing numerical integration
  279. _assert_inverts(
  280. sp.chndtridf,
  281. _noncentral_chi_cdf,
  282. 1, [Arg(0, 100, inclusive_a=False), ProbArg(),
  283. Arg(0, 100, inclusive_a=False)],
  284. n=1000, rtol=1e-4, atol=1e-15)
  285. @pytest.mark.xfail(run=False)
  286. def test_chndtrinc(self):
  287. # Use a larger atol since mpmath is doing numerical integration
  288. _assert_inverts(
  289. sp.chndtrinc,
  290. _noncentral_chi_cdf,
  291. 2, [Arg(0, 100, inclusive_a=False), IntArg(1, 100), ProbArg()],
  292. n=1000, rtol=1e-4, atol=1e-15)
  293. def test_chndtrix(self):
  294. # Use a larger atol since mpmath is doing numerical integration
  295. _assert_inverts(
  296. sp.chndtrix,
  297. _noncentral_chi_cdf,
  298. 0, [ProbArg(), IntArg(1, 100), Arg(0, 100, inclusive_a=False)],
  299. n=1000, rtol=1e-4, atol=1e-15,
  300. endpt_atol=[1e-6, None, None])
  301. def test_tklmbda_zero_shape(self):
  302. # When lmbda = 0 the CDF has a simple closed form
  303. one = mpmath.mpf(1)
  304. assert_mpmath_equal(
  305. lambda x: sp.tklmbda(x, 0),
  306. lambda x: one/(mpmath.exp(-x) + one),
  307. [Arg()], rtol=1e-7)
  308. def test_tklmbda_neg_shape(self):
  309. _assert_inverts(
  310. sp.tklmbda,
  311. _tukey_lmbda_quantile,
  312. 0, [ProbArg(), Arg(-25, 0, inclusive_b=False)],
  313. spfunc_first=False, rtol=1e-5,
  314. endpt_atol=[1e-9, 1e-5])
  315. @pytest.mark.xfail(run=False)
  316. def test_tklmbda_pos_shape(self):
  317. _assert_inverts(
  318. sp.tklmbda,
  319. _tukey_lmbda_quantile,
  320. 0, [ProbArg(), Arg(0, 100, inclusive_a=False)],
  321. spfunc_first=False, rtol=1e-5)
  322. # The values of lmdba are chosen so that 1/lmbda is exact.
  323. @pytest.mark.parametrize('lmbda', [0.5, 1.0, 8.0])
  324. def test_tklmbda_lmbda1(self, lmbda):
  325. bound = 1/lmbda
  326. assert_equal(sp.tklmbda([-bound, bound], lmbda), [0.0, 1.0])
  327. funcs = [
  328. ("btdtria", 3),
  329. ("btdtrib", 3),
  330. ("bdtrik", 3),
  331. ("bdtrin", 3),
  332. ("chdtriv", 2),
  333. ("chndtr", 3),
  334. ("chndtrix", 3),
  335. ("chndtridf", 3),
  336. ("chndtrinc", 3),
  337. ("fdtridfd", 3),
  338. ("ncfdtr", 4),
  339. ("ncfdtri", 4),
  340. ("ncfdtridfn", 4),
  341. ("ncfdtridfd", 4),
  342. ("ncfdtrinc", 4),
  343. ("gdtrix", 3),
  344. ("gdtrib", 3),
  345. ("gdtria", 3),
  346. ("nbdtrik", 3),
  347. ("nbdtrin", 3),
  348. ("nrdtrimn", 3),
  349. ("nrdtrisd", 3),
  350. ("pdtrik", 2),
  351. ("stdtr", 2),
  352. ("stdtrit", 2),
  353. ("stdtridf", 2),
  354. ("nctdtr", 3),
  355. ("nctdtrit", 3),
  356. ("nctdtridf", 3),
  357. ("nctdtrinc", 3),
  358. ("tklmbda", 2),
  359. ]
  360. @pytest.mark.parametrize('func,numargs', funcs, ids=[x[0] for x in funcs])
  361. def test_nonfinite(func, numargs):
  362. rng = np.random.default_rng(1701299355559735)
  363. func = getattr(sp, func)
  364. args_choices = [(float(x), np.nan, np.inf, -np.inf) for x in rng.random(numargs)]
  365. for args in itertools.product(*args_choices):
  366. res = func(*args)
  367. if any(np.isnan(x) for x in args):
  368. # Nan inputs should result to nan output
  369. assert_equal(res, np.nan)
  370. else:
  371. # All other inputs should return something (but not
  372. # raise exceptions or cause hangs)
  373. pass
  374. def test_chndtrix_gh2158():
  375. # test that gh-2158 is resolved; previously this blew up
  376. res = sp.chndtrix(0.999999, 2, np.arange(20.)+1e-6)
  377. # Generated in R
  378. # options(digits=16)
  379. # ncp <- seq(0, 19) + 1e-6
  380. # print(qchisq(0.999999, df = 2, ncp = ncp))
  381. res_exp = [27.63103493142305, 35.25728589950540, 39.97396073236288,
  382. 43.88033702110538, 47.35206403482798, 50.54112500166103,
  383. 53.52720257322766, 56.35830042867810, 59.06600769498512,
  384. 61.67243118946381, 64.19376191277179, 66.64228141346548,
  385. 69.02756927200180, 71.35726934749408, 73.63759723904816,
  386. 75.87368842650227, 78.06984431185720, 80.22971052389806,
  387. 82.35640899964173, 84.45263768373256]
  388. assert_allclose(res, res_exp)
  389. def test_nctdtrinc_gh19896():
  390. # test that gh-19896 is resolved.
  391. # Compared to SciPy 1.11 results from Fortran code.
  392. dfarr = [0.001, 0.98, 9.8, 98, 980, 10000, 98, 9.8, 0.98, 0.001]
  393. parr = [0.001, 0.1, 0.3, 0.8, 0.999, 0.001, 0.1, 0.3, 0.8, 0.999]
  394. tarr = [0.0015, 0.15, 1.5, 15, 300, 0.0015, 0.15, 1.5, 15, 300]
  395. desired = [3.090232306168629, 1.406141304556198, 2.014225177124157,
  396. 13.727067118283456, 278.9765683871208, 3.090232306168629,
  397. 1.4312427877936222, 2.014225177124157, 3.712743137978295,
  398. -3.086951096691082]
  399. actual = sp.nctdtrinc(dfarr, parr, tarr)
  400. assert_allclose(actual, desired, rtol=5e-12, atol=0.0)
  401. def test_stdtr_stdtrit_neg_inf():
  402. # -inf was treated as +inf and values from the normal were returned
  403. assert np.all(np.isnan(sp.stdtr(-np.inf, [-np.inf, -1.0, 0.0, 1.0, np.inf])))
  404. assert np.all(np.isnan(sp.stdtrit(-np.inf, [0.0, 0.25, 0.5, 0.75, 1.0])))
  405. def test_bdtrik_nbdtrik_inf():
  406. y = np.array(
  407. [np.nan,-np.inf,-10.0, -1.0, 0.0, .00001, .5, 0.9999, 1.0, 10.0, np.inf])
  408. y = y[:,None]
  409. p = np.atleast_2d(
  410. [np.nan, -np.inf, -10.0, -1.0, 0.0, .00001, .5, 1.0, np.inf])
  411. assert np.all(np.isnan(sp.bdtrik(y, np.inf, p)))
  412. assert np.all(np.isnan(sp.nbdtrik(y, np.inf, p)))
  413. @pytest.mark.parametrize(
  414. "dfn,dfd,nc,f,expected_cdf",
  415. [[100.0, 0.1, 0.1, 100.0, 0.29787396410092676],
  416. [100.0, 100.0, 0.01, 0.1, 4.4344737598690424e-26],
  417. [100.0, 0.01, 0.1, 0.01, 0.002848616633080384],
  418. [10.0, 0.01, 1.0, 0.1, 0.012339557729057956],
  419. [100.0, 100.0, 0.01, 0.01, 1.8926477420964936e-72],
  420. [1.0, 100.0, 100.0, 0.1, 1.7925940526821304e-22],
  421. [1.0, 0.01, 100.0, 10.0, 0.012334711965024968],
  422. [1.0, 0.01, 10.0, 0.01, 0.00021944525290299],
  423. [10.0, 1.0, 0.1, 100.0, 0.9219345555070705],
  424. [0.1, 0.1, 1.0, 1.0, 0.3136335813423239],
  425. [100.0, 100.0, 0.1, 10.0, 1.0],
  426. [1.0, 0.1, 100.0, 10.0, 0.02926064279680897],
  427. [1e-100, 3, 1.5, 1e100, 0.611815287345399]
  428. ]
  429. )
  430. def test_ncfdtr_ncfdtri(dfn, dfd, nc, f, expected_cdf):
  431. # Reference values computed with mpmath with the following script
  432. #
  433. # import numpy as np
  434. #
  435. # from mpmath import mp
  436. # from scipy.special import ncfdtr
  437. #
  438. # mp.dps = 100
  439. #
  440. # def mp_ncfdtr(dfn, dfd, nc, f):
  441. # # Uses formula 26.2.20 from Abramowitz and Stegun.
  442. # dfn, dfd, nc, f = map(mp.mpf, (dfn, dfd, nc, f))
  443. # def term(j):
  444. # result = mp.exp(-nc/2)*(nc/2)**j / mp.factorial(j)
  445. # result *= mp.betainc(
  446. # dfn/2 + j, dfd/2, 0, f*dfn/(f*dfn + dfd), regularized=True
  447. # )
  448. # return result
  449. # result = mp.nsum(term, [0, mp.inf])
  450. # return float(result)
  451. #
  452. # dfn = np.logspace(-2, 2, 5)
  453. # dfd = np.logspace(-2, 2, 5)
  454. # nc = np.logspace(-2, 2, 5)
  455. # f = np.logspace(-2, 2, 5)
  456. #
  457. # dfn, dfd, nc, f = np.meshgrid(dfn, dfd, nc, f)
  458. # dfn, dfd, nc, f = map(np.ravel, (dfn, dfd, nc, f))
  459. #
  460. # cases = []
  461. # re = []
  462. # for x0, x1, x2, x3 in zip(*(dfn, dfd, nc, f)):
  463. # observed = ncfdtr(x0, x1, x2, x3)
  464. # expected = mp_ncfdtr(x0, x1, x2, x3)
  465. # cases.append((x0, x1, x2, x3, expected))
  466. # re.append((abs(expected - observed)/abs(expected)))
  467. #
  468. # assert np.max(re) < 1e-13
  469. #
  470. # rng = np.random.default_rng(1234)
  471. # sample_idx = rng.choice(len(re), replace=False, size=12)
  472. # cases = np.array(cases)[sample_idx].tolist()
  473. assert_allclose(sp.ncfdtr(dfn, dfd, nc, f), expected_cdf, rtol=1e-13, atol=0)
  474. # testing tails where the CDF reaches 0 or 1 does not make sense for inverses
  475. # of a CDF as they are not bijective in these regions
  476. if 0 < expected_cdf < 1:
  477. assert_allclose(sp.ncfdtri(dfn, dfd, nc, expected_cdf), f, rtol=5e-11)
  478. @pytest.mark.parametrize(
  479. "args",
  480. [(-1.0, 0.1, 0.1, 0.5),
  481. (1, -1.0, 0.1, 0.5),
  482. (1, 1, -1.0, 0.5),
  483. (1, 1, 1, 100),
  484. (1, 1, 1, -1)]
  485. )
  486. def test_ncfdtri_domain_error(args):
  487. with sp.errstate(domain="raise"):
  488. with pytest.raises(sp.SpecialFunctionError, match="domain"):
  489. sp.ncfdtri(*args)
  490. class TestNoncentralTFunctions:
  491. # Reference values computed with mpmath with the following script
  492. # Formula from:
  493. # Lenth, Russell V (1989). "Algorithm AS 243: Cumulative Distribution Function
  494. # of the Non-central t Distribution". Journal of the Royal Statistical Society,
  495. # Series C. 38 (1): 185-189
  496. #
  497. # Warning: may take a long time to run
  498. #
  499. # from mpmath import mp
  500. # mp.dps = 400
  501. # def nct_cdf(df, nc, x):
  502. # df, nc, x = map(mp.mpf, (df, nc, x))
  503. # def f(df, nc, x):
  504. # phi = mp.ncdf(-nc)
  505. # y = x * x / (x * x + df)
  506. # constant = mp.exp(-nc * nc / 2.)
  507. # def term(j):
  508. # intermediate = constant * (nc *nc / 2.)**j
  509. # p = intermediate/mp.factorial(j)
  510. # q = nc / (mp.sqrt(2.) * mp.gamma(j + 1.5)) * intermediate
  511. # first_beta_term = mp.betainc(j + 0.5, df/2., x2=y,
  512. # regularized=True)
  513. # second_beta_term = mp.betainc(j + mp.one, df/2., x2=y,
  514. # regularized=True)
  515. # return p * first_beta_term + q * second_beta_term
  516. # sum_term = mp.nsum(term, [0, mp.inf])
  517. # f = phi + 0.5 * sum_term
  518. # return f
  519. # if x >= 0:
  520. # result = f(df, nc, x)
  521. # else:
  522. # result = mp.one - f(df, -nc, x)
  523. # return float(result)
  524. @pytest.mark.parametrize("df, nc, x, expected_cdf", [
  525. (0.98, -3.8, 0.0015, 0.9999279987514815),
  526. (0.98, -3.8, 0.15, 0.9999528361700505),
  527. (0.98, -3.8, 1.5, 0.9999908823016942),
  528. (0.98, -3.8, 15, 0.9999990264591945),
  529. (0.98, 0.38, 0.0015, 0.35241533122693),
  530. (0.98, 0.38, 0.15, 0.39749697267146983),
  531. (0.98, 0.38, 1.5, 0.716862963488558),
  532. (0.98, 0.38, 15, 0.9656246449257494),
  533. (0.98, 3.8, 0.0015, 7.26973354942293e-05),
  534. (0.98, 3.8, 0.15, 0.00012416481147589105),
  535. (0.98, 3.8, 1.5, 0.035388035775454095),
  536. (0.98, 3.8, 15, 0.7954826975430583),
  537. (0.98, 38, 0.0015, 3.02106943e-316),
  538. (0.98, 38, 0.15, 6.069970616996603e-309),
  539. (0.98, 38, 1.5, 2.591995360483094e-97),
  540. (0.98, 38, 15, 0.011927265886910935),
  541. (9.8, -3.8, 0.0015, 0.9999280776192786),
  542. (9.8, -3.8, 0.15, 0.9999599410685442),
  543. (9.8, -3.8, 1.5, 0.9999997432394788),
  544. (9.8, -3.8, 15, 0.9999999999999984),
  545. (9.8, 0.38, 0.0015, 0.3525155979107491),
  546. (9.8, 0.38, 0.15, 0.40763120140379194),
  547. (9.8, 0.38, 1.5, 0.8476794017024651),
  548. (9.8, 0.38, 15, 0.9999999297116268),
  549. (9.8, 3.8, 0.0015, 7.277620328149153e-05),
  550. (9.8, 3.8, 0.15, 0.00013024802220900652),
  551. (9.8, 3.8, 1.5, 0.013477432800072933),
  552. (9.8, 3.8, 15, 0.999850151230648),
  553. (9.8, 38, 0.0015, 3.05066095e-316),
  554. (9.8, 38, 0.15, 1.79065514676e-313),
  555. (9.8, 38, 1.5, 2.0935940165900746e-249),
  556. (9.8, 38, 15, 2.252076291604796e-09),
  557. (98, -3.8, 0.0015, 0.9999280875149109),
  558. (98, -3.8, 0.15, 0.9999608250170452),
  559. (98, -3.8, 1.5, 0.9999999304757682),
  560. (98, -3.8, 15, 1.0),
  561. (98, 0.38, 0.0015, 0.35252817848596313),
  562. (98, 0.38, 0.15, 0.40890253001794846),
  563. (98, 0.38, 1.5, 0.8664672830006552),
  564. (98, 0.38, 15, 1.0),
  565. (98, 3.8, 0.0015, 7.278609891281275e-05),
  566. (98, 3.8, 0.15, 0.0001310318674827004),
  567. (98, 3.8, 1.5, 0.010990879189991727),
  568. (98, 3.8, 15, 0.9999999999999989),
  569. (98, 38, 0.0015, 3.05437385e-316),
  570. (98, 38, 0.15, 9.1668336166e-314),
  571. (98, 38, 1.5, 1.8085884236563926e-288),
  572. (98, 38, 15, 2.7740532792035907e-50),
  573. (980, -3.8, 0.0015, 0.9999280885188965),
  574. (980, -3.8, 0.15, 0.9999609144559273),
  575. (980, -3.8, 1.5, 0.9999999410050979),
  576. (980, -3.8, 15, 1.0),
  577. (980, 0.38, 0.0015, 0.3525294548792812),
  578. (980, 0.38, 0.15, 0.4090315324657382),
  579. (980, 0.38, 1.5, 0.8684247068517293),
  580. (980, 0.38, 15, 1.0),
  581. (980, 3.8, 0.0015, 7.278710289828983e-05),
  582. (980, 3.8, 0.15, 0.00013111131667906573),
  583. (980, 3.8, 1.5, 0.010750678886113882),
  584. (980, 3.8, 15, 1.0),
  585. (980, 38, 0.0015, 3.0547506e-316),
  586. (980, 38, 0.15, 8.6191646313e-314),
  587. # revisit when boost1.90 is released,
  588. # see https://github.com/boostorg/math/issues/1308
  589. pytest.param(980, 38, 1.5, 1.1824454111413493e-291,
  590. marks=pytest.mark.xfail(
  591. reason="Bug in underlying Boost math implementation")),
  592. (980, 38, 15, 5.407535300713606e-105)
  593. ])
  594. def test_gh19896(self, df, nc, x, expected_cdf):
  595. # test that gh-19896 is resolved.
  596. # Originally this was a regression test that used the old Fortran results
  597. # as a reference. The Fortran results were not accurate, so the reference
  598. # values were recomputed with mpmath.
  599. nctdtr_result = sp.nctdtr(df, nc, x)
  600. assert_allclose(nctdtr_result, expected_cdf, rtol=1e-13, atol=1e-303)
  601. def test_nctdtr_gh8344(self):
  602. # test that gh-8344 is resolved.
  603. df, nc, x = 3000, 3, 0.1
  604. expected = 0.0018657780826323328
  605. assert_allclose(sp.nctdtr(df, nc, x), expected, rtol=1e-14)
  606. @pytest.mark.parametrize(
  607. "df, nc, x, expected, rtol",
  608. # revisit tolerances when boost1.90 is released,
  609. # see https://github.com/boostorg/math/issues/1308
  610. [[3., 5., -2., 1.5645373999149622e-09, 2e-8],
  611. [1000., 10., 1., 1.1493552133826623e-19, 1e-13],
  612. [1e-5, -6., 2., 0.9999999990135003, 1e-13],
  613. [10., 20., 0.15, 6.426530505957303e-88, 1e-13],
  614. [1., 1., np.inf, 1.0, 0.0],
  615. [1., 1., -np.inf, 0.0, 0.0]
  616. ]
  617. )
  618. def test_nctdtr_accuracy(self, df, nc, x, expected, rtol):
  619. assert_allclose(sp.nctdtr(df, nc, x), expected, rtol=rtol)
  620. @pytest.mark.parametrize("df, nc, x, expected_cdf", [
  621. (0.98, 38, 1.5, 2.591995360483094e-97),
  622. (3000, 3, 0.1, 0.0018657780826323328),
  623. (0.98, -3.8, 15, 0.9999990264591945),
  624. (9.8, 38, 15, 2.252076291604796e-09),
  625. ])
  626. def test_nctdtrit(self, df, nc, x, expected_cdf):
  627. assert_allclose(sp.nctdtrit(df, nc, expected_cdf), x, rtol=1e-10)
  628. class TestNoncentralChiSquaredFunctions:
  629. def test_chndtr_and_inverses_against_wolfram_alpha(self):
  630. # Each row holds (x, nu, lam, expected_value)
  631. # These values were computed using Wolfram Alpha with
  632. # CDF[NoncentralChiSquareDistribution[nu, lam], x]
  633. values = np.array([
  634. [25.00, 20.0, 400, 4.1210655112396197139e-57],
  635. [25.00, 8.00, 250, 2.3988026526832425878e-29],
  636. [0.001, 8.00, 40., 5.3761806201366039084e-24],
  637. [0.010, 8.00, 40., 5.45396231055999457039e-20],
  638. [20.00, 2.00, 107, 1.39390743555819597802e-9],
  639. [22.50, 2.00, 107, 7.11803307138105870671e-9],
  640. [25.00, 2.00, 107, 3.11041244829864897313e-8],
  641. [3.000, 2.00, 1.0, 0.62064365321954362734],
  642. [350.0, 300., 10., 0.93880128006276407710],
  643. [100.0, 13.5, 10., 0.99999999650104210949],
  644. [700.0, 20.0, 400, 0.99999999925680650105],
  645. [150.0, 13.5, 10., 0.99999999999999983046],
  646. [160.0, 13.5, 10., 0.99999999999999999518], # 1.0
  647. ])
  648. cdf = sp.chndtr(values[:, 0], values[:, 1], values[:, 2])
  649. assert_allclose(cdf, values[:, 3], rtol=1e-13)
  650. # the last two values are very close to 1.0, so we do not
  651. # test the inverses for them
  652. x = sp.chndtrix(values[:, 3], values[:, 1], values[:, 2])
  653. assert_allclose(x[:-2], values[:-2, 0], rtol=1e-8)
  654. df = sp.chndtridf(values[:, 0], values[:, 3], values[:, 2])
  655. assert_allclose(df[:-2], values[:-2, 1], rtol=1e-8)
  656. nc = sp.chndtrinc(values[:, 0], values[:, 1], values[:, 3])
  657. assert_allclose(nc[:-2], values[:-2, 2], rtol=1e-8)
  658. # CDF Reference values computed with mpmath with the following script
  659. # Formula from:
  660. # https://www.boost.org/doc/libs/1_87_0/libs/math/doc/html/math_toolkit/dist_ref/dists/nc_chi_squared_dist.html # noqa: E501
  661. # which cites:
  662. # Computing discrete mixtures of continuous distributions: noncentral chisquare,
  663. # noncentral t and the distribution of the square of the
  664. # sample multiple correlation coefficient",
  665. # Denise Benton and K. Krishnamoorthy,
  666. # Computational Statistics & Data Analysis, 43, (2003), 249-267
  667. # Warning: may take a long time to run
  668. # from mpmath import mp
  669. #
  670. # mp.dps = 400
  671. #
  672. # def noncentral_chi_squared_cdf(x, df, nc):
  673. # x, df, nc = map(mp.mpf, (x, df, nc))
  674. # def term(i):
  675. # return mp.exp(-nc/2) * (nc/2)**i / mp.factorial(i) *
  676. # mp.gammainc(df/2 + i, 0, x/2, regularized=True)
  677. # return float(mp.nsum(term, [0, mp.inf]))
  678. @pytest.mark.parametrize(
  679. "x, df, nc, expected_cdf, rtol_cdf, rtol_inv",
  680. [(0.1, 200, 50, 1.1311224867205481e-299, 1e-13, 1e-13),
  681. (1e-12, 20, 50, 3.737446313006551e-141, 1e-13, 1e-13),
  682. (1, 200, 50, 8.09760974833666e-200, 1e-13, 1e-13),
  683. (9e4, 1e5, 1e3, 1.6895533704217566e-141, 5e-12, 5e-12),
  684. (30, 3, 1.5, 0.9999508759095675, 5e-13, 5e-13),
  685. (500, 300, 1.5, 0.9999999999944232, 1e-13, 1e-5),
  686. (1400, 30, 1e3, 0.9999999612246238, 1e-13, 1e-9),
  687. (400, 50, 200, 0.9999948255072892, 1e-13, 1e-11)]
  688. )
  689. def test_tails(self, x, df, nc, expected_cdf, rtol_cdf, rtol_inv):
  690. chndtr_result = sp.chndtr(x, df, nc)
  691. assert_allclose(chndtr_result, expected_cdf, rtol=rtol_cdf)
  692. assert_allclose(sp.chndtrix(expected_cdf, df, nc), x, rtol=rtol_inv)
  693. assert_allclose(sp.chndtridf(x, expected_cdf, nc), df, rtol=rtol_inv)
  694. assert_allclose(sp.chndtrinc(x, df, expected_cdf), nc, rtol=rtol_inv)
  695. # test round-trip calculation
  696. assert_allclose(sp.chndtrix(chndtr_result, df, nc), x, rtol=rtol_inv)
  697. assert_allclose(sp.chndtridf(x, chndtr_result, nc), df, rtol=1e-7)
  698. assert_allclose(sp.chndtrinc(x, df, chndtr_result), nc, rtol=1e-5)
  699. @pytest.mark.parametrize("x, df",
  700. [(1, 3), (1, 0), (1, np.inf), (np.inf, 1)]
  701. )
  702. def test_chndtr_with_nc_zero_equals_chdtr(self, x, df):
  703. assert_allclose(sp.chndtr(x, df, 0), sp.chdtr(df, x), rtol=1e-15)
  704. @pytest.mark.parametrize("fun",
  705. [sp.chndtr, sp.chndtrix, sp.chndtridf, sp.chndtrinc]
  706. )
  707. @pytest.mark.parametrize("args",
  708. [(-1, 1, 1), (1, -1, 1), (1, 1, -1), (-1, -1, 1),
  709. (-1, 1, -1), (1, -1, -1), (-1, -1, -1)]
  710. )
  711. def test_domain_error(self, args, fun):
  712. with sp.errstate(domain="raise"):
  713. with pytest.raises(sp.SpecialFunctionError, match="domain"):
  714. fun(*args)
  715. @pytest.mark.parametrize("fun",
  716. [sp.chndtr, sp.chndtrix, sp.chndtridf, sp.chndtrinc]
  717. )
  718. @pytest.mark.parametrize("args",
  719. [(np.nan, 1, 1), (1, np.nan, 1), (1, 1, np.nan),
  720. (np.nan, np.nan, 1), (np.nan, 1, np.nan), (1, np.nan, np.nan),
  721. (np.nan, np.nan, np.nan)]
  722. )
  723. def test_nan_propagation(self, fun, args):
  724. assert np.isnan(fun(*args))
  725. @pytest.mark.parametrize(
  726. "x, df, nc, expected",
  727. [(1, 0, 1, np.nan),
  728. (1, 0, np.inf, np.nan),
  729. (1, np.inf, 1, np.nan),
  730. (1, np.inf, np.inf, 0),
  731. (1, 1, np.inf, 0),
  732. (np.inf, 0, 1, np.nan),
  733. (np.inf, 1, np.inf, np.nan),
  734. (np.inf, 1, 1, 1),
  735. (np.inf, np.inf, np.inf, np.nan)]
  736. )
  737. def test_chndtr_edge_cases(self, x, df, nc, expected):
  738. assert_allclose(sp.chndtr(x, df, nc), expected, rtol=1e-15)
  739. @pytest.mark.parametrize("x", [0.1, 100])
  740. def test_chdtriv_p_equals_1_returns_0(x):
  741. assert sp.chdtriv(1, x) == 0
  742. class TestPdtrik:
  743. @pytest.mark.parametrize("p, m, expected",
  744. [(0, 0.5, 0),
  745. (0.5, 0, 0)])
  746. def test_edge_cases(self, p, m, expected):
  747. assert sp.pdtrik(p, m) == expected
  748. @pytest.mark.parametrize("m", (0.1, 1, 10))
  749. def test_p_equals_1_returns_nan(self, m):
  750. assert np.isnan(sp.pdtrik(1, m))
  751. def test_small_probabilities(self):
  752. # Edge case: m = 0 or very small.
  753. k = sp.pdtrik([[0], [0.25], [0.95]], [0, 1e-20, 1e-6])
  754. assert_array_equal(k, np.zeros((3, 3)))
  755. def test_roundtrip_against_pdtr(self):
  756. m = [10, 50, 500]
  757. k = 5
  758. p = sp.pdtr(k, m)
  759. assert_allclose(sp.pdtrik(p, m), k, rtol=1e-15)
  760. @pytest.mark.parametrize("p, m, k",
  761. [(1.8976107553682285e-40, 100, 2),
  762. (0.48670120172085135, 100, 99),
  763. (8.30383406699052e-69, 1000, 500),
  764. (2.252837804125894e-227, 100_000, 90_000)])
  765. def test_accuracy(self, p, m, k):
  766. # Reference values for p were computed with mpmath using
  767. # mp.gammainc(k+1, a=m, regularized=True)
  768. assert_allclose(sp.pdtrik(p, m), k, rtol=1e-15)
  769. @pytest.mark.parametrize("a, b, p, ref", [
  770. (0, 0, 0, np.nan),
  771. (0, 0, 1, np.nan),
  772. (0, np.inf, 0, np.nan),
  773. (0, np.inf, 1, np.nan),
  774. (np.inf, 0, 0, np.nan),
  775. (np.inf, 0, 1, np.nan),
  776. (np.inf, np.inf, 0, np.nan),
  777. (np.inf, np.inf, 1, np.nan)
  778. ])
  779. def test_gdtrix_edge_cases(a, b, p, ref):
  780. assert_equal(sp.gdtrix(a, b, p), ref)
  781. @pytest.mark.parametrize("p, b, x, ref", [
  782. (0, 0, 0, np.nan),
  783. (0, 0, np.inf, np.nan),
  784. (0, 1, 0, np.nan),
  785. (0, 1, np.inf, 0),
  786. (np.inf, 0, 0, np.nan),
  787. (np.inf, 0, np.inf, np.nan),
  788. (np.inf, 1, 0, np.nan),
  789. (np.inf, 1, np.inf, np.nan)
  790. ])
  791. def test_gdtria_edge_cases(p, b, x, ref):
  792. assert_equal(sp.gdtria(p, b, x), ref)