Projects with multiple files or functions¶
Brian works with the minimal hassle if the whole of your code is in a
single Python module (
.py file). This is fine when learning Brian
or for quick projects, but for larger, more realistic projects with
the source code separated into multiple files, there are some small
issues you need to be aware of. These issues essentially revolve
around the use of the ‘’magic’’ functions
run(), etc. The way
these functions work is to look for objects of the required type that
have been instantiated (created) in the same ‘’execution frame’’ as
run() function. In a small script, that is normally just
any objects that have been defined in that script. However, if you
define objects in a different module, or in a function, then the
magic functions won’t be able to find them.
There are three main approaches then to splitting code over multiple files (or functions).
run() function works by creating a
object automatically, and then running that network. Instead of doing
this automatically, you can create your own
Rather than writing something like:
group1 = ... group2 = ... C = Connection(group1,group2) ... run(1*second)
You do this:
group1 = ... group2 = ... C = Connection(group1, group2) ... net = Network(group1, group2, C) net.run(1*second)
In other words, you explicitly say which objects are in your network.
Note that any
function decorated with
network_operation() should be included in the
Network. See the documentation for
Network for more details.
This is the preferred solution for almost all cases. You may want to use either of the following two solutions if you think your code may be used by someone else, or if you want to make it into an extension to Brian.
magic_return() decorator or
magic_return() decorator is used as follows:
@magic_return def f(): ... return obj
Any object returned by a function decorated by
magic_return() will be
considered to have been instantiated in the execution frame that called the
function. In other words, the magic functions will find that object even
though it was really instantiated in a different execution frame.
In more complicated scenarios, you may want to use the
function. For example:
def f(): ... magic_register(obj1, obj2) return (obj1, obj2)
Use derived classes¶
Rather than writing a function which returns an object, you could instead
write a derived class of the object type. So, suppose you wanted to have an
object that emitted N equally spaced spikes, with an interval dt between
them, you could use the
SpikeGeneratorGroup class as follows:
@magic_return def equally_spaced_spike_group(N, dt): spikes = [(0,i*dt) for i in range(N)] return SpikeGeneratorGroup(spikes)
Or alternatively, you could derive a class from
class EquallySpacedSpikeGroup(SpikeGeneratorGroup): def __init__(self, N, t): spikes = [(0,i*dt) for i in range(N)] SpikeGeneratorGroup.__init__(self, spikes)
You would use these objects in the following ways:
obj1 = equally_spaced_spike_group(100, 10*ms) obj2 = EquallySpacedSpikeGroup(100, 10*ms)
For simple examples like the one above, there’s no particular benefit to using derived classes, but using derived classes allows you to add methods to your derived class for example, which might be useful. For more experienced Python programmers, or those who are thinking about making their code into an extension for Brian, this is probably the preferred approach.
Finally, it may be useful to note that there is a protocol for one object
to ‘contain’ other objects. That is, suppose you want to have an object
that can be treated as a simple
NeuronGroup by the person using it,
but actually instantiates several objects (perhaps internal
objects). These objects need to be added to the
in order for them to be run with the simulation, but the user shouldn’t need
to have to know about them. To this end, for any object added to a
Network, if it has an attribute
contained_objects, then any
objects in that container will also be added to the network.