Skip to content
Snippets Groups Projects
Unverified Commit fe1784ad authored by Thorsten Hater's avatar Thorsten Hater
Browse files

Version 0.11

parent 80799a5d
Branches
No related tags found
No related merge requests found
%% Cell type:markdown id:a66f325c-0b73-4a9f-a863-db75a54e20ab tags:
# Welcome
This tutorial is a hands-on introduction to Arbor and detailled biophysical cell models, without
much regard for background in theory. Please ask questions early and often.
**Disclaimer**: For compatibility with the eBrain collaboratory, this assumes Arbor 0.9.0, which is quite outdated.
%% Cell type:markdown id:bb8fb18d-f34f-46e8-81a7-f20c9273d820 tags:
# Setup
Before you start:
```
python3 -m venv venv
source venv/bin/activate
pip3 install arbor==0.10 numpy seaborn matplotlib pandas
```
If you did this _after_ starting Jupyter, you'll need to restart!
Check everything is in order
%% Cell type:code id:dd9548cc-823f-49cf-aeb8-75348d68ac8f tags:
``` python
%matplotlib inline
import arbor as A
A.config()
```
%% Output
{'mpi': False,
'mpi4py': False,
'gpu': None,
'vectorize': False,
'profiling': False,
'neuroml': True,
'bundled': True,
'version': '0.10.0',
'bundled': False,
'version': '0.11.0',
'source': 'unknown commit',
'build_config': 'RELEASE',
'arch': 'native',
'prefix': '/var/folders/m_/cksx93ys47x4621g0zbw_m4m0000gn/T/tmpcq521qip/wheel/platlib/arbor',
'prefix': '/var/folders/7g/cnfrzbd555q9lmpsr1byngs80000gn/T/tmpdxrhu4mr/wheel/platlib/arbor',
'python_lib_path': '',
'binary_path': 'bin',
'lib_path': 'lib',
'data_path': 'share',
'CXX': '/Applications/Xcode_15.4.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++',
'pybind-version': '2.11.1',
'timestamp': 'Aug 9 2024 10:52:04'}
'pybind-version': '2.13.6',
'timestamp': 'Apr 24 2025 18:04:13'}
%% Cell type:code id:0eb57417-f46a-47e2-a611-ebc7d8705b33 tags:
``` python
assert(A.config()['version'] == '0.10.0')
assert(A.config()['version'] == '0.11.0')
```
%% Cell type:markdown id:b2f531c4-fb2a-4d4d-94c7-c8e61d59133b tags:
# Introduction
In this tutorial we will get you started on the very basics of building a single cell model in Arbor.
%% Cell type:markdown id:87ed8c94-666c-4a04-ad21-9501fb663457 tags:
## Helpers
These are details, but if you want, you can read them.
%% Cell type:code id:6de1f20f-ae30-4193-9fc3-4e7ceacacad9 tags:
``` python
from utils import plot_morphology
```
%% Cell type:markdown id:64e0bd8e-fd14-4ade-910b-4b1f723e5bba tags:
# A Single Cell Model in Arbor
%% Cell type:markdown id:9326d0e8-f66e-431e-b5a9-90357c1b5f76 tags:
We are going to start with the simplest model in Arbor, a single cell without network connections or complex distribution of dynamics.
%% Cell type:code id:5c8f51cf-623a-4e0b-a203-8518df79d80f tags:
``` python
# Arbor's unit support
from arbor import units as U
# we will also get support to plot things
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
```
%% Cell type:markdown id:883ac8c5-8628-4ef7-8aa9-ca01c394c250 tags:
## Morphologies
%% Cell type:markdown id:cf57b7fd-eae5-4009-88e0-627c70673ef9 tags:
In Arbor, we separate the main concerns
- Cell / Network models
- Discretisation
- Execution on a computer
Cell models are built from
- morphology
- parametrisation
- discretisation
We will start our model with the morphology, which is a structure of connected segments describing the physical layout of the cell.
%% Cell type:code id:3bf63afc-05ac-4841-981c-a008fa2e754c tags:
``` python
mrf = A.load_swc_neuron('Acker2008.swc')
```
%% Cell type:code id:cd3a6a23-4aa3-42f9-ac2f-4d1f33dfcd36 tags:
``` python
# we can take a look at the cell
plot_morphology(mrf)
```
%% Output
(<Figure size 1000x1000 with 1 Axes>,
<Axes3D: xlabel='x $(\\mu m)$', ylabel='y $(\\mu m)$', zlabel='z $(\\mu m)$'>)
%% Cell type:markdown id:10537544-1178-4f48-8354-3bfd89cf2319 tags:
## Adding Dynamics and Parameters
%% Cell type:markdown id:2c41fe19-b1b9-4150-b2f5-4f5915906c4a tags:
Loading the morphology gave us access to the geometry of the cell in the data file.
However, the SWC data format specifies some predefined labels for parts of the cell:
- `dend`
- `soma`
- `axon`
- `apic`
to reference these later, we use a dict-like structure:
%% Cell type:code id:ed103931-11e7-4061-8bb5-390c6bc7b6bf tags:
``` python
lbl = mrf.labels
# SWC specified tags are loaded with SWC
lbl
```
%% Output
(label_dict (region "soma" (tag 1)) (region "dend" (tag 3)) (region "apic" (tag 4)) (region "axon" (tag 2)))
%% Cell type:markdown id:b0f67cbb-3a73-4c98-828e-22073c088537 tags:
Note that Arbor differentiates between sets of points (`locset`) and contiguous parts
of the morphology (`region`). The specification format might look a bit weird, but is
easy to handle after a short introduction. For now, we will just use them as is and won't
waste much thought.
We can add more by writing expressions like
%% Cell type:code id:dd45db12-bafc-4f2e-afd5-ac08fd8a407a tags:
``` python
lbl['ctr'] = '(location 0 0.5)'
```
%% Cell type:markdown id:ec1c99cd-799b-4a5d-af1f-e98d701407b2 tags:
If this doesn't make sense, don't worry, for now it's sufficient to note that this gives us the centre
of the first segment (which is the soma by definition). Arbor's way of defining regions and locations
is extensively documented and allows placing objects on cells without caring about the way it will be
split into compartments. Using labels is optional, if convenient.
Let's take a look at the currently defined items
%% Cell type:code id:197c81e5-e3bc-4568-9ae2-7d3fec37857c tags:
``` python
lbl
```
%% Output
(label_dict (region "soma" (tag 1)) (region "dend" (tag 3)) (region "apic" (tag 4)) (region "axon" (tag 2)) (locset "ctr" (location 0 0.5)))
%% Cell type:markdown id:14348b72-f5a7-49d9-abea-39e45a933d19 tags:
Now, we can add items to the cell, which is done by manipulating a new object, the `decor`,
which is simply a list of places and the things to attach to them. This split allows us to
use morphologies and decors multiple times in the same simulation.
%% Cell type:code id:48ac3953-a9d5-40c3-9edd-d1933136da2b tags:
``` python
# empty decoration
dec = A.decor()
# ion channels are attached to regions using `paint`
# we add Hodgkin-Huxley to the soma and a passive
# current to the dendrite
dec.paint('"soma"', A.density('hh'))
dec.paint('"dend"', A.density('pas'))
```
%% Output
<arbor._arbor.decor at 0x10ed37bb0>
<arbor._arbor.decor at 0x10e5f7b70>
%% Cell type:markdown id:630e3b30-91ca-4878-88d9-e7968e8935d6 tags:
From morphology, decor, and labels we can build a cell
%% Cell type:code id:d7aa322a-3a05-4c9d-9ec1-a7831c933d3d tags:
``` python
cell = A.cable_cell(mrf.morphology, dec, lbl)
```
%% Cell type:markdown id:922bd0f7-1f71-40ca-aaba-0c69e08d432f tags:
which in turn can be directly used to make a simulation
%% Cell type:code id:9cf72d16-9da3-42b7-a88a-24446ab27ea5 tags:
``` python
sim = A.single_cell_model(cell)
```
%% Cell type:markdown id:ab5fcf65-6ca4-4db5-a889-0bb7ac974388 tags:
and run
%% Cell type:code id:93f5eaeb-7471-4205-9667-1b911ebd4c7e tags:
``` python
sim.run(tfinal=1 * U.s, dt=0.1 * U.ms)
```
%% Cell type:markdown id:3b3f2f9d-552c-4aed-b570-31409846cde2 tags:
## Adding Plotting
%% Cell type:markdown id:5c642115-800e-429f-beb7-a94c9d46e3e3 tags:
So far, we have run the simulation, but haven't seen it 'work'. Let's change that.
%% Cell type:code id:1f428d32-ea08-4c6b-b25b-c9859d2f6506 tags:
``` python
# Make a new simulation object, forgetting all the state we had
# before. There is no need to restart Python or reset anything else!
dec.paint('(all)', Vm=-69 * U.mV)
cell = A.cable_cell(mrf.morphology, dec, lbl)
sim = A.single_cell_model(cell)
# add membrane potential recording
# - at the soma centre
# - get a handle 'Um'
# - one sample every 1ms
sim.probe(what='voltage', where='"ctr"', frequency=1*U.kHz, tag='Um')
# re-run the simulation
sim.run(tfinal=1 * U.s, dt=0.1 * U.ms)
```
%% Cell type:markdown id:ae8b8495-22e5-4e47-ae79-28f0663ca37d tags:
Now, we can plot the voltage trace
%% Cell type:code id:d9f85c62-b70f-4836-ae06-8e5a4838d779 tags:
``` python
fg, ax = plt.subplots()
ax.plot(sim.traces[0].time, sim.traces[0].value)
```
%% Output
[<matplotlib.lines.Line2D at 0x10ebcb470>]
[<matplotlib.lines.Line2D at 0x10e41e270>]
%% Cell type:markdown id:67f88fdb-5bae-472f-a6ba-2bd227b21bfa tags:
## Adding an Input
%% Cell type:markdown id:1d29166c-0048-4411-a153-607e18978ac1 tags:
While we can plot the membrane potential, it is not overly interesting, as the cell
just evolves under it's own volition. Adding a stimulus current should change that.
%% Cell type:code id:62d70b44-c7ec-4b1a-abf0-f1b60071357a tags:
``` python
# we could just add to the decor, but doing this multiple
# time will add multiple current clamps. So, we copy it
dec_ic = A.decor(dec)
# nb. attaching items to locsets requires `place`
# and giving a name!
dec_ic.place('"ctr"', A.iclamp(tstart=100*U.ms, duration=10*U.ms, current=20*U.nA), 'ic')
dec_ic.place('"ctr"', A.iclamp(tstart=200*U.ms, duration=10*U.ms, current=20*U.nA), 'ic')
# and rebuild the cell
cell = A.cable_cell(mrf.morphology, dec_ic, lbl)
# next, just re-run everything
sim = A.single_cell_model(cell)
sim.probe(what='voltage', where='"ctr"', frequency=1*U.kHz, tag='Um')
sim.run(tfinal=500*U.ms, dt=0.1*U.ms)
# and plot
fg, ax = plt.subplots()
ax.plot(sim.traces[0].time, sim.traces[0].value)
```
%% Output
[<matplotlib.lines.Line2D at 0x10ec04b30>]
[<matplotlib.lines.Line2D at 0x10e694ad0>]
%% Cell type:markdown id:548e8183-1cbc-45a3-a2b4-cc41025cd162 tags:
## Summary
You have seen how cable cells are made from simple components, how simulations are run, and how data is obtained from the model.
If you are coming from NEURON or Brian2, some things might seem familiar and others might seem strange.
Arbor is closest in the underlying theory and design to NEURON, but there are substantial differences in the interface and low level implementation (very much for the better, we hope).
If you are interested, here's some pointers for further reading:
- `single_cell_model` is a convenience class over Arbor's `recipe` which is used to construct networks and more elaborate single cell models
- `modcc` and `arbor-build-catalogue` allow for the construction of your own ion channels from an language called `NMODL`. Most NEURON channels are written in this language and can easily be adapted to work with Arbor.
- there are more parameters on cells, channels, and simulations than we have shown so far
- in `single_cell_model` data collection is much simplified, but also way less powerful than in the `recipe` interface
- we haven't talked about discretisation so far, by default one compartment per segment is used
- the region/locset definitions in this example only scratch the surface of what is possible
The tutorial continues in the next part `02` introducing the `recipe`; if you have time left, you can investigate some of the ideas below.
%% Cell type:markdown id:1646f898-14e8-407c-9761-ed41e3a1d296 tags:
# Ideas
- Place more / different stimuli in different places
- Plot the voltage at different places, simultaneously, by adding more probes with varying `where` arguments.
Here's the documentation for placing things at different sets of locations: https://docs.arbor-sim.org/en/v0.10.0/concepts/labels.html#locset-expressions
%% Cell type:code id:f304a758-b842-4b18-8e7a-a36b2444d1f8 tags:
``` python
```
Here's the documentation for placing things at different sets of locations: https://docs.arbor-sim.org/en/v0.11.0/concepts/labels.html#locset-expressions
......
%% Cell type:markdown id:fe988a6d-5f04-43a2-ad24-eab68e9d8836 tags:
# Unlocking the Recipe
In the first part, we learned to utilise Arbor through `single_cell_model`, a convenient interface.
However, it is hiding Arbor's true potential. To see it in action, we need to use the more verbose `recipe`,
which will give options for:
- multiple cells in the same simulation, including heterogeneous mixtures
- network connections and gap junctions between cells
- event-based inputs
- reading out more state variables
This short interlude will recreate the last tutorial's setup using the `recipe`. To understand the central
idea, one can consider them as a list of instructions to be handed to Arbor's simulation core, which will
then create the simulation by repeatedly interrogating the instructions. The name, hence, is analoguous to
a real-world _recipe_ which is present while baking a cake and used to look up steps and measurements.
%% Cell type:markdown id:bd687d56-3bdd-4abb-b602-28512383c373 tags:
## To make a Recipe
In programming terms, a recipe is a class bundling a list of callbacks to used create a simulation.
This construction allows Arbor to optimise memory allocation and layout, create only what is actually
needed, and distribute work across hardware without sharing.
Once again, we start by importing some libraries
%% Cell type:code id:14c35e56-8851-4201-a2e5-1fc23cc457d4 tags:
``` python
# begin by importing Arbor
import arbor as A
from arbor import units as U
# we will also get support to plot things
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
```
%% Cell type:markdown id:28a0c6f7-8e58-4648-965b-8ad2442a619a tags:
Now, we create a Python class inheriting from Arbor's `recipe` class.
%% Cell type:code id:96934046-ddd0-430e-b948-4301fcc4e04c tags:
``` python
# WON'T RUN
class recipe(A.recipe):
def __init__(self):
A.recipe.__init__(self)
```
%% Cell type:markdown id:b9366b6f-550d-4bcb-b109-8f38018e0296 tags:
We can try and use this to run a simulation like so
%% Cell type:code id:4938aaa2-efd0-4777-bfe1-1cefb13d09ce tags:
``` python
rec = recipe()
sim = A.simulation(rec)
```
%% Output
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[6], line 2
Cell In[3], line 2
1 rec = recipe()
----> 2 sim = A.simulation(rec)
RuntimeError: Tried to call pure virtual function "recipe::num_cells"
%% Cell type:markdown id:b27f1d55-a3d2-4d26-8052-9b35d41eed0c tags:
... but are greeted with failure, since our implementation is incomplete.
Each recipe _must_ declare
- _how many_ cells are used (`num_cells`)
- _what kind_ each cell is (`cell_kind`)
- _the description_ of each cell (`cell_description`)
For this example --- since we just want to recreate the single cell model from part I --- we can fix `num_cells` to return `1`
%% Cell type:code id:5b23d044-2f16-47f7-9ef4-79e809eb0d90 tags:
``` python
# WON'T RUN
class recipe(A.recipe):
def __init__(self):
A.recipe.__init__(self)
def num_cells(self):
return 1
rec = recipe()
sim = A.simulation(rec)
```
%% Output
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[7], line 11
Cell In[4], line 11
8 return 1
10 rec = recipe()
---> 11 sim = A.simulation(rec)
RuntimeError: Tried to call pure virtual function "recipe::cell_kind"
%% Cell type:markdown id:6c60ce48-b267-4992-87db-a288f80ee893 tags:
Failure again, but expected this time, we need to tell Arbor which `kind` our cell is.
As we want to imbue different cells with different properties, Arbor includes each cell's global id (`gid`) in the relevant callbacks, so we can dispatch based on the id.
`gid`s run between `0` and `num_cells`.
Biophysical cells have kind `cable`, so let's return that.
%% Cell type:code id:f6225485-e050-4263-94bf-16c4d3d221ed tags:
``` python
# WON'T RUN
class recipe(A.recipe):
def __init__(self):
A.recipe.__init__(self)
def num_cells(self):
return 1
def cell_kind(self, gid):
assert(gid == 0)
return A.cell_kind.cable
rec = recipe()
sim = A.simulation(rec)
```
%% Output
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[8], line 15
Cell In[5], line 15
12 return A.cell_kind.cable
14 rec = recipe()
---> 15 sim = A.simulation(rec)
RuntimeError: Tried to call pure virtual function "recipe::cell_description"
%% Cell type:markdown id:65f0bca2-ad6f-4164-a152-2bee53cde749 tags:
Finally, we need to return the cell's description, which is --- here, there are more options in Arbor --- the `cable_cell` object we created in Part I.
We also add global defaults for cable cells.
Thus:
%% Cell type:code id:654b070c-d972-4d65-82a1-c20af5e9828b tags:
``` python
class recipe(A.recipe):
def __init__(self):
A.recipe.__init__(self)
def num_cells(self):
return 1
def cell_kind(self, gid):
assert(gid == 0)
return A.cell_kind.cable
def cell_description(self, gid):
assert(gid == 0)
# morphology
raw = A.load_swc_neuron('Acker2008.swc')
mrf = raw.morphology
# labels
lbl = raw.labels
lbl['ctr'] = '(location 0 0.5)'
# decor
dec = A.decor()
dec.paint('"soma"', A.density('hh'))
dec.paint('"dend"', A.density('pas'))
dec.place('"ctr"', A.iclamp(tstart=100*U.ms, duration=10*U.ms, current=20*U.nA), 'ic')
dec.place('"ctr"', A.iclamp(tstart=200*U.ms, duration=10*U.ms, current=20*U.nA), 'ic')
return A.cable_cell(mrf, dec, lbl)
def global_properties(self, kind):
assert(kind == A.cell_kind.cable)
return A.neuron_cable_properties()
rec = recipe()
sim = A.simulation(rec)
```
%% Cell type:markdown id:c815e78d-b22e-42dd-b13a-4a156386fefd tags:
Comparing to before we added about 15 lines so far, while gaining a lot of flexibility.
Note, since this is a Python _class_ like any other, we could inherit from it and extend
existing simulations by standard OOP practices.
Let's also add the membrane voltage probe and show how to pull data from a probe:
%% Cell type:code id:99c0d6af-8ef4-4cf9-a6cf-d1ec1db898ca tags:
``` python
class recipe(A.recipe):
def __init__(self):
A.recipe.__init__(self)
def num_cells(self):
return 1
def cell_kind(self, gid):
assert(gid == 0)
return A.cell_kind.cable
def cell_description(self, gid):
assert(gid == 0)
# morphology
raw = A.load_swc_neuron('Acker2008.swc')
mrf = raw.morphology
# labels
lbl = raw.labels
lbl['ctr'] = '(location 0 0.5)'
# decor
dec = A.decor()
dec.paint('"soma"', A.density('hh'))
dec.paint('"dend"', A.density('pas'))
dec.place('"ctr"', A.iclamp(tstart=100*U.ms, duration=10*U.ms, current=20*U.nA), 'ic')
dec.place('"ctr"', A.iclamp(tstart=200*U.ms, duration=10*U.ms, current=20*U.nA), 'ic')
dec.place('"ctr"', A.threshold_detector(-20*U.mV), 'det')
return A.cable_cell(mrf, dec, lbl)
def global_properties(self, kind):
assert(kind == A.cell_kind.cable)
return A.neuron_cable_properties()
def probes(self, gid):
assert(gid == 0)
return [A.cable_probe_membrane_voltage('"ctr"', tag='Um')]
rec = recipe()
sim = A.simulation(rec)
sim.record(A.spike_recording.all)
# sample first probe on gid=0 on a regular 1kHz raster.
u_m = sim.sample((0, 'Um'), A.regular_schedule(1*U.ms))
sim.run(1000*U.ms, 0.1*U.ms)
```
%% Output
1000.0
%% Cell type:markdown id:d12b50d5-940b-411b-a009-dda2d8f478e4 tags:
Sampling warrants a bit of explanation. The process of measurement is deliberately split into creating a measurement and retrieving data from it.
Thus, experiments can provide lots of options for tapping cell state, but pay (almost) no overhead, unless data is actually requested.
(Note: The `sample` interface is a bit awkward and has been improved in newer versions)
Now, let's plot the data by asking the simulation about the data it collected for the handle returned by `sample`
%% Cell type:code id:11e5bc55-f8da-4c4f-9d62-b4c37451e38c tags:
``` python
import numpy as np
sim.spikes()['time']
```
%% Output
array([100.1091586, 200.1091586])
%% Cell type:code id:41bb4f96-135a-4e42-a7c4-8ed3ad164ddd tags:
``` python
fg, ax = plt.subplots()
# each probe may create multiple measurements if locations
# resolve to multiple points
for data, meta in sim.samples(u_m):
# data contains
# - the time in column 0
# - measurements in columns 1:
# meta contains the locations, one per column in data
ax.plot(data[:, 0], data[:, 1], label=f"@{str(meta)}")
spk = sim.spikes()['time']
ax.scatter(spk, np.ones_like(spk)*-20, c='r')
ax.set_xlabel('Time $t/ms$')
ax.set_ylabel('Potential $U_m/mV$')
ax.legend()
```
%% Output
<matplotlib.legend.Legend at 0x1296a6960>
<matplotlib.legend.Legend at 0x117462f90>
%% Cell type:markdown id:5b61ae19-5c25-439a-a0c2-095028797a59 tags:
# Summary
We have removed the convenience layer set up by `single_cell_model` and seen how Arbor 'really' works.
Recipes have more callbacks that are optional to override (like `probes`) to facilitate the building of larger, interconnected simulations. From here, you have two options:
- `03a` import single cell models from a standard database and learn about discretisation and matching experimental data
- `03b` build networks of simple cells, delve into connections, spike inputs, and simulations of multiple cell kinds
If you want to play with this example some more, look below for a few ideas.
%% Cell type:markdown id:17083197-2aec-457d-9db3-0e0f0f462785 tags:
# Ideas
- Try to create a set of indedependent cells. Which callbacks need to change?
- Plot the membrane potential for each
- Vary the cell layout / inputs depending on the `gid`.
- Probe and plot different state variables. See here https://docs.arbor-sim.org/en/v0.10.0/python/probe_sample.html#api
%% Cell type:code id:ba66fe62-1ddd-41e7-a431-06fb936918cc tags:
``` python
```
- Probe and plot different state variables. See here https://docs.arbor-sim.org/en/v0.11.0/python/probe_sample.html#api
......
Source diff could not be displayed: it is too large. Options to address this: view the blob.
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment