neuronsimulator / nrn

NEURON Simulator
http://nrn.readthedocs.io
Other
393 stars 115 forks source link

Setting `threshold` on `gid_connect` created `NetCon` affects spike transmission. #2135

Open Helveg opened 1 year ago

Helveg commented 1 year ago

Context

In the following reproducer, if you uncomment line 31: nc_recv.threshold = -50.3, the spike dissapears. According to the docs this should not be the case:

This is not the NetCon that monitors the spike source variable for threshold crossings, so its threshold parameter will not affect simulations. Threshold crossings are determined by the detector that belonged to the NetCon used to associate the presynaptic spike source with a gid (see ParallelContext.cell() and ParallelContext.threshold()).

Overview of the issue

Setting the threshold of the receiving NetCon, created with gid_connect, affects the spike transmission. The print shows that the max value reached is -50.309099522998906; setting -50.4 there is a spike, setting -50.3 there isn't anymore, showing that the threshold of this NetCon affects the transmission, and makes it dissapear from spike_record(-1, ...) on that node.

Expected result/behavior

I expect the threshold not to affect simulation, as per the docs

NEURON setup

Minimal working example - MWE

class Issue: def _setup_neuron(self): pc = h.ParallelContext() if not pc.id(): mys = h.Section() syn = h.ExpSyn(mys(0.5)) syn.tau = 2 stim = h.NetStim() stim.number = 5 stim.start = 0 stim.interval = 10 ncstim = h.NetCon(stim, syn_) ncstim.weight[0] = 0.04 ncstim.delay = 1 nc_emit = h.NetCon(my_s(0.5)._ref_v, None, sec=my_s) nc_emit.threshold = -52 nc_emit.delay = 1 pc.set_gid2node(3032, pc.id()) pc.cell(3032, nc_emit) pc.outputcell(3032) self._store3 = [mys, stim, syn, ncstim, nc_emit]

    my_s2 = h.Section()
    syn_2 = h.ExpSyn(my_s2(0.5))
    syn_2.tau = 2
    nc_recv = pc.gid_connect(3032, syn_2)
    # nc_recv.threshold = -50.3
    nc_recv.weight[0] = 0.04
    self._store4 = [my_s2, syn_2, nc_recv]
    return h.Vector().record(h._ref_t), h.Vector().record(my_s2(0.5)._ref_v)

def test(self):
    tpn, vpn = self._setup_neuron()
    r = h.Vector().record(self._store3[0](0.5)._ref_v)

    pc = h.ParallelContext()
    spt, spid = h.Vector(), h.Vector()
    pc.spike_record(-1, spt, spid)

    pc.set_maxstep(10)
    h.finitialize()
    pc.psolve(100)

    print("Treshold upper bound:", max(r))
    spikes = [3032] if not pc.id() else []
    print(list(spid))
    assert len(spikes) == len(spid), "expected 2 on main, 0 elsewhere"
    assert spikes == sorted(list(spid)), "expected 2 on main, 0 elsewhere"
    ar2 = np.array(pc.py_allgather(list(vpn)))
    assert ar2.shape[1] > 0, "No signal recorded"
    assert np.allclose(np.diff(ar2, axis=0), 0), "Diff output on diff nodes"

Issue().test()

nrnhines commented 1 year ago

This is not the NetCon that monitors the spike source variable for threshold crossings, so its threshold parameter will not affect simulations.

Very bad sentence. It's true when the source and NetCon are on different MPI ranks, but is completely wrong for the case where the source and NetCon are on the same MPI rank.

I now deeply regret ever associating threshold with NetCon. Threshold is a property of the source threshold crossing detector (internally called a PreSyn). ARTIFICIAL_CELLs do not need a threshold detector as they initiate events with their NET_RECEIVE block (though they still need a PreSyn to enqueue all the NetCon instances that have that PreSyn as the source, and notify the interprocessor spike distribution system that the PreSyn has fired.) So the threshold for all the NetCon with the same source is the last execution value of any NetCon.threshold = ... or ParallelContext.threshold(gid, thresh) For this reason I got into the habit of setting threshold in the Cell constructor and never setting it otherwise. Also if one creates a NetCon without a threshold argument, the existing threshold is not changed.

https://www.neuron.yale.edu/neuron/static/py_doc/modelspec/programmatic/network/netcon.html discusses the issue a bit and in particular:

A source used by multiple NetCon instances is shared by those instances to allow faster threshold detection (ie on a per source basis instead of a per NetCon basis). Therefore, there is really only one threshold for all NetCon objects that share a source. However, delay and weight are distinct for each NetCon object.

The only way one can have multiple threshold values at the same location is to create a threshold detector point process with a NET_RECEIVE block implemented with a WATCH statement and calling net_event.