summaryrefslogtreecommitdiff
path: root/numpy/random/_bounded_integers.pyx.in
blob: 6743001d6d582a32876ae2d96d3a9287a93fb423 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#!python
#cython: wraparound=False, nonecheck=False, boundscheck=False, cdivision=True

import numpy as np
cimport numpy as np

__all__ = []

np.import_array()


cdef extern from "numpy/random/distributions.h":
    # Generate random numbers in closed interval [off, off + rng].
    uint64_t random_bounded_uint64(bitgen_t *bitgen_state,
                                   uint64_t off, uint64_t rng,
                                   uint64_t mask, bint use_masked) nogil
    uint32_t random_buffered_bounded_uint32(bitgen_t *bitgen_state,
                                            uint32_t off, uint32_t rng,
                                            uint32_t mask, bint use_masked,
                                            int *bcnt, uint32_t *buf) nogil
    uint16_t random_buffered_bounded_uint16(bitgen_t *bitgen_state,
                                            uint16_t off, uint16_t rng,
                                            uint16_t mask, bint use_masked,
                                            int *bcnt, uint32_t *buf) nogil
    uint8_t random_buffered_bounded_uint8(bitgen_t *bitgen_state,
                                          uint8_t off, uint8_t rng,
                                          uint8_t mask, bint use_masked,
                                          int *bcnt, uint32_t *buf) nogil
    np.npy_bool random_buffered_bounded_bool(bitgen_t *bitgen_state,
                                             np.npy_bool off, np.npy_bool rng,
                                             np.npy_bool mask, bint use_masked,
                                             int *bcnt, uint32_t *buf) nogil
    void random_bounded_uint64_fill(bitgen_t *bitgen_state,
                                    uint64_t off, uint64_t rng, np.npy_intp cnt,
                                    bint use_masked,
                                    uint64_t *out) nogil
    void random_bounded_uint32_fill(bitgen_t *bitgen_state,
                                    uint32_t off, uint32_t rng, np.npy_intp cnt,
                                    bint use_masked,
                                    uint32_t *out) nogil
    void random_bounded_uint16_fill(bitgen_t *bitgen_state,
                                    uint16_t off, uint16_t rng, np.npy_intp cnt,
                                    bint use_masked,
                                    uint16_t *out) nogil
    void random_bounded_uint8_fill(bitgen_t *bitgen_state,
                                   uint8_t off, uint8_t rng, np.npy_intp cnt,
                                   bint use_masked,
                                   uint8_t *out) nogil
    void random_bounded_bool_fill(bitgen_t *bitgen_state,
                                  np.npy_bool off, np.npy_bool rng, np.npy_intp cnt,
                                  bint use_masked,
                                  np.npy_bool *out) nogil


cdef object format_bounds_error(bint closed, object low):
    # Special case low == 0 to provide a better exception for users
    # since low = 0 is the default single-argument case.
    if not np.any(low):
        comp = '<' if closed else '<='
        return f'high {comp} 0'
    else:
        comp = '>' if closed else '>='
        return f'low {comp} high'


{{
py:
type_info = (('uint32', 'uint32', 'uint64', 'NPY_UINT64', 0, 0, 0, '0X100000000ULL'),
          ('uint16', 'uint16', 'uint32', 'NPY_UINT32', 1, 16, 0, '0X10000UL'),
          ('uint8', 'uint8', 'uint16', 'NPY_UINT16', 3, 8, 0, '0X100UL'),
          ('bool','bool', 'uint8', 'NPY_UINT8', 31, 1, 0, '0x2UL'),
          ('int32', 'uint32', 'uint64', 'NPY_INT64', 0, 0, '-0x80000000LL', '0x80000000LL'),
          ('int16', 'uint16', 'uint32', 'NPY_INT32', 1, 16, '-0x8000LL', '0x8000LL' ),
          ('int8', 'uint8', 'uint16', 'NPY_INT16', 3, 8, '-0x80LL', '0x80LL' ),
)}}
{{for  nptype, utype, nptype_up, npctype, remaining, bitshift, lb, ub in type_info}}
{{ py: otype = nptype + '_' if nptype == 'bool' else nptype }}
cdef object _rand_{{nptype}}_broadcast(np.ndarray low, np.ndarray high, object size,
                                       bint use_masked, bint closed,
                                       bitgen_t *state, object lock):
    """
    Array path for smaller integer types

    This path is simpler since the high value in the open interval [low, high)
    must be in-range for the next larger type, {{nptype_up}}. Here we case to
    this type for checking and the recast to {{nptype}} when producing the
    random integers.
    """
    cdef {{utype}}_t rng, last_rng, off, val, mask, out_val, is_open
    cdef uint32_t buf
    cdef {{utype}}_t *out_data
    cdef {{nptype_up}}_t low_v, high_v
    cdef np.ndarray low_arr, high_arr, out_arr
    cdef np.npy_intp i, cnt
    cdef np.broadcast it
    cdef int buf_rem = 0

    # Array path
    is_open = not closed
    low_arr = <np.ndarray>low
    high_arr = <np.ndarray>high

    if np.can_cast(low_arr, np.{{otype}}):
        pass  # cannot be out-of-bounds
    elif np.any(np.less(low_arr, np.{{otype}}({{lb}}))):
        raise ValueError('low is out of bounds for {{nptype}}')

    if closed:
        high_comp = np.greater_equal
        low_high_comp = np.greater
    else:
        high_comp = np.greater
        low_high_comp = np.greater_equal

    if np.can_cast(high_arr, np.{{otype}}):
        pass  # cannot be out-of-bounds
    elif np.any(high_comp(high_arr, np.{{nptype_up}}({{ub}}))):
        raise ValueError('high is out of bounds for {{nptype}}')

    if np.any(low_high_comp(low_arr, high_arr)):
        raise ValueError(format_bounds_error(closed, low_arr))

    low_arr = <np.ndarray>np.PyArray_FROM_OTF(low, np.{{npctype}}, np.NPY_ALIGNED | np.NPY_FORCECAST)
    high_arr = <np.ndarray>np.PyArray_FROM_OTF(high, np.{{npctype}}, np.NPY_ALIGNED | np.NPY_FORCECAST)

    if size is not None:
        out_arr = <np.ndarray>np.empty(size, np.{{otype}})
    else:
        it = np.PyArray_MultiIterNew2(low_arr, high_arr)
        out_arr = <np.ndarray>np.empty(it.shape, np.{{otype}})

    it = np.PyArray_MultiIterNew3(low_arr, high_arr, out_arr)
    out_data = <{{utype}}_t *>np.PyArray_DATA(out_arr)
    cnt = np.PyArray_SIZE(out_arr)
    mask = last_rng = 0
    with lock, nogil:
        for i in range(cnt):
            low_v = (<{{nptype_up}}_t*>np.PyArray_MultiIter_DATA(it, 0))[0]
            high_v = (<{{nptype_up}}_t*>np.PyArray_MultiIter_DATA(it, 1))[0]
            # Subtract 1 since generator produces values on the closed int [off, off+rng]
            rng = <{{utype}}_t>((high_v - is_open) - low_v)
            off = <{{utype}}_t>(<{{nptype_up}}_t>low_v)

            if rng != last_rng:
                # Smallest bit mask >= max
                mask = <{{utype}}_t>_gen_mask(rng)

            out_data[i] = random_buffered_bounded_{{utype}}(state, off, rng, mask, use_masked, &buf_rem, &buf)

            np.PyArray_MultiIter_NEXT(it)
    return out_arr
{{endfor}}
{{
py:
big_type_info = (('uint64', 'uint64', 'NPY_UINT64', '0x0ULL', '0xFFFFFFFFFFFFFFFFULL'),
                 ('int64', 'uint64', 'NPY_INT64', '-0x8000000000000000LL', '0x7FFFFFFFFFFFFFFFLL' )
)}}
{{for  nptype, utype, npctype, lb, ub in big_type_info}}
{{ py: otype = nptype}}
cdef object _rand_{{nptype}}_broadcast(object low, object high, object size,
                                       bint use_masked, bint closed,
                                       bitgen_t *state, object lock):
    """
    Array path for 64-bit integer types

    Requires special treatment since the high value can be out-of-range for
    the largest (64 bit) integer type since the generator is specified on the
    interval [low,high).

    The internal generator does not have this issue since it generates from
    the closes interval [low, high-1] and high-1 is always in range for the
    64 bit integer type.
    """

    cdef np.ndarray low_arr, low_arr_orig, high_arr, high_arr_orig, out_arr
    cdef np.npy_intp i, cnt, n
    cdef np.broadcast it
    cdef object closed_upper
    cdef uint64_t *out_data
    cdef {{nptype}}_t *highm1_data
    cdef {{nptype}}_t low_v, high_v
    cdef uint64_t rng, last_rng, val, mask, off, out_val, is_open

    low_arr_orig = <np.ndarray>low
    high_arr_orig = <np.ndarray>high

    is_open = not closed

    # The following code tries to cast safely, but failing that goes via
    # Python `int()` because it is very difficult to cast integers in a
    # truly safe way (i.e. so it raises on out-of-bound).
    # We correct if the interval is not closed in this step if we go the long
    # route.  (Not otherwise, since the -1 could overflow in theory.)
    if np.can_cast(low_arr_orig, np.{{otype}}):
        low_arr = <np.ndarray>np.PyArray_FROM_OTF(low_arr_orig, np.{{npctype}}, np.NPY_ALIGNED)
    else:
        low_arr = <np.ndarray>np.empty_like(low_arr_orig, dtype=np.{{otype}})
        flat = low_arr_orig.flat
        low_data = <{{nptype}}_t *>np.PyArray_DATA(low_arr)
        cnt = np.PyArray_SIZE(low_arr)
        for i in range(cnt):
            lower = int(flat[i])
            if lower < {{lb}} or lower > {{ub}}:
                raise ValueError('low is out of bounds for {{nptype}}')
            low_data[i] = lower

    del low_arr_orig

    if np.can_cast(high_arr_orig, np.{{otype}}):
        high_arr = <np.ndarray>np.PyArray_FROM_OTF(high_arr_orig, np.{{npctype}}, np.NPY_ALIGNED)
    else:
        high_arr = np.empty_like(high_arr_orig, dtype=np.{{otype}})
        flat = high_arr_orig.flat
        high_data = <{{nptype}}_t *>np.PyArray_DATA(high_arr)
        cnt = np.PyArray_SIZE(high_arr)
        for i in range(cnt):
            closed_upper = int(flat[i]) - is_open
            if closed_upper > {{ub}}:
                raise ValueError('high is out of bounds for {{nptype}}')
            if closed_upper < {{lb}}:
                raise ValueError(format_bounds_error(closed, low_arr))
            high_data[i] = closed_upper

        is_open = 0  # we applied is_open in this path already

    del high_arr_orig

    # Since we have the largest supported integer dtypes, they must be within
    # range at this point; otherwise conversion would have failed.  Check that
    # it is never true that `high <= low`` if closed and `high < low` if not
    if not is_open:
        low_high_comp = np.greater
    else:
        low_high_comp = np.greater_equal

    if np.any(low_high_comp(low_arr, high_arr)):
        raise ValueError(format_bounds_error(closed, low_arr))

    if size is not None:
        out_arr = <np.ndarray>np.empty(size, np.{{otype}})
    else:
        it = np.PyArray_MultiIterNew2(low_arr, high_arr)
        out_arr = <np.ndarray>np.empty(it.shape, np.{{otype}})

    it = np.PyArray_MultiIterNew3(low_arr, high_arr, out_arr)
    out_data = <uint64_t *>np.PyArray_DATA(out_arr)
    n = np.PyArray_SIZE(out_arr)
    mask = last_rng = 0
    with lock, nogil:
        for i in range(n):
            low_v = (<{{nptype}}_t*>np.PyArray_MultiIter_DATA(it, 0))[0]
            high_v = (<{{nptype}}_t*>np.PyArray_MultiIter_DATA(it, 1))[0]
            # Generator produces values on the closed int [off, off+rng]
            rng = <{{utype}}_t>((high_v - is_open) - low_v)
            off = <{{utype}}_t>(<{{nptype}}_t>low_v)

            if rng != last_rng:
                mask = _gen_mask(rng)
            out_data[i] = random_bounded_uint64(state, off, rng, mask, use_masked)

            np.PyArray_MultiIter_NEXT(it)

    return out_arr
{{endfor}}
{{
py:
type_info = (('uint64', 'uint64', '0x0ULL', '0xFFFFFFFFFFFFFFFFULL'),
             ('uint32', 'uint32', '0x0UL', '0XFFFFFFFFUL'),
             ('uint16', 'uint16', '0x0UL', '0XFFFFUL'),
             ('uint8', 'uint8', '0x0UL', '0XFFUL'),
             ('bool', 'bool', '0x0UL', '0x1UL'),
             ('int64', 'uint64', '-0x8000000000000000LL', '0x7FFFFFFFFFFFFFFFL'),
             ('int32', 'uint32', '-0x80000000L', '0x7FFFFFFFL'),
             ('int16', 'uint16', '-0x8000L', '0x7FFFL' ),
             ('int8', 'uint8', '-0x80L', '0x7FL' )
)}}
{{for  nptype, utype, lb, ub in type_info}}
{{ py: otype = nptype + '_' if nptype == 'bool' else nptype }}
cdef object _rand_{{nptype}}(object low, object high, object size,
                             bint use_masked, bint closed,
                             bitgen_t *state, object lock):
    """
    _rand_{{nptype}}(low, high, size, use_masked, *state, lock)

    Return random `np.{{otype}}` integers from `low` (inclusive) to `high` (exclusive).

    Return random integers from the "discrete uniform" distribution in the
    interval [`low`, `high`).  If `high` is None (the default),
    then results are from [0, `low`). On entry the arguments are presumed
    to have been validated for size and order for the `np.{{otype}}` type.

    Parameters
    ----------
    low : int or array-like
        Lowest (signed) integer to be drawn from the distribution (unless
        ``high=None``, in which case this parameter is the *highest* such
        integer).
    high : int or array-like
        If provided, one above the largest (signed) integer to be drawn from the
        distribution (see above for behavior if ``high=None``).
    size : int or tuple of ints
        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
        single value is returned.
    use_masked : bool
        If True then rejection sampling with a range mask is used else Lemire's algorithm is used.
    closed : bool
        If True then sample from [low, high].  If False, sample [low, high)
    state : bit generator
        Bit generator state to use in the core random number generators
    lock : threading.Lock
        Lock to prevent multiple using a single generator simultaneously

    Returns
    -------
    out : python scalar or ndarray of np.{{otype}}
          `size`-shaped array of random integers from the appropriate
          distribution, or a single such random int if `size` not provided.

    Notes
    -----
    The internal integer generator produces values from the closed
    interval [low, high-(not closed)].  This requires some care since
    high can be out-of-range for {{utype}}. The scalar path leaves
    integers as Python integers until the 1 has been subtracted to
    avoid needing to cast to a larger type.
    """
    cdef np.ndarray out_arr, low_arr, high_arr
    cdef {{utype}}_t rng, off, out_val
    cdef {{utype}}_t *out_data
    cdef np.npy_intp i, n, cnt

    if size is not None:
        if (np.prod(size) == 0):
            return np.empty(size, dtype=np.{{otype}})

    low_arr = <np.ndarray>np.array(low, copy=False)
    high_arr = <np.ndarray>np.array(high, copy=False)
    low_ndim = np.PyArray_NDIM(low_arr)
    high_ndim = np.PyArray_NDIM(high_arr)
    if low_ndim == 0 and high_ndim == 0:
        low = int(low_arr)
        high = int(high_arr)
        # Subtract 1 since internal generator produces on closed interval [low, high]
        if not closed:
            high -= 1

        if low < {{lb}}:
            raise ValueError("low is out of bounds for {{nptype}}")
        if high > {{ub}}:
            raise ValueError("high is out of bounds for {{nptype}}")
        if low > high:  # -1 already subtracted, closed interval
            raise ValueError(format_bounds_error(closed, low))

        rng = <{{utype}}_t>(high - low)
        off = <{{utype}}_t>(<{{nptype}}_t>low)
        if size is None:
            with lock:
                random_bounded_{{utype}}_fill(state, off, rng, 1, use_masked, &out_val)
            return np.{{otype}}(<{{nptype}}_t>out_val)
        else:
            out_arr = <np.ndarray>np.empty(size, np.{{otype}})
            cnt = np.PyArray_SIZE(out_arr)
            out_data = <{{utype}}_t *>np.PyArray_DATA(out_arr)
            with lock, nogil:
                random_bounded_{{utype}}_fill(state, off, rng, cnt, use_masked, out_data)
            return out_arr
    return _rand_{{nptype}}_broadcast(low_arr, high_arr, size, use_masked, closed, state, lock)
{{endfor}}