Skip to content

Commit 270552e

Browse files
author
Marco Dal Molin
committed
Revised version of the documentation
1 parent d963545 commit 270552e

27 files changed

Lines changed: 1013 additions & 922 deletions

doc/build_element.rst

Lines changed: 84 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
.. and existing ones modified. Once the guide will reach its final
88
.. version, this box will disappear.
99
10-
.. note:: If you build your own component using SuperflexPy, you should also
11-
share your implementation with the community and contribute to the
10+
.. note:: If you build your own component using SuperflexPy, we would appreciate
11+
if share your implementation with the community (see
12+
:ref:`contribute`). Remember to contribute also to the
1213
:ref:`elements_list` page to make other users aware of your
1314
implementation.
1415

@@ -17,8 +18,8 @@
1718
Expand SuperflexPy: build customized elements
1819
=============================================
1920

20-
In this page we will illustrate how to create customized elements using the
21-
SuperflexPy framework.
21+
This page illustrates how to create customized elements using the SuperflexPy
22+
framework.
2223

2324
The examples include three elements:
2425

@@ -33,18 +34,18 @@ functionalities of SuperflexPy, please have a look at the elements that have
3334
been already implemented (importing path
3435
:code:`superflexpy.implementation.elements`).
3536

36-
In this page we report, for brevity, only the code, without docstring. The
37-
complete code used to generate this page is available at the path
38-
:code:`doc/source/build_element_code.py`.
37+
.. _linear_reservoir:
3938

4039
Linear reservoir
4140
----------------
4241

43-
The linear reservoir is one of the simplest reservoir that can be built. The
44-
idea is that the output flux is a linear function of the state of the
45-
reservoir.
42+
.. image:: pics/build_element/reservoir.png
43+
:align: center
4644

47-
The element is controlled by the following differential equation
45+
The linear reservoir is the simplest type of dynamic reservoir. Its output flux
46+
is a linear function of the state of the reservoir.
47+
48+
The reservoir is controlled by the following differential equation
4849

4950
.. math::
5051
@@ -56,126 +57,109 @@ with
5657
5758
Q=kS
5859
59-
The solution of the differential equation can be approximated using a numerical
60-
method with the equation that, in the general case, becomes:
61-
62-
.. math::
63-
64-
\frac{S_{t+1} - S_{t}}{\Delta t}=P - Q(S)
65-
66-
Several numerical methods exist to approximate the solution of the differential
67-
equation and, usually, they differ for the state used to evaluate the fluxes:
68-
implicit Euler, for example, uses the state at the end of the time step
69-
(:math:`S_{t+1}`)
70-
71-
.. math::
72-
73-
\frac{S_{t+1} - S_{t}}{\Delta t}=P - kS_{t+1}
74-
75-
explicit Euler uses the state at the beginning of the time step (:math:`S_t`)
76-
77-
.. math::
78-
79-
\frac{S_{t+1} - S_{t}}{\Delta t}=P - kS_{t}
80-
81-
and so on for other methods.
82-
8360
Note that, even if for this simple case the differential equation can be solved
8461
analytically and the solution of the numerical approximation can be found
85-
without iteration, we will use anyway the numerical solver offered by
86-
SuperflexPy to illustrate how to proceed in a more general case where such
87-
option is not available.
62+
without iteration, we will use anyway the numerical approximator offered by
63+
SuperflexPy (see :ref:`numerical_solver`) to illustrate how to proceed in a
64+
more general case where analytical solutions are not available.
8865

8966
The framework provides the class :code:`ODEsElement` that has most of the
90-
methods required to solve the element. The class implementing the element will
91-
inherit from this and implement only a few methods.
67+
methods required to solve the element. The class implementing the reservoir will
68+
inherit from :code:`ODEsElement` and implement only a few methods that
69+
specialize its behavior.
9270

9371
.. literalinclude:: build_element_code.py
9472
:language: python
9573
:lines: 1, 3, 9, 10
9674
:linenos:
9775

98-
The first method to implement is the class initializer
76+
The first method to implement is the class initializer :code:`__init__`
9977

10078
.. literalinclude:: build_element_code.py
10179
:language: python
10280
:lines: 29-46
10381
:linenos:
10482

10583
The main purpose of the method (lines 9-16) is to deal with the numerical
106-
solver used for solving the differential equation. In this case we can accept
107-
two architectures: pure python or numba. The option selected will change the
108-
function used to calculate the fluxes. Keep in mind that, since some operations
109-
the python implementation of the fluxes is still used, this must be always
110-
present.
84+
solver. In this case we can accept two architectures: pure Python or numba. The
85+
option selected will change the function used to calculate the fluxes. Keep in
86+
mind that, since some methods may still need the Python implementation of the
87+
fluxes is still used, this must be always implemented.
11188

112-
The second method to define is the one that maps the (ordered) list of input
113-
fluxes to a dictionary that gives a name to these fluxes.
89+
The second method to define is :code:`set_input`, which maps the (ordered) list
90+
of input fluxes to a dictionary that gives a name to these fluxes.
11491

11592
.. literalinclude:: build_element_code.py
11693
:language: python
11794
:lines: 48, 59-60
11895
:linenos:
11996

120-
Note that the name (key) of the input flux must be the same used for the
121-
correspondent variable in the flux functions.
97+
Note that the key of the dictionary used to identify the input flux must be the
98+
same used for the corresponding variable in the flux functions.
12299

123-
The third method to implement is the one that runs the model, solving the
124-
differential equation and returning the output flux.
100+
The third method to implement is :code:`get_output`, which runs the model,
101+
solving the differential equation and returning the output flux.
125102

126103
.. literalinclude:: build_element_code.py
127104
:language: python
128105
:lines: 62, 73-77, 79-88
129106
:linenos:
130107

131-
The method takes, as input, the parameter solve: if :code:`False`, the state
132-
array of the reservoir will not be calculated again, potentially producing a
133-
different result, and the output will be computed based on the state that is
134-
already stored. This is the desired behavior in case of post-run inspection of
135-
the element.
108+
The method takes, as input, the parameter :code:`solve`: if :code:`False`, the
109+
state array of the reservoir will not be calculated again. The outputs will,
110+
therefore, be computed based on the state that is already stored from a previous
111+
run of the reservoir. This is the desired behavior in case of post-run
112+
inspection, when we want to get the output of the reservoir without solving it
113+
again.
136114

137-
Line 4 transforms the states dictionary in an ordered list, line 5 call the
138-
built-in solver of the differential equation, line 7 updates the state of the
139-
model to the one of the updated value, lines 9-14 call the external numerical
140-
solver to get the values of the fluxes (note that, for this operation, the
141-
python implementation of the fluxes is used always).
115+
Line 4 transforms the states dictionary to an ordered list. Line 5 calls the
116+
built-in solver of the differential equation. Line 7 updates the state of the
117+
model to the one of the updated value. Lines 9-14 call the external numerical
118+
approximator to get the values of the fluxes (note that, for this operation, the
119+
Python implementation of the fluxes is used always).
142120

143-
The last method(s) to implement is the one that defines the fluxes:
144-
in this case the methods are two, one for the python implementation and one for
145-
numba.
121+
The last methods to implement are :code:`_fluxes_function_python` (pure Python)
122+
and :code:`_fluxes_function_numba` (numba optimized), which are responsible for
123+
calculating the fluxes of the reservoir.
146124

147125
.. literalinclude:: build_element_code.py
148126
:language: python
149127
:lines: 90-124
150128
:linenos:
151129

152-
They are both private static methods. Their input consists of the state used to
130+
:code:`_fluxes_function_python` and :code:`_fluxes_function_numba` are both
131+
private static methods. Their input consists of the state used to
153132
compute the fluxes (:code:`S`), initial state (:code:`S0`, used to define the
154133
maximum possible state for the reservoir), index to use in the arrays
155134
(:code:`ind`, all inputs are arrays and, when solving for a single time step,
156135
the index indicates the time step to look for), input fluxes (:code:`P`), and
157136
parameters (:code:`k`). The output is a tuple containing three elements:
158137

159138
- tuple with the values of the fluxes calculated according to the state;
160-
positive sign for incoming fluxes, negative for outgoing;
139+
positive sign for incoming fluxes (e.g. precipitation, :code:`P`), negative
140+
for outgoing (e.g. streamflow, :code:`- k * S`);
161141
- lower bound for the search of the state;
162142
- upper bound for the search of the state;
163143

164144
The implementation for the numba solver differs in two aspects:
165145

166146
- the usage of the numba decorator that defines the types of the input
167147
variables (lines 24-25)
168-
- the fact that the method works only for a single time step and not for the
169-
vectorized solution (use python method for that)
148+
- the method works only for a single time step and not for the vectorized
149+
solution since, for this operation, the Python implementation is fast enough
170150

171151
.. _build_lag:
172152

173153
Half-triangular lag function
174154
----------------------------
175155

156+
.. image:: pics/build_element/lag.png
157+
:align: center
158+
159+
176160
The half-triangular lag function is a function that has the shape of a right
177161
triangle, growing linearly until :math:`t_{\textrm{lag}}` and then zero. The
178-
growth rate (:math:`\alpha`) is designed such as the total area of the triangle
162+
growth rate :math:`\alpha` is calculated such as the total area of the triangle
179163
is one.
180164

181165
.. math::
@@ -184,10 +168,10 @@ is one.
184168
& f_{\textrm{lag}}=0 & \quad \textrm{for }t>t_{\textrm{lag}}
185169
186170
SuperflexPy provides the class :code:`LagElement` that contains most of the
187-
functionalities needed to solve a lag function. The class implementing a
188-
customized lag function will inherit from it and implement only the necessary
189-
methods that are needed to calculate the transformation that needs to be
190-
applied to the incoming flux.
171+
functionalities needed to calculate the output of a lag function. The class
172+
implementing a customized lag function will inherit from :code:`LagElement` and
173+
implement only the methods needed to apply the transformation to the incoming
174+
flux.
191175

192176
.. literalinclude:: build_element_code.py
193177
:language: python
@@ -202,70 +186,68 @@ The only method to implement is the private method used to calculate the
202186
:lines: 146-160
203187
:linenos:
204188

205-
that makes use of a secondary private static method
189+
The method :code:`_build_weight` makes use of a secondary private static method
206190

207191
.. literalinclude:: build_element_code.py
208192
:language: python
209193
:lines: 162-172
210194
:linenos:
211195

212-
This method returns the value of the area of a triangle that is proportional
213-
to the lag function, with a smaller base :code:`bin`. The
196+
This method returns the value of the area :math:`A_i` of a triangle that is
197+
proportional to the lag function, with a smaller base :code:`bin`. The
214198
:code:`_build_weight` method, on the other hand, uses the output of this
215-
function to calculate the weight array.
199+
function (:math:`A_i` and :math:`A_{i-1}`) to calculate the weight array
200+
:math:`W_i`.
216201

217-
Note that this choice of using a second static method to calculate the weight
218-
array, while being convenient, is specific to this particular case; other
219-
implementation of the :code:`_build_weight` method are possible and welcome.
202+
Note that other implementations of the :code:`_build_weight` method (without
203+
using auxiliary methods) are possible.
220204

221205
Parameterized splitter
222206
----------------------
223207

224-
A splitter is an element that takes the flux coming from one element upstream
225-
and divides it to feed multiple elements downstream. Usually, the behavior of
226-
such an element is controlled by some parameters that define the part of the
208+
A splitter is an element that takes the flux from an upstream element and
209+
divides it to feed multiple downstream elements. Usually, the behavior of
210+
such an element is controlled by parameters that define the portion of the
227211
flux that goes into a specific element.
228212

229-
While SuperflexPy can support infinite fluxes (e.g., for simulating transport
230-
processes) and an infinite number of downstream elements, the simplest case
231-
that we are presenting here has a single flux that gets split into two
232-
downstream elements. In this example, the system needs only one parameter
233-
(:math:`\alpha_{\textrm{split}}`) to be defined, with the downstream fluxes
234-
that are
213+
The simple case that we are presenting here has a single flux that gets split
214+
into two downstream elements. In this example, the system needs only one
215+
parameter (:math:`\alpha_{\textrm{split}}`) to be defined, with the downstream
216+
fluxes that are
235217

236218
.. math::
237219
238220
& Q_1^{\textrm{out}} = \alpha_{\textrm{split}} Q^{\textrm{in}} \\
239221
& Q_2^{\textrm{out}} = \left(1-\alpha_{\textrm{split}}\right) Q^{\textrm{in}}
240222
241-
SuperflexPy provides the class :code:`ParameterizedElement` that is designed for
242-
all the generic elements that are controlled by some parameters but do not have
223+
SuperflexPy provides the class :code:`ParameterizedElement` that can be extended
224+
to implement all the elements that are controlled by parameters but do not have
243225
a state. The class implementing the parameterized splitter will inherit from
244-
this and implement only some methods.
226+
:code:`ParameterizedElement` and implement only some methods.
245227

246228
.. literalinclude:: build_element_code.py
247229
:language: python
248230
:lines: 5, 176, 177
249231
:linenos:
250232

251-
The first thing to define are two private attributes defining how many upstream
252-
and downstream elements the splitter has; this information is used by the unit
253-
when constructing the model structure.
233+
First, we have to define two private attributes defining how many upstream and
234+
downstream elements the splitter has; this information is used by the unit when
235+
constructing the model structure.
254236

255237
.. literalinclude:: build_element_code.py
256238
:language: python
257239
:lines: 194-195
258240
:linenos:
259241

260-
After that we need to define the function that takes the inputs and the one
261-
that calculates the outputs of the splitter.
242+
After that we need to define the method that takes the inputs and the method
243+
that calculates the outputs.
262244

263245
.. literalinclude:: build_element_code.py
264246
:language: python
265247
:lines: 197, 208-211, 227-233
266248
:linenos:
267249

268-
The two methods have the same structure of the one implemented as part of the
269-
linear reservoir example. Note that, in this case, the argument :code:`solve` of
270-
the :code:`get_output` method is not used but it is still required to maintain
271-
the interface consistent.
250+
The two methods have the same structure of the ones implemented as part of the
251+
:ref:`linear_reservoir` example. Note that, in this case, the argument
252+
:code:`solve` of :code:`get_output` is not used, but it is still required to
253+
maintain a consistent interface.

0 commit comments

Comments
 (0)