Multi-scale modeling¶
Tutorial 1.3 covered how to generate biophysically detailed compartmental models of a neuron. Tutorial 2.1 and Tutorial 2.2 covered how to embed neuron models into a network reconstruction, and how to generate synaptic activity that reflects an in vivo condition.
This tutorial will show how to combine the two models into a single multi-scale simlation.
[1]:
from pathlib import Path
tutorial_output_dir = f"{Path.home()}/isf_tutorial_output" # <-- Change this to your desired output directory
[2]:
import Interface as I
from getting_started import getting_started_dir
db = I.DataBase(tutorial_output_dir)['network_modeling']
if not 'biophysical_constraints' in db.keys():
cell_parameter_file = I.os.path.join(
getting_started_dir,
'example_data',
'biophysical_constraints',
'86_C2_center.param')
I.shutil.copy(cell_parameter_file, db.create_managed_folder('biophysical_constraints'))
cell_parameter_file = db['biophysical_constraints'].join('86_C2_center.param')
[INFO] ISF: Current version: heads/develop
[INFO] ISF: Current pid: 50493
[INFO] ISF: Loading mechanisms:
Warning: no DISPLAY environment variable.
--No graphics will be displayed.
[INFO] ISF: Loaded modules with __version__ attribute are:
IPython: 8.12.2, Interface: heads/develop, PIL: 10.4.0, _brotli: 1.0.9, _csv: 1.0, _ctypes: 1.1.0, _curses: b'2.2', _decimal: 1.70, argparse: 1.1, backcall: 0.2.0, blosc: 1.11.1, bluepyopt: 1.9.126, brotli: 1.0.9, certifi: 2024.08.30, cffi: 1.17.0, charset_normalizer: 3.4.0, click: 7.1.2, cloudpickle: 3.1.0, colorama: 0.4.6, comm: 0.2.2, csv: 1.0, ctypes: 1.1.0, cycler: 0.12.1, cytoolz: 0.12.3, dash: 2.18.2, dask: 2.30.0, dateutil: 2.9.0, deap: 1.4, debugpy: 1.8.5, decimal: 1.70, decorator: 5.1.1, defusedxml: 0.7.1, distributed: 2.30.0, distutils: 3.8.20, django: 1.8.19, entrypoints: 0.4, executing: 2.1.0, fasteners: 0.17.3, flask: 1.1.4, fsspec: 2024.10.0, future: 1.0.0, greenlet: 3.1.1, idna: 3.10, ipaddress: 1.0, ipykernel: 6.29.5, ipywidgets: 8.1.5, isf_pandas_msgpack: 0.2.2, itsdangerous: 1.1.0, jedi: 0.19.1, jinja2: 2.11.3, joblib: 1.4.2, json: 2.0.9, jupyter_client: 7.3.4, jupyter_core: 5.7.2, kiwisolver: 1.4.5, logging: 0.5.1.2, markupsafe: 2.0.1, matplotlib: 3.5.1, msgpack: 1.0.8, neuron: 7.8.2+, numcodecs: 0.12.1, numexpr: 2.8.6, numpy: 1.19.2, packaging: 24.2, pandas: 1.1.3, parameters: 0.2.1, parso: 0.8.4, past: 1.0.0, pexpect: 4.9.0, pickleshare: 0.7.5, platform: 1.0.8, platformdirs: 4.3.6, plotly: 5.24.1, prompt_toolkit: 3.0.48, psutil: 6.0.0, ptyprocess: 0.7.0, pure_eval: 0.2.3, pydevd: 2.9.5, pygments: 2.18.0, pyparsing: 3.1.4, pytz: 2024.2, re: 2.2.1, requests: 2.32.3, scandir: 1.10.0, scipy: 1.5.2, seaborn: 0.12.2, six: 1.16.0, sklearn: 0.23.2, socketserver: 0.4, socks: 1.7.1, sortedcontainers: 2.4.0, stack_data: 0.6.2, statsmodels: 0.13.2, sumatra: 0.7.4, tables: 3.8.0, tblib: 3.0.0, tlz: 0.12.3, toolz: 1.0.0, traitlets: 5.14.3, urllib3: 2.2.3, wcwidth: 0.2.13, werkzeug: 1.0.1, yaml: 5.3.1, zarr: 2.15.0, zlib: 1.0, zmq: 26.2.0, zstandard: 0.19.0
Simulating synaptic activity¶
The previous tutorials on network modeling provided us with a functional network that mimics the in vivo condition of the passive touch of a whisker.
Here, we will simulate one single trail for each whisker stimulus and save the result in the database under the simrun
key.
[3]:
delayeds = []
# Delete previous results, if they exist
if 'simrun' in db.keys():
del db['simrun']
# Create new folder for results
db.create_managed_folder('simrun')
for whisker in ['B1', 'B2', 'B3', 'C1', 'C2', 'C3', 'D1', 'D2', 'D3']:
network_file = db['network_param'].join('{}_stim.param'.format(whisker))
dir_prefix = db['simrun'].join('stim_{}'.format(whisker))
nSweeps = 1 # number of consecutive simulations per process
nprocs = 1 # number of processes simulating that task in parallel
tStop = 300 # stop the simulation at 300ms
d = I.simrun_run_new_simulations(
cell_parameter_file, # defined in the above cell
network_file,
dirPrefix = dir_prefix,
nSweeps = nSweeps,
nprocs = nprocs,
tStop = tStop,
silent = False)
delayeds.append(d)
I.simrun_run_new_simulations
returns dask.delayed
objects. These describe how the computation can be run, but it is not computed yet. We send it to our distributed cluster for that:
[4]:
client = I.get_client()
client
[4]:
Client
|
Cluster
|
[5]:
futures = client.compute(delayeds)
You can inspect the progress of the simulation at the dask Dashboard
, linked above.
After this is complete, the “simrun” folder in the DataBase contains the simulation results
[6]:
db['simrun'].listdir()
[6]:
['Loader.json', 'metadata.json']
Analyzing the results¶
here, we use DataBase to parse our simulation results to high-performance file formats. For more info, refer to the data_base.db_initializers. These initialize simulation results in a database for convenient access to the data using the widely used pandas and dask libraries. It also converts the data to an optimized binary format: msgpack. This gives the following advantages:
the
msgpack
format is extremely fastmsgpack
allows forblosc
compression, reducing space on hard drive (<25% of original data)cell activation data is categorized i.e. repeated values are replaced by an integer
[8]:
I.db_init_simrun_general.init(
db,
db['simrun'],
client=client
)
Now, the following keys are set:
Key |
Description |
Type |
---|---|---|
|
activation of synapses for each simulationtrail |
|
|
activation of presynaptic cells for each simulationtrail |
|
|
soma voltage traces |
|
|
spike times |
|
|
voltage traces on the dendrite as specified in the cell parameter file |
|
|
original filenames |
|
|
mapping between simulation trial and used parameter files |
|
|
folder containing the cell parameter files |
|
|
folder containing the network parameterfiles |
|
|
all simulation trials |
|
|
original path from which the simulation has been loaded |
|
Let’s have a look at the synapse activation data frame:
[9]:
db['synapse_activation']
[9]:
synapse_type | synapse_ID | soma_distance | section_ID | section_pt_ID | dendrite_label | 0 | 1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
npartitions=9 | |||||||||||||
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | object | int64 | float64 | int64 | int64 | object | float64 | float64 | float64 | float64 | float64 | float64 | float64 |
stim_B2/results/20250327-1823_seed1559350411_pid36745/000000 | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
stim_D3/results/20250327-1823_seed622584983_pid36112/000000 | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
stim_D3/results/20250327-1823_seed622584983_pid36112/000000 | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
This is a dask dataframe object. Instead of loading the data fully, it is a pointer to the actual data. Only when actual computations need to be done, are the values of the data loaded. Until then, it will only provide relevant metadata. Let’s explicitly load a small fraction of the data to have a look:
[10]:
head = db['synapse_activation'].head(2)
head
[10]:
synapse_type | synapse_ID | soma_distance | section_ID | section_pt_ID | dendrite_label | 0 | 1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
sim_trial_index | |||||||||||||
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | L1_B1 | 3 | 1441.802898 | 46 | 19 | ApicalDendrite | 99.623881 | NaN | NaN | NaN | NaN | NaN | NaN |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | L1_B2 | 6 | 1366.422297 | 78 | 31 | ApicalDendrite | 253.943241 | NaN | NaN | NaN | NaN | NaN | NaN |
We can see that the dataframe saves each synapse activation, including the synapse type, where on the neuron this synapse was (both in terms of soma distance, and section ID). The activation times are saved in the numbered clumns on the right. It is possible for a synapse to activate more than once during a simulation, which is why there are multiple numbered columns.
We can select data using the sim_trial_index
as well, which indexes all synapse activations that belong to a single simulation:
[11]:
some_index = db['synapse_activation'].compute().index[0]
db['synapse_activation'].loc[some_index]
[11]:
synapse_type | synapse_ID | soma_distance | section_ID | section_pt_ID | dendrite_label | 0 | 1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
npartitions=1 | |||||||||||||
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | object | int64 | float64 | int64 | int64 | object | float64 | float64 | float64 | float64 | float64 | float64 | float64 |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
This returns a dask dataframe again; the query is not computed yet. To do this, call compute:
[12]:
db['synapse_activation'].loc[some_index].compute()
[12]:
synapse_type | synapse_ID | soma_distance | section_ID | section_pt_ID | dendrite_label | 0 | 1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
sim_trial_index | |||||||||||||
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | L1_B1 | 3 | 1441.802898 | 46 | 19 | ApicalDendrite | 99.623881 | NaN | NaN | NaN | NaN | NaN | NaN |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | L1_B2 | 6 | 1366.422297 | 78 | 31 | ApicalDendrite | 253.943241 | NaN | NaN | NaN | NaN | NaN | NaN |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | L1_B3 | 1 | 1227.108477 | 40 | 169 | ApicalDendrite | 137.176301 | NaN | NaN | NaN | NaN | NaN | NaN |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | L1_C1 | 2 | 1484.417691 | 57 | 87 | ApicalDendrite | 279.390388 | NaN | NaN | NaN | NaN | NaN | NaN |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | L1_C1 | 3 | 1439.699450 | 57 | 68 | ApicalDendrite | 279.390388 | NaN | NaN | NaN | NaN | NaN | NaN |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | VPM_C2 | 240 | 18.796248 | 94 | 23 | Dendrite | 110.169047 | NaN | NaN | NaN | NaN | NaN | NaN |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | VPM_C2 | 252 | 53.605914 | 85 | 72 | Dendrite | 259.144935 | NaN | NaN | NaN | NaN | NaN | NaN |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | VPM_C2 | 254 | 299.458583 | 33 | 46 | ApicalDendrite | 259.144935 | NaN | NaN | NaN | NaN | NaN | NaN |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | VPM_D1 | 8 | 156.063845 | 138 | 107 | Dendrite | 64.026858 | NaN | NaN | NaN | NaN | NaN | NaN |
stim_B1/results/20250327-1823_seed4095671765_pid36080/000000 | VPM_Gamma | 1 | 300.193010 | 20 | 153 | ApicalDendrite | 286.024718 | NaN | NaN | NaN | NaN | NaN | NaN |
3435 rows × 13 columns
Analytics¶
What is the average distance of synapses from the soma by celltype?
[13]:
# load dask dataframe
sa = db['synapse_activation']
# assign a new column: the celltype is the first part of the synapse_type column
sa['celltype'] = sa.map_partitions(lambda x: x.synapse_type.str.split('_').str[0])
# groupb by this value, compute the mean soma distance in each group, compute the result on the cluster
sa.groupby('celltype').apply(lambda x: x.soma_distance.mean()).compute(scheduler=client)
[WARNING] py: `meta` is not specified, inferred from partial data. Please provide `meta` if the result is unexpected.
Before: .apply(func)
After: .apply(func, meta={'x': 'f8', 'y': 'f8'}) for dataframe result
or: .apply(func, meta=('x', 'f8')) for series result
[13]:
celltype
L56Trans 167.630698
L1 1397.626394
L4py 276.866549
L6ct 174.283450
SymLocal1 1161.161267
SymLocal6 151.661303
L4sp 505.903436
L45Sym 201.920454
L5tt 234.639963
SymLocal4 120.095730
L23Trans 891.155617
L45Peak 361.240906
L4ss 410.789081
L2 753.778724
SymLocal3 218.974242
SymLocal5 154.413647
VPM 266.467875
L34 498.396412
L5st 786.884596
L6cc 322.750214
L6ccinv 227.546271
SymLocal2 676.986399
dtype: float64
Visualization¶
It is easy to create an animation of a simulation trail from the database:
[14]:
cell = I.simrun_simtrial_to_cell_object(db, some_index)
2D animation of membrane voltage¶
[15]:
if not 'D3_stim_animation' in db.keys():
I.cell_to_animation(
cell,
xlim = [0,1500],
ylim = [-90, 50],
tstart = 245-25,
tend = 245+25,
tstep = 0.2,
outdir = db.create_managed_folder('D3_stim_animation'))
I.display_animation(db['D3_stim_animation'].join('*', '*png'), embedded = True)
3D animation of the membrane voltage and synapse activations¶
[16]:
from visualize.cell_morphology_visualizer import CellMorphologyVisualizer
# # To remake the visualization, uncomment this code
# if I.os.path.exists(db._basedir/'D3_stim_animation_3d'):
# I.shutil.rmtree(db._basedir/'D3_stim_animation_3d')
images_path = str(db._basedir/'D3_stim_animation_3d')
cmv = CellMorphologyVisualizer(
cell,
t_start=245-25,
t_stop=245+25,
t_step=0.2
)
cell_population = cell.synapses.keys()
cell_population_colors = {
synapse: I.plt.cm.Set3(i/len(cell_population))
for i, synapse in enumerate(cell_population)}
cell_population_colors['inactive'] = "#f0f0f0"
cmv.population_to_color_dict = cell_population_colors
cmv.animation(
images_path=images_path,
color="voltage",
show_synapses=True,
client=I.get_client(),
)