Find the synapse objects efferent from specific neurons

I’m trying to access the synaptic variables of a specific subset of neurons, but I’m having a hard time identifying all the synapse objects that are efferent from these. How would one find the synapse objects efferent from specific neurons in a population?

assuming you have a single group of synpases S, you could look for those which come from neuron neuron_id with

S[neuron_id, :]

accessing synaptic variables
that approach should also work for a list of neuron_ids.

Another option is to use Brian’s string-based syntax for indexing and the S.i property which stores the index of the presynaptic neuron:

S['i==5'] #simple enough for hardcoded values
S[f'i=={neuron_id}'] #not sure whether this string substitution is necessary
S['i<5'] #string-based syntax allows for additional range selection 

monitoring synaptic variables

Got it, thanks!

print('Synapses efferent from 2 to 4\n\t', S[2:4, :])
print('Synapses afferent to 2 to 4\n\t', S[:, 2:4])
print('Value of w for synapses efferent from neurons 2 to 4\n\t', S.w[2:4, :])
print('Value of w for synapses afferent to neurons 2 to 4\n\t', S.w[:, 2:4])

I was also able to index non-contiguous neurons this way:

spiked_idx = np.where(spiked_neurons)[0]
synapses_spiked = synapse_monitor[spiked_idx, :]

Another question I have is how I could slice the monitors to only return data within a certain timeframe. For example, how would I return the values recorded between 1 and 2 seconds? I can do something like synapse_monitor.t[(synapse_monitor.t>2*second)&(synapse_monitor.t<3*second)] but the synaptic variables themselves don’t have any time information attached, so I wouldn’t be sure of how to filter them by time.
I guess I could do np.where((synapse_monitor.t>2*second)&(synapse_monitor.t<3*second)) to get the indices and then access the variable arrays using those, but is there a cleaner way?

1 Like

i would have to double check this works, but yeah I do something along the lines of what you’ve outlined: (I don’t think the np.where is necessary)

#generally the time vector is identical across monitors, but this assumes they're recording at the same rate:
t = synapse_monitor.t 

t_range = (t>t0) & (t<t1) # should be an array of booleans which can be used as an index
voltage_monitor.v[ t_range ]

the detail here I haven’t double checked is you just need to make sure you’re indexing the right dimension (i.e. time and not neuron index).

Great, thanks!

I have another query that I hope you can help me with:
what if I wanted to get the pairs of neurons connected within a subgroup?

Let’s say I want to get all the connected pairs within the first 64 neurons, I’m trying something like:

for i, j in zip( S.i[ :64, : ], S.j[ :, :64 ] ):
    print( I,'->' ,j )

but I’m not at all sure that this is correct.

And if I wanted to get all the connected pairs with a pre-synaptic neuron inside the first 64 subgroup? Would this be correct?:

for i, j in zip( S.i[ :64, : ], S.j[ :, : ] ):
    print( I,'->' ,j )

I’m trying to build a graph from the network in order to be able to visualise it in something like NetworkX or Dash. Any suggestions of the best way of approaching this? I’d like to be able to interactively see what the neuronal topology looks like.

Hi @Tioz90 . No, this is not quite correct. The expression zip( S.i[ :64, : ], S.j[ :, :64 ] ) will give you the source indices of all synapses originating from the group, and the target indices of all synapses targetting the group. But the two lists of indices will not correspond to each other – the first will also include entries for synapses coming from the group but going outside the group, and the second will include entries for synapses coming from outside the group and going inside. In general, you will have to use the same indexing for i and j to get two arrays of the same size, where individual entries correspond to the same synapses. Here’s a quick example (using a full size of 10, and a subgroup size of 5 for simplicity):

>>> G = NeuronGroup(10, '')
>>> S = Synapses(G, G)
>>> S.connect(0.5)
>>> len(S)
43
>>> list(zip(S.i, S.j))  # all synapses
[(0, 0), (0, 3), (0, 5), (0, 9), (1, 3), (1, 6), (2, 0), (2, 1), (2, 2), (2, 3), (2, 6), (3, 0), (3, 1), (3, 4), (3, 6), (3, 8), (4, 0), (4, 1), (4, 8), (5, 4), (5, 7), (5, 9), (6, 1), (6, 2), (6, 3), (6, 4), (6, 7), (6, 8), (6, 9), (7, 1), (7, 2), (7, 4), (7, 5), (7, 7), (8, 0), (8, 1), (8, 3), (8, 5), (8, 8), (9, 3), (9, 4), (9, 6), (9, 7)]

Now, using your proposed loop would give us the following source and target indices:

>>> S.i[:5, :]
array([0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4],
      dtype=int32)
>>> S.j[:, :5]
array([0, 3, 3, 0, 1, 2, 3, 0, 1, 4, 0, 1, 4, 1, 2, 3, 4, 1, 2, 4, 0, 1,
       3, 3, 4], dtype=int32)

You can immediately see that this does not work, since the arrays do not have the same length. Also, if you look at the first four entries of the two arrays, they seem to imply that there are two connections 0→3, and two connections 0→0. Instead, you get the correct result if you use the same indexing for both S.i and S.j:

>>> S.i[:5, :5]
array([0, 0, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4], dtype=int32)
>>> S.j[:5, :5]
array([0, 3, 3, 0, 1, 2, 3, 0, 1, 4, 0, 1], dtype=int32)

This is identical to the list of all synapses, by filtering out everything that starts or ends outside of the group. To verify, we can compare Brian’s filtering and a manual filtering in Python:

>>> list(zip(S.i[:5, :5], S.j[:5, :5]))
[(0, 0), (0, 3), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 4), (4, 0), (4, 1)]
>>> [(i, j) for i, j in zip(S.i, S.j) if i < 5 and j < 5]
[(0, 0), (0, 3), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 4), (4, 0), (4, 1)]

The same holds when you want to look at neurons originating from the subgroup, you are interested in the source and target indices from the same synapses, so they have to use the same indexing:

>> list(zip(S.i[:5, :], S.j[:5, :]))
[(0, 0), (0, 3), (0, 5), (0, 9), (1, 3), (1, 6), (2, 0), (2, 1), (2, 2), (2, 3), (2, 6), (3, 0), (3, 1), (3, 4), (3, 6), (3, 8), (4, 0), (4, 1), (4, 8)]

Finally, note that instead of indices, you can also use an actual subgroup as an index:

>>> subgroup = G[:5]
>>> list(zip(S.i[subgroup, subgroup], S.j[subgroup, subgroup]))  # within the group
[(0, 0), (0, 3), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 4), (4, 0), (4, 1)]
>>> list(zip(S.i[subgroup, :], S.j[subgroup, :]))  # originating from group
[(0, 0), (0, 3), (0, 5), (0, 9), (1, 3), (1, 6), (2, 0), (2, 1), (2, 2), (2, 3), (2, 6), (3, 0), (3, 1), (3, 4), (3, 6), (3, 8), (4, 0), (4, 1), (4, 8)]

I think any tool for network visualization should be happy with input like the one above, i.e. a lists of source and target indices or pairs of (source, target) indices. Sometimes a dictionary structure can also be handy, i.e. a mapping from source indices to all its targets (here created for all connections within the subgroup):

>>> import collections
>>> conn_dict = collections.defaultdict(list)
>>> for i, j in zip(S.i[subgroup, subgroup], S.j[subgroup, subgroup]):
...     conn_dict[i].append(j)
... 
>>> print(dict(conn_dict))
{0: [0, 3], 1: [3], 2: [0, 1, 2, 3], 3: [0, 1, 4], 4: [0, 1]}

For simple visualizations, you can also have a look at the plotting code in the Synapses tutorial, and at the plot functions in the brian2tools package.

Got it, even though it can all be a bit confusing. Basically, S.i[:5, :5] means take all synapses where the pre and post neurons have id in range(0,5) and give me the pre synaptic neurons of those synapses. Correct?
Adding some explicit methods like Synapses.get_pre_neurons() and Synapses.get_post_neurons() - instead of relying on slicing - could go a long way. :slight_smile:

I think the classic adjacency matrix view that brian2tools is a great general-purpose high-level view.

If you’re okay focusing on a small (10-node) subset of nodes you could take a look at my (work in progress) circuit visualizer
[code and list of keyboard shortcuts here]
[live in-browser demo here]

marcel’s code for generating lists of pairs, or a dictionary of all outputs for each input node should work in the textbox entry at the bottom… but unfortunately integer indices will need to be replaced with letters of the alphabet (let me know if you’re interested in a snippet of python to help generate that format)

otherwise you should just need to import that output list into a networkx graph, and they you can display the network interactively with plotly / dash:

if you run into any roadblocks in doing that translation let me know and I’ll try to help

Very nice, thanks! I’ll look into it, but I’m currently trying to look at larger networks than that. I would definitely like to receive some tips on the best way of representing and characterising in a visual way the connectivity of large(-ish) networks (~300 nodes for now).

I’ve currently had some luck with PyVis (see pic)but I’m open to trying other tools. Would you particularly recommend PlotLy?

Plotly is great for general purpose interactive plots from python. [example from delayed gaussian connections in brian]
But PyVis looks like it has all the features you’d want from networkx + plotly anyway, so if PyVis is working well for you, it’s not worth switching in my opinion.

It looks like your network is quite densely connected which can make these plots quite hard to follow. My initial guess as to some tricks which might help include:

  • sorting / ordering the node position based on any clustering / subnetwork structure
  • color coding edges by node
  • trying an “adjacency matrix” view like those in brian2tools

you might also find some more inspiration in the networkx gallery here:
https://networkx.org/documentation/stable/auto_examples/index.html

1 Like