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