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
1718Expand 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
2324The examples include three elements:
2425
@@ -33,18 +34,18 @@ functionalities of SuperflexPy, please have a look at the elements that have
3334been 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
4039Linear 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
8461analytically 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
8966The 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
10583The 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
153132compute the fluxes (:code: `S `), initial state (:code: `S0 `, used to define the
154133maximum 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,
156135the index indicates the time step to look for), input fluxes (:code: `P `), and
157136parameters (: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
164144The 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
173153Half-triangular lag function
174154----------------------------
175155
156+ .. image :: pics/build_element/lag.png
157+ :align: center
158+
159+
176160The half-triangular lag function is a function that has the shape of a right
177161triangle, 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
179163is 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
221205Parameterized 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
227211flux 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
243225a 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