Skip to content

Population#

A Population object represents a group of identical neurons. It is associated with a geometry (defining the number of neurons and optionally its spatial structure), a neuron type and optionally a name.

ANNarchy.core.Population.Population #

Container for a population of homogeneous neurons.

Source code in ANNarchy/core/Population.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
class Population :
    """
    Container for a population of homogeneous neurons.
    """

    def __init__(self, geometry, neuron, name=None, stop_condition=None, storage_order='post_to_pre', copied=False):
        """
        :param geometry: population geometry as tuple. If an integer is given, it is the size of the population.
        :param neuron: instance of ``ANNarchy.Neuron``. It can be user-defined or a built-in model.
        :param name: unique name of the population (optional, it defaults to ``pop0``, ``pop1``, etc).
        :param stop_condition: a single condition on a neural variable which can stop the simulation whenever it is true.

        Example:

        ```python
        pop = Population(100, neuron=Izhikevich, name="Excitatory population")
        ```

        """
        # Check if the network has already been compiled
        if Global._network[0]['compiled'] and not copied:
            Global._error('You cannot add a population after the network has been compiled.')

        # Store the provided geometry
        # automatically defines w, h, d, size
        if isinstance(geometry, (int, float)):
            # 1D
            self.geometry = (int(geometry), )
            self.width = int(geometry)
            self.height = int(1)
            self.depth = int(1)
            self.dimension = int(1)
        elif isinstance(geometry, tuple):
            # a tuple is given, can be 1 .. N dimensional
            self.geometry = ()
            for d in geometry:
                self.geometry += (int(d),)
            self.width = int(geometry[0])
            if len(geometry)>=2:
                self.height = int(geometry[1])
            else:
                self.height = int(1)
            if len(geometry)>=3:
                self.depth = int(geometry[2])
            else:
                self.depth = int(1)

            self.dimension = len(geometry)
        else:
            Global._error('Population(): the geometry must be either an integer or a tuple.')

        # Compute the size
        size = int(1)
        for i in range(len(self.geometry)):
            size *= int(self.geometry[i])
        self.size = int(size)
        self.ranks = np.arange(self.size, dtype="int32")

        # Store the neuron type
        if inspect.isclass(neuron):
            self.neuron_type = neuron()
        else:
            self.neuron_type = copy.deepcopy(neuron)
        self.neuron_type._analyse()

        # Store the stop condition
        self.stop_condition = stop_condition

        # Attribute a name if not provided
        self.id = len(Global._network[0]['populations'])
        self.class_name = 'pop'+str(self.id)

        if name:
            self.name = name
        else:
            self.name = self.class_name

        # Add the population to the global variable
        Global._network[0]['populations'].append(self)

        # Get a list of parameters and variables
        self.parameters = []
        self.variables = []
        for param in self.neuron_type.description['parameters']:
            self.parameters.append(param['name'])
        for var in self.neuron_type.description['variables']:
            self.variables.append(var['name'])
        self.attributes = self.parameters + self.variables

        # Get a list of user-defined functions
        self.functions = [func['name'] for func in self.neuron_type.description['functions']]

        # Store initial values
        self.init = {}
        for param in self.neuron_type.description['parameters']:
            self.init[param['name']] = param['init']
        for var in self.neuron_type.description['variables']:
            self.init[var['name']] = var['init']

        # List of targets actually connected
        self.targets = []

        # List of global operations needed by connected projections
        self.global_operations = []

        # Maximum delay of connected projections
        self.max_delay = 0

        # Spiking neurons: do they have to compute an average?
        self._compute_mean_fr = -1.

        # Finalize initialization
        self.initialized = False
        self.cyInstance = None
        self.enabled = True

        # Rank <-> Coordinates methods
        # for the one till three dimensional case we use cython optimized functions.
        from ANNarchy.core.cython_ext import Coordinates
        if self.dimension==1:
            self._rank_from_coord = Coordinates.get_rank_from_1d_coord
            self._coord_from_rank = Coordinates.get_1d_coord
        elif self.dimension==2:
            self._rank_from_coord = Coordinates.get_rank_from_2d_coord
            self._coord_from_rank = Coordinates.get_2d_coord
        elif self.dimension==3:
            self._rank_from_coord = Coordinates.get_rank_from_3d_coord
            self._coord_from_rank = Coordinates.get_3d_coord
        else:
            self._rank_from_coord = np.ravel_multi_index
            self._coord_from_rank = np.unravel_index

        self._norm_coord_dict = {
            1: Coordinates.get_normalized_1d_coord,
            2: Coordinates.get_normalized_2d_coord,
            3: Coordinates.get_normalized_3d_coord
        }

        # Recorded variables
        self._monitor = None

        # Is overwritten by SpecificPopulations
        self._specific_template = {}

        # Storage order. TODO: why?
        self._storage_order = storage_order

    def _copy(self):
        "Returns a copy of the population when creating networks. Internal use only."
        return Population(geometry=self.geometry, neuron=self.neuron_type, name=self.name, stop_condition=self.stop_condition, storage_order=self._storage_order, copied=True)

    def _generate(self):
        "Overriden by specific populations to generate the code."
        pass

    def _instantiate(self, module):
        """
        Instantiates the population after compilation of the C++ simulation core.
        The function should solely called by Compiler._instantiate().

        :param:     module  cython module (ANNarchyCore instance)
        """
        if Global.config["profiling"]:
            import time
            t1 = time.time()

        try:
            self.cyInstance = getattr(module, self.class_name+'_wrapper')(self.size, self.max_delay)
        except:
            Global._error('unable to instantiate the population', self.name)

        if Global.config["profiling"]:
            t2 = time.time()
            Global._profiler.add_entry(t1, t2, "pop"+str(self.id), "instantiate")

    def _init_attributes(self):
        """ Method used after compilation to initialize the attributes."""
        # Initialize the population
        self.initialized = True

        # Transfer the initial values of all attributes
        for name, value in self.init.items():
            if isinstance(value, Global.Constant):
                self.__setattr__(name, value.value)
            else:
                self.__setattr__(name, value)


        # Activate the population
        self.cyInstance.activate(self.enabled)

        # Reset to generate the right structures
        self.cyInstance.reset()

        # If the spike population has a refractory period:
        if self.neuron_type.type == 'spike' and self.neuron_type.description['refractory']:
            if not isinstance(self.neuron_type.description['refractory'], str): # the variable will be used directly
                self.refractory = self.neuron_type.description['refractory']

        # Spiking neurons can compute a mean FR
        if self.neuron_type.type == 'spike':
            getattr(self.cyInstance, 'compute_firing_rate')(self._compute_mean_fr)

    def size_in_bytes(self):
        """
        Returns the size of allocated memory on the C++ side. Please note that this does not contain monitored data and works only if compile() was invoked.
        """
        if self.initialized:
            return self.cyInstance.size_in_bytes()
        else:
            return 0

    def _clear(self):
        """
        Deallocates container within the C++ instance. The population object is not usable anymore after calling this function.

        Warning: should be only called by the net deconstruction ( in context of parallel_run() ).
        """
        if self.initialized:
            self.cyInstance.clear()
            self.initialized = False

    def reset(self, attributes=-1):
        """
        Resets all parameters and variables of the population to the value they had before the call to compile().

        :param attributes: list of attributes (parameter or variable) which should be reinitialized. Default: all attributes.
        """
        if attributes == -1:
            try:
                self.set(self.init)
            except Exception as e:
                Global._print(e)
                Global._error("Population.reset(): something went wrong while resetting.")
        else: # only some of them
            for var in attributes:
                # check it exists
                if not var in self.attributes:
                    Global._warning("Population.reset():", var, "is not an attribute of the population, skipping.")
                    continue

                try:
                    self.__setattr__(var, self.init[var])
                except Exception as e:
                    Global._print(e)
                    Global._warning("Population.reset(): something went wrong while resetting", var)

        self.cyInstance.activate(self.enabled)
        self.cyInstance.reset()

    def clear(self):
        """
        Clears all spiking events previously emitted (history of spikes, delayed spikes).

        Can be useful if you do not want to totally reset a population (i.e. all variables), only to clear the spiking history between two trials.

        Note: does nothing for rate-coded networks.
        """
        self.cyInstance.reset()

    def enable(self):
        """
        (Re)-enables computations in this population, after they were disabled by the ``disable()`` method.

        The status of the population is accessible through the ``enabled`` flag.
        """
        if self.initialized:
            self.cyInstance.activate(True)
        self.enabled = True

    def disable(self):
        """
        Temporarily disables computations in this population (including the projections leading to it).

        You can re-enable it with the ``enable()`` method.
        """
        if self.initialized:
            self.cyInstance.activate(False)
        self.enabled = False

    def __getattr__(self, name):
        # Method called when accessing an attribute.
        if name == 'initialized' or not hasattr(self, 'initialized'): # Before the end of the constructor
            return object.__getattribute__(self, name)
        elif hasattr(self, 'attributes'):
            if name in self.attributes:
                if self.initialized: # access after compile()
                    return self._get_cython_attribute(name)
                else: # access before compile()
                    if name in self.neuron_type.description['local']:
                        if isinstance(self.init[name], np.ndarray):
                            return self.init[name]
                        else:
                            return np.array([self.init[name]] * self.size).reshape(self.geometry)
                    else:
                        return self.init[name]
            elif name in self.functions:
                return self._function(name)
            else:
                return object.__getattribute__(self, name)
        return object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        # Method called when setting an attribute.
        if name == 'initialized' or not hasattr(self, 'initialized'): # Before the end of the constructor
            object.__setattr__(self, name, value)
        elif hasattr(self, 'attributes'):
            if name in self.attributes:
                if not self.initialized:
                    if isinstance(value, RandomDistribution): # Make sure it is generated only once
                        self.init[name] = np.array(value.get_values(self.size)).reshape(self.geometry)
                    else:
                        self.init[name] = value
                else:
                    self._set_cython_attribute(name, value)
            else:
                object.__setattr__(self, name, value)
        else:
            object.__setattr__(self, name, value)

    def _get_cython_attribute(self, attribute):
        """
        Returns the value of the given attribute for all neurons in the population,
        as a Numpy array having the same geometry as the population if it is local.

        :param attribute: should be a string representing the variables's name.

        """
        try:
            ctype = self._get_attribute_cpp_type(attribute)
            if attribute in self.neuron_type.description['local']:
                data = self.cyInstance.get_local_attribute_all(attribute, ctype)
                return data.reshape(self.geometry)
            else:
                return self.cyInstance.get_global_attribute(attribute, ctype)
        except Exception as e:
            Global._print(e)
            Global._error(' the variable ' +  attribute +  ' does not exist in this population (' + self.name + ')')

    def _set_cython_attribute(self, attribute, value):
        """
        Sets the value of the given attribute for all neurons in the population,
        as a Numpy array having the same geometry as the population if it is local.

        :param attribute: should be a string representing the variables's name.
        :param value: a value or Numpy array of the right size.

        """
        try:
            ctype = self._get_attribute_cpp_type(attribute)
            if attribute in self.neuron_type.description['local']:
                if isinstance(value, np.ndarray):
                    self.cyInstance.set_local_attribute_all(attribute, value.reshape(self.size), ctype)
                elif isinstance(value, list):
                    self.cyInstance.set_local_attribute_all(attribute, np.array(value).reshape(self.size), ctype)
                else:
                    self.cyInstance.set_local_attribute_all(attribute, value * np.ones( self.size ), ctype)
            else:
                self.cyInstance.set_global_attribute(attribute, value, ctype)
        except Exception as e:
            Global._debug(e)
            err_msg = """Population.set(): either the variable '%(attr)s' does not exist in the population '%(pop)s', or the provided array does not have the right size."""
            Global._error(err_msg  % { 'attr': attribute, 'pop': self.name } )

    def _get_attribute_cpp_type(self, attribute):
        """
        Determine C++ data type for a given attribute
        """
        ctype = None
        for var in self.neuron_type.description['variables']+self.neuron_type.description['parameters']:
            if var['name'] == attribute:
                ctype = var['ctype']
                break

        return ctype

    def __len__(self):
        # Number of neurons in the population.
        return self.size


    def set(self, values):
        """
        Sets the value of neural variables and parameters.

        Example:

        ```python
        pop.set({ 'tau' : 20.0, 'r'= np.random.rand((8,8)) } )
        ```

        :param values: dictionary of attributes to be updated.

        """
        for name, value in values.items():
            self.__setattr__(name, value)

    def get(self, name):
        """
        Returns the value of neural variables and parameters.

        :param name: attribute name as a string.
        """
        return self.__getattr__(name)



    ################################
    ## Access to functions
    ################################
    def _function(self, func):
        "Access a user defined function"
        if not self.initialized:
            Global._warning('the network is not compiled yet, cannot access the function ' + func)
            return

        return getattr(self.cyInstance, func)

    ################################
    ## Access to weighted sums
    ################################
    def sum(self, target):
        """
        Returns the array of weighted sums corresponding to the target:

        ```python
        excitatory = pop.sum('exc')
        ```

        For spiking networks, this is equivalent to accessing the conductances directly:

        ```python
        excitatory = pop.g_exc
        ```

        If no incoming projection has the given target, the method returns zeros.

        **Note:** it is not possible to distinguish the original population when the same target is used.

        :param target: the desired projection target.
        """
        # Check if the network is initialized
        if not self.initialized:
            Global._warning('sum(): the population', self.name, 'is not initialized yet.')
            return np.zeros(self.geometry)
        # Check if a projection has this type
        if not target in self.targets:
            Global._warning('sum(): the population', self.name, 'receives no projection with the target', target)
            return np.zeros(self.geometry)
        # Spiking neurons already have conductances available
        if self.neuron_type.type == 'spike':
            return getattr(self, 'g_'+target)
        # Otherwise, call the Cython method
        return self.cyInstance.get_local_attribute_all("_sum_"+target, Global.config["precision"])

    ################################
    ## Refractory period
    ################################
    @property
    def refractory(self):
        if self.neuron_type.description['type'] == 'spike':
            if self.initialized:
                if not isinstance(self.neuron_type.description['refractory'], str):
                    return Global.config['dt']*self.cyInstance.get_refractory()
                else:
                    return getattr(self, self.neuron_type.description['refractory'])
            else :
                return self.neuron_type.description['refractory']
        else:
            Global._warning('Rate-coded neurons do not have refractory periods...')
            return None

    @refractory.setter
    def refractory(self, value):
        if self.neuron_type.description['type'] == 'spike':

            if isinstance(self.neuron_type.description['refractory'], str):
                Global._warning("The refractory period is linked to the neural variable", self.neuron_type.description['refractory'], ", doing nothing... Change its value instead.")
                return

            if self.initialized:
                if isinstance(value, RandomDistribution):
                    refs = (value.get_values(self.size)/Global.config['dt']).astype(int)
                elif isinstance(value, np.ndarray):
                    refs = (value / Global.config['dt']).astype(int).reshape(self.size)
                else:
                    refs = (value/ Global.config['dt']*np.ones(self.size)).astype(int)
                # TODO cast into int
                self.cyInstance.set_refractory(refs)
            else: # not initialized yet, saving for later
                self.neuron_type.description['refractory'] = value
        else:
            Global._warning('Rate-coded neurons do not have refractory periods...')

    ################################
    ## Spiking neurons can compute a mean FR
    ################################
    def compute_firing_rate(self, window):
        """
        Tells spiking neurons in the population to compute their mean firing rate over the given window and store the values in the variable `r`.

        This method has an effect on spiking neurons only.

        If this method is not called, `r` will always be 0.0. `r` can of course be accessed and recorded as any other variable.

        :param window: window in ms over which the spikes will be counted.
        """
        if Global._check_paradigm('cuda'):
            Global._warning('compute_firing_rate() is currently being evaluated on the host-side, so may be slow ... ')

        if self.neuron_type.type == 'rate':
            Global._error('compute_firing_rate(): the neuron is already rate-coded...')

        self._compute_mean_fr = float(window)

        if self.initialized:
            getattr(self.cyInstance, 'compute_firing_rate')(self._compute_mean_fr)

    ################################
    ## Access to individual neurons
    ################################
    def neuron(self, *coord):
        """
        Returns an ``IndividualNeuron`` object wrapping the neuron with the provided rank or coordinates.
        """
        # Transform arguments
        if len(coord) == 1:
            if isinstance(coord[0], int):
                rank = coord[0]
                if not rank < self.size:
                    Global._error(' when accessing neuron', str(rank), ': the population', self.name, 'has only', self.size, 'neurons (geometry '+ str(self.geometry) +').')
            else:
                rank = self.rank_from_coordinates( coord[0] )
                if rank is None:
                    return None
        else: # a tuple
            rank = self.rank_from_coordinates( coord )
            if rank is None:
                return None

        # Return corresponding neuron
        return IndividualNeuron(self, rank)

    @property
    def neurons(self):
        """
        Returns iteratively each neuron in the population.

        For instance, if you want to iterate over all neurons of a population:

        ```python
        for neuron in pop.neurons:
            neuron.r = 0.0
        ```

        Alternatively, one could also benefit from the ``__iter__`` special command. 
        The following code is equivalent:

        ```python
        for neuron in pop:
            neuron.r = 0.0
        ```
        """
        for neur_rank in range(self.size):
            yield self.neuron(neur_rank)

    # Iterators
    def __getitem__(self, *args, **kwds):
        """ 
        Returns neurons froms coordinates in the population.

        If only one argument is given, it is interpeted as a rank and returns a single neuron.

        If slices are given, it returns a PopulationView object.
        """
        indices =  args[0]
        try:
            if np.issubdtype(indices, int):
                indices = int(indices)
        except:
            pass

        if isinstance(indices, int): # a single neuron
            return PopulationView(self, ranks=np.array([int(indices)]), geometry=(1,))

        elif isinstance(indices, (list)):
            indices = np.array(indices)
            return PopulationView(self, indices, geometry=(len(indices),))

        elif isinstance(indices, (np.ndarray)):
            # Sanity check
            if isinstance(indices, (np.ndarray)):
                if indices.ndim != 1:
                    Global._error('only one-dimensional lists/arrays are allowed to address a population.')

            return PopulationView(self, indices, geometry=(len(indices),))

        elif isinstance(indices, slice): # a single slice of ranks
            start, stop, step = indices.start, indices.stop, indices.step

            # no value defined for a position
            if indices.start is None:
                start = 0
            if indices.stop is None:
                stop = self.size
            if indices.step is None:
                step = 1

            # start or end arguments are negative
            if start < 0:
                start = self.size + start
            if stop < 0:
                stop = self.size + stop

            # generate a new set of indices
            rk_range = np.arange(start, stop, step, dtype="int32")
            return PopulationView(self, rk_range, geometry=(len(rk_range),))

        elif isinstance(indices, tuple): # a tuple
            slices = False
            for idx in indices: # check if there are slices in the coordinates
                if isinstance(idx, slice): # there is at least one
                    slices = True
            if not slices: # return one neuron
                return self.neuron(indices)
            else: # Compute a list of ranks from the slices
                coords = []
                # Expand the slices
                for rank in range(len(indices)):
                    idx = indices[rank]
                    if isinstance(idx, int): # no slice
                        coords.append([idx])
                    elif isinstance(idx, slice): # slice
                        start, stop, step = idx.start, idx.stop, idx.step
                        if idx.start is None:
                            start = 0
                        if idx.stop is None:
                            stop = self.geometry[rank]
                        if idx.step is None:
                            step = 1
                        rk_range = list(range(start, stop, step))
                        coords.append(rk_range)
                # Generate all ranks from the indices
                if self.dimension == 2:
                    ranks = [self.rank_from_coordinates((x, y)) for x in coords[0] for y in coords[1]]
                    geometry = (len(coords[0]), len(coords[1]))
                elif self.dimension == 3:
                    ranks = [self.rank_from_coordinates((x, y, z)) for x in coords[0] for y in coords[1] for z in coords[2]]
                    geometry = (len(coords[0]), len(coords[1]), len(coords[2]))
                elif self.dimension == 4:
                    ranks = [self.rank_from_coordinates((x, y, z, k)) for x in coords[0] for y in coords[1] for z in coords[2] for k in coords[3]]
                    geometry = (len(coords[0]), len(coords[1]), len(coords[2]), len(coords[3]))
                else:
                    Global._error("Slicing is implemented only for population with 4 dimensions at maximum", self.geometry)
                if not max(ranks) < self.size:
                    Global._error("Indices do not match the geometry of the population", self.geometry)

                return PopulationView(self, ranks, geometry=geometry)

        Global._warning('Population' + self.name + ': can not address the population with', indices)
        return None

    def __iter__(self):
        # Returns iteratively each neuron in the population in ascending rank order.
        for neur_rank in range(self.size):
            yield self.neuron(neur_rank)

    ################################
    ## Coordinate transformations
    ################################
    def rank_from_coordinates(self, coord):
        """
        Returns the rank of a neuron based on coordinates.

        :param coord: coordinate tuple, can be multidimensional.
        """
        try:
            rank = self._rank_from_coord( coord, self.geometry )
        except:
            Global._error('rank_from_coordinates(): There is no neuron of coordinates', coord, 'in the population', self.name, self.geometry)

        if rank > self.size:
            Global._error('rank_from_coordinates(), neuron', str(coord), ': the population' , self.name , 'has only', self.size, 'neurons (geometry '+ str(self.geometry) +').')
        else:
            return rank

    def coordinates_from_rank(self, rank):
        """
        Returns the coordinates of a neuron based on its rank.

        :param rank: rank of the neuron.
        """
        # Check the rank
        if not rank < self.size:
            Global._error('The given rank', str(rank), 'is larger than the size of the population', str(self.size) + '.')

        try:
            coord = self._coord_from_rank( rank, self.geometry )
        except:
            Global._error('The given rank', str(rank), 'is larger than the size of the population', str(self.size) + '.')
        else:
            return coord

    def normalized_coordinates_from_rank(self, rank, norm=1.):
        """
        Returns normalized coordinates of a neuron based on its rank. 
        The geometry of the population is mapped to the hypercube $[0, 1]^d$

        :param rank: rank of the neuron
        :param norm: norm of the cube (default = 1.0)

        """
        try:
            normal = self._norm_coord_dict[self.dimension](rank, self.geometry)
        except KeyError:
            coord = self.coordinates_from_rank(rank)

            normal = tuple()
            for dim in range(self.dimension):
                if self._geometry[dim] > 1:
                    normal += ( norm * float(coord[dim])/float(self.geometry[dim]-1), )
                else:
                    normal += (float(rank)/(float(self.size)-1.0),) # default?

        return normal


    ################################
    ## Save/load methods
    ################################
    def _data(self):
        "Returns a dictionary containing all information about the population. Used for saving."
        desc = {}
        desc['name'] = self.name
        desc['geometry'] = self.geometry
        desc['size'] = self.size
        # Attributes
        desc['attributes'] = self.attributes
        desc['parameters'] = self.parameters
        desc['variables'] = self.variables
        # Save all attributes
        for var in self.attributes:
            try:
                ctype = self._get_attribute_cpp_type(var)
                if var in self.neuron_type.description['local']:
                    data = self.cyInstance.get_local_attribute_all(var, ctype)
                    desc[var] = data.reshape(self.geometry)
                else:
                    desc[var] = self.cyInstance.get_global_attribute(var, ctype)

            except:
                Global._warning('Can not save the attribute ' + var + ' in the population ' + self.name + '.')

        return desc

    def save(self, filename):
        """
        Saves all information about the population (structure, current value of parameters and variables) into a file.

        * If the file name is '.npz', the data will be saved and compressed using `np.savez_compressed` (recommended).

        * If the file name ends with '.gz', the data will be pickled into a binary file and compressed using gzip.

        * If the file name is '.mat', the data will be saved as a Matlab 7.2 file. Scipy must be installed.

        * Otherwise, the data will be pickled into a simple binary text file using pickle.

        **Warning:** The '.mat' data will not be loadable by ANNarchy, it is only for external analysis purpose.

        :param filename: filename, may contain relative or absolute path.

        Example:

        ```python
        pop.save('pop1.npz')
        pop.save('pop1.txt')
        pop.save('pop1.txt.gz')
        pop.save('pop1.mat')
        ```

        """
        from ANNarchy.core.IO import _save_data
        _save_data(filename, self._data())


    def load(self, filename, pickle_encoding=None):
        """
        Load the saved state of the population by `Population.save()`.

        Warning: Matlab data can not be loaded.

        Example:

        ```python
        pop.load('pop1.npz')
        pop.load('pop1.txt')
        pop.load('pop1.txt.gz')
        ```

        :param filename: the filename with relative or absolute path.

        """
        from ANNarchy.core.IO import _load_data
        self._load_pop_data(_load_data(filename, pickle_encoding))

    def _load_pop_data(self, desc):
        """
        Updates the population with the stored data set.
        """
        if not 'attributes' in desc.keys():
            Global._error('Saved with a too old version of ANNarchy (< 4.2).', exit=True)

        for var in desc['attributes']:
            try:
                self._set_cython_attribute(var, desc[var])

            except Exception as e:
                Global._print(e)
                Global._warning('Can not load the variable ' + var + ' in the population ' + self.name)
                Global._print('Skipping this variable.')
                continue

neurons property #

Returns iteratively each neuron in the population.

For instance, if you want to iterate over all neurons of a population:

for neuron in pop.neurons:
    neuron.r = 0.0

Alternatively, one could also benefit from the __iter__ special command. The following code is equivalent:

for neuron in pop:
    neuron.r = 0.0

__init__(geometry, neuron, name=None, stop_condition=None, storage_order='post_to_pre', copied=False) #

Parameters:

  • geometry

    population geometry as tuple. If an integer is given, it is the size of the population.

  • neuron

    instance of ANNarchy.Neuron. It can be user-defined or a built-in model.

  • name

    unique name of the population (optional, it defaults to pop0, pop1, etc).

  • stop_condition

    a single condition on a neural variable which can stop the simulation whenever it is true. Example: python pop = Population(100, neuron=Izhikevich, name="Excitatory population")

Source code in ANNarchy/core/Population.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def __init__(self, geometry, neuron, name=None, stop_condition=None, storage_order='post_to_pre', copied=False):
    """
    :param geometry: population geometry as tuple. If an integer is given, it is the size of the population.
    :param neuron: instance of ``ANNarchy.Neuron``. It can be user-defined or a built-in model.
    :param name: unique name of the population (optional, it defaults to ``pop0``, ``pop1``, etc).
    :param stop_condition: a single condition on a neural variable which can stop the simulation whenever it is true.

    Example:

    ```python
    pop = Population(100, neuron=Izhikevich, name="Excitatory population")
    ```

    """
    # Check if the network has already been compiled
    if Global._network[0]['compiled'] and not copied:
        Global._error('You cannot add a population after the network has been compiled.')

    # Store the provided geometry
    # automatically defines w, h, d, size
    if isinstance(geometry, (int, float)):
        # 1D
        self.geometry = (int(geometry), )
        self.width = int(geometry)
        self.height = int(1)
        self.depth = int(1)
        self.dimension = int(1)
    elif isinstance(geometry, tuple):
        # a tuple is given, can be 1 .. N dimensional
        self.geometry = ()
        for d in geometry:
            self.geometry += (int(d),)
        self.width = int(geometry[0])
        if len(geometry)>=2:
            self.height = int(geometry[1])
        else:
            self.height = int(1)
        if len(geometry)>=3:
            self.depth = int(geometry[2])
        else:
            self.depth = int(1)

        self.dimension = len(geometry)
    else:
        Global._error('Population(): the geometry must be either an integer or a tuple.')

    # Compute the size
    size = int(1)
    for i in range(len(self.geometry)):
        size *= int(self.geometry[i])
    self.size = int(size)
    self.ranks = np.arange(self.size, dtype="int32")

    # Store the neuron type
    if inspect.isclass(neuron):
        self.neuron_type = neuron()
    else:
        self.neuron_type = copy.deepcopy(neuron)
    self.neuron_type._analyse()

    # Store the stop condition
    self.stop_condition = stop_condition

    # Attribute a name if not provided
    self.id = len(Global._network[0]['populations'])
    self.class_name = 'pop'+str(self.id)

    if name:
        self.name = name
    else:
        self.name = self.class_name

    # Add the population to the global variable
    Global._network[0]['populations'].append(self)

    # Get a list of parameters and variables
    self.parameters = []
    self.variables = []
    for param in self.neuron_type.description['parameters']:
        self.parameters.append(param['name'])
    for var in self.neuron_type.description['variables']:
        self.variables.append(var['name'])
    self.attributes = self.parameters + self.variables

    # Get a list of user-defined functions
    self.functions = [func['name'] for func in self.neuron_type.description['functions']]

    # Store initial values
    self.init = {}
    for param in self.neuron_type.description['parameters']:
        self.init[param['name']] = param['init']
    for var in self.neuron_type.description['variables']:
        self.init[var['name']] = var['init']

    # List of targets actually connected
    self.targets = []

    # List of global operations needed by connected projections
    self.global_operations = []

    # Maximum delay of connected projections
    self.max_delay = 0

    # Spiking neurons: do they have to compute an average?
    self._compute_mean_fr = -1.

    # Finalize initialization
    self.initialized = False
    self.cyInstance = None
    self.enabled = True

    # Rank <-> Coordinates methods
    # for the one till three dimensional case we use cython optimized functions.
    from ANNarchy.core.cython_ext import Coordinates
    if self.dimension==1:
        self._rank_from_coord = Coordinates.get_rank_from_1d_coord
        self._coord_from_rank = Coordinates.get_1d_coord
    elif self.dimension==2:
        self._rank_from_coord = Coordinates.get_rank_from_2d_coord
        self._coord_from_rank = Coordinates.get_2d_coord
    elif self.dimension==3:
        self._rank_from_coord = Coordinates.get_rank_from_3d_coord
        self._coord_from_rank = Coordinates.get_3d_coord
    else:
        self._rank_from_coord = np.ravel_multi_index
        self._coord_from_rank = np.unravel_index

    self._norm_coord_dict = {
        1: Coordinates.get_normalized_1d_coord,
        2: Coordinates.get_normalized_2d_coord,
        3: Coordinates.get_normalized_3d_coord
    }

    # Recorded variables
    self._monitor = None

    # Is overwritten by SpecificPopulations
    self._specific_template = {}

    # Storage order. TODO: why?
    self._storage_order = storage_order

clear() #

Clears all spiking events previously emitted (history of spikes, delayed spikes).

Can be useful if you do not want to totally reset a population (i.e. all variables), only to clear the spiking history between two trials.

Note: does nothing for rate-coded networks.

Source code in ANNarchy/core/Population.py
266
267
268
269
270
271
272
273
274
def clear(self):
    """
    Clears all spiking events previously emitted (history of spikes, delayed spikes).

    Can be useful if you do not want to totally reset a population (i.e. all variables), only to clear the spiking history between two trials.

    Note: does nothing for rate-coded networks.
    """
    self.cyInstance.reset()

compute_firing_rate(window) #

Tells spiking neurons in the population to compute their mean firing rate over the given window and store the values in the variable r.

This method has an effect on spiking neurons only.

If this method is not called, r will always be 0.0. r can of course be accessed and recorded as any other variable.

Parameters:

  • window

    window in ms over which the spikes will be counted.

Source code in ANNarchy/core/Population.py
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
def compute_firing_rate(self, window):
    """
    Tells spiking neurons in the population to compute their mean firing rate over the given window and store the values in the variable `r`.

    This method has an effect on spiking neurons only.

    If this method is not called, `r` will always be 0.0. `r` can of course be accessed and recorded as any other variable.

    :param window: window in ms over which the spikes will be counted.
    """
    if Global._check_paradigm('cuda'):
        Global._warning('compute_firing_rate() is currently being evaluated on the host-side, so may be slow ... ')

    if self.neuron_type.type == 'rate':
        Global._error('compute_firing_rate(): the neuron is already rate-coded...')

    self._compute_mean_fr = float(window)

    if self.initialized:
        getattr(self.cyInstance, 'compute_firing_rate')(self._compute_mean_fr)

coordinates_from_rank(rank) #

Returns the coordinates of a neuron based on its rank.

Parameters:

  • rank

    rank of the neuron.

Source code in ANNarchy/core/Population.py
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
def coordinates_from_rank(self, rank):
    """
    Returns the coordinates of a neuron based on its rank.

    :param rank: rank of the neuron.
    """
    # Check the rank
    if not rank < self.size:
        Global._error('The given rank', str(rank), 'is larger than the size of the population', str(self.size) + '.')

    try:
        coord = self._coord_from_rank( rank, self.geometry )
    except:
        Global._error('The given rank', str(rank), 'is larger than the size of the population', str(self.size) + '.')
    else:
        return coord

disable() #

Temporarily disables computations in this population (including the projections leading to it).

You can re-enable it with the enable() method.

Source code in ANNarchy/core/Population.py
286
287
288
289
290
291
292
293
294
def disable(self):
    """
    Temporarily disables computations in this population (including the projections leading to it).

    You can re-enable it with the ``enable()`` method.
    """
    if self.initialized:
        self.cyInstance.activate(False)
    self.enabled = False

enable() #

(Re)-enables computations in this population, after they were disabled by the disable() method.

The status of the population is accessible through the enabled flag.

Source code in ANNarchy/core/Population.py
276
277
278
279
280
281
282
283
284
def enable(self):
    """
    (Re)-enables computations in this population, after they were disabled by the ``disable()`` method.

    The status of the population is accessible through the ``enabled`` flag.
    """
    if self.initialized:
        self.cyInstance.activate(True)
    self.enabled = True

get(name) #

Returns the value of neural variables and parameters.

Parameters:

  • name

    attribute name as a string.

Source code in ANNarchy/core/Population.py
413
414
415
416
417
418
419
def get(self, name):
    """
    Returns the value of neural variables and parameters.

    :param name: attribute name as a string.
    """
    return self.__getattr__(name)

load(filename, pickle_encoding=None) #

Load the saved state of the population by Population.save().

Warning: Matlab data can not be loaded.

Example:

pop.load('pop1.npz')
pop.load('pop1.txt')
pop.load('pop1.txt.gz')

Parameters:

  • filename

    the filename with relative or absolute path.

Source code in ANNarchy/core/Population.py
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
def load(self, filename, pickle_encoding=None):
    """
    Load the saved state of the population by `Population.save()`.

    Warning: Matlab data can not be loaded.

    Example:

    ```python
    pop.load('pop1.npz')
    pop.load('pop1.txt')
    pop.load('pop1.txt.gz')
    ```

    :param filename: the filename with relative or absolute path.

    """
    from ANNarchy.core.IO import _load_data
    self._load_pop_data(_load_data(filename, pickle_encoding))

neuron(*coord) #

Returns an IndividualNeuron object wrapping the neuron with the provided rank or coordinates.

Source code in ANNarchy/core/Population.py
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
def neuron(self, *coord):
    """
    Returns an ``IndividualNeuron`` object wrapping the neuron with the provided rank or coordinates.
    """
    # Transform arguments
    if len(coord) == 1:
        if isinstance(coord[0], int):
            rank = coord[0]
            if not rank < self.size:
                Global._error(' when accessing neuron', str(rank), ': the population', self.name, 'has only', self.size, 'neurons (geometry '+ str(self.geometry) +').')
        else:
            rank = self.rank_from_coordinates( coord[0] )
            if rank is None:
                return None
    else: # a tuple
        rank = self.rank_from_coordinates( coord )
        if rank is None:
            return None

    # Return corresponding neuron
    return IndividualNeuron(self, rank)

normalized_coordinates_from_rank(rank, norm=1.0) #

Returns normalized coordinates of a neuron based on its rank. The geometry of the population is mapped to the hypercube \([0, 1]^d\)

Parameters:

  • rank

    rank of the neuron

  • norm

    norm of the cube (default = 1.0)

Source code in ANNarchy/core/Population.py
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
def normalized_coordinates_from_rank(self, rank, norm=1.):
    """
    Returns normalized coordinates of a neuron based on its rank. 
    The geometry of the population is mapped to the hypercube $[0, 1]^d$

    :param rank: rank of the neuron
    :param norm: norm of the cube (default = 1.0)

    """
    try:
        normal = self._norm_coord_dict[self.dimension](rank, self.geometry)
    except KeyError:
        coord = self.coordinates_from_rank(rank)

        normal = tuple()
        for dim in range(self.dimension):
            if self._geometry[dim] > 1:
                normal += ( norm * float(coord[dim])/float(self.geometry[dim]-1), )
            else:
                normal += (float(rank)/(float(self.size)-1.0),) # default?

    return normal

rank_from_coordinates(coord) #

Returns the rank of a neuron based on coordinates.

Parameters:

  • coord

    coordinate tuple, can be multidimensional.

Source code in ANNarchy/core/Population.py
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
def rank_from_coordinates(self, coord):
    """
    Returns the rank of a neuron based on coordinates.

    :param coord: coordinate tuple, can be multidimensional.
    """
    try:
        rank = self._rank_from_coord( coord, self.geometry )
    except:
        Global._error('rank_from_coordinates(): There is no neuron of coordinates', coord, 'in the population', self.name, self.geometry)

    if rank > self.size:
        Global._error('rank_from_coordinates(), neuron', str(coord), ': the population' , self.name , 'has only', self.size, 'neurons (geometry '+ str(self.geometry) +').')
    else:
        return rank

reset(attributes=-1) #

Resets all parameters and variables of the population to the value they had before the call to compile().

Parameters:

  • attributes

    list of attributes (parameter or variable) which should be reinitialized. Default: all attributes.

Source code in ANNarchy/core/Population.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def reset(self, attributes=-1):
    """
    Resets all parameters and variables of the population to the value they had before the call to compile().

    :param attributes: list of attributes (parameter or variable) which should be reinitialized. Default: all attributes.
    """
    if attributes == -1:
        try:
            self.set(self.init)
        except Exception as e:
            Global._print(e)
            Global._error("Population.reset(): something went wrong while resetting.")
    else: # only some of them
        for var in attributes:
            # check it exists
            if not var in self.attributes:
                Global._warning("Population.reset():", var, "is not an attribute of the population, skipping.")
                continue

            try:
                self.__setattr__(var, self.init[var])
            except Exception as e:
                Global._print(e)
                Global._warning("Population.reset(): something went wrong while resetting", var)

    self.cyInstance.activate(self.enabled)
    self.cyInstance.reset()

save(filename) #

Saves all information about the population (structure, current value of parameters and variables) into a file.

  • If the file name is '.npz', the data will be saved and compressed using np.savez_compressed (recommended).

  • If the file name ends with '.gz', the data will be pickled into a binary file and compressed using gzip.

  • If the file name is '.mat', the data will be saved as a Matlab 7.2 file. Scipy must be installed.

  • Otherwise, the data will be pickled into a simple binary text file using pickle.

Warning: The '.mat' data will not be loadable by ANNarchy, it is only for external analysis purpose.

Parameters:

  • filename

    filename, may contain relative or absolute path. Example: python pop.save('pop1.npz') pop.save('pop1.txt') pop.save('pop1.txt.gz') pop.save('pop1.mat')

Source code in ANNarchy/core/Population.py
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
def save(self, filename):
    """
    Saves all information about the population (structure, current value of parameters and variables) into a file.

    * If the file name is '.npz', the data will be saved and compressed using `np.savez_compressed` (recommended).

    * If the file name ends with '.gz', the data will be pickled into a binary file and compressed using gzip.

    * If the file name is '.mat', the data will be saved as a Matlab 7.2 file. Scipy must be installed.

    * Otherwise, the data will be pickled into a simple binary text file using pickle.

    **Warning:** The '.mat' data will not be loadable by ANNarchy, it is only for external analysis purpose.

    :param filename: filename, may contain relative or absolute path.

    Example:

    ```python
    pop.save('pop1.npz')
    pop.save('pop1.txt')
    pop.save('pop1.txt.gz')
    pop.save('pop1.mat')
    ```

    """
    from ANNarchy.core.IO import _save_data
    _save_data(filename, self._data())

set(values) #

Sets the value of neural variables and parameters.

Example:

pop.set({ 'tau' : 20.0, 'r'= np.random.rand((8,8)) } )

Parameters:

  • values

    dictionary of attributes to be updated.

Source code in ANNarchy/core/Population.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
def set(self, values):
    """
    Sets the value of neural variables and parameters.

    Example:

    ```python
    pop.set({ 'tau' : 20.0, 'r'= np.random.rand((8,8)) } )
    ```

    :param values: dictionary of attributes to be updated.

    """
    for name, value in values.items():
        self.__setattr__(name, value)

sum(target) #

Returns the array of weighted sums corresponding to the target:

excitatory = pop.sum('exc')

For spiking networks, this is equivalent to accessing the conductances directly:

excitatory = pop.g_exc

If no incoming projection has the given target, the method returns zeros.

Note: it is not possible to distinguish the original population when the same target is used.

Parameters:

  • target

    the desired projection target.

Source code in ANNarchy/core/Population.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
def sum(self, target):
    """
    Returns the array of weighted sums corresponding to the target:

    ```python
    excitatory = pop.sum('exc')
    ```

    For spiking networks, this is equivalent to accessing the conductances directly:

    ```python
    excitatory = pop.g_exc
    ```

    If no incoming projection has the given target, the method returns zeros.

    **Note:** it is not possible to distinguish the original population when the same target is used.

    :param target: the desired projection target.
    """
    # Check if the network is initialized
    if not self.initialized:
        Global._warning('sum(): the population', self.name, 'is not initialized yet.')
        return np.zeros(self.geometry)
    # Check if a projection has this type
    if not target in self.targets:
        Global._warning('sum(): the population', self.name, 'receives no projection with the target', target)
        return np.zeros(self.geometry)
    # Spiking neurons already have conductances available
    if self.neuron_type.type == 'spike':
        return getattr(self, 'g_'+target)
    # Otherwise, call the Cython method
    return self.cyInstance.get_local_attribute_all("_sum_"+target, Global.config["precision"])