Source code for kwutil.util_random

"""
Implements a version of `ensure_rng` that does not require on numpy (but allows
for it). This mirrors :kwarray:`ensure_rng`, which is a numpy-first
implementation.
"""
import random
try:
    import numpy as np
    np_random = np.random
    _RANDOM_CLASSES = (random.Random, np.random.RandomState)
    _FLOAT_TYPES = (float, np.floating)
    _INT_TYPES = (int, np.integer)
except ImportError:
    np = None
    np_random = None
    _RANDOM_CLASSES = (random.Random,)
    _FLOAT_TYPES = (float,)
    _INT_TYPES = (int,)

_SEED_MAX = int(2 ** 32 - 1)


[docs] def _npstate_to_pystate(npstate): """ Convert state of a NumPy RandomState object to a state that can be used by Python's Random. Derived from [SO44313620]_. References: .. [SO44313620] https://stackoverflow.com/questions/44313620/convert-randomstate Example: >>> # xdoctest: +REQUIRES(module:numpy) >>> import numpy as np >>> py_rng = random.Random(0) >>> np_rng = np.random.RandomState(seed=0) >>> npstate = np_rng.get_state() >>> pystate = _npstate_to_pystate(npstate) >>> py_rng.setstate(pystate) >>> assert np_rng.rand() == py_rng.random() """ PY_VERSION = 3 version, keys, pos, has_gauss, cached_gaussian_ = npstate keys_pos = tuple(map(int, keys)) + (int(pos),) cached_gaussian_ = cached_gaussian_ if has_gauss else None pystate = (PY_VERSION, keys_pos, cached_gaussian_) return pystate
[docs] def _pystate_to_npstate(pystate): """ Convert state of a Python Random object to state usable by NumPy RandomState. Derived from [SO44313620]_. References: .. [SO44313620] https://stackoverflow.com/questions/44313620/convert-randomstate Example: >>> # xdoctest: +REQUIRES(module:numpy) >>> import numpy as np >>> py_rng = random.Random(0) >>> np_rng = np.random.RandomState(seed=0) >>> pystate = py_rng.getstate() >>> npstate = _pystate_to_npstate(pystate) >>> np_rng.set_state(npstate) >>> assert np_rng.rand() == py_rng.random() """ NP_VERSION = 'MT19937' version, keys_pos_, cached_gaussian_ = pystate keys, pos = keys_pos_[:-1], keys_pos_[-1] keys = np.array(keys, dtype=np.uint32) has_gauss = cached_gaussian_ is not None cached_gaussian = cached_gaussian_ if has_gauss else 0.0 npstate = (NP_VERSION, keys, pos, has_gauss, cached_gaussian) return npstate
[docs] def _coerce_rng_type(rng): """ Internal method that transforms input seeds into an integer form. """ if rng is None or isinstance(rng, _RANDOM_CLASSES): pass elif rng is random: rng = rng._inst elif rng is np_random: rng = np.random.mtrand._rand # elif isinstance(rng, str): # # todo convert string to rng # pass elif isinstance(rng, _FLOAT_TYPES): rng = float(rng) # Coerce the float into an integer a, b = rng.as_integer_ratio() if b == 1: rng = a else: s = max(a.bit_length(), b.bit_length()) rng = (b << s) | a elif isinstance(rng, _INT_TYPES): rng = int(rng) else: raise TypeError( 'Cannot coerce {!r} to a random object'.format(type(rng))) return rng
[docs] def ensure_rng(rng=None, api='python'): """ Coerces input into a random number generator. This function is useful for ensuring that your code uses a controlled internal random state that is independent of other modules. If the input is None, then a global random state is returned. If the input is a numeric value, then that is used as a seed to construct a random state. If the input is a random number generator, then another random number generator with the same state is returned. Depending on the api, this random state is either return as-is, or used to construct an equivalent random state with the requested api. Args: rng (int | float | None | numpy.random.RandomState | random.Random): if None, then defaults to the global rng. Otherwise this can be an integer or a RandomState class. Defaults to the global random. api (str): specify the type of random number generator to use. This can either be 'numpy' for a :class:`numpy.random.RandomState` object or 'python' for a :class:`random.Random` object. Defaults to numpy. Returns: (numpy.random.RandomState | random.Random) : rng - either a numpy or python random number generator, depending on the setting of ``api``. Example: >>> # xdoctest: +REQUIRES(module:numpy) >>> from kwutil.util_random import * # NOQA >>> from kwutil.util_random import ensure_rng >>> rng = ensure_rng(None) >>> ensure_rng(0, 'python').randint(0, 1000) 864 >>> # xdoctest: +REQUIRES(module:numpy) >>> import numpy as np >>> ensure_rng(np.random.RandomState(1)).randint(0, 1000) 427 Example: >>> from kwutil.util_random import * # NOQA >>> from kwutil.util_random import ensure_rng >>> num = 4 >>> print('--- Python as PYTHON ---') >>> py_rng = random.Random(0) >>> pp_nums = [py_rng.random() for _ in range(num)] >>> print(pp_nums) >>> print('--- Numpy as PYTHON ---') >>> # xdoctest: +REQUIRES(module:numpy) >>> import numpy as np >>> np_rng = ensure_rng(random.Random(0), api='numpy') >>> np_nums = [np_rng.rand() for _ in range(num)] >>> print(np_nums) >>> print('--- Numpy as NUMPY---') >>> np_rng = np.random.RandomState(seed=0) >>> nn_nums = [np_rng.rand() for _ in range(num)] >>> print(nn_nums) >>> print('--- Python as NUMPY---') >>> py_rng = ensure_rng(np.random.RandomState(seed=0), api='python') >>> pn_nums = [py_rng.random() for _ in range(num)] >>> print(pn_nums) >>> assert np_nums == pp_nums >>> assert pn_nums == nn_nums Example: >>> # Test that random modules can be coerced >>> # xdoctest: +REQUIRES(module:numpy) >>> from kwutil.util_random import * # NOQA >>> import random >>> import numpy as np >>> ensure_rng(random, api='python') >>> ensure_rng(random, api='numpy') >>> ensure_rng(np.random, api='python') >>> ensure_rng(np.random, api='numpy') """ rng = _coerce_rng_type(rng) if api == 'numpy': assert np is not None, 'requires numpy' if rng is None: # This is the underlying random state of the np.random module rng = np.random.mtrand._rand # Dont do this because it seeds using dev/urandom # rng = np.random.RandomState(seed=None) elif isinstance(rng, int): rng = np.random.RandomState(seed=rng % _SEED_MAX) elif isinstance(rng, random.Random): # Convert python to numpy random state py_rng = rng pystate = py_rng.getstate() npstate = _pystate_to_npstate(pystate) rng = np_rng = np_random.RandomState(seed=0) np_rng.set_state(npstate) elif api == 'python': if rng is None: # This is the underlying random state of the random module rng = random._inst elif isinstance(rng, int): rng = random.Random(rng % _SEED_MAX) elif np is not None and isinstance(rng, np_random.RandomState): # Convert numpy to python random state np_rng = rng npstate = np_rng.get_state() pystate = _npstate_to_pystate(npstate) rng = py_rng = random.Random(0) py_rng.setstate(pystate) else: raise KeyError('unknown rng api={}'.format(api)) return rng