Salome HOME
Extending sampling control and output
[modules/adao.git] / doc / en / ref_operator_requirements.rst
1 ..
2    Copyright (C) 2008-2023 EDF R&D
3
4    This file is part of SALOME ADAO module.
5
6    This library is free software; you can redistribute it and/or
7    modify it under the terms of the GNU Lesser General Public
8    License as published by the Free Software Foundation; either
9    version 2.1 of the License, or (at your option) any later version.
10
11    This library is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14    Lesser General Public License for more details.
15
16    You should have received a copy of the GNU Lesser General Public
17    License along with this library; if not, write to the Free Software
18    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
19
20    See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
21
22    Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
23
24 .. _section_ref_operator_requirements:
25
26 Requirements for functions describing an operator
27 -------------------------------------------------
28
29 .. index:: single: setObservationOperator
30 .. index:: single: setEvolutionModel
31 .. index:: single: setControlModel
32
33 The availability of the operators of observation, and sometimes of evolution,
34 are required to implement the data assimilation or optimization procedures. As
35 the evolution operator is considered in its incremental form, which represents
36 the transition between two successive states, it is then formally similar to
37 the observation operator and the way to describe them is unique.
38
39 These operators include the **physical simulation by numerical calculations**.
40 But they also include **filtering, projection or restriction** of simulated
41 quantities, which are necessary to compare the simulation to the observation.
42
43 Schematically, an operator :math:`O` has to give a output simulation or
44 solution for specified input parameters. Part of the input parameters can be
45 modified during the optimization procedure. So the mathematical representation
46 of such a process is a function. It was briefly described in the section
47 :ref:`section_theory`. It is generalized here by the relation:
48
49 .. math:: \mathbf{y} = O( \mathbf{x} )
50
51 between the pseudo-observations outputs :math:`\mathbf{y}` and the input
52 parameters :math:`\mathbf{x}` using the observation or evolution :math:`O`
53 operator. The same functional representation can be used for the linear tangent
54 model :math:`\mathbf{O}` of :math:`O` and its adjoint :math:`\mathbf{O}^*`,
55 also required by some data assimilation or optimization algorithms.
56
57 On input and output of these operators, the :math:`\mathbf{x}` and
58 :math:`\mathbf{y}` variables, or their increments, are mathematically vectors,
59 and they can be given by the user as non-oriented vectors (of type list or
60 Numpy array) or oriented ones (of type Numpy matrix).
61
62 Then, **to fully describe an operator, the user has only to provide a function
63 that completely and only realize the functional operation**.
64
65 This function is usually given as a **Python function or script**, that can be
66 in particular executed as an independent Python function or in a YACS node.
67 These function or script can, with no differences, launch external codes or use
68 internal Python or SALOME calls and methods. If the algorithm requires the 3
69 aspects of the operator (direct form, tangent form and adjoint form), the user
70 has to give the 3 functions or to approximate them using ADAO.
71
72 There are for the user 3 practical methods to provide an operator functional
73 representation, which are different depending on the chosen argument:
74
75 - :ref:`section_ref_operator_one`
76 - :ref:`section_ref_operator_funcs`
77 - :ref:`section_ref_operator_switch`
78
79 In case of ADAO scripted interface (TUI), only the first two are necessary
80 because the third is included in the second. In case of ADAO graphical
81 interface EFICAS, these methods are chosen in the "*FROM*"  field of each
82 operator having a "*Function*" value as "*INPUT_TYPE*", as shown by the
83 following figure:
84
85   .. eficas_operator_function:
86   .. image:: images/eficas_operator_function.png
87     :align: center
88     :width: 100%
89   .. centered::
90     **Choosing graphically an operator functional representation**
91
92 In ADAO textual interface (TUI), in the specific case illustrated above, the
93 same approach is taken by writing :
94 ::
95
96     ...
97     case.set( 'ObservationOperator',
98         OneFunction = True,
99         Script = 'scripts_for_JDC.py'
100         )
101     ...
102
103 .. _section_ref_operator_one:
104
105 First functional form: one direct operator only
106 +++++++++++++++++++++++++++++++++++++++++++++++
107
108 .. index:: single: OneFunction
109 .. index:: single: ScriptWithOneFunction
110 .. index:: single: DirectOperator
111 .. index:: single: DifferentialIncrement
112 .. index:: single: CenteredFiniteDifference
113
114 The first one consist in providing only one function, potentially non-linear,
115 and to approximate the associated tangent and adjoint operators.
116
117 This is done in ADAO by using, in the ADAO graphical interface EFICAS, the
118 keyword "*ScriptWithOneFunction*" for the description by a script. In the
119 textual interface, it is the keyword "*OneFunction*", possibly combined with
120 "*Script*" keyword depending on whether it is a function or a script. If it is
121 by external script, the user must provide a file containing a function that has
122 the mandatory name "*DirectOperator*". For example, an external script can
123 follow the generic template::
124
125     def DirectOperator( X ):
126         """ Direct non-linear simulation operator """
127         ...
128         ...
129         ...
130         # Result: Y = O(X)
131         return "a vector similar to Y"
132
133 In this case, the user has also provide a value for the differential increment
134 (or keep the default value), using through the graphical interface (GUI) or
135 textual one (TUI) the keyword "*DifferentialIncrement*" as parameter, which has
136 a default value of 1%. This coefficient will be used in the finite differences
137 approximation to build the tangent and adjoint operators. The finite
138 differences approximation order can also be chosen through the GUI, using the
139 keyword "*CenteredFiniteDifference*", with ``False`` or 0 for an uncentered
140 schema of first order (which is the default value), and with ``True`` or 1 for
141 a centered schema of second order (and of twice the first order computational
142 cost). If necessary and if possible, :ref:`subsection_ref_parallel_df` can be
143 used. In all cases, an internal cache mechanism is used to restrict the number
144 of operator evaluations at the minimum possible in a sequential or parallel
145 execution scheme for numerical approximations of the tangent and adjoint
146 operators, to avoid redundant calculations. One can refer to the section
147 dealing with :ref:`subsection_iterative_convergence_control` to discover the
148 interaction with the convergence parameters.
149
150 This first operator definition form allows easily to test the functional form
151 before its use in an ADAO case, greatly reducing the complexity of operator
152 implementation. One can then use the "*FunctionTest*" ADAO checking algorithm
153 (see the section on the :ref:`section_ref_algorithm_FunctionTest`) specifically
154 designed for this test.
155
156 **Important:** the name "*DirectOperator*" is mandatory when using an
157 independant Python script. The type of the input ``X`` argument can be either a
158 list of float values, a Numpy array or a Numpy matrix, and the user function
159 has to accept and treat all these cases. The type of the output argument ``Y``
160 must also be equivalent to a list of real values.
161
162 Various forms of operators are available in several scripts included in the
163 :ref:`section_docu_examples`.
164
165 .. _section_ref_operator_funcs:
166
167 Second functional form: three operators direct, tangent and adjoint
168 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
169
170 .. index:: single: ThreeFunctions
171 .. index:: single: ScriptWithFunctions
172 .. index:: single: DirectOperator
173 .. index:: single: TangentOperator
174 .. index:: single: AdjointOperator
175
176 .. warning::
177
178   In general, it is recommended to use the first functional form rather than
179   the second one. A small performance improvement is not a good reason to use a
180   detailed implementation as this second functional form.
181
182 The second one consist in providing directly the three associated operators
183 :math:`O`, :math:`\mathbf{O}` and :math:`\mathbf{O}^*`. This is done by using
184 the keyword "*ScriptWithFunctions*" for the description of the chosen operator
185 in the ADAO graphical interface EFICAS. In the textual interface, it is the
186 keyword "*ThreeFunctions*", possibly combined with "*Script*" keyword depending
187 on whether it is a function or a script. The user have to provide in one script
188 three functions, with the three mandatory names "*DirectOperator*",
189 "*TangentOperator*" and "*AdjointOperator*". For example, the external script
190 can follow the template::
191
192     def DirectOperator( X ):
193         """ Direct non-linear simulation operator """
194         ...
195         ...
196         ...
197         return "a vector similar to Y"
198
199     def TangentOperator( pair = (X, dX) ):
200         """ Tangent linear operator, around X, applied to dX """
201         X, dX = pair
202         ...
203         ...
204         ...
205         return "a vector similar to Y"
206
207     def AdjointOperator( pair = (X, Y) ):
208         """ Adjoint operator, around X, applied to Y """
209         X, Y = pair
210         ...
211         ...
212         ...
213         return "a vector similar to X"
214
215 Another time, this second operator definition allow easily to test the
216 functional forms before their use in an ADAO case, reducing the complexity of
217 operator implementation.
218
219 For some algorithms (in particular filters without ensemble), it is required
220 that the tangent and adjoint functions can return the matrix equivalent to the
221 linear operator. In this case, when respectively the ``dX`` or the ``Y``
222 arguments are ``None``, the user script has to return the associated matrix.
223 The templates of the "*TangentOperator*" and "*AddOperator*" functions then
224 become the following::
225
226     def TangentOperator( pair = (X, dX) ):
227         """ Tangent linear operator, around X, applied to dX """
228         X, dX = pair
229         ...
230         ...
231         ...
232         if dX is None or len(dX) == 0:
233             return "the matrix of the tangent linear operator"
234         else:
235             return "a vector similar to Y"
236
237     def AdjointOperator( pair = (X, Y) ):
238         """ Adjoint operator, around X, applied to Y """
239         X, Y = pair
240         ...
241         ...
242         ...
243         if Y is None or len(Y) == 0:
244             return "the adjoint linear operator matrix"
245         else:
246             return "a vector similar to X"
247
248 **Important:** the names "*DirectOperator*", "*TangentOperator*" and
249 "*AdjointOperator*" are mandatory when using an independant Python script. The
250 type of the ``X``, Y``, ``dX`` input or output arguments can be either a list
251 of float values, a Numpy array or a Numpy matrix. The user function has to
252 treat these cases in his script.
253
254 .. _section_ref_operator_switch:
255
256 Third functional form: three operators with a switch
257 ++++++++++++++++++++++++++++++++++++++++++++++++++++
258
259 .. index:: single: ScriptWithSwitch
260 .. index:: single: DirectOperator
261 .. index:: single: TangentOperator
262 .. index:: single: AdjointOperator
263
264 **It is recommended not to use this third functional form without a strong
265 numerical or physical reason. A performance improvement is not a good reason to
266 use the implementation complexity of this third functional form. Only an
267 inability to use the first or second forms justifies the use of the third.**
268
269 This third form give more possibilities to control the execution of the three
270 functions representing the operator, allowing advanced usage and control over
271 each execution of the simulation code. This is done by using the keyword
272 "*ScriptWithSwitch*" for the description of the chosen operator in the ADAO
273 graphical interface EFICAS. In the textual interface, you only have to use the
274 keyword "*ThreeFunctions*" above to also define this case, with the right
275 functions. The user have to provide a switch in one script to control the
276 execution of the direct, tangent and adjoint forms of its simulation code. The
277 user can then, for example, use other approximations for the tangent and
278 adjoint codes, or introduce more complexity in the argument treatment of the
279 functions. But it will be far more complicated to implement and debug.
280
281 If, however, you want to use this third form, we recommend using the following
282 template for the switch. It requires an external script or code named here
283 "*Physical_simulation_functions.py*", containing three functions named
284 "*DirectOperator*", "*TangentOperator*" and "*AdjointOperator*" as previously.
285 Here is the switch template::
286
287     import Physical_simulation_functions
288     import numpy, logging, codecs, pickle
289     def loads( data ):
290         return pickle.loads(codecs.decode(data.encode(), "base64"))
291     #
292     method = ""
293     for param in computation["specificParameters"]:
294         if param["name"] == "method":
295             method = loads(param["value"])
296     if method not in ["Direct", "Tangent", "Adjoint"]:
297         raise ValueError("No valid computation method is given")
298     logging.info("Found method is \'%s\'"%method)
299     #
300     logging.info("Loading operator functions")
301     Function = Physical_simulation_functions.DirectOperator
302     Tangent  = Physical_simulation_functions.TangentOperator
303     Adjoint  = Physical_simulation_functions.AdjointOperator
304     #
305     logging.info("Executing the possible computations")
306     data = []
307     if method == "Direct":
308         logging.info("Direct computation")
309         Xcurrent = computation["inputValues"][0][0][0]
310         data = Function(numpy.matrix( Xcurrent ).T)
311     if method == "Tangent":
312         logging.info("Tangent computation")
313         Xcurrent  = computation["inputValues"][0][0][0]
314         dXcurrent = computation["inputValues"][0][0][1]
315         data = Tangent(numpy.matrix(Xcurrent).T, numpy.matrix(dXcurrent).T)
316     if method == "Adjoint":
317         logging.info("Adjoint computation")
318         Xcurrent = computation["inputValues"][0][0][0]
319         Ycurrent = computation["inputValues"][0][0][1]
320         data = Adjoint((numpy.matrix(Xcurrent).T, numpy.matrix(Ycurrent).T))
321     #
322     logging.info("Formatting the output")
323     it = numpy.ravel(data)
324     outputValues = [[[[]]]]
325     for val in it:
326       outputValues[0][0][0].append(val)
327     #
328     result = {}
329     result["outputValues"]        = outputValues
330     result["specificOutputInfos"] = []
331     result["returnCode"]          = 0
332     result["errorMessage"]        = ""
333
334 All various modifications could be done from this template hypothesis.
335
336 .. _section_ref_operator_control:
337
338 Special case of controlled evolution or observation operator
339 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
340
341 In some cases, the evolution or the observation operator is required to be
342 controlled by an external input control, given *a priori*. In this case, the
343 generic form of the incremental model :math:`O` is slightly modified as
344 follows:
345
346 .. math:: \mathbf{y} = O( \mathbf{x}, \mathbf{u})
347
348 where :math:`\mathbf{u}` is the control over one state increment. In fact, the
349 direct operator has to be applied to a pair of variables :math:`(X,U)`.
350 Schematically, the operator :math:`O` has to be set up as a function applicable
351 on a pair :math:`\mathbf{(X, U)}` as follows::
352
353     def DirectOperator( pair = (X, U) ):
354         """ Direct non-linear simulation operator """
355         X, U = pair
356         ...
357         ...
358         ...
359         return something like X(n+1) (evolution) or Y(n+1) (observation)
360
361 The tangent and adjoint operators have the same signature as previously, noting
362 that the derivatives has to be done only partially against :math:`\mathbf{x}`.
363 In such a case with explicit control, only the second functional form (using
364 "*ScriptWithFunctions*") and third functional form (using "*ScriptWithSwitch*")
365 can be used.
366
367 .. _section_ref_operator_dimensionless:
368
369 Additional notes on dimensionless transformation of operators
370 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
371
372 .. index:: single: Nondimensionalization
373 .. index:: single: Dimensionless
374
375 It is common that physical quantities, in input or output of the operators,
376 have significant differences in magnitude or rate of change. One way to avoid
377 numerical difficulties is to use, or to set, a dimensionless version of
378 calculations carried out in operators [WikipediaND]_. In principle, since
379 physical simulation should be as dimensionless as possible, it is at first
380 recommended to use the existing dimensionless capacity of the calculation code.
381
382 However, in the common case where we can not dispose of it, it is often useful
383 to surround the calculation to remove dimension for input or output. A simple
384 way to do this is to convert the input parameters :math:`\mathbf{x}` which are
385 arguments of a function like "*DirectOperator*". One mostly use the default
386 values :math:`\mathbf{x}^b` (background, or nominal value). Provided that each
387 component of :math:`\mathbf{x}^b` is non zero, one can indeed use a
388 multiplicative correction. For this, one can for example state:
389
390 .. math:: \mathbf{x} = \mathbf{\alpha}\mathbf{x}^b
391
392 and then optimize the multiplicative parameter :math:`\mathbf{\alpha}`.  This
393 parameter has as default value (or as background) a vector of 1. In the same
394 way, one can use additive correction if it is more interesting from a physical
395 point of view. In this case, one can state:
396
397 .. math:: \mathbf{x} =\mathbf{x}^b + \mathbf{\alpha}
398
399 and then optimize the additive parameter :math:`\mathbf{\alpha}`. In this case,
400 the parameter has for background value a vector of 0.
401
402 Be careful, applying a dimensionless transformation also requires changing the
403 associated error covariances in an ADAO formulation of the optimization
404 problem.
405
406 Such a process is rarely enough to avoid all the numerical problems, but it
407 often improves a lot the numeric conditioning of the optimization.
408
409 .. index:: single: InputFunctionAsMulti
410
411 Dealing explicitly with "multiple" functions
412 ++++++++++++++++++++++++++++++++++++++++++++
413
414 .. warning::
415
416   It is strongly recommended not to use this explicit "multiple" functions
417   definition without a very strong computing justification. This treatment is
418   already done by default in ADAO to increase performances. Only the very
419   experienced user, seeking to manage particularly difficult cases, can be
420   interested in this extension. Despite its simplicity, there is an explicit
421   risk of significantly worsening performance, or getting weird runtime errors.
422
423 It is possible, when defining operator's functions, to set them as functions
424 that treat not only one argument, but a series of arguments, to give back on
425 output the corresponding value series. Writing it as pseudo-code, the
426 "multiple" function, here named ``MultiFunctionO``, representing the classical
427 operator :math:`O` named "*DirectOperator*", does::
428
429     def MultiFunctionO( Inputs ):
430         """ Multiple ! """
431         Outputs = []
432         for X in Inputs:
433             Y = DirectOperator( X )
434             Outputs.append( Y )
435         return Outputs
436
437 The length of the output (that is, the number of calculated values) is equal to
438 the length of the input (that is, the number of states for which one want to
439 calculate the value by the operator).
440
441 This possibility is only available in the TUI textual interface for ADAO. For
442 this, when defining an operator's function, in the same time one usually define
443 the function or the external script, it can be set using an additional boolean
444 parameter "*InputFunctionAsMulti*" that the definition is one of a "multiple"
445 function. For example, if it is the observation operator that is defined in
446 this way, one should write (knowing that all other optional commands remain
447 unchanged):
448 ::
449
450     case.set( 'ObservationOperator',
451         OneFunction          = MultiFunctionO,
452         ...
453         InputFunctionAsMulti = True,
454         )