It is possible to add new objects to the Nengo reference simulator. This involves several steps and the creation of several objects. In this example, we'll go through these steps in order to add a new neuron type to Nengo: a rectified linear neuron.
The RectifiedLinear
class is what you will use
in model scripts to denote that a particular ensemble
should be simulated using a rectified linear neuron
instead of one of the existing neuron types (e.g., LIF
).
Normally, these kinds of frontend classes exist
in either nengo/objects.py
or nengo/neurons.py
.
Look at these files for examples of how to make your own.
In this case, becuase we're making a neuron type,
we'll use nengo.neurons.LIF
as an example
of how to make RectifiedLinear
.
In [ ]:
import numpy as np
import matplotlib.pyplot as plt
import nengo
class RectifiedLinear(nengo.objects.Neurons): # 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 rates(self, x, gain, bias):
"""Firing rates for encoded value x."""
return np.maximum(np.zeros_like(x), gain * (x + bias))
def gain_bias(self, max_rates, intercepts):
"""Return gain and bias given maximum firing rate and x-intercept."""
bias = intercepts
gain = (max_rates - bias) / (1. + bias)
return gain, bias
The Operator
(located in nengo/builder.py
) defines
the function that the reference simulator will execute
on every timestep. Most new neuron types and learning rules
will require a new Operator
, unless the function
being computed can be done by combining several
existing operators.
In this case, we will make a new operator that outputs the firing rate of each neuron on every timestep.
In [ ]:
class SimRectifiedLinear(nengo.builder.Operator):
"""Set output to the firing rate of a rectified linear neuron model."""
def __init__(self, output, J, neurons):
self.output = output # Output signal of the ensemble
self.J = J # Input current from the ensmble
self.neurons = neurons # The RectifiedLinear instance
# Operators must explicitly tell the simulator what signals
# they read, set, update, and increment
self.reads = [J]
self.updates = [output]
self.sets = []
self.incs = []
# If we needed additional signals that aren't in one of the
# reads, updates, sets, or incs lists, we can initialize them
# by making an `init_signals(self, signals, dt)` method.
def make_step(self, signals, dt):
"""Return a function that the Simulator will execute on each step.
`signals` contains a dictionary mapping each signal to
an ndarray which can be used in the step function.
`dt` is the simulator timestep.
"""
J = signals[self.J]
output = signals[self.output]
def step():
# Gain and bias are already taken into account here,
# so we just need to rectify and multiply by dt
output[...] = dt * np.maximum(np.zeros_like(J), J)
return step
In order for nengo.builder.Builder
to construct signals and operators
for the Simulator to use,
you must create and register a build function
with nengo.builder.Builder
.
This function should take as arguments
a RectifiedLinear
instance,
some other arguments specific to the type,
and a nengo.builder.Model
instance.
The function should add the approrpiate
signals, operators, and other artifacts
to the Model
instance,
and then register the build function
with nengo.builder.Builder
.
In [ ]:
from nengo.builder import Signal, Copy, Builder, BuiltNeurons # For convenience
def build_rectified_linear(neurons, max_rates, intercepts, model):
gain, bias = neurons.gain_bias(max_rates, intercepts)
model.sig_in[neurons] = Signal(np.zeros(neurons.n_neurons), name="%s.input" % neurons.label)
model.sig_out[neurons] = Signal(np.zeros(neurons.n_neurons), name="%s.output" % neurons.label)
model.operators.append(Copy(src=Signal(bias, name="%s.bias" % neurons.label),
dst=model.sig_in[neurons]))
model.operators.append(SimRectifiedLinear(
output=model.sig_out[neurons], J=model.sig_in[neurons], neurons=neurons))
for probe in neurons.probes["output"]:
Builder.build(probe, dimensions=neurons.n_neurons, model=model)
model.params[neurons] = BuiltNeurons(gain=gain, bias=bias)
Builder.register_builder(build_rectified_linear, RectifiedLinear)
In [ ]:
from nengo.utils.ensemble import tuning_curves
model = nengo.Network()
with model:
encoders = np.tile([[1],[-1]], (4,1))
intercepts = np.linspace(-0.8, 0.8, 8)
intercepts *= encoders[:,0]
A = nengo.Ensemble(RectifiedLinear(8), dimensions=1, intercepts=intercepts,
max_rates=nengo.utils.distributions.Uniform(80, 100),
encoders=encoders)
sim = nengo.Simulator(model)
eval_points, activities = tuning_curves(A, sim)
plt.plot(eval_points, activities, lw=2)
plt.xlabel("Input signal")
plt.ylabel("Firing rate (Hz)")
In [ ]:
model = nengo.Network(label='2D Representation', seed=10)
with model:
neurons = nengo.Ensemble(RectifiedLinear(100), dimensions=2)
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 = nengo.Probe(neurons, 'decoded_output', synapse=0.01)
sim = nengo.Simulator(model)
sim.run(5)
In [ ]:
import matplotlib.pyplot as plt
plt.plot(sim.trange(), sim.data[neurons_probe], label="Decoded output")
plt.plot(sim.trange(), sim.data[sin_probe], 'r', label="Sine")
plt.plot(sim.trange(), sim.data[cos_probe], 'k', label="Cosine")
plt.legend()