#!/usr/bin/env python
# coding: utf-8
# # SYDE556/750 Assignment 3: Connecting Neurons
#
# - Due Date: March 12th
# - Total marks: 10 (10% of final grade)
# - Late penalty: 1 mark per day
#
# - It is recommended that you use a language with a matrix library and graphing capabilities. Two main suggestions are Python and MATLAB.
# - You can (and are encouraged to) use code from your previous assignments.
# - If you are having difficulties re-using code from your previous assignments, you can optionally use the following functions implemented using nengo (to install nengo, run `pip install nengo`):
# In[68]:
import nengo
import numpy as np
def compute_lif_decoder(n_neurons, dimensions, encoders, max_rates, intercepts, tau_ref, tau_rc, radius, x_pts, function):
"""
Parameters:
n_neurons: number of neurons (integer)
dimensions: number of dimensions (integer)
encoders: the encoders for the neurons (array of shape (n_neurons, dimensions))
max_rates: the maximum firing rate for each neuron (array of shape (n_neurons))
intercepts: the x-intercept for each neuron (array of shape (n_neurons))
tau_ref: refractory period for the neurons (float)
tau_rc: membrane time constant for the neurons (float)
radius: the range of values the neurons are optimized over (float)
x_pts: the x-values to use to solve for the decoders (array of shape (S, dimensions))
function: the function to approximate
Returns:
A (the tuning curve matrix)
dec (the decoders)
"""
model = nengo.Network()
with model:
ens = nengo.Ensemble(n_neurons=n_neurons,
dimensions=dimensions,
encoders=encoders,
max_rates=max_rates,
intercepts=[x/radius for x in intercepts],
neuron_type=nengo.LIF(tau_rc=tau_rc, tau_ref=tau_ref),
radius=radius)
sim = nengo.Simulator(model)
x_pts = np.array(x_pts)
if len(x_pts.shape) == 1:
x_pts.shape = x_pts.shape[0], 1
_, A = nengo.utils.ensemble.tuning_curves(ens, sim, inputs=x_pts)
target = [function(xx) for xx in x_pts]
solver = nengo.solvers.LstsqL2()
dec, info = solver(A, target)
return A, dec
def generate_signal(T, max_freq, rms, dt):
"""
Parameters:
T: the period of time to generate a random signal for
max_freq: the highest frequency in the signal
rms: the RMS power of the signal
dt: the time step (usually 0.001)
Returns:
signal (an array of length T/dt containing the random signal)
"""
signal = nengo.processes.WhiteSignal(period=T, high=max_freq, rms=rms)
return signal.run(T, dt=dt)
def generate_spikes(n_neurons, dimensions, encoders, max_rates, intercepts, tau_ref, tau_rc, radius, x, dt):
"""
Parameters:
n_neurons: number of neurons (integer)
dimensions: number of dimensions (integer)
encoders: the encoders for the neurons (array of shape (n_neurons, dimensions))
max_rates: the maximum firing rate for each neuron (array of shape (n_neurons))
intercepts: the x-intercept for each neuron (array of shape (n_neurons))
tau_ref: refractory period for the neurons (float)
tau_rc: membrane time constant for the neurons (float)
radius: the range of values the neurons are optimized over (float)
x: the input signal to feed into the neurons (array of shape (T, dimensions))
dt: the time step of the simulation (usually 0.001)
Returns:
spikes (a (timesteps, n_neurons) array of the spiking outputs)
"""
model = nengo.Network()
with model:
stim = nengo.Node(lambda t: x[int(t/dt)-1])
ens = nengo.Ensemble(n_neurons=n_neurons,
dimensions=dimensions,
encoders=encoders,
max_rates=max_rates,
intercepts=[x/radius for x in intercepts],
neuron_type=nengo.LIF(tau_rc=tau_rc, tau_ref=tau_ref),
radius=radius)
nengo.Connection(stim, ens, synapse=None)
p = nengo.Probe(ens.neurons)
sim = nengo.Simulator(model, dt=dt)
T = len(x)*dt
sim.run(T)
return sim.data[p]
# ## 1) Decoding from a population
#
# As you did in previous assignments, make a population of 20 LIF neurons representing a 1-dimensional value, and compute a decoder for them. For parameters, $\tau_{ref}$=0.002s, $\tau_{RC}$=0.02s, the maximum firing rates are chosen randomly from a uniform distribution between 100 and 200Hz (at the max radius), and the x-intercepts are chosen randomly from a uniform distribution between -2 and 2. Remember that the $\alpha$ and $J^{bias}$ terms are computed based on these x-intercepts and maximum firing rates.
#
# It is generally easiest to compute decoders using the original method from Assignment 1, where we use the rate-mode approximation for the neurons to generate the $A$ matrix, then find $\Gamma=A^T A + \sigma^2 I$. You can use this approach to find decoders, and these decoders should work even when you simulate the neurons in terms of spikes (in question 2 on). The only difference will be that they will need to be scaled by ``dt``, your simulation time step.
#
# Use this same method for computing decoders for this whole assignment.
#
#
# - [0.5 marks] Plot the tuning curves (firing rate of each neuron for different $x$ values between -2 and 2)
# - [0.5 marks] Compute the decoders and plot $(x-\hat{x})$. When computing decoders, take into account noise ($\sigma$=0.1 times 200Hz). When computing $\hat{x}$, add random gaussian noise with $\sigma$=0.1 times 200Hz to the activity. Report the Root Mean-Squared Error (RMSE).
#
# ## 2) Decoding from two spiking neurons
#
# Choose a neuron from part 1 that has a firing rate of somewhere between 20-50Hz for $x$=0. Using that neuron's $\alpha$ and $J^{bias}$ value, construct two neurons: both with the same $\alpha$ and $J^{bias}$, but one with $e$=+1 and the other with $e$=-1. With the function from the last assignment, generate a random input $x(t)$ that is 1 second long, with rms=1, dt=0.001, and an upper limit of 5Hz. Feed that signal into the two neurons and generate spikes. Decode the spikes back into $\hat{x}(t)$ using a post-synaptic current filter $h(t)$ with a time constant of $\tau$=0.005.
#
#
# - [0.5 marks] Plot the post-synaptic current $h(t)=e^{-t/\tau}/ \int e^{-t'/\tau} dt'$
# - [0.5 marks] Plot the original signal $x(t)$, the spikes, and the decoded $\hat{x}(t)$ all on the same graph
# - [0.5 marks] Compute the RMSE of the decoding
#
# ## 3) Decoding from many neurons
#
# Repeat question 2, but with more neurons. Instead of picking particular neurons, randomly generate them with x-intercepts uniformly distributed between -2 and 2 and with maximum firing rates between 100 and 200 Hz. Randomly choose encoder values to be either -1 or +1.
#
#
# - [2 marks] Plot Root Mean-Squared Error as the number of neurons increases, on a log plot. Try 8 neurons, 16 neurons, 32, 64, 128, up to 256. For the RMSE for a particular number of neurons, average over at least 5 randomly generated groups of neurons. For each group of neurons, randomly generate the signal $x(t)$. Use the same parameters as in question 2. Note: the RMSE should go down as the number of neurons increases
#
# ## 4) Connecting two groups of neurons
#
# For this question, use two groups of neurons with intercepts between [-1, 1] (i.e. radius = 1) to compute $y = 2x+1$. The first group of neurons will represent $x$ and the second group will represent $y$.
#
# Start by computing decoders. You will need two decoders: one to decode $f(x)=2x+1$ from the first population, and one to decode $f(y)=y$ (the standard representational decoder) from the second population. Remember that $\Upsilon$ can change depending on what function you want to decode.
#
# Use the same neuron parameters as for previous questions (other than the radius), and use 200 randomly generated neurons in each population.
#
#
# - [1 mark] Show the behaviour of the system with an input of $x(t)=t-1$ for 1 second (a linear ramp from -1 to 0). Plot the ideal $x(t)$ and $y(t)$ values, along with $\hat{y}(t)$.
#
# - Note that you should use the decoders that work for any input over the range of intercepts: do not re-compute decoders for any particular input (i.e. set of $x$ values).
# - Input $x(t)$ into the first group of neurons and produce spikes. Decode from those spikes using the decoder for $f(x)=2x+1$. Input that decoded result into the second group of neurons to produce spikes. Use the second decoder ($f(y)=y$) to decode $\hat{y}(t)$.
# - Make sure the maximum firing rates are now at -1 or 1 (i.e., the radius is 1).
#
#
#
- [0.5 marks] Repeat part (a) with an input that is ten randomly chosen values between -1 and 0, each one held for 0.1 seconds (a randomly varying step input)
#
# - [0.5 marks] Repeat part (a) with an input that is $x(t)=0.2sin(6\pi t)$. Briefly discuss the results for this question.
#
# ## 5) Connecting three groups of neurons
#
# For this question, use three groups of neurons with intercepts from [-1, 1] to compute $z = 2y+0.5x$. Follow the same steps as question 4, but take the decoded outputs from the first two groups of neurons ($f(y)=2y$ and $f(x)=0.5x$), add them together, and feed that into the third group of neurons.
#
#
# - [1 mark] Plot $x(t)$, $y(t)$, the ideal $z(t)$, and the decoded $\hat{z}(t)$ for an input of $x(t)=cos(3\pi t)$ and $y(t)=0.5 sin (2 \pi t)$ (over 1.0 seconds)
#
# - [0.5 marks] Plot $x(t)$, $y(t)$, the ideal $z(t)$, and the decoded $\hat{z}(t)$ for a random input over 1 second. For $x(t)$ use a random signal with a limit of 8 Hz and `rms`=1. For $y(t)$ use a random signal with a limit of 5 Hz and `rms`=0.5.
#
#
# ## 6) Computing with vectors
#
# Do the same thing as questions 4 and 5, but with 2-dimensional vectors instead of scalars. Everything else is the same. For your encoders $e$, randomly generate them over the unit circle.
#
# The function to compute is $w = x-3y+2z-2q$. This requires five groups of neurons: $x$, $y$, $z$, $q$, and $w$. Each of them represents a 2-dimensional value. The outputs from $x$, $y$, $z$, and $q$ all feed into $w$.
#
#
# - [1 mark] Plot the decoded output $\hat{w}(t)$ and the ideal $w$ for $x=[0.5,1], y=[0.1,0.3], z=[0.2,0.1], q = [0.4,-0.2]$. (Note that these are all constants so they don't change over time, but still plot it for 1.0 seconds on one or more 2D graphs)
# - [0.5 marks] Produce the same plot for $x=[0.5,1], y=[sin(4\pi t),0.3], z=[0.2,0.1], q = [sin(4\pi t),-0.2]$.
# - [0.5 marks] Describe your results and discuss why and how they stray from the expected answer.
# In[ ]: