vortex-nwp 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. vortex/__init__.py +159 -0
  2. vortex/algo/__init__.py +13 -0
  3. vortex/algo/components.py +2462 -0
  4. vortex/algo/mpitools.py +1953 -0
  5. vortex/algo/mpitools_templates/__init__.py +1 -0
  6. vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
  7. vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
  8. vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
  9. vortex/algo/serversynctools.py +171 -0
  10. vortex/config.py +112 -0
  11. vortex/data/__init__.py +19 -0
  12. vortex/data/abstractstores.py +1510 -0
  13. vortex/data/containers.py +835 -0
  14. vortex/data/contents.py +622 -0
  15. vortex/data/executables.py +275 -0
  16. vortex/data/flow.py +119 -0
  17. vortex/data/geometries.ini +2689 -0
  18. vortex/data/geometries.py +799 -0
  19. vortex/data/handlers.py +1230 -0
  20. vortex/data/outflow.py +67 -0
  21. vortex/data/providers.py +487 -0
  22. vortex/data/resources.py +207 -0
  23. vortex/data/stores.py +1390 -0
  24. vortex/data/sync_templates/__init__.py +0 -0
  25. vortex/gloves.py +309 -0
  26. vortex/layout/__init__.py +20 -0
  27. vortex/layout/contexts.py +577 -0
  28. vortex/layout/dataflow.py +1220 -0
  29. vortex/layout/monitor.py +969 -0
  30. vortex/nwp/__init__.py +14 -0
  31. vortex/nwp/algo/__init__.py +21 -0
  32. vortex/nwp/algo/assim.py +537 -0
  33. vortex/nwp/algo/clim.py +1086 -0
  34. vortex/nwp/algo/coupling.py +831 -0
  35. vortex/nwp/algo/eda.py +840 -0
  36. vortex/nwp/algo/eps.py +785 -0
  37. vortex/nwp/algo/forecasts.py +886 -0
  38. vortex/nwp/algo/fpserver.py +1303 -0
  39. vortex/nwp/algo/ifsnaming.py +463 -0
  40. vortex/nwp/algo/ifsroot.py +404 -0
  41. vortex/nwp/algo/monitoring.py +263 -0
  42. vortex/nwp/algo/mpitools.py +694 -0
  43. vortex/nwp/algo/odbtools.py +1258 -0
  44. vortex/nwp/algo/oopsroot.py +916 -0
  45. vortex/nwp/algo/oopstests.py +220 -0
  46. vortex/nwp/algo/request.py +660 -0
  47. vortex/nwp/algo/stdpost.py +1641 -0
  48. vortex/nwp/data/__init__.py +30 -0
  49. vortex/nwp/data/assim.py +380 -0
  50. vortex/nwp/data/boundaries.py +314 -0
  51. vortex/nwp/data/climfiles.py +521 -0
  52. vortex/nwp/data/configfiles.py +153 -0
  53. vortex/nwp/data/consts.py +954 -0
  54. vortex/nwp/data/ctpini.py +149 -0
  55. vortex/nwp/data/diagnostics.py +209 -0
  56. vortex/nwp/data/eda.py +147 -0
  57. vortex/nwp/data/eps.py +432 -0
  58. vortex/nwp/data/executables.py +1045 -0
  59. vortex/nwp/data/fields.py +111 -0
  60. vortex/nwp/data/gridfiles.py +380 -0
  61. vortex/nwp/data/logs.py +584 -0
  62. vortex/nwp/data/modelstates.py +363 -0
  63. vortex/nwp/data/monitoring.py +193 -0
  64. vortex/nwp/data/namelists.py +696 -0
  65. vortex/nwp/data/obs.py +840 -0
  66. vortex/nwp/data/oopsexec.py +74 -0
  67. vortex/nwp/data/providers.py +207 -0
  68. vortex/nwp/data/query.py +206 -0
  69. vortex/nwp/data/stores.py +160 -0
  70. vortex/nwp/data/surfex.py +337 -0
  71. vortex/nwp/syntax/__init__.py +9 -0
  72. vortex/nwp/syntax/stdattrs.py +437 -0
  73. vortex/nwp/tools/__init__.py +10 -0
  74. vortex/nwp/tools/addons.py +40 -0
  75. vortex/nwp/tools/agt.py +67 -0
  76. vortex/nwp/tools/bdap.py +59 -0
  77. vortex/nwp/tools/bdcp.py +41 -0
  78. vortex/nwp/tools/bdm.py +24 -0
  79. vortex/nwp/tools/bdmp.py +54 -0
  80. vortex/nwp/tools/conftools.py +1661 -0
  81. vortex/nwp/tools/drhook.py +66 -0
  82. vortex/nwp/tools/grib.py +294 -0
  83. vortex/nwp/tools/gribdiff.py +104 -0
  84. vortex/nwp/tools/ifstools.py +203 -0
  85. vortex/nwp/tools/igastuff.py +273 -0
  86. vortex/nwp/tools/mars.py +68 -0
  87. vortex/nwp/tools/odb.py +657 -0
  88. vortex/nwp/tools/partitioning.py +258 -0
  89. vortex/nwp/tools/satrad.py +71 -0
  90. vortex/nwp/util/__init__.py +6 -0
  91. vortex/nwp/util/async.py +212 -0
  92. vortex/nwp/util/beacon.py +40 -0
  93. vortex/nwp/util/diffpygram.py +447 -0
  94. vortex/nwp/util/ens.py +279 -0
  95. vortex/nwp/util/hooks.py +139 -0
  96. vortex/nwp/util/taskdeco.py +85 -0
  97. vortex/nwp/util/usepygram.py +697 -0
  98. vortex/nwp/util/usetnt.py +101 -0
  99. vortex/proxy.py +6 -0
  100. vortex/sessions.py +374 -0
  101. vortex/syntax/__init__.py +9 -0
  102. vortex/syntax/stdattrs.py +867 -0
  103. vortex/syntax/stddeco.py +185 -0
  104. vortex/toolbox.py +1117 -0
  105. vortex/tools/__init__.py +20 -0
  106. vortex/tools/actions.py +523 -0
  107. vortex/tools/addons.py +316 -0
  108. vortex/tools/arm.py +96 -0
  109. vortex/tools/compression.py +325 -0
  110. vortex/tools/date.py +27 -0
  111. vortex/tools/ddhpack.py +10 -0
  112. vortex/tools/delayedactions.py +782 -0
  113. vortex/tools/env.py +541 -0
  114. vortex/tools/folder.py +834 -0
  115. vortex/tools/grib.py +738 -0
  116. vortex/tools/lfi.py +953 -0
  117. vortex/tools/listings.py +423 -0
  118. vortex/tools/names.py +637 -0
  119. vortex/tools/net.py +2124 -0
  120. vortex/tools/odb.py +10 -0
  121. vortex/tools/parallelism.py +368 -0
  122. vortex/tools/prestaging.py +210 -0
  123. vortex/tools/rawfiles.py +10 -0
  124. vortex/tools/schedulers.py +480 -0
  125. vortex/tools/services.py +940 -0
  126. vortex/tools/storage.py +996 -0
  127. vortex/tools/surfex.py +61 -0
  128. vortex/tools/systems.py +3976 -0
  129. vortex/tools/targets.py +440 -0
  130. vortex/util/__init__.py +9 -0
  131. vortex/util/config.py +1122 -0
  132. vortex/util/empty.py +24 -0
  133. vortex/util/helpers.py +216 -0
  134. vortex/util/introspection.py +69 -0
  135. vortex/util/iosponge.py +80 -0
  136. vortex/util/roles.py +49 -0
  137. vortex/util/storefunctions.py +129 -0
  138. vortex/util/structs.py +26 -0
  139. vortex/util/worker.py +162 -0
  140. vortex_nwp-2.0.0.dist-info/METADATA +67 -0
  141. vortex_nwp-2.0.0.dist-info/RECORD +144 -0
  142. vortex_nwp-2.0.0.dist-info/WHEEL +5 -0
  143. vortex_nwp-2.0.0.dist-info/licenses/LICENSE +517 -0
  144. vortex_nwp-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2462 @@
1
+ # pylint: disable=unused-argument
2
+
3
+ """
4
+ Abstract class for any AlgoComponent (:class:`AlgoComponent`) or AlgoComponent's
5
+ Mixins (:class:`AlgoComponentDecoMixin`).
6
+
7
+ Some very generic concrete AlgoComponent classes are also provided:
8
+
9
+ * :class:`Expresso`: launch a simple script;
10
+ * :class:`BlindRun`: launch a simple executable (no MPI);
11
+ * :class:`Parallel`: launch an MPI application.
12
+
13
+ Additional abstract classes provide multiprocessing support (through the
14
+ :mod:`taylorism` package):
15
+
16
+ * :class:`TaylorRun`: launch a piece of Python code on several processes;
17
+ * :class:`ParaExpresso`: launch a script multiple times (in parallel);
18
+ * :class:`ParaBlindRun`: launch an executable multiple times (in parallel).
19
+
20
+ Such classes are based on the :mod:`taylorism` (the developer should be familiar
21
+ with this package) and uses "Worker" classes provided in the
22
+ :mod:`vortex.tools.parallelism` package.
23
+
24
+ A few examples of AlgoComponent classes are shipped with the code
25
+ (see :ref:`examples_algo`). In addition to the documentation provided
26
+ in :ref:`stepbystep-index`, it might help.
27
+
28
+ When class inheritance is not applicable or ineffective, The AlgoComponent's
29
+ Mixins are a powerful tool to mutualise some pieces of code. See the
30
+ :class:`AlgoComponentDecoMixin` class documentation for more details.
31
+ """
32
+
33
+ import collections.abc
34
+ import contextlib
35
+ import copy
36
+ import functools
37
+ import importlib
38
+ import locale
39
+ import logging
40
+ import multiprocessing
41
+ import queue
42
+ import shlex
43
+ import sys
44
+ import tempfile
45
+ import traceback as py_traceback
46
+
47
+ from bronx.fancies import loggers
48
+ from bronx.stdtypes import date
49
+ from bronx.syntax.decorators import nicedeco
50
+ import footprints
51
+ from taylorism import Boss
52
+ import vortex
53
+ import vortex.config as config
54
+ from vortex.algo import mpitools
55
+ from vortex.syntax.stdattrs import DelayedEnvValue
56
+ from vortex.tools.parallelism import ParallelResultParser
57
+
58
+ #: No automatic export
59
+ __all__ = []
60
+
61
+ logger = loggers.getLogger(__name__)
62
+
63
+
64
+ class AlgoComponentError(Exception):
65
+ """Generic exception class for Algo Components."""
66
+
67
+ pass
68
+
69
+
70
+ class AlgoComponentAssertionError(AlgoComponentError):
71
+ """Assertion exception class for Algo Components."""
72
+
73
+ pass
74
+
75
+
76
+ class DelayedAlgoComponentError(AlgoComponentError):
77
+ """Triggered when exceptions occurred during the execution but were delayed."""
78
+
79
+ def __init__(self, excs):
80
+ super().__init__("One or several errors occurred during the run.")
81
+ self._excs = excs
82
+
83
+ def __iter__(self):
84
+ yield from self._excs
85
+
86
+ def __str__(self):
87
+ outstr = "One or several errors occurred during the run. In order of appearance:\n"
88
+ outstr += "\n".join(
89
+ [
90
+ "{:3d}. {!s} (type: {!s})".format(i + 1, exc, type(exc))
91
+ for i, exc in enumerate(self)
92
+ ]
93
+ )
94
+ return outstr
95
+
96
+
97
+ class ParallelInconsistencyAlgoComponentError(Exception):
98
+ """Generic exception class for Algo Components."""
99
+
100
+ def __init__(self, target):
101
+ msg = "The len of {:s} is inconsistent with the number or ResourceHandlers."
102
+ super().__init__(msg.format(target))
103
+
104
+
105
+ @nicedeco
106
+ def _clsmtd_mixin_locked(f):
107
+ """
108
+ This is a utility decorator (for class methods) : it ensures that the method can only
109
+ be called on a bare :class:`AlgoComponentDecoMixin` class.
110
+ """
111
+
112
+ def wrapped_clsmethod(cls, *kargs, **kwargs):
113
+ if issubclass(cls, AlgoComponent):
114
+ raise RuntimeError(
115
+ "This class method should not be called once the mixin is in use."
116
+ )
117
+ return f(cls, *kargs, **kwargs)
118
+
119
+ return wrapped_clsmethod
120
+
121
+
122
+ def algo_component_deco_mixin_autodoc(cls):
123
+ """
124
+ Decorator that adds an automatic documentation on any :class:`AlgoComponentDecoMixin`
125
+ class.
126
+ """
127
+ extradoc = ""
128
+
129
+ # Document extra footprints
130
+ if cls.MIXIN_AUTO_FPTWEAK and cls._MIXIN_EXTRA_FOOTPRINTS:
131
+ extradoc += "\nThe following footprints will be applied to the target classes:\n\n"
132
+ for fp in cls._MIXIN_EXTRA_FOOTPRINTS:
133
+ if isinstance(fp, footprints.Footprint):
134
+ extradoc += footprints.doc.format_docstring(
135
+ fp, footprints.setup.docstrings, abstractfpobj=True
136
+ )
137
+ extradoc += "\n"
138
+
139
+ # Document decorating classes
140
+ if cls.MIXIN_AUTO_DECO:
141
+ for what, desc in (
142
+ ("PREPARE_PREHOOKS", "before the original ``prepare`` method"),
143
+ ("PREPARE_HOOKS", "after the original ``prepare`` method"),
144
+ ("POSTFIX_PREHOOKS", "before the original ``postfix`` method"),
145
+ ("POSTFIX_HOOKS", "after the original ``postfix`` method"),
146
+ ("SPAWN_HOOKS", "after the original ``spawn_hook`` method"),
147
+ (
148
+ "CLI_OPTS_EXTEND",
149
+ "to alter the result of the ``spawn_command_options`` method",
150
+ ),
151
+ (
152
+ "STDIN_OPTS_EXTEND",
153
+ "to alter the result of the ``spawn_stdin_options`` method",
154
+ ),
155
+ (
156
+ "_MIXIN_EXECUTE_OVERWRITE",
157
+ "instead of the original ``execute`` method",
158
+ ),
159
+ (
160
+ "MPIBINS_HOOKS",
161
+ "to alter the result of the ``_bootstrap_mpibins_hack`` method",
162
+ ),
163
+ (
164
+ "MPIENVELOPE_HOOKS",
165
+ "to alter the result of the ``_bootstrap_mpienvelope_hack`` method",
166
+ ),
167
+ ):
168
+ what = "_MIXIN_{:s}".format(what)
169
+ if getattr(cls, what, ()):
170
+ extradoc += "\nThe following method(s) will be called {:s}:\n\n".format(
171
+ desc
172
+ )
173
+ extradoc += "\n".join(
174
+ " * {!r}".format(cb) for cb in getattr(cls, what)
175
+ )
176
+ extradoc += "\n"
177
+
178
+ if extradoc:
179
+ extradoc = (
180
+ "\n .. note:: The following documentation is automatically generated. "
181
+ + "From a developer point of view, using the present mixin class "
182
+ + "will result in the following actions:\n"
183
+ + " \n".join(
184
+ [" " + t if t else "" for t in extradoc.split("\n")]
185
+ )
186
+ )
187
+
188
+ if isinstance(getattr(cls, "__doc__", None), str):
189
+ cls.__doc__ += "\n" + extradoc
190
+ else:
191
+ cls.__doc__ = extradoc
192
+
193
+ return cls
194
+
195
+
196
+ class AlgoComponentDecoMixin:
197
+ """
198
+ This is the base class for any Mixin class targeting :class:`AlgoComponent`
199
+ classes.
200
+
201
+ Like any Mixin class, this Mixin class primary use is to define methods that
202
+ will be available to the child class.
203
+
204
+ However, this class will also interact with the :class:`AlgoComponentMeta`
205
+ metaclass to alter the behaviour of the :class:`AlgoComponent` class it is
206
+ used with. Several "alterations" will be made to the resulting
207
+ :class:`AlgoComponent` class.
208
+
209
+ * A bunch of footprints' attribute can be added to the resulting class.
210
+ This is controlled by the :data:`MIXIN_AUTO_FPTWEAK` and
211
+ :data:`_MIXIN_EXTRA_FOOTPRINTS` class variables.
212
+ If :data:`MIXIN_AUTO_FPTWEAK` is ``True`` (which is the default), the
213
+ :class:`~footrprints.Footprint` objects listed in the
214
+ :data:`_MIXIN_EXTRA_FOOTPRINTS` tuple will be prepended to the resulting
215
+ :class:`AlgoComponent` class footprint definition.
216
+
217
+ * The ``execute`` method of the resulting class can be overwritten by
218
+ the method referenced in the :data:`_MIXIN_EXECUTE_OVERWRITE` class
219
+ variable. This is allowed only if no ``execute`` method is defined
220
+ manually and if no other :class:`AlgoComponentDecoMixin` tries to
221
+ overwrite it as well. If these two conditions are not met, a
222
+ :class:`RuntimeError` exception will be thrown by the the
223
+ :class:`AlgoComponentMeta` metaclass.
224
+
225
+ * A bunch of the :class:`AlgoComponent`'s methods can be decorated. This
226
+ is controlled by the :data:`MIXIN_AUTO_DECO` class variable (``True``
227
+ by default) and a bunch of other class variables containing tuples.
228
+ They are described below:
229
+
230
+ * :data:`_MIXIN_PREPARE_PREHOOKS`: Tuple of methods that will be
231
+ executed before the original prepare method. Such methods receive
232
+ the same arguments list than the original decorated method.
233
+
234
+ * :data:`_MIXIN_PREPARE_HOOKS`: Tuple of methods that will be
235
+ executed after the original prepare method. Such methods receive
236
+ the same arguments list than the original decorated method.
237
+
238
+ * :data:`_MIXIN_EXECUTE_FINALISE_HOOKS`: Tuple of method that will
239
+ be executed after any execution (even if the execution failed).
240
+
241
+ * :data:`_MIXIN_FAIL_EXECUTE_HOOKS`: Tuple of method that will
242
+ be executed if the execution fails (the original exception
243
+ will be re-raised afterwards)
244
+
245
+ * :data:`_MIXIN_POSTFIX_PREHOOKS`: Tuple of methods that will be
246
+ executed before the original postfix method. Such methods receive
247
+ the same arguments list than the original decorated method.
248
+
249
+ * :data:`_MIXIN_POSTFIX_HOOKS`: Tuple of methods that will be
250
+ executed after the original postfix method. Such methods receive
251
+ the same arguments list than the original decorated method.
252
+
253
+ * :data:`_MIXIN_SPAWN_HOOKS`: Tuple of methods that will be
254
+ executed after the original spawn_hook method. Such methods receive
255
+ the same arguments list than the original decorated method.
256
+
257
+ * :data:`_MIXIN_CLI_OPTS_EXTEND`: Tuple of methods that will be
258
+ executed after the original ``spawn_command_options`` method. Such
259
+ method will receive one argument (``self`` set aside): the value
260
+ returned by the original ``spawn_command_options`` method.
261
+
262
+ * :data:`_MIXIN_STDIN_OPTS_EXTEND`: Tuple of methods that will be
263
+ executed after the original ``spawn_stdin_options`` method. Such
264
+ method will receive one argument (``self`` set aside): the value
265
+ returned by the original ``spawn_stdin_options`` method.
266
+
267
+ """
268
+
269
+ MIXIN_AUTO_FPTWEAK = True
270
+ MIXIN_AUTO_DECO = True
271
+
272
+ _MIXIN_EXTRA_FOOTPRINTS = ()
273
+
274
+ _MIXIN_PREPARE_PREHOOKS = ()
275
+ _MIXIN_PREPARE_HOOKS = ()
276
+ _MIXIN_EXECUTE_FINALISE_HOOKS = ()
277
+ _MIXIN_FAIL_EXECUTE_HOOKS = ()
278
+ _MIXIN_POSTFIX_PREHOOKS = ()
279
+ _MIXIN_POSTFIX_HOOKS = ()
280
+ _MIXIN_SPAWN_HOOKS = ()
281
+
282
+ _MIXIN_CLI_OPTS_EXTEND = ()
283
+ _MIXIN_STDIN_OPTS_EXTEND = ()
284
+
285
+ _MIXIN_EXECUTE_OVERWRITE = None
286
+
287
+ def __new__(cls, *args, **kwargs):
288
+ if not issubclass(cls, AlgoComponent):
289
+ # This class cannot be instanciated by itself !
290
+ raise RuntimeError(
291
+ "< {0.__name__:s} > is a mixin class: it cannot be instantiated.".format(
292
+ cls
293
+ )
294
+ )
295
+ else:
296
+ return super().__new__(cls)
297
+
298
+ @classmethod
299
+ @_clsmtd_mixin_locked
300
+ def mixin_tweak_footprint(cls, fplocal):
301
+ """Update the footprint definition list."""
302
+ for fp in cls._MIXIN_EXTRA_FOOTPRINTS:
303
+ assert isinstance(fp, footprints.Footprint)
304
+ fplocal.insert(0, fp)
305
+
306
+ @classmethod
307
+ @_clsmtd_mixin_locked
308
+ def _get_algo_wrapped(
309
+ cls, targetcls, targetmtd, hooks, prehooks=(), reentering=False
310
+ ):
311
+ """Wraps **targetcls**'s **targetmtd** method."""
312
+ orig_mtd = getattr(targetcls, targetmtd)
313
+ if prehooks and reentering:
314
+ raise ValueError(
315
+ "Conflicting values between prehooks and reenterin."
316
+ )
317
+
318
+ def wrapped_method(self, *kargs, **kwargs):
319
+ for phook in prehooks:
320
+ phook(self, *kargs, **kwargs)
321
+ rv = orig_mtd(self, *kargs, **kwargs)
322
+ if reentering:
323
+ kargs = [
324
+ rv,
325
+ ] + list(kargs)
326
+ for phook in hooks:
327
+ rv = phook(self, *kargs, **kwargs)
328
+ if reentering:
329
+ kargs[0] = rv
330
+ if reentering:
331
+ return rv
332
+
333
+ wrapped_method.__name__ = orig_mtd.__name__
334
+ wrapped_method.__doc__ = (orig_mtd.__doc__ or "").rstrip(
335
+ "\n"
336
+ ) + "\n\nDecorated by :class:`{0.__module__:s}{0.__name__:s}`.".format(
337
+ cls
338
+ )
339
+ wrapped_method.__dict__.update(orig_mtd.__dict__)
340
+ return wrapped_method
341
+
342
+ @classmethod
343
+ @_clsmtd_mixin_locked
344
+ def mixin_algo_deco(cls, targetcls):
345
+ """
346
+ Applies all the necessary decorators to the **targetcls**
347
+ :class:`AlgoComponent` class.
348
+ """
349
+ if not issubclass(targetcls, AlgoComponent):
350
+ raise RuntimeError(
351
+ "This class can only be mixed in AlgoComponent classes."
352
+ )
353
+ for targetmtd, hooks, prehooks, reenter in [
354
+ (
355
+ "prepare",
356
+ cls._MIXIN_PREPARE_HOOKS,
357
+ cls._MIXIN_PREPARE_PREHOOKS,
358
+ False,
359
+ ),
360
+ ("fail_execute", cls._MIXIN_FAIL_EXECUTE_HOOKS, (), False),
361
+ ("execute_finalise", cls._MIXIN_EXECUTE_FINALISE_HOOKS, (), False),
362
+ (
363
+ "postfix",
364
+ cls._MIXIN_POSTFIX_HOOKS,
365
+ cls._MIXIN_POSTFIX_PREHOOKS,
366
+ False,
367
+ ),
368
+ ("spawn_hook", cls._MIXIN_SPAWN_HOOKS, (), False),
369
+ ("spawn_command_options", cls._MIXIN_CLI_OPTS_EXTEND, (), True),
370
+ ("spawn_stdin_options", cls._MIXIN_STDIN_OPTS_EXTEND, (), True),
371
+ ]:
372
+ if hooks or prehooks:
373
+ setattr(
374
+ targetcls,
375
+ targetmtd,
376
+ cls._get_algo_wrapped(
377
+ targetcls, targetmtd, hooks, prehooks, reenter
378
+ ),
379
+ )
380
+ return targetcls
381
+
382
+ @classmethod
383
+ @_clsmtd_mixin_locked
384
+ def mixin_execute_overwrite(cls):
385
+ return cls._MIXIN_EXECUTE_OVERWRITE
386
+
387
+ @classmethod
388
+ def mixin_execute_companion(cls):
389
+ """Find on which class "super" should be called (if_MIXIN_EXECUTE_OVERWRITE is used)."""
390
+ comp = getattr(cls, "_algo_meta_execute_companion", ())
391
+ if not comp:
392
+ raise RuntimeError("unable to find a suitable companion class")
393
+ return comp
394
+
395
+
396
+ class AlgoComponentMpiDecoMixin(AlgoComponentDecoMixin):
397
+ """
398
+ This is the base class for Mixin class targeting :class:`Parallel`
399
+ classes.
400
+
401
+ It inherits all the behaviour of the :class:`AlgoComponentDecoMixin` base
402
+ class. But in addition, it allows to decorate additional :class:`Parallel`'s
403
+ methods using the following class variables:
404
+
405
+ * :data:`_MIXIN_MPIBINS_HOOKS`: Tuple of methods that will be
406
+ executed after the original ``_bootstrap_mpibins_hack`` method. Such
407
+ methods will receive five arguments (``self`` set aside):
408
+
409
+ * The list of :class:`mpitools.MpiBinaryDescription` objects returned
410
+ by the original ``_bootstrap_mpibins_hack`` method;
411
+ * The list of :class:`mpitools.MpiBinaryDescription` objects as
412
+ provided by the first caller;
413
+ * The list of binary ResourceHandlers as provided to the ``run``
414
+ method;
415
+ * A dictionary of options as provided to the ``run`` method;
416
+ * A boolean indicating if an MPI envelope is provided by the user.
417
+
418
+ * :data:`_MIXIN_MPIENVELOPE_HOOKS`: Tuple of methods that will be
419
+ executed after the original ``_bootstrap_mpienvelope_hack`` method. Such
420
+ methods will receive four arguments (``self`` set aside):
421
+
422
+ * The list of dictionaries describing the envelope returned
423
+ by the original``_bootstrap_mpienvelope_hack`` method;
424
+ * The list of dictionaries describing the envelope as
425
+ provided by the first caller;
426
+ * The list of binary ResourceHandlers as provided to the ``run``
427
+ method;
428
+ * A dictionary of options as provided to the ``run`` method;
429
+ * The :class:`mpitools.MpiTool` that is used to generate the
430
+ MPI command line
431
+
432
+ """
433
+
434
+ _MIXIN_MPIBINS_HOOKS = ()
435
+ _MIXIN_MPIENVELOPE_HOOKS = ()
436
+ _MIXIN_MPIENVELOPE_POSTHOOKS = ()
437
+
438
+ @classmethod
439
+ @_clsmtd_mixin_locked
440
+ def mixin_algo_deco(cls, targetcls):
441
+ """
442
+ Applies all the necessary decorators to the **targetcls**
443
+ :class:`AlgoComponent` class.
444
+ """
445
+ targetcls = AlgoComponentDecoMixin.mixin_algo_deco(targetcls)
446
+ if not issubclass(targetcls, Parallel):
447
+ raise RuntimeError(
448
+ "This class can only be mixed in Parallel classes."
449
+ )
450
+ for targetmtd, hooks, prehooks, reenter in [
451
+ ("_bootstrap_mpibins_hack", cls._MIXIN_MPIBINS_HOOKS, (), True),
452
+ (
453
+ "_bootstrap_mpienvelope_hack",
454
+ cls._MIXIN_MPIENVELOPE_HOOKS,
455
+ (),
456
+ True,
457
+ ),
458
+ (
459
+ "_bootstrap_mpienvelope_posthack",
460
+ cls._MIXIN_MPIENVELOPE_POSTHOOKS,
461
+ (),
462
+ True,
463
+ ),
464
+ ]:
465
+ if hooks or prehooks:
466
+ setattr(
467
+ targetcls,
468
+ targetmtd,
469
+ cls._get_algo_wrapped(
470
+ targetcls, targetmtd, hooks, prehooks, reenter
471
+ ),
472
+ )
473
+ return targetcls
474
+
475
+
476
+ class AlgoComponentMeta(footprints.FootprintBaseMeta):
477
+ """Meta class for building :class:`AlgoComponent` classes.
478
+
479
+ In addition of performing footprints' usual stuff, it processes mixin classes
480
+ that derives from the :class:`AlgoComponentDecoMixin` class. See the
481
+ documentation of this class for more details.
482
+ """
483
+
484
+ def __new__(cls, n, b, d):
485
+ # Mixin candidates: a mixin must only be dealt with once hence the
486
+ # condition on issubclass(base, AlgoComponent)
487
+ candidates = [
488
+ base
489
+ for base in b
490
+ if (
491
+ issubclass(base, AlgoComponentDecoMixin)
492
+ and not issubclass(base, AlgoComponent)
493
+ )
494
+ ]
495
+ # Tweak footprints
496
+ todobases = [base for base in candidates if base.MIXIN_AUTO_FPTWEAK]
497
+ if todobases:
498
+ fplocal = d.get("_footprint", list())
499
+ if not isinstance(fplocal, list):
500
+ fplocal = [
501
+ fplocal,
502
+ ]
503
+ for base in todobases:
504
+ base.mixin_tweak_footprint(fplocal)
505
+ d["_footprint"] = fplocal
506
+ # Overwrite the execute method...
507
+ todobases_exc = [
508
+ base
509
+ for base in candidates
510
+ if base.mixin_execute_overwrite() is not None
511
+ ]
512
+ if len(todobases_exc) > 1:
513
+ raise RuntimeError(
514
+ "Cannot overwrite < execute > multiple times: {:s}".format(
515
+ ",".join([base.__name__ for base in todobases_exc])
516
+ )
517
+ )
518
+ if todobases_exc:
519
+ if "execute" in d:
520
+ raise RuntimeError(
521
+ "< execute > is already defined in the target class: cannot proceed"
522
+ )
523
+ d["execute"] = todobases_exc[0].mixin_execute_overwrite()
524
+ # Create the class as usual
525
+ fpcls = super().__new__(cls, n, b, d)
526
+ if todobases_exc:
527
+ setattr(fpcls, "_algo_meta_execute_companion", fpcls)
528
+ # Apply decorators
529
+ todobases = [base for base in candidates if base.MIXIN_AUTO_DECO]
530
+ for base in reversed(todobases):
531
+ base.mixin_algo_deco(fpcls)
532
+ return fpcls
533
+
534
+
535
+ class AlgoComponent(footprints.FootprintBase, metaclass=AlgoComponentMeta):
536
+ """Component in charge of any kind of processing."""
537
+
538
+ _SERVERSYNC_RAISEONEXIT = True
539
+ _SERVERSYNC_RUNONSTARTUP = True
540
+ _SERVERSYNC_STOPONEXIT = True
541
+
542
+ _abstract = True
543
+ _collector = ("component",)
544
+ _footprint = dict(
545
+ info="Abstract algo component",
546
+ attr=dict(
547
+ engine=dict(
548
+ info="The way the executable should be run.",
549
+ values=[
550
+ "algo",
551
+ ],
552
+ ),
553
+ flyput=dict(
554
+ info="Activate a background job in charge off on the fly processing.",
555
+ type=bool,
556
+ optional=True,
557
+ default=False,
558
+ access="rwx",
559
+ doc_visibility=footprints.doc.visibility.GURU,
560
+ doc_zorder=-99,
561
+ ),
562
+ flypoll=dict(
563
+ info="The system method called by the flyput background job.",
564
+ optional=True,
565
+ default="io_poll",
566
+ access="rwx",
567
+ doc_visibility=footprints.doc.visibility.GURU,
568
+ doc_zorder=-99,
569
+ ),
570
+ flyargs=dict(
571
+ info="Arguments for the *flypoll* method.",
572
+ type=footprints.FPTuple,
573
+ optional=True,
574
+ default=footprints.FPTuple(),
575
+ doc_visibility=footprints.doc.visibility.GURU,
576
+ doc_zorder=-99,
577
+ ),
578
+ flymapping=dict(
579
+ info="Allow renaming of output files during on the fly processing.",
580
+ optional=True,
581
+ default=False,
582
+ access="rwx",
583
+ doc_visibility=footprints.doc.visibility.GURU,
584
+ doc_zorder=-99,
585
+ ),
586
+ timeout=dict(
587
+ info="Default timeout (in sec.) used when waiting for an expected resource.",
588
+ type=int,
589
+ optional=True,
590
+ default=180,
591
+ doc_zorder=-50,
592
+ ),
593
+ server_run=dict(
594
+ info="Run the executable as a server.",
595
+ type=bool,
596
+ optional=True,
597
+ values=[False],
598
+ default=False,
599
+ access="rwx",
600
+ doc_visibility=footprints.doc.visibility.ADVANCED,
601
+ ),
602
+ serversync_method=dict(
603
+ info="The method that is used to synchronise with the server.",
604
+ optional=True,
605
+ doc_visibility=footprints.doc.visibility.GURU,
606
+ ),
607
+ serversync_medium=dict(
608
+ info="The medium that is used to synchronise with the server.",
609
+ optional=True,
610
+ doc_visibility=footprints.doc.visibility.GURU,
611
+ ),
612
+ extendpypath=dict(
613
+ info="The list of things to be prepended in the python's path.",
614
+ type=footprints.FPList,
615
+ default=footprints.FPList([]),
616
+ optional=True,
617
+ ),
618
+ ),
619
+ )
620
+
621
+ def __init__(self, *args, **kw):
622
+ logger.debug("Algo component init %s", self.__class__)
623
+ self._fslog = list()
624
+ self._promises = None
625
+ self._expected = None
626
+ self._delayed_excs = list()
627
+ self._server_synctool = None
628
+ self._server_process = None
629
+ super().__init__(*args, **kw)
630
+
631
+ @property
632
+ def realkind(self):
633
+ """Default kind is ``algo``."""
634
+ return "algo"
635
+
636
+ @property
637
+ def fslog(self):
638
+ """Changes on the filesystem during the execution."""
639
+ return self._fslog
640
+
641
+ def fstag(self):
642
+ """Defines a tag specific to the current algo component."""
643
+ return "-".join((self.realkind, self.engine))
644
+
645
+ def fsstamp(self, opts):
646
+ """Ask the current context to put a stamp on file system."""
647
+ self.context.fstrack_stamp(tag=self.fstag())
648
+
649
+ def fscheck(self, opts):
650
+ """Ask the current context to check changes on file system since last stamp."""
651
+ self._fslog.append(self.context.fstrack_check(tag=self.fstag()))
652
+
653
+ @property
654
+ def promises(self):
655
+ """Build and return list of actual promises of the current component."""
656
+ if self._promises is None:
657
+ self._promises = [
658
+ x
659
+ for x in self.context.sequence.outputs()
660
+ if x.rh.provider.expected
661
+ ]
662
+ return self._promises
663
+
664
+ @property
665
+ def expected_resources(self):
666
+ """Return the list of really expected inputs."""
667
+ if self._expected is None:
668
+ self._expected = [
669
+ x
670
+ for x in self.context.sequence.effective_inputs()
671
+ if x.rh.is_expected()
672
+ ]
673
+ return self._expected
674
+
675
+ def delayed_exception_add(self, exc, traceback=True):
676
+ """Store the exception so that it will be handled at the end of the run."""
677
+ logger.error("An exception is delayed")
678
+ if traceback:
679
+ (exc_type, exc_value, exc_traceback) = sys.exc_info()
680
+ print("Exception type: {!s}".format(exc_type))
681
+ print("Exception info: {!s}".format(exc_value))
682
+ print("Traceback:")
683
+ print("\n".join(py_traceback.format_tb(exc_traceback)))
684
+ self._delayed_excs.append(exc)
685
+
686
+ def algoassert(self, assertion, msg=""):
687
+ if not assertion:
688
+ raise AlgoComponentAssertionError(msg)
689
+
690
+ def grab(self, sec, comment="resource", sleep=10, timeout=None):
691
+ """Wait for a given resource and get it if expected."""
692
+ local = sec.rh.container.localpath()
693
+ self.system.header("Wait for " + comment + " ... [" + local + "]")
694
+ if timeout is None:
695
+ timeout = self.timeout
696
+ if sec.rh.wait(timeout=timeout, sleep=sleep):
697
+ if sec.rh.is_expected():
698
+ sec.get(incache=True)
699
+ elif sec.fatal:
700
+ logger.critical("Missing expected resource <%s>", local)
701
+ raise ValueError("Could not get " + local)
702
+ else:
703
+ logger.error("Missing expected resource <%s>", local)
704
+
705
+ def export(self, packenv):
706
+ """Export environment variables in given pack."""
707
+ for k, v in config.from_config(section=packenv).items():
708
+ if k not in self.env:
709
+ logger.info("Setting %s env %s = %s", packenv.upper(), k, v)
710
+ self.env[k] = v
711
+
712
+ def prepare(self, rh, opts):
713
+ """Set some defaults env values."""
714
+ if config.is_defined(section="env"):
715
+ self.export("env")
716
+
717
+ def absexcutable(self, xfile):
718
+ """Retuns the absolute pathname of the ``xfile`` executable."""
719
+ absx = self.system.path.abspath(xfile)
720
+ return absx
721
+
722
+ def flyput_method(self):
723
+ """Check out what could be a valid io_poll command."""
724
+ return getattr(
725
+ self, "io_poll_method", getattr(self.system, self.flypoll, None)
726
+ )
727
+
728
+ def flyput_args(self):
729
+ """Return actual io_poll prefixes."""
730
+ return getattr(self, "io_poll_args", tuple(self.flyargs))
731
+
732
+ def flyput_kwargs(self):
733
+ """Return actual io_poll prefixes."""
734
+ return getattr(self, "io_poll_kwargs", dict())
735
+
736
+ def flyput_check(self):
737
+ """Check default args for io_poll command."""
738
+ actual_args = list()
739
+ if self.flymapping:
740
+ # No checks when mapping is activated
741
+ return self.flyput_args()
742
+ else:
743
+ for arg in self.flyput_args():
744
+ logger.info("Check arg <%s>", arg)
745
+ if any(
746
+ [
747
+ x.rh.container.basename.startswith(arg)
748
+ for x in self.promises
749
+ ]
750
+ ):
751
+ logger.info(
752
+ "Match some promise %s",
753
+ str(
754
+ [
755
+ x.rh.container.basename
756
+ for x in self.promises
757
+ if x.rh.container.basename.startswith(arg)
758
+ ]
759
+ ),
760
+ )
761
+ actual_args.append(arg)
762
+ else:
763
+ logger.info(
764
+ "Do not match any promise %s",
765
+ str([x.rh.container.basename for x in self.promises]),
766
+ )
767
+ return actual_args
768
+
769
+ def flyput_sleep(self):
770
+ """Return a sleeping time in seconds between io_poll commands."""
771
+ return getattr(
772
+ self, "io_poll_sleep", self.env.get("IO_POLL_SLEEP", 20)
773
+ )
774
+
775
+ def flyput_outputmapping(self, item):
776
+ """Map output to another filename."""
777
+ return item, "unknown"
778
+
779
+ def _flyput_job_internal_search(
780
+ self, io_poll_method, io_poll_args, io_poll_kwargs
781
+ ):
782
+ data = list()
783
+ for arg in io_poll_args:
784
+ logger.info("Polling check arg %s", arg)
785
+ rc = io_poll_method(arg, **io_poll_kwargs)
786
+ try:
787
+ data.extend(rc.result)
788
+ except AttributeError:
789
+ data.extend(rc)
790
+ data = [x for x in data if x]
791
+ logger.info("Polling retrieved data %s", str(data))
792
+ return data
793
+
794
+ def _flyput_job_internal_put(self, data):
795
+ for thisdata in data:
796
+ if self.flymapping:
797
+ mappeddata, mappedfmt = self.flyput_outputmapping(thisdata)
798
+ if not mappeddata:
799
+ raise AlgoComponentError(
800
+ "The mapping method failed for {:s}.".format(thisdata)
801
+ )
802
+ if thisdata != mappeddata:
803
+ logger.info(
804
+ "Linking <%s> to <%s> (fmt=%s) before put",
805
+ thisdata,
806
+ mappeddata,
807
+ mappedfmt,
808
+ )
809
+ self.system.cp(
810
+ thisdata, mappeddata, intent="in", fmt=mappedfmt
811
+ )
812
+ else:
813
+ mappeddata = thisdata
814
+ candidates = [
815
+ x
816
+ for x in self.promises
817
+ if x.rh.container.abspath
818
+ == self.system.path.abspath(mappeddata)
819
+ ]
820
+ if candidates:
821
+ logger.info("Polled data is promised <%s>", mappeddata)
822
+ bingo = candidates.pop()
823
+ bingo.put(incache=True)
824
+ else:
825
+ logger.warning("Polled data not promised <%s>", mappeddata)
826
+
827
+ def flyput_job(
828
+ self,
829
+ io_poll_method,
830
+ io_poll_args,
831
+ io_poll_kwargs,
832
+ event_complete,
833
+ event_free,
834
+ queue_context,
835
+ ):
836
+ """Poll new data resources."""
837
+ logger.info("Polling with method %s", str(io_poll_method))
838
+ logger.info("Polling with args %s", str(io_poll_args))
839
+
840
+ time_sleep = self.flyput_sleep()
841
+ redo = True
842
+
843
+ # Start recording the changes in the current context
844
+ ctxrec = self.context.get_recorder()
845
+
846
+ while redo and not event_complete.is_set():
847
+ event_free.clear()
848
+ try:
849
+ data = self._flyput_job_internal_search(
850
+ io_poll_method, io_poll_args, io_poll_kwargs
851
+ )
852
+ self._flyput_job_internal_put(data)
853
+ except Exception as trouble:
854
+ logger.error(
855
+ "Polling trouble: %s. %s",
856
+ str(trouble),
857
+ py_traceback.format_exc(),
858
+ )
859
+ redo = False
860
+ finally:
861
+ event_free.set()
862
+ if redo and not data and not event_complete.is_set():
863
+ logger.info("Get asleep for %d seconds...", time_sleep)
864
+ self.system.sleep(time_sleep)
865
+
866
+ # Stop recording and send back the results
867
+ ctxrec.unregister()
868
+ logger.info("Sending the Context recorder to the master process.")
869
+ queue_context.put(ctxrec)
870
+ queue_context.close()
871
+
872
+ if redo:
873
+ logger.info("Polling exit on complete event")
874
+ else:
875
+ logger.warning("Polling exit on abort")
876
+
877
+ def flyput_begin(self):
878
+ """Launch a co-process to handle promises."""
879
+
880
+ nope = (None, None, None, None)
881
+ if not self.flyput:
882
+ return nope
883
+
884
+ sh = self.system
885
+ sh.subtitle("On the fly - Begin")
886
+
887
+ if not self.promises:
888
+ logger.info("No promise, no co-process")
889
+ return nope
890
+
891
+ # Find out a polling method
892
+ io_poll_method = self.flyput_method()
893
+ if not io_poll_method:
894
+ logger.error(
895
+ "No method or shell function defined for polling data"
896
+ )
897
+ return nope
898
+
899
+ # Be sure that some default args could match local promises names
900
+ io_poll_args = self.flyput_check()
901
+ if not io_poll_args:
902
+ logger.error("Could not check default arguments for polling data")
903
+ return nope
904
+
905
+ # Additional named attributes
906
+ io_poll_kwargs = self.flyput_kwargs()
907
+
908
+ # Define events for a nice termination
909
+ event_stop = multiprocessing.Event()
910
+ event_free = multiprocessing.Event()
911
+ queue_ctx = multiprocessing.Queue()
912
+
913
+ p_io = multiprocessing.Process(
914
+ name=self.footprint_clsname(),
915
+ target=self.flyput_job,
916
+ args=(
917
+ io_poll_method,
918
+ io_poll_args,
919
+ io_poll_kwargs,
920
+ event_stop,
921
+ event_free,
922
+ queue_ctx,
923
+ ),
924
+ )
925
+
926
+ # The co-process is started
927
+ p_io.start()
928
+
929
+ return (p_io, event_stop, event_free, queue_ctx)
930
+
931
+ def manual_flypolling(self):
932
+ """Call the flyput method and returns the list of newly available files."""
933
+ # Find out a polling method
934
+ io_poll_method = self.flyput_method()
935
+ if not io_poll_method:
936
+ raise AlgoComponentError("Unable to find an io_poll_method")
937
+ # Find out some polling prefixes
938
+ io_poll_args = self.flyput_check()
939
+ if not io_poll_args:
940
+ raise AlgoComponentError("Unable to find an io_poll_args")
941
+ # Additional named attributes
942
+ io_poll_kwargs = self.flyput_kwargs()
943
+ # Starting polling each of the prefixes
944
+ return self._flyput_job_internal_search(
945
+ io_poll_method, io_poll_args, io_poll_kwargs
946
+ )
947
+
948
+ def manual_flypolling_job(self):
949
+ """Call the flyput method and deal with promised files."""
950
+ data = self.manual_flypolling()
951
+ self._flyput_job_internal_put(data)
952
+
953
+ def flyput_end(self, p_io, e_complete, e_free, queue_ctx):
954
+ """Wait for the co-process in charge of promises."""
955
+ e_complete.set()
956
+ logger.info("Waiting for polling process... <%s>", p_io.pid)
957
+ t0 = date.now()
958
+ e_free.wait(60)
959
+ # Get the Queue and update the context
960
+ time_sleep = self.flyput_sleep()
961
+ try:
962
+ # allow 5 sec to put data into queue (it should be more than enough)
963
+ ctxrec = queue_ctx.get(block=True, timeout=time_sleep + 5)
964
+ except queue.Empty:
965
+ logger.warning("Impossible to get the Context recorder")
966
+ ctxrec = None
967
+ finally:
968
+ queue_ctx.close()
969
+ if ctxrec is not None:
970
+ ctxrec.replay_in(self.context)
971
+ p_io.join(30)
972
+ t1 = date.now()
973
+ waiting = t1 - t0
974
+ logger.info(
975
+ "Waiting for polling process took %f seconds",
976
+ waiting.total_seconds(),
977
+ )
978
+ if p_io.is_alive():
979
+ logger.warning("Force termination of polling process")
980
+ p_io.terminate()
981
+ logger.info("Polling still alive ? %s", str(p_io.is_alive()))
982
+ return not p_io.is_alive()
983
+
984
+ def server_begin(self, rh, opts):
985
+ """Start a subprocess and run the server in it."""
986
+ self._server_event = multiprocessing.Event()
987
+ self._server_process = multiprocessing.Process(
988
+ name=self.footprint_clsname(),
989
+ target=self.server_job,
990
+ args=(rh, opts),
991
+ )
992
+ self._server_process.start()
993
+
994
+ def server_job(self, rh, opts):
995
+ """Actually run the server and catch all Exceptions.
996
+
997
+ If the server crashes, is killed or whatever, the Exception is displayed
998
+ and the appropriate Event is set.
999
+ """
1000
+ self.system.signal_intercept_on()
1001
+ try:
1002
+ self.execute_single(rh, opts)
1003
+ except Exception:
1004
+ (exc_type, exc_value, exc_traceback) = sys.exc_info()
1005
+ print("Exception type: {!s}".format(exc_type))
1006
+ print("Exception info: {!s}".format(exc_value))
1007
+ print("Traceback:")
1008
+ print("\n".join(py_traceback.format_tb(exc_traceback)))
1009
+ # Alert the main process of the error
1010
+ self._server_event.set()
1011
+
1012
+ def server_alive(self):
1013
+ """Is the server still running ?"""
1014
+ return (
1015
+ self._server_process is not None
1016
+ and self._server_process.is_alive()
1017
+ )
1018
+
1019
+ def server_end(self):
1020
+ """End the server.
1021
+
1022
+ A first attempt is made to terminate it nicely. If it doesn't work,
1023
+ a SIGTERM is sent.
1024
+ """
1025
+ rc = False
1026
+ # This test should always succeed...
1027
+ if (
1028
+ self._server_synctool is not None
1029
+ and self._server_process is not None
1030
+ ):
1031
+ # Is the process still running ?
1032
+ if self._server_process.is_alive():
1033
+ # Try to stop it nicely
1034
+ if (
1035
+ self._SERVERSYNC_STOPONEXIT
1036
+ and self._server_synctool.trigger_stop()
1037
+ ):
1038
+ t0 = date.now()
1039
+ self._server_process.join(30)
1040
+ waiting = date.now() - t0
1041
+ logger.info(
1042
+ "Waiting for the server to stop took %f seconds",
1043
+ waiting.total_seconds(),
1044
+ )
1045
+ rc = not self._server_event.is_set()
1046
+ # Be less nice if needed...
1047
+ if (
1048
+ not self._SERVERSYNC_STOPONEXIT
1049
+ ) or self._server_process.is_alive():
1050
+ logger.warning("Force termination of the server process")
1051
+ self._server_process.terminate()
1052
+ self.system.sleep(
1053
+ 1
1054
+ ) # Allow some time for the process to terminate
1055
+ if not self._SERVERSYNC_STOPONEXIT:
1056
+ rc = False
1057
+ else:
1058
+ rc = not self._server_event.is_set()
1059
+ logger.info(
1060
+ "Server still alive ? %s", str(self._server_process.is_alive())
1061
+ )
1062
+ # We are done with the server
1063
+ self._server_synctool = None
1064
+ self._server_process = None
1065
+ del self._server_event
1066
+ # Check the rc
1067
+ if not rc:
1068
+ raise AlgoComponentError("The server process ended badly.")
1069
+ return rc
1070
+
1071
+ def spawn_pre_dirlisting(self):
1072
+ """Print a directory listing just before run."""
1073
+ self.system.subtitle(
1074
+ "{:s} : directory listing (pre-execution)".format(self.realkind)
1075
+ )
1076
+ self.system.dir(output=False, fatal=False)
1077
+
1078
+ def spawn_hook(self):
1079
+ """Last chance to say something before execution."""
1080
+ pass
1081
+
1082
+ def spawn(self, args, opts, stdin=None):
1083
+ """
1084
+ Spawn in the current system the command as defined in raw ``args``.
1085
+
1086
+ The followings environment variables could drive part of the execution:
1087
+
1088
+ * VORTEX_DEBUG_ENV : dump current environment before spawn
1089
+ """
1090
+ sh = self.system
1091
+
1092
+ if self.env.true("vortex_debug_env"):
1093
+ sh.subtitle(
1094
+ "{:s} : dump environment (os bound: {!s})".format(
1095
+ self.realkind, self.env.osbound()
1096
+ )
1097
+ )
1098
+ self.env.osdump()
1099
+
1100
+ # On-the-fly coprocessing initialisation
1101
+ p_io, e_complete, e_free, q_ctx = self.flyput_begin()
1102
+
1103
+ sh.remove("core")
1104
+ sh.softlink("/dev/null", "core")
1105
+ self.spawn_hook()
1106
+ self.target.spawn_hook(sh)
1107
+ self.spawn_pre_dirlisting()
1108
+ sh.subtitle("{:s} : start execution".format(self.realkind))
1109
+ try:
1110
+ sh.spawn(
1111
+ args, output=False, stdin=stdin, fatal=opts.get("fatal", True)
1112
+ )
1113
+ finally:
1114
+ # On-the-fly coprocessing cleaning
1115
+ if p_io:
1116
+ self.flyput_end(p_io, e_complete, e_free, q_ctx)
1117
+
1118
+ def spawn_command_options(self):
1119
+ """Prepare options for the resource's command line."""
1120
+ return dict()
1121
+
1122
+ def spawn_command_line(self, rh):
1123
+ """Split the shell command line of the resource to be run."""
1124
+ opts = self.spawn_command_options()
1125
+ return shlex.split(rh.resource.command_line(**opts))
1126
+
1127
+ def spawn_stdin_options(self):
1128
+ """Prepare options for the resource's stdin generator."""
1129
+ return dict()
1130
+
1131
+ def spawn_stdin(self, rh):
1132
+ """Generate the stdin File-Like object of the resource to be run."""
1133
+ opts = self.spawn_stdin_options()
1134
+ stdin_text = rh.resource.stdin_text(**opts)
1135
+ if stdin_text is not None:
1136
+ plocale = locale.getlocale()[1] or "ascii"
1137
+ tmpfh = tempfile.TemporaryFile(dir=self.system.pwd(), mode="w+b")
1138
+ if isinstance(stdin_text, str):
1139
+ tmpfh.write(stdin_text.encode(plocale))
1140
+ else:
1141
+ tmpfh.write(stdin_text)
1142
+ tmpfh.seek(0)
1143
+ return tmpfh
1144
+ else:
1145
+ return None
1146
+
1147
+ def execute_single(self, rh, opts):
1148
+ """Abstract method.
1149
+
1150
+ When server_run is True, this method is used to start the server.
1151
+ Otherwise, this method is called by each :meth:`execute` call.
1152
+ """
1153
+ pass
1154
+
1155
+ def execute(self, rh, opts):
1156
+ """Abstract method."""
1157
+ if self.server_run:
1158
+ # First time here ?
1159
+ if self._server_synctool is None:
1160
+ if self.serversync_method is None:
1161
+ raise ValueError("The serversync_method must be provided.")
1162
+ self._server_synctool = footprints.proxy.serversynctool(
1163
+ method=self.serversync_method,
1164
+ medium=self.serversync_medium,
1165
+ raiseonexit=self._SERVERSYNC_RAISEONEXIT,
1166
+ )
1167
+ self._server_synctool.set_servercheck_callback(
1168
+ self.server_alive
1169
+ )
1170
+ self.server_begin(rh, opts)
1171
+ # Wait for the first request
1172
+ self._server_synctool.trigger_wait()
1173
+ if self._SERVERSYNC_RUNONSTARTUP:
1174
+ self._server_synctool.trigger_run()
1175
+ else:
1176
+ # Acknowledge that we are ready and wait for the next request
1177
+ self._server_synctool.trigger_run()
1178
+ else:
1179
+ self.execute_single(rh, opts)
1180
+
1181
+ def fail_execute(self, e, rh, kw):
1182
+ """This method is called if :meth:`execute` raise an exception."""
1183
+ pass
1184
+
1185
+ def execute_finalise(self, opts):
1186
+ """Abstract method.
1187
+
1188
+ This method is called inconditionaly when :meth:`execute` exits (even
1189
+ if an Exception was raised).
1190
+ """
1191
+ if self.server_run:
1192
+ self.server_end()
1193
+
1194
+ def postfix_post_dirlisting(self):
1195
+ self.system.subtitle(
1196
+ "{:s} : directory listing (post-run)".format(self.realkind)
1197
+ )
1198
+ self.system.dir(output=False, fatal=False)
1199
+
1200
+ def postfix(self, rh, opts):
1201
+ """Some basic informations."""
1202
+ self.postfix_post_dirlisting()
1203
+
1204
+ def dumplog(self, opts):
1205
+ """Dump to local file the internal log of the current algo component."""
1206
+ self.system.pickle_dump(self.fslog, "log." + self.fstag())
1207
+
1208
+ def delayed_exceptions(self, opts):
1209
+ """Gather all the delayed exceptions and raises one if necessary."""
1210
+ if len(self._delayed_excs) > 0:
1211
+ excstmp = self._delayed_excs
1212
+ self._delayed_excs = list()
1213
+ raise DelayedAlgoComponentError(excstmp)
1214
+
1215
+ def valid_executable(self, rh):
1216
+ """
1217
+ Return a boolean value according to the effective executable nature
1218
+ of the resource handler provided.
1219
+ """
1220
+ return True
1221
+
1222
+ def abortfabrik(self, step, msg):
1223
+ """A shortcut to avoid next steps of the run."""
1224
+
1225
+ def fastexit(self, *args, **kw):
1226
+ logger.warning(
1227
+ "Run <%s> skipped because abort occurred [%s]", step, msg
1228
+ )
1229
+
1230
+ return fastexit
1231
+
1232
+ def abort(self, msg="Not documented"):
1233
+ """A shortcut to avoid next steps of the run."""
1234
+ for step in ("prepare", "execute", "postfix"):
1235
+ setattr(self, step, self.abortfabrik(step, msg))
1236
+
1237
+ def run(self, rh=None, **kw):
1238
+ """Sequence for execution : prepare / execute / postfix."""
1239
+ self._status = True
1240
+
1241
+ # Get instance shorcuts to context and system objects
1242
+ self.ticket = vortex.sessions.current()
1243
+ self.context = self.ticket.context
1244
+ self.system = self.context.system
1245
+ self.target = kw.pop("target", None)
1246
+ if self.target is None:
1247
+ self.target = self.system.default_target
1248
+
1249
+ # Before trying to do anything, check the executable
1250
+ if not self.valid_executable(rh):
1251
+ logger.warning(
1252
+ "Resource %s is not a valid executable", rh.resource
1253
+ )
1254
+ return False
1255
+
1256
+ # A cloned environment will be bound to the OS
1257
+ self.env = self.context.env.clone()
1258
+ with self.env:
1259
+ # The actual "run" recipe
1260
+ self.prepare(rh, kw) # 1
1261
+ self.fsstamp(kw) # 2
1262
+ try:
1263
+ self.execute(rh, kw) # 3
1264
+ except Exception as e:
1265
+ self.fail_execute(e, rh, kw) # 3.1
1266
+ raise
1267
+ finally:
1268
+ self.execute_finalise(kw) # 3.2
1269
+ self.fscheck(kw) # 4
1270
+ self.postfix(rh, kw) # 5
1271
+ self.dumplog(kw) # 6
1272
+ self.delayed_exceptions(kw) # 7
1273
+
1274
+ # Free local references
1275
+ self.env = None
1276
+ self.system = None
1277
+
1278
+ return self._status
1279
+
1280
+ def quickview(self, nb=0, indent=0):
1281
+ """Standard glance to objects."""
1282
+ tab = " " * indent
1283
+ print("{}{:02d}. {:s}".format(tab, nb, repr(self)))
1284
+ for subobj in ("kind", "engine", "interpreter"):
1285
+ obj = getattr(self, subobj, None)
1286
+ if obj:
1287
+ print("{} {:s}: {!s}".format(tab, subobj, obj))
1288
+ print()
1289
+
1290
+ def setlink(
1291
+ self,
1292
+ initrole=None,
1293
+ initkind=None,
1294
+ initname=None,
1295
+ inittest=lambda x: True,
1296
+ ):
1297
+ """Set a symbolic link for actual resource playing defined role."""
1298
+ initsec = [
1299
+ x
1300
+ for x in self.context.sequence.effective_inputs(
1301
+ role=initrole, kind=initkind
1302
+ )
1303
+ if inittest(x.rh)
1304
+ ]
1305
+
1306
+ if not initsec:
1307
+ logger.warning(
1308
+ "Could not find logical role %s with kind %s - assuming already renamed",
1309
+ initrole,
1310
+ initkind,
1311
+ )
1312
+
1313
+ if len(initsec) > 1:
1314
+ logger.warning(
1315
+ "More than one role %s with kind %s", initrole, initkind
1316
+ )
1317
+
1318
+ if initname is not None:
1319
+ for l in [x.rh.container.localpath() for x in initsec]:
1320
+ if not self.system.path.exists(initname):
1321
+ self.system.symlink(l, initname)
1322
+ break
1323
+
1324
+ return initsec
1325
+
1326
+
1327
+ class PythonFunction(AlgoComponent):
1328
+ """Execute a function defined in Python module. The function is passed the
1329
+ current :class:`sequence <vortex.layout.dataflow.Sequence>`, as well as a
1330
+ keyword arguments described by attribute ``func_kwargs``. Example:
1331
+
1332
+ .. code-block:: python
1333
+
1334
+ >>> exe = toolbox.executable(
1335
+ ... role = 'Script',
1336
+ ... format = 'ascii',
1337
+ ... hostname = 'localhost',
1338
+ ... kind = 'script',
1339
+ ... language = 'python',
1340
+ ... local = 'module.py',
1341
+ ... remote = '/path/to/module.py',
1342
+ ... tube = 'file',
1343
+ ... )
1344
+ >>> tbalgo = toolbox.algo(
1345
+ ... engine="function",
1346
+ ... func_name="my_plugin_entry_point_function",
1347
+ ... func_kwargs={ntasks: 35, subnproc: 4},
1348
+ ... )
1349
+ >>> tbalgo.run(exe[0])
1350
+
1351
+ .. code-block:: python
1352
+
1353
+ # /path/to/module.py
1354
+ # ...
1355
+ def my_plugin_entry_point_function(
1356
+ sequence, ntasks, subnproc,
1357
+ ):
1358
+ for input in sequence.effective_inputs(role=gridpoint):
1359
+ # ...
1360
+ """
1361
+
1362
+ _footprint = dict(
1363
+ info="Execute a Python function in a given module",
1364
+ attr=dict(
1365
+ engine=dict(values=["function"]),
1366
+ func_name=dict(info="The function's name"),
1367
+ func_kwargs=dict(
1368
+ info=(
1369
+ "A dictionary containing the function's keyword arguments"
1370
+ ),
1371
+ type=footprints.FPDict,
1372
+ default=footprints.FPDict({}),
1373
+ optional=True,
1374
+ ),
1375
+ ),
1376
+ )
1377
+
1378
+ def prepare(self, rh, opts):
1379
+ spec = importlib.util.spec_from_file_location(
1380
+ name="module", location=rh.container.localpath()
1381
+ )
1382
+ mod = importlib.util.module_from_spec(spec)
1383
+ sys.path.extend(self.extendpypath)
1384
+ try:
1385
+ spec.loader.exec_module(mod)
1386
+ except AttributeError:
1387
+ raise AttributeError
1388
+ self.func = getattr(mod, self.func_name)
1389
+
1390
+ def execute(self, rh, opts):
1391
+ self.func(
1392
+ self.context.sequence,
1393
+ **self.func_kwargs,
1394
+ )
1395
+
1396
+ def execute_finalise(self, opts):
1397
+ for p in self.extendpypath:
1398
+ sys.path.remove(p)
1399
+
1400
+
1401
+ class ExecutableAlgoComponent(AlgoComponent):
1402
+ """Component in charge of running executable resources."""
1403
+
1404
+ _abstract = True
1405
+
1406
+ def valid_executable(self, rh):
1407
+ """
1408
+ Return a boolean value according to the effective executable nature
1409
+ of the resource handler provided.
1410
+ """
1411
+ return rh is not None
1412
+
1413
+
1414
+ class xExecutableAlgoComponent(ExecutableAlgoComponent):
1415
+ """Component in charge of running executable resources."""
1416
+
1417
+ _abstract = True
1418
+
1419
+ def valid_executable(self, rh):
1420
+ """
1421
+ Return a boolean value according to the effective executable nature
1422
+ of the resource handler provided.
1423
+ """
1424
+ rc = super().valid_executable(rh)
1425
+ if rc:
1426
+ # Ensure that the input file is executable
1427
+ xrh = (
1428
+ rh
1429
+ if isinstance(rh, (list, tuple))
1430
+ else [
1431
+ rh,
1432
+ ]
1433
+ )
1434
+ for arh in xrh:
1435
+ self.system.xperm(arh.container.localpath(), force=True)
1436
+ return rc
1437
+
1438
+
1439
+ class TaylorRun(AlgoComponent):
1440
+ """
1441
+ Run any taylorism Worker in the current environment.
1442
+
1443
+ This abstract class includes helpers to use the taylorism package in order
1444
+ to introduce an external parallelisation. It is designed to work well with a
1445
+ taylorism Worker class that inherits from
1446
+ :class:`vortex.tools.parallelism.TaylorVortexWorker`.
1447
+ """
1448
+
1449
+ _abstract = True
1450
+ _footprint = dict(
1451
+ info="Abstract algo component based on the taylorism package.",
1452
+ attr=dict(
1453
+ kind=dict(),
1454
+ verbose=dict(
1455
+ info="Run in verbose mode",
1456
+ type=bool,
1457
+ default=False,
1458
+ optional=True,
1459
+ doc_zorder=-50,
1460
+ ),
1461
+ ntasks=dict(
1462
+ info="The maximum number of parallel tasks",
1463
+ type=int,
1464
+ default=DelayedEnvValue("VORTEX_SUBMIT_TASKS", 1),
1465
+ optional=True,
1466
+ ),
1467
+ ),
1468
+ )
1469
+
1470
+ def __init__(self, *kargs, **kwargs):
1471
+ super().__init__(*kargs, **kwargs)
1472
+ self._boss = None
1473
+
1474
+ def _default_common_instructions(self, rh, opts):
1475
+ """Create a common instruction dictionary that will be used by the workers."""
1476
+ return dict(kind=self.kind, taskdebug=self.verbose)
1477
+
1478
+ def _default_pre_execute(self, rh, opts):
1479
+ """Various initialisations. In particular it creates the task scheduler (Boss)."""
1480
+ # Start the task scheduler
1481
+ self._boss = Boss(
1482
+ verbose=self.verbose,
1483
+ scheduler=footprints.proxy.scheduler(
1484
+ limit="threads", max_threads=self.ntasks
1485
+ ),
1486
+ )
1487
+ self._boss.make_them_work()
1488
+
1489
+ def _add_instructions(self, common_i, individual_i):
1490
+ """Give a new set of instructions to the Boss."""
1491
+ self._boss.set_instructions(common_i, individual_i)
1492
+
1493
+ def _default_post_execute(self, rh, opts):
1494
+ """Summarise the results of the various tasks that were run."""
1495
+ logger.info(
1496
+ "All the input files were dealt with: now waiting for the parallel processing to finish"
1497
+ )
1498
+ self._boss.wait_till_finished()
1499
+ logger.info(
1500
+ "The parallel processing has finished. here are the results:"
1501
+ )
1502
+ report = self._boss.get_report()
1503
+ prp = ParallelResultParser(self.context)
1504
+ for r in report["workers_report"]:
1505
+ rc = prp(r)
1506
+ if isinstance(rc, Exception):
1507
+ self.delayed_exception_add(rc, traceback=False)
1508
+ rc = False
1509
+ self._default_rc_action(rh, opts, r, rc)
1510
+
1511
+ def _default_rc_action(self, rh, opts, report, rc):
1512
+ """How should we process the return code ?"""
1513
+ if not rc:
1514
+ logger.warning(
1515
+ "Apparently something went sideways with this task (rc=%s).",
1516
+ str(rc),
1517
+ )
1518
+
1519
+ def execute(self, rh, opts):
1520
+ """
1521
+ This should be adapted to your needs...
1522
+
1523
+ A usual sequence is::
1524
+
1525
+ self._default_pre_execute(rh, opts)
1526
+ common_i = self._default_common_instructions(rh, opts)
1527
+ # Update the common instructions
1528
+ common_i.update(dict(someattribute='Toto', ))
1529
+
1530
+ # Your own code here
1531
+
1532
+ # Give some instructions to the boss
1533
+ self._add_instructions(common_i, dict(someattribute=['Toto', ],))
1534
+
1535
+ # Your own code here
1536
+
1537
+ self._default_post_execute(rh, opts)
1538
+
1539
+ """
1540
+ raise NotImplementedError
1541
+
1542
+
1543
+ class Expresso(ExecutableAlgoComponent):
1544
+ """Run a script resource in the good environment."""
1545
+
1546
+ _footprint = dict(
1547
+ info="AlgoComponent that simply runs a script",
1548
+ attr=dict(
1549
+ interpreter=dict(
1550
+ info="The interpreter needed to run the script.",
1551
+ values=["current", "awk", "ksh", "bash", "perl", "python"],
1552
+ ),
1553
+ interpreter_path=dict(
1554
+ info="The interpreter command.",
1555
+ optional=True,
1556
+ ),
1557
+ engine=dict(values=["exec", "launch"]),
1558
+ ),
1559
+ )
1560
+
1561
+ @property
1562
+ def _actual_interpreter(self):
1563
+ """Return the interpreter command."""
1564
+ if self.interpreter == "current":
1565
+ if self.interpreter_path is not None:
1566
+ raise ValueError(
1567
+ "*interpreter=current* and *interpreter_path* attributes are incompatible"
1568
+ )
1569
+ return sys.executable
1570
+ else:
1571
+ if self.interpreter_path is None:
1572
+ return self.interpreter
1573
+ else:
1574
+ if self.system.xperm(self.interpreter_path):
1575
+ return self.interpreter_path
1576
+ else:
1577
+ raise AlgoComponentError(
1578
+ "The '{:s}' interpreter is not executable".format(
1579
+ self.interpreter_path
1580
+ )
1581
+ )
1582
+
1583
+ def _interpreter_args_fix(self, rh, opts):
1584
+ absexec = self.absexcutable(rh.container.localpath())
1585
+ if self.interpreter == "awk":
1586
+ return ["-f", absexec]
1587
+ else:
1588
+ return [
1589
+ absexec,
1590
+ ]
1591
+
1592
+ def execute_single(self, rh, opts):
1593
+ """
1594
+ Run the specified resource handler through the current interpreter,
1595
+ using the resource command_line method as args.
1596
+ """
1597
+ # Generic config
1598
+ args = [
1599
+ self._actual_interpreter,
1600
+ ]
1601
+ args.extend(self._interpreter_args_fix(rh, opts))
1602
+ args.extend(self.spawn_command_line(rh))
1603
+ logger.info("Run script %s", args)
1604
+ rh_stdin = self.spawn_stdin(rh)
1605
+ if rh_stdin is not None:
1606
+ plocale = locale.getlocale()[1] or "ascii"
1607
+ logger.info(
1608
+ "Script stdin:\n%s", rh_stdin.read().decode(plocale, "replace")
1609
+ )
1610
+ rh_stdin.seek(0)
1611
+ # Python path stuff
1612
+ newpypath = ":".join(self.extendpypath)
1613
+ if "pythonpath" in self.env:
1614
+ newpypath += ":{:s}".format(self.env.pythonpath)
1615
+ # launching the program...
1616
+ with self.env.delta_context(pythonpath=newpypath):
1617
+ self.spawn(args, opts, stdin=rh_stdin)
1618
+
1619
+
1620
+ class ParaExpresso(TaylorRun):
1621
+ """
1622
+ Run any script in the current environment.
1623
+
1624
+ This abstract class includes helpers to use the taylorism package in order
1625
+ to introduce an external parallelisation. It is designed to work well with a
1626
+ taylorism Worker class that inherits from
1627
+ :class:`vortex.tools.parallelism.VortexWorkerBlindRun`.
1628
+ """
1629
+
1630
+ _abstract = True
1631
+ _footprint = dict(
1632
+ info="AlgoComponent that simply runs a script using the taylorism package.",
1633
+ attr=dict(
1634
+ interpreter=dict(
1635
+ info="The interpreter needed to run the script.",
1636
+ values=["current", "awk", "ksh", "bash", "perl", "python"],
1637
+ ),
1638
+ engine=dict(values=["exec", "launch"]),
1639
+ interpreter_path=dict(
1640
+ info="The full path to the interpreter.",
1641
+ optional=True,
1642
+ ),
1643
+ extendpypath=dict(
1644
+ info="The list of things to be prepended in the python's path.",
1645
+ type=footprints.FPList,
1646
+ default=footprints.FPList([]),
1647
+ optional=True,
1648
+ ),
1649
+ ),
1650
+ )
1651
+
1652
+ def valid_executable(self, rh):
1653
+ """
1654
+ Return a boolean value according to the effective executable nature
1655
+ of the resource handler provided.
1656
+ """
1657
+ return rh is not None
1658
+
1659
+ def _interpreter_args_fix(self, rh, opts):
1660
+ absexec = self.absexcutable(rh.container.localpath())
1661
+ if self.interpreter == "awk":
1662
+ return ["-f", absexec]
1663
+ else:
1664
+ return [
1665
+ absexec,
1666
+ ]
1667
+
1668
+ def _default_common_instructions(self, rh, opts):
1669
+ """Create a common instruction dictionary that will be used by the workers."""
1670
+ ddict = super()._default_common_instructions(rh, opts)
1671
+ actual_interpreter = (
1672
+ sys.executable
1673
+ if self.interpreter == "current"
1674
+ else self.interpreter
1675
+ )
1676
+ ddict["progname"] = actual_interpreter
1677
+ ddict["progargs"] = footprints.FPList(
1678
+ self._interpreter_args_fix(rh, opts) + self.spawn_command_line(rh)
1679
+ )
1680
+ ddict["progenvdelta"] = footprints.FPDict()
1681
+ # Deal with the python path
1682
+ newpypath = ":".join(self.extendpypath)
1683
+ if "pythonpath" in self.env:
1684
+ self.env.pythonpath += ":{:s}".format(newpypath)
1685
+ if newpypath:
1686
+ ddict["progenvdelta"]["pythonpath"] = newpypath
1687
+ return ddict
1688
+
1689
+
1690
+ class BlindRun(xExecutableAlgoComponent):
1691
+ """
1692
+ Run any executable resource in the current environment. Mandatory argument is:
1693
+ * engine ( values = blind )
1694
+ """
1695
+
1696
+ _footprint = dict(
1697
+ info="AlgoComponent that simply runs a serial binary",
1698
+ attr=dict(engine=dict(values=["blind"])),
1699
+ )
1700
+
1701
+ def execute_single(self, rh, opts):
1702
+ """
1703
+ Run the specified resource handler as an absolute executable,
1704
+ using the resource command_line method as args.
1705
+ """
1706
+
1707
+ args = [self.absexcutable(rh.container.localpath())]
1708
+ args.extend(self.spawn_command_line(rh))
1709
+ logger.info("BlindRun executable resource %s", args)
1710
+ rh_stdin = self.spawn_stdin(rh)
1711
+ if rh_stdin is not None:
1712
+ plocale = locale.getlocale()[1] or "ascii"
1713
+ logger.info(
1714
+ "BlindRun executable stdin (fileno:%d):\n%s",
1715
+ rh_stdin.fileno(),
1716
+ rh_stdin.read().decode(plocale, "replace"),
1717
+ )
1718
+ rh_stdin.seek(0)
1719
+ self.spawn(args, opts, stdin=rh_stdin)
1720
+
1721
+
1722
+ class ParaBlindRun(TaylorRun):
1723
+ """
1724
+ Run any executable resource (without MPI) in the current environment.
1725
+
1726
+ This abstract class includes helpers to use the taylorism package in order
1727
+ to introduce an external parallelisation. It is designed to work well with a
1728
+ taylorism Worker class that inherits from
1729
+ :class:`vortex.tools.parallelism.VortexWorkerBlindRun`.
1730
+ """
1731
+
1732
+ _abstract = True
1733
+ _footprint = dict(
1734
+ info="Abstract AlgoComponent that runs a serial binary using the taylorism package.",
1735
+ attr=dict(
1736
+ engine=dict(values=["blind"]),
1737
+ taskset=dict(
1738
+ info="Topology/Method to set up the CPU affinity of the child task.",
1739
+ default=None,
1740
+ optional=True,
1741
+ values=[
1742
+ "{:s}{:s}".format(t, m)
1743
+ for t in ("raw", "socketpacked", "numapacked")
1744
+ for m in ("", "_taskset", "_gomp", "_omp", "_ompverbose")
1745
+ ],
1746
+ ),
1747
+ taskset_bsize=dict(
1748
+ info="The number of threads used by one task",
1749
+ type=int,
1750
+ default=1,
1751
+ optional=True,
1752
+ ),
1753
+ ),
1754
+ )
1755
+
1756
+ def valid_executable(self, rh):
1757
+ """
1758
+ Return a boolean value according to the effective executable nature
1759
+ of the resource handler provided.
1760
+ """
1761
+ rc = rh is not None
1762
+ if rc:
1763
+ # Ensure that the input file is executable
1764
+ xrh = (
1765
+ rh
1766
+ if isinstance(rh, (list, tuple))
1767
+ else [
1768
+ rh,
1769
+ ]
1770
+ )
1771
+ for arh in xrh:
1772
+ self.system.xperm(arh.container.localpath(), force=True)
1773
+ return rc
1774
+
1775
+ def _default_common_instructions(self, rh, opts):
1776
+ """Create a common instruction dictionary that will be used by the workers."""
1777
+ ddict = super()._default_common_instructions(rh, opts)
1778
+ ddict["progname"] = self.absexcutable(rh.container.localpath())
1779
+ ddict["progargs"] = footprints.FPList(self.spawn_command_line(rh))
1780
+ ddict["progtaskset"] = self.taskset
1781
+ ddict["progtaskset_bsize"] = self.taskset_bsize
1782
+ return ddict
1783
+
1784
+
1785
+ class Parallel(xExecutableAlgoComponent):
1786
+ """
1787
+ Run a binary launched with MPI support.
1788
+ """
1789
+
1790
+ _footprint = dict(
1791
+ info="AlgoComponent that simply runs an MPI binary",
1792
+ attr=dict(
1793
+ engine=dict(values=["parallel"]),
1794
+ mpitool=dict(
1795
+ info="The object used to launch the parallel program",
1796
+ optional=True,
1797
+ type=mpitools.MpiTool,
1798
+ doc_visibility=footprints.doc.visibility.GURU,
1799
+ ),
1800
+ mpiname=dict(
1801
+ info=(
1802
+ "The mpiname of a class in the mpitool collector "
1803
+ + "(used only if *mpitool* is not provided)"
1804
+ ),
1805
+ optional=True,
1806
+ alias=["mpi"],
1807
+ doc_visibility=footprints.doc.visibility.GURU,
1808
+ ),
1809
+ mpiverbose=dict(
1810
+ info="Boost logging verbosity in mpitools",
1811
+ optional=True,
1812
+ default=False,
1813
+ doc_visibility=footprints.doc.visibility.GURU,
1814
+ ),
1815
+ binaries=dict(
1816
+ info="List of MpiBinaryDescription objects",
1817
+ optional=True,
1818
+ type=footprints.FPList,
1819
+ doc_visibility=footprints.doc.visibility.GURU,
1820
+ ),
1821
+ binarysingle=dict(
1822
+ info="If *binaries* is missing, the default binary role for single binaries",
1823
+ optional=True,
1824
+ default="basicsingle",
1825
+ doc_visibility=footprints.doc.visibility.GURU,
1826
+ ),
1827
+ binarymulti=dict(
1828
+ info="If *binaries* is missing, the default binary role for multiple binaries",
1829
+ type=footprints.FPList,
1830
+ optional=True,
1831
+ default=footprints.FPList(
1832
+ [
1833
+ "basic",
1834
+ ]
1835
+ ),
1836
+ doc_visibility=footprints.doc.visibility.GURU,
1837
+ ),
1838
+ ),
1839
+ )
1840
+
1841
+ def _mpitool_attributes(self, opts):
1842
+ """Return the dictionary of attributes needed to create the mpitool object."""
1843
+ # Read the appropriate configuration in the target file
1844
+ conf_dict = config.from_config(section="mpitool")
1845
+ if self.mpiname:
1846
+ conf_dict["mpiname"] = self.mpiname
1847
+ # Make "mpirun" the default mpi command name
1848
+ if "mpiname" not in conf_dict.keys():
1849
+ conf_dict["mpiname"] = "mpirun"
1850
+ possible_attrs = functools.reduce(
1851
+ lambda s, t: s | t,
1852
+ [
1853
+ set(cls.footprint_retrieve().attr.keys())
1854
+ for cls in footprints.proxy.mpitools
1855
+ ],
1856
+ )
1857
+ nonkeys = set(conf_dict.keys()) - possible_attrs
1858
+ if nonkeys:
1859
+ msg = (
1860
+ "The following keywords are unknown configuration"
1861
+ 'keys for section "mpitool":\n'
1862
+ )
1863
+
1864
+ raise ValueError(msg + "\n".join(nonkeys))
1865
+ return conf_dict
1866
+
1867
+ def spawn_command_line(self, rh):
1868
+ """Split the shell command line of the resource to be run."""
1869
+ return [super(Parallel, self).spawn_command_line(r) for r in rh]
1870
+
1871
+ def _bootstrap_mpibins_hack(self, bins, rh, opts, use_envelope):
1872
+ return copy.deepcopy(bins)
1873
+
1874
+ def _bootstrap_mpienvelope_hack(self, envelope, rh, opts, mpi):
1875
+ return copy.deepcopy(envelope)
1876
+
1877
+ def _bootstrap_mpienvelope_posthack(self, envelope, rh, opts, mpi):
1878
+ return None
1879
+
1880
+ def _bootstrap_mpitool(self, rh, opts):
1881
+ """Initialise the mpitool object and finds out the command line."""
1882
+
1883
+ # Rh is a list binaries...
1884
+ if not isinstance(rh, collections.abc.Iterable):
1885
+ rh = [
1886
+ rh,
1887
+ ]
1888
+
1889
+ # Find the MPI launcher
1890
+ mpi = self.mpitool
1891
+ if not mpi:
1892
+ mpi = footprints.proxy.mpitool(
1893
+ sysname=self.system.sysname, **self._mpitool_attributes(opts)
1894
+ )
1895
+ if not mpi:
1896
+ logger.critical(
1897
+ "Component %s could not find any mpitool",
1898
+ self.footprint_clsname(),
1899
+ )
1900
+ raise AttributeError("No valid mpitool attr could be found.")
1901
+
1902
+ # Setup various useful things (env, system, ...)
1903
+ mpi.import_basics(self)
1904
+
1905
+ mpi_opts = opts.get("mpiopts", dict())
1906
+
1907
+ envelope = []
1908
+ use_envelope = "envelope" in mpi_opts
1909
+ if use_envelope:
1910
+ envelope = mpi_opts.pop("envelope")
1911
+ if envelope == "auto":
1912
+ blockspec = dict(
1913
+ nn=self.env.get("VORTEX_SUBMIT_NODES", 1),
1914
+ )
1915
+ if "VORTEX_SUBMIT_TASKS" in self.env:
1916
+ blockspec["nnp"] = self.env.get("VORTEX_SUBMIT_TASKS")
1917
+ else:
1918
+ raise ValueError(
1919
+ "when envelope='auto', VORTEX_SUBMIT_TASKS must be set up."
1920
+ )
1921
+ envelope = [
1922
+ blockspec,
1923
+ ]
1924
+ elif isinstance(envelope, dict):
1925
+ envelope = [
1926
+ envelope,
1927
+ ]
1928
+ elif isinstance(envelope, (list, tuple)):
1929
+ pass
1930
+ else:
1931
+ raise AttributeError("Invalid envelope specification")
1932
+ if envelope:
1933
+ envelope_ntasks = sum([d["nn"] * d["nnp"] for d in envelope])
1934
+ if not envelope:
1935
+ use_envelope = False
1936
+
1937
+ if not use_envelope:
1938
+ # Some MPI presets
1939
+ mpi_desc = dict()
1940
+ for mpi_k in ("tasks", "openmp"):
1941
+ mpi_kenv = "VORTEX_SUBMIT_" + mpi_k.upper()
1942
+ if mpi_kenv in self.env:
1943
+ mpi_desc[mpi_k] = self.env.get(mpi_kenv)
1944
+
1945
+ # Binaries may be grouped together on the same nodes
1946
+ bin_groups = mpi_opts.pop("groups", [])
1947
+
1948
+ # Find out the command line
1949
+ bargs = self.spawn_command_line(rh)
1950
+
1951
+ # Potential Source files
1952
+ sources = []
1953
+
1954
+ # The usual case: no indications, 1 binary + a potential ioserver
1955
+ if len(rh) == 1 and not self.binaries:
1956
+ # In such a case, defining group does not makes sense
1957
+ self.algoassert(
1958
+ not bin_groups,
1959
+ "With only one binary, groups should not be defined",
1960
+ )
1961
+
1962
+ # The main program
1963
+ allowbind = mpi_opts.pop("allowbind", True)
1964
+ distribution = mpi_opts.pop(
1965
+ "distribution",
1966
+ self.env.get("VORTEX_MPIBIN_DEF_DISTRIBUTION", None),
1967
+ )
1968
+ if use_envelope:
1969
+ master = footprints.proxy.mpibinary(
1970
+ kind=self.binarysingle,
1971
+ ranks=envelope_ntasks,
1972
+ openmp=self.env.get("VORTEX_SUBMIT_OPENMP", None),
1973
+ allowbind=allowbind,
1974
+ distribution=distribution,
1975
+ )
1976
+ else:
1977
+ master = footprints.proxy.mpibinary(
1978
+ kind=self.binarysingle,
1979
+ nodes=self.env.get("VORTEX_SUBMIT_NODES", 1),
1980
+ allowbind=allowbind,
1981
+ distribution=distribution,
1982
+ **mpi_desc,
1983
+ )
1984
+ master.options = mpi_opts
1985
+ master.master = self.absexcutable(rh[0].container.localpath())
1986
+ master.arguments = bargs[0]
1987
+ bins = [
1988
+ master,
1989
+ ]
1990
+ # Source files ?
1991
+ if hasattr(rh[0].resource, "guess_binary_sources"):
1992
+ sources.extend(
1993
+ rh[0].resource.guess_binary_sources(rh[0].provider)
1994
+ )
1995
+
1996
+ # Multiple binaries are to be launched: no IO server support here.
1997
+ elif len(rh) > 1 and not self.binaries:
1998
+ # Binary roles
1999
+ if len(self.binarymulti) == 1:
2000
+ bnames = self.binarymulti * len(rh)
2001
+ else:
2002
+ if len(self.binarymulti) != len(rh):
2003
+ raise ParallelInconsistencyAlgoComponentError(
2004
+ "self.binarymulti"
2005
+ )
2006
+ bnames = self.binarymulti
2007
+
2008
+ # Check mpiopts shape
2009
+ for k, v in mpi_opts.items():
2010
+ if not isinstance(v, collections.abc.Iterable):
2011
+ raise ValueError(
2012
+ "In such a case, mpiopts must be Iterable"
2013
+ )
2014
+ if len(v) != len(rh):
2015
+ raise ParallelInconsistencyAlgoComponentError(
2016
+ "mpiopts[{:s}]".format(k)
2017
+ )
2018
+ # Check bin_group shape
2019
+ if bin_groups:
2020
+ if len(bin_groups) != len(rh):
2021
+ raise ParallelInconsistencyAlgoComponentError("bin_group")
2022
+
2023
+ # Create MpiBinaryDescription objects
2024
+ bins = list()
2025
+ allowbinds = mpi_opts.pop(
2026
+ "allowbind",
2027
+ [
2028
+ True,
2029
+ ]
2030
+ * len(rh),
2031
+ )
2032
+ distributions = mpi_opts.pop(
2033
+ "distribution",
2034
+ [
2035
+ self.env.get("VORTEX_MPIBIN_DEF_DISTRIBUTION", None),
2036
+ ]
2037
+ * len(rh),
2038
+ )
2039
+ for i, r in enumerate(rh):
2040
+ if use_envelope:
2041
+ bins.append(
2042
+ footprints.proxy.mpibinary(
2043
+ kind=bnames[i],
2044
+ allowbind=allowbinds[i],
2045
+ distribution=distributions[i],
2046
+ )
2047
+ )
2048
+ else:
2049
+ bins.append(
2050
+ footprints.proxy.mpibinary(
2051
+ kind=bnames[i],
2052
+ nodes=self.env.get("VORTEX_SUBMIT_NODES", 1),
2053
+ allowbind=allowbinds[i],
2054
+ distribution=distributions[i],
2055
+ **mpi_desc,
2056
+ )
2057
+ )
2058
+ # Reshape mpiopts
2059
+ bins[i].options = {k: v[i] for k, v in mpi_opts.items()}
2060
+ if bin_groups:
2061
+ bins[i].group = bin_groups[i]
2062
+ bins[i].master = self.absexcutable(r.container.localpath())
2063
+ bins[i].arguments = bargs[i]
2064
+ # Source files ?
2065
+ if hasattr(r.resource, "guess_binary_sources"):
2066
+ sources.extend(r.resource.guess_binary_sources(r.provider))
2067
+
2068
+ # Nothing to do: binary descriptions are provided by the user
2069
+ else:
2070
+ if len(self.binaries) != len(rh):
2071
+ raise ParallelInconsistencyAlgoComponentError("self.binaries")
2072
+ bins = self.binaries
2073
+ for i, r in enumerate(rh):
2074
+ bins[i].master = self.absexcutable(r.container.localpath())
2075
+ bins[i].arguments = bargs[i]
2076
+
2077
+ # The global envelope
2078
+ envelope = self._bootstrap_mpienvelope_hack(envelope, rh, opts, mpi)
2079
+ if envelope:
2080
+ mpi.envelope = envelope
2081
+
2082
+ # The binaries description
2083
+ mpi.binaries = self._bootstrap_mpibins_hack(
2084
+ bins, rh, opts, use_envelope
2085
+ )
2086
+ upd_envelope = self._bootstrap_mpienvelope_posthack(
2087
+ envelope, rh, opts, mpi
2088
+ )
2089
+ if upd_envelope:
2090
+ mpi.envelope = upd_envelope
2091
+
2092
+ # The source files
2093
+ mpi.sources = sources
2094
+
2095
+ if envelope:
2096
+ # Check the consistency between nranks and the total number of processes
2097
+ envelope_ntasks = sum([d.nprocs for d in mpi.envelope])
2098
+ mpibins_total = sum([m.nprocs for m in mpi.binaries])
2099
+ if not envelope_ntasks == mpibins_total:
2100
+ raise AlgoComponentError(
2101
+ (
2102
+ "The number of requested ranks ({:d}) must be equal "
2103
+ "to the number of processes available in the envelope ({:d})"
2104
+ ).format(mpibins_total, envelope_ntasks)
2105
+ )
2106
+
2107
+ args = mpi.mkcmdline()
2108
+ for b in mpi.binaries:
2109
+ logger.info(
2110
+ "Run %s in parallel mode. Args: %s.",
2111
+ b.master,
2112
+ " ".join(b.arguments),
2113
+ )
2114
+ logger.info("Full MPI command line: %s", " ".join(args))
2115
+
2116
+ # Setup various useful things (env, system, ...)
2117
+ mpi.import_basics(self)
2118
+
2119
+ return mpi, args
2120
+
2121
+ @contextlib.contextmanager
2122
+ def _tweak_mpitools_logging(self):
2123
+ if self.mpiverbose:
2124
+ m_loggers = dict()
2125
+ for m_logger_name in [
2126
+ l for l in loggers.lognames if "mpitools" in l
2127
+ ]:
2128
+ m_logger = loggers.getLogger(m_logger_name)
2129
+ m_loggers[m_logger] = m_logger.level
2130
+ m_logger.setLevel(logging.DEBUG)
2131
+ try:
2132
+ yield
2133
+ finally:
2134
+ for m_logger, prev_level in m_loggers.items():
2135
+ m_logger.setLevel(prev_level)
2136
+ else:
2137
+ yield
2138
+
2139
+ def execute_single(self, rh, opts):
2140
+ """Run the specified resource handler through the `mpitool` launcher
2141
+
2142
+ An argument named `mpiopts` could be provided as a dictionary: it may
2143
+ contain indications on the number of nodes, tasks, ...
2144
+ """
2145
+
2146
+ self.system.subtitle("{:s} : parallel engine".format(self.realkind))
2147
+
2148
+ with self._tweak_mpitools_logging():
2149
+ # Return a mpitool object and the mpicommand line
2150
+ mpi, args = self._bootstrap_mpitool(rh, opts)
2151
+
2152
+ # Specific parallel settings
2153
+ mpi.setup(opts)
2154
+
2155
+ # This is actual running command
2156
+ self.spawn(args, opts)
2157
+
2158
+ # Specific parallel cleaning
2159
+ mpi.clean(opts)
2160
+
2161
+
2162
+ @algo_component_deco_mixin_autodoc
2163
+ class ParallelIoServerMixin(AlgoComponentMpiDecoMixin):
2164
+ """Adds an IOServer capabilities (footprints attributes + MPI bianries alteration)."""
2165
+
2166
+ _MIXIN_EXTRA_FOOTPRINTS = [
2167
+ footprints.Footprint(
2168
+ info="Abstract IoServer footprints' attributes.",
2169
+ attr=dict(
2170
+ ioserver=dict(
2171
+ info="The object used to launch the IOserver part of the binary.",
2172
+ type=mpitools.MpiBinaryIOServer,
2173
+ optional=True,
2174
+ default=None,
2175
+ doc_visibility=footprints.doc.visibility.GURU,
2176
+ ),
2177
+ ioname=dict(
2178
+ info=(
2179
+ "The binary_kind of a class in the mpibinary collector "
2180
+ + "(used only if *ioserver* is not provided)"
2181
+ ),
2182
+ optional=True,
2183
+ default="ioserv",
2184
+ doc_visibility=footprints.doc.visibility.GURU,
2185
+ ),
2186
+ iolocation=dict(
2187
+ info="Location of the IO server within the binary list",
2188
+ type=int,
2189
+ default=-1,
2190
+ optional=True,
2191
+ ),
2192
+ ),
2193
+ ),
2194
+ ]
2195
+
2196
+ def _bootstrap_mpibins_ioserver_hack(
2197
+ self, bins, bins0, rh, opts, use_envelope
2198
+ ):
2199
+ """If requested, adds an extra binary that will act as an IOServer."""
2200
+ master = bins[-1]
2201
+ # A potential IO server
2202
+ io = self.ioserver
2203
+ if not io and int(self.env.get("VORTEX_IOSERVER_NODES", -1)) >= 0:
2204
+ io = footprints.proxy.mpibinary(
2205
+ kind=self.ioname,
2206
+ nodes=self.env.VORTEX_IOSERVER_NODES,
2207
+ tasks=(
2208
+ self.env.VORTEX_IOSERVER_TASKS
2209
+ or master.options.get("nnp", master.tasks)
2210
+ ),
2211
+ openmp=(
2212
+ self.env.VORTEX_IOSERVER_OPENMP
2213
+ or master.options.get("openmp", master.openmp)
2214
+ ),
2215
+ iolocation=self.iolocation,
2216
+ )
2217
+ io.options = {
2218
+ x[3:]: opts[x] for x in opts.keys() if x.startswith("io_")
2219
+ }
2220
+ io.master = master.master
2221
+ io.arguments = master.arguments
2222
+ if (
2223
+ not io
2224
+ and int(self.env.get("VORTEX_IOSERVER_COMPANION_TASKS", -1)) >= 0
2225
+ ):
2226
+ io = footprints.proxy.mpibinary(
2227
+ kind=self.ioname,
2228
+ nodes=master.options.get("nn", master.nodes),
2229
+ tasks=self.env.VORTEX_IOSERVER_COMPANION_TASKS,
2230
+ openmp=(
2231
+ self.env.VORTEX_IOSERVER_OPENMP
2232
+ or master.options.get("openmp", master.openmp)
2233
+ ),
2234
+ )
2235
+ io.options = {
2236
+ x[3:]: opts[x] for x in opts.keys() if x.startswith("io_")
2237
+ }
2238
+ io.master = master.master
2239
+ io.arguments = master.arguments
2240
+ if master.group is not None:
2241
+ # The master binary is already in a group ! Use it.
2242
+ io.group = master.group
2243
+ else:
2244
+ io.group = "auto_masterwithio"
2245
+ master.group = "auto_masterwithio"
2246
+ if (
2247
+ not io
2248
+ and self.env.get("VORTEX_IOSERVER_INCORE_TASKS", None) is not None
2249
+ ):
2250
+ if hasattr(master, "incore_iotasks"):
2251
+ master.incore_iotasks = self.env.VORTEX_IOSERVER_INCORE_TASKS
2252
+ if (
2253
+ not io
2254
+ and self.env.get("VORTEX_IOSERVER_INCORE_FIXER", None) is not None
2255
+ ):
2256
+ if hasattr(master, "incore_iotasks_fixer"):
2257
+ master.incore_iotasks_fixer = (
2258
+ self.env.VORTEX_IOSERVER_INCORE_FIXER
2259
+ )
2260
+ if (
2261
+ not io
2262
+ and self.env.get("VORTEX_IOSERVER_INCORE_DIST", None) is not None
2263
+ ):
2264
+ if hasattr(master, "incore_iodist"):
2265
+ master.incore_iodist = self.env.VORTEX_IOSERVER_INCORE_DIST
2266
+ if io:
2267
+ rh.append(rh[0])
2268
+ if master.group is None:
2269
+ if "nn" in master.options:
2270
+ master.options["nn"] = (
2271
+ master.options["nn"] - io.options["nn"]
2272
+ )
2273
+ else:
2274
+ logger.warning(
2275
+ 'The "nn" option is not available in the master binary '
2276
+ + "mpi options. Consequently it can be fixed..."
2277
+ )
2278
+ if self.iolocation >= 0:
2279
+ bins.insert(self.iolocation, io)
2280
+ else:
2281
+ bins.append(io)
2282
+ return bins
2283
+
2284
+ _MIXIN_MPIBINS_HOOKS = (_bootstrap_mpibins_ioserver_hack,)
2285
+
2286
+
2287
+ @algo_component_deco_mixin_autodoc
2288
+ class ParallelOpenPalmMixin(AlgoComponentMpiDecoMixin):
2289
+ """Class mixin to be used with OpenPALM programs.
2290
+
2291
+ It will automatically add the OpenPALM driver binary to the list of
2292
+ binaries. The location of the OpenPALM driver should be automatically
2293
+ detected provided that a section with ``role=OpenPALM Driver`` lies in the
2294
+ input's sequence. Alternatively, the path to the OpenPALM driver can be
2295
+ provided using the **openpalm_driver** footprint's argument.
2296
+ """
2297
+
2298
+ _MIXIN_EXTRA_FOOTPRINTS = [
2299
+ footprints.Footprint(
2300
+ info="Abstract OpenPALM footprints' attributes.",
2301
+ attr=dict(
2302
+ openpalm_driver=dict(
2303
+ info=(
2304
+ "The path to the OpenPALM driver binary. "
2305
+ + "When omitted, the input sequence is looked up "
2306
+ + "for section with ``role=OpenPALM Driver``."
2307
+ ),
2308
+ optional=True,
2309
+ doc_visibility=footprints.doc.visibility.ADVANCED,
2310
+ ),
2311
+ openpalm_overcommit=dict(
2312
+ info=(
2313
+ "Run the OpenPALM driver on the first node in addition "
2314
+ + "to existing tasks. Otherwise dedicated tasks are used."
2315
+ ),
2316
+ type=bool,
2317
+ default=True,
2318
+ optional=True,
2319
+ doc_visibility=footprints.doc.visibility.ADVANCED,
2320
+ ),
2321
+ openpalm_binddriver=dict(
2322
+ info="Try to bind the OpenPALM driver binary.",
2323
+ type=bool,
2324
+ optional=True,
2325
+ default=False,
2326
+ doc_visibility=footprints.doc.visibility.ADVANCED,
2327
+ ),
2328
+ openpalm_binkind=dict(
2329
+ info="The binary kind for the OpenPALM driver.",
2330
+ optional=True,
2331
+ default="basic",
2332
+ doc_visibility=footprints.doc.visibility.GURU,
2333
+ ),
2334
+ ),
2335
+ ),
2336
+ ]
2337
+
2338
+ @property
2339
+ def _actual_openpalm_driver(self):
2340
+ """Returns the OpenPALM's driver location."""
2341
+ path = self.openpalm_driver
2342
+ if path is None:
2343
+ drivers = self.context.sequence.effective_inputs(
2344
+ role="OpenPALMDriver"
2345
+ )
2346
+ if not drivers:
2347
+ raise AlgoComponentError("No OpenPALM driver was provided.")
2348
+ elif len(drivers) > 1:
2349
+ raise AlgoComponentError(
2350
+ "Several OpenPALM driver were provided."
2351
+ )
2352
+ path = drivers[0].rh.container.localpath()
2353
+ else:
2354
+ if not self.system.path.exists(path):
2355
+ raise AlgoComponentError(
2356
+ "No OpenPALM driver was provider ({:s} does not exists).".format(
2357
+ path
2358
+ )
2359
+ )
2360
+ return path
2361
+
2362
+ def _bootstrap_mpibins_openpalm_hack(
2363
+ self, bins, bins0, rh, opts, use_envelope
2364
+ ):
2365
+ """Adds the OpenPALM driver to the binary list."""
2366
+ single_bin = len(bins) == 1
2367
+ master = bins[0]
2368
+ driver = footprints.proxy.mpibinary(
2369
+ kind=self.openpalm_binkind,
2370
+ nodes=1,
2371
+ tasks=self.env.VORTEX_OPENPALM_DRV_TASKS or 1,
2372
+ openmp=self.env.VORTEX_OPENPALM_DRV_OPENMP or 1,
2373
+ allowbind=opts.pop(
2374
+ "palmdrv_bind",
2375
+ self.env.get(
2376
+ "VORTEX_OPENPALM_DRV_BIND", self.openpalm_binddriver
2377
+ ),
2378
+ ),
2379
+ )
2380
+ driver.options = {
2381
+ x[8:]: opts[x] for x in opts.keys() if x.startswith("palmdrv_")
2382
+ }
2383
+ driver.master = self._actual_openpalm_driver
2384
+ self.system.xperm(driver.master, force=True)
2385
+ bins.insert(0, driver)
2386
+ if not self.openpalm_overcommit and single_bin:
2387
+ # Tweak the number of tasks of the master program in order to accommodate
2388
+ # the driver
2389
+ # NB: If multiple binaries are provided, the user must do this by
2390
+ # himself (i.e. leave enough room for the driver's task).
2391
+ if "nn" in master.options:
2392
+ master.options["nn"] = master.options["nn"] - 1
2393
+ else:
2394
+ # Ok, tweak nprocs instead (an envelope might be defined)
2395
+ try:
2396
+ nprocs = master.nprocs
2397
+ except mpitools.MpiException:
2398
+ logger.error(
2399
+ 'Neither the "nn" option nor the nprocs is '
2400
+ + "available for the master binary. Consequently "
2401
+ + "it can be fixed..."
2402
+ )
2403
+ else:
2404
+ master.options["np"] = nprocs - driver.nprocs
2405
+ return bins
2406
+
2407
+ _MIXIN_MPIBINS_HOOKS = (_bootstrap_mpibins_openpalm_hack,)
2408
+
2409
+ def _bootstrap_mpienvelope_openpalm_posthack(
2410
+ self, env, env0, rh, opts, mpi
2411
+ ):
2412
+ """
2413
+ Tweak the MPI envelope in order to execute the OpenPALM driver on the
2414
+ appropriate node.
2415
+ """
2416
+ master = mpi.binaries[
2417
+ 1
2418
+ ] # The first "real" program that will be launched
2419
+ driver = mpi.binaries[0] # The OpenPALM driver
2420
+ if self.openpalm_overcommit:
2421
+ # Execute the driver on the first compute node
2422
+ if env or env0:
2423
+ env = env or copy.deepcopy(env0)
2424
+ # An envelope is already defined... update it
2425
+ if not ("nn" in env[0] and "nnp" in env[0]):
2426
+ raise AlgoComponentError(
2427
+ "'nn' and 'nnp' must be defined in the envelope"
2428
+ )
2429
+ if env[0]["nn"] > 1:
2430
+ env[0]["nn"] -= 1
2431
+ newenv = copy.copy(env[0])
2432
+ newenv["nn"] = 1
2433
+ newenv["nnp"] += driver.nprocs
2434
+ env.insert(0, newenv)
2435
+ else:
2436
+ env[0]["nnp"] += driver.nprocs
2437
+ else:
2438
+ # Setup a new envelope
2439
+ if not ("nn" in master.options and "nnp" in master.options):
2440
+ raise AlgoComponentError(
2441
+ "'nn' and 'nnp' must be defined for the master executable"
2442
+ )
2443
+ env = [
2444
+ dict(
2445
+ nn=1,
2446
+ nnp=master.options["nnp"] + driver.nprocs,
2447
+ openmp=master.options.get("openmp", 1),
2448
+ )
2449
+ ]
2450
+ if master.options["nn"] > 1:
2451
+ env.append(
2452
+ dict(
2453
+ nn=master.options["nn"] - 1,
2454
+ nnp=master.options["nnp"],
2455
+ openmp=master.options.get("openmp", 1),
2456
+ )
2457
+ )
2458
+ if len(mpi.binaries) > 2:
2459
+ env.extend([b.options for b in mpi.binaries[2:]])
2460
+ return env
2461
+
2462
+ _MIXIN_MPIENVELOPE_POSTHOOKS = (_bootstrap_mpienvelope_openpalm_posthack,)