summaryrefslogtreecommitdiff
path: root/doc/source/reference/random/extending.rst
blob: 22f9cb7e47a1e637974e6324b32f665f0fcfa7d1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
.. currentmodule:: numpy.random

Extending
---------
The BitGenerators have been designed to be extendable using standard tools for
high-performance Python -- numba and Cython.  The `~Generator` object can also
be used with user-provided BitGenerators as long as these export a small set of
required functions.

Numba
=====
Numba can be used with either CTypes or CFFI.  The current iteration of the
BitGenerators all export a small set of functions through both interfaces.

This example shows how numba can be used to produce Box-Muller normals using
a pure Python implementation which is then compiled.  The random numbers are
provided by ``ctypes.next_double``.

.. code-block:: python

    from numpy.random import PCG64
    import numpy as np
    import numba as nb

    x = PCG64()
    f = x.ctypes.next_double
    s = x.ctypes.state
    state_addr = x.ctypes.state_address

    def normals(n, state):
        out = np.empty(n)
        for i in range((n+1)//2):
            x1 = 2.0*f(state) - 1.0
            x2 = 2.0*f(state) - 1.0
            r2 = x1*x1 + x2*x2
            while r2 >= 1.0 or r2 == 0.0:
                x1 = 2.0*f(state) - 1.0
                x2 = 2.0*f(state) - 1.0
                r2 = x1*x1 + x2*x2
            g = np.sqrt(-2.0*np.log(r2)/r2)
            out[2*i] = g*x1
            if 2*i+1 < n:
                out[2*i+1] = g*x2
        return out

    # Compile using Numba
    print(normals(10, s).var())
    # Warm up
    normalsj = nb.jit(normals, nopython=True)
    # Must use state address not state with numba
    normalsj(1, state_addr)
    %timeit normalsj(1000000, state_addr)
    print('1,000,000 Box-Muller (numba/PCG64) randoms')
    %timeit np.random.standard_normal(1000000)
    print('1,000,000 Box-Muller (NumPy) randoms')


Both CTypes and CFFI allow the more complicated distributions to be used
directly in Numba after compiling the file distributions.c into a DLL or so.
An example showing the use of a more complicated distribution is in the
examples folder.

.. _randomgen_cython:

Cython
======

Cython can be used to unpack the ``PyCapsule`` provided by a BitGenerator.
This example uses `~pcg64.PCG64` and
``random_gauss_zig``, the Ziggurat-based generator for normals, to fill an
array.  The usual caveats for writing high-performance code using Cython --
removing bounds checks and wrap around, providing array alignment information
-- still apply.

.. code-block:: cython

    import numpy as np
    cimport numpy as np
    cimport cython
    from cpython.pycapsule cimport PyCapsule_IsValid, PyCapsule_GetPointer
    from numpy.random.common cimport *
    from numpy.random.distributions cimport random_gauss_zig
    from numpy.random import PCG64


    @cython.boundscheck(False)
    @cython.wraparound(False)
    def normals_zig(Py_ssize_t n):
        cdef Py_ssize_t i
        cdef bitgen_t *rng
        cdef const char *capsule_name = "BitGenerator"
        cdef double[::1] random_values

        x = PCG64()
        capsule = x.capsule
        if not PyCapsule_IsValid(capsule, capsule_name):
            raise ValueError("Invalid pointer to anon_func_state")
        rng = <bitgen_t *> PyCapsule_GetPointer(capsule, capsule_name)
        random_values = np.empty(n)
        # Best practice is to release GIL and acquire the lock
        with x.lock, nogil:
            for i in range(n):
                random_values[i] = random_gauss_zig(rng)
        randoms = np.asarray(random_values)
        return randoms

The BitGenerator can also be directly accessed using the members of the basic
RNG structure.

.. code-block:: cython

    @cython.boundscheck(False)
    @cython.wraparound(False)
    def uniforms(Py_ssize_t n):
        cdef Py_ssize_t i
        cdef bitgen_t *rng
        cdef const char *capsule_name = "BitGenerator"
        cdef double[::1] random_values

        x = PCG64()
        capsule = x.capsule
        # Optional check that the capsule if from a BitGenerator
        if not PyCapsule_IsValid(capsule, capsule_name):
            raise ValueError("Invalid pointer to anon_func_state")
        # Cast the pointer
        rng = <bitgen_t *> PyCapsule_GetPointer(capsule, capsule_name)
        random_values = np.empty(n)
        with x.lock, nogil:
            for i in range(n):
                # Call the function
                random_values[i] = rng.next_double(rng.state)
        randoms = np.asarray(random_values)
        return randoms

These functions along with a minimal setup file are included in the
examples folder.

New Basic RNGs
==============
`~Generator` can be used with other user-provided BitGenerators. The simplest
way to write a new BitGenerator is to examine the pyx file of one of the
existing BitGenerators. The key structure that must be provided is the
``capsule`` which contains a ``PyCapsule`` to a struct pointer of type
``bitgen_t``,

.. code-block:: c

  typedef struct bitgen {
    void *state;
    uint64_t (*next_uint64)(void *st);
    uint32_t (*next_uint32)(void *st);
    double (*next_double)(void *st);
    uint64_t (*next_raw)(void *st);
  } bitgen_t;

which provides 5 pointers. The first is an opaque pointer to the data structure
used by the BitGenerators.  The next three are function pointers which return
the next 64- and 32-bit unsigned integers, the next random double and the next
raw value.  This final function is used for testing and so can be set to
the next 64-bit unsigned integer function if not needed. Functions inside
``Generator`` use this structure as in

.. code-block:: c

  bitgen_state->next_uint64(bitgen_state->state)