Brian2 and simulation based inference with sbi / standalone mode

Hey everyone,

I am using Brian2 as a simulator for simulation based inference with the sbi package.

It works already really well with prefs.codegen.target = 'cython' even when I use multiple workers in simulate_for_sbi. Now I’m considering to build the simulator in standalone mode. I found this example but I don’t think this one will work for me. If I understand the example correctly it runs exactly the same network with one parameter changed. However, I have parameters that define the random connection probability which I need to randomly regenerate between runs. Maybe that means the standalone mode is not good for my use case?

The docs read: When launching new simulation runs as described above, you can also change parameters of the model. Note that this only concerns parameters that are included in equations, you cannot change externally defined constants.

The connection probability between two neuron groups that I want to change is not included in any of the equations so I assume I should just stay in cython mode?

Best,
Daniel

Hi @danielmk. Just to make sure there is no misunderstanding: you want to change the value for the connection probability (to a random value :face_with_raised_eyebrow:? Or should it be one of the parameters of your model that you optimize/infer over?), and not just have a different realization of the random connectivity each time?

Hey @mstimberg ,

Sorry for being unclear. I have multiple populations in my model and when I call synapse.connect(p) each of their probabilities p is supposed to be part of the set of parameters subject to simulation based inference. That means the actual value of p during a simulation run is randomly drawn from the prior distribution and then passed to the simulator to create another sample from the simulator.

Does that make sense? I put this in social/feedback because my code is not yet in a shareable state yet and I thought maybe I can already get some ideas if it’s worth to explore standalone mode. But if the topic is too general/confusing without code I can also come back with it when I have something more concrete.

Best,
Daniel

Hi. This makes perfect sense. It is actually possible to do this, with a not too terrible workaround: you’ll have to store the probability somewhere, and since the synapses do not yet exist the only place you can store them are the pre- and post-synaptic neuron groups. In general this would be used to have connection probabilites that depend on the pre- or post-synaptic cell (or both), but you can also just store a fixed probability here. One caveat: the run_args are applied “late”, and by default this would be after the creation of synapses (in order to make it possible to set synaptic parameters). You can change this using device.apply_run_args(). Here’s a simple example that does not simulate anything but only returns the number of created synapses. I hope it conveys the general idea:

from brian2 import *
set_device('cpp_standalone', build_on_run=False)
group = NeuronGroup(10, "connection_p : 1 (constant, shared)")
synapses = Synapses(group, group, "")
device.apply_run_args()
synapses.connect(p="connection_p_pre")
run(0*ms)

# Compile but don't run
device.build(run=False, directory="/tmp/test_reconnect")

def run_with_p(p):
    device.run(run_args={group.connection_p: p})
    return len(synapses)

for p in [0, 0.1, 0.5, 1.0]:
    print(run_with_p(p))

Thank you for the quick reply. I implementing your suggestion into my code and it makes sense to me but now that I am actually doing it I realized that the number of neurons per neuron group is also a free parameter and even worse, the tau_facil in a Tsodyks Markram process is a free parameter and when it is 0 I need to change the equation of the synapse.

I guess I can use the same workaround but it makes the correct place to call device.apply_run_args() a bit complicated. I assume I can only call it after all objects that have run args are already created? If an object doesn’t exist yet it should raise an error if I try to apply a run arg to it? Unfortunately I was not able to find documentation on device.apply_run_args(). Maybe there is a way to only apply specific run args? If documentation exists and I missed it I’d be happy to read it. Thank you for your help.

There’s indeed not much documentation for apply_run_args (except for Computational methods and efficiency — Brian 2 2.5.4.post102 documentation and Standalone implementation — Brian 2 2.5.4.post102 documentation), but it can only be used as a single call, not for specific run args. I think in your case, you could work around the limitation for the tau_facil parameter by again using the run_args to set a parameter on a pre/post-synaptic neuron group (where it is not used, obviously), and then do something like syn.tau_facil = "tau_facil_pre" at a later point to set it from the pre-synaptic parameter.
All that being said, it is unfortunately not possible to change the number of neurons in this way. This is a hard limitation in Brian that is not easy to remove – we’ve done some early steps in that direction, but it is not straightforward. The only work around at the moment (but not sure if that is feasible in your case) would be to have some kind of exists: boolean (constant) parameter in your neuron model, together with the number of neurons you want to have (saym real_N : integer (constant, shared)). After real_N has been set by the run args, you can then set neurons.exists = 'i < real_N' (so it’d be False for all the “unnecessary” neurons). Then, you could either reference in the threshold condition (... and exists) to make sure only existing neurons spike, or use in your synaptic connection statement connect("exists_pre and exists_post", p=...).

Thank you for the detailed explanation. For now I will proceed with cython since changing the number of neurons is important at this stage. However, I will keep all these workarounds in mind in case a version with fixed neurons becomes relevant to my project.