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,1258 @@
1
+ """
2
+ AlgoComponents to work with Observational DataBases.
3
+ """
4
+
5
+ from collections import defaultdict
6
+ import copy
7
+ import re
8
+ import time
9
+
10
+ from bronx.fancies import loggers
11
+ from bronx.stdtypes.date import utcnow
12
+ from bronx.stdtypes.dictionaries import Foo
13
+ from bronx.system.memory import convert_bytes_in_unit
14
+ import footprints
15
+ from taylorism import Boss
16
+
17
+ from vortex.tools.systems import ExecutionError
18
+
19
+ from vortex.algo.components import Parallel, ParaBlindRun
20
+ from vortex.tools.parallelism import VortexWorkerBlindRun
21
+
22
+ from ..syntax.stdattrs import arpifs_cycle
23
+
24
+ from ..data.obs import ObsMapContent, ObsMapItem, ObsRefContent, ObsRefItem
25
+ from ..tools import odb, drhook
26
+
27
+
28
+ #: No automatic export
29
+ __all__ = []
30
+
31
+ logger = loggers.getLogger(__name__)
32
+
33
+
34
+ class Raw2OdbExecutionError(ExecutionError):
35
+ def __init__(self, odb_database):
36
+ self.odb_database = odb_database
37
+ super().__init__("Raw2odb execution failed.")
38
+
39
+ def __str__(self):
40
+ return "Error while running bator for ODB database < {:s} >".format(
41
+ self.odb_database
42
+ )
43
+
44
+
45
+ class Bateur(VortexWorkerBlindRun):
46
+ """
47
+ Worker for parallel BATOR run. It returns in its report a synthesis about
48
+ actual run-time and consumed memory (versus predictions).
49
+ """
50
+
51
+ _footprint = [
52
+ arpifs_cycle,
53
+ dict(
54
+ info="Bateur: launches a single bator execution in a parallel context",
55
+ attr=dict(
56
+ base=dict(
57
+ info="name of the odb database to process",
58
+ ),
59
+ workdir=dict(
60
+ info="working directory of the run",
61
+ ),
62
+ inputsize=dict(
63
+ info="input files total size in bytes",
64
+ type=int,
65
+ default=0,
66
+ ),
67
+ ),
68
+ ),
69
+ ]
70
+
71
+ @property
72
+ def memory_in_bytes(self):
73
+ return self.memory * 1024 * 1024
74
+
75
+ def vortex_task(self, **kwargs):
76
+ odb_drv = odb.OdbDriver(self.cycle, self.system, self.system.env)
77
+ self.system.cd("wkdir_" + self.base)
78
+
79
+ dbpath = self.system.path.join(self.workdir, "ECMA." + self.base)
80
+ listpath = self.system.path.join(self.workdir, "listing." + self.base)
81
+
82
+ odb_drv.fix_db_path("ecma", dbpath)
83
+
84
+ real_time = -time.time()
85
+ start_time = utcnow().isoformat()
86
+ rdict = dict(rc=True)
87
+ try:
88
+ self.local_spawn(listpath)
89
+ except ExecutionError:
90
+ rdict["rc"] = Raw2OdbExecutionError(self.base)
91
+ real_time += time.time()
92
+
93
+ if self.system.memory_info is not None:
94
+ realMem = self.system.memory_info.children_maxRSS("B")
95
+ memRatio = (
96
+ (realMem / float(self.memory_in_bytes))
97
+ if self.memory_in_bytes > 0
98
+ else None
99
+ )
100
+ else:
101
+ realMem = None
102
+ memRatio = None
103
+
104
+ rdict["synthesis"] = dict(
105
+ base=self.base,
106
+ inputsize=self.inputsize,
107
+ mem_expected=self.memory_in_bytes,
108
+ mem_real=realMem,
109
+ mem_ratio=memRatio,
110
+ time_expected=self.expected_time,
111
+ time_start=start_time,
112
+ time_real=real_time,
113
+ time_ratio=(
114
+ real_time / float(self.expected_time)
115
+ if self.expected_time > 0
116
+ else None
117
+ ),
118
+ sched_id=self.scheduler_ticket,
119
+ )
120
+
121
+ # Save a copy of io assign map in the new database
122
+ if self.system.path.isdir(dbpath):
123
+ self.system.cp(
124
+ self.system.path.join(
125
+ self.workdir, "odb_db_template", "IOASSIGN"
126
+ ),
127
+ self.system.path.join(dbpath, "IOASSIGN"),
128
+ )
129
+ else:
130
+ logger.warning("ODB database not created: " + self.base)
131
+
132
+ return rdict
133
+
134
+
135
+ class Raw2ODBparallel(
136
+ ParaBlindRun, odb.OdbComponentDecoMixin, drhook.DrHookDecoMixin
137
+ ):
138
+ """Convert raw observations files to ODB using taylorism."""
139
+
140
+ _footprint = dict(
141
+ attr=dict(
142
+ kind=dict(
143
+ values=["raw2odb", "bufr2odb", "obsoul2odb"],
144
+ remap=dict(
145
+ bufr2odb="raw2odb",
146
+ obsoul2odb="raw2odb",
147
+ ),
148
+ ),
149
+ engine=dict(
150
+ values=[
151
+ "blind",
152
+ "parallel",
153
+ ] # parallel -> for backward compatibility
154
+ ),
155
+ ioassign=dict(
156
+ optional=False,
157
+ ),
158
+ lamflag=dict(
159
+ info="Activate LAMFLAG (i.e work for Limited Area Model)",
160
+ type=bool,
161
+ optional=True,
162
+ default=False,
163
+ ),
164
+ ontime=dict(
165
+ info="Check observation's resources date vs own data attribute.",
166
+ type=bool,
167
+ optional=True,
168
+ default=True,
169
+ ),
170
+ mapall=dict(
171
+ info="All observation files must be accounted for in an obsmap file. ",
172
+ type=bool,
173
+ optional=True,
174
+ default=False,
175
+ ),
176
+ maponly=dict(
177
+ info=(
178
+ "Work only with observation files listed in the obsmap files. "
179
+ + "(if False, obsmap entries may be automatically generated)."
180
+ ),
181
+ type=bool,
182
+ optional=True,
183
+ default=False,
184
+ ),
185
+ member=dict(
186
+ info=(
187
+ "The current member's number "
188
+ + "(may be omitted in deterministic configurations)."
189
+ ),
190
+ optional=True,
191
+ type=int,
192
+ ),
193
+ dataid=dict(
194
+ info=(
195
+ "The ODB databases created by Bator contain an identifier "
196
+ + "that is specified as a command-line argument. This "
197
+ + "switch tweaks the way the command-line argument is "
198
+ + "generated."
199
+ ),
200
+ values=["empty", "hh"],
201
+ default="hh",
202
+ optional=True,
203
+ ),
204
+ ntasks=dict(
205
+ info=(
206
+ "The maximum number of allowed concurrent task for "
207
+ "parallel execution."
208
+ ),
209
+ default=1,
210
+ optional=True,
211
+ ),
212
+ maxmemory=dict(
213
+ info="The maximum amount of usable memory (in GiB)",
214
+ type=int,
215
+ optional=True,
216
+ ),
217
+ parallel_const=dict(
218
+ info=(
219
+ "Constant that are used to predict execution time and "
220
+ + "memory consumption for a given ODB database."
221
+ ),
222
+ type=footprints.FPDict,
223
+ optional=True,
224
+ ),
225
+ )
226
+ )
227
+
228
+ _donot_link_roles = [
229
+ "Observations",
230
+ "Obsmap",
231
+ "IOPoll",
232
+ "LFIScripts",
233
+ "LFITOOLS",
234
+ "Binary",
235
+ "Bator",
236
+ "Batodb",
237
+ ]
238
+
239
+ def __init__(self, *kargs, **kwargs):
240
+ super().__init__(*kargs, **kwargs)
241
+ self.para_synthesis = dict()
242
+ self.obspack = dict()
243
+ self.obsmapout = list()
244
+ self._effective_maxmem = None
245
+
246
+ @property
247
+ def effective_maxmem(self):
248
+ """Return the maximum amount of usable memory (in MiB)."""
249
+ if self._effective_maxmem is None:
250
+ if self.maxmemory:
251
+ self._effective_maxmem = (
252
+ self.maxmemory * 1024
253
+ ) # maxmemory in GB
254
+ else:
255
+ sys_maxmem = self.system.memory_info.system_RAM("MiB")
256
+ # System memory minus 20% or minus 4GB
257
+ self._effective_maxmem = max(
258
+ sys_maxmem * 0.8, sys_maxmem - 4 * 1024
259
+ )
260
+ return self._effective_maxmem
261
+
262
+ def input_obs(self):
263
+ """Find out which are the usable observations."""
264
+ obsall = [
265
+ x
266
+ for x in self.context.sequence.effective_inputs(
267
+ kind="observations"
268
+ )
269
+ ]
270
+ obsall.sort(key=lambda s: s.rh.resource.part)
271
+
272
+ # Looking for valid raw observations
273
+ sizemin = self.env.VORTEX_OBS_SIZEMIN or 80
274
+ obsok = list()
275
+ for secobs in obsall:
276
+ rhobs = secobs.rh
277
+ if rhobs.resource.nativefmt == "odb":
278
+ logger.warning(
279
+ "Observations set [%s] is ODB ready", rhobs.resource.part
280
+ )
281
+ continue
282
+ if rhobs.container.totalsize < sizemin:
283
+ logger.warning(
284
+ "Observations set [%s] is far too small: %d",
285
+ rhobs.resource.part,
286
+ rhobs.container.totalsize,
287
+ )
288
+ else:
289
+ logger.info(
290
+ "Observations set [%s] has size: %d",
291
+ rhobs.resource.part,
292
+ int(rhobs.container.totalsize),
293
+ )
294
+ obsok.append(Foo(rh=rhobs, refdata=list(), mapped=False))
295
+
296
+ # Check the observations dates
297
+ for obs in [obs for obs in obsok if obs.rh.resource.date != self.date]:
298
+ logger.warning(
299
+ "Observation [%s] %s [time mismatch: %s / %s]",
300
+ "discarded" if self.ontime else "is questionable",
301
+ obs.rh.resource.part,
302
+ obs.rh.resource.date.isoformat(),
303
+ self.date.isoformat(),
304
+ )
305
+ if self.ontime:
306
+ obsok = [obs for obs in obsok if obs.rh.resource.date == self.date]
307
+
308
+ return obsok
309
+
310
+ def _retrieve_refdatainfo(self, obslist):
311
+ """Look for refdata resources and link their content with the obslist."""
312
+ refmap = dict()
313
+ refall = list(self.context.sequence.effective_inputs(kind="refdata"))
314
+ for rdata in refall:
315
+ logger.info("Inspect refdata " + rdata.rh.container.localpath())
316
+ self.system.subtitle(rdata.role)
317
+ rdata.rh.container.cat()
318
+ for item in rdata.rh.contents:
319
+ refmap[(item.fmt.lower(), item.data, item.instr)] = (
320
+ rdata.rh,
321
+ item,
322
+ )
323
+
324
+ # Build actual refdata
325
+ for obs in obslist:
326
+ thispart = obs.rh.resource.part
327
+ thisfmt = obs.rh.container.actualfmt.lower()
328
+ logger.info(
329
+ " ".join(
330
+ ("Building information for [", thisfmt, "/", thispart, "]")
331
+ )
332
+ )
333
+
334
+ # Gather equivalent refdata lines
335
+ if not self.system.path.exists("norefdata." + thispart) and (
336
+ not self.env.VORTEX_OBSDB_NOREF
337
+ or not re.search(
338
+ thispart, self.env.VORTEX_OBSDB_NOREF, re.IGNORECASE
339
+ )
340
+ ):
341
+ for k, v in refmap.items():
342
+ x_fmt, x_data = k[:2]
343
+ if x_fmt == thisfmt and x_data == thispart:
344
+ rdata, item = v
345
+ obs.refdata.append(rdata.contents.formatted_data(item))
346
+ return refmap, refall
347
+
348
+ def _map_refdatainfo(self, refmap, refall, imap, thismap):
349
+ """Associate obsmap entries with refdata entries."""
350
+ thiskey = (imap.fmt.lower(), imap.data, imap.instr)
351
+ if thiskey in refmap:
352
+ rdata, item = refmap[thiskey]
353
+ thismap.refdata.append(rdata.contents.formatted_data(item))
354
+ else:
355
+ logger.warning(
356
+ "Creating automatic refdata entry for " + str(thiskey)
357
+ )
358
+ item = ObsRefItem(
359
+ imap.data, imap.fmt, imap.instr, self.date.ymd, self.date.hh
360
+ )
361
+ if refall:
362
+ thismap.refdata.append(
363
+ refall[0].rh.contents.formatted_data(item)
364
+ )
365
+ else:
366
+ logger.error("No default for formatting data %s", item)
367
+ thismap.refdata.append(ObsRefContent.formatted_data(item))
368
+
369
+ @staticmethod
370
+ def _new_obspack_item():
371
+ """Create a now entry in obspack."""
372
+ return Foo(
373
+ mapping=list(), standalone=False, refdata=list(), obsfile=dict()
374
+ )
375
+
376
+ def prepare(self, rh, opts):
377
+ """Get a look at raw observations input files."""
378
+
379
+ sh = self.system
380
+ cycle = rh.resource.cycle
381
+
382
+ # First create the proper IO assign table for any of the resulting ECMA databases
383
+ self.odb_create_db("ECMA", "odb_db_template")
384
+ self.env.IOASSIGN = sh.path.abspath(
385
+ sh.path.join("odb_db_template", "IOASSIGN")
386
+ )
387
+
388
+ # Looking for input observations
389
+ obsok = self.input_obs()
390
+
391
+ # Building refdata map for direct access to (fmt, data, instr) entries
392
+ if cycle < "cy42_op1":
393
+ # Refdata information is not needed anymore with cy42_op1
394
+ refmap, refall = self._retrieve_refdatainfo(obsok)
395
+
396
+ # Looking for obs maps
397
+ mapitems = list()
398
+ for omsec in self.context.sequence.effective_inputs(kind="obsmap"):
399
+ logger.info(
400
+ " ".join(
401
+ (
402
+ "Gathering information from map",
403
+ omsec.rh.container.localpath(),
404
+ )
405
+ )
406
+ )
407
+ sh.subtitle(omsec.role)
408
+ omsec.rh.container.cat()
409
+ mapitems.extend(omsec.rh.contents)
410
+
411
+ self.obspack = defaultdict(self._new_obspack_item) # Reset the obspack
412
+ for imap in mapitems:
413
+ # Match observation files and obsmap entries + Various checks
414
+ logger.info("Inspect " + str(imap))
415
+ candidates = [
416
+ obs
417
+ for obs in obsok
418
+ if (
419
+ obs.rh.resource.part == imap.data
420
+ and obs.rh.container.actualfmt.lower() == imap.fmt.lower()
421
+ )
422
+ ]
423
+ if not candidates:
424
+ errmsg = (
425
+ "No input obsfile could match [data:{:s}/fmt:{:s}]".format(
426
+ imap.data, imap.fmt
427
+ )
428
+ )
429
+ if self.mapall:
430
+ raise ValueError(errmsg)
431
+ else:
432
+ logger.warning(errmsg)
433
+ continue
434
+ candidates[-1].mapped = True
435
+ # Build the obspack entry
436
+ thismap = self.obspack[imap.odb]
437
+ thismap.mapping.append(imap)
438
+ thismap.obsfile[imap.fmt.upper() + "." + imap.data] = candidates[
439
+ -1
440
+ ]
441
+ # Map refdata and obsmap entries
442
+ if cycle < "cy42_op1":
443
+ # Refdata information is not needed anymore with cy42_op1
444
+ self._map_refdatainfo(refmap, refall, imap, thismap)
445
+
446
+ # Deal with observations that are not described in the obsmap
447
+ for notmap in [obs for obs in obsok if not obs.mapped]:
448
+ thispart = notmap.rh.resource.part
449
+ logger.info("Inspect not mapped obs " + thispart)
450
+ if thispart not in self.obspack:
451
+ thisfmt = notmap.rh.container.actualfmt.upper()
452
+ thismsg = "standalone obs entry [data:{:s} / fmt:{:s}]".format(
453
+ thispart, thisfmt
454
+ )
455
+ if self.maponly:
456
+ logger.warning("Ignore " + thismsg)
457
+ else:
458
+ logger.warning("Active " + thismsg)
459
+ thismap = self.obspack[thispart]
460
+ thismap.standalone = thisfmt
461
+ thismap.mapping.append(
462
+ ObsMapItem(thispart, thispart, thisfmt, thispart)
463
+ )
464
+ thismap.refdata = notmap.refdata
465
+ thismap.obsfile[thisfmt.upper() + "." + thispart] = notmap
466
+
467
+ # Informations about timeslots
468
+ logger.info("The timeslot definition is: %s", str(self.slots))
469
+ if cycle < "cy42_op1":
470
+ # ficdate is not needed anymore with cy42_op1...
471
+ self.slots.as_file(self.date, "ficdate")
472
+ else:
473
+ # From cy42_op1 onward, we only need environment variables
474
+ for var, value in self.slots.as_environment().items():
475
+ logger.info("Setting env %s = %s", var, str(value))
476
+ self.env[var] = value
477
+
478
+ # Let ancestors handling most of the env setting
479
+ super().prepare(rh, opts)
480
+ self.env.update(
481
+ BATOR_NBPOOL=self.npool,
482
+ BATODB_NBPOOL=self.npool,
483
+ BATOR_NBSLOT=self.slots.nslot,
484
+ BATODB_NBSLOT=self.slots.nslot,
485
+ )
486
+ self.env.default(
487
+ TIME_INIT_YYYYMMDD=self.date.ymd,
488
+ TIME_INIT_HHMMSS=self.date.hm + "00",
489
+ )
490
+ if self.lamflag:
491
+ for lamvar in ("BATOR_LAMFLAG", "BATODB_LAMFLAG"):
492
+ logger.info("Setting env %s = %d", lamvar, 1)
493
+ self.env[lamvar] = 1
494
+
495
+ if self.member is not None:
496
+ for nam in self.context.sequence.effective_inputs(
497
+ kind=("namelist", "namelistfp")
498
+ ):
499
+ nam.rh.contents.setmacro("MEMBER", self.member)
500
+ logger.info(
501
+ "Setup macro MEMBER=%s in %s",
502
+ self.member,
503
+ nam.rh.container.actualpath(),
504
+ )
505
+ if nam.rh.contents.dumps_needs_update:
506
+ nam.rh.save()
507
+
508
+ def spawn_command_options(self):
509
+ """Any data useful to build the command line."""
510
+ opts_dict = super().spawn_command_options()
511
+ opts_dict["dataid"] = self.dataid
512
+ opts_dict["date"] = self.date
513
+ return opts_dict
514
+
515
+ def _default_pre_execute(self, rh, opts):
516
+ """Change default initialisation to use LongerFirstScheduler"""
517
+ # Start the task scheduler
518
+ self._boss = Boss(
519
+ verbose=self.verbose,
520
+ scheduler=footprints.proxy.scheduler(
521
+ limit="threads+memory",
522
+ max_threads=self.ntasks,
523
+ max_memory=self.effective_maxmem,
524
+ ),
525
+ )
526
+ self._boss.make_them_work()
527
+
528
+ def execute(self, rh, opts):
529
+ """
530
+ For each base, a directory is created such that each worker works in his
531
+ directory. Symlinks are created into these working directories.
532
+ """
533
+
534
+ sh = self.system
535
+ cycle = rh.resource.cycle
536
+
537
+ batnam = [
538
+ x.rh
539
+ for x in self.context.sequence.effective_inputs(
540
+ role="NamelistBatodb"
541
+ )
542
+ ]
543
+ # Give a glance to the actual namelist
544
+ if batnam:
545
+ sh.subtitle("Namelist Raw2ODB")
546
+ batnam[0].container.cat()
547
+
548
+ self.obsmapout = list() # Reset the obsmapout
549
+ scheduler_instructions = defaultdict(list)
550
+
551
+ workdir = sh.pwd()
552
+
553
+ for odbset, thispack in self.obspack.items():
554
+ odbname = self.virtualdb.upper() + "." + odbset
555
+ sh.title("Cocooning ODB set: " + odbname)
556
+ with sh.cdcontext("wkdir_" + odbset, create=True):
557
+ for inpt in [
558
+ s
559
+ for s in self.context.sequence.inputs()
560
+ if s.stage == "get"
561
+ ]:
562
+ if inpt.role not in self._donot_link_roles:
563
+ logger.info(
564
+ "creating softlink: %s -> %s",
565
+ inpt.rh.container.localpath(),
566
+ sh.path.join(
567
+ workdir, inpt.rh.container.localpath()
568
+ ),
569
+ )
570
+ sh.softlink(
571
+ sh.path.join(
572
+ workdir, inpt.rh.container.localpath()
573
+ ),
574
+ inpt.rh.container.localpath(),
575
+ )
576
+
577
+ if cycle < "cy42_op1":
578
+ # Special stuff for cy < 42
579
+ logger.info("creating softlink for ficdate.")
580
+ sh.softlink(sh.path.join(workdir, "ficdate"), "ficdate")
581
+
582
+ odb_input_size = 0
583
+ for obsname, obsinfo in thispack.obsfile.items():
584
+ logger.info(
585
+ "creating softlink: %s -> %s",
586
+ obsname,
587
+ sh.path.join(
588
+ workdir, obsinfo.rh.container.localpath()
589
+ ),
590
+ )
591
+ sh.softlink(
592
+ sh.path.join(
593
+ workdir, obsinfo.rh.container.localpath()
594
+ ),
595
+ obsname,
596
+ )
597
+ if thispack.standalone and cycle < "cy42_op1":
598
+ logger.info(
599
+ "creating softlink: %s -> %s",
600
+ thispack.standalone,
601
+ sh.path.join(
602
+ workdir, obsinfo.rh.container.localpath()
603
+ ),
604
+ )
605
+ sh.softlink(
606
+ sh.path.join(
607
+ workdir, obsinfo.rh.container.localpath()
608
+ ),
609
+ thispack.standalone,
610
+ )
611
+
612
+ odb_input_size += obsinfo.rh.container.totalsize
613
+
614
+ # Fill the actual refdata according to information gathered in prepare stage
615
+ if cycle < "cy42_op1":
616
+ if thispack.refdata:
617
+ with open("refdata", "w") as fd:
618
+ for rdentry in thispack.refdata:
619
+ fd.write(str(rdentry + "\n"))
620
+ sh.subtitle("Local refdata for: {:s}".format(odbname))
621
+ sh.cat("refdata", output=False)
622
+ # Drive bator with a batormap file (from cy42_op1 onward)
623
+ else:
624
+ with open("batormap", "w") as fd:
625
+ for mapentry in sorted(thispack.mapping):
626
+ fd.write(
627
+ str(
628
+ ObsMapContent.formatted_data(mapentry)
629
+ + "\n"
630
+ )
631
+ )
632
+ sh.subtitle("Local batormap for: {:s}".format(odbname))
633
+ sh.cat("batormap", output=False)
634
+
635
+ self.obsmapout.extend(thispack.mapping)
636
+
637
+ # Compute the expected memory and time
638
+ if isinstance(self.parallel_const, dict):
639
+ pconst = self.parallel_const.get(
640
+ odbset,
641
+ self.parallel_const.get("default", (999999.0, 1.0)),
642
+ )
643
+ offsets = self.parallel_const.get(
644
+ "offset", (0.0, 0.0)
645
+ ) # In MiB for the memory
646
+ else:
647
+ pconst = (999999.0, 1.0)
648
+ offsets = (0.0, 0.0)
649
+ bTime = (odb_input_size * pconst[1] / 1048576) + offsets[1]
650
+ bMemory = odb_input_size * pconst[0] + (
651
+ offsets[0] * 1024 * 1024
652
+ )
653
+ bMemory = bMemory / 1024.0 / 1024.0
654
+ if bMemory > self.effective_maxmem:
655
+ logger.info(
656
+ "For %s, the computed memory needs exceed the node limit.",
657
+ odbset,
658
+ )
659
+ logger.info(
660
+ "Memory requirement reseted to %d (originally %d.)",
661
+ int(self.effective_maxmem),
662
+ int(bMemory),
663
+ )
664
+ bMemory = self.effective_maxmem
665
+ scheduler_instructions["name"].append(
666
+ "ODB_database_{:s}".format(odbset)
667
+ )
668
+ scheduler_instructions["base"].append(odbset)
669
+ scheduler_instructions["memory"].append(bMemory)
670
+ scheduler_instructions["expected_time"].append(bTime)
671
+ scheduler_instructions["inputsize"].append(odb_input_size)
672
+
673
+ sh.title("Launching Bator using taylorism...")
674
+ self._default_pre_execute(rh, opts)
675
+ common_i = self._default_common_instructions(rh, opts)
676
+ # Update the common instructions
677
+ common_i.update(
678
+ dict(
679
+ workdir=workdir,
680
+ cycle=cycle,
681
+ )
682
+ )
683
+
684
+ self._add_instructions(common_i, scheduler_instructions)
685
+
686
+ post_opts = copy.copy(opts)
687
+ post_opts["synthesis"] = self.para_synthesis
688
+ self._default_post_execute(rh, post_opts)
689
+
690
+ def _default_rc_action(self, rh, opts, report, rc):
691
+ super()._default_rc_action(rh, opts, report, rc)
692
+ my_report = report["report"].get("synthesis", None)
693
+ if my_report:
694
+ opts["synthesis"][my_report.pop("base")] = my_report
695
+
696
+ def postfix(self, rh, opts):
697
+ """Post conversion cleaning."""
698
+ sh = self.system
699
+
700
+ # Remove empty ECMA databases from the output obsmap
701
+ self.obsmapout = [
702
+ x
703
+ for x in self.obsmapout
704
+ if (
705
+ sh.path.isdir("ECMA." + x.odb)
706
+ and sh.path.isdir("ECMA." + x.odb + "/1")
707
+ )
708
+ ]
709
+
710
+ # At least one non-empty database is needed...
711
+ self.algoassert(
712
+ self.obsmapout, "At least one non-empty ODB database is expected"
713
+ )
714
+
715
+ # Generate the output bator_map
716
+ with open("batodb_map.out", "w") as fd:
717
+ for x in sorted(self.obsmapout):
718
+ fd.write(str(ObsMapContent.formatted_data(x) + "\n"))
719
+
720
+ # Generate a global refdata (if cycle allows it and if possible)
721
+ if rh.resource.cycle < "cy42_op1":
722
+ rdrh_dict = {
723
+ y.rh.resource.part: y.rh
724
+ for y in self.context.sequence.effective_inputs(kind="refdata")
725
+ if y.rh.resource.part != "all"
726
+ }
727
+ with open("refdata_global", "w") as rdg:
728
+ for x in sorted(self.obsmapout):
729
+ if (
730
+ x.data in rdrh_dict
731
+ and sh.path.getsize(
732
+ rdrh_dict[x.data].container.localpath()
733
+ )
734
+ > 0
735
+ ):
736
+ with open(
737
+ rdrh_dict[x.data].container.localpath()
738
+ ) as rdl:
739
+ rdg.write(rdl.readline())
740
+ elif (
741
+ sh.path.exists("refdata." + x.data)
742
+ and sh.path.getsize("refdata." + x.data) > 0
743
+ ):
744
+ with open("refdata." + x.data) as rdl:
745
+ rdg.write(rdl.readline())
746
+ else:
747
+ logger.info(
748
+ "Unable to create a global refdata entry for data="
749
+ + x.data
750
+ )
751
+
752
+ sh.json_dump(self.para_synthesis, "parallel_exec_synthesis.json")
753
+
754
+ # Print the parallel execution summary
755
+ sh.subtitle("Here is the parallel execution synthesis: memory aspects")
756
+ header = "Database InputSize(MiB) PredMem(GiB) RealMem(GiB) Real/Pred Ratio"
757
+ rfmt = "{:8s} {:>15.0f} {:>12.1f} {:>12.1f} {:>15.2f}"
758
+ print(header)
759
+ for row in sorted(self.para_synthesis.keys()):
760
+ srep = self.para_synthesis[row]
761
+ print(
762
+ rfmt.format(
763
+ row,
764
+ convert_bytes_in_unit(srep["inputsize"], "MiB"),
765
+ convert_bytes_in_unit(srep["mem_expected"], "GiB"),
766
+ (
767
+ 99.99
768
+ if srep["mem_real"] is None
769
+ else convert_bytes_in_unit(srep["mem_real"], "GiB")
770
+ ),
771
+ (
772
+ 99.99
773
+ if srep["mem_ratio"] is None
774
+ else srep["mem_ratio"]
775
+ ),
776
+ )
777
+ )
778
+
779
+ sh.subtitle(
780
+ "Here is the parallel execution synthesis: elapsed time aspects"
781
+ )
782
+ header = (
783
+ "Database InputSize(MiB) PredTime(s) RealTime(s) Real/Pred Ratio"
784
+ )
785
+ rfmt = "{:8s} {:>15.0f} {:>11.1f} {:>11.1f} {:>15.2f}"
786
+ print(header)
787
+ for row in sorted(self.para_synthesis.keys()):
788
+ srep = self.para_synthesis[row]
789
+ print(
790
+ rfmt.format(
791
+ row,
792
+ convert_bytes_in_unit(srep["inputsize"], "MiB"),
793
+ srep["time_expected"],
794
+ srep["time_real"],
795
+ (
796
+ 99.99
797
+ if srep["time_ratio"] is None
798
+ else srep["time_ratio"]
799
+ ),
800
+ )
801
+ )
802
+
803
+ sh.subtitle("Here is the parallel execution synthesis: timeline")
804
+ header = "Database StartTime(UTC) PredMem(GiB) RealTime(s) ExecSlot"
805
+ rfmt = "{:8s} {:>40s} {:>11.1f} {:>12.1f} {:>8s}"
806
+ print(header)
807
+ for row, srep in sorted(
808
+ self.para_synthesis.items(), key=lambda x: x[1]["time_start"]
809
+ ):
810
+ print(
811
+ rfmt.format(
812
+ row,
813
+ srep["time_start"],
814
+ convert_bytes_in_unit(srep["mem_expected"], "GiB"),
815
+ srep["time_real"],
816
+ str(srep["sched_id"]),
817
+ )
818
+ )
819
+
820
+ print(
821
+ "\nThe memory limit was set to: {:.1f} GiB".format(
822
+ self.effective_maxmem / 1024.0
823
+ )
824
+ )
825
+
826
+ super().postfix(rh, opts)
827
+
828
+
829
+ class OdbAverage(Parallel, odb.OdbComponentDecoMixin, drhook.DrHookDecoMixin):
830
+ """TODO the father of this component is very much welcome."""
831
+
832
+ _footprint = dict(
833
+ attr=dict(
834
+ kind=dict(
835
+ values=["average"],
836
+ ),
837
+ binarysingle=dict(
838
+ default="basicobsort",
839
+ ),
840
+ ioassign=dict(),
841
+ outdb=dict(
842
+ optional=True,
843
+ default="ccma",
844
+ value=["ecma", "ccma"],
845
+ ),
846
+ maskname=dict(
847
+ optional=True,
848
+ default="mask4x4.txt",
849
+ ),
850
+ )
851
+ )
852
+
853
+ def _mpitool_attributes(self, opts):
854
+ conf_dict = super()._mpitool_attributes(opts)
855
+ conf_dict.update({"mplbased": True})
856
+ return conf_dict
857
+
858
+ def prepare(self, rh, opts):
859
+ """Find any ODB candidate in input files."""
860
+
861
+ sh = self.system
862
+
863
+ # Looking for input observations
864
+ obsall = [
865
+ x
866
+ for x in self.lookupodb()
867
+ if x.rh.resource.layout.lower() == "ecma"
868
+ ]
869
+ # One database at a time
870
+ if len(obsall) != 1:
871
+ raise ValueError("One and only one ECMA input should be here")
872
+ self.bingo = ecma = obsall[0]
873
+
874
+ # First create a fake CCMA
875
+ self.layout_new = self.outdb.upper()
876
+ ccma_path = self.odb_create_db(self.layout_new)
877
+ self.odb.fix_db_path(self.layout_new, ccma_path)
878
+
879
+ self.layout_in = ecma.rh.resource.layout.upper()
880
+ ecma_path = sh.path.abspath(ecma.rh.container.localpath())
881
+ self.odb.fix_db_path(self.layout_in, ecma_path)
882
+
883
+ self.odb.ioassign_gather(ecma_path, ccma_path)
884
+
885
+ ecma_pool = sh.path.join(ecma_path, "1")
886
+ if not sh.path.isdir(ecma_pool):
887
+ logger.error("The input ECMA base is empty")
888
+ self.abort("No ECMA input")
889
+ return
890
+
891
+ self.odb.create_poolmask(self.layout_new, ccma_path)
892
+
893
+ # Some extra settings
894
+ self.env.update(
895
+ TO_ODB_CANARI=0,
896
+ TO_ODB_REDUC=self.env.TO_ODB_REDUC or 1,
897
+ TO_ODB_SETACTIVE=1,
898
+ )
899
+
900
+ # Let ancesters handling most of the env setting
901
+ super().prepare(rh, opts)
902
+
903
+ def spawn_command_options(self):
904
+ """Prepare command line options to binary."""
905
+ return dict(
906
+ dbin=self.layout_in,
907
+ dbout=self.layout_new,
908
+ npool=self.npool,
909
+ nslot=self.slots.nslot,
910
+ date=self.date,
911
+ masksize=4,
912
+ )
913
+
914
+ def execute(self, rh, opts):
915
+ """To mask input."""
916
+
917
+ sh = self.system
918
+
919
+ mask = [
920
+ x.rh
921
+ for x in self.context.sequence.effective_inputs(kind="atmsmask")
922
+ ]
923
+ if not mask:
924
+ raise ValueError("Could not find any MASK input")
925
+
926
+ # Have a look to mask file
927
+ if mask[0].container.localpath() != self.maskname:
928
+ sh.softlink(mask[0].container.localpath(), self.maskname)
929
+
930
+ sh.subtitle("Mask")
931
+ mask[0].container.cat()
932
+
933
+ # Standard execution
934
+ super().execute(rh, opts)
935
+
936
+ def postfix(self, rh, opts):
937
+ """Post shuffle / average cleaning."""
938
+ sh = self.system
939
+
940
+ with sh.cdcontext(self.layout_new):
941
+ for ccma in sh.glob("{:s}.*".format(self.layout_new)):
942
+ slurp = sh.cat(ccma, outsplit=False).replace(
943
+ self.layout_new, self.layout_in
944
+ )
945
+ with open(
946
+ ccma.replace(self.layout_new, self.layout_in), "w"
947
+ ) as fd:
948
+ fd.write(str(slurp))
949
+ sh.rm(ccma)
950
+
951
+ sh.mv(
952
+ self.layout_new, self.layout_in + "." + self.bingo.rh.resource.part
953
+ )
954
+
955
+ super().postfix(rh, opts)
956
+
957
+
958
+ class OdbCompress(Parallel, odb.OdbComponentDecoMixin, drhook.DrHookDecoMixin):
959
+ """Take a screening ODB ECMA database and create the compressed CCMA database."""
960
+
961
+ _footprint = dict(
962
+ attr=dict(
963
+ kind=dict(
964
+ values=["odbcompress"],
965
+ ),
966
+ ioassign=dict(),
967
+ )
968
+ )
969
+
970
+ def _mpitool_attributes(self, opts):
971
+ conf_dict = super()._mpitool_attributes(opts)
972
+ conf_dict.update({"mplbased": True})
973
+ return conf_dict
974
+
975
+ def prepare(self, rh, opts):
976
+ """Find any ODB candidate in input files and fox ODB env accordingly."""
977
+
978
+ obsall = [
979
+ x
980
+ for x in self.lookupodb()
981
+ if x.rh.resource.layout.lower() == "ecma"
982
+ ]
983
+ if len(obsall) > 1:
984
+ obsvirtual = [o for o in obsall if o.rh.resource.part == "virtual"]
985
+ if len(obsvirtual) != 1:
986
+ raise ValueError(
987
+ "One and only one virtual database must be provided"
988
+ )
989
+ ecma = obsvirtual[0]
990
+ elif len(obsall) == 1:
991
+ ecma = obsall[0]
992
+ else:
993
+ raise ValueError("No ECMA database provided")
994
+
995
+ # First create a fake CCMA
996
+ self.layout_new = "ccma"
997
+ ccma_path = self.odb_create_db(self.layout_new)
998
+ self.odb.fix_db_path(self.layout_new, ccma_path)
999
+
1000
+ self.layout_in = ecma.rh.resource.layout.upper()
1001
+ ecma_path = self.system.path.abspath(ecma.rh.container.localpath())
1002
+ self.odb.fix_db_path(self.layout_in, ecma_path)
1003
+
1004
+ self.odb.ioassign_gather(ecma_path, ccma_path)
1005
+
1006
+ self.odb.create_poolmask(self.layout_new, ccma_path)
1007
+
1008
+ self.odb_rw_or_overwrite_method(*obsall)
1009
+
1010
+ # Let ancesters handling most of the env setting
1011
+ super().prepare(rh, opts)
1012
+
1013
+ def spawn_command_options(self):
1014
+ """Prepare command line options to binary."""
1015
+ return dict(
1016
+ dbin=self.layout_in,
1017
+ dbout=self.layout_new,
1018
+ npool=self.npool,
1019
+ nslot=self.slots.nslot,
1020
+ date=self.date,
1021
+ )
1022
+
1023
+
1024
+ class OdbMatchup(Parallel, odb.OdbComponentDecoMixin, drhook.DrHookDecoMixin):
1025
+ """Report some information from post-minim CCMA to post-screening ECMA base."""
1026
+
1027
+ _footprint = dict(
1028
+ attr=dict(
1029
+ kind=dict(
1030
+ values=["matchup"],
1031
+ ),
1032
+ fcmalayout=dict(
1033
+ optional=True,
1034
+ value=["ecma", "ccma", "CCMA", "ECMA"],
1035
+ remap=dict(CCMA="ccma", ECMA="ecma"),
1036
+ ),
1037
+ )
1038
+ )
1039
+
1040
+ def _mpitool_attributes(self, opts):
1041
+ conf_dict = super()._mpitool_attributes(opts)
1042
+ conf_dict.update({"mplbased": True})
1043
+ return conf_dict
1044
+
1045
+ def prepare(self, rh, opts):
1046
+ """Find ODB candidates in input files."""
1047
+
1048
+ sh = self.system
1049
+
1050
+ # Looking for input observations
1051
+ obsscr_virtual = [
1052
+ x
1053
+ for x in self.lookupodb()
1054
+ if x.rh.resource.stage.startswith("screen")
1055
+ and x.rh.resource.part == "virtual"
1056
+ ]
1057
+ obsscr_parts = [
1058
+ x
1059
+ for x in self.lookupodb()
1060
+ if x.rh.resource.stage.startswith("screen")
1061
+ and x.rh.resource.part != "virtual"
1062
+ ]
1063
+ obscompressed = [
1064
+ x
1065
+ for x in self.lookupodb()
1066
+ if x.rh.resource.stage.startswith("min")
1067
+ or x.rh.resource.stage.startswith("traj")
1068
+ ]
1069
+
1070
+ # One database at a time
1071
+ if not obsscr_virtual:
1072
+ raise ValueError("Could not find any ODB screening input")
1073
+ if not obscompressed:
1074
+ raise ValueError("Could not find any ODB minim input")
1075
+
1076
+ # Set actual layout and path
1077
+ ecma = obsscr_virtual.pop(0)
1078
+ ccma = obscompressed.pop(0)
1079
+ self.layout_screening = ecma.rh.resource.layout
1080
+ self.layout_compressed = ccma.rh.resource.layout
1081
+ self.layout_fcma = (
1082
+ self.layout_compressed
1083
+ if self.fcmalayout is None
1084
+ else self.fcmalayout
1085
+ )
1086
+ ecma_path = sh.path.abspath(ecma.rh.container.localpath())
1087
+ ccma_path = sh.path.abspath(ccma.rh.container.localpath())
1088
+
1089
+ self.odb.fix_db_path(self.layout_screening, ecma_path)
1090
+ self.odb.fix_db_path(self.layout_compressed, ccma_path)
1091
+ self.odb.ioassign_gather(ccma_path, ecma_path)
1092
+
1093
+ # Ok, but why ???
1094
+ sh.cp(
1095
+ sh.path.join(ecma_path, "ECMA.dd"),
1096
+ sh.path.join(ccma_path, "ECMA.dd"),
1097
+ )
1098
+
1099
+ # Let ancesters handling most of the env setting
1100
+ super().prepare(rh, opts)
1101
+
1102
+ # Fix the input database intent
1103
+ self.odb_rw_or_overwrite_method(ecma)
1104
+ self.odb_rw_or_overwrite_method(*obsscr_parts)
1105
+
1106
+ def spawn_command_options(self):
1107
+ """Prepare command line options to binary."""
1108
+ return dict(
1109
+ dbin=self.layout_compressed,
1110
+ dbout=self.layout_screening,
1111
+ npool=self.npool,
1112
+ nslot=self.slots.nslot,
1113
+ date=self.date,
1114
+ fcma=self.layout_fcma,
1115
+ )
1116
+
1117
+
1118
+ class OdbReshuffle(
1119
+ Parallel, odb.OdbComponentDecoMixin, drhook.DrHookDecoMixin
1120
+ ):
1121
+ """Take a bunch of ECMA databases and create new ones with an updated number of pools."""
1122
+
1123
+ _footprint = dict(
1124
+ attr=dict(
1125
+ kind=dict(
1126
+ values=["reshuffle"],
1127
+ ),
1128
+ )
1129
+ )
1130
+
1131
+ _OUT_DIRECTORY = "reshuffled"
1132
+ _BARE_OUT_LAYOUT = "ccma"
1133
+
1134
+ def _mpitool_attributes(self, opts):
1135
+ conf_dict = super()._mpitool_attributes(opts)
1136
+ conf_dict.update({"mplbased": True})
1137
+ return conf_dict
1138
+
1139
+ def prepare(self, rh, opts):
1140
+ """Find ODB candidates in input files."""
1141
+
1142
+ # Looking for input observations
1143
+ obs_in_virtual = [
1144
+ x for x in self.lookupodb() if x.rh.resource.part == "virtual"
1145
+ ]
1146
+ if obs_in_virtual:
1147
+ raise ValueError("Do not input a Virtual database")
1148
+ self.obs_in_parts = [
1149
+ x for x in self.lookupodb() if x.rh.resource.part != "virtual"
1150
+ ]
1151
+
1152
+ # Find the input layout
1153
+ in_layout = {x.rh.resource.layout for x in self.obs_in_parts}
1154
+ if len(in_layout) != 1:
1155
+ raise ValueError(
1156
+ "Incoherent layout in input databases or no input databases"
1157
+ )
1158
+ self.layout_in = in_layout.pop()
1159
+
1160
+ # Some extra settings
1161
+ self.env.update(TO_ODB_FULL=1)
1162
+
1163
+ # prepare the ouputs' directory
1164
+ self.system.mkdir(self._OUT_DIRECTORY)
1165
+
1166
+ super().prepare(rh, opts)
1167
+
1168
+ def execute(self, rh, opts):
1169
+ """Loop on available databases."""
1170
+ sh = self.system
1171
+ for a_db in self.obs_in_parts:
1172
+ sh.subtitle(
1173
+ "Dealing with {:s}".format(a_db.rh.container.localpath())
1174
+ )
1175
+
1176
+ ecma_path = sh.path.abspath(a_db.rh.container.localpath())
1177
+ ccma_path = sh.path.abspath(
1178
+ sh.path.join(
1179
+ self._OUT_DIRECTORY,
1180
+ ".".join([self.layout_in.upper(), a_db.rh.resource.part]),
1181
+ )
1182
+ )
1183
+ self.odb_create_db(self._BARE_OUT_LAYOUT, dbpath=ccma_path)
1184
+ self.odb.fix_db_path(self.layout_in, ecma_path)
1185
+ self.odb.fix_db_path(self._BARE_OUT_LAYOUT, ccma_path)
1186
+ self.odb.ioassign_gather(ccma_path, ecma_path)
1187
+
1188
+ # Apparently te binary tries to write in the input databse,
1189
+ # no idea why but...
1190
+ self.odb_rw_or_overwrite_method(a_db)
1191
+
1192
+ super().execute(rh, opts)
1193
+
1194
+ # CCMA -> ECMA
1195
+ self.odb.change_layout(
1196
+ self._BARE_OUT_LAYOUT, self.layout_in, ccma_path
1197
+ )
1198
+
1199
+ def postfix(self, rh, opts):
1200
+ """Create a virtual database for output data."""
1201
+ self.system.subtitle("Creating the virtual database")
1202
+ virtual_db = self.odb_merge_if_needed(
1203
+ self.obs_in_parts, subdir=self._OUT_DIRECTORY
1204
+ )
1205
+ logger.info(
1206
+ "The output virtual DB was created: %s",
1207
+ self.system.path.join(self._OUT_DIRECTORY, virtual_db),
1208
+ )
1209
+
1210
+ def spawn_command_options(self):
1211
+ """Prepare command line options to binary."""
1212
+ return dict(
1213
+ dbin=self.layout_in,
1214
+ dbout=self._BARE_OUT_LAYOUT,
1215
+ npool=self.npool,
1216
+ )
1217
+
1218
+
1219
+ class FlagsCompute(
1220
+ Parallel, odb.OdbComponentDecoMixin, drhook.DrHookDecoMixin
1221
+ ):
1222
+ """Compute observations flags."""
1223
+
1224
+ _footprint = dict(
1225
+ info="Computation of observations flags.",
1226
+ attr=dict(
1227
+ kind=dict(
1228
+ values=["flagscomp"],
1229
+ ),
1230
+ ),
1231
+ )
1232
+
1233
+ def execute(self, rh, opts):
1234
+ """Spawn the binary for each of the input databases."""
1235
+ # Look for the input databases
1236
+ input_databases = self.context.sequence.effective_inputs(
1237
+ role="ECMA",
1238
+ kind="observations",
1239
+ )
1240
+ # Check that there is at least one database
1241
+ if len(input_databases) < 1:
1242
+ raise AttributeError("No database in input. Stop.")
1243
+
1244
+ for input_database in input_databases:
1245
+ ecma = input_database.rh
1246
+ ecma_filename = ecma.container.filename
1247
+ # Environment variable to set DB path
1248
+ self.odb.fix_db_path(ecma.resource.layout, ecma.container.abspath)
1249
+ self.env.setvar("ODB_ECMA", ecma_filename)
1250
+ logger.info("Variable %s set to %s.", "ODB_ECMA", ecma_filename)
1251
+ # Path to the IOASSIGN file
1252
+ self.env.IOASSIGN = self.system.path.join(
1253
+ ecma.container.abspath, "IOASSIGN"
1254
+ )
1255
+ # Let ancesters handling most of the env setting
1256
+ super().execute(rh, opts)
1257
+ # Rename the output file according to the name of the part of the observations treated
1258
+ self.system.mv("BDM_CQ", "_".join(["BDM_CQ", ecma.resource.part]))