Arbitrarily nested neuron groups with per-group iterator
(Discussed during Bernstein conference with @babsey @heplesser @terhorstd @poojanbabu) In the future it may be desirable to support creating arbitrary nested groups of populations in NEST the way it is currently proposed for NEST Desktop on a Python level by @babsey. This would allow applying connection rules and e.g. weight values on a per-group-item level via a group item aware iterator as opposed to strictly flattened across all neuron in a NodeCollection.
Syntax brainstorming:
pop1 = nest.Create("iaf_psc_exp", 100)
pop2 = nest.Create("iaf_psc_exp", 200)
group1 = nest.Group([pop1, pop2])
# Retrieve:
pop1 = group1[0] // same as pop1
pop1_neuron0 = group1[0][0] // same as pop1[0]
nest.Connect(g1, g1, "all_to_all", syn_spec={"weight":[23, 25]})
// Would apply weight 23.0 to all connections from n1-> nX and 25.0 to n2->nX
// However, this could also be taken to mean n1->n1: 23.0 and n2->n2: 25.0. Needs a clearer syntax!
- Doing parameters purely as lists might get really messy and unclear, calls for named arg implementation.
- Some of the functionality already exists as part of composite node collections on a C++ level, but these are currently not exposed to the user.
- There might not be a need for an explicit
nest.Groupbut some method or operator overload is needed to distinguish from the defaultn1 + n2flattened collection addition. - While there's some similarities to pooling connections as done in exabrainprep this should be a conceptually independent change.
Great step that you open this tread. One or more extensions/suggestions/corrections to your comment:
- [ ]
g1is undefined. You named the variablegroup1. - [ ] We also talked about naming the group, e.g.
nest.Group([pop1, pop2], name="my_group")But naming is optional.
To better understand, we can make a usecase: lets say popE is excitatory and popI is inhibitory. A group of popE and popI represents a layer.
popE = nest.Create("iaf_psc_alpha", 800) # excitatory
popI = nest.Create("iaf_psc_alpha", 200) # inhibitory
layer = nest.Group([popE, popI], name="L2/3")
nest.Connect(layer, layer, syn_spec={"weight": [2, -8]})
# This implies that popE connects to layer with weight `w1=2` whereas popI to layer with `w2=-8`
# As summary the weight is shown as [wE, wI]
# A more complex connection step could be
nest.Connect(layer, layer, syn_spec={"weight": [[4, 2], [-8, -12]]})
# In my understanding the weight is shown as [[wEE, wIE], [wEI, wII]]
Actually, we don't need anything new in NEST (almost ...) to make this happen: We can get it for free from Pandas DataFrames!
The following code generates something like the neural populations of the multi-area model, but with three interneuron populations per layer, randomly selected neuron numbers and each population assigned randomly one of three neuron models and stores them in a Pandas dataframe:
import nest
import pandas as pd
import random
random.seed(123)
areas = ['V1', 'V2', 'VP', 'V3', 'V3A', 'MT', 'V4t', 'V4', 'VOT', 'MSTd',
'PIP', 'PO', 'DP', 'MIP', 'MDP', 'VIP', 'LIP', 'PITv', 'PITd',
'MSTl', 'CITv', 'CITd', 'FEF', 'TF', 'AITv', 'FST', '7a', 'STPp',
'STPa', '46', 'AITd', 'TH']
layers = ['L23', 'L4', 'L5', 'L6']
pops = ['Pyr', 'SOM', 'VIP', 'PV']
models = ['iaf_psc_alpha', 'aeif_psc_alpha', 'iaf_psc_delta']
d = pd.DataFrame.from_records(
{'Area': a, 'Layer': l, 'Population': p,
'NC': nest.Create(random.choice(models), n=random.randint(1, 2000))}
for a in areas for l in layers for p in pops
)
Using the multilevel indexing features of Pandas, we can then create nice tables:
df = d.set_index(['Area', 'Layer', 'Population']).unstack(1).unstack().droplevel(0, axis=1)
df.style.format('{:5}')
will give (requires small patch to NodeCollection in hl_api_types.py to work)
and to pick L5 from two areas
df.loc[['46', 'AITd'], 'L5'].style.format('{:5}')
yields
We can do much more with Pandas multi-level indexing and table reshaping tools.
Everything we select are collections of node collections, so by just summing over values, we can pass things to Connect commands. For example, to connect all L5 populations of areas 46 and AITd to all populations in L4 and L6 of area 7a, we can just do
src = df.loc[['46', 'AITd'], 'L5']
tgt = df.loc['7a', ['L4', 'L6']].to_frame().T
srcn = src.sum().sum()
tgtn = tgt.sum().sum()
nest.Connect(srcn, tgtn, {'rule': 'fixed_indegree', 'indegree': 10})
The double summing is needed to sum over rows and columns of data frames.
There are still some hickups, in particular to NodeCollection.__getattr__() forwarding too much to the NEST kernel for lookup, but that can be ironed out on the Python level.
Issue automatically marked stale!