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