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