diff --git a/randomstate/randomstate.pyx b/randomstate/randomstate.pyx index 3aa8b12a..183e1dce 100644 --- a/randomstate/randomstate.pyx +++ b/randomstate/randomstate.pyx @@ -949,33 +949,42 @@ cdef class RandomState: key = np.dtype(dtype).name if not key in _randint_type: raise TypeError('Unsupported dtype "%s" for randint' % key) + lowbnd, highbnd = _randint_type[key] - if low < lowbnd: + # TODO: Do not cast these inputs to Python int + # + # This is a workaround until gh-8851 is resolved (bug in NumPy + # integer comparison and subtraction involving uint64 and non- + # uint64). Afterwards, remove these two lines. + ilow = int(low) + ihigh = int(high) + + if ilow < lowbnd: raise ValueError("low is out of bounds for %s" % (key,)) - if high > highbnd: + if ihigh > highbnd: raise ValueError("high is out of bounds for %s" % (key,)) - if low >= high: + if ilow >= ihigh: raise ValueError("low >= high") if key == 'int32': - ret = _rand_int32(low, high - 1, size, &self.rng_state, self.lock) + ret = _rand_int32(ilow, ihigh - 1, size, &self.rng_state, self.lock) elif key == 'int64': - ret = _rand_int64(low, high - 1, size, &self.rng_state, self.lock) + ret = _rand_int64(ilow, ihigh - 1, size, &self.rng_state, self.lock) elif key == 'int16': - ret = _rand_int16(low, high - 1, size, &self.rng_state, self.lock) + ret = _rand_int16(ilow, ihigh - 1, size, &self.rng_state, self.lock) elif key == 'int8': - ret = _rand_int8(low, high - 1, size, &self.rng_state, self.lock) + ret = _rand_int8(ilow, ihigh - 1, size, &self.rng_state, self.lock) elif key == 'uint64': - ret = _rand_uint64(low, high - 1, size, &self.rng_state, self.lock) + ret = _rand_uint64(ilow, ihigh - 1, size, &self.rng_state, self.lock) elif key == 'uint32': - ret = _rand_uint32(low, high - 1, size, &self.rng_state, self.lock) + ret = _rand_uint32(ilow, ihigh - 1, size, &self.rng_state, self.lock) elif key == 'uint16': - ret = _rand_uint16(low, high - 1, size, &self.rng_state, self.lock) + ret = _rand_uint16(ilow, ihigh - 1, size, &self.rng_state, self.lock) elif key == 'uint8': - ret = _rand_uint8(low, high - 1, size, &self.rng_state, self.lock) + ret = _rand_uint8(ilow, ihigh - 1, size, &self.rng_state, self.lock) elif key == 'bool': - ret = _rand_bool(low, high - 1, size, &self.rng_state, self.lock) + ret = _rand_bool(ilow, ihigh - 1, size, &self.rng_state, self.lock) if size is None: if dtype in (np.bool, np.int, np.long): @@ -1021,7 +1030,7 @@ cdef class RandomState: ---------- a : 1-D array-like or int If an ndarray, a random sample is generated from its elements. - If an int, the random sample is generated as if a was np.arange(n) + If an int, the random sample is generated as if a were np.arange(a) 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. Default is None, in which case a @@ -1035,7 +1044,7 @@ cdef class RandomState: Returns ------- - samples : 1-D ndarray, shape (size,) + samples : single item or ndarray The generated random samples Raises @@ -4094,8 +4103,8 @@ cdef class RandomState: Instead of specifying the full covariance matrix, popular approximations include: - - Spherical covariance (*cov* is a multiple of the identity matrix) - - Diagonal covariance (*cov* has non-negative elements, and only on + - Spherical covariance (`cov` is a multiple of the identity matrix) + - Diagonal covariance (`cov` has non-negative elements, and only on the diagonal) This geometrical property can be seen in two dimensions by plotting @@ -4507,6 +4516,7 @@ cdef class RandomState: with self.lock: for i in reversed(range(1, n)): j = random_interval(&self.rng_state, i) + if i == j : continue # i == j is not needed and memcpy is undefined. buf[...] = x[j] x[j] = x[i] x[i] = buf diff --git a/randomstate/tests/test_numpy_mt19937.py b/randomstate/tests/test_numpy_mt19937.py index 8abf7b08..841fd7e8 100644 --- a/randomstate/tests/test_numpy_mt19937.py +++ b/randomstate/tests/test_numpy_mt19937.py @@ -158,22 +158,42 @@ def test_rng_zero_and_extremes(self): for dt in self.itype: lbnd = 0 if dt is np.bool_ else np.iinfo(dt).min ubnd = 2 if dt is np.bool_ else np.iinfo(dt).max + 1 + tgt = ubnd - 1 assert_equal(self.rfunc(tgt, tgt + 1, size=1000, dtype=dt), tgt) + tgt = lbnd assert_equal(self.rfunc(tgt, tgt + 1, size=1000, dtype=dt), tgt) + tgt = (lbnd + ubnd) // 2 assert_equal(self.rfunc(tgt, tgt + 1, size=1000, dtype=dt), tgt) + def test_full_range(self): + # Test for ticket #1690 + + for dt in self.itype: + lbnd = 0 if dt is np.bool_ else np.iinfo(dt).min + ubnd = 2 if dt is np.bool_ else np.iinfo(dt).max + 1 + + try: + self.rfunc(lbnd, ubnd, dtype=dt) + except Exception as e: + raise AssertionError("No error should have been raised, " + "but one was with the following " + "message:\n\n%s" % str(e)) + def test_in_bounds_fuzz(self): # Don't use fixed seed mt19937.seed() + for dt in self.itype[1:]: for ubnd in [4, 8, 16]: vals = self.rfunc(2, ubnd, size=2**16, dtype=dt) assert_(vals.max() < ubnd) assert_(vals.min() >= 2) - vals = self.rfunc(0, 2, size=2**16, dtype=np.bool) + + vals = self.rfunc(0, 2, size=2 ** 16, dtype=np.bool_) + assert_(vals.max() < 2) assert_(vals.min() >= 0) @@ -210,6 +230,29 @@ def test_repeatability(self): res = hashlib.md5(val).hexdigest() assert_(tgt[np.dtype(np.bool).name] == res) + def test_int64_uint64_corner_case(self): + # When stored in Numpy arrays, `lbnd` is casted + # as np.int64, and `ubnd` is casted as np.uint64. + # Checking whether `lbnd` >= `ubnd` used to be + # done solely via direct comparison, which is incorrect + # because when Numpy tries to compare both numbers, + # it casts both to np.float64 because there is + # no integer superset of np.int64 and np.uint64. However, + # `ubnd` is too large to be represented in np.float64, + # causing it be round down to np.iinfo(np.int64).max, + # leading to a ValueError because `lbnd` now equals + # the new `ubnd`. + + dt = np.int64 + tgt = np.iinfo(np.int64).max + lbnd = np.int64(np.iinfo(np.int64).max) + ubnd = np.uint64(np.iinfo(np.int64).max + 1) + + # None of these function calls should + # generate a ValueError now. + actual = mt19937.randint(lbnd, ubnd, dtype=dt) + assert_equal(actual, tgt) + def test_respect_dtype_singleton(self): # See gh-7203 for dt in self.itype: diff --git a/randomstate/tests/test_numpy_mt19937_regressions.py b/randomstate/tests/test_numpy_mt19937_regressions.py index b10fe691..2a53f7ee 100644 --- a/randomstate/tests/test_numpy_mt19937_regressions.py +++ b/randomstate/tests/test_numpy_mt19937_regressions.py @@ -55,15 +55,6 @@ def test_permutation_longs(self): b = mt19937.permutation(long(12)) assert_array_equal(a, b) - def test_randint_range(self): - # Test for ticket #1690 - lmax = np.iinfo('l').max - lmin = np.iinfo('l').min - try: - mt19937.randint(lmin, lmax) - except: - raise AssertionError - def test_shuffle_mixed_dimension(self): # Test for trac ticket #2074 for t in [[1, 2, 3, None], diff --git a/setup.py b/setup.py index 306cd351..dcd72112 100644 --- a/setup.py +++ b/setup.py @@ -173,7 +173,11 @@ def is_pure(self): try: - subprocess.call(['pandoc', '--from=markdown', '--to=rst', '--output=README.rst', 'README.md']) + import os + readme_orig_time = os.path.getmtime('README.md') + readme_mod_time = os.path.getmtime('README.rst') + if readme_orig_time > readme_mod_time: + subprocess.call(['pandoc', '--from=markdown', '--to=rst', '--output=README.rst', 'README.md']) except: pass # Generate files and extensions