From 52ecd5a97ce3ff4dad72935b8bf67ba2cfb6908e Mon Sep 17 00:00:00 2001 From: Kevin Sheppard Date: Fri, 4 Dec 2020 23:03:23 +0000 Subject: [PATCH 1/2] BUG: Enforce high >= low on uniform number generators Check that high is weakly larger than low and raise if now closes #17905 --- numpy/random/_generator.pyx | 15 ++++++++------- numpy/random/mtrand.pyx | 15 ++++++++------- numpy/random/tests/test_generator_mt19937.py | 6 ++++++ numpy/random/tests/test_random.py | 6 ++++++ 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/numpy/random/_generator.pyx b/numpy/random/_generator.pyx index 7ffa367751aa..cd951526bfe0 100644 --- a/numpy/random/_generator.pyx +++ b/numpy/random/_generator.pyx @@ -859,7 +859,8 @@ cdef class Generator: greater than or equal to low. The default value is 0. high : float or array_like of floats Upper boundary of the output interval. All values generated will be - less than high. The default value is 1.0. + less than high. The default value is 1.0. high - low must be + non-negative. size : int or tuple of ints, optional Output shape. If the given shape is, e.g., ``(m, n, k)``, then ``m * n * k`` samples are drawn. If size is ``None`` (default), @@ -914,7 +915,7 @@ cdef class Generator: """ cdef bint is_scalar = True cdef np.ndarray alow, ahigh, arange - cdef double _low, _high, range + cdef double _low, _high, rng cdef object temp alow = np.PyArray_FROM_OTF(low, np.NPY_DOUBLE, np.NPY_ALIGNED) @@ -923,13 +924,13 @@ cdef class Generator: if np.PyArray_NDIM(alow) == np.PyArray_NDIM(ahigh) == 0: _low = PyFloat_AsDouble(low) _high = PyFloat_AsDouble(high) - range = _high - _low - if not np.isfinite(range): - raise OverflowError('Range exceeds valid bounds') + rng = _high - _low + if not np.isfinite(rng): + raise OverflowError('high - low range exceeds valid bounds') return cont(&random_uniform, &self._bitgen, size, self.lock, 2, _low, '', CONS_NONE, - range, '', CONS_NONE, + rng, 'high - low', CONS_NON_NEGATIVE, 0.0, '', CONS_NONE, None) @@ -943,7 +944,7 @@ cdef class Generator: raise OverflowError('Range exceeds valid bounds') return cont(&random_uniform, &self._bitgen, size, self.lock, 2, alow, '', CONS_NONE, - arange, '', CONS_NONE, + arange, 'high - low', CONS_NON_NEGATIVE, 0.0, '', CONS_NONE, None) diff --git a/numpy/random/mtrand.pyx b/numpy/random/mtrand.pyx index d43e7f5aa6d9..7f4fe1c3c89d 100644 --- a/numpy/random/mtrand.pyx +++ b/numpy/random/mtrand.pyx @@ -1026,7 +1026,8 @@ cdef class RandomState: greater than or equal to low. The default value is 0. high : float or array_like of floats Upper boundary of the output interval. All values generated will be - less than or equal to high. The default value is 1.0. + less than or equal to high. The default value is 1.0. high - low must be + non-negative. size : int or tuple of ints, optional Output shape. If the given shape is, e.g., ``(m, n, k)``, then ``m * n * k`` samples are drawn. If size is ``None`` (default), @@ -1095,7 +1096,7 @@ cdef class RandomState: """ cdef bint is_scalar = True cdef np.ndarray alow, ahigh, arange - cdef double _low, _high, range + cdef double _low, _high, rng cdef object temp alow = np.PyArray_FROM_OTF(low, np.NPY_DOUBLE, np.NPY_ALIGNED) @@ -1104,13 +1105,13 @@ cdef class RandomState: if np.PyArray_NDIM(alow) == np.PyArray_NDIM(ahigh) == 0: _low = PyFloat_AsDouble(low) _high = PyFloat_AsDouble(high) - range = _high - _low - if not np.isfinite(range): - raise OverflowError('Range exceeds valid bounds') + rng = _high - _low + if not np.isfinite(rng): + raise OverflowError('High - low range exceeds valid bounds') return cont(&random_uniform, &self._bitgen, size, self.lock, 2, _low, '', CONS_NONE, - range, '', CONS_NONE, + rng, 'high - low', CONS_NON_NEGATIVE, 0.0, '', CONS_NONE, None) @@ -1123,7 +1124,7 @@ cdef class RandomState: raise OverflowError('Range exceeds valid bounds') return cont(&random_uniform, &self._bitgen, size, self.lock, 2, alow, '', CONS_NONE, - arange, '', CONS_NONE, + arange, 'high - low', CONS_NON_NEGATIVE, 0.0, '', CONS_NONE, None) diff --git a/numpy/random/tests/test_generator_mt19937.py b/numpy/random/tests/test_generator_mt19937.py index b69cd38d4a4a..4b534fcece75 100644 --- a/numpy/random/tests/test_generator_mt19937.py +++ b/numpy/random/tests/test_generator_mt19937.py @@ -1666,6 +1666,12 @@ def test_uniform_range_bounds(self): # DBL_MAX by increasing fmin a bit random.uniform(low=np.nextafter(fmin, 1), high=fmax / 1e17) + def test_uniform_neg_range(self): + func = random.uniform + assert_raises(ValueError, func, 2, 1) + assert_raises(ValueError, func, [1, 2], [1, 1]) + assert_raises(ValueError, func, [[0, 1],[2, 3]], 2) + def test_scalar_exception_propagation(self): # Tests that exceptions are correctly propagated in distributions # when called with objects that throw exceptions when converted to diff --git a/numpy/random/tests/test_random.py b/numpy/random/tests/test_random.py index c13fc39e3339..473ca08e405d 100644 --- a/numpy/random/tests/test_random.py +++ b/numpy/random/tests/test_random.py @@ -916,6 +916,12 @@ def test_uniform_range_bounds(self): # account for i386 extended precision DBL_MAX / 1e17 + DBL_MAX > # DBL_MAX by increasing fmin a bit np.random.uniform(low=np.nextafter(fmin, 1), high=fmax / 1e17) + + def test_uniform_neg_range(self): + func = np.random.uniform + assert_raises(ValueError, func, 2, 1) + assert_raises(ValueError, func, [1, 2], [1, 1]) + assert_raises(ValueError, func, [[0, 1],[2, 3]], 2) def test_scalar_exception_propagation(self): # Tests that exceptions are correctly propagated in distributions From a3bb19df580454a6b98c34e29a00c271c2e411af Mon Sep 17 00:00:00 2001 From: Kevin Sheppard Date: Sat, 5 Dec 2020 21:58:09 +0000 Subject: [PATCH 2/2] Revert changes to mtrand This doesn't qualify for fixing under the NEP. --- .../upcoming_changes/17921.compatibility.rst | 6 ++++++ numpy/random/_generator.pyx | 8 ++------ numpy/random/mtrand.pyx | 15 +++++++-------- numpy/random/tests/test_generator_mt19937.py | 9 +++++++++ numpy/random/tests/test_random.py | 6 ------ 5 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 doc/release/upcoming_changes/17921.compatibility.rst diff --git a/doc/release/upcoming_changes/17921.compatibility.rst b/doc/release/upcoming_changes/17921.compatibility.rst new file mode 100644 index 000000000000..a1e2fb2d0408 --- /dev/null +++ b/doc/release/upcoming_changes/17921.compatibility.rst @@ -0,0 +1,6 @@ +Validate input values in ``Generator.uniform`` +---------------------------------------------- +Checked that ``high - low >= 0`` in ``np.random.Generator.uniform``. Raises +``ValueError`` if ``low > high``. Previously out-of-order inputs were accepted +and silently swapped, so that if ``low > high``, the value generated was +``high + (low - high) * random()``. diff --git a/numpy/random/_generator.pyx b/numpy/random/_generator.pyx index cd951526bfe0..e00bc4d989d5 100644 --- a/numpy/random/_generator.pyx +++ b/numpy/random/_generator.pyx @@ -859,8 +859,8 @@ cdef class Generator: greater than or equal to low. The default value is 0. high : float or array_like of floats Upper boundary of the output interval. All values generated will be - less than high. The default value is 1.0. high - low must be - non-negative. + less than high. high - low must be non-negative. The default value + is 1.0. size : int or tuple of ints, optional Output shape. If the given shape is, e.g., ``(m, n, k)``, then ``m * n * k`` samples are drawn. If size is ``None`` (default), @@ -886,10 +886,6 @@ cdef class Generator: anywhere within the interval ``[a, b)``, and zero elsewhere. When ``high`` == ``low``, values of ``low`` will be returned. - If ``high`` < ``low``, the results are officially undefined - and may eventually raise an error, i.e. do not rely on this - function to behave when passed arguments satisfying that - inequality condition. Examples -------- diff --git a/numpy/random/mtrand.pyx b/numpy/random/mtrand.pyx index 7f4fe1c3c89d..d43e7f5aa6d9 100644 --- a/numpy/random/mtrand.pyx +++ b/numpy/random/mtrand.pyx @@ -1026,8 +1026,7 @@ cdef class RandomState: greater than or equal to low. The default value is 0. high : float or array_like of floats Upper boundary of the output interval. All values generated will be - less than or equal to high. The default value is 1.0. high - low must be - non-negative. + less than or equal to high. The default value is 1.0. size : int or tuple of ints, optional Output shape. If the given shape is, e.g., ``(m, n, k)``, then ``m * n * k`` samples are drawn. If size is ``None`` (default), @@ -1096,7 +1095,7 @@ cdef class RandomState: """ cdef bint is_scalar = True cdef np.ndarray alow, ahigh, arange - cdef double _low, _high, rng + cdef double _low, _high, range cdef object temp alow = np.PyArray_FROM_OTF(low, np.NPY_DOUBLE, np.NPY_ALIGNED) @@ -1105,13 +1104,13 @@ cdef class RandomState: if np.PyArray_NDIM(alow) == np.PyArray_NDIM(ahigh) == 0: _low = PyFloat_AsDouble(low) _high = PyFloat_AsDouble(high) - rng = _high - _low - if not np.isfinite(rng): - raise OverflowError('High - low range exceeds valid bounds') + range = _high - _low + if not np.isfinite(range): + raise OverflowError('Range exceeds valid bounds') return cont(&random_uniform, &self._bitgen, size, self.lock, 2, _low, '', CONS_NONE, - rng, 'high - low', CONS_NON_NEGATIVE, + range, '', CONS_NONE, 0.0, '', CONS_NONE, None) @@ -1124,7 +1123,7 @@ cdef class RandomState: raise OverflowError('Range exceeds valid bounds') return cont(&random_uniform, &self._bitgen, size, self.lock, 2, alow, '', CONS_NONE, - arange, 'high - low', CONS_NON_NEGATIVE, + arange, '', CONS_NONE, 0.0, '', CONS_NONE, None) diff --git a/numpy/random/tests/test_generator_mt19937.py b/numpy/random/tests/test_generator_mt19937.py index 4b534fcece75..c4fb5883c925 100644 --- a/numpy/random/tests/test_generator_mt19937.py +++ b/numpy/random/tests/test_generator_mt19937.py @@ -1666,6 +1666,15 @@ def test_uniform_range_bounds(self): # DBL_MAX by increasing fmin a bit random.uniform(low=np.nextafter(fmin, 1), high=fmax / 1e17) + def test_uniform_zero_range(self): + func = random.uniform + result = func(1.5, 1.5) + assert_allclose(result, 1.5) + result = func([0.0, np.pi], [0.0, np.pi]) + assert_allclose(result, [0.0, np.pi]) + result = func([[2145.12], [2145.12]], [2145.12, 2145.12]) + assert_allclose(result, 2145.12 + np.zeros((2, 2))) + def test_uniform_neg_range(self): func = random.uniform assert_raises(ValueError, func, 2, 1) diff --git a/numpy/random/tests/test_random.py b/numpy/random/tests/test_random.py index 473ca08e405d..c13fc39e3339 100644 --- a/numpy/random/tests/test_random.py +++ b/numpy/random/tests/test_random.py @@ -916,12 +916,6 @@ def test_uniform_range_bounds(self): # account for i386 extended precision DBL_MAX / 1e17 + DBL_MAX > # DBL_MAX by increasing fmin a bit np.random.uniform(low=np.nextafter(fmin, 1), high=fmax / 1e17) - - def test_uniform_neg_range(self): - func = np.random.uniform - assert_raises(ValueError, func, 2, 1) - assert_raises(ValueError, func, [1, 2], [1, 1]) - assert_raises(ValueError, func, [[0, 1],[2, 3]], 2) def test_scalar_exception_propagation(self): # Tests that exceptions are correctly propagated in distributions