scipy 1.15.3__cp311-cp311-win_amd64.whl → 1.16.0rc2__cp311-cp311-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (759) hide show
  1. scipy/__config__.py +8 -8
  2. scipy/__init__.py +3 -6
  3. scipy/_cyutility.cp311-win_amd64.dll.a +0 -0
  4. scipy/_cyutility.cp311-win_amd64.pyd +0 -0
  5. scipy/_lib/_array_api.py +486 -161
  6. scipy/_lib/_array_api_compat_vendor.py +9 -0
  7. scipy/_lib/_bunch.py +4 -0
  8. scipy/_lib/_ccallback_c.cp311-win_amd64.dll.a +0 -0
  9. scipy/_lib/_ccallback_c.cp311-win_amd64.pyd +0 -0
  10. scipy/_lib/_docscrape.py +1 -1
  11. scipy/_lib/_elementwise_iterative_method.py +15 -26
  12. scipy/_lib/_fpumode.cp311-win_amd64.dll.a +0 -0
  13. scipy/_lib/_fpumode.cp311-win_amd64.pyd +0 -0
  14. scipy/_lib/_sparse.py +41 -0
  15. scipy/_lib/_test_ccallback.cp311-win_amd64.dll.a +0 -0
  16. scipy/_lib/_test_ccallback.cp311-win_amd64.pyd +0 -0
  17. scipy/_lib/_test_deprecation_call.cp311-win_amd64.dll.a +0 -0
  18. scipy/_lib/_test_deprecation_call.cp311-win_amd64.pyd +0 -0
  19. scipy/_lib/_test_deprecation_def.cp311-win_amd64.dll.a +0 -0
  20. scipy/_lib/_test_deprecation_def.cp311-win_amd64.pyd +0 -0
  21. scipy/_lib/_testutils.py +6 -2
  22. scipy/_lib/_uarray/_uarray.cp311-win_amd64.dll.a +0 -0
  23. scipy/_lib/_uarray/_uarray.cp311-win_amd64.pyd +0 -0
  24. scipy/_lib/_util.py +222 -125
  25. scipy/_lib/array_api_compat/__init__.py +4 -4
  26. scipy/_lib/array_api_compat/_internal.py +19 -6
  27. scipy/_lib/array_api_compat/common/__init__.py +1 -1
  28. scipy/_lib/array_api_compat/common/_aliases.py +365 -193
  29. scipy/_lib/array_api_compat/common/_fft.py +94 -64
  30. scipy/_lib/array_api_compat/common/_helpers.py +413 -180
  31. scipy/_lib/array_api_compat/common/_linalg.py +116 -40
  32. scipy/_lib/array_api_compat/common/_typing.py +179 -10
  33. scipy/_lib/array_api_compat/cupy/__init__.py +1 -4
  34. scipy/_lib/array_api_compat/cupy/_aliases.py +61 -41
  35. scipy/_lib/array_api_compat/cupy/_info.py +16 -6
  36. scipy/_lib/array_api_compat/cupy/_typing.py +24 -39
  37. scipy/_lib/array_api_compat/dask/array/__init__.py +6 -3
  38. scipy/_lib/array_api_compat/dask/array/_aliases.py +267 -108
  39. scipy/_lib/array_api_compat/dask/array/_info.py +105 -34
  40. scipy/_lib/array_api_compat/dask/array/fft.py +5 -8
  41. scipy/_lib/array_api_compat/dask/array/linalg.py +21 -22
  42. scipy/_lib/array_api_compat/numpy/__init__.py +13 -15
  43. scipy/_lib/array_api_compat/numpy/_aliases.py +98 -49
  44. scipy/_lib/array_api_compat/numpy/_info.py +36 -16
  45. scipy/_lib/array_api_compat/numpy/_typing.py +27 -43
  46. scipy/_lib/array_api_compat/numpy/fft.py +11 -5
  47. scipy/_lib/array_api_compat/numpy/linalg.py +75 -22
  48. scipy/_lib/array_api_compat/torch/__init__.py +3 -5
  49. scipy/_lib/array_api_compat/torch/_aliases.py +262 -159
  50. scipy/_lib/array_api_compat/torch/_info.py +27 -16
  51. scipy/_lib/array_api_compat/torch/_typing.py +3 -0
  52. scipy/_lib/array_api_compat/torch/fft.py +17 -18
  53. scipy/_lib/array_api_compat/torch/linalg.py +16 -16
  54. scipy/_lib/array_api_extra/__init__.py +26 -3
  55. scipy/_lib/array_api_extra/_delegation.py +171 -0
  56. scipy/_lib/array_api_extra/_lib/__init__.py +1 -0
  57. scipy/_lib/array_api_extra/_lib/_at.py +463 -0
  58. scipy/_lib/array_api_extra/_lib/_backends.py +46 -0
  59. scipy/_lib/array_api_extra/_lib/_funcs.py +937 -0
  60. scipy/_lib/array_api_extra/_lib/_lazy.py +357 -0
  61. scipy/_lib/array_api_extra/_lib/_testing.py +278 -0
  62. scipy/_lib/array_api_extra/_lib/_utils/__init__.py +1 -0
  63. scipy/_lib/array_api_extra/_lib/_utils/_compat.py +74 -0
  64. scipy/_lib/array_api_extra/_lib/_utils/_compat.pyi +45 -0
  65. scipy/_lib/array_api_extra/_lib/_utils/_helpers.py +559 -0
  66. scipy/_lib/array_api_extra/_lib/_utils/_typing.py +10 -0
  67. scipy/_lib/array_api_extra/_lib/_utils/_typing.pyi +105 -0
  68. scipy/_lib/array_api_extra/testing.py +359 -0
  69. scipy/_lib/decorator.py +2 -2
  70. scipy/_lib/doccer.py +1 -7
  71. scipy/_lib/messagestream.cp311-win_amd64.dll.a +0 -0
  72. scipy/_lib/messagestream.cp311-win_amd64.pyd +0 -0
  73. scipy/_lib/pyprima/__init__.py +212 -0
  74. scipy/_lib/pyprima/cobyla/__init__.py +0 -0
  75. scipy/_lib/pyprima/cobyla/cobyla.py +559 -0
  76. scipy/_lib/pyprima/cobyla/cobylb.py +714 -0
  77. scipy/_lib/pyprima/cobyla/geometry.py +226 -0
  78. scipy/_lib/pyprima/cobyla/initialize.py +215 -0
  79. scipy/_lib/pyprima/cobyla/trustregion.py +492 -0
  80. scipy/_lib/pyprima/cobyla/update.py +289 -0
  81. scipy/_lib/pyprima/common/__init__.py +0 -0
  82. scipy/_lib/pyprima/common/_bounds.py +34 -0
  83. scipy/_lib/pyprima/common/_linear_constraints.py +46 -0
  84. scipy/_lib/pyprima/common/_nonlinear_constraints.py +54 -0
  85. scipy/_lib/pyprima/common/_project.py +173 -0
  86. scipy/_lib/pyprima/common/checkbreak.py +93 -0
  87. scipy/_lib/pyprima/common/consts.py +47 -0
  88. scipy/_lib/pyprima/common/evaluate.py +99 -0
  89. scipy/_lib/pyprima/common/history.py +38 -0
  90. scipy/_lib/pyprima/common/infos.py +30 -0
  91. scipy/_lib/pyprima/common/linalg.py +435 -0
  92. scipy/_lib/pyprima/common/message.py +290 -0
  93. scipy/_lib/pyprima/common/powalg.py +131 -0
  94. scipy/_lib/pyprima/common/preproc.py +277 -0
  95. scipy/_lib/pyprima/common/present.py +5 -0
  96. scipy/_lib/pyprima/common/ratio.py +54 -0
  97. scipy/_lib/pyprima/common/redrho.py +47 -0
  98. scipy/_lib/pyprima/common/selectx.py +296 -0
  99. scipy/_lib/tests/test__util.py +105 -121
  100. scipy/_lib/tests/test_array_api.py +166 -35
  101. scipy/_lib/tests/test_bunch.py +7 -0
  102. scipy/_lib/tests/test_ccallback.py +2 -10
  103. scipy/_lib/tests/test_public_api.py +13 -0
  104. scipy/cluster/_hierarchy.cp311-win_amd64.dll.a +0 -0
  105. scipy/cluster/_hierarchy.cp311-win_amd64.pyd +0 -0
  106. scipy/cluster/_optimal_leaf_ordering.cp311-win_amd64.dll.a +0 -0
  107. scipy/cluster/_optimal_leaf_ordering.cp311-win_amd64.pyd +0 -0
  108. scipy/cluster/_vq.cp311-win_amd64.dll.a +0 -0
  109. scipy/cluster/_vq.cp311-win_amd64.pyd +0 -0
  110. scipy/cluster/hierarchy.py +393 -223
  111. scipy/cluster/tests/test_hierarchy.py +273 -335
  112. scipy/cluster/tests/test_vq.py +45 -61
  113. scipy/cluster/vq.py +39 -35
  114. scipy/conftest.py +263 -157
  115. scipy/constants/_constants.py +4 -1
  116. scipy/constants/tests/test_codata.py +2 -2
  117. scipy/constants/tests/test_constants.py +11 -18
  118. scipy/datasets/_download_all.py +15 -1
  119. scipy/datasets/_fetchers.py +7 -1
  120. scipy/datasets/_utils.py +1 -1
  121. scipy/differentiate/_differentiate.py +25 -25
  122. scipy/differentiate/tests/test_differentiate.py +24 -25
  123. scipy/fft/_basic.py +20 -0
  124. scipy/fft/_helper.py +3 -34
  125. scipy/fft/_pocketfft/helper.py +29 -1
  126. scipy/fft/_pocketfft/pypocketfft.cp311-win_amd64.dll.a +0 -0
  127. scipy/fft/_pocketfft/pypocketfft.cp311-win_amd64.pyd +0 -0
  128. scipy/fft/_pocketfft/tests/test_basic.py +2 -4
  129. scipy/fft/_pocketfft/tests/test_real_transforms.py +4 -4
  130. scipy/fft/_realtransforms.py +13 -0
  131. scipy/fft/tests/test_basic.py +27 -25
  132. scipy/fft/tests/test_fftlog.py +16 -7
  133. scipy/fft/tests/test_helper.py +18 -34
  134. scipy/fft/tests/test_real_transforms.py +8 -10
  135. scipy/fftpack/convolve.cp311-win_amd64.dll.a +0 -0
  136. scipy/fftpack/convolve.cp311-win_amd64.pyd +0 -0
  137. scipy/fftpack/tests/test_basic.py +2 -4
  138. scipy/fftpack/tests/test_real_transforms.py +8 -9
  139. scipy/integrate/_bvp.py +9 -3
  140. scipy/integrate/_cubature.py +3 -2
  141. scipy/integrate/_dop.cp311-win_amd64.dll.a +0 -0
  142. scipy/integrate/_dop.cp311-win_amd64.pyd +0 -0
  143. scipy/integrate/_lsoda.cp311-win_amd64.dll.a +0 -0
  144. scipy/integrate/_lsoda.cp311-win_amd64.pyd +0 -0
  145. scipy/integrate/_ode.py +9 -2
  146. scipy/integrate/_odepack.cp311-win_amd64.dll.a +0 -0
  147. scipy/integrate/_odepack.cp311-win_amd64.pyd +0 -0
  148. scipy/integrate/_quad_vec.py +21 -29
  149. scipy/integrate/_quadpack.cp311-win_amd64.dll.a +0 -0
  150. scipy/integrate/_quadpack.cp311-win_amd64.pyd +0 -0
  151. scipy/integrate/_quadpack_py.py +11 -7
  152. scipy/integrate/_quadrature.py +3 -3
  153. scipy/integrate/_rules/_base.py +2 -2
  154. scipy/integrate/_tanhsinh.py +48 -47
  155. scipy/integrate/_test_multivariate.cp311-win_amd64.dll.a +0 -0
  156. scipy/integrate/_test_multivariate.cp311-win_amd64.pyd +0 -0
  157. scipy/integrate/_test_odeint_banded.cp311-win_amd64.dll.a +0 -0
  158. scipy/integrate/_test_odeint_banded.cp311-win_amd64.pyd +0 -0
  159. scipy/integrate/_vode.cp311-win_amd64.dll.a +0 -0
  160. scipy/integrate/_vode.cp311-win_amd64.pyd +0 -0
  161. scipy/integrate/tests/test__quad_vec.py +0 -6
  162. scipy/integrate/tests/test_banded_ode_solvers.py +85 -0
  163. scipy/integrate/tests/test_cubature.py +21 -35
  164. scipy/integrate/tests/test_quadrature.py +6 -8
  165. scipy/integrate/tests/test_tanhsinh.py +56 -48
  166. scipy/interpolate/__init__.py +70 -58
  167. scipy/interpolate/_bary_rational.py +22 -22
  168. scipy/interpolate/_bsplines.py +119 -66
  169. scipy/interpolate/_cubic.py +65 -50
  170. scipy/interpolate/_dfitpack.cp311-win_amd64.dll.a +0 -0
  171. scipy/interpolate/_dfitpack.cp311-win_amd64.pyd +0 -0
  172. scipy/interpolate/_dierckx.cp311-win_amd64.dll.a +0 -0
  173. scipy/interpolate/_dierckx.cp311-win_amd64.pyd +0 -0
  174. scipy/interpolate/_fitpack.cp311-win_amd64.dll.a +0 -0
  175. scipy/interpolate/_fitpack.cp311-win_amd64.pyd +0 -0
  176. scipy/interpolate/_fitpack2.py +9 -6
  177. scipy/interpolate/_fitpack_impl.py +32 -26
  178. scipy/interpolate/_fitpack_repro.py +23 -19
  179. scipy/interpolate/_interpnd.cp311-win_amd64.dll.a +0 -0
  180. scipy/interpolate/_interpnd.cp311-win_amd64.pyd +0 -0
  181. scipy/interpolate/_interpolate.py +30 -12
  182. scipy/interpolate/_ndbspline.py +13 -18
  183. scipy/interpolate/_ndgriddata.py +5 -8
  184. scipy/interpolate/_polyint.py +95 -31
  185. scipy/interpolate/_ppoly.cp311-win_amd64.dll.a +0 -0
  186. scipy/interpolate/_ppoly.cp311-win_amd64.pyd +0 -0
  187. scipy/interpolate/_rbf.py +2 -2
  188. scipy/interpolate/_rbfinterp.py +1 -1
  189. scipy/interpolate/_rbfinterp_pythran.cp311-win_amd64.dll.a +0 -0
  190. scipy/interpolate/_rbfinterp_pythran.cp311-win_amd64.pyd +0 -0
  191. scipy/interpolate/_rgi.py +31 -26
  192. scipy/interpolate/_rgi_cython.cp311-win_amd64.dll.a +0 -0
  193. scipy/interpolate/_rgi_cython.cp311-win_amd64.pyd +0 -0
  194. scipy/interpolate/dfitpack.py +0 -20
  195. scipy/interpolate/interpnd.py +1 -2
  196. scipy/interpolate/tests/test_bary_rational.py +2 -2
  197. scipy/interpolate/tests/test_bsplines.py +97 -1
  198. scipy/interpolate/tests/test_fitpack2.py +39 -1
  199. scipy/interpolate/tests/test_interpnd.py +32 -20
  200. scipy/interpolate/tests/test_interpolate.py +48 -4
  201. scipy/interpolate/tests/test_rgi.py +2 -1
  202. scipy/io/_fast_matrix_market/__init__.py +2 -0
  203. scipy/io/_fast_matrix_market/_fmm_core.cp311-win_amd64.dll.a +0 -0
  204. scipy/io/_fast_matrix_market/_fmm_core.cp311-win_amd64.pyd +0 -0
  205. scipy/io/_harwell_boeing/_fortran_format_parser.py +19 -16
  206. scipy/io/_harwell_boeing/hb.py +7 -11
  207. scipy/io/_idl.py +5 -7
  208. scipy/io/_netcdf.py +15 -5
  209. scipy/io/_test_fortran.cp311-win_amd64.dll.a +0 -0
  210. scipy/io/_test_fortran.cp311-win_amd64.pyd +0 -0
  211. scipy/io/arff/tests/test_arffread.py +3 -3
  212. scipy/io/matlab/__init__.py +5 -3
  213. scipy/io/matlab/_mio.py +4 -1
  214. scipy/io/matlab/_mio5.py +19 -13
  215. scipy/io/matlab/_mio5_utils.cp311-win_amd64.dll.a +0 -0
  216. scipy/io/matlab/_mio5_utils.cp311-win_amd64.pyd +0 -0
  217. scipy/io/matlab/_mio_utils.cp311-win_amd64.dll.a +0 -0
  218. scipy/io/matlab/_mio_utils.cp311-win_amd64.pyd +0 -0
  219. scipy/io/matlab/_miobase.py +4 -1
  220. scipy/io/matlab/_streams.cp311-win_amd64.dll.a +0 -0
  221. scipy/io/matlab/_streams.cp311-win_amd64.pyd +0 -0
  222. scipy/io/matlab/tests/test_mio.py +46 -18
  223. scipy/io/matlab/tests/test_mio_funcs.py +1 -1
  224. scipy/io/tests/test_mmio.py +7 -1
  225. scipy/io/tests/test_wavfile.py +41 -0
  226. scipy/io/wavfile.py +57 -10
  227. scipy/linalg/_basic.py +113 -86
  228. scipy/linalg/_cythonized_array_utils.cp311-win_amd64.dll.a +0 -0
  229. scipy/linalg/_cythonized_array_utils.cp311-win_amd64.pyd +0 -0
  230. scipy/linalg/_decomp.py +22 -9
  231. scipy/linalg/_decomp_cholesky.py +28 -13
  232. scipy/linalg/_decomp_cossin.py +45 -30
  233. scipy/linalg/_decomp_interpolative.cp311-win_amd64.dll.a +0 -0
  234. scipy/linalg/_decomp_interpolative.cp311-win_amd64.pyd +0 -0
  235. scipy/linalg/_decomp_ldl.py +4 -1
  236. scipy/linalg/_decomp_lu.py +18 -6
  237. scipy/linalg/_decomp_lu_cython.cp311-win_amd64.dll.a +0 -0
  238. scipy/linalg/_decomp_lu_cython.cp311-win_amd64.pyd +0 -0
  239. scipy/linalg/_decomp_polar.py +2 -0
  240. scipy/linalg/_decomp_qr.py +6 -2
  241. scipy/linalg/_decomp_qz.py +3 -0
  242. scipy/linalg/_decomp_schur.py +3 -1
  243. scipy/linalg/_decomp_svd.py +13 -2
  244. scipy/linalg/_decomp_update.cp311-win_amd64.dll.a +0 -0
  245. scipy/linalg/_decomp_update.cp311-win_amd64.pyd +0 -0
  246. scipy/linalg/_expm_frechet.py +4 -0
  247. scipy/linalg/_fblas.cp311-win_amd64.dll.a +0 -0
  248. scipy/linalg/_fblas.cp311-win_amd64.pyd +0 -0
  249. scipy/linalg/_flapack.cp311-win_amd64.dll.a +0 -0
  250. scipy/linalg/_flapack.cp311-win_amd64.pyd +0 -0
  251. scipy/linalg/_linalg_pythran.cp311-win_amd64.dll.a +0 -0
  252. scipy/linalg/_linalg_pythran.cp311-win_amd64.pyd +0 -0
  253. scipy/linalg/_matfuncs.py +187 -4
  254. scipy/linalg/_matfuncs_expm.cp311-win_amd64.dll.a +0 -0
  255. scipy/linalg/_matfuncs_expm.cp311-win_amd64.pyd +0 -0
  256. scipy/linalg/_matfuncs_schur_sqrtm.cp311-win_amd64.dll.a +0 -0
  257. scipy/linalg/_matfuncs_schur_sqrtm.cp311-win_amd64.pyd +0 -0
  258. scipy/linalg/_matfuncs_sqrtm.py +1 -99
  259. scipy/linalg/_matfuncs_sqrtm_triu.cp311-win_amd64.dll.a +0 -0
  260. scipy/linalg/_matfuncs_sqrtm_triu.cp311-win_amd64.pyd +0 -0
  261. scipy/linalg/_procrustes.py +2 -0
  262. scipy/linalg/_sketches.py +17 -6
  263. scipy/linalg/_solve_toeplitz.cp311-win_amd64.dll.a +0 -0
  264. scipy/linalg/_solve_toeplitz.cp311-win_amd64.pyd +0 -0
  265. scipy/linalg/_solvers.py +7 -2
  266. scipy/linalg/_special_matrices.py +26 -36
  267. scipy/linalg/cython_blas.cp311-win_amd64.dll.a +0 -0
  268. scipy/linalg/cython_blas.cp311-win_amd64.pyd +0 -0
  269. scipy/linalg/cython_lapack.cp311-win_amd64.dll.a +0 -0
  270. scipy/linalg/cython_lapack.cp311-win_amd64.pyd +0 -0
  271. scipy/linalg/lapack.py +22 -2
  272. scipy/linalg/tests/_cython_examples/meson.build +7 -0
  273. scipy/linalg/tests/test_basic.py +31 -16
  274. scipy/linalg/tests/test_batch.py +588 -0
  275. scipy/linalg/tests/test_cythonized_array_utils.py +0 -2
  276. scipy/linalg/tests/test_decomp.py +40 -3
  277. scipy/linalg/tests/test_decomp_cossin.py +14 -0
  278. scipy/linalg/tests/test_decomp_ldl.py +1 -1
  279. scipy/linalg/tests/test_lapack.py +115 -7
  280. scipy/linalg/tests/test_matfuncs.py +157 -102
  281. scipy/linalg/tests/test_procrustes.py +0 -7
  282. scipy/linalg/tests/test_solve_toeplitz.py +1 -1
  283. scipy/linalg/tests/test_special_matrices.py +1 -5
  284. scipy/ndimage/__init__.py +1 -0
  285. scipy/ndimage/_ctest.cp311-win_amd64.dll.a +0 -0
  286. scipy/ndimage/_ctest.cp311-win_amd64.pyd +0 -0
  287. scipy/ndimage/_cytest.cp311-win_amd64.dll.a +0 -0
  288. scipy/ndimage/_cytest.cp311-win_amd64.pyd +0 -0
  289. scipy/ndimage/_delegators.py +8 -2
  290. scipy/ndimage/_filters.py +453 -5
  291. scipy/ndimage/_interpolation.py +36 -6
  292. scipy/ndimage/_measurements.py +4 -2
  293. scipy/ndimage/_morphology.py +5 -0
  294. scipy/ndimage/_nd_image.cp311-win_amd64.dll.a +0 -0
  295. scipy/ndimage/_nd_image.cp311-win_amd64.pyd +0 -0
  296. scipy/ndimage/_ni_docstrings.py +5 -1
  297. scipy/ndimage/_ni_label.cp311-win_amd64.dll.a +0 -0
  298. scipy/ndimage/_ni_label.cp311-win_amd64.pyd +0 -0
  299. scipy/ndimage/_ni_support.py +1 -5
  300. scipy/ndimage/_rank_filter_1d.cp311-win_amd64.dll.a +0 -0
  301. scipy/ndimage/_rank_filter_1d.cp311-win_amd64.pyd +0 -0
  302. scipy/ndimage/_support_alternative_backends.py +18 -6
  303. scipy/ndimage/tests/test_filters.py +370 -259
  304. scipy/ndimage/tests/test_fourier.py +7 -9
  305. scipy/ndimage/tests/test_interpolation.py +68 -61
  306. scipy/ndimage/tests/test_measurements.py +18 -35
  307. scipy/ndimage/tests/test_morphology.py +143 -131
  308. scipy/ndimage/tests/test_splines.py +1 -3
  309. scipy/odr/__odrpack.cp311-win_amd64.dll.a +0 -0
  310. scipy/odr/__odrpack.cp311-win_amd64.pyd +0 -0
  311. scipy/optimize/_basinhopping.py +13 -7
  312. scipy/optimize/_bglu_dense.cp311-win_amd64.dll.a +0 -0
  313. scipy/optimize/_bglu_dense.cp311-win_amd64.pyd +0 -0
  314. scipy/optimize/_bracket.py +17 -24
  315. scipy/optimize/_chandrupatla.py +9 -10
  316. scipy/optimize/_cobyla_py.py +104 -123
  317. scipy/optimize/_constraints.py +14 -10
  318. scipy/optimize/_differentiable_functions.py +371 -230
  319. scipy/optimize/_differentialevolution.py +4 -3
  320. scipy/optimize/_direct.cp311-win_amd64.dll.a +0 -0
  321. scipy/optimize/_direct.cp311-win_amd64.pyd +0 -0
  322. scipy/optimize/_dual_annealing.py +1 -1
  323. scipy/optimize/_elementwise.py +1 -4
  324. scipy/optimize/_group_columns.cp311-win_amd64.dll.a +0 -0
  325. scipy/optimize/_group_columns.cp311-win_amd64.pyd +0 -0
  326. scipy/optimize/_highspy/_core.cp311-win_amd64.dll.a +0 -0
  327. scipy/optimize/_highspy/_core.cp311-win_amd64.pyd +0 -0
  328. scipy/optimize/_highspy/_highs_options.cp311-win_amd64.dll.a +0 -0
  329. scipy/optimize/_highspy/_highs_options.cp311-win_amd64.pyd +0 -0
  330. scipy/optimize/_lbfgsb.cp311-win_amd64.dll.a +0 -0
  331. scipy/optimize/_lbfgsb.cp311-win_amd64.pyd +0 -0
  332. scipy/optimize/_lbfgsb_py.py +57 -16
  333. scipy/optimize/_linprog_doc.py +2 -2
  334. scipy/optimize/_linprog_highs.py +2 -2
  335. scipy/optimize/_linprog_ip.py +25 -10
  336. scipy/optimize/_linprog_util.py +14 -16
  337. scipy/optimize/_lsap.cp311-win_amd64.dll.a +0 -0
  338. scipy/optimize/_lsap.cp311-win_amd64.pyd +0 -0
  339. scipy/optimize/_lsq/common.py +3 -3
  340. scipy/optimize/_lsq/dogbox.py +16 -2
  341. scipy/optimize/_lsq/givens_elimination.cp311-win_amd64.dll.a +0 -0
  342. scipy/optimize/_lsq/givens_elimination.cp311-win_amd64.pyd +0 -0
  343. scipy/optimize/_lsq/least_squares.py +198 -126
  344. scipy/optimize/_lsq/lsq_linear.py +6 -6
  345. scipy/optimize/_lsq/trf.py +35 -8
  346. scipy/optimize/_milp.py +3 -1
  347. scipy/optimize/_minimize.py +105 -36
  348. scipy/optimize/_minpack.cp311-win_amd64.dll.a +0 -0
  349. scipy/optimize/_minpack.cp311-win_amd64.pyd +0 -0
  350. scipy/optimize/_minpack_py.py +21 -14
  351. scipy/optimize/_moduleTNC.cp311-win_amd64.dll.a +0 -0
  352. scipy/optimize/_moduleTNC.cp311-win_amd64.pyd +0 -0
  353. scipy/optimize/_nnls.py +20 -21
  354. scipy/optimize/_nonlin.py +34 -3
  355. scipy/optimize/_numdiff.py +288 -110
  356. scipy/optimize/_optimize.py +86 -48
  357. scipy/optimize/_pava_pybind.cp311-win_amd64.dll.a +0 -0
  358. scipy/optimize/_pava_pybind.cp311-win_amd64.pyd +0 -0
  359. scipy/optimize/_remove_redundancy.py +5 -5
  360. scipy/optimize/_root_scalar.py +1 -1
  361. scipy/optimize/_shgo.py +6 -0
  362. scipy/optimize/_shgo_lib/_complex.py +1 -1
  363. scipy/optimize/_slsqp_py.py +216 -124
  364. scipy/optimize/_slsqplib.cp311-win_amd64.dll.a +0 -0
  365. scipy/optimize/_slsqplib.cp311-win_amd64.pyd +0 -0
  366. scipy/optimize/_spectral.py +1 -1
  367. scipy/optimize/_tnc.py +8 -1
  368. scipy/optimize/_trlib/_trlib.cp311-win_amd64.dll.a +0 -0
  369. scipy/optimize/_trlib/_trlib.cp311-win_amd64.pyd +0 -0
  370. scipy/optimize/_trustregion.py +20 -6
  371. scipy/optimize/_trustregion_constr/canonical_constraint.py +7 -7
  372. scipy/optimize/_trustregion_constr/equality_constrained_sqp.py +1 -1
  373. scipy/optimize/_trustregion_constr/minimize_trustregion_constr.py +11 -3
  374. scipy/optimize/_trustregion_constr/projections.py +12 -8
  375. scipy/optimize/_trustregion_constr/qp_subproblem.py +9 -9
  376. scipy/optimize/_trustregion_constr/tests/test_projections.py +7 -7
  377. scipy/optimize/_trustregion_constr/tests/test_qp_subproblem.py +77 -77
  378. scipy/optimize/_trustregion_constr/tr_interior_point.py +5 -5
  379. scipy/optimize/_trustregion_exact.py +0 -1
  380. scipy/optimize/_zeros.cp311-win_amd64.dll.a +0 -0
  381. scipy/optimize/_zeros.cp311-win_amd64.pyd +0 -0
  382. scipy/optimize/_zeros_py.py +97 -17
  383. scipy/optimize/cython_optimize/_zeros.cp311-win_amd64.dll.a +0 -0
  384. scipy/optimize/cython_optimize/_zeros.cp311-win_amd64.pyd +0 -0
  385. scipy/optimize/slsqp.py +0 -1
  386. scipy/optimize/tests/test__basinhopping.py +1 -1
  387. scipy/optimize/tests/test__differential_evolution.py +4 -4
  388. scipy/optimize/tests/test__linprog_clean_inputs.py +5 -3
  389. scipy/optimize/tests/test__numdiff.py +66 -22
  390. scipy/optimize/tests/test__remove_redundancy.py +2 -2
  391. scipy/optimize/tests/test__shgo.py +9 -1
  392. scipy/optimize/tests/test_bracket.py +36 -46
  393. scipy/optimize/tests/test_chandrupatla.py +133 -135
  394. scipy/optimize/tests/test_cobyla.py +74 -45
  395. scipy/optimize/tests/test_constraints.py +1 -1
  396. scipy/optimize/tests/test_differentiable_functions.py +226 -6
  397. scipy/optimize/tests/test_lbfgsb_hessinv.py +22 -0
  398. scipy/optimize/tests/test_least_squares.py +125 -13
  399. scipy/optimize/tests/test_linear_assignment.py +3 -3
  400. scipy/optimize/tests/test_linprog.py +3 -3
  401. scipy/optimize/tests/test_lsq_linear.py +6 -6
  402. scipy/optimize/tests/test_minimize_constrained.py +2 -2
  403. scipy/optimize/tests/test_minpack.py +4 -4
  404. scipy/optimize/tests/test_nnls.py +43 -3
  405. scipy/optimize/tests/test_nonlin.py +36 -0
  406. scipy/optimize/tests/test_optimize.py +95 -17
  407. scipy/optimize/tests/test_slsqp.py +36 -4
  408. scipy/optimize/tests/test_zeros.py +34 -1
  409. scipy/signal/__init__.py +12 -23
  410. scipy/signal/_delegators.py +568 -0
  411. scipy/signal/_filter_design.py +459 -241
  412. scipy/signal/_fir_filter_design.py +262 -90
  413. scipy/signal/_lti_conversion.py +3 -2
  414. scipy/signal/_ltisys.py +118 -91
  415. scipy/signal/_max_len_seq_inner.cp311-win_amd64.dll.a +0 -0
  416. scipy/signal/_max_len_seq_inner.cp311-win_amd64.pyd +0 -0
  417. scipy/signal/_peak_finding_utils.cp311-win_amd64.dll.a +0 -0
  418. scipy/signal/_peak_finding_utils.cp311-win_amd64.pyd +0 -0
  419. scipy/signal/_polyutils.py +172 -0
  420. scipy/signal/_short_time_fft.py +519 -70
  421. scipy/signal/_signal_api.py +30 -0
  422. scipy/signal/_signaltools.py +719 -399
  423. scipy/signal/_sigtools.cp311-win_amd64.dll.a +0 -0
  424. scipy/signal/_sigtools.cp311-win_amd64.pyd +0 -0
  425. scipy/signal/_sosfilt.cp311-win_amd64.dll.a +0 -0
  426. scipy/signal/_sosfilt.cp311-win_amd64.pyd +0 -0
  427. scipy/signal/_spectral_py.py +230 -50
  428. scipy/signal/_spline.cp311-win_amd64.dll.a +0 -0
  429. scipy/signal/_spline.cp311-win_amd64.pyd +0 -0
  430. scipy/signal/_spline_filters.py +108 -68
  431. scipy/signal/_support_alternative_backends.py +73 -0
  432. scipy/signal/_upfirdn.py +4 -1
  433. scipy/signal/_upfirdn_apply.cp311-win_amd64.dll.a +0 -0
  434. scipy/signal/_upfirdn_apply.cp311-win_amd64.pyd +0 -0
  435. scipy/signal/_waveforms.py +2 -11
  436. scipy/signal/_wavelets.py +1 -1
  437. scipy/signal/fir_filter_design.py +1 -0
  438. scipy/signal/spline.py +4 -11
  439. scipy/signal/tests/_scipy_spectral_test_shim.py +2 -171
  440. scipy/signal/tests/test_bsplines.py +114 -79
  441. scipy/signal/tests/test_cont2discrete.py +9 -2
  442. scipy/signal/tests/test_filter_design.py +721 -481
  443. scipy/signal/tests/test_fir_filter_design.py +332 -140
  444. scipy/signal/tests/test_savitzky_golay.py +4 -3
  445. scipy/signal/tests/test_short_time_fft.py +221 -3
  446. scipy/signal/tests/test_signaltools.py +2144 -1348
  447. scipy/signal/tests/test_spectral.py +50 -6
  448. scipy/signal/tests/test_splines.py +161 -96
  449. scipy/signal/tests/test_upfirdn.py +84 -50
  450. scipy/signal/tests/test_waveforms.py +20 -0
  451. scipy/signal/tests/test_windows.py +607 -466
  452. scipy/signal/windows/_windows.py +287 -148
  453. scipy/sparse/__init__.py +23 -4
  454. scipy/sparse/_base.py +270 -108
  455. scipy/sparse/_bsr.py +7 -4
  456. scipy/sparse/_compressed.py +59 -231
  457. scipy/sparse/_construct.py +90 -38
  458. scipy/sparse/_coo.py +115 -181
  459. scipy/sparse/_csc.py +4 -4
  460. scipy/sparse/_csparsetools.cp311-win_amd64.dll.a +0 -0
  461. scipy/sparse/_csparsetools.cp311-win_amd64.pyd +0 -0
  462. scipy/sparse/_csr.py +2 -2
  463. scipy/sparse/_data.py +48 -48
  464. scipy/sparse/_dia.py +105 -18
  465. scipy/sparse/_dok.py +0 -23
  466. scipy/sparse/_index.py +4 -4
  467. scipy/sparse/_matrix.py +23 -0
  468. scipy/sparse/_sparsetools.cp311-win_amd64.dll.a +0 -0
  469. scipy/sparse/_sparsetools.cp311-win_amd64.pyd +0 -0
  470. scipy/sparse/_sputils.py +37 -22
  471. scipy/sparse/base.py +0 -9
  472. scipy/sparse/bsr.py +0 -14
  473. scipy/sparse/compressed.py +0 -23
  474. scipy/sparse/construct.py +0 -6
  475. scipy/sparse/coo.py +0 -14
  476. scipy/sparse/csc.py +0 -3
  477. scipy/sparse/csgraph/_flow.cp311-win_amd64.dll.a +0 -0
  478. scipy/sparse/csgraph/_flow.cp311-win_amd64.pyd +0 -0
  479. scipy/sparse/csgraph/_matching.cp311-win_amd64.dll.a +0 -0
  480. scipy/sparse/csgraph/_matching.cp311-win_amd64.pyd +0 -0
  481. scipy/sparse/csgraph/_min_spanning_tree.cp311-win_amd64.dll.a +0 -0
  482. scipy/sparse/csgraph/_min_spanning_tree.cp311-win_amd64.pyd +0 -0
  483. scipy/sparse/csgraph/_reordering.cp311-win_amd64.dll.a +0 -0
  484. scipy/sparse/csgraph/_reordering.cp311-win_amd64.pyd +0 -0
  485. scipy/sparse/csgraph/_shortest_path.cp311-win_amd64.dll.a +0 -0
  486. scipy/sparse/csgraph/_shortest_path.cp311-win_amd64.pyd +0 -0
  487. scipy/sparse/csgraph/_tools.cp311-win_amd64.dll.a +0 -0
  488. scipy/sparse/csgraph/_tools.cp311-win_amd64.pyd +0 -0
  489. scipy/sparse/csgraph/_traversal.cp311-win_amd64.dll.a +0 -0
  490. scipy/sparse/csgraph/_traversal.cp311-win_amd64.pyd +0 -0
  491. scipy/sparse/csgraph/tests/test_matching.py +14 -2
  492. scipy/sparse/csgraph/tests/test_pydata_sparse.py +4 -1
  493. scipy/sparse/csgraph/tests/test_shortest_path.py +83 -27
  494. scipy/sparse/csr.py +0 -5
  495. scipy/sparse/data.py +1 -6
  496. scipy/sparse/dia.py +0 -7
  497. scipy/sparse/dok.py +0 -10
  498. scipy/sparse/linalg/_dsolve/_superlu.cp311-win_amd64.dll.a +0 -0
  499. scipy/sparse/linalg/_dsolve/_superlu.cp311-win_amd64.pyd +0 -0
  500. scipy/sparse/linalg/_dsolve/linsolve.py +9 -0
  501. scipy/sparse/linalg/_dsolve/tests/test_linsolve.py +35 -28
  502. scipy/sparse/linalg/_eigen/arpack/_arpack.cp311-win_amd64.dll.a +0 -0
  503. scipy/sparse/linalg/_eigen/arpack/_arpack.cp311-win_amd64.pyd +0 -0
  504. scipy/sparse/linalg/_eigen/arpack/arpack.py +23 -17
  505. scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py +6 -6
  506. scipy/sparse/linalg/_interface.py +17 -18
  507. scipy/sparse/linalg/_isolve/_gcrotmk.py +4 -4
  508. scipy/sparse/linalg/_isolve/iterative.py +51 -45
  509. scipy/sparse/linalg/_isolve/lgmres.py +6 -6
  510. scipy/sparse/linalg/_isolve/minres.py +5 -5
  511. scipy/sparse/linalg/_isolve/tfqmr.py +7 -7
  512. scipy/sparse/linalg/_isolve/utils.py +2 -8
  513. scipy/sparse/linalg/_matfuncs.py +1 -1
  514. scipy/sparse/linalg/_norm.py +1 -1
  515. scipy/sparse/linalg/_propack/_cpropack.cp311-win_amd64.dll.a +0 -0
  516. scipy/sparse/linalg/_propack/_cpropack.cp311-win_amd64.pyd +0 -0
  517. scipy/sparse/linalg/_propack/_dpropack.cp311-win_amd64.dll.a +0 -0
  518. scipy/sparse/linalg/_propack/_dpropack.cp311-win_amd64.pyd +0 -0
  519. scipy/sparse/linalg/_propack/_spropack.cp311-win_amd64.dll.a +0 -0
  520. scipy/sparse/linalg/_propack/_spropack.cp311-win_amd64.pyd +0 -0
  521. scipy/sparse/linalg/_propack/_zpropack.cp311-win_amd64.dll.a +0 -0
  522. scipy/sparse/linalg/_propack/_zpropack.cp311-win_amd64.pyd +0 -0
  523. scipy/sparse/linalg/_special_sparse_arrays.py +39 -38
  524. scipy/sparse/linalg/tests/test_pydata_sparse.py +14 -0
  525. scipy/sparse/tests/test_arithmetic1d.py +5 -2
  526. scipy/sparse/tests/test_base.py +214 -42
  527. scipy/sparse/tests/test_common1d.py +7 -7
  528. scipy/sparse/tests/test_construct.py +1 -1
  529. scipy/sparse/tests/test_coo.py +272 -4
  530. scipy/sparse/tests/test_sparsetools.py +5 -0
  531. scipy/sparse/tests/test_sputils.py +36 -7
  532. scipy/spatial/_ckdtree.cp311-win_amd64.dll.a +0 -0
  533. scipy/spatial/_ckdtree.cp311-win_amd64.pyd +0 -0
  534. scipy/spatial/_distance_pybind.cp311-win_amd64.dll.a +0 -0
  535. scipy/spatial/_distance_pybind.cp311-win_amd64.pyd +0 -0
  536. scipy/spatial/_distance_wrap.cp311-win_amd64.dll.a +0 -0
  537. scipy/spatial/_distance_wrap.cp311-win_amd64.pyd +0 -0
  538. scipy/spatial/_hausdorff.cp311-win_amd64.dll.a +0 -0
  539. scipy/spatial/_hausdorff.cp311-win_amd64.pyd +0 -0
  540. scipy/spatial/_qhull.cp311-win_amd64.dll.a +0 -0
  541. scipy/spatial/_qhull.cp311-win_amd64.pyd +0 -0
  542. scipy/spatial/_voronoi.cp311-win_amd64.dll.a +0 -0
  543. scipy/spatial/_voronoi.cp311-win_amd64.pyd +0 -0
  544. scipy/spatial/distance.py +49 -42
  545. scipy/spatial/tests/test_distance.py +15 -1
  546. scipy/spatial/tests/test_kdtree.py +1 -0
  547. scipy/spatial/tests/test_qhull.py +7 -2
  548. scipy/spatial/transform/__init__.py +5 -3
  549. scipy/spatial/transform/_rigid_transform.cp311-win_amd64.dll.a +0 -0
  550. scipy/spatial/transform/_rigid_transform.cp311-win_amd64.pyd +0 -0
  551. scipy/spatial/transform/_rotation.cp311-win_amd64.dll.a +0 -0
  552. scipy/spatial/transform/_rotation.cp311-win_amd64.pyd +0 -0
  553. scipy/spatial/transform/tests/test_rigid_transform.py +1221 -0
  554. scipy/spatial/transform/tests/test_rotation.py +1213 -832
  555. scipy/spatial/transform/tests/test_rotation_groups.py +3 -3
  556. scipy/spatial/transform/tests/test_rotation_spline.py +29 -8
  557. scipy/special/__init__.py +1 -47
  558. scipy/special/_add_newdocs.py +34 -772
  559. scipy/special/_basic.py +22 -25
  560. scipy/special/_comb.cp311-win_amd64.dll.a +0 -0
  561. scipy/special/_comb.cp311-win_amd64.pyd +0 -0
  562. scipy/special/_ellip_harm_2.cp311-win_amd64.dll.a +0 -0
  563. scipy/special/_ellip_harm_2.cp311-win_amd64.pyd +0 -0
  564. scipy/special/_gufuncs.cp311-win_amd64.dll.a +0 -0
  565. scipy/special/_gufuncs.cp311-win_amd64.pyd +0 -0
  566. scipy/special/_logsumexp.py +67 -58
  567. scipy/special/_orthogonal.pyi +1 -1
  568. scipy/special/_specfun.cp311-win_amd64.dll.a +0 -0
  569. scipy/special/_specfun.cp311-win_amd64.pyd +0 -0
  570. scipy/special/_special_ufuncs.cp311-win_amd64.dll.a +0 -0
  571. scipy/special/_special_ufuncs.cp311-win_amd64.pyd +0 -0
  572. scipy/special/_spherical_bessel.py +4 -4
  573. scipy/special/_support_alternative_backends.py +212 -119
  574. scipy/special/_test_internal.cp311-win_amd64.dll.a +0 -0
  575. scipy/special/_test_internal.cp311-win_amd64.pyd +0 -0
  576. scipy/special/_testutils.py +4 -4
  577. scipy/special/_ufuncs.cp311-win_amd64.dll.a +0 -0
  578. scipy/special/_ufuncs.cp311-win_amd64.pyd +0 -0
  579. scipy/special/_ufuncs.pyi +1 -0
  580. scipy/special/_ufuncs.pyx +215 -1400
  581. scipy/special/_ufuncs_cxx.cp311-win_amd64.dll.a +0 -0
  582. scipy/special/_ufuncs_cxx.cp311-win_amd64.pyd +0 -0
  583. scipy/special/_ufuncs_cxx.pxd +2 -15
  584. scipy/special/_ufuncs_cxx.pyx +5 -44
  585. scipy/special/_ufuncs_cxx_defs.h +2 -16
  586. scipy/special/_ufuncs_defs.h +0 -8
  587. scipy/special/cython_special.cp311-win_amd64.dll.a +0 -0
  588. scipy/special/cython_special.cp311-win_amd64.pyd +0 -0
  589. scipy/special/cython_special.pxd +1 -1
  590. scipy/special/tests/_cython_examples/meson.build +10 -1
  591. scipy/special/tests/test_basic.py +153 -20
  592. scipy/special/tests/test_boost_ufuncs.py +3 -0
  593. scipy/special/tests/test_cdflib.py +35 -11
  594. scipy/special/tests/test_gammainc.py +16 -0
  595. scipy/special/tests/test_hyp2f1.py +2 -2
  596. scipy/special/tests/test_log1mexp.py +85 -0
  597. scipy/special/tests/test_logsumexp.py +206 -64
  598. scipy/special/tests/test_mpmath.py +1 -0
  599. scipy/special/tests/test_nan_inputs.py +1 -1
  600. scipy/special/tests/test_orthogonal.py +17 -18
  601. scipy/special/tests/test_sf_error.py +3 -2
  602. scipy/special/tests/test_sph_harm.py +6 -7
  603. scipy/special/tests/test_support_alternative_backends.py +211 -76
  604. scipy/stats/__init__.py +4 -1
  605. scipy/stats/_ansari_swilk_statistics.cp311-win_amd64.dll.a +0 -0
  606. scipy/stats/_ansari_swilk_statistics.cp311-win_amd64.pyd +0 -0
  607. scipy/stats/_axis_nan_policy.py +5 -12
  608. scipy/stats/_biasedurn.cp311-win_amd64.dll.a +0 -0
  609. scipy/stats/_biasedurn.cp311-win_amd64.pyd +0 -0
  610. scipy/stats/_continued_fraction.py +387 -0
  611. scipy/stats/_continuous_distns.py +277 -310
  612. scipy/stats/_correlation.py +1 -1
  613. scipy/stats/_covariance.py +6 -3
  614. scipy/stats/_discrete_distns.py +39 -32
  615. scipy/stats/_distn_infrastructure.py +39 -12
  616. scipy/stats/_distribution_infrastructure.py +900 -238
  617. scipy/stats/_entropy.py +9 -10
  618. scipy/{_lib → stats}/_finite_differences.py +1 -1
  619. scipy/stats/_hypotests.py +83 -50
  620. scipy/stats/_kde.py +53 -49
  621. scipy/stats/_ksstats.py +1 -1
  622. scipy/stats/_levy_stable/__init__.py +7 -15
  623. scipy/stats/_levy_stable/levyst.cp311-win_amd64.dll.a +0 -0
  624. scipy/stats/_levy_stable/levyst.cp311-win_amd64.pyd +0 -0
  625. scipy/stats/_morestats.py +118 -73
  626. scipy/stats/_mstats_basic.py +13 -17
  627. scipy/stats/_mstats_extras.py +8 -8
  628. scipy/stats/_multivariate.py +89 -113
  629. scipy/stats/_new_distributions.py +97 -20
  630. scipy/stats/_page_trend_test.py +12 -5
  631. scipy/stats/_probability_distribution.py +265 -43
  632. scipy/stats/_qmc.py +14 -9
  633. scipy/stats/_qmc_cy.cp311-win_amd64.dll.a +0 -0
  634. scipy/stats/_qmc_cy.cp311-win_amd64.pyd +0 -0
  635. scipy/stats/_qmvnt.py +16 -95
  636. scipy/stats/_qmvnt_cy.cp311-win_amd64.dll.a +0 -0
  637. scipy/stats/_qmvnt_cy.cp311-win_amd64.pyd +0 -0
  638. scipy/stats/_quantile.py +335 -0
  639. scipy/stats/_rcont/rcont.cp311-win_amd64.dll.a +0 -0
  640. scipy/stats/_rcont/rcont.cp311-win_amd64.pyd +0 -0
  641. scipy/stats/_resampling.py +4 -29
  642. scipy/stats/_sampling.py +1 -1
  643. scipy/stats/_sobol.cp311-win_amd64.dll.a +0 -0
  644. scipy/stats/_sobol.cp311-win_amd64.pyd +0 -0
  645. scipy/stats/_stats.cp311-win_amd64.dll.a +0 -0
  646. scipy/stats/_stats.cp311-win_amd64.pyd +0 -0
  647. scipy/stats/_stats_mstats_common.py +21 -2
  648. scipy/stats/_stats_py.py +550 -476
  649. scipy/stats/_stats_pythran.cp311-win_amd64.dll.a +0 -0
  650. scipy/stats/_stats_pythran.cp311-win_amd64.pyd +0 -0
  651. scipy/stats/_unuran/unuran_wrapper.cp311-win_amd64.dll.a +0 -0
  652. scipy/stats/_unuran/unuran_wrapper.cp311-win_amd64.pyd +0 -0
  653. scipy/stats/_unuran/unuran_wrapper.pyi +2 -1
  654. scipy/stats/_variation.py +6 -8
  655. scipy/stats/_wilcoxon.py +13 -7
  656. scipy/stats/tests/common_tests.py +6 -4
  657. scipy/stats/tests/test_axis_nan_policy.py +62 -24
  658. scipy/stats/tests/test_continued_fraction.py +173 -0
  659. scipy/stats/tests/test_continuous.py +379 -60
  660. scipy/stats/tests/test_continuous_basic.py +18 -12
  661. scipy/stats/tests/test_discrete_basic.py +14 -8
  662. scipy/stats/tests/test_discrete_distns.py +16 -16
  663. scipy/stats/tests/test_distributions.py +95 -75
  664. scipy/stats/tests/test_entropy.py +40 -48
  665. scipy/stats/tests/test_fit.py +4 -3
  666. scipy/stats/tests/test_hypotests.py +153 -24
  667. scipy/stats/tests/test_kdeoth.py +109 -41
  668. scipy/stats/tests/test_marray.py +289 -0
  669. scipy/stats/tests/test_morestats.py +79 -47
  670. scipy/stats/tests/test_mstats_basic.py +3 -3
  671. scipy/stats/tests/test_multivariate.py +434 -83
  672. scipy/stats/tests/test_qmc.py +13 -10
  673. scipy/stats/tests/test_quantile.py +199 -0
  674. scipy/stats/tests/test_rank.py +119 -112
  675. scipy/stats/tests/test_resampling.py +47 -56
  676. scipy/stats/tests/test_sampling.py +9 -4
  677. scipy/stats/tests/test_stats.py +799 -939
  678. scipy/stats/tests/test_variation.py +8 -6
  679. scipy/version.py +2 -2
  680. scipy-1.16.0rc2.dist-info/DELVEWHEEL +2 -0
  681. {scipy-1.15.3.dist-info → scipy-1.16.0rc2.dist-info}/LICENSE.txt +4 -4
  682. {scipy-1.15.3.dist-info → scipy-1.16.0rc2.dist-info}/METADATA +11 -11
  683. {scipy-1.15.3.dist-info → scipy-1.16.0rc2.dist-info}/RECORD +685 -693
  684. scipy/_lib/array_api_extra/_funcs.py +0 -484
  685. scipy/_lib/array_api_extra/_typing.py +0 -8
  686. scipy/interpolate/_bspl.cp311-win_amd64.dll.a +0 -0
  687. scipy/interpolate/_bspl.cp311-win_amd64.pyd +0 -0
  688. scipy/optimize/_cobyla.cp311-win_amd64.dll.a +0 -0
  689. scipy/optimize/_cobyla.cp311-win_amd64.pyd +0 -0
  690. scipy/optimize/_cython_nnls.cp311-win_amd64.dll.a +0 -0
  691. scipy/optimize/_cython_nnls.cp311-win_amd64.pyd +0 -0
  692. scipy/optimize/_slsqp.cp311-win_amd64.dll.a +0 -0
  693. scipy/optimize/_slsqp.cp311-win_amd64.pyd +0 -0
  694. scipy/spatial/qhull_src/COPYING.txt +0 -38
  695. scipy/special/libsf_error_state.dll +0 -0
  696. scipy/special/libsf_error_state.dll.a +0 -0
  697. scipy/special/tests/test_log_softmax.py +0 -109
  698. scipy/special/tests/test_xsf_cuda.py +0 -114
  699. scipy/special/xsf/binom.h +0 -89
  700. scipy/special/xsf/cdflib.h +0 -100
  701. scipy/special/xsf/cephes/airy.h +0 -307
  702. scipy/special/xsf/cephes/besselpoly.h +0 -51
  703. scipy/special/xsf/cephes/beta.h +0 -257
  704. scipy/special/xsf/cephes/cbrt.h +0 -131
  705. scipy/special/xsf/cephes/chbevl.h +0 -85
  706. scipy/special/xsf/cephes/chdtr.h +0 -193
  707. scipy/special/xsf/cephes/const.h +0 -87
  708. scipy/special/xsf/cephes/ellie.h +0 -293
  709. scipy/special/xsf/cephes/ellik.h +0 -251
  710. scipy/special/xsf/cephes/ellpe.h +0 -107
  711. scipy/special/xsf/cephes/ellpk.h +0 -117
  712. scipy/special/xsf/cephes/expn.h +0 -260
  713. scipy/special/xsf/cephes/gamma.h +0 -398
  714. scipy/special/xsf/cephes/hyp2f1.h +0 -596
  715. scipy/special/xsf/cephes/hyperg.h +0 -361
  716. scipy/special/xsf/cephes/i0.h +0 -149
  717. scipy/special/xsf/cephes/i1.h +0 -158
  718. scipy/special/xsf/cephes/igam.h +0 -421
  719. scipy/special/xsf/cephes/igam_asymp_coeff.h +0 -195
  720. scipy/special/xsf/cephes/igami.h +0 -313
  721. scipy/special/xsf/cephes/j0.h +0 -225
  722. scipy/special/xsf/cephes/j1.h +0 -198
  723. scipy/special/xsf/cephes/jv.h +0 -715
  724. scipy/special/xsf/cephes/k0.h +0 -164
  725. scipy/special/xsf/cephes/k1.h +0 -163
  726. scipy/special/xsf/cephes/kn.h +0 -243
  727. scipy/special/xsf/cephes/lanczos.h +0 -112
  728. scipy/special/xsf/cephes/ndtr.h +0 -275
  729. scipy/special/xsf/cephes/poch.h +0 -85
  730. scipy/special/xsf/cephes/polevl.h +0 -167
  731. scipy/special/xsf/cephes/psi.h +0 -194
  732. scipy/special/xsf/cephes/rgamma.h +0 -111
  733. scipy/special/xsf/cephes/scipy_iv.h +0 -811
  734. scipy/special/xsf/cephes/shichi.h +0 -248
  735. scipy/special/xsf/cephes/sici.h +0 -224
  736. scipy/special/xsf/cephes/sindg.h +0 -221
  737. scipy/special/xsf/cephes/tandg.h +0 -139
  738. scipy/special/xsf/cephes/trig.h +0 -58
  739. scipy/special/xsf/cephes/unity.h +0 -186
  740. scipy/special/xsf/cephes/zeta.h +0 -172
  741. scipy/special/xsf/config.h +0 -304
  742. scipy/special/xsf/digamma.h +0 -205
  743. scipy/special/xsf/error.h +0 -57
  744. scipy/special/xsf/evalpoly.h +0 -47
  745. scipy/special/xsf/expint.h +0 -266
  746. scipy/special/xsf/hyp2f1.h +0 -694
  747. scipy/special/xsf/iv_ratio.h +0 -173
  748. scipy/special/xsf/lambertw.h +0 -150
  749. scipy/special/xsf/loggamma.h +0 -163
  750. scipy/special/xsf/sici.h +0 -200
  751. scipy/special/xsf/tools.h +0 -427
  752. scipy/special/xsf/trig.h +0 -164
  753. scipy/special/xsf/wright_bessel.h +0 -843
  754. scipy/special/xsf/zlog1.h +0 -35
  755. scipy/stats/_mvn.cp311-win_amd64.dll.a +0 -0
  756. scipy/stats/_mvn.cp311-win_amd64.pyd +0 -0
  757. scipy-1.15.3.dist-info/DELVEWHEEL +0 -2
  758. /scipy-1.15.3-cp311-cp311-win_amd64.whl → /scipy-1.16.0rc2-cp311-cp311-win_amd64.whl +0 -0
  759. {scipy-1.15.3.dist-info → scipy-1.16.0rc2.dist-info}/WHEEL +0 -0
@@ -1,15 +1,18 @@
1
1
  import functools
2
2
  from abc import ABC, abstractmethod
3
3
  from functools import cached_property
4
+ import inspect
4
5
  import math
5
6
 
6
7
  import numpy as np
7
8
  from numpy import inf
8
9
 
9
- from scipy._lib._util import _lazywhere, _rng_spawn
10
+ from scipy._lib._array_api import xp_promote
11
+ from scipy._lib._util import _rng_spawn, _RichResult
10
12
  from scipy._lib._docscrape import ClassDoc, NumpyDocString
11
13
  from scipy import special, stats
12
- from scipy.integrate import tanhsinh as _tanhsinh
14
+ from scipy.special._ufuncs import _log1mexp
15
+ from scipy.integrate import tanhsinh as _tanhsinh, nsum
13
16
  from scipy.optimize._bracket import _bracket_root, _bracket_minimum
14
17
  from scipy.optimize._chandrupatla import _chandrupatla, _chandrupatla_minimize
15
18
  from scipy.stats._probability_distribution import _ProbabilityDistribution
@@ -207,7 +210,7 @@ class _Domain(ABC):
207
210
  or not (False). Used for input validation.
208
211
  get_numerical_endpoints()
209
212
  Gets the numerical values of the domain endpoints, which may have been
210
- defined symbolically.
213
+ defined symbolically or through a callable.
211
214
  __str__()
212
215
  Returns a text representation of the domain (e.g. ``[0, b)``).
213
216
  Used for generating documentation.
@@ -232,8 +235,8 @@ class _Domain(ABC):
232
235
  raise NotImplementedError()
233
236
 
234
237
 
235
- class _SimpleDomain(_Domain):
236
- r""" Representation of a simply-connected domain defined by two endpoints.
238
+ class _Interval(_Domain):
239
+ r""" Representation of an interval defined by two endpoints.
237
240
 
238
241
  Each endpoint may be a finite scalar, positive or negative infinity, or
239
242
  be given by a single parameter. The domain may include the endpoints or
@@ -247,10 +250,12 @@ class _SimpleDomain(_Domain):
247
250
  ----------
248
251
  symbols : dict
249
252
  Inherited. A map from special values to symbols for use in `__str__`.
250
- endpoints : 2-tuple of float(s) and/or str(s)
253
+ endpoints : 2-tuple of float(s) and/or str(s) and/or callable(s).
251
254
  A tuple with two values. Each may be either a float (the numerical
252
- value of the endpoints of the domain) or a string (the name of the
253
- parameters that will define the endpoint).
255
+ value of the endpoints of the domain), a string (the name of the
256
+ parameters that will define the endpoint), or a callable taking the
257
+ parameters used to define the endpoints of the domain as keyword only
258
+ arguments and returning a numerical value for the endpoint.
254
259
  inclusive : 2-tuple of bools
255
260
  A tuple with two boolean values; each indicates whether the
256
261
  corresponding endpoint is included within the domain or not.
@@ -261,9 +266,11 @@ class _SimpleDomain(_Domain):
261
266
  Records any parameters used to define the endpoints of the domain
262
267
  get_numerical_endpoints(parameter_values)
263
268
  Gets the numerical values of the domain endpoints, which may have been
264
- defined symbolically.
269
+ defined symbolically or through a callable.
265
270
  contains(item, parameter_values)
266
271
  Determines whether the argument is contained within the domain
272
+ draw(size, rng, proportions, parameter_values)
273
+ Draws random values based on the domain.
267
274
 
268
275
  """
269
276
  def __init__(self, endpoints=(-inf, inf), inclusive=(False, False)):
@@ -298,8 +305,9 @@ class _SimpleDomain(_Domain):
298
305
  def get_numerical_endpoints(self, parameter_values):
299
306
  r""" Get the numerical values of the domain endpoints.
300
307
 
301
- Domain endpoints may be defined symbolically. This returns numerical
302
- values of the endpoints given numerical values for any variables.
308
+ Domain endpoints may be defined symbolically or through a callable.
309
+ This returns numerical values of the endpoints given numerical values for
310
+ any variables.
303
311
 
304
312
  Parameters
305
313
  ----------
@@ -313,15 +321,22 @@ class _SimpleDomain(_Domain):
313
321
  Numerical values of the endpoints
314
322
 
315
323
  """
316
- # TODO: ensure outputs are floats
317
324
  a, b = self.endpoints
318
325
  # If `a` (`b`) is a string - the name of the parameter that defines
319
326
  # the endpoint of the domain - then corresponding numerical values
320
- # will be found in the `parameter_values` dictionary. Otherwise, it is
321
- # itself the array of numerical values of the endpoint.
327
+ # will be found in the `parameter_values` dictionary.
328
+ # If a callable, it will be executed with `parameter_values` passed as
329
+ # keyword arguments, and it will return the numerical values.
330
+ # Otherwise, it is itself the array of numerical values of the endpoint.
322
331
  try:
323
- a = np.asarray(parameter_values.get(a, a))
324
- b = np.asarray(parameter_values.get(b, b))
332
+ if callable(a):
333
+ a = a(**parameter_values)
334
+ else:
335
+ a = np.asarray(parameter_values.get(a, a))
336
+ if callable(b):
337
+ b = b(**parameter_values)
338
+ else:
339
+ b = np.asarray(parameter_values.get(b, b))
325
340
  except TypeError as e:
326
341
  message = ("The endpoints of the distribution are defined by "
327
342
  "parameters, but their values were not provided. When "
@@ -329,7 +344,9 @@ class _SimpleDomain(_Domain):
329
344
  "all required distribution parameters as keyword "
330
345
  "arguments.")
331
346
  raise TypeError(message) from e
332
-
347
+ # Floating point types are used for even integer parameters.
348
+ # Convert to float here to ensure consistency throughout framework.
349
+ a, b = xp_promote(a, b, force_floating=True, xp=np)
333
350
  return a, b
334
351
 
335
352
  def contains(self, item, parameter_values=None):
@@ -366,44 +383,6 @@ class _SimpleDomain(_Domain):
366
383
  in_right = item <= b if right_inclusive else item < b
367
384
  return in_left & in_right
368
385
 
369
-
370
- class _RealDomain(_SimpleDomain):
371
- r""" Represents a simply-connected subset of the real line; i.e., an interval
372
-
373
- Completes the implementation of the `_SimpleDomain` class for simple
374
- domains on the real line.
375
-
376
- Methods
377
- -------
378
- define_parameters(*parameters)
379
- (Inherited) Records any parameters used to define the endpoints of the
380
- domain.
381
- get_numerical_endpoints(parameter_values)
382
- (Inherited) Gets the numerical values of the domain endpoints, which
383
- may have been defined symbolically.
384
- contains(item, parameter_values)
385
- (Inherited) Determines whether the argument is contained within the
386
- domain
387
- __str__()
388
- Returns a string representation of the domain, e.g. "[a, b)".
389
- draw(size, rng, proportions, parameter_values)
390
- Draws random values based on the domain. Proportions of values within
391
- the domain, on the endpoints of the domain, outside the domain,
392
- and having value NaN are specified by `proportions`.
393
-
394
- """
395
-
396
- def __str__(self):
397
- a, b = self.endpoints
398
- left_inclusive, right_inclusive = self.inclusive
399
-
400
- left = "[" if left_inclusive else "("
401
- a = self.symbols.get(a, f"{a}")
402
- right = "]" if right_inclusive else ")"
403
- b = self.symbols.get(b, f"{b}")
404
-
405
- return f"{left}{a}, {b}{right}"
406
-
407
386
  def draw(self, n, type_, min, max, squeezed_base_shape, rng=None):
408
387
  r""" Draw random values from the domain.
409
388
 
@@ -428,6 +407,9 @@ class _RealDomain(_SimpleDomain):
428
407
  """
429
408
  rng = np.random.default_rng(rng)
430
409
 
410
+ def ints(*args, **kwargs): return rng.integers(*args, **kwargs, endpoint=True)
411
+ uniform = rng.uniform if isinstance(self, _RealInterval) else ints
412
+
431
413
  # get copies of min and max with no nans so that uniform doesn't fail
432
414
  min_nn, max_nn = min.copy(), max.copy()
433
415
  i = np.isnan(min_nn) | np.isnan(max_nn)
@@ -437,7 +419,7 @@ class _RealDomain(_SimpleDomain):
437
419
  shape = (n,) + squeezed_base_shape
438
420
 
439
421
  if type_ == 'in':
440
- z = rng.uniform(min_nn, max_nn, size=shape)
422
+ z = uniform(min_nn, max_nn, size=shape)
441
423
 
442
424
  elif type_ == 'on':
443
425
  z_on_shape = shape
@@ -447,9 +429,8 @@ class _RealDomain(_SimpleDomain):
447
429
  z[~i] = max
448
430
 
449
431
  elif type_ == 'out':
450
- # make this work for infinite bounds
451
- z = min_nn - rng.uniform(size=shape)
452
- zr = max_nn + rng.uniform(size=shape)
432
+ z = min_nn - uniform(1, 5, size=shape) # 1, 5 is arbitary; we just want
433
+ zr = max_nn + uniform(1, 5, size=shape) # some numbers outside domain
453
434
  i = rng.random(size=n) < 0.5
454
435
  z[i] = zr[i]
455
436
 
@@ -459,16 +440,114 @@ class _RealDomain(_SimpleDomain):
459
440
  return z
460
441
 
461
442
 
462
- class _IntegerDomain(_SimpleDomain):
463
- r""" Representation of a domain of consecutive integers.
443
+ class _RealInterval(_Interval):
444
+ r""" Represents a simply-connected subset of the real line; i.e., an interval
445
+
446
+ Completes the implementation of the `_Interval` class for intervals
447
+ on the real line.
448
+
449
+ Methods
450
+ -------
451
+ define_parameters(*parameters)
452
+ (Inherited) Records any parameters used to define the endpoints of the
453
+ domain.
454
+ get_numerical_endpoints(parameter_values)
455
+ (Inherited) Gets the numerical values of the domain endpoints, which
456
+ may have been defined symbolically.
457
+ contains(item, parameter_values)
458
+ (Inherited) Determines whether the argument is contained within the
459
+ domain
460
+ __str__()
461
+ Returns a string representation of the domain, e.g. "[a, b)".
462
+ """
463
+
464
+ def __str__(self):
465
+ a, b = self.endpoints
466
+ a, b = self._get_endpoint_str(a, "f1"), self._get_endpoint_str(b, "f2")
467
+ left_inclusive, right_inclusive = self.inclusive
468
+ left = "[" if left_inclusive else "("
469
+ right = "]" if right_inclusive else ")"
470
+
471
+ return f"{left}{a}, {b}{right}"
472
+
473
+ def _get_endpoint_str(self, endpoint, funcname):
474
+ if callable(endpoint):
475
+ if endpoint.__doc__ is not None:
476
+ return endpoint.__doc__
477
+ params = inspect.signature(endpoint).parameters.values()
478
+ params = [
479
+ p.name for p in params if p.kind == inspect.Parameter.KEYWORD_ONLY
480
+ ]
481
+ return f"{funcname}({','.join(params)})"
482
+ return self.symbols.get(endpoint, f"{endpoint}")
483
+
484
+
485
+ class _IntegerInterval(_Interval):
486
+ r""" Represents an interval of integers
464
487
 
465
- Completes the implementation of the `_SimpleDomain` class for domains
466
- composed of consecutive integer values.
488
+ Completes the implementation of the `_Interval` class for simple
489
+ domains on the integers.
490
+
491
+ Methods
492
+ -------
493
+ define_parameters(*parameters)
494
+ (Inherited) Records any parameters used to define the endpoints of the
495
+ domain.
496
+ get_numerical_endpoints(parameter_values)
497
+ (Inherited) Gets the numerical values of the domain endpoints, which
498
+ may have been defined symbolically.
499
+ contains(item, parameter_values)
500
+ (Overridden) Determines whether the argument is contained within the
501
+ domain
502
+ draw(n, type_, min, max, squeezed_base_shape, rng=None)
503
+ (Inherited) Draws random values based on the domain.
504
+ __str__()
505
+ Returns a string representation of the domain, e.g. "{a, a+1, ..., b-1, b}".
467
506
 
468
- To be completed when needed.
469
507
  """
470
- def __init__(self):
471
- raise NotImplementedError
508
+ def contains(self, item, parameter_values=None):
509
+ super_contains = super().contains(item, parameter_values)
510
+ integral = (item == np.round(item))
511
+ return super_contains & integral
512
+
513
+ def __str__(self):
514
+ a, b = self.endpoints
515
+ a = self.symbols.get(a, a)
516
+ b = self.symbols.get(b, b)
517
+
518
+ a_str, b_str = isinstance(a, str), isinstance(b, str)
519
+ a_inf = a == r"-\infty" if a_str else np.isinf(a)
520
+ b_inf = b == r"\infty" if b_str else np.isinf(b)
521
+
522
+ # This doesn't work well for cases where ``a`` is floating point
523
+ # number large enough that ``nextafter(a, inf) > a + 1``, and
524
+ # similarly for ``b`` and nextafter(b, -inf). There may not be any
525
+ # distributions fit for SciPy where we would actually need to handle these
526
+ # cases though.
527
+ ap1 = f"{a} + 1" if a_str else f"{a + 1}"
528
+ bm1 = f"{b} - 1" if b_str else f"{b - 1}"
529
+
530
+ if not a_str and not b_str:
531
+ gap = b - a
532
+ if gap == 3:
533
+ return f"\\{{{a}, {ap1}, {bm1}, {b}\\}}"
534
+ if gap == 2:
535
+ return f"\\{{{a}, {ap1}, {b}\\}}"
536
+ if gap == 1:
537
+ return f"\\{{{a}, {b}\\}}"
538
+ if gap == 0:
539
+ return f"\\{{{a}\\}}"
540
+
541
+ if not a_inf and b_inf:
542
+ ap2 = f"{a} + 2" if a_str else f"{a + 2}"
543
+ return f"\\{{{a}, {ap1}, {ap2}, ...\\}}"
544
+ if a_inf and not b_inf:
545
+ bm2 = f"{b} - 2" if b_str else f"{b - 2}"
546
+ return f"\\{{{b}, {bm1}, {bm2}, ...\\}}"
547
+ if a_inf and b_inf:
548
+ return "\\{..., -2, -1, 0, 1, 2, ...\\}"
549
+
550
+ return f"\\{{{a}, {ap1}, ..., {bm1}, {b}\\}}"
472
551
 
473
552
 
474
553
  class _Parameter(ABC):
@@ -516,7 +595,7 @@ class _Parameter(ABC):
516
595
  self.symbol = symbol or name
517
596
  self.domain = domain
518
597
  if typical is not None and not isinstance(typical, _Domain):
519
- typical = _RealDomain(typical)
598
+ typical = domain.__class__(typical)
520
599
  self.typical = typical or domain
521
600
 
522
601
  def __str__(self):
@@ -818,7 +897,7 @@ class _Parameterization:
818
897
  # we can draw values in order a, b, c.
819
898
  parameter_values = {}
820
899
 
821
- if not len(sizes) or not np.iterable(sizes[0]):
900
+ if sizes is None or not len(sizes) or not np.iterable(sizes[0]):
822
901
  sizes = [sizes]*len(self.parameters)
823
902
 
824
903
  for size, param in zip(sizes, self.parameters.values()):
@@ -835,6 +914,7 @@ def _set_invalid_nan(f):
835
914
  # Wrapper for input / output validation and standardization of distribution
836
915
  # functions that accept either the quantile or percentile as an argument:
837
916
  # logpdf, pdf
917
+ # logpmf, pmf
838
918
  # logcdf, cdf
839
919
  # logccdf, ccdf
840
920
  # ilogcdf, icdf
@@ -850,12 +930,15 @@ def _set_invalid_nan(f):
850
930
  endpoints = {'icdf': (0, 1), 'iccdf': (0, 1),
851
931
  'ilogcdf': (-np.inf, 0), 'ilogccdf': (-np.inf, 0)}
852
932
  replacements = {'logpdf': (-inf, -inf), 'pdf': (0, 0),
933
+ 'logpmf': (-inf, -inf), 'pmf': (0, 0),
853
934
  '_logcdf1': (-inf, 0), '_logccdf1': (0, -inf),
854
935
  '_cdf1': (0, 1), '_ccdf1': (1, 0)}
855
- replace_strict = {'pdf', 'logpdf'}
936
+ replace_strict = {'pdf', 'logpdf', 'pmf', 'logpmf'}
856
937
  replace_exact = {'icdf', 'iccdf', 'ilogcdf', 'ilogccdf'}
857
938
  clip = {'_cdf1', '_ccdf1'}
858
939
  clip_log = {'_logcdf1', '_logccdf1'}
940
+ # relevant to discrete distributions only
941
+ replace_non_integral = {'pmf', 'logpmf', 'pdf', 'logpdf'}
859
942
 
860
943
  @functools.wraps(f)
861
944
  def filtered(self, x, *args, **kwargs):
@@ -866,6 +949,9 @@ def _set_invalid_nan(f):
866
949
  x = np.asarray(x)
867
950
  dtype = self._dtype
868
951
  shape = self._shape
952
+ discrete = isinstance(self, DiscreteDistribution)
953
+ keep_low_endpoint = discrete and method_name in {'_cdf1', '_logcdf1',
954
+ '_ccdf1', '_logccdf1'}
869
955
 
870
956
  # Ensure that argument is at least as precise as distribution
871
957
  # parameters, which are already at least floats. This will avoid issues
@@ -894,7 +980,7 @@ def _set_invalid_nan(f):
894
980
  # and the result will be set to the appropriate value.
895
981
  left_inc, right_inc = self._variable.domain.inclusive
896
982
  mask_low = (x < low if (method_name in replace_strict and left_inc)
897
- else x <= low)
983
+ or keep_low_endpoint else x <= low)
898
984
  mask_high = (x > high if (method_name in replace_strict and right_inc)
899
985
  else x >= high)
900
986
  mask_invalid = (mask_low | mask_high)
@@ -911,6 +997,14 @@ def _set_invalid_nan(f):
911
997
  any_endpoint = (mask_endpoint if mask_endpoint.shape == ()
912
998
  else np.any(mask_endpoint))
913
999
 
1000
+ # Check for non-integral arguments to PMF method
1001
+ # or PDF of a discrete distribution.
1002
+ any_non_integral = False
1003
+ if discrete and method_name in replace_non_integral:
1004
+ mask_non_integral = (x != np.floor(x))
1005
+ any_non_integral = (mask_non_integral if mask_non_integral.shape == ()
1006
+ else np.any(mask_non_integral))
1007
+
914
1008
  # Set out-of-domain arguments to NaN. The result will be set to the
915
1009
  # appropriate value later.
916
1010
  if any_invalid:
@@ -928,12 +1022,19 @@ def _set_invalid_nan(f):
928
1022
 
929
1023
  if res.shape != shape: # faster to check first
930
1024
  res = np.broadcast_to(res, self._shape)
931
- res_needs_copy = res_needs_copy or any_invalid or any_endpoint
1025
+ res_needs_copy = (res_needs_copy or any_invalid
1026
+ or any_endpoint or any_non_integral)
932
1027
 
933
1028
  if res_needs_copy:
934
1029
  res = np.array(res, dtype=dtype, copy=True)
935
1030
 
936
- # For arguments outside the function domain, replace results
1031
+ # For non-integral arguments to PMF (and PDF of discrete distribution)
1032
+ # replace with zero.
1033
+ if any_non_integral:
1034
+ zero = -np.inf if method_name in {'logpmf', 'logpdf'} else 0
1035
+ res[mask_non_integral & ~np.isnan(res)] = zero
1036
+
1037
+ # For arguments outside the function domain, replace results
937
1038
  if any_invalid:
938
1039
  replace_low, replace_high = (
939
1040
  replacements.get(method_name, (np.nan, np.nan)))
@@ -954,7 +1055,8 @@ def _set_invalid_nan(f):
954
1055
  a[mask_high_endpoint] if method_name.endswith('ccdf')
955
1056
  else b[mask_high_endpoint])
956
1057
 
957
- res[mask_low_endpoint] = replace_low_endpoint
1058
+ if not keep_low_endpoint:
1059
+ res[mask_low_endpoint] = replace_low_endpoint
958
1060
  res[mask_high_endpoint] = replace_high_endpoint
959
1061
 
960
1062
  # Clip probabilities to [0, 1]
@@ -1147,52 +1249,11 @@ def _kwargs2args(f, args=None, kwargs=None):
1147
1249
  def wrapped(x, *args):
1148
1250
  return f(x, *args[:n_args], **dict(zip(names, args[n_args:])))
1149
1251
 
1150
- args = list(args) + list(kwargs.values())
1252
+ args = tuple(args) + tuple(kwargs.values())
1151
1253
 
1152
1254
  return wrapped, args
1153
1255
 
1154
1256
 
1155
- def _log1mexp(x):
1156
- r"""Compute the log of the complement of the exponential.
1157
-
1158
- This function is equivalent to::
1159
-
1160
- log1mexp(x) = np.log(1-np.exp(x))
1161
-
1162
- but avoids loss of precision when ``np.exp(x)`` is nearly 0 or 1.
1163
-
1164
- Parameters
1165
- ----------
1166
- x : array_like
1167
- Input array.
1168
-
1169
- Returns
1170
- -------
1171
- y : ndarray
1172
- An array of the same shape as `x`.
1173
-
1174
- Examples
1175
- --------
1176
- >>> import numpy as np
1177
- >>> from scipy.stats._distribution_infrastructure import _log1mexp
1178
- >>> x = 1e-300 # log of a number very close to 1
1179
- >>> _log1mexp(x) # log of the complement of a number very close to 1
1180
- -690.7755278982137
1181
- >>> # np.log1p(-np.exp(x)) # -inf; emits warning
1182
-
1183
- """
1184
- def f1(x):
1185
- # good for exp(x) close to 0
1186
- return np.log1p(-np.exp(x))
1187
-
1188
- def f2(x):
1189
- # good for exp(x) close to 1
1190
- with np.errstate(divide='ignore'):
1191
- return np.real(np.log(-special.expm1(x + 0j)))
1192
-
1193
- return _lazywhere(x < -1, (x,), f=f1, f2=f2)[()]
1194
-
1195
-
1196
1257
  def _logexpxmexpy(x, y):
1197
1258
  """ Compute the log of the difference of the exponentials of two arguments.
1198
1259
 
@@ -1260,7 +1321,7 @@ def _combine_docs(dist_family, *, include_examples=True):
1260
1321
  fields.remove('Examples')
1261
1322
 
1262
1323
  doc = ClassDoc(dist_family)
1263
- superdoc = ClassDoc(ContinuousDistribution)
1324
+ superdoc = ClassDoc(UnivariateDistribution)
1264
1325
  for field in fields:
1265
1326
  if field in {"Methods", "Attributes"}:
1266
1327
  doc[field] = superdoc[field]
@@ -1278,7 +1339,7 @@ def _combine_docs(dist_family, *, include_examples=True):
1278
1339
  def _generate_domain_support(dist_family):
1279
1340
  n_parameterizations = len(dist_family._parameterizations)
1280
1341
 
1281
- domain = f"\nfor :math:`x` in {dist_family._variable.domain}.\n"
1342
+ domain = f"\nfor :math:`x \\in {dist_family._variable.domain}`.\n"
1282
1343
 
1283
1344
  if n_parameterizations == 0:
1284
1345
  support = """
@@ -1328,7 +1389,7 @@ def _generate_example(dist_family):
1328
1389
 
1329
1390
  p = 0.32
1330
1391
  x = round(X.icdf(p), 2)
1331
- y = round(X.icdf(2 * p), 2)
1392
+ y = round(X.icdf(2 * p), 2) # noqa: F841
1332
1393
 
1333
1394
  example = f"""
1334
1395
  To use the distribution class, it must be instantiated using keyword
@@ -1363,19 +1424,24 @@ def _generate_example(dist_family):
1363
1424
  """
1364
1425
 
1365
1426
  example += f"""
1366
- To evaluate the probability density function of the underlying distribution
1427
+ To evaluate the probability density/mass function of the underlying distribution
1367
1428
  at argument ``x={x}``:
1368
1429
 
1369
1430
  >>> x = {x}
1370
- >>> X.pdf(x)
1371
- {X.pdf(x)}
1431
+ >>> X.pdf(x), X.pmf(x)
1432
+ {X.pdf(x), X.pmf(x)}
1372
1433
 
1373
1434
  The cumulative distribution function, its complement, and the logarithm
1374
1435
  of these functions are evaluated similarly.
1375
1436
 
1376
1437
  >>> np.allclose(np.exp(X.logccdf(x)), 1 - X.cdf(x))
1377
1438
  True
1439
+ """
1378
1440
 
1441
+ # When two-arg CDF is implemented for DiscreteDistribution, consider removing
1442
+ # the special-casing here.
1443
+ if issubclass(dist_family, ContinuousDistribution):
1444
+ example_continuous = f"""
1379
1445
  The inverse of these functions with respect to the argument ``x`` is also
1380
1446
  available.
1381
1447
 
@@ -1391,22 +1457,40 @@ def _generate_example(dist_family):
1391
1457
  >>> y = {y}
1392
1458
  >>> np.allclose(X.ccdf(x, y), 1 - (X.cdf(y) - X.cdf(x)))
1393
1459
  True
1460
+ """
1461
+ example += example_continuous
1394
1462
 
1463
+ example += f"""
1395
1464
  There are methods for computing measures of central tendency,
1396
1465
  dispersion, higher moments, and entropy.
1397
1466
 
1398
1467
  >>> X.mean(), X.median(), X.mode()
1399
1468
  {X.mean(), X.median(), X.mode()}
1469
+
1400
1470
  >>> X.variance(), X.standard_deviation()
1401
1471
  {X.variance(), X.standard_deviation()}
1472
+
1402
1473
  >>> X.skewness(), X.kurtosis()
1403
1474
  {X.skewness(), X.kurtosis()}
1475
+
1404
1476
  >>> np.allclose(X.moment(order=6, kind='standardized'),
1405
1477
  ... X.moment(order=6, kind='central') / X.variance()**3)
1406
1478
  True
1479
+ """
1480
+
1481
+ # When logentropy is implemented for DiscreteDistribution, remove special-casing
1482
+ if issubclass(dist_family, ContinuousDistribution):
1483
+ example += """
1407
1484
  >>> np.allclose(np.exp(X.logentropy()), X.entropy())
1408
1485
  True
1486
+ """
1487
+ else:
1488
+ example += f"""
1489
+ >>> X.entropy()
1490
+ {X.entropy()}
1491
+ """
1409
1492
 
1493
+ example += f"""
1410
1494
  Pseudo-random samples can be drawn from
1411
1495
  the underlying distribution using ``sample``.
1412
1496
 
@@ -1419,7 +1503,7 @@ def _generate_example(dist_family):
1419
1503
  return example
1420
1504
 
1421
1505
 
1422
- class ContinuousDistribution(_ProbabilityDistribution):
1506
+ class UnivariateDistribution(_ProbabilityDistribution):
1423
1507
  r""" Class that represents a continuous statistical distribution.
1424
1508
 
1425
1509
  Parameters
@@ -2004,12 +2088,24 @@ class ContinuousDistribution(_ProbabilityDistribution):
2004
2088
  args = np.broadcast_arrays(*args)
2005
2089
  # If we know the median or mean, consider breaking up the interval
2006
2090
  rtol = None if _isnull(self.tol) else self.tol
2007
- res = _tanhsinh(f, a, b, args=args, log=log, rtol=rtol)
2008
2091
  # For now, we ignore the status, but I want to return the error
2009
2092
  # estimate - see question 5 at the top.
2010
- return res.integral
2011
-
2012
- def _solve_bounded(self, f, p, *, bounds=None, params=None):
2093
+ if isinstance(self, ContinuousDistribution):
2094
+ res = _tanhsinh(f, a, b, args=args, log=log, rtol=rtol)
2095
+ return res.integral
2096
+ else:
2097
+ res = nsum(f, a, b, args=args, log=log, tolerances=dict(rtol=rtol)).sum
2098
+ res = np.asarray(res)
2099
+ # The result should be nan when parameters are nan, so need to special
2100
+ # case this.
2101
+ cond = np.isnan(params.popitem()[1]) if params else np.True_
2102
+ cond = np.broadcast_to(cond, a.shape)
2103
+ res[(a > b)] = -np.inf if log else 0 # fix in nsum?
2104
+ res[cond] = np.nan
2105
+
2106
+ return res[()]
2107
+
2108
+ def _solve_bounded(self, f, p, *, bounds=None, params=None, xatol=None):
2013
2109
  # Finds the argument of a function that produces the desired output.
2014
2110
  # Much of this should be added to _bracket_root / _chandrupatla.
2015
2111
  xmin, xmax = self._support(**params) if bounds is None else bounds
@@ -2018,7 +2114,10 @@ class ContinuousDistribution(_ProbabilityDistribution):
2018
2114
  p, xmin, xmax = np.broadcast_arrays(p, xmin, xmax)
2019
2115
  if not p.size:
2020
2116
  # might need to figure out result type based on p
2021
- return np.empty(p.shape, dtype=self._dtype)
2117
+ res = _RichResult()
2118
+ empty = np.empty(p.shape, dtype=self._dtype)
2119
+ res.xl, res.x, res.xr = empty, empty, empty
2120
+ res.fl, res.fr = empty, empty
2022
2121
 
2023
2122
  def f2(x, _p, **kwargs): # named `_p` to avoid conflict with shape `p`
2024
2123
  return f(x, **kwargs) - _p
@@ -2039,8 +2138,11 @@ class ContinuousDistribution(_ProbabilityDistribution):
2039
2138
  res = _bracket_root(f3, xl0=xl0, xr0=xr0, xmin=xmin, xmax=xmax, args=args)
2040
2139
  # For now, we ignore the status, but I want to use the bracket width
2041
2140
  # as an error estimate - see question 5 at the top.
2141
+
2042
2142
  xrtol = None if _isnull(self.tol) else self.tol
2043
- return _chandrupatla(f3, a=res.xl, b=res.xr, args=args, xrtol=xrtol).x
2143
+ xatol = None if xatol is None else xatol
2144
+ tolerances = dict(xrtol=xrtol, xatol=xatol, fatol=0, frtol=0)
2145
+ return _chandrupatla(f3, a=res.xl, b=res.xr, args=args, **tolerances)
2044
2146
 
2045
2147
  ## Other
2046
2148
 
@@ -2058,7 +2160,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2058
2160
  # For more complete discussion of the considerations, see:
2059
2161
  # https://github.com/scipy/scipy/pull/21050#discussion_r1707798901
2060
2162
  method = getattr(self.__class__, method_name, None)
2061
- super_method = getattr(ContinuousDistribution, method_name, None)
2163
+ super_method = getattr(UnivariateDistribution, method_name, None)
2062
2164
  return method is not super_method
2063
2165
 
2064
2166
  ### Distribution properties
@@ -2187,8 +2289,8 @@ class ContinuousDistribution(_ProbabilityDistribution):
2187
2289
 
2188
2290
  def _logentropy_quadrature(self, **params):
2189
2291
  def logintegrand(x, **params):
2190
- logpdf = self._logpdf_dispatch(x, **params)
2191
- return logpdf + np.log(0j+logpdf)
2292
+ logpxf = self._logpxf_dispatch(x, **params)
2293
+ return logpxf + np.log(0j+logpxf)
2192
2294
  res = self._quadrature(logintegrand, params=params, log=True)
2193
2295
  return _log_real_standardize(res + np.pi*1j)
2194
2296
 
@@ -2214,9 +2316,12 @@ class ContinuousDistribution(_ProbabilityDistribution):
2214
2316
 
2215
2317
  def _entropy_quadrature(self, **params):
2216
2318
  def integrand(x, **params):
2217
- pdf = self._pdf_dispatch(x, **params)
2218
- logpdf = self._logpdf_dispatch(x, **params)
2219
- return logpdf * pdf
2319
+ pxf = self._pxf_dispatch(x, **params)
2320
+ logpxf = self._logpxf_dispatch(x, **params)
2321
+ temp = np.asarray(pxf)
2322
+ i = (pxf != 0) # 0 * inf -> nan; should be 0
2323
+ temp[i] = pxf[i]*logpxf[i]
2324
+ return temp
2220
2325
  return -self._quadrature(integrand, params=params)
2221
2326
 
2222
2327
  @_set_invalid_nan_property
@@ -2254,17 +2359,18 @@ class ContinuousDistribution(_ProbabilityDistribution):
2254
2359
  def _mode_formula(self, **params):
2255
2360
  raise NotImplementedError(self._not_implemented)
2256
2361
 
2257
- def _mode_optimization(self, **params):
2362
+ def _mode_optimization(self, xatol=None, **params):
2258
2363
  if not self._size:
2259
2364
  return np.empty(self._shape, dtype=self._dtype)
2260
2365
 
2261
2366
  a, b = self._support(**params)
2262
2367
  m = self._median_dispatch(**params)
2263
2368
 
2264
- f, args = _kwargs2args(lambda x, **params: -self._pdf_dispatch(x, **params),
2369
+ f, args = _kwargs2args(lambda x, **params: -self._pxf_dispatch(x, **params),
2265
2370
  args=(), kwargs=params)
2266
2371
  res_b = _bracket_minimum(f, m, xmin=a, xmax=b, args=args)
2267
- res = _chandrupatla_minimize(f, res_b.xl, res_b.xm, res_b.xr, args=args)
2372
+ res = _chandrupatla_minimize(f, res_b.xl, res_b.xm, res_b.xr,
2373
+ args=args, xatol=xatol)
2268
2374
  mode = np.asarray(res.x)
2269
2375
  mode_at_boundary = res_b.status == -1
2270
2376
  mode_at_left = mode_at_boundary & (res_b.fl <= res_b.fm)
@@ -2328,7 +2434,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2328
2434
  # See the note corresponding with the "Distribution Parameters" for more
2329
2435
  # information.
2330
2436
 
2331
- ## Probability Density Functions
2437
+ ## Probability Density/Mass Functions
2332
2438
 
2333
2439
  @_set_invalid_nan
2334
2440
  def logpdf(self, x, /, *, method=None):
@@ -2366,6 +2472,43 @@ class ContinuousDistribution(_ProbabilityDistribution):
2366
2472
  def _pdf_logexp(self, x, **params):
2367
2473
  return np.exp(self._logpdf_dispatch(x, **params))
2368
2474
 
2475
+ @_set_invalid_nan
2476
+ def logpmf(self, x, /, *, method=None):
2477
+ return self._logpmf_dispatch(x, method=method, **self._parameters)
2478
+
2479
+ @_dispatch
2480
+ def _logpmf_dispatch(self, x, *, method=None, **params):
2481
+ if self._overrides('_logpmf_formula'):
2482
+ method = self._logpmf_formula
2483
+ elif _isnull(self.tol): # ensure that developers override _logpmf
2484
+ method = self._logpmf_logexp
2485
+ return method
2486
+
2487
+ def _logpmf_formula(self, x, **params):
2488
+ raise NotImplementedError(self._not_implemented)
2489
+
2490
+ def _logpmf_logexp(self, x, **params):
2491
+ with np.errstate(divide='ignore'):
2492
+ return np.log(self._pmf_dispatch(x, **params))
2493
+
2494
+ @_set_invalid_nan
2495
+ def pmf(self, x, /, *, method=None):
2496
+ return self._pmf_dispatch(x, method=method, **self._parameters)
2497
+
2498
+ @_dispatch
2499
+ def _pmf_dispatch(self, x, *, method=None, **params):
2500
+ if self._overrides('_pmf_formula'):
2501
+ method = self._pmf_formula
2502
+ else:
2503
+ method = self._pmf_logexp
2504
+ return method
2505
+
2506
+ def _pmf_formula(self, x, **params):
2507
+ raise NotImplementedError(self._not_implemented)
2508
+
2509
+ def _pmf_logexp(self, x, **params):
2510
+ return np.exp(self._logpmf_dispatch(x, **params))
2511
+
2369
2512
  ## Cumulative Distribution Functions
2370
2513
 
2371
2514
  def logcdf(self, x, y=None, /, *, method=None):
@@ -2431,7 +2574,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2431
2574
  return out[()]
2432
2575
 
2433
2576
  def _logcdf2_quadrature(self, x, y, **params):
2434
- logres = self._quadrature(self._logpdf_dispatch, limits=(x, y),
2577
+ logres = self._quadrature(self._logpxf_dispatch, limits=(x, y),
2435
2578
  log=True, params=params)
2436
2579
  return logres
2437
2580
 
@@ -2472,7 +2615,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2472
2615
 
2473
2616
  def _logcdf_quadrature(self, x, **params):
2474
2617
  a, _ = self._support(**params)
2475
- return self._quadrature(self._logpdf_dispatch, limits=(a, x),
2618
+ return self._quadrature(self._logpxf_dispatch, limits=(a, x),
2476
2619
  params=params, log=True)
2477
2620
 
2478
2621
  def cdf(self, x, y=None, /, *, method=None):
@@ -2536,11 +2679,11 @@ class ContinuousDistribution(_ProbabilityDistribution):
2536
2679
  params_mask = {key: np.broadcast_to(val, mask.shape)[mask]
2537
2680
  for key, val in params.items()}
2538
2681
  out = np.asarray(out)
2539
- out[mask] = self._cdf2_quadrature(x[mask], y[mask], *params_mask)
2682
+ out[mask] = self._cdf2_quadrature(x[mask], y[mask], **params_mask)
2540
2683
  return out[()]
2541
2684
 
2542
2685
  def _cdf2_quadrature(self, x, y, **params):
2543
- return self._quadrature(self._pdf_dispatch, limits=(x, y), params=params)
2686
+ return self._quadrature(self._pxf_dispatch, limits=(x, y), params=params)
2544
2687
 
2545
2688
  @_set_invalid_nan
2546
2689
  def _cdf1(self, x, *, method):
@@ -2582,7 +2725,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2582
2725
 
2583
2726
  def _cdf_quadrature(self, x, **params):
2584
2727
  a, _ = self._support(**params)
2585
- return self._quadrature(self._pdf_dispatch, limits=(a, x),
2728
+ return self._quadrature(self._pxf_dispatch, limits=(a, x),
2586
2729
  params=params)
2587
2730
 
2588
2731
  def logccdf(self, x, y=None, /, *, method=None):
@@ -2650,7 +2793,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2650
2793
 
2651
2794
  def _logccdf_quadrature(self, x, **params):
2652
2795
  _, b = self._support(**params)
2653
- return self._quadrature(self._logpdf_dispatch, limits=(x, b),
2796
+ return self._quadrature(self._logpxf_dispatch, limits=(x, b),
2654
2797
  params=params, log=True)
2655
2798
 
2656
2799
  def ccdf(self, x, y=None, /, *, method=None):
@@ -2720,7 +2863,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2720
2863
 
2721
2864
  def _ccdf_quadrature(self, x, **params):
2722
2865
  _, b = self._support(**params)
2723
- return self._quadrature(self._pdf_dispatch, limits=(x, b),
2866
+ return self._quadrature(self._pxf_dispatch, limits=(x, b),
2724
2867
  params=params)
2725
2868
 
2726
2869
  ## Inverse cumulative distribution functions
@@ -2746,7 +2889,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2746
2889
  return self._ilogccdf_dispatch(_log1mexp(x), **params)
2747
2890
 
2748
2891
  def _ilogcdf_inversion(self, x, **params):
2749
- return self._solve_bounded(self._logcdf_dispatch, x, params=params)
2892
+ return self._solve_bounded_continuous(self._logcdf_dispatch, x, params=params)
2750
2893
 
2751
2894
  @_set_invalid_nan
2752
2895
  def icdf(self, p, /, *, method=None):
@@ -2781,7 +2924,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2781
2924
  return out[()]
2782
2925
 
2783
2926
  def _icdf_inversion(self, x, **params):
2784
- return self._solve_bounded(self._cdf_dispatch, x, params=params)
2927
+ return self._solve_bounded_continuous(self._cdf_dispatch, x, params=params)
2785
2928
 
2786
2929
  @_set_invalid_nan
2787
2930
  def ilogccdf(self, logp, /, *, method=None):
@@ -2804,7 +2947,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2804
2947
  return self._ilogcdf_dispatch(_log1mexp(x), **params)
2805
2948
 
2806
2949
  def _ilogccdf_inversion(self, x, **params):
2807
- return self._solve_bounded(self._logccdf_dispatch, x, params=params)
2950
+ return self._solve_bounded_continuous(self._logccdf_dispatch, x, params=params)
2808
2951
 
2809
2952
  @_set_invalid_nan
2810
2953
  def iccdf(self, p, /, *, method=None):
@@ -2839,7 +2982,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
2839
2982
  return out[()]
2840
2983
 
2841
2984
  def _iccdf_inversion(self, x, **params):
2842
- return self._solve_bounded(self._ccdf_dispatch, x, params=params)
2985
+ return self._solve_bounded_continuous(self._ccdf_dispatch, x, params=params)
2843
2986
 
2844
2987
  ### Sampling Functions
2845
2988
  # The following functions for drawing samples from the distribution are
@@ -2877,13 +3020,13 @@ class ContinuousDistribution(_ProbabilityDistribution):
2877
3020
  sample_shape = (shape,) if not np.iterable(shape) else tuple(shape)
2878
3021
  full_shape = sample_shape + self._shape
2879
3022
  rng = np.random.default_rng(rng) if not isinstance(rng, qmc.QMCEngine) else rng
2880
- res = self._sample_dispatch(sample_shape, full_shape, method=method,
2881
- rng=rng, **self._parameters)
3023
+ res = self._sample_dispatch(full_shape, method=method, rng=rng,
3024
+ **self._parameters)
2882
3025
 
2883
3026
  return res.astype(self._dtype, copy=False)
2884
3027
 
2885
3028
  @_dispatch
2886
- def _sample_dispatch(self, sample_shape, full_shape, *, method, rng, **params):
3029
+ def _sample_dispatch(self, full_shape, *, method, rng, **params):
2887
3030
  # make sure that tests catch if sample is 0d array
2888
3031
  if self._overrides('_sample_formula') and not isinstance(rng, qmc.QMCEngine):
2889
3032
  method = self._sample_formula
@@ -2891,20 +3034,21 @@ class ContinuousDistribution(_ProbabilityDistribution):
2891
3034
  method = self._sample_inverse_transform
2892
3035
  return method
2893
3036
 
2894
- def _sample_formula(self, sample_shape, full_shape, *, rng, **params):
3037
+ def _sample_formula(self, full_shape, *, rng, **params):
2895
3038
  raise NotImplementedError(self._not_implemented)
2896
3039
 
2897
- def _sample_inverse_transform(self, sample_shape, full_shape, *, rng, **params):
3040
+ def _sample_inverse_transform(self, full_shape, *, rng, **params):
2898
3041
  if isinstance(rng, qmc.QMCEngine):
2899
- uniform = self._qmc_uniform(sample_shape, full_shape, qrng=rng, **params)
3042
+ uniform = self._qmc_uniform(full_shape, qrng=rng, **params)
2900
3043
  else:
2901
3044
  uniform = rng.random(size=full_shape, dtype=self._dtype)
2902
3045
  return self._icdf_dispatch(uniform, **params)
2903
3046
 
2904
- def _qmc_uniform(self, sample_shape, full_shape, *, qrng, **params):
3047
+ def _qmc_uniform(self, full_shape, *, qrng, **params):
2905
3048
  # Generate QMC uniform sample(s) on unit interval with specified shape;
2906
3049
  # if `sample_shape != ()`, then each slice along axis 0 is independent.
2907
3050
 
3051
+ sample_shape = full_shape[:len(full_shape)-len(self._shape)]
2908
3052
  # Determine the number of independent sequences and the length of each.
2909
3053
  n_low_discrepancy = sample_shape[0] if sample_shape else 1
2910
3054
  n_independent = math.prod(full_shape[1:] if sample_shape else full_shape)
@@ -3022,7 +3166,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
3022
3166
  moment = self._moment_raw_general(order, **params)
3023
3167
 
3024
3168
  if moment is None and 'quadrature' in methods:
3025
- moment = self._moment_integrate_pdf(order, center=self._zero, **params)
3169
+ moment = self._moment_from_pxf(order, center=self._zero, **params)
3026
3170
 
3027
3171
  if moment is None and 'quadrature_icdf' in methods:
3028
3172
  moment = self._moment_integrate_icdf(order, center=self._zero, **params)
@@ -3085,7 +3229,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
3085
3229
  if moment is None and 'quadrature' in methods:
3086
3230
  mean = self._moment_raw_dispatch(self._one, **params,
3087
3231
  methods=self._moment_methods)
3088
- moment = self._moment_integrate_pdf(order, center=mean, **params)
3232
+ moment = self._moment_from_pxf(order, center=mean, **params)
3089
3233
 
3090
3234
  if moment is None and 'quadrature_icdf' in methods:
3091
3235
  mean = self._moment_raw_dispatch(self._one, **params,
@@ -3176,10 +3320,10 @@ class ContinuousDistribution(_ProbabilityDistribution):
3176
3320
  general_standard_moments = {0: self._one, 1: self._zero, 2: self._one}
3177
3321
  return general_standard_moments.get(order, None)
3178
3322
 
3179
- def _moment_integrate_pdf(self, order, center, **params):
3323
+ def _moment_from_pxf(self, order, center, **params):
3180
3324
  def integrand(x, order, center, **params):
3181
- pdf = self._pdf_dispatch(x, **params)
3182
- return pdf*(x-center)**order
3325
+ pxf = self._pxf_dispatch(x, **params)
3326
+ return pxf*(x-center)**order
3183
3327
  return self._quadrature(integrand, args=(order, center), params=params)
3184
3328
 
3185
3329
  def _moment_integrate_icdf(self, order, center, **params):
@@ -3215,7 +3359,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
3215
3359
 
3216
3360
  def _logmoment_quad(self, order, logcenter, **params):
3217
3361
  def logintegrand(x, order, logcenter, **params):
3218
- logpdf = self._logpdf_dispatch(x, **params)
3362
+ logpdf = self._logpxf_dispatch(x, **params)
3219
3363
  return logpdf + order * _logexpxmexpy(np.log(x + 0j), logcenter)
3220
3364
  ## if logx == logcenter, `_logexpxmexpy` returns (-inf + 0j)
3221
3365
  ## multiplying by order produces (-inf + nan j) - bad
@@ -3231,7 +3375,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
3231
3375
 
3232
3376
  ### Convenience
3233
3377
 
3234
- def plot(self, x='x', y='pdf', *, t=('cdf', 0.0005, 0.9995), ax=None):
3378
+ def plot(self, x='x', y=None, *, t=None, ax=None):
3235
3379
  r"""Plot a function of the distribution.
3236
3380
 
3237
3381
  Convenience function for quick visualization of the distribution
@@ -3242,14 +3386,18 @@ class ContinuousDistribution(_ProbabilityDistribution):
3242
3386
  x, y : str, optional
3243
3387
  String indicating the quantities to be used as the abscissa and
3244
3388
  ordinate (horizontal and vertical coordinates), respectively.
3245
- Defaults are ``'x'`` (the domain of the random variable) and
3246
- ``'pdf'`` (the probability density function). Valid values are:
3247
- 'x', 'pdf', 'cdf', 'ccdf', 'icdf', 'iccdf', 'logpdf', 'logcdf',
3248
- 'logccdf', 'ilogcdf', 'ilogccdf'.
3389
+ Defaults are ``'x'`` (the domain of the random variable) and either
3390
+ ``'pdf'`` (the probability density function) (continuous) or
3391
+ ``'pdf'`` (the probability density function) (discrete).
3392
+ Valid values are:
3393
+ 'x', 'pdf', 'pmf', 'cdf', 'ccdf', 'icdf', 'iccdf', 'logpdf', 'logpmf',
3394
+ 'logcdf', 'logccdf', 'ilogcdf', 'ilogccdf'.
3249
3395
  t : 3-tuple of (str, float, float), optional
3250
3396
  Tuple indicating the limits within which the quantities are plotted.
3251
- Default is ``('cdf', 0.001, 0.999)`` indicating that the central
3252
- 99.9% of the distribution is to be shown. Valid values are:
3397
+ The default is ``('cdf', 0.0005, 0.9995)`` if the domain is infinite,
3398
+ indicating that the central 99.9% of the distribution is to be shown;
3399
+ otherwise, endpoints of the support are used where they are finite.
3400
+ Valid values are:
3253
3401
  'x', 'cdf', 'ccdf', 'icdf', 'iccdf', 'logcdf', 'logccdf',
3254
3402
  'ilogcdf', 'ilogccdf'.
3255
3403
  ax : `matplotlib.axes`, optional
@@ -3307,14 +3455,24 @@ class ContinuousDistribution(_ProbabilityDistribution):
3307
3455
  # - when the parameters of the distribution are an array,
3308
3456
  # use the full range of abscissae for all curves
3309
3457
 
3458
+ discrete = isinstance(self, DiscreteDistribution)
3310
3459
  t_is_quantile = {'x', 'icdf', 'iccdf', 'ilogcdf', 'ilogccdf'}
3311
3460
  t_is_probability = {'cdf', 'ccdf', 'logcdf', 'logccdf'}
3312
3461
  valid_t = t_is_quantile.union(t_is_probability)
3313
- valid_xy = valid_t.union({'pdf', 'logpdf'})
3462
+ valid_xy = valid_t.union({'pdf', 'logpdf', 'pmf', 'logpmf'})
3463
+ y_default = 'pmf' if discrete else 'pdf'
3464
+ y = y_default if y is None else y
3314
3465
 
3315
3466
  ndim = self._ndim
3316
3467
  x_name, y_name = x, y
3317
- t_name, tlim = t[0], np.asarray(t[1:])
3468
+ t_name = 'cdf' if t is None else t[0]
3469
+
3470
+ a, b = self.support()
3471
+ tliml_default = 0 if np.all(np.isfinite(a)) else 0.0005
3472
+ tliml = tliml_default if t is None else t[1]
3473
+ tlimr_default = 1 if np.all(np.isfinite(b)) else 0.9995
3474
+ tlimr = tlimr_default if t is None else t[2]
3475
+ tlim = np.asarray([tliml, tlimr])
3318
3476
  tlim = tlim[:, np.newaxis] if ndim else tlim
3319
3477
 
3320
3478
  # pdf/logpdf are not valid for `t` because we can't easily invert them
@@ -3330,7 +3488,7 @@ class ContinuousDistribution(_ProbabilityDistribution):
3330
3488
 
3331
3489
  message = (f'Argument `y` of `{self.__class__.__name__}.plot` "'
3332
3490
  f'must be one of {valid_xy}')
3333
- if t_name not in valid_xy:
3491
+ if y_name not in valid_xy:
3334
3492
  raise ValueError(message)
3335
3493
 
3336
3494
  # This could just be a warning
@@ -3363,16 +3521,32 @@ class ContinuousDistribution(_ProbabilityDistribution):
3363
3521
  raise ValueError(message)
3364
3522
 
3365
3523
  # form quantile grid
3366
- grid = np.linspace(0, 1, 300)
3367
- grid = grid[:, np.newaxis] if ndim else grid
3368
- q = qlim[0] + (qlim[1] - qlim[0]) * grid
3524
+ if discrete and x_name in t_is_quantile:
3525
+ # should probably aggregate for large ranges
3526
+ q = np.arange(np.min(qlim[0]), np.max(qlim[1]) + 1)
3527
+ q = q[:, np.newaxis] if ndim else q
3528
+ else:
3529
+ grid = np.linspace(0, 1, 300)
3530
+ grid = grid[:, np.newaxis] if ndim else grid
3531
+ q = qlim[0] + (qlim[1] - qlim[0]) * grid
3532
+ q = np.round(q) if discrete else q
3369
3533
 
3370
3534
  # compute requested x and y at quantile grid
3371
3535
  x = q if x_name in t_is_quantile else getattr(self, x_name)(q)
3372
3536
  y = q if y_name in t_is_quantile else getattr(self, y_name)(q)
3373
3537
 
3374
3538
  # make plot
3375
- ax.plot(x, y)
3539
+ x, y = np.broadcast_arrays(x.T, np.atleast_2d(y.T))
3540
+ for xi, yi in zip(x, y): # plot is vectorized, but bar/step don't seem to be
3541
+ if discrete and x_name in t_is_quantile and y_name == 'pmf':
3542
+ # should this just be a step plot, too?
3543
+ ax.bar(xi, yi, alpha=np.sqrt(1/y.shape[0])) # alpha heuristic
3544
+ elif discrete and x_name in t_is_quantile:
3545
+ values = yi
3546
+ edges = np.concatenate((xi, [xi[-1]+1]))
3547
+ ax.stairs(values, edges, baseline=None)
3548
+ else:
3549
+ ax.plot(xi, yi)
3376
3550
  ax.set_xlabel(f"${x_name}$")
3377
3551
  ax.set_ylabel(f"${y_name}$")
3378
3552
  ax.set_title(str(self))
@@ -3422,8 +3596,176 @@ class ContinuousDistribution(_ProbabilityDistribution):
3422
3596
  # these methods reasonably efficiently.
3423
3597
 
3424
3598
 
3599
+ class ContinuousDistribution(UnivariateDistribution):
3600
+ def _overrides(self, method_name):
3601
+ if method_name in {'_logpmf_formula', '_pmf_formula'}:
3602
+ return True
3603
+ return super()._overrides(method_name)
3604
+
3605
+ def _pmf_formula(self, x, **params):
3606
+ return np.zeros_like(x)
3607
+
3608
+ def _logpmf_formula(self, x, **params):
3609
+ return np.full_like(x, -np.inf)
3610
+
3611
+ def _pxf_dispatch(self, x, *, method=None, **params):
3612
+ return self._pdf_dispatch(x, method=method, **params)
3613
+
3614
+ def _logpxf_dispatch(self, x, *, method=None, **params):
3615
+ return self._logpdf_dispatch(x, method=method, **params)
3616
+
3617
+ def _solve_bounded_continuous(self, func, p, params, xatol=None):
3618
+ return self._solve_bounded(func, p, params=params, xatol=xatol).x
3619
+
3620
+
3621
+ class DiscreteDistribution(UnivariateDistribution):
3622
+ def _overrides(self, method_name):
3623
+ if method_name in {'_logpdf_formula', '_pdf_formula'}:
3624
+ return True
3625
+ return super()._overrides(method_name)
3626
+
3627
+ def _logpdf_formula(self, x, **params):
3628
+ if params:
3629
+ p = next(iter(params.values()))
3630
+ nan_result = np.isnan(x) | np.isnan(p)
3631
+ else:
3632
+ nan_result = np.isnan(x)
3633
+ return np.where(nan_result, np.nan, np.inf)
3634
+
3635
+ def _pdf_formula(self, x, **params):
3636
+ if params:
3637
+ p = next(iter(params.values()))
3638
+ nan_result = np.isnan(x) | np.isnan(p)
3639
+ else:
3640
+ nan_result = np.isnan(x)
3641
+ return np.where(nan_result, np.nan, np.inf)
3642
+
3643
+ def _pxf_dispatch(self, x, *, method=None, **params):
3644
+ return self._pmf_dispatch(x, method=method, **params)
3645
+
3646
+ def _logpxf_dispatch(self, x, *, method=None, **params):
3647
+ return self._logpmf_dispatch(x, method=method, **params)
3648
+
3649
+ def _cdf_quadrature(self, x, **params):
3650
+ return super()._cdf_quadrature(np.floor(x), **params)
3651
+
3652
+ def _logcdf_quadrature(self, x, **params):
3653
+ return super()._logcdf_quadrature(np.floor(x), **params)
3654
+
3655
+ def _ccdf_quadrature(self, x, **params):
3656
+ return super()._ccdf_quadrature(np.floor(x + 1), **params)
3657
+
3658
+ def _logccdf_quadrature(self, x, **params):
3659
+ return super()._logccdf_quadrature(np.floor(x + 1), **params)
3660
+
3661
+ def _cdf2(self, x, y, *, method):
3662
+ raise NotImplementedError(
3663
+ "Two argument cdf functions are currently only supported for "
3664
+ "continuous distributions.")
3665
+
3666
+ def _ccdf2(self, x, y, *, method):
3667
+ raise NotImplementedError(
3668
+ "Two argument cdf functions are currently only supported for "
3669
+ "continuous distributions.")
3670
+
3671
+ def _logcdf2(self, x, y, *, method):
3672
+ raise NotImplementedError(
3673
+ "Two argument cdf functions are currently only supported for "
3674
+ "continuous distributions.")
3675
+
3676
+ def _logccdf2(self, x, y, *, method):
3677
+ raise NotImplementedError(
3678
+ "Two argument cdf functions are currently only supported for "
3679
+ "continuous distributions.")
3680
+
3681
+ def _solve_bounded_discrete(self, func, p, params, comp):
3682
+ res = self._solve_bounded(func, p, params=params, xatol=0.9)
3683
+ x = np.asarray(np.floor(res.xr))
3684
+
3685
+ # if _chandrupatla finds exact inverse, the bracket may not have been reduced
3686
+ # enough for `np.floor(res.x)` to be the appropriate value of `x`.
3687
+ mask = res.fun == 0
3688
+ x[mask] = np.floor(res.x[mask])
3689
+
3690
+ xmin, xmax = self._support(**params)
3691
+ p, xmin, xmax = np.broadcast_arrays(p, xmin, xmax)
3692
+ mask = comp(func(xmin, **params), p)
3693
+ x[mask] = xmin[mask]
3694
+
3695
+ return x
3696
+
3697
+ def _base_discrete_inversion(self, p, func, comp, /, **params):
3698
+ # For discrete distributions, icdf(p) is defined as the minimum n
3699
+ # such that cdf(n) >= p. iccdf(p) is defined as the minimum n such
3700
+ # that ccdf(n) <= p, or equivalently as iccdf(p) = icdf(1 - p).
3701
+
3702
+ # First try to find where cdf(x) == p for the continuous extension of the
3703
+ # cdf. res.xl and res.xr will be a bracket for this root. The parameter
3704
+ # xatol in solve_bounded controls the bracket width. We thus know that
3705
+ # know cdf(res.xr) >= p, cdf(res.xl) <= p, and |res.xr - res.xl| <= 0.9.
3706
+ # This means the minimum integer n such that cdf(n) >= p is either floor(x)
3707
+ # or floor(x) + 1.
3708
+ x = self._solve_bounded_discrete(func, p, params=params, comp=comp)
3709
+ # comp should be <= for ccdf, >= for cdf.
3710
+ f = func(x, **params)
3711
+ res = np.where(comp(f, p), x, x + 1.0)
3712
+ # xr is a bracket endpoint, and will usually be a finite value even when
3713
+ # the computed result should be nan. We need to explicitly handle this
3714
+ # case.
3715
+ res[np.isnan(f) | np.isnan(p)] = np.nan
3716
+ return res[()]
3717
+
3718
+ def _icdf_inversion(self, x, **params):
3719
+ return self._base_discrete_inversion(x, self._cdf_dispatch,
3720
+ np.greater_equal, **params)
3721
+
3722
+ def _ilogcdf_inversion(self, x, **params):
3723
+ return self._base_discrete_inversion(x, self._logcdf_dispatch,
3724
+ np.greater_equal, **params)
3725
+
3726
+ def _iccdf_inversion(self, x, **params):
3727
+ return self._base_discrete_inversion(x, self._ccdf_dispatch,
3728
+ np.less_equal, **params)
3729
+
3730
+ def _ilogccdf_inversion(self, x, **params):
3731
+ return self._base_discrete_inversion(x, self._logccdf_dispatch,
3732
+ np.less_equal, **params)
3733
+
3734
+ def _mode_optimization(self, **params):
3735
+ # If `x` is the true mode of a unimodal continuous function, we can find
3736
+ # the mode among integers by rounding in each direction and checking
3737
+ # which is better. If the difference between `x` and the nearest integer
3738
+ # is less than `xatol`, the computed value of `x` may end up on the wrong
3739
+ # side of the nearest integer. Setting `xatol=0.5` guarantees that at most
3740
+ # three integers need to be checked, the two nearest integers, ``floor(x)``
3741
+ # and ``round(x)`` and the nearest integer other than these.
3742
+ x = super()._mode_optimization(xatol=0.5, **params)
3743
+ low, high = self.support()
3744
+ xl, xr = np.floor(x), np.ceil(x)
3745
+ nearest = np.round(x)
3746
+ # Clip to stay within support. There will be redundant calculation
3747
+ # when clipping since `xo` will be one of `xl` or `xr`, but let's
3748
+ # keep the implementation simple for now.
3749
+ xo = np.clip(nearest + np.copysign(1, nearest - x), low, high)
3750
+ x = np.stack([xl, xo, xr])
3751
+ idx = np.argmax(self._pmf_dispatch(x, **params), axis=0)
3752
+ return np.choose(idx, [xl, xo, xr])
3753
+
3754
+ def _logentropy_quadrature(self, **params):
3755
+ def logintegrand(x, **params):
3756
+ logpmf = self._logpmf_dispatch(x, **params)
3757
+ # Entropy summand is -pmf*log(pmf), so log-entropy summand is
3758
+ # logpmf + log(logpmf) + pi*j. But pmf is always between 0 and 1,
3759
+ # so logpmf is always negative, and so log(logpmf) = log(-logpmf) + pi*j.
3760
+ # The two imaginary components "cancel" each other out (which we would
3761
+ # expect because each term of the entropy summand is positive).
3762
+ return np.where(np.isfinite(logpmf), logpmf + np.log(-logpmf), -np.inf)
3763
+ return self._quadrature(logintegrand, params=params, log=True)
3764
+
3765
+
3425
3766
  # Special case the names of some new-style distributions in `make_distribution`
3426
3767
  _distribution_names = {
3768
+ # Continuous
3427
3769
  'argus': 'ARGUS',
3428
3770
  'betaprime': 'BetaPrime',
3429
3771
  'chi2': 'ChiSquared',
@@ -3496,41 +3838,127 @@ _distribution_names = {
3496
3838
  'weibull_min': 'Weibull',
3497
3839
  'weibull_max': 'ReflectedWeibull',
3498
3840
  'wrapcauchy': 'WrappedCauchyLine',
3841
+ # Discrete
3842
+ 'betabinom': 'BetaBinomial',
3843
+ 'betanbinom': 'BetaNegativeBinomial',
3844
+ 'dlaplace': 'LaplaceDiscrete',
3845
+ 'geom': 'Geometric',
3846
+ 'hypergeom': 'Hypergeometric',
3847
+ 'logser': 'LogarithmicSeries',
3848
+ 'nbinom': 'NegativeBinomial',
3849
+ 'nchypergeom_fisher': 'NoncentralHypergeometricFisher',
3850
+ 'nchypergeom_wallenius': 'NoncentralHypergeometricWallenius',
3851
+ 'nhypergeom': 'NegativeHypergeometric',
3852
+ 'poisson_binom': 'PoissonBinomial',
3853
+ 'randint': 'UniformDiscrete',
3854
+ 'yulesimon': 'YuleSimon',
3855
+ 'zipf': 'Zeta',
3499
3856
  }
3500
3857
 
3501
3858
 
3502
3859
  # beta, genextreme, gengamma, t, tukeylambda need work for 1D arrays
3503
3860
  def make_distribution(dist):
3504
- """Generate a `ContinuousDistribution` from an instance of `rv_continuous`
3861
+ """Generate a `UnivariateDistribution` class from a compatible object
3862
+
3863
+ The argument may be an instance of `rv_continuous` or an instance of
3864
+ another class that satisfies the interface described below.
3505
3865
 
3506
- The returned value is a `ContinuousDistribution` subclass. Like any subclass
3507
- of `ContinuousDistribution`, it must be instantiated (i.e. by passing all shape
3508
- parameters as keyword arguments) before use. Once instantiated, the resulting
3509
- object will have the same interface as any other instance of
3510
- `ContinuousDistribution`; e.g., `scipy.stats.Normal`.
3866
+ The returned value is a `ContinuousDistribution` subclass if the input is an
3867
+ instance of `rv_continuous` or a `DiscreteDistribution` subclass if the input
3868
+ is an instance of `rv_discrete`. Like any subclass of `UnivariateDistribution`,
3869
+ it must be instantiated (i.e. by passing all shape parameters as keyword
3870
+ arguments) before use. Once instantiated, the resulting object will have the
3871
+ same interface as any other instance of `UnivariateDistribution`; e.g.,
3872
+ `scipy.stats.Normal`, `scipy.stats.Binomial`.
3511
3873
 
3512
3874
  .. note::
3513
3875
 
3514
3876
  `make_distribution` does not work perfectly with all instances of
3515
- `rv_continuous`. Known failures include `levy_stable` and `vonmises`,
3516
- and some methods of some distributions will not support array shape
3517
- parameters.
3877
+ `rv_continuous`. Known failures include `levy_stable`, `vonmises`,
3878
+ `hypergeom`, 'nchypergeom_fisher', 'nchypergeom_wallenius', and
3879
+ `poisson_binom`. Some methods of some distributions will not support
3880
+ array shape parameters.
3518
3881
 
3519
3882
  Parameters
3520
3883
  ----------
3521
3884
  dist : `rv_continuous`
3522
- Instance of `rv_continuous`.
3885
+ Instance of `rv_continuous`, `rv_discrete`, or an instance of any class with
3886
+ the following attributes:
3887
+
3888
+ __make_distribution_version__ : str
3889
+ A string containing the version number of SciPy in which this interface
3890
+ is defined. The preferred interface may change in future SciPy versions,
3891
+ in which case support for an old interface version may be deprecated
3892
+ and eventually removed.
3893
+ parameters : dict or tuple
3894
+ If a dictionary, each key is the name of a parameter,
3895
+ and the corresponding value is either a dictionary or tuple.
3896
+ If the value is a dictionary, it may have the following items, with default
3897
+ values used for entries which aren't present.
3898
+
3899
+ endpoints : tuple, default: (-inf, inf)
3900
+ A tuple defining the lower and upper endpoints of the domain of the
3901
+ parameter; allowable values are floats, the name (string) of another
3902
+ parameter, or a callable taking parameters as keyword only
3903
+ arguments and returning the numerical value of an endpoint for
3904
+ given parameter values.
3905
+
3906
+ inclusive : tuple of bool, default: (False, False)
3907
+ A tuple specifying whether the endpoints are included within the domain
3908
+ of the parameter.
3909
+
3910
+ typical : tuple, default: ``endpoints``
3911
+ Defining endpoints of a typical range of values of a parameter. Can be
3912
+ used for sampling parameter values for testing. Behaves like the
3913
+ ``endpoints`` tuple above, and should define a subinterval of the
3914
+ domain given by ``endpoints``.
3915
+
3916
+ A tuple value ``(a, b)`` associated to a key in the ``parameters``
3917
+ dictionary is equivalent to ``{endpoints: (a, b)}``.
3918
+
3919
+ Custom distributions with multiple parameterizations can be defined by
3920
+ having the ``parameters`` attribute be a tuple of dictionaries with
3921
+ the structure described above. In this case, ``dist``\'s class must also
3922
+ define a method ``process_parameters`` to map between the different
3923
+ parameterizations. It must take all parameters from all parameterizations
3924
+ as optional keyword arguments and return a dictionary mapping parameters to
3925
+ values, filling in values from other parameterizations using values from
3926
+ the supplied parameterization. See example.
3927
+
3928
+ support : dict or tuple
3929
+ A dictionary describing the support of the distribution or a tuple
3930
+ describing the endpoints of the support. This behaves identically to
3931
+ the values of the parameters dict described above, except that the key
3932
+ ``typical`` is ignored.
3933
+
3934
+ The class **must** also define a ``pdf`` method and **may** define methods
3935
+ ``logentropy``, ``entropy``, ``median``, ``mode``, ``logpdf``,
3936
+ ``logcdf``, ``cdf``, ``logccdf``, ``ccdf``,
3937
+ ``ilogcdf``, ``icdf``, ``ilogccdf``, ``iccdf``,
3938
+ ``moment``, and ``sample``.
3939
+ If defined, these methods must accept the parameters of the distribution as
3940
+ keyword arguments and also accept any positional-only arguments accepted by
3941
+ the corresponding method of `ContinuousDistribution`.
3942
+ When multiple parameterizations are defined, these methods must accept
3943
+ all parameters from all parameterizations. The ``moment`` method
3944
+ must accept the ``order`` and ``kind`` arguments by position or keyword, but
3945
+ may return ``None`` if a formula is not available for the arguments; in this
3946
+ case, the infrastructure will fall back to a default implementation. The
3947
+ ``sample`` method must accept ``shape`` by position or keyword, but contrary
3948
+ to the public method of the same name, the argument it receives will be the
3949
+ *full* shape of the output array - that is, the shape passed to the public
3950
+ method prepended to the broadcasted shape of random variable parameters.
3523
3951
 
3524
3952
  Returns
3525
3953
  -------
3526
- CustomDistribution : `ContinuousDistribution`
3527
- A subclass of `ContinuousDistribution` corresponding with `dist`. The
3954
+ CustomDistribution : `UnivariateDistribution`
3955
+ A subclass of `UnivariateDistribution` corresponding with `dist`. The
3528
3956
  initializer requires all shape parameters to be passed as keyword arguments
3529
- (using the same names as the instance of `rv_continuous`).
3957
+ (using the same names as the instance of `rv_continuous`/`rv_discrete`).
3530
3958
 
3531
3959
  Notes
3532
3960
  -----
3533
- The documentation of `ContinuousDistribution` is not rendered. See below for
3961
+ The documentation of `UnivariateDistribution` is not rendered. See below for
3534
3962
  an example of how to instantiate the class (i.e. pass all shape parameters of
3535
3963
  `dist` to the initializer as keyword arguments). Documentation of all methods
3536
3964
  is identical to that of `scipy.stats.Normal`. Use ``help`` on the returned
@@ -3541,8 +3969,12 @@ def make_distribution(dist):
3541
3969
  >>> import numpy as np
3542
3970
  >>> import matplotlib.pyplot as plt
3543
3971
  >>> from scipy import stats
3544
- >>> LogU = stats.make_distribution(stats.loguniform)
3545
- >>> X = LogU(a=1.0, b=3.0)
3972
+ >>> from scipy import special
3973
+
3974
+ Create a `ContinuousDistribution` from `scipy.stats.loguniform`.
3975
+
3976
+ >>> LogUniform = stats.make_distribution(stats.loguniform)
3977
+ >>> X = LogUniform(a=1.0, b=3.0)
3546
3978
  >>> np.isclose((X + 0.25).median(), stats.loguniform.ppf(0.5, 1, 3, loc=0.25))
3547
3979
  np.True_
3548
3980
  >>> X.plot()
@@ -3551,30 +3983,160 @@ def make_distribution(dist):
3551
3983
  >>> plt.legend(('pdf', 'histogram'))
3552
3984
  >>> plt.show()
3553
3985
 
3986
+ Create a custom distribution.
3987
+
3988
+ >>> class MyLogUniform:
3989
+ ... @property
3990
+ ... def __make_distribution_version__(self):
3991
+ ... return "1.16.0"
3992
+ ...
3993
+ ... @property
3994
+ ... def parameters(self):
3995
+ ... return {'a': {'endpoints': (0, np.inf),
3996
+ ... 'inclusive': (False, False)},
3997
+ ... 'b': {'endpoints': ('a', np.inf),
3998
+ ... 'inclusive': (False, False)}}
3999
+ ...
4000
+ ... @property
4001
+ ... def support(self):
4002
+ ... return {'endpoints': ('a', 'b'), 'inclusive': (True, True)}
4003
+ ...
4004
+ ... def pdf(self, x, a, b):
4005
+ ... return 1 / (x * (np.log(b)- np.log(a)))
4006
+ >>>
4007
+ >>> MyLogUniform = stats.make_distribution(MyLogUniform())
4008
+ >>> Y = MyLogUniform(a=1.0, b=3.0)
4009
+ >>> np.isclose(Y.cdf(2.), X.cdf(2.))
4010
+ np.True_
4011
+
4012
+ Create a custom distribution with variable support.
4013
+
4014
+ >>> class MyUniformCube:
4015
+ ... @property
4016
+ ... def __make_distribution_version__(self):
4017
+ ... return "1.16.0"
4018
+ ...
4019
+ ... @property
4020
+ ... def parameters(self):
4021
+ ... return {"a": (-np.inf, np.inf),
4022
+ ... "b": {'endpoints':('a', np.inf), 'inclusive':(True, False)}}
4023
+ ...
4024
+ ... @property
4025
+ ... def support(self):
4026
+ ... def left(*, a, b):
4027
+ ... return a**3
4028
+ ...
4029
+ ... def right(*, a, b):
4030
+ ... return b**3
4031
+ ... return (left, right)
4032
+ ...
4033
+ ... def pdf(self, x, *, a, b):
4034
+ ... return 1 / (3*(b - a)*np.cbrt(x)**2)
4035
+ ...
4036
+ ... def cdf(self, x, *, a, b):
4037
+ ... return (np.cbrt(x) - a) / (b - a)
4038
+ >>>
4039
+ >>> MyUniformCube = stats.make_distribution(MyUniformCube())
4040
+ >>> X = MyUniformCube(a=-2, b=2)
4041
+ >>> Y = stats.Uniform(a=-2, b=2)**3
4042
+ >>> X.support()
4043
+ (-8.0, 8.0)
4044
+ >>> np.isclose(X.cdf(2.1), Y.cdf(2.1))
4045
+ np.True_
4046
+
4047
+ Create a custom distribution with multiple parameterizations. Here we create a
4048
+ custom version of the beta distribution that has an alternative parameterization
4049
+ in terms of the mean ``mu`` and a dispersion parameter ``nu``.
4050
+
4051
+ >>> class MyBeta:
4052
+ ... @property
4053
+ ... def __make_distribution_version__(self):
4054
+ ... return "1.16.0"
4055
+ ...
4056
+ ... @property
4057
+ ... def parameters(self):
4058
+ ... return ({"a": (0, np.inf), "b": (0, np.inf)},
4059
+ ... {"mu": (0, 1), "nu": (0, np.inf)})
4060
+ ...
4061
+ ... def process_parameters(self, a=None, b=None, mu=None, nu=None):
4062
+ ... if a is not None and b is not None:
4063
+ ... nu = a + b
4064
+ ... mu = a / nu
4065
+ ... else:
4066
+ ... a = mu * nu
4067
+ ... b = nu - a
4068
+ ... return dict(a=a, b=b, mu=mu, nu=nu)
4069
+ ...
4070
+ ... @property
4071
+ ... def support(self):
4072
+ ... return {'endpoints': (0, 1)}
4073
+ ...
4074
+ ... def pdf(self, x, a, b, mu, nu):
4075
+ ... return special._ufuncs._beta_pdf(x, a, b)
4076
+ ...
4077
+ ... def cdf(self, x, a, b, mu, nu):
4078
+ ... return special.betainc(a, b, x)
4079
+ >>>
4080
+ >>> MyBeta = stats.make_distribution(MyBeta())
4081
+ >>> X = MyBeta(a=2.0, b=2.0)
4082
+ >>> Y = MyBeta(mu=0.5, nu=4.0)
4083
+ >>> np.isclose(X.pdf(0.3), Y.pdf(0.3))
4084
+ np.True_
4085
+
3554
4086
  """
3555
- if dist in {stats.levy_stable, stats.vonmises}:
4087
+ if dist in {stats.levy_stable, stats.vonmises, stats.hypergeom,
4088
+ stats.nchypergeom_fisher, stats.nchypergeom_wallenius,
4089
+ stats.poisson_binom}:
3556
4090
  raise NotImplementedError(f"`{dist.name}` is not supported.")
3557
4091
 
3558
- if not isinstance(dist, stats.rv_continuous):
3559
- message = "The argument must be an instance of `rv_continuous`."
4092
+ if isinstance(dist, stats.rv_continuous | stats.rv_discrete):
4093
+ return _make_distribution_rv_generic(dist)
4094
+ elif getattr(dist, "__make_distribution_version__", "0.0.0") >= "1.16.0":
4095
+ return _make_distribution_custom(dist)
4096
+ else:
4097
+ message = ("The argument must be an instance of `rv_continuous`, "
4098
+ "`rv_discrete`, or an instance of a class with attribute "
4099
+ "`__make_distribution_version__ >= 1.16`.")
3560
4100
  raise ValueError(message)
3561
4101
 
4102
+ def _make_distribution_rv_generic(dist):
3562
4103
  parameters = []
3563
4104
  names = []
3564
4105
  support = getattr(dist, '_support', (dist.a, dist.b))
3565
4106
  for shape_info in dist._shape_info():
3566
- domain = _RealDomain(endpoints=shape_info.endpoints,
4107
+ domain = _RealInterval(endpoints=shape_info.endpoints,
3567
4108
  inclusive=shape_info.inclusive)
3568
4109
  param = _RealParameter(shape_info.name, domain=domain)
3569
4110
  parameters.append(param)
3570
4111
  names.append(shape_info.name)
3571
4112
 
3572
- _x_support = _RealDomain(endpoints=support, inclusive=(True, True))
3573
- _x_param = _RealParameter('x', domain=_x_support, typical=(-1, 1))
3574
-
3575
4113
  repr_str = _distribution_names.get(dist.name, dist.name.capitalize())
4114
+ if isinstance(dist, stats.rv_continuous):
4115
+ old_class, new_class = stats.rv_continuous, ContinuousDistribution
4116
+ else:
4117
+ old_class, new_class = stats.rv_discrete, DiscreteDistribution
3576
4118
 
3577
- class CustomDistribution(ContinuousDistribution):
4119
+ def _overrides(method_name):
4120
+ return (getattr(dist.__class__, method_name, None)
4121
+ is not getattr(old_class, method_name, None))
4122
+
4123
+ if _overrides("_get_support"):
4124
+ def left(**parameter_values):
4125
+ a, _ = dist._get_support(**parameter_values)
4126
+ return np.asarray(a)[()]
4127
+
4128
+ def right(**parameter_values):
4129
+ _, b = dist._get_support(**parameter_values)
4130
+ return np.asarray(b)[()]
4131
+
4132
+ endpoints = (left, right)
4133
+ else:
4134
+ endpoints = support
4135
+
4136
+ _x_support = _RealInterval(endpoints=endpoints, inclusive=(True, True))
4137
+ _x_param = _RealParameter('x', domain=_x_support, typical=(-1, 1))
4138
+
4139
+ class CustomDistribution(new_class):
3578
4140
  _parameterizations = ([_Parameterization(*parameters)] if parameters
3579
4141
  else [])
3580
4142
  _variable = _x_param
@@ -3587,14 +4149,7 @@ def make_distribution(dist):
3587
4149
  s = super().__str__()
3588
4150
  return s.replace('CustomDistribution', repr_str)
3589
4151
 
3590
- # override the domain's `get_numerical_endpoints` rather than the
3591
- # distribution's `_support` to ensure that `_support` takes care
3592
- # of any required broadcasting, etc.
3593
- def get_numerical_endpoints(parameter_values):
3594
- a, b = dist._get_support(**parameter_values)
3595
- return np.asarray(a)[()], np.asarray(b)[()]
3596
-
3597
- def _sample_formula(self, _, full_shape=(), *, rng=None, **kwargs):
4152
+ def _sample_formula(self, full_shape=(), *, rng=None, **kwargs):
3598
4153
  return dist._rvs(size=full_shape, random_state=rng, **kwargs)
3599
4154
 
3600
4155
  def _moment_raw_formula(self, order, **kwargs):
@@ -3625,6 +4180,8 @@ def make_distribution(dist):
3625
4180
 
3626
4181
  methods = {'_logpdf': '_logpdf_formula',
3627
4182
  '_pdf': '_pdf_formula',
4183
+ '_logpmf': '_logpmf_formula',
4184
+ '_pmf': '_pmf_formula',
3628
4185
  '_logcdf': '_logcdf_formula',
3629
4186
  '_cdf': '_cdf_formula',
3630
4187
  '_logsf': '_logccdf_formula',
@@ -3642,19 +4199,11 @@ def make_distribution(dist):
3642
4199
  continue
3643
4200
  # If method of old distribution overrides generic implementation...
3644
4201
  method = getattr(dist.__class__, old_method, None)
3645
- super_method = getattr(stats.rv_continuous, old_method, None)
4202
+ super_method = getattr(old_class, old_method, None)
3646
4203
  if method is not super_method:
3647
4204
  # Make it an attribute of the new object with the new name
3648
4205
  setattr(CustomDistribution, new_method, getattr(dist, old_method))
3649
4206
 
3650
- def _overrides(method_name):
3651
- return (getattr(dist.__class__, method_name, None)
3652
- is not getattr(stats.rv_continuous, method_name, None))
3653
-
3654
- if _overrides('_get_support'):
3655
- domain = CustomDistribution._variable.domain
3656
- domain.get_numerical_endpoints = get_numerical_endpoints
3657
-
3658
4207
  if _overrides('_munp'):
3659
4208
  CustomDistribution._moment_raw_formula = _moment_raw_formula
3660
4209
 
@@ -3670,7 +4219,7 @@ def make_distribution(dist):
3670
4219
  support_etc = _combine_docs(CustomDistribution, include_examples=False).lstrip()
3671
4220
  docs = [
3672
4221
  f"This class represents `scipy.stats.{dist.name}` as a subclass of "
3673
- "`ContinuousDistribution`.",
4222
+ f"`{new_class}`.",
3674
4223
  f"The `repr`/`str` of class instances is `{repr_str}`.",
3675
4224
  f"The PDF of the distribution is defined {support_etc}"
3676
4225
  ]
@@ -3679,6 +4228,93 @@ def make_distribution(dist):
3679
4228
  return CustomDistribution
3680
4229
 
3681
4230
 
4231
+ def _get_domain_info(info):
4232
+ domain_info = {"endpoints": info} if isinstance(info, tuple) else info
4233
+ typical = domain_info.pop("typical", None)
4234
+ return domain_info, typical
4235
+
4236
+
4237
+ def _make_distribution_custom(dist):
4238
+ dist_parameters = (
4239
+ dist.parameters if isinstance(dist.parameters, tuple) else (dist.parameters, )
4240
+ )
4241
+ parameterizations = []
4242
+ for parameterization in dist_parameters:
4243
+ # The attribute name ``parameters`` appears reasonable from a user facing
4244
+ # perspective, but there is a little tension here with the internal. It's
4245
+ # important to keep in mind that the ``parameters`` attribute in a
4246
+ # user-created custom distribution specifies ``_parameterizations`` within
4247
+ # the infrastructure.
4248
+ parameters = []
4249
+
4250
+ for name, info in parameterization.items():
4251
+ domain_info, typical = _get_domain_info(info)
4252
+ domain = _RealInterval(**domain_info)
4253
+ param = _RealParameter(name, domain=domain, typical=typical)
4254
+ parameters.append(param)
4255
+ parameterizations.append(_Parameterization(*parameters) if parameters else [])
4256
+
4257
+ domain_info, _ = _get_domain_info(dist.support)
4258
+ _x_support = _RealInterval(**domain_info)
4259
+ _x_param = _RealParameter('x', domain=_x_support)
4260
+ repr_str = dist.__class__.__name__
4261
+
4262
+ class CustomDistribution(ContinuousDistribution):
4263
+ _parameterizations = parameterizations
4264
+ _variable = _x_param
4265
+
4266
+ def __repr__(self):
4267
+ s = super().__repr__()
4268
+ return s.replace('CustomDistribution', repr_str)
4269
+
4270
+ def __str__(self):
4271
+ s = super().__str__()
4272
+ return s.replace('CustomDistribution', repr_str)
4273
+
4274
+ methods = {'sample', 'logentropy', 'entropy',
4275
+ 'median', 'mode', 'logpdf', 'pdf',
4276
+ 'logcdf2', 'logcdf', 'cdf2', 'cdf',
4277
+ 'logccdf2', 'logccdf', 'ccdf2', 'ccdf',
4278
+ 'ilogcdf', 'icdf', 'ilogccdf', 'iccdf'}
4279
+
4280
+ for method in methods:
4281
+ if hasattr(dist, method):
4282
+ # Make it an attribute of the new object with the new name
4283
+ new_method = f"_{method}_formula"
4284
+ setattr(CustomDistribution, new_method, getattr(dist, method))
4285
+
4286
+ if hasattr(dist, 'moment'):
4287
+ def _moment_raw_formula(self, order, **kwargs):
4288
+ return dist.moment(order, kind='raw', **kwargs)
4289
+
4290
+ def _moment_central_formula(self, order, **kwargs):
4291
+ return dist.moment(order, kind='central', **kwargs)
4292
+
4293
+ def _moment_standardized_formula(self, order, **kwargs):
4294
+ return dist.moment(order, kind='standardized', **kwargs)
4295
+
4296
+ CustomDistribution._moment_raw_formula = _moment_raw_formula
4297
+ CustomDistribution._moment_central_formula = _moment_central_formula
4298
+ CustomDistribution._moment_standardized_formula = _moment_standardized_formula
4299
+
4300
+ if hasattr(dist, 'process_parameters'):
4301
+ setattr(
4302
+ CustomDistribution,
4303
+ "_process_parameters",
4304
+ getattr(dist, "process_parameters")
4305
+ )
4306
+
4307
+ support_etc = _combine_docs(CustomDistribution, include_examples=False).lstrip()
4308
+ docs = [
4309
+ f"This class represents `{repr_str}` as a subclass of "
4310
+ "`ContinuousDistribution`.",
4311
+ f"The PDF of the distribution is defined {support_etc}"
4312
+ ]
4313
+ CustomDistribution.__doc__ = ("\n".join(docs))
4314
+
4315
+ return CustomDistribution
4316
+
4317
+
3682
4318
  # Rough sketch of how we might shift/scale distributions. The purpose of
3683
4319
  # making it a separate class is for
3684
4320
  # a) simplicity of the ContinuousDistribution class and
@@ -3750,6 +4386,9 @@ def _shift_scale_inverse_function(func):
3750
4386
 
3751
4387
  class TransformedDistribution(ContinuousDistribution):
3752
4388
  def __init__(self, X, /, *args, **kwargs):
4389
+ if not isinstance(X, ContinuousDistribution):
4390
+ message = "Transformations are currently only supported for continuous RVs."
4391
+ raise NotImplementedError(message)
3753
4392
  self._copy_parameterization()
3754
4393
  self._variable = X._variable
3755
4394
  self._dist = X
@@ -3805,11 +4444,11 @@ class TruncatedDistribution(TransformedDistribution):
3805
4444
  # - if the mode of `_dist` is within the support, it's still the mode
3806
4445
  # - rejection sampling might be more efficient than inverse transform
3807
4446
 
3808
- _lb_domain = _RealDomain(endpoints=(-inf, 'ub'), inclusive=(True, False))
4447
+ _lb_domain = _RealInterval(endpoints=(-inf, 'ub'), inclusive=(True, False))
3809
4448
  _lb_param = _RealParameter('lb', symbol=r'b_l',
3810
4449
  domain=_lb_domain, typical=(0.1, 0.2))
3811
4450
 
3812
- _ub_domain = _RealDomain(endpoints=('lb', inf), inclusive=(False, True))
4451
+ _ub_domain = _RealInterval(endpoints=('lb', inf), inclusive=(False, True))
3813
4452
  _ub_param = _RealParameter('ub', symbol=r'b_u',
3814
4453
  domain=_ub_domain, typical=(0.8, 0.9))
3815
4454
 
@@ -3940,7 +4579,7 @@ def truncate(X, lb=-np.inf, ub=np.inf):
3940
4579
  Furthermore, `truncate` can be applied to any random variable:
3941
4580
 
3942
4581
  >>> Rayleigh = stats.make_distribution(stats.rayleigh)
3943
- >>> W = stats.truncate(Rayleigh(), lb=0, ub=3)
4582
+ >>> W = stats.truncate(Rayleigh(), lb=0.5, ub=3)
3944
4583
  >>> W.plot()
3945
4584
  >>> plt.show()
3946
4585
 
@@ -3951,11 +4590,11 @@ def truncate(X, lb=-np.inf, ub=np.inf):
3951
4590
  class ShiftedScaledDistribution(TransformedDistribution):
3952
4591
  """Distribution with a standard shift/scale transformation."""
3953
4592
  # Unclear whether infinite loc/scale will work reasonably in all cases
3954
- _loc_domain = _RealDomain(endpoints=(-inf, inf), inclusive=(True, True))
4593
+ _loc_domain = _RealInterval(endpoints=(-inf, inf), inclusive=(True, True))
3955
4594
  _loc_param = _RealParameter('loc', symbol=r'\mu',
3956
4595
  domain=_loc_domain, typical=(1, 2))
3957
4596
 
3958
- _scale_domain = _RealDomain(endpoints=(-inf, inf), inclusive=(True, True))
4597
+ _scale_domain = _RealInterval(endpoints=(-inf, inf), inclusive=(True, True))
3959
4598
  _scale_param = _RealParameter('scale', symbol=r'\sigma',
3960
4599
  domain=_scale_domain, typical=(0.1, 10))
3961
4600
 
@@ -4047,6 +4686,26 @@ class ShiftedScaledDistribution(TransformedDistribution):
4047
4686
  pdf = self._dist._pdf_dispatch(x, *args, **params)
4048
4687
  return pdf / np.abs(scale)
4049
4688
 
4689
+ def _logpmf_dispatch(self, x, *args, loc, scale, sign, **params):
4690
+ x = self._transform(x, loc, scale)
4691
+ logpmf = self._dist._logpmf_dispatch(x, *args, **params)
4692
+ return logpmf - np.log(np.abs(scale))
4693
+
4694
+ def _pmf_dispatch(self, x, *args, loc, scale, sign, **params):
4695
+ x = self._transform(x, loc, scale)
4696
+ pmf = self._dist._pmf_dispatch(x, *args, **params)
4697
+ return pmf / np.abs(scale)
4698
+
4699
+ def _logpxf_dispatch(self, x, *args, loc, scale, sign, **params):
4700
+ x = self._transform(x, loc, scale)
4701
+ logpxf = self._dist._logpxf_dispatch(x, *args, **params)
4702
+ return logpxf - np.log(np.abs(scale))
4703
+
4704
+ def _pxf_dispatch(self, x, *args, loc, scale, sign, **params):
4705
+ x = self._transform(x, loc, scale)
4706
+ pxf = self._dist._pxf_dispatch(x, *args, **params)
4707
+ return pxf / np.abs(scale)
4708
+
4050
4709
  # Sorry about the magic. This is just a draft to show the behavior.
4051
4710
  @_shift_scale_distribution_function
4052
4711
  def _logcdf_dispatch(self, x, *, method=None, **params):
@@ -4124,10 +4783,9 @@ class ShiftedScaledDistribution(TransformedDistribution):
4124
4783
  return self._moment_transform_center(
4125
4784
  order, raw_moments, loc, self._zero)
4126
4785
 
4127
- def _sample_dispatch(self, sample_shape, full_shape, *,
4786
+ def _sample_dispatch(self, full_shape, *,
4128
4787
  rng, loc, scale, sign, method, **params):
4129
- rvs = self._dist._sample_dispatch(
4130
- sample_shape, full_shape, method=method, rng=rng, **params)
4788
+ rvs = self._dist._sample_dispatch(full_shape, method=method, rng=rng, **params)
4131
4789
  return self._itransform(rvs, loc=loc, scale=scale, sign=sign, **params)
4132
4790
 
4133
4791
  def __add__(self, loc):
@@ -4213,12 +4871,12 @@ class OrderStatisticDistribution(TransformedDistribution):
4213
4871
 
4214
4872
  """
4215
4873
 
4216
- # These can be restricted to _IntegerDomain/_IntegerParameter in a separate
4874
+ # These can be restricted to _IntegerInterval/_IntegerParameter in a separate
4217
4875
  # PR if desired.
4218
- _r_domain = _RealDomain(endpoints=(1, 'n'), inclusive=(True, True))
4876
+ _r_domain = _RealInterval(endpoints=(1, 'n'), inclusive=(True, True))
4219
4877
  _r_param = _RealParameter('r', domain=_r_domain, typical=(1, 2))
4220
4878
 
4221
- _n_domain = _RealDomain(endpoints=(1, np.inf), inclusive=(True, True))
4879
+ _n_domain = _RealInterval(endpoints=(1, np.inf), inclusive=(True, True))
4222
4880
  _n_param = _RealParameter('n', domain=_n_domain, typical=(1, 4))
4223
4881
 
4224
4882
  _r_domain.define_parameters(_n_param)
@@ -4617,6 +5275,14 @@ class Mixture(_ProbabilityDistribution):
4617
5275
  self._raise_if_method(method)
4618
5276
  return self._logsum('logpdf', x)
4619
5277
 
5278
+ def pmf(self, x, /, *, method=None):
5279
+ self._raise_if_method(method)
5280
+ return self._sum('pmf', x)
5281
+
5282
+ def logpmf(self, x, /, *, method=None):
5283
+ self._raise_if_method(method)
5284
+ return self._logsum('logpmf', x)
5285
+
4620
5286
  def cdf(self, x, y=None, /, *, method=None):
4621
5287
  self._raise_if_method(method)
4622
5288
  args = (x,) if y is None else (x, y)
@@ -4814,10 +5480,8 @@ class MonotonicTransformedDistribution(TransformedDistribution):
4814
5480
  def _iccdf_dispatch(self, p, *args, **params):
4815
5481
  return self._g(self._icxdf(p, *args, **params))
4816
5482
 
4817
- def _sample_dispatch(self, sample_shape, full_shape, *,
4818
- method, rng, **params):
4819
- rvs = self._dist._sample_dispatch(
4820
- sample_shape, full_shape, method=method, rng=rng, **params)
5483
+ def _sample_dispatch(self, full_shape, *, method, rng, **params):
5484
+ rvs = self._dist._sample_dispatch(full_shape, method=method, rng=rng, **params)
4821
5485
  return self._g(rvs)
4822
5486
 
4823
5487
 
@@ -4904,7 +5568,7 @@ class FoldedDistribution(TransformedDistribution):
4904
5568
  a, b = self._dist._support(**params)
4905
5569
  xl = np.maximum(-x, a)
4906
5570
  xr = np.minimum(x, b)
4907
- return self._dist._logccdf2_dispatch(xl, xr, *args, method=method,
5571
+ return self._dist._logccdf2_dispatch(xl, xr, *args, method=method,
4908
5572
  **params).real
4909
5573
 
4910
5574
  def _ccdf_dispatch(self, x, *args, method=None, **params):
@@ -4914,10 +5578,8 @@ class FoldedDistribution(TransformedDistribution):
4914
5578
  xr = np.minimum(x, b)
4915
5579
  return self._dist._ccdf2_dispatch(xl, xr, *args, method=method, **params)
4916
5580
 
4917
- def _sample_dispatch(self, sample_shape, full_shape, *,
4918
- method, rng, **params):
4919
- rvs = self._dist._sample_dispatch(
4920
- sample_shape, full_shape, method=method, rng=rng, **params)
5581
+ def _sample_dispatch(self, full_shape, *, method, rng, **params):
5582
+ rvs = self._dist._sample_dispatch(full_shape, method=method, rng=rng, **params)
4921
5583
  return np.abs(rvs)
4922
5584
 
4923
5585
  def __repr__(self):