1"""
2Defines a common implementation of the built-in PyNN Connector classes.
3
4Simulator modules may use these directly, or may implement their own versions
5for improved performance.
6
7:copyright: Copyright 2006-2021 by the PyNN team, see AUTHORS.
8:license: CeCILL, see LICENSE for details.
9"""
10
11from pyNN.random import RandomDistribution, AbstractRNG, NumpyRNG, get_mpi_config
12from pyNN.core import IndexBasedExpression
13from pyNN import errors, descriptions
14from pyNN.recording import files
15from pyNN.parameters import LazyArray
16from pyNN.standardmodels import StandardSynapseType
17from pyNN.common import Population
18import numpy as np
19from itertools import repeat
20import logging
21from copy import copy, deepcopy
22
23from lazyarray import arccos, arcsin, arctan, arctan2, ceil, cos, cosh, exp, \
24    fabs, floor, fmod, hypot, ldexp, log, log10, modf, power, \
25    sin, sinh, sqrt, tan, tanh, maximum, minimum
26from numpy import e, pi
27
28try:
29    import csa
30    haveCSA = True
31except ImportError:
32    haveCSA = False
33
34logger = logging.getLogger("PyNN")
35
36
37def _get_rng(rng):
38    if isinstance(rng, AbstractRNG):
39        return rng
40    elif rng is None:
41        return NumpyRNG(seed=151985012)
42    else:
43        raise Exception("rng must be either None, or a subclass of pyNN.random.AbstractRNG")
44
45
46class Connector(object):
47    """
48    Base class for connectors.
49
50    All connector sub-classes have the following optional keyword arguments:
51        `safe`:
52            if True, check that weights and delays have valid values. If False,
53            this check is skipped.
54        `callback`:
55            a function that will be called with the fractional progress of the
56            connection routine. An example would be `progress_bar.set_level`.
57    """
58
59    def __init__(self, safe=True, callback=None):
60        """
61        docstring needed
62        """
63        self.safe = safe
64        self.callback = callback
65        if callback is not None:
66            assert callable(callback)
67
68    def connect(self, projection):
69        raise NotImplementedError()
70
71    def get_parameters(self):
72        P = {}
73        for name in self.parameter_names:
74            P[name] = getattr(self, name)
75        return P
76
77    def _generate_distance_map(self, projection):
78        position_generators = (projection.pre.position_generator,
79                               projection.post.position_generator)
80        return LazyArray(projection.space.distance_generator(*position_generators),
81                         shape=projection.shape)
82
83    def _parameters_from_synapse_type(self, projection, distance_map=None):
84        """
85        Obtain the parameters to be used for the connections from the projection's `synapse_type`
86        attribute. Each parameter value is a `LazyArray`.
87        """
88        if distance_map is None:
89            distance_map = self._generate_distance_map(projection)
90        parameter_space = projection.synapse_type.native_parameters
91        # TODO: in the documentation, we claim that a parameter value can be
92        #       a list or 1D array of the same length as the number of connections.
93        #       We do not currently handle this scenario, although it is only
94        #       really useful for fixed-number connectors anyway.
95        #       Probably the best solution is to remove the parameter at this stage,
96        #       then set it after the connections have already been created.
97        parameter_space.shape = (projection.pre.size, projection.post.size)
98        for name, map in parameter_space.items():
99            if callable(map.base_value):
100                if isinstance(map.base_value, IndexBasedExpression):
101                    # Assumes map is a function of index and hence requires the projection to
102                    # determine its value. It and its index function are copied so as to be able
103                    # to set the projection without altering the connector, which would perhaps
104                    # not be expected from the 'connect' call.
105                    new_map = copy(map)
106                    new_map.base_value = copy(map.base_value)
107                    new_map.base_value.projection = projection
108                    parameter_space[name] = new_map
109                else:
110                    # Assumes map is a function of distance
111                    parameter_space[name] = map(distance_map)
112        return parameter_space
113
114    def describe(self, template='connector_default.txt', engine='default'):
115        """
116        Returns a human-readable description of the connection method.
117
118        The output may be customized by specifying a different template
119        togther with an associated template engine (see ``pyNN.descriptions``).
120
121        If template is None, then a dictionary containing the template context
122        will be returned.
123        """
124        context = {'name': self.__class__.__name__,
125                   'parameters': self.get_parameters()}
126        return descriptions.render(engine, template, context)
127
128
129class MapConnector(Connector):
130    """
131    Abstract base class for Connectors based on connection maps, where a map is a 2D lazy array
132    containing either the (boolean) connectivity matrix (aka adjacency matrix, connection set mask, etc.)
133    or the values of a synaptic connection parameter.
134    """
135
136    def _standard_connect(self, projection, connection_map_generator, distance_map=None):
137        """
138
139        `connection_map_generator` should be a function or other callable, with one optional
140        argument `mask`, which returns an iterable.
141
142        The iterable should produce one element per post-synaptic neuron.
143
144        Each element should be either:
145            (i) a boolean array, indicating which of the pre-synaptic neurons
146                should be connected to,
147            (ii) an integer array indicating the same thing using indices,
148            (iii) or a single boolean, meaning connect to all/none.
149
150        The `mask` argument, a boolean array, can be used to limit processing to just
151        neurons which exist on the local MPI node.
152
153        todo: explain the argument `distance_map`.
154        """
155
156        column_indices = np.arange(projection.post.size)
157        postsynaptic_indices = projection.post.id_to_index(projection.post.all_cells)
158
159        if (projection.synapse_type.native_parameters.parallel_safe
160                or hasattr(self, "rng") and self.rng.parallel_safe):
161
162            # If any of the synapse parameters are based on parallel-safe random number generators,
163            # we need to iterate over all post-synaptic cells, so we can generate then
164            # throw away the random numbers for the non-local nodes.
165            logger.debug("Parallel-safe iteration.")
166            components = (
167                column_indices,
168                postsynaptic_indices,
169                projection.post._mask_local,
170                connection_map_generator())
171        else:
172            # Otherwise, we only need to iterate over local post-synaptic cells.
173            mask = projection.post._mask_local
174            components = (
175                column_indices[mask],
176                postsynaptic_indices[mask],
177                repeat(True),
178                connection_map_generator(mask))
179
180        parameter_space = self._parameters_from_synapse_type(projection, distance_map)
181
182        # Loop over columns of the connection_map array (equivalent to looping over post-synaptic neurons)
183        for count, (col, postsynaptic_index, local, source_mask) in enumerate(zip(*components)):
184            # `col`: column index
185            # `postsynaptic_index`: index of the post-synaptic neuron
186            # `local`: boolean - does the post-synaptic neuron exist on this MPI node
187            # `source_mask`: boolean numpy array, indicating which of the pre-synaptic neurons should be connected to,
188            #                or a single boolean, meaning connect to all/none of the pre-synaptic neurons
189            #                It can also be an array of addresses
190            _proceed = False
191            if source_mask is True or source_mask.any():
192                _proceed = True
193            elif type(source_mask) == np.ndarray:
194                if source_mask.dtype == bool:
195                    if source_mask.any():
196                        _proceed = True
197                elif len(source_mask) > 0:
198                    _proceed = True
199            if _proceed:
200                # Convert from boolean to integer mask, if necessary
201                if source_mask is True:
202                    source_mask = np.arange(projection.pre.size, dtype=int)
203                elif source_mask.dtype == bool:
204                    source_mask = source_mask.nonzero()[0]
205
206                # Evaluate the lazy arrays containing the synaptic parameters
207                connection_parameters = {}
208                for name, map in parameter_space.items():
209                    if map.is_homogeneous:
210                        connection_parameters[name] = map.evaluate(simplify=True)
211                    else:
212                        connection_parameters[name] = map[source_mask, col]
213
214                # Check that parameter values are valid
215                if self.safe:
216                    # it might be cheaper to do the weight and delay check before evaluating the larray,
217                    # however this is challenging to do if the base value is a function or if there are
218                    # a lot of operations, so for simplicity we do the check after evaluation
219                    syn = projection.synapse_type
220                    if hasattr(syn, "parameter_checks"):
221                        #raise Exception(f"{connection_parameters} {syn.parameter_checks}")
222                        for parameter_name, check in syn.parameter_checks.items():
223                            native_parameter_name = syn.translations[parameter_name]["translated_name"]
224                            # note that for delays we should also apply units scaling to the check values
225                            # since this currently only affects Brian we can probably handle that separately
226                            # (for weights the checks are all based on zero)
227                            if native_parameter_name in connection_parameters:
228                                check(connection_parameters[native_parameter_name], projection)
229
230                if local:
231                    # Connect the neurons
232                    #logger.debug("Connecting to %d from %s" % (postsynaptic_index, source_mask))
233                    projection._convergent_connect(
234                        source_mask, postsynaptic_index, **connection_parameters)
235                    if self.callback:
236                        self.callback(count / projection.post.local_size)
237
238    def _connect_with_map(self, projection, connection_map, distance_map=None):
239        """
240        Create connections according to a connection map.
241
242        Arguments:
243
244            `projection`:
245                the `Projection` that is being created.
246            `connection_map`:
247                a boolean `LazyArray` of the same shape as `projection`, representing the connectivity matrix.
248            `distance_map`:
249                TODO
250        """
251        logger.debug("Connecting %s using a connection map" % projection.label)
252        self._standard_connect(projection, connection_map.by_column, distance_map)
253
254    def _get_connection_map_no_self_connections(self, projection):
255        if (isinstance(projection.pre, Population)
256                and isinstance(projection.post, Population)
257                and projection.pre == projection.post):
258            # special case, expected to be faster than the default, below
259            connection_map = LazyArray(lambda i, j: i != j, shape=projection.shape)
260        else:
261            # this could be optimized by checking parent or component populations
262            # but should handle both views and assemblies
263            a = np.broadcast_to(projection.pre.all_cells,
264                                   (projection.post.size, projection.pre.size)).T
265            b = projection.post.all_cells
266            connection_map = LazyArray(a != b, shape=projection.shape)
267        return connection_map
268
269    def _get_connection_map_no_mutual_connections(self, projection):
270        if (isinstance(projection.pre, Population)
271            and isinstance(projection.post, Population)
272                and projection.pre == projection.post):
273            connection_map = LazyArray(lambda i, j: i > j, shape=projection.shape)
274        else:
275            raise NotImplementedError("todo")
276        return connection_map
277
278
279class AllToAllConnector(MapConnector):
280    """
281    Connects all cells in the presynaptic population to all cells in the
282    postsynaptic population.
283
284    Takes any of the standard :class:`Connector` optional arguments and, in
285    addition:
286
287        `allow_self_connections`:
288            if the connector is used to connect a Population to itself, this
289            flag determines whether a neuron is allowed to connect to itself,
290            or only to other neurons in the Population.
291    """
292    parameter_names = ('allow_self_connections',)
293
294    def __init__(self, allow_self_connections=True, safe=True,
295                 callback=None):
296        """
297        Create a new connector.
298        """
299        Connector.__init__(self, safe, callback)
300        assert isinstance(allow_self_connections, bool)
301        self.allow_self_connections = allow_self_connections
302
303    def connect(self, projection):
304        if not self.allow_self_connections:
305            connection_map = self._get_connection_map_no_self_connections(projection)
306        elif self.allow_self_connections == 'NoMutual':
307            connection_map = self._get_connection_map_no_mutual_connections(projection)
308        else:
309            connection_map = LazyArray(True, shape=projection.shape)
310        self._connect_with_map(projection, connection_map)
311
312
313class FixedProbabilityConnector(MapConnector):
314    """
315    For each pair of pre-post cells, the connection probability is constant.
316
317    Takes any of the standard :class:`Connector` optional arguments and, in
318    addition:
319
320        `p_connect`:
321            a float between zero and one. Each potential connection is created
322            with this probability.
323        `allow_self_connections`:
324            if the connector is used to connect a Population to itself, this
325            flag determines whether a neuron is allowed to connect to itself,
326            or only to other neurons in the Population.
327        `rng`:
328            an :class:`RNG` instance used to evaluate whether connections exist
329    """
330    parameter_names = ('allow_self_connections', 'p_connect')
331
332    def __init__(self, p_connect, allow_self_connections=True,
333                 rng=None, safe=True, callback=None):
334        """
335        Create a new connector.
336        """
337        Connector.__init__(self, safe, callback)
338        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
339        self.allow_self_connections = allow_self_connections
340        self.p_connect = float(p_connect)
341        assert 0 <= self.p_connect
342        self.rng = _get_rng(rng)
343
344    def connect(self, projection):
345        random_map = LazyArray(RandomDistribution('uniform', (0, 1), rng=self.rng),
346                               projection.shape)
347        connection_map = random_map < self.p_connect
348        if not self.allow_self_connections:
349            mask = self._get_connection_map_no_self_connections(projection)
350            connection_map *= mask
351        elif self.allow_self_connections == 'NoMutual':
352            mask = self._get_connection_map_no_mutual_connections(projection)
353            connection_map *= mask
354        self._connect_with_map(projection, connection_map)
355
356
357class DistanceDependentProbabilityConnector(MapConnector):
358    """
359    For each pair of pre-post cells, the connection probability depends on distance.
360
361    Takes any of the standard :class:`Connector` optional arguments and, in
362    addition:
363
364        `d_expression`:
365            the right-hand side of a valid Python expression for probability,
366            involving 'd', e.g. "exp(-abs(d))", or "d<3"
367        `allow_self_connections`:
368            if the connector is used to connect a Population to itself, this
369            flag determines whether a neuron is allowed to connect to itself,
370            or only to other neurons in the Population.
371        `rng`:
372            an :class:`RNG` instance used to evaluate whether connections exist
373    """
374    parameter_names = ('allow_self_connections', 'd_expression')
375
376    def __init__(self, d_expression, allow_self_connections=True,
377                 rng=None, safe=True, callback=None):
378        """
379        Create a new connector.
380        """
381        Connector.__init__(self, safe, callback)
382        assert isinstance(d_expression, str) or callable(d_expression)
383        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
384        try:
385            if isinstance(d_expression, str):
386                d = 0
387                assert 0 <= eval(d_expression), eval(d_expression)
388                d = 1e12
389                assert 0 <= eval(d_expression), eval(d_expression)
390        except ZeroDivisionError as err:
391            raise ZeroDivisionError("Error in the distance expression %s. %s" %
392                                    (d_expression, err))
393        self.d_expression = d_expression
394        self.allow_self_connections = allow_self_connections
395        self.distance_function = eval("lambda d: %s" % self.d_expression)
396        self.rng = _get_rng(rng)
397
398    def connect(self, projection):
399        distance_map = self._generate_distance_map(projection)
400        probability_map = self.distance_function(distance_map)
401        random_map = LazyArray(RandomDistribution('uniform', (0, 1), rng=self.rng),
402                               projection.shape)
403        connection_map = random_map < probability_map
404        if not self.allow_self_connections:
405            mask = self._get_connection_map_no_self_connections(projection)
406            connection_map *= mask
407        elif self.allow_self_connections == 'NoMutual':
408            mask = self._get_connection_map_no_mutual_connections(projection)
409            connection_map *= mask
410        self._connect_with_map(projection, connection_map, distance_map)
411
412
413class IndexBasedProbabilityConnector(MapConnector):
414    """
415    For each pair of pre-post cells, the connection probability depends on an arbitrary functions
416    that takes the indices of the pre and post populations.
417
418    Takes any of the standard :class:`Connector` optional arguments and, in
419    addition:
420
421        `index_expression`:
422            a function that takes the two cell indices as inputs and calculates the
423            probability matrix from it.
424        `allow_self_connections`:
425            if the connector is used to connect a Population to itself, this
426            flag determines whether a neuron is allowed to connect to itself,
427            or only to other neurons in the Population.
428        `rng`:
429            an :class:`RNG` instance used to evaluate whether connections exist
430    """
431    parameter_names = ('allow_self_connections', 'index_expression')
432
433    def __init__(self, index_expression, allow_self_connections=True,
434                 rng=None, safe=True, callback=None):
435        """
436        Create a new connector.
437        """
438        Connector.__init__(self, safe, callback)
439        assert callable(index_expression)
440        assert isinstance(index_expression, IndexBasedExpression)
441        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
442        self.index_expression = index_expression
443        self.allow_self_connections = allow_self_connections
444        self.rng = _get_rng(rng)
445
446    def connect(self, projection):
447        # The index function is copied so as to avoid the connector being altered by the "connect"
448        # function, which is probably unexpected behaviour.
449        index_expression = copy(self.index_expression)
450        index_expression.projection = projection
451        probability_map = LazyArray(index_expression, projection.shape)
452        random_map = LazyArray(RandomDistribution('uniform', (0, 1), rng=self.rng),
453                               projection.shape)
454        connection_map = random_map < probability_map
455        if not self.allow_self_connections:
456            mask = self._get_connection_map_no_self_connections(projection)
457            connection_map *= mask
458        elif self.allow_self_connections == 'NoMutual':
459            mask = self._get_connection_map_no_mutual_connections(projection)
460            connection_map *= mask
461        self._connect_with_map(projection, connection_map)
462
463
464class DisplacementDependentProbabilityConnector(IndexBasedProbabilityConnector):
465
466    class DisplacementExpression(IndexBasedExpression):
467        """
468        A displacement based expression function used to determine the connection probability
469        and the value of variable connection parameters of a projection
470        """
471
472        def __init__(self, disp_function):
473            """
474            `disp_function`: a function that takes a 3xN numpy position matrix and maps each row
475                             (displacement) to a probability between 0 and 1
476            """
477            self._disp_function = disp_function
478
479        def __call__(self, i, j):
480            disp = (self.projection.post.positions.T[j] - self.projection.pre.positions.T[i]).T
481            return self._disp_function(disp)
482
483    def __init__(self, disp_function, allow_self_connections=True,
484                 rng=None, safe=True, callback=None):
485        super(DisplacementDependentProbabilityConnector, self).__init__(
486            self.DisplacementExpression(disp_function),
487            allow_self_connections=allow_self_connections, rng=rng, callback=callback)
488
489
490class FromListConnector(Connector):
491    """
492    Make connections according to a list.
493
494    Arguments:
495        `conn_list`:
496            a list of tuples, one tuple for each connection. Each tuple should contain:
497            `(pre_idx, post_idx, p1, p2, ..., pn)` where `pre_idx` is the index
498            (i.e. order in the Population, not the ID) of the presynaptic
499            neuron, `post_idx` is the index of the postsynaptic neuron, and
500            p1, p2, etc. are the synaptic parameters (e.g. weight, delay,
501            plasticity parameters).
502        `column_names`:
503            the names of the parameters p1, p2, etc. If not provided, it is
504            assumed the parameters are 'weight', 'delay' (for backwards
505            compatibility). This should be specified using a tuple.
506        `safe`:
507            if True, check that weights and delays have valid values. If False,
508            this check is skipped.
509        `callback`:
510            if True, display a progress bar on the terminal.
511    """
512    parameter_names = ('conn_list',)
513
514    def __init__(self, conn_list, column_names=None, safe=True, callback=None):
515        """
516        Create a new connector.
517        """
518        Connector.__init__(self, safe=safe, callback=callback)
519        self.conn_list = np.array(conn_list)
520        if len(conn_list) > 0:
521            n_columns = self.conn_list.shape[1]
522            if column_names is None:
523                if n_columns == 2:
524                    self.column_names = ()
525                elif n_columns == 4:
526                    self.column_names = ('weight', 'delay')
527                else:
528                    raise TypeError("Argument 'column_names' is required.")
529            else:
530                self.column_names = column_names
531                if n_columns != len(self.column_names) + 2:
532                    raise ValueError("connection list has %d parameter columns, but %d column names provided." % (
533                        n_columns - 2, len(self.column_names)))
534        else:
535            self.column_names = ()
536
537    def connect(self, projection):
538        """Connect-up a Projection."""
539        logger.debug("conn_list (original) = \n%s", self.conn_list)
540        synapse_parameter_names = projection.synapse_type.get_parameter_names()
541        for name in self.column_names:
542            if name not in synapse_parameter_names:
543                raise ValueError("%s is not a valid parameter for %s" % (
544                                 name, projection.synapse_type.__class__.__name__))
545        if self.conn_list.size == 0:
546            return
547        if np.any(self.conn_list[:, 0] >= projection.pre.size):
548            raise errors.ConnectionError("source index out of range")
549        # need to do some profiling, to figure out the best way to do this:
550        #  - order of sorting/filtering by local
551        #  - use np.unique, or just do in1d(self.conn_list)?
552        idx = np.argsort(self.conn_list[:, 1])
553        targets = np.unique(self.conn_list[:, 1]).astype(int)
554        local = np.in1d(targets,
555                           np.arange(projection.post.size)[projection.post._mask_local],
556                           assume_unique=True)
557        local_targets = targets[local]
558        self.conn_list = self.conn_list[idx]
559        left = np.searchsorted(self.conn_list[:, 1], local_targets, 'left')
560        right = np.searchsorted(self.conn_list[:, 1], local_targets, 'right')
561        logger.debug("idx = %s", idx)
562        logger.debug("targets = %s", targets)
563        logger.debug("local_targets = %s", local_targets)
564        logger.debug("conn_list (sorted by target) = \n%s", self.conn_list)
565        logger.debug("left = %s", left)
566        logger.debug("right = %s", right)
567
568        for tgt, l, r in zip(local_targets, left, right):
569            sources = self.conn_list[l:r, 0].astype(int)
570            connection_parameters = deepcopy(projection.synapse_type.parameter_space)
571
572            connection_parameters.shape = (r - l,)
573            for col, name in enumerate(self.column_names, 2):
574                connection_parameters.update(**{name: self.conn_list[l:r, col]})
575            if isinstance(projection.synapse_type, StandardSynapseType):
576                connection_parameters = projection.synapse_type.translate(
577                    connection_parameters)
578            connection_parameters.evaluate()
579            projection._convergent_connect(sources, tgt, **connection_parameters)
580
581
582class FromFileConnector(FromListConnector):
583    """
584    Make connections according to a list read from a file.
585
586    Arguments:
587        `file`:
588            either an open file object or the filename of a file containing a
589            list of connections, in the format required by `FromListConnector`.
590            Column headers, if included in the file,  must be specified using
591            a list or tuple, e.g.::
592
593                # columns = ["i", "j", "weight", "delay", "U", "tau_rec"]
594
595            Note that the header requires `#` at the beginning of the line.
596
597        `distributed`:
598            if this is True, then each node will read connections from a file
599            called `filename.x`, where `x` is the MPI rank. This speeds up
600            loading connections for distributed simulations.
601        `safe`:
602            if True, check that weights and delays have valid values. If False,
603            this check is skipped.
604        `callback`:
605            if True, display a progress bar on the terminal.
606    """
607    parameter_names = ('file', 'distributed')
608
609    def __init__(self, file, distributed=False, safe=True, callback=None):
610        """
611        Create a new connector.
612        """
613        Connector.__init__(self, safe=safe, callback=callback)
614        if isinstance(file, str):
615            file = files.StandardTextFile(file, mode='r')
616        self.file = file
617        self.distributed = distributed
618
619    def connect(self, projection):
620        """Connect-up a Projection."""
621        if self.distributed:
622            self.file.rename("%s.%d" % (self.file.name,
623                                        projection._simulator.state.mpi_rank))
624        self.column_names = self.file.get_metadata().get('columns', ('weight', 'delay'))
625        for ignore in "ij":
626            if ignore in self.column_names:
627                self.column_names.remove(ignore)
628        self.conn_list = self.file.read()
629        FromListConnector.connect(self, projection)
630
631
632class FixedNumberConnector(MapConnector):
633    # base class - should not be instantiated
634    parameter_names = ('allow_self_connections', 'n')
635
636    def __init__(self, n, allow_self_connections=True, with_replacement=False,
637                 rng=None, safe=True, callback=None):
638        """
639        Create a new connector.
640        """
641        Connector.__init__(self, safe, callback)
642        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
643        self.allow_self_connections = allow_self_connections
644        self.with_replacement = with_replacement
645        self.n = n
646        if isinstance(n, int):
647            assert n >= 0
648        elif isinstance(n, RandomDistribution):
649            # weak check that the random distribution is ok
650            assert np.all(np.array(n.next(100)) >=
651                             0), "the random distribution produces negative numbers"
652        else:
653            raise TypeError("n must be an integer or a RandomDistribution object")
654        self.rng = _get_rng(rng)
655
656    def _rng_uniform_int_exclude(self, n, size, exclude):
657        res = self.rng.next(n, 'uniform_int', {"low": 0, "high": size}, mask=None)
658        logger.debug("RNG0 res=%s" % res)
659        idx = np.where(res == exclude)[0]
660        logger.debug("RNG1 exclude=%d, res=%s idx=%s" % (exclude, res, idx))
661        while idx.size > 0:
662            redrawn = self.rng.next(idx.size, 'uniform_int', {"low": 0, "high": size}, mask=None)
663            res[idx] = redrawn
664            idx = idx[np.where(res == exclude)[0]]
665            logger.debug("RNG2 exclude=%d redrawn=%s res=%s idx=%s" % (exclude, redrawn, res, idx))
666        return res
667
668
669class FixedNumberPostConnector(FixedNumberConnector):
670    """
671    Each pre-synaptic neuron is connected to exactly `n` post-synaptic neurons
672    chosen at random.
673
674    The sampling behaviour is controlled by the `with_replacement` argument.
675
676    "With replacement" means that each post-synaptic neuron is chosen from the
677    entire population. There is always therefore a possibility of multiple
678    connections between a given pair of neurons.
679
680    "Without replacement" means that once a neuron has been selected, it cannot
681    be selected again until the entire population has been selected. This means
682    that if `n` is less than the size of the post-synaptic population, there
683    are no multiple connections. If `n` is greater than the size of the post-
684    synaptic population, all possible single connections are made before
685    starting to add duplicate connections.
686
687    Takes any of the standard :class:`Connector` optional arguments and, in
688    addition:
689
690        `n`:
691            either a positive integer, or a `RandomDistribution` that produces
692            positive integers. If `n` is a `RandomDistribution`, then the
693            number of post-synaptic neurons is drawn from this distribution
694            for each pre-synaptic neuron.
695        `with_replacement`:
696            if True, the selection of neurons to connect is made from the
697            entire population. If False, once a neuron is selected it cannot
698            be selected again until the entire population has been connected.
699        `allow_self_connections`:
700            if the connector is used to connect a Population to itself, this
701            flag determines whether a neuron is allowed to connect to itself,
702            or only to other neurons in the Population.
703        `rng`:
704            an :class:`RNG` instance used to evaluate which potential connections
705            are created.
706    """
707
708    def _get_num_post(self):
709        if isinstance(self.n, int):
710            n_post = self.n
711        else:
712            n_post = self.n.next()
713        return n_post
714
715    def connect(self, projection):
716        connections = [[] for i in range(projection.post.size)]
717        for source_index in range(projection.pre.size):
718            n = self._get_num_post()
719            if self.with_replacement:
720                if not self.allow_self_connections and projection.pre == projection.post:
721                    targets = self._rng_uniform_int_exclude(n, projection.post.size, source_index)
722                else:
723                    targets = self.rng.next(
724                        n, 'uniform_int', {"low": 0, "high": projection.post.size}, mask=None)
725            else:
726                all_cells = np.arange(projection.post.size)
727                if not self.allow_self_connections and projection.pre == projection.post:
728                    all_cells = all_cells[all_cells != source_index]
729                full_sets = n // all_cells.size
730                remainder = n % all_cells.size
731                target_sets = []
732                if full_sets > 0:
733                    target_sets = [all_cells] * full_sets
734                if remainder > 0:
735                    target_sets.append(self.rng.permutation(all_cells)[:remainder])
736                targets = np.hstack(target_sets)
737            assert targets.size == n
738            for target_index in targets:
739                connections[target_index].append(source_index)
740
741        def build_source_masks(mask=None):
742            if mask is None:
743                return [np.array(x) for x in connections]
744            else:
745                return [np.array(x) for x in np.array(connections)[mask]]
746        self._standard_connect(projection, build_source_masks)
747
748
749class FixedNumberPreConnector(FixedNumberConnector):
750    """
751    Each post-synaptic neuron is connected to exactly `n` pre-synaptic neurons
752    chosen at random.
753
754    The sampling behaviour is controlled by the `with_replacement` argument.
755
756    "With replacement" means that each pre-synaptic neuron is chosen from the
757    entire population. There is always therefore a possibility of multiple
758    connections between a given pair of neurons.
759
760    "Without replacement" means that once a neuron has been selected, it cannot
761    be selected again until the entire population has been selected. This means
762    that if `n` is less than the size of the pre-synaptic population, there
763    are no multiple connections. If `n` is greater than the size of the pre-
764    synaptic population, all possible single connections are made before
765    starting to add duplicate connections.
766
767    Takes any of the standard :class:`Connector` optional arguments and, in
768    addition:
769
770        `n`:
771            either a positive integer, or a `RandomDistribution` that produces
772            positive integers. If `n` is a `RandomDistribution`, then the
773            number of pre-synaptic neurons is drawn from this distribution
774            for each post-synaptic neuron.
775        `with_replacement`:
776            if True, the selection of neurons to connect is made from the
777            entire population. If False, once a neuron is selected it cannot
778            be selected again until the entire population has been connected.
779        `allow_self_connections`:
780            if the connector is used to connect a Population to itself, this
781            flag determines whether a neuron is allowed to connect to itself,
782            or only to other neurons in the Population.
783        `rng`:
784            an :class:`RNG` instance used to evaluate which potential connections
785            are created.
786    """
787
788    def _get_num_pre(self, size, mask=None):
789        if isinstance(self.n, int):
790            if mask is None:
791                n_pre = repeat(self.n, size)
792            else:
793                n_pre = repeat(self.n, mask.sum())
794        else:
795            if mask is None:
796                n_pre = self.n.next(size)
797            else:
798                if self.n.rng.parallel_safe:
799                    n_pre = self.n.next(size)[mask]
800                else:
801                    n_pre = self.n.next(mask.sum())
802        return n_pre
803
804    def connect(self, projection):
805        if self.with_replacement:
806            if self.allow_self_connections or projection.pre != projection.post:
807                def build_source_masks(mask=None):
808                    n_pre = self._get_num_pre(projection.post.size, mask)
809                    for n in n_pre:
810                        sources = self.rng.next(
811                            n, 'uniform_int', {"low": 0, "high": projection.pre.size}, mask=None)
812                        assert sources.size == n
813                        yield sources
814            else:
815                def build_source_masks(mask=None):
816                    n_pre = self._get_num_pre(projection.post.size, mask)
817                    if self.rng.parallel_safe or mask is None:
818                        for i, n in enumerate(n_pre):
819                            sources = self._rng_uniform_int_exclude(n, projection.pre.size, i)
820                            assert sources.size == n
821                            yield sources
822                    else:
823                        # TODO: use mask to obtain indices i
824                        raise NotImplementedError(
825                            "allow_self_connections=False currently requires a parallel safe RNG.")
826        else:
827            if self.allow_self_connections or projection.pre != projection.post:
828                def build_source_masks(mask=None):
829                    # where n > projection.pre.size, first all pre-synaptic cells
830                    # are connected one or more times, then the remainder
831                    # are chosen randomly
832                    n_pre = self._get_num_pre(projection.post.size, mask)
833                    all_cells = np.arange(projection.pre.size)
834                    for n in n_pre:
835                        full_sets = n // projection.pre.size
836                        remainder = n % projection.pre.size
837                        source_sets = []
838                        if full_sets > 0:
839                            source_sets = [all_cells] * full_sets
840                        if remainder > 0:
841                            source_sets.append(self.rng.permutation(all_cells)[:remainder])
842                        sources = np.hstack(source_sets)
843                        assert sources.size == n
844                        yield sources
845            else:
846                def build_source_masks(mask=None):
847                    # where n > projection.pre.size, first all pre-synaptic cells
848                    # are connected one or more times, then the remainder
849                    # are chosen randomly
850                    n_pre = self._get_num_pre(projection.post.size, mask)
851                    all_cells = np.arange(projection.pre.size)
852                    if self.rng.parallel_safe or mask is None:
853                        for i, n in enumerate(n_pre):
854                            full_sets = n // (projection.pre.size - 1)
855                            remainder = n % (projection.pre.size - 1)
856                            allowed_cells = all_cells[all_cells != i]
857                            source_sets = []
858                            if full_sets > 0:
859                                source_sets = [allowed_cells] * full_sets
860                            if remainder > 0:
861                                source_sets.append(self.rng.permutation(allowed_cells)[:remainder])
862                            sources = np.hstack(source_sets)
863                            assert sources.size == n
864                            yield sources
865                    else:
866                        raise NotImplementedError(
867                            "allow_self_connections=False currently requires a parallel safe RNG.")
868
869        self._standard_connect(projection, build_source_masks)
870
871
872class OneToOneConnector(MapConnector):
873    """
874    Where the pre- and postsynaptic populations have the same size, connect
875    cell *i* in the presynaptic population to cell *i* in the postsynaptic
876    population for all *i*.
877
878    Takes any of the standard :class:`Connector` optional arguments.
879    """
880    parameter_names = tuple()
881
882    def connect(self, projection):
883        """Connect-up a Projection."""
884        connection_map = LazyArray(lambda i, j: i == j, shape=projection.shape)
885        self._connect_with_map(projection, connection_map)
886
887
888class SmallWorldConnector(Connector):
889    """
890    Connect cells so as to create a small-world network.
891
892    Takes any of the standard :class:`Connector` optional arguments and, in
893    addition:
894
895        `degree`:
896            the region length where nodes will be connected locally.
897        `rewiring`:
898            the probability of rewiring each edge.
899        `allow_self_connections`:
900            if the connector is used to connect a Population to itself, this
901            flag determines whether a neuron is allowed to connect to itself,
902            or only to other neurons in the Population.
903        `n_connections`:
904            if specified, the number of efferent synaptic connections per neuron.
905        `rng`:
906            an :class:`RNG` instance used to evaluate which connections
907            are created.
908    """
909    parameter_names = ('allow_self_connections', 'degree', 'rewiring', 'n_connections')
910
911    def __init__(self, degree, rewiring, allow_self_connections=True,
912                 n_connections=None, rng=None, safe=True, callback=None):
913        """
914        Create a new connector.
915        """
916        Connector.__init__(self, safe, callback)
917        assert 0 <= rewiring <= 1
918        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
919        self.rewiring = rewiring
920        self.d_expression = "d < %g" % degree
921        self.allow_self_connections = allow_self_connections
922        self.n_connections = n_connections
923        self.rng = _get_rng(rng)
924
925    def connect(self, projection):
926        """Connect-up a Projection."""
927        raise NotImplementedError
928
929
930class CSAConnector(MapConnector):
931    """
932    Use the Connection Set Algebra (Djurfeldt, 2012) to connect cells.
933
934    Takes any of the standard :class:`Connector` optional arguments and, in
935    addition:
936
937        `cset`:
938            a connection set object.
939    """
940    parameter_names = ('cset',)
941
942    if haveCSA:
943        def __init__(self, cset, safe=True, callback=None):
944            """
945            """
946            Connector.__init__(self, safe=safe, callback=callback)
947            self.cset = cset
948            arity = csa.arity(cset)
949            assert arity in (0, 2), 'must specify mask or connection-set with arity 0 or 2'
950    else:
951        def __init__(self, cset, safe=True, callback=None):
952            raise RuntimeError("CSAConnector not available---couldn't import csa module")
953
954    def connect(self, projection):
955        """Connect-up a Projection."""
956        # Cut out finite part
957        c = csa.cross((0, projection.pre.size - 1), (0, projection.post.size - 1)) * \
958            self.cset  # can't we cut out just the columns we want?
959
960        if csa.arity(self.cset) == 2:
961            # Connection-set with arity 2
962            for (i, j, weight, delay) in c:
963                projection._convergent_connect(
964                    [projection.pre[i]], projection.post[j], weight, delay)
965        elif csa.arity(self.cset) == 0:
966            # inefficient implementation as a starting point
967            connection_map = np.zeros((projection.pre.size, projection.post.size), dtype=bool)
968            for addr in c:
969                connection_map[addr] = True
970            self._connect_with_map(projection, LazyArray(connection_map))
971        else:
972            raise NotImplementedError
973
974
975class CloneConnector(MapConnector):
976    """
977    Connects cells with the same connectivity pattern as a previous projection.
978    """
979    parameter_names = ('reference_projection',)
980
981    def __init__(self, reference_projection, safe=True, callback=None):
982        """
983        Create a new CloneConnector.
984
985        `reference_projection` -- the projection to clone the connectivity pattern from
986        """
987        MapConnector.__init__(self, safe, callback=callback)
988        self.reference_projection = reference_projection
989
990    def connect(self, projection):
991        if (projection.pre != self.reference_projection.pre or
992                projection.post != self.reference_projection.post):
993            raise errors.ConnectionError("Pre and post populations must match between reference ({0}"
994                                         "  and {1}) and clone projections ({2} and {3}) for "
995                                         "CloneConnector"
996                                         .format(self.reference_projection.pre,
997                                                 self.reference_projection.post,
998                                                 projection.pre, projection.post))
999        connection_map = LazyArray(~np.isnan(self.reference_projection.get(['weight'], 'array',
1000                                                                              gather='all')[0]))
1001        self._connect_with_map(projection, connection_map)
1002
1003
1004class ArrayConnector(MapConnector):
1005    """
1006    Provide an explicit boolean connection matrix, with shape (m, n) where m is
1007    the size of the presynaptic population and n that of the postsynaptic
1008    population.
1009    """
1010    parameter_names = ('array',)
1011
1012    def __init__(self, array, safe=True, callback=None):
1013        """
1014        Create a new connector.
1015        """
1016        Connector.__init__(self, safe, callback)
1017        self.array = array
1018
1019    def connect(self, projection):
1020        connection_map = LazyArray(self.array, projection.shape)
1021        self._connect_with_map(projection, connection_map)
1022
1023
1024class FixedTotalNumberConnector(FixedNumberConnector):
1025    parameter_names = ('allow_self_connections', 'n')
1026
1027    def __init__(self, n, allow_self_connections=True, with_replacement=True,
1028                 rng=None, safe=True, callback=None):
1029        """
1030        Create a new connector.
1031        """
1032        Connector.__init__(self, safe, callback)
1033        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
1034        self.allow_self_connections = allow_self_connections
1035        self.with_replacement = with_replacement
1036        self.n = n
1037        if isinstance(n, int):
1038            assert n >= 0
1039        elif isinstance(n, RandomDistribution):
1040            # weak check that the random distribution is ok
1041            assert np.all(np.array(n.next(100)) >=
1042                             0), "the random distribution produces negative numbers"
1043        else:
1044            raise TypeError("n must be an integer or a RandomDistribution object")
1045        self.rng = _get_rng(rng)
1046
1047    def connect(self, projection):
1048        # This implementation is not "parallel safe" for random numbers.
1049        # todo: support the `parallel_safe` flag.
1050
1051        # Determine number of processes and current rank
1052        rank = projection._simulator.state.mpi_rank
1053        num_processes = projection._simulator.state.num_processes
1054
1055        # Assume that targets are equally distributed over processes
1056        targets_per_process = int(len(projection.post) / num_processes)
1057
1058        # Calculate the number of synapses on each process
1059        bino = RandomDistribution('binomial',
1060                                  [self.n, targets_per_process / len(projection.post)],
1061                                  rng=self.rng)
1062        num_conns_on_vp = np.zeros(num_processes, dtype=int)
1063        sum_dist = 0
1064        sum_partitions = 0
1065        for k in range(num_processes):
1066            p_local = targets_per_process / (len(projection.post) - sum_dist)
1067            bino.parameters['p'] = p_local
1068            bino.parameters['n'] = self.n - sum_partitions
1069            num_conns_on_vp[k] = bino.next()
1070            sum_dist += targets_per_process
1071            sum_partitions += num_conns_on_vp[k]
1072
1073        # Draw random sources and targets
1074        connections = [[] for i in range(projection.post.size)]
1075        possible_targets = np.arange(projection.post.size)[projection.post._mask_local]
1076        for i in range(num_conns_on_vp[rank]):
1077            source_index = self.rng.next(1, 'uniform_int',
1078                                         {"low": 0, "high": projection.pre.size},
1079                                         mask=None)[0]
1080            target_index = self.rng.choice(possible_targets, size=1)[0]
1081            connections[target_index].append(source_index)
1082
1083        def build_source_masks(mask=None):
1084            if mask is None:
1085                return [np.array(x) for x in connections]
1086            else:
1087                return [np.array(x) for x in np.array(connections)[mask]]
1088        self._standard_connect(projection, build_source_masks)
1089