A detailed single cell recipe#
This example builds the same single cell model as A detailed single cell model,
except using a arbor.recipe
and arbor.simulation
instead of a arbor.single_cell_model
.
This time, we’ll learn a bit more about setting up advanced features using a arbor.recipe
.
Note
Concepts covered in this example:
Building a
arbor.recipe
.Building an
arbor.context
.Create a
arbor.simulation
.Running the simulation and visualizing the results,
The cell#
We reuse the cell construction code from original example where it is explained in detail. Constructing cells outside the recipe is not required, but may be convenient and potentially faster if many copies of the same cell are required.
The recipe#
The arbor.single_cell_model
used in the original example created a
arbor.recipe
under the hood, and abstracted away the details so we were
unaware of its existence. In this example, we will examine the recipe in detail:
how to create one and why it is needed.
# (5) Create a class that inherits from A.recipe
class single_recipe(A.recipe):
# (5.1) Define the class constructor
def __init__(self):
# The base C++ class constructor must be called first, to ensure that
# all memory in the C++ class is initialized correctly.
A.recipe.__init__(self)
self.the_props = A.cable_global_properties()
self.the_props.set_property(
Vm=-65 * U.mV,
tempK=300 * U.Kelvin,
rL=35.4 * U.Ohm * U.cm,
cm=0.01 * U.F / U.m2,
)
self.the_props.set_ion(
ion="na",
int_con=10 * U.mM,
ext_con=140 * U.mM,
rev_pot=50 * U.mV,
method="nernst/na",
)
self.the_props.set_ion(
ion="k", int_con=54.4 * U.mM, ext_con=2.5 * U.mM, rev_pot=-77 * U.mV
)
self.the_props.set_ion(
ion="ca", int_con=5e-5 * U.mM, ext_con=2 * U.mM, rev_pot=132.5 * U.mV
)
self.the_props.catalogue.extend(A.allen_catalogue(), "")
# (5.2) Override the num_cells method
def num_cells(self):
return 1
# (5.3) Override the cell_kind method
def cell_kind(self, _):
return A.cell_kind.cable
# (5.4) Override the cell_description method
def cell_description(self, _):
return cell
# (5.5) Override the probes method
def probes(self, _):
return [A.cable_probe_membrane_voltage('"custom_terminal"', "Um")]
# (5.6) Override the global_properties method
def global_properties(self, _):
Let’s go through the recipe point by point.
Step (5.1) defines the class constructor. As per arbor.recipe
instructions, we call arbor.recipe.__init__(self)
as the very first thing.
This is to ensure correct initialization.
We then create the self.the_props
variable. This will hold the global
properties of the model, which apply to all the cells in the network. We
initialize it with arbor.cable_global_properties
, which comes with the
default
mechanism catalogue built-in. We set all the properties of the
system similar to what we did in the original example. One last important step is to extend
self.the_props
to include the Allen catalogue, because it holds the Ih
mechanism. The hh and pas mechanisms came with the default catalogue.
Step (5.2) overrides the num_cells()
method. It takes no
arguments and returns the count of cells in the simulation. The global id (gid)
of cells runs between 0 and the value returned from here and is used to query
recipes for cell descriptions. Technically, this method doesn’t need overriding,
but the default is zero, resulting in an empty simulation.
Step (5.3) overrides the cell_kind()
method. It takes
one argument: gid
. Given the gid, this method returns the kind of the cell.
Our defined cell is a arbor.cell_kind.cable
, so we simply return that.
Arbor uses the kind to determine what description is expected from cell_description
;
if the two do not match, an error will occur.
Step (5.4) overrides the cell_description()
method. It
takes one argument: gid
. Given the gid, this method returns the cell
description which is the cell object passed to the constructor of the recipe. We
return cell
, the cell created just above.
Note
While splitting the kind and description into two methods may seem redundant, it allows Arbor to optimize the simulation layout before constructing any cells.
Step (5.5) overrides the probes()
method. It takes one
argument: gid
. Given the gid, this method returns all the probes on the
cell. The probes can be of many different kinds measuring different quantities
on different locations of the cell. Like in the original example, we will create
the voltage probe at the "custom_terminal"
locset. This probe was registered
directly using the arbor.single_cell_model
object. Now it has to be
explicitly created and registered in the recipe.
Step (5.6) overrides the global_properties()
method. It
takes one argument: kind
. This method returns the default global properties
of the model which apply to all cells in the network of that kind. We only use
cable
cells in this example (but there are more) and thus always return a
cable_cell_properties
object. We return self.the_props
which we defined
in step (1).
# Instantiate recipe
Now we can instantiate a single_recipe
object.
The simulation#
We have all we need to create a arbor.simulation
object.
Before we run the simulation, however, we need to register what results we expect once execution is over.
This was handled by the arbor.single_cell_model
object in the original example.
# Instruct the simulation to record the spikes and sample the probe
sim.record(A.spike_recording.all)
We would like to get a list of the spikes on the cell during the runtime of the
simulation, and we would like to plot the voltage registered by the probe on the
“custom_terminal” locset. Without the call to sample
, the probe will be
present, but no data will recorded. This can help to toggle different probes and
possible save some memory on the unsampled ones. Sampling requires a unqiue
identifier for the probe we want to attach to, which consists of the gid of the
cell and the label we gave to the place
method when setting the probe. Each
sampler is identified by an opaque handle that can be used to retrieve the
recorded data.
We can now run the simulation we just instantiated for a duration of 100 ms with a time step of 0.025 ms.
# (7) Run the simulation
The results#
The last step is result collection. We instructed the simulation to record the spikes on the cell, and to sample the probe.
We can print the times of the spikes:
# (8) Print spikes
spikes = sim.spikes()
print(len(spikes), "spikes recorded:")
for (gid, lid), t in spikes:
The probe results, again, warrant some more explanation:
}
sim.samples
takes a handle
that is associated to the probe we wish to
examine. These opaque objects are returned from the calls to sim.sample
.
Each call returns a list of (data, meta)
objects. Here, meta
describes
the location set of the probe, which can be a single place or a list of places.
The other item – data
– is a numpy array comprising one column for the
time and one for each eantry in the location list. The size of the returned list
depends on the number of discrete locations pointed to by the handle. We placed
the probe on the “custom_terminal” locset which is represented by 2 locations on
the morphology. We therefore expect the length of sim.samples(handle)
to
be 2.
We plot the results using pandas and seaborn as we did in the original example, and expect the same results:
# (8) Plot the membrane potential
df = pd.concat(
[
pd.DataFrame(
{
"t/ms": data[:, 0],
"U/mV": data[:, 1],
"Location": str(meta),
"Variable": "voltage",
}
)
for data, meta in sim.samples(handle)
],
ignore_index=True,
)
sns.relplot(
data=df,
kind="line",
x="t/ms",
y="U/mV",
hue="Location",
col="Variable",
errorbar=None,
).savefig("single_cell_recipe_result.svg")
The following plot is generated. Identical to the plot of the original example.
The full code#
You can find the full code of the example at
python/examples/single_cell_detailed_recipe.py
.