diff options
Diffstat (limited to 'numpy/random/_pcg64.pyx')
-rw-r--r-- | numpy/random/_pcg64.pyx | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/numpy/random/_pcg64.pyx b/numpy/random/_pcg64.pyx new file mode 100644 index 000000000..05d27db5c --- /dev/null +++ b/numpy/random/_pcg64.pyx @@ -0,0 +1,270 @@ +import numpy as np +cimport numpy as np + +from libc.stdint cimport uint32_t, uint64_t +from ._common cimport uint64_to_double, wrap_int +from numpy.random cimport BitGenerator + +__all__ = ['PCG64'] + +cdef extern from "src/pcg64/pcg64.h": + # Use int as generic type, actual type read from pcg64.h and is platform dependent + ctypedef int pcg64_random_t + + struct s_pcg64_state: + pcg64_random_t *pcg_state + int has_uint32 + uint32_t uinteger + + ctypedef s_pcg64_state pcg64_state + + uint64_t pcg64_next64(pcg64_state *state) nogil + uint32_t pcg64_next32(pcg64_state *state) nogil + void pcg64_jump(pcg64_state *state) + void pcg64_advance(pcg64_state *state, uint64_t *step) + void pcg64_set_seed(pcg64_state *state, uint64_t *seed, uint64_t *inc) + void pcg64_get_state(pcg64_state *state, uint64_t *state_arr, int *has_uint32, uint32_t *uinteger) + void pcg64_set_state(pcg64_state *state, uint64_t *state_arr, int has_uint32, uint32_t uinteger) + +cdef uint64_t pcg64_uint64(void* st) nogil: + return pcg64_next64(<pcg64_state *>st) + +cdef uint32_t pcg64_uint32(void *st) nogil: + return pcg64_next32(<pcg64_state *> st) + +cdef double pcg64_double(void* st) nogil: + return uint64_to_double(pcg64_next64(<pcg64_state *>st)) + + +cdef class PCG64(BitGenerator): + """ + PCG64(seed_seq=None) + + BitGenerator for the PCG-64 pseudo-random number generator. + + Parameters + ---------- + seed : {None, int, array_like[ints], SeedSequence}, optional + A seed to initialize the `BitGenerator`. If None, then fresh, + unpredictable entropy will be pulled from the OS. If an ``int`` or + ``array_like[ints]`` is passed, then it will be passed to + `SeedSequence` to derive the initial `BitGenerator` state. One may also + pass in a `SeedSequence` instance. + + Notes + ----- + PCG-64 is a 128-bit implementation of O'Neill's permutation congruential + generator ([1]_, [2]_). PCG-64 has a period of :math:`2^{128}` and supports + advancing an arbitrary number of steps as well as :math:`2^{127}` streams. + The specific member of the PCG family that we use is PCG XSL RR 128/64 + as described in the paper ([2]_). + + ``PCG64`` provides a capsule containing function pointers that produce + doubles, and unsigned 32 and 64- bit integers. These are not + directly consumable in Python and must be consumed by a ``Generator`` + or similar object that supports low-level access. + + Supports the method :meth:`advance` to advance the RNG an arbitrary number of + steps. The state of the PCG-64 RNG is represented by 2 128-bit unsigned + integers. + + **State and Seeding** + + The ``PCG64`` state vector consists of 2 unsigned 128-bit values, + which are represented externally as Python ints. One is the state of the + PRNG, which is advanced by a linear congruential generator (LCG). The + second is a fixed odd increment used in the LCG. + + The input seed is processed by `SeedSequence` to generate both values. The + increment is not independently settable. + + **Parallel Features** + + The preferred way to use a BitGenerator in parallel applications is to use + the `SeedSequence.spawn` method to obtain entropy values, and to use these + to generate new BitGenerators: + + >>> from numpy.random import Generator, PCG64, SeedSequence + >>> sg = SeedSequence(1234) + >>> rg = [Generator(PCG64(s)) for s in sg.spawn(10)] + + **Compatibility Guarantee** + + ``PCG64`` makes a guarantee that a fixed seed and will always produce + the same random integer stream. + + References + ---------- + .. [1] `"PCG, A Family of Better Random Number Generators" + <http://www.pcg-random.org/>`_ + .. [2] O'Neill, Melissa E. `"PCG: A Family of Simple Fast Space-Efficient + Statistically Good Algorithms for Random Number Generation" + <https://www.cs.hmc.edu/tr/hmc-cs-2014-0905.pdf>`_ + """ + + cdef pcg64_state rng_state + cdef pcg64_random_t pcg64_random_state + + def __init__(self, seed=None): + BitGenerator.__init__(self, seed) + self.rng_state.pcg_state = &self.pcg64_random_state + + self._bitgen.state = <void *>&self.rng_state + self._bitgen.next_uint64 = &pcg64_uint64 + self._bitgen.next_uint32 = &pcg64_uint32 + self._bitgen.next_double = &pcg64_double + self._bitgen.next_raw = &pcg64_uint64 + # Seed the _bitgen + val = self._seed_seq.generate_state(4, np.uint64) + pcg64_set_seed(&self.rng_state, + <uint64_t *>np.PyArray_DATA(val), + (<uint64_t *>np.PyArray_DATA(val) + 2)) + self._reset_state_variables() + + cdef _reset_state_variables(self): + self.rng_state.has_uint32 = 0 + self.rng_state.uinteger = 0 + + cdef jump_inplace(self, jumps): + """ + Jump state in-place + Not part of public API + + Parameters + ---------- + jumps : integer, positive + Number of times to jump the state of the rng. + + Notes + ----- + The step size is phi-1 when multiplied by 2**128 where phi is the + golden ratio. + """ + step = 0x9e3779b97f4a7c15f39cc0605cedc835 + self.advance(step * int(jumps)) + + def jumped(self, jumps=1): + """ + jumped(jumps=1) + + Returns a new bit generator with the state jumped. + + Jumps the state as-if jumps * 210306068529402873165736369884012333109 + random numbers have been generated. + + Parameters + ---------- + jumps : integer, positive + Number of times to jump the state of the bit generator returned + + Returns + ------- + bit_generator : PCG64 + New instance of generator jumped iter times + + Notes + ----- + The step size is phi-1 when multiplied by 2**128 where phi is the + golden ratio. + """ + cdef PCG64 bit_generator + + bit_generator = self.__class__() + bit_generator.state = self.state + bit_generator.jump_inplace(jumps) + + return bit_generator + + @property + def state(self): + """ + Get or set the PRNG state + + Returns + ------- + state : dict + Dictionary containing the information required to describe the + state of the PRNG + """ + cdef np.ndarray state_vec + cdef int has_uint32 + cdef uint32_t uinteger + + # state_vec is state.high, state.low, inc.high, inc.low + state_vec = <np.ndarray>np.empty(4, dtype=np.uint64) + pcg64_get_state(&self.rng_state, + <uint64_t *>np.PyArray_DATA(state_vec), + &has_uint32, &uinteger) + state = int(state_vec[0]) * 2**64 + int(state_vec[1]) + inc = int(state_vec[2]) * 2**64 + int(state_vec[3]) + return {'bit_generator': self.__class__.__name__, + 'state': {'state': state, 'inc': inc}, + 'has_uint32': has_uint32, + 'uinteger': uinteger} + + @state.setter + def state(self, value): + cdef np.ndarray state_vec + cdef int has_uint32 + cdef uint32_t uinteger + if not isinstance(value, dict): + raise TypeError('state must be a dict') + bitgen = value.get('bit_generator', '') + if bitgen != self.__class__.__name__: + raise ValueError('state must be for a {0} ' + 'RNG'.format(self.__class__.__name__)) + state_vec = <np.ndarray>np.empty(4, dtype=np.uint64) + state_vec[0] = value['state']['state'] // 2 ** 64 + state_vec[1] = value['state']['state'] % 2 ** 64 + state_vec[2] = value['state']['inc'] // 2 ** 64 + state_vec[3] = value['state']['inc'] % 2 ** 64 + has_uint32 = value['has_uint32'] + uinteger = value['uinteger'] + pcg64_set_state(&self.rng_state, + <uint64_t *>np.PyArray_DATA(state_vec), + has_uint32, uinteger) + + def advance(self, delta): + """ + advance(delta) + + Advance the underlying RNG as-if delta draws have occurred. + + Parameters + ---------- + delta : integer, positive + Number of draws to advance the RNG. Must be less than the + size state variable in the underlying RNG. + + Returns + ------- + self : PCG64 + RNG advanced delta steps + + Notes + ----- + Advancing a RNG updates the underlying RNG state as-if a given + number of calls to the underlying RNG have been made. In general + there is not a one-to-one relationship between the number output + random values from a particular distribution and the number of + draws from the core RNG. This occurs for two reasons: + + * The random values are simulated using a rejection-based method + and so, on average, more than one value from the underlying + RNG is required to generate an single draw. + * The number of bits required to generate a simulated value + differs from the number of bits generated by the underlying + RNG. For example, two 16-bit integer values can be simulated + from a single draw of a 32-bit RNG. + + Advancing the RNG state resets any pre-computed random numbers. + This is required to ensure exact reproducibility. + """ + delta = wrap_int(delta, 128) + + cdef np.ndarray d = np.empty(2, dtype=np.uint64) + d[0] = delta // 2**64 + d[1] = delta % 2**64 + pcg64_advance(&self.rng_state, <uint64_t *>np.PyArray_DATA(d)) + self._reset_state_variables() + return self |