diff options
author | Charles Harris <charlesr.harris@gmail.com> | 2023-02-23 18:01:17 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-23 18:01:17 -0500 |
commit | d92cc2d1c7c7153525e03c4d10377714d85cfde6 (patch) | |
tree | 4a174d573ecf93af648272417517820f34277c50 /numpy/random | |
parent | 56eee255dba30eeeb084098b37554052c64e84b5 (diff) | |
parent | 4e6f77d6f9a0cef98158af167a80862205662c0f (diff) | |
download | numpy-d92cc2d1c7c7153525e03c4d10377714d85cfde6.tar.gz |
Merge pull request #23195 from seberg/public-rng-spawn
API: Add `rng.spawn()`, `bit_gen.spawn()`, and `bit_gen.seed_seq`
Diffstat (limited to 'numpy/random')
-rw-r--r-- | numpy/random/_generator.pyi | 1 | ||||
-rw-r--r-- | numpy/random/_generator.pyx | 54 | ||||
-rw-r--r-- | numpy/random/bit_generator.pyi | 3 | ||||
-rw-r--r-- | numpy/random/bit_generator.pyx | 59 | ||||
-rw-r--r-- | numpy/random/tests/test_direct.py | 40 |
5 files changed, 157 insertions, 0 deletions
diff --git a/numpy/random/_generator.pyi b/numpy/random/_generator.pyi index f0d814fef..23c04e472 100644 --- a/numpy/random/_generator.pyi +++ b/numpy/random/_generator.pyi @@ -72,6 +72,7 @@ class Generator: def __reduce__(self) -> tuple[Callable[[str], Generator], tuple[str], dict[str, Any]]: ... @property def bit_generator(self) -> BitGenerator: ... + def spawn(self, n_children: int) -> list[Generator]: ... def bytes(self, length: int) -> bytes: ... @overload def standard_normal( # type: ignore[misc] diff --git a/numpy/random/_generator.pyx b/numpy/random/_generator.pyx index 83a4b2ad5..faf19eaf2 100644 --- a/numpy/random/_generator.pyx +++ b/numpy/random/_generator.pyx @@ -238,6 +238,58 @@ cdef class Generator: """ return self._bit_generator + def spawn(self, int n_children): + """ + Create new independent child generators. + + See :ref:`seedsequence-spawn` for additional notes on spawning + children. + + .. versionadded:: 1.25.0 + + Returns + ------- + child_generators : list of Generators + + Raises + ------ + TypeError + When the underlying SeedSequence does not implement spawning. + + See Also + -------- + random.BitGenerator.spawn, random.SeedSequence.spawn : + Equivalent method on the bit generator and seed sequence. + bit_generator : + The bit generator instance used by the generator. + + Examples + -------- + Starting from a seeded default generator: + + >>> # High quality entropy created with: f"0x{secrets.randbits(128):x}" + >>> entropy = 0x3034c61a9ae04ff8cb62ab8ec2c4b501 + >>> rng = np.random.default_rng(entropy) + + Create two new generators for example for parallel executation: + + >>> child_rng1, child_rng2 = rng.spawn(2) + + Drawn numbers from each are independent but derived from the initial + seeding entropy: + + >>> rng.uniform(), child_rng1.uniform(), child_rng2.uniform() + (0.19029263503854454, 0.9475673279178444, 0.4702687338396767) + + It is safe to spawn additional children from the original ``rng`` or + the children: + + >>> more_child_rngs = rng.spawn(20) + >>> nested_spawn = child_rng1.spawn(20) + + """ + return [type(self)(g) for g in self._bit_generator.spawn(n_children)] + def random(self, size=None, dtype=np.float64, out=None): """ random(size=None, dtype=np.float64, out=None) @@ -4825,6 +4877,8 @@ def default_rng(seed=None): ----- If ``seed`` is not a `BitGenerator` or a `Generator`, a new `BitGenerator` is instantiated. This function does not manage a default global instance. + + See :ref:`seeding_and_entropy` for more information about seeding. Examples -------- diff --git a/numpy/random/bit_generator.pyi b/numpy/random/bit_generator.pyi index e6e3b10cd..8b9779cad 100644 --- a/numpy/random/bit_generator.pyi +++ b/numpy/random/bit_generator.pyi @@ -96,6 +96,9 @@ class BitGenerator(abc.ABC): def state(self) -> Mapping[str, Any]: ... @state.setter def state(self, value: Mapping[str, Any]) -> None: ... + @property + def seed_seq(self) -> ISeedSequence: ... + def spawn(self, n_children: int) -> list[BitGenerator]: ... @overload def random_raw(self, size: None = ..., output: Literal[True] = ...) -> int: ... # type: ignore[misc] @overload diff --git a/numpy/random/bit_generator.pyx b/numpy/random/bit_generator.pyx index 47804c487..06f8c9753 100644 --- a/numpy/random/bit_generator.pyx +++ b/numpy/random/bit_generator.pyx @@ -212,6 +212,9 @@ class ISpawnableSeedSequence(ISeedSequence): Spawn a number of child `SeedSequence` s by extending the `spawn_key`. + See :ref:`seedsequence-spawn` for additional notes on spawning + children. + Parameters ---------- n_children : int @@ -451,6 +454,9 @@ cdef class SeedSequence(): Spawn a number of child `SeedSequence` s by extending the `spawn_key`. + See :ref:`seedsequence-spawn` for additional notes on spawning + children. + Parameters ---------- n_children : int @@ -458,6 +464,12 @@ cdef class SeedSequence(): Returns ------- seqs : list of `SeedSequence` s + + See Also + -------- + random.Generator.spawn, random.BitGenerator.spawn : + Equivalent method on the generator and bit generator. + """ cdef uint32_t i @@ -551,6 +563,53 @@ cdef class BitGenerator(): def state(self, value): raise NotImplementedError('Not implemented in base BitGenerator') + @property + def seed_seq(self): + """ + Get the seed sequence used to initialize the bit generator. + + .. versionadded:: 1.25.0 + + Returns + ------- + seed_seq : ISeedSequence + The SeedSequence object used to initialize the BitGenerator. + This is normally a `np.random.SeedSequence` instance. + + """ + return self._seed_seq + + def spawn(self, int n_children): + """ + Create new independent child bit generators. + + See :ref:`seedsequence-spawn` for additional notes on spawning + children. Some bit generators also implement ``jumped`` + as a different approach for creating independent streams. + + .. versionadded:: 1.25.0 + + Returns + ------- + child_bit_generators : list of BitGenerators + + Raises + ------ + TypeError + When the underlying SeedSequence does not implement spawning. + + See Also + -------- + random.Generator.spawn, random.SeedSequence.spawn : + Equivalent method on the generator and seed sequence. + + """ + if not isinstance(self._seed_seq, ISpawnableSeedSequence): + raise TypeError( + "The underlying SeedSequence does not implement spawning.") + + return [type(self)(seed=s) for s in self._seed_seq.spawn(n_children)] + def random_raw(self, size=None, output=True): """ random_raw(self, size=None) diff --git a/numpy/random/tests/test_direct.py b/numpy/random/tests/test_direct.py index 58d966adf..fa2ae866b 100644 --- a/numpy/random/tests/test_direct.py +++ b/numpy/random/tests/test_direct.py @@ -148,6 +148,46 @@ def test_seedsequence(): assert len(dummy.spawn(10)) == 10 +def test_generator_spawning(): + """ Test spawning new generators and bit_generators directly. + """ + rng = np.random.default_rng() + seq = rng.bit_generator.seed_seq + new_ss = seq.spawn(5) + expected_keys = [seq.spawn_key + (i,) for i in range(5)] + assert [c.spawn_key for c in new_ss] == expected_keys + + new_bgs = rng.bit_generator.spawn(5) + expected_keys = [seq.spawn_key + (i,) for i in range(5, 10)] + assert [bg.seed_seq.spawn_key for bg in new_bgs] == expected_keys + + new_rngs = rng.spawn(5) + expected_keys = [seq.spawn_key + (i,) for i in range(10, 15)] + found_keys = [rng.bit_generator.seed_seq.spawn_key for rng in new_rngs] + assert found_keys == expected_keys + + # Sanity check that streams are actually different: + assert new_rngs[0].uniform() != new_rngs[1].uniform() + + +def test_non_spawnable(): + from numpy.random.bit_generator import ISeedSequence + + class FakeSeedSequence: + def generate_state(self, n_words, dtype=np.uint32): + return np.zeros(n_words, dtype=dtype) + + ISeedSequence.register(FakeSeedSequence) + + rng = np.random.default_rng(FakeSeedSequence()) + + with pytest.raises(TypeError, match="The underlying SeedSequence"): + rng.spawn(5) + + with pytest.raises(TypeError, match="The underlying SeedSequence"): + rng.bit_generator.spawn(5) + + class Base: dtype = np.uint64 data2 = data1 = {} |