Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ff953f1
Add irk_rand_<type>_broadcast for randint array-like bounds
vlad-perevezentsev Sep 1, 2026
7ebcee0
Add broadcast path to randint for array-like low/high
vlad-perevezentsev Sep 1, 2026
79e6c2e
Fix flake8 D102 ignore to cover tests/*.py
vlad-perevezentsev Sep 1, 2026
36bc4b7
Add TestRandint with array-like bounds tests
vlad-perevezentsev Sep 1, 2026
6fba7b8
Unskip test_randint in test_numpy_random.py
vlad-perevezentsev Sep 1, 2026
293e5ed
Fix and cover high-1 overflow for narrow randint bounds
vlad-perevezentsev Sep 1, 2026
fca9f13
Optimize randint broadcast draws via Lemire's method
vlad-perevezentsev Sep 1, 2026
ec64973
Merge remote-tracking branch 'origin/master' into randint_array_like_…
vlad-perevezentsev Sep 1, 2026
79ce520
Add gh-168 to changelog
vlad-perevezentsev Sep 1, 2026
1b54bcb
Align randint examples with the others
vlad-perevezentsev Sep 2, 2026
ebae45a
Fix and cover randint OOB for int8/int16 array bounds
vlad-perevezentsev Sep 2, 2026
d514e61
Fix randint array bounds with size=() to match NumPy
vlad-perevezentsev Sep 2, 2026
7eac1b1
Speed up randint validation by checking original bounds
vlad-perevezentsev Sep 2, 2026
9df3c8a
Support all BRNGs in array-bounds randint via viRngUniform fallback
vlad-perevezentsev Sep 2, 2026
61ae313
Single-pass word assembly in BRNG fallback
vlad-perevezentsev Sep 2, 2026
482237d
Merge remote-tracking branch 'origin/master' into randint_array_like_…
vlad-perevezentsev Sep 2, 2026
f44003c
Add scalar full-range randint tests per BRNG (skip broken)
vlad-perevezentsev Sep 3, 2026
27ca6d3
Merge branch 'master' into randint_array_like_bounds
ndgrigorian Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ extend-ignore =
per-file-ignores =
mkl_random/__init__.py: F401
mkl_random/interfaces/__init__.py: F401
mkl_random/tests/*.py: D102
mkl_random/tests/**/*.py: D102

filename = *.py, *.pyx, *.pxi, *.pxd
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# [dev] (MM/DD/YYYY)

### Added
* Added support for `array_like` (broadcastable) `low`/`high` bounds in `randint` [gh-168](https://github.com/IntelPython/mkl_random/pull/168)

### Changed
* Pinned Cython in the Coverity Scan workflow so generated code stays stable between scans, and added `coverity/README.md` documenting the known Cython-boilerplate false positives and the scan review checklist [gh-164](https://github.com/IntelPython/mkl_random/pull/164)
Expand Down
315 changes: 279 additions & 36 deletions mkl_random/mklrand.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,70 @@ cdef extern from "mkl_distributions.h":
cnp.npy_int64 high
) noexcept nogil

void irk_rand_bool_broadcast(
irk_state *state,
cnp.npy_intp len,
cnp.npy_bool *res,
const cnp.npy_bool *low,
const cnp.npy_bool *high
) noexcept nogil
void irk_rand_uint8_broadcast(
irk_state *state,
cnp.npy_intp len,
cnp.npy_uint8 *res,
const cnp.npy_uint8 *low,
const cnp.npy_uint8 *high
) noexcept nogil
void irk_rand_int8_broadcast(
irk_state *state,
cnp.npy_intp len,
cnp.npy_int8 *res,
const cnp.npy_int8 *low,
const cnp.npy_int8 *high
) noexcept nogil
void irk_rand_uint16_broadcast(
irk_state *state,
cnp.npy_intp len,
cnp.npy_uint16 *res,
const cnp.npy_uint16 *low,
const cnp.npy_uint16 *high
) noexcept nogil
void irk_rand_int16_broadcast(
irk_state *state,
cnp.npy_intp len,
cnp.npy_int16 *res,
const cnp.npy_int16 *low,
const cnp.npy_int16 *high
) noexcept nogil
void irk_rand_uint32_broadcast(
irk_state *state,
cnp.npy_intp len,
cnp.npy_uint32 *res,
const cnp.npy_uint32 *low,
const cnp.npy_uint32 *high
) noexcept nogil
void irk_rand_int32_broadcast(
irk_state *state,
cnp.npy_intp len,
cnp.npy_int32 *res,
const cnp.npy_int32 *low,
const cnp.npy_int32 *high
) noexcept nogil
void irk_rand_uint64_broadcast(
irk_state *state,
cnp.npy_intp len,
cnp.npy_uint64 *res,
const cnp.npy_uint64 *low,
const cnp.npy_uint64 *high
) noexcept nogil
void irk_rand_int64_broadcast(
irk_state *state,
cnp.npy_intp len,
cnp.npy_int64 *res,
const cnp.npy_int64 *low,
const cnp.npy_int64 *high
) noexcept nogil

void irk_long_vec(
irk_state *state, cnp.npy_intp len, long *res
) noexcept nogil
Expand Down Expand Up @@ -1873,22 +1937,26 @@ cdef class _MKLRandomState:
# a few places. It would be easy to template them.

def _choose_randint_type(self, dtype):
_randint_type = {
"bool": (0, 2, self._rand_bool),
"int8": (-2**7, 2**7, self._rand_int8),
"int16": (-2**15, 2**15, self._rand_int16),
"int32": (-2**31, 2**31, self._rand_int32),
"int64": (-2**63, 2**63, self._rand_int64),
"uint8": (0, 2**8, self._rand_uint8),
"uint16": (0, 2**16, self._rand_uint16),
"uint32": (0, 2**32, self._rand_uint32),
"uint64": (0, 2**64, self._rand_uint64)
_randint_bounds = {
"bool": (0, 2),
"int8": (-2**7, 2**7),
"int16": (-2**15, 2**15),
"int32": (-2**31, 2**31),
"int64": (-2**63, 2**63),
"uint8": (0, 2**8),
"uint16": (0, 2**16),
"uint32": (0, 2**32),
"uint64": (0, 2**64),
}

key = np.dtype(dtype).name
if key not in _randint_type:
if key not in _randint_bounds:
raise TypeError(f'Unsupported dtype "{key}" for randint')
return _randint_type[key]

lowbnd, highbnd = _randint_bounds[key]
return (lowbnd, highbnd,
getattr(self, f"_rand_{key}"),
getattr(self, f"_rand_{key}_broadcast"))

# generates typed random integer in [low, high]
def _rand_bool(self, cnp.npy_bool low, cnp.npy_bool high, size):
Expand Down Expand Up @@ -2119,6 +2187,150 @@ cdef class _MKLRandomState:
irk_rand_uint64_vec(self.internal_state, cnt, out, low, high)
return array

# Broadcasted variants of the typed generators for randint
def _rand_bool_broadcast(self, cnp.ndarray low, cnp.ndarray high,
cnp.ndarray out):
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
cdef cnp.npy_bool *out_p = <cnp.npy_bool *>cnp.PyArray_DATA(out)
cdef cnp.npy_bool *low_p = <cnp.npy_bool *>cnp.PyArray_DATA(low)
cdef cnp.npy_bool *high_p = <cnp.npy_bool *>cnp.PyArray_DATA(high)
with nogil:
irk_rand_bool_broadcast(
self.internal_state, cnt, out_p, low_p, high_p
)

def _rand_int8_broadcast(self, cnp.ndarray low, cnp.ndarray high,
cnp.ndarray out):
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
cdef cnp.npy_int8 *out_p = <cnp.npy_int8 *>cnp.PyArray_DATA(out)
cdef cnp.npy_int8 *low_p = <cnp.npy_int8 *>cnp.PyArray_DATA(low)
cdef cnp.npy_int8 *high_p = <cnp.npy_int8 *>cnp.PyArray_DATA(high)
with nogil:
irk_rand_int8_broadcast(
self.internal_state, cnt, out_p, low_p, high_p
)

def _rand_int16_broadcast(self, cnp.ndarray low, cnp.ndarray high,
cnp.ndarray out):
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
cdef cnp.npy_int16 *out_p = <cnp.npy_int16 *>cnp.PyArray_DATA(out)
cdef cnp.npy_int16 *low_p = <cnp.npy_int16 *>cnp.PyArray_DATA(low)
cdef cnp.npy_int16 *high_p = <cnp.npy_int16 *>cnp.PyArray_DATA(high)
with nogil:
irk_rand_int16_broadcast(
self.internal_state, cnt, out_p, low_p, high_p
)

def _rand_int32_broadcast(self, cnp.ndarray low, cnp.ndarray high,
cnp.ndarray out):
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
cdef cnp.npy_int32 *out_p = <cnp.npy_int32 *>cnp.PyArray_DATA(out)
cdef cnp.npy_int32 *low_p = <cnp.npy_int32 *>cnp.PyArray_DATA(low)
cdef cnp.npy_int32 *high_p = <cnp.npy_int32 *>cnp.PyArray_DATA(high)
with nogil:
irk_rand_int32_broadcast(
self.internal_state, cnt, out_p, low_p, high_p
)

def _rand_int64_broadcast(self, cnp.ndarray low, cnp.ndarray high,
cnp.ndarray out):
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
cdef cnp.npy_int64 *out_p = <cnp.npy_int64 *>cnp.PyArray_DATA(out)
cdef cnp.npy_int64 *low_p = <cnp.npy_int64 *>cnp.PyArray_DATA(low)
cdef cnp.npy_int64 *high_p = <cnp.npy_int64 *>cnp.PyArray_DATA(high)
with nogil:
irk_rand_int64_broadcast(
self.internal_state, cnt, out_p, low_p, high_p
)

def _rand_uint8_broadcast(self, cnp.ndarray low, cnp.ndarray high,
cnp.ndarray out):
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
cdef cnp.npy_uint8 *out_p = <cnp.npy_uint8 *>cnp.PyArray_DATA(out)
cdef cnp.npy_uint8 *low_p = <cnp.npy_uint8 *>cnp.PyArray_DATA(low)
cdef cnp.npy_uint8 *high_p = <cnp.npy_uint8 *>cnp.PyArray_DATA(high)
with nogil:
irk_rand_uint8_broadcast(
self.internal_state, cnt, out_p, low_p, high_p
)

def _rand_uint16_broadcast(self, cnp.ndarray low, cnp.ndarray high,
cnp.ndarray out):
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
cdef cnp.npy_uint16 *out_p = <cnp.npy_uint16 *>cnp.PyArray_DATA(out)
cdef cnp.npy_uint16 *low_p = <cnp.npy_uint16 *>cnp.PyArray_DATA(low)
cdef cnp.npy_uint16 *high_p = <cnp.npy_uint16 *>cnp.PyArray_DATA(high)
with nogil:
irk_rand_uint16_broadcast(
self.internal_state, cnt, out_p, low_p, high_p
)

def _rand_uint32_broadcast(self, cnp.ndarray low, cnp.ndarray high,
cnp.ndarray out):
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
cdef cnp.npy_uint32 *out_p = <cnp.npy_uint32 *>cnp.PyArray_DATA(out)
cdef cnp.npy_uint32 *low_p = <cnp.npy_uint32 *>cnp.PyArray_DATA(low)
cdef cnp.npy_uint32 *high_p = <cnp.npy_uint32 *>cnp.PyArray_DATA(high)
with nogil:
irk_rand_uint32_broadcast(
self.internal_state, cnt, out_p, low_p, high_p
)

def _rand_uint64_broadcast(self, cnp.ndarray low, cnp.ndarray high,
cnp.ndarray out):
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
cdef cnp.npy_uint64 *out_p = <cnp.npy_uint64 *>cnp.PyArray_DATA(out)
cdef cnp.npy_uint64 *low_p = <cnp.npy_uint64 *>cnp.PyArray_DATA(low)
cdef cnp.npy_uint64 *high_p = <cnp.npy_uint64 *>cnp.PyArray_DATA(high)
with nogil:
irk_rand_uint64_broadcast(
self.internal_state, cnt, out_p, low_p, high_p
)

def _randint_broadcast(self, low_arr, high_arr, size, _dtype,
lowbnd, highbnd, broadcast_func):
# output shape
# `size` if given, else the broadcast of the bounds
if size is None:
out_shape = np.broadcast_shapes(low_arr.shape, high_arr.shape)
elif isinstance(size, (int, np.integer)):
out_shape = (int(size),)
else:
out_shape = tuple(int(s) for s in size)

# size-1 bounds may collapse into a smaller-rank `size`
bshape = np.broadcast_shapes(low_arr.shape, high_arr.shape, out_shape)
if np.prod(bshape) != np.prod(out_shape):
raise ValueError("shape mismatch: bounds cannot broadcast to size")
low_b = np.broadcast_to(low_arr, bshape)
high_b = np.broadcast_to(high_arr, bshape)

if np.prod(out_shape) == 0:
return np.empty(out_shape, dtype=_dtype)

max_high = int(np.max(high_arr))
if int(np.min(low_arr)) < lowbnd:
raise ValueError(f"low is out of bounds for {_dtype.name}")
if max_high > highbnd:
raise ValueError(f"high is out of bounds for {_dtype.name}")
if np.any(low_arr >= high_arr):
raise ValueError("low >= high")

# inclusive high (high - 1); widen small dtypes to int64 to avoid
# overflow, but subtract first when high exceeds int64
if max_high <= 2**63 - 1:
high_incl = high_b.astype(np.int64) - 1
else:
high_incl = high_b - 1
low_c = np.ascontiguousarray(low_b, dtype=_dtype)
high_c = np.ascontiguousarray(high_incl, dtype=_dtype)
out = np.empty(out_shape, dtype=_dtype)

with self.lock:
broadcast_func(low_c, high_c, out)

return out

def randint(self, low, high=None, size=None, dtype=int):
"""
randint(low, high=None, size=None, dtype=int)
Expand All @@ -2131,13 +2343,15 @@ cdef class _MKLRandomState:

Parameters
----------
low : int
low : int or array_like of ints
Lowest (signed) integer to be drawn from the distribution (unless
``high=None``, in which case this parameter is the *highest* such
integer).
high : int, optional
integer). If an array is given, it must broadcast with `high` (and
with `size`, if provided).
high : int or array_like of ints, optional
If provided, one above the largest (signed) integer to be drawn
from the distribution (see above for behavior if ``high=None``).
If an array is given, it must broadcast with `low`.
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
Expand Down Expand Up @@ -2166,16 +2380,32 @@ cdef class _MKLRandomState:
Examples
--------
>>> mkl_random.randint(2, size=10)
array([1, 0, 0, 0, 1, 1, 0, 0, 1, 0])
array([1, 0, 0, 0, 1, 1, 0, 0, 1, 0]) # random
>>> mkl_random.randint(1, size=10)
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

Generate a 2 x 4 array of ints between 0 and 4, inclusive:

>>> mkl_random.randint(5, size=(2, 4))
array([[4, 0, 2, 1],
array([[4, 0, 2, 1], # random
[3, 2, 2, 0]])

Generate a 1 x 3 array with 3 different upper bounds

>>> mkl_random.randint(1, [3, 5, 10])
array([2, 4, 7]) # random

Generate a 1 by 3 array with 3 different lower bounds

>>> mkl_random.randint([1, 5, 7], 10)
array([6, 6, 9]) # random

Generate a 2 by 4 array using broadcasting with dtype of uint8

>>> mkl_random.randint([1, 3, 5, 7], [[10], [20]], dtype=np.uint8)
array([[ 8, 7, 7, 7], # random
[18, 17, 19, 17]], dtype=uint8)

"""
if high is None:
high = low
Expand All @@ -2191,30 +2421,43 @@ cdef class _MKLRandomState:
"ValueError", DeprecationWarning)
_dtype = _dtype.newbyteorder()

if size is not None:
if (np.prod(size) == 0):
return np.empty(size, dtype=np.dtype(_dtype))
lowbnd, highbnd, randfunc, broadcast_func = \
self._choose_randint_type(_dtype)

lowbnd, highbnd, randfunc = self._choose_randint_type(_dtype)
low_arr = np.asarray(low)
high_arr = np.asarray(high)

if low < lowbnd:
raise ValueError(
f"low is out of bounds for {np.dtype(_dtype).name}"
)
if high > highbnd:
raise ValueError(
f"high is out of bounds for {np.dtype(_dtype).name}"
)
if low >= high:
raise ValueError("low >= high")
if low_arr.ndim == 0 and high_arr.ndim == 0:
# Fast path for scalar
if size is not None and np.prod(size) == 0:
return np.empty(size, dtype=_dtype)

with self.lock:
ret = randfunc(low, high - 1, size)
low = int(low)
high = int(high)

if size is None and dtype in (bool, int):
return dtype(ret)
if low < lowbnd:
raise ValueError(
f"low is out of bounds for {_dtype.name}"
)
if high > highbnd:
raise ValueError(
f"high is out of bounds for {_dtype.name}"
)
if low >= high:
raise ValueError("low >= high")

return ret
with self.lock:
ret = randfunc(low, high - 1, size)

if size is None and dtype in (bool, int):
return dtype(ret)

return ret

# Broadcast path( at least one of `low`/`high` is array_like)
return self._randint_broadcast(
low_arr, high_arr, size, _dtype, lowbnd, highbnd, broadcast_func
)

def bytes(self, cnp.npy_intp length):
"""
Expand Down
Loading
Loading