Algorithms in Nengo: Path Planning#

This tutorial covers the following:

  • Delay Nodes used to represent difficulty of traversal for each map location

  • Hints on how to implement inhibition (turn neurons off once a map location has fired)

  • How to build your AER

Set up#

Ensure you are using your 495 Virtual Environment before you begin!

Import Nengo and other supporting libraries into your program to get started:

import matplotlib.pyplot as plt
import numpy as np

import nengo

from nengo.processes import Piecewise
from nengo.processes import WhiteSignal

Delays using Nengo Nodes#

We can use a Nengo node to implement an n-timestep delayed connection by using a node. Why a node? Nodes are useful for more complex functions than what we covered in Tutorial3b. Why a delay? Often times you want to line up inputs perfectly in time and a delay can make that happen. In the case of Path Planning, we want to use the delay to represent the cost of our map locations. If a map location is more difficult to traverse (i.e. higher cost), then it takes longer to get through - hence a delay. The spike won’t emit from that map location until the delay node allows it to do so.

This example uses object-oriented programming to set up the delay. You can find a pretty good tutorial on the topic here. You can also see how np.roll works here.

The class Delay starts off with an array called self.history of 250 zeros (each time step is dt=.001 seconds and we want a time_delay=25 seconds, which is in total 250 time steps). For the first actual time step of the system (\(t=1\)), the self.history array updates to \(\begin{bmatrix} 0 & 0 & ... & x(1) \end{bmatrix}\), but what leaves the node is the first element of the array, 0. This is what we want! We are delaying our signal while storing what will come 250 timesteps later. At the next time step (\(t=2\)), the self.history array updates to \(\begin{bmatrix} 0 & 0 & ... & x(1) & x(2) \end{bmatrix}\). Keep going until time step \(t=250\) and you should get the self.history array \(\begin{bmatrix} x(1) & x(2) & ... & x(249) & x(250) \end{bmatrix}\) and the first element \(x(1)\) is what leaves the node. That means at time step 250, you’re just now seeing the start of the original signal, i.e. you’ve delayed the signal by 250 timesteps!

model = nengo.Network(label="Delayed connection")
with model:
    # We'll use white noise as input
    inp = nengo.Node(WhiteSignal(2, high=5), size_out=1)
    A = nengo.Ensemble(40, dimensions=1)
    nengo.Connection(inp, A)


# We'll make a simple object to implement the delayed connection
class Delay:
    def __init__(self, dimensions, timesteps=50):
        self.history = np.zeros((timesteps, dimensions))

    def step(self, t, x):
        self.history = np.roll(self.history, -1)
        self.history[-1] = x
        return self.history[0]

dt = 0.001 # default nengo timestep
time_delay = .25 # seconds by which you wish to delay the signal
delay = Delay(1, timesteps=int(time_delay / dt))

with model:
    delaynode = nengo.Node(delay.step, size_in=1, size_out=1)
    nengo.Connection(A, delaynode)

    # Send the delayed output through an ensemble
    B = nengo.Ensemble(40, dimensions=1)
    nengo.Connection(delaynode, B)

    # Probe the input at the delayed output
    A_probe = nengo.Probe(A, synapse=0.01)
    B_probe = nengo.Probe(B, synapse=0.01)
# Run for 2 seconds
with nengo.Simulator(model) as sim:
    sim.run(2)

# Plot the results
plt.figure()
plt.subplot(2, 1, 1)
plt.plot(sim.trange(), sim.data[A_probe], lw=2)
plt.title("Input")
plt.subplot(2, 1, 2)
plt.plot(sim.trange(), sim.data[B_probe], lw=2)
plt.axvline(time_delay, c="k")
plt.title("Delayed output")
plt.tight_layout()

Discussion#

The delay nodes should be placed prior to the map locations they are delaying. Think through this and decide for yourself whether or not that makes sense.

If you aren’t convinced, consider the delay the difficulty of traversal. Example: a map location has a weight / difficulty of 3 which means it’ll take 3 seconds to get through said map location. If you place the delay just prior to the map location neuron, the map location will begin firing at the same(ish) time as the delay node. This occurs 3 seconds after it receives a spike from it’s neighbor. The output spike will then go to the delay nodes of it’s neighbors, which have delays of whatever the neighbor’s weights are. Rinse. Repeat.

Inhibition using Nengo Nodes#

To inhibit your neurons, you will want to create a new node that performs the inhibition activity. This will be a significant portion of your lab! It is important to understand how to use nodes for functions like this because every final project will likely require at least one - make sure you get this right and fully understand how you did it.

Some tips:

  • Once a map location begins spiking, it fires for a brief pulse, then stops firing

  • This means you first have a drastic increase (started_spiking = True). Prior, this was False

  • Once started_spiking = True, you should have a drastic decrease soon after (finished_spiking = True)

  • Once finished_spiking = True, you can just return 0 forever after (instead of x)!

Building your AER#

Creating a table of map locations and time of first spike isn’t as scary as it may seem at first glance. You know how to probe neurons, and you know how to plot the probed data. That means that at every time step of your simulation, your neuron outputs have a value. When said value hits that first rapid increase, you can find at what time this happens within the simulation by analyzing the amplitude and store that time for a particular map location. Let’s look at an example.

model = nengo.Network(label="AER")
with model:

    inp_node = nengo.Node(Piecewise({0: 0, .25: 1, .5: 0})) # quick spike into start node

    inp_neuron = nengo.Ensemble(100, 1)

    nengo.Connection(inp_node,inp_neuron)

    neuron_probe = nengo.Probe(inp_neuron, synapse=0.01)

# Build the simulator to run the model containing just input encoding
with nengo.Simulator(model) as sim:
    # Run it for 1 second
    sim.run(1)

# Plot the decoded input from the neuron
plt.figure()
plt.plot(sim.trange(), sim.data[neuron_probe], "r")
plt.xlabel("time [s]")
plt.title("Neuron Output")

From here, let’s figure out where the first spike occurs using sim.data[neuron_probe] (should be around .25 seconds).

All of this happens OFF-NEURON, i.e. we don’t build the map or find our optimal path within our Nengo model. We do this in regular Python-land.

# note that sim.data[neuron_probe] is an Nx1 array, 
#  which means there's a second dimension we don't care about.
#  The spike data occurs in dimension 0. Grab that one.
aer = sim.data[neuron_probe][:,0]>.25
spikes_locs = np.where(aer == True)
first_spike = np.min(spikes_locs)

print("This is time step where the pulse first begins: ", first_spike)

dt = .001 # default nengo timestep
print("This time step occurs at approximately ", first_spike*dt, " seconds")

Discussion#

Build your AER table for every neuron in your map. Then using your map, work backwards (end location to start location) to find your optimal path!

There is an alternate way of finding your first spike from each map location! Instead of using output spikes to determine when a node first spiked, you can store your first spike for each map location in your inhibition node and grab that data later via object oriented programming (something like inhib.first_spike). This method would be cleaner and easier (no crazy for loops or issues with neuron noise), but you do what is most intuitive to you.