vortex-nwp 2.0.0b1__py3-none-any.whl → 2.1.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 (141) hide show
  1. vortex/__init__.py +75 -47
  2. vortex/algo/__init__.py +3 -2
  3. vortex/algo/components.py +944 -618
  4. vortex/algo/mpitools.py +802 -497
  5. vortex/algo/mpitools_templates/__init__.py +1 -0
  6. vortex/algo/serversynctools.py +34 -33
  7. vortex/config.py +19 -22
  8. vortex/data/__init__.py +9 -3
  9. vortex/data/abstractstores.py +593 -655
  10. vortex/data/containers.py +217 -162
  11. vortex/data/contents.py +65 -39
  12. vortex/data/executables.py +93 -102
  13. vortex/data/flow.py +40 -34
  14. vortex/data/geometries.py +228 -132
  15. vortex/data/handlers.py +436 -227
  16. vortex/data/outflow.py +15 -15
  17. vortex/data/providers.py +185 -163
  18. vortex/data/resources.py +48 -42
  19. vortex/data/stores.py +540 -417
  20. vortex/data/sync_templates/__init__.py +0 -0
  21. vortex/gloves.py +114 -87
  22. vortex/layout/__init__.py +1 -8
  23. vortex/layout/contexts.py +150 -84
  24. vortex/layout/dataflow.py +353 -202
  25. vortex/layout/monitor.py +264 -128
  26. vortex/nwp/__init__.py +5 -2
  27. vortex/nwp/algo/__init__.py +14 -5
  28. vortex/nwp/algo/assim.py +205 -151
  29. vortex/nwp/algo/clim.py +683 -517
  30. vortex/nwp/algo/coupling.py +447 -225
  31. vortex/nwp/algo/eda.py +437 -229
  32. vortex/nwp/algo/eps.py +403 -231
  33. vortex/nwp/algo/forecasts.py +416 -275
  34. vortex/nwp/algo/fpserver.py +683 -307
  35. vortex/nwp/algo/ifsnaming.py +205 -145
  36. vortex/nwp/algo/ifsroot.py +215 -122
  37. vortex/nwp/algo/monitoring.py +137 -76
  38. vortex/nwp/algo/mpitools.py +330 -190
  39. vortex/nwp/algo/odbtools.py +637 -353
  40. vortex/nwp/algo/oopsroot.py +454 -273
  41. vortex/nwp/algo/oopstests.py +90 -56
  42. vortex/nwp/algo/request.py +287 -206
  43. vortex/nwp/algo/stdpost.py +878 -522
  44. vortex/nwp/data/__init__.py +22 -4
  45. vortex/nwp/data/assim.py +125 -137
  46. vortex/nwp/data/boundaries.py +121 -68
  47. vortex/nwp/data/climfiles.py +193 -211
  48. vortex/nwp/data/configfiles.py +73 -69
  49. vortex/nwp/data/consts.py +426 -401
  50. vortex/nwp/data/ctpini.py +59 -43
  51. vortex/nwp/data/diagnostics.py +94 -66
  52. vortex/nwp/data/eda.py +50 -51
  53. vortex/nwp/data/eps.py +195 -146
  54. vortex/nwp/data/executables.py +440 -434
  55. vortex/nwp/data/fields.py +63 -48
  56. vortex/nwp/data/gridfiles.py +183 -111
  57. vortex/nwp/data/logs.py +250 -217
  58. vortex/nwp/data/modelstates.py +180 -151
  59. vortex/nwp/data/monitoring.py +72 -99
  60. vortex/nwp/data/namelists.py +254 -202
  61. vortex/nwp/data/obs.py +400 -308
  62. vortex/nwp/data/oopsexec.py +22 -20
  63. vortex/nwp/data/providers.py +90 -65
  64. vortex/nwp/data/query.py +71 -82
  65. vortex/nwp/data/stores.py +49 -36
  66. vortex/nwp/data/surfex.py +136 -137
  67. vortex/nwp/syntax/__init__.py +1 -1
  68. vortex/nwp/syntax/stdattrs.py +173 -111
  69. vortex/nwp/tools/__init__.py +2 -2
  70. vortex/nwp/tools/addons.py +22 -17
  71. vortex/nwp/tools/agt.py +24 -12
  72. vortex/nwp/tools/bdap.py +16 -5
  73. vortex/nwp/tools/bdcp.py +4 -1
  74. vortex/nwp/tools/bdm.py +3 -0
  75. vortex/nwp/tools/bdmp.py +14 -9
  76. vortex/nwp/tools/conftools.py +728 -378
  77. vortex/nwp/tools/drhook.py +12 -8
  78. vortex/nwp/tools/grib.py +65 -39
  79. vortex/nwp/tools/gribdiff.py +22 -17
  80. vortex/nwp/tools/ifstools.py +82 -42
  81. vortex/nwp/tools/igastuff.py +167 -143
  82. vortex/nwp/tools/mars.py +14 -2
  83. vortex/nwp/tools/odb.py +234 -125
  84. vortex/nwp/tools/partitioning.py +61 -37
  85. vortex/nwp/tools/satrad.py +27 -12
  86. vortex/nwp/util/async.py +83 -55
  87. vortex/nwp/util/beacon.py +10 -10
  88. vortex/nwp/util/diffpygram.py +174 -86
  89. vortex/nwp/util/ens.py +144 -63
  90. vortex/nwp/util/hooks.py +30 -19
  91. vortex/nwp/util/taskdeco.py +28 -24
  92. vortex/nwp/util/usepygram.py +278 -172
  93. vortex/nwp/util/usetnt.py +31 -17
  94. vortex/sessions.py +72 -39
  95. vortex/syntax/__init__.py +1 -1
  96. vortex/syntax/stdattrs.py +410 -171
  97. vortex/syntax/stddeco.py +31 -22
  98. vortex/toolbox.py +327 -192
  99. vortex/tools/__init__.py +11 -2
  100. vortex/tools/actions.py +110 -121
  101. vortex/tools/addons.py +111 -92
  102. vortex/tools/arm.py +42 -22
  103. vortex/tools/compression.py +72 -69
  104. vortex/tools/date.py +11 -4
  105. vortex/tools/delayedactions.py +242 -132
  106. vortex/tools/env.py +75 -47
  107. vortex/tools/folder.py +342 -171
  108. vortex/tools/grib.py +341 -162
  109. vortex/tools/lfi.py +423 -216
  110. vortex/tools/listings.py +109 -40
  111. vortex/tools/names.py +218 -156
  112. vortex/tools/net.py +655 -299
  113. vortex/tools/parallelism.py +93 -61
  114. vortex/tools/prestaging.py +55 -31
  115. vortex/tools/schedulers.py +172 -105
  116. vortex/tools/services.py +403 -334
  117. vortex/tools/storage.py +293 -358
  118. vortex/tools/surfex.py +24 -24
  119. vortex/tools/systems.py +1234 -643
  120. vortex/tools/targets.py +156 -100
  121. vortex/util/__init__.py +1 -1
  122. vortex/util/config.py +378 -327
  123. vortex/util/empty.py +2 -2
  124. vortex/util/helpers.py +56 -24
  125. vortex/util/introspection.py +18 -12
  126. vortex/util/iosponge.py +8 -4
  127. vortex/util/roles.py +4 -6
  128. vortex/util/storefunctions.py +39 -13
  129. vortex/util/structs.py +3 -3
  130. vortex/util/worker.py +29 -17
  131. vortex_nwp-2.1.0.dist-info/METADATA +67 -0
  132. vortex_nwp-2.1.0.dist-info/RECORD +144 -0
  133. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info}/WHEEL +1 -1
  134. vortex/layout/appconf.py +0 -109
  135. vortex/layout/jobs.py +0 -1276
  136. vortex/layout/nodes.py +0 -1424
  137. vortex/layout/subjobs.py +0 -464
  138. vortex_nwp-2.0.0b1.dist-info/METADATA +0 -50
  139. vortex_nwp-2.0.0b1.dist-info/RECORD +0 -146
  140. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info/licenses}/LICENSE +0 -0
  141. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,11 @@ from bronx.fancies.dump import lightdump, fulldump
11
11
  from bronx.stdtypes.date import Date, Time, Period
12
12
  from bronx.compat.functools import cached_property
13
13
 
14
- from vortex.algo.components import AlgoComponentError, AlgoComponentDecoMixin, Parallel
14
+ from vortex.algo.components import (
15
+ AlgoComponentError,
16
+ AlgoComponentDecoMixin,
17
+ Parallel,
18
+ )
15
19
  from vortex.algo.components import algo_component_deco_mixin_autodoc
16
20
  from vortex.data import geometries
17
21
  from vortex.tools import grib
@@ -25,7 +29,7 @@ __all__ = []
25
29
  logger = footprints.loggers.getLogger(__name__)
26
30
 
27
31
 
28
- OOPSMemberInfos = namedtuple('OOPSMemberInfos', ('member', 'date'))
32
+ OOPSMemberInfos = namedtuple("OOPSMemberInfos", ("member", "date"))
29
33
 
30
34
 
31
35
  class EnsSizeAlgoComponentError(AlgoComponentError):
@@ -35,11 +39,17 @@ class EnsSizeAlgoComponentError(AlgoComponentError):
35
39
  self.nominal_ens_size = nominal_ens_size
36
40
  self.actual_ens_size = actual_ens_size
37
41
  self.min_ens_size = min_ens_size
38
- super().__init__('{:d} found ({:d} required)'.format(actual_ens_size, min_ens_size))
42
+ super().__init__(
43
+ "{:d} found ({:d} required)".format(actual_ens_size, min_ens_size)
44
+ )
39
45
 
40
46
  def __reduce__(self):
41
47
  red = list(super().__reduce__())
42
- red[1] = (self.nominal_ens_size, self.actual_ens_size, self.min_ens_size)
48
+ red[1] = (
49
+ self.nominal_ens_size,
50
+ self.actual_ens_size,
51
+ self.min_ens_size,
52
+ )
43
53
  return tuple(red)
44
54
 
45
55
 
@@ -47,17 +57,17 @@ class EnsSizeAlgoComponentError(AlgoComponentError):
47
57
  class OOPSMemberDecoMixin(AlgoComponentDecoMixin):
48
58
  """Add a member footprints' attribute and use it in the configuration files."""
49
59
 
50
- _MIXIN_EXTRA_FOOTPRINTS = (algo_member, )
60
+ _MIXIN_EXTRA_FOOTPRINTS = (algo_member,)
51
61
 
52
62
  def _algo_member_deco_setup(self, rh, opts): # @UnusedVariable
53
63
  """Update the configuration files."""
54
64
  if self.member is not None:
55
- self._generic_config_subs['member'] = self.member
65
+ self._generic_config_subs["member"] = self.member
56
66
  for namrh in self.updatable_namelists:
57
- namrh.contents.setmacro('MEMBER', self.member)
58
- namrh.contents.setmacro('PERTURB', self.member)
67
+ namrh.contents.setmacro("MEMBER", self.member)
68
+ namrh.contents.setmacro("PERTURB", self.member)
59
69
 
60
- _MIXIN_PREPARE_HOOKS = (_algo_member_deco_setup, )
70
+ _MIXIN_PREPARE_HOOKS = (_algo_member_deco_setup,)
61
71
 
62
72
 
63
73
  @algo_component_deco_mixin_autodoc
@@ -73,42 +83,49 @@ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
73
83
  :note: Effective terms are considered (i.e term - (current_date - resource_date))
74
84
  """
75
85
 
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
- )
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",
107
98
  )
108
- ),)
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
+ )
109
124
 
110
125
  @staticmethod
111
- def _stateless_members_detect(smap, basedate, section_check_cb, ensminsize=None, utest=False):
126
+ def _stateless_members_detect(
127
+ smap, basedate, section_check_cb, ensminsize=None, utest=False
128
+ ):
112
129
  """
113
130
  This method does not really need to be static but this way it allows for
114
131
  unit-testing (see ``tests.tests_algo.test_oopspara.py``).
@@ -124,15 +141,22 @@ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
124
141
  for arole, srole in smap.items():
125
142
  # Gather data
126
143
  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)
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)
130
151
  # Sanity checks and filtering
131
152
  role_members_info = set(allmembers[arole].keys())
132
153
  if None in {a_member.member for a_member in role_members_info}:
133
154
  # Ignore sections when some sections have no members defined
134
155
  if len(role_members_info) > 1:
135
- logger.warning('Role: %s. Only some sections have a member number.', arole)
156
+ logger.warning(
157
+ "Role: %s. Only some sections have a member number.",
158
+ arole,
159
+ )
136
160
  role_members_info = set()
137
161
  if len(role_members_info) > 1:
138
162
  if not members:
@@ -140,7 +164,9 @@ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
140
164
  else:
141
165
  # Consistency check on members numbering
142
166
  if members != role_members_info:
143
- raise AlgoComponentError('Inconsistent members numbering')
167
+ raise AlgoComponentError(
168
+ "Inconsistent members numbering"
169
+ )
144
170
  else:
145
171
  # If there is only one member, ignore it: it's not really an ensemble!
146
172
  del allmembers[arole]
@@ -155,14 +181,19 @@ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
155
181
  # Be verbose...
156
182
  if lagged:
157
183
  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)))
184
+ logger.info(
185
+ "Members detected from date=%s: %s",
186
+ a_date,
187
+ ",".join(sorted(str(m) for m in a_mset)),
188
+ )
160
189
  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))
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))
164
195
  r_members = sorted(allmembers.keys())
165
- logger.info('Members roles: %s', ','.join(r_members))
196
+ logger.info("Members roles: %s", ",".join(r_members))
166
197
 
167
198
  # Look for effective terms
168
199
  alleffterms = dict()
@@ -171,24 +202,30 @@ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
171
202
  for minfo, mterms in srole.items():
172
203
  effterms = set()
173
204
  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)
205
+ effterms.add(
206
+ term - (basedate - minfo.date)
207
+ if term is not None and minfo.date is not None
208
+ else None
209
+ )
177
210
  # Intra-role consistency
178
211
  if first_effterms is None:
179
212
  first_effterms = effterms
180
213
  else:
181
214
  # Consistency check on members numbering
182
215
  if effterms != first_effterms:
183
- raise AlgoComponentError('Inconsistent effective terms between members sets (role={:s})'
184
- .format(arole))
216
+ raise AlgoComponentError(
217
+ "Inconsistent effective terms between members sets (role={:s})".format(
218
+ arole
219
+ )
220
+ )
185
221
  # If there are more than one term, consider it
186
222
  if len(first_effterms) > 1:
187
223
  # Check that there is no None in the way
188
224
  if None in first_effterms:
189
225
  raise AlgoComponentError(
190
- 'For a given role, all of the resources or none of then should have a term (role={:s})'
191
- .format(arole)
226
+ "For a given role, all of the resources or none of then should have a term (role={:s})".format(
227
+ arole
228
+ )
192
229
  )
193
230
  # Remove Nones
194
231
  first_effterms = {e for e in first_effterms if e is not None}
@@ -200,19 +237,34 @@ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
200
237
  l_effterms = []
201
238
  if alleffterms:
202
239
  # Hard check only when multiple effetive terms are found
203
- multieffterms = {r: ets for r, ets in alleffterms.items() if len(ets) > 1}
240
+ multieffterms = {
241
+ r: ets for r, ets in alleffterms.items() if len(ets) > 1
242
+ }
204
243
  if multieffterms:
205
- if sum(1 for _ in itertools.groupby(multieffterms.values())) > 1:
206
- raise AlgoComponentError('Inconsistent effective terms between relevant roles')
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
+ )
207
251
  r_effterms = sorted(multieffterms.keys())
208
252
  _, l_effterms = multieffterms.popitem()
209
253
  else:
210
- if sum(1 for _ in itertools.groupby(alleffterms.values())) == 1:
254
+ if (
255
+ sum(1 for _ in itertools.groupby(alleffterms.values()))
256
+ == 1
257
+ ):
211
258
  r_effterms = sorted(alleffterms.keys())
212
259
  _, l_effterms = alleffterms.popitem()
213
260
  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())))
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
+ )
216
268
 
217
269
  # Theoretical ensemble size
218
270
  nominal_ens_size = len(members)
@@ -222,55 +274,87 @@ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
222
274
  # Look for missing resources in the various relevant roles
223
275
  broken = list()
224
276
  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)])
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
+ )
229
285
  for s, arole in broken:
230
286
  if not utest:
231
- logger.warning('Missing items: %s (role: %s).',
232
- s.rh.container.localpath(), arole)
287
+ logger.warning(
288
+ "Missing items: %s (role: %s).",
289
+ s.rh.container.localpath(),
290
+ arole,
291
+ )
233
292
  if broken:
234
- logger.warning('Throwing away member: %s', mb)
293
+ logger.warning("Throwing away member: %s", mb)
235
294
  else:
236
295
  eff_members.add(mb)
237
296
  # Sanity checks depending on ensminsize
238
297
  if ensminsize is None and len(eff_members) != nominal_ens_size:
239
- raise EnsSizeAlgoComponentError(nominal_ens_size, len(eff_members), nominal_ens_size)
298
+ raise EnsSizeAlgoComponentError(
299
+ nominal_ens_size, len(eff_members), nominal_ens_size
300
+ )
240
301
  elif ensminsize is not None and len(eff_members) < ensminsize:
241
- raise EnsSizeAlgoComponentError(nominal_ens_size, len(eff_members), ensminsize)
302
+ raise EnsSizeAlgoComponentError(
303
+ nominal_ens_size, len(eff_members), ensminsize
304
+ )
242
305
 
243
306
  members = eff_members
244
307
 
245
308
  l_members = [m.member for m in sorted(members)]
246
309
  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)
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
+ )
252
325
 
253
326
  def members_detect(self):
254
327
  """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}
328
+ sectionsmap = {
329
+ r: self.context.sequence.filtered_inputs(
330
+ role=r, no_alternates=True
331
+ )
332
+ for r in self._membersdetect_roles
333
+ }
257
334
  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)
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
+ )
268
350
  except EnsSizeAlgoComponentError as e:
269
351
  if self.strict_mbdetect and self.ens_failure_conf_objects is None:
270
352
  raise
271
353
  else:
272
354
  logger.warning("Members detection failed: %s", str(e))
273
- logger.info("'strict_mbdetect' is False... going on with empty lists.")
355
+ logger.info(
356
+ "'strict_mbdetect' is False... going on with empty lists."
357
+ )
274
358
  self._ens_members_num = []
275
359
  self._ens_members_date = []
276
360
  self._ens_members_offset = []
@@ -281,12 +365,17 @@ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
281
365
  # Find the new configuration object
282
366
  main_conf = None
283
367
  for sconf, ssub in self._individual_config_subs.items():
284
- if getattr(sconf.rh.resource, 'objects', '') == self.ens_failure_conf_objects:
368
+ if (
369
+ getattr(sconf.rh.resource, "objects", "")
370
+ == self.ens_failure_conf_objects
371
+ ):
285
372
  main_conf = sconf
286
373
  main_conf_sub = ssub
287
374
  break
288
375
  if main_conf is None:
289
- raise AlgoComponentError('Alternative configuration file was not found')
376
+ raise AlgoComponentError(
377
+ "Alternative configuration file was not found"
378
+ )
290
379
  # Update the config ordered dictionary
291
380
  del self._individual_config_subs[main_conf]
292
381
  new_individual_config_subs = OrderedDict()
@@ -294,31 +383,40 @@ class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
294
383
  for sconf, ssub in self._individual_config_subs.items():
295
384
  new_individual_config_subs[sconf] = ssub
296
385
  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)
386
+ logger.info(
387
+ "Using an alternative configuration file (objects=%s, role=%s)",
388
+ self.ens_failure_conf_objects,
389
+ main_conf.role,
390
+ )
299
391
 
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
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
304
398
  # Legacy:
305
- self._generic_config_subs['members'] = self._ens_members_num
399
+ self._generic_config_subs["members"] = self._ens_members_num
306
400
  # Namelist stuff
307
401
  for namrh in self.updatable_namelists:
308
- namrh.contents.setmacro('ENS_MEMBERS', len(self._ens_members_num))
402
+ namrh.contents.setmacro("ENS_MEMBERS", len(self._ens_members_num))
309
403
  if self._ens_members_num:
310
- namrh.contents.setmacro('ENS_AUTO_NSTRIN', len(self._ens_members_num))
404
+ namrh.contents.setmacro(
405
+ "ENS_AUTO_NSTRIN", len(self._ens_members_num)
406
+ )
311
407
  else:
312
- namrh.contents.setmacro('ENS_AUTO_NSTRIN', self._ens_nominal_size)
313
- self._generic_config_subs['ens_effterms'] = self._ens_effterms
408
+ namrh.contents.setmacro(
409
+ "ENS_AUTO_NSTRIN", self._ens_nominal_size
410
+ )
411
+ self._generic_config_subs["ens_effterms"] = self._ens_effterms
314
412
  # Legacy:
315
- self._generic_config_subs['effterms'] = self._ens_effterms
413
+ self._generic_config_subs["effterms"] = self._ens_effterms
316
414
 
317
415
  def _membersd_setup(self, rh, opts): # @UnusedVariable
318
416
  """Set up the members/terms detection."""
319
417
  self.members_detect()
320
418
 
321
- _MIXIN_PREPARE_HOOKS = (_membersd_setup, )
419
+ _MIXIN_PREPARE_HOOKS = (_membersd_setup,)
322
420
 
323
421
 
324
422
  @algo_component_deco_mixin_autodoc
@@ -331,137 +429,171 @@ class OOPSMembersTermsDecoMixin(AlgoComponentDecoMixin):
331
429
  the configuration file substitutions dictionary ``_generic_config_subs``.
332
430
  """
333
431
 
334
- _MIXIN_EXTRA_FOOTPRINTS = (oops_members_terms_lists, )
432
+ _MIXIN_EXTRA_FOOTPRINTS = (oops_members_terms_lists,)
335
433
 
336
434
  def _membersterms_deco_setup(self, rh, opts): # @UnusedVariable
337
435
  """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
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
344
444
 
345
- _MIXIN_PREPARE_HOOKS = (_membersterms_deco_setup, )
445
+ _MIXIN_PREPARE_HOOKS = (_membersterms_deco_setup,)
346
446
 
347
447
 
348
448
  @algo_component_deco_mixin_autodoc
349
449
  class OOPSTimestepDecoMixin(AlgoComponentDecoMixin):
350
450
  """Add a timsestep attribute and handle substitutions."""
351
451
 
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
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
+ ),
359
461
  ),
360
462
  ),
361
- ),)
463
+ )
362
464
 
363
465
  def _timestep_deco_setup(self, rh, opts): # @UnusedVariable
364
466
  """Set up the timestep in config and namelists."""
365
467
  if self.timestep is not None:
366
- self._generic_config_subs['timestep'] = Period(seconds=self.timestep)
468
+ self._generic_config_subs["timestep"] = Period(
469
+ seconds=self.timestep
470
+ )
367
471
  logger.info("Set macro TIMESTEP=%f in namelists.", self.timestep)
368
472
  for namrh in self.updatable_namelists:
369
- namrh.contents.setmacro('TIMESTEP', self.timestep)
473
+ namrh.contents.setmacro("TIMESTEP", self.timestep)
370
474
 
371
- _MIXIN_PREPARE_HOOKS = (_timestep_deco_setup, )
475
+ _MIXIN_PREPARE_HOOKS = (_timestep_deco_setup,)
372
476
 
373
477
 
374
478
  @algo_component_deco_mixin_autodoc
375
479
  class OOPSIncrementalDecoMixin(AlgoComponentDecoMixin):
376
480
  """Add incremental attributes and handle substitutions."""
377
481
 
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(),
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
+ ),
398
504
  ),
399
505
  ),
400
- ),)
506
+ )
401
507
 
402
508
  def _incremental_deco_setup(self, rh, opts): # @UnusedVariable
403
509
  """Set up the incremental DA settings in config and namelists."""
404
510
  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}
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
+ }
408
520
  if len(sizes) != 1:
409
- raise ValueError('Inconsistent sizes between incr_tsteps and incr_niters')
521
+ raise ValueError(
522
+ "Inconsistent sizes between incr_tsteps and incr_niters"
523
+ )
410
524
  actual_tsteps = [float(t) for t in (self.incremental_tsteps or ())]
411
525
  actual_tsteps_p = [Period(seconds=t) for t in actual_tsteps]
412
526
  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 ())]
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
+ ]
415
533
  if actual_tsteps:
416
- self._generic_config_subs['incremental_tsteps'] = actual_tsteps_p
534
+ self._generic_config_subs["incremental_tsteps"] = (
535
+ actual_tsteps_p
536
+ )
417
537
  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)
538
+ logger.info(
539
+ "Set macro UPD%d_TIMESTEP=%f macro in namelists.",
540
+ upd_i,
541
+ tstep,
542
+ )
420
543
  for namrh in self.updatable_namelists:
421
- namrh.contents.setmacro('UPD{:d}_TIMESTEP'.format(upd_i), tstep)
544
+ namrh.contents.setmacro(
545
+ "UPD{:d}_TIMESTEP".format(upd_i), tstep
546
+ )
422
547
  if actual_niters:
423
- self._generic_config_subs['incremental_niters'] = actual_niters
548
+ self._generic_config_subs["incremental_niters"] = actual_niters
424
549
  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)
550
+ logger.info(
551
+ "Set macro UPD%d_NITER=%d macro in namelists.",
552
+ upd_i,
553
+ niter,
554
+ )
427
555
  for namrh in self.updatable_namelists:
428
- namrh.contents.setmacro('UPD{:d}_NITER'.format(upd_i), niter)
556
+ namrh.contents.setmacro(
557
+ "UPD{:d}_NITER".format(upd_i), niter
558
+ )
429
559
  if actual_geos:
430
- self._generic_config_subs['incremental_geos'] = actual_geos
560
+ self._generic_config_subs["incremental_geos"] = actual_geos
431
561
 
432
562
  _MIXIN_PREPARE_HOOKS = (_incremental_deco_setup,)
433
563
 
434
564
 
435
- class OOPSParallel(Parallel,
436
- drhook.DrHookDecoMixin,
437
- grib.EcGribDecoMixin,
438
- satrad.SatRadDecoMixin):
565
+ class OOPSParallel(
566
+ Parallel,
567
+ drhook.DrHookDecoMixin,
568
+ grib.EcGribDecoMixin,
569
+ satrad.SatRadDecoMixin,
570
+ ):
439
571
  """Abstract AlgoComponent for any OOPS run."""
440
572
 
441
573
  _abstract = True
442
574
  _footprint = dict(
443
- info = "Any OOPS Run (abstract).",
444
- attr = dict(
445
- kind = dict(
446
- values = ['oorun'],
575
+ info="Any OOPS Run (abstract).",
576
+ attr=dict(
577
+ kind=dict(
578
+ values=["oorun"],
447
579
  ),
448
- date = dict(
449
- info = 'The current run date.',
450
- access = 'rwx',
451
- type = Date,
452
- doc_zorder = -50,
580
+ date=dict(
581
+ info="The current run date.",
582
+ access="rwx",
583
+ type=Date,
584
+ doc_zorder=-50,
453
585
  ),
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,
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,
460
592
  ),
461
593
  binarysingle=dict(
462
- default = 'basicnwp',
594
+ default="basicnwp",
463
595
  ),
464
- )
596
+ ),
465
597
  )
466
598
 
467
599
  def __init__(self, *kargs, **kwargs):
@@ -480,13 +612,18 @@ class OOPSParallel(Parallel,
480
612
  def valid_executable(self, rh):
481
613
  """Be sure that the specified executable has a cycle attribute."""
482
614
  valid = super().valid_executable(rh)
483
- if hasattr(rh.resource, 'cycle'):
615
+ if hasattr(rh.resource, "cycle"):
484
616
  self._oops_cycle = rh.resource.cycle
485
617
  return valid
486
618
  else:
487
- logger.error('The binary < %s > has no cycle attribute', repr(rh))
619
+ logger.error("The binary < %s > has no cycle attribute", repr(rh))
488
620
  return False
489
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
+
490
627
  def prepare(self, rh, opts):
491
628
  """Preliminary setups."""
492
629
  super().prepare(rh, opts)
@@ -508,24 +645,31 @@ class OOPSParallel(Parallel,
508
645
  """Prepare options for the binary's command line."""
509
646
  mconfig = list(self._individual_config_subs.keys())[0]
510
647
  configfile = mconfig.rh.container.localpath()
511
- options = {'configfile': configfile}
648
+ options = {"configfile": configfile}
512
649
  return options
513
650
 
514
651
  @cached_property
515
652
  def updatable_namelists(self):
516
- return [s.rh for s in self.context.sequence.effective_inputs(role='Namelist')]
653
+ return [
654
+ s.rh
655
+ for s in self.context.sequence.effective_inputs(role="Namelist")
656
+ ]
517
657
 
518
658
  def set_config_rendering(self):
519
659
  """
520
660
  Look into effective inputs for configuration files and register them for
521
661
  a later rendering using bronx' templating system.
522
662
  """
523
- mconfig = self.context.sequence.effective_inputs(role='MainConfig')
524
- gconfig = self.context.sequence.effective_inputs(role='Config')
663
+ mconfig = self.context.sequence.effective_inputs(role="MainConfig")
664
+ gconfig = self.context.sequence.effective_inputs(role="Config")
525
665
  if len(mconfig) > 1:
526
- raise AlgoComponentError("Only one Main Config section may be provided.")
666
+ raise AlgoComponentError(
667
+ "Only one Main Config section may be provided."
668
+ )
527
669
  if len(mconfig) == 0 and len(gconfig) != 1:
528
- raise AlgoComponentError("Please provide a Main Config section or a unique Config section.")
670
+ raise AlgoComponentError(
671
+ "Please provide a Main Config section or a unique Config section."
672
+ )
529
673
  if len(mconfig) == 1:
530
674
  gconfig.insert(0, mconfig[0])
531
675
  self._individual_config_subs = {sconf: dict() for sconf in gconfig}
@@ -534,40 +678,53 @@ class OOPSParallel(Parallel,
534
678
  """Render registered configuration files using the bronx' templating system."""
535
679
  l_first = True
536
680
  for sconf, sdict in self._individual_config_subs.items():
537
- self.system.subtitle('Configuration file rendering for: {:s}'
538
- .format(sconf.rh.container.localpath()))
681
+ self.system.subtitle(
682
+ "Configuration file rendering for: {:s}".format(
683
+ sconf.rh.container.localpath()
684
+ )
685
+ )
539
686
  l_subs = dict(now=self.date, date=self.date)
540
687
  l_subs.update(self._generic_config_subs)
541
688
  l_subs.update(sdict)
542
689
  l_subs.update(self.config_subs)
543
690
  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))
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
+ )
547
696
  continue
548
697
  try:
549
- sconf.rh.contents.bronx_tpl_render(** l_subs)
698
+ sconf.rh.contents.bronx_tpl_render(**l_subs)
550
699
  except Exception:
551
- logger.error('The config file rendering failed. The substitution dict was: \n%s',
552
- lightdump(l_subs))
700
+ logger.error(
701
+ "The config file rendering failed. The substitution dict was: \n%s",
702
+ lightdump(l_subs),
703
+ )
553
704
  raise
554
705
  self._last_l_subs[sconf] = l_subs
555
706
  if l_first:
556
707
  print(fulldump(sconf.rh.contents.data))
557
708
  sconf.rh.save()
558
709
  else:
559
- logger.info("It's not necessary to update the file (no changes).")
710
+ logger.info(
711
+ "It's not necessary to update the file (no changes)."
712
+ )
560
713
  l_first = False
561
714
 
562
715
  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')
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")
565
722
  if todo:
566
723
  for namrh in todo:
567
- logger.info('Rewriting %s.', namrh.container.localpath())
724
+ logger.info("Rewriting %s.", namrh.container.localpath())
568
725
  namrh.save()
569
726
  else:
570
- logger.info('None of the namelists need to be rewritten.')
727
+ logger.info("None of the namelists need to be rewritten.")
571
728
 
572
729
  def boost_defaults(self):
573
730
  """Set defaults for BOOST environment variables.
@@ -576,14 +733,14 @@ class OOPSParallel(Parallel,
576
733
  depends on the code's cycle number.
577
734
  """
578
735
  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'
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",
587
744
  }
588
745
  }
589
746
  cydefaults = None
@@ -591,9 +748,11 @@ class OOPSParallel(Parallel,
591
748
  if k < self.oops_cycle:
592
749
  cydefaults = defdict
593
750
  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))
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))
597
756
  self.env.default(**cydefaults)
598
757
 
599
758
  def eckit_defaults(self):
@@ -603,10 +762,12 @@ class OOPSParallel(Parallel,
603
762
  depends on the code's cycle number.
604
763
  """
605
764
  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'),
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
+ ),
610
771
  }
611
772
  }
612
773
  cydefaults = None
@@ -614,9 +775,11 @@ class OOPSParallel(Parallel,
614
775
  if k < self.oops_cycle:
615
776
  cydefaults = defdict
616
777
  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))
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))
620
783
  self.env.default(**cydefaults)
621
784
 
622
785
 
@@ -625,15 +788,15 @@ class OOPSODB(OOPSParallel, odb.OdbComponentDecoMixin):
625
788
 
626
789
  _abstract = True
627
790
  _footprint = dict(
628
- info = "OOPS ObsOperator Test run.",
629
- attr = dict(
630
- kind = dict(
631
- values = ['oorunodb'],
791
+ info="OOPS ObsOperator Test run.",
792
+ attr=dict(
793
+ kind=dict(
794
+ values=["oorunodb"],
632
795
  ),
633
- binarysingle = dict(
634
- default = 'basicnwpobsort',
796
+ binarysingle=dict(
797
+ default="basicnwpobsort",
635
798
  ),
636
- )
799
+ ),
637
800
  )
638
801
 
639
802
  #: If ``True``, an empty CCMA database will be created before the run and
@@ -648,18 +811,24 @@ class OOPSODB(OOPSParallel, odb.OdbComponentDecoMixin):
648
811
 
649
812
  # Looking for input observations
650
813
  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.')
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
+ )
656
825
  cma = allcma[0]
657
826
  cma_path = sh.path.abspath(cma.rh.container.localpath())
658
827
  else:
659
828
  cma_path = self.odb_merge_if_needed(allcma)
660
829
  if self._OOPSODB_CCMA_DIRECT:
661
- ccma_path = self.odb_create_db(layout='CCMA')
662
- self.odb.fix_db_path('CCMA', ccma_path)
830
+ ccma_path = self.odb_create_db(layout="CCMA")
831
+ self.odb.fix_db_path("CCMA", ccma_path)
663
832
 
664
833
  # Set ODB environment
665
834
  self.odb.fix_db_path(self.virtualdb, cma_path)
@@ -669,51 +838,61 @@ class OOPSODB(OOPSParallel, odb.OdbComponentDecoMixin):
669
838
  else:
670
839
  self.odb.ioassign_gather(cma_path)
671
840
 
672
- if self.virtualdb.lower() != 'ccma':
841
+ if self.virtualdb.lower() != "ccma":
673
842
  self.odb.create_poolmask(self.virtualdb, cma_path)
674
- self.odb.shuffle_setup(self.slots,
675
- mergedirect=True,
676
- ccmadirect=self._OOPSODB_CCMA_DIRECT)
843
+ self.odb.shuffle_setup(
844
+ self.slots,
845
+ mergedirect=True,
846
+ ccmadirect=self._OOPSODB_CCMA_DIRECT,
847
+ )
677
848
 
678
849
  # Fix the input databases intent
679
- self.odb_rw_or_overwrite_method(* allcma)
850
+ self.odb_rw_or_overwrite_method(*allcma)
680
851
 
681
852
  # Look for extras ODB raw
682
853
  self.odb_handle_raw_dbs()
683
854
 
684
855
  # 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):
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
+ ):
698
875
  """Any kind of OOPS analysis (screening/thining step excluded)."""
699
876
 
700
877
  _footprint = dict(
701
- info = "OOPS minimisation.",
702
- attr = dict(
703
- kind = dict(
704
- values = ['ooanalysis', 'oominim'],
705
- remap = dict(autoremap='first'),
878
+ info="OOPS minimisation.",
879
+ attr=dict(
880
+ kind=dict(
881
+ values=["ooanalysis", "oominim"],
882
+ remap=dict(autoremap="first"),
706
883
  ),
707
- virtualdb = dict(
708
- default = 'ccma',
884
+ virtualdb=dict(
885
+ default="ccma",
709
886
  ),
710
887
  withscreening=dict(
711
- values = [False, ],
712
- type = bool,
713
- optional = True,
714
- default = False,
888
+ values=[
889
+ False,
890
+ ],
891
+ type=bool,
892
+ optional=True,
893
+ default=False,
715
894
  ),
716
- )
895
+ ),
717
896
  )
718
897
 
719
898
 
@@ -723,13 +902,15 @@ class OOPSAnalysisWithScreening(OOPSAnalysis):
723
902
  _OOPSODB_CCMA_DIRECT = True
724
903
 
725
904
  _footprint = dict(
726
- attr = dict(
727
- virtualdb = dict(
728
- default = 'ecma',
905
+ attr=dict(
906
+ virtualdb=dict(
907
+ default="ecma",
729
908
  ),
730
- withscreening = dict(
731
- values = [True, ],
732
- optional = False,
909
+ withscreening=dict(
910
+ values=[
911
+ True,
912
+ ],
913
+ optional=False,
733
914
  ),
734
915
  )
735
916
  )