"""
Defines a common implementation of the built-in PyNN Connector classes.
Simulator modules may use these directly, or may implement their own versions
for improved performance.
:copyright: Copyright 2006-2024 by the PyNN team, see AUTHORS.
:license: CeCILL, see LICENSE for details.
"""
from .random import RandomDistribution, AbstractRNG, NumpyRNG
from .core import IndexBasedExpression
from . import errors, descriptions
from .recording import files
from .parameters import LazyArray
from .standardmodels import StandardSynapseType
import numpy as np
from itertools import repeat
import logging
from copy import copy, deepcopy
# the following imports are for use within eval()
from lazyarray import arccos, arcsin, arctan, arctan2, ceil, cos, cosh, exp, \
fabs, floor, fmod, hypot, ldexp, log, log10, modf, power, \
sin, sinh, sqrt, tan, tanh, maximum, minimum # noqa: F401
from numpy import e, pi # noqa: F401
try:
import csa
haveCSA = True
except ImportError:
haveCSA = False
logger = logging.getLogger("PyNN")
def _get_rng(rng):
if isinstance(rng, AbstractRNG):
return rng
elif rng is None:
return NumpyRNG(seed=151985012)
else:
raise Exception("rng must be either None, or a subclass of pyNN.random.AbstractRNG")
[docs]
class Connector(object):
"""
Base class for connectors.
All connector sub-classes have the following optional keyword arguments:
`location_selector`:
TO DO
`safe`:
if True, check that weights and delays have valid values. If False,
this check is skipped.
`callback`:
a function that will be called with the fractional progress of the
connection routine. An example would be `progress_bar.set_level`.
"""
def __init__(self, location_selector=None, safe=True, callback=None):
"""
docstring needed
"""
self.safe = safe
self.callback = callback
self.location_selector = location_selector
if callback is not None:
assert callable(callback)
[docs]
def connect(self, projection):
raise NotImplementedError()
[docs]
def get_parameters(self):
P = {}
for name in self.parameter_names:
P[name] = getattr(self, name)
return P
def _generate_distance_map(self, projection):
position_generators = (projection.pre.position_generator,
projection.post.position_generator)
return LazyArray(projection.space.distance_generator(*position_generators),
shape=projection.shape)
def _parameters_from_synapse_type(self, projection, distance_map=None):
"""
Obtain the parameters to be used for the connections from the projection's `synapse_type`
attribute. Each parameter value is a `LazyArray`.
"""
if distance_map is None:
distance_map = self._generate_distance_map(projection)
parameter_space = projection.synapse_type.native_parameters
# TODO: in the documentation, we claim that a parameter value can be
# a list or 1D array of the same length as the number of connections.
# We do not currently handle this scenario, although it is only
# really useful for fixed-number connectors anyway.
# Probably the best solution is to remove the parameter at this stage,
# then set it after the connections have already been created.
parameter_space.shape = (projection.pre.size, projection.post.size)
for name, map in parameter_space.items():
if callable(map.base_value):
if isinstance(map.base_value, IndexBasedExpression):
# Assumes map is a function of index and hence requires the projection to
# determine its value. It and its index function are copied so as to be able
# to set the projection without altering the connector, which would perhaps
# not be expected from the 'connect' call.
new_map = copy(map)
new_map.base_value = copy(map.base_value)
new_map.base_value.projection = projection
parameter_space[name] = new_map
else:
# Assumes map is a function of distance
parameter_space[name] = map(distance_map)
return parameter_space
[docs]
def describe(self, template='connector_default.txt', engine='default'):
"""
Returns a human-readable description of the connection method.
The output may be customized by specifying a different template
togther with an associated template engine (see ``pyNN.descriptions``).
If template is None, then a dictionary containing the template context
will be returned.
"""
context = {'name': self.__class__.__name__,
'parameters': self.get_parameters()}
return descriptions.render(engine, template, context)
class MapConnector(Connector):
"""
Abstract base class for Connectors based on connection maps, where a map is a 2D lazy array
containing either the (boolean) connectivity matrix (aka adjacency matrix, connection set
mask, etc.) or the values of a synaptic connection parameter.
"""
def _standard_connect(self, projection, connection_map_generator, distance_map=None):
"""
`connection_map_generator` should be a function or other callable, with one optional
argument `mask`, which returns an iterable.
The iterable should produce one element per post-synaptic neuron.
Each element should be either:
(i) a boolean array, indicating which of the pre-synaptic neurons
should be connected to,
(ii) an integer array indicating the same thing using indices,
(iii) or a single boolean, meaning connect to all/none.
The `mask` argument, a boolean array, can be used to limit processing to just
neurons which exist on the local MPI node.
todo: explain the argument `distance_map`.
"""
column_indices = np.arange(projection.post.size)
postsynaptic_indices = projection.post.id_to_index(projection.post.all_cells)
if (projection.synapse_type.native_parameters.parallel_safe
or hasattr(self, "rng") and self.rng.parallel_safe):
# If any of the synapse parameters are based on parallel-safe random number generators,
# we need to iterate over all post-synaptic cells, so we can generate then
# throw away the random numbers for the non-local nodes.
logger.debug("Parallel-safe iteration.")
components = (
column_indices,
postsynaptic_indices,
projection.post._mask_local,
connection_map_generator())
else:
# Otherwise, we only need to iterate over local post-synaptic cells.
mask = projection.post._mask_local
components = (
column_indices[mask],
postsynaptic_indices[mask],
repeat(True),
connection_map_generator(mask))
parameter_space = self._parameters_from_synapse_type(projection, distance_map)
# Loop over columns of the connection_map array
# (equivalent to looping over post-synaptic neurons)
for count, (col, postsynaptic_index, local, source_mask) in enumerate(zip(*components)):
# `col`: column index
# `postsynaptic_index`: index of the post-synaptic neuron
# `local`: boolean - does the post-synaptic neuron exist on this MPI node
# `source_mask`: boolean numpy array, indicating which of the pre-synaptic neurons
# should be connected to, or a single boolean, meaning connect to
# all/none of the pre-synaptic neurons.
# It can also be an array of addresses.
_proceed = False
if source_mask is True or source_mask.any():
_proceed = True
elif type(source_mask) == np.ndarray:
if source_mask.dtype == bool:
if source_mask.any():
_proceed = True
elif len(source_mask) > 0:
_proceed = True
if _proceed:
# Convert from boolean to integer mask, if necessary
if source_mask is True:
source_mask = np.arange(projection.pre.size, dtype=int)
elif source_mask.dtype == bool:
source_mask = source_mask.nonzero()[0]
# Evaluate the lazy arrays containing the synaptic parameters
connection_parameters = {}
for name, map in parameter_space.items():
if map.is_homogeneous:
connection_parameters[name] = map.evaluate(simplify=True)
else:
connection_parameters[name] = map[source_mask, col]
# Check that parameter values are valid
if self.safe:
# it might be cheaper to do the weight and delay check before evaluating the
# larray, however this is challenging to do if the base value is a function or
# if there are a lot of operations, so for simplicity we do the check after
# evaluation
syn = projection.synapse_type
if hasattr(syn, "parameter_checks"):
for parameter_name, check in syn.parameter_checks.items():
native_parameter_name = syn.translations[parameter_name]["translated_name"] # noqa:E501
# note that for delays we should also apply units scaling to the check
# values, since this currently only affects Brian we can probably
# handle that separately (for weights, checks are all based on zero)
if native_parameter_name in connection_parameters:
check(connection_parameters[native_parameter_name], projection)
if local:
# Connect the neurons
projection._convergent_connect(
source_mask, postsynaptic_index,
location_selector=self.location_selector,
**connection_parameters)
if self.callback:
self.callback(count / projection.post.local_size)
def _connect_with_map(self, projection, connection_map, distance_map=None):
"""
Create connections according to a connection map.
Arguments:
`projection`:
the `Projection` that is being created.
`connection_map`:
a boolean `LazyArray` of the same shape as `projection`,
representing the connectivity matrix.
`distance_map`:
TODO
"""
logger.debug("Connecting %s using a connection map" % projection.label)
self._standard_connect(projection, connection_map.by_column, distance_map)
def _get_connection_map_no_self_connections(self, projection):
from pyNN.common import Population
if (isinstance(projection.pre, Population)
and isinstance(projection.post, Population)
and projection.pre == projection.post):
# special case, expected to be faster than the default, below
connection_map = LazyArray(lambda i, j: i != j, shape=projection.shape)
else:
# this could be optimized by checking parent or component populations
# but should handle both views and assemblies
a = np.broadcast_to(projection.pre.all_cells,
(projection.post.size, projection.pre.size)).T
b = projection.post.all_cells
connection_map = LazyArray(a != b, shape=projection.shape)
return connection_map
def _get_connection_map_no_mutual_connections(self, projection):
from pyNN.common import Population
if (isinstance(projection.pre, Population)
and isinstance(projection.post, Population)
and projection.pre == projection.post):
connection_map = LazyArray(lambda i, j: i > j, shape=projection.shape)
else:
raise NotImplementedError("todo")
return connection_map
[docs]
class AllToAllConnector(MapConnector):
"""
Connects all cells in the presynaptic population to all cells in the
postsynaptic population.
Takes any of the standard :class:`Connector` optional arguments and, in
addition:
`allow_self_connections`:
if the connector is used to connect a Population to itself, this
flag determines whether a neuron is allowed to connect to itself,
or only to other neurons in the Population.
"""
parameter_names = ('allow_self_connections',)
def __init__(self, allow_self_connections=True,
location_selector=None,
safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe, callback)
assert isinstance(allow_self_connections, bool)
self.allow_self_connections = allow_self_connections
def connect(self, projection):
if not self.allow_self_connections:
connection_map = self._get_connection_map_no_self_connections(projection)
elif self.allow_self_connections == 'NoMutual':
connection_map = self._get_connection_map_no_mutual_connections(projection)
else:
connection_map = LazyArray(True, shape=projection.shape)
self._connect_with_map(projection, connection_map)
[docs]
class FixedProbabilityConnector(MapConnector):
"""
For each pair of pre-post cells, the connection probability is constant.
Takes any of the standard :class:`Connector` optional arguments and, in
addition:
`p_connect`:
a float between zero and one. Each potential connection is created
with this probability.
`allow_self_connections`:
if the connector is used to connect a Population to itself, this
flag determines whether a neuron is allowed to connect to itself,
or only to other neurons in the Population.
`rng`:
an :class:`RNG` instance used to evaluate whether connections exist
"""
parameter_names = ('allow_self_connections', 'p_connect')
def __init__(self, p_connect, allow_self_connections=True,
location_selector=None,
rng=None, safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe, callback)
assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
self.allow_self_connections = allow_self_connections
self.p_connect = float(p_connect)
assert 0 <= self.p_connect
self.rng = _get_rng(rng)
def connect(self, projection):
random_map = LazyArray(RandomDistribution('uniform', (0, 1), rng=self.rng),
projection.shape)
connection_map = random_map < self.p_connect
if not self.allow_self_connections:
mask = self._get_connection_map_no_self_connections(projection)
connection_map *= mask
elif self.allow_self_connections == 'NoMutual':
mask = self._get_connection_map_no_mutual_connections(projection)
connection_map *= mask
self._connect_with_map(projection, connection_map)
[docs]
class DistanceDependentProbabilityConnector(MapConnector):
"""
For each pair of pre-post cells, the connection probability depends on distance.
Takes any of the standard :class:`Connector` optional arguments and, in
addition:
`d_expression`:
the right-hand side of a valid Python expression for probability,
involving 'd', e.g. "exp(-abs(d))", or "d<3"
`allow_self_connections`:
if the connector is used to connect a Population to itself, this
flag determines whether a neuron is allowed to connect to itself,
or only to other neurons in the Population.
`rng`:
an :class:`RNG` instance used to evaluate whether connections exist
"""
parameter_names = ('allow_self_connections', 'd_expression')
def __init__(self, d_expression, allow_self_connections=True,
location_selector=None,
rng=None, safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe, callback)
assert isinstance(d_expression, str) or callable(d_expression)
assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
try:
if isinstance(d_expression, str):
d = 0 # noqa: F841 (`d` is used in eval)
assert 0 <= eval(d_expression), eval(d_expression)
d = 1e12 # noqa: F841
assert 0 <= eval(d_expression), eval(d_expression)
except ZeroDivisionError as err:
raise ZeroDivisionError("Error in the distance expression %s. %s" %
(d_expression, err))
self.d_expression = d_expression
self.allow_self_connections = allow_self_connections
self.distance_function = eval("lambda d: %s" % self.d_expression)
self.rng = _get_rng(rng)
def connect(self, projection):
distance_map = self._generate_distance_map(projection)
probability_map = self.distance_function(distance_map)
random_map = LazyArray(RandomDistribution('uniform', (0, 1), rng=self.rng),
projection.shape)
connection_map = random_map < probability_map
if not self.allow_self_connections:
mask = self._get_connection_map_no_self_connections(projection)
connection_map *= mask
elif self.allow_self_connections == 'NoMutual':
mask = self._get_connection_map_no_mutual_connections(projection)
connection_map *= mask
self._connect_with_map(projection, connection_map, distance_map)
[docs]
class IndexBasedProbabilityConnector(MapConnector):
"""
For each pair of pre-post cells, the connection probability depends on an arbitrary functions
that takes the indices of the pre and post populations.
Takes any of the standard :class:`Connector` optional arguments and, in
addition:
`index_expression`:
a function that takes the two cell indices as inputs and calculates the
probability matrix from it.
`allow_self_connections`:
if the connector is used to connect a Population to itself, this
flag determines whether a neuron is allowed to connect to itself,
or only to other neurons in the Population.
`rng`:
an :class:`RNG` instance used to evaluate whether connections exist
"""
parameter_names = ('allow_self_connections', 'index_expression')
def __init__(self, index_expression, allow_self_connections=True,
location_selector=None,
rng=None, safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe, callback)
assert callable(index_expression)
assert isinstance(index_expression, IndexBasedExpression)
assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
self.index_expression = index_expression
self.allow_self_connections = allow_self_connections
self.rng = _get_rng(rng)
def connect(self, projection):
# The index function is copied so as to avoid the connector being altered by the "connect"
# function, which is probably unexpected behaviour.
index_expression = copy(self.index_expression)
index_expression.projection = projection
probability_map = LazyArray(index_expression, projection.shape)
random_map = LazyArray(RandomDistribution('uniform', (0, 1), rng=self.rng),
projection.shape)
connection_map = random_map < probability_map
if not self.allow_self_connections:
mask = self._get_connection_map_no_self_connections(projection)
connection_map *= mask
elif self.allow_self_connections == 'NoMutual':
mask = self._get_connection_map_no_mutual_connections(projection)
connection_map *= mask
self._connect_with_map(projection, connection_map)
[docs]
class DisplacementDependentProbabilityConnector(IndexBasedProbabilityConnector):
"""
For each pair of pre-post cells, the connection probability depends on the
displacement of the two neurons, i.e. on the triplet (dx, dy, dz) where
dx is the distance between the x-coordinates of the two neurons, and so on.
Takes any of the standard :class:`Connector` optional arguments and, in
addition:
`disp_function`:
the right-hand side of a valid Python expression for probability,
involving an array named 'd' whose first dimension has size 3.
e.g. "(d[0] < 3) * (d[1] < 2) * exp(-abs(d[2]))"
`allow_self_connections`:
if the connector is used to connect a Population to itself, this
flag determines whether a neuron is allowed to connect to itself,
or only to other neurons in the Population.
`rng`:
an :class:`RNG` instance used to evaluate whether connections exist
"""
class DisplacementExpression(IndexBasedExpression):
"""
A displacement based expression function used to determine the connection probability
and the value of variable connection parameters of a projection
"""
def __init__(self, disp_function):
"""
`disp_function`: a function that takes a 3xN numpy displacement matrix and maps each
row (displacement) to a probability between 0 and 1
"""
self._disp_function = disp_function
def __call__(self, i, j):
disp = (self.projection.post.positions.T[j] - self.projection.pre.positions.T[i]).T
return self._disp_function(disp)
def __init__(self, disp_function, allow_self_connections=True,
location_selector=None,
rng=None, safe=True, callback=None):
super(DisplacementDependentProbabilityConnector, self).__init__(
self.DisplacementExpression(disp_function),
allow_self_connections=allow_self_connections, rng=rng, callback=callback)
[docs]
class FromListConnector(Connector):
"""
Make connections according to a list.
Arguments:
`conn_list`:
a list of tuples, one tuple for each connection. Each tuple should contain:
`(pre_idx, post_idx, p1, p2, ..., pn)` where `pre_idx` is the index
(i.e. order in the Population, not the ID) of the presynaptic
neuron, `post_idx` is the index of the postsynaptic neuron, and
p1, p2, etc. are the synaptic parameters (e.g. weight, delay,
plasticity parameters).
`column_names`:
the names of the parameters p1, p2, etc. If not provided, it is
assumed the parameters are 'weight', 'delay' (for backwards
compatibility). This should be specified using a tuple.
`safe`:
if True, check that weights and delays have valid values. If False,
this check is skipped.
`callback`:
if True, display a progress bar on the terminal.
"""
parameter_names = ('conn_list',)
def __init__(self, conn_list, column_names=None,
location_selector=None, safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe=safe, callback=callback)
self.conn_list = np.array(conn_list)
if len(conn_list) > 0:
n_columns = self.conn_list.shape[1]
if column_names is None:
if n_columns == 2:
self.column_names = ()
elif n_columns == 4:
self.column_names = ('weight', 'delay')
else:
raise TypeError("Argument 'column_names' is required.")
else:
self.column_names = column_names
if n_columns != len(self.column_names) + 2:
raise ValueError(f"connection list has {n_columns - 2} parameter columns, "
f"but {len(self.column_names)} column names provided.")
else:
self.column_names = ()
def connect(self, projection):
"""Connect-up a Projection."""
logger.debug("conn_list (original) = \n%s", self.conn_list)
synapse_parameter_names = projection.synapse_type.get_parameter_names()
for name in self.column_names:
if name not in synapse_parameter_names:
raise ValueError("%s is not a valid parameter for %s" % (
name, projection.synapse_type.__class__.__name__))
if self.conn_list.size == 0:
return
if np.any(self.conn_list[:, 0] >= projection.pre.size):
raise errors.ConnectionError("source index out of range")
# need to do some profiling, to figure out the best way to do this:
# - order of sorting/filtering by local
# - use np.unique, or just do in1d(self.conn_list)?
idx = np.argsort(self.conn_list[:, 1])
targets = np.unique(self.conn_list[:, 1]).astype(int)
local = np.in1d(targets,
np.arange(projection.post.size)[projection.post._mask_local],
assume_unique=True)
local_targets = targets[local]
self.conn_list = self.conn_list[idx]
left = np.searchsorted(self.conn_list[:, 1], local_targets, 'left')
right = np.searchsorted(self.conn_list[:, 1], local_targets, 'right')
logger.debug("idx = %s", idx)
logger.debug("targets = %s", targets)
logger.debug("local_targets = %s", local_targets)
logger.debug("conn_list (sorted by target) = \n%s", self.conn_list)
logger.debug("left = %s", left)
logger.debug("right = %s", right)
for tgt, l, r in zip(local_targets, left, right):
sources = self.conn_list[l:r, 0].astype(int)
connection_parameters = deepcopy(projection.synapse_type.parameter_space)
connection_parameters.shape = (r - l,)
for col, name in enumerate(self.column_names, 2):
connection_parameters.update(**{name: self.conn_list[l:r, col]})
if isinstance(projection.synapse_type, StandardSynapseType):
connection_parameters = projection.synapse_type.translate(
connection_parameters)
connection_parameters.evaluate()
projection._convergent_connect(sources, tgt,
location_selector=self.location_selector,
**connection_parameters)
[docs]
class FromFileConnector(FromListConnector):
"""
Make connections according to a list read from a file.
Arguments:
`file`:
either an open file object or the filename of a file containing a
list of connections, in the format required by `FromListConnector`.
Column headers, if included in the file, must be specified using
a list or tuple, e.g.::
# columns = ["i", "j", "weight", "delay", "U", "tau_rec"]
Note that the header requires `#` at the beginning of the line.
`distributed`:
if this is True, then each node will read connections from a file
called `filename.x`, where `x` is the MPI rank. This speeds up
loading connections for distributed simulations.
`safe`:
if True, check that weights and delays have valid values. If False,
this check is skipped.
`callback`:
if True, display a progress bar on the terminal.
"""
parameter_names = ('file', 'distributed')
def __init__(self, file, distributed=False,
location_selector=None, safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe=safe, callback=callback)
if isinstance(file, str):
file = files.StandardTextFile(file, mode='r')
self.file = file
self.distributed = distributed
def connect(self, projection):
"""Connect-up a Projection."""
if self.distributed:
self.file.rename("%s.%d" % (self.file.name,
projection._simulator.state.mpi_rank))
self.column_names = self.file.get_metadata().get('columns', ('weight', 'delay'))
for ignore in "ij":
if ignore in self.column_names:
self.column_names.remove(ignore)
self.conn_list = self.file.read()
FromListConnector.connect(self, projection)
class FixedNumberConnector(MapConnector):
# base class - should not be instantiated
parameter_names = ('allow_self_connections', 'n')
def __init__(self, n, allow_self_connections=True, with_replacement=False,
location_selector=None,
rng=None, safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe, callback)
assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
self.allow_self_connections = allow_self_connections
self.with_replacement = with_replacement
self.n = n
if isinstance(n, int):
assert n >= 0
elif isinstance(n, RandomDistribution):
# weak check that the random distribution is ok
err_msg = "the random distribution produces negative numbers"
assert np.all(np.array(n.next(100)) >= 0), err_msg
else:
raise TypeError("n must be an integer or a RandomDistribution object")
self.rng = _get_rng(rng)
def _rng_uniform_int_exclude(self, n, size, exclude):
res = self.rng.next(n, 'uniform_int', {"low": 0, "high": size}, mask=None)
logger.debug("RNG0 res=%s" % res)
idx = np.where(res == exclude)[0]
logger.debug("RNG1 exclude=%d, res=%s idx=%s" % (exclude, res, idx))
while idx.size > 0:
redrawn = self.rng.next(idx.size, 'uniform_int', {"low": 0, "high": size}, mask=None)
res[idx] = redrawn
idx = idx[np.where(res == exclude)[0]]
logger.debug("RNG2 exclude=%d redrawn=%s res=%s idx=%s" % (exclude, redrawn, res, idx))
return res
[docs]
class FixedNumberPostConnector(FixedNumberConnector):
"""
Each pre-synaptic neuron is connected to exactly `n` post-synaptic neurons
chosen at random.
The sampling behaviour is controlled by the `with_replacement` argument.
"With replacement" means that each post-synaptic neuron is chosen from the
entire population. There is always therefore a possibility of multiple
connections between a given pair of neurons.
"Without replacement" means that once a neuron has been selected, it cannot
be selected again until the entire population has been selected. This means
that if `n` is less than the size of the post-synaptic population, there
are no multiple connections. If `n` is greater than the size of the post-
synaptic population, all possible single connections are made before
starting to add duplicate connections.
Takes any of the standard :class:`Connector` optional arguments and, in
addition:
`n`:
either a positive integer, or a `RandomDistribution` that produces
positive integers. If `n` is a `RandomDistribution`, then the
number of post-synaptic neurons is drawn from this distribution
for each pre-synaptic neuron.
`with_replacement`:
if True, the selection of neurons to connect is made from the
entire population. If False, once a neuron is selected it cannot
be selected again until the entire population has been connected.
`allow_self_connections`:
if the connector is used to connect a Population to itself, this
flag determines whether a neuron is allowed to connect to itself,
or only to other neurons in the Population.
`rng`:
an :class:`RNG` instance used to evaluate which potential connections
are created.
"""
def _get_num_post(self):
if isinstance(self.n, int):
n_post = self.n
else:
n_post = self.n.next()
return n_post
def connect(self, projection):
connections = [[] for i in range(projection.post.size)]
for source_index in range(projection.pre.size):
n = self._get_num_post()
if self.with_replacement:
if not self.allow_self_connections and projection.pre == projection.post:
targets = self._rng_uniform_int_exclude(n, projection.post.size, source_index)
else:
targets = self.rng.next(
n, 'uniform_int', {"low": 0, "high": projection.post.size}, mask=None)
else:
all_cells = np.arange(projection.post.size)
if not self.allow_self_connections and projection.pre == projection.post:
all_cells = all_cells[all_cells != source_index]
full_sets = n // all_cells.size
remainder = n % all_cells.size
target_sets = []
if full_sets > 0:
target_sets = [all_cells] * full_sets
if remainder > 0:
target_sets.append(self.rng.permutation(all_cells)[:remainder])
targets = np.hstack(target_sets)
assert targets.size == n
for target_index in targets:
connections[target_index].append(source_index)
def build_source_masks(mask=None):
if mask is None:
return [np.array(x) for x in connections]
else:
return [np.array(x) for x in np.array(connections)[mask]]
self._standard_connect(projection, build_source_masks)
[docs]
class FixedNumberPreConnector(FixedNumberConnector):
"""
Each post-synaptic neuron is connected to exactly `n` pre-synaptic neurons
chosen at random.
The sampling behaviour is controlled by the `with_replacement` argument.
"With replacement" means that each pre-synaptic neuron is chosen from the
entire population. There is always therefore a possibility of multiple
connections between a given pair of neurons.
"Without replacement" means that once a neuron has been selected, it cannot
be selected again until the entire population has been selected. This means
that if `n` is less than the size of the pre-synaptic population, there
are no multiple connections. If `n` is greater than the size of the pre-
synaptic population, all possible single connections are made before
starting to add duplicate connections.
Takes any of the standard :class:`Connector` optional arguments and, in
addition:
`n`:
either a positive integer, or a `RandomDistribution` that produces
positive integers. If `n` is a `RandomDistribution`, then the
number of pre-synaptic neurons is drawn from this distribution
for each post-synaptic neuron.
`with_replacement`:
if True, the selection of neurons to connect is made from the
entire population. If False, once a neuron is selected it cannot
be selected again until the entire population has been connected.
`allow_self_connections`:
if the connector is used to connect a Population to itself, this
flag determines whether a neuron is allowed to connect to itself,
or only to other neurons in the Population.
`rng`:
an :class:`RNG` instance used to evaluate which potential connections
are created.
"""
def _get_num_pre(self, size, mask=None):
if isinstance(self.n, int):
if mask is None:
n_pre = repeat(self.n, size)
else:
n_pre = repeat(self.n, mask.sum())
else:
if mask is None:
n_pre = self.n.next(size)
else:
if self.n.rng.parallel_safe:
n_pre = self.n.next(size)[mask]
else:
n_pre = self.n.next(mask.sum())
return n_pre
def connect(self, projection):
if self.with_replacement:
if self.allow_self_connections or projection.pre != projection.post:
def build_source_masks(mask=None):
n_pre = self._get_num_pre(projection.post.size, mask)
for n in n_pre:
sources = self.rng.next(
n, 'uniform_int', {"low": 0, "high": projection.pre.size}, mask=None)
assert sources.size == n
yield sources
else:
def build_source_masks(mask=None):
n_pre = self._get_num_pre(projection.post.size, mask)
if self.rng.parallel_safe or mask is None:
for i, n in enumerate(n_pre):
sources = self._rng_uniform_int_exclude(n, projection.pre.size, i)
assert sources.size == n
yield sources
else:
# TODO: use mask to obtain indices i
raise NotImplementedError(
"allow_self_connections=False currently requires a parallel safe RNG.")
else:
if self.allow_self_connections or projection.pre != projection.post:
def build_source_masks(mask=None):
# where n > projection.pre.size, first all pre-synaptic cells
# are connected one or more times, then the remainder
# are chosen randomly
n_pre = self._get_num_pre(projection.post.size, mask)
all_cells = np.arange(projection.pre.size)
for n in n_pre:
full_sets = n // projection.pre.size
remainder = n % projection.pre.size
source_sets = []
if full_sets > 0:
source_sets = [all_cells] * full_sets
if remainder > 0:
source_sets.append(self.rng.permutation(all_cells)[:remainder])
sources = np.hstack(source_sets)
assert sources.size == n
yield sources
else:
def build_source_masks(mask=None):
# where n > projection.pre.size, first all pre-synaptic cells
# are connected one or more times, then the remainder
# are chosen randomly
n_pre = self._get_num_pre(projection.post.size, mask)
all_cells = np.arange(projection.pre.size)
if self.rng.parallel_safe or mask is None:
for i, n in enumerate(n_pre):
full_sets = n // (projection.pre.size - 1)
remainder = n % (projection.pre.size - 1)
allowed_cells = all_cells[all_cells != i]
source_sets = []
if full_sets > 0:
source_sets = [allowed_cells] * full_sets
if remainder > 0:
source_sets.append(self.rng.permutation(allowed_cells)[:remainder])
sources = np.hstack(source_sets)
assert sources.size == n
yield sources
else:
raise NotImplementedError(
"allow_self_connections=False currently requires a parallel safe RNG.")
self._standard_connect(projection, build_source_masks)
[docs]
class OneToOneConnector(MapConnector):
"""
Where the pre- and postsynaptic populations have the same size, connect
cell *i* in the presynaptic population to cell *i* in the postsynaptic
population for all *i*.
Takes any of the standard :class:`Connector` optional arguments.
"""
parameter_names = tuple()
def connect(self, projection):
"""Connect-up a Projection."""
connection_map = LazyArray(lambda i, j: i == j, shape=projection.shape)
self._connect_with_map(projection, connection_map)
[docs]
class SmallWorldConnector(Connector):
"""
Connect cells so as to create a small-world network.
Takes any of the standard :class:`Connector` optional arguments and, in
addition:
`degree`:
the region length where nodes will be connected locally.
`rewiring`:
the probability of rewiring each edge.
`allow_self_connections`:
if the connector is used to connect a Population to itself, this
flag determines whether a neuron is allowed to connect to itself,
or only to other neurons in the Population.
`n_connections`:
if specified, the number of efferent synaptic connections per neuron.
`rng`:
an :class:`RNG` instance used to evaluate which connections
are created.
"""
parameter_names = ('allow_self_connections', 'degree', 'rewiring', 'n_connections')
def __init__(self, degree, rewiring, allow_self_connections=True,
n_connections=None, location_selector=None,
rng=None, safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe, callback)
assert 0 <= rewiring <= 1
assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
self.rewiring = rewiring
self.d_expression = "d < %g" % degree
self.allow_self_connections = allow_self_connections
self.n_connections = n_connections
self.rng = _get_rng(rng)
def connect(self, projection):
"""Connect-up a Projection."""
raise NotImplementedError
[docs]
class CSAConnector(MapConnector):
"""
Use the Connection Set Algebra (Djurfeldt, 2012) to connect cells.
Takes any of the standard :class:`Connector` optional arguments and, in
addition:
`cset`:
a connection set object.
"""
parameter_names = ('cset',)
if haveCSA:
def __init__(self, cset, location_selector=None, safe=True, callback=None):
"""
"""
Connector.__init__(self, location_selector, safe=safe, callback=callback)
self.cset = cset
arity = csa.arity(cset)
assert arity in (0, 2), 'must specify mask or connection-set with arity 0 or 2'
else:
def __init__(self, cset, safe=True, callback=None):
raise RuntimeError("CSAConnector not available---couldn't import csa module")
def connect(self, projection):
"""Connect-up a Projection."""
# Cut out finite part
c = csa.cross((0, projection.pre.size - 1), (0, projection.post.size - 1)) * \
self.cset # can't we cut out just the columns we want?
if csa.arity(self.cset) == 2:
# Connection-set with arity 2
for (i, j, weight, delay) in c:
projection._convergent_connect([projection.pre[i]], projection.post[j],
location_selector=self.location_selector,
weight=weight, delay=delay)
elif csa.arity(self.cset) == 0:
# inefficient implementation as a starting point
connection_map = np.zeros((projection.pre.size, projection.post.size), dtype=bool)
for addr in c:
connection_map[addr] = True
self._connect_with_map(projection, LazyArray(connection_map))
else:
raise NotImplementedError
[docs]
class CloneConnector(MapConnector):
"""
Connects cells with the same connectivity pattern as a previous projection.
"""
parameter_names = ('reference_projection',)
def __init__(self, reference_projection, safe=True, callback=None):
"""
Create a new CloneConnector.
`reference_projection` -- the projection to clone the connectivity pattern from
"""
MapConnector.__init__(self, location_selector=None,
safe=safe, callback=callback)
self.reference_projection = reference_projection
def connect(self, projection):
if (projection.pre != self.reference_projection.pre or
projection.post != self.reference_projection.post):
raise errors.ConnectionError(
"Pre and post populations must match between reference ({0}"
" and {1}) and clone projections ({2} and {3}) for CloneConnector".format(
self.reference_projection.pre,
self.reference_projection.post,
projection.pre, projection.post))
connection_map = LazyArray(~np.isnan(self.reference_projection.get(['weight'], 'array',
gather='all')[0]))
self._connect_with_map(projection, connection_map)
[docs]
class ArrayConnector(MapConnector):
"""
Provide an explicit boolean connection matrix, with shape (m, n) where m is
the size of the presynaptic population and n that of the postsynaptic
population.
"""
parameter_names = ('array',)
def __init__(self, array, location_selector=None, safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe, callback)
self.array = array
def connect(self, projection):
connection_map = LazyArray(self.array, projection.shape)
self._connect_with_map(projection, connection_map)
[docs]
class FixedTotalNumberConnector(FixedNumberConnector):
parameter_names = ('allow_self_connections', 'n')
def __init__(self, n, allow_self_connections=True, with_replacement=True,
location_selector=None,
rng=None, safe=True, callback=None):
"""
Create a new connector.
"""
Connector.__init__(self, location_selector, safe, callback)
assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
self.allow_self_connections = allow_self_connections
self.with_replacement = with_replacement
self.n = n
if isinstance(n, int):
assert n >= 0
elif isinstance(n, RandomDistribution):
# weak check that the random distribution is ok
err_msg = "the random distribution produces negative numbers"
assert np.all(np.array(n.next(100)) >= 0), err_msg
else:
raise TypeError("n must be an integer or a RandomDistribution object")
self.rng = _get_rng(rng)
def connect(self, projection):
# This implementation is not "parallel safe" for random numbers.
# todo: support the `parallel_safe` flag.
# Determine number of processes and current rank
rank = projection._simulator.state.mpi_rank
num_processes = projection._simulator.state.num_processes
# Assume that targets are equally distributed over processes
targets_per_process = int(len(projection.post) / num_processes)
# Calculate the number of synapses on each process
bino = RandomDistribution('binomial',
[self.n, targets_per_process / len(projection.post)],
rng=self.rng)
num_conns_on_vp = np.zeros(num_processes, dtype=int)
sum_dist = 0
sum_partitions = 0
for k in range(num_processes):
p_local = targets_per_process / (len(projection.post) - sum_dist)
bino.parameters['p'] = p_local
bino.parameters['n'] = self.n - sum_partitions
num_conns_on_vp[k] = bino.next()
sum_dist += targets_per_process
sum_partitions += num_conns_on_vp[k]
# Draw random sources and targets
connections = [[] for i in range(projection.post.size)]
possible_targets = np.arange(projection.post.size)[projection.post._mask_local]
for i in range(num_conns_on_vp[rank]):
source_index = self.rng.next(1, 'uniform_int',
{"low": 0, "high": projection.pre.size},
mask=None)[0]
target_index = self.rng.choice(possible_targets, size=1)[0]
connections[target_index].append(source_index)
def build_source_masks(mask=None):
if mask is None:
return [np.array(x) for x in connections]
else:
return [np.array(x) for x in np.array(connections)[mask]]
self._standard_connect(projection, build_source_masks)