diff options
Diffstat (limited to 'numpy/random/randomgen/pcg32.pyx')
-rw-r--r-- | numpy/random/randomgen/pcg32.pyx | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/numpy/random/randomgen/pcg32.pyx b/numpy/random/randomgen/pcg32.pyx new file mode 100644 index 000000000..5f2b34807 --- /dev/null +++ b/numpy/random/randomgen/pcg32.pyx @@ -0,0 +1,385 @@ +from libc.stdlib cimport malloc, free +from cpython.pycapsule cimport PyCapsule_New + +try: + from threading import Lock +except ImportError: + from dummy_threading import Lock + +import numpy as np +cimport numpy as np + +from .common cimport * +from .distributions cimport brng_t +from .entropy import random_entropy + +np.import_array() + + +cdef extern from "src/pcg32/pcg32.h": + + cdef struct pcg_state_setseq_64: + uint64_t state + uint64_t inc + + ctypedef pcg_state_setseq_64 pcg32_random_t + + struct s_pcg32_state: + pcg32_random_t *pcg_state + + ctypedef s_pcg32_state pcg32_state + + uint64_t pcg32_next64(pcg32_state *state) nogil + uint32_t pcg32_next32(pcg32_state *state) nogil + double pcg32_next_double(pcg32_state *state) nogil + void pcg32_jump(pcg32_state *state) + void pcg32_advance_state(pcg32_state *state, uint64_t step) + void pcg32_set_seed(pcg32_state *state, uint64_t seed, uint64_t inc) + +cdef uint64_t pcg32_uint64(void* st) nogil: + return pcg32_next64(<pcg32_state *>st) + +cdef uint32_t pcg32_uint32(void *st) nogil: + return pcg32_next32(<pcg32_state *> st) + +cdef double pcg32_double(void* st) nogil: + return pcg32_next_double(<pcg32_state *>st) + +cdef uint64_t pcg32_raw(void* st) nogil: + return <uint64_t>pcg32_next32(<pcg32_state *> st) + + +cdef class PCG32: + u""" + PCG32(seed=None, inc=0) + + Container for the PCG-32 pseudo-random number generator. + + PCG-32 is a 64-bit implementation of O'Neill's permutation congruential + generator ([1]_, [2]_). PCG-32 has a period of :math:`2^{64}` and supports + advancing an arbitrary number of steps as well as :math:`2^{63}` streams. + + ``PCG32`` exposes no user-facing API except ``generator``,``state``, + ``cffi`` and ``ctypes``. Designed for use in a ``RandomGenerator`` object. + + **Compatibility Guarantee** + + ``PCG32`` makes a guarantee that a fixed seed will always produce the same + results. + + Parameters + ---------- + seed : {None, long}, optional + Random seed initializing the pseudo-random number generator. + Can be an integer in [0, 2**64] or ``None`` (the default). + If `seed` is ``None``, then ``PCG32`` will try to read data + from ``/dev/urandom`` (or the Windows analog) if available. If + unavailable, a 64-bit hash of the time and process ID is used. + inc : {None, int}, optional + Stream to return. + Can be an integer in [0, 2**64] or ``None`` (the default). If `inc` is + ``None``, then 0 is used. Can be used with the same seed to + produce multiple streams using other values of inc. + + Notes + ----- + Supports the method advance to advance the PRNG an arbitrary number of + steps. The state of the PCG-32 PRNG is represented by 2 128-bit unsigned + integers. + + See ``PCG32`` for a similar implementation with a smaller period. + + **Parallel Features** + + ``PCG32`` can be used in parallel applications in one of two ways. + The preferable method is to use sub-streams, which are generated by using the + same value of ``seed`` and incrementing the second value, ``inc``. + + >>> from randomgen import RandomGenerator, PCG32 + >>> rg = [RandomGenerator(PCG32(1234, i + 1)) for i in range(10)] + + The alternative method is to call ``advance`` on a instance to + produce non-overlapping sequences. + + >>> rg = [RandomGenerator(PCG32(1234, i + 1)) for i in range(10)] + >>> for i in range(10): + ... rg[i].advance(i * 2**32) + + **State and Seeding** + + The ``PCG32`` state vector consists of 2 unsigned 64-bit values/ + ``PCG32`` is seeded using a single 64-bit unsigned integer. In addition, + a second 64-bit unsigned integer is used to set the 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" + """ + cdef pcg32_state *rng_state + cdef brng_t *_brng + cdef public object capsule + cdef object _ctypes + cdef object _cffi + cdef object _generator + cdef public object lock + + def __init__(self, seed=None, inc=0): + self.rng_state = <pcg32_state *>malloc(sizeof(pcg32_state)) + self.rng_state.pcg_state = <pcg32_random_t *>malloc(sizeof(pcg32_random_t)) + self._brng = <brng_t *>malloc(sizeof(brng_t)) + self.seed(seed, inc) + self.lock = Lock() + + self._brng.state = <void *>self.rng_state + self._brng.next_uint64 = &pcg32_uint64 + self._brng.next_uint32 = &pcg32_uint32 + self._brng.next_double = &pcg32_double + self._brng.next_raw = &pcg32_raw + + self._ctypes = None + self._cffi = None + self._generator = None + + cdef const char *name = "BasicRNG" + self.capsule = PyCapsule_New(<void *>self._brng, name, NULL) + + # Pickling support: + def __getstate__(self): + return self.state + + def __setstate__(self, state): + self.state = state + + def __reduce__(self): + from ._pickle import __brng_ctor + return (__brng_ctor, + (self.state['brng'],), + self.state) + + def __dealloc__(self): + free(self.rng_state) + free(self._brng) + + def random_raw(self, size=None, output=True): + """ + random_raw(self, size=None) + + Return randoms as generated by the underlying BasicRNG + + Parameters + ---------- + size : int or tuple of ints, optional + Output shape. If the given shape is, e.g., ``(m, n, k)``, then + ``m * n * k`` samples are drawn. Default is None, in which case a + single value is returned. + output : bool, optional + Output values. Used for performance testing since the generated + values are not returned. + + Returns + ------- + out : uint or ndarray + Drawn samples. + + Notes + ----- + This method directly exposes the the raw underlying pseudo-random + number generator. All values are returned as unsigned 64-bit + values irrespective of the number of bits produced by the PRNG. + + See the class docstring for the number of bits returned. + """ + return random_raw(self._brng, self.lock, size, output) + + def _benchmark(self, Py_ssize_t cnt, method=u'uint64'): + return benchmark(self._brng, self.lock, cnt, method) + + def seed(self, seed=None, inc=0): + """ + seed(seed=None, inc=0) + + Seed the generator. + + This method is called when ``PCG32`` is initialized. It can be + called again to re-seed the generator. For details, see + ``PCG32``. + + Parameters + ---------- + seed : int, optional + Seed for ``PCG32``. + inc : int, optional + Increment to use for PCG stream + + Raises + ------ + ValueError + If seed values are out of range for the PRNG. + """ + ub = 2 ** 64 + if seed is None: + try: + seed = <np.ndarray>random_entropy(2) + except RuntimeError: + seed = <np.ndarray>random_entropy(2, 'fallback') + seed = seed.view(np.uint64).squeeze() + else: + err_msg = 'seed must be a scalar integer between 0 and ' \ + '{ub}'.format(ub=ub) + if not np.isscalar(seed): + raise TypeError(err_msg) + if int(seed) != seed: + raise TypeError(err_msg) + if seed < 0 or seed > ub: + raise ValueError(err_msg) + + if not np.isscalar(inc): + raise TypeError('inc must be a scalar integer between 0 ' + 'and {ub}'.format(ub=ub)) + if inc < 0 or inc > ub or int(inc) != inc: + raise ValueError('inc must be a scalar integer between 0 ' + 'and {ub}'.format(ub=ub)) + + pcg32_set_seed(self.rng_state, <uint64_t>seed, <uint64_t>inc) + + @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 + """ + return {'brng': self.__class__.__name__, + 'state': {'state': self.rng_state.pcg_state.state, + 'inc': self.rng_state.pcg_state.inc}} + + @state.setter + def state(self, value): + if not isinstance(value, dict): + raise TypeError('state must be a dict') + brng = value.get('brng', '') + if brng != self.__class__.__name__: + raise ValueError('state must be for a {0} ' + 'PRNG'.format(self.__class__.__name__)) + self.rng_state.pcg_state.state = value['state']['state'] + self.rng_state.pcg_state.inc = value['state']['inc'] + + 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 : PCG32 + 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. + """ + pcg32_advance_state(self.rng_state, <uint64_t>delta) + return self + + def jump(self, np.npy_intp iter=1): + """ + jump(iter=1) + + Jumps the state as-if 2**32 random numbers have been generated + + Parameters + ---------- + iter : integer, positive + Number of times to jump the state of the rng. + + Returns + ------- + self : PCG32 + RNG jumped iter times + """ + return self.advance(iter * 2**32) + + @property + def ctypes(self): + """ + ctypes interface + + Returns + ------- + interface : namedtuple + Named tuple containing ctypes wrapper + + * state_address - Memory address of the state struct + * state - pointer to the state struct + * next_uint64 - function pointer to produce 64 bit integers + * next_uint32 - function pointer to produce 32 bit integers + * next_double - function pointer to produce doubles + * brng - pointer to the Basic RNG struct + """ + if self._ctypes is None: + self._ctypes = prepare_ctypes(self._brng) + + return self._ctypes + + @property + def cffi(self): + """ + CFFI interface + + Returns + ------- + interface : namedtuple + Named tuple containing CFFI wrapper + + * state_address - Memory address of the state struct + * state - pointer to the state struct + * next_uint64 - function pointer to produce 64 bit integers + * next_uint32 - function pointer to produce 32 bit integers + * next_double - function pointer to produce doubles + * brng - pointer to the Basic RNG struct + """ + if self._cffi is not None: + return self._cffi + self._cffi = prepare_cffi(self._brng) + return self._cffi + + @property + def generator(self): + """ + Return a RandomGenerator object + + Returns + ------- + gen : numpy.random.randomgen.generator.RandomGenerator + Random generator used this instance as the core PRNG + """ + if self._generator is None: + from .generator import RandomGenerator + self._generator = RandomGenerator(self) + return self._generator |