2 Copyright (C) 2008-2023 EDF R&D
4 This file is part of SALOME ADAO module.
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.
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.
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
20 See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
22 Author: Jean-Philippe Argaud, jean-philippe.argaud@edf.fr, EDF R&D
24 .. _section_ref_operator_requirements:
26 Requirements for functions describing an operator
27 -------------------------------------------------
29 .. index:: single: setObservationOperator
30 .. index:: single: setEvolutionModel
31 .. index:: single: setControlModel
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.
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.
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:
49 .. math:: \mathbf{y} = O( \mathbf{x} )
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.
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).
62 Then, **to fully describe an operator, the user has only to provide a function
63 that completely and only realize the functional operation**.
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.
72 There are for the user 3 practical methods to provide an operator functional
73 representation, which are different depending on the chosen argument:
75 - :ref:`section_ref_operator_one`
76 - :ref:`section_ref_operator_funcs`
77 - :ref:`section_ref_operator_switch`
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
85 .. eficas_operator_function:
86 .. image:: images/eficas_operator_function.png
90 **Choosing graphically an operator functional representation**
92 In ADAO textual interface (TUI), in the specific case illustrated above, the
93 same approach is taken by writing :
97 case.set( 'ObservationOperator',
99 Script = 'scripts_for_JDC.py'
103 .. _section_ref_operator_one:
105 First functional form: one direct operator only
106 +++++++++++++++++++++++++++++++++++++++++++++++
108 .. index:: single: OneFunction
109 .. index:: single: ScriptWithOneFunction
110 .. index:: single: DirectOperator
111 .. index:: single: DifferentialIncrement
112 .. index:: single: CenteredFiniteDifference
114 The first one consist in providing only one function, potentially non-linear,
115 and to approximate the associated tangent and adjoint operators.
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::
125 def DirectOperator( X ):
126 """ Direct non-linear simulation operator """
131 return "a vector similar to Y"
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.
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.
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.
162 Various forms of operators are available in several scripts included in the
163 :ref:`section_docu_examples`.
165 .. _section_ref_operator_funcs:
167 Second functional form: three operators direct, tangent and adjoint
168 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
170 .. index:: single: ThreeFunctions
171 .. index:: single: ScriptWithFunctions
172 .. index:: single: DirectOperator
173 .. index:: single: TangentOperator
174 .. index:: single: AdjointOperator
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.
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::
192 def DirectOperator( X ):
193 """ Direct non-linear simulation operator """
197 return "a vector similar to Y"
199 def TangentOperator( pair = (X, dX) ):
200 """ Tangent linear operator, around X, applied to dX """
205 return "a vector similar to Y"
207 def AdjointOperator( pair = (X, Y) ):
208 """ Adjoint operator, around X, applied to Y """
213 return "a vector similar to X"
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.
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::
226 def TangentOperator( pair = (X, dX) ):
227 """ Tangent linear operator, around X, applied to dX """
232 if dX is None or len(dX) == 0:
233 return "the matrix of the tangent linear operator"
235 return "a vector similar to Y"
237 def AdjointOperator( pair = (X, Y) ):
238 """ Adjoint operator, around X, applied to Y """
243 if Y is None or len(Y) == 0:
244 return "the adjoint linear operator matrix"
246 return "a vector similar to X"
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.
254 .. _section_ref_operator_switch:
256 Third functional form: three operators with a switch
257 ++++++++++++++++++++++++++++++++++++++++++++++++++++
259 .. index:: single: ScriptWithSwitch
260 .. index:: single: DirectOperator
261 .. index:: single: TangentOperator
262 .. index:: single: AdjointOperator
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.**
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.
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::
287 import Physical_simulation_functions
288 import numpy, logging, codecs, pickle
290 return pickle.loads(codecs.decode(data.encode(), "base64"))
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)
300 logging.info("Loading operator functions")
301 Function = Physical_simulation_functions.DirectOperator
302 Tangent = Physical_simulation_functions.TangentOperator
303 Adjoint = Physical_simulation_functions.AdjointOperator
305 logging.info("Executing the possible computations")
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))
322 logging.info("Formatting the output")
323 it = numpy.ravel(data)
324 outputValues = [[[[]]]]
326 outputValues[0][0][0].append(val)
329 result["outputValues"] = outputValues
330 result["specificOutputInfos"] = []
331 result["returnCode"] = 0
332 result["errorMessage"] = ""
334 All various modifications could be done from this template hypothesis.
336 .. _section_ref_operator_control:
338 Special case of controlled evolution or observation operator
339 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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
346 .. math:: \mathbf{y} = O( \mathbf{x}, \mathbf{u})
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::
353 def DirectOperator( pair = (X, U) ):
354 """ Direct non-linear simulation operator """
359 return something like X(n+1) (evolution) or Y(n+1) (observation)
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*")
367 .. _section_ref_operator_dimensionless:
369 Additional notes on dimensionless transformation of operators
370 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
372 .. index:: single: Nondimensionalization
373 .. index:: single: Dimensionless
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.
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:
390 .. math:: \mathbf{x} = \mathbf{\alpha}\mathbf{x}^b
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:
397 .. math:: \mathbf{x} =\mathbf{x}^b + \mathbf{\alpha}
399 and then optimize the additive parameter :math:`\mathbf{\alpha}`. In this case,
400 the parameter has for background value a vector of 0.
402 Be careful, applying a dimensionless transformation also requires changing the
403 associated error covariances in an ADAO formulation of the optimization
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.
409 .. index:: single: InputFunctionAsMulti
411 Dealing explicitly with "multiple" functions
412 ++++++++++++++++++++++++++++++++++++++++++++
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.
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::
429 def MultiFunctionO( Inputs ):
433 Y = DirectOperator( X )
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).
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
450 case.set( 'ObservationOperator',
451 OneFunction = MultiFunctionO,
453 InputFunctionAsMulti = True,