nest-simulator icon indicating copy to clipboard operation
nest-simulator copied to clipboard

Arbitrarily nested neuron groups with per-group iterator

Open otcathatsya opened this issue 1 year ago • 3 comments

(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.Group but some method or operator overload is needed to distinguish from the default n1 + n2 flattened collection addition.
  • While there's some similarities to pooling connections as done in exabrainprep this should be a conceptually independent change.

otcathatsya avatar Oct 01 '24 14:10 otcathatsya

Great step that you open this tread. One or more extensions/suggestions/corrections to your comment:

  • [ ] g1 is undefined. You named the variable group1.
  • [ ] 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]]

babsey avatar Oct 02 '24 07:10 babsey

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)

image

and to pick L5 from two areas

df.loc[['46', 'AITd'], 'L5'].style.format('{:5}')

yields image

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.

heplesser avatar Oct 03 '24 22:10 heplesser

Issue automatically marked stale!

github-actions[bot] avatar Dec 03 '24 08:12 github-actions[bot]