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 the 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).

Use the Network object explicitly

The magic run() function works by creating a Network object automatically, and then running that network. Instead of doing this automatically, you can create your own Network object. 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 NeuronGroup, Connection, Monitor or 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.

Use the magic_return() decorator or magic_register() function

The 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 magic_register() function. For example:

def f():
        ...
        magic_register(obj1, obj2)
        return (obj1, obj2)

This does the same thing as magic_return() but can be used with multiple objects. Also, you can specify a level (see documentation on magic_register() for more details).

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 SpikeGeneratorGroup as follows:

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 Connection objects). These objects need to be added to the Network object 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.