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,920 @@
1
+ """
2
+ AlgoComponents to build model's climatology files.
3
+ """
4
+
5
+ import copy
6
+
7
+ from bronx.datagrip import namelist
8
+ from bronx.fancies import loggers
9
+ import footprints
10
+
11
+ from vortex.algo.components import BlindRun, AlgoComponent, Parallel, TaylorRun
12
+ from vortex.data.geometries import HorizontalGeometry
13
+ from vortex.tools.grib import EcGribDecoMixin
14
+ from vortex.tools.parallelism import TaylorVortexWorker
15
+ from .ifsroot import IFSParallel
16
+ from ..tools.drhook import DrHookDecoMixin
17
+
18
+
19
+ #: No automatic export
20
+ __all__ = []
21
+
22
+ logger = loggers.getLogger(__name__)
23
+
24
+
25
+ class BuildPGD(BlindRun, DrHookDecoMixin, EcGribDecoMixin):
26
+ """Preparation of physiographic fields for Surfex."""
27
+
28
+ _footprint = dict(
29
+ info = "Physiographic fields for Surfex.",
30
+ attr = dict(
31
+ kind = dict(
32
+ values = ['buildpgd'],
33
+ ),
34
+ )
35
+ )
36
+
37
+
38
+ class BuildPGD_MPI(Parallel, DrHookDecoMixin, EcGribDecoMixin):
39
+ """Preparation of physiographic fields for Surfex."""
40
+
41
+ _footprint = dict(
42
+ info = "Physiographic fields for Surfex.",
43
+ attr = dict(
44
+ kind = dict(
45
+ values = ['buildpgd'],
46
+ ),
47
+ )
48
+ )
49
+
50
+
51
+ class C923(IFSParallel):
52
+ """Preparation of climatologic fields."""
53
+
54
+ _footprint = dict(
55
+ info = "Climatologic fields for Arpege/Arome.",
56
+ attr = dict(
57
+ kind = dict(
58
+ values = ['c923'],
59
+ ),
60
+ step = dict(
61
+ info = """Step of conf 923 (NAMCLI::N923).
62
+ Defines the kind of fields and database processed.""",
63
+ type = int,
64
+ values = footprints.util.rangex(1, 10)
65
+ ),
66
+ orog_in_pgd = dict(
67
+ info = """Whether orography may be read in a PGD file.
68
+ (NAMCLA::LIPGD=.TRUE.)""",
69
+ type = bool,
70
+ optional = True,
71
+ default = False,
72
+ ),
73
+ input_orog_name = dict(
74
+ info = "Filename for input orography file (case LNORO=.T.).",
75
+ optional = True,
76
+ default = 'Neworog',
77
+ ),
78
+ xpname = dict(
79
+ default = 'CLIM',
80
+ ),
81
+ )
82
+ )
83
+
84
+ def prepare(self, rh, opts):
85
+ super().prepare(rh, opts)
86
+ # check PGD if needed
87
+ if self.orog_in_pgd:
88
+ pgd = self.context.sequence.effective_inputs(role=('Pgd',))
89
+ if len(pgd) == 0:
90
+ raise ValueError("As long as 'orog_in_pgd' attribute of this " +
91
+ "algo component is True, a 'Role: Pgd' " +
92
+ "resource must be provided.")
93
+ pgd = pgd[0].rh
94
+ if pgd.resource.nativefmt == 'fa':
95
+ self.algoassert(
96
+ pgd.container.basename == self.input_orog_name,
97
+ "Local name for resource Pgd must be '{}'".
98
+ format(self.input_orog_name))
99
+ elif pgd.resource.nativefmt == 'lfi':
100
+ raise NotImplementedError('CY43T2 onwards: lfi PGD should not be used.')
101
+
102
+ def find_namelists(self, opts=None):
103
+ namrh_list = [x.rh
104
+ for x in self.context.sequence.effective_inputs(role=('Namelist',))]
105
+ self.algoassert(len(namrh_list) == 1,
106
+ "One and only one namelist necessary as input.")
107
+ return namrh_list
108
+
109
+ def prepare_namelist_delta(self, rh, namcontents, namlocal):
110
+ super().prepare_namelist_delta(rh, namcontents, namlocal)
111
+ namcontents['NAMMCC']['N923'] = self.step
112
+ namcontents.setmacro('LPGD', self.orog_in_pgd)
113
+ return True
114
+
115
+
116
+ class FinalizePGD(AlgoComponent):
117
+ """
118
+ Finalise PGD file: report spectrally optimized orography from Clim to PGD,
119
+ and add E-zone.
120
+
121
+ .. deprecated:: since Vortex 1.3.0, use :class:`SetFilteredOrogInPGD` instead.
122
+ """
123
+
124
+ _footprint = dict(
125
+ info = "Finalisation of PGD.",
126
+ attr = dict(
127
+ kind = dict(
128
+ values = ['finalize_pgd'],
129
+ ),
130
+ pgd_out_name = dict(
131
+ optional = True,
132
+ default = 'PGD_final.fa'
133
+ ),
134
+ )
135
+ )
136
+
137
+ def __init__(self, *args, **kwargs):
138
+ super().__init__(*args, **kwargs)
139
+ from ..util.usepygram import epygram_checker
140
+ ev = '1.2.14'
141
+ self.algoassert(epygram_checker.is_available(version=ev), "Epygram >= " + ev +
142
+ " is needed here")
143
+
144
+ def execute(self, rh, opts): # @UnusedVariable
145
+ """Convert SURFGEOPOTENTIEL from clim to SFX.ZS in pgd."""
146
+ import numpy
147
+ from ..util.usepygram import epygram, epy_env_prepare
148
+ from bronx.meteo.constants import g0
149
+ # Handle resources
150
+ clim = self.context.sequence.effective_inputs(role=('Clim',))
151
+ self.algoassert(len(clim) == 1, "One and only one Clim has to be provided")
152
+ pgdin = self.context.sequence.effective_inputs(role=('InputPGD',))
153
+ self.algoassert(len(pgdin) == 1, "One and only one InputPGD has to be provided")
154
+ if self.system.path.exists(self.pgd_out_name):
155
+ raise OSError("The output pgd file {!r} already exists.".format(self.pgd_out_name))
156
+ # copy fields
157
+ with epy_env_prepare(self.ticket):
158
+ epyclim = clim[0].rh.contents.data
159
+ epypgd = pgdin[0].rh.contents.data
160
+ epyclim.open()
161
+ epypgd.open()
162
+ pgdout = epygram.formats.resource(self.pgd_out_name, 'w', fmt='FA',
163
+ headername=epyclim.headername,
164
+ geometry=epyclim.geometry,
165
+ cdiden=epypgd.cdiden,
166
+ validity=epypgd.validity,
167
+ processtype=epypgd.processtype)
168
+ g = epyclim.readfield('SURFGEOPOTENTIEL')
169
+ g.operation('/', g0)
170
+ g.fid['FA'] = 'SFX.ZS'
171
+ for f in epypgd.listfields():
172
+ fld = epypgd.readfield(f)
173
+ if f == 'SFX.ZS':
174
+ fld = g
175
+ elif (isinstance(fld, epygram.fields.H2DField) and
176
+ fld.geometry.grid.get('LAMzone') is not None):
177
+ ext_data = numpy.ma.masked_equal(numpy.zeros(g.data.shape), 0.)
178
+ ext_data[:fld.geometry.dimensions['Y'],
179
+ :fld.geometry.dimensions['X']] = fld.data[:, :]
180
+ fld = footprints.proxy.fields.almost_clone(fld, geometry=g.geometry)
181
+ fld.setdata(ext_data)
182
+ pgdout.writefield(fld, compression=epypgd.fieldscompression.get(f, None))
183
+
184
+
185
+ class SetFilteredOrogInPGD(AlgoComponent):
186
+ """
187
+ Report spectrally optimized, filtered orography from Clim to PGD.
188
+ """
189
+
190
+ _footprint = dict(
191
+ info = "Report spectrally optimized, filtered orography from Clim to PGD.",
192
+ attr = dict(
193
+ kind = dict(
194
+ values = ['set_filtered_orog_in_pgd'],
195
+ ),
196
+ )
197
+ )
198
+
199
+ def __init__(self, *args, **kwargs):
200
+ super().__init__(*args, **kwargs)
201
+ from ..util.usepygram import epygram_checker
202
+ ev = '1.3.2'
203
+ self.algoassert(epygram_checker.is_available(version=ev), "Epygram >= " + ev +
204
+ " is needed here")
205
+
206
+ def execute(self, rh, opts): # @UnusedVariable
207
+ """Convert SURFGEOPOTENTIEL from clim to SFX.ZS in pgd."""
208
+ from ..util.usepygram import epygram_checker, epy_env_prepare
209
+ from bronx.meteo.constants import g0
210
+ # Handle resources
211
+ clim = self.context.sequence.effective_inputs(role=('Clim',))
212
+ self.algoassert(len(clim) == 1, "One and only one Clim to be provided")
213
+ pgdin = self.context.sequence.effective_inputs(role=('InputPGD',))
214
+ self.algoassert(len(pgdin) == 1, "One and only one InputPGD to be provided")
215
+ # copy fields
216
+ with epy_env_prepare(self.ticket):
217
+ epyclim = clim[0].rh.contents.data
218
+ epypgd = pgdin[0].rh.contents.data
219
+ epyclim.open()
220
+ epypgd.open(openmode='a')
221
+ # read spectrally fitted surface geopotential
222
+ g = epyclim.readfield('SURFGEOPOTENTIEL')
223
+ # convert to SURFEX orography
224
+ g.operation('/', g0)
225
+ g.fid['FA'] = 'SFX.ZS'
226
+ # write as orography
227
+ if epygram_checker.is_available(version='1.3.6'):
228
+ epypgd.fieldencoding(g.fid['FA'], update_fieldscompression=True)
229
+ else:
230
+ # blank read, just to update fieldscompression
231
+ epypgd.readfield(g.fid['FA'], getdata=False)
232
+ epypgd.writefield(g, compression=epypgd.fieldscompression.get(g.fid['FA'], None))
233
+ epypgd.close()
234
+
235
+
236
+ class MakeLAMDomain(AlgoComponent):
237
+ """
238
+ Wrapper to call Epygram domain making functions and generate
239
+ namelist deltas for geometry (BuildPGD & C923).
240
+ """
241
+
242
+ _footprint = dict(
243
+ attr = dict(
244
+ kind = dict(
245
+ values = ['make_domain', 'make_lam_domain'],
246
+ ),
247
+ geometry = dict(
248
+ info = "The horizontal geometry to be generated.",
249
+ type = HorizontalGeometry,
250
+ ),
251
+ mode = dict(
252
+ info = ("Kind of input for building geometry:" +
253
+ "'center_dims' to build domain given its centre and" +
254
+ "dimensions; 'lonlat_included' to build domain given" +
255
+ "an included lon/lat area."),
256
+ values = ['center_dims', 'lonlat_included']
257
+ ),
258
+ geom_params = dict(
259
+ info = ("Set of parameters and/or options to be passed to" +
260
+ "epygram.geometries.domain_making.build.build_geometry()" +
261
+ "or" +
262
+ "epygram.geometries.domain_making.build.build_geometry_fromlonlat()"),
263
+ type = footprints.FPDict,
264
+ ),
265
+ truncation = dict(
266
+ info = ("Type of spectral truncation, among" +
267
+ "('linear', 'quadratic', 'cubic')."),
268
+ optional = True,
269
+ default = 'linear',
270
+ ),
271
+ orography_truncation = dict(
272
+ info = ("Type of truncation of orography, among" +
273
+ "('linear', 'quadratic', 'cubic')."),
274
+ optional = True,
275
+ default = 'quadratic',
276
+ ),
277
+ e_zone_in_pgd = dict(
278
+ info = "Add E-zone sizes in BuildPGD namelist.",
279
+ optional = True,
280
+ type = bool,
281
+ default = False
282
+ ),
283
+ i_width_in_pgd = dict(
284
+ info = "Add I-width size in BuildPGD namelist.",
285
+ optional = True,
286
+ type = bool,
287
+ default = False
288
+ ),
289
+ # plot
290
+ illustration = dict(
291
+ info = "Create the domain illustration image.",
292
+ type = bool,
293
+ optional = True,
294
+ default = True
295
+ ),
296
+ illustration_fmt = dict(
297
+ info = "The format of the domain illustration image.",
298
+ values = ['png', 'pdf'],
299
+ optional = True,
300
+ default = 'png'
301
+ ),
302
+ plot_params = dict(
303
+ info = "Plot geometry parameters.",
304
+ type = footprints.FPDict,
305
+ optional = True,
306
+ default = footprints.FPDict({'background': True})
307
+ ),
308
+ )
309
+ )
310
+
311
+ def __init__(self, *args, **kwargs):
312
+ super().__init__(*args, **kwargs)
313
+ from ..util.usepygram import epygram_checker
314
+ ev = '1.2.14'
315
+ if self.e_zone_in_pgd:
316
+ ev = '1.3.2'
317
+ if self.i_width_in_pgd:
318
+ ev = '1.3.3'
319
+ self.algoassert(epygram_checker.is_available(version=ev), "Epygram >= " + ev +
320
+ " is needed here")
321
+ self._check_geometry()
322
+
323
+ def _check_geometry(self):
324
+ if self.mode == 'center_dims':
325
+ params = ['center_lon', 'center_lat', 'Xpoints_CI', 'Ypoints_CI',
326
+ 'resolution']
327
+ params_extended = params + ['tilting', 'Iwidth', 'force_projection',
328
+ 'maximize_CI_in_E', 'reference_lat']
329
+ elif self.mode == 'lonlat_included':
330
+ params = ['lonmin', 'lonmax', 'latmin', 'latmax',
331
+ 'resolution']
332
+ params_extended = params + ['Iwidth', 'force_projection', 'maximize_CI_in_E']
333
+ self.algoassert(set(params).issubset(set(self.geom_params.keys())),
334
+ "With mode=={!s}, geom_params must contain at least {!s}".
335
+ format(self.mode, params))
336
+ self.algoassert(set(self.geom_params.keys()).issubset(set(params_extended)),
337
+ "With mode=={!s}, geom_params must contain at most {!s}".
338
+ format(self.mode, params))
339
+
340
+ def execute(self, rh, opts): # @UnusedVariable
341
+ from ..util.usepygram import epygram
342
+ dm = epygram.geometries.domain_making
343
+ if self.mode == 'center_dims':
344
+ build_func = dm.build.build_geometry
345
+ lonlat_included = None
346
+ elif self.mode == 'lonlat_included':
347
+ build_func = dm.build.build_geometry_fromlonlat
348
+ lonlat_included = self.geom_params
349
+ # build geometry
350
+ geometry = build_func(interactive=False, **self.geom_params)
351
+ # summary, plot, namelists:
352
+ with open(self.geometry.tag + '_summary.txt', 'w') as o:
353
+ o.write(str(dm.output.summary(geometry)))
354
+ if self.illustration:
355
+ dm.output.plot_geometry(geometry,
356
+ lonlat_included=lonlat_included,
357
+ out='.'.join([self.geometry.tag,
358
+ self.illustration_fmt]),
359
+ **self.plot_params)
360
+ dm_extra_params = dict()
361
+ if self.e_zone_in_pgd:
362
+ dm_extra_params['Ezone_in_pgd'] = self.e_zone_in_pgd
363
+ if self.i_width_in_pgd:
364
+ dm_extra_params['Iwidth_in_pgd'] = self.i_width_in_pgd
365
+ namelists = dm.output.lam_geom2namelists(geometry,
366
+ truncation=self.truncation,
367
+ orography_subtruncation=self.orography_truncation,
368
+ **dm_extra_params)
369
+ dm.output.write_namelists(namelists, prefix=self.geometry.tag)
370
+
371
+
372
+ class MakeGaussGeometry(Parallel):
373
+ """
374
+ Wrapper to call Gauss geometry making RGRID and generate
375
+ namelist deltas for geometry (BuildPGD & C923).
376
+ """
377
+
378
+ _footprint = dict(
379
+ attr = dict(
380
+ kind = dict(
381
+ values = ['make_gauss_grid'],
382
+ ),
383
+ geometry = dict(
384
+ info = "The vortex horizontal geometry to be generated.",
385
+ type = HorizontalGeometry,
386
+ ),
387
+ truncation = dict(
388
+ info = 'nominal truncation',
389
+ type = int,
390
+ ),
391
+ grid = dict(
392
+ info = 'type of grid with regards to truncation, among (linear, quadratic, cubic)',
393
+ optional = True,
394
+ default = 'linear'
395
+ ),
396
+ orography_grid = dict(
397
+ info = 'orography subtruncation (linear, quadratic, cubic)',
398
+ optional = True,
399
+ default = 'quadratic'
400
+ ),
401
+ stretching = dict(
402
+ info = 'stretching factor',
403
+ type = float,
404
+ optional = True,
405
+ default = 1.,
406
+ ),
407
+ pole = dict(
408
+ info = 'pole of stretching (lon, lat), angles in degrees',
409
+ type = footprints.FPDict,
410
+ optional = True,
411
+ default = {'lon': 0., 'lat': 90.}
412
+ ),
413
+ # RGRID commandline options
414
+ latitudes = dict(
415
+ info = 'number of Gaussian latitudes',
416
+ type = int,
417
+ optional = True,
418
+ default = None
419
+ ),
420
+ longitudes = dict(
421
+ info = 'maximum (equatorial) number of longitudes',
422
+ type = int,
423
+ optional = True,
424
+ default = None
425
+ ),
426
+ orthogonality = dict(
427
+ info = 'orthogonality precision, as Log10() value',
428
+ type = int,
429
+ optional = True,
430
+ default = None
431
+ ),
432
+ aliasing = dict(
433
+ info = 'allowed aliasing, as a Log10() value',
434
+ type = int,
435
+ optional = True,
436
+ default = None
437
+ ),
438
+ oddity = dict(
439
+ info = 'odd numbers allowed (1) or not (0)',
440
+ type = int,
441
+ optional = True,
442
+ default = None
443
+ ),
444
+ verbosity = dict(
445
+ info = 'verbosity (0 or 1)',
446
+ type = int,
447
+ optional = True,
448
+ default = None
449
+ ),
450
+ # plot
451
+ illustration = dict(
452
+ info = "Create the domain illustration image.",
453
+ type = bool,
454
+ optional = True,
455
+ default = True
456
+ ),
457
+ illustration_fmt = dict(
458
+ info = "The format of the domain illustration image.",
459
+ values = ['png', 'pdf'],
460
+ optional = True,
461
+ default = 'png'
462
+ ),
463
+ plot_params = dict(
464
+ info = "Plot geometry parameters.",
465
+ type = footprints.FPDict,
466
+ optional = True,
467
+ default = footprints.FPDict({'background': True})
468
+ ),
469
+ )
470
+ )
471
+
472
+ def __init__(self, *args, **kwargs):
473
+ super().__init__(*args, **kwargs)
474
+ from ..util.usepygram import epygram_checker
475
+ ev = '1.2.14'
476
+ self.algoassert(epygram_checker.is_available(version=ev), "Epygram >= " + ev +
477
+ " is needed here")
478
+ self._complete_dimensions()
479
+ self._unit = 4
480
+
481
+ def _complete_dimensions(self):
482
+ from ..util.usepygram import epygram_checker
483
+ if epygram_checker.is_available(version='1.4.4'):
484
+ from epygram.geometries.SpectralGeometry import complete_gridpoint_dimensions
485
+ longitudes, latitudes = complete_gridpoint_dimensions(self.longitudes,
486
+ self.latitudes,
487
+ self.truncation,
488
+ self.grid,
489
+ self.stretching)
490
+ self._attributes['longitudes'] = longitudes
491
+ self._attributes['latitudes'] = latitudes
492
+ else:
493
+ self._old_internal_complete_dimensions()
494
+
495
+ def _old_internal_complete_dimensions(self):
496
+ from epygram.geometries.SpectralGeometry import gridpoint_dims_from_truncation
497
+ if self.latitudes is None and self.longitudes is None:
498
+ dims = gridpoint_dims_from_truncation({'max': self.truncation},
499
+ grid=self.grid)
500
+ self._attributes['latitudes'] = dims['lat_number']
501
+ self._attributes['longitudes'] = dims['max_lon_number']
502
+ elif self.longitudes is None:
503
+ self._attributes['longitudes'] = 2 * self.latitudes
504
+ elif self.latitudes is None:
505
+ if self.longitudes % 4 != 0:
506
+ self._attributes['latitudes'] = self.longitudes // 2 + 1
507
+ else:
508
+ self._attributes['latitudes'] = self.longitudes // 2
509
+
510
+ def spawn_command_options(self):
511
+ """Prepare options for the resource's command line."""
512
+ options = {'t': str(self.truncation),
513
+ 'g': str(self.latitudes),
514
+ 'l': str(self.longitudes),
515
+ 'f': str(self._unit)}
516
+ options_dict = {'orthogonality': 'o',
517
+ 'aliasing': 'a',
518
+ 'oddity': 'n',
519
+ 'verbosity': 'v'}
520
+ for k in options_dict.keys():
521
+ if getattr(self, k) is not None:
522
+ options[options_dict[k]] = str(getattr(self, k))
523
+ return options
524
+
525
+ def postfix(self, rh, opts):
526
+ """Complete and write namelists."""
527
+ from ..util.usepygram import epygram_checker
528
+ if epygram_checker.is_available(version='1.4.4'):
529
+ from epygram.geometries.domain_making.output import gauss_rgrid2namelists
530
+ gauss_rgrid2namelists('fort.{!s}'.format(self._unit),
531
+ self.geometry.tag,
532
+ self.latitudes,
533
+ self.longitudes,
534
+ self.truncation,
535
+ self.stretching,
536
+ self.orography_grid,
537
+ self.pole)
538
+ else:
539
+ self._old_internal_postfix(rh, opts)
540
+ super().postfix(rh, opts)
541
+
542
+ def _old_internal_postfix(self, rh, opts):
543
+ """Complete and write namelists."""
544
+ import math
545
+ from epygram.geometries.SpectralGeometry import truncation_from_gridpoint_dims
546
+ # complete scalar parameters
547
+ nam = namelist.NamelistSet()
548
+ nam.add(namelist.NamelistBlock('NAM_PGD_GRID'))
549
+ nam.add(namelist.NamelistBlock('NAMDIM'))
550
+ nam.add(namelist.NamelistBlock('NAMGEM'))
551
+ nam['NAM_PGD_GRID']['CGRID'] = 'GAUSS'
552
+ nam['NAMDIM']['NDGLG'] = self.latitudes
553
+ nam['NAMDIM']['NDLON'] = self.longitudes
554
+ nam['NAMDIM']['NSMAX'] = self.truncation
555
+ nam['NAMGEM']['NHTYP'] = 2
556
+ nam['NAMGEM']['NSTTYP'] = 2 if self.pole != {'lon': 0., 'lat': 90.} else 1
557
+ nam['NAMGEM']['RMUCEN'] = math.sin(math.radians(float(self.pole['lat'])))
558
+ nam['NAMGEM']['RLOCEN'] = math.radians(float(self.pole['lon']))
559
+ nam['NAMGEM']['RSTRET'] = self.stretching
560
+ # numbers of longitudes
561
+ with open('fort.{!s}'.format(self._unit)) as n:
562
+ namrgri = namelist.namparse(n)
563
+ nam.merge(namrgri)
564
+ # PGD namelist
565
+ nam_pgd = copy.deepcopy(nam)
566
+ nam_pgd['NAMGEM'].delvar('NHTYP')
567
+ nam_pgd['NAMGEM'].delvar('NSTTYP')
568
+ nam_pgd['NAMDIM'].delvar('NSMAX')
569
+ nam_pgd['NAMDIM'].delvar('NDLON')
570
+ with open('.'.join([self.geometry.tag,
571
+ 'namel_buildpgd',
572
+ 'geoblocks']),
573
+ 'w') as out:
574
+ out.write(nam_pgd.dumps(sorting=namelist.SECOND_ORDER_SORTING))
575
+ # C923 namelist
576
+ del nam['NAM_PGD_GRID']
577
+ with open('.'.join([self.geometry.tag,
578
+ 'namel_c923',
579
+ 'geoblocks']),
580
+ 'w') as out:
581
+ out.write(nam.dumps(sorting=namelist.SECOND_ORDER_SORTING))
582
+ # subtruncated grid for orography
583
+ from ..util.usepygram import epygram_checker
584
+ ev = '1.4.4'
585
+ if epygram_checker.is_available(version=ev):
586
+ trunc_nsmax = truncation_from_gridpoint_dims({'lat_number': self.latitudes,
587
+ 'max_lon_number': self.longitudes},
588
+ grid=self.orography_grid,
589
+ stretching_coef=self.stretching
590
+ )['max']
591
+ else:
592
+ trunc_nsmax = truncation_from_gridpoint_dims({'lat_number': self.latitudes,
593
+ 'max_lon_number': self.longitudes},
594
+ grid=self.orography_grid
595
+ )['max']
596
+ nam['NAMDIM']['NSMAX'] = trunc_nsmax
597
+ with open('.'.join([self.geometry.tag,
598
+ 'namel_c923_orography',
599
+ 'geoblocks']),
600
+ 'w') as out:
601
+ out.write(nam.dumps(sorting=namelist.SECOND_ORDER_SORTING))
602
+ # C927 (fullpos) namelist
603
+ nam = namelist.NamelistSet()
604
+ nam.add(namelist.NamelistBlock('NAMFPD'))
605
+ nam.add(namelist.NamelistBlock('NAMFPG'))
606
+ nam['NAMFPD']['NLAT'] = self.latitudes
607
+ nam['NAMFPD']['NLON'] = self.longitudes
608
+ nam['NAMFPG']['NFPMAX'] = self.truncation
609
+ nam['NAMFPG']['NFPHTYP'] = 2
610
+ nam['NAMFPG']['NFPTTYP'] = 2 if self.pole != {'lon': 0., 'lat': 90.} else 1
611
+ nam['NAMFPG']['FPMUCEN'] = math.sin(math.radians(float(self.pole['lat'])))
612
+ nam['NAMFPG']['FPLOCEN'] = math.radians(float(self.pole['lon']))
613
+ nam['NAMFPG']['FPSTRET'] = self.stretching
614
+ nrgri = [v for _, v in sorted(namrgri['NAMRGRI'].items())]
615
+ for i in range(len(nrgri)):
616
+ nam['NAMFPG']['NFPRGRI({:>4})'.format(i + 1)] = nrgri[i]
617
+ with open('.'.join([self.geometry.tag,
618
+ 'namel_c927',
619
+ 'geoblocks']),
620
+ 'w') as out:
621
+ out.write(nam.dumps(sorting=namelist.SECOND_ORDER_SORTING))
622
+
623
+
624
+ class MakeBDAPDomain(AlgoComponent):
625
+ """
626
+ Wrapper to call Epygram domain making functions and generate
627
+ namelist deltas for BDAP (lonlat) geometry (BuildPGD & C923).
628
+ """
629
+
630
+ _footprint = dict(
631
+ attr = dict(
632
+ kind = dict(
633
+ values = ['make_domain', 'make_bdap_domain'],
634
+ ),
635
+ geometry = dict(
636
+ info = "The horizontal geometry to be generated.",
637
+ type = HorizontalGeometry,
638
+ ),
639
+ mode = dict(
640
+ info = ("Kind of input for building geometry:" +
641
+ "'boundaries' to build domain given its lon/lat boundaries" +
642
+ "(+ resolution); 'inside_model' to build domain given" +
643
+ "a model geometry to be included in (+ resolution)."),
644
+ values = ['boundaries', 'inside_model']
645
+ ),
646
+ resolution = dict(
647
+ info = "Resolution in degrees.",
648
+ type = float,
649
+ optional = True,
650
+ default = None,
651
+ ),
652
+ resolution_x=dict(
653
+ info="X resolution in degrees (if different from Y).",
654
+ type=float,
655
+ optional = True,
656
+ default = None,
657
+ ),
658
+ resolution_y=dict(
659
+ info="Y resolution in degrees (if different from X).",
660
+ type=float,
661
+ optional = True,
662
+ default = None,
663
+ ),
664
+ boundaries = dict(
665
+ info = "Lonlat boundaries of the domain, case mode='boundaries'.",
666
+ type = footprints.FPDict,
667
+ optional = True,
668
+ default = None,
669
+ ),
670
+ model_clim = dict(
671
+ info = "Filename of the model clim, case mode='inside_model'.",
672
+ optional = True,
673
+ default = None,
674
+ ),
675
+ # plot
676
+ illustration = dict(
677
+ info = "Create the domain illustration image.",
678
+ type = bool,
679
+ optional = True,
680
+ default = True
681
+ ),
682
+ illustration_fmt = dict(
683
+ info = "The format of the domain illustration image.",
684
+ values = ['png', 'pdf'],
685
+ optional = True,
686
+ default = 'png'
687
+ ),
688
+ plot_params = dict(
689
+ info = "Plot geometry parameters.",
690
+ type = footprints.FPDict,
691
+ optional = True,
692
+ default = footprints.FPDict({'background': True})
693
+ ),
694
+ )
695
+ )
696
+
697
+ def __init__(self, *args, **kwargs):
698
+ super().__init__(*args, **kwargs)
699
+ from ..util.usepygram import epygram_checker
700
+ ev = '1.2.14'
701
+ self.algoassert(epygram_checker.is_available(version=ev), "Epygram >= " + ev +
702
+ " is needed here")
703
+ if self.mode == 'boundaries':
704
+ params = ['lonmin', 'lonmax', 'latmin', 'latmax']
705
+ self.algoassert(set(params) == set(self.boundaries.keys()),
706
+ "With mode=={}, boundaries must contain at least {}".
707
+ format(self.mode, str(params)))
708
+ if self.model_clim is not None:
709
+ logger.info('attribute *model_clim* ignored')
710
+ elif self.mode == 'inside_model':
711
+ self.algoassert(self.model_clim is not None,
712
+ "attribute *model_clim* must be provided with " +
713
+ "mode=='inside_model'.")
714
+ self.algoassert(self.sh.path.exists(self.model_clim))
715
+ if self.boundaries is not None:
716
+ logger.info('attribute *boundaries* ignored')
717
+ if self.resolution is None:
718
+ self.algoassert(None not in (self.resolution_x, self.resolution_y),
719
+ "Must provide *resolution* OR *resolution_x/resolution_y*")
720
+ else:
721
+ self.algoassert(self.resolution_x is None and self.resolution_y is None,
722
+ "Must provide *resolution* OR *resolution_x/resolution_y*")
723
+
724
+ def execute(self, rh, opts): # @UnusedVariable
725
+ from ..util.usepygram import epygram
726
+ dm = epygram.geometries.domain_making
727
+ if self.mode == 'inside_model':
728
+ r = epygram.formats.resource(self.model_clim, 'r')
729
+ if r.format == 'FA':
730
+ g = r.readfield('SURFGEOPOTENTIEL')
731
+ else:
732
+ raise NotImplementedError()
733
+ boundaries = dm.build.compute_lonlat_included(g.geometry)
734
+ else:
735
+ boundaries = self.boundaries
736
+ # build geometry
737
+ if self.resolution is None:
738
+ geometry = dm.build.build_lonlat_geometry(boundaries,
739
+ resolution=(self.resolution_x,
740
+ self.resolution_y))
741
+ else:
742
+ geometry = dm.build.build_lonlat_geometry(boundaries,
743
+ resolution=self.resolution)
744
+ # summary, plot, namelists:
745
+ if self.illustration:
746
+ fig, _ = geometry.plotgeometry(color='red',
747
+ title=self.geometry.tag,
748
+ **self.plot_params)
749
+ fig.savefig('.'.join([self.geometry.tag,
750
+ self.illustration_fmt]),
751
+ bbox_inches='tight')
752
+ namelists = dm.output.regll_geom2namelists(geometry)
753
+ dm.output.write_namelists(namelists, prefix=self.geometry.tag)
754
+ self.system.symlink('.'.join([self.geometry.tag,
755
+ 'namel_c923',
756
+ 'geoblocks']),
757
+ '.'.join([self.geometry.tag,
758
+ 'namel_c923_orography',
759
+ 'geoblocks']))
760
+
761
+
762
+ class AddPolesToGLOB(TaylorRun):
763
+ """
764
+ Add poles to a GLOB* regular FA Lon/Lat file that do not contain them.
765
+ """
766
+
767
+ _footprint = dict(
768
+ info = "Add poles to a GLOB* regular FA Lon/Lat file that do not contain them.",
769
+ attr = dict(
770
+ kind = dict(
771
+ values = ['add_poles'],
772
+ ),
773
+ )
774
+ )
775
+
776
+ def __init__(self, *args, **kwargs):
777
+ super().__init__(*args, **kwargs)
778
+ from ..util.usepygram import epygram_checker
779
+ ev = '1.3.4'
780
+ self.algoassert(epygram_checker.is_available(version=ev), "Epygram >= " + ev +
781
+ " is needed here")
782
+
783
+ def execute(self, rh, opts): # @UnusedVariable
784
+ """Add poles to a GLOB* regular FA Lon/Lat file that do not contain them."""
785
+ self._default_pre_execute(rh, opts)
786
+ common_i = self._default_common_instructions(rh, opts)
787
+ clims = self.context.sequence.effective_inputs(role=('Clim',))
788
+ self._add_instructions(common_i, dict(filename=[s.rh.container.localpath()
789
+ for s in clims]))
790
+ self._default_post_execute(rh, opts)
791
+
792
+
793
+ class _AddPolesWorker(TaylorVortexWorker):
794
+ _footprint = dict(
795
+ attr = dict(
796
+ kind = dict(
797
+ values = ['add_poles']
798
+ ),
799
+ filename = dict(
800
+ info='The file to be processed.'),
801
+ )
802
+ )
803
+
804
+ def vortex_task(self, **_):
805
+ from ..util.usepygram import add_poles_to_reglonlat_file, epy_env_prepare
806
+ with epy_env_prepare(self.ticket):
807
+ add_poles_to_reglonlat_file(self.filename)
808
+
809
+
810
+ class Festat(Parallel):
811
+ """
812
+ Class to run the festat binary.
813
+ """
814
+
815
+ _footprint = dict(
816
+ info = "Run festat",
817
+ attr = dict(
818
+ kind = dict(
819
+ values = ['run_festat', ],
820
+ ),
821
+ nb_digits = dict(
822
+ info = "Number of digits on which the name of the files should be written",
823
+ type = int,
824
+ default = 3,
825
+ optional = True,
826
+ ),
827
+ prefix = dict(
828
+ info = "Name of the files for the binary",
829
+ optional = True,
830
+ default = "CNAME",
831
+ ),
832
+ ),
833
+ )
834
+
835
+ _nb_input_files = 0
836
+
837
+ def prepare(self, rh, opts):
838
+ # Check the namelist
839
+ input_namelist = self.context.sequence.effective_inputs(role="Namelist", kind="namelist")
840
+ if len(input_namelist) != 1:
841
+ logger.error("One and only one namelist must be provided.")
842
+ raise ValueError("One and only one namelist must be provided.")
843
+ else:
844
+ input_namelist = input_namelist[0].rh
845
+ # Create links for the input files
846
+ maxinsec = 10 ** self.nb_digits
847
+ insec = self.context.sequence.effective_inputs(role="InputFiles")
848
+ nbinsec = len(insec)
849
+ if nbinsec > maxinsec:
850
+ logger.error("The number of input files %s exceed the maximum number of files available %s.",
851
+ nbinsec, maxinsec)
852
+ raise ValueError("The number of input files exceed the maximum number of files available.")
853
+ else:
854
+ logger.info("%s input files will be treated.", nbinsec)
855
+ i = 0
856
+ for sec in insec:
857
+ i += 1
858
+ self.system.symlink(sec.rh.container.actualpath(),
859
+ "{prefix}{number}".format(prefix=self.prefix,
860
+ number=str(i).zfill(self.nb_digits)))
861
+ # Put the number of sections and the prefix of the input files in the namelist
862
+ namcontents = input_namelist.contents
863
+ logger.info('Setup macro CNAME=%s in %s', self.prefix, input_namelist.container.actualpath())
864
+ namcontents.setmacro('CNAME', self.prefix)
865
+ logger.info('Setup macro NCASES=%s in %s', i, input_namelist.container.actualpath())
866
+ namcontents.setmacro('NCASES', i)
867
+ namcontents.rewrite(input_namelist.container)
868
+ self._nb_input_files = i
869
+ # Call the super class
870
+ super().prepare(rh, opts)
871
+
872
+ def postfix(self, rh, opts):
873
+ # Rename stabal files
874
+ list_stabal = self.system.glob("stab*")
875
+ for stabal in list_stabal:
876
+ self.system.mv(stabal,
877
+ "{stabal}.ncases_{ncases}".format(stabal=stabal, ncases=self._nb_input_files))
878
+ # Deal with diag files
879
+ list_diag_stat = self.system.glob("co*y")
880
+ if len(list_diag_stat) > 0:
881
+ diastat_dir_name = "dia.stat.ncases_{ncases}".format(ncases=self._nb_input_files)
882
+ self.system.mkdir(diastat_dir_name)
883
+ for file in list_diag_stat:
884
+ self.system.mv(file, diastat_dir_name + "/")
885
+ self.system.tar(diastat_dir_name + ".tar", diastat_dir_name)
886
+ list_diag_expl = self.system.glob("expl*y")
887
+ if len(list_diag_expl) > 0:
888
+ diaexpl_dir_name = "dia.expl.ncases_{ncases}".format(ncases=self._nb_input_files)
889
+ self.system.mkdir(diaexpl_dir_name)
890
+ for file in list_diag_expl:
891
+ self.system.mv(file, diaexpl_dir_name + "/")
892
+ self.system.tar(diaexpl_dir_name + ".tar", diaexpl_dir_name)
893
+ # Call the superclass
894
+ super().postfix(rh, opts)
895
+
896
+
897
+ class Fediacov(Parallel):
898
+ """
899
+ Class to compute diagnostics about covariance.
900
+ """
901
+
902
+ _footprint = dict(
903
+ info = "Run fediacov",
904
+ attr = dict(
905
+ kind = dict(
906
+ values = ['run_fediacov', ],
907
+ ),
908
+ ),
909
+ )
910
+
911
+ def postfix(self, rh, opts):
912
+ # Deal with diag files
913
+ list_diag = self.system.glob("*y")
914
+ if len(list_diag) > 0:
915
+ self.system.mkdir("diag")
916
+ for file in list_diag:
917
+ self.system.mv(file, "diag/")
918
+ self.system.tar("diag.tar", "diag")
919
+ # Call the superclass
920
+ super().postfix(rh, opts)