This is going to be a pretty niche post, but there is some great work by Aaron Voelker from my old lab that has inspired me to do a post. The work is from an upcoming paper, which is all up on Aaron’s GitHub. It applies to building neural models using the Neural Engineering Framework (NEF). There’s a bunch of material on the NEF out there already, (e.g. the book How to Build a Brain by Dr. Chris Eliasmith, an online intro, and you can also check out Nengo, which is neural model development software with some good tutorials on the NEF) so I’m going to assume you already know the basics of the NEF for this post.
Additionally, this is applicable to simulating these models on digital systems, which, probably, most of you are. If you’re not, however! Then use standard NEF methods.
And then last note before starting, these methods are most relevant for systems with fast dynamics (relative to simulation time). If your system dynamics are pretty slow, you can likely get away with the continuous time solution if you resist change and learning. And we’ll see this in the example point attractor system at the end of the post! But even for slowly evolving systems, I would still recommend at least skipping to the end and seeing how to use the library shortcuts when coding your own models. The example code is also all up on my GitHub.
NEF modeling with continuous lowpass filter dynamics
Basic state space equations for linear time-invariant (LTI) systems (i.e. dynamics can be captured with a matrix and the matrices don’t change over time) are:
- is the system state,
- is the system output,
- is the system input,
- is called the state matrix,
- is called the input matrix,
- is called the output matrix, and
- is called the feedthrough matrix,
and the system diagram looks like this:
and the transfer function, which is written in Laplace space and captures the system output over system input, for the system is
where is the Laplace variable.
Now, because it’s a neural system we don’t have a perfect integrator in the middle, we instead have a synaptic filter, , giving:
So our goal is: given some synaptic filter , we want to generate some modified transfer function, , such that has the same dynamics as our desired system, . In other words, find an such that
Alrighty. Let’s do that.
The transfer function for our neural system is
The effect of the synapse is well captured by a lowpass filter, , making our equation
To get this into a form where we can start to modify the system state matrices to compensate for the filter effects, we have to isolate . To do that, we can do the following basic math jujitsu
OK. Now, we can make by substituting for our and matrices with
then we get
and voila! We have created an such that . Said another way, we have created a system that compensates for the synaptic filtering effects to achieve our desired system dynamics!
So, to compensate for the continuous lowpass filter, we use and when implementing our model and we’re golden.
And so that’s what we’ve been doing for a long time when building our models. Assuming a continuous lowpass filter and going along our merry way. Aaron, however, shrewdly noticed that computers are digital, and thusly that the standard NEF methods are not a fully accurate way of compensating for the filter that is actually being applied in simulation.
To convert our continuous system state equations to discrete state equations we need to make two changes: 1) the first is a variable change to denote the that we’re in discrete time, and we’ll use instead of , and 2) we need to calculate the discrete version our system, i.e. transform .
The first step is easy, the second step more complicated. To discretize the system we’ll use the zero-order hold (ZOH) method (also referred to as discretization assuming zero-order hold).
Zero-order hold discretization
Zero-order hold (ZOH) systems simply hold their input over a specified amount of time. The use of ZOH here is that during discretization we assume the input control signal stays constant until the next sample time.
There are good write ups on the derivation of the discretization both on wikipedia and in these course notes from Purdue. I mostly followed the wikipedia derivation, but there were a few steps that get glossed over, so I thought I’d just write it out fully here and hopefully save someone some pain. Also for just a general intro I found these slides from Paul Oh at Drexel University really helpful.
OK. First we’ll solve an LTI system, and then we’ll discretize it.
So, you’ve got yourself a continuous LTI system
and you want to solve for . Rearranging things to put all the on one side gives
Looking through our identity library to find something that might help us here (after a long and grueling search) we come across:
We now left multiply our system by (note the negative in the exponent)
Looking at this carefully, we identify the left-hand side as the result of a chain rule, so we can rewrite it as
From here we integrate both sides, giving
To isolate the term on the left-hand side now multiply by :
OK! We solved for .
To discretize our solution we’re going to assume that we’re sampling the system at even intervals, i.e. each sample is at for some time step , and that the input is constant between samples (this is where the ZOH comes in). To simplify our notation as we go, we also define
So using our new notation, we have
Now we want to get things back into the form:
To start, let’s write out the equation for
We want to relate to . Being incredibly clever, we see that we can left multiply by , to get
and can rearrange to solve for a term we saw in :
Plugging this in, we get
OK, we’re getting close.
At this point we’ve got things in the right form, but we can still clean up that second term on the right-hand side quite a bit. First, note that using our starting assumption (that is constant), we can take outside the integral.
Next, bring that term inside the integral:
And now we’re going to simplify the integral using variable substitution. Let , which means also that . Evaluating the upper and lower bounds of the integral, when then and when then . With this, we can rewrite our equation:
The astute will notice our integral integrates from T to 0 instead of 0 to T. Fortunately for us, we know . We can just swap the bounds and multiply by , giving:
And finally, we can evaluate our integral by recalling that and assuming that is invertible:
We did it! The state and input matrices for our digital system are:
And that’s the hard part of discretization, the rest of the system is easy:
where, fortunately for us
This then gives us our discrete system transfer function:
NEF modeling with continuous lowpass filter dynamics
Now that we know how to discretize our system, we can look at compensating for the lowpass filter dynamics in discrete time. The equation for the discrete time lowpass filter is
Plugging that in to the discrete transfer fuction gets us
and we see that if we choose
then we get
And now congratulations are in order. Proper compensation for the discrete lowpass filter dynamics has finally been achieved!
Point attractor example
What difference does this actually make in modelling? Well, everyone likes examples, so let’s have one.
Here are the dynamics for a second-order point attractor system:
with and being the system position, velocity, and acceleration, respectively, is the target position, and and are gain values. So the acceleration is just going to be set such that it drives the system towards the target position, and compensates for velocity.
Converting this from a second order system to a first order system we have
which we’ll rewrite compactly as
OK, we’ve got our state space equation of the dynamical system we want to implement.
Given a simulation time step , we’ll first calculate the discrete state matrices:
Great! Easy. Now we can calculate the state matrices that will compensate for the discrete lowpass filter:
Alright! So that’s our system now, a basic point attractor implementation in Nengo 2.3 looks like this:
tau = 0.1 # synaptic time constant # the A matrix for our point attractor A = np.array([[0.0, 1.0], [-alpha*beta, -alpha]]) # the B matrix for our point attractor B = np.array([[0.0], [alpha*beta]]) # account for discrete lowpass filter a = np.exp(-dt/tau) if analog: A = tau * A + np.eye(2) B = tau * B else: # discretize Ad = expm(A*dt) Bd = np.dot(np.linalg.inv(A), np.dot((Ad - np.eye(2)), B)) A = 1.0 / (1.0 - a) * (Ad - a * np.eye(2)) B = 1.0 / (1.0 - a) * Bd net = nengo.Network(label='Point Attractor') net.config[nengo.Connection].synapse = nengo.Lowpass(tau) with config, net: net.ydy = nengo.Ensemble(n_neurons=n_neurons, dimensions=2, # set it up so neurons are tuned to one dimensions only encoders=nengo.dists.Choice([[1, 0], [-1, 0], [0, 1], [0, -1]])) # set up Ax part of point attractor nengo.Connection(net.ydy, net.ydy, transform=A) # hook up input net.input = nengo.Node(size_in=1, size_out=1) # set up Bu part of point attractor nengo.Connection(net.input, net.ydy, transform=B) # hook up output net.output = nengo.Node(size_in=1, size_out=1) # add in forcing function nengo.Connection(net.ydy, net.output, synapse=None)
Note that for calculating
Ad we’re using
expm which is the matrix exp function from
scipy.linalg package. The
numpy.exp does an elementwise
exp, which is definitely not what we want here, and you will get some confusing bugs if you’re not careful.
Code for implementing and also testing under some different gains is up on my GitHub, and generates the following plots for dt=0.001:
In the above results you can see that the when the gains are low, and thus the system dynamics are slower, that you can’t really tell a difference between the continuous and discrete filter compensation. But! As you get larger gains and faster dynamics, the resulting effects become much more visible.
If you’re building your own system, then I also recommend using the
ss2sim function from Aaron’s
nengolib library. It automatically handles compensation for any synapses and generates the matrices that account for discrete or continuous implementations automatically. Using the library looks like:
tau = 0.1 # synaptic time constant synapse = nengo.Lowpass(tau) # the A matrix for our point attractor A = np.array([[0.0, 1.0], [-alpha*beta, -alpha]]) # the B matrix for our point attractor B = np.array([[0.0], [alpha*beta]]) from nengolib.synapses import ss2sim C = np.eye(2) D = np.zeros((2, 2)) linsys = ss2sim((A, B, C, D), synapse=synapse, dt=None if analog else dt) A = linsys.A B = linsys.B
So there you are! Go forward and model free of error introduced by improperly accounting for discrete simulation! If, like me, you’re doing anything with neural modelling and motor control (i.e. systems with very quickly evolving dynamics), then hopefully you’ve found all this work particularly interesting, as I did.
There’s a ton of extensions and different directions that this work can be and has already been taken, with a bunch of really neat systems developed using this more accurate accounting for synaptic filtering as a base. You can read up on this and applications to modelling time delays and time cells and lots lots more up on Aaron’s GitHub, and hisrecent papers, which are listed on his lab webpage.