Live Probes during simulation

Hi there,

I am trying to use sim.data[probe] during the simulation, as I need to feed one function with the live output for some kind of reset.
Unfortunately it turns out that sim.data[probe] is always empty during the simulation and it seems like it only get mapped at the end of the simulation such that the data is provided afterwards.
If needed, I could of course provide some code but actually my general question is just:
Is there any way I could access the probes live while the simulation is stil running?

Thanks in advance!

Hi @MarieFie and welcome!

The answer to your question is yes! You can use sim.step() to progress the simulation by one step, access sim.data[probe], and then do what you need to do with its contents. Like so:

with nengo.Simulator(model) as sim:
    for step in range(10):
        sim.step()
        # ... do stuff here with sim.data[probe]

To provide a somewhat contrived example, here is one way of using the probe data to update the input to the simulation in between each step:

import nengo
import numpy as np

live_value = np.zeros(1)  # mutated in between simulation steps

with nengo.Network() as model:
    stim = nengo.Node(output=lambda t: live_value)
    x = nengo.Node(size_in=1)
    nengo.Connection(stim, x, synapse=None)
    
    p = nengo.Probe(x)
    
with nengo.Simulator(model) as sim:
    for step in range(10):
        sim.step()
        # Increment the value by 0.1 each step
        live_value[:] = sim.data[p][-1] + 0.1

print(sim.data[p])

However, this is a pattern that I would actually discourage as it’s not the “Nengo way” of doing things. A much better (easier to read and extend) would be to use a nengo.Node and/or function to close the loop inside of the simulation itself. Here’s an example of how you can rewrite the above example in such a manner:

import nengo

with nengo.Network() as model:
    stim = nengo.Node(size_in=1, output=lambda t, x: x)
    x = nengo.Node(size_in=1, output=lambda t, x: x)
    nengo.Connection(stim, x, synapse=None)

    # Close the loop within the simulation itself
    # synapse=0 is a one step delay
    nengo.Connection(x, stim, synapse=0, function=lambda x: x + 0.1)
    
    p = nengo.Probe(x)
    
with nengo.Simulator(model) as sim:
    sim.run_steps(10)

print(sim.data[p])

Note that both snippets of code produce the same output. The second version is preferred. To apply some more complicated function when closing the loop, you can replace the connection with something like:

def my_func(x):
    # ... do stuff here to transform x into the input for the next step
    return next_input

nengo.Connection(x, stim, synapse=0, function=my_func)

Note that for this function to be computed ideally (i.e., perfectly, and not using neurons), x needs to be a nengo.Node. You can always put whatever information you need into a nengo.Node, and then compute the function you need as shown here or using the function on the node itself as in nengo.Node(..., output=my_func).

Hope this helps. If you run into another roadblock it may help to post a minimal example showing the issue. Thanks!

1 Like

Thanks a lot! This was exactly what I was looking for.
Indeed I didn’t know that a stepwise simulation was possible. Thank you also for pointing out the different opportunities s.th. I now understood why the second solution is better.

Unfortunately now that I’ve created the additional node and connected it to the pos ensemble, it strongly influences my simulation results. Could you maybe explain why this is the case and by chance have an idea how to solve it?

So the basic idea of the project is to build a bioinspired solution of the SLAM problem in Nengo. At the moment I am working at the reset/refresh of the internal map whenever a specific landmark is seen (which could be seen as one property of place cells). For now, there is only the head direction ensemble (hd) which keeps track of the robots head direction and the position network (pos) which kind of represents the robots actual position but also have some grid cell properties.

The model looks like the following:

#Model
#---------------

model = nengo.Network(seed = seed)

with model:
#Nodes and Ensembles

#input nodes
input_node_hd = nengo.Node(input_hd)
initialise_node_hd = nengo.Node(initialise_hd, size_in = dim_hd)

input_node_pos = nengo.Node(input_pos)
initialise_node_pos = nengo.Node(initialise_pos, size_in = dim_pos)

#hd ensemble keeps track of the head direction
hd = nengo.Ensemble(neurons_hd, dimensions=dim_hd, 
                        eval_points=evalp_hd,
                        encoders=encoders_hd, 
                        radius = radius_hd,
                        noise=None)
   
#pos ensemble keeps track of the x and y coordinates
pos = nengo.Ensemble(neurons_pos, dimensions=dim_pos, 
                        eval_points=evalp_pos,
                        encoders=encoders_pos, 
                        radius = radius_pos,
                        noise=None)

hd2pos = nengo.Ensemble(neurons_hd, dimensions=3, 
                        radius = np.sqrt(3),
                        noise=None)


#added to receive live data from probe
x = nengo.Node(size_in=dim_pos, output=lambda t, x:x)


#Connections

#connect inputs
nengo.Connection(initialise_node_hd, hd, synapse = None)
nengo.Connection(hd, initialise_node_hd, synapse = tau)
nengo.Connection(input_node_hd, hd, synapse=tau)

nengo.Connection(initialise_node_pos, pos, synapse = None)
nengo.Connection(pos, initialise_node_pos, synapse = tau)
nengo.Connection(input_node_pos, hd2pos, synapse=tau)

#recurrent connections
nengo.Connection(hd, hd, function = rotate_hd, eval_points=evalp_hd, synapse=tau, scale_eval_points=False)
nengo.Connection(pos, pos, function = rotate_pos, eval_points=evalp_pos, synapse=tau, scale_eval_points=False)
 
#connect pos and hd network
nengo.Connection(hd, hd2pos, function = decode_hd, synapse=tau, scale_eval_points=False)
nengo.Connection(hd2pos, pos, function = convert_hd, synapse=tau) 

#added to receive live data from probe
pos2probe = nengo.Connection(pos, x, synapse=None)
probe2pos = nengo.Connection(x, pos, synapse=0, function=lambda x: x+0.001)

    
#Probes
hdp = nengo.Probe(hd, synapse=tau)
#hdspp = nengo.Probe(hd.neurons, 'spikes', synapse=tau)
posp = nengo.Probe(pos, synapse=tau)
#posspp = nengo.Probe(pos.neurons, 'spikes', synapse=tau)

#added to receive live data from probe
p = nengo.Probe(x)

So I added the 4 lines of code #added to receive live data from code and now my plot of the Rotating Fourier Components of the POS network is changing from the top to the bottom plot:

Can you help me why is there such a huge influence coming from the new node I’ve created and how can I avoid this? Yet I have only created the node, connected it and set the probe, I’ve not integrated the data of the probe anywhere, so I’m not sure where the changes are coming from.
Actually I already had a similar problem some time ago when adding a new ensemble has changed the complete behaviour of the other ensembles without being connected to them.

And, I don’t know if this is important but I am working with nengo_loihi.

Thanks a lot! I would appreciate any kind of tips / explanations.

Hi @MarieFie,

I looked over your code, and I think there may have been a misunderstanding between how @arvoelke has implemented his example, and what you wanted to achieved from your Nengo model. First, let me explain what @arvoelke’s code is doing, and then I’ll go into how to modify it to do what I believe you at trying to do.

Arvoelke’s code

This is the second version of the code (which is easier to follow, but as he mentioned, both versions of his code do the same thing:

with nengo.Network() as model:
    stim = nengo.Node(size_in=1, output=lambda t, x: x)
    x = nengo.Node(size_in=1, output=lambda t, x: x)
    nengo.Connection(stim, x, synapse=None)

    # Close the loop within the simulation itself
    # synapse=0 is a one step delay
    nengo.Connection(x, stim, synapse=0, function=lambda x: x + 0.1)

This code demonstrates 2 things:

a. How to feed live (modified) data back into a Nengo model.
b. How to read (and apply a function to) live Nengo data.

Both of these functions are achieved with this single line of Nengo code:

nengo.Connection(x, stim, synapse=0, function=lambda x: x + 0.1)

so, let’s break it down. The creation of the connection from x to stim (i.e., nengo.Connection(x, stim)) allows Nengo to take the live data output from the x node and feed it back to the stim node (achieving a), while the addition of the function parameter to the connection directs Nengo to apply the lambda x: x + 0.1 function to this feedback connection.

Now, what does this model compute? Well, the flow of information is as follows:

  • A value goes from stim to x, with no transformation.
  • Then it goes from x back to stim with the function x = x + 0.1 applied to it.

This means that on every time step, the value fed back to stim will be the output value of stim + 0.1 (i.e., it is a monotonically increasing line, stepping up in value by 0.1 every time step).

Generalizing the example

We can take @arvoelke’s network and generalize the structure into something to be applied to other models. Let us suppose that a general Nengo model consists of these components:

  • A nengo.Node that serves to provide a stimulus signal to the rest of your model.
  • A nengo.Ensemble that performs some computation on the stimulus signal (in the examples below, I’m going to use just 1 ensemble, but in reality, this can be a multi-layered network of ensembles).
  • A Nengo object that serves to provide an output signal (this can be one of the nengo.Ensembles mentioned above)

The general structure of the model is thus:

with nengo.Network() as model:
    stim = nengo.Node(stim_func)
    ens = nengo.Ensemble(n_neurons, dim, ...)  # Also the "output" of the model
    nengo.Connection(stim, ens)

Probing live data

Your original problem was that you wanted to work with the “live” data from the ens output while the Nengo simulation is running. How would we go about doing this? For the purpose of the example below, let us consider that you have the function process_data() that is used to process the model’s output data while the simulation is running, and you want to connect it into the Nengo model framework.

As @arvoelke mentioned above, you can do so by doing the following.
First, we create a nengo.Probe to record the output of ens, like so:

with model:
    p_out = nengo.Probe(ens)

Then, in the nengo.Simulator block, instead of calling sim.run(sim_time), we instead run the simulation for the equivalent number of timesteps, but manually calling the sim.step() in each loop iteration:

with nengo.Simulator(model) as sim:
    for step in range(n_timesteps):
        # Step the simulation by one time step
        sim.step()  
        # Call `process_data()` with the data collected by the `p_out`
        # probe
        process_data(sim.data[p_out])  

However, while this method of using live data has its uses in more complex model setups, there are a couple of caveats to note. First (as mentioned by @arvoelke), this isn’t the “Nengo way” of doing things. Second, the values returned by sim.data[p_out] is cumulative over the simulation run (or however much the simulation has run whenever sim.data[p_out] is accessed. What this means is that if the simulation has been run for 10 timesteps, sim.data[p_out] will contain 10 timesteps worth of data. This has an additional two effects:

  1. The process_data() function needs to be aware of this, and only access the last element (if desired) of the sim.data[p_out] values.
  2. Because Nengo keeps all of the probed data until the simulation is ended, long simulation runs can run into memory issues as more and more of it is used to store the probed data.

The more Nengo way

Instead of using the nengo.Probe's and manually calling sim.step(), we can instead take advantage of nengo.Node's operate within a Nengo simulation to achieve our desired live data processing. nengo.Node's are multi-purpose, and when it is provided with a function as the output parameter will cause the Nengo simulator to call that function on every timestep of the simulation (which is half of what we want to achieve!). And, if you provide a value for the size_in parameter, the output function of a nengo.Node will be called on whatever values or signals are connected to the nengo.Node. Let us re-examine the generic nengo model and the process_data() function for a more concrete example.

To our model, we need to add a nengo.Node with the following parameters:

  • output=process_data, so that we can call the process_data() on every timestep of the Nengo simulation.
  • size_in=dim, so that we can connect the output of ens to the nengo.Node.

And finally, we need to connect the output of ens to our new nengo.Node.
Putting all of this together, we get:

with model:
    # Make the node to call `proces_data()`
    process_node = nengo.Node(output=process_data, size_in=dim)
    # Connect the output of `ens` to `process_node`

Note that there is one important change you will have to make to the process_data() function. Functions provided to nengo.Node's have to take 2 parameters (if size_in is specified):

  • t: The parameter that represents the current time stamp of the simulation.
  • x: A vector array of size size_in that is the value of the signal connected to the nengo.Node.

Here’s an example of a process_data() function that simply prints the received output to the screen at every time step:

def process_data(t, x):
    print("Timestamp:", t, " x value:", x)

Modifying your code

Using the information above, here are the specific modifications you’ll need to make to the code you included in your post above:

  1. Modify the nengo.Node that processes the “live data from probe”, from:

    x = nengo.Node(size_in=dim_pos, output=lambda t, x: x)
    

    to

    x = nengo.Node(size_in=dim_pos, output=<function that processes your data>)
    
  2. Remove the probe2pos connection, from:

    # added to receive live data from probe
    pos2probe = nengo.Connection(pos, x, synapse=None)
    probe2pos = nengo.Connection(x, pos, synapse=0, function=lambda x: x + 0.001)
    

    to

    # added to receive live data from probe
    pos2probe = nengo.Connection(pos, x, synapse=None)
    

Feeding live data back to model after processing it

I got a sense that in your original query you wanted to process (and act on) the live Nengo data outside of the Nengo simulation, and that the processed output of your Nengo model was not going to be fed back as an input to your model. If you wanted to achieve this functionality (as was the case with @arvoelke’s example), here are the steps you will need to take to do this:

  1. Modify the process_data() function such that it returns a value. This value does not have to be the same dimensionality as the input.
  2. Add the input_size parameter to the stimulus node that requires the fed-back signals. The size of input_size should be the same as the dimensionality as the output of process_data().
  3. Create a nengo.Connection between the process_node and the stim node, with a synapse=0.

Re-creating Arvoelke’s example

If you are interested, all of the steps above can be put together to re-create @arvoelke’s example. First, this is the “generic” Nengo model of his example:

with nengo.Network() as model:
    stim = nengo.Node(0)
    x = nengo.Node(size_in=1, output=lambda t, x: x)
    nengo.Connection(stim, x, synapse=None)

Next, we define the “process” function, that adds 0.1 to an input value.

def process_data(t, x):
    x = x + 0.1

And then we create the “process” node to call the process_data() function, and the necessary Nengo connections:

with model:
    proc_node = nengo.Node(size_in=1, output=process_data)
    nengo.Connection(x, proc_node, synapse=None)

Now, because we need to feed the processed data back to stimulus, we need to redefine the process_data() function to actually returned the modified value of x:

def process_data(t, x):
    x = x + 0.1
    return x

And in order for stim to be able to receive the output of proc_node, we’ll need to make the size_in changes, as well as the synapse=0 connection. Thus the model is redefined as:

with nengo.Network() as model:
    stim = nengo.Node(size_in=1)
    x = nengo.Node(size_in=1, output=lambda t, x: x)
    nengo.Connection(stim, x, synapse=None)

    proc_node = nengo.Node(size_in=1, output=process_data)
    nengo.Connection(x, proc_node, synapse=None)

    # Feedback connection
    nengo.Connection(proc_node, stim, synapse=0)

Looking at the code above, you’ll notice that it isn’t quite the same as what @arvoelke had created. That’s because we can make some simplifications to the model:

  • Because the process_data() function is simple, we can replace it with a lambda function. Namely: lambda t, x: x + 0.1
  • Because both x and stim (i.e., the Nengo objects connected to proc_node) are nengo.Node's, we can remove proc_node entirely, and do the process_data() function directly on a connection from x to stim.

Making both of these changes results in this code:

with nengo.Network() as model:
    stim = nengo.Node(size_in=1)
    x = nengo.Node(size_in=1, output=lambda t, x: x)
    nengo.Connection(stim, x, synapse=None)

# Remove unused code
#    proc_node = nengo.Node(size_in=1, output=process_data)
#    nengo.Connection(x, proc_node, synapse=None)

    # Feedback connection
    nengo.Connection(proc_node, stim, synapse=0, function=lambda t, x: x+ 0.1)

which is then identical to @arvoelke’s original code.

If you have any questions specific to your code (e.g., how to get your specific process_data() function working), do post a reply, and I’ll do my best to help! :smiley:

2 Likes

Thanks for the detailed explanation, indeed I just had the memory issue when running a huge simulation using Nengo and I wanted to store the Probes data into an external file, is it possible to capture information like synaptic weights using Nodes also ? or we need to use the stepwise approach.

This is how the memory keeps rising when collecting information using Probes, until the process is stopped :

Capture d’écran_2020-10-16_10-08-30

1 Like

With the help of another thread here, I managed to preserve only the last captured data each time in order to avoid the memory issue, and save the rest of it to the drive for later analysis, by using the Node and sim._sim_data.

Here is the code maybe someone will find it useful:

DataLog.py

import os
from datetime import datetime
import matplotlib.pyplot as plt
import numpy as np

class DataLog:

"""
Class to collect data from simulation and store it somewhere
"""

def __init__(self):
    self.sim = None
    self.f = None

def set(self,sim,path):
    """
    Set the simulation argument

    Parameters
    ----------
    sim : Simulator
        Simulator argument
    path : str
        Path where to save the log file
    """
    self.sim = sim
    self.f = open(path,"w")

def storeToFile(self,label,data):
    """
    Store the log to the file
    """
    i = 0
    for d in data[0]:
        self.f.write(label+":"+str(i)+":"+str(d)+"\n") # store it the way you like
        i = i + 1
    
def closeLog(self):
    """
    Close log file 
    """
    self.f.close()

def __call__(self, t):
     """
     This is called each step and preserve only the last stored info
     """
    if self.sim is not None:
        assert len(self.sim.model.probes) != 0 , "No Probes to store"

        for probe in self.sim.model.probes:
            if len(self.sim._sim_data[probe]) != 0: 
                self.sim._sim_data[probe] = [self.sim._sim_data[probe][-1]] # preserving the last one

                    self.storeToFile(str(t)+probe.label,self.sim._sim_data[probe])

and we can use it this way :

.....
log = DataLog()
with model:
      stim = nengo.Node(0)
      L1 = nengo.Ensemble(10, 1)
      conn1 = nengo.Connection(stim,L1)
      # add the probes ...
      # add another node for the log
      nengo.Node(log)

with nengo.Simulator(model) as sim:
      log.set(sim,"Log.txt") # set the log file name
     sim.run(50)

# finally close the file
log.closeLog()

Unfortunately, for the synaptic weights, you’ll have to use the nengo.Probe to probe it, or to set up your simulation to manually step through (and read from the model.params[conn].weights parameter) the simulation.

A big thank you for your detailed explanation - it not only helped me to solve the problem but also to gain a much better understanding.
Thanks a lot to everyone for the great support within this forum!