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,916 @@
1
+ """
2
+ AlgoComponents for OOPS.
3
+ """
4
+
5
+ import itertools
6
+ from collections import OrderedDict, defaultdict, namedtuple
7
+ import functools
8
+
9
+ import footprints
10
+ from bronx.fancies.dump import lightdump, fulldump
11
+ from bronx.stdtypes.date import Date, Time, Period
12
+ from bronx.compat.functools import cached_property
13
+
14
+ from vortex.algo.components import (
15
+ AlgoComponentError,
16
+ AlgoComponentDecoMixin,
17
+ Parallel,
18
+ )
19
+ from vortex.algo.components import algo_component_deco_mixin_autodoc
20
+ from vortex.data import geometries
21
+ from vortex.tools import grib
22
+ from ..syntax.stdattrs import ArpIfsSimplifiedCycle as IfsCycle
23
+ from ..syntax.stdattrs import algo_member, oops_members_terms_lists
24
+ from ..tools import drhook, odb, satrad
25
+
26
+ #: No automatic export
27
+ __all__ = []
28
+
29
+ logger = footprints.loggers.getLogger(__name__)
30
+
31
+
32
+ OOPSMemberInfos = namedtuple("OOPSMemberInfos", ("member", "date"))
33
+
34
+
35
+ class EnsSizeAlgoComponentError(AlgoComponentError):
36
+ """Exception raised when the ensemble is too small."""
37
+
38
+ def __init__(self, nominal_ens_size, actual_ens_size, min_ens_size):
39
+ self.nominal_ens_size = nominal_ens_size
40
+ self.actual_ens_size = actual_ens_size
41
+ self.min_ens_size = min_ens_size
42
+ super().__init__(
43
+ "{:d} found ({:d} required)".format(actual_ens_size, min_ens_size)
44
+ )
45
+
46
+ def __reduce__(self):
47
+ red = list(super().__reduce__())
48
+ red[1] = (
49
+ self.nominal_ens_size,
50
+ self.actual_ens_size,
51
+ self.min_ens_size,
52
+ )
53
+ return tuple(red)
54
+
55
+
56
+ @algo_component_deco_mixin_autodoc
57
+ class OOPSMemberDecoMixin(AlgoComponentDecoMixin):
58
+ """Add a member footprints' attribute and use it in the configuration files."""
59
+
60
+ _MIXIN_EXTRA_FOOTPRINTS = (algo_member,)
61
+
62
+ def _algo_member_deco_setup(self, rh, opts): # @UnusedVariable
63
+ """Update the configuration files."""
64
+ if self.member is not None:
65
+ self._generic_config_subs["member"] = self.member
66
+ for namrh in self.updatable_namelists:
67
+ namrh.contents.setmacro("MEMBER", self.member)
68
+ namrh.contents.setmacro("PERTURB", self.member)
69
+
70
+ _MIXIN_PREPARE_HOOKS = (_algo_member_deco_setup,)
71
+
72
+
73
+ @algo_component_deco_mixin_autodoc
74
+ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
75
+ """Tries to detect a members/terms list using the sequence's inputs
76
+
77
+ This mixin class is intended to be used with AlgoComponent classes. It will
78
+ automatically add footprints' attributes related to this feature, crawl into
79
+ the sequence's input after the ``prepare`` step and, depending on the result
80
+ of the members/terms detection add ``members`` and ``effterms`` entries into
81
+ the configuration file substitutions dictionary ``_generic_config_subs``.
82
+
83
+ :note: Effective terms are considered (i.e term - (current_date - resource_date))
84
+ """
85
+
86
+ _membersdetect_roles = tuple(
87
+ p + r
88
+ for p in ("", "Ensemble")
89
+ for r in (
90
+ "ModelState",
91
+ "Guess",
92
+ "InitialCondition",
93
+ "Background",
94
+ "SurfaceModelState",
95
+ "SurfaceGuess",
96
+ "SurfaceInitialCondition",
97
+ "SurfaceBackground",
98
+ )
99
+ )
100
+
101
+ _MIXIN_EXTRA_FOOTPRINTS = (
102
+ footprints.Footprint(
103
+ info="Abstract mbdetect footprint",
104
+ attr=dict(
105
+ ens_minsize=dict(
106
+ info="For a multi-member algocomponent, the minimum of the ensemble.",
107
+ optional=True,
108
+ type=int,
109
+ ),
110
+ ens_failure_conf_objects=dict(
111
+ info="For a multi-member algocomponent, alternative config file when the ensemble is too small.",
112
+ optional=True,
113
+ ),
114
+ strict_mbdetect=dict(
115
+ info="Performs a strict members/terms detection",
116
+ type=bool,
117
+ optional=True,
118
+ default=True,
119
+ doc_zorder=-60,
120
+ ),
121
+ ),
122
+ ),
123
+ )
124
+
125
+ @staticmethod
126
+ def _stateless_members_detect(
127
+ smap, basedate, section_check_cb, ensminsize=None, utest=False
128
+ ):
129
+ """
130
+ This method does not really need to be static but this way it allows for
131
+ unit-testing (see ``tests.tests_algo.test_oopspara.py``).
132
+ """
133
+
134
+ # Look for members
135
+ # The ensemble is possibly lagged... be careful
136
+ allmembers = defaultdict(
137
+ functools.partial(defaultdict, functools.partial(defaultdict, set))
138
+ )
139
+ members = set()
140
+ r_members = []
141
+ for arole, srole in smap.items():
142
+ # Gather data
143
+ for s in srole:
144
+ minfo = OOPSMemberInfos(
145
+ getattr(s.rh.provider, "member", None),
146
+ getattr(s.rh.resource, "date", None),
147
+ )
148
+ allmembers[arole][minfo][
149
+ getattr(s.rh.resource, "term", None)
150
+ ].add(s)
151
+ # Sanity checks and filtering
152
+ role_members_info = set(allmembers[arole].keys())
153
+ if None in {a_member.member for a_member in role_members_info}:
154
+ # Ignore sections when some sections have no members defined
155
+ if len(role_members_info) > 1:
156
+ logger.warning(
157
+ "Role: %s. Only some sections have a member number.",
158
+ arole,
159
+ )
160
+ role_members_info = set()
161
+ if len(role_members_info) > 1:
162
+ if not members:
163
+ members = role_members_info
164
+ else:
165
+ # Consistency check on members numbering
166
+ if members != role_members_info:
167
+ raise AlgoComponentError(
168
+ "Inconsistent members numbering"
169
+ )
170
+ else:
171
+ # If there is only one member, ignore it: it's not really an ensemble!
172
+ del allmembers[arole]
173
+
174
+ lagged = False
175
+ if members:
176
+ # Is it a lagged ensemble ?
177
+ members_by_date = defaultdict(set)
178
+ for m in members:
179
+ members_by_date[m.date].add(m.member)
180
+ lagged = len(members_by_date.keys()) > 1
181
+ # Be verbose...
182
+ if lagged:
183
+ for a_date, a_mset in members_by_date.items():
184
+ logger.info(
185
+ "Members detected from date=%s: %s",
186
+ a_date,
187
+ ",".join(sorted(str(m) for m in a_mset)),
188
+ )
189
+ else:
190
+ logger.info(
191
+ "Members detected: %s",
192
+ ",".join(sorted(str(m[0]) for m in members)),
193
+ )
194
+ logger.info("Total number of detected members: %d", len(members))
195
+ r_members = sorted(allmembers.keys())
196
+ logger.info("Members roles: %s", ",".join(r_members))
197
+
198
+ # Look for effective terms
199
+ alleffterms = dict()
200
+ for arole, srole in allmembers.items():
201
+ first_effterms = None
202
+ for minfo, mterms in srole.items():
203
+ effterms = set()
204
+ for term in mterms.keys():
205
+ effterms.add(
206
+ term - (basedate - minfo.date)
207
+ if term is not None and minfo.date is not None
208
+ else None
209
+ )
210
+ # Intra-role consistency
211
+ if first_effterms is None:
212
+ first_effterms = effterms
213
+ else:
214
+ # Consistency check on members numbering
215
+ if effterms != first_effterms:
216
+ raise AlgoComponentError(
217
+ "Inconsistent effective terms between members sets (role={:s})".format(
218
+ arole
219
+ )
220
+ )
221
+ # If there are more than one term, consider it
222
+ if len(first_effterms) > 1:
223
+ # Check that there is no None in the way
224
+ if None in first_effterms:
225
+ raise AlgoComponentError(
226
+ "For a given role, all of the resources or none of then should have a term (role={:s})".format(
227
+ arole
228
+ )
229
+ )
230
+ # Remove Nones
231
+ first_effterms = {e for e in first_effterms if e is not None}
232
+ if len(first_effterms):
233
+ alleffterms[arole] = first_effterms
234
+
235
+ # Check consistency and be verbose
236
+ r_effterms = []
237
+ l_effterms = []
238
+ if alleffterms:
239
+ # Hard check only when multiple effetive terms are found
240
+ multieffterms = {
241
+ r: ets for r, ets in alleffterms.items() if len(ets) > 1
242
+ }
243
+ if multieffterms:
244
+ if (
245
+ sum(1 for _ in itertools.groupby(multieffterms.values()))
246
+ > 1
247
+ ):
248
+ raise AlgoComponentError(
249
+ "Inconsistent effective terms between relevant roles"
250
+ )
251
+ r_effterms = sorted(multieffterms.keys())
252
+ _, l_effterms = multieffterms.popitem()
253
+ else:
254
+ if (
255
+ sum(1 for _ in itertools.groupby(alleffterms.values()))
256
+ == 1
257
+ ):
258
+ r_effterms = sorted(alleffterms.keys())
259
+ _, l_effterms = alleffterms.popitem()
260
+ l_effterms = sorted(l_effterms)
261
+ logger.info(
262
+ "Effective terms detected: %s",
263
+ ",".join([str(t) for t in effterms]),
264
+ )
265
+ logger.info(
266
+ "Terms roles: %s", ",".join(sorted(alleffterms.keys()))
267
+ )
268
+
269
+ # Theoretical ensemble size
270
+ nominal_ens_size = len(members)
271
+ if nominal_ens_size:
272
+ eff_members = set()
273
+ for mb in members:
274
+ # Look for missing resources in the various relevant roles
275
+ broken = list()
276
+ for arole in allmembers.keys():
277
+ broken.extend(
278
+ [
279
+ (s, arole)
280
+ for t, slist in allmembers[arole][mb].items()
281
+ for s in slist
282
+ if not section_check_cb(s)
283
+ ]
284
+ )
285
+ for s, arole in broken:
286
+ if not utest:
287
+ logger.warning(
288
+ "Missing items: %s (role: %s).",
289
+ s.rh.container.localpath(),
290
+ arole,
291
+ )
292
+ if broken:
293
+ logger.warning("Throwing away member: %s", mb)
294
+ else:
295
+ eff_members.add(mb)
296
+ # Sanity checks depending on ensminsize
297
+ if ensminsize is None and len(eff_members) != nominal_ens_size:
298
+ raise EnsSizeAlgoComponentError(
299
+ nominal_ens_size, len(eff_members), nominal_ens_size
300
+ )
301
+ elif ensminsize is not None and len(eff_members) < ensminsize:
302
+ raise EnsSizeAlgoComponentError(
303
+ nominal_ens_size, len(eff_members), ensminsize
304
+ )
305
+
306
+ members = eff_members
307
+
308
+ l_members = [m.member for m in sorted(members)]
309
+ l_members_d = [m.date for m in sorted(members)]
310
+ l_members_o = [
311
+ None if m.date is None else (basedate - m.date)
312
+ for m in sorted(members)
313
+ ]
314
+
315
+ return (
316
+ l_members,
317
+ l_members_d,
318
+ l_members_o,
319
+ l_effterms,
320
+ lagged,
321
+ nominal_ens_size,
322
+ r_members,
323
+ r_effterms,
324
+ )
325
+
326
+ def members_detect(self):
327
+ """Detect the members/terms list and update the substitution dictionary."""
328
+ sectionsmap = {
329
+ r: self.context.sequence.filtered_inputs(
330
+ role=r, no_alternates=True
331
+ )
332
+ for r in self._membersdetect_roles
333
+ }
334
+ try:
335
+ (
336
+ self._ens_members_num,
337
+ self._ens_members_date,
338
+ self._ens_members_offset,
339
+ self._ens_effterms,
340
+ self._ens_is_lagged,
341
+ self._ens_nominal_size,
342
+ _,
343
+ _,
344
+ ) = self._stateless_members_detect(
345
+ sectionsmap,
346
+ self.date,
347
+ self.context.sequence.is_somehow_viable,
348
+ self.ens_minsize,
349
+ )
350
+ except EnsSizeAlgoComponentError as e:
351
+ if self.strict_mbdetect and self.ens_failure_conf_objects is None:
352
+ raise
353
+ else:
354
+ logger.warning("Members detection failed: %s", str(e))
355
+ logger.info(
356
+ "'strict_mbdetect' is False... going on with empty lists."
357
+ )
358
+ self._ens_members_num = []
359
+ self._ens_members_date = []
360
+ self._ens_members_offset = []
361
+ self._ens_is_lagged = False
362
+ self._ens_effterms = []
363
+ self._ens_nominal_size = e.nominal_ens_size
364
+ if self.ens_failure_conf_objects:
365
+ # Find the new configuration object
366
+ main_conf = None
367
+ for sconf, ssub in self._individual_config_subs.items():
368
+ if (
369
+ getattr(sconf.rh.resource, "objects", "")
370
+ == self.ens_failure_conf_objects
371
+ ):
372
+ main_conf = sconf
373
+ main_conf_sub = ssub
374
+ break
375
+ if main_conf is None:
376
+ raise AlgoComponentError(
377
+ "Alternative configuration file was not found"
378
+ )
379
+ # Update the config ordered dictionary
380
+ del self._individual_config_subs[main_conf]
381
+ new_individual_config_subs = OrderedDict()
382
+ new_individual_config_subs[main_conf] = main_conf_sub
383
+ for sconf, ssub in self._individual_config_subs.items():
384
+ new_individual_config_subs[sconf] = ssub
385
+ self._individual_config_subs = new_individual_config_subs
386
+ logger.info(
387
+ "Using an alternative configuration file (objects=%s, role=%s)",
388
+ self.ens_failure_conf_objects,
389
+ main_conf.role,
390
+ )
391
+
392
+ self._generic_config_subs["ens_members_num"] = self._ens_members_num
393
+ self._generic_config_subs["ens_members_date"] = self._ens_members_date
394
+ self._generic_config_subs["ens_members_offset"] = (
395
+ self._ens_members_offset
396
+ )
397
+ self._generic_config_subs["ens_is_lagged"] = self._ens_is_lagged
398
+ # Legacy:
399
+ self._generic_config_subs["members"] = self._ens_members_num
400
+ # Namelist stuff
401
+ for namrh in self.updatable_namelists:
402
+ namrh.contents.setmacro("ENS_MEMBERS", len(self._ens_members_num))
403
+ if self._ens_members_num:
404
+ namrh.contents.setmacro(
405
+ "ENS_AUTO_NSTRIN", len(self._ens_members_num)
406
+ )
407
+ else:
408
+ namrh.contents.setmacro(
409
+ "ENS_AUTO_NSTRIN", self._ens_nominal_size
410
+ )
411
+ self._generic_config_subs["ens_effterms"] = self._ens_effterms
412
+ # Legacy:
413
+ self._generic_config_subs["effterms"] = self._ens_effterms
414
+
415
+ def _membersd_setup(self, rh, opts): # @UnusedVariable
416
+ """Set up the members/terms detection."""
417
+ self.members_detect()
418
+
419
+ _MIXIN_PREPARE_HOOKS = (_membersd_setup,)
420
+
421
+
422
+ @algo_component_deco_mixin_autodoc
423
+ class OOPSMembersTermsDecoMixin(AlgoComponentDecoMixin):
424
+ """Adds members/terms footprints' attributes and use them in configuration files.
425
+
426
+ This mixin class is intended to be used with AlgoComponent classes. It will
427
+ automatically add footprints' attributes ``members`` and ``terms`` and add
428
+ the corresponding ``members`` and ``effterms`` entries into
429
+ the configuration file substitutions dictionary ``_generic_config_subs``.
430
+ """
431
+
432
+ _MIXIN_EXTRA_FOOTPRINTS = (oops_members_terms_lists,)
433
+
434
+ def _membersterms_deco_setup(self, rh, opts): # @UnusedVariable
435
+ """Setup the configuration file."""
436
+ actualmembers = [
437
+ m if isinstance(m, int) else int(m) for m in self.members
438
+ ]
439
+ actualterms = [
440
+ t if isinstance(t, Time) else Time(t) for t in self.terms
441
+ ]
442
+ self._generic_config_subs["members"] = actualmembers
443
+ self._generic_config_subs["effterms"] = actualterms
444
+
445
+ _MIXIN_PREPARE_HOOKS = (_membersterms_deco_setup,)
446
+
447
+
448
+ @algo_component_deco_mixin_autodoc
449
+ class OOPSTimestepDecoMixin(AlgoComponentDecoMixin):
450
+ """Add a timsestep attribute and handle substitutions."""
451
+
452
+ _MIXIN_EXTRA_FOOTPRINTS = (
453
+ footprints.Footprint(
454
+ info="Abstract timestep footprint",
455
+ attr=dict(
456
+ timestep=dict(
457
+ info="A possible model timestep (in seconds).",
458
+ optional=True,
459
+ type=float,
460
+ ),
461
+ ),
462
+ ),
463
+ )
464
+
465
+ def _timestep_deco_setup(self, rh, opts): # @UnusedVariable
466
+ """Set up the timestep in config and namelists."""
467
+ if self.timestep is not None:
468
+ self._generic_config_subs["timestep"] = Period(
469
+ seconds=self.timestep
470
+ )
471
+ logger.info("Set macro TIMESTEP=%f in namelists.", self.timestep)
472
+ for namrh in self.updatable_namelists:
473
+ namrh.contents.setmacro("TIMESTEP", self.timestep)
474
+
475
+ _MIXIN_PREPARE_HOOKS = (_timestep_deco_setup,)
476
+
477
+
478
+ @algo_component_deco_mixin_autodoc
479
+ class OOPSIncrementalDecoMixin(AlgoComponentDecoMixin):
480
+ """Add incremental attributes and handle substitutions."""
481
+
482
+ _MIXIN_EXTRA_FOOTPRINTS = (
483
+ footprints.Footprint(
484
+ info="Abstract incremental_* footprint",
485
+ attr=dict(
486
+ incremental_tsteps=dict(
487
+ info="Timestep for each of the outer loop iteration (in seconds).",
488
+ optional=True,
489
+ type=footprints.FPList,
490
+ default=footprints.FPList(),
491
+ ),
492
+ incremental_niters=dict(
493
+ info="Inner loop size for each of the outer loop iteration.",
494
+ optional=True,
495
+ type=footprints.FPList,
496
+ default=footprints.FPList(),
497
+ ),
498
+ incremental_geos=dict(
499
+ info="Geometry for each of the outer loop iteration.",
500
+ optional=True,
501
+ type=footprints.FPList,
502
+ default=footprints.FPList(),
503
+ ),
504
+ ),
505
+ ),
506
+ )
507
+
508
+ def _incremental_deco_setup(self, rh, opts): # @UnusedVariable
509
+ """Set up the incremental DA settings in config and namelists."""
510
+ if self.incremental_tsteps or self.incremental_niters:
511
+ sizes = {
512
+ len(t)
513
+ for t in [
514
+ self.incremental_tsteps,
515
+ self.incremental_niters,
516
+ self.incremental_geos,
517
+ ]
518
+ if t
519
+ }
520
+ if len(sizes) != 1:
521
+ raise ValueError(
522
+ "Inconsistent sizes between incr_tsteps and incr_niters"
523
+ )
524
+ actual_tsteps = [float(t) for t in (self.incremental_tsteps or ())]
525
+ actual_tsteps_p = [Period(seconds=t) for t in actual_tsteps]
526
+ actual_niters = [int(t) for t in (self.incremental_niters or ())]
527
+ actual_geos = [
528
+ g
529
+ if isinstance(g, geometries.Geometry)
530
+ else geometries.get(tag=g)
531
+ for g in (self.incremental_geos or ())
532
+ ]
533
+ if actual_tsteps:
534
+ self._generic_config_subs["incremental_tsteps"] = (
535
+ actual_tsteps_p
536
+ )
537
+ for upd_i, tstep in enumerate(actual_tsteps, start=1):
538
+ logger.info(
539
+ "Set macro UPD%d_TIMESTEP=%f macro in namelists.",
540
+ upd_i,
541
+ tstep,
542
+ )
543
+ for namrh in self.updatable_namelists:
544
+ namrh.contents.setmacro(
545
+ "UPD{:d}_TIMESTEP".format(upd_i), tstep
546
+ )
547
+ if actual_niters:
548
+ self._generic_config_subs["incremental_niters"] = actual_niters
549
+ for upd_i, niter in enumerate(actual_niters, start=1):
550
+ logger.info(
551
+ "Set macro UPD%d_NITER=%d macro in namelists.",
552
+ upd_i,
553
+ niter,
554
+ )
555
+ for namrh in self.updatable_namelists:
556
+ namrh.contents.setmacro(
557
+ "UPD{:d}_NITER".format(upd_i), niter
558
+ )
559
+ if actual_geos:
560
+ self._generic_config_subs["incremental_geos"] = actual_geos
561
+
562
+ _MIXIN_PREPARE_HOOKS = (_incremental_deco_setup,)
563
+
564
+
565
+ class OOPSParallel(
566
+ Parallel,
567
+ drhook.DrHookDecoMixin,
568
+ grib.EcGribDecoMixin,
569
+ satrad.SatRadDecoMixin,
570
+ ):
571
+ """Abstract AlgoComponent for any OOPS run."""
572
+
573
+ _abstract = True
574
+ _footprint = dict(
575
+ info="Any OOPS Run (abstract).",
576
+ attr=dict(
577
+ kind=dict(
578
+ values=["oorun"],
579
+ ),
580
+ date=dict(
581
+ info="The current run date.",
582
+ access="rwx",
583
+ type=Date,
584
+ doc_zorder=-50,
585
+ ),
586
+ config_subs=dict(
587
+ info="Substitutions to be performed in the config file (before run)",
588
+ optional=True,
589
+ type=footprints.FPDict,
590
+ default=footprints.FPDict(),
591
+ doc_zorder=-60,
592
+ ),
593
+ binarysingle=dict(
594
+ default="basicnwp",
595
+ ),
596
+ ),
597
+ )
598
+
599
+ def __init__(self, *kargs, **kwargs):
600
+ """Declare some hidden attributes for a later use."""
601
+ super().__init__(*kargs, **kwargs)
602
+ self._oops_cycle = None
603
+ self._generic_config_subs = dict()
604
+ self._individual_config_subs = OrderedDict()
605
+ self._last_l_subs = dict()
606
+
607
+ @property
608
+ def oops_cycle(self):
609
+ """The binary's cycle number."""
610
+ return self._oops_cycle
611
+
612
+ def valid_executable(self, rh):
613
+ """Be sure that the specified executable has a cycle attribute."""
614
+ valid = super().valid_executable(rh)
615
+ if hasattr(rh.resource, "cycle"):
616
+ self._oops_cycle = rh.resource.cycle
617
+ return valid
618
+ else:
619
+ logger.error("The binary < %s > has no cycle attribute", repr(rh))
620
+ return False
621
+
622
+ def _mpitool_attributes(self, opts):
623
+ conf_dict = super()._mpitool_attributes(opts)
624
+ conf_dict.update({"mplbased": True})
625
+ return conf_dict
626
+
627
+ def prepare(self, rh, opts):
628
+ """Preliminary setups."""
629
+ super().prepare(rh, opts)
630
+ # Look for channels namelists and set appropriate links
631
+ self.setchannels()
632
+ # Register all of the config files
633
+ self.set_config_rendering()
634
+ # Looking for low-level-libs defaults...
635
+ self.boost_defaults()
636
+ self.eckit_defaults()
637
+
638
+ def spawn_hook(self):
639
+ """Perform configuration file rendering before executing the binary."""
640
+ self.do_config_rendering()
641
+ self.do_namelist_rendering()
642
+ super().spawn_hook()
643
+
644
+ def spawn_command_options(self):
645
+ """Prepare options for the binary's command line."""
646
+ mconfig = list(self._individual_config_subs.keys())[0]
647
+ configfile = mconfig.rh.container.localpath()
648
+ options = {"configfile": configfile}
649
+ return options
650
+
651
+ @cached_property
652
+ def updatable_namelists(self):
653
+ return [
654
+ s.rh
655
+ for s in self.context.sequence.effective_inputs(role="Namelist")
656
+ ]
657
+
658
+ def set_config_rendering(self):
659
+ """
660
+ Look into effective inputs for configuration files and register them for
661
+ a later rendering using bronx' templating system.
662
+ """
663
+ mconfig = self.context.sequence.effective_inputs(role="MainConfig")
664
+ gconfig = self.context.sequence.effective_inputs(role="Config")
665
+ if len(mconfig) > 1:
666
+ raise AlgoComponentError(
667
+ "Only one Main Config section may be provided."
668
+ )
669
+ if len(mconfig) == 0 and len(gconfig) != 1:
670
+ raise AlgoComponentError(
671
+ "Please provide a Main Config section or a unique Config section."
672
+ )
673
+ if len(mconfig) == 1:
674
+ gconfig.insert(0, mconfig[0])
675
+ self._individual_config_subs = {sconf: dict() for sconf in gconfig}
676
+
677
+ def do_config_rendering(self):
678
+ """Render registered configuration files using the bronx' templating system."""
679
+ l_first = True
680
+ for sconf, sdict in self._individual_config_subs.items():
681
+ self.system.subtitle(
682
+ "Configuration file rendering for: {:s}".format(
683
+ sconf.rh.container.localpath()
684
+ )
685
+ )
686
+ l_subs = dict(now=self.date, date=self.date)
687
+ l_subs.update(self._generic_config_subs)
688
+ l_subs.update(sdict)
689
+ l_subs.update(self.config_subs)
690
+ if l_subs != self._last_l_subs.get(sconf, dict()):
691
+ if not hasattr(sconf.rh.contents, "bronx_tpl_render"):
692
+ logger.error(
693
+ 'The < %s > content object has no "bronx_tpl_render" method. Skipping it.',
694
+ repr(sconf.rh.contents),
695
+ )
696
+ continue
697
+ try:
698
+ sconf.rh.contents.bronx_tpl_render(**l_subs)
699
+ except Exception:
700
+ logger.error(
701
+ "The config file rendering failed. The substitution dict was: \n%s",
702
+ lightdump(l_subs),
703
+ )
704
+ raise
705
+ self._last_l_subs[sconf] = l_subs
706
+ if l_first:
707
+ print(fulldump(sconf.rh.contents.data))
708
+ sconf.rh.save()
709
+ else:
710
+ logger.info(
711
+ "It's not necessary to update the file (no changes)."
712
+ )
713
+ l_first = False
714
+
715
+ def do_namelist_rendering(self):
716
+ todo = [
717
+ r
718
+ for r in self.updatable_namelists
719
+ if r.contents.dumps_needs_update
720
+ ]
721
+ self.system.subtitle("Updating namelists")
722
+ if todo:
723
+ for namrh in todo:
724
+ logger.info("Rewriting %s.", namrh.container.localpath())
725
+ namrh.save()
726
+ else:
727
+ logger.info("None of the namelists need to be rewritten.")
728
+
729
+ def boost_defaults(self):
730
+ """Set defaults for BOOST environment variables.
731
+
732
+ Do not overwrite pre-initialised ones. The default list of variables
733
+ depends on the code's cycle number.
734
+ """
735
+ defaults = {
736
+ IfsCycle("cy1"): {
737
+ "BOOST_TEST_CATCH_SYSTEM_ERRORS": "no",
738
+ "BOOST_TEST_DETECT_FP_EXCEPTIONS": "no",
739
+ "BOOST_TEST_LOG_FORMAT": "XML",
740
+ "BOOST_TEST_LOG_LEVEL": "message",
741
+ "BOOST_TEST_OUTPUT_FORMAT": "XML",
742
+ "BOOST_TEST_REPORT_FORMAT": "XML",
743
+ "BOOST_TEST_RESULT_CODE": "yes",
744
+ }
745
+ }
746
+ cydefaults = None
747
+ for k, defdict in sorted(defaults.items(), reverse=True):
748
+ if k < self.oops_cycle:
749
+ cydefaults = defdict
750
+ break
751
+ self.algoassert(
752
+ cydefaults is not None,
753
+ "BOOST defaults not found for cycle: {!s}".format(self.oops_cycle),
754
+ )
755
+ logger.info("Setting up BOOST defaults:%s", lightdump(cydefaults))
756
+ self.env.default(**cydefaults)
757
+
758
+ def eckit_defaults(self):
759
+ """Set defaults for eckit environment variables.
760
+
761
+ Do not overwrite pre-initialised ones. The default list of variables
762
+ depends on the code's cycle number.
763
+ """
764
+ defaults = {
765
+ IfsCycle("cy1"): {
766
+ "ECKIT_MPI_INIT_THREAD": (
767
+ "MPI_THREAD_MULTIPLE"
768
+ if int(self.env.get("OMP_NUM_THREADS", "1")) > 1
769
+ else "MPI_THREAD_SINGLE"
770
+ ),
771
+ }
772
+ }
773
+ cydefaults = None
774
+ for k, defdict in sorted(defaults.items(), reverse=True):
775
+ if k < self.oops_cycle:
776
+ cydefaults = defdict
777
+ break
778
+ self.algoassert(
779
+ cydefaults is not None,
780
+ "eckit defaults not found for cycle: {!s}".format(self.oops_cycle),
781
+ )
782
+ logger.info("Setting up eckit defaults:%s", lightdump(cydefaults))
783
+ self.env.default(**cydefaults)
784
+
785
+
786
+ class OOPSODB(OOPSParallel, odb.OdbComponentDecoMixin):
787
+ """Abstract AlgoComponent for any OOPS run requiring ODB databases."""
788
+
789
+ _abstract = True
790
+ _footprint = dict(
791
+ info="OOPS ObsOperator Test run.",
792
+ attr=dict(
793
+ kind=dict(
794
+ values=["oorunodb"],
795
+ ),
796
+ binarysingle=dict(
797
+ default="basicnwpobsort",
798
+ ),
799
+ ),
800
+ )
801
+
802
+ #: If ``True``, an empty CCMA database will be created before the run and
803
+ #: necessary environment variables will be added in order for the executable
804
+ #: to populate this database at the end of the run.
805
+ _OOPSODB_CCMA_DIRECT = False
806
+
807
+ def prepare(self, rh, opts):
808
+ """Setup ODB stuff."""
809
+ super().prepare(rh, opts)
810
+ sh = self.system
811
+
812
+ # Looking for input observations
813
+ allodb = self.lookupodb()
814
+ allcma = [
815
+ x for x in allodb if x.rh.resource.layout.lower() == self.virtualdb
816
+ ]
817
+ if self.virtualdb.lower() == "ccma":
818
+ self.algoassert(
819
+ len(allcma) == 1, "A unique CCMA database is to be provided."
820
+ )
821
+ self.algoassert(
822
+ not self._OOPSODB_CCMA_DIRECT,
823
+ "_OOPSODB_CCMA_DIRECT needs to be False if virtualdb=ccma.",
824
+ )
825
+ cma = allcma[0]
826
+ cma_path = sh.path.abspath(cma.rh.container.localpath())
827
+ else:
828
+ cma_path = self.odb_merge_if_needed(allcma)
829
+ if self._OOPSODB_CCMA_DIRECT:
830
+ ccma_path = self.odb_create_db(layout="CCMA")
831
+ self.odb.fix_db_path("CCMA", ccma_path)
832
+
833
+ # Set ODB environment
834
+ self.odb.fix_db_path(self.virtualdb, cma_path)
835
+
836
+ if self._OOPSODB_CCMA_DIRECT:
837
+ self.odb.ioassign_gather(cma_path, ccma_path)
838
+ else:
839
+ self.odb.ioassign_gather(cma_path)
840
+
841
+ if self.virtualdb.lower() != "ccma":
842
+ self.odb.create_poolmask(self.virtualdb, cma_path)
843
+ self.odb.shuffle_setup(
844
+ self.slots,
845
+ mergedirect=True,
846
+ ccmadirect=self._OOPSODB_CCMA_DIRECT,
847
+ )
848
+
849
+ # Fix the input databases intent
850
+ self.odb_rw_or_overwrite_method(*allcma)
851
+
852
+ # Look for extras ODB raw
853
+ self.odb_handle_raw_dbs()
854
+
855
+ # Allow assimilation window / timeslots configuration
856
+ self._generic_config_subs["window_length"] = self.slots.window
857
+ self._generic_config_subs["window_lmargin"] = Period(-self.slots.start)
858
+ self._generic_config_subs["window_rmargin"] = (
859
+ self.slots.window + self.slots.start
860
+ )
861
+ self._generic_config_subs["timeslot_length"] = self.slots.chunk
862
+ self._generic_config_subs["timeslot_centered"] = self.slots.center
863
+ self._generic_config_subs["timeslot_centers"] = (
864
+ self.slots.as_centers_fromstart()
865
+ )
866
+
867
+
868
+ class OOPSAnalysis(
869
+ OOPSODB,
870
+ OOPSTimestepDecoMixin,
871
+ OOPSIncrementalDecoMixin,
872
+ OOPSMemberDecoMixin,
873
+ OOPSMembersTermsDetectDecoMixin,
874
+ ):
875
+ """Any kind of OOPS analysis (screening/thining step excluded)."""
876
+
877
+ _footprint = dict(
878
+ info="OOPS minimisation.",
879
+ attr=dict(
880
+ kind=dict(
881
+ values=["ooanalysis", "oominim"],
882
+ remap=dict(autoremap="first"),
883
+ ),
884
+ virtualdb=dict(
885
+ default="ccma",
886
+ ),
887
+ withscreening=dict(
888
+ values=[
889
+ False,
890
+ ],
891
+ type=bool,
892
+ optional=True,
893
+ default=False,
894
+ ),
895
+ ),
896
+ )
897
+
898
+
899
+ class OOPSAnalysisWithScreening(OOPSAnalysis):
900
+ """Any kind of OOPS analysis with screening/thining step."""
901
+
902
+ _OOPSODB_CCMA_DIRECT = True
903
+
904
+ _footprint = dict(
905
+ attr=dict(
906
+ virtualdb=dict(
907
+ default="ecma",
908
+ ),
909
+ withscreening=dict(
910
+ values=[
911
+ True,
912
+ ],
913
+ optional=False,
914
+ ),
915
+ )
916
+ )