class SpikeGeneratorGroup(Group, CodeRunner, SpikeSource): ''' SpikeGeneratorGroup(N, indices, times, dt=None, clock=None, period=0*second, when='thresholds', order=0, sorted=False, name='spikegeneratorgroup*', codeobj_class=None) A group emitting spikes at given times. Parameters ---------- N : int The number of "neurons" in this group indices : array of integers The indices of the spiking cells times : `Quantity` The spike times for the cells given in ``indices``. Has to have the same length as ``indices``. period : `Quantity`, optional If this is specified, it will repeat spikes with this period. A period of 0s means not repeating spikes. dt : `Quantity`, optional The time step to be used for the simulation. Cannot be combined with the `clock` argument. clock : `Clock`, optional The update clock to be used. If neither a clock, nor the `dt` argument is specified, the `defaultclock` will be used. when : str, optional When to run within a time step, defaults to the ``'thresholds'`` slot. order : int, optional The priority of of this group for operations occurring at the same time step and in the same scheduling slot. Defaults to 0. sorted : bool, optional Whether the given indices and times are already sorted. Set to ``True`` if your events are already sorted (first by spike time, then by index), this can save significant time at construction if your arrays contain large numbers of spikes. Defaults to ``False``. Notes ----- * If `sorted` is set to ``True``, the given arrays will not be copied (only affects runtime mode).. ''' @check_units(N=1, indices=1, times=second, period=second) def __init__(self, N, indices, times, dt=None, clock=None, period=0*second, when='thresholds', order=0, sorted=False, name='spikegeneratorgroup*', codeobj_class=None): Group.__init__(self, dt=dt, clock=clock, when=when, order=order, name=name) # We store the indices and times also directly in the Python object, # this way we can use them for checks in `before_run` even in standalone # TODO: Remove this when the checks in `before_run` have been moved to the template #: Array of spiking neuron indices. self._neuron_index = None #: Array of spiking neuron times. self._spike_time = None #: "Dirty flag" that will be set when spikes are changed after the #: `before_run` check self._spikes_changed = True # Let other objects know that we emit spikes events self.events = {'spike': None} self.codeobj_class = codeobj_class if N < 1 or int(N) != N: raise TypeError('N has to be an integer >=1.') N = int(N) self.start = 0 self.stop = N self.variables = Variables(self) self.variables.create_clock_variables(self._clock) indices, times = self._check_args(indices, times, period, N, sorted, self._clock.dt) self.variables.add_constant('N', value=N) self.variables.add_array('period', dimensions=second.dim, size=1, constant=True, read_only=True, scalar=True, dtype=self._clock.variables['t'].dtype) self.variables.add_arange('i', N) self.variables.add_dynamic_array('spike_number', values=np.arange(len(indices)), size=len(indices), dtype=np.int32, read_only=True, constant=True, index='spike_number', unique=True) self.variables.add_dynamic_array('neuron_index', values=indices, size=len(indices), dtype=np.int32, index='spike_number', read_only=True, constant=True) self.variables.add_dynamic_array('spike_time', values=times, size=len(times), dimensions=second.dim, index='spike_number', read_only=True, constant=True, dtype=self._clock.variables['t'].dtype) self.variables.add_dynamic_array('_timebins', size=len(times), index='spike_number', read_only=True, constant=True, dtype=np.int32) self.variables.add_array('_period_bins', size=1, constant=True, read_only=True, scalar=True, dtype=np.int32) self.variables.add_array('_spikespace', size=N+1, dtype=np.int32) self.variables.add_array('_lastindex', size=1, values=0, dtype=np.int32, read_only=True, scalar=True) #: Remember the dt we used the last time when we checked the spike bins #: to not repeat the work for multiple runs with the same dt self._previous_dt = None CodeRunner.__init__(self, self, code='', template='spikegenerator', clock=self._clock, when=when, order=order, name=None) # Activate name attribute access self._enable_group_attributes() self.variables['period'].set_value(period) def _full_state(self): state = super(SpikeGeneratorGroup, self)._full_state() # Store the internal information we use to decide whether to rebuild # the time bins state['_previous_dt'] = self._previous_dt state['_spikes_changed'] = self._spikes_changed return state def _restore_from_full_state(self, state): state = state.copy() # copy to avoid errors for multiple restores self._previous_dt = state.pop('_previous_dt') self._spikes_changed = state.pop('_spikes_changed') super(SpikeGeneratorGroup, self)._restore_from_full_state(state) def before_run(self, run_namespace): # Do some checks on the period vs. dt dt = self.dt_[:] # make a copy period = self.period_ if period < np.inf and period != 0: if period < dt: raise ValueError('The period of %s is %s, which is smaller ' 'than its dt of %s.' % (self.name, self.period[:], dt*second)) if self._spikes_changed: current_t = self.variables['t'].get_value().item() timesteps = timestep(self._spike_time, dt) current_step = timestep(current_t, dt) in_the_past = np.nonzero(timesteps < current_step)[0] if len(in_the_past): logger.warn('The SpikeGeneratorGroup contains spike times ' 'earlier than the start time of the current run ' '(t = {}), these spikes will be ' 'ignored.'.format(str(current_t*second)), name_suffix='ignored_spikes') self.variables['_lastindex'].set_value(in_the_past[-1] + 1) else: self.variables['_lastindex'].set_value(0) # Check that we don't have more than one spike per neuron in a time bin if self._previous_dt is None or dt != self._previous_dt or self._spikes_changed: # We shift all the spikes by a tiny amount to make sure that spikes # at exact multiples of dt do not end up in the previous time bin # This shift has to be quite significant relative to machine # epsilon, we use 1e-3 of the dt here shift = 1e-3*dt timebins = np.asarray(np.asarray(self._spike_time + shift)/dt, dtype=np.int32) # time is already in sorted order, so it's enough to check if the condition # that timebins[i]==timebins[i+1] and self._neuron_index[i]==self._neuron_index[i+1] # is ever both true if (np.logical_and(np.diff(timebins)==0, np.diff(self._neuron_index)==0)).any(): raise ValueError('Using a dt of %s, some neurons of ' 'SpikeGeneratorGroup "%s" spike more than ' 'once during a time step.' % (str(self.dt), self.name)) self.variables['_timebins'].set_value(timebins) period_bins = np.round(period / dt) max_int = np.iinfo(np.int32).max if period_bins > max_int: logger.warn('Periods longer than {} timesteps (={}) are not ' 'supported, the period will therefore be ' 'considered infinite. Set the period to 0*second ' 'to avoid this ' 'warning.'.format(max_int, str(max_int*dt*second)), 'spikegenerator_long_period') period = period_bins = 0 if np.abs(period_bins * dt - period) > period * np.finfo(dt.dtype).eps: raise NotImplementedError('The period of %s is %s, which is ' 'not an integer multiple of its dt ' 'of %s.' % (self.name, self.period[:], dt * second)) self.variables['_period_bins'].set_value(period_bins) self._previous_dt = dt self._spikes_changed = False super(SpikeGeneratorGroup, self).before_run(run_namespace=run_namespace) @check_units(indices=1, times=second, period=second) def set_spikes(self, indices, times, period=0*second, sorted=False): ''' set_spikes(indices, times, period=0*second, sorted=False) Change the spikes that this group will generate. This can be used to set the input for a second run of a model based on the output of a first run (if the input for the second run is already known before the first run, then all the information should simply be included in the initial `SpikeGeneratorGroup` initializer call, instead). Parameters ---------- indices : array of integers The indices of the spiking cells times : `Quantity` The spike times for the cells given in ``indices``. Has to have the same length as ``indices``. period : `Quantity`, optional If this is specified, it will repeat spikes with this period. A period of 0s means not repeating spikes. sorted : bool, optional Whether the given indices and times are already sorted. Set to ``True`` if your events are already sorted (first by spike time, then by index), this can save significant time at construction if your arrays contain large numbers of spikes. Defaults to ``False``. ''' indices, times = self._check_args(indices, times, period, self.N, sorted, self.dt) self.variables['period'].set_value(period) self.variables['neuron_index'].resize(len(indices)) self.variables['spike_time'].resize(len(indices)) self.variables['spike_number'].resize(len(indices)) self.variables['spike_number'].set_value(np.arange(len(indices))) self.variables['_timebins'].resize(len(indices)) self.variables['neuron_index'].set_value(indices) self.variables['spike_time'].set_value(times) # _lastindex and _timebins will be set as part of before_run def _check_args(self, indices, times, period, N, sorted, dt): times = Quantity(times) if len(indices) != len(times): raise ValueError(('Length of the indices and times array must ' 'match, but %d != %d') % (len(indices), len(times))) if period < 0*second: raise ValueError('The period cannot be negative.') elif len(times) and period != 0*second: period_bins = np.round(period / dt) # Note: we have to use the timestep function here, to use the same # binning as in the actual simulation max_bin = timestep(np.max(times), dt) if max_bin >= period_bins: raise ValueError('The period has to be greater than the ' 'maximum of the spike times') if len(times) and np.min(times) < 0*second: raise ValueError('Spike times cannot be negative') if len(indices) and (np.min(indices) < 0 or np.max(indices) >= N): raise ValueError('Indices have to lie in the interval [0, %d[' % N) times = np.asarray(times) indices = np.asarray(indices) if not sorted: # sort times and indices first by time, then by indices I = np.lexsort((indices, times)) indices = indices[I] times = times[I] # We store the indices and times also directly in the Python object, # this way we can use them for checks in `before_run` even in standalone # TODO: Remove this when the checks in `before_run` have been moved to the template self._neuron_index = indices self._spike_time = times self._spikes_changed = True return indices, times @property def spikes(self): ''' The spikes returned by the most recent thresholding operation. ''' # Note that we have to directly access the ArrayVariable object here # instead of using the Group mechanism by accessing self._spikespace # Using the latter would cut _spikespace to the length of the group spikespace = self.variables['_spikespace'].get_value() return spikespace[:spikespace[-1]] def __repr__(self): return ('{cls}({N}, indices=<length {l} array>, ' 'times=<length {l} array>').format(cls=self.__class__.__name__, N=self.N, l=self.variables['neuron_index'].size)
class PoissonGroup(Group, SpikeSource): ''' Poisson spike source Parameters ---------- N : int Number of neurons rates : `Quantity`, str Single rate, array of rates of length N, or a string expression evaluating to a rate. This string expression will be evaluated at every time step, it can therefore be time-dependent (e.g. refer to a `TimedArray`). dt : `Quantity`, optional The time step to be used for the simulation. Cannot be combined with the `clock` argument. clock : `Clock`, optional The update clock to be used. If neither a clock, nor the `dt` argument is specified, the `defaultclock` will be used. when : str, optional When to run within a time step, defaults to the ``'thresholds'`` slot. order : int, optional The priority of of this group for operations occurring at the same time step and in the same scheduling slot. Defaults to 0. name : str, optional Unique name, or use poissongroup, poissongroup_1, etc. ''' add_to_magic_network = True @check_units(rates=Hz) def __init__(self, N, rates, dt=None, clock=None, when='thresholds', order=0, namespace=None, name='poissongroup*', codeobj_class=None): if namespace is None: namespace = {} #: The group-specific namespace self.namespace = namespace Group.__init__(self, dt=dt, clock=clock, when=when, order=order, name=name) self.codeobj_class = codeobj_class self._N = N = int(N) # TODO: In principle, it would be nice to support Poisson groups with # refactoriness, but we can't currently, since the refractoriness # information is reset in the state updater which we are not using # We could either use a specific template or simply not bother and make # users write their own NeuronGroup (with threshold rand() < rates*dt) # for more complex use cases. self.variables = Variables(self) # standard variables self.variables.add_constant('N', value=self._N) self.variables.add_arange('i', self._N, constant=True, read_only=True) self.variables.add_array('_spikespace', size=N + 1, dtype=np.int32) self.variables.create_clock_variables(self._clock) # The firing rates if isinstance(rates, str): self.variables.add_subexpression('rates', dimensions=Hz.dim, expr=rates) else: self.variables.add_array('rates', size=N, dimensions=Hz.dim) self._rates = rates self.start = 0 self.stop = N self._refractory = False self.events = {'spike': 'rand() < rates * dt'} self.thresholder = {'spike': Thresholder(self)} self.contained_objects.append(self.thresholder['spike']) self._enable_group_attributes() if not isinstance(rates, str): self.rates = rates def __getitem__(self, item): if not isinstance(item, slice): raise TypeError( 'Subgroups can only be constructed using slicing syntax') start, stop, step = item.indices(self._N) if step != 1: raise IndexError('Subgroups have to be contiguous') if start >= stop: raise IndexError('Illegal start/end values for subgroup, %d>=%d' % (start, stop)) return Subgroup(self, start, stop) def before_run(self, run_namespace=None): rates_var = self.variables['rates'] if isinstance(rates_var, Subexpression): # Check that the units of the expression make sense expr = rates_var.expr identifiers = get_identifiers(expr) variables = self.resolve_all(identifiers, run_namespace, user_identifiers=identifiers) unit = parse_expression_dimensions(rates_var.expr, variables) fail_for_dimension_mismatch( unit, Hz, "The expression provided for " "PoissonGroup's 'rates' " "argument, has to have units " "of Hz") super(PoissonGroup, self).before_run(run_namespace) @property def spikes(self): ''' The spikes returned by the most recent thresholding operation. ''' # Note that we have to directly access the ArrayVariable object here # instead of using the Group mechanism by accessing self._spikespace # Using the latter would cut _spikespace to the length of the group spikespace = self.variables['_spikespace'].get_value() return spikespace[:spikespace[-1]] def __repr__(self): description = '{classname}({N}, rates={rates})' return description.format(classname=self.__class__.__name__, N=self.N, rates=repr(self._rates))
class PopulationRateMonitor(Group, CodeRunner): ''' Record instantaneous firing rates, averaged across neurons from a `NeuronGroup` or other spike source. Parameters ---------- source : (`NeuronGroup`, `SpikeSource`) The source of spikes to record. name : str, optional A unique name for the object, otherwise will use ``source.name+'_ratemonitor_0'``, etc. codeobj_class : class, optional The `CodeObject` class to run code with. dtype : dtype, optional The dtype to use to store the ``rate`` variable. Defaults to `~numpy.float64`, i.e. double precision. Notes ----- Currently, this monitor can only monitor the instantaneous firing rates at each time step of the source clock. Any binning/smoothing of the firing rates has to be done manually afterwards. ''' invalidates_magic_network = False add_to_magic_network = True def __init__(self, source, name='ratemonitor*', codeobj_class=None, dtype=np.float64): #: The group we are recording from self.source = source self.codeobj_class = codeobj_class CodeRunner.__init__(self, group=self, code='', template='ratemonitor', clock=source.clock, when='end', order=0, name=name) self.add_dependency(source) self.variables = Variables(self) # Handle subgroups correctly start = getattr(source, 'start', 0) stop = getattr(source, 'stop', len(source)) self.variables.add_constant('_source_start', start) self.variables.add_constant('_source_stop', stop) self.variables.add_reference('_spikespace', source) self.variables.add_dynamic_array('rate', size=0, dimensions=hertz.dim, read_only=True, dtype=dtype) self.variables.add_dynamic_array( 't', size=0, dimensions=second.dim, read_only=True, dtype=self._clock.variables['t'].dtype) self.variables.add_reference('_num_source_neurons', source, 'N') self.variables.add_array('N', dtype=np.int32, size=1, scalar=True, read_only=True) self.variables.create_clock_variables(self._clock, prefix='_clock_') self._enable_group_attributes() def resize(self, new_size): # Note that this does not set N, this has to be done in the template # since we use a restricted pointer to access it (which promises that # we only change the value through this pointer) self.variables['rate'].resize(new_size) self.variables['t'].resize(new_size) def reinit(self): ''' Clears all recorded rates ''' raise NotImplementedError() @check_units(width=second) def smooth_rate(self, window='gaussian', width=None): ''' smooth_rate(self, window='gaussian', width=None) Return a smooth version of the population rate. Parameters ---------- window : str, ndarray The window to use for smoothing. Can be a string to chose a predefined window(``'flat'`` for a rectangular, and ``'gaussian'`` for a Gaussian-shaped window). In this case the width of the window is determined by the ``width`` argument. Note that for the Gaussian window, the ``width`` parameter specifies the standard deviation of the Gaussian, the width of the actual window is ``4*width + dt`` (rounded to the nearest dt). For the flat window, the width is rounded to the nearest odd multiple of dt to avoid shifting the rate in time. Alternatively, an arbitrary window can be given as a numpy array (with an odd number of elements). In this case, the width in units of time depends on the ``dt`` of the simulation, and no ``width`` argument can be specified. The given window will be automatically normalized to a sum of 1. width : `Quantity`, optional The width of the ``window`` in seconds (for a predefined window). Returns ------- rate : `Quantity` The population rate in Hz, smoothed with the given window. Note that the rates are smoothed and not re-binned, i.e. the length of the returned array is the same as the length of the ``rate`` attribute and can be plotted against the `PopulationRateMonitor` 's ``t`` attribute. ''' if width is None and isinstance(window, str): raise TypeError('Need a width when using a predefined window.') if width is not None and not isinstance(window, str): raise TypeError('Can only specify a width for a predefined window') if isinstance(window, str): if window == 'gaussian': width_dt = int(np.round(2 * width / self.clock.dt)) # Rounding only for the size of the window, not for the standard # deviation of the Gaussian window = np.exp(-np.arange(-width_dt, width_dt + 1)**2 * 1. / (2 * (width / self.clock.dt)**2)) elif window == 'flat': width_dt = int(width / 2 / self.clock.dt) * 2 + 1 used_width = width_dt * self.clock.dt if abs(used_width - width) > 1e-6 * self.clock.dt: logger.info('width adjusted from %s to %s' % (width, used_width), 'adjusted_width', once=True) window = np.ones(width_dt) else: raise NotImplementedError('Unknown pre-defined window "%s"' % window) else: try: window = np.asarray(window) except TypeError: raise TypeError('Cannot use a window of type %s' % type(window)) if window.ndim != 1: raise TypeError('The provided window has to be ' 'one-dimensional.') if len(window) % 2 != 1: raise TypeError('The window has to have an odd number of ' 'values.') return Quantity(np.convolve(self.rate_, window * 1. / sum(window), mode='same'), dim=hertz.dim) def __repr__(self): description = '<{classname}, recording {source}>' return description.format(classname=self.__class__.__name__, source=self.source.name)
class Subgroup(Group, SpikeSource): ''' Subgroup of any `Group` Parameters ---------- source : SpikeSource The source object to subgroup. start, stop : int Select only spikes with indices from ``start`` to ``stop-1``. name : str, optional A unique name for the group, or use ``source.name+'_subgroup_0'``, etc. ''' def __init__(self, source, start, stop, name=None): # First check if the source is itself a Subgroup # If so, then make this a Subgroup of the original Group if isinstance(source, Subgroup): source = source.source start = start + source.start stop = stop + source.start self.source = source else: self.source = weakproxy_with_fallback(source) # Store a reference to the source's equations (if any) self.equations = None if hasattr(self.source, 'equations'): self.equations = weakproxy_with_fallback(self.source.equations) if name is None: name = source.name + '_subgroup*' # We want to update the spikes attribute after it has been updated # by the parent, we do this in slot 'thresholds' with an order # one higher than the parent order to ensure it takes place after the # parent threshold operation Group.__init__(self, clock=source._clock, when='thresholds', order=source.order+1, name=name) self._N = stop-start self.start = start self.stop = stop self.events = self.source.events # All the variables have to go via the _sub_idx to refer to the # appropriate values in the source group self.variables = Variables(self, default_index='_sub_idx') # overwrite the meaning of N and i if self.start > 0: self.variables.add_constant('_offset', value=self.start) self.variables.add_reference('_source_i', source, 'i') self.variables.add_subexpression('i', dtype=source.variables['i'].dtype, expr='_source_i - _offset', index='_idx') else: # no need to calculate anything if this is a subgroup starting at 0 self.variables.add_reference('i', source) self.variables.add_constant('N', value=self._N) self.variables.add_constant('_source_N', value=len(source)) # add references for all variables in the original group self.variables.add_references(source, list(source.variables.keys())) # Only the variable _sub_idx itself is stored in the subgroup # and needs the normal index for this group self.variables.add_arange('_sub_idx', size=self._N, start=self.start, index='_idx') # special indexing for subgroups self._indices = Indexing(self, self.variables['_sub_idx']) # Deal with special indices for key, value in self.source.variables.indices.items(): if value == '0': self.variables.indices[key] = '0' elif value == '_idx': continue # nothing to do, already uses _sub_idx correctly else: raise ValueError(('Do not know how to deal with variable %s ' 'using index %s in a subgroup') % (key, value)) self.namespace = self.source.namespace self.codeobj_class = self.source.codeobj_class self._enable_group_attributes() spikes = property(lambda self: self.source.spikes) def __getitem__(self, item): if not isinstance(item, slice): raise TypeError('Subgroups can only be constructed using slicing syntax') start, stop, step = item.indices(self._N) if step != 1: raise IndexError('Subgroups have to be contiguous') if start >= stop: raise IndexError('Illegal start/end values for subgroup, %d>=%d' % (start, stop)) return Subgroup(self.source, self.start + start, self.start + stop) def __repr__(self): description = '<{classname} {name} of {source} from {start} to {end}>' return description.format(classname=self.__class__.__name__, name=repr(self.name), source=repr(self.source.name), start=self.start, end=self.stop)
class Clock(VariableOwner): ''' An object that holds the simulation time and the time step. Parameters ---------- dt : float The time step of the simulation as a float name : str, optional An explicit name, if not specified gives an automatically generated name Notes ----- Clocks are run in the same `Network.run` iteration if `~Clock.t` is the same. The condition for two clocks to be considered as having the same time is ``abs(t1-t2)<epsilon*abs(t1)``, a standard test for equality of floating point values. The value of ``epsilon`` is ``1e-14``. ''' def __init__(self, dt, name='clock*'): # We need a name right away because some devices (e.g. cpp_standalone) # need a name for the object when creating the variables Nameable.__init__(self, name=name) self._old_dt = None self.variables = Variables(self) self.variables.add_array('timestep', size=1, dtype=np.int64, read_only=True, scalar=True) self.variables.add_array('t', dimensions=second.dim, size=1, dtype=np.double, read_only=True, scalar=True) self.variables.add_array('dt', dimensions=second.dim, size=1, values=float(dt), dtype=np.float, read_only=True, constant=True, scalar=True) self.variables.add_constant('N', value=1) self._enable_group_attributes() self.dt = dt logger.diagnostic("Created clock {name} with dt={dt}".format( name=self.name, dt=self.dt)) @check_units(t=second) def _set_t_update_dt(self, target_t=0 * second): new_dt = self.dt_ old_dt = self._old_dt target_t = float(target_t) if old_dt is not None and new_dt != old_dt: self._old_dt = None # Only allow a new dt which allows to correctly set the new time step check_dt(new_dt, old_dt, target_t) new_timestep = self._calc_timestep(target_t) # Since these attributes are read-only for normal users, we have to # update them via the variables object directly self.variables['timestep'].set_value(new_timestep) self.variables['t'].set_value(new_timestep * new_dt) logger.diagnostic("Setting Clock {name} to t={t}, dt={dt}".format( name=self.name, t=self.t, dt=self.dt)) def _calc_timestep(self, target_t): ''' Calculate the integer time step for the target time. If it cannot be exactly represented (up to 0.01% of dt), round up. Parameters ---------- target_t : float The target time in seconds Returns ------- timestep : int The target time in integers (based on dt) ''' new_i = np.int64(np.round(target_t / self.dt_)) new_t = new_i * self.dt_ if (new_t == target_t or np.abs(new_t - target_t) / self.dt_ <= Clock.epsilon_dt): new_timestep = new_i else: new_timestep = np.int64(np.ceil(target_t / self.dt_)) return new_timestep def __repr__(self): return 'Clock(dt=%r, name=%r)' % (self.dt, self.name) def _get_dt_(self): return self.variables['dt'].get_value().item() @check_units(dt_=1) def _set_dt_(self, dt_): self._old_dt = self._get_dt_() self.variables['dt'].set_value(dt_) @check_units(dt=second) def _set_dt(self, dt): self._set_dt_(float(dt)) dt = property( fget=lambda self: Quantity(self.dt_, dim=second.dim), fset=_set_dt, doc='''The time step of the simulation in seconds.''', ) dt_ = property( fget=_get_dt_, fset=_set_dt_, doc='''The time step of the simulation as a float (in seconds)''') @check_units(start=second, end=second) def set_interval(self, start, end): ''' set_interval(self, start, end) Set the start and end time of the simulation. Sets the start and end value of the clock precisely if possible (using epsilon) or rounding up if not. This assures that multiple calls to `Network.run` will not re-run the same time step. ''' self._set_t_update_dt(target_t=start) end = float(end) self._i_end = self._calc_timestep(end) if self._i_end > 2**40: logger.warn( 'The end time of the simulation has been set to {}, ' 'which based on the dt value of {} means that {} ' 'time steps will be simulated. This can lead to ' 'numerical problems, e.g. the times t will not ' 'correspond to exact multiples of ' 'dt.'.format(str(end * second), str(self.dt), self._i_end), 'many_timesteps') #: The relative difference for times (in terms of dt) so that they are #: considered identical. epsilon_dt = 1e-4
class EventMonitor(Group, CodeRunner): ''' Record events from a `NeuronGroup` or another event source. The recorded events can be accessed in various ways: the attributes `~EventMonitor.i` and `~EventMonitor.t` store all the indices and event times, respectively. Alternatively, you can get a dictionary mapping neuron indices to event trains, by calling the `event_trains` method. Parameters ---------- source : `NeuronGroup`, `SpikeSource` The source of events to record. event : str The name of the event to record variables : str or sequence of str, optional Which variables to record at the time of the event (in addition to the index of the neuron). Can be the name of a variable or a list of names. record : bool, optional Whether or not to record each event in `i` and `t` (the `count` will always be recorded). Defaults to ``True``. when : str, optional When to record the events, by default records events in the same slot where the event is emitted. order : int, optional The priority of of this group for operations occurring at the same time step and in the same scheduling slot. Defaults to the order where the event is emitted + 1, i.e. it will be recorded directly afterwards. name : str, optional A unique name for the object, otherwise will use ``source.name+'_eventmonitor_0'``, etc. codeobj_class : class, optional The `CodeObject` class to run code with. See Also -------- SpikeMonitor ''' invalidates_magic_network = False add_to_magic_network = True def __init__(self, source, event, variables=None, record=True, when=None, order=None, name='eventmonitor*', codeobj_class=None): if not isinstance(source, SpikeSource): raise TypeError( ('%s can only monitor groups producing spikes ' '(such as NeuronGroup), but the given argument ' 'is of type %s.') % (self.__class__.__name__, type(source))) #: The source we are recording from self.source = source #: Whether to record times and indices of events self.record = record #: The array of event counts (length = size of target group) self.count = None del self.count # this is handled by the Variable mechanism if when is None: if order is not None: raise ValueError( 'Cannot specify order if when is not specified.') if hasattr(source, 'thresholder'): parent_obj = source.thresholder[event] else: parent_obj = source when = parent_obj.when order = parent_obj.order + 1 elif order is None: order = 0 #: The event that we are listening to self.event = event if variables is None: variables = {} elif isinstance(variables, str): variables = {variables} #: The additional variables that will be recorded self.record_variables = set(variables) for variable in variables: if variable not in source.variables: raise ValueError(("'%s' is not a variable of the recorded " "group" % variable)) if self.record: self.record_variables |= {'i', 't'} # Some dummy code so that code generation takes care of the indexing # and subexpressions code = [ '_to_record_%s = _source_%s' % (v, v) for v in self.record_variables ] code = '\n'.join(code) self.codeobj_class = codeobj_class # Since this now works for general events not only spikes, we have to # pass the information about which variable to use to the template, # it can not longer simply refer to "_spikespace" eventspace_name = '_{}space'.format(event) # Handle subgroups correctly start = getattr(source, 'start', 0) stop = getattr(source, 'stop', len(source)) source_N = getattr(source, '_source_N', len(source)) Nameable.__init__(self, name=name) self.variables = Variables(self) self.variables.add_reference(eventspace_name, source) for variable in self.record_variables: source_var = source.variables[variable] self.variables.add_reference('_source_%s' % variable, source, variable) self.variables.add_auxiliary_variable('_to_record_%s' % variable, dimensions=source_var.dim, dtype=source_var.dtype) self.variables.add_dynamic_array(variable, size=0, dimensions=source_var.dim, dtype=source_var.dtype, read_only=True) self.variables.add_arange('_source_idx', size=len(source)) self.variables.add_array('count', size=len(source), dtype=np.int32, read_only=True, index='_source_idx') self.variables.add_constant('_source_start', start) self.variables.add_constant('_source_stop', stop) self.variables.add_constant('_source_N', source_N) self.variables.add_array('N', size=1, dtype=np.int32, read_only=True, scalar=True) record_variables = { varname: self.variables[varname] for varname in self.record_variables } template_kwds = { 'eventspace_variable': source.variables[eventspace_name], 'record_variables': record_variables, 'record': self.record } needed_variables = {eventspace_name} | self.record_variables CodeRunner.__init__( self, group=self, code=code, template='spikemonitor', name=None, # The name has already been initialized clock=source.clock, when=when, order=order, needed_variables=needed_variables, template_kwds=template_kwds) self.variables.create_clock_variables(self._clock, prefix='_clock_') self.add_dependency(source) self.written_readonly_vars = { self.variables[varname] for varname in self.record_variables } self._enable_group_attributes() def resize(self, new_size): # Note that this does not set N, this has to be done in the template # since we use a restricted pointer to access it (which promises that # we only change the value through this pointer) for variable in self.record_variables: self.variables[variable].resize(new_size) def reinit(self): ''' Clears all recorded spikes ''' raise NotImplementedError() @property def it(self): ''' Returns the pair (`i`, `t`). ''' if not self.record: raise AttributeError('Indices and times have not been recorded.' 'Set the record argument to True to record ' 'them.') return self.i, self.t @property def it_(self): ''' Returns the pair (`i`, `t_`). ''' if not self.record: raise AttributeError('Indices and times have not been recorded.' 'Set the record argument to True to record ' 'them.') return self.i, self.t_ def _values_dict(self, first_pos, sort_indices, used_indices, var): sorted_values = self.state(var, use_units=False)[sort_indices] dim = self.variables[var].dim event_values = {} current_pos = 0 # position in the all_indices array for idx in range(len(self.source)): if current_pos < len( used_indices) and used_indices[current_pos] == idx: if current_pos < len(used_indices) - 1: event_values[idx] = Quantity(sorted_values[ first_pos[current_pos]:first_pos[current_pos + 1]], dim=dim, copy=False) else: event_values[idx] = Quantity( sorted_values[first_pos[current_pos]:], dim=dim, copy=False) current_pos += 1 else: event_values[idx] = Quantity([], dim=dim) return event_values def values(self, var): ''' Return a dictionary mapping neuron indices to arrays of variable values at the time of the events (sorted by time). Parameters ---------- var : str The name of the variable. Returns ------- values : dict Dictionary mapping each neuron index to an array of variable values at the time of the events Examples -------- >>> from angela2 import * >>> G = NeuronGroup(2, """counter1 : integer ... counter2 : integer ... max_value : integer""", ... threshold='counter1 >= max_value', ... reset='counter1 = 0') >>> G.run_regularly('counter1 += 1; counter2 += 1') # doctest: +ELLIPSIS CodeRunner(...) >>> G.max_value = [50, 100] >>> mon = EventMonitor(G, event='spike', variables='counter2') >>> run(10*ms) >>> counter2_values = mon.values('counter2') >>> print(counter2_values[0]) [ 50 100] >>> print(counter2_values[1]) [100] ''' if not self.record: raise AttributeError('Indices and times have not been recorded.' 'Set the record argument to True to record ' 'them.') indices = self.i[:] # We have to make sure that the sort is stable, otherwise our spike # times do not necessarily remain sorted. sort_indices = np.argsort(indices, kind='mergesort') used_indices, first_pos = np.unique(self.i[:][sort_indices], return_index=True) return self._values_dict(first_pos, sort_indices, used_indices, var) def all_values(self): ''' Return a dictionary mapping recorded variable names (including ``t``) to a dictionary mapping neuron indices to arrays of variable values at the time of the events (sorted by time). This is equivalent to (but more efficient than) calling `values` for each variable and storing the result in a dictionary. Returns ------- all_values : dict Dictionary mapping variable names to dictionaries which themselves are mapping neuron indicies to arrays of variable values at the time of the events. Examples -------- >>> from angela2 import * >>> G = NeuronGroup(2, """counter1 : integer ... counter2 : integer ... max_value : integer""", ... threshold='counter1 >= max_value', ... reset='counter1 = 0') >>> G.run_regularly('counter1 += 1; counter2 += 1') # doctest: +ELLIPSIS CodeRunner(...) >>> G.max_value = [50, 100] >>> mon = EventMonitor(G, event='spike', variables='counter2') >>> run(10*ms) >>> all_values = mon.all_values() >>> print(all_values['counter2'][0]) [ 50 100] >>> print(all_values['t'][1]) [ 9.9] ms ''' if not self.record: raise AttributeError('Indices and times have not been recorded.' 'Set the record argument to True to record ' 'them.') indices = self.i[:] sort_indices = np.argsort(indices) used_indices, first_pos = np.unique(self.i[:][sort_indices], return_index=True) all_values_dict = {} for varname in self.record_variables - {'i'}: all_values_dict[varname] = self._values_dict( first_pos, sort_indices, used_indices, varname) return all_values_dict def event_trains(self): ''' Return a dictionary mapping event indices to arrays of event times. Equivalent to calling ``values('t')``. Returns ------- event_trains : dict Dictionary that stores an array with the event times for each neuron index. See Also -------- SpikeMonitor.spike_trains ''' return self.values('t') @property def num_events(self): ''' Returns the total number of recorded events. ''' return self.N[:] def __repr__(self): description = '<{classname}, recording event "{event}" from {source}>' return description.format(classname=self.__class__.__name__, event=self.event, source=self.group.name)
class NeuronGroup(Group, SpikeSource): ''' A group of neurons. Parameters ---------- N : int Number of neurons in the group. model : (str, `Equations`) The differential equations defining the group method : (str, function), optional The numerical integration method. Either a string with the name of a registered method (e.g. "euler") or a function that receives an `Equations` object and returns the corresponding abstract code. If no method is specified, a suitable method will be chosen automatically. threshold : str, optional The condition which produces spikes. Should be a single line boolean expression. reset : str, optional The (possibly multi-line) string with the code to execute on reset. refractory : {str, `Quantity`}, optional Either the length of the refractory period (e.g. ``2*ms``), a string expression that evaluates to the length of the refractory period after each spike (e.g. ``'(1 + rand())*ms'``), or a string expression evaluating to a boolean value, given the condition under which the neuron stays refractory after a spike (e.g. ``'v > -20*mV'``) events : dict, optional User-defined events in addition to the "spike" event defined by the ``threshold``. Has to be a mapping of strings (the event name) to strings (the condition) that will be checked. namespace: dict, optional A dictionary mapping identifier names to objects. If not given, the namespace will be filled in at the time of the call of `Network.run`, with either the values from the ``namespace`` argument of the `Network.run` method or from the local context, if no such argument is given. dtype : (`dtype`, `dict`), optional The `numpy.dtype` that will be used to store the values, or a dictionary specifying the type for variable names. If a value is not provided for a variable (or no value is provided at all), the preference setting `core.default_float_dtype` is used. codeobj_class : class, optional The `CodeObject` class to run code with. dt : `Quantity`, optional The time step to be used for the simulation. Cannot be combined with the `clock` argument. clock : `Clock`, optional The update clock to be used. If neither a clock, nor the `dt` argument is specified, the `defaultclock` will be used. order : int, optional The priority of of this group for operations occurring at the same time step and in the same scheduling slot. Defaults to 0. name : str, optional A unique name for the group, otherwise use ``neurongroup_0``, etc. Notes ----- `NeuronGroup` contains a `StateUpdater`, `Thresholder` and `Resetter`, and these are run at the 'groups', 'thresholds' and 'resets' slots (i.e. the values of their `when` attribute take these values). The `order` attribute will be passed down to the contained objects but can be set individually by setting the `order` attribute of the `state_updater`, `thresholder` and `resetter` attributes, respectively. ''' add_to_magic_network = True def __init__(self, N, model, method=('exact', 'euler', 'heun'), method_options=None, threshold=None, reset=None, refractory=False, events=None, namespace=None, dtype=None, dt=None, clock=None, order=0, name='neurongroup*', codeobj_class=None): Group.__init__(self, dt=dt, clock=clock, when='start', order=order, name=name) if dtype is None: dtype = {} if isinstance(dtype, MutableMapping): dtype['lastspike'] = self._clock.variables['t'].dtype self.codeobj_class = codeobj_class try: self._N = N = int(N) except ValueError: if isinstance(N, str): raise TypeError( "First NeuronGroup argument should be size, not equations." ) raise if N < 1: raise ValueError("NeuronGroup size should be at least 1, was " + str(N)) self.start = 0 self.stop = self._N ##### Prepare and validate equations if isinstance(model, str): model = Equations(model) if not isinstance(model, Equations): raise TypeError(('model has to be a string or an Equations ' 'object, is "%s" instead.') % type(model)) # Check flags model.check_flags({ DIFFERENTIAL_EQUATION: ('unless refractory', ), PARAMETER: ('constant', 'shared', 'linked'), SUBEXPRESSION: ('shared', 'constant over dt') }) # add refractoriness #: The original equations as specified by the user (i.e. without #: the multiplied `int(not_refractory)` term for equations marked as #: `(unless refractory)`) self.user_equations = model if refractory is not False: model = add_refractoriness(model) uses_refractoriness = len(model) and any([ 'unless refractory' in eq.flags for eq in model.values() if eq.type == DIFFERENTIAL_EQUATION ]) # Separate subexpressions depending whether they are considered to be # constant over a time step or not model, constant_over_dt = extract_constant_subexpressions(model) self.equations = model self._linked_variables = set() logger.diagnostic("Creating NeuronGroup of size {self._N}, " "equations {self.equations}.".format(self=self)) if namespace is None: namespace = {} #: The group-specific namespace self.namespace = namespace # All of the following will be created in before_run #: The refractory condition or timespan self._refractory = refractory if uses_refractoriness and refractory is False: logger.warn( 'Model equations use the "unless refractory" flag but ' 'no refractory keyword was given.', 'no_refractory') #: The state update method selected by the user self.method_choice = method if events is None: events = {} if threshold is not None: if 'spike' in events: raise ValueError(("The NeuronGroup defines both a threshold " "and a 'spike' event")) events['spike'] = threshold # Setup variables # Since we have to create _spikespace and possibly other "eventspace" # variables, we pass the supported events self._create_variables(dtype, events=list(events.keys())) #: Events supported by this group self.events = events #: Code that is triggered on events (e.g. reset) self.event_codes = {} #: Checks the spike threshold (or abitrary user-defined events) self.thresholder = {} #: Reset neurons which have spiked (or perform arbitrary actions for #: user-defined events) self.resetter = {} for event_name in events.keys(): if not isinstance(event_name, str): raise TypeError(('Keys in the "events" dictionary have to be ' 'strings, not type %s.') % type(event_name)) if not _valid_event_name(event_name): raise TypeError(("The name '%s' cannot be used as an event " "name.") % event_name) # By default, user-defined events are checked after the threshold when = 'thresholds' if event_name == 'spike' else 'after_thresholds' # creating a Thresholder will take care of checking the validity # of the condition thresholder = Thresholder(self, event=event_name, when=when) self.thresholder[event_name] = thresholder self.contained_objects.append(thresholder) if reset is not None: self.run_on_event('spike', reset, when='resets') #: Performs numerical integration step self.state_updater = StateUpdater(self, method, method_options) self.contained_objects.append(self.state_updater) #: Update the "constant over a time step" subexpressions self.subexpression_updater = None if len(constant_over_dt): self.subexpression_updater = SubexpressionUpdater( self, constant_over_dt) self.contained_objects.append(self.subexpression_updater) if refractory is not False: # Set the refractoriness information self.variables['lastspike'].set_value(-1e4 * second) self.variables['not_refractory'].set_value(True) # Activate name attribute access self._enable_group_attributes() @property def spikes(self): ''' The spikes returned by the most recent thresholding operation. ''' # Note that we have to directly access the ArrayVariable object here # instead of using the Group mechanism by accessing self._spikespace # Using the latter would cut _spikespace to the length of the group spikespace = self.variables['_spikespace'].get_value() return spikespace[:spikespace[-1]] def state(self, name, use_units=True, level=0): try: return Group.state(self, name, use_units=use_units, level=level + 1) except KeyError as ex: if name in self._linked_variables: raise TypeError(('Link target for variable %s has not been ' 'set.') % name) else: raise ex def run_on_event(self, event, code, when='after_resets', order=None): ''' Run code triggered by a custom-defined event (see `NeuronGroup` documentation for the specification of events).The created `Resetter` object will be automatically added to the group, it therefore does not need to be added to the network manually. However, a reference to the object will be returned, which can be used to later remove it from the group or to set it to inactive. Parameters ---------- event : str The name of the event that should trigger the code code : str The code that should be executed when : str, optional The scheduling slot that should be used to execute the code. Defaults to `'after_resets'`. order : int, optional The order for operations in the same scheduling slot. Defaults to the order of the `NeuronGroup`. Returns ------- obj : `Resetter` A reference to the object that will be run. ''' if event not in self.events: error_message = "Unknown event '%s'." % event if event == 'spike': error_message += ' Did you forget to define a threshold?' raise ValueError(error_message) if event in self.resetter: raise ValueError(("Cannot add code for event '%s', code for this " "event has already been added.") % event) self.event_codes[event] = code resetter = Resetter(self, when=when, order=order, event=event) self.resetter[event] = resetter self.contained_objects.append(resetter) return resetter def set_event_schedule(self, event, when='after_thresholds', order=None): ''' Change the scheduling slot for checking the condition of an event. Parameters ---------- event : str The name of the event for which the scheduling should be changed when : str, optional The scheduling slot that should be used to check the condition. Defaults to `'after_thresholds'`. order : int, optional The order for operations in the same scheduling slot. Defaults to the order of the `NeuronGroup`. ''' if event not in self.thresholder: raise ValueError("Unknown event '%s'." % event) order = order if order is not None else self.order self.thresholder[event].when = when self.thresholder[event].order = order def __setattr__(self, key, value): # attribute access is switched off until this attribute is created by # _enable_group_attributes if not hasattr( self, '_group_attribute_access_active') or key in self.__dict__: object.__setattr__(self, key, value) elif key in self._linked_variables: if not isinstance(value, LinkedVariable): raise ValueError( ('Cannot set a linked variable directly, link ' 'it to another variable using "linked_var".')) linked_var = value.variable if isinstance(linked_var, DynamicArrayVariable): raise NotImplementedError(('Linking to variable %s is not ' 'supported, can only link to ' 'state variables of fixed ' 'size.') % linked_var.name) eq = self.equations[key] if eq.dim is not linked_var.dim: raise DimensionMismatchError( ('Unit of variable %s does not ' 'match its link target %s') % (key, linked_var.name)) if not isinstance(linked_var, Subexpression): var_length = len(linked_var) else: var_length = len(linked_var.owner) if value.index is not None: try: index_array = np.asarray(value.index) if not np.issubsctype(index_array.dtype, np.int): raise TypeError() except TypeError: raise TypeError(('The index for a linked variable has ' 'to be an integer array')) size = len(index_array) source_index = value.group.variables.indices[value.name] if source_index not in ('_idx', '0'): # we are indexing into an already indexed variable, # calculate the indexing into the target variable index_array = value.group.variables[ source_index].get_value()[index_array] if not index_array.ndim == 1 or size != len(self): raise TypeError( ('Index array for linked variable %s ' 'has to be a one-dimensional array of ' 'length %d, but has shape ' '%s') % (key, len(self), str(index_array.shape))) if min(index_array) < 0 or max(index_array) >= var_length: raise ValueError('Index array for linked variable %s ' 'contains values outside of the valid ' 'range [0, %d[' % (key, var_length)) self.variables.add_array('_%s_indices' % key, size=size, dtype=index_array.dtype, constant=True, read_only=True, values=index_array) index = '_%s_indices' % key else: if linked_var.scalar or (var_length == 1 and self._N != 1): index = '0' else: index = value.group.variables.indices[value.name] if index == '_idx': target_length = var_length else: target_length = len(value.group.variables[index]) # we need a name for the index that does not clash with # other names and a reference to the index new_index = '_' + value.name + '_index_' + index self.variables.add_reference(new_index, value.group, index) index = new_index if len(self) != target_length: raise ValueError( ('Cannot link variable %s to %s, the size of ' 'the target group does not match ' '(%d != %d). You can provide an indexing ' 'scheme with the "index" keyword to link ' 'groups with different sizes') % (key, linked_var.name, len(self), target_length)) self.variables.add_reference(key, value.group, value.name, index=index) log_msg = ('Setting {target}.{targetvar} as a link to ' '{source}.{sourcevar}').format( target=self.name, targetvar=key, source=value.variable.owner.name, sourcevar=value.variable.name) if index is not None: log_msg += '(using "{index}" as index variable)'.format( index=index) logger.diagnostic(log_msg) else: if isinstance(value, LinkedVariable): raise TypeError( ('Cannot link variable %s, it has to be marked ' 'as a linked variable with "(linked)" in the ' 'model equations.') % key) else: Group.__setattr__(self, key, value, level=1) def __getitem__(self, item): start, stop = to_start_stop(item, self._N) return Subgroup(self, start, stop) def _create_variables(self, user_dtype, events): ''' Create the variables dictionary for this `NeuronGroup`, containing entries for the equation variables and some standard entries. ''' self.variables = Variables(self) self.variables.add_constant('N', self._N) # Standard variables always present for event in events: self.variables.add_array('_{}space'.format(event), size=self._N + 1, dtype=np.int32, constant=False) # Add the special variable "i" which can be used to refer to the neuron index self.variables.add_arange('i', size=self._N, constant=True, read_only=True) # Add the clock variables self.variables.create_clock_variables(self._clock) for eq in self.equations.values(): dtype = get_dtype(eq, user_dtype) check_identifier_pre_post(eq.varname) if eq.type in (DIFFERENTIAL_EQUATION, PARAMETER): if 'linked' in eq.flags: # 'linked' cannot be combined with other flags if not len(eq.flags) == 1: raise SyntaxError(('The "linked" flag cannot be ' 'combined with other flags')) self._linked_variables.add(eq.varname) else: constant = 'constant' in eq.flags shared = 'shared' in eq.flags size = 1 if shared else self._N self.variables.add_array(eq.varname, size=size, dimensions=eq.dim, dtype=dtype, constant=constant, scalar=shared) elif eq.type == SUBEXPRESSION: self.variables.add_subexpression(eq.varname, dimensions=eq.dim, expr=str(eq.expr), dtype=dtype, scalar='shared' in eq.flags) else: raise AssertionError('Unknown type of equation: ' + eq.eq_type) # Add the conditional-write attribute for variables with the # "unless refractory" flag if self._refractory is not False: for eq in self.equations.values(): if (eq.type == DIFFERENTIAL_EQUATION and 'unless refractory' in eq.flags): not_refractory_var = self.variables['not_refractory'] var = self.variables[eq.varname] var.set_conditional_write(not_refractory_var) # Stochastic variables for xi in self.equations.stochastic_variables: self.variables.add_auxiliary_variable( xi, dimensions=(second**-0.5).dim) # Check scalar subexpressions for eq in self.equations.values(): if eq.type == SUBEXPRESSION and 'shared' in eq.flags: var = self.variables[eq.varname] for identifier in var.identifiers: if identifier in self.variables: if not self.variables[identifier].scalar: raise SyntaxError( ('Shared subexpression %s refers ' 'to non-shared variable %s.') % (eq.varname, identifier)) def before_run(self, run_namespace=None): # Check units self.equations.check_units(self, run_namespace=run_namespace) # Check that subexpressions that refer to stateful functions are labeled # as "constant over dt" check_subexpressions(self, self.equations, run_namespace) super(NeuronGroup, self).before_run(run_namespace=run_namespace) def _repr_html_(self): text = [ r'NeuronGroup "%s" with %d neurons.<br>' % (self.name, self._N) ] text.append(r'<b>Model:</b><nr>') text.append(sympy.latex(self.equations)) def add_event_to_text(event): if event == 'spike': event_header = 'Spiking behaviour' event_condition = 'Threshold condition' event_code = 'Reset statement(s)' else: event_header = 'Event "%s"' % event event_condition = 'Event condition' event_code = 'Executed statement(s)' condition = self.events[event] text.append( r'<b>%s:</b><ul style="list-style-type: none; margin-top: 0px;">' % event_header) text.append(r'<li><i>%s: </i>' % event_condition) text.append('<code>%s</code></li>' % str(condition)) statements = self.event_codes.get(event, None) if statements is not None: text.append(r'<li><i>%s:</i>' % event_code) if '\n' in str(statements): text.append('</br>') text.append(r'<code>%s</code></li>' % str(statements)) text.append('</ul>') if 'spike' in self.events: add_event_to_text('spike') for event in self.events: if event != 'spike': add_event_to_text(event) return '\n'.join(text)