"""
Provides wrappers for several random number generators (RNGs), giving them all a
common interface so that they can be used interchangeably in PyNN.
Classes:
NumpyRNG - uses the numpy.random.RandomState RNG
GSLRNG - uses the RNGs from the Gnu Scientific Library
NativeRNG - indicates to the simulator that it should use it's own,
built-in RNG
RandomDistribution - produces random numbers from a specific distribution
:copyright: Copyright 2006-2016 by the PyNN team, see AUTHORS.
:license: CeCILL, see LICENSE for details.
"""
from copy import deepcopy
import logging
import numpy.random
from lazyarray import VectorizedIterable
try:
import pygsl.rng
have_gsl = True
except (ImportError, Warning):
have_gsl = False
import time
logger = logging.getLogger("PyNN")
available_distributions = {
'binomial': ('n', 'p'),
'gamma': ('k', 'theta'),
'exponential': ('beta',),
'lognormal': ('mu', 'sigma'),
'normal': ('mu', 'sigma'),
'normal_clipped': ('mu', 'sigma', 'low', 'high'),
'normal_clipped_to_boundary':
('mu', 'sigma', 'low', 'high'),
'poisson': ('lambda_',),
'uniform': ('low', 'high'),
'uniform_int': ('low', 'high'),
'vonmises': ('mu', 'kappa'),
}
MAX_REDRAWS = 1000 # for clipped distributions
def get_mpi_config():
try:
from mpi4py import MPI
mpi_rank = MPI.COMM_WORLD.rank
num_processes = MPI.COMM_WORLD.size
except ImportError:
mpi_rank = 0
num_processes = 1
return mpi_rank, num_processes
class AbstractRNG(object):
"""Abstract class for wrapping random number generators. The idea is to be
able to use either simulator-native rngs, which may be more efficient, or a
standard Python rng, e.g. a numpy.random.RandomState object, which would
allow the same random numbers to be used across different simulators, or
simply to read externally-generated numbers from files."""
def __init__(self, seed=None):
if seed is not None:
assert isinstance(seed, int), "`seed` must be an int, not a %s" % type(seed).__name__
self.seed = seed
# define some aliases
self.random = self.next
self.sample = self.next
def __repr__(self):
return "%s(seed=%r)" % (self.__class__.__name__, self.seed)
def next(self, n=None, distribution=None, parameters=None, mask_local=None):
"""Return `n` random numbers from the specified distribution.
If:
* `n` is None, return a float,
* `n` >= 1, return a Numpy array,
* `n` < 0, raise an Exception,
* `n` is 0, return an empty array.
If called with distribution=None, returns uniformly distributed floats in the range [0, 1)
"""
raise NotImplementedError
class WrappedRNG(AbstractRNG):
def __init__(self, seed=None, parallel_safe=True):
AbstractRNG.__init__(self, seed)
self.parallel_safe = parallel_safe
self.mpi_rank, self.num_processes = get_mpi_config()
if self.seed is not None and not parallel_safe:
self.seed += self.mpi_rank # ensure different nodes get different sequences
if self.mpi_rank != 0:
logger.warning("Changing the seed to %s on node %d" % (self.seed, self.mpi_rank))
def next(self, n=None, distribution=None, parameters=None, mask_local=None):
if distribution is None:
distribution = 'uniform'
if parameters is None:
parameters = {"low": 0.0, "high": 1.0}
if n == 0:
rarr = numpy.random.rand(0) # We return an empty array
elif n is None:
rarr = self._next(distribution, 1, parameters)
elif n > 0:
if self.num_processes > 1 and not self.parallel_safe:
# n is the number for the whole model, so if we do not care about
# having exactly the same random numbers independent of the
# number of processors (m), we only need generate n/m+1 per node
# (assuming round-robin distribution of cells between processors)
if mask_local is None:
n = n // self.num_processes + 1
elif mask_local is not False:
n = mask_local.sum()
rarr = self._next(distribution, n, parameters)
else:
raise ValueError("The sample number must be positive")
if not isinstance(rarr, numpy.ndarray):
rarr = numpy.array(rarr)
if self.parallel_safe and self.num_processes > 1:
if hasattr(mask_local, 'size'): # strip out the random numbers that
assert mask_local.size == n # should be used on other processors.
rarr = rarr[mask_local]
if n is None:
return rarr[0]
else:
return rarr
next.__doc__ = AbstractRNG.next.__doc__
def _clipped(self, gen, low=-numpy.inf, high=numpy.inf, size=None):
""" """
res = gen(size)
iterations = 0
errmsg = "Maximum number of redraws exceeded. Check the parameterization of your distribution."
if size is None:
while res < low or res > high:
# limit the number of iterations. Possibility of infinite loop, depending on parameters
if iterations > MAX_REDRAWS:
raise Exception(errmsg)
res = gen(size)
iterations += 1
else:
idx = numpy.where((res > high) | (res < low))[0]
while idx.size > 0:
if iterations > MAX_REDRAWS:
raise Exception(errmsg)
redrawn = gen(idx.size)
res[idx] = redrawn
idx = idx[numpy.where((redrawn > high) | (redrawn < low))[0]]
iterations += 1
return res
def describe(self):
return "%s() with seed %s for MPI rank %d (MPI processes %d). %s parallel safe." % (
self.__class__.__name__, self.seed, self.mpi_rank, self.num_processes, self.parallel_safe and "Is" or "Not")
[docs]class NumpyRNG(WrappedRNG):
"""Wrapper for the :class:`numpy.random.RandomState` class (Mersenne Twister PRNG)."""
translations = {
'binomial': ('binomial', {'n': 'n', 'p': 'p'}),
'gamma': ('gamma', {'k': 'shape', 'theta': 'scale'}),
'exponential': ('exponential', {'beta': 'scale'}),
'lognormal': ('lognormal', {'mu': 'mean', 'sigma': 'sigma'}),
'normal': ('normal', {'mu': 'loc', 'sigma': 'scale'}),
'normal_clipped': ('normal_clipped', {'mu': 'mu', 'sigma': 'sigma', 'low': 'low', 'high': 'high'}),
'normal_clipped_to_boundary':
('normal_clipped_to_boundary', {'mu': 'mu', 'sigma': 'sigma', 'low': 'low', 'high': 'high'}),
'poisson': ('poisson', {'lambda_': 'lam'}),
'uniform': ('uniform', {'low': 'low', 'high': 'high'}),
'uniform_int': ('randint', {'low': 'low', 'high': 'high'}),
'vonmises': ('vonmises', {'mu': 'mu', 'kappa': 'kappa'}),
}
def __init__(self, seed=None, parallel_safe=True):
WrappedRNG.__init__(self, seed, parallel_safe)
self.rng = numpy.random.RandomState()
if self.seed is not None:
self.rng.seed(self.seed)
else:
self.rng.seed()
def __getattr__(self, name):
"""
This is to give the PyNN RNGs the same methods as the wrapped RNGs
(:class:`numpy.random.RandomState` or the GSL RNGs.)
"""
return getattr(self.rng, name)
def _next(self, distribution, n, parameters):
# TODO: allow non-standardized distributions to pass through without translation
distribution_np, parameter_map = self.translations[distribution]
if set(parameters.keys()) != set(parameter_map.keys()):
# all parameters must be provided. We do not provide default values (this can be discussed).
errmsg = "Incorrect parameterization of random distribution. Expected %s, got %s."
raise KeyError(errmsg % (parameter_map.keys(), parameters.keys()))
parameters_np = dict((parameter_map[k], v) for k, v in parameters.items())
if hasattr(self, distribution_np):
f_distr = getattr(self, distribution_np)
else:
f_distr = getattr(self.rng, distribution_np)
return f_distr(size=n, **parameters_np)
def __deepcopy__(self, memo):
obj = NumpyRNG.__new__(NumpyRNG)
WrappedRNG.__init__(obj, seed=deepcopy(self.seed, memo),
parallel_safe=deepcopy(self.parallel_safe, memo))
obj.rng = deepcopy(self.rng)
return obj
[docs] def normal_clipped(self, mu=0.0, sigma=1.0, low=-numpy.inf, high=numpy.inf, size=None):
""" """
# not sure how well this works with parallel_safe, mask_local
gen = lambda n: self.rng.normal(loc=mu, scale=sigma, size=n)
return self._clipped(gen, low=low, high=high, size=size)
[docs] def normal_clipped_to_boundary(self, mu=0.0, sigma=1.0, low=-numpy.inf, high=numpy.inf, size=None):
# Not recommended, used `normal_clipped` instead.
# Provided because some models in the literature use this.
res = self.rng.normal(loc=mu, scale=sigma, size=size)
return numpy.maximum(numpy.minimum(res, high), low)
[docs]class GSLRNG(WrappedRNG):
"""Wrapper for the GSL random number generators."""
translations = {
'binomial': ('binomial', {'n': 'n', 'p': 'p'}),
'gamma': ('gamma', {'k': 'k', 'theta': 'theta'}),
'exponential': ('exponential', {'beta': 'mu'}),
'lognormal': ('lognormal', {'mu': 'zeta', 'sigma': 'sigma'}),
'normal': ('normal', {'mu': 'mu', 'sigma': 'sigma'}),
'normal_clipped': ('normal_clipped', {'mu': 'mu', 'sigma': 'sigma', 'low': 'low', 'high': 'high'}),
'poisson': ('poisson', {'lambda_': 'mu'}),
'uniform': ('flat', {'low': 'a', 'high': 'b'}),
'uniform_int': ('uniform_int', {'low': 'low', 'high': 'high'}),
}
def __init__(self, seed=None, type='mt19937', parallel_safe=True):
if not have_gsl:
raise ImportError("GSLRNG: Cannot import pygsl")
WrappedRNG.__init__(self, seed, parallel_safe)
self.rng = getattr(pygsl.rng, type)()
if self.seed is not None:
self.rng.set(self.seed)
else:
self.seed = int(time.time())
self.rng.set(self.seed)
def __getattr__(self, name):
"""This is to give GSLRNG the same methods as the GSL RNGs."""
return getattr(self.rng, name)
def _next(self, distribution, n, parameters):
distribution_gsl, parameter_map = self.translations[distribution]
if set(parameters.keys()) != set(parameter_map.keys()):
# all parameters must be provided. We do not provide default values (this can be discussed).
errmsg = "Incorrect parameterization of random distribution. Expected %s, got %s."
raise KeyError(errmsg % (parameter_map.keys(), parameters.keys()))
parameters_gsl = dict((parameter_map[k], v) for k, v in parameters.items())
if hasattr(self, distribution_gsl):
f_distr = getattr(self, distribution_gsl)
else:
f_distr = getattr(self.rng, distribution_gsl)
# Has this been tested? If so, move most of _next to Wrapped RNG since there is almost complete overlap with NumpyRNG._next
values = f_distr(size=n, **parameters_gsl)
if n == 1:
values = [values] # to be consistent with NumpyRNG
return values
[docs] def gamma(self, k, theta, size=None):
""" """
return self.rng.gamma(k, 1 / theta, size)
[docs] def normal(self, mu=0.0, sigma=1.0, size=None):
""" """
return mu + self.rng.gaussian(sigma, size)
[docs] def normal_clipped(self, mu=0.0, sigma=1.0, low=-numpy.inf, high=numpy.inf, size=None):
""" """
gen = lambda n: self.normal(mu, sigma, n)
return self._clipped(gen, low=low, high=high, size=size)
# should add a wrapper for the built-in Python random module.
[docs]class NativeRNG(AbstractRNG):
"""
Signals that the simulator's own native RNG should be used.
Each simulator module should implement a class of the same name which
inherits from this and which sets the seed appropriately.
"""
def __str__(self):
return "NativeRNG(seed=%s)" % self.seed
[docs]class RandomDistribution(VectorizedIterable):
"""
Class which defines a next(n) method which returns an array of `n` random
numbers from a given distribution.
Arguments:
`distribution`:
the name of a random number distribution.
`parameters_pos`:
parameters of the distribution, provided as a tuple. For the correct
ordering, see `random.available_distributions`.
`rng`:
if present, should be a :class:`NumpyRNG`, :class:`GSLRNG` or
:class:`NativeRNG` object.
`parameters_named`:
parameters of the distribution, provided as keyword arguments.
Parameters may be provided either through `parameters_pos` or through
`parameters_named`, but not both. All parameters must be provided, there
are no default values. Parameter names are, in general, as used in Wikipedia.
Examples:
>>> rd = RandomDistribution('uniform', (-70, -50))
>>> rd = RandomDistribution('normal', mu=0.5, sigma=0.1)
>>> rng = NumpyRNG(seed=8658764)
>>> rd = RandomDistribution('gamma', k=2.0, theta=5.0, rng=rng)
Available distributions:
========================== ==================== ====================================================
Name Parameters Comments
-------------------------- -------------------- ----------------------------------------------------
binomial n, p
gamma k, theta
exponential beta
lognormal mu, sigma
normal mu, sigma
normal_clipped mu, sigma, low, high Values outside (low, high) are redrawn
normal_clipped_to_boundary mu, sigma, low, high Values below/above low/high are set to low/high
poisson lambda_ Trailing underscore since lambda is a Python keyword
uniform low, high
uniform_int low, high
vonmises mu, kappa
========================== ==================== ====================================================
"""
def __init__(self, distribution, parameters_pos=None, rng=None, **parameters_named):
"""
Create a new RandomDistribution.
"""
self.name = distribution
self.parameters = self._resolve_parameters(parameters_pos, parameters_named)
if rng:
assert isinstance(rng, AbstractRNG), "rng must be a pyNN.random RNG object"
self.rng = rng
else: # use numpy.random.RandomState() by default
self.rng = NumpyRNG() # should we provide a seed?
[docs] def next(self, n=None, mask_local=None):
"""Return `n` random numbers from the distribution."""
res = self.rng.next(n=n,
distribution=self.name,
parameters=self.parameters,
mask_local=mask_local)
return res
def __str__(self):
return "RandomDistribution('%(name)s', %(parameters)s, %(rng)s)" % self.__dict__
def _resolve_parameters(self, positional, named):
if positional is None:
if set(named.keys()) != set(available_distributions[self.name]):
errmsg = "Incorrect parameterization of random distribution. Expected %s, got %s."
raise KeyError(errmsg % (available_distributions[self.name], tuple(named.keys())))
return named
elif len(named) == 0:
expected_parameter_names = available_distributions[self.name]
if len(positional) != len(expected_parameter_names):
errmsg = "Incorrect number of parameters for random distribution. For %s received %s"
raise ValueError(errmsg % (expected_parameter_names, positional))
else:
return dict((name, value) for name, value in zip(expected_parameter_names, positional))
else:
raise ValueError("Mixed positional and named parameters")