• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NeuralEnsemble / PyNN / 968

pending completion
968

cron

travis-ci-com

GitHub
Merge pull request #761 from apdavison/pytest

Migrate test suite from nose to pytest

10 of 10 new or added lines in 5 files covered. (100.0%)

6955 of 9974 relevant lines covered (69.73%)

0.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.22
/pyNN/connectors.py
1
"""
2
Defines a common implementation of the built-in PyNN Connector classes.
3

4
Simulator modules may use these directly, or may implement their own versions
5
for improved performance.
6

7
:copyright: Copyright 2006-2022 by the PyNN team, see AUTHORS.
8
:license: CeCILL, see LICENSE for details.
9
"""
10

11
from pyNN.random import RandomDistribution, AbstractRNG, NumpyRNG, get_mpi_config
1✔
12
from pyNN.core import IndexBasedExpression
1✔
13
from pyNN import errors, descriptions
1✔
14
from pyNN.recording import files
1✔
15
from pyNN.parameters import LazyArray
1✔
16
from pyNN.standardmodels import StandardSynapseType
1✔
17
import numpy as np
1✔
18
from itertools import repeat
1✔
19
import logging
1✔
20
from copy import copy, deepcopy
1✔
21

22
from lazyarray import arccos, arcsin, arctan, arctan2, ceil, cos, cosh, exp, \
1✔
23
    fabs, floor, fmod, hypot, ldexp, log, log10, modf, power, \
24
    sin, sinh, sqrt, tan, tanh, maximum, minimum
25
from numpy import e, pi
1✔
26

27
try:
1✔
28
    import csa
1✔
29
    haveCSA = True
×
30
except ImportError:
1✔
31
    haveCSA = False
1✔
32

33
logger = logging.getLogger("PyNN")
1✔
34

35

36
def _get_rng(rng):
1✔
37
    if isinstance(rng, AbstractRNG):
1✔
38
        return rng
1✔
39
    elif rng is None:
1✔
40
        return NumpyRNG(seed=151985012)
1✔
41
    else:
42
        raise Exception("rng must be either None, or a subclass of pyNN.random.AbstractRNG")
×
43

44

45
class Connector(object):
1✔
46
    """
47
    Base class for connectors.
48

49
    All connector sub-classes have the following optional keyword arguments:
50
        `safe`:
51
            if True, check that weights and delays have valid values. If False,
52
            this check is skipped.
53
        `callback`:
54
            a function that will be called with the fractional progress of the
55
            connection routine. An example would be `progress_bar.set_level`.
56
    """
57

58
    def __init__(self, safe=True, callback=None):
1✔
59
        """
60
        docstring needed
61
        """
62
        self.safe = safe
1✔
63
        self.callback = callback
1✔
64
        if callback is not None:
1✔
65
            assert callable(callback)
×
66

67
    def connect(self, projection):
1✔
68
        raise NotImplementedError()
×
69

70
    def get_parameters(self):
1✔
71
        P = {}
1✔
72
        for name in self.parameter_names:
1✔
73
            P[name] = getattr(self, name)
1✔
74
        return P
1✔
75

76
    def _generate_distance_map(self, projection):
1✔
77
        position_generators = (projection.pre.position_generator,
1✔
78
                               projection.post.position_generator)
79
        return LazyArray(projection.space.distance_generator(*position_generators),
1✔
80
                         shape=projection.shape)
81

82
    def _parameters_from_synapse_type(self, projection, distance_map=None):
1✔
83
        """
84
        Obtain the parameters to be used for the connections from the projection's `synapse_type`
85
        attribute. Each parameter value is a `LazyArray`.
86
        """
87
        if distance_map is None:
1✔
88
            distance_map = self._generate_distance_map(projection)
1✔
89
        parameter_space = projection.synapse_type.native_parameters
1✔
90
        # TODO: in the documentation, we claim that a parameter value can be
91
        #       a list or 1D array of the same length as the number of connections.
92
        #       We do not currently handle this scenario, although it is only
93
        #       really useful for fixed-number connectors anyway.
94
        #       Probably the best solution is to remove the parameter at this stage,
95
        #       then set it after the connections have already been created.
96
        parameter_space.shape = (projection.pre.size, projection.post.size)
1✔
97
        for name, map in parameter_space.items():
1✔
98
            if callable(map.base_value):
1✔
99
                if isinstance(map.base_value, IndexBasedExpression):
1✔
100
                    # Assumes map is a function of index and hence requires the projection to
101
                    # determine its value. It and its index function are copied so as to be able
102
                    # to set the projection without altering the connector, which would perhaps
103
                    # not be expected from the 'connect' call.
104
                    new_map = copy(map)
1✔
105
                    new_map.base_value = copy(map.base_value)
1✔
106
                    new_map.base_value.projection = projection
1✔
107
                    parameter_space[name] = new_map
1✔
108
                else:
109
                    # Assumes map is a function of distance
110
                    parameter_space[name] = map(distance_map)
1✔
111
        return parameter_space
1✔
112

113
    def describe(self, template='connector_default.txt', engine='default'):
1✔
114
        """
115
        Returns a human-readable description of the connection method.
116

117
        The output may be customized by specifying a different template
118
        togther with an associated template engine (see ``pyNN.descriptions``).
119

120
        If template is None, then a dictionary containing the template context
121
        will be returned.
122
        """
123
        context = {'name': self.__class__.__name__,
1✔
124
                   'parameters': self.get_parameters()}
125
        return descriptions.render(engine, template, context)
1✔
126

127

128
class MapConnector(Connector):
1✔
129
    """
130
    Abstract base class for Connectors based on connection maps, where a map is a 2D lazy array
131
    containing either the (boolean) connectivity matrix (aka adjacency matrix, connection set mask, etc.)
132
    or the values of a synaptic connection parameter.
133
    """
134

135
    def _standard_connect(self, projection, connection_map_generator, distance_map=None):
1✔
136
        """
137

138
        `connection_map_generator` should be a function or other callable, with one optional
139
        argument `mask`, which returns an iterable.
140

141
        The iterable should produce one element per post-synaptic neuron.
142

143
        Each element should be either:
144
            (i) a boolean array, indicating which of the pre-synaptic neurons
145
                should be connected to,
146
            (ii) an integer array indicating the same thing using indices,
147
            (iii) or a single boolean, meaning connect to all/none.
148

149
        The `mask` argument, a boolean array, can be used to limit processing to just
150
        neurons which exist on the local MPI node.
151

152
        todo: explain the argument `distance_map`.
153
        """
154

155
        column_indices = np.arange(projection.post.size)
1✔
156
        postsynaptic_indices = projection.post.id_to_index(projection.post.all_cells)
1✔
157

158
        if (projection.synapse_type.native_parameters.parallel_safe
1✔
159
                or hasattr(self, "rng") and self.rng.parallel_safe):
160

161
            # If any of the synapse parameters are based on parallel-safe random number generators,
162
            # we need to iterate over all post-synaptic cells, so we can generate then
163
            # throw away the random numbers for the non-local nodes.
164
            logger.debug("Parallel-safe iteration.")
1✔
165
            components = (
1✔
166
                column_indices,
167
                postsynaptic_indices,
168
                projection.post._mask_local,
169
                connection_map_generator())
170
        else:
171
            # Otherwise, we only need to iterate over local post-synaptic cells.
172
            mask = projection.post._mask_local
1✔
173
            components = (
1✔
174
                column_indices[mask],
175
                postsynaptic_indices[mask],
176
                repeat(True),
177
                connection_map_generator(mask))
178

179
        parameter_space = self._parameters_from_synapse_type(projection, distance_map)
1✔
180

181
        # Loop over columns of the connection_map array (equivalent to looping over post-synaptic neurons)
182
        for count, (col, postsynaptic_index, local, source_mask) in enumerate(zip(*components)):
1✔
183
            # `col`: column index
184
            # `postsynaptic_index`: index of the post-synaptic neuron
185
            # `local`: boolean - does the post-synaptic neuron exist on this MPI node
186
            # `source_mask`: boolean numpy array, indicating which of the pre-synaptic neurons should be connected to,
187
            #                or a single boolean, meaning connect to all/none of the pre-synaptic neurons
188
            #                It can also be an array of addresses
189
            _proceed = False
1✔
190
            if source_mask is True or source_mask.any():
1✔
191
                _proceed = True
1✔
192
            elif type(source_mask) == np.ndarray:
1✔
193
                if source_mask.dtype == bool:
1✔
194
                    if source_mask.any():
1✔
195
                        _proceed = True
×
196
                elif len(source_mask) > 0:
1✔
197
                    _proceed = True
1✔
198
            if _proceed:
1✔
199
                # Convert from boolean to integer mask, if necessary
200
                if source_mask is True:
1✔
201
                    source_mask = np.arange(projection.pre.size, dtype=int)
1✔
202
                elif source_mask.dtype == bool:
1✔
203
                    source_mask = source_mask.nonzero()[0]
1✔
204

205
                # Evaluate the lazy arrays containing the synaptic parameters
206
                connection_parameters = {}
1✔
207
                for name, map in parameter_space.items():
1✔
208
                    if map.is_homogeneous:
1✔
209
                        connection_parameters[name] = map.evaluate(simplify=True)
1✔
210
                    else:
211
                        connection_parameters[name] = map[source_mask, col]
1✔
212

213
                # Check that parameter values are valid
214
                if self.safe:
1✔
215
                    # it might be cheaper to do the weight and delay check before evaluating the larray,
216
                    # however this is challenging to do if the base value is a function or if there are
217
                    # a lot of operations, so for simplicity we do the check after evaluation
218
                    syn = projection.synapse_type
1✔
219
                    if hasattr(syn, "parameter_checks"):
1✔
220
                        #raise Exception(f"{connection_parameters} {syn.parameter_checks}")
221
                        for parameter_name, check in syn.parameter_checks.items():
1✔
222
                            native_parameter_name = syn.translations[parameter_name]["translated_name"]
1✔
223
                            # note that for delays we should also apply units scaling to the check values
224
                            # since this currently only affects Brian we can probably handle that separately
225
                            # (for weights the checks are all based on zero)
226
                            if native_parameter_name in connection_parameters:
1✔
227
                                check(connection_parameters[native_parameter_name], projection)
1✔
228

229
                if local:
1✔
230
                    # Connect the neurons
231
                    #logger.debug("Connecting to %d from %s" % (postsynaptic_index, source_mask))
232
                    projection._convergent_connect(
1✔
233
                        source_mask, postsynaptic_index, **connection_parameters)
234
                    if self.callback:
1✔
235
                        self.callback(count / projection.post.local_size)
×
236

237
    def _connect_with_map(self, projection, connection_map, distance_map=None):
1✔
238
        """
239
        Create connections according to a connection map.
240

241
        Arguments:
242

243
            `projection`:
244
                the `Projection` that is being created.
245
            `connection_map`:
246
                a boolean `LazyArray` of the same shape as `projection`, representing the connectivity matrix.
247
            `distance_map`:
248
                TODO
249
        """
250
        logger.debug("Connecting %s using a connection map" % projection.label)
1✔
251
        self._standard_connect(projection, connection_map.by_column, distance_map)
1✔
252

253
    def _get_connection_map_no_self_connections(self, projection):
1✔
254
        from pyNN.common import Population
1✔
255
        if (isinstance(projection.pre, Population)
1✔
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)
1✔
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,
1✔
264
                                   (projection.post.size, projection.pre.size)).T
265
            b = projection.post.all_cells
1✔
266
            connection_map = LazyArray(a != b, shape=projection.shape)
1✔
267
        return connection_map
1✔
268

269
    def _get_connection_map_no_mutual_connections(self, projection):
1✔
270
        from pyNN.common import Population
×
271
        if (isinstance(projection.pre, Population)
×
272
            and isinstance(projection.post, Population)
273
                and projection.pre == projection.post):
274
            connection_map = LazyArray(lambda i, j: i > j, shape=projection.shape)
×
275
        else:
276
            raise NotImplementedError("todo")
×
277
        return connection_map
×
278

279

280
class AllToAllConnector(MapConnector):
1✔
281
    """
282
    Connects all cells in the presynaptic population to all cells in the
283
    postsynaptic population.
284

285
    Takes any of the standard :class:`Connector` optional arguments and, in
286
    addition:
287

288
        `allow_self_connections`:
289
            if the connector is used to connect a Population to itself, this
290
            flag determines whether a neuron is allowed to connect to itself,
291
            or only to other neurons in the Population.
292
    """
293
    parameter_names = ('allow_self_connections',)
1✔
294

295
    def __init__(self, allow_self_connections=True, safe=True,
1✔
296
                 callback=None):
297
        """
298
        Create a new connector.
299
        """
300
        Connector.__init__(self, safe, callback)
1✔
301
        assert isinstance(allow_self_connections, bool)
1✔
302
        self.allow_self_connections = allow_self_connections
1✔
303

304
    def connect(self, projection):
1✔
305
        if not self.allow_self_connections:
1✔
306
            connection_map = self._get_connection_map_no_self_connections(projection)
1✔
307
        elif self.allow_self_connections == 'NoMutual':
1✔
308
            connection_map = self._get_connection_map_no_mutual_connections(projection)
×
309
        else:
310
            connection_map = LazyArray(True, shape=projection.shape)
1✔
311
        self._connect_with_map(projection, connection_map)
1✔
312

313

314
class FixedProbabilityConnector(MapConnector):
1✔
315
    """
316
    For each pair of pre-post cells, the connection probability is constant.
317

318
    Takes any of the standard :class:`Connector` optional arguments and, in
319
    addition:
320

321
        `p_connect`:
322
            a float between zero and one. Each potential connection is created
323
            with this probability.
324
        `allow_self_connections`:
325
            if the connector is used to connect a Population to itself, this
326
            flag determines whether a neuron is allowed to connect to itself,
327
            or only to other neurons in the Population.
328
        `rng`:
329
            an :class:`RNG` instance used to evaluate whether connections exist
330
    """
331
    parameter_names = ('allow_self_connections', 'p_connect')
1✔
332

333
    def __init__(self, p_connect, allow_self_connections=True,
1✔
334
                 rng=None, safe=True, callback=None):
335
        """
336
        Create a new connector.
337
        """
338
        Connector.__init__(self, safe, callback)
1✔
339
        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
1✔
340
        self.allow_self_connections = allow_self_connections
1✔
341
        self.p_connect = float(p_connect)
1✔
342
        assert 0 <= self.p_connect
1✔
343
        self.rng = _get_rng(rng)
1✔
344

345
    def connect(self, projection):
1✔
346
        random_map = LazyArray(RandomDistribution('uniform', (0, 1), rng=self.rng),
1✔
347
                               projection.shape)
348
        connection_map = random_map < self.p_connect
1✔
349
        if not self.allow_self_connections:
1✔
350
            mask = self._get_connection_map_no_self_connections(projection)
1✔
351
            connection_map *= mask
1✔
352
        elif self.allow_self_connections == 'NoMutual':
1✔
353
            mask = self._get_connection_map_no_mutual_connections(projection)
×
354
            connection_map *= mask
×
355
        self._connect_with_map(projection, connection_map)
1✔
356

357

358
class DistanceDependentProbabilityConnector(MapConnector):
1✔
359
    """
360
    For each pair of pre-post cells, the connection probability depends on distance.
361

362
    Takes any of the standard :class:`Connector` optional arguments and, in
363
    addition:
364

365
        `d_expression`:
366
            the right-hand side of a valid Python expression for probability,
367
            involving 'd', e.g. "exp(-abs(d))", or "d<3"
368
        `allow_self_connections`:
369
            if the connector is used to connect a Population to itself, this
370
            flag determines whether a neuron is allowed to connect to itself,
371
            or only to other neurons in the Population.
372
        `rng`:
373
            an :class:`RNG` instance used to evaluate whether connections exist
374
    """
375
    parameter_names = ('allow_self_connections', 'd_expression')
1✔
376

377
    def __init__(self, d_expression, allow_self_connections=True,
1✔
378
                 rng=None, safe=True, callback=None):
379
        """
380
        Create a new connector.
381
        """
382
        Connector.__init__(self, safe, callback)
1✔
383
        assert isinstance(d_expression, str) or callable(d_expression)
1✔
384
        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
1✔
385
        try:
1✔
386
            if isinstance(d_expression, str):
1✔
387
                d = 0
1✔
388
                assert 0 <= eval(d_expression), eval(d_expression)
1✔
389
                d = 1e12
1✔
390
                assert 0 <= eval(d_expression), eval(d_expression)
1✔
391
        except ZeroDivisionError as err:
×
392
            raise ZeroDivisionError("Error in the distance expression %s. %s" %
×
393
                                    (d_expression, err))
394
        self.d_expression = d_expression
1✔
395
        self.allow_self_connections = allow_self_connections
1✔
396
        self.distance_function = eval("lambda d: %s" % self.d_expression)
1✔
397
        self.rng = _get_rng(rng)
1✔
398

399
    def connect(self, projection):
1✔
400
        distance_map = self._generate_distance_map(projection)
1✔
401
        probability_map = self.distance_function(distance_map)
1✔
402
        random_map = LazyArray(RandomDistribution('uniform', (0, 1), rng=self.rng),
1✔
403
                               projection.shape)
404
        connection_map = random_map < probability_map
1✔
405
        if not self.allow_self_connections:
1✔
406
            mask = self._get_connection_map_no_self_connections(projection)
×
407
            connection_map *= mask
×
408
        elif self.allow_self_connections == 'NoMutual':
1✔
409
            mask = self._get_connection_map_no_mutual_connections(projection)
×
410
            connection_map *= mask
×
411
        self._connect_with_map(projection, connection_map, distance_map)
1✔
412

413

414
class IndexBasedProbabilityConnector(MapConnector):
1✔
415
    """
416
    For each pair of pre-post cells, the connection probability depends on an arbitrary functions
417
    that takes the indices of the pre and post populations.
418

419
    Takes any of the standard :class:`Connector` optional arguments and, in
420
    addition:
421

422
        `index_expression`:
423
            a function that takes the two cell indices as inputs and calculates the
424
            probability matrix from it.
425
        `allow_self_connections`:
426
            if the connector is used to connect a Population to itself, this
427
            flag determines whether a neuron is allowed to connect to itself,
428
            or only to other neurons in the Population.
429
        `rng`:
430
            an :class:`RNG` instance used to evaluate whether connections exist
431
    """
432
    parameter_names = ('allow_self_connections', 'index_expression')
1✔
433

434
    def __init__(self, index_expression, allow_self_connections=True,
1✔
435
                 rng=None, safe=True, callback=None):
436
        """
437
        Create a new connector.
438
        """
439
        Connector.__init__(self, safe, callback)
1✔
440
        assert callable(index_expression)
1✔
441
        assert isinstance(index_expression, IndexBasedExpression)
1✔
442
        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
1✔
443
        self.index_expression = index_expression
1✔
444
        self.allow_self_connections = allow_self_connections
1✔
445
        self.rng = _get_rng(rng)
1✔
446

447
    def connect(self, projection):
1✔
448
        # The index function is copied so as to avoid the connector being altered by the "connect"
449
        # function, which is probably unexpected behaviour.
450
        index_expression = copy(self.index_expression)
1✔
451
        index_expression.projection = projection
1✔
452
        probability_map = LazyArray(index_expression, projection.shape)
1✔
453
        random_map = LazyArray(RandomDistribution('uniform', (0, 1), rng=self.rng),
1✔
454
                               projection.shape)
455
        connection_map = random_map < probability_map
1✔
456
        if not self.allow_self_connections:
1✔
457
            mask = self._get_connection_map_no_self_connections(projection)
×
458
            connection_map *= mask
×
459
        elif self.allow_self_connections == 'NoMutual':
1✔
460
            mask = self._get_connection_map_no_mutual_connections(projection)
×
461
            connection_map *= mask
×
462
        self._connect_with_map(projection, connection_map)
1✔
463

464

465
class DisplacementDependentProbabilityConnector(IndexBasedProbabilityConnector):
1✔
466
    """
467
    For each pair of pre-post cells, the connection probability depends on the
468
    displacement of the two neurons, i.e. on the triplet (dx, dy, dz) where
469
    dx is the distance between the x-coordinates of the two neurons, and so on.
470

471
    Takes any of the standard :class:`Connector` optional arguments and, in
472
    addition:
473

474
        `disp_function`:
475
            the right-hand side of a valid Python expression for probability,
476
            involving an array named 'd' whose first dimension has size 3.
477
            e.g. "(d[0] < 3) * (d[1] < 2) * exp(-abs(d[2]))"
478
        `allow_self_connections`:
479
            if the connector is used to connect a Population to itself, this
480
            flag determines whether a neuron is allowed to connect to itself,
481
            or only to other neurons in the Population.
482
        `rng`:
483
            an :class:`RNG` instance used to evaluate whether connections exist
484
    """
485

486
    class DisplacementExpression(IndexBasedExpression):
1✔
487
        """
488
        A displacement based expression function used to determine the connection probability
489
        and the value of variable connection parameters of a projection
490
        """
491

492
        def __init__(self, disp_function):
1✔
493
            """
494
            `disp_function`: a function that takes a 3xN numpy displacement matrix and maps each row
495
                             (displacement) to a probability between 0 and 1
496
            """
497
            self._disp_function = disp_function
1✔
498

499
        def __call__(self, i, j):
1✔
500
            disp = (self.projection.post.positions.T[j] - self.projection.pre.positions.T[i]).T
1✔
501
            return self._disp_function(disp)
1✔
502

503
    def __init__(self, disp_function, allow_self_connections=True,
1✔
504
                 rng=None, safe=True, callback=None):
505
        super(DisplacementDependentProbabilityConnector, self).__init__(
1✔
506
            self.DisplacementExpression(disp_function),
507
            allow_self_connections=allow_self_connections, rng=rng, callback=callback)
508

509

510
class FromListConnector(Connector):
1✔
511
    """
512
    Make connections according to a list.
513

514
    Arguments:
515
        `conn_list`:
516
            a list of tuples, one tuple for each connection. Each tuple should contain:
517
            `(pre_idx, post_idx, p1, p2, ..., pn)` where `pre_idx` is the index
518
            (i.e. order in the Population, not the ID) of the presynaptic
519
            neuron, `post_idx` is the index of the postsynaptic neuron, and
520
            p1, p2, etc. are the synaptic parameters (e.g. weight, delay,
521
            plasticity parameters).
522
        `column_names`:
523
            the names of the parameters p1, p2, etc. If not provided, it is
524
            assumed the parameters are 'weight', 'delay' (for backwards
525
            compatibility). This should be specified using a tuple.
526
        `safe`:
527
            if True, check that weights and delays have valid values. If False,
528
            this check is skipped.
529
        `callback`:
530
            if True, display a progress bar on the terminal.
531
    """
532
    parameter_names = ('conn_list',)
1✔
533

534
    def __init__(self, conn_list, column_names=None, safe=True, callback=None):
1✔
535
        """
536
        Create a new connector.
537
        """
538
        Connector.__init__(self, safe=safe, callback=callback)
1✔
539
        self.conn_list = np.array(conn_list)
1✔
540
        if len(conn_list) > 0:
1✔
541
            n_columns = self.conn_list.shape[1]
1✔
542
            if column_names is None:
1✔
543
                if n_columns == 2:
1✔
544
                    self.column_names = ()
1✔
545
                elif n_columns == 4:
1✔
546
                    self.column_names = ('weight', 'delay')
1✔
547
                else:
548
                    raise TypeError("Argument 'column_names' is required.")
×
549
            else:
550
                self.column_names = column_names
1✔
551
                if n_columns != len(self.column_names) + 2:
1✔
552
                    raise ValueError("connection list has %d parameter columns, but %d column names provided." % (
×
553
                        n_columns - 2, len(self.column_names)))
554
        else:
555
            self.column_names = ()
1✔
556

557
    def connect(self, projection):
1✔
558
        """Connect-up a Projection."""
559
        logger.debug("conn_list (original) = \n%s", self.conn_list)
1✔
560
        synapse_parameter_names = projection.synapse_type.get_parameter_names()
1✔
561
        for name in self.column_names:
1✔
562
            if name not in synapse_parameter_names:
1✔
563
                raise ValueError("%s is not a valid parameter for %s" % (
×
564
                                 name, projection.synapse_type.__class__.__name__))
565
        if self.conn_list.size == 0:
1✔
566
            return
1✔
567
        if np.any(self.conn_list[:, 0] >= projection.pre.size):
1✔
568
            raise errors.ConnectionError("source index out of range")
1✔
569
        # need to do some profiling, to figure out the best way to do this:
570
        #  - order of sorting/filtering by local
571
        #  - use np.unique, or just do in1d(self.conn_list)?
572
        idx = np.argsort(self.conn_list[:, 1])
1✔
573
        targets = np.unique(self.conn_list[:, 1]).astype(int)
1✔
574
        local = np.in1d(targets,
1✔
575
                           np.arange(projection.post.size)[projection.post._mask_local],
576
                           assume_unique=True)
577
        local_targets = targets[local]
1✔
578
        self.conn_list = self.conn_list[idx]
1✔
579
        left = np.searchsorted(self.conn_list[:, 1], local_targets, 'left')
1✔
580
        right = np.searchsorted(self.conn_list[:, 1], local_targets, 'right')
1✔
581
        logger.debug("idx = %s", idx)
1✔
582
        logger.debug("targets = %s", targets)
1✔
583
        logger.debug("local_targets = %s", local_targets)
1✔
584
        logger.debug("conn_list (sorted by target) = \n%s", self.conn_list)
1✔
585
        logger.debug("left = %s", left)
1✔
586
        logger.debug("right = %s", right)
1✔
587

588
        for tgt, l, r in zip(local_targets, left, right):
1✔
589
            sources = self.conn_list[l:r, 0].astype(int)
1✔
590
            connection_parameters = deepcopy(projection.synapse_type.parameter_space)
1✔
591

592
            connection_parameters.shape = (r - l,)
1✔
593
            for col, name in enumerate(self.column_names, 2):
1✔
594
                connection_parameters.update(**{name: self.conn_list[l:r, col]})
1✔
595
            if isinstance(projection.synapse_type, StandardSynapseType):
1✔
596
                connection_parameters = projection.synapse_type.translate(
1✔
597
                    connection_parameters)
598
            connection_parameters.evaluate()
1✔
599
            projection._convergent_connect(sources, tgt, **connection_parameters)
1✔
600

601

602
class FromFileConnector(FromListConnector):
1✔
603
    """
604
    Make connections according to a list read from a file.
605

606
    Arguments:
607
        `file`:
608
            either an open file object or the filename of a file containing a
609
            list of connections, in the format required by `FromListConnector`.
610
            Column headers, if included in the file,  must be specified using
611
            a list or tuple, e.g.::
612

613
                # columns = ["i", "j", "weight", "delay", "U", "tau_rec"]
614

615
            Note that the header requires `#` at the beginning of the line.
616

617
        `distributed`:
618
            if this is True, then each node will read connections from a file
619
            called `filename.x`, where `x` is the MPI rank. This speeds up
620
            loading connections for distributed simulations.
621
        `safe`:
622
            if True, check that weights and delays have valid values. If False,
623
            this check is skipped.
624
        `callback`:
625
            if True, display a progress bar on the terminal.
626
    """
627
    parameter_names = ('file', 'distributed')
1✔
628

629
    def __init__(self, file, distributed=False, safe=True, callback=None):
1✔
630
        """
631
        Create a new connector.
632
        """
633
        Connector.__init__(self, safe=safe, callback=callback)
1✔
634
        if isinstance(file, str):
1✔
635
            file = files.StandardTextFile(file, mode='r')
1✔
636
        self.file = file
1✔
637
        self.distributed = distributed
1✔
638

639
    def connect(self, projection):
1✔
640
        """Connect-up a Projection."""
641
        if self.distributed:
1✔
642
            self.file.rename("%s.%d" % (self.file.name,
1✔
643
                                        projection._simulator.state.mpi_rank))
644
        self.column_names = self.file.get_metadata().get('columns', ('weight', 'delay'))
1✔
645
        for ignore in "ij":
1✔
646
            if ignore in self.column_names:
1✔
647
                self.column_names.remove(ignore)
1✔
648
        self.conn_list = self.file.read()
1✔
649
        FromListConnector.connect(self, projection)
1✔
650

651

652
class FixedNumberConnector(MapConnector):
1✔
653
    # base class - should not be instantiated
654
    parameter_names = ('allow_self_connections', 'n')
1✔
655

656
    def __init__(self, n, allow_self_connections=True, with_replacement=False,
1✔
657
                 rng=None, safe=True, callback=None):
658
        """
659
        Create a new connector.
660
        """
661
        Connector.__init__(self, safe, callback)
1✔
662
        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
1✔
663
        self.allow_self_connections = allow_self_connections
1✔
664
        self.with_replacement = with_replacement
1✔
665
        self.n = n
1✔
666
        if isinstance(n, int):
1✔
667
            assert n >= 0
1✔
668
        elif isinstance(n, RandomDistribution):
1✔
669
            # weak check that the random distribution is ok
670
            assert np.all(np.array(n.next(100)) >=
1✔
671
                             0), "the random distribution produces negative numbers"
672
        else:
673
            raise TypeError("n must be an integer or a RandomDistribution object")
×
674
        self.rng = _get_rng(rng)
1✔
675

676
    def _rng_uniform_int_exclude(self, n, size, exclude):
1✔
677
        res = self.rng.next(n, 'uniform_int', {"low": 0, "high": size}, mask=None)
1✔
678
        logger.debug("RNG0 res=%s" % res)
1✔
679
        idx = np.where(res == exclude)[0]
1✔
680
        logger.debug("RNG1 exclude=%d, res=%s idx=%s" % (exclude, res, idx))
1✔
681
        while idx.size > 0:
1✔
682
            redrawn = self.rng.next(idx.size, 'uniform_int', {"low": 0, "high": size}, mask=None)
1✔
683
            res[idx] = redrawn
1✔
684
            idx = idx[np.where(res == exclude)[0]]
1✔
685
            logger.debug("RNG2 exclude=%d redrawn=%s res=%s idx=%s" % (exclude, redrawn, res, idx))
1✔
686
        return res
1✔
687

688

689
class FixedNumberPostConnector(FixedNumberConnector):
1✔
690
    """
691
    Each pre-synaptic neuron is connected to exactly `n` post-synaptic neurons
692
    chosen at random.
693

694
    The sampling behaviour is controlled by the `with_replacement` argument.
695

696
    "With replacement" means that each post-synaptic neuron is chosen from the
697
    entire population. There is always therefore a possibility of multiple
698
    connections between a given pair of neurons.
699

700
    "Without replacement" means that once a neuron has been selected, it cannot
701
    be selected again until the entire population has been selected. This means
702
    that if `n` is less than the size of the post-synaptic population, there
703
    are no multiple connections. If `n` is greater than the size of the post-
704
    synaptic population, all possible single connections are made before
705
    starting to add duplicate connections.
706

707
    Takes any of the standard :class:`Connector` optional arguments and, in
708
    addition:
709

710
        `n`:
711
            either a positive integer, or a `RandomDistribution` that produces
712
            positive integers. If `n` is a `RandomDistribution`, then the
713
            number of post-synaptic neurons is drawn from this distribution
714
            for each pre-synaptic neuron.
715
        `with_replacement`:
716
            if True, the selection of neurons to connect is made from the
717
            entire population. If False, once a neuron is selected it cannot
718
            be selected again until the entire population has been connected.
719
        `allow_self_connections`:
720
            if the connector is used to connect a Population to itself, this
721
            flag determines whether a neuron is allowed to connect to itself,
722
            or only to other neurons in the Population.
723
        `rng`:
724
            an :class:`RNG` instance used to evaluate which potential connections
725
            are created.
726
    """
727

728
    def _get_num_post(self):
1✔
729
        if isinstance(self.n, int):
1✔
730
            n_post = self.n
1✔
731
        else:
732
            n_post = self.n.next()
1✔
733
        return n_post
1✔
734

735
    def connect(self, projection):
1✔
736
        connections = [[] for i in range(projection.post.size)]
1✔
737
        for source_index in range(projection.pre.size):
1✔
738
            n = self._get_num_post()
1✔
739
            if self.with_replacement:
1✔
740
                if not self.allow_self_connections and projection.pre == projection.post:
1✔
741
                    targets = self._rng_uniform_int_exclude(n, projection.post.size, source_index)
1✔
742
                else:
743
                    targets = self.rng.next(
1✔
744
                        n, 'uniform_int', {"low": 0, "high": projection.post.size}, mask=None)
745
            else:
746
                all_cells = np.arange(projection.post.size)
1✔
747
                if not self.allow_self_connections and projection.pre == projection.post:
1✔
748
                    all_cells = all_cells[all_cells != source_index]
1✔
749
                full_sets = n // all_cells.size
1✔
750
                remainder = n % all_cells.size
1✔
751
                target_sets = []
1✔
752
                if full_sets > 0:
1✔
753
                    target_sets = [all_cells] * full_sets
1✔
754
                if remainder > 0:
1✔
755
                    target_sets.append(self.rng.permutation(all_cells)[:remainder])
1✔
756
                targets = np.hstack(target_sets)
1✔
757
            assert targets.size == n
1✔
758
            for target_index in targets:
1✔
759
                connections[target_index].append(source_index)
1✔
760

761
        def build_source_masks(mask=None):
1✔
762
            if mask is None:
1✔
763
                return [np.array(x) for x in connections]
1✔
764
            else:
765
                return [np.array(x) for x in np.array(connections)[mask]]
×
766
        self._standard_connect(projection, build_source_masks)
1✔
767

768

769
class FixedNumberPreConnector(FixedNumberConnector):
1✔
770
    """
771
    Each post-synaptic neuron is connected to exactly `n` pre-synaptic neurons
772
    chosen at random.
773

774
    The sampling behaviour is controlled by the `with_replacement` argument.
775

776
    "With replacement" means that each pre-synaptic neuron is chosen from the
777
    entire population. There is always therefore a possibility of multiple
778
    connections between a given pair of neurons.
779

780
    "Without replacement" means that once a neuron has been selected, it cannot
781
    be selected again until the entire population has been selected. This means
782
    that if `n` is less than the size of the pre-synaptic population, there
783
    are no multiple connections. If `n` is greater than the size of the pre-
784
    synaptic population, all possible single connections are made before
785
    starting to add duplicate connections.
786

787
    Takes any of the standard :class:`Connector` optional arguments and, in
788
    addition:
789

790
        `n`:
791
            either a positive integer, or a `RandomDistribution` that produces
792
            positive integers. If `n` is a `RandomDistribution`, then the
793
            number of pre-synaptic neurons is drawn from this distribution
794
            for each post-synaptic neuron.
795
        `with_replacement`:
796
            if True, the selection of neurons to connect is made from the
797
            entire population. If False, once a neuron is selected it cannot
798
            be selected again until the entire population has been connected.
799
        `allow_self_connections`:
800
            if the connector is used to connect a Population to itself, this
801
            flag determines whether a neuron is allowed to connect to itself,
802
            or only to other neurons in the Population.
803
        `rng`:
804
            an :class:`RNG` instance used to evaluate which potential connections
805
            are created.
806
    """
807

808
    def _get_num_pre(self, size, mask=None):
1✔
809
        if isinstance(self.n, int):
1✔
810
            if mask is None:
1✔
811
                n_pre = repeat(self.n, size)
1✔
812
            else:
813
                n_pre = repeat(self.n, mask.sum())
1✔
814
        else:
815
            if mask is None:
1✔
816
                n_pre = self.n.next(size)
1✔
817
            else:
818
                if self.n.rng.parallel_safe:
×
819
                    n_pre = self.n.next(size)[mask]
×
820
                else:
821
                    n_pre = self.n.next(mask.sum())
×
822
        return n_pre
1✔
823

824
    def connect(self, projection):
1✔
825
        if self.with_replacement:
1✔
826
            if self.allow_self_connections or projection.pre != projection.post:
1✔
827
                def build_source_masks(mask=None):
1✔
828
                    n_pre = self._get_num_pre(projection.post.size, mask)
1✔
829
                    for n in n_pre:
1✔
830
                        sources = self.rng.next(
1✔
831
                            n, 'uniform_int', {"low": 0, "high": projection.pre.size}, mask=None)
832
                        assert sources.size == n
1✔
833
                        yield sources
1✔
834
            else:
835
                def build_source_masks(mask=None):
1✔
836
                    n_pre = self._get_num_pre(projection.post.size, mask)
1✔
837
                    if self.rng.parallel_safe or mask is None:
1✔
838
                        for i, n in enumerate(n_pre):
1✔
839
                            sources = self._rng_uniform_int_exclude(n, projection.pre.size, i)
1✔
840
                            assert sources.size == n
1✔
841
                            yield sources
1✔
842
                    else:
843
                        # TODO: use mask to obtain indices i
844
                        raise NotImplementedError(
×
845
                            "allow_self_connections=False currently requires a parallel safe RNG.")
846
        else:
847
            if self.allow_self_connections or projection.pre != projection.post:
1✔
848
                def build_source_masks(mask=None):
1✔
849
                    # where n > projection.pre.size, first all pre-synaptic cells
850
                    # are connected one or more times, then the remainder
851
                    # are chosen randomly
852
                    n_pre = self._get_num_pre(projection.post.size, mask)
1✔
853
                    all_cells = np.arange(projection.pre.size)
1✔
854
                    for n in n_pre:
1✔
855
                        full_sets = n // projection.pre.size
1✔
856
                        remainder = n % projection.pre.size
1✔
857
                        source_sets = []
1✔
858
                        if full_sets > 0:
1✔
859
                            source_sets = [all_cells] * full_sets
1✔
860
                        if remainder > 0:
1✔
861
                            source_sets.append(self.rng.permutation(all_cells)[:remainder])
1✔
862
                        sources = np.hstack(source_sets)
1✔
863
                        assert sources.size == n
1✔
864
                        yield sources
1✔
865
            else:
866
                def build_source_masks(mask=None):
1✔
867
                    # where n > projection.pre.size, first all pre-synaptic cells
868
                    # are connected one or more times, then the remainder
869
                    # are chosen randomly
870
                    n_pre = self._get_num_pre(projection.post.size, mask)
1✔
871
                    all_cells = np.arange(projection.pre.size)
1✔
872
                    if self.rng.parallel_safe or mask is None:
1✔
873
                        for i, n in enumerate(n_pre):
1✔
874
                            full_sets = n // (projection.pre.size - 1)
1✔
875
                            remainder = n % (projection.pre.size - 1)
1✔
876
                            allowed_cells = all_cells[all_cells != i]
1✔
877
                            source_sets = []
1✔
878
                            if full_sets > 0:
1✔
879
                                source_sets = [allowed_cells] * full_sets
1✔
880
                            if remainder > 0:
1✔
881
                                source_sets.append(self.rng.permutation(allowed_cells)[:remainder])
1✔
882
                            sources = np.hstack(source_sets)
1✔
883
                            assert sources.size == n
1✔
884
                            yield sources
1✔
885
                    else:
886
                        raise NotImplementedError(
×
887
                            "allow_self_connections=False currently requires a parallel safe RNG.")
888

889
        self._standard_connect(projection, build_source_masks)
1✔
890

891

892
class OneToOneConnector(MapConnector):
1✔
893
    """
894
    Where the pre- and postsynaptic populations have the same size, connect
895
    cell *i* in the presynaptic population to cell *i* in the postsynaptic
896
    population for all *i*.
897

898
    Takes any of the standard :class:`Connector` optional arguments.
899
    """
900
    parameter_names = tuple()
1✔
901

902
    def connect(self, projection):
1✔
903
        """Connect-up a Projection."""
904
        connection_map = LazyArray(lambda i, j: i == j, shape=projection.shape)
1✔
905
        self._connect_with_map(projection, connection_map)
1✔
906

907

908
class SmallWorldConnector(Connector):
1✔
909
    """
910
    Connect cells so as to create a small-world network.
911

912
    Takes any of the standard :class:`Connector` optional arguments and, in
913
    addition:
914

915
        `degree`:
916
            the region length where nodes will be connected locally.
917
        `rewiring`:
918
            the probability of rewiring each edge.
919
        `allow_self_connections`:
920
            if the connector is used to connect a Population to itself, this
921
            flag determines whether a neuron is allowed to connect to itself,
922
            or only to other neurons in the Population.
923
        `n_connections`:
924
            if specified, the number of efferent synaptic connections per neuron.
925
        `rng`:
926
            an :class:`RNG` instance used to evaluate which connections
927
            are created.
928
    """
929
    parameter_names = ('allow_self_connections', 'degree', 'rewiring', 'n_connections')
1✔
930

931
    def __init__(self, degree, rewiring, allow_self_connections=True,
1✔
932
                 n_connections=None, rng=None, safe=True, callback=None):
933
        """
934
        Create a new connector.
935
        """
936
        Connector.__init__(self, safe, callback)
×
937
        assert 0 <= rewiring <= 1
×
938
        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
×
939
        self.rewiring = rewiring
×
940
        self.d_expression = "d < %g" % degree
×
941
        self.allow_self_connections = allow_self_connections
×
942
        self.n_connections = n_connections
×
943
        self.rng = _get_rng(rng)
×
944

945
    def connect(self, projection):
1✔
946
        """Connect-up a Projection."""
947
        raise NotImplementedError
×
948

949

950
class CSAConnector(MapConnector):
1✔
951
    """
952
    Use the Connection Set Algebra (Djurfeldt, 2012) to connect cells.
953

954
    Takes any of the standard :class:`Connector` optional arguments and, in
955
    addition:
956

957
        `cset`:
958
            a connection set object.
959
    """
960
    parameter_names = ('cset',)
1✔
961

962
    if haveCSA:
1✔
963
        def __init__(self, cset, safe=True, callback=None):
×
964
            """
965
            """
966
            Connector.__init__(self, safe=safe, callback=callback)
×
967
            self.cset = cset
×
968
            arity = csa.arity(cset)
×
969
            assert arity in (0, 2), 'must specify mask or connection-set with arity 0 or 2'
×
970
    else:
971
        def __init__(self, cset, safe=True, callback=None):
1✔
972
            raise RuntimeError("CSAConnector not available---couldn't import csa module")
×
973

974
    def connect(self, projection):
1✔
975
        """Connect-up a Projection."""
976
        # Cut out finite part
977
        c = csa.cross((0, projection.pre.size - 1), (0, projection.post.size - 1)) * \
×
978
            self.cset  # can't we cut out just the columns we want?
979

980
        if csa.arity(self.cset) == 2:
×
981
            # Connection-set with arity 2
982
            for (i, j, weight, delay) in c:
×
983
                projection._convergent_connect(
×
984
                    [projection.pre[i]], projection.post[j], weight, delay)
985
        elif csa.arity(self.cset) == 0:
×
986
            # inefficient implementation as a starting point
987
            connection_map = np.zeros((projection.pre.size, projection.post.size), dtype=bool)
×
988
            for addr in c:
×
989
                connection_map[addr] = True
×
990
            self._connect_with_map(projection, LazyArray(connection_map))
×
991
        else:
992
            raise NotImplementedError
×
993

994

995
class CloneConnector(MapConnector):
1✔
996
    """
997
    Connects cells with the same connectivity pattern as a previous projection.
998
    """
999
    parameter_names = ('reference_projection',)
1✔
1000

1001
    def __init__(self, reference_projection, safe=True, callback=None):
1✔
1002
        """
1003
        Create a new CloneConnector.
1004

1005
        `reference_projection` -- the projection to clone the connectivity pattern from
1006
        """
1007
        MapConnector.__init__(self, safe, callback=callback)
1✔
1008
        self.reference_projection = reference_projection
1✔
1009

1010
    def connect(self, projection):
1✔
1011
        if (projection.pre != self.reference_projection.pre or
1✔
1012
                projection.post != self.reference_projection.post):
1013
            raise errors.ConnectionError("Pre and post populations must match between reference ({0}"
1✔
1014
                                         "  and {1}) and clone projections ({2} and {3}) for "
1015
                                         "CloneConnector"
1016
                                         .format(self.reference_projection.pre,
1017
                                                 self.reference_projection.post,
1018
                                                 projection.pre, projection.post))
1019
        connection_map = LazyArray(~np.isnan(self.reference_projection.get(['weight'], 'array',
1✔
1020
                                                                              gather='all')[0]))
1021
        self._connect_with_map(projection, connection_map)
1✔
1022

1023

1024
class ArrayConnector(MapConnector):
1✔
1025
    """
1026
    Provide an explicit boolean connection matrix, with shape (m, n) where m is
1027
    the size of the presynaptic population and n that of the postsynaptic
1028
    population.
1029
    """
1030
    parameter_names = ('array',)
1✔
1031

1032
    def __init__(self, array, safe=True, callback=None):
1✔
1033
        """
1034
        Create a new connector.
1035
        """
1036
        Connector.__init__(self, safe, callback)
1✔
1037
        self.array = array
1✔
1038

1039
    def connect(self, projection):
1✔
1040
        connection_map = LazyArray(self.array, projection.shape)
1✔
1041
        self._connect_with_map(projection, connection_map)
1✔
1042

1043

1044
class FixedTotalNumberConnector(FixedNumberConnector):
1✔
1045
    parameter_names = ('allow_self_connections', 'n')
1✔
1046

1047
    def __init__(self, n, allow_self_connections=True, with_replacement=True,
1✔
1048
                 rng=None, safe=True, callback=None):
1049
        """
1050
        Create a new connector.
1051
        """
1052
        Connector.__init__(self, safe, callback)
1✔
1053
        assert isinstance(allow_self_connections, bool) or allow_self_connections == 'NoMutual'
1✔
1054
        self.allow_self_connections = allow_self_connections
1✔
1055
        self.with_replacement = with_replacement
1✔
1056
        self.n = n
1✔
1057
        if isinstance(n, int):
1✔
1058
            assert n >= 0
1✔
1059
        elif isinstance(n, RandomDistribution):
×
1060
            # weak check that the random distribution is ok
1061
            assert np.all(np.array(n.next(100)) >=
×
1062
                             0), "the random distribution produces negative numbers"
1063
        else:
1064
            raise TypeError("n must be an integer or a RandomDistribution object")
×
1065
        self.rng = _get_rng(rng)
1✔
1066

1067
    def connect(self, projection):
1✔
1068
        # This implementation is not "parallel safe" for random numbers.
1069
        # todo: support the `parallel_safe` flag.
1070

1071
        # Determine number of processes and current rank
1072
        rank = projection._simulator.state.mpi_rank
1✔
1073
        num_processes = projection._simulator.state.num_processes
1✔
1074

1075
        # Assume that targets are equally distributed over processes
1076
        targets_per_process = int(len(projection.post) / num_processes)
1✔
1077

1078
        # Calculate the number of synapses on each process
1079
        bino = RandomDistribution('binomial',
1✔
1080
                                  [self.n, targets_per_process / len(projection.post)],
1081
                                  rng=self.rng)
1082
        num_conns_on_vp = np.zeros(num_processes, dtype=int)
1✔
1083
        sum_dist = 0
1✔
1084
        sum_partitions = 0
1✔
1085
        for k in range(num_processes):
1✔
1086
            p_local = targets_per_process / (len(projection.post) - sum_dist)
1✔
1087
            bino.parameters['p'] = p_local
1✔
1088
            bino.parameters['n'] = self.n - sum_partitions
1✔
1089
            num_conns_on_vp[k] = bino.next()
1✔
1090
            sum_dist += targets_per_process
1✔
1091
            sum_partitions += num_conns_on_vp[k]
1✔
1092

1093
        # Draw random sources and targets
1094
        connections = [[] for i in range(projection.post.size)]
1✔
1095
        possible_targets = np.arange(projection.post.size)[projection.post._mask_local]
1✔
1096
        for i in range(num_conns_on_vp[rank]):
1✔
1097
            source_index = self.rng.next(1, 'uniform_int',
1✔
1098
                                         {"low": 0, "high": projection.pre.size},
1099
                                         mask=None)[0]
1100
            target_index = self.rng.choice(possible_targets, size=1)[0]
1✔
1101
            connections[target_index].append(source_index)
1✔
1102

1103
        def build_source_masks(mask=None):
1✔
1104
            if mask is None:
1✔
1105
                return [np.array(x) for x in connections]
1✔
1106
            else:
1107
                return [np.array(x) for x in np.array(connections)[mask]]
×
1108
        self._standard_connect(projection, build_source_masks)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc