A simple single cell recipe#
This example builds the same single cell model as A simple single cell model,
except using a arbor.recipe
and arbor.simulation
instead of a arbor.single_cell_model
.
Recipes are an important concept in Arbor. They represent the most versatile tool for building a complex network of cells. We will go though this example of a model of a single cell, before using the recipe to represent more complex networks in subsequent examples.
Note
Concepts covered in this example:
Building a
arbor.recipe
.Using the recipe, default context and domain decomposition to create an
arbor.simulation
Running the simulation and visualizing the results.
The cell#
Let’s copy the cell description from the original example, where construction of the cell is explained in detail.
import arbor as A
# (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm
tree = A.segment_tree()
tree.append(A.mnpos, A.mpoint(-3, 0, 0, 3), A.mpoint(3, 0, 0, 3), tag=1)
# (2) Define the soma and its midpoint
labels = A.label_dict({"soma": "(tag 1)", "midpoint": "(location 0 0.5)"})
# (3) Create cell and set properties
decor = (
A.decor()
.set_property(Vm=-40 * U.mV)
.paint('"soma"', A.density("hh"))
.place('"midpoint"', A.iclamp(10 * U.ms, 2 * U.ms, 0.8 * U.nA), "iclamp")
.place('"midpoint"', A.threshold_detector(-10 * U.mV), "detector")
)
The recipe#
In the original example, the
arbor.single_cell_model
creates a arbor.recipe
under the hood,
and abstracts away a few details that you may want control over in more complex
simulations. Let’s go into those abstractions and create an analogous
arbor.recipe
manually.
Creating a recipe starts with creating a class that inherits from
arbor.recipe
. There are a number of methods that must be overridden,
and a number than can optionally be overridden, as explained in the
arbor.recipe
documentation. Beyond this, it is up to you, the user, to
structure your code as you find convenient.
One of the methods that must be overridden is arbor.recipe.num_cells()
. It
returns 0 by default and models without cells are quite boring!
# This constitutes the corresponding generic recipe version of
# `single_cell_model.py`.
class single_recipe(A.recipe):
# (4.1) Base constructor must be called, to ensure correct initialization.
def __init__(self):
A.recipe.__init__(self)
self.the_props = A.neuron_cable_properties()
# (4.2) Override the num_cells method
def num_cells(self):
return 1
# (4.3) Override the cell_kind method
def cell_kind(self, _):
return A.cell_kind.cable
# (4.4) Override the cell_description method
def cell_description(self, gid):
return cell
# (4.5) Override the probes method with a voltage probe located on "midpoint"
def probes(self, _):
return [A.cable_probe_membrane_voltage('"midpoint"', "Um")]
# (4.6) Override the global_properties method
def global_properties(self, kind):
return self.the_props
# (5) Instantiate recipe.
recipe = single_recipe()
Step (4) describes the recipe that will reflect our single cell model.
Step (4.1) defines the class constructor. It can take any shape you need,
but it is important to call base class’ constructor as the first action. If the
overridden methods of the class need to return an object, it may be a good idea
to have the returned object be a member of the class. With this constructor, we
could easily change the cell and probes of the model, should we want to do so.
Here we initialize the cell properties to match Neuron’s defaults using Arbor’s
built-in arbor.neuron_cable_properties()
and extend with Arbor’s own
arbor.default_catalogue()
.
Step (4.2) states that this model has one cell.
Step (4.3) returns arbor.cell_kind.cable
, the
arbor.cell_kind
associated with the cable cell defined above. If you
mix multiple cell kinds and descriptions in one recipe, make sure a particular
gid
returns matching cell kinds and descriptions.
Step (4.4) returns the cell description defined earlier. If we were
modelling multiple cells of different kinds, we would need to make sure that the
cell returned by arbor.recipe.cell_description()
has the same cell kind as
returned by arbor.recipe.cell_kind()
for every gid
.
Step (4.5) returns the same probe as in the single_cell_model
: a single
voltage probe located at “midpoint”.
Step (4.6) returns the properties that will be applied to all cells of that kind in the model.
More methods may be overridden if your model requires that, see
arbor.recipe
for options.
Now we instantiate the recipe
# (5) Instantiate recipe.
recipe = single_recipe()
The simulation#
arbor.single_cell_model
does not only take care of the recipe, it also
takes care of defining how the simulation will be run. When you create and use
your own recipe, you can to do this manually, in the form of defining a
execution context and a domain decomposition. Fortunately, the default
constructors of arbor.context
and arbor.partition_load_balance
are sufficient for this model, and is what arbor.single_cell_model
does
under the hood! In addition, if all you need is the default context and domain
decomposition, they can be left out and the arbor.simulation
object can
be contructed from just the recipe.
The details of manual hardware configuration will be left for another tutorial.
# (6) Create simulation. When their defaults are sufficient, context and domain
# decomposition don't have to be manually specified and the simulation can be
# created with just the recipe as argument.
sim = A.simulation(recipe)
# (7) Create and run simulation and set up 10 kHz (every 0.1 ms) sampling on the
# probe. The probe is located on cell 0, and is the 0th probe on that cell, thus
# has probeset_id (0, 0).
sim.record(A.spike_recording.all)
handle = sim.sample((0, "Um"), A.regular_schedule(0.1 * U.ms))
sim.run(tfinal=30 * U.ms)
Step (6) instantiates the simulation.
Step (7) sets up the probe added in step 5. In the
arbor.single_cell_model
version of this example, the probe frequency
and simulation duration are the same. Note that the frequency is set with a
arbor.regular_schedule
, which takes a time and not a frequency. Also
note that spike recording must be switched on. For extraction of the probe
traces later on, we store a handle. Then, we
start the simulation.
The results#
Apart from creating arbor.recipe
ourselves, we have changed nothing
about this simulation compared to the original tutorial. If we create the same analysis of the results we
therefore expect the same results.
# (8) Collect results.
spikes = sim.spikes()
data, meta = sim.samples(handle)[0]
print("{} spikes:".format(len(spikes)))
for t in spikes["time"]:
print(f" * {t:3.3f} ms")
print("Plotting results ...")
df = pd.DataFrame({"t/ms": data[:, 0], "U/mV": data[:, 1]})
sns.relplot(data=df, kind="line", x="t/ms", y="U/mV", errorbar=None).savefig(
"single_cell_recipe_result.svg"
)
df.to_csv("single_cell_recipe_result.csv", float_format="%g")
Step (8) plots the measured potentials during the runtime of the simulation.
Retrieving the sampled quantities is a little different, these have to be
accessed through the simulation object: arbor.simulation.spikes()
and
arbor.simulation.samples()
.
We should be seeing something like this:
You can find the source code for this example in full at python/examples/single_cell_recipe.py
.