Skip to content

Commit df32f30

Browse files
authored
Merge pull request #189 from IntelPython/fix/method-validation-and-empty-mean
fix: validate the sampling method and reject an empty mean in multinormal_cholesky
2 parents 183a62e + 6a369f8 commit df32f30

4 files changed

Lines changed: 143 additions & 22 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
* Added a thread-safety section to the how-to guide for free-threaded Python [gh-159](https://github.com/IntelPython/mkl_random/pull/159)
1313
* Added an [ASV](https://asv.readthedocs.io/en/stable/) benchmark suite under `benchmarks/` and a `benchmark` optional dependency group [gh-184](https://github.com/IntelPython/mkl_random/pull/184)
1414
* Enabled support of Python 3.15 [gh-188](https://github.com/IntelPython/mkl_random/pull/188)
15+
* Exported the sampling method ids `ICDF`, `BOXMULLER`, `BOXMULLER2`, `POISNORM` and `PTPE` from `mkl_random`, as the `method` docstrings already documented [gh-189](https://github.com/IntelPython/mkl_random/pull/189)
1516

1617
### Changed
18+
* Unsupported `method` values (e.g. a misspelled name, a method the function does not implement, `None`, `bool`, `float`) now emit a `UserWarning` listing the supported methods before falling back to the function's default method, instead of falling back silently [gh-189](https://github.com/IntelPython/mkl_random/pull/189)
1719
* Sped up `normal`, `uniform`, `exponential`, `laplace`, `gumbel`, `logistic`, `rayleigh` and `lognormal` for array-valued parameters. Seeded results change for these array paths; scalar paths are unchanged [gh-171](https://github.com/IntelPython/mkl_random/pull/171)
1820
* Array parameters for these distributions must broadcast to the requested `size` without adding dimensions; previously accepted mismatches now raise `ValueError` [gh-171](https://github.com/IntelPython/mkl_random/pull/171)
1921
* `uniform` with array-valued bounds may return `high` due to floating-point rounding [gh-171](https://github.com/IntelPython/mkl_random/pull/171)
@@ -28,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2830
* Reduced the memory use and sped up integer generation with `WH`, `MCG31`, `R250` and `MRG32K3A`, which lack `viRngUniformBits` support [gh-178](https://github.com/IntelPython/mkl_random/pull/178)
2931

3032
### Fixed
33+
* Fixed `multinormal_cholesky` and the other functions taking `method` routing integer-like ids such as `np.int64(0)`, `np.int64(2)` or `False` to `BoxMuller`; NumPy integers now select the method they name [gh-189](https://github.com/IntelPython/mkl_random/pull/189)
34+
* Fixed `multinormal_cholesky` raising `ZeroDivisionError` for an empty `mean`; it now raises `ValueError` [gh-189](https://github.com/IntelPython/mkl_random/pull/189)
3135
* Fixed an out-of-range integer `brng` indexing `brng_list` past its end, which returned uninitialized memory as random values; it now warns and falls back to `MT19937` [gh-177](https://github.com/IntelPython/mkl_random/pull/177)
3236
* Fixed `brng=0` being treated as unset, which left the state unseeded [gh-177](https://github.com/IntelPython/mkl_random/pull/177)
3337
* Fixed `MKLRandomState(seed, brng=None)` reading past `brng_list` and returning uninitialized memory; it now uses `MT19937` [gh-177](https://github.com/IntelPython/mkl_random/pull/177)

‎mkl_random/__init__.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
from . import _init_helper
2929
from ._version import __version__
3030
from .mklrand import (
31+
BOXMULLER,
32+
BOXMULLER2,
33+
ICDF,
34+
POISNORM,
35+
PTPE,
3136
MKLRandomState,
3237
RandomState,
3338
beta,
@@ -105,6 +110,11 @@
105110
__all__ = [
106111
"MKLRandomState",
107112
"RandomState",
113+
"ICDF",
114+
"BOXMULLER",
115+
"BOXMULLER2",
116+
"POISNORM",
117+
"PTPE",
108118
"seed",
109119
"get_state",
110120
"set_state",

‎mkl_random/mklrand.pyx‎

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,18 +1650,51 @@ _method_alias_dict_poisson = {
16501650
}
16511651

16521652

1653+
_method_names = {
1654+
ICDF: "ICDF",
1655+
BOXMULLER: "BoxMuller",
1656+
BOXMULLER2: "BoxMuller2",
1657+
POISNORM: "POISNORM",
1658+
PTPE: "PTPE",
1659+
}
1660+
1661+
16531662
def choose_method(method, mlist, alias_dict = None):
1654-
if (method not in mlist):
1655-
if (alias_dict is None) or (not isinstance(alias_dict, dict)):
1656-
# issue warning
1657-
return mlist[0]
1658-
else:
1659-
if method not in alias_dict.keys():
1660-
return mlist[0]
1661-
else:
1662-
return alias_dict[method]
1663-
else:
1664-
return method
1663+
"""
1664+
Resolve ``method`` to one of the module-level method constants in
1665+
``mlist``. Callers compare the result with ``is``, so the constant itself
1666+
is returned, never an equal-valued object such as ``np.int64(2)``.
1667+
Unsupported values, including ``bool`` and ``float``, which compare
1668+
equal to the integer constants, warn with the list of supported methods
1669+
and fall back to ``mlist[0]``.
1670+
"""
1671+
for candidate in mlist:
1672+
# the constants themselves, including the default arguments
1673+
if candidate is method:
1674+
return candidate
1675+
1676+
index = None
1677+
if isinstance(method, str):
1678+
if isinstance(alias_dict, dict):
1679+
index = alias_dict.get(method)
1680+
elif not isinstance(method, (bool, np.bool_)):
1681+
try:
1682+
index = operator.index(method)
1683+
except TypeError:
1684+
pass
1685+
for candidate in mlist:
1686+
if candidate == index:
1687+
return candidate
1688+
1689+
names = [_method_names.get(m, m) for m in mlist]
1690+
supported = ", ".join(f"\"{name}\"" for name in names)
1691+
warnings.warn(
1692+
f"The sampling method {method!r} is not supported. "
1693+
f"Supported methods are {supported}; \"{names[0]}\" "
1694+
"will be used instead",
1695+
UserWarning
1696+
)
1697+
return mlist[0]
16651698

16661699

16671700
_brng_dict = {
@@ -3197,7 +3230,7 @@ cdef class _MKLRandomState:
31973230
Output shape. If the given shape is, e.g., ``(m, n, k)``, then
31983231
``m * n * k`` samples are drawn. Default is None, in which case a
31993232
single value is returned.
3200-
method : 'ICDF, 'BoxMuller', 'BoxMuller2', optional
3233+
method : 'ICDF', 'BoxMuller', 'BoxMuller2', optional
32013234
Sampling method used by Intel MKL. Can also be specified using
32023235
tokens mkl_random.ICDF, mkl_random.BOXMULLER, mkl_random.BOXMULLER2
32033236
@@ -3270,7 +3303,7 @@ cdef class _MKLRandomState:
32703303
Output shape. If the given shape is, e.g., ``(m, n, k)``, then
32713304
``m * n * k`` samples are drawn. Default is None, in which case a
32723305
single value is returned.
3273-
method : 'ICDF, 'BoxMuller', 'BoxMuller2', optional
3306+
method : 'ICDF', 'BoxMuller', 'BoxMuller2', optional
32743307
Sampling method used by Intel MKL. Can also be specified using
32753308
tokens mkl_random.ICDF, mkl_random.BOXMULLER, mkl_random.BOXMULLER2
32763309
@@ -5258,7 +5291,7 @@ cdef class _MKLRandomState:
52585291
Output shape. If the given shape is, e.g., ``(m, n, k)``, then
52595292
``m * n * k`` samples are drawn. Default is None, in which case a
52605293
single value is returned.
5261-
method : 'ICDF, 'BoxMuller', optional
5294+
method : 'ICDF', 'BoxMuller', optional
52625295
Sampling method used by Intel MKL. Can also be specified using
52635296
tokens mkl_random.ICDF, mkl_random.BOXMULLER
52645297
@@ -5959,7 +5992,7 @@ cdef class _MKLRandomState:
59595992
Output shape. If the given shape is, e.g., ``(m, n, k)``, then
59605993
``m * n * k`` samples are drawn. Default is None, in which case a
59615994
single value is returned.
5962-
method : 'POISNORM, 'PTPE', optional
5995+
method : 'POISNORM', 'PTPE', optional
59635996
Sampling method used by Intel MKL. Can also be specified using
59645997
tokens mkl_random.POISNORM, mkl_random.PTPE
59655998
@@ -6693,9 +6726,6 @@ cdef class _MKLRandomState:
66936726
Output shape. If the given shape is, e.g., ``(m, n, k)``, then
66946727
``m * n * k`` samples are drawn. Default is None, in which case a
66956728
single value is returned.
6696-
method : 'ICDF, 'BoxMuller', 'BoxMuller2', optional
6697-
Sampling method used by Intel MKL. Can also be specified using
6698-
tokens mkl_random.ICDF, mkl_random.BOXMULLER, mkl_random.BOXMULLER2
66996729
67006730
Returns
67016731
-------
@@ -7308,7 +7338,7 @@ cdef class MKLRandomState(_MKLRandomState):
73087338
generated, and packed in an `m`-by-`n`-by-`k` arrangement. Because
73097339
each sample is `N`-dimensional, the output shape is ``(m,n,k,N)``.
73107340
If no shape is specified, a single (`N`-D) sample is returned.
7311-
method : 'ICDF, 'BoxMuller', 'BoxMuller2', optional
7341+
method : 'ICDF', 'BoxMuller', 'BoxMuller2', optional
73127342
Sampling method used by Intel MKL. Can also be specified using
73137343
tokens mkl_random.ICDF, mkl_random.BOXMULLER, mkl_random.BOXMULLER2
73147344
@@ -7409,6 +7439,8 @@ cdef class MKLRandomState(_MKLRandomState):
74097439
if marr.ndim != 1:
74107440
raise ValueError("mean must be 1 dimensional")
74117441
dim = marr.shape[0]
7442+
if dim < 1:
7443+
raise ValueError("mean must have at least one element")
74127444
if (tarr.ndim == 2):
74137445
storage_mode = MATRIX
74147446
if (tarr.shape[0] != tarr.shape[1]):

‎mkl_random/tests/test_random.py‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,9 +1001,13 @@ def test_randomdist_lognormal(randomdist):
10011001
]
10021002
)
10031003
np.testing.assert_allclose(actual, desired, atol=1e-6, rtol=1e-10)
1004-
actual = rnd.lognormal(
1005-
mean=0.123456789, sigma=2.0, size=(3, 2), method="Box-Muller2"
1006-
)
1004+
# lognormal only supports ICDF and BoxMuller, so this falls back to ICDF
1005+
with pytest.warns(
1006+
UserWarning, match='Supported methods are "ICDF", "BoxMuller";'
1007+
):
1008+
actual = rnd.lognormal(
1009+
mean=0.123456789, sigma=2.0, size=(3, 2), method="Box-Muller2"
1010+
)
10071011
desired = np.array(
10081012
[
10091013
[0.2585388231094821, 0.43734953048924663],
@@ -1529,6 +1533,77 @@ def test_normal_family_array_methods(name, method, normal_method, size):
15291533
)
15301534

15311535

1536+
@pytest.mark.parametrize("ch", [np.zeros((0, 0)), np.zeros(0)])
1537+
def test_multinormal_cholesky_empty_mean(ch):
1538+
with pytest.raises(ValueError, match="at least one element"):
1539+
rnd.MKLRandomState(123).multinormal_cholesky(np.array([]), ch, size=3)
1540+
1541+
1542+
@pytest.mark.parametrize("method", ["ICDF", "BoxMuller", "BoxMuller2"])
1543+
@pytest.mark.parametrize("to_int", [int, np.int64, np.uint8])
1544+
def test_multinormal_cholesky_integer_method(method, to_int):
1545+
# Integer-like method ids must select the same method as the name.
1546+
ids = {"ICDF": 0, "BoxMuller": 1, "BoxMuller2": 2}
1547+
mean = np.array([0.1, -0.2])
1548+
chol_mat = np.array([[1.0, 0.0], [-0.5, 1.0]])
1549+
expected = rnd.MKLRandomState(123).multinormal_cholesky(
1550+
mean, chol_mat, size=8, method=method
1551+
)
1552+
with warnings.catch_warnings():
1553+
warnings.simplefilter("error")
1554+
actual = rnd.MKLRandomState(123).multinormal_cholesky(
1555+
mean, chol_mat, size=8, method=to_int(ids[method])
1556+
)
1557+
np.testing.assert_array_equal(actual, expected)
1558+
1559+
1560+
@pytest.mark.parametrize(
1561+
"method", ["bogus", 7, -1, 1.0, None, False, True, [1]]
1562+
)
1563+
def test_unsupported_method_warns_and_uses_default(method):
1564+
# bool and float compare equal to the integer method ids, but are not
1565+
# valid method specifications.
1566+
mean = np.array([0.1, -0.2])
1567+
chol_mat = np.array([[1.0, 0.0], [-0.5, 1.0]])
1568+
expected = rnd.MKLRandomState(123).multinormal_cholesky(
1569+
mean, chol_mat, size=8, method="ICDF"
1570+
)
1571+
with pytest.warns(UserWarning, match="not supported"):
1572+
actual = rnd.MKLRandomState(123).multinormal_cholesky(
1573+
mean, chol_mat, size=8, method=method
1574+
)
1575+
np.testing.assert_array_equal(actual, expected)
1576+
1577+
1578+
def test_unsupported_method_uses_each_functions_default():
1579+
expected = rnd.MKLRandomState(123).poisson(3.0, size=8, method="POISNORM")
1580+
with pytest.warns(UserWarning, match='"POISNORM" will be used instead'):
1581+
actual = rnd.MKLRandomState(123).poisson(3.0, size=8, method="bogus")
1582+
np.testing.assert_array_equal(actual, expected)
1583+
1584+
1585+
@pytest.mark.parametrize(
1586+
"token,func,name",
1587+
[
1588+
("ICDF", "standard_normal", "ICDF"),
1589+
("BOXMULLER", "standard_normal", "BoxMuller"),
1590+
("BOXMULLER2", "standard_normal", "BoxMuller2"),
1591+
("POISNORM", "poisson", "POISNORM"),
1592+
("PTPE", "poisson", "PTPE"),
1593+
],
1594+
)
1595+
def test_method_tokens_are_exported(token, func, name):
1596+
# The docstrings refer to the method ids as mkl_random.ICDF etc.
1597+
assert token in rnd.__all__
1598+
expected = getattr(rnd.MKLRandomState(123), func)(size=8, method=name)
1599+
with warnings.catch_warnings():
1600+
warnings.simplefilter("error")
1601+
actual = getattr(rnd.MKLRandomState(123), func)(
1602+
size=8, method=getattr(rnd, token)
1603+
)
1604+
np.testing.assert_array_equal(actual, expected)
1605+
1606+
15321607
@pytest.mark.parametrize(
15331608
"name,draw,p",
15341609
[

0 commit comments

Comments
 (0)