Synapse of integrator

I’m trying to implement an integrator in Nengo.

I followed your documentation, but in step 3 you mention that the synapse of the connection between the input and the ensemble is tau, which is reasonable.

But then I find that you have already implemented such integrator built in in Nengo. But in its implementation you set that synapse as None.

Which implementation is the correct one?

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.

Thank you for your detailed explanation @xchoo. Actually,

very interesting. I will test it to see what the difference is.

I now understand the logic behind. But I have a problem with this:

Since such a “contract” between Nengo and the users leads to not intuitive results when composite models.

Say I want to implement a PI controller (PID without the D) and I write a new network for it, something like:

input = nengo.Node(size_in=1)
output = nengo.Node(size_in=1)
proportional = nengo.Ensemble(n_neurons, dimensions=1)
integrator = nengo.networks.Integrator(tau, n_neurons, 1)

nengo.Connection(input, proportional, synapse=None) # Using synapse=None like Nengo
nengo.Connection(input, integrator.input, synapse=???) # What is the synapse value here?

... # Rest of connections to output

In that case, there is a problem choosing the right synapse. If we were to connect to the PI network, like we did before:

my_input = nengo.Node(lambda t: sin(t))
nengo.Connection(my_input, pi.input, syanpse=???)

The connections are:

         ???          ???                 None
my_input --> pi.input --> integrator.input --> integrator.ensemble
                      \
                       \--> proportional
                       None

There are two options:

1. Using None in the input synapse

If I follow Nengo’s contracts and use None in the synapse between pi.input and integrator.input, as you mentioned, the user will be responsible to use the correct tau, but that tau will also affect the proportional section, which is not desired:

         tau         None                 None
my_input --> pi.input --> integrator.input --> integrator.ensemble
                      \
                       \--> proportional
                       None

2. Using tau in the input synapse

But if we use tau in the synapse between pi.input and integrator.input (like the Integrator network designed for), the user will have to use None outside, which is what you tried to avoid in your Integrator implementation:

        None          tau                 None
my_input --> pi.input --> integrator.input --> integrator.ensemble
                      \
                       \--> proportional
                       None

But in this case you should also set the synapse value between the pi.input and proportinal, otherwise, the synapse value is None all the way…

Both ways are confusing. But using the rule that “input synapses have a synapse value of None”, makes it hard to composite models without breaking this rule.

Yes, it is, and for this reason we only do this for networks that are relatively simple:

We do this for simple networks to allow the user the flexibility to use the networks as they see best for their networks. As an example, if we were to set the input synapse on the integrator, this will restrict the user from setting their own input synapse for the integrator.

For more complex networks (e.g., the basal ganglia, thalamus networks), we tend to treat the network as a black box. That is to say that the synapses and parameters of the network are set in such a way that all the user has to do is connect to it, and it will perform the function as described.

The implementation of the PI controller would fall under the latter category. Since the PI controller has two parts (the P and I terms) that work in unison to perform a function, you’ll want to treat the system as a whole. Using your network diagram, there are then a few synapses you’ll need to determine:

         ???          ???                  None
my_input --> pi.input --> integrator.input --> integrator.ensemble
                      \
                       \--> proportional
                       ???

There are a few network configurations you can use to build this PI network:

The “ideal” network

         ???          tau                  None
my_input --> pi.input --> integrator.input --> integrator.ensemble
                      \
                       \--> proportional
                       None

This network configuration is what I would consider the “ideal” network. That is to say, if you were to implement the network without any neurons the behaviour you get from this network will be close to ideal. With the input synapse to integrator.input set to tau, the integrator sub-network is “ideal”, and without any neurons to worry about, the synapse to proportional can be None since no filtering is applied.

Dealing with Neurons
There are two ways you can deal with the neuron spiking noise (and the need for filtering) with such a network. The first approach would be to leave the network as it is configured above:

         def          tau                  None
my_input --> pi.input --> integrator.input --> integrator.ensemble
                      \
                       \--> proportional
                       None

and allow the user to specify the synapse to the entire PI network. This thus presents a filtered input to pi.input and the entire PI network will work on this filtered signal. It’s true that in this case, the integrator sub-network will essentially operate with 2 synapses (one default, one tau), but when you consider the overall behaviour of the PI network, mathematically, it is still correct (since the input presented to both the P and I terms are the same).

Another way to deal with neuron spiking noise is to specify that the user has to connect to the PI network with a synapse of None, and then setting the synapse on proportional to whatever you need it to be (I set it to the default in the diagram below):

         None         tau                  None
my_input --> pi.input --> integrator.input --> integrator.ensemble
                      \
                       \--> proportional
                       def

In this instance, the integrator will be more “ideal” but there will be a slight lag between the proportional and integrator values since they are operating on differently filtered signals.

Both approaches are valid, and the differences in behaviour would be minor, but personally, I would implement the first approach.

@xchoo thank you again for your detailed explanation! I will go with the first approach and try it as well.

That’s right, my mistake. I will update my original post