import inspect
from brian2.core.base import BrianObject
__all__ = ["NetworkOperation", "network_operation"]
[docs]class NetworkOperation(BrianObject):
"""Object with function that is called every time step.
Parameters
----------
function : function
The function to call every time step, should take either no arguments
in which case it is called as ``function()`` or one argument, in which
case it is called with the current `Clock` time (`Quantity`).
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
In which scheduling slot to execute the operation during a time step.
Defaults to ``'start'``. See :ref:`scheduling` for possible values.
order : int, optional
The priority of this operation for operations occurring at the same time
step and in the same scheduling slot. Defaults to 0.
See Also
--------
network_operation, Network, BrianObject
"""
add_to_magic_network = True
def __init__(
self,
function,
dt=None,
clock=None,
when="start",
order=0,
name="networkoperation*",
):
BrianObject.__init__(
self, dt=dt, clock=clock, when=when, order=order, name=name
)
#: The function to be called each time step
self.function = function
is_method = inspect.ismethod(function)
if hasattr(function, "__code__"):
argcount = function.__code__.co_argcount
if is_method:
if argcount == 2:
self._has_arg = True
elif argcount == 1:
self._has_arg = False
else:
raise TypeError(
f"Method '{function.__name__}' cannot be used as a "
"network operation, it needs to have either "
"only 'self' or 'self, t' as arguments, but it "
f"has {argcount} arguments."
)
else:
if argcount >= 1 and function.__code__.co_varnames[0] == "self":
raise TypeError(
"The first argument of the function "
"'{function.__name__}' is 'self', suggesting it "
"is an instance method and not a function. Did "
"you use @network_operation on a class method? "
"This will not work, explicitly create a "
"NetworkOperation object instead -- see "
"the documentation for more "
"details."
)
if argcount == 1:
self._has_arg = True
elif argcount == 0:
self._has_arg = False
else:
raise TypeError(
f"Function '{function.__name__}' cannot be used as "
"a network operation, it needs to have either "
"only 't' as an argument or have no arguments, "
f"but it has {argcount} arguments."
)
else:
self._has_arg = False
[docs] def run(self):
if self._has_arg:
self.function(self._clock.t)
else:
self.function()
[docs]def network_operation(*args, **kwds):
"""
network_operation(when=None)
Decorator to make a function get called every time step of a simulation.
The function being decorated should either have no arguments, or a single
argument which will be called with the current time ``t``.
Parameters
----------
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
In which scheduling slot to execute the operation during a time step.
Defaults to ``'start'``. See :ref:`scheduling` for possible values.
order : int, optional
The priority of this operation for operations occurring at the same time
step and in the same scheduling slot. Defaults to 0.
Examples
--------
Print something each time step:
>>> from brian2 import *
>>> @network_operation
... def f():
... print('something')
...
>>> net = Network(f)
Print the time each time step:
>>> @network_operation
... def f(t):
... print('The time is', t)
...
>>> net = Network(f)
Specify a dt, etc.:
>>> @network_operation(dt=0.5*ms, when='end')
... def f():
... print('This will happen at the end of each timestep.')
...
>>> net = Network(f)
Notes
-----
Converts the function into a `NetworkOperation`.
If using the form::
@network_operations(when='end')
def f():
...
Then the arguments to network_operation must be keyword arguments.
See Also
--------
NetworkOperation, Network, BrianObject
"""
# Notes on this decorator:
# Normally, a decorator comes in two types, with or without arguments. If
# it has no arguments, e.g.
# @decorator
# def f():
# ...
# then the decorator function is defined with an argument, and that
# argument is the function f. In this case, the decorator function
# returns a new function in place of f.
#
# However, you can also define:
# @decorator(arg)
# def f():
# ...
# in which case the argument to the decorator function is arg, and the
# decorator function returns a 'function factory', that is a callable
# object that takes a function as argument and returns a new function.
#
# It might be clearer just to note that the first form above is equivalent
# to:
# f = decorator(f)
# and the second to:
# f = decorator(arg)(f)
#
# In this case, we're allowing the decorator to be called either with or
# without an argument, so we have to look at the arguments and determine
# if it's a function argument (in which case we do the first case above),
# or if the arguments are arguments to the decorator, in which case we
# do the second case above.
#
# Here, the 'function factory' is the locally defined class
# do_network_operation, which is a callable object that takes a function
# as argument and returns a NetworkOperation object.
class do_network_operation:
def __init__(self, **kwds):
self.kwds = kwds
def __call__(self, f):
new_network_operation = NetworkOperation(f, **self.kwds)
# Depending on whether we were called as @network_operation or
# @network_operation(...) we need different levels, the level is
# 2 in the first case and 1 in the second case (because in the
# first case we go originalcaller->network_operation->do_network_operation
# and in the second case we go originalcaller->do_network_operation
# at the time when this method is called).
new_network_operation.__name__ = f.__name__
new_network_operation.__doc__ = f.__doc__
new_network_operation.__dict__.update(f.__dict__)
return new_network_operation
if len(args) == 1 and callable(args[0]):
# We're in case (1), the user has written:
# @network_operation
# def f():
# ...
# and the single argument to the decorator is the function f
return do_network_operation()(args[0])
else:
# We're in case (2), the user has written:
# @network_operation(...)
# def f():
# ...
# and the arguments must be keyword arguments
return do_network_operation(**kwds)