In this example, we will create a Leaky-Integrate-and-Fire neuron model.
Each neuron models need to implement the trait fr.univ_lille.cristal.emeraude.n2s3.core.Neuron
. This one requires the creation of two methods :
defaultConnection : NeuronConnection
, which create a new instance of a connection when no explicit model is given.processSomaMessage(timestamp : Timestamp, message : Message, fromSynapse : Option[Int], ends : NeuronEnds) : Unit
, which process incoming messages.Basically, a minimal neuron model would be :
class InputNeuron extends Neuron { def defaultConnection = new Synapse def processSomaMessage(timestamp: Timestamp, message: Message, fromSynapse : Option[Int], ends : NeuronEnds): Unit = { } }
Now, let's add some behaviour to our neuron. A Leaky-Integrate-and-Fire has three basic behaviours:
To accomplish that, we need three class attributes:
var membranePotential : Float = 0f // Internal membrane action potential val membraneThreshold : Float = 1f // Membrane threshold, with default value of 1 val membraneLeak : Time = 10 MilliSecond // Membrane leak factor, with default value of 10ms
However, as the simulator compute only analytic models, we need to add an internal variable to keep track of the previous neuron states.
var lastTimestamp : Timestamp = 0 // last proceeded timestamp
When a post-synaptic spike reaches the soma, a message WeightedSpike(weight : Float)
is sent to the processSomaMessage
method with the current simulation timestamp in parameter. The processing of this message will be:
timestamp
) and the last proceeded timestamp (lastTimestamp
).lastTimestamp
variableWe can translate those steps by the following scala code:
def processSomaMessage(timestamp : Timestamp, message : Message, fromSynapse : Option[Int], ends : NeuronEnds) : Unit = message match { case WeightedSpike(weight) => membranePotential *= exp(-(timestamp - lastTimestamp).toDouble / membraneLeak.timestamp.toDouble).toFloat // apply the leakage membranePotential += weight // integrate the spike if(membranePotential >= membraneThreshold) { // check the threshold ends.sendToAllOutput(timestamp, ShapelessSpike) // send a pre-synaptic spike to all outgoing neurons } lastTimestamp = timestamp // update the last proceeded timestamp }
The NeuronEnds
object allow to communicate with both the incoming and the outgoing synapses.
Now we have to behaviour of our neuron, we need to add some event and properties in order to interact with the simulator.
We can add a property for each alterable model parameter:
addProperty[Float](NeuronThreshold, () => membranePotential, membranePotential = _) addProperty[Time](NeuronLeakage, () => membraneLeak, membraneLeak = _)
We can also add a connection property in order to interact with the synapses :
addConnectionProperty[Float](SynapseWeight, { case synapse: FloatSynapse => Some(synapse.getWeight) case _ => None }, (connection, value) => connection match { case synapse: FloatSynapse => synapse.setWeight(value) case _ => } )
Those properties allow to set and get the parameters of the neuron and synapse models, by sending SetProperty
/GetProperty
/SetConnectionProperty
/GetConnectionProperty
messages
The next step is to add some event. Basically, we can add the event NeuronFireEvent
when the membrane potential goes above the threshold, and the event NeuronPotentialEvent
after the membrane potential update.
We need first to declare the supported events. No need to declare the NeuronFireEvent
, it is handled by default in the Neuron
trait.
addEvent(NeuronPotentialEvent)
Then, we need to trigger the event each time it is necessary :
def processSomaMessage(timestamp : Timestamp, message : Message, fromSynapse : Option[Int], ends : NeuronEnds) : Unit = message match { case WeightedSpike(weight) => membranePotential *= exp(-(timestamp - lastTimestamp).toDouble / membraneLeak.timestamp.toDouble).toFloat membranePotential += weight triggerEventWith(NeuronPotentialEvent, NeuronPotentialResponse(timestamp, this.container.self, membranePotential)) // update membrane potential if(membranePotential >= membraneThreshold) { triggerEventWith(NeuronFireEvent, NeuronFireResponse(timestamp, getNetworkAddress)) // fire ends.sendToAllOutput(timestamp, ShapelessSpike) } lastTimestamp = timestamp }
Each connection need to extend the NeuronConnection
trait, which force to implement method processConnectionMessage(timestamp : Timestamp, message : Message, ends : ConnectionEnds) : Unit
. This last is called each time a message arise in the synapse.
However, basic implementation already exists in N2S3, such as the class WeightedSynapse
which add a weight to the synapse.
Let's create a synapse with a STDP mechanism. The principle is to increase the synapse weight when a pre-synaptic spike occurs a little time before a post-synaptic spike. Conversely, we decrease the synapse weight when a post-synaptic spike occurs a little time before a pre-synaptic spike.
First of all, create the new class and add the model parameters. in our models we have :
Like in the neuron model, we need to keep track of the previous computation. We introduce timestampLastPre
and timestampLastPost
which save the last proceeded timestamp, respectively, for a pre-synaptic and a post-synaptic spike.
Then, we need to handle two messages:
We can translate the synapse behaviour into scala code :
class Synapse(w : Float) extends WeightedSynapse[Float](w) { var alphaLTP = 0.03125 var alphaLTD = 0.85*a_exc val tauLTP = 16.8 MilliSecond var tauLTD = 33.7 MilliSecond var timestampLastPre : Timestamp = -Long.MaxValue var timestampLastPost : Timestamp = -Long.MaxValue def processConnectionMessage(timestamp : Timestamp, message : Message, ends : ConnectionEnds) : Unit = message match { case ShapelessSpike => weight = math.max(0, weight - alphaLTD * math.exp(-(timestamp - timestampLastPost).toDouble / tauLTD.timestamp.toDouble)).toFloat timestampLastPre = timestamp ends.sendToOutput(timestamp, WeightedSpike(weight)) // send a spike with the current weight of the synapse to the post neuron case BackwardSpike => weight = math.min(1, weight + alphaLTP * math.exp(-(timestamp - timestampLastPre).toDouble / tauLTP.timestamp.toDouble)).toFloat timestampLastPost = timestamp } } }
The ConnectionEnds
allow to communicate with the pre neuron and the post neuron