Connect two NEST simulations using MUSIC

Let’s look at an example of two NEST simulations connected through MUSIC. We’ll implement the simple network in Figure 15 from the introduction to this tutorial.

We need a sending process, a receiving process and a MUSIC configuration file.

To try out the example, save the following sending process code in a Python file called send.py.

 1#!/usr/bin/env python3
 2
 3import nest
 4nest.overwrite_files = True
 5
 6neurons = nest.Create('iaf_psc_alpha', 2, {'I_e': [400.0, 405.0]})
 7
 8music_out = nest.Create('music_event_out_proxy', 1, {'port_name':'p_out'})
 9
10for i, neuron in enumerate(neurons):
11    nest.Connect(neuron, music_out, "one_to_one", {'music_channel': i})
12
13srecorder = nest.Create("spike_recorder")
14srecorder.set(record_to="ascii", label="send")
15
16nest.Connect(neurons, srecorder)
17
18nest.Simulate(1000.0)

The sending process is quite straightforward. We import the NEST library and set a useful kernel parameter. On line 6, we create two simple intergrate-and-fire neuron models, one with a current input of 400mA, and one with 405mA, just so they will respond differently. If you use ipython to work interactively, you can check their current status dictionary with neurons.get(). The definitive documentation for NEST models is the header file, in this case models/iaf_psc_alpha.h in the NEST source.

We create a single music_event_out_proxy for our output on line 8, and set the port name. We loop over all the neurons on lines 10-11 and connect them to the proxy one by one, each one with a different output channel. As we saw earlier, each MUSIC port can have any number of channels. Since the proxy is a device, it ignores any weight or delay settings here.

Lastly, we create a spike recorder, set the parameters (which we could have done directly in the Create() call) and connect the neurons to the spike recorder so we can see what we’re sending. Then we simulate for one second.

For the receiving process script, receive.py we do:

 1#!/usr/bin/env python3
 2
 3import nest
 4nest.overwrite_files = True
 5
 6music_in = nest.Create("music_event_in_proxy", 2, {'port_name': 'p_in'})
 7
 8music_in.music_channel = [c for c in range(len(music_in))]
 9
10nest.SetAcceptableLatency('p_in', 2.0)
11
12parrots = nest.Create("parrot_neuron", 2)
13
14srecorder = nest.Create("spike_recorder")
15srecorder.set(record_to="ascii", label="receive")
16
17nest.Connect(music_in, parrots, 'one_to_one', {"weight":1.0, "delay": 2.0})
18nest.Connect(parrots, srecorder)
19
20nest.Simulate(1000.0)

The receiving process follows the same logic, but is just a little more involved. We create two music_event_in_proxy — one per channel — on line 6 and set the input port name. As we discussed above, a NEST node can accept many inputs but only emit one stream of data, so we need one input proxy per channel to be able to distinguish the channels from each other. On line 8 we set the input channel for each input proxy.

The SetAcceptableLatency command on line 10 sets the maximum time, in milliseconds, that MUSIC is allowed to delay delivery of spikes transmitted through the named port. This should never be more than the minimum of the delays from the input proxies to their targets; that’s the 2.0 ms we set on line 10 in our case.

On line 12 we create a set of parrot neurons. They simply repeat the input they’re given. On lines 14-15 we create and configure a spike recorder to save our inputs. We connect the input proxies one-to-one with the parrot neurons on line 17, then the parrot neurons to the spike recorder on line 18. We will discuss the reasons for this in a moment. Finally we simulate for one second.

Lastly, we have the MUSIC configuration file python.music:

[from]
    binary=./send.py
    np=2

[to]
    binary=./receive.py
    np=2

from.p_out -> to.p_in [2]

The MUSIC configuration file structure is straightforward. We define one process from and one to. For each process we set the name of the binary we wish to run and the number of MPI processes it should use. On line 9 we finally define a connection from output port p_out in process from to input port p_in in process to, with two channels.

If our programs had taken command line options we could have added them with the args command:

binary=./send.py
args= --option -o somefile

Run the simulation on the command line like this:

mpirun -np 4 music python.music

You should get a screenful of information scrolling past, and then be left with four new data files, named something like send-N-0.spikes, send-N-1.spikes, receive-M-0.spikes and receive-M-1.spikes. The names and suffixes are of course the same that we set in send.py and receive.py above. The first numeral is the node ID of the spike recorder that recorded and saved the data, and the final numeral is the rank order of each process that generated the file.

Collate the data files:

cat send-*spikes | sort -k 2 -n  >send.spikes
cat receive-*spikes | sort -k 2 -n  >receive.spikes

We run the files together, and sort the output numerically (\(-n\)) by the second column (\(-k\)). Let’s look at the beginning of the two files side by side:

send.spikes                receive.spikes

2   26.100                 4   28.100
1   27.800                 3   29.800
2   54.200                 4   56.200
1   57.600                 3   59.600
2   82.300                 4   84.300
1   87.400                 3   89.400
2   110.40                 4   112.40
1   117.20                 3   119.20

As expected, the received spikes are two milliseconds later than the sent spikes. The delay parameter for the connection from the input proxies to the parrot neurons in receive.py on line 10 accounts for the delay.

Also — and it may be obvious in a simple model like this — the neuron IDs on the sending side and the IDs on the receiving side have no fixed relationship. The sending neurons have ID 1 and 2, while the recipients have 3 and 4. If you need to map events in one simulation to events in another, you have to record this information by other means.

Continuous Inputs

MUSIC can send not just spike events, but also continuous inputs and messages. In NEST there are devices to receive, but not send, such inputs. The NEST documentation has a few examples such as this one below:

 1#!/usr/bin/python3
 2
 3import nest
 4
 5mcip = nest.Create('music_cont_in_proxy')
 6mcip.port_name = 'contdata'
 7
 8time = 0
 9while time < 1000:
10    nest.Simulate (10)
11    data = mcip.get('data')
12    print(data)
13    time += 10

The start mirrors our earlier receiving example: you create a continuous input proxy (a single input in this case) and set the port name.

NEST has no general facility to actually apply continuous-valued inputs directly into models. Its neurons deal only with spike events. To use the input you need to create a loop on lines 9-13 where you simulate for a short period, explicitly read the value on line 11, apply it to the simulation model, then simulate for a period again.

People sometimes try to use this pattern to control the rate of a Poisson generator from outside the simulation. You get the rate from outside as a continuous value, then apply it to the Poisson generator that in turn stimulates input neurons in your network.

The problem is that you need to suspend the simulation every cycle, drop out to the Python interpreter, run a bit of code, then call back in to the simulator core and restart the simulation again. This is acceptable if you do it every few hundred or thousand milliseconds or so, but with an input that may change every few milliseconds this becomes very, very slow.

A much better approach is to forgo the use of the NEST Poisson generator. Generate a Poisson sequence of spike events in the outside process, and send the spike events directly into the simulation like we did in our earlier Python example. This is far more effective, and the outside process is not limited to the generators implemented in NEST but can create any kind of spiking input. In the next section we will take a look at how to do this.