Skip to content

Commit e5cbcf5

Browse files
Merge pull request #196 from IntelPython/fix/geometric-numpy-convention
Count trials in geometric, as the docstring and NumPy do
2 parents 8264f7a + d7b5ada commit e5cbcf5

3 files changed

Lines changed: 32 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
* Replaced the `mklrand_py_helper.h` shim with `cpython.bytes` cimports and dropped the no-op `__future__` import, since the project requires Python 3.10+ [gh-192](https://github.com/IntelPython/mkl_random/pull/192)
3232
* `negative_binomial` now rejects `p == 0`, which MKL does not support, with a `ValueError`; the documented range of `p` is `(0, 1]` [gh-195](https://github.com/IntelPython/mkl_random/pull/195)
3333
* `poisson`, `negative_binomial`, `geometric` and `logseries` now reject `NaN` parameters with a `ValueError`, as NumPy does, instead of passing them to MKL [gh-195](https://github.com/IntelPython/mkl_random/pull/195)
34+
* `geometric` samples are now one larger than before for every `p`, so results for a fixed seed change; `geometric(p) - 1` reproduces the old values. All other streams are unaffected [gh-196](https://github.com/IntelPython/mkl_random/pull/196)
3435

3536
### Fixed
3637
* Fixed `poisson` with a scalar `lam == 0` returning uninitialized memory as random values, since MKL requires `lam > 0`. The same applied to array-valued `lam` with `method="PTPE"`. Both now return zeros, as the default array path and NumPy already did [gh-195](https://github.com/IntelPython/mkl_random/pull/195)
@@ -56,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5657
* Fixed the `tomaxint` example, which compared against the Python 2 `sys.maxint` [gh-192](https://github.com/IntelPython/mkl_random/pull/192)
5758
* Fixed the remaining Python 2 references in the `random_sample`, `random_integers` and `randint` docstrings and in the `zipf` implementation comment: the `<type '...'>` reprs, `np.int` and `sys.maxint` [gh-192](https://github.com/IntelPython/mkl_random/pull/192)
5859
* Fixed the `random_integers` docstring, which described the result as `numpy.int_` rather than a C `long`, documented the wrong scalar return type, and passed a float `size` that no longer works [gh-192](https://github.com/IntelPython/mkl_random/pull/192)
60+
* Fixed `geometric` returning the number of failures before the first success instead of the number of trials, which contradicted its own docstring and NumPy. The support now starts at `1`, `P(1)` is `p` and the mean is `1 / p` [gh-196](https://github.com/IntelPython/mkl_random/pull/196)
5961

6062
### Removed
6163
* Removed the `python-gil` constraint from the conda recipes, which pinned `mkl_random` to GIL-enabled Python 3.14 builds [gh-159](https://github.com/IntelPython/mkl_random/pull/159)

‎mkl_random/src/mkl_distributions.cpp‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,10 @@ void irk_multinomial_vec(irk_state *state,
13061306
void irk_geometric_vec(irk_state *state, npy_intp len, int *res, const double p)
13071307
{
13081308
int err = 0;
1309+
/* res and len advance in the chunk loop, so the +1 pass below
1310+
needs the whole range */
1311+
npy_intp i = 0, total = len;
1312+
int *out = res;
13091313

13101314
if (len < 1)
13111315
return;
@@ -1323,12 +1327,18 @@ void irk_geometric_vec(irk_state *state, npy_intp len, int *res, const double p)
13231327
err = viRngGeometric(VSL_RNG_METHOD_GEOMETRIC_ICDF, state->stream, len,
13241328
res, p);
13251329
assert(err == VSL_STATUS_OK);
1330+
1331+
/* MKL counts the failures, the number of trials also counts the
1332+
success that ended them; overflowed values, which MKL writes as
1333+
negative, are kept */
1334+
DIST_PRAGMA_VECTOR
1335+
for (i = 0; i < total; ++i)
1336+
out[i] += (out[i] >= 0 && out[i] < INT_MAX) ? 1 : 0;
13261337
}
13271338
else {
13281339
if (p == 1.0) {
1329-
npy_intp i;
13301340
for (i = 0; i < len; ++i)
1331-
res[i] = 0;
1341+
res[i] = 1;
13321342
}
13331343
else {
13341344
assert(p >= 0.0);

‎mkl_random/tests/test_random.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -922,10 +922,27 @@ def test_randomdist_gamma(randomdist):
922922
def test_randomdsit_geometric(randomdist):
923923
rnd.seed(randomdist.seed, brng=randomdist.brng)
924924
actual = rnd.geometric(0.123456789, size=(3, 2))
925-
desired = np.array([[0, 30], [13, 2], [4, 0]])
925+
desired = np.array([[1, 31], [14, 3], [5, 1]])
926926
np.testing.assert_array_equal(actual, desired)
927927

928928

929+
def test_geometric_counts_trials():
930+
# The number of trials, as the docstring and NumPy define it, so the
931+
# support starts at one, P(1) is p and the mean is 1 / p
932+
p = 0.35
933+
samples = rnd.MKLRandomState(1).geometric(p, 100_000)
934+
assert samples.min() == 1
935+
np.testing.assert_allclose((samples == 1).mean(), p, atol=0.01)
936+
np.testing.assert_allclose(samples.mean(), 1 / p, rtol=0.01)
937+
938+
ones = np.ones(5, dtype="int32")
939+
np.testing.assert_array_equal(rnd.geometric(1.0, 5), ones, strict=True)
940+
np.testing.assert_array_equal(rnd.geometric(np.ones(5)), ones, strict=True)
941+
942+
# MKL marks draws that overflow int with INT_MIN, which must survive the +1
943+
assert (rnd.geometric(1e-300, 3) == np.iinfo(np.int32).min).all()
944+
945+
929946
def test_geometric_exceptions():
930947
for p in (0.0, -0.5, 1.5, np.nan):
931948
assert_raises(ValueError, rnd.geometric, p)

0 commit comments

Comments
 (0)