diff --git a/doc/install/python.rst b/doc/install/python.rst index 051836fa1c06044de38c9132728252b50b4ca303..ce64f662094584459582fc0f1696818bba0e5cf5 100644 --- a/doc/install/python.rst +++ b/doc/install/python.rst @@ -43,7 +43,7 @@ to see information about the version and enabled features: >>> print(arbor.__config__) You are now ready to use Arbor! You can continue reading these documentation pages, have a look at the -:ref:`Python API reference<pyoverview>` , or visit the :ref:`Quick Start page<tutorialsimplecell>`. +:ref:`Python API reference<pyoverview>` , or visit the :ref:`Quick Start page<tutorialsinglecell>`. .. Note:: To get help in case of problems installing with pip, run pip with the ``--verbose`` flag, and attach the output diff --git a/doc/python/single_cell_model.rst b/doc/python/single_cell_model.rst index 20be222e25930e25c859238f10e54b1867c63c03..fb030c80d92b84142ccdf055b8b2c5fabc2d38ae 100644 --- a/doc/python/single_cell_model.rst +++ b/doc/python/single_cell_model.rst @@ -70,7 +70,7 @@ Single cell model .. Note:: - The :class:`single_cell_model` is used in our :ref:`tutorials <gs_other_examples>`. + The :class:`single_cell_model` is used in our :ref:`tutorials <tutorialsinglecell>`. The examples illustrate how to construct a :class:`cable_cell` and use it to form a :class:`single_cell_model`; how to add probes; how to run the model; and how to visualize the results. \ No newline at end of file diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst index f52907ddf6a2241b91be1c2543b942854ac92bff..c7f38777bb56480ea965ade3d790b500034c3763 100644 --- a/doc/tutorial/index.rst +++ b/doc/tutorial/index.rst @@ -21,5 +21,6 @@ Tutorials :caption: Tutorials single_cell_model + single_cell_recipe single_cell_detailed single_cell_detailed_recipe diff --git a/doc/tutorial/single_cell_detailed.rst b/doc/tutorial/single_cell_detailed.rst index 6558a5b0e6f9807bbac1d414c85b7bbda8fd1cd6..78823febbf05f276824bc1bd9ac1f44b9ec76407 100644 --- a/doc/tutorial/single_cell_detailed.rst +++ b/doc/tutorial/single_cell_detailed.rst @@ -3,7 +3,7 @@ A detailed single cell model ============================ -We can expand on the :ref:`single segment cell example <tutorialsimplecell>` to create a more +We can expand on the :ref:`single segment cell example <tutorialsinglecell>` to create a more complex single cell model, and go through the process in more detail. .. Note:: diff --git a/doc/tutorial/single_cell_detailed_recipe.rst b/doc/tutorial/single_cell_detailed_recipe.rst index 32df752764c9c91f4d3b8649a1718ee84a06e784..ad00e01a658bd1d279f0d876d5620b52a8a063f8 100644 --- a/doc/tutorial/single_cell_detailed_recipe.rst +++ b/doc/tutorial/single_cell_detailed_recipe.rst @@ -1,9 +1,9 @@ -.. _tutorialsinglecellrecipe: +.. _tutorialsinglecellswcrecipe: A detailed single cell recipe ============================= -This example aims to build the same single cell model as +This example builds the same single cell model as :ref:`the previous tutorial <tutorialsinglecellswc>`, except using a :class:`arbor.recipe` and :class:`arbor.simulation` instead of a :class:`arbor.single_cell_model`. @@ -113,7 +113,7 @@ We will add one more thing to this section. We will create the voltage probe at In the previous example, this probe was registered directly using the :class:`arbor.single_cell_model` object. Now it has to be explicitly created and registered in the recipe. -.. _tutorialsinglecellrecipe-probe: +.. _tutorialsinglecellswcrecipe-probe: .. code-block:: python probe = arbor.cable_probe_membrane_voltage('"custom_terminal"') @@ -345,7 +345,7 @@ 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 :class:`arbor.cell_member`, with :class:`arbor.cell_member.gid` = 0 and :class:`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 :ref:`only probe <tutorialsinglecellrecipe-probe>` we created on +cell with gid = 0, which is id of the :ref:`only probe <tutorialsinglecellswcrecipe-probe>` we created on the only cell in the model. Next, we instructed the simulation to sample ``probe_id`` at a frequency of 50KHz. That function returns a diff --git a/doc/tutorial/single_cell_model.rst b/doc/tutorial/single_cell_model.rst index ac6acdf3aa66e6aa7927b403bbdb3510cb49261e..14e2aeb8dcfe8673967a2dc0b83b0c0e438b1971 100644 --- a/doc/tutorial/single_cell_model.rst +++ b/doc/tutorial/single_cell_model.rst @@ -1,4 +1,4 @@ -.. _tutorialsimplecell: +.. _tutorialsinglecell: A simple single cell model ========================== @@ -19,6 +19,10 @@ introduce Arbor's cell modelling concepts and approach. 5. Building a :class:`arbor.single_cell_model` object. 6. Running a simulation and visualising the results. +.. _tutorialsinglecell-cell: + +The cell +-------- The most trivial representation of a cell in Arbor is to model the entire cell as a single cylinder. The following example shows the steps required to construct a model of a @@ -66,14 +70,14 @@ of the segment; and the tag. Step **(2)** creates a dictionary of labels (:class:`arbor.label_dict<arbor.label_dict>`). Labels give names to :ref:`regions<labels-region>` and :ref:`location<labels-locset>` described using a DSL based on s-expressions. Labels from the dictionary can then be used to facilitate adding synapses, -dynamics, stimulii and probes to the cell. We add two labels: +dynamics, stimuli and probes to the cell. We add two labels: * ``soma`` defines a *region* with ``(tag 1)``. Note that this corresponds to the ``tag`` parameter that was used to define the single segment in step (1). * ``center`` defines a *location* at ``(location 0 0.5)``, which is the mid point ``0.5`` of branch ``0``, which corresponds to the center of the soma on the morphology defined in step (1). -Step **(3)** constructs a :class:`arbor.decor` that describes the distributation and placement +Step **(3)** constructs a :class:`arbor.decor` that describes the distribution and placement of dynamics and properties on a cell. The cell's default properties can be modified, and we can use :meth:`arbor.decor.paint` and :meth:`arbor.decor.place` to further customise it in the following way: @@ -87,16 +91,15 @@ following way: HH dynamics on the region we previously named ``"soma"`` in our label dictionary. * :meth:`arbor.decor.place` is used to add objects on a precise :class:`arbor.location` on a cell. Examples of objects that are *placed* are synapses, - spike detectors, current stimulii, and probes. In the above example we place a current stimulus + spike detectors, current stimuli, and probes. In the above example we place a current stimulus :class:`arbor.iclamp` with a duration of 2 ms and a current of 0.8 nA, starting at 10 ms on the location we previously labelled ``"center"``. We also place a :class:`arbor.spike_detector` with a threshold of -10 mV on the same location. -Step **(4)** constructs the :class:`arbor.cable_cell` from the segment tree and dictionary of labeled -regions and locations. +Step **(4)** constructs the :class:`arbor.cable_cell` from the segment tree and dictionary of labelled regions and locations. -Single cell model ----------------------------------------------------- +The single cell model +--------------------- Once the cell description has been built, the next step is to build and run the simulation. Arbor provides an interface for constructing single cell models with the @@ -124,8 +127,8 @@ probe ('"center"'), and the frequency at which we want to sample (10kHz). Step **(7)** runs the actual simulation for a duration of 30 ms. -Results ----------------------------------------------------- +The results +----------- Our cell and model have been defined and we have run our simulation. Now we can look at what the spike detector and a voltage probes from our model have produced. @@ -160,13 +163,7 @@ We should be seeing something like this: Plot of the potential over time for the voltage probe added in step (6). -You can find the source code for this example in full at ``python/examples/single_cell_model.py``. - -.. Todo:: - An example with a more complex cell geometry (loaded from NeuroML/SWC?). - This would show how to define and use morphology regions and locsets. - Introduce CV discretization control. - Probe and sample state variables in hh mechanism along with voltage. +The full code +------------- -.. Todo:: - Add a small ring network implemented via a recipe. This introduces connections, gids, and reveals the recipe plumbing that is hidden inside the single_cell_model. +You can find the source code for this example in full at ``python/examples/single_cell_model.py``. diff --git a/doc/tutorial/single_cell_recipe.rst b/doc/tutorial/single_cell_recipe.rst new file mode 100644 index 0000000000000000000000000000000000000000..dc1f7e7b7eda528b4d3a87a266da3e360c9c5ce4 --- /dev/null +++ b/doc/tutorial/single_cell_recipe.rst @@ -0,0 +1,207 @@ +.. _tutorialsinglecellrecipe: + +A simple single cell recipe +=========================== + +This example builds the same single cell model as +:ref:`the previous tutorial <tutorialsinglecell>`, except using a :class:`arbor.recipe` +and :class:`arbor.simulation` instead of a :class:`arbor.single_cell_model`. + +.. Note:: + + **Concepts covered in this example:** + + 1. Building a :class:`arbor.recipe`. + 2. Using the recipe, context and domain decomposition to create a :class:`arbor.simulation` + 3. Running the simulation and visualizing the results. + +The cell +-------- + +We can immediately paste the cell description code from the +:ref:`previous example <tutorialsinglecell-cell>` where it is explained in detail. + +.. code-block:: python + + import arbor + + # (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm + tree = arbor.segment_tree() + tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1) + + # (2) Define the soma and its center + labels = arbor.label_dict({'soma': '(tag 1)', + 'center': '(location 0 0.5)'}) + + # (3) Create cell and set properties + decor = arbor.decor() + decor.set_property(Vm=-40) + decor.paint('"soma"', 'hh') + decor.place('"center"', arbor.iclamp( 10, 2, 0.8)) + decor.place('"center"', arbor.spike_detector(-10)) + cell = arbor.cable_cell(tree, labels, decor) + +The recipe +---------- + +The :class:`arbor.single_cell_model` of the previous example created a :class:`arbor.recipe` under +the hood, and abstracted away the details so we were unaware of its existence. + +Creating an analogous recipe starts with creating a class that inherits from :class:`arbor.recipe` +and overrides and implements some of :class:`arbor.recipe` methods. Not all methods +have to be overridden, but some will always have to be, such as :meth:`arbor.recipe.num_cells`. +It returns `0` by default and models without cells are quite boring! + +.. code-block:: python + + # (4) Define a recipe for a single cell and set of probes upon it. + # This constitutes the corresponding generic recipe version of + # `single_cell_model.py`. + + class single_recipe (arbor.recipe): + def __init__(self, cell, probes): + # (4.1) 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_cell = cell + self.the_probes = probes + self.the_props = arbor.neuron_cable_propetries() + self.the_cat = arbor.default_catalogue() + self.the_props.register(self.the_cat) + + def num_cells(self): + # (4.2) Override the num_cells method + return 1 + + def num_sources(self, gid): + # (4.3) Override the num_sources method + return 1 + + def cell_kind(self, gid): + # (4.4) Override the cell_kind method + return arbor.cell_kind.cable + + def cell_description(self, gid): + # (4.5) Override the cell_description method + return self.the_cell + + def probes(self, gid): + # (4.6) Override the probes method + return self.the_probes + + def global_properties(self, kind): + # (4.7) Override the global_properties method + return self.the_props + +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. 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 :meth:`arbor.neuron_cable_properties` and +extend with Arbor's own :meth:`arbor.default_catalogue`. + +Step **(4.2)** defines that this model has one cell. + +Step **(4.3)** defines that this model has one source. + +Step **(4.4)** returns :class:`arbor.cell_kind.cable`, the :class:`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.5)** returns the cell description passed in on class initialisation. If we +were modelling multiple cells of different kinds, we would need to make sure that the +cell returned by :meth:arbor.recipe.cell_description has the same cell kind as +returned by :meth:arbor.recipe.cell_kind for every :gen:gid. + +Step **(4.6)** returns the probes passed in at class initialisation. + +Step **(4.7)** returns the properties that will be applied to all cells of that kind in the model. + +More methods can be overridden if your model requires that, see :class:`arbor.recipe` for options. + +Step **(5)** instantiates the recipe with the cable cell described earlier, and a single voltage probe located at "center". + +The context and domain decomposition +------------------------------------ + +:class:`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'll need to do this manually, in the form of defining a execution context +and a domain decomposition. Fortunately, the default constructors of :class:`arbor. +context` and :class:`arbor.partition_load_balance` are sufficient for this model, and +is what :class:`arbor.single_cell_model` does under the hood! We'll leave the details +of this subject for another tutorial. + +.. code-block:: python + + # (6) Create a default execution context and a default domain decomposition. + + context = arbor.context() + domains = arbor.partition_load_balance(recipe, context) + +Step **(6)** sets up a default context and domains. + +The simulation +-------------- + +.. code-block:: python + + # (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 probe_id (0, 0). + + sim = arbor.simulation(recipe, domains, context) + sim.record(arbor.spike_recording.all) + handle = sim.sample((0, 0), arbor.regular_schedule(0.1)) + sim.run(tfinal=30) + +Step **(7)** instantiates the simulation and sets up the probe added in step 5. In the +:class:`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 :class:`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. + +The results +---------------------------------------------------- + +Apart from creating :class:`arbor.recipe` ourselves, we have changed nothing +about this simulation compared to :ref:`the previous tutorial <tutorialsinglecell>`. +If we create the same analysis of the results we therefore expect the same results. + +.. code-block:: python + + # (8) Collect results. + + spikes = sim.spikes() + data, meta = sim.samples(handle)[0] + + if len(spikes)>0: + print('{} spikes:'.format(len(spikes))) + for t in spikes['time']: + print('{:3.3f}'.format(t)) + else: + print('no spikes') + + print("Plotting results ...") + seaborn.set_theme() # Apply some styling to the plot + df = pandas.DataFrame({'t/ms': data[:, 0], 'U/mV': data[:, 1]}) + seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV", ci=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: :meth:`arbor.simulation.spikes` and :meth:`arbor.simulation.samples`. + +We should be seeing something like this: + +.. figure:: single_cell_model_result.svg + :width: 400 + :align: center + + Plot of the potential over time for the voltage probe added in step (5). + +You can find the source code for this example in full at ``python/examples/single_cell_recipe.py``. diff --git a/python/example/single_cell_detailed_recipe.py b/python/example/single_cell_detailed_recipe.py index 46a9cd6410dd5e2cf9212ba3ee1252a789983e3d..f7dfaf082470a95f30d28dcf4235ec1041a56b52 100644 --- a/python/example/single_cell_detailed_recipe.py +++ b/python/example/single_cell_detailed_recipe.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import arbor import pandas import seaborn diff --git a/python/example/single_cell_recipe.py b/python/example/single_cell_recipe.py index f910afa27ce4cd3be10d4195348f1c1db96f2de3..057eb4964d734a636793b9672a8c98f10363be0f 100644 --- a/python/example/single_cell_recipe.py +++ b/python/example/single_cell_recipe.py @@ -1,13 +1,33 @@ +#!/usr/bin/env python3 + import arbor -import numpy, pandas, seaborn # You may have to pip install these. +import pandas, seaborn # You may have to pip install these. # The corresponding generic recipe version of `single_cell_model.py`. -# (1) Define a recipe for a single cell and set of probes upon it. +# (1) Create a morphology with a single (cylindrical) segment of length=diameter=6 μm +tree = arbor.segment_tree() +tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1) + +# (2) Define the soma and its center +labels = arbor.label_dict({'soma': '(tag 1)', + 'center': '(location 0 0.5)'}) + +# (3) Create cell and set properties +decor = arbor.decor() +decor.set_property(Vm=-40) +decor.paint('"soma"', 'hh') +decor.place('"center"', arbor.iclamp( 10, 2, 0.8)) +decor.place('"center"', arbor.spike_detector(-10)) +cell = arbor.cable_cell(tree, labels, decor) + +# (4) Define a recipe for a single cell and set of probes upon it. +# This constitutes the corresponding generic recipe version of +# `single_cell_model.py`. class single_recipe (arbor.recipe): def __init__(self, cell, probes): - # The base C++ class constructor must be called first, to ensure that + # (4.1) 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_cell = cell @@ -34,46 +54,28 @@ class single_recipe (arbor.recipe): def global_properties(self, kind): return self.the_props -# (2) Create a cell. +# (5) Instantiate recipe with a voltage probe located on "center". -# Morphology -tree = arbor.segment_tree() -tree.append(arbor.mnpos, arbor.mpoint(-3, 0, 0, 3), arbor.mpoint(3, 0, 0, 3), tag=1) +recipe = single_recipe(cell, [arbor.cable_probe_membrane_voltage('"center"')]) -# Label dictionary -labels = arbor.label_dict() -labels['centre'] = '(location 0 0.5)' - -# Decorations -decor = arbor.decor() -decor.set_property(Vm=-40) -decor.paint('(all)', 'hh') -decor.place('"centre"', arbor.iclamp( 10, 2, 0.8)) -decor.place('"centre"', arbor.spike_detector(-10)) - -cell = arbor.cable_cell(tree, labels, decor) - -# (3) Instantiate recipe with a voltage probe. - -recipe = single_recipe(cell, [arbor.cable_probe_membrane_voltage('"centre"')]) - -# (4) Instantiate simulation and set up sampling on probe id (0, 0). +# (6) Create a default execution context and a default domain decomposition. context = arbor.context() domains = arbor.partition_load_balance(recipe, context) -sim = arbor.simulation(recipe, domains, context) +# (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 probe_id (0, 0). + +sim = arbor.simulation(recipe, domains, context) sim.record(arbor.spike_recording.all) handle = sim.sample((0, 0), arbor.regular_schedule(0.1)) +sim.run(tfinal=30) -# (6) Run simulation for 30 ms of simulated activity and collect results. +# (8) Collect results. -sim.run(tfinal=30) spikes = sim.spikes() data, meta = sim.samples(handle)[0] -# (7) Print spike times, if any. - if len(spikes)>0: print('{} spikes:'.format(len(spikes))) for t in spikes['time']: @@ -81,13 +83,9 @@ if len(spikes)>0: else: print('no spikes') -# (8) Plot the recorded voltages over time. - print("Plotting results ...") seaborn.set_theme() # Apply some styling to the plot df = pandas.DataFrame({'t/ms': data[:, 0], 'U/mV': data[:, 1]}) seaborn.relplot(data=df, kind="line", x="t/ms", y="U/mV", ci=None).savefig('single_cell_recipe_result.svg') -# (9) Optionally, you can store your results for later processing. - df.to_csv('single_cell_recipe_result.csv', float_format='%g')