How to add and address variables in the right way while adding a new neuron model

I want to create a new neuron type and a new learning rule to Nengo. I am not so good at object oriented programming yet.

When creating a new neuron type, how can I deal with the spikes themselves and not the current. For the example of Rectified Linear Neuron model,as I understand J is the input current matrix, and x is the output spikes, right?

Now I need my function input to be normalized (from 0 to 1) , and similarly for the output. how to get normalized input? Can I use x as my normalized output, right?

After I define these normalized input an output , how can I address them properly? I mean for J we can add in builder by J=model.sig[neurons]['in'] but what about these new variables we add, how can I add them?

It might be easiest if you post the nengo code for the neuron model you are trying to get working, so we can see what J and x are in your setup.

1 Like

Sorry for not explaining what J and X is, because the code is just borrowed from example of extending Nengo (example of Rectified Linear Neurons) and took only the function (rates) from “Direct” neurons type. So I though they were something common to be used.

The Code:

# In[ ]:

import numpy as np
import matplotlib.pyplot as plt

import nengo

class RectifiedLinear(nengo.neurons.NeuronType):  # Neuron types must subclass `nengo.Neurons`
    """A rectified linear neuron model."""
    # We don't need any additional parameters here;
    # gain and bias are sufficient. But, if we wanted
    # more parameters, we could accept them by creating
    # an __init__ method.
    def gain_bias(self, max_rates, intercepts):
        """Return gain and bias given maximum firing rate and x-intercept."""
        gain = max_rates / (1 - intercepts)
        bias = -intercepts * gain
        return gain, bias
    def rates(self, x, gain, bias):
        """Always returns ``x``."""
        return x

    def step_math(self, dt, J, output):
        """Compute rates in Hz for input current (incl. bias)"""

        output[...] = np.maximum(0., J)

# In[ ]:
from nengo.builder import Operator

class SimRectifiedLinear(Operator):
    """Set a neuron model output for the given input current.

    Implements ``neurons.step_math(dt, J, output, *states)``.

    neurons : NeuronType
        The `.NeuronType`, which defines a ``step_math`` function.
    J : Signal
        The input current.
    output : Signal
        The neuron output signal that will be set.
    states : list, optional (Default: None)
        A list of additional neuron state signals set by ``step_math``.
    tag : str, optional (Default: None)
        A label associated with the operator, for debugging purposes.

    J : Signal
        The input current.
    neurons : NeuronType
        The `.NeuronType`, which defines a ``step_math`` function.
    output : Signal
        The neuron output signal that will be set.
    states : list
        A list of additional neuron state signals set by ``step_math``.
    tag : str or None
        A label associated with the operator, for debugging purposes.

    1. sets ``[output] + states``
    2. incs ``[]``
    3. reads ``[J]``
    4. updates ``[]``

    def __init__(self, neurons, J, output, states=None, tag=None):
        super(SimRectifiedLinear, self).__init__(tag=tag)
        self.neurons = neurons
        self.J = J
        self.output = output
        self.states = [] if states is None else states

        self.sets = [output]
        self.incs = []
        self.reads = [J]
        self.updates = []

    def _descstr(self):
        return '%s, %s, %s' % (self.neurons, self.J, self.output)

    def make_step(self, signals, dt, rng):
        J = signals[self.J]
        output = signals[self.output]

        def step_simrectifiedlinear():
            self.neurons.step_math(dt, J, output)
        return step_simrectifiedlinear

# In[ ]:

from nengo.builder import Builder
from nengo.builder.operator import Copy
from nengo.builder.signal import Signal
from nengo.neurons import NeuronType

def build_rectified_linear(model, neuron_type, neurons):
        output=model.sig[neurons]['out'], J=model.sig[neurons]['in'],neurons=neuron_type))

You are defining whatever behaviour you want for your neuron within the step_math function. So if you want your input currents and output values to be normalized then you would just do

def step_math(self, dt, J, output):
    # normalize input
    norm_J = J / np.linalg.norm(J)

    out = np.maximum(0., J)

    # normalize output
    norm_out = out / np.linalg.norm(x)

    output[...] = norm_out

Note that this is normalizing so that the total length of the input/output vector is 1, which may not be what you want. But whatever type of normalization you would like to do, you can just insert them in the same way.

Similarly for the rates function, you’re just specifying what the output should be for the given input (x), so you can apply the same normalizations within that function.

Thank you for you reply, but there is something still unclear for me.
In the documentation, under the "class nengo.neurons.NeuronType"
for “rates(x, gain, bias)” , there is the parameter “x” defined as "Vector-space input"
and for “step_math(dt, J, output)” , there is the parameter “J” defined as "Input currents associated with each neuron"
My question is what is the difference between x and J , and how is x translated into J (what equation relates them)
My aim is to create neuron that uses mean firing rate (for my initial trials) not a spiking one. That’s why I was trying to understand what each parameter stands for.

For example I build a network like in the RectifiedLinear Neuron type example:

# In[]:
model = nengo.Network(label='2D Representation', seed=10)
with model:
    neurons = nengo.Ensemble(1000, dimensions=2, neuron_type=RectifiedLinear(),
             max_rates=nengo.dists.Uniform(80, 100))
    sin = nengo.Node(output=np.sin)
    cos = nengo.Node(output=np.cos)
    nengo.Connection(sin, neurons[0])
    nengo.Connection(cos, neurons[1])
    sin_probe = nengo.Probe(sin, 'output')
    cos_probe = nengo.Probe(cos, 'output')
    #neurons_probe_input = nengo.Probe(neurons, 'input', synapse=0.01)
    neurons_probe = nengo.Probe(neurons, 'decoded_output', synapse=0.01)
with nengo.Simulator(model) as sim:

# In[ ]:
#plt.plot(sim.trange(),[neurons_probe_input], label="Input")
plt.plot(sim.trange(),[neurons_probe], label="Decoded output")
plt.plot(sim.trange(),[sin_probe], 'r', label="Sine")
plt.plot(sim.trange(),[cos_probe], 'k', label="Cosine")

Then I tried to print x.shape and I get very strange dimensions.
for n_neurons from 1 to 500 , x.shape = (1000,n_neurons)
for n_neurons more than 500,x.shape = (2*n_neurons,n_neurons)

For example if n_neurons=200 , x.shape = (1000,200)
while if n_neurons = 1000, x.shape = (2000,1000)

Can anyone help understand what does these dimensions represent or even is there another way to just use input rate of firing for each neuron in an ensemble?

The relationship between vector space and neural currents is defined using the Neural Engineering Framework (specifically the encoding formula, $J_i = \alpha_i e_i x + J^{bias}$). You can learn more about how the NEF works here (although there are also whole books written on this).

However, I suspect that in your case you are wanting to just directly connect to the neurons (i.e., ignoring the vector space). To do that you do nengo.Connection(my_input, my_ensemble.neurons) (instead of nengo.Connection(my_input, my_ensemble)). Then you are just directly connecting to the input currents ($J$) of the neurons in my_ensemble.

The rates function is called with a set of random inputs, which are used to evaluate the behaviour of your neuron model. The shape of x is (number of evaluation points, number of neurons). So in your case you were seeing 1000/2000 evaluation points, and 200/1000 neurons.

1 Like

Thank you , that was really helpful