Skip to content

Commit

Permalink
Merge pull request #15 from bashtage/legacy-meta
Browse files Browse the repository at this point in the history
Legacy meta
  • Loading branch information
bashtage authored Mar 28, 2018
2 parents c849e70 + 5ef4b62 commit 5e79b69
Show file tree
Hide file tree
Showing 12 changed files with 140 additions and 25 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ NumPy RandomState evolution.
This is a library and generic interface for alternative random
generators in Python and NumPy.

### Compatibility Warning
RandomGen no longer supports Box-Muller normal variates and so it not
## Compatibility Warning

`RandomGenerator` does notsupports Box-Muller normal variates and so it not
100% compatible with NumPy (or randomstate). Box-Muller normals are slow
to generate and all functions which previously relied on Box-Muller
normals now use the faster Ziggurat implementation.
normals now use the faster Ziggurat implementation. If you require backward
compatibility, a legacy generator, ``LegacyGenerator``, has been created
which can fully reproduce the sequence produced by NumPy.

## Features

Expand Down
15 changes: 9 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ This is a library and generic interface for alternative random
generators in Python and NumPy.

Compatibility Warning
~~~~~~~~~~~~~~~~~~~~~

RandomGen no longer supports Box-Muller normal variates and so it not
100% compatible with NumPy (or randomstate). Box-Muller normals are slow
to generate and all functions which previously relied on Box-Muller
normals now use the faster Ziggurat implementation.
---------------------

``RandomGenerator`` does notsupports Box-Muller normal variates and so
it not 100% compatible with NumPy (or randomstate). Box-Muller normals
are slow to generate and all functions which previously relied on
Box-Muller normals now use the faster Ziggurat implementation. If you
require backward compatibility, a legacy generator, ``LegacyGenerator``,
has been created which can fully reproduce the sequence produced by
NumPy.

Features
--------
Expand Down
8 changes: 8 additions & 0 deletions doc/source/generator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ changed by passing an instantized basic RNG to
.. autoclass::
RandomGenerator

Seed and State Manipulation
===========================
.. autosummary::
:toctree: generated/

~RandomGenerator.seed
~RandomGenerator.state

Simple random data
==================
.. autosummary::
Expand Down
11 changes: 7 additions & 4 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,14 @@ What's New or Different
~~~~~~~~~~~~~~~~~~~~~~~
.. warning::

The Box-Muller method used to produce NumPy's normals is no longer available.
It is not possible to exactly reproduce the random values produced from NumPy
The Box-Muller method used to produce NumPy's normals is no longer available
in :class:`~randomgen.generator.RandomGenerator`. It is not possible to
reproduce the random values using :class:`~randomgen.generator.RandomGenerator`
for the normal distribution or any other distribution that relies on the
normal such as the gamma or student's t.

normal such as the gamma or student's t. If you require backward compatibility, a
legacy generator, :class:`~randomgen.legacy.LegacyGenerator`, has been created
which can fully reproduce the sequence produced by NumPy.

* The normal, exponential and gamma generators use 256-step Ziggurat
methods which are 2-10 times faster than NumPy's Box-Muller or inverse CDF
implementations.
Expand Down
2 changes: 1 addition & 1 deletion doc/source/legacy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ when accessing the state so that these extra values are saved.
lg.standard_exponential()
.. currentmodule:: randomgen.legacy
.. currentmodule:: randomgen.legacy.legacy

.. autoclass::
LegacyGenerator
Expand Down
9 changes: 6 additions & 3 deletions doc/source/new-or-different.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ What's New or Different

.. warning::

The Box-Muller method used to produce NumPy's normals is no longer available.
It is not possible to exactly reproduce the random values produced from NumPy
The Box-Muller method used to produce NumPy's normals is no longer available
in :class:`~randomgen.generator.RandomGenerator`. It is not possible to
reproduce the random values using :class:`~randomgen.generator.RandomGenerator`
for the normal distribution or any other distribution that relies on the
normal such as the gamma or student's t.
normal such as the gamma or student's t. If you require backward compatibility, a
legacy generator, :class:`~randomgen.legacy.LegacyGenerator`, has been created
which can fully reproduce the sequence produced by NumPy.


* :func:`~randomgen.entropy.random_entropy` provides access to the system
Expand Down
29 changes: 29 additions & 0 deletions randomgen/generator.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,30 @@ cdef class RandomGenerator:
Reseed the basic RNG.
Parameters depend on the basic RNG used.
Notes
-----
Arguments are directly passed to the basic RNG. This is a convenience
function.
The best method to access seed is to directly use a basic RNG instance.
This example demonstrates this best practice.
>>> from randomgen import RandomGenerator, PCG64
>>> brng = PCG64(1234567891011)
>>> rg = brng.generator
>>> brng.seed(1110987654321)
The method used to create the generator is not important.
>>> brng = PCG64(1234567891011)
>>> rg = RandomGenerator(brng)
>>> brng.seed(1110987654321)
These best practice examples are equivalent to
>>> rg = RandomGenerator(PCG64(1234567891011))
>>> rg.seed(1110987654321)
"""
# TODO: Should this remain
self._basicrng.seed(*args, **kwargs)
Expand All @@ -150,6 +174,11 @@ cdef class RandomGenerator:
state : dict
Dictionary containing the information required to describe the
state of the Basic RNG
Notes
-----
This is a trivial pass-through function. RandomGenerator does not
directly contain or manipulate the basic RNG's state.
"""
return self._basicrng.state

Expand Down
29 changes: 27 additions & 2 deletions randomgen/legacy/_legacy.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ cdef class _LegacyGenerator:
return self.__str__() + ' at 0x{:X}'.format(id(self))

def __str__(self):
return 'RandomGenerator(' + self._basicrng.__class__.__name__ + ')'
return self.__class__.__name__ + '(' + self._basicrng.__class__.__name__ + ')'

# Pickling support:
def __getstate__(self):
Expand All @@ -131,7 +131,33 @@ cdef class _LegacyGenerator:
Reseed the basic RNG.
Parameters depend on the basic RNG used.
Notes
-----
Arguments are directly passed to the basic RNG. This is a convenience
function.
The best method to access seed is to directly use a basic RNG instance.
This example demonstrates this best practice.
>>> from randomgen import MT19937
>>> from randomgen.legacy import LegacyGenerator
>>> brng = MT19937(123456789)
>>> lg = brng.generator
>>> brng.seed(987654321)
The method used to create the generator is not important.
>>> brng = MT19937(123456789)
>>> lg = LegacyGenerator(brng)
>>> brng.seed(987654321)
These best practice examples are equivalent to
>>> lg = LegacyGenerator(MT19937(123456789))
>>> lg.seed(987654321)
"""

# TODO: Should this remain
self._basicrng.seed(*args, **kwargs)
self._reset_gauss()
Expand Down Expand Up @@ -168,7 +194,6 @@ cdef class _LegacyGenerator:
st['has_gauss'] = value[3]
st['gauss'] = value[4]
value = st

self._aug_state.gauss = value.get('gauss', 0.0)
self._aug_state.has_gauss = value.get('has_gauss', 0)
self._basicrng.state = value
Expand Down
37 changes: 33 additions & 4 deletions randomgen/legacy/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,38 @@
import randomgen.pickle


class LegacyGenerator(RandomGenerator):
_LEGACY_ATTRIBUTES = tuple(a for a in dir(
_LegacyGenerator) if not a.startswith('_'))

_LEGACY_ATTRIBUTES += ('__getstate__', '__setstate__', '__reduce__')


def with_metaclass(meta, *bases):
"""Create a base class with a metaclass."""
# This requires a bit of explanation: the basic idea is to make a dummy
# metaclass for one level of class instantiation that replaces itself with
# the actual metaclass.

# From six, https://raw.githubusercontent.com/benjaminp/six
class metaclass(type):

def __new__(cls, name, this_bases, d):
return meta(name, bases, d)

@classmethod
def __prepare__(cls, name, this_bases):
return meta.__prepare__(name, bases)
return type.__new__(metaclass, 'temporary_class', (), {})


class LegacyGeneratorType(type):
def __getattribute__(self, name):
if name in _LEGACY_ATTRIBUTES:
return object.__getattribute__(_LegacyGenerator, name)
return object.__getattribute__(RandomGenerator, name)


class LegacyGenerator(with_metaclass(LegacyGeneratorType, RandomGenerator)):
"""
LegacyGenerator(brng=None)
Expand Down Expand Up @@ -57,8 +88,6 @@ class LegacyGenerator(RandomGenerator):
>>> rs.standard_exponential()
1.6465621229906502
"""
_LEGACY_ATTRIBUTES = tuple(a for a in dir(
_LegacyGenerator) if not a.startswith('_'))

def __init__(self, brng=None):
if brng is None:
Expand All @@ -67,7 +96,7 @@ def __init__(self, brng=None):
self.__legacy = _LegacyGenerator(brng)

def __getattribute__(self, name):
if name in LegacyGenerator._LEGACY_ATTRIBUTES:
if name in _LEGACY_ATTRIBUTES:
return self.__legacy.__getattribute__(name)
return object.__getattribute__(self, name)

Expand Down
1 change: 0 additions & 1 deletion randomgen/tests/test_against_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,6 @@ def test_dir(self):
'print_function', 'RandomState']
mod += known_exlcuded
diff = set(npmod).difference(mod)
print(diff)
assert_equal(len(diff), 0)

# Tests using legacy generator
Expand Down
12 changes: 12 additions & 0 deletions randomgen/tests/test_legacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pickle

from randomgen.legacy import LegacyGenerator


def test_pickle():
lg = LegacyGenerator()
lg.random_sample(100)
lg.standard_normal()
lg2 = pickle.loads(pickle.dumps(lg))
assert lg.standard_normal() == lg2.standard_normal()
assert lg.random_sample() == lg2.random_sample()
3 changes: 2 additions & 1 deletion randomgen/tests/test_numpy_mt19937.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ def test_n_zero(self):
# This test addresses issue #3480.
zeros = np.zeros(2, dtype='int')
for p in [0, .5, 1]:
assert_(random.binomial(0, p) == 0)
val = random.binomial(0, p)
assert val == 0
assert_array_equal(random.binomial(zeros, p), zeros)

def test_p_is_nan(self):
Expand Down

0 comments on commit 5e79b69

Please sign in to comment.