This demo implements a controlled one-dimensional neural integrator that is functionally the same as the controlled integrator in the previous example. However, the control signal is zero for integration, less than one for low-pass filtering, and greater than 1 for saturation. This behavior maps more directly to the differential equation used to describe an integrator: $\dot{x} = \mathrm{Ax}(t) + \mathrm{Bu}(t)$ The control in this circuit is A in that equation. This is also the controlled integrator described in the book “How to build a brain.”
In [ ]:
import nengo
model = nengo.Network(label='Controlled Integrator 2')
with model:
# Make a population with 225 LIF neurons representing a 2 dimensional signal,
# with a larger radius to accommodate large inputs
A = nengo.Ensemble(nengo.LIF(225), dimensions=2, radius=1.5)
We will be running 1 second of simulation time again, so we will use the same Python function input_func to define our input signal. This piece-wise function sits at 0 until .2 seconds into the simulation, then jumps up to 5, back to 0, down to -10, back to 0, then up to 5, and then back to 0. Our integrator will respond by ramping up when the input is positive, and descending when the input is negative.
In [ ]:
from nengo.utils.functions import piecewise
with model:
# Create a piecewise step function for input
input_func = piecewise({0.2: 5, 0.3: 0, 0.44: -10, 0.54: 0, 0.8: 5, 0.9: 0})
inp = nengo.Node(output=input_func)
# Connect the Input signal to ensemble A.
tau = 0.1
nengo.Connection(inp, A, transform=[[tau], [0]], synapse=0.1)
The control signal will be 0 for the first part of the simulation, and -0.5 for the second part. This means that at the beginning of the simulation, the integrator will act as an optimal integrator, and partway though the simulation (at t = 0.6), it will switch to being a leaky integrator.
In [ ]:
with model:
#Another piecewise function that changes half way through the run
control_func = piecewise({0: 0, 0.6: -0.5})
control = nengo.Node(output=control_func)
# -- Connect the "Control" signal to the second of A's two input channels
# using the `transform` matrix.
nengo.Connection(control, A, transform=[[0], [1]], synapse=0.005)
In [ ]:
with model:
# Note the changes from the previous example to the function being defined.
nengo.Connection(A, A,
function=lambda x: x[0] * x[1] + x[0],
transform=[[1], [0]],
synapse=tau)
# Record both dimensions of A
A_probe = nengo.Probe(A, 'decoded_output', synapse=0.01)
In [ ]:
sim = nengo.Simulator(model) # Create a simulator
sim.run(1.4) # Run for 1.4 seconds
In [ ]:
import matplotlib.pyplot as plt
import numpy as np
# Plot the value and control signals, along with the exact integral
t = sim.trange()
dt = t[1] - t[0]
input_sig = list(map(input_func, t))
control_sig = list(map(control_func, t))
ref = dt * np.cumsum(input_sig)
plt.figure(figsize=(6, 8))
plt.subplot(2, 1, 1)
plt.plot(t, input_sig, label='Input')
plt.ylim(-11, 11)
plt.ylabel('Input')
plt.legend(loc="lower left", frameon=False)
plt.subplot(212)
plt.plot(t, ref, 'k--', label='exact')
plt.plot(t, sim.data[A_probe][:,0], label='A (value)')
plt.plot(t, sim.data[A_probe][:,1], label='A (control)')
plt.ylim([-1.1, 1.1])
plt.xlabel('Time (s)')
plt.ylabel('x(t)')
plt.legend(loc="lower left", frameon=False)
The above plot shows the output of our system, specifically the (integrated) value stored by the A population, along with the control signal represented by the A population. The exact value of the integral, as performed by a perfect (non-neural) integrator, is shown for reference.
When the control value is 0 (t < 0.6), the neural integrator performs near-perfect integration. However, when the control value drops to -0.5 (t > 0.6), the integrator becomes a leaky integrator. This means that with negative input, its stored value drifts towards zero.