Tutorial: Single Cell Simulation with External Feedforward Input (with BioNet)#

In the previous tutorial we built a single cell and stimulated it with a current injection. In this example we will keep our single-cell network, but instead of stimulation by a step current, we’ll set-up an external network that synapses onto our cell.

Note - scripts and files for running this tutorial can be found in the directory sources/chapter02/

Requirements:

  • Python 2.7 or 3.6+

  • bmtk

  • NEURON 7.4+

For more information on the BioNet Simulator, please see the BioNet Overview.

For more information on BMTK and SONATA format, please see the Overview of BMTK and Network Builder pages.

1: Building the network.#

Similar to the previous tutorial (see for description of input parameters), we want to build and save a network consisting of a single biophysically detailed cell.

[1]:
from bmtk.builder.networks import NetworkBuilder

# Main network
cortex = NetworkBuilder('mcortex')
cortex.add_nodes(
    cell_name='Scnn1a_473845048',
    potental='exc',
    model_type='biophysical',
    model_template='ctdb:Biophys1.hoc',
    model_processing='aibs_perisomatic',
    dynamics_params='472363762_fit.json',
    morphology='Scnn1a_473845048_m.swc'
)

[2]:
cortex.build()
cortex.save_nodes(output_dir='sim_ch02/network')

Next we will create a collection of external spike-generating cells that will synapse onto our cell. To do this we create a second network which can represent some external inputs (such as input from the thalamus to our cortical cell). We will call our network “mthalamus”, and it will consist of 10 cells. These cells are not biophysical but instead “virtual” cells. Virtual cells don’t have a morphology or the normal properties of a neuron, but rather act as spike generators.

[3]:
# Input network
thalamus = NetworkBuilder('mthalamus')
thalamus.add_nodes(
    N=10,
    pop_name='tON',
    potential='exc',
    model_type='virtual'
)

Now that we built our nodes (cells), we want to create a connection between our 10 thalamic cells onto our cortex cell. To do so we use the add_edges function like so:

[4]:
# Connect the input network to the main network
thalamus.add_edges(
    source={'pop_name': 'tON'}, target=cortex.nodes(),
    connection_rule=5,
    syn_weight=0.001,
    delay=2.0,
    weight_function=None,
    target_sections=['basal', 'apical'],
    distance_range=[0.0, 150.0],
    dynamics_params='AMPA_ExcToExc.json',
    model_template='exp2syn'
)
[4]:
<bmtk.builder.connection_map.ConnectionMap at 0x7f71ede5ac40>

Let us break down how this method call works:

thalamus.add_edges(source={'pop_name': 'tON'}, target=cortex.nodes(),
  • Here we specify which set of nodes to use as sources and targets. Our source/pre-synaptic cells are all thalmus cells with the property “pop_name=tON”, which in this case is every thalamus cell (We could also use source=thalamus.nodes(), or source={'level_of_detail': 'filter'}). The target/post-synaptic cells are all cell(s) of the “cortex” network.

connection_rule=5,
  • The connection_rule parameter determines how many synapses exist between every source/target pair. In this very trivial case we are indicating that for every thalamic –> cortical cell connection, there are 5 synapses. In future tutorials we will show how we can create more complex customized rules.

syn_weight=0.001,
delay=2.0,
weight_function=None,
  • Here we are specifying the connection weight. For every connection in this edge-type, there is a strength of 1e-03 and a synaptic delay of 2 ms. The weight function is used to adjust the weights before runtime. Later we will show how to create customized weight functions.

target_sections=['basal', 'apical'],
distance_range=[0.0, 150.0],
  • This is used by BioNet to determine where on the post-synaptic cell to place the synapse. By default placement is random within the given section and range.

dynamics_params='AMPA_ExcToExc.json',
model_template='exp2syn')
  • The dynamics_params file contains the parameters of the synapse, including the time constant and potential. Here we are using an AMPA type synaptic model with an Excitatory connection. The model_template parameter is used by BioNet to convert the model into a valid NEURON synaptic object.

Finally we are ready to build the model and save the thalamic nodes and edges.

[5]:
thalamus.build()
thalamus.save_nodes(output_dir='sim_ch02/network')
thalamus.save_edges(output_dir='sim_ch02/network')

The network/ directory will contain multiple nodes and edges files. It should have nodes (and node-types) files for both the thalamus and cortex network. And edges (and edge-types) files for the thalamus –> cortex connections. Nodes and edges for different networks and their connections are spread out across different files which allows us in the future to rebuild, edit or replace part of setup in a piecemeal and efficient manner.

Spike Trains#

We need to give our 10 thalamic cells spike trains. There are multiple ways to do this, but an easy way is to use a sonata hdf5 file. The following function will create a file to provide the spikes for our 10 cells:

[6]:
from bmtk.utils.reports.spike_trains import PoissonSpikeGenerator

psg = PoissonSpikeGenerator(population='mthalamus')
psg.add(
    node_ids=range(10),  # Have 10 nodes to match mthalamus
    firing_rate=10.0,    # 10 Hz, we can also pass in a nonhomoegenous function/array
    times=(0.0, 3.0)    # Firing starts at 0 s up to 3 s
)
psg.to_sonata('sim_ch02/inputs/mthalamus_spikes.h5')

The spikes files are save as a Sonata formated spikes train. BMTK also allows CSV files and NWB files to set the spike train - we just have to specify the appropriate format in the configuration file (see bottom of tutorial for an example).

[7]:
print('Number of spikes: {}'.format(psg.n_spikes()))
print('Units: {}'.format(psg.units()))

psg.to_dataframe().head()
Number of spikes: 315
Units: ms
[7]:
node_ids timestamps population
0 0 45.327109 mthalamus
1 0 77.644564 mthalamus
2 0 163.482462 mthalamus
3 0 517.296866 mthalamus
4 0 535.154362 mthalamus

2: Setting up BioNet environment#

File structure:#

Before running a simulation, we will need to create the runtime environment, including parameter files, run-script and configuration files. If using the tutorial these files will already be in place, however you should run the following anyway in a command-line:

$ python -m bmtk.utils.sim_setup  \
   --report-vars v,cai            \
   --network sim_ch02/network     \
   --spikes-inputs mthalamus:sim_ch02/inputs/mthalamus_spikes.h5 \
   --dt 0.1             \
   --tstop 3000.0       \
   --include-examples   \
   --compile-mechanisms \
   bionet sim_ch02

Or call the function directly in python:

[ ]:
from bmtk.utils.sim_setup import build_env_bionet

build_env_bionet(
    base_dir='sim_ch02',
    config_file='config.json',
    network_dir='sim_ch02/network',
    tstop=3000.0, dt=0.1,
    report_vars=['v', 'cai'],    # Record membrane potential and calcium (default soma)
    spikes_inputs=[('mthalamus', # Name of population which spikes will be generated for
                    'sim_ch02/inputs/mthalamus_spikes.h5')],
    include_examples=True,       # Copies components files
    compile_mechanisms=True      # Will try to compile NEURON mechanisms
)

Add input network parameters to config file:#

The last thing that we need to do is to update the configuration file to read the thalamus_spikes.csv as input. To do so we open simulation_config.json in a text editor and add the following to the input section:

{
  "inputs": {
    "mthalamus_spikes": {
      "input_type": "spikes",
      "module": "sonata",
      "input_file": "${BASE_DIR}/input/mthalamus_spikes.csv",
      "node_set": "mthalamus"
    }
  }
}

3. Running the simulation#

Once our config file is setup we can run a simulation either through the command line:

$ cd sim_ch02/
$ python run_bionet.py config.json

or through the script:

[9]:
from bmtk.simulator import bionet


conf = bionet.Config.from_json('sim_ch02/config.json')
conf.build_env()
net = bionet.BioNetwork.from_config(conf)
sim = bionet.BioSimulator.from_config(conf, network=net)
sim.run()
2022-08-08 16:06:48,667 [INFO] Created log file
INFO:NEURONIOUtils:Created log file
numprocs=1
2022-08-08 16:06:48,750 [INFO] Building cells.
INFO:NEURONIOUtils:Building cells.
2022-08-08 16:06:48,919 [INFO] Building recurrent connections
INFO:NEURONIOUtils:Building recurrent connections
2022-08-08 16:06:48,922 [INFO] Building virtual cell stimulations for mthalamus_spikes
INFO:NEURONIOUtils:Building virtual cell stimulations for mthalamus_spikes
2022-08-08 16:06:48,955 [INFO] Running simulation for 3000.000 ms with the time step 0.100 ms
INFO:NEURONIOUtils:Running simulation for 3000.000 ms with the time step 0.100 ms
2022-08-08 16:06:48,956 [INFO] Starting timestep: 0 at t_sim: 0.000 ms
INFO:NEURONIOUtils:Starting timestep: 0 at t_sim: 0.000 ms
2022-08-08 16:06:48,957 [INFO] Block save every 5000 steps
INFO:NEURONIOUtils:Block save every 5000 steps
2022-08-08 16:06:49,289 [INFO]     step:5000 t_sim:500.00 ms
INFO:NEURONIOUtils:    step:5000 t_sim:500.00 ms
2022-08-08 16:06:49,582 [INFO]     step:10000 t_sim:1000.00 ms
INFO:NEURONIOUtils:    step:10000 t_sim:1000.00 ms
2022-08-08 16:06:49,881 [INFO]     step:15000 t_sim:1500.00 ms
INFO:NEURONIOUtils:    step:15000 t_sim:1500.00 ms
2022-08-08 16:06:50,176 [INFO]     step:20000 t_sim:2000.00 ms
INFO:NEURONIOUtils:    step:20000 t_sim:2000.00 ms
2022-08-08 16:06:50,495 [INFO]     step:25000 t_sim:2500.00 ms
INFO:NEURONIOUtils:    step:25000 t_sim:2500.00 ms
2022-08-08 16:06:50,800 [INFO]     step:30000 t_sim:3000.00 ms
INFO:NEURONIOUtils:    step:30000 t_sim:3000.00 ms
2022-08-08 16:06:50,808 [INFO] Simulation completed in 1.853 seconds
INFO:NEURONIOUtils:Simulation completed in 1.853 seconds

4. Analyzing the run#

[10]:
from bmtk.analyzer.spike_trains import to_dataframe

results_df = to_dataframe(config_file='sim_ch02/config.json')
print('Number of Spikes: {}'.format(len(results_df)))
results_df.head()
Number of Spikes: 57
[10]:
timestamps node_ids population
0 45.0 0 mcortex
1 1375.1 0 mcortex
2 1532.4 0 mcortex
3 1686.9 0 mcortex
4 1780.4 0 mcortex
[11]:
from bmtk.analyzer.compartment import plot_traces

_ = plot_traces(config_file='sim_ch02/config.json', node_ids=[0], report_name='v_report')
_ = plot_traces(config_file='sim_ch02/config.json', node_ids=[0], report_name='cai_report')
../_images/tutorials_tutorial_02_single_cell_syn_22_0.png
../_images/tutorials_tutorial_02_single_cell_syn_22_1.png

5. Additional information#

Changing edge properties#

When using the Network Builder add_edges method, we gave all the edges the same parameter values (delay, weight, target_section, etc.). All connections created by this method call constitute a single edge-type that share the same parameters, and are specified in the mthalamic_edge_type.csv file:

[12]:
import pandas as pd
pd.read_csv('sim_ch02/network/mthalamus_mcortex_edge_types.csv', sep=' ')
[12]:
edge_type_id target_query source_query model_template weight_function target_sections delay dynamics_params syn_weight distance_range
0 100 * pop_name=='tON' exp2syn NaN ['basal', 'apical'] 2.0 AMPA_ExcToExc.json 0.001 [0.0, 150.0]

(if in the build script called add_edges multiple times, we’d have multiple edge-types).

Using a simple text-editor, we can modify this file directly or change parameters before a simulation run without having to rebuild the entire network (although for a network this small it may not be beneficial).

Weight function#

By default BioNet uses the value in syn_weight to set a synaptic weight, which is a constant stored in the network files. Often we will want to adjust the synaptic weight between simulations, but don’t want to have to regenerate the network. BioNet allows us to specify custom synaptic weight functions that will calculate the synaptic weight before each simulation.

To do so first we must set the value of ‘weight_function’ column. Either we can open up the file mthalamus_mcortex_edge_types.csv with a text-editor and change the column.

edge_type_id

target_query

source_query

weight_function

100

*

pop_name==’tON’

adjusted_weight

or we can rebuild the edges:

thalamus.add_edges(source={'pop_name': 'tON'}, target=cortex.nodes(),
                   connection_rule=5,
                   syn_weight=0.001,
                   weight_function=adjusted_weight,
                   delay=2.0,
                   target_sections=['basal', 'apical'],
                   distance_range=[0.0, 150.0],
                   dynamics_params='AMPA_ExcToExc.json',
                   model_template='exp2syn')

Then we write a custom weight function. The weight functions will be called during the simulation when building each synapse, and requires three parameters - target_cell, source_cell, and edge_props. These three parameters are dictionaries which can be used to access properties of the source node, target node, and edge, respectively. The function must return a floating point number which will be used to set the synaptic weight:

def adjusted_weights(target_cell, source_cell, edge_props):
    if target_cell['cell_name'] == 'Scnn1a':
        return edge_prop["weight_max"]*0.5
    elif target_cell['cell_name'] == 'Rorb'
        return edge_prop["weight_max"]*1.5
    else:
        ...

Finally we must tell BioNet where to access the function which we can do by using the add_weight_function:

from bmtk.simulator import bionet

bionet.nrn.add_weight_function(adjusted_weights)

conf = bionet.Config.from_json('config.json')
...

Using NWB files to set spike trains#

Instead of using CSV files to set the spike trains of our external network, we can also use NWB files. The typical setup would look like the following in the config file:

{
  "inputs": {
    "LGN_spikes": {
      "input_type": "spikes",
      "module": "nwb",
      "input_file": "$INPUT_DIR/lgn_spikes.nwb",
      "node_set": "lgn",
      "trial": "trial_0"
    },
  }
}