========================== Building networks: neurons ========================== .. testsetup:: from pyNN.mock import * from pyNN.utility import init_logging from pyNN.random import NumpyRNG log = init_logging("doctest_neurons.log", debug=True) .. _section-cell-types: Cell types ========== In PyNN, the system of equations that defines a neuronal model is encapsulated in a :class:`CellType` class. PyNN provides a library of "standard" cell types (see :doc:`standardmodels`) which work the same across all backend simulators - an example is the :class:`IF_cond_exp` model - an integrate-and-fire (I&F) neuron with conductance-based, exponential-decay synapses. For any given simulator, it is also possible to wrap a native model - a NEST or NEURON model, for example - in a :class:`CellType` class so that it works in PyNN (see documentation for individual :doc:`backends`). It should be noted that these "cell types" are *mathematical* cell types. Two or more different *biological* cell types may be represented by the same mathematical cell type, but with different parameterizations. For example, in the thalamocortical model of `Destexhe (2009)`_, thalamocortical relay neurons and cortical neurons are both modelled with the adaptive exponential I&F neuron model (AdExp): .. testcode:: :hide: log.info("========== create cell types =========") .. doctest:: >>> refractory_period = RandomDistribution('uniform', [2.0, 3.0], rng=NumpyRNG(seed=4242)) >>> ctx_parameters = { ... 'cm': 0.25, 'tau_m': 20.0, 'v_rest': -60, 'v_thresh': -50, 'tau_refrac': refractory_period, ... 'v_reset': -60, 'v_spike': -50.0, 'a': 1.0, 'b': 0.005, 'tau_w': 600, 'delta_T': 2.5, ... 'tau_syn_E': 5.0, 'e_rev_E': 0.0, 'tau_syn_I': 10.0, 'e_rev_I': -80 } >>> tc_parameters = ctx_parameters.copy() >>> tc_parameters.update({'a': 20.0, 'b': 0.0}) >>> thalamocortical_type = EIF_cond_exp_isfa_ista(**tc_parameters) >>> cortical_type = EIF_cond_exp_isfa_ista(**ctx_parameters) .. todo: check details + parameters of Destexhe model (see :doc:`parameters` for more on specifying parameter values). To see the list of parameter names for a given cell type, use the :meth:`get_parameter_names()` method: .. doctest:: >>> IF_cond_exp.get_parameter_names() ['tau_refrac', 'cm', 'tau_syn_E', 'v_rest', 'tau_syn_I', 'tau_m', 'e_rev_E', 'i_offset', 'e_rev_I', 'v_thresh', 'v_reset'] while the default values for the parameters are in the :attr:`default_parameters` attribute: .. doctest:: >>> print(IF_cond_exp.default_parameters) {'tau_refrac': 0.1, 'cm': 1.0, 'tau_syn_E': 5.0, 'v_rest': -65.0, 'tau_syn_I': 5.0, 'tau_m': 20.0, 'e_rev_E': 0.0, 'i_offset': 0.0, 'e_rev_I': -70.0, 'v_thresh': -50.0, 'v_reset': -65.0} Note that what we have created here are *neuron type* objects. These can be regarded as templates, from which we will construct the actual neurons in our network. Populations =========== Since PyNN is designed for modelling networks containing many neurons, the default level of abstraction in PyNN is not the single neuron but a population of neurons of a given type, represented by the :class:`Population` class: .. testcode:: :hide: log.info("========== create populations =========") .. doctest:: >>> tc_cells = Population(100, thalamocortical_type) >>> ctx_cells = Population(500, cortical_type) To create a :class:`Population`, we need to specify at minimum the number of neurons and the cell type. Three additional arguments may optionally be specified: * the spatial structure of the population; * initial values for the neuron state variables; * a label. .. testcode:: :hide: log.info("========== create populations with structure =========") .. doctest:: >>> from pyNN.space import Grid2D, RandomStructure, Sphere >>> tc_cells = Population(100, thalamocortical_type, ... structure=RandomStructure(boundary=Sphere(radius=200.0)), ... initial_values={'v': -70.0}, ... label="Thalamocortical neurons") >>> from pyNN.random import RandomDistribution >>> v_init = RandomDistribution('uniform', (-70.0, -60.0)) >>> ctx_cells = Population(500, cortical_type, ... structure=Grid2D(dx=10.0, dy=10.0), ... initial_values={'v': v_init}, ... label="Cortical neurons") (see :doc:`space` for more detail on spatial structure and :doc:`parameters` for more on specifying initial values.) For backwards compatibility and for ease of transitioning from other simulator languages, the :func:`create` function is available as an alias for :class:`Population`. The following two lines are equivalent:: >>> cells = create(my_cell_type, n=100) >>> cells = Population(100, my_cell_type) (Note the different argument order). Views ===== It is common to work with only a subset of the neurons in a :class:`Population` - to modify their parameters, make connections or record from them. Any subset of neurons in a population may be addressed using the usual Python indexing and slicing notation, for example: .. doctest:: >>> id = ctx_cells[47] # the 48th neuron in a Population >>> view = ctx_cells[:80] # the first eighty neurons >>> view = ctx_cells[::2] # every second neuron >>> view = ctx_cells[45, 91, 7] # a specific set of neurons It is also possible to address a random sample of neurons within a population using the :meth:`sample()` method: .. doctest:: >>> view = ctx_cells.sample(50, rng=NumpyRNG(seed=6538)) # select 50 neurons at random In the first of these examples, the object that is returned is an :class:`ID` object, representing a single neuron. :class:`ID` objects are discussed below. In all of these examples except the first, the object that is returned is a :class:`PopulationView`. A :class:`PopulationView` holds references to a subset of neurons in a :class:`Population`, which means that any changes in the view are also reflected in the real population (and vice versa). :class:`PopulationView` objects behave in most ways as real :class:`Population` objects; notably, they can be used in a :class:`Projection` (see :doc:`connections`) and combined with other :class:`Population` or :class:`PopulationView` objects to create an :class:`Assembly`. The :attr:`parent` attribute of a :class:`PopulationView` has a reference to the :class:`Population` that is being viewed, and the :attr:`mask` attribute contains the indices of the neurons that are in the view. .. doctest:: >>> view.parent.label 'Cortical neurons' >>> view.mask array([150, 181, 53, 149, 496, 499, 240, 444, 13, 100, 28, 19, 101, 122, 143, 486, 467, 492, 406, 90, 136, 173, 8, 341, 5, 348, 188, 63, 129, 416, 307, 298, 60, 180, 382, 47, 484, 370, 223, 147, 72, 32, 261, 193, 249, 212, 58, 87, 86, 456]) Assemblies ========== As discussed above, a :class:`Population` is a homogeneous collection of neurons, in the sense that all neurons have the same cell type. An :class:`Assembly` is an aggregate of :class:`Population` and :class:`PopulationView` objects, and as such can represent a heterogeneous collection of neurons, of multiple cell types. An :class:`Assembly` can be created by adding together :class:`Population` and :class:`PopulationView` objects: .. doctest:: >>> all_cells = tc_cells + ctx_cells >>> cells_for_plotting = tc_cells[:10] + ctx_cells[:50] or by using the :class:`Assembly` constructor: .. doctest:: >>> all_cells = Assembly(tc_cells, ctx_cells) An assembly behaves in most ways like a :class:`Population`, e.g. for setting and retrieving parameters, specifying which neurons to record from, etc. It can also be specified as the source or target of a :class:`Projection`. In this case, all the neurons in the component populations are treated as identical for the purposes of the connection algorithm (note that if the post-synaptic receptor type is specified (with the :attr:`receptor_type` argument), an Exception will be raised if not all component neuron types possess that receptor type). Individual populations within an :class:`Assembly` may be accessed via their labels, e.g.: .. doctest:: >>> all_cells.get_population("Thalamocortical neurons") Population(100, EIF_cond_exp_isfa_ista(), structure=RandomStructure(origin=(0.0, 0.0, 0.0), boundary=Sphere(radius=200.0), rng=NumpyRNG(seed=None)), label='Thalamocortical neurons') Iterating over an assembly returns individual IDs, ordered by population. Similarly, the :attr:`size` attribute of an :class:`Assembly` gives the total number of neurons it contains. To iterate over or count populations, use the :attr:`populations` attribute: .. doctest:: >>> for p in all_cells.populations: ... print("%-23s %4d %s" % (p.label, p.size, p.celltype.__class__.__name__)) Thalamocortical neurons 100 EIF_cond_exp_isfa_ista Cortical neurons 500 EIF_cond_exp_isfa_ista Inspecting and modifying parameter values and initial conditions ================================================================ Although both parameter values and initial conditions may be specified when creating a :class:`Population` (and this is generally the most efficient place to do it), it is also possible to modify them later. The :meth:`get()` method of :class:`Population`, :class:`PopulationView` and :class:`Assembly` returns the current value(s) of one or more parameters: .. doctest:: >>> ctx_cells.get('tau_m') 20.0 >>> all_cells[0:10].get('v_reset') -60.0 If the parameter is homogeneous across the group, a single number will be returned, otherwise :meth:`get()` will return a NumPy array containing the parameter values for all neurons: .. doctest:: >>> ctx_cells.get('tau_refrac') array([ 2.64655001, 2.15914942, 2.53500179, ... It is also possible to ask for multiple parameters at once, in which case a list of values in the same order as the list of parameter names will be returned. .. doctest:: >>> ctx_cells.get(['tau_m', 'cm']) [20.0, 0.25] When running a distributed simulation using MPI_, :meth:`get()` will by default return values for only those neurons that exist on the current MPI node. To get the values for all neurons, use ``get(parameter_name, gather=True)``. To modify parameter values, use the :class:`set()` method. To set the same value for all neurons, pass a single number as the parameter value: .. doctest:: >>> ctx_cells.set(a=2.0, b=0.2) To set different values for different neurons there are several options - see :doc:`parameters` for more details. To modify the initial values of model variables, use the :meth:`initialize()` method: .. doctest:: >>> ctx_cells.initialize(v=RandomDistribution('normal', (-65.0, 2.0)), ... w=0.0) The default initial values may be inspected as follows: .. doctest:: >>> ctx_cells.celltype.default_initial_values {'gsyn_exc': 0.0, 'gsyn_inh': 0.0, 'w': 0.0, 'v': -70.6} Injecting current into neurons ============================== Static or time-varying currents may be injected into neurons using either the :meth:`inject_into()` method of the :class:`CurrentSource`: .. doctest:: >>> pulse = DCSource(amplitude=0.5, start=20.0, stop=80.0) >>> pulse.inject_into(tc_cells) or the :meth:`inject()` method of the :class:`Population`, :class:`PopulationView` or :class:`Assembly`: .. doctest:: >>> import numpy >>> times = numpy.arange(0.0, 100.0, 1.0) >>> amplitudes = 0.1*numpy.sin(times*numpy.pi/100.0) >>> sine_wave = StepCurrentSource(times=times, amplitudes=amplitudes) >>> ctx_cells[80:90].inject(sine_wave) See :doc:`injecting_current` for more about injecting currents. Recording variables and retrieving recorded data ================================================ Just as each cell type has a well-defined set of parameters (whose values are constant over time), so it has a well-defined set of state variables, such as the membrane potential, whose values change over the course of a simulation. The :attr:`recordable` attribute of a :class:`CellType` class contains a list of these variables, as well as the 'spikes' variable, which is used to record the times of action potentials: .. doctest:: >>> ctx_cells.celltype.recordable ['spikes', 'v', 'w', 'gsyn_exc', 'gsyn_inh'] The :meth:`record()` method specifies which variables should be recorded: .. doctest:: >>> all_cells.record('spikes') >>> ctx_cells.sample(10).record(('v', 'w')) #, sampling_interval=0.2) Note that the sampling interval must be an integer multiple of the simulation time step (except for simulators which allow use of variable time-step integration methods). .. todo:: discuss specifying filename in record() At the end of a simulation, the recorded data can be retrieved using the :meth:`get_data()` method: .. doctest:: >>> t = run(0.2) >>> data_block = all_cells.get_data() or written to file using :meth:`write_data()`: .. doctest:: >>> from neo.io import NeoHdf5IO >>> h5file = NeoHdf5IO("my_data.h5") >>> ctx_cells.write_data(h5file) >>> h5file.close() .. testcleanup:: import os if os.path.exists("my_data.h5"): os.remove("my_data.h5") :meth:`get_data()` returns a Neo_ :class:`Block` object. For more information on Neo see the documentation at http://packages.python.org/neo. Here, it will suffice to note that a :class:`Block` is the top-level container, and contains one or more :class:`Segments`. Each :class:`Segment` is a container for data that share a common time basis, and can contain lists of :class:`AnalogSignal` and :class:`SpikeTrain` objects. These data objects inherit from NumPy array, and so can be treated in further processing (analysis, visualization, etc.) in exactly the same way as plain arrays, but in addition they carry metadata about units, sampling interval, etc. :meth:`write_data()` also makes use of Neo, and allows writing to any of the several output file formats supported by Neo. Note that as a short-cut, you can just give a filename to :meth:`write_data()`; the output format will then be determined based on the filename extension ('.h5' for HDF5, '.txt' for ASCII, etc.) if possible, otherwise the default file format (determined by the value of ``pyNN.recording.DEFAULT_FILE_FORMAT``) will be used. For more details, see :doc:`data_handling`. Working with individual neurons =============================== Although it is usually more convenient and more efficient to work with populations of neurons, it is occasionally convienient to work with individual neurons, represented as an :class:`ID` object: .. doctest:: >>> tc_cells[47] 709 For the simulator backends shipped with PyNN, the :class:`ID` class is a subclass of :class:`int`, and in the case of NEURON and NEST matches the global ID (gid) used internally by the simulator. There is no requirement that IDs be integers, however, nor that they have the same value across different simulators. The :attr:`parent` attribute contains a reference to the parent :class:`population`: .. doctest:: >>> a_cell = tc_cells[47] >>> a_cell.parent.label 'Thalamocortical neurons' To recover the index of a neuron within its parent given the :class:`ID`, use :meth:`Population.id_to_index()`, e.g.: .. doctest:: >>> tc_cells.id_to_index(a_cell) 47 The ``ID`` object allows direct access to the parameters of individual neurons, e.g.: .. doctest:: >>> a_cell.tau_m 20.0 To change several parameters at once for a single neuron, use the :meth:`set_parameters()` method: .. doctest:: >>> a_cell.set_parameters(tau_m=10.0, cm=0.5) >>> a_cell.tau_m 10.0 >>> a_cell.cm 0.5 .. _`Destexhe (2009)`: http://cns.iaf.cnrs-gif.fr/abstracts/TCX2008.html .. _MPI: http://en.wikipedia.org/wiki/Message_Passing_Interface .. _Neo: http://neuralensemble.org/neo/