Skip to content

Commit 8716c83

Browse files
authored
Merge pull request #8 from bashtage/sync-randomgen
MAINT, ENH: Sync randomgen
2 parents 610b5e0 + c8bc266 commit 8716c83

File tree

7 files changed

+151
-65
lines changed

7 files changed

+151
-65
lines changed

numpy/random/distributions.pxd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,6 @@ cdef extern from "src/distributions/distributions.h":
144144
np.npy_bool off, np.npy_bool rng, np.npy_intp cnt,
145145
bint use_masked,
146146
np.npy_bool *out) nogil
147+
148+
void random_multinomial(brng_t *brng_state, int64_t n, int64_t *mnix,
149+
double *pix, np.npy_intp d, binomial_t *binomial) nogil

numpy/random/generator.pyx

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3642,7 +3642,7 @@ cdef class RandomGenerator:
36423642
x.shape = tuple(final_shape)
36433643
return x
36443644

3645-
def multinomial(self, np.npy_intp n, object pvals, size=None):
3645+
def multinomial(self, object n, object pvals, size=None):
36463646
"""
36473647
multinomial(n, pvals, size=None)
36483648
@@ -3658,7 +3658,7 @@ cdef class RandomGenerator:
36583658
36593659
Parameters
36603660
----------
3661-
n : int
3661+
n : int or array-like of ints
36623662
Number of experiments.
36633663
pvals : sequence of floats, length p
36643664
Probabilities of each of the ``p`` different outcomes. These
@@ -3697,6 +3697,18 @@ cdef class RandomGenerator:
36973697
For the first run, we threw 3 times 1, 4 times 2, etc. For the second,
36983698
we threw 2 times 1, 4 times 2, etc.
36993699
3700+
Now, do one experiment throwing the dice 10 time, and 10 times again,
3701+
and another throwing the dice 20 times, and 20 times again:
3702+
3703+
>>> np.random.multinomial([[10], [20]], [1/6.]*6, size=2)
3704+
array([[[2, 4, 0, 1, 2, 1],
3705+
[1, 3, 0, 3, 1, 2]],
3706+
[[1, 4, 4, 4, 4, 3],
3707+
[3, 3, 2, 5, 5, 2]]]) # random
3708+
3709+
The first array shows the outcomes of throwing the dice 10 times, and
3710+
the second shows the outcomes from throwing the dice 20 times.
3711+
37003712
A loaded die is more likely to land on number 6:
37013713
37023714
>>> np.random.multinomial(100, [1/7.]*5 + [2/7.])
@@ -3717,19 +3729,44 @@ cdef class RandomGenerator:
37173729
array([100, 0])
37183730
37193731
"""
3720-
cdef np.npy_intp d, i, j, dn, sz
3721-
cdef np.ndarray parr "arrayObject_parr", mnarr "arrayObject_mnarr"
3732+
3733+
cdef np.npy_intp d, i, sz, offset
3734+
cdef np.ndarray parr, mnarr, on, temp_arr
37223735
cdef double *pix
37233736
cdef int64_t *mnix
3724-
cdef double Sum
3737+
cdef int64_t ni
3738+
cdef np.broadcast it
37253739

37263740
d = len(pvals)
3741+
on = <np.ndarray>np.PyArray_FROM_OTF(n, np.NPY_INT64, np.NPY_ALIGNED)
37273742
parr = <np.ndarray>np.PyArray_FROM_OTF(pvals, np.NPY_DOUBLE, np.NPY_ALIGNED)
37283743
pix = <double*>np.PyArray_DATA(parr)
3729-
3744+
check_array_constraint(parr, 'pvals', CONS_BOUNDED_0_1)
37303745
if kahan_sum(pix, d-1) > (1.0 + 1e-12):
37313746
raise ValueError("sum(pvals[:-1]) > 1.0")
37323747

3748+
if np.PyArray_NDIM(on) != 0: # vector
3749+
check_array_constraint(on, 'n', CONS_NON_NEGATIVE)
3750+
if size is None:
3751+
it = np.PyArray_MultiIterNew1(on)
3752+
else:
3753+
temp = np.empty(size, dtype=np.int8)
3754+
temp_arr = <np.ndarray>temp
3755+
it = np.PyArray_MultiIterNew2(on, temp_arr)
3756+
shape = it.shape + (d,)
3757+
multin = np.zeros(shape, dtype=np.int64)
3758+
mnarr = <np.ndarray>multin
3759+
mnix = <int64_t*>np.PyArray_DATA(mnarr)
3760+
offset = 0
3761+
sz = it.size
3762+
with self.lock, nogil:
3763+
for i in range(sz):
3764+
ni = (<int64_t*>np.PyArray_MultiIter_DATA(it, 0))[0]
3765+
random_multinomial(self._brng, ni, &mnix[offset], pix, d, self._binomial)
3766+
offset += d
3767+
np.PyArray_MultiIter_NEXT(it)
3768+
return multin
3769+
37333770
if size is None:
37343771
shape = (d,)
37353772
else:
@@ -3742,23 +3779,13 @@ cdef class RandomGenerator:
37423779
mnarr = <np.ndarray>multin
37433780
mnix = <int64_t*>np.PyArray_DATA(mnarr)
37443781
sz = np.PyArray_SIZE(mnarr)
3745-
3782+
ni = n
3783+
check_constraint(ni, 'n', CONS_NON_NEGATIVE)
3784+
offset = 0
37463785
with self.lock, nogil:
3747-
i = 0
3748-
while i < sz:
3749-
Sum = 1.0
3750-
dn = n
3751-
for j in range(d-1):
3752-
mnix[i+j] = random_binomial(self._brng, pix[j]/Sum, dn,
3753-
self._binomial)
3754-
dn = dn - mnix[i+j]
3755-
if dn <= 0:
3756-
break
3757-
Sum = Sum - pix[j]
3758-
if dn > 0:
3759-
mnix[i+d-1] = dn
3760-
3761-
i = i + d
3786+
for i in range(sz // d):
3787+
random_multinomial(self._brng, ni, &mnix[offset], pix, d, self._binomial)
3788+
offset += d
37623789

37633790
return multin
37643791

numpy/random/mtrand.pyx

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3790,16 +3790,16 @@ cdef class RandomState:
37903790
array([100, 0])
37913791
37923792
"""
3793-
cdef np.npy_intp d, i, j, dn, sz
3794-
cdef np.ndarray parr "arrayObject_parr", mnarr "arrayObject_mnarr"
3793+
cdef np.npy_intp d, i, sz, offset
3794+
cdef np.ndarray parr, mnarr
37953795
cdef double *pix
37963796
cdef int64_t *mnix
3797-
cdef double Sum
3797+
cdef int64_t ni
37983798

37993799
d = len(pvals)
38003800
parr = <np.ndarray>np.PyArray_FROM_OTF(pvals, np.NPY_DOUBLE, np.NPY_ALIGNED)
38013801
pix = <double*>np.PyArray_DATA(parr)
3802-
3802+
check_array_constraint(parr, 'pvals', CONS_BOUNDED_0_1)
38033803
if kahan_sum(pix, d-1) > (1.0 + 1e-12):
38043804
raise ValueError("sum(pvals[:-1]) > 1.0")
38053805

@@ -3815,23 +3815,13 @@ cdef class RandomState:
38153815
mnarr = <np.ndarray>multin
38163816
mnix = <int64_t*>np.PyArray_DATA(mnarr)
38173817
sz = np.PyArray_SIZE(mnarr)
3818-
3818+
ni = n
3819+
check_constraint(ni, 'n', CONS_NON_NEGATIVE)
3820+
offset = 0
38193821
with self.lock, nogil:
3820-
i = 0
3821-
while i < sz:
3822-
Sum = 1.0
3823-
dn = n
3824-
for j in range(d-1):
3825-
mnix[i+j] = random_binomial(self._brng, pix[j]/Sum, dn,
3826-
self._binomial)
3827-
dn = dn - mnix[i+j]
3828-
if dn <= 0:
3829-
break
3830-
Sum = Sum - pix[j]
3831-
if dn > 0:
3832-
mnix[i+d-1] = dn
3833-
3834-
i = i + d
3822+
for i in range(sz // d):
3823+
random_multinomial(self._brng, ni, &mnix[offset], pix, d, self._binomial)
3824+
offset += d
38353825

38363826
return multin
38373827

@@ -4152,9 +4142,7 @@ randn = _rand.randn
41524142
random = _rand.random_sample
41534143
random_integers = _rand.random_integers
41544144
random_sample = _rand.random_sample
4155-
ranf = _rand.random_sample
41564145
rayleigh = _rand.rayleigh
4157-
sample = _rand.random_sample
41584146
seed = _rand.seed
41594147
set_state = _rand.set_state
41604148
shuffle = _rand.shuffle
@@ -4170,6 +4158,21 @@ wald = _rand.wald
41704158
weibull = _rand.weibull
41714159
zipf = _rand.zipf
41724160

4161+
# Old aliases that should not be removed
4162+
def sample(*args, **kwargs):
4163+
"""
4164+
This is an alias of `random_sample`. See `random_sample` for the complete
4165+
documentation.
4166+
"""
4167+
return _rand.random_sample(*args, **kwargs)
4168+
4169+
def ranf(*args, **kwargs):
4170+
"""
4171+
This is an alias of `random_sample`. See `random_sample` for the complete
4172+
documentation.
4173+
"""
4174+
return _rand.random_sample(*args, **kwargs)
4175+
41734176
__all__ = [
41744177
'beta',
41754178
'binomial',
@@ -4203,7 +4206,9 @@ __all__ = [
42034206
'randn',
42044207
'random_integers',
42054208
'random_sample',
4209+
'ranf',
42064210
'rayleigh',
4211+
'sample',
42074212
'seed',
42084213
'set_state',
42094214
'shuffle',

numpy/random/src/distributions/distributions.c

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,25 +1048,31 @@ int64_t random_geometric(brng_t *brng_state, double p) {
10481048
}
10491049

10501050
int64_t random_zipf(brng_t *brng_state, double a) {
1051-
double T, U, V;
1052-
int64_t X;
10531051
double am1, b;
10541052

10551053
am1 = a - 1.0;
10561054
b = pow(2.0, am1);
1057-
do {
1058-
U = 1.0 - next_double(brng_state);
1059-
V = next_double(brng_state);
1060-
X = (int64_t)floor(pow(U, -1.0 / am1));
1061-
/* The real result may be above what can be represented in a int64.
1062-
* It will get casted to -sys.maxint-1. Since this is
1063-
* a straightforward rejection algorithm, we can just reject this value
1064-
* in the rejection condition below. This function then models a Zipf
1055+
while (1) {
1056+
double T, U, V, X;
1057+
1058+
U = 1.0 - random_double(brng_state);
1059+
V = random_double(brng_state);
1060+
X = floor(pow(U, -1.0 / am1));
1061+
/*
1062+
* The real result may be above what can be represented in a signed
1063+
* long. Since this is a straightforward rejection algorithm, we can
1064+
* just reject this value. This function then models a Zipf
10651065
* distribution truncated to sys.maxint.
10661066
*/
1067+
if (X > LONG_MAX || X < 1.0) {
1068+
continue;
1069+
}
1070+
10671071
T = pow(1.0 + 1.0 / X, am1);
1068-
} while (((V * X * (T - 1.0) / (b - 1.0)) > (T / b)) || X < 1);
1069-
return X;
1072+
if (V * X * (T - 1.0) / (b - 1.0) <= T / b) {
1073+
return (long)X;
1074+
}
1075+
}
10701076
}
10711077

10721078
double random_triangular(brng_t *brng_state, double left, double mode,
@@ -1179,8 +1185,10 @@ int64_t random_hypergeometric(brng_t *brng_state, int64_t good, int64_t bad,
11791185
int64_t sample) {
11801186
if (sample > 10) {
11811187
return random_hypergeometric_hrua(brng_state, good, bad, sample);
1182-
} else {
1188+
} else if (sample > 0) {
11831189
return random_hypergeometric_hyp(brng_state, good, bad, sample);
1190+
} else {
1191+
return 0;
11841192
}
11851193
}
11861194

@@ -1809,3 +1817,21 @@ void random_bounded_bool_fill(brng_t *brng_state, npy_bool off, npy_bool rng,
18091817
out[i] = buffered_bounded_bool(brng_state, off, rng, mask, &bcnt, &buf);
18101818
}
18111819
}
1820+
1821+
void random_multinomial(brng_t *brng_state, int64_t n, int64_t *mnix,
1822+
double *pix, npy_intp d, binomial_t *binomial) {
1823+
double remaining_p = 1.0;
1824+
npy_intp j;
1825+
int64_t dn = n;
1826+
for (j = 0; j < (d - 1); j++) {
1827+
mnix[j] = random_binomial(brng_state, pix[j] / remaining_p, dn, binomial);
1828+
dn = dn - mnix[j];
1829+
if (dn <= 0) {
1830+
break;
1831+
}
1832+
remaining_p -= pix[j];
1833+
if (dn > 0) {
1834+
mnix[d - 1] = dn;
1835+
}
1836+
}
1837+
}

numpy/random/src/distributions/distributions.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,7 @@ DECLDIR void random_bounded_bool_fill(brng_t *brng_state, npy_bool off,
217217
npy_bool rng, npy_intp cnt,
218218
bool use_masked, npy_bool *out);
219219

220+
DECLDIR void random_multinomial(brng_t *brng_state, int64_t n, int64_t *mnix,
221+
double *pix, npy_intp d, binomial_t *binomial);
222+
220223
#endif

numpy/random/tests/test_generator_mt19937.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ def test_size(self):
9090

9191
def test_invalid_prob(self):
9292
assert_raises(ValueError, random.multinomial, 100, [1.1, 0.2])
93+
assert_raises(ValueError, random.multinomial, 100, [-.1, 0.9])
94+
95+
def test_invalid_n(self):
96+
assert_raises(ValueError, random.multinomial, -1, [0.8, 0.2])
97+
assert_raises(ValueError, random.multinomial, [-1] * 10, [0.8, 0.2])
9398

9499

95100
class TestSetState(object):
@@ -804,8 +809,7 @@ def test_geometric_exceptions(self):
804809
assert_raises(ValueError, random.geometric, [1.1] * 10)
805810
assert_raises(ValueError, random.geometric, -0.1)
806811
assert_raises(ValueError, random.geometric, [-0.1] * 10)
807-
with suppress_warnings() as sup:
808-
sup.record(RuntimeWarning)
812+
with np.errstate(invalid='ignore'):
809813
assert_raises(ValueError, random.geometric, np.nan)
810814
assert_raises(ValueError, random.geometric, [np.nan] * 10)
811815

@@ -888,8 +892,7 @@ def test_logseries(self):
888892
assert_array_equal(actual, desired)
889893

890894
def test_logseries_exceptions(self):
891-
with suppress_warnings() as sup:
892-
sup.record(RuntimeWarning)
895+
with np.errstate(invalid='ignore'):
893896
assert_raises(ValueError, random.logseries, np.nan)
894897
assert_raises(ValueError, random.logseries, [np.nan] * 10)
895898

@@ -964,8 +967,7 @@ def test_negative_binomial(self):
964967
assert_array_equal(actual, desired)
965968

966969
def test_negative_binomial_exceptions(self):
967-
with suppress_warnings() as sup:
968-
sup.record(RuntimeWarning)
970+
with np.errstate(invalid='ignore'):
969971
assert_raises(ValueError, random.negative_binomial, 100, np.nan)
970972
assert_raises(ValueError, random.negative_binomial, 100,
971973
[np.nan] * 10)
@@ -1046,8 +1048,7 @@ def test_poisson_exceptions(self):
10461048
assert_raises(ValueError, random.poisson, [lamneg] * 10)
10471049
assert_raises(ValueError, random.poisson, lambig)
10481050
assert_raises(ValueError, random.poisson, [lambig] * 10)
1049-
with suppress_warnings() as sup:
1050-
sup.record(RuntimeWarning)
1051+
with np.errstate(invalid='ignore'):
10511052
assert_raises(ValueError, random.poisson, np.nan)
10521053
assert_raises(ValueError, random.poisson, [np.nan] * 10)
10531054

@@ -1849,6 +1850,23 @@ def test_logseries(self):
18491850
assert_raises(ValueError, logseries, bad_p_one * 3)
18501851
assert_raises(ValueError, logseries, bad_p_two * 3)
18511852

1853+
def test_multinomial(self):
1854+
random.brng.seed(self.seed)
1855+
actual = random.multinomial([5, 20], [1 / 6.] * 6, size=(3, 2))
1856+
desired = np.array([[[1, 1, 1, 1, 0, 1],
1857+
[4, 5, 1, 4, 3, 3]],
1858+
[[1, 1, 1, 0, 0, 2],
1859+
[2, 0, 4, 3, 7, 4]],
1860+
[[1, 2, 0, 0, 2, 2],
1861+
[3, 2, 3, 4, 2, 6]]], dtype=np.int64)
1862+
assert_array_equal(actual, desired)
1863+
1864+
random.brng.seed(self.seed)
1865+
actual = random.multinomial([5, 20], [1 / 6.] * 6)
1866+
desired = np.array([[1, 1, 1, 1, 0, 1],
1867+
[4, 5, 1, 4, 3, 3]], dtype=np.int64)
1868+
assert_array_equal(actual, desired)
1869+
18521870

18531871
class TestThread(object):
18541872
# make sure each state produces the same sequence even in threads

numpy/random/tests/test_randomstate.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ def test_size(self):
112112

113113
def test_invalid_prob(self):
114114
assert_raises(ValueError, random.multinomial, 100, [1.1, 0.2])
115+
assert_raises(ValueError, random.multinomial, 100, [-.1, 0.9])
116+
117+
def test_invalid_n(self):
118+
assert_raises(ValueError, random.multinomial, -1, [0.8, 0.2])
115119

116120

117121
class TestSetState(object):

0 commit comments

Comments
 (0)