Simulation#
Compiling the network#
Once all the relevant information has been defined, one needs to
actually compile the network, by calling the ANNarchy.compile()
method:
compile()
The optimized C++ code will be generated in the annarchy/
subfolder
relative to your script, compiled, the underlying objects created and
made available to the Python interface.
You can specify the following arguments to compile()
:
directory
: relative path to the directory where files will be generated and compiled (default:annarchy/
)populations
andprojections
: to compile only a subpart of the network, see Network.compiler
: to select which C++ compiler will be used. By defaultg++
on Linux andclang++
on OS X are used, you can change it here. Note that only these two compilers are supported for now, and that they must be in your$PATH
.compiler_flags
: to select which flags are passed to the compiler. By default it is-march=native -O2
, but you can fine-tune it here. Beware that-O3
is most often a bad idea!
Simulating the network#
After the network is correctly compiled, the simulation can be run for
the specified duration (in milliseconds) through the
ANNarchy.simulate()
method:
simulate(1000.0) # Simulate for 1 second
The provided duration should be a multiple of dt
. If not, the number
of simulation steps performed will be approximated.
In some cases, you may want to perform only one step of the simulation,
instead of specifing the duration. The ANNarchy.step()
can then be
used.
step() # Simulate for 1 step
Early-stopping#
In some cases, it is desired to stop the simulation whenever a criterion is fulfilled (for example, a neural integrator exceeds a certain threshold), not after a fixed amount of time.
There is the possibility to define a stop_condition
at the
Population
level:
pop1 = Population( ... , stop_condition = "r > 1.0")
When calling the simulate_until()
method instead of simulate()
:
t = simulate_until(max_duration=1000.0, populations=pop1)
the simulation will be stopped whenever the stop_condition
of pop1
is met, i.e. when the firing rate of any neuron of pop1 is above 1.0.
If the condition is never met, the simulation will last maximally
max_duration
. The methods returns the effective duration of the
simulation (to compute reaction times, for example).
The stop_condition
can use any logical operation on the parameters and
variables of the neuron associated to the population:
pop1 = Population( ... , stop_condition = "(r > 1.0) and (mp < 2.0)")
By default, the simulation stops when at least one neuron in the
population fulfills the criterion. If you want to stop the simulation
when all neurons fulfill the condition, you can use the flag all
after the condition:
pop1 = Population( ... , stop_condition = "r > 1.0 : all")
The flag any
is the default behavior and can be omitted.
The stop criterion can depend on several populations, by providing a
list of populations to the populations
argument instead of a single
population:
t = simulate_until(max_duration=1000.0, populations=[pop1, pop2])
The simulation will then stop when the criterion is met in both
populations at the same time. If you want that the simulation stops when
at least one population meets its criterion, you can specify the
operator
argument:
t = simulate_until(max_duration=1000.0, populations=[pop1, pop2], operator='or')
The default value of operator
is a 'and'
function between the
populations' criteria.
Warning
Global operations (min, max, mean) are not possible inside the
stop_condition
. If you need them, store them in a variable in the
equations
argument of the neuron and use it as the condition:
equations = """
r = ...
max_r = max(r)
"""
Setting inputs periodically#
In most cases, your simulation will be decomposed into a series of fixed-duration trials, where you basically set inputs at the beginning of the trial, run the simulation for a fixed duration, and possibly read out results at the end:
# Iterate over 100 trials
result = []
for trial in range(100):
# Set inputs to the network
pop.I = Uniform(0.0, 1.0)
# Simulate for 1 second
simulate(1000.)
# Save the output
result.append(pop.r)
For convenience, we provide the decorator every
, which allows to
register a python method and call it automatically during the simulation
with a fixed period:
result = []
@every(period=1000.)
def set inputs(n):
# Set inputs to the network
pop.I = Uniform(0.0, 1.0)
# Save the output of the previous step
if n > 0:
result.append(pop.r)
simulate(100 * 1000.)
In this example, set_inputs()
will be executed just before the steps
corresponding to times t = 0., 1000., 2000., and so on until t = 100000.
The method can have any name, but must accept only one argument, the
integer n
which will be incremented at each call of the method (i.e.
it will take the values 0, 1, 2 until 99). This can for example be used
to access data in a numpy array:
images = np.random.random((100, 640, 480))
@every(period=1000.)
def set inputs(n):
# Set inputs to the network
pop.I = images[n, :, :]
simulate(100 * 1000.)
One can define several methods that will be called in the order of their definition:
@every(period=1000.)
def set inputs(n):
pop.I = 1.0
@every(period=1000.)
def reset inputs(n):
pop.I = 0.0
In this example, set_inputs()
will be called first, followed by
reset_inputs
, so pop.I
will finally be 0.0. The decorator every
accepts an argument offset
defining a delay within the period to call
the method:
@every(period=1000.)
def set inputs(n):
pop.I = 1.0
@every(period=1000., offset=500.)
def reset inputs(n):
pop.I = 0.0
In this case, set_inputs()
will be called at times 0, 1000, 2000...
while reset_inputs()
will be called at times 500, 1500, 2500...,
allowing to structure a trial more effectively. The offset
can be set
negative, in which case it will be relative to the end of the trial:
@every(period=1000., offset=-100.)
def reset inputs(n):
pop.I = 0.0
In this example, the method will be called at times 900, 1900, 2900 and
so on. The offset
value can not be longer than the period
, by
definition. If you try to do so, a modulo operation will anyway be
applied (i.e. an offset of 1500 with a period of 1000 becomes 500).
Finally, the wait
argument allows to delay the first call to the
method from a fixed interval:
@every(period=1000., wait=5000.)
def reset inputs(n):
pop.I = 0.0
In this case, the method will be called at times 5000, 6000 and so on.
Between two calls to simulate()
, the callbacks can be disabled or
re-enabled using the following methods:
@every(period=1000.)
def reset inputs(n):
pop.I = 0.0
# Simulate with callbacks
simulate(10000.)
# Disable callbacks
disable_callbacks()
# Simulate without callbacks
simulate(10000.)
# Re-enable callbacks
enable_callbacks()
# Simulate with callbacks
simulate(10000.)
Note that the period is always relative to the time when simulate()
is
called, so if no offset is defined, the callbacks will be called before
the first step of a simulation, no matter how long the previous
simulation lasted. In the current state, it is not possible yet to
enable/disable callbacks selectively, it is all or none.
Callbacks can only be used with simulate()
, not with step()
or
simulate_until()
.