User Tools

Site Tools


createmodels

Create new Models

Neuron models

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:

  • Integrate new spike to the membrane action potential
  • Apply a leakage to this potential
  • Fire when the potential goes above a specified threshold

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:

  • update the membrane action potential by applying the leakage, according to the time difference between the current timestamp (timestamp) and the last proceeded timestamp (lastTimestamp).
  • add the spike weight to the membrane.
  • check if the membrane reaches the threshold, and if it's the case, emit an output spike to all outgoing neurons.
  • update lastTimestamp variable

We 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
  }

Synapse models

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 :

  • alphaLTP/alphaLTD, which are multiplicative factors, respectively, during the LTP and LTD
  • tauLTP/tauLTD, which are the time factors for LTP and LTD

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:

  • ShapelessSpike, which represent the pre-synaptic spikes
  • BackwardSpike, which will be a message sent by the post neuron to all his incoming synapses when it fire.

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

createmodels.txt ยท Last modified: 2016/11/10 11:33 by Pierre.Falez@univ-lille1.fr