I didn’t want to make the previous post overly long, so here’s a follow up post! 
The code I posted in the previous post is the “simple” first-pass implementation of the logic function but as you have observed, near the discontinuity, the approximated function breaks down quite a bit because of the decoder’s inability to approximate such a sharp function. However, with a slightly more advanced knowledge of Nengo, one can build a more faithful implementation of the function you specified.
If you examine the function you want to represent, part of the function specifies that the output should be 0 if a certain condition is met. In Nengo, one can use inhibitory gating to quiet (set to 0) the output of any neural ensemble regardless of the input it is being provided, thus all that is required to implement the desired function is to inhibit the output of C when the threshold conditions of B are not met.
Doing this, however, is a little complex. The first step is to figure out what the inhibitory signal needs to be given the value of B. From the logic formulation, the output of C should be 0 if abs(B) \leq threshold. Since the inhibitory signal needs to have a non-zero value when the inhibition is desired, we need to create a network that will output a non-zero value when abs(B) \leq threshold. Creating an ensemble that outputs a value (e.g., 1) only when the input is above a certain threshold is not too difficult. Here we can take advantage of the neuron encoders and intercepts to achieve this:
with nengo.Network() as model:
inhib = nengo.Ensemble(
50, 1,
intercepts=nengo.dists.Uniform(0, 0.1),
encoders=nengo.dists.Choice([[1]]),
)
inhib_out = nengo.Node(size_in=1)
nengo.Connection(inhib , inhib_out, function=lambda x: 1)
In the code above, the ensemble is configured such that all of the encoders are in the positive direction. This ensures that all of the neurons in the ensemble respond in a similar manner w.r.t to the sign of the input (i.e., no neurons will be active when x=-1, and all neurons will be active when x=1). Next, the intercepts are set be between [0, 0.1). This means that the neurons in the ensemble will only be active when the input to the ensemble (x) is greater than 0. Since we are not concerned with the output, and only that it has to be a non-zero value, we squish the intercepts of the neurons to be in the small range to maximum the representation of the sharp discontinuity at the threshold value. Finally, we apply an output function of lambda x: 1
to ensure that when the ensemble is active, some non-zero output is produced.
The threshold ensemble above only implements the function inhib\_out= 1 \text{ if } x \geq 0. To implement the function inhib = 1 \text{ if } abs(B) \leq threshold, we need another ensemble. This ensemble will represent the value of B, and approximate the function abs(B):
with model:
ensB = nengo.Ensemble(30, 1)
nengo.Connection(ensB, inhib, function=lambda x: -abs(x) + thresh)
In the code above, we do a little bit of math to connect the value of ensemble B to the inhibitory threshold ensemble. Since the inhibitory threshold ensemble is active when it’s input is \geq 0, if we provide it the input value of -abs(B) + thresh, this will ensure that for all values of B < thresh, the input to the inhibitory ensemble will be \leq 0, and for all values of B > thresh, the input to the inhibitory ensemble will be \geq 0, as desired.
Next, we’ll need to construct the rest of the network. This part is relatively straightforward. Looking at the desired logic function, C = A when it is not 0. Thus, we’ll need an ensemble to “pass through” the value of A to the output C:
with model:
ensA = nengo.Ensemble(30, 1)
ensC = nengo.Ensemble(30, 1)
nengo.Connection(ensA, ensC)
Finally, to have this “pass through” ensemble output 0 when desired, we just need to do the inhibitory connection:
with model:
nengo.Connection(inhib_out, ensC.neurons, transform=[[-20]] * ensC.n_neurons)
In the code above, we put a large weight on the inhibitory transform just to ensure that the inhibition takes place even if the output of inhib_out
is weak.
Putting all of this code together, we get this:
import matplotlib.pyplot as plt
import nengo
import numpy as np
thresh = 0.5
with nengo.Network() as model2:
inpA = nengo.Node(lambda t: np.cos(t * 3 * np.pi))
inpB = nengo.Node(lambda t: t - 1.5)
out = nengo.Node(size_in=1)
inhibout = nengo.Node(size_in=1)
ensA = nengo.Ensemble(30, 1)
ensB = nengo.Ensemble(30, 1)
inhib = nengo.Ensemble(
50,
1,
intercepts=nengo.dists.Uniform(0, 0.1),
encoders=nengo.dists.Choice([[1]]),
)
ensC = nengo.Ensemble(30, 1)
nengo.Connection(inpA, ensA)
nengo.Connection(inpB, ensB)
nengo.Connection(ensB, inhib, function=lambda x: -abs(x) + thresh)
nengo.Connection(ensA, ensC)
nengo.Connection(
inhib, ensC.neurons, transform=[[-20]] * ensC.n_neurons, function=lambda x: 1
)
nengo.Connection(inhib, inhibout, function=lambda x: 1)
nengo.Connection(ensC, out)
p_ina = nengo.Probe(inpA)
p_inb = nengo.Probe(inpB)
p_inh = nengo.Probe(inhibout, synapse=0.005)
p_out = nengo.Probe(out, synapse=0.005)
with nengo.Simulator(model2) as sim2:
sim2.run(3)
plt.figure()
plt.plot(sim2.trange(), sim2.data[p_ina])
plt.plot(sim2.trange(), sim2.data[p_inb])
plt.plot(sim2.trange(), sim2.data[p_inh])
plt.plot(sim2.trange(), sim2.data[p_out])
plt.plot([0, 3, 0, 0, 3], [thresh, thresh, None, -thresh, -thresh], "--")
plt.legend(["A", "B", "Inhib", "C", "Thresh"])
plt.show()
Note that in the code above, the inhibout
node isn’t really necessary since I’ve put the function=lambda x: 1
configuration in the connection from inhib
to ensC.neurons
.
And this produces this output, which is more faithful to the original function.
You can play around with the number of neurons in the inhibitory ensemble, the intercept values (currently 0 to 0.1), the inhibitory weight (currently at -20), and the inhibitory connection synapse (currently default) to get different responses to the inhibition. I recommend doing some exploration. 