Hi @nrofis,
Both implementations are correct, and the differences in the implementations come down to how synapses are applied when a nengo.Connection
is created.
Before I continue, just a brief explanation on the effect the input tau
has on the integrator output. The input tau
is the value assigned to the synapse on the connection from the input node to the integrator, i.e., this connection:
# Connect the input
nengo.Connection(
input, A, transform=[[tau]], synapse=tau
) # The same time constant as recurrent to make it more 'ideal'
There are two ways to implement this connection:
- Set the synapse value to
tau
, as is done in the example.
- Leave the synapse value to the default (i.e., don’t include the
synapse=tau
code at all).
If we compare two identical integrators, with this being the only change, this is the result:
As you can see from the plot above, when the input synapse matches the time constant of the feedback synapse, the output of the integrator is more “ideal”. However, if the synapse is left as the default value, the output of the integrator “jumps” whenever the input changes. Because the output of the integrator is input + feedback
, when the input and feedback time constants are not matched, there is a temporal difference between changes to the input and changes to the feedback signal (the input signal changes quicker than the feedback does) which cause the “jumps” in the output of the integrator.
Integrator Network Input Synapse
Now, an explanation to why the input synapse of the built-in integrator network is None
instead of tau
.
All of the built-in networks in Nengo follow similar interfaces. Regardless of how complex the network is, to make it straightforward for users to connect to and from the network, each network is standardized to have a .input
and a .output
object (these objects are typically nengo.Node
objects).
When you connect to these networks then, you are actually connecting to the input and output objects. This, however, poses an issue. Consider, the integrator network as an example:
input = nengo.Node(size_in=dimensions)
ensemble = nengo.Ensemble(n_neurons, dimensions=dimensions)
nengo.Connection(ensemble, ensemble, synapse=recurrent_tau)
nengo.Connection(
input, ensemble, transform=recurrent_tau, synapse=None
)
output = ensemble
If you were to connect to the integrator network, for example like so:
my_input = nengo.Node(lambda t: sin(t))
nengo.Connection(my_input, integrator.input)
this is actually what is happening under the hood:
input = nengo.Node(size_in=dimensions)
ensemble = nengo.Ensemble(n_neurons, dimensions=dimensions)
nengo.Connection(ensemble, ensemble, synapse=recurrent_tau)
nengo.Connection(
input, ensemble, transform=recurrent_tau, synapse=None
)
output = ensemble
my_input = nengo.Node(lambda t: sin(t))
nengo.Connection(my_input, input)
Now, if you examine the code carefully, you’ll see that there are actually two connections between my_input
and the integrator’s ensemble
:
my_input --> input --> ensemble
And, for each of these connections, a synapse can be assigned to it.
Suppose the integrator network’s input synapse is configured to tau
by default. With these two connections, the connection chain would look like so:
0.005 tau
my_input --> input --> ensemble
where the first connection would have the default synaptic time constant of 0.005s, and the second connection would have the synaptic time constant of tau
. This would produce an integrator behaviour that definitely does not match the reference network.
To avoid this, the built-in networks in Nengo are thus configured such that all input synapses (for networks where the input synapse is not required to add some additional smoothing to the input signal) to have a synapse value of None. This allows the user to specify their own connection synapse value and be sure that the network behaviour reflects that.