FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

ENH: Add the capability to swap the singleton bit generator · numpy/numpy@affb903 · GitHub

Repository navigation

Commit affb903

Browse files
committed
ENH: Add the capability to swap the singleton bit generator
Add a new version or seed that supports seeding any bit gen Add set/get_bit_generator as explicity methodds to support swapping closes #21808
1 parent 3784656 commit affb903

3 files changed

Lines changed: 197 additions & 15 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
The bit generator underlying the singleton RandomState can be changed
2+
---------------------------------------------------------------------
3+
The singleton ``RandomState`` instance exposed in the ``numpy.random`` module
4+
id initialized using system-provided entropy with the ``MT19937` bit generator.
5+
The function ``set_bit_generator`` allows the default bit generator to be
6+
replaced with a user-provided bit generator. This function has been introduced to
7+
provide a method allowing seemless integration of a high-quality, modern bit generator
8+
in new code with existing code that makes use of the singleton-provided random
9+
variate generating functions.
10+
11+
The preferred method to generate reproducible random numbers is to use a modern
12+
bit generator in an instance of ``Generator``. The function ``default_rng``
13+
simplifies instantization.
14+
15+
>>> rg = np.random.default_rng(3728973198)
16+
>>> rg.random()
17+
18+
The same bit generator can then shared with the singleton instance so that
19+
calling functions in the ``random`` module will use the same bit generator.
20+
21+
>>> np.random.set_bit_generator(rg.bit_generator)
22+
>>> np.random.normal()
23+
24+
The swap is permanent (until reversed) and so any call to functions
25+
in the ``random`` module will use the new bit generator.

‎numpy/random/mtrand.pyx‎

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,7 @@ cdef class RandomState:
186186
else:
187187
bit_generator = seed
188188

189-
self._bit_generator = bit_generator
190-
capsule = bit_generator.capsule
191-
cdef const char *name = "BitGenerator"
192-
if not PyCapsule_IsValid(capsule, name):
193-
raise ValueError("Invalid bit generator. The bit generator must "
194-
"be instantized.")
195-
self._bitgen = (<bitgen_t *> PyCapsule_GetPointer(capsule, name))[0]
196-
self._aug_state.bit_generator = &self._bitgen
197-
self._reset_gauss()
198-
self.lock = bit_generator.lock
189+
self._initialize_bit_generator(bit_generator)
199190

200191
def __repr__(self):
201192
return self.__str__() + ' at 0x{:X}'.format(id(self))
@@ -217,13 +208,25 @@ cdef class RandomState:
217208
from ._pickle import __randomstate_ctor
218209
return __randomstate_ctor, (state['bit_generator'],), state
219210

211+
cdef _initialize_bit_generator(self, bit_generator):
212+
self._bit_generator = bit_generator
213+
capsule = bit_generator.capsule
214+
cdef const char *name = "BitGenerator"
215+
if not PyCapsule_IsValid(capsule, name):
216+
raise ValueError("Invalid bit generator. The bit generator must "
217+
"be instantized.")
218+
self._bitgen = (<bitgen_t *> PyCapsule_GetPointer(capsule, name))[0]
219+
self._aug_state.bit_generator = &self._bitgen
220+
self._reset_gauss()
221+
self.lock = bit_generator.lock
222+
220223
cdef _reset_gauss(self):
221224
self._aug_state.has_gauss = 0
222225
self._aug_state.gauss = 0.0
223226

224227
def seed(self, seed=None):
225228
"""
226-
seed(self, seed=None)
229+
seed(seed=None)
227230
228231
Reseed a legacy MT19937 BitGenerator
229232
@@ -248,7 +251,7 @@ cdef class RandomState:
248251

249252
def get_state(self, legacy=True):
250253
"""
251-
get_state()
254+
get_state(legacy=True)
252255
253256
Return a tuple representing the internal state of the generator.
254257
@@ -258,12 +261,13 @@ cdef class RandomState:
258261
----------
259262
legacy : bool, optional
260263
Flag indicating to return a legacy tuple state when the BitGenerator
261-
is MT19937, instead of a dict.
264+
is MT19937, instead of a dict. Raises ValueError if the underlying
265+
bit generator is not an instance of MT19937.
262266
263267
Returns
264268
-------
265269
out : {tuple(str, ndarray of 624 uints, int, int, float), dict}
266-
The returned tuple has the following items:
270+
If legacy is True, the returned tuple has the following items:
267271
268272
1. the string 'MT19937'.
269273
2. a 1-D array of 624 unsigned integer keys.
@@ -293,6 +297,11 @@ cdef class RandomState:
293297
legacy = False
294298
st['has_gauss'] = self._aug_state.has_gauss
295299
st['gauss'] = self._aug_state.gauss
300+
if legacy and not isinstance(self._bit_generator, _MT19937):
301+
raise ValueError(
302+
"legacy can only be True when the underlyign bitgenerator is "
303+
"an instance of MT19937."
304+
)
296305
if legacy:
297306
return (st['bit_generator'], st['state']['key'], st['state']['pos'],
298307
st['has_gauss'], st['gauss'])
@@ -4689,7 +4698,6 @@ random = _rand.random
46894698
random_integers = _rand.random_integers
46904699
random_sample = _rand.random_sample
46914700
rayleigh = _rand.rayleigh
4692-
seed = _rand.seed
46934701
set_state = _rand.set_state
46944702
shuffle = _rand.shuffle
46954703
standard_cauchy = _rand.standard_cauchy
@@ -4704,6 +4712,85 @@ wald = _rand.wald
47044712
weibull = _rand.weibull
47054713
zipf = _rand.zipf
47064714

4715+
def seed(seed=None):
4716+
"""
4717+
seed(seed=None)
4718+
4719+
Reseed the singleton RandomState instance.
4720+
4721+
Notes
4722+
-----
4723+
This is a convenience, legacy function that exists to support
4724+
older code that uses the singleton RandomState. Best practice
4725+
is to use a dedicated ``Generator`` instance rather than
4726+
the random variate generation methods exposed directly in
4727+
the random module.
4728+
4729+
See Also
4730+
--------
4731+
numpy.random.Generator
4732+
"""
4733+
if isinstance(_rand._bit_generator, _MT19937):
4734+
return _rand.seed(seed)
4735+
else:
4736+
bg_type = type(_rand._bit_generator)
4737+
_rand._bit_generator.state = bg_type(seed).state
4738+
4739+
def get_bit_generator():
4740+
"""
4741+
Returns the singleton RandomState's bit generator
4742+
4743+
Returns
4744+
-------
4745+
BitGenerator
4746+
The bit generator that underlies the singleton RandomState instance
4747+
4748+
Notes
4749+
-----
4750+
The singleton RandomState provides the random variate generators in the
4751+
NumPy random namespace. This function, and its counterpart set method,
4752+
provides a path to hot-swap the default MT19937 bit generator with a
4753+
user provided alternative. These function are intended to provide
4754+
a continuous path where a single underlying bit generator can be
4755+
used both with an instance of ``Generator`` and with the singleton
4756+
instance of RandomState.
4757+
4758+
See Also
4759+
--------
4760+
set_bit_generator
4761+
numpy.random.Generator
4762+
"""
4763+
return _rand._bit_generator
4764+
4765+
def set_bit_generator(bitgen):
4766+
"""
4767+
Sets the singleton RandomState's bit generator
4768+
4769+
Parameters
4770+
----------
4771+
bitgen
4772+
A bit generator instance
4773+
4774+
Notes
4775+
-----
4776+
The singleton RandomState provides the random variate generators in the
4777+
NumPy random namespace. This function, and its counterpart get method,
4778+
provides a path to hot-swap the default MT19937 bit generator with a
4779+
user provided alternative. These function are intended to provide
4780+
a continuous path where a single underlying bit generator can be
4781+
used both with an instance of ``Generator`` and with the singleton
4782+
instance of RandomState.
4783+
4784+
See Also
4785+
--------
4786+
get_bit_generator
4787+
numpy.random.Generator
4788+
"""
4789+
cdef RandomState singleton
4790+
singleton = _rand
4791+
singleton._initialize_bit_generator(bitgen)
4792+
4793+
47074794
# Old aliases that should not be removed
47084795
def sample(*args, **kwargs):
47094796
"""
@@ -4730,6 +4817,7 @@ __all__ = [
47304817
'f',
47314818
'gamma',
47324819
'geometric',
4820+
'get_bit_generator',
47334821
'get_state',
47344822
'gumbel',
47354823
'hypergeometric',
@@ -4757,6 +4845,7 @@ __all__ = [
47574845
'rayleigh',
47584846
'sample',
47594847
'seed',
4848+
'set_bit_generator',
47604849
'set_state',
47614850
'shuffle',
47624851
'standard_cauchy',

‎numpy/random/tests/test_randomstate.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ def int_func(request):
5353
INT_FUNC_HASHES[request.param])
5454

5555

56+
@pytest.fixture
57+
def restore_singleton_bitgen():
58+
"""Ensures that the singleton bitgen is restored after a test"""
59+
orig_bitgen = np.random.get_bit_generator()
60+
yield
61+
np.random.set_bit_generator(orig_bitgen)
62+
63+
5664
def assert_mt19937_state_equal(a, b):
5765
assert_equal(a['bit_generator'], b['bit_generator'])
5866
assert_array_equal(a['state']['key'], b['state']['key'])
@@ -2020,3 +2028,63 @@ def test_broadcast_size_error():
20202028
random.binomial([1, 2], 0.3, size=(2, 1))
20212029
with pytest.raises(ValueError):
20222030
random.binomial([1, 2], [0.3, 0.7], size=(2, 1))
2031+
2032+
2033+
def test_hot_swap(restore_singleton_bitgen):
2034+
# GH 21808
2035+
def_bg = np.random.default_rng(0)
2036+
bg = def_bg.bit_generator
2037+
np.random.set_bit_generator(bg)
2038+
assert isinstance(np.random.mtrand._rand._bit_generator, type(bg))
2039+
2040+
second_bg = np.random.get_bit_generator()
2041+
assert bg is second_bg
2042+
2043+
2044+
def test_seed_alt_bit_gen(restore_singleton_bitgen):
2045+
# GH 21808
2046+
bg = PCG64(0)
2047+
np.random.set_bit_generator(bg)
2048+
state = np.random.get_state(legacy=False)
2049+
np.random.seed(1)
2050+
new_state = np.random.get_state(legacy=False)
2051+
print(state)
2052+
print(new_state)
2053+
assert state["bit_generator"] == "PCG64"
2054+
assert state["state"]["state"] != new_state["state"]["state"]
2055+
assert state["state"]["inc"] != new_state["state"]["inc"]
2056+
2057+
2058+
def test_state_error_alt_bit_gen(restore_singleton_bitgen):
2059+
# GH 21808
2060+
state = np.random.get_state()
2061+
bg = PCG64(0)
2062+
np.random.set_bit_generator(bg)
2063+
with pytest.raises(ValueError, match="state must be for a PCG64"):
2064+
np.random.set_state(state)
2065+
2066+
2067+
def test_swap_worked(restore_singleton_bitgen):
2068+
# GH 21808
2069+
np.random.seed(98765)
2070+
vals = np.random.randint(0, 2 ** 30, 10)
2071+
bg = PCG64(0)
2072+
state = bg.state
2073+
np.random.set_bit_generator(bg)
2074+
state_direct = np.random.get_state(legacy=False)
2075+
for field in state:
2076+
assert state[field] == state_direct[field]
2077+
np.random.seed(98765)
2078+
pcg_vals = np.random.randint(0, 2 ** 30, 10)
2079+
assert not np.all(vals == pcg_vals)
2080+
new_state = bg.state
2081+
assert new_state["state"]["state"] != state["state"]["state"]
2082+
assert new_state["state"]["inc"] == new_state["state"]["inc"]
2083+
2084+
2085+
def test_swapped_singleton_against_direct(restore_singleton_bitgen):
2086+
np.random.set_bit_generator(PCG64(98765))
2087+
singleton_vals = np.random.randint(0, 2 ** 30, 10)
2088+
rg = np.random.RandomState(PCG64(98765))
2089+
non_singleton_vals = rg.randint(0, 2 ** 30, 10)
2090+
assert_equal(non_singleton_vals, singleton_vals)

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL