Example 6: Spike-Timing-Dependent Plasticity (STDP)

This example introduces learning and plasticity. We connect two neurons using the STDPSynapse, a continuous, smooth approximation of STDP.

We drive the presynaptic neuron with a constant current, and the postsynaptic neuron with a slightly different constant current so they spike at different frequencies. The STDP rule will continuously strengthen or weaken the synaptic weight based on their relative spike times. We will plot the dynamic weight variable w to watch learning happen in real-time!

using MTKNeuralToolkit
using MTKNeuralToolkit.HodgkinHuxley: SodiumChannel, PotassiumChannel, LeakChannel
using ModelingToolkit: mtkcompile, @named
using OrdinaryDiffEq
using Plots

##. Build Two Neurons

top = Scalar()

function build_neuron(name::Symbol)
    @named cap  = Capacitor(topology=top, C=1.0)
    @named na   = SodiumChannel(topology=top)
    @named k    = PotassiumChannel(topology=top)
    @named leak = LeakChannel(topology=top)
    return build_compartment(cap, [na, k, leak]; name=name, V_init=-65.0, topology=top)
end

pre_neuron  = build_neuron(:pre_neuron)
post_neuron = build_neuron(:post_neuron)
Compartment{NoMorphology, NoGeometry, Float64}(Model post_neuron:
Subsystems (8): see hierarchy(post_neuron)
  cap
  injector
  syn_injector
  p
  ⋮
Equations (52):
  36 standard: see equations(post_neuron)
  16 connecting: see equations(expand_connections(post_neuron))
Unknowns (51): see unknowns(post_neuron)
  cap₊v(t)
  cap₊i(t)
  cap₊p₊v(t)
  cap₊p₊i(t)
  ⋮
Parameters (7): see parameters(post_neuron)
  cap₊C
  na₊g
  na₊E_rev
  k₊g
  ⋮, (V = post_neuron₊cap₊v(t), p_pin = Model post_neuron₊p:
Equations (2):
  2 connecting: see equations(expand_connections(post_neuron₊p))
Unknowns (2): see unknowns(post_neuron₊p)
  v(t)
  i(t), n_pin = Model post_neuron₊n:
Equations (2):
  2 connecting: see equations(expand_connections(post_neuron₊n))
Unknowns (2): see unknowns(post_neuron₊n)
  v(t)
  i(t), I_ext = post_neuron₊injector₊I₊u(t), I_syn = post_neuron₊syn_injector₊I₊u(t), cap_name = :cap), -65.0, Scalar(), NoGeometry(), NoMorphology())

Define the STDP Synapse

The STDPSynapse continuously updates its weight w based on the traces x (pre) and y (post).

  • If pre spikes before post, x is high when y spikes -> LTP (weight increases)
  • If post spikes before pre, y is high when x spikes -> LTD (weight decreases)
@named stdp_syn = STDPSynapse(g_max=2.0, E_rev=0.0, V_th=0.0, slope=2.0,
                              τ_s=5.0, τ_plus=20.0, τ_minus=20.0,
                              A_plus=0.1, A_minus=0.1, w_init=0.5, w_max=1.0, w_min=0.0)

synapse_specs = [
    SynapseSpec(
        pre_neuron.interfaces.V,
        post_neuron.interfaces.V,
        post_neuron.interfaces.I_syn,
        stdp_syn
    )
]
1-element Vector{SynapseSpec}:
 SynapseSpec(pre_neuron₊cap₊v(t), post_neuron₊cap₊v(t), post_neuron₊syn_injector₊I₊u(t), Model stdp_syn:
Equations (5):
  5 standard: see equations(stdp_syn)
Unknowns (7): see unknowns(stdp_syn)
  s(t)
  w(t)
  x(t)
  y(t)
  ⋮
Parameters (11): see parameters(stdp_syn)
  g_max
  τ_s
  τ_plus
  τ_minus
  ⋮, nothing)

Driving Stimuli (Phase-Shifted Sine Waves)

To demonstrate BOTH potentiation and depression, we drive both neurons with the same frequency, but shift the phase. Swap the driver with a higher phase to see either LTP or LTD

using ModelingToolkitStandardLibrary.Blocks: Sine

Pre fires ~10ms before Post (Phase offset of 1 radian)

@named pre_driver  = Sine(amplitude=8.0, frequency=0.2, offset=8.0, phase=1.0)
@named post_driver = Sine(amplitude=8.0, frequency=0.2, offset=8.0, phase=0.0)

drivers = [
    (1, pre_driver),  # Pre fires second
    (2, post_driver)  # Post fires first -> causes LTD
]
2-element Vector{Tuple{Int64, ModelingToolkitBase.System}}:
 (1, Model pre_driver:
Subsystems (1): see hierarchy(pre_driver)
  output
Equations (1):
  1 standard: see equations(pre_driver)
Unknowns (1): see unknowns(pre_driver)
  output₊u(t): Inner variable in RealOutput output
Parameters (5): see parameters(pre_driver)
  offset
  start_time
  amplitude
  frequency
  ⋮)
 (2, Model post_driver:
Subsystems (1): see hierarchy(post_driver)
  output
Equations (1):
  1 standard: see equations(post_driver)
Unknowns (1): see unknowns(post_driver)
  output₊u(t): Inner variable in RealOutput output
Parameters (5): see parameters(post_driver)
  offset
  start_time
  amplitude
  frequency
  ⋮)

Build and Simulate

net = build_acausal_network([pre_neuron, post_neuron];
                            synapse_specs=synapse_specs,
                            drivers=drivers,
                            name=:stdp_net)

println("Compiling STDP network...")
sys = mtkcompile(net.sys)
prob = ODEProblem(sys, [], (0.0, 500.0), jac=true, sparse=true)

println("Solving...")
sol = solve(prob, Rosenbrock23(), reltol=1e-4, abstol=1e-4)
retcode: Success
Interpolation: specialized 2nd order "free" stiffness-aware interpolation
t: 7995-element Vector{Float64}:
   0.0
   0.002893114846611352
   0.008829242592298848
   0.01896847733888128
   0.034855942072418426
   0.05740747299190596
   0.087434087710729
   0.12514630534363663
   0.17053256489091745
   0.22324463261734082
   ⋮
 499.6161828496841
 499.6700056976823
 499.72382854568053
 499.77765139367875
 499.831474241677
 499.8852970896752
 499.9391199376734
 499.99294278567163
 500.0
u: 7995-element Vector{Vector{Float64}}:
 [0.0, 0.0, 0.5, 0.0, 0.052, 0.596, 0.317, -65.0, 0.052, 0.596, 0.317, -65.0]
 [2.2349627521889976e-17, 2.245853576014826e-17, 0.5, 2.2453672638795856e-17, 0.05201292703461028, 0.5959997882886603, 0.31700053149976487, -64.95747173816885, 0.05201219459646022, 0.5959999038565976, 0.31700045247461317, -64.97690924988831]
 [6.901310457204732e-17, 7.004748951418226e-17, 0.5, 7.000154883749462e-17, 0.05204892805074109, 0.595997770330704, 0.3170027037263184, -64.8703206146502, 0.05204217738993674, 0.5959988455542649, 0.3170019688435469, -64.92940110022968]
 [1.5129805871055e-16, 1.5623335009379267e-16, 0.5, 1.5601600924133225e-16, 0.05213910269008907, 0.5959893922832651, 0.31700977850526413, -64.72179313218808, 0.05210849424492114, 0.5959943458066783, 0.3170063957654718, -64.84784541345317]
 [2.871122031222918e-16, 3.046738337457775e-16, 0.5, 3.0391057234871303e-16, 0.052349992106445906, 0.5959637237860609, 0.31702940298819776, -64.48987392677607, 0.05224941241660008, 0.5959804025734794, 0.3170180287718263, -64.71899951055983]
 [4.954569122307232e-16, 5.468359826352734e-16, 0.5, 5.446448217383877e-16, 0.052786062704842984, 0.5959008899376282, 0.3170751743023435, -64.16230076831522, 0.05252319577387247, 0.5959459562049402, 0.31704450487680935, -64.53385391016991]
 [8.044613889486188e-16, 9.365013652927842e-16, 0.5, 9.310089006760626e-16, 0.05359500101953108, 0.5957688431588444, 0.317168789011358, -63.72893333869959, 0.0530133960034179, 0.5958728563290904, 0.31709821532706123, -64.28313244160809]
 [1.2517322463325767e-15, 1.560213705096528e-15, 0.5, 1.5477676444427228e-15, 0.0549433567459485, 0.5955239673362855, 0.3173393418066993, -63.188825459179014, 0.0538168285543435, 0.5957357552860524, 0.31719623188775786, -63.96125551612892]
 [1.8953113880890367e-15, 2.568710884380669e-15, 0.5, 2.542480030679201e-15, 0.05700157264380797, 0.5951110514064736, 0.3176230435644306, -62.54444715422939, 0.05503788190648683, 0.5955015068798041, 0.3173606389783952, -63.56331787249344]
 [2.8243610307153937e-15, 4.223807653330196e-15, 0.5, 4.1713457622318265e-15, 0.0599194777827596, 0.5944665642774342, 0.3180605179728477, -61.80289421648331, 0.056776016177501096, 0.5951303314652179, 0.317617545700887, -63.086336815251975]
 ⋮
 [1.5189736186085252, 1.6958985001676352, 0.6840784890826908, 0.6636813372563515, 0.025794649282151125, 0.13352865236569558, 0.6848585686079179, -74.84318190774667, 0.6845365934195154, 0.06419288359502504, 0.7387481765887064, -46.42170247181388]
 [1.5148913385678309, 1.6913407300276393, 0.684078489082819, 0.6565753749087314, 0.022629263178202175, 0.13871001431586577, 0.6802090480315028, -74.77648135715685, 0.6420535516948124, 0.06491075553711885, 0.7367432324862814, -48.91744534050865]
 [1.5108200297563465, 1.6867952090102452, 0.6840784890828552, 0.6495454953105443, 0.020518949617924262, 0.14383370938651627, 0.6756101499221989, -74.6932366539164, 0.5953252521772688, 0.06595797898962943, 0.7344227850111781, -51.4743279476606]
 [1.5067596626898425, 1.6822619041956262, 0.6840784890828652, 0.6425908838522443, 0.01913570296061986, 0.1488966524871843, 0.6710627853082327, -74.59915259371658, 0.544545881854167, 0.06734656645326768, 0.7317925420557432, -54.06065241650421]
 [1.5027102079624566, 1.677740782752428, 0.684078489082868, 0.6357107346462193, 0.018253529444389446, 0.15389711847287774, 0.6665673992672829, -74.49788056492146, 0.49033161502725686, 0.0690882616377398, 0.7288642315815405, -56.62030068286512]
 [1.4986716362470995, 1.6732318119375316, 0.6840784890828687, 0.6289042504334065, 0.017717144597412297, 0.1588342534212001, 0.6621241367305422, -74.39182425953895, 0.43385247027064766, 0.07119060459042546, 0.7256577842367586, -59.07336673698253]
 [1.4946439182954279, 1.6687349590958158, 0.684078489082869, 0.6221706424909071, 0.017419930298213135, 0.1637077763763247, 0.6577329434009384, -74.2826267210835, 0.37685756180150887, 0.07365135588997174, 0.7222024551990379, -61.325904874476784]
 [1.4906270249376843, 1.6642501916599213, 0.6840784890828691, 0.6155091305405911, 0.01728863006769731, 0.16851779632525354, 0.6533936274228829, -74.1714651899382, 0.3215008160293049, 0.07645286356017564, 0.7185361077892682, -63.28927527361712]
 [1.4901011340036865, 1.663663046738317, 0.6840784890828691, 0.6146409873109907, 0.017281067140864675, 0.16914379236522828, 0.6528284940949416, -74.15679034788597, 0.3144856603993208, 0.07684419393461812, 0.7180418043035744, -63.520175678635646]

Plot the Results

p1 = plot(sol, idxs=[sys.pre_neuron.cap.v, sys.post_neuron.cap.v],
          title="Neuron Voltages", ylabel="V (mV)",
          labels=["Pre" "Post"], legend=:right)
Example block output

Plot the plastic weight variable! We reach into the synapse namespace to get w.

p2 = plot(sol, idxs=[sys.stdp_syn.w],
          title="STDP Synaptic Weight (w)",
          ylabel="Weight", xlabel="Time (ms)", legend=false)
Example block output

Plot the pre and post synaptic traces (x and y) which drive the weight update

p3 = plot(sol, idxs=[sys.stdp_syn.x, sys.stdp_syn.y],
          title="STDP Activity Traces",
          labels=["x (Pre)" "y (Post)"], legend=:right)

plot(p1, p2, p3, layout=(3,1), size=(800, 800))
Example block output

This page was generated using Literate.jl.