ValueError: Equations of type 'parameter' cannot have a flag 'linked', only the following flags are allowed: ['constant', 'shared']

Description of problem

I am trying to implement the homeostatic inhibitory STDP rule used in Zenke (2015) in Brian2. The rule uses a symmetric STDP kernel that is modulated in size and sign by a global factor G, which corresponds to the difference between a (smoothed) estimate of the population firing rate H and a desired firing rate gamma. I am trying to implement this rule using a synapse model with a linked variable. However, my current attempt results in a cryptic error message.

Full traceback of error (if relevant)

Traceback (most recent call last):
  File "/home/paul/work/gothard/code/inhibitory_homeostatic_plasticity_with_shared_global_factor.py", line 61, in <module>
    synapses = b2.Synapses(
               ^^^^^^^^^^^^
  File "/home/paul/opt/miniconda3/envs/brian2/lib/python3.11/site-packages/brian2/synapses/synapses.py", line 853, in __init__
    model.check_flags(
  File "/home/paul/opt/miniconda3/envs/brian2/lib/python3.11/site-packages/brian2/equations/equations.py", line 1234, in check_flags
    raise ValueError(
ValueError: Equations of type 'parameter' cannot have a flag 'linked', only the following flags are allowed: ['constant', 'shared']

Minimal code to reproduce problem

"""
This script implements a STDP rule with a global/shared factor as in Zenke et al. (2015).
"""

import numpy as np
import matplotlib.pyplot as plt
import brian2 as b2

b2.start_scope()

net = b2.Network(b2.collect())

# set (target) population firing rate; 
# in a non-toy simulation, spikes would be recorded from a large pyramidal neuron population
total_epochs = 11
epoch_length = 1 * b2.second
population_size = 1
spike_rates = np.geomspace(1, 50, total_epochs)
spike_rates_array = b2.TimedArray(spike_rates * b2.Hz, dt=epoch_length)
spike_generators = b2.PoissonGroup(population_size, rates="spike_rates_array(t)")
net.add(spike_generators)

# compute global factor G
gamma = 5 * b2.Hz
tau_H = 10 * b2.second
global_factor_model = """
G = H - gamma : Hz
dH/dt = -H/tau_H : Hz
"""
global_factor = b2.NeuronGroup(1, model=global_factor_model)
net.add(global_factor)
spike_aggregator = b2.Synapses(spike_generators, global_factor, on_pre="H_post += 1/(tau_H * population_size)")
spike_aggregator.connect()
net.add(spike_aggregator)

# initialize the pre- and post-synaptic neuron
indices = np.zeros(total_epochs)
times = epoch_length * np.arange(total_epochs) + 0.5 * epoch_length
presynaptic_neuron = b2.SpikeGeneratorGroup(1, indices, times)
net.add(presynaptic_neuron)
delta = 10 * b2.msecond
postsynaptic_neuron = b2.SpikeGeneratorGroup(1, indices, times + delta)
net.add(postsynaptic_neuron)

# initialize the synapse
tau_stdp = 20. * b2.msecond
eta = 1. * b2.nsiemens * b2.second
synapse_model = """
G : Hz (linked)
wij : siemens
dzi/dt = -zi / tau_stdp : 1 (event-driven)
dzj/dt = -zj / tau_stdp : 1 (event-driven)
"""
on_pre = """
zi += 1.
wij += eta * G * zj
"""
on_post = """
zj += 1.
wij += eta * G * (zi + 1)
"""
synapses = b2.Synapses(
    presynaptic_neuron, postsynaptic_neuron,
    model=synapse_model, on_pre=on_pre, on_post=on_post
)
synapses.connect(i=0, j=0)
# synapses.G = b2.linked_var(global_factor.G)
synapses.G = b2.linked_var(global_factor, "G")
synapses.wij[:] = 0 * b2.nsiemens
net.add(synapses)

# monitor synaptic variables
monitor = b2.StateMonitor(synapses, ["G", "wij"], record=True, dt=epoch_length)
net.add(monitor)

# run
net.run(total_epochs * epoch_length)

# plot
fig, axes = plt.subplots(2, 1, sharex=True)
axes[0].plot(np.squeeze(monitor.G / b2.Hz), color="gray")
axes[0].set_ylabel("Global factor [Hz]")
axes[1].plot(np.diff(np.squeeze(monitor.wij / b2.nsiemens)), color="gray")
axes[1].set_ylabel("Weight [nS]")
axes[-1].set_xlabel("Epoch")
plt.show()

Any insight or workaround would be appreciated.

Hi @paulbrodersen,
great question (and an example reproducing something from Zenke (2015) would be a nice addition to the documentation :blush: ). The reason for the error is that the linked flag is only allowed in NeuronGroup equations, it cannot be used in Synapses. This is because when we establish a link with linked_var, we establish a mapping of indices between the two groups – this does not work for synapses in the general case, since their number is not fixed. However, this is an issue for non-trivial mappings, and we could actually allow linked variables in Synapses for linking to groups of size 1 (as in your example), where there’s no ambiguity about the mapping.

Until we add this feature, there’s a workaround you can use: the linked variable mechanism does several checks, verifies that the mapping is feasible and so on, and then establishes an internal reference to the variable from the other group. You can skip the linked variable mechanism and directly use the internal mechanism. For that, you have to remove G : Hz (linked) from the synaptic equations, and replace the synapses.G = ... assignment by:

synapses.variables.add_reference("G", group=global_factor, index='0')

(the index='0' is a special case for scalar/shared variables, that are always index as [0]).

Hope that works for you!

Hope that works for you!

It does. As always: thank you for the fast and informative response!

(and an example reproducing something from Zenke (2015) would be a nice addition to the documentation :blush: ).

Happy to contribute whatever I can. Would a sanitized version of the code above suffice (as a proof-of-principle), or would you rather see it being used in anger as a homeostatic mechanism in a simulation with large neuronal populations (which invariably will incur additional complexity).

Hi @paulbrodersen – either would be nice, actually. Something like your example above would be a nice addition to the synapses section of the documentation, demonstrating how one could set up such a mechanism (similar in spirit to Example: spike_based_homeostasis — Brian 2 2.8.0.2 documentation). Reproducing one of the figures from Zenke (2015) would be great for the frompapers section, but is maybe a bit too much work.

1 Like

Made a PR with a slightly sanitized version of the script above.

1 Like