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,1641 @@
1
+ """
2
+ Various Post-Processing AlgoComponents.
3
+ """
4
+
5
+ import collections
6
+ import json
7
+ import re
8
+ import time
9
+
10
+ from bronx.datagrip.namelist import NamelistBlock
11
+ from bronx.fancies import loggers
12
+ from footprints.stdtypes import FPTuple
13
+ import footprints
14
+ from taylorism import Boss
15
+
16
+ from vortex.layout.monitor import (
17
+ BasicInputMonitor,
18
+ AutoMetaGang,
19
+ MetaGang,
20
+ EntrySt,
21
+ GangSt,
22
+ )
23
+ from vortex.algo.components import (
24
+ AlgoComponentDecoMixin,
25
+ AlgoComponentError,
26
+ algo_component_deco_mixin_autodoc,
27
+ )
28
+ from vortex.algo.components import (
29
+ TaylorRun,
30
+ BlindRun,
31
+ ParaBlindRun,
32
+ Parallel,
33
+ Expresso,
34
+ )
35
+ from vortex.syntax.stdattrs import DelayedEnvValue, FmtInt
36
+ from vortex.tools.grib import EcGribDecoMixin
37
+ from vortex.tools.parallelism import (
38
+ TaylorVortexWorker,
39
+ VortexWorkerBlindRun,
40
+ ParallelResultParser,
41
+ )
42
+ from vortex.tools.systems import ExecutionError
43
+
44
+ from ..tools.grib import GRIBFilter
45
+ from ..tools.drhook import DrHookDecoMixin
46
+
47
+ #: No automatic export
48
+ __all__ = []
49
+
50
+ logger = loggers.getLogger(__name__)
51
+
52
+
53
+ class _FA2GribWorker(VortexWorkerBlindRun):
54
+ """The taylorism worker that actually do the gribing (in parallel).
55
+
56
+ This is called indirectly by taylorism when :class:`Fa2Grib` is used.
57
+ """
58
+
59
+ _footprint = dict(
60
+ attr=dict(
61
+ kind=dict(values=["fa2grib"]),
62
+ # Progrid parameters
63
+ fortnam=dict(),
64
+ fortinput=dict(),
65
+ compact=dict(),
66
+ timeshift=dict(type=int),
67
+ timeunit=dict(type=int),
68
+ numod=dict(type=int),
69
+ sciz=dict(type=int),
70
+ scizoffset=dict(type=int, optional=True),
71
+ # Input/Output data
72
+ file_in=dict(),
73
+ file_out=dict(),
74
+ member=dict(
75
+ type=FmtInt,
76
+ optional=True,
77
+ ),
78
+ )
79
+ )
80
+
81
+ def vortex_task(self, **kwargs):
82
+ logger.info("Starting the Fa2Grib processing for tag=%s", self.name)
83
+
84
+ thisoutput = "GRIDOUTPUT"
85
+ rdict = dict(rc=True)
86
+
87
+ # First, check that the hooks were applied
88
+ for thisinput in [
89
+ x
90
+ for x in self.context.sequence.inputs()
91
+ if x.rh.container.localpath() == self.file_in
92
+ ]:
93
+ if thisinput.rh.delayhooks:
94
+ thisinput.rh.apply_get_hooks()
95
+
96
+ # Jump into a working directory
97
+ cwd = self.system.pwd()
98
+ tmpwd = self.system.path.join(cwd, self.file_out + ".process.d")
99
+ self.system.mkdir(tmpwd)
100
+ self.system.cd(tmpwd)
101
+
102
+ # Build the local namelist block
103
+ nb = NamelistBlock(name="NAML")
104
+ nb.NBDOM = 1
105
+ nb.CHOPER = self.compact
106
+ nb.INUMOD = self.numod
107
+ if self.scizoffset is not None:
108
+ nb.ISCIZ = self.scizoffset + (
109
+ self.member if self.member is not None else 0
110
+ )
111
+ else:
112
+ if self.sciz:
113
+ nb.ISCIZ = self.sciz
114
+ if self.timeshift:
115
+ nb.IHCTPI = self.timeshift
116
+ if self.timeunit:
117
+ nb.ITUNIT = self.timeunit
118
+ nb["CLFSORT(1)"] = thisoutput
119
+ nb["CDNOMF(1)"] = self.fortinput
120
+ with open(self.fortnam, "w") as namfd:
121
+ namfd.write(nb.dumps())
122
+
123
+ # Finally set the actual init file
124
+ self.system.softlink(
125
+ self.system.path.join(cwd, self.file_in), self.fortinput
126
+ )
127
+
128
+ # Standard execution
129
+ list_name = self.system.path.join(cwd, self.file_out + ".listing")
130
+ try:
131
+ self.local_spawn(list_name)
132
+ except ExecutionError as e:
133
+ rdict["rc"] = e
134
+
135
+ # Freeze the current output
136
+ if self.system.path.exists(thisoutput):
137
+ self.system.move(
138
+ thisoutput, self.system.path.join(cwd, self.file_out)
139
+ )
140
+ else:
141
+ logger.warning("Missing some grib output: %s", self.file_out)
142
+ rdict["rc"] = False
143
+
144
+ # Final cleaning
145
+ self.system.cd(cwd)
146
+ self.system.remove(tmpwd)
147
+
148
+ if self.system.path.exists(self.file_out):
149
+ # Deal with promised resources
150
+ expected = [
151
+ x
152
+ for x in self.context.sequence.outputs()
153
+ if x.rh.provider.expected
154
+ and x.rh.container.localpath() == self.file_out
155
+ ]
156
+ for thispromise in expected:
157
+ thispromise.put(incache=True)
158
+
159
+ logger.info("Fa2Grib processing is done for tag=%s", self.name)
160
+
161
+ return rdict
162
+
163
+
164
+ class _GribFilterWorker(TaylorVortexWorker):
165
+ """The taylorism worker that actually filter the gribfiles.
166
+
167
+ This is called indirectly by taylorism when :class:`Fa2Grib` is used.
168
+ """
169
+
170
+ _footprint = dict(
171
+ attr=dict(
172
+ kind=dict(values=["gribfilter"]),
173
+ # Filter settings
174
+ filters=dict(
175
+ type=FPTuple,
176
+ ),
177
+ concatenate=dict(
178
+ type=bool,
179
+ ),
180
+ # Put files if they are expected
181
+ put_promises=dict(
182
+ type=bool,
183
+ optional=True,
184
+ default=True,
185
+ ),
186
+ # Input/Output data
187
+ file_in=dict(),
188
+ file_outfmt=dict(),
189
+ file_outintent=dict(
190
+ optional=True,
191
+ default="in",
192
+ ),
193
+ )
194
+ )
195
+
196
+ def vortex_task(self, **kwargs):
197
+ logger.info("Starting the GribFiltering for tag=%s", self.file_in)
198
+
199
+ rdict = dict(rc=True)
200
+
201
+ # Create the filtering object and add filters
202
+ gfilter = GRIBFilter(concatenate=self.concatenate)
203
+ if self.filters:
204
+ gfilter.add_filters(*list(self.filters))
205
+
206
+ # Process the input file
207
+ newfiles = gfilter(self.file_in, self.file_outfmt, self.file_outintent)
208
+
209
+ if newfiles:
210
+ if self.put_promises:
211
+ # Deal with promised resources
212
+ allpromises = [
213
+ x
214
+ for x in self.context.sequence.outputs()
215
+ if x.rh.provider.expected
216
+ ]
217
+ for newfile in newfiles:
218
+ expected = [
219
+ x
220
+ for x in allpromises
221
+ if x.rh.container.localpath() == newfile
222
+ ]
223
+ for thispromise in expected:
224
+ thispromise.put(incache=True)
225
+ else:
226
+ logger.warning("No file has been generated.")
227
+ rdict["rc"] = False
228
+
229
+ logger.info("GribFiltering is done for tag=%s", self.name)
230
+
231
+ return rdict
232
+
233
+
234
+ def parallel_grib_filter(
235
+ context,
236
+ inputs,
237
+ outputs,
238
+ intents=(),
239
+ cat=False,
240
+ filters=FPTuple(),
241
+ nthreads=8,
242
+ ):
243
+ """A simple method that calls the GRIBFilter class in parallel.
244
+
245
+ :param vortex.layout.contexts.Context context: the current context
246
+ :param list[str] inputs: the list of input file names
247
+ :param list[str] outputs: the list of output file names
248
+ :param list[str] intents: the list of intent (in|inout) for output files (in if omitted)
249
+ :param bool cat: whether or not to concatenate the input files (False by default)
250
+ :param tuple filters: a list of filters to apply (as a list of JSON dumps)
251
+ :param int nthreads: the maximum number of tasks used concurently (8 by default)
252
+ """
253
+ if not cat and len(filters) == 0:
254
+ raise AlgoComponentError(
255
+ "cat must be true or filters must be provided"
256
+ )
257
+ if len(inputs) != len(outputs):
258
+ raise AlgoComponentError(
259
+ "inputs and outputs must have the same length"
260
+ )
261
+ if len(intents) != len(outputs):
262
+ intents = FPTuple(
263
+ [
264
+ "in",
265
+ ]
266
+ * len(outputs)
267
+ )
268
+ boss = Boss(
269
+ scheduler=footprints.proxy.scheduler(
270
+ limit="threads", max_threads=nthreads
271
+ )
272
+ )
273
+ common_i = dict(
274
+ kind="gribfilter", filters=filters, concatenate=cat, put_promises=False
275
+ )
276
+ for ifile, ofile, intent in zip(inputs, outputs, intents):
277
+ logger.info(
278
+ "%s -> %s (intent: %s) added to the GRIBfilter task's list",
279
+ ifile,
280
+ ofile,
281
+ intent,
282
+ )
283
+ boss.set_instructions(
284
+ common_i,
285
+ dict(
286
+ name=[
287
+ ifile,
288
+ ],
289
+ file_in=[
290
+ ifile,
291
+ ],
292
+ file_outfmt=[
293
+ ofile,
294
+ ],
295
+ file_outintent=[
296
+ intent,
297
+ ],
298
+ ),
299
+ )
300
+ boss.make_them_work()
301
+ boss.wait_till_finished()
302
+ logger.info("All files are processed.")
303
+ report = boss.get_report()
304
+ prp = ParallelResultParser(context)
305
+ for r in report["workers_report"]:
306
+ if isinstance(prp(r), Exception):
307
+ raise AlgoComponentError("An error occurred in GRIBfilter.")
308
+
309
+
310
+ class Fa2Grib(ParaBlindRun):
311
+ """Standard FA conversion, e.g. with PROGRID as a binary resource."""
312
+
313
+ _footprint = dict(
314
+ attr=dict(
315
+ kind=dict(
316
+ values=["fa2grib"],
317
+ ),
318
+ timeout=dict(
319
+ type=int,
320
+ optional=True,
321
+ default=300,
322
+ ),
323
+ refreshtime=dict(
324
+ type=int,
325
+ optional=True,
326
+ default=20,
327
+ ),
328
+ fatal=dict(
329
+ type=bool,
330
+ optional=True,
331
+ default=True,
332
+ ),
333
+ fortnam=dict(
334
+ optional=True,
335
+ default="fort.4",
336
+ ),
337
+ fortinput=dict(
338
+ optional=True,
339
+ default="fort.11",
340
+ ),
341
+ compact=dict(
342
+ optional=True,
343
+ default=DelayedEnvValue("VORTEX_GRIB_COMPACT", "L"),
344
+ ),
345
+ timeshift=dict(
346
+ type=int,
347
+ optional=True,
348
+ default=DelayedEnvValue("VORTEX_GRIB_SHIFT", 0),
349
+ ),
350
+ timeunit=dict(
351
+ type=int,
352
+ optional=True,
353
+ default=DelayedEnvValue("VORTEX_GRIB_TUNIT", 1),
354
+ ),
355
+ numod=dict(
356
+ type=int,
357
+ optional=True,
358
+ default=DelayedEnvValue("VORTEX_GRIB_NUMOD", 221),
359
+ ),
360
+ sciz=dict(
361
+ type=int,
362
+ optional=True,
363
+ default=DelayedEnvValue("VORTEX_GRIB_SCIZ", 0),
364
+ ),
365
+ scizoffset=dict(
366
+ type=int,
367
+ optional=True,
368
+ ),
369
+ )
370
+ )
371
+
372
+ def prepare(self, rh, opts):
373
+ """Set some variables according to target definition."""
374
+ super().prepare(rh, opts)
375
+ self.system.remove(self.fortinput)
376
+ self.env.DR_HOOK_NOT_MPI = 1
377
+ self.system.subtitle(
378
+ "{:s} : directory listing (pre-run)".format(self.realkind)
379
+ )
380
+ self.system.dir(output=False, fatal=False)
381
+
382
+ def execute(self, rh, opts):
383
+ """Loop on the various initial conditions provided."""
384
+
385
+ self._default_pre_execute(rh, opts)
386
+
387
+ common_i = self._default_common_instructions(rh, opts)
388
+ # Update the common instructions
389
+ common_i.update(
390
+ dict(
391
+ fortnam=self.fortnam,
392
+ fortinput=self.fortinput,
393
+ compact=self.compact,
394
+ numod=self.numod,
395
+ sciz=self.sciz,
396
+ scizoffset=self.scizoffset,
397
+ timeshift=self.timeshift,
398
+ timeunit=self.timeunit,
399
+ )
400
+ )
401
+ tmout = False
402
+
403
+ # Monitor for the input files
404
+ bm = BasicInputMonitor(
405
+ self.context,
406
+ caching_freq=self.refreshtime,
407
+ role="Gridpoint",
408
+ kind="gridpoint",
409
+ )
410
+ with bm:
411
+ while not bm.all_done or len(bm.available) > 0:
412
+ while bm.available:
413
+ s = bm.pop_available().section
414
+ file_in = s.rh.container.localpath()
415
+ # Find the name of the output file
416
+ if s.rh.provider.member is not None:
417
+ file_out = "GRIB{:s}_{!s}+{:s}".format(
418
+ s.rh.resource.geometry.area,
419
+ s.rh.provider.member,
420
+ s.rh.resource.term.fmthm,
421
+ )
422
+ else:
423
+ file_out = "GRIB{:s}+{:s}".format(
424
+ s.rh.resource.geometry.area,
425
+ s.rh.resource.term.fmthm,
426
+ )
427
+ logger.info(
428
+ "Adding input file %s to the job list", file_in
429
+ )
430
+ self._add_instructions(
431
+ common_i,
432
+ dict(
433
+ name=[
434
+ file_in,
435
+ ],
436
+ file_in=[
437
+ file_in,
438
+ ],
439
+ file_out=[
440
+ file_out,
441
+ ],
442
+ member=[
443
+ s.rh.provider.member,
444
+ ],
445
+ ),
446
+ )
447
+
448
+ if not (bm.all_done or len(bm.available) > 0):
449
+ # Timeout ?
450
+ tmout = bm.is_timedout(self.timeout)
451
+ if tmout:
452
+ break
453
+ # Wait a little bit :-)
454
+ time.sleep(1)
455
+ bm.health_check(interval=30)
456
+
457
+ self._default_post_execute(rh, opts)
458
+
459
+ for failed_file in [
460
+ e.section.rh.container.localpath() for e in bm.failed.values()
461
+ ]:
462
+ logger.error(
463
+ "We were unable to fetch the following file: %s", failed_file
464
+ )
465
+ if self.fatal:
466
+ self.delayed_exception_add(
467
+ IOError("Unable to fetch {:s}".format(failed_file)),
468
+ traceback=False,
469
+ )
470
+
471
+ if tmout:
472
+ raise OSError("The waiting loop timed out")
473
+
474
+
475
+ class StandaloneGRIBFilter(TaylorRun):
476
+ _footprint = dict(
477
+ attr=dict(
478
+ kind=dict(
479
+ values=["gribfilter"],
480
+ ),
481
+ timeout=dict(
482
+ type=int,
483
+ optional=True,
484
+ default=300,
485
+ ),
486
+ refreshtime=dict(
487
+ type=int,
488
+ optional=True,
489
+ default=20,
490
+ ),
491
+ concatenate=dict(
492
+ type=bool,
493
+ default=False,
494
+ optional=True,
495
+ ),
496
+ fatal=dict(
497
+ type=bool,
498
+ optional=True,
499
+ default=True,
500
+ ),
501
+ )
502
+ )
503
+
504
+ def prepare(self, rh, opts):
505
+ """Set some variables according to target definition."""
506
+ super().prepare(rh, opts)
507
+ self.system.subtitle(
508
+ "{:s} : directory listing (pre-run)".format(self.realkind)
509
+ )
510
+ self.system.dir(output=False, fatal=False)
511
+
512
+ def execute(self, rh, opts):
513
+ # We re-serialise data because footprints don't like dictionaries
514
+ filters = [
515
+ json.dumps(x.rh.contents.data)
516
+ for x in self.context.sequence.effective_inputs(
517
+ role="GRIBFilteringRequest", kind="filtering_request"
518
+ )
519
+ ]
520
+ filters = FPTuple(filters)
521
+
522
+ self._default_pre_execute(rh, opts)
523
+
524
+ common_i = self._default_common_instructions(rh, opts)
525
+ # Update the common instructions
526
+ common_i.update(dict(concatenate=self.concatenate, filters=filters))
527
+ tmout = False
528
+
529
+ # Monitor for the input files
530
+ bm = BasicInputMonitor(
531
+ self.context,
532
+ caching_freq=self.refreshtime,
533
+ role="Gridpoint",
534
+ kind="gridpoint",
535
+ )
536
+ with bm:
537
+ while not bm.all_done or len(bm.available) > 0:
538
+ while bm.available:
539
+ s = bm.pop_available().section
540
+ file_in = s.rh.container.localpath()
541
+ file_outfmt = re.sub(
542
+ r"^(.*?)((:?\.[^.]*)?)$",
543
+ r"\1_{filtername:s}\2",
544
+ file_in,
545
+ )
546
+
547
+ logger.info(
548
+ "Adding input file %s to the job list", file_in
549
+ )
550
+ self._add_instructions(
551
+ common_i,
552
+ dict(
553
+ name=[
554
+ file_in,
555
+ ],
556
+ file_in=[
557
+ file_in,
558
+ ],
559
+ file_outfmt=[
560
+ file_outfmt,
561
+ ],
562
+ ),
563
+ )
564
+
565
+ if not (bm.all_done or len(bm.available) > 0):
566
+ # Timeout ?
567
+ tmout = bm.is_timedout(self.timeout)
568
+ if tmout:
569
+ break
570
+ # Wait a little bit :-)
571
+ time.sleep(1)
572
+ bm.health_check(interval=30)
573
+
574
+ self._default_post_execute(rh, opts)
575
+
576
+ for failed_file in [
577
+ e.section.rh.container.localpath() for e in bm.failed.values()
578
+ ]:
579
+ logger.error(
580
+ "We were unable to fetch the following file: %s", failed_file
581
+ )
582
+ if self.fatal:
583
+ self.delayed_exception_add(
584
+ IOError("Unable to fetch {:s}".format(failed_file)),
585
+ traceback=False,
586
+ )
587
+
588
+ if tmout:
589
+ raise OSError("The waiting loop timed out")
590
+
591
+
592
+ class AddField(BlindRun):
593
+ """Miscellaneous manipulation on input FA resources."""
594
+
595
+ _footprint = dict(
596
+ attr=dict(
597
+ kind=dict(
598
+ values=["addcst", "addconst", "addfield"],
599
+ remap=dict(
600
+ addconst="addcst",
601
+ ),
602
+ ),
603
+ fortnam=dict(
604
+ optional=True,
605
+ default="fort.4",
606
+ ),
607
+ fortinput=dict(
608
+ optional=True,
609
+ default="fort.11",
610
+ ),
611
+ fortoutput=dict(
612
+ optional=True,
613
+ default="fort.12",
614
+ ),
615
+ )
616
+ )
617
+
618
+ def prepare(self, rh, opts):
619
+ """Set some variables according to target definition."""
620
+ super().prepare(rh, opts)
621
+ self.system.remove(self.fortinput)
622
+ self.env.DR_HOOK_NOT_MPI = 1
623
+
624
+ def execute(self, rh, opts):
625
+ """Loop on the various initial conditions provided."""
626
+
627
+ # Is there any namelist provided ?
628
+ namrh = [
629
+ x.rh
630
+ for x in self.context.sequence.effective_inputs(
631
+ role=("Namelist"), kind="namelist"
632
+ )
633
+ ]
634
+ if namrh:
635
+ self.system.softlink(namrh[0].container.localpath(), self.fortnam)
636
+ else:
637
+ logger.warning("Do not find any namelist for %s", self.kind)
638
+
639
+ # Look for some sources files
640
+ srcrh = [
641
+ x.rh
642
+ for x in self.context.sequence.effective_inputs(
643
+ role=("Gridpoint", "Sources"), kind="gridpoint"
644
+ )
645
+ ]
646
+ srcrh.sort(key=lambda rh: rh.resource.term)
647
+
648
+ for r in srcrh:
649
+ self.system.title(
650
+ "Loop on domain {:s} and term {:s}".format(
651
+ r.resource.geometry.area, r.resource.term.fmthm
652
+ )
653
+ )
654
+
655
+ # Some cleaning
656
+ self.system.remove(self.fortinput)
657
+ self.system.remove(self.fortoutput)
658
+
659
+ # Prepare double input
660
+ self.system.link(r.container.localpath(), self.fortinput)
661
+ self.system.cp(r.container.localpath(), self.fortoutput)
662
+
663
+ # Standard execution
664
+ opts["loop"] = r.resource.term
665
+ super().execute(rh, opts)
666
+
667
+ # Some cleaning
668
+ self.system.rmall("DAPDIR", self.fortinput, self.fortoutput)
669
+
670
+ def postfix(self, rh, opts):
671
+ """Post add cleaning."""
672
+ super().postfix(rh, opts)
673
+ self.system.remove(self.fortnam)
674
+
675
+
676
+ class DegradedDiagPEError(AlgoComponentError):
677
+ """Exception raised when some of the members are missing in the calculations."""
678
+
679
+ def __init__(self, ginfo, missings):
680
+ super().__init__()
681
+ self._ginfo = ginfo
682
+ self._missings = missings
683
+
684
+ def __str__(self):
685
+ outstr = (
686
+ "Missing input data for geometry={0.area:s}, term={1!s}:\n".format(
687
+ self._ginfo["geometry"], self._ginfo["term"]
688
+ )
689
+ )
690
+ for k, missing in self._missings.items():
691
+ for member in missing:
692
+ outstr += "{:s}: member #{!s}\n".format(k, member)
693
+ return outstr
694
+
695
+
696
+ class DiagPE(BlindRun, DrHookDecoMixin, EcGribDecoMixin):
697
+ """Execution of diagnostics on grib input (ensemble forecasts specific)."""
698
+
699
+ _footprint = dict(
700
+ attr=dict(
701
+ kind=dict(
702
+ values=["diagpe"],
703
+ ),
704
+ method=dict(
705
+ info="The method used to compute the diagnosis",
706
+ values=["neighbour"],
707
+ ),
708
+ numod=dict(
709
+ type=int,
710
+ info="The GRIB model number",
711
+ optional=True,
712
+ default=DelayedEnvValue("VORTEX_GRIB_NUMOD", 118),
713
+ ),
714
+ timeout=dict(
715
+ type=int,
716
+ optional=True,
717
+ default=900,
718
+ ),
719
+ refreshtime=dict(
720
+ type=int,
721
+ optional=True,
722
+ default=20,
723
+ ),
724
+ missinglimit=dict(
725
+ type=int,
726
+ optional=True,
727
+ default=0,
728
+ ),
729
+ waitlimit=dict(
730
+ type=int,
731
+ optional=True,
732
+ default=900,
733
+ ),
734
+ fatal=dict(
735
+ type=bool,
736
+ optional=True,
737
+ default=True,
738
+ ),
739
+ gribfilter_tasks=dict(
740
+ type=int,
741
+ optional=True,
742
+ default=8,
743
+ ),
744
+ ),
745
+ )
746
+
747
+ _method2output_map = dict(neighbour="GRIB_PE_VOISIN")
748
+
749
+ def spawn_hook(self):
750
+ """Usually a good habit to dump the fort.4 namelist."""
751
+ super().spawn_hook()
752
+ if self.system.path.exists("fort.4"):
753
+ self.system.subtitle(
754
+ "{:s} : dump namelist <fort.4>".format(self.realkind)
755
+ )
756
+ self.system.cat("fort.4", output=False)
757
+
758
+ def _actual_execute(
759
+ self, gmembers, ifilters, filters, basedate, finalterm, rh, opts, gang
760
+ ):
761
+ mygeometry = gang.info["geometry"]
762
+ myterm = gang.info["term"]
763
+
764
+ self.system.title(
765
+ "Start processing for geometry={:s}, term={!s}.".format(
766
+ mygeometry.area, myterm
767
+ )
768
+ )
769
+
770
+ # Find out what is the common set of members
771
+ members = set(
772
+ gmembers
773
+ ) # gmembers is mutable: we need a copy of it (hence the explicit set())
774
+ missing_members = dict()
775
+ for subgang in gang.memberslist:
776
+ smembers = {
777
+ s.section.rh.provider.member
778
+ for s in subgang.memberslist
779
+ if s.state == EntrySt.available
780
+ }
781
+ ufomembers = {
782
+ s.section.rh.provider.member
783
+ for s in subgang.memberslist
784
+ if s.state == EntrySt.ufo
785
+ }
786
+ missing_members[subgang.nickname] = (
787
+ gmembers - smembers - ufomembers
788
+ )
789
+ members &= smembers
790
+ # Record an error
791
+ if members != gmembers:
792
+ newexc = DegradedDiagPEError(gang.info, missing_members)
793
+ logger.error("Some of the data are missing for this geometry/term")
794
+ if self.fatal:
795
+ self.delayed_exception_add(newexc, traceback=False)
796
+ else:
797
+ logger.info(
798
+ "Fatal is false consequently no exception is recorded. It would look like this:"
799
+ )
800
+ print(newexc)
801
+ members = sorted(members)
802
+
803
+ # This is hopeless :-(
804
+ if gang.state == GangSt.failed:
805
+ return
806
+
807
+ # If needed, concatenate or filter the "superset" files
808
+ supersets = list()
809
+ for subgang in gang.memberslist:
810
+ supersets.extend(
811
+ [
812
+ (
813
+ s.section.rh.container.localpath(),
814
+ re.sub(
815
+ r"^[a-zA-Z]+_(.*)$",
816
+ r"\1",
817
+ s.section.rh.container.localpath(),
818
+ ),
819
+ )
820
+ for s in subgang.memberslist
821
+ if s.section.role == "GridpointSuperset"
822
+ ]
823
+ )
824
+ supersets_todo = [
825
+ (s, t) for s, t in supersets if not self.system.path.exists(t)
826
+ ]
827
+ if supersets_todo:
828
+ if len(ifilters):
829
+ parallel_grib_filter(
830
+ self.context,
831
+ [s for s, t in supersets_todo],
832
+ [t for s, t in supersets_todo],
833
+ filters=ifilters,
834
+ nthreads=self.gribfilter_tasks,
835
+ )
836
+ else:
837
+ parallel_grib_filter(
838
+ self.context,
839
+ [s for s, t in supersets_todo],
840
+ [t for s, t in supersets_todo],
841
+ cat=True,
842
+ nthreads=self.gribfilter_tasks,
843
+ )
844
+
845
+ # Tweak the namelist
846
+ namsec = self.setlink(
847
+ initrole="Namelist", initkind="namelist", initname="fort.4"
848
+ )
849
+ for nam in [x.rh for x in namsec if "NAM_PARAM" in x.rh.contents]:
850
+ logger.info(
851
+ "Substitute the date (%s) to AAAAMMJJHH namelist entry",
852
+ basedate.ymdh,
853
+ )
854
+ nam.contents["NAM_PARAM"]["AAAAMMJJHH"] = basedate.ymdh
855
+ logger.info(
856
+ "Substitute the number of members (%d) to NBRUN namelist entry",
857
+ len(members),
858
+ )
859
+ nam.contents["NAM_PARAM"]["NBRUN"] = len(members)
860
+ logger.info(
861
+ "Substitute the the number of terms to NECH(0) namelist entry"
862
+ )
863
+ nam.contents["NAM_PARAM"]["NECH(0)"] = 1
864
+ logger.info(
865
+ "Substitute the ressource term to NECH(1) namelist entry"
866
+ )
867
+ # NB: term should be expressed in minutes
868
+ nam.contents["NAM_PARAM"]["NECH(1)"] = int(myterm)
869
+ nam.contents["NAM_PARAM"]["ECHFINALE"] = finalterm.hour
870
+ # Now, update the model number for the GRIB files
871
+ logger.info(
872
+ "Substitute the model number (%d) to namelist entry",
873
+ self.numod,
874
+ )
875
+ nam.contents["NAM_PARAM"]["NMODELE"] = self.numod
876
+ # Add the NAM_PARAMPE block
877
+ if "NAM_NMEMBRES" in nam.contents:
878
+ # Cleaning is needed...
879
+ del nam.contents["NAM_NMEMBRES"]
880
+ newblock = nam.contents.newblock("NAM_NMEMBRES")
881
+ for i, member in enumerate(members):
882
+ newblock["NMEMBRES({:d})".format(i + 1)] = int(member)
883
+ # We are done with the namelist
884
+ nam.save()
885
+
886
+ # Standard execution
887
+ opts["loop"] = myterm
888
+ super().execute(rh, opts)
889
+
890
+ actualname = r"{:s}_{:s}\+{:s}".format(
891
+ self._method2output_map[self.method], mygeometry.area, myterm.fmthm
892
+ )
893
+ # Find out the output file and filter it
894
+ filtered_out = list()
895
+ if len(filters):
896
+ for candidate in [
897
+ f
898
+ for f in self.system.glob(
899
+ self._method2output_map[self.method] + "*"
900
+ )
901
+ if re.match(actualname, f)
902
+ ]:
903
+ logger.info("Starting GRIB filtering on %s.", candidate)
904
+ filtered_out.extend(
905
+ filters(candidate, candidate + "_{filtername:s}")
906
+ )
907
+
908
+ # The diagnostic output may be promised
909
+ expected = [
910
+ x
911
+ for x in self.promises
912
+ if (
913
+ re.match(actualname, x.rh.container.localpath())
914
+ or x.rh.container.localpath() in filtered_out
915
+ )
916
+ ]
917
+ for thispromise in expected:
918
+ thispromise.put(incache=True)
919
+
920
+ def execute(self, rh, opts):
921
+ """Loop on the various grib files provided."""
922
+
923
+ # Intialise a GRIBFilter for output files (at least try to)
924
+ gfilter = GRIBFilter(concatenate=False)
925
+ # We re-serialise data because footprints don't like dictionaries
926
+ ofilters = [
927
+ x.rh.contents.data
928
+ for x in self.context.sequence.effective_inputs(
929
+ role="GRIBFilteringRequest", kind="filtering_request"
930
+ )
931
+ ]
932
+ gfilter.add_filters(ofilters)
933
+
934
+ # Do we need to filter input files ?
935
+ # We re-serialise data because footprints don't like dictionaries
936
+ ifilters = [
937
+ json.dumps(x.rh.contents.data)
938
+ for x in self.context.sequence.effective_inputs(
939
+ role="GRIBInputFilteringRequest"
940
+ )
941
+ ]
942
+
943
+ # Monitor for the input files
944
+ bm = BasicInputMonitor(
945
+ self.context,
946
+ caching_freq=self.refreshtime,
947
+ role=(re.compile(r"^Gridpoint"), "Sources"),
948
+ kind="gridpoint",
949
+ )
950
+ # Check that the date is consistent among inputs
951
+ basedates = set()
952
+ members = set()
953
+ for rhI in [s.section.rh for s in bm.memberslist]:
954
+ basedates.add(rhI.resource.date)
955
+ members.add(rhI.provider.member)
956
+ if len(basedates) > 1:
957
+ raise AlgoComponentError(
958
+ "The date must be consistent among the input resources"
959
+ )
960
+ basedate = basedates.pop()
961
+ # Setup BasicGangs
962
+ basicmeta = AutoMetaGang()
963
+ basicmeta.autofill(
964
+ bm,
965
+ ("term", "safeblock", "geometry"),
966
+ allowmissing=self.missinglimit,
967
+ waitlimit=self.waitlimit,
968
+ )
969
+ # Find out what are the terms, domains and blocks
970
+ geometries = set()
971
+ terms = collections.defaultdict(set)
972
+ blocks = collections.defaultdict(set)
973
+ reverse = dict()
974
+ for m in basicmeta.memberslist:
975
+ (geo, term, block) = (
976
+ m.info["geometry"],
977
+ m.info["term"],
978
+ m.info["safeblock"],
979
+ )
980
+ geometries.add(geo)
981
+ terms[geo].add(term)
982
+ blocks[geo].add(block)
983
+ reverse[(geo, term, block)] = m
984
+ for geometry in geometries:
985
+ terms[geometry] = sorted(terms[geometry])
986
+ # Setup the MetaGang that fits our needs
987
+ complexmeta = MetaGang()
988
+ complexgangs = collections.defaultdict(collections.deque)
989
+ for geometry in geometries:
990
+ nterms = len(terms[geometry])
991
+ for i_term, term in enumerate(terms[geometry]):
992
+ elementary_meta = MetaGang()
993
+ elementary_meta.info = dict(geometry=geometry, term=term)
994
+ cterms = [
995
+ terms[geometry][i]
996
+ for i in range(i_term, min(i_term + 2, nterms))
997
+ ]
998
+ for inside_term in cterms:
999
+ for inside_block in blocks[geometry]:
1000
+ try:
1001
+ elementary_meta.add_member(
1002
+ reverse[(geometry, inside_term, inside_block)]
1003
+ )
1004
+ except KeyError:
1005
+ raise KeyError(
1006
+ "Something is wrong in the inputs: check again !"
1007
+ )
1008
+ complexmeta.add_member(elementary_meta)
1009
+ complexgangs[geometry].append(elementary_meta)
1010
+
1011
+ # Now, starts monitoring everything
1012
+ with bm:
1013
+ current_gang = dict()
1014
+ for geometry in geometries:
1015
+ try:
1016
+ current_gang[geometry] = complexgangs[geometry].popleft()
1017
+ except IndexError:
1018
+ current_gang[geometry] = None
1019
+
1020
+ while any([g is not None for g in current_gang.values()]):
1021
+ for geometry, a_gang in [
1022
+ (g, current_gang[g])
1023
+ for g in geometries
1024
+ if (
1025
+ current_gang[g] is not None
1026
+ and current_gang[g].state is not GangSt.ufo
1027
+ )
1028
+ ]:
1029
+ self._actual_execute(
1030
+ members,
1031
+ ifilters,
1032
+ gfilter,
1033
+ basedate,
1034
+ terms[geometry][-1],
1035
+ rh,
1036
+ opts,
1037
+ a_gang,
1038
+ )
1039
+
1040
+ # Next one
1041
+ try:
1042
+ current_gang[geometry] = complexgangs[
1043
+ geometry
1044
+ ].popleft()
1045
+ except IndexError:
1046
+ current_gang[geometry] = None
1047
+
1048
+ if not (
1049
+ bm.all_done
1050
+ or any(
1051
+ gang is not None and gang.state is not GangSt.ufo
1052
+ for gang in current_gang.values()
1053
+ )
1054
+ ):
1055
+ # Timeout ?
1056
+ bm.is_timedout(self.timeout, IOError)
1057
+ # Wait a little bit :-)
1058
+ time.sleep(1)
1059
+ bm.health_check(interval=30)
1060
+
1061
+
1062
+ @algo_component_deco_mixin_autodoc
1063
+ class _DiagPIDecoMixin(AlgoComponentDecoMixin):
1064
+ """Class variables and methods usefull for DiagPI."""
1065
+
1066
+ _MIXIN_EXTRA_FOOTPRINTS = [
1067
+ footprints.Footprint(
1068
+ attr=dict(
1069
+ kind=dict(
1070
+ values=["diagpi", "diaglabo"],
1071
+ ),
1072
+ numod=dict(
1073
+ info="The GRIB model number",
1074
+ type=int,
1075
+ optional=True,
1076
+ default=DelayedEnvValue("VORTEX_GRIB_NUMOD", 62),
1077
+ ),
1078
+ gribcat=dict(type=bool, optional=True, default=False),
1079
+ gribfilter_tasks=dict(
1080
+ type=int,
1081
+ optional=True,
1082
+ default=8,
1083
+ ),
1084
+ ),
1085
+ )
1086
+ ]
1087
+
1088
+ def _prepare_pihook(self, rh, opts):
1089
+ """Set some variables according to target definition."""
1090
+
1091
+ # Check for input files to concatenate
1092
+ if self.gribcat:
1093
+ srcsec = self.context.sequence.effective_inputs(
1094
+ role=("Gridpoint", "Sources", "Preview", "Previous"),
1095
+ kind="gridpoint",
1096
+ )
1097
+ cat_list_in = [sec for sec in srcsec if not sec.rh.is_expected()]
1098
+ outsec = self.context.sequence.effective_inputs(
1099
+ role="GridpointOutputPrepare"
1100
+ )
1101
+ cat_list_out = [sec for sec in outsec if not sec.rh.is_expected()]
1102
+ self._automatic_cat(cat_list_in, cat_list_out)
1103
+
1104
+ # prepare for delayed filtering
1105
+ self._delayed_filtering = []
1106
+
1107
+ def _postfix_pihook(self, rh, opts):
1108
+ """Filter outputs."""
1109
+ if self._delayed_filtering:
1110
+ self._batch_filter(self._delayed_filtering)
1111
+
1112
+ def _spawn_pihook(self):
1113
+ """Usually a good habit to dump the fort.4 namelist."""
1114
+ if self.system.path.exists("fort.4"):
1115
+ self.system.subtitle(
1116
+ "{:s} : dump namelist <fort.4>".format(self.realkind)
1117
+ )
1118
+ self.system.cat("fort.4", output=False)
1119
+
1120
+ _MIXIN_PREPARE_HOOKS = (_prepare_pihook,)
1121
+ _MIXIN_POSTFIX_HOOKS = (_postfix_pihook,)
1122
+ _MIXIN_SPAWN_HOOKS = (_spawn_pihook,)
1123
+
1124
+ def _automatic_cat(self, list_in, list_out):
1125
+ """Concatenate the *list_in* and *list_out* input files."""
1126
+ if self.gribcat:
1127
+ inputs = []
1128
+ outputs = []
1129
+ intents = []
1130
+ for seclist, intent in zip((list_in, list_out), ("in", "inout")):
1131
+ for isec in seclist:
1132
+ tmpin = isec.rh.container.localpath() + ".tmpcat"
1133
+ self.system.move(
1134
+ isec.rh.container.localpath(), tmpin, fmt="grib"
1135
+ )
1136
+ inputs.append(tmpin)
1137
+ outputs.append(isec.rh.container.localpath())
1138
+ intents.append(intent)
1139
+ parallel_grib_filter(
1140
+ self.context,
1141
+ inputs,
1142
+ outputs,
1143
+ intents,
1144
+ cat=True,
1145
+ nthreads=self.gribfilter_tasks,
1146
+ )
1147
+ for ifile in inputs:
1148
+ self.system.rm(ifile, fmt="grib")
1149
+
1150
+ def _batch_filter(self, candidates):
1151
+ """If no promises are made, the GRIB are filtered at once at the end."""
1152
+ # We re-serialise data because footprints don't like dictionaries
1153
+ filters = [
1154
+ json.dumps(x.rh.contents.data)
1155
+ for x in self.context.sequence.effective_inputs(
1156
+ role="GRIBFilteringRequest", kind="filtering_request"
1157
+ )
1158
+ ]
1159
+ parallel_grib_filter(
1160
+ self.context,
1161
+ candidates,
1162
+ [f + "_{filtername:s}" for f in candidates],
1163
+ filters=FPTuple(filters),
1164
+ nthreads=self.gribfilter_tasks,
1165
+ )
1166
+
1167
+ def _execute_picommons(self, rh, opts):
1168
+ """Loop on the various grib files provided."""
1169
+
1170
+ # Intialise a GRIBFilter (at least try to)
1171
+ gfilter = GRIBFilter(concatenate=False)
1172
+ gfilter.add_filters(self.context)
1173
+
1174
+ srcsec = self.context.sequence.effective_inputs(
1175
+ role=("Gridpoint", "Sources"), kind="gridpoint"
1176
+ )
1177
+ srcsec.sort(key=lambda s: s.rh.resource.term)
1178
+
1179
+ outsec = self.context.sequence.effective_inputs(
1180
+ role="GridpointOutputPrepare"
1181
+ )
1182
+ if outsec:
1183
+ outsec.sort(key=lambda s: s.rh.resource.term)
1184
+
1185
+ for sec in srcsec:
1186
+ r = sec.rh
1187
+ self.system.title(
1188
+ "Loop on domain {:s} and term {:s}".format(
1189
+ r.resource.geometry.area, r.resource.term.fmthm
1190
+ )
1191
+ )
1192
+ # Tweak the namelist
1193
+ namsec = self.setlink(
1194
+ initrole="Namelist", initkind="namelist", initname="fort.4"
1195
+ )
1196
+ for nam in [x.rh for x in namsec if "NAM_PARAM" in x.rh.contents]:
1197
+ logger.info(
1198
+ "Substitute the date (%s) to AAAAMMJJHH namelist entry",
1199
+ r.resource.date.ymdh,
1200
+ )
1201
+ nam.contents["NAM_PARAM"]["AAAAMMJJHH"] = r.resource.date.ymdh
1202
+ logger.info(
1203
+ "Substitute the the number of terms to NECH(0) namelist entry"
1204
+ )
1205
+ nam.contents["NAM_PARAM"]["NECH(0)"] = 1
1206
+ logger.info(
1207
+ "Substitute the ressource term to NECH(1) namelist entry"
1208
+ )
1209
+ # NB: term should be expressed in minutes
1210
+ nam.contents["NAM_PARAM"]["NECH(1)"] = int(r.resource.term)
1211
+ # Add the member number in a dedicated namelist block
1212
+ if r.provider.member is not None:
1213
+ mblock = nam.contents.newblock("NAM_PARAMPE")
1214
+ mblock["NMEMBER"] = int(r.provider.member)
1215
+ # Now, update the model number for the GRIB files
1216
+ if "NAM_DIAG" in nam.contents:
1217
+ nmod = self.numod
1218
+ logger.info(
1219
+ "Substitute the model number (%d) to namelist entry",
1220
+ nmod,
1221
+ )
1222
+ for namk in ("CONV", "BR", "HIV", "ECHOT", "ICA", "PSN"):
1223
+ if (
1224
+ namk in nam.contents["NAM_DIAG"]
1225
+ and nam.contents["NAM_DIAG"][namk] != 0
1226
+ ):
1227
+ nam.contents["NAM_DIAG"][namk] = nmod
1228
+ # We are done with the namelist
1229
+ nam.save()
1230
+
1231
+ cat_list_in = []
1232
+ cat_list_out = []
1233
+
1234
+ # Expect the input grib file to be here
1235
+ if sec.rh.is_expected():
1236
+ cat_list_in.append(sec)
1237
+ self.grab(sec, comment="diagpi source")
1238
+ if outsec:
1239
+ out = outsec.pop(0)
1240
+ assert out.rh.resource.term == sec.rh.resource.term
1241
+ if out.rh.is_expected():
1242
+ cat_list_out.append(out)
1243
+ self.grab(out, comment="diagpi output")
1244
+
1245
+ # Also link in previous grib files in order to compute some winter diagnostics
1246
+ srcpsec = [
1247
+ x
1248
+ for x in self.context.sequence.effective_inputs(
1249
+ role=("Preview", "Previous"), kind="gridpoint"
1250
+ )
1251
+ if x.rh.resource.term < r.resource.term
1252
+ ]
1253
+ for pr in srcpsec:
1254
+ if pr.rh.is_expected():
1255
+ cat_list_in.append(pr)
1256
+ self.grab(
1257
+ pr, comment="diagpi additional source for winter diag"
1258
+ )
1259
+
1260
+ self._automatic_cat(cat_list_in, cat_list_out)
1261
+
1262
+ # Standard execution
1263
+ opts["loop"] = r.resource.term
1264
+ super(self.mixin_execute_companion(), self).execute(rh, opts)
1265
+
1266
+ actualname = r"GRIB[-_A-Z]+{:s}\+{:s}(?:_member\d+)?$".format(
1267
+ r.resource.geometry.area, r.resource.term.fmthm
1268
+ )
1269
+ # Find out the output file and filter it
1270
+ filtered_out = list()
1271
+ if len(gfilter):
1272
+ for candidate in [
1273
+ f
1274
+ for f in self.system.glob("GRIB*")
1275
+ if re.match(actualname, f)
1276
+ ]:
1277
+ if len(self.promises):
1278
+ logger.info(
1279
+ "Starting GRIB filtering on %s.", candidate
1280
+ )
1281
+ filtered_out.extend(
1282
+ gfilter(candidate, candidate + "_{filtername:s}")
1283
+ )
1284
+ else:
1285
+ self._delayed_filtering.append(candidate)
1286
+
1287
+ # The diagnostic output may be promised
1288
+ expected = [
1289
+ x
1290
+ for x in self.promises
1291
+ if (
1292
+ re.match(actualname, x.rh.container.localpath())
1293
+ or x.rh.container.localpath() in filtered_out
1294
+ )
1295
+ ]
1296
+ for thispromise in expected:
1297
+ thispromise.put(incache=True)
1298
+
1299
+ _MIXIN_EXECUTE_OVERWRITE = _execute_picommons
1300
+
1301
+
1302
+ class DiagPI(BlindRun, _DiagPIDecoMixin, EcGribDecoMixin):
1303
+ """Execution of diagnostics on grib input (deterministic forecasts specific)."""
1304
+
1305
+ pass
1306
+
1307
+
1308
+ class DiagPIMPI(Parallel, _DiagPIDecoMixin, EcGribDecoMixin):
1309
+ """Execution of diagnostics on grib input (deterministic forecasts specific)."""
1310
+
1311
+ pass
1312
+
1313
+
1314
+ class Fa2GaussGrib(BlindRun, DrHookDecoMixin):
1315
+ """Standard FA conversion, e.g. with GOBPTOUT as a binary resource."""
1316
+
1317
+ _footprint = dict(
1318
+ attr=dict(
1319
+ kind=dict(
1320
+ values=["fa2gaussgrib"],
1321
+ ),
1322
+ fortinput=dict(
1323
+ optional=True,
1324
+ default="PFFPOS_FIELDS",
1325
+ ),
1326
+ numod=dict(
1327
+ type=int,
1328
+ optional=True,
1329
+ default=DelayedEnvValue("VORTEX_GRIB_NUMOD", 212),
1330
+ ),
1331
+ verbose=dict(
1332
+ type=bool,
1333
+ optional=True,
1334
+ default=False,
1335
+ ),
1336
+ )
1337
+ )
1338
+
1339
+ def execute(self, rh, opts):
1340
+ """Loop on the various initial conditions provided."""
1341
+
1342
+ thisoutput = "GRID_" + self.fortinput[7:14] + "1"
1343
+
1344
+ gpsec = self.context.sequence.effective_inputs(
1345
+ role=("Historic", "ModelState")
1346
+ )
1347
+ gpsec.sort(key=lambda s: s.rh.resource.term)
1348
+
1349
+ for sec in gpsec:
1350
+ r = sec.rh
1351
+
1352
+ self.system.title(
1353
+ "Loop on files: {:s}".format(r.container.localpath())
1354
+ )
1355
+
1356
+ # Some preventive cleaning
1357
+ self.system.remove(thisoutput)
1358
+ self.system.remove("fort.4")
1359
+
1360
+ # Build the local namelist block
1361
+ nb = NamelistBlock(name="NAML")
1362
+ nb.NBDOM = 1
1363
+ nb.INUMOD = self.numod
1364
+
1365
+ nb["LLBAVE"] = self.verbose
1366
+ nb["CDNOMF(1)"] = self.fortinput
1367
+ with open("fort.4", "w") as namfd:
1368
+ namfd.write(nb.dumps())
1369
+
1370
+ self.system.header(
1371
+ "{:s} : local namelist {:s} dump".format(
1372
+ self.realkind, "fort.4"
1373
+ )
1374
+ )
1375
+ self.system.cat("fort.4", output=False)
1376
+
1377
+ # Expect the input FP file source to be there...
1378
+ self.grab(sec, comment="fullpos source")
1379
+
1380
+ # Finally set the actual init file
1381
+ self.system.softlink(r.container.localpath(), self.fortinput)
1382
+
1383
+ # Standard execution
1384
+ super().execute(rh, opts)
1385
+
1386
+ # Freeze the current output
1387
+ if self.system.path.exists(thisoutput):
1388
+ self.system.move(
1389
+ thisoutput,
1390
+ "GGRID" + r.container.localpath()[6:],
1391
+ fmt="grib",
1392
+ )
1393
+ else:
1394
+ logger.warning("Missing some grib output for %s", thisoutput)
1395
+
1396
+ # Some cleaning
1397
+ self.system.rmall(self.fortinput)
1398
+
1399
+
1400
+ class Reverser(BlindRun, DrHookDecoMixin):
1401
+ """Compute the initial state for Ctpini."""
1402
+
1403
+ _footprint = dict(
1404
+ info="Compute initial state for Ctpini.",
1405
+ attr=dict(
1406
+ kind=dict(
1407
+ values=["reverser"],
1408
+ ),
1409
+ param_iter=dict(
1410
+ type=int,
1411
+ ),
1412
+ condlim=dict(
1413
+ type=int,
1414
+ ),
1415
+ ano_type=dict(
1416
+ type=int,
1417
+ ),
1418
+ ),
1419
+ )
1420
+
1421
+ def prepare(self, rh, opts):
1422
+ # Get info about the directives files directory
1423
+ directives = self.context.sequence.effective_inputs(
1424
+ role="Directives", kind="ctpini_directives_file"
1425
+ )
1426
+ if len(directives) < 1:
1427
+ logger.error("No directive file found. Stop")
1428
+ raise ValueError("No directive file found.")
1429
+ if len(directives) > 1:
1430
+ logger.warning(
1431
+ "Multiple directive files found. This is strange..."
1432
+ )
1433
+ # Substitute values in the simili namelist
1434
+ param = self.context.sequence.effective_inputs(role="Param")
1435
+ if len(param) < 1:
1436
+ logger.error("No parameter file found. Stop")
1437
+ raise ValueError("No parameter file found.")
1438
+ elif len(param) > 1:
1439
+ logger.warning(
1440
+ "Multiple files for parameter, the first %s is taken",
1441
+ param[0].rh.container.filename,
1442
+ )
1443
+ param = param[0].rh
1444
+ paramct = param.contents
1445
+ dictkeyvalue = dict()
1446
+ dictkeyvalue[r"param_iter"] = str(self.param_iter)
1447
+ dictkeyvalue[r"condlim"] = str(self.condlim)
1448
+ dictkeyvalue[r"ano_type"] = str(self.ano_type)
1449
+ paramct.setitems(dictkeyvalue)
1450
+ param.save()
1451
+ logger.info("Here is the parameter file (after substitution):")
1452
+ param.container.cat()
1453
+ # Call the parent's prepare
1454
+ super().prepare(rh, opts)
1455
+
1456
+
1457
+ class DegradedEnsembleDiagError(AlgoComponentError):
1458
+ """Exception raised when some of the members are missing."""
1459
+
1460
+ pass
1461
+
1462
+
1463
+ class FailedEnsembleDiagError(DegradedEnsembleDiagError):
1464
+ """Exception raised when too many members are missing."""
1465
+
1466
+ pass
1467
+
1468
+
1469
+ class PyEnsembleDiag(Expresso):
1470
+ """Execution of diagnostics on grib input (ensemble forecasts specific)."""
1471
+
1472
+ _footprint = dict(
1473
+ attr=dict(
1474
+ kind=dict(
1475
+ values=["py_diag_ens"],
1476
+ ),
1477
+ timeout=dict(
1478
+ type=int,
1479
+ optional=True,
1480
+ default=1200,
1481
+ ),
1482
+ refreshtime=dict(
1483
+ type=int,
1484
+ optional=True,
1485
+ default=20,
1486
+ ),
1487
+ missinglimit=dict(
1488
+ type=int,
1489
+ optional=True,
1490
+ default=0,
1491
+ ),
1492
+ waitlimit=dict(
1493
+ type=int,
1494
+ optional=True,
1495
+ default=900,
1496
+ ),
1497
+ ),
1498
+ )
1499
+
1500
+ def __init__(self, *kargs, **kwargs):
1501
+ super().__init__(*kargs, **kwargs)
1502
+ self._cl_args = dict()
1503
+
1504
+ def spawn_command_options(self):
1505
+ """Prepare options for the resource's command line."""
1506
+ return self._cl_args
1507
+
1508
+ def _actual_execute(self, rh, opts, input_rhs, **infos):
1509
+ """Actually run the script for a specific bunch of input files (**inpu_rhs**)."""
1510
+ output_fname = (
1511
+ "ensdiag_{safeblock:s}_{geometry.tag:s}_{term.fmthm}.grib".format(
1512
+ **infos
1513
+ )
1514
+ )
1515
+ self._cl_args = dict(flowconf="flowconf.json", output=output_fname)
1516
+
1517
+ # Create the JSON file that will be ingested by the script
1518
+ self.system.json_dump(
1519
+ dict(
1520
+ date=input_rhs[0].resource.date.ymdhm,
1521
+ term=infos["term"].fmthm,
1522
+ geometry=infos["geometry"].tag,
1523
+ area=infos["geometry"].area,
1524
+ block=infos["safeblock"],
1525
+ grib_files=[r.container.localpath() for r in input_rhs],
1526
+ ),
1527
+ self._cl_args["flowconf"],
1528
+ )
1529
+
1530
+ # Actualy run the post-processing script
1531
+ super().execute(rh, opts)
1532
+
1533
+ # The diagnostic output may be promised
1534
+ for thispromise in [
1535
+ x
1536
+ for x in self.promises
1537
+ if output_fname == x.rh.container.localpath()
1538
+ ]:
1539
+ thispromise.put(incache=True)
1540
+
1541
+ @staticmethod
1542
+ def _gang_txt_id(gang):
1543
+ """A string that identifies the input data currently being processed."""
1544
+ return (
1545
+ "term={term.fmthm:s}, "
1546
+ + "geometry={geometry.tag:s} "
1547
+ + "and block={safeblock:s}"
1548
+ ).format(**gang.info)
1549
+
1550
+ def _handle_gang_rescue(self, gang):
1551
+ """If some of the entries are missing, create a delayed exception."""
1552
+ if gang.state in (GangSt.pcollectable, GangSt.failed):
1553
+ txt_id = self._gang_txt_id(gang)
1554
+ self.system.subtitle("WARNING: Missing data for " + txt_id)
1555
+ for st in (EntrySt.ufo, EntrySt.failed, EntrySt.expected):
1556
+ if gang.members[st]:
1557
+ print(
1558
+ "Here is the list of Resource Handler with status < {:s} >:".format(
1559
+ st
1560
+ )
1561
+ )
1562
+ for i, e in enumerate(gang.members[st]):
1563
+ e.section.rh.quickview(nb=i + 1, indent=1)
1564
+ self.delayed_exception_add(
1565
+ FailedEnsembleDiagError(
1566
+ "Too many inputs are missing for " + txt_id
1567
+ )
1568
+ if gang.state == GangSt.failed
1569
+ else DegradedEnsembleDiagError(
1570
+ "Some of the inputs are missing for " + txt_id
1571
+ ),
1572
+ traceback=False,
1573
+ )
1574
+
1575
+ def execute(self, rh, opts):
1576
+ """Loop on the various grib files provided."""
1577
+
1578
+ # Monitor for the input files
1579
+ bm = BasicInputMonitor(
1580
+ self.context, caching_freq=self.refreshtime, role="Gridpoint"
1581
+ )
1582
+
1583
+ # Check that the date is consistent among inputs
1584
+ basedates = set()
1585
+ members = set()
1586
+ for rhI in [s.section.rh for s in bm.memberslist]:
1587
+ basedates.add(rhI.resource.date)
1588
+ members.add(rhI.provider.member)
1589
+ if len(basedates) > 1:
1590
+ raise AlgoComponentError(
1591
+ "The date must be consistent among the input resources"
1592
+ )
1593
+
1594
+ # Setup BasicGangs
1595
+ basicmeta = AutoMetaGang()
1596
+ basicmeta.autofill(
1597
+ bm,
1598
+ ("term", "safeblock", "geometry"),
1599
+ allowmissing=self.missinglimit,
1600
+ waitlimit=self.waitlimit,
1601
+ )
1602
+
1603
+ # Now, starts monitoring everything
1604
+ with bm:
1605
+ while basicmeta.has_ufo() or basicmeta.has_pcollectable():
1606
+ for thegang in basicmeta.consume_pcolectable():
1607
+ txt_id = self._gang_txt_id(thegang)
1608
+ self.system.title("Dealing with " + txt_id)
1609
+
1610
+ available = thegang.members[EntrySt.available]
1611
+ self._handle_gang_rescue(thegang)
1612
+
1613
+ self._actual_execute(
1614
+ rh,
1615
+ opts,
1616
+ [e.section.rh for e in available],
1617
+ **thegang.info,
1618
+ )
1619
+
1620
+ self.system.highlight("Done with " + txt_id)
1621
+
1622
+ if (
1623
+ not bm.all_done
1624
+ and basicmeta.has_ufo()
1625
+ and not basicmeta.has_pcollectable()
1626
+ ):
1627
+ # Timeout ?
1628
+ tmout = bm.is_timedout(self.timeout)
1629
+ if tmout:
1630
+ break
1631
+ # Wait a little bit :-)
1632
+ time.sleep(1)
1633
+ bm.health_check(interval=30)
1634
+
1635
+ # Warn for failed gangs
1636
+ if basicmeta.members[GangSt.failed]:
1637
+ self.system.title(
1638
+ "One or several (term, geometry, block) group(s) could not be processed"
1639
+ )
1640
+ for thegang in basicmeta.members[GangSt.failed]:
1641
+ self._handle_gang_rescue(thegang)