File: GSL.rst

package info (click to toggle)
brian 2.9.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,872 kB
  • sloc: python: 51,820; cpp: 2,033; makefile: 108; sh: 72
file content (238 lines) | stat: -rw-r--r-- 12,433 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
Solving differential equations with the GNU Scientific Library
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Conventionally, Brian generates its own code performing :doc:`../user/numerical_integration`
according to the chosen algorithm (see the section on :doc:`codegen`).
Another option is to let the differential equation solvers defined in the
`GNU Scientific Library (GSL) <https://www.gnu.org/software/gsl/doc/html/ode-initval.html>`_
solve the given equations. In addition to offering a few extra integration methods,
the GSL integrator comes with the option of having an adaptable timestep. The
latter functionality can have benefits for the speed with which large simulations
can be run. This is because it allows the use of larger timesteps for the overhead
loops in Python, without losing the accuracy of the numerical integration at points
where small timesteps are necessary. In addition, a major benefit of using the ODE
solvers from GSL is that an estimation is performed on how wrong the current solution
is, so that simulations can be performed with some confidence on accuracy.
(Note however that the confidence of accuracy is based on estimation!)

StateUpdateMethod
-----------------

Translation of equations to abstract code
+++++++++++++++++++++++++++++++++++++++++

The first part of Brian's code generation is the translation of equations to what we
call 'abstract code'. In the case of Brian's stateupdaters so far, this abstract
code describes the calculations that need to be done to update differential variables
depending on their equations as is explained in the section on :doc:`../advanced/state_update`.
In the case of preparing the equations for GSL integration this is a bit different.
Instead of writing down the computations that have to be done to reach the new value
of the variable after a time step, the equations have to be described in a way that
GSL understands. The differential equations have to be defined in a function and
the function is given to GSL. This is best explained with an example. If we have
the following equations (taken from the adaptive threshold example):

.. code-block:: none

   dv/dt = -v/(10*ms) : volt
   dvt/dt = (10*mV - vt)/(15*ms) : volt

We would describe the equations to GSL as follows:

.. code-block:: C

   v = y[0]
   vt = y[1]
   f[0] = -v/(10e-3)
   f[1] = (10e-3 - vt)

Each differential variable gets an index. Its value at any time is saved in the
``y``-array and the derivatives are saved in the ``f``-array.
However, doing this translation in the stateupdater would mean that Brian has to
deal with variable descriptions that contain array accessing: something that for
example sympy doesn't do. Because we still want to use Brian's existing parsing
and checking mechanisms, we needed to find a way to describe the abstract code with
only 'normal' variable names.
Our solution is to replace the ``y[0]``, ``f[0]``, etc. with a 'normal' variable name
that is later replaced just before the final code generation (in the `GSLCodeGenerator`).
It has a tag and all the information needed to write the final code. As an example,
the GSL abstract code for the above equations would be:

.. code-block:: C

    v = _gsl_y0
    vt = _gsl_y1
    _gsl_f0 = -v/(10e-3)
    _gsl_f1 = (10e-3 - vt)

In the `GSLCodeGenerator` these tags get replaced by the actual accessing of the arrays.

Return value of the StateUpdateMethod
+++++++++++++++++++++++++++++++++++++

So far, for each each code generation language (numpy, cython) there was just
one set of rules of how to translate abstract code to real code, described in
its respective `CodeObject` and `CodeGenerator`. If the target language is set
to Cython, the stateupdater will use the `CythonCodeObject`, just like other
objects such as the `StateMonitor`. However, to achieve the above decribed
translations of the abstract code generated by the `StateUpdateMethod`, we
need a special `CythonCodeObject` for the stateupdater alone (which at its turn
can contain the special `CodeGenerator`), and this `CodeObject` should be
selected based on the chosen `StateUpdateMethod`.

In order to achieve `CodeObject` selection based on the chosen stateupdater, the
`StateUpdateMethod` returns a class that can be called with an object, and the
appropriate `CodeObject` is added as an attribute to the given object. The return
value of this callable is the abstract code describing the equations in a
language that makes sense to the `GSLCodeGenerator`.

GSLCodeObject
-------------
Each target language has its own `GSLCodeObject` that is derived from the
already existing code object of its language. There are only minimal changes
to the already existing code object:

* Overwrite ``stateupate`` template: a new version of the ``stateupdate``
  template is given (``stateupdate.cpp`` for C++ standalone and
  ``stateupdate.pyx`` for cython).
* Have a GSL specific generator_class: `GSLCythonCodeGenerator`
* Add the attribute ``original_generator_class``: the conventional
  target-language generator is used to do the bulk of the translation to get
  from abstract code to language-specific code.

This defining of GSL-specific code objects also  allowed us to catch compilation
errors so we can give the user some information on that it might be GSL-related
(overwriting the ``compile()`` method in the case of cython). In the case of
the C++ `CodeObject` such overriding wasn't really possible so compilation
errors in this case might be quite undescriptive.

GSLCodeGenerator
----------------
This is where the magic happens. Roughly 1000 lines of code define the
translation  of abstract code to code that uses the GNU Scientific Library's ODE
solvers to achieve state updates.

Upon a call to `run`, the code objects necessary for the simulation get made.
The code for this is described in the device. Part of making the code objects
is generating the code that describes the code objects. This starts with a
call to ``translate``, which in the case of GSL brings us to
the `GSLCodeGenerator.translate()`. This method is built up as follows:

* Some GSL-specific preparatory work:

  - Check whether the equations contain variable names that are reserved for
    the GSL code.
  - Add the 'gsl tags' (see section on StateUpdateMethod) to the
    variables known to Brian as non-scalars. This is necessary to ensure that
    all equations containing 'gsl tags' are considered vector equations, and
    thus added to Brian's vector code.
  - Add GSL integrator meta variables as official Brian variables, so these
    are also taken into account upon translation. The meta variables that are
    possible are described in the user manual (e.g. GSL's step taken in a
    single overhead step '_step_count').
  - Save function names. The original generators delete the function names
    from the variables dictionary once they are processed. However, we need to
    know later in the GSL part of the code generation whether a certain encountered
    variable name refers to a function or not.

* Brian's general preparatory work. This piece of code is directly copied from
  the base CodeGenerator and is thus similar to what is done normally.
* A call to ``original_generator.translate()`` to get the abstract code translated
  into code that is target-language specific.
* A lot of statements to translate the target-language specific code to
  GSL-target-language specific code, described in more detail below.

The biggest difference between conventional Brian code and GSL code is that
the stateupdate-decribing lines are contained directly in the ``main()`` or in a
separate function, respectively. In both cases, the equations describing the
system refer to parameters that are in the Brian namespace (e.g. "dv/dt =
-v/tau" needs access to "tau"). How can we access Brian's namespace in this
separate function that is needed with GSL?

To explain the solution we first need some background information on this
'separate function' that is given to the GSL integrators: ``_GSL_func``.
This function always gets three arguments:

* ``double t``: the current time. This is relevant when the equations are
  dependent on time.
* ``const double _GSL_y[]``': an array containing the current values of the
  differential variables (const because the cannot be changed by _GSL_func
  itself).
* ``double f[]``: an array containing the derivatives of the differential
  variables (i.e. the equations describing the differential system).
* ``void * params``: a pointer.

The pointer can be a pointer to whatever you want, and can thus point to a
data structure containing the system parameters (such as tau). To achieve
a structure containing all the parameters of the system, a considerable
amount of code has to be added/changed to that generated by conventional Brian:

* The data structure, _GSL_dataholder, has to be defined with all variables
  needed in the vector code. For this reason, also the datatype of each variable is required.

  - This is done in the method `GSLCodeGenerator.write_dataholder`

* Instead of referring to the variables by their name only (e.g. ``dv/dt =
  -v/tau``), the variables have to be accessed as part of the data structure
  (e.g. ``dv/dt = -v/_GSL_dataholder->tau`` in the case of cpp). Also, as
  mentioned earlier, we want to translate the 'gsl tags' to what they should be
  in the final code (e.g. ``_gsl_f0`` to ``f[0]``).

  - This is done in the method `GSLCodeGenerator.translate_vector_code`. It works
    based on the
    to_replace dictionary (generated in the methods
    `GSLCodeGenerator.diff_var_to_replace` and
    `GSLCodeGenerator.to_replace_vector_vars`) that
    simply contains the old variables as keys and
    new variables as values, and is given to the word_replace function.

* The values of the variables in the data structure have to be set to the
  values of the variables in the Brian namespace.

  - This is done in the method `GSLCodeGenerator.unpack_namespace`, and for the
    'scalar' variables that require calculation first it is done in the method
    `GSLCodeGenerator.translate_scalar_code`.

In addition, a few more 'support' functions are generated for the GSL script:


* ``int _set_dimension(size_t * dimension)``: sets the dimension of the system.
  Required for GSL.
* ``double* _assign_memory_y()``: allocates the right amount of memory for the y
  array (also according to the dimension of the system).
* ``int _fill_y_vector(_dataholder* _GSL_dataholder, double* _GSL_y, int _idx)``:
  pulls out the values for each differential variable out of the 'Brian' array
  into the y-vector. This happens in the vector loop
  (e.g. ``y[0] = _GSL_dataholder->_ptr_array_neurongroup_v[_idx];`` for C++).
* ``int _empty_y_vector(_dataholder* _GSL_dataholder, double* _GSL_y, int _idx)``:
  the opposite of _fill_y_vector. Pulls final numerical solutions from the y array
  and gives it back to Brian's namespace.
* ``double* _set_GSL_scale_array()``: sets the array bound for each differential
  variable, for which the values are based on ``method_options['absolute_error']`` and
  ``method_options['absolute_error_per_variable']``.

All of this is written in support functions so that the vector code in the ``main()``
can stay almost constant for any simulation.

Stateupdate templates
---------------------

There is many extra things that need to be done for each simulation when using GSL
compared to conventional Brian stateupdaters. These are summarized in this section.

Things that need to be done for every type of simulation (either before, in or after ``main()``):

* Cython-only: define the structs and functions that we will be using in cython language.
* Prepare the ``gsl_odeiv2_system``: give function pointer, set dimension, give pointer to ``_GSL_dataholder`` as params.
* Allocate the driver (name for the struct that contains the info necessary to perform GSL integration)
* Define dt.

Things that need to be done every loop iteration for every type of simulation:

* Define t and t1 (t + dt).
* Transfer the values in the Brian arrays to the y-array that will be given to GSL.
* Set ``_GSL_dataholder._idx`` (in case we need to access array variables in ``_GSL_func``).
* Initialize the driver (reset counters, set ``dt_start``).
* Apply driver (either with adaptable- or fixed time step).
* Optionally save certain meta-variables
* Transfer values from GSL's y-vector to Brian arrays