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