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:

  1. Building a arbor.recipe.

  2. Building an arbor.context.

  3. Create a arbor.simulation.

  4. Running the simulation and visualizing the results,

The cell

We can copy the cell description code or reuse single_cell_detailed.swc from the original example where it is explained in detail.

The recipe

The arbor.single_cell_model of 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 arbor.recipe
class single_recipe(arbor.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.
        arbor.recipe.__init__(self)

        self.the_props = arbor.cable_global_properties()
        self.the_props.set_property(Vm=-65, tempK=300, rL=35.4, cm=0.01)
        self.the_props.set_ion(
            ion="na", int_con=10, ext_con=140, rev_pot=50, method="nernst/na"
        )
        self.the_props.set_ion(ion="k", int_con=54.4, ext_con=2.5, rev_pot=-77)
        self.the_props.set_ion(ion="ca", int_con=5e-5, ext_con=2, rev_pot=132.5)
        self.the_props.catalogue.extend(arbor.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, gid):
        return arbor.cell_kind.cable

    # (5.4) Override the cell_description method
    def cell_description(self, gid):
        return cell

    # (5.5) Override the probes method
    def probes(self, gid):
        return [arbor.cable_probe_membrane_voltage('"custom_terminal"')]

    # (5.6) Override the global_properties method
    def global_properties(self, gid):
        return self.the_props


# Instantiate recipe
recipe = single_recipe()

Let’s go through the recipe point by point.

Step (5) creates a single_recipe class that inherits from arbor.recipe. arbor.recipe.num_cells(), arbor.recipe.cell_kind() and arbor.recipe.cell_description() always have to be implemented by the user. We’ll also implement arbor.recipe.global_properties() to be able to decorate arbor.cell_kind.cable cells with mechanisms and arbor.recipe.probes() to be able to insert the probe.

Step (5.1) defines the class constructor. As per arbor.recipe instructions, we call arbor.recipe.__init__(self) to ensure correct initialization of memory in the C++ class.

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. We simply return 1, as we are only simulating one cell in this example.

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.

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.

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).

Note

You may wonder why the method arbor.recipe.cell_kind() is required, since it can be inferred by examining the cell description. The recipe was designed to allow building simulations efficiently in a distributed system with minimum communication. Some parts of the model initialization require only the cell kind, not the full cell description which can be quite expensive to build. Providing these descriptions separately saves time and resources for the user.

More information on the recipe can be found here.

Now we can instantiate a single_recipe object.

# Instantiate recipe
recipe = single_recipe()

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.

# (6) Create a simulation
sim = arbor.simulation(recipe)

# Instruct the simulation to record the spikes and sample the probe
sim.record(arbor.spike_recording.all)

probeset_id = arbor.cell_member(0, 0)
handle = sim.sample(probeset_id, arbor.regular_schedule(0.02))

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.

The lines handling probe sampling warrant a second look. First, we declared probe_id to be a arbor.cell_member, with arbor.cell_member.gid = 0 and arbor.cell_member.index = 0. This variable serves as a global identifier of a probe on a cell, namely the first declared probe on the cell with gid = 0, which is id of the only probe we created on the only cell in the model.

Next, we instructed the simulation to sample probe_id at a frequency of 50 kHz. That function returns a handle which we will use to extract the results of the sampling after running the simulation.

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
sim.run(tfinal=100, dt=0.025)

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 or display the results
spikes = sim.spikes()
print(len(spikes), "spikes recorded:")
for s in spikes:
    print(s)

The probe results, again, warrant some more explanation:

data = []
meta = []
for d, m in sim.samples(handle):
    data.append(d)
    meta.append(m)

sim.samples() takes a handle of the probe we wish to examine. It returns a list of (data, meta) terms: data being the time and value series of the probed quantity; and meta being the location of the probe. 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:

df_list = []
for i in range(len(data)):
    df_list.append(
        pandas.DataFrame(
            {
                "t/ms": data[i][:, 0],
                "U/mV": data[i][:, 1],
                "Location": str(meta[i]),
                "Variable": "voltage",
            }
        )
    )
df = pandas.concat(df_list, ignore_index=True)
seaborn.relplot(
    data=df, kind="line", x="t/ms", y="U/mV", hue="Location", col="Variable", ci=None
).savefig("single_cell_recipe_result.svg")

The following plot is generated. Identical to the plot of the original example.

../_images/single_cell_detailed_result.svg

The full code

You can find the full code of the example at python/examples/single_cell_detailed_recipe.py.