Skip to content

Commit 2d64b38

Browse files
Merge pull request #168 from IntelPython/randint_array_like_bounds
Support array_like low/high bounds for `randint`
2 parents 6b7962f + ec149e3 commit 2d64b38

7 files changed

Lines changed: 850 additions & 131 deletions

File tree

.flake8

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extend-ignore =
2626
per-file-ignores =
2727
mkl_random/__init__.py: F401
2828
mkl_random/interfaces/__init__.py: F401
29+
mkl_random/tests/*.py: D102
2930
mkl_random/tests/**/*.py: D102
3031

3132
filename = *.py, *.pyx, *.pxi, *.pxd

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
# [dev] (MM/DD/YYYY)
88

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

1112
### Changed
1213
* 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)

mkl_random/mklrand.pyx

Lines changed: 279 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,70 @@ cdef extern from "mkl_distributions.h":
392392
cnp.npy_int64 high
393393
) noexcept nogil
394394

395+
void irk_rand_bool_broadcast(
396+
irk_state *state,
397+
cnp.npy_intp len,
398+
cnp.npy_bool *res,
399+
const cnp.npy_bool *low,
400+
const cnp.npy_bool *high
401+
) noexcept nogil
402+
void irk_rand_uint8_broadcast(
403+
irk_state *state,
404+
cnp.npy_intp len,
405+
cnp.npy_uint8 *res,
406+
const cnp.npy_uint8 *low,
407+
const cnp.npy_uint8 *high
408+
) noexcept nogil
409+
void irk_rand_int8_broadcast(
410+
irk_state *state,
411+
cnp.npy_intp len,
412+
cnp.npy_int8 *res,
413+
const cnp.npy_int8 *low,
414+
const cnp.npy_int8 *high
415+
) noexcept nogil
416+
void irk_rand_uint16_broadcast(
417+
irk_state *state,
418+
cnp.npy_intp len,
419+
cnp.npy_uint16 *res,
420+
const cnp.npy_uint16 *low,
421+
const cnp.npy_uint16 *high
422+
) noexcept nogil
423+
void irk_rand_int16_broadcast(
424+
irk_state *state,
425+
cnp.npy_intp len,
426+
cnp.npy_int16 *res,
427+
const cnp.npy_int16 *low,
428+
const cnp.npy_int16 *high
429+
) noexcept nogil
430+
void irk_rand_uint32_broadcast(
431+
irk_state *state,
432+
cnp.npy_intp len,
433+
cnp.npy_uint32 *res,
434+
const cnp.npy_uint32 *low,
435+
const cnp.npy_uint32 *high
436+
) noexcept nogil
437+
void irk_rand_int32_broadcast(
438+
irk_state *state,
439+
cnp.npy_intp len,
440+
cnp.npy_int32 *res,
441+
const cnp.npy_int32 *low,
442+
const cnp.npy_int32 *high
443+
) noexcept nogil
444+
void irk_rand_uint64_broadcast(
445+
irk_state *state,
446+
cnp.npy_intp len,
447+
cnp.npy_uint64 *res,
448+
const cnp.npy_uint64 *low,
449+
const cnp.npy_uint64 *high
450+
) noexcept nogil
451+
void irk_rand_int64_broadcast(
452+
irk_state *state,
453+
cnp.npy_intp len,
454+
cnp.npy_int64 *res,
455+
const cnp.npy_int64 *low,
456+
const cnp.npy_int64 *high
457+
) noexcept nogil
458+
395459
void irk_long_vec(
396460
irk_state *state, cnp.npy_intp len, long *res
397461
) noexcept nogil
@@ -1873,22 +1937,26 @@ cdef class _MKLRandomState:
18731937
# a few places. It would be easy to template them.
18741938

18751939
def _choose_randint_type(self, dtype):
1876-
_randint_type = {
1877-
"bool": (0, 2, self._rand_bool),
1878-
"int8": (-2**7, 2**7, self._rand_int8),
1879-
"int16": (-2**15, 2**15, self._rand_int16),
1880-
"int32": (-2**31, 2**31, self._rand_int32),
1881-
"int64": (-2**63, 2**63, self._rand_int64),
1882-
"uint8": (0, 2**8, self._rand_uint8),
1883-
"uint16": (0, 2**16, self._rand_uint16),
1884-
"uint32": (0, 2**32, self._rand_uint32),
1885-
"uint64": (0, 2**64, self._rand_uint64)
1940+
_randint_bounds = {
1941+
"bool": (0, 2),
1942+
"int8": (-2**7, 2**7),
1943+
"int16": (-2**15, 2**15),
1944+
"int32": (-2**31, 2**31),
1945+
"int64": (-2**63, 2**63),
1946+
"uint8": (0, 2**8),
1947+
"uint16": (0, 2**16),
1948+
"uint32": (0, 2**32),
1949+
"uint64": (0, 2**64),
18861950
}
18871951

18881952
key = np.dtype(dtype).name
1889-
if key not in _randint_type:
1953+
if key not in _randint_bounds:
18901954
raise TypeError(f'Unsupported dtype "{key}" for randint')
1891-
return _randint_type[key]
1955+
1956+
lowbnd, highbnd = _randint_bounds[key]
1957+
return (lowbnd, highbnd,
1958+
getattr(self, f"_rand_{key}"),
1959+
getattr(self, f"_rand_{key}_broadcast"))
18921960

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

2190+
# Broadcasted variants of the typed generators for randint
2191+
def _rand_bool_broadcast(self, cnp.ndarray low, cnp.ndarray high,
2192+
cnp.ndarray out):
2193+
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
2194+
cdef cnp.npy_bool *out_p = <cnp.npy_bool *>cnp.PyArray_DATA(out)
2195+
cdef cnp.npy_bool *low_p = <cnp.npy_bool *>cnp.PyArray_DATA(low)
2196+
cdef cnp.npy_bool *high_p = <cnp.npy_bool *>cnp.PyArray_DATA(high)
2197+
with nogil:
2198+
irk_rand_bool_broadcast(
2199+
self.internal_state, cnt, out_p, low_p, high_p
2200+
)
2201+
2202+
def _rand_int8_broadcast(self, cnp.ndarray low, cnp.ndarray high,
2203+
cnp.ndarray out):
2204+
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
2205+
cdef cnp.npy_int8 *out_p = <cnp.npy_int8 *>cnp.PyArray_DATA(out)
2206+
cdef cnp.npy_int8 *low_p = <cnp.npy_int8 *>cnp.PyArray_DATA(low)
2207+
cdef cnp.npy_int8 *high_p = <cnp.npy_int8 *>cnp.PyArray_DATA(high)
2208+
with nogil:
2209+
irk_rand_int8_broadcast(
2210+
self.internal_state, cnt, out_p, low_p, high_p
2211+
)
2212+
2213+
def _rand_int16_broadcast(self, cnp.ndarray low, cnp.ndarray high,
2214+
cnp.ndarray out):
2215+
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
2216+
cdef cnp.npy_int16 *out_p = <cnp.npy_int16 *>cnp.PyArray_DATA(out)
2217+
cdef cnp.npy_int16 *low_p = <cnp.npy_int16 *>cnp.PyArray_DATA(low)
2218+
cdef cnp.npy_int16 *high_p = <cnp.npy_int16 *>cnp.PyArray_DATA(high)
2219+
with nogil:
2220+
irk_rand_int16_broadcast(
2221+
self.internal_state, cnt, out_p, low_p, high_p
2222+
)
2223+
2224+
def _rand_int32_broadcast(self, cnp.ndarray low, cnp.ndarray high,
2225+
cnp.ndarray out):
2226+
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
2227+
cdef cnp.npy_int32 *out_p = <cnp.npy_int32 *>cnp.PyArray_DATA(out)
2228+
cdef cnp.npy_int32 *low_p = <cnp.npy_int32 *>cnp.PyArray_DATA(low)
2229+
cdef cnp.npy_int32 *high_p = <cnp.npy_int32 *>cnp.PyArray_DATA(high)
2230+
with nogil:
2231+
irk_rand_int32_broadcast(
2232+
self.internal_state, cnt, out_p, low_p, high_p
2233+
)
2234+
2235+
def _rand_int64_broadcast(self, cnp.ndarray low, cnp.ndarray high,
2236+
cnp.ndarray out):
2237+
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
2238+
cdef cnp.npy_int64 *out_p = <cnp.npy_int64 *>cnp.PyArray_DATA(out)
2239+
cdef cnp.npy_int64 *low_p = <cnp.npy_int64 *>cnp.PyArray_DATA(low)
2240+
cdef cnp.npy_int64 *high_p = <cnp.npy_int64 *>cnp.PyArray_DATA(high)
2241+
with nogil:
2242+
irk_rand_int64_broadcast(
2243+
self.internal_state, cnt, out_p, low_p, high_p
2244+
)
2245+
2246+
def _rand_uint8_broadcast(self, cnp.ndarray low, cnp.ndarray high,
2247+
cnp.ndarray out):
2248+
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
2249+
cdef cnp.npy_uint8 *out_p = <cnp.npy_uint8 *>cnp.PyArray_DATA(out)
2250+
cdef cnp.npy_uint8 *low_p = <cnp.npy_uint8 *>cnp.PyArray_DATA(low)
2251+
cdef cnp.npy_uint8 *high_p = <cnp.npy_uint8 *>cnp.PyArray_DATA(high)
2252+
with nogil:
2253+
irk_rand_uint8_broadcast(
2254+
self.internal_state, cnt, out_p, low_p, high_p
2255+
)
2256+
2257+
def _rand_uint16_broadcast(self, cnp.ndarray low, cnp.ndarray high,
2258+
cnp.ndarray out):
2259+
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
2260+
cdef cnp.npy_uint16 *out_p = <cnp.npy_uint16 *>cnp.PyArray_DATA(out)
2261+
cdef cnp.npy_uint16 *low_p = <cnp.npy_uint16 *>cnp.PyArray_DATA(low)
2262+
cdef cnp.npy_uint16 *high_p = <cnp.npy_uint16 *>cnp.PyArray_DATA(high)
2263+
with nogil:
2264+
irk_rand_uint16_broadcast(
2265+
self.internal_state, cnt, out_p, low_p, high_p
2266+
)
2267+
2268+
def _rand_uint32_broadcast(self, cnp.ndarray low, cnp.ndarray high,
2269+
cnp.ndarray out):
2270+
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
2271+
cdef cnp.npy_uint32 *out_p = <cnp.npy_uint32 *>cnp.PyArray_DATA(out)
2272+
cdef cnp.npy_uint32 *low_p = <cnp.npy_uint32 *>cnp.PyArray_DATA(low)
2273+
cdef cnp.npy_uint32 *high_p = <cnp.npy_uint32 *>cnp.PyArray_DATA(high)
2274+
with nogil:
2275+
irk_rand_uint32_broadcast(
2276+
self.internal_state, cnt, out_p, low_p, high_p
2277+
)
2278+
2279+
def _rand_uint64_broadcast(self, cnp.ndarray low, cnp.ndarray high,
2280+
cnp.ndarray out):
2281+
cdef cnp.npy_intp cnt = cnp.PyArray_SIZE(out)
2282+
cdef cnp.npy_uint64 *out_p = <cnp.npy_uint64 *>cnp.PyArray_DATA(out)
2283+
cdef cnp.npy_uint64 *low_p = <cnp.npy_uint64 *>cnp.PyArray_DATA(low)
2284+
cdef cnp.npy_uint64 *high_p = <cnp.npy_uint64 *>cnp.PyArray_DATA(high)
2285+
with nogil:
2286+
irk_rand_uint64_broadcast(
2287+
self.internal_state, cnt, out_p, low_p, high_p
2288+
)
2289+
2290+
def _randint_broadcast(self, low_arr, high_arr, size, _dtype,
2291+
lowbnd, highbnd, broadcast_func):
2292+
# output shape
2293+
# `size` if given, else the broadcast of the bounds
2294+
if size is None:
2295+
out_shape = np.broadcast_shapes(low_arr.shape, high_arr.shape)
2296+
elif isinstance(size, (int, np.integer)):
2297+
out_shape = (int(size),)
2298+
else:
2299+
out_shape = tuple(int(s) for s in size)
2300+
2301+
# size-1 bounds may collapse into a smaller-rank `size`
2302+
bshape = np.broadcast_shapes(low_arr.shape, high_arr.shape, out_shape)
2303+
if np.prod(bshape) != np.prod(out_shape):
2304+
raise ValueError("shape mismatch: bounds cannot broadcast to size")
2305+
low_b = np.broadcast_to(low_arr, bshape)
2306+
high_b = np.broadcast_to(high_arr, bshape)
2307+
2308+
if np.prod(out_shape) == 0:
2309+
return np.empty(out_shape, dtype=_dtype)
2310+
2311+
max_high = int(np.max(high_arr))
2312+
if int(np.min(low_arr)) < lowbnd:
2313+
raise ValueError(f"low is out of bounds for {_dtype.name}")
2314+
if max_high > highbnd:
2315+
raise ValueError(f"high is out of bounds for {_dtype.name}")
2316+
if np.any(low_arr >= high_arr):
2317+
raise ValueError("low >= high")
2318+
2319+
# inclusive high (high - 1); widen small dtypes to int64 to avoid
2320+
# overflow, but subtract first when high exceeds int64
2321+
if max_high <= 2**63 - 1:
2322+
high_incl = high_b.astype(np.int64) - 1
2323+
else:
2324+
high_incl = high_b - 1
2325+
low_c = np.ascontiguousarray(low_b, dtype=_dtype)
2326+
high_c = np.ascontiguousarray(high_incl, dtype=_dtype)
2327+
out = np.empty(out_shape, dtype=_dtype)
2328+
2329+
with self.lock:
2330+
broadcast_func(low_c, high_c, out)
2331+
2332+
return out
2333+
21222334
def randint(self, low, high=None, size=None, dtype=int):
21232335
"""
21242336
randint(low, high=None, size=None, dtype=int)
@@ -2131,13 +2343,15 @@ cdef class _MKLRandomState:
21312343
21322344
Parameters
21332345
----------
2134-
low : int
2346+
low : int or array_like of ints
21352347
Lowest (signed) integer to be drawn from the distribution (unless
21362348
``high=None``, in which case this parameter is the *highest* such
2137-
integer).
2138-
high : int, optional
2349+
integer). If an array is given, it must broadcast with `high` (and
2350+
with `size`, if provided).
2351+
high : int or array_like of ints, optional
21392352
If provided, one above the largest (signed) integer to be drawn
21402353
from the distribution (see above for behavior if ``high=None``).
2354+
If an array is given, it must broadcast with `low`.
21412355
size : int or tuple of ints, optional
21422356
Output shape. If the given shape is, e.g., ``(m, n, k)``, then
21432357
``m * n * k`` samples are drawn. Default is None, in which case a
@@ -2166,16 +2380,32 @@ cdef class _MKLRandomState:
21662380
Examples
21672381
--------
21682382
>>> mkl_random.randint(2, size=10)
2169-
array([1, 0, 0, 0, 1, 1, 0, 0, 1, 0])
2383+
array([1, 0, 0, 0, 1, 1, 0, 0, 1, 0]) # random
21702384
>>> mkl_random.randint(1, size=10)
21712385
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
21722386
21732387
Generate a 2 x 4 array of ints between 0 and 4, inclusive:
21742388
21752389
>>> mkl_random.randint(5, size=(2, 4))
2176-
array([[4, 0, 2, 1],
2390+
array([[4, 0, 2, 1], # random
21772391
[3, 2, 2, 0]])
21782392
2393+
Generate a 1 x 3 array with 3 different upper bounds
2394+
2395+
>>> mkl_random.randint(1, [3, 5, 10])
2396+
array([2, 4, 7]) # random
2397+
2398+
Generate a 1 by 3 array with 3 different lower bounds
2399+
2400+
>>> mkl_random.randint([1, 5, 7], 10)
2401+
array([6, 6, 9]) # random
2402+
2403+
Generate a 2 by 4 array using broadcasting with dtype of uint8
2404+
2405+
>>> mkl_random.randint([1, 3, 5, 7], [[10], [20]], dtype=np.uint8)
2406+
array([[ 8, 7, 7, 7], # random
2407+
[18, 17, 19, 17]], dtype=uint8)
2408+
21792409
"""
21802410
if high is None:
21812411
high = low
@@ -2191,30 +2421,43 @@ cdef class _MKLRandomState:
21912421
"ValueError", DeprecationWarning)
21922422
_dtype = _dtype.newbyteorder()
21932423

2194-
if size is not None:
2195-
if (np.prod(size) == 0):
2196-
return np.empty(size, dtype=np.dtype(_dtype))
2424+
lowbnd, highbnd, randfunc, broadcast_func = \
2425+
self._choose_randint_type(_dtype)
21972426

2198-
lowbnd, highbnd, randfunc = self._choose_randint_type(_dtype)
2427+
low_arr = np.asarray(low)
2428+
high_arr = np.asarray(high)
21992429

2200-
if low < lowbnd:
2201-
raise ValueError(
2202-
f"low is out of bounds for {np.dtype(_dtype).name}"
2203-
)
2204-
if high > highbnd:
2205-
raise ValueError(
2206-
f"high is out of bounds for {np.dtype(_dtype).name}"
2207-
)
2208-
if low >= high:
2209-
raise ValueError("low >= high")
2430+
if low_arr.ndim == 0 and high_arr.ndim == 0:
2431+
# Fast path for scalar
2432+
if size is not None and np.prod(size) == 0:
2433+
return np.empty(size, dtype=_dtype)
22102434

2211-
with self.lock:
2212-
ret = randfunc(low, high - 1, size)
2435+
low = int(low)
2436+
high = int(high)
22132437

2214-
if size is None and dtype in (bool, int):
2215-
return dtype(ret)
2438+
if low < lowbnd:
2439+
raise ValueError(
2440+
f"low is out of bounds for {_dtype.name}"
2441+
)
2442+
if high > highbnd:
2443+
raise ValueError(
2444+
f"high is out of bounds for {_dtype.name}"
2445+
)
2446+
if low >= high:
2447+
raise ValueError("low >= high")
22162448

2217-
return ret
2449+
with self.lock:
2450+
ret = randfunc(low, high - 1, size)
2451+
2452+
if size is None and dtype in (bool, int):
2453+
return dtype(ret)
2454+
2455+
return ret
2456+
2457+
# Broadcast path (at least one of `low`/`high` is array_like)
2458+
return self._randint_broadcast(
2459+
low_arr, high_arr, size, _dtype, lowbnd, highbnd, broadcast_func
2460+
)
22182461

22192462
def bytes(self, cnp.npy_intp length):
22202463
"""

0 commit comments

Comments
 (0)