"""
This model defines the `NeuronGroup`, the core of most simulations.
"""
from collections.abc import Sequence, MutableMapping
import numbers
import string
import numpy as np
import sympy
from pyparsing import Word
from brian2.codegen.translation import analyse_identifiers
from brian2.core.preferences import prefs
from brian2.core.spikesource import SpikeSource
from brian2.core.variables import (Variables, LinkedVariable,
DynamicArrayVariable, Subexpression)
from brian2.equations.equations import (Equations, DIFFERENTIAL_EQUATION,
SUBEXPRESSION, PARAMETER,
check_subexpressions,
extract_constant_subexpressions)
from brian2.equations.refractory import add_refractoriness
from brian2.parsing.expressions import (parse_expression_dimensions,
is_boolean_expression)
from brian2.stateupdaters.base import StateUpdateMethod
from brian2.units.allunits import second
from brian2.units.fundamentalunits import (Quantity, DIMENSIONLESS,
DimensionMismatchError,
fail_for_dimension_mismatch)
from brian2.utils.logger import get_logger
from brian2.utils.stringtools import get_identifiers
from .group import Group, CodeRunner, get_dtype
from .subgroup import Subgroup
__all__ = ['NeuronGroup']
logger = get_logger(__name__)
IDENTIFIER = Word(f"{string.ascii_letters}_",
f"{string.ascii_letters + string.digits}_").setResultsName('identifier')
def _valid_event_name(event_name):
"""
Helper function to check whether a name is a valid name for an event.
Parameters
----------
event_name : str
The name to check
Returns
-------
is_valid : bool
Whether the given name is valid
"""
parse_result = list(IDENTIFIER.scanString(event_name))
# parse_result[0][0][0] refers to the matched string -- this should be the
# full identifier, if not it is an illegal identifier like "3foo" which only
# matched on "foo"
return len(parse_result) == 1 and parse_result[0][0][0] == event_name
def _guess_membrane_potential(equations):
"""
Little helper function to guess which variable represents the membrane
potential. This follows the same logic as in Brian1 but is only used to
give a suggestion in the error message when a Brian1-style syntax is used
for threshold or reset.
"""
if len(equations) == 1:
return list(equations.keys())[0]
for name, eq in equations.items():
if name in ['V', 'v', 'Vm', 'vm']:
return name
# nothing found
return None
# Note that we do not register this function with
# Equations.register_identifier_check, because we do not want this check to
# apply unconditionally to all equation objects ("x_post = ... : ... (summed)"
# needs to be allowed)
[docs]def check_identifier_pre_post(identifier):
'Do not allow names ending in ``_pre`` or ``_post`` to avoid confusion.'
if identifier.endswith('_pre') or identifier.endswith('_post'):
raise ValueError(f"'{identifier}' cannot be used as a variable name, the "
f"'_pre' and '_post' suffixes are used to refer to pre- and "
f"post-synaptic variables in synapses.")
[docs]def to_start_stop(item, N):
"""
Helper function to transform a single number, a slice or an array of
contiguous indices to a start and stop value. This is used to allow for
some flexibility in the syntax of specifying subgroups in `.NeuronGroup`
and `.SpatialNeuron`.
Parameters
----------
item : slice, int or sequence
The slice, index, or sequence of indices to use. Note that a sequence
of indices has to be a sorted ascending sequence of subsequent integers.
N : int
The total number of elements in the group.
Returns
-------
start : int
The start value of the slice.
stop : int
The stop value of the slice.
Examples
--------
>>> from brian2.groups.neurongroup import to_start_stop
>>> to_start_stop(slice(3, 6), 10)
(3, 6)
>>> to_start_stop(slice(3, None), 10)
(3, 10)
>>> to_start_stop(5, 10)
(5, 6)
>>> to_start_stop([3, 4, 5], 10)
(3, 6)
>>> to_start_stop([3, 5, 7], 10)
Traceback (most recent call last):
...
IndexError: Subgroups can only be constructed using contiguous indices.
"""
if isinstance(item, slice):
start, stop, step = item.indices(N)
elif isinstance(item, numbers.Integral):
start = item
stop = item + 1
step = 1
elif (isinstance(item, (Sequence, np.ndarray)) and
not isinstance(item, str)):
if not (len(item) > 0 and np.all(np.diff(item) == 1)):
raise IndexError("Subgroups can only be constructed using "
"contiguous indices.")
if not np.issubdtype(np.asarray(item).dtype, np.integer):
raise TypeError("Subgroups can only be constructed using integer "
"values.")
start = int(item[0])
stop = int(item[-1]) + 1
step = 1
else:
raise TypeError("Subgroups can only be constructed using slicing "
"syntax, a single index, or an array of contiguous "
"indices.")
if step != 1:
raise IndexError("Subgroups have to be contiguous")
if start >= stop:
raise IndexError(
f"Illegal start/end values for subgroup, {int(start)}>={int(stop)}")
if start >= N:
raise IndexError(f"Illegal start value for subgroup, {int(start)}>={int(N)}")
if stop > N:
raise IndexError(f"Illegal stop value for subgroup, {int(stop)}>{int(N)}")
if start < 0:
raise IndexError("Indices have to be positive.")
return start, stop
[docs]class StateUpdater(CodeRunner):
"""
The `CodeRunner` that updates the state variables of a `NeuronGroup`
at every timestep.
"""
def __init__(self, group, method, method_options=None):
self.method_choice = method
self.method_options = method_options
CodeRunner.__init__(self, group,
'stateupdate',
code='', # will be set in update_abstract_code
clock=group.clock,
when='groups',
order=group.order,
name=f"{group.name}_stateupdater",
check_units=False,
generate_empty_code=False)
def _get_refractory_code(self, run_namespace):
ref = self.group._refractory
if ref is False:
# No refractoriness
abstract_code = ''
elif isinstance(ref, Quantity):
fail_for_dimension_mismatch(ref, second, ('Refractory period has to '
'be specified in units '
'of seconds but got '
'{value}'),
value=ref)
if prefs.legacy.refractory_timing:
abstract_code = f'not_refractory = (t - lastspike) > {ref:f}\n'
else:
abstract_code = f'not_refractory = timestep(t - lastspike, dt) >= timestep({ref:f}, dt)\n'
else:
identifiers = get_identifiers(ref)
variables = self.group.resolve_all(identifiers,
run_namespace,
user_identifiers=identifiers)
dims = parse_expression_dimensions(str(ref), variables)
if dims is second.dim:
if prefs.legacy.refractory_timing:
abstract_code = f'(t - lastspike) > {ref}\n'
else:
abstract_code = f'not_refractory = timestep(t - lastspike, dt) >= timestep({ref}, dt)\n'
elif dims is DIMENSIONLESS:
if not is_boolean_expression(str(ref), variables):
raise TypeError("Refractory expression is dimensionless "
"but not a boolean value. It needs to "
"either evaluate to a timespan or to a "
"boolean value.")
# boolean condition
# we have to be a bit careful here, we can't just use the given
# condition as it is, because we only want to *leave*
# refractoriness, based on the condition
abstract_code = f'not_refractory = not_refractory or not ({ref})\n'
else:
raise TypeError(f"Refractory expression has to evaluate to a "
f"timespan or a boolean value, expression"
f"'{ref}' has units {dims} instead")
return abstract_code
[docs] def update_abstract_code(self, run_namespace):
# Update the not_refractory variable for the refractory period mechanism
self.abstract_code = self._get_refractory_code(run_namespace=run_namespace)
# Get the names used in the refractory code
_, used_known, unknown = analyse_identifiers(self.abstract_code, self.group.variables,
recursive=True)
# Get all names used in the equations (and always get "dt")
names = self.group.equations.names
external_names = self.group.equations.identifiers | {'dt'}
variables = self.group.resolve_all(used_known | unknown | names | external_names,
run_namespace,
# we don't need to raise any warnings
# for the user here, warnings will
# be raised in create_runner_codeobj
user_identifiers=set())
if len(self.group.equations.diff_eq_names) > 0:
stateupdate_output = StateUpdateMethod.apply_stateupdater(self.group.equations,
variables,
self.method_choice,
method_options=self.method_options,
group_name=self.group.name)
if isinstance(stateupdate_output, str):
self.abstract_code += stateupdate_output
else:
# Note that the reason to send self along with this method is so the StateUpdater
# can be modified! i.e. in GSL StateUpdateMethod a custom CodeObject gets added
# to the StateUpdater together with some auxiliary information
self.abstract_code += stateupdate_output(self)
user_code = '\n'.join([f'{var} = {expr}'
for var, expr in
self.group.equations.get_substituted_expressions(variables)])
self.user_code = user_code
[docs]class SubexpressionUpdater(CodeRunner):
"""
The `CodeRunner` that updates the state variables storing the values of
subexpressions that have been marked as "constant over dt".
"""
def __init__(self, group, subexpressions, when='before_start'):
code_lines = []
for subexpr in subexpressions.ordered:
code_lines.append(f'{subexpr.varname} = {subexpr.expr}')
code = '\n'.join(code_lines)
CodeRunner.__init__(self, group,
'stateupdate',
code=code, # will be set in update_abstract_code
clock=group.clock,
when=when,
order=group.order,
name=f"{group.name}_subexpression_update*")
[docs]class Thresholder(CodeRunner):
"""
The `CodeRunner` that applies the threshold condition to the state
variables of a `NeuronGroup` at every timestep and sets its ``spikes``
and ``refractory_until`` attributes.
"""
def __init__(self, group, when='thresholds', event='spike'):
self.event = event
if group._refractory is False or event != 'spike':
template_kwds = {'_uses_refractory': False}
needed_variables = []
else:
template_kwds = {'_uses_refractory': True}
needed_variables=['t', 'not_refractory', 'lastspike']
# 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 = f'_{event}space'
template_kwds['eventspace_variable'] = group.variables[eventspace_name]
needed_variables.append(eventspace_name)
self.variables = Variables(self)
self.variables.add_auxiliary_variable('_cond', dtype=bool)
CodeRunner.__init__(self, group,
'threshold',
code='', # will be set in update_abstract_code
clock=group.clock,
when=when,
order=group.order,
name=f"{group.name}_{event}_thresholder",
needed_variables=needed_variables,
template_kwds=template_kwds)
[docs] def update_abstract_code(self, run_namespace):
code = self.group.events[self.event]
# Raise a useful error message when the user used a Brian1 syntax
if not isinstance(code, str):
if isinstance(code, Quantity):
t = 'a quantity'
else:
t = f'{type(code)}'
error_msg = f'Threshold condition has to be a string, not {t}.'
if self.event == 'spike':
try:
vm_var = _guess_membrane_potential(self.group.equations)
except AttributeError: # not a group with equations...
vm_var = None
if vm_var is not None:
error_msg += f" Probably you intended to use '{vm_var} > ...'?"
raise TypeError(error_msg)
self.user_code = f"_cond = {code}"
identifiers = get_identifiers(code)
variables = self.group.resolve_all(identifiers,
run_namespace,
user_identifiers=identifiers)
if not is_boolean_expression(code, variables):
raise TypeError(f"Threshold condition '{code}' is not a boolean "
f"expression")
if self.group._refractory is False or self.event != 'spike':
self.abstract_code = f'_cond = {code}'
else:
self.abstract_code = f'_cond = ({code}) and not_refractory'
[docs]class Resetter(CodeRunner):
"""
The `CodeRunner` that applies the reset statement(s) to the state
variables of neurons that have spiked in this timestep.
"""
def __init__(self, group, when='resets', order=None, event='spike'):
self.event = event
# 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 = f'_{event}space'
template_kwds = {'eventspace_variable': group.variables[eventspace_name]}
needed_variables= [eventspace_name]
order = order if order is not None else group.order
CodeRunner.__init__(self, group,
'reset',
code='', # will be set in update_abstract_code
clock=group.clock,
when=when,
order=order,
name=f"{group.name}_{event}_resetter",
override_conditional_write=['not_refractory'],
needed_variables=needed_variables,
template_kwds=template_kwds)
[docs] def update_abstract_code(self, run_namespace):
code = self.group.event_codes[self.event]
# Raise a useful error message when the user used a Brian1 syntax
if not isinstance(code, str):
if isinstance(code, Quantity):
t = 'a quantity'
else:
t = f'{type(code)}'
error_msg = f'Reset statement has to be a string, not {t}.'
if self.event == 'spike':
vm_var = _guess_membrane_potential(self.group.equations)
if vm_var is not None:
error_msg += f" Probably you intended to use '{vm_var} = ...'?"
raise TypeError(error_msg)
self.abstract_code = code
[docs]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,
namespace=namespace, 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(f"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(f"model has to be a string or an Equations "
f"object, is '{type(model)}' instead.")
# 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(f"Creating NeuronGroup of size {self._N}, "
f"equations {self.equations}.")
# 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 and (reset is None and refractory is False):
if not('rand(' in threshold or 'randn(' in threshold):
logger.warn(f"The NeuronGroup '{self.name}' sets a threshold but "
f"neither a reset condition nor a refractory "
f"condition has been set. Did you forget either of "
f"those? If this was intended, set the reset "
f"argument to an empty string in order to avoid "
f"this warning.",
name_suffix='only_threshold')
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(f"Keys in the 'events' dictionary have to be "
f"strings, not type {event_name}.")
if not _valid_event_name(event_name):
raise TypeError(f"The name '{event_name}' cannot be used as an event "
f"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]]
[docs] 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(f"Link target for variable {name} has not been "
f"set.")
else:
raise ex
[docs] 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'`. See :ref:`scheduling` for possible values.
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 = f"Unknown event '{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
[docs] 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'`. See :ref:`scheduling` for possible values.
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(f"Unknown event '{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(f"Linking to variable {linked_var.name} is "
f"not supported, can only link to "
f"state variables of fixed size.")
eq = self.equations[key]
if eq.dim is not linked_var.dim:
raise DimensionMismatchError(f"Unit of variable '{key}' does not "
f"match its link target "
f"'{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, 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(f"Index array for linked variable '{key}' "
f"has to be a one-dimensional array of "
f"length {len(self)}, but has shape "
f"{index_array.shape!s}")
if min(index_array) < 0 or max(index_array) >= var_length:
raise ValueError(f"Index array for linked variable {key} "
f"contains values outside of the valid "
f"range [0, {var_length}[")
self.variables.add_array(f'_{key}_indices',
size=size, dtype=index_array.dtype,
constant=True, read_only=True,
values=index_array)
index = f'_{key}_indices'
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 = f"_{value.name}_index_{index}"
self.variables.add_reference(new_index,
value.group,
index)
index = new_index
if len(self) != target_length:
raise ValueError(f"Cannot link variable '{key}' to "
f"'{linked_var.name}', the size of the "
f"target group does not match "
f"({len(self)} != {target_length}). You can "
f"provide an indexing scheme with the "
f"'index' keyword to link groups with "
f"different sizes")
self.variables.add_reference(key,
value.group,
value.name,
index=index)
source = value.variable.owner.name,
sourcevar = value.variable.name
log_msg = (f"Setting {self.name}.{key} as a link to "
f"{source}.{sourcevar}")
if index is not None:
log_msg += f'(using "{index}" as index variable)'
logger.diagnostic(log_msg)
else:
if isinstance(value, LinkedVariable):
raise TypeError(f"Cannot link variable '{key}', it has to be marked "
f"as a linked variable with '(linked)' in the model "
f"equations.")
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(f'_{event}space',
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(f"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(f"Shared subexpression '{eq.varname}' "
f"refers to non-shared variable "
f"'{identifier}'.")
[docs] 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 = [rf"NeuronGroup '{self.name}' with {self._N} neurons.<br>"]
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 = f'Event "{event}"'
event_condition = 'Event condition'
event_code = 'Executed statement(s)'
condition = self.events[event]
text.append(rf'<b>{event_header}:</b><ul style="list-style-type: none; margin-top: 0px;">')
text.append(rf'<li><i>{event_condition}: </i>')
text.append(f'<code>{str(condition)}</code></li>')
statements = self.event_codes.get(event, None)
if statements is not None:
text.append(fr'<li><i>{event_code}:</i>')
if '\n' in str(statements):
text.append('</br>')
text.append(fr'<code>{str(statements)}</code></li>')
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)