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.Ensemble
s 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:
- 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.
- 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:
-
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>)
-
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:
- Modify the
process_data()
function such that it returns a value. This value does not have to be the same dimensionality as the input.
- 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()
.
- 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!