File: developer-equations.txt

package info (click to toggle)
brian 1.4.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, stretch
  • size: 23,436 kB
  • sloc: python: 68,707; cpp: 29,040; ansic: 5,182; sh: 111; makefile: 61
file content (193 lines) | stat: -rw-r--r-- 9,623 bytes parent folder | download | duplicates (3)
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
Equations
=========
An Equation is a set of single lines in a string:
    (1) ``dx/dt = f : unit`` (differential equation)
    (2) ``x = f : unit`` (equation)
    (3) ``x = y`` (alias)
    (4) ``x : unit`` (parameter)
    
The equations may be defined on multiple lines with the character \.
Comments using # may also be included.

Two special variables are defined: t (time) and xi (white noise).
Ultimately, it should be possible (using Sympy) to define equations implicitly,
e.g.: 'tau*dv/dt=-v : unit' (although it makes unit specification ambiguous).

An equation can be seen as a set of functions or code and a namespace to evaluate
them. A key part of object construction is the construction of the namespace
(after parsing).

Namespace construction
----------------------
The namespaces are stored in eq._namespace. Each equation (string) has a specific namespace.

Proposition for a simplification: there could be just one namespace per Equation object rather
than per string. Possible conflicts would be dealt with when equations are added (with prefix
as when inserting static variables, see below).

Variable substitution
~~~~~~~~~~~~~~~~~~~~~
These are simply string substitutions.

* Equation('dv/dt=-v/tau:volt',tau='taum')

The name of the variable (tau) is changed in the string to taum.

* Equation('dv/dt=-v/tau:volt',tau=None)

The name of the variable (tau) is changed in the string to a unique identifier.

Explicit namespace
~~~~~~~~~~~~~~~~~~
* Equation('dv/dt=-v/tau:volt',tau=2*ms)

The namespace is explicitly given: {'tau':2*ms}. In this case, Brian does not try
to build a namespace "magically", so the namespace must be exhaustive.
Units need not be passed.

Implicit namespace
~~~~~~~~~~~~~~~~~~
* Equation('dv/dt=-v/tau:volt')

The namespace is built from the globals and locals in the caller's frame.
For each identifier in the string, the name is looked up in:
1) locals of caller,
2) globals of caller,
3) globals of equations.py module (typically units).
Identifiers can be any Python object (for example functions).

Issues
~~~~~~
* Special variables (xi and t) are not taken into account
  at this stage, i.e., they are integrated in the namespace if present.
  This should probably be fixed and a warning should be raised.
  A warning is raised for t at the preparation stage (see below).
* If identifiers are not found, then no error is raised. This is to allow
  equations to be built in several pieces, which is useful in particular for
  library objects.
* If an identifier is found whose name is the same as the name of a variable,
  then no error is raised here and it is included in the namespace. This is difficult
  to avoid in the case when equations are built in several pieces (e.g. the conflict
  appears only when the pieces are put together). A warning is issued at the
  preparation stage (see below).

Attributes after initialisation
-------------------------------
After initialisation, an Equation object contains:

* a namespace (_namespace)
* a dictionary of units for all variables (_units)
* a dictionary of strings corresponding to each variable (right hand side of each
  equation), including parameters and aliases (_string). Parameters are defined as differential
  equations with RHS 0*unit/second. All comments are removed and multiline strings are
  concatenated.
* a list of variables of non-differential equations (_eq_names)
* a list of variables of differential equations, including parameters (_diffeq_names)
* a list of variables of differential equations, excluding parameters (_diffeq_names_nonzero)
* a dictionary of aliases (_alias), mapping a variable name to its alias

There is no explicit list of parameters, maybe it should be added.
Nothing more is done at initialisation time (no units checking, etc).
The reason is that the equation set might not be complete at this time, in the case when
equations are built in several pieces. Various checks are done using the prepare() method.

Finalisation (prepare())
------------------------
The Equation object is finalised by an explicit call to the prepare() method.

Finding Vm
~~~~~~~~~~
The first step is to find the name of the membrane potential variable (getVm()).
This is useful when the variable name for threshold or reset is not given (e.g. threshold=10*mV).
The method looks for one these names: 'v','V','vm','Vm'. If one is present, it is the
membrane potential variable. If none or more than one is present, no variable is found.
If it is found, the corresponding variable is swapped with the first variable in the
_diffeq_names list (note: not in the _diffeq_names_nonzero list). Otherwise, nothing happens.
This way, the first variable in the list is the membrane potential.
Possibly, a warning could be issued if it is not found. The problem it might issue
warnings too often. A better way would be to issue warnings only when threshold and reset
are used ambiguously (i.e., no Vm found and more than 1 variable).

Cleaning the namespace
~~~~~~~~~~~~~~~~~~~~~~
Then variables and t are removed from the namespace if present (N.B.: xi does not appear to be
removed), and warnings are issued using log_warn (method clean_namespace()).

Compiling functions
~~~~~~~~~~~~~~~~~~~
This is done by the compile_functions() method.
Python functions are created from the string definition of equations.
For each equation/differential equation, the list of identifiers is obtained from the string definition,
then only those referring to variables are kept. A Python lambda function of these remaining identifiers is then
compiled (using eval) and put in the _function dictionary.

Compiled functions are used for:

* checking units
* obtaining the list of arguments (this could be done independently)
* state updates

This step might be avoided and replaced by eval calls. It might actually be a little simpler because
arguments would be replaced by namespace. It seems to be faster with the current implementation,
but the string could be compiled with compile() (then evaluated in the relevant namespace).
Besides, with the way it is currently evaluated in the Euler update: ``f(*[S[var] for var in f.func_code.co_varnames])``,
it is not faster than direct evaluation in the namespace.

Checking units
~~~~~~~~~~~~~~
This is done by the check_units() method.
First, the static equations are ordered (see next section).

To check the units of a static equation, one calls the associated function (giving the RHS) where the
arguments are units (e.g., 1*volt for v, etc.) and adds the units of the LHS. A dimension error is raised
if it is not homogeneous. Currently, the message states "The differential equation is not homogeneous" but it
should be adapted to non-differential equations. One problem with this way of checking units is that the RHS function
may not be defined at the point it is checked.

Differential equations are checked in the same way, with two specificities: the units of RHS should be the units
of the variable divided by second (dx/dt), and noise (xi) has units of second**-.5 (this is put in the globals of
the function, which might not be a very clean way to do it).

Ordering static variables
~~~~~~~~~~~~~~~~~~~~~~~~~
It seems that this method (set_eq_order()) is already called by check_units() and therefore it is probably
not necessary to call it here.
This method computes the dependency graph of (static) equations on other static variables,
which must have no cycle (otherwise an error is raised). From that graph, an update list is built and
put in _eq_names. Then for each variable (static or differential), the list of dependent static variables is built
and sorted in update order. The result is put in the _dependencies dictionary.

This is a necessary step to calculate the RHS of any equation: it gives the ordered list of static variables
to calculate first before calculating the RHS.

Inserting static variables into differential equations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The value of static variables are then replaced by their string value (RHS) in all differential equations
(substitute_eq()). The previous step (ordering) ensures that the result if correct and does not depend on
static variables anymore.
To avoid namespace conflicts, all identifiers in the namespace of a static variable is augmented by a
prefix: name+'_' (e.g. 'x_y' for identifier y in equation 'x=2*y'). Then namespaces are merged.

It might not be optimal to do it in this way, because some of calculations will be done several times in
an update step. It might be better to keep the static variables separate.

Recompiling functions
~~~~~~~~~~~~~~~~~~~~~
Functions are then recompiled so that differential equations are now independent of static variables.

Checking free variables
~~~~~~~~~~~~~~~~~~~~~~~
Finally, the list of undefined identifiers is checked (free_variables()) and a warning is issued if
any is found.

Freezing
--------
Freezing is done by calling compile_functions(freeze=True). Each string expression is then frozen
with optimiser.freeze(), which replaces identifiers by their float value. This step does not necessarily
succeed, in which case a warning (not an error) is issued.

Adding Equation objects
-----------------------
Adding equations consists simply in merging the lists/dictionaries of variables, namespaces, strings, units
and functions. Conflicts raise an error.
This step must precede preparation of the object.