vortex-nwp 2.0.0b1__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. vortex/__init__.py +75 -47
  2. vortex/algo/__init__.py +3 -2
  3. vortex/algo/components.py +944 -618
  4. vortex/algo/mpitools.py +802 -497
  5. vortex/algo/mpitools_templates/__init__.py +1 -0
  6. vortex/algo/serversynctools.py +34 -33
  7. vortex/config.py +19 -22
  8. vortex/data/__init__.py +9 -3
  9. vortex/data/abstractstores.py +593 -655
  10. vortex/data/containers.py +217 -162
  11. vortex/data/contents.py +65 -39
  12. vortex/data/executables.py +93 -102
  13. vortex/data/flow.py +40 -34
  14. vortex/data/geometries.py +228 -132
  15. vortex/data/handlers.py +436 -227
  16. vortex/data/outflow.py +15 -15
  17. vortex/data/providers.py +185 -163
  18. vortex/data/resources.py +48 -42
  19. vortex/data/stores.py +540 -417
  20. vortex/data/sync_templates/__init__.py +0 -0
  21. vortex/gloves.py +114 -87
  22. vortex/layout/__init__.py +1 -8
  23. vortex/layout/contexts.py +150 -84
  24. vortex/layout/dataflow.py +353 -202
  25. vortex/layout/monitor.py +264 -128
  26. vortex/nwp/__init__.py +5 -2
  27. vortex/nwp/algo/__init__.py +14 -5
  28. vortex/nwp/algo/assim.py +205 -151
  29. vortex/nwp/algo/clim.py +683 -517
  30. vortex/nwp/algo/coupling.py +447 -225
  31. vortex/nwp/algo/eda.py +437 -229
  32. vortex/nwp/algo/eps.py +403 -231
  33. vortex/nwp/algo/forecasts.py +416 -275
  34. vortex/nwp/algo/fpserver.py +683 -307
  35. vortex/nwp/algo/ifsnaming.py +205 -145
  36. vortex/nwp/algo/ifsroot.py +215 -122
  37. vortex/nwp/algo/monitoring.py +137 -76
  38. vortex/nwp/algo/mpitools.py +330 -190
  39. vortex/nwp/algo/odbtools.py +637 -353
  40. vortex/nwp/algo/oopsroot.py +454 -273
  41. vortex/nwp/algo/oopstests.py +90 -56
  42. vortex/nwp/algo/request.py +287 -206
  43. vortex/nwp/algo/stdpost.py +878 -522
  44. vortex/nwp/data/__init__.py +22 -4
  45. vortex/nwp/data/assim.py +125 -137
  46. vortex/nwp/data/boundaries.py +121 -68
  47. vortex/nwp/data/climfiles.py +193 -211
  48. vortex/nwp/data/configfiles.py +73 -69
  49. vortex/nwp/data/consts.py +426 -401
  50. vortex/nwp/data/ctpini.py +59 -43
  51. vortex/nwp/data/diagnostics.py +94 -66
  52. vortex/nwp/data/eda.py +50 -51
  53. vortex/nwp/data/eps.py +195 -146
  54. vortex/nwp/data/executables.py +440 -434
  55. vortex/nwp/data/fields.py +63 -48
  56. vortex/nwp/data/gridfiles.py +183 -111
  57. vortex/nwp/data/logs.py +250 -217
  58. vortex/nwp/data/modelstates.py +180 -151
  59. vortex/nwp/data/monitoring.py +72 -99
  60. vortex/nwp/data/namelists.py +254 -202
  61. vortex/nwp/data/obs.py +400 -308
  62. vortex/nwp/data/oopsexec.py +22 -20
  63. vortex/nwp/data/providers.py +90 -65
  64. vortex/nwp/data/query.py +71 -82
  65. vortex/nwp/data/stores.py +49 -36
  66. vortex/nwp/data/surfex.py +136 -137
  67. vortex/nwp/syntax/__init__.py +1 -1
  68. vortex/nwp/syntax/stdattrs.py +173 -111
  69. vortex/nwp/tools/__init__.py +2 -2
  70. vortex/nwp/tools/addons.py +22 -17
  71. vortex/nwp/tools/agt.py +24 -12
  72. vortex/nwp/tools/bdap.py +16 -5
  73. vortex/nwp/tools/bdcp.py +4 -1
  74. vortex/nwp/tools/bdm.py +3 -0
  75. vortex/nwp/tools/bdmp.py +14 -9
  76. vortex/nwp/tools/conftools.py +728 -378
  77. vortex/nwp/tools/drhook.py +12 -8
  78. vortex/nwp/tools/grib.py +65 -39
  79. vortex/nwp/tools/gribdiff.py +22 -17
  80. vortex/nwp/tools/ifstools.py +82 -42
  81. vortex/nwp/tools/igastuff.py +167 -143
  82. vortex/nwp/tools/mars.py +14 -2
  83. vortex/nwp/tools/odb.py +234 -125
  84. vortex/nwp/tools/partitioning.py +61 -37
  85. vortex/nwp/tools/satrad.py +27 -12
  86. vortex/nwp/util/async.py +83 -55
  87. vortex/nwp/util/beacon.py +10 -10
  88. vortex/nwp/util/diffpygram.py +174 -86
  89. vortex/nwp/util/ens.py +144 -63
  90. vortex/nwp/util/hooks.py +30 -19
  91. vortex/nwp/util/taskdeco.py +28 -24
  92. vortex/nwp/util/usepygram.py +278 -172
  93. vortex/nwp/util/usetnt.py +31 -17
  94. vortex/sessions.py +72 -39
  95. vortex/syntax/__init__.py +1 -1
  96. vortex/syntax/stdattrs.py +410 -171
  97. vortex/syntax/stddeco.py +31 -22
  98. vortex/toolbox.py +327 -192
  99. vortex/tools/__init__.py +11 -2
  100. vortex/tools/actions.py +110 -121
  101. vortex/tools/addons.py +111 -92
  102. vortex/tools/arm.py +42 -22
  103. vortex/tools/compression.py +72 -69
  104. vortex/tools/date.py +11 -4
  105. vortex/tools/delayedactions.py +242 -132
  106. vortex/tools/env.py +75 -47
  107. vortex/tools/folder.py +342 -171
  108. vortex/tools/grib.py +341 -162
  109. vortex/tools/lfi.py +423 -216
  110. vortex/tools/listings.py +109 -40
  111. vortex/tools/names.py +218 -156
  112. vortex/tools/net.py +655 -299
  113. vortex/tools/parallelism.py +93 -61
  114. vortex/tools/prestaging.py +55 -31
  115. vortex/tools/schedulers.py +172 -105
  116. vortex/tools/services.py +403 -334
  117. vortex/tools/storage.py +293 -358
  118. vortex/tools/surfex.py +24 -24
  119. vortex/tools/systems.py +1234 -643
  120. vortex/tools/targets.py +156 -100
  121. vortex/util/__init__.py +1 -1
  122. vortex/util/config.py +378 -327
  123. vortex/util/empty.py +2 -2
  124. vortex/util/helpers.py +56 -24
  125. vortex/util/introspection.py +18 -12
  126. vortex/util/iosponge.py +8 -4
  127. vortex/util/roles.py +4 -6
  128. vortex/util/storefunctions.py +39 -13
  129. vortex/util/structs.py +3 -3
  130. vortex/util/worker.py +29 -17
  131. vortex_nwp-2.1.0.dist-info/METADATA +67 -0
  132. vortex_nwp-2.1.0.dist-info/RECORD +144 -0
  133. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info}/WHEEL +1 -1
  134. vortex/layout/appconf.py +0 -109
  135. vortex/layout/jobs.py +0 -1276
  136. vortex/layout/nodes.py +0 -1424
  137. vortex/layout/subjobs.py +0 -464
  138. vortex_nwp-2.0.0b1.dist-info/METADATA +0 -50
  139. vortex_nwp-2.0.0b1.dist-info/RECORD +0 -146
  140. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info/licenses}/LICENSE +0 -0
  141. {vortex_nwp-2.0.0b1.dist-info → vortex_nwp-2.1.0.dist-info}/top_level.txt +0 -0
vortex/nwp/algo/eda.py CHANGED
@@ -24,27 +24,30 @@ class IFSEdaAbstractAlgo(IFSParallel):
24
24
 
25
25
  _abstract = True
26
26
  _footprint = dict(
27
- info='Base class for any EDA related task',
27
+ info="Base class for any EDA related task",
28
28
  attr=dict(
29
- inputnaming = dict(
30
- info = 'Prescribe your own naming template for input files.',
31
- optional = True,
32
- doc_visibility = footprints.doc.visibility.ADVANCED,
29
+ inputnaming=dict(
30
+ info="Prescribe your own naming template for input files.",
31
+ optional=True,
32
+ doc_visibility=footprints.doc.visibility.ADVANCED,
33
33
  ),
34
- )
34
+ ),
35
35
  )
36
36
 
37
37
  def naming_convention(self, kind, rh, actualfmt=None, **kwargs):
38
38
  """Take into account the *inputnaming* attribute."""
39
- if kind == 'edainput':
40
- return super().naming_convention(kind, rh,
41
- actualfmt=actualfmt,
42
- namingformat=self.inputnaming,
43
- **kwargs)
39
+ if kind == "edainput":
40
+ return super().naming_convention(
41
+ kind,
42
+ rh,
43
+ actualfmt=actualfmt,
44
+ namingformat=self.inputnaming,
45
+ **kwargs,
46
+ )
44
47
  else:
45
- return super().naming_convention(kind, rh,
46
- actualfmt=actualfmt,
47
- **kwargs)
48
+ return super().naming_convention(
49
+ kind, rh, actualfmt=actualfmt, **kwargs
50
+ )
48
51
 
49
52
 
50
53
  class IFSEdaEnsembleAbstractAlgo(IFSEdaAbstractAlgo):
@@ -55,24 +58,24 @@ class IFSEdaEnsembleAbstractAlgo(IFSEdaAbstractAlgo):
55
58
  to deal with missing members).
56
59
  """
57
60
 
58
- _INPUTS_ROLE = 'ModelState'
61
+ _INPUTS_ROLE = "ModelState"
59
62
 
60
63
  _abstract = True
61
64
  _footprint = dict(
62
- info='Base class for any EDA related task',
65
+ info="Base class for any EDA related task",
63
66
  attr=dict(
64
- nbmember = dict(
65
- info = "The number of members to deal will (auto-detected if omitted)",
66
- type = int,
67
- optional = True,
67
+ nbmember=dict(
68
+ info="The number of members to deal will (auto-detected if omitted)",
69
+ type=int,
70
+ optional=True,
68
71
  ),
69
- nbmin = dict(
70
- info = "The minimum number of input members that is mandatory to go one",
71
- type = int,
72
- optional = True,
73
- default = 2,
74
- )
75
- )
72
+ nbmin=dict(
73
+ info="The minimum number of input members that is mandatory to go one",
74
+ type=int,
75
+ optional=True,
76
+ default=2,
77
+ ),
78
+ ),
76
79
  )
77
80
 
78
81
  def __init__(self, *kargs, **kwargs):
@@ -97,56 +100,80 @@ class IFSEdaEnsembleAbstractAlgo(IFSEdaAbstractAlgo):
97
100
  @staticmethod
98
101
  def _members_sorting_key(s):
99
102
  """Return the sorting key for the **s** section."""
100
- member = getattr(s.rh.provider, 'member', None)
101
- member = - math.inf if member is None else member
102
- block = getattr(s.rh.provider, 'block', '')
103
- filename = getattr(s.rh.container, 'basename', '')
103
+ member = getattr(s.rh.provider, "member", None)
104
+ member = -math.inf if member is None else member
105
+ block = getattr(s.rh.provider, "block", "")
106
+ filename = getattr(s.rh.container, "basename", "")
104
107
  return member, block, filename
105
108
 
106
109
  def _members_effective_inputs(self):
107
110
  """The list of effective sections representing input data."""
108
- return sorted(self.context.sequence.effective_inputs(role=self._INPUTS_ROLE),
109
- key=self._members_sorting_key)
111
+ return sorted(
112
+ self.context.sequence.effective_inputs(role=self._INPUTS_ROLE),
113
+ key=self._members_sorting_key,
114
+ )
110
115
 
111
116
  def _members_all_inputs(self):
112
117
  """The list of sections representing input data."""
113
- return sorted(self.context.sequence.filtered_inputs(role=self._INPUTS_ROLE),
114
- key=self._members_sorting_key)
118
+ return sorted(
119
+ self.context.sequence.filtered_inputs(role=self._INPUTS_ROLE),
120
+ key=self._members_sorting_key,
121
+ )
115
122
 
116
123
  def _check_members_list_numbering(self, rh, mlist, mformats):
117
124
  """Check if, for the **mlist** members list, some renaming id needed."""
118
125
  if len(set(mlist)) != len(mlist):
119
- logger.warning("Some members are duplicated. That's very strange...")
126
+ logger.warning(
127
+ "Some members are duplicated. That's very strange..."
128
+ )
120
129
  if self.nbmember is None:
121
- self.algoassert(len(mformats) <= 1, 'Mixed formats are not allowed !')
130
+ self.algoassert(
131
+ len(mformats) <= 1, "Mixed formats are not allowed !"
132
+ )
122
133
  elif self.nbmember and len(mformats) > 1:
123
- logger.info('%s have mixed formats... please correct that.', self._INPUTS_ROLE)
134
+ logger.info(
135
+ "%s have mixed formats... please correct that.",
136
+ self._INPUTS_ROLE,
137
+ )
124
138
  if mlist and self.nbmember is not None:
125
139
  # Consistency check
126
140
  if len(mlist) != self.nbmember:
127
- logger.warning('Discrepancy between *nbmember* and effective input files...' +
128
- ' sticking with *nbmember*')
141
+ logger.warning(
142
+ "Discrepancy between *nbmember* and effective input files..."
143
+ + " sticking with *nbmember*"
144
+ )
129
145
  else:
130
146
  logger.info("The input files member numbers checks out !")
131
147
  return False # Ok, apparently the user knows what she/he is doing
132
148
  elif self.nbmember and not mlist:
133
149
  return False # Ok, apparently the user knows what she/he is doing
134
150
  elif mlist and self.nbmember is None:
135
- innc = self.naming_convention(kind='edainput', variant=self.kind, rh=rh,
136
- totalnumber=self.actual_totalnumber,
137
- actualfmt=mformats.pop())
138
- checkfiles = [m for m in range(1, len(mlist) + 1)
139
- if self.system.path.exists(innc(number=m))]
151
+ innc = self.naming_convention(
152
+ kind="edainput",
153
+ variant=self.kind,
154
+ rh=rh,
155
+ totalnumber=self.actual_totalnumber,
156
+ actualfmt=mformats.pop(),
157
+ )
158
+ checkfiles = [
159
+ m
160
+ for m in range(1, len(mlist) + 1)
161
+ if self.system.path.exists(innc(number=m))
162
+ ]
140
163
  if len(checkfiles) == len(mlist):
141
164
  logger.info("The input files numbering checks out !")
142
- return False # Ok, apparently the user knows what she/he is doing
165
+ return (
166
+ False # Ok, apparently the user knows what she/he is doing
167
+ )
143
168
  elif len(checkfiles) == 0:
144
169
  return True
145
170
  else:
146
- raise AlgoComponentError('Members renumbering is needed but some ' +
147
- 'files are blocking the way !')
171
+ raise AlgoComponentError(
172
+ "Members renumbering is needed but some "
173
+ + "files are blocking the way !"
174
+ )
148
175
  elif len(mlist) == 0 and self.nbmember is None:
149
- raise AlgoComponentError('No input files where found !')
176
+ raise AlgoComponentError("No input files where found !")
150
177
 
151
178
  def modelstate_needs_renumbering(self, rh):
152
179
  """Check if, for the **mlist** members list, some renaming id needed."""
@@ -160,35 +187,46 @@ class IFSEdaEnsembleAbstractAlgo(IFSEdaAbstractAlgo):
160
187
  def modelstate_renumbering(self, rh, mlist):
161
188
  """Actualy rename the effective inputs."""
162
189
  eff_format = mlist[0].rh.container.actualfmt
163
- innc = self.naming_convention(kind='edainput', variant=self.kind, rh=rh,
164
- totalnumber=self.actual_totalnumber,
165
- actualfmt=eff_format)
190
+ innc = self.naming_convention(
191
+ kind="edainput",
192
+ variant=self.kind,
193
+ rh=rh,
194
+ totalnumber=self.actual_totalnumber,
195
+ actualfmt=eff_format,
196
+ )
166
197
  for i, s in enumerate(mlist, start=1):
167
- logger.info("Soft-Linking %s to %s",
168
- s.rh.container.localpath(), innc(number=i))
198
+ logger.info(
199
+ "Soft-Linking %s to %s",
200
+ s.rh.container.localpath(),
201
+ innc(number=i),
202
+ )
169
203
  self.system.softlink(s.rh.container.localpath(), innc(number=i))
170
204
 
171
205
  def prepare_namelist_delta(self, rh, namcontents, namlocal):
172
206
  """Update the namelists with EDA related macros."""
173
- nam_updated = super(IFSEdaAbstractAlgo, self).prepare_namelist_delta(rh,
174
- namcontents,
175
- namlocal)
207
+ nam_updated = super(IFSEdaAbstractAlgo, self).prepare_namelist_delta(
208
+ rh, namcontents, namlocal
209
+ )
176
210
  if self.actual_nbe is not None:
177
- self._set_nam_macro(namcontents, namlocal, 'NBE', self.actual_nbe)
211
+ self._set_nam_macro(namcontents, namlocal, "NBE", self.actual_nbe)
178
212
  nam_updated = True
179
213
  return nam_updated
180
214
 
181
215
  def prepare(self, rh, opts):
182
216
  """Check the input files and act on it."""
183
- self.system.subtitle('Solving the input files nightmare...')
217
+ self.system.subtitle("Solving the input files nightmare...")
184
218
  if self.modelstate_needs_renumbering(rh):
185
219
  eff_sections = self._members_effective_inputs()
186
220
  if len(eff_sections) < self.actual_nbmin:
187
- raise AlgoComponentError('Not enough input files to continue...')
188
- logger.info("Starting input files renumbering. %d effective members found",
189
- len(eff_sections))
221
+ raise AlgoComponentError(
222
+ "Not enough input files to continue..."
223
+ )
224
+ logger.info(
225
+ "Starting input files renumbering. %d effective members found",
226
+ len(eff_sections),
227
+ )
190
228
  self.modelstate_renumbering(rh, eff_sections)
191
- self.system.subtitle('Other IFS related settings')
229
+ self.system.subtitle("Other IFS related settings")
192
230
  super().prepare(rh, opts)
193
231
 
194
232
 
@@ -200,24 +238,24 @@ class IFSEdaLaggedEnsembleAbstractAlgo(IFSEdaEnsembleAbstractAlgo):
200
238
  able to deal with missing members).
201
239
  """
202
240
 
203
- _PADDING_ROLE = 'PaddingModelState'
241
+ _PADDING_ROLE = "PaddingModelState"
204
242
 
205
243
  _abstract = True
206
244
  _footprint = dict(
207
- info='Base class for any EDA related task',
245
+ info="Base class for any EDA related task",
208
246
  attr=dict(
209
- nblag = dict(
210
- info = "The number of lagged dates (auto-detected if omitted)",
211
- type = int,
212
- optional = True,
247
+ nblag=dict(
248
+ info="The number of lagged dates (auto-detected if omitted)",
249
+ type=int,
250
+ optional=True,
213
251
  ),
214
- padding = dict(
215
- info = "Fill the gaps with some kind of climatological data",
216
- type = bool,
217
- optional = True,
218
- default = False,
219
- )
220
- )
252
+ padding=dict(
253
+ info="Fill the gaps with some kind of climatological data",
254
+ type=bool,
255
+ optional=True,
256
+ default=False,
257
+ ),
258
+ ),
221
259
  )
222
260
 
223
261
  def __init__(self, *kargs, **kwargs):
@@ -232,7 +270,11 @@ class IFSEdaLaggedEnsembleAbstractAlgo(IFSEdaEnsembleAbstractAlgo):
232
270
  @property
233
271
  def actual_totalnumber(self):
234
272
  """The total number of members."""
235
- return self.actual_nbe * self.actual_nresx if self.padding else self.actual_nbe
273
+ return (
274
+ self.actual_nbe * self.actual_nresx
275
+ if self.padding
276
+ else self.actual_nbe
277
+ )
236
278
 
237
279
  @property
238
280
  def actual_nbmin(self):
@@ -242,7 +284,7 @@ class IFSEdaLaggedEnsembleAbstractAlgo(IFSEdaEnsembleAbstractAlgo):
242
284
  def _members_sorting_key(self, s):
243
285
  """Return the sorting key for the **s** section."""
244
286
  stuple = list(super()._members_sorting_key(s))
245
- rdate = getattr(s.rh.resource, 'date')
287
+ rdate = getattr(s.rh.resource, "date")
246
288
  stuple.insert(0, rdate)
247
289
  return tuple(stuple)
248
290
 
@@ -256,8 +298,10 @@ class IFSEdaLaggedEnsembleAbstractAlgo(IFSEdaEnsembleAbstractAlgo):
256
298
  if all_dates and self.nblag is not None:
257
299
  # Consistency check
258
300
  if len(all_dates) != self.nblag:
259
- logger.warning('Discrepancy between *nblag* and input files...' +
260
- ' sticking with *nblag*')
301
+ logger.warning(
302
+ "Discrepancy between *nblag* and input files..."
303
+ + " sticking with *nblag*"
304
+ )
261
305
  elif all_dates and self.nblag is None:
262
306
  self._actual_nresx = len(all_dates)
263
307
 
@@ -266,18 +310,30 @@ class IFSEdaLaggedEnsembleAbstractAlgo(IFSEdaEnsembleAbstractAlgo):
266
310
  # For each date, check the member's list
267
311
  d_blocks = dict()
268
312
  for a_date in all_dates:
269
- d_sections = [sec for sec in all_sections if sec.rh.resource.date == a_date]
270
- d_members = sorted([sec.rh.provider.member for sec in d_sections])
313
+ d_sections = [
314
+ sec
315
+ for sec in all_sections
316
+ if sec.rh.resource.date == a_date
317
+ ]
318
+ d_members = sorted(
319
+ [sec.rh.provider.member for sec in d_sections]
320
+ )
271
321
  d_formats = {sec.rh.container.actualfmt for sec in d_sections}
272
322
  d_blocks[a_date] = (d_members, d_formats)
273
323
  ref_date, (eff_members, eff_formats) = d_blocks.popitem()
274
324
  for a_date, a_data in d_blocks.items():
275
- self.algoassert(a_data[0] == eff_members,
276
- "Inconsistent members list (date={!s} vs {!s})."
277
- .format(a_date, ref_date))
278
- self.algoassert(a_data[1] == eff_formats,
279
- "Inconsistent formats list (date={!s} vs {!s})."
280
- .format(a_date, ref_date))
325
+ self.algoassert(
326
+ a_data[0] == eff_members,
327
+ "Inconsistent members list (date={!s} vs {!s}).".format(
328
+ a_date, ref_date
329
+ ),
330
+ )
331
+ self.algoassert(
332
+ a_data[1] == eff_formats,
333
+ "Inconsistent formats list (date={!s} vs {!s}).".format(
334
+ a_date, ref_date
335
+ ),
336
+ )
281
337
  d_blocks[ref_date] = (eff_members, eff_formats)
282
338
  if eff_members and self.nbmember is None:
283
339
  # Here, NBE is the number of members for one date
@@ -288,8 +344,10 @@ class IFSEdaLaggedEnsembleAbstractAlgo(IFSEdaEnsembleAbstractAlgo):
288
344
  for member in a_data[0]:
289
345
  eff_members.append((a_date, member))
290
346
  else:
291
- eff_members = [(sec.rh.resource.date, sec.rh.provider.member)
292
- for sec in eff_sections]
347
+ eff_members = [
348
+ (sec.rh.resource.date, sec.rh.provider.member)
349
+ for sec in eff_sections
350
+ ]
293
351
  eff_formats = {sec.rh.container.actualfmt for sec in eff_sections}
294
352
  if eff_members and self.nbmember is None:
295
353
  # Here, NBE is the number of members for all dates
@@ -301,37 +359,64 @@ class IFSEdaLaggedEnsembleAbstractAlgo(IFSEdaEnsembleAbstractAlgo):
301
359
  """Actualy rename the effective inputs."""
302
360
  if self.padding:
303
361
  eff_format = mlist[0].rh.container.actualfmt
304
- innc = self.naming_convention(kind='edainput', variant=self.kind, rh=rh,
305
- totalnumber=self.actual_totalnumber,
306
- actualfmt=eff_format)
362
+ innc = self.naming_convention(
363
+ kind="edainput",
364
+ variant=self.kind,
365
+ rh=rh,
366
+ totalnumber=self.actual_totalnumber,
367
+ actualfmt=eff_format,
368
+ )
307
369
  all_sections = self._members_all_inputs()
308
- paddingstuff = self.context.sequence.effective_inputs(role=self._PADDING_ROLE)
370
+ paddingstuff = self.context.sequence.effective_inputs(
371
+ role=self._PADDING_ROLE
372
+ )
309
373
  for i, s in enumerate(all_sections, start=1):
310
- if s.stage == 'get' and s.rh.container.exists():
311
- logger.info("Soft-Linking %s to %s", s.rh.container.localpath(),
312
- innc(number=i))
313
- self.system.softlink(s.rh.container.localpath(), innc(number=i))
374
+ if s.stage == "get" and s.rh.container.exists():
375
+ logger.info(
376
+ "Soft-Linking %s to %s",
377
+ s.rh.container.localpath(),
378
+ innc(number=i),
379
+ )
380
+ self.system.softlink(
381
+ s.rh.container.localpath(), innc(number=i)
382
+ )
314
383
  else:
315
384
  mypadding = None
316
385
  for p in paddingstuff:
317
- if getattr(p.rh.resource, 'ipert',
318
- getattr(p.rh.resource, 'number', None)) == i:
386
+ if (
387
+ getattr(
388
+ p.rh.resource,
389
+ "ipert",
390
+ getattr(p.rh.resource, "number", None),
391
+ )
392
+ == i
393
+ ):
319
394
  mypadding = p
320
395
  break
321
396
  else:
322
- if (getattr(p.rh.resource, 'date', None) == s.rh.resource.date and
323
- getattr(p.rh.provider, 'member', None) == s.rh.provider.member):
397
+ if (
398
+ getattr(p.rh.resource, "date", None)
399
+ == s.rh.resource.date
400
+ and getattr(p.rh.provider, "member", None)
401
+ == s.rh.provider.member
402
+ ):
324
403
  mypadding = p
325
404
  break
326
405
  if mypadding is not None:
327
- logger.warning("Soft-Linking Padding data %s to %s",
328
- mypadding.rh.container.localpath(),
329
- innc(number=i))
330
- self.system.softlink(mypadding.rh.container.localpath(),
331
- innc(number=i))
406
+ logger.warning(
407
+ "Soft-Linking Padding data %s to %s",
408
+ mypadding.rh.container.localpath(),
409
+ innc(number=i),
410
+ )
411
+ self.system.softlink(
412
+ mypadding.rh.container.localpath(), innc(number=i)
413
+ )
332
414
  else:
333
- raise AlgoComponentError('No padding data where found for i= {:d}: {!s}'
334
- .format(i, s))
415
+ raise AlgoComponentError(
416
+ "No padding data where found for i= {:d}: {!s}".format(
417
+ i, s
418
+ )
419
+ )
335
420
  else:
336
421
  super().modelstate_renumbering(rh, mlist)
337
422
 
@@ -339,7 +424,9 @@ class IFSEdaLaggedEnsembleAbstractAlgo(IFSEdaEnsembleAbstractAlgo):
339
424
  """Update the namelists with EDA related macros."""
340
425
  nam_updated = super().prepare_namelist_delta(rh, namcontents, namlocal)
341
426
  if self.actual_nresx is not None:
342
- self._set_nam_macro(namcontents, namlocal, 'NRESX', self.actual_nresx)
427
+ self._set_nam_macro(
428
+ namcontents, namlocal, "NRESX", self.actual_nresx
429
+ )
343
430
  nam_updated = True
344
431
  return nam_updated
345
432
 
@@ -348,17 +435,17 @@ class IFSEdaFemars(IFSEdaAbstractAlgo):
348
435
  """Convert some FA file in ECMWF-GRIB files. PLEASE DO NOT USE !"""
349
436
 
350
437
  _footprint = dict(
351
- info='Convert some FA file in ECMWF-GRIB files.',
438
+ info="Convert some FA file in ECMWF-GRIB files.",
352
439
  attr=dict(
353
440
  kind=dict(
354
- values=['femars'],
441
+ values=["femars"],
355
442
  ),
356
- rawfiles = dict(
357
- type = bool,
358
- optional = True,
359
- default = False,
443
+ rawfiles=dict(
444
+ type=bool,
445
+ optional=True,
446
+ default=False,
360
447
  ),
361
- )
448
+ ),
362
449
  )
363
450
 
364
451
  def postfix(self, rh, opts):
@@ -366,31 +453,36 @@ class IFSEdaFemars(IFSEdaAbstractAlgo):
366
453
  sh = self.system
367
454
  # Gather rawfiles in folders
368
455
  if self.rawfiles:
369
- flist = sh.glob('tmprawfile_D000_L*')
370
- dest = 'rawfiles'
371
- logger.info('Creating a rawfiles pack: %s', dest)
456
+ flist = sh.glob("tmprawfile_D000_L*")
457
+ dest = "rawfiles"
458
+ logger.info("Creating a rawfiles pack: %s", dest)
372
459
  sh.mkdir(dest)
373
460
  for fic in flist:
374
- sh.mv(fic, dest, fmt='grib')
461
+ sh.mv(fic, dest, fmt="grib")
375
462
  super().postfix(rh, opts)
376
463
 
377
464
 
378
465
  class IFSInflationLike(IFSEdaAbstractAlgo):
379
466
  """Apply the inflation scheme on a given modelstate."""
380
467
 
381
- _RUNSTORE = 'RUNOUT'
382
- _USELESS_MATCH = re.compile(r'^(?P<target>\w+)\+term\d+:\d+$')
468
+ _RUNSTORE = "RUNOUT"
469
+ _USELESS_MATCH = re.compile(r"^(?P<target>\w+)\+term\d+:\d+$")
383
470
 
384
471
  _footprint = dict(
385
- info='Operations around the background error covariance matrix',
472
+ info="Operations around the background error covariance matrix",
386
473
  attr=dict(
387
474
  kind=dict(
388
- values=['infl', 'pert', ],
475
+ values=[
476
+ "infl",
477
+ "pert",
478
+ ],
389
479
  ),
390
480
  conf=dict(
391
- values=[701, ]
392
- )
393
- )
481
+ values=[
482
+ 701,
483
+ ]
484
+ ),
485
+ ),
394
486
  )
395
487
 
396
488
  def __init__(self, *kargs, **kwargs):
@@ -400,35 +492,57 @@ class IFSInflationLike(IFSEdaAbstractAlgo):
400
492
  def _check_effective_terms(self, roles):
401
493
  eff_terms = None
402
494
  for role in roles:
403
- eterm = {sec.rh.resource.term
404
- for sec in self.context.sequence.effective_inputs(role=role)}
495
+ eterm = {
496
+ sec.rh.resource.term
497
+ for sec in self.context.sequence.effective_inputs(role=role)
498
+ }
405
499
  if eterm:
406
500
  if eff_terms is None:
407
501
  eff_terms = eterm
408
502
  else:
409
503
  if eff_terms != eterm:
410
- raise AlgoComponentError("Inconsistencies between inputs effective terms.")
504
+ raise AlgoComponentError(
505
+ "Inconsistencies between inputs effective terms."
506
+ )
411
507
  return sorted(eff_terms)
412
508
 
413
- def _link_stuff_in(self, role, actualterm, targetnc, targetintent='in', wastebasket=None):
414
- estuff = [sec
415
- for sec in self.context.sequence.effective_inputs(role=role)
416
- if sec.rh.resource.term == actualterm]
509
+ def _link_stuff_in(
510
+ self, role, actualterm, targetnc, targetintent="in", wastebasket=None
511
+ ):
512
+ estuff = [
513
+ sec
514
+ for sec in self.context.sequence.effective_inputs(role=role)
515
+ if sec.rh.resource.term == actualterm
516
+ ]
417
517
  if len(estuff) > 1:
418
- logger.warning('Multiple %s for the same date ! Going on...', role)
518
+ logger.warning(
519
+ "Multiple %s for the same date ! Going on...", role
520
+ )
419
521
  elif len(estuff) == 1:
420
522
  # Detect the inputs format
421
523
  actfmt = estuff[0].rh.container.actualfmt
422
524
  nconv = self.naming_convention(actualfmt=actfmt, **targetnc)
423
525
  targetname = nconv(**targetnc)
424
526
  if self.system.path.exists(targetname):
425
- logger.info("%s: %s already exists. Hopping for the best...",
426
- role, targetname)
527
+ logger.info(
528
+ "%s: %s already exists. Hopping for the best...",
529
+ role,
530
+ targetname,
531
+ )
427
532
  else:
428
- logger.info("%s: copying (intent=%s) %s to %s", role, targetintent,
429
- estuff[0].rh.container.localpath(), targetname)
430
- self.system.cp(estuff[0].rh.container.localpath(), targetname,
431
- fmt=actfmt, intent=targetintent)
533
+ logger.info(
534
+ "%s: copying (intent=%s) %s to %s",
535
+ role,
536
+ targetintent,
537
+ estuff[0].rh.container.localpath(),
538
+ targetname,
539
+ )
540
+ self.system.cp(
541
+ estuff[0].rh.container.localpath(),
542
+ targetname,
543
+ fmt=actfmt,
544
+ intent=targetintent,
545
+ )
432
546
  if wastebasket is not None:
433
547
  wastebasket.append((targetname, actfmt))
434
548
  return nconv, targetname, estuff[0]
@@ -437,92 +551,152 @@ class IFSInflationLike(IFSEdaAbstractAlgo):
437
551
  def execute(self, rh, opts):
438
552
  """Loop on the various terms provided."""
439
553
 
440
- eff_terms = self._check_effective_terms(['ModelState',
441
- 'EnsembleMean',
442
- 'Guess'])
443
- fix_curclim = self.do_climfile_fixer(rh, convkind='modelclim')
444
- fix_clclim = self.do_climfile_fixer(rh, convkind='closest_modelclim')
554
+ eff_terms = self._check_effective_terms(
555
+ ["ModelState", "EnsembleMean", "Guess"]
556
+ )
557
+ fix_curclim = self.do_climfile_fixer(rh, convkind="modelclim")
558
+ fix_clclim = self.do_climfile_fixer(rh, convkind="closest_modelclim")
445
559
 
446
560
  if eff_terms:
447
561
  for actualterm in eff_terms:
448
562
  wastebasket = list()
449
- self.system.title('Loop on term {!s}'.format(actualterm))
450
- self.system.subtitle('Solving the input files nightmare...')
563
+ self.system.title("Loop on term {!s}".format(actualterm))
564
+ self.system.subtitle("Solving the input files nightmare...")
451
565
  # Ensemble Mean ?
452
- mean_number = 2 if self.model == 'arome' else 0
453
- targetnc = dict(kind='edainput', variant='infl', rh=rh, number=mean_number)
454
- self._link_stuff_in('EnsembleMean', actualterm, targetnc,
455
- wastebasket=wastebasket)
566
+ mean_number = 2 if self.model == "arome" else 0
567
+ targetnc = dict(
568
+ kind="edainput", variant="infl", rh=rh, number=mean_number
569
+ )
570
+ self._link_stuff_in(
571
+ "EnsembleMean",
572
+ actualterm,
573
+ targetnc,
574
+ wastebasket=wastebasket,
575
+ )
456
576
  # Model State ?
457
- targetnc = dict(kind='edainput', variant='infl', rh=rh, number=1)
458
- _, _, mstate = self._link_stuff_in('ModelState', actualterm, targetnc,
459
- wastebasket=wastebasket)
577
+ targetnc = dict(
578
+ kind="edainput", variant="infl", rh=rh, number=1
579
+ )
580
+ _, _, mstate = self._link_stuff_in(
581
+ "ModelState", actualterm, targetnc, wastebasket=wastebasket
582
+ )
460
583
  # Control ?
461
- control_number = 0 if self.model == 'arome' else 2
462
- targetnc = dict(kind='edainput', variant='infl', rh=rh, number=control_number)
463
- self._link_stuff_in('Control', actualterm, targetnc,
464
- wastebasket=wastebasket)
584
+ control_number = 0 if self.model == "arome" else 2
585
+ targetnc = dict(
586
+ kind="edainput",
587
+ variant="infl",
588
+ rh=rh,
589
+ number=control_number,
590
+ )
591
+ self._link_stuff_in(
592
+ "Control", actualterm, targetnc, wastebasket=wastebasket
593
+ )
465
594
  # Guess ?
466
- targetnc = dict(kind='edaoutput', variant='infl', rh=rh, number=1, term=Time(0))
467
- outnc, _, _ = self._link_stuff_in('Guess', actualterm, targetnc,
468
- targetintent='inout')
595
+ targetnc = dict(
596
+ kind="edaoutput",
597
+ variant="infl",
598
+ rh=rh,
599
+ number=1,
600
+ term=Time(0),
601
+ )
602
+ outnc, _, _ = self._link_stuff_in(
603
+ "Guess", actualterm, targetnc, targetintent="inout"
604
+ )
469
605
  if outnc is None:
470
- outnc = self.naming_convention(kind='edaoutput', variant='infl', rh=rh)
606
+ outnc = self.naming_convention(
607
+ kind="edaoutput", variant="infl", rh=rh
608
+ )
471
609
  # Fix clim !
472
610
  if fix_curclim and mstate:
473
611
  month = Month((mstate.rh.resource.date + actualterm).ymdh)
474
- self.climfile_fixer(rh=rh, convkind='modelclim', month=month,
475
- inputrole=('GlobalClim', 'InitialClim'),
476
- inputkind='clim_model')
612
+ self.climfile_fixer(
613
+ rh=rh,
614
+ convkind="modelclim",
615
+ month=month,
616
+ inputrole=("GlobalClim", "InitialClim"),
617
+ inputkind="clim_model",
618
+ )
477
619
  if fix_clclim and mstate:
478
- closestmonth = Month((mstate.rh.resource.date + actualterm).ymdh + ':closest')
479
- self.climfile_fixer(rh=rh, convkind='closest_modelclim', month=closestmonth,
480
- inputrole=('GlobalClim', 'InitialClim'),
481
- inputkind='clim_model')
620
+ closestmonth = Month(
621
+ (mstate.rh.resource.date + actualterm).ymdh
622
+ + ":closest"
623
+ )
624
+ self.climfile_fixer(
625
+ rh=rh,
626
+ convkind="closest_modelclim",
627
+ month=closestmonth,
628
+ inputrole=("GlobalClim", "InitialClim"),
629
+ inputkind="clim_model",
630
+ )
482
631
  # Deal with useless stuff... SADLY !
483
- useless = [sec
484
- for sec in self.context.sequence.effective_inputs(role='Useless')
485
- if (sec.rh.resource.term == actualterm and
486
- self._USELESS_MATCH.match(sec.rh.container.localpath()))]
632
+ useless = [
633
+ sec
634
+ for sec in self.context.sequence.effective_inputs(
635
+ role="Useless"
636
+ )
637
+ if (
638
+ sec.rh.resource.term == actualterm
639
+ and self._USELESS_MATCH.match(
640
+ sec.rh.container.localpath()
641
+ )
642
+ )
643
+ ]
487
644
  for a_useless in useless:
488
- targetname = self._USELESS_MATCH.match(a_useless.rh.container.localpath()).group('target')
645
+ targetname = self._USELESS_MATCH.match(
646
+ a_useless.rh.container.localpath()
647
+ ).group("target")
489
648
  if self.system.path.exists(targetname):
490
- logger.warning("Some useless stuff is already here: %s. I don't care...",
491
- targetname)
649
+ logger.warning(
650
+ "Some useless stuff is already here: %s. I don't care...",
651
+ targetname,
652
+ )
492
653
  else:
493
- logger.info("Dealing with useless stuff: %s -> %s",
494
- a_useless.rh.container.localpath(), targetname)
495
- self.system.cp(a_useless.rh.container.localpath(), targetname,
496
- fmt=a_useless.rh.container.actualfmt, intent='in')
497
- wastebasket.append((targetname, a_useless.rh.container.actualfmt))
654
+ logger.info(
655
+ "Dealing with useless stuff: %s -> %s",
656
+ a_useless.rh.container.localpath(),
657
+ targetname,
658
+ )
659
+ self.system.cp(
660
+ a_useless.rh.container.localpath(),
661
+ targetname,
662
+ fmt=a_useless.rh.container.actualfmt,
663
+ intent="in",
664
+ )
665
+ wastebasket.append(
666
+ (targetname, a_useless.rh.container.actualfmt)
667
+ )
498
668
 
499
669
  # Standard execution
500
670
  super().execute(rh, opts)
501
671
 
502
672
  # The concatenated listing
503
- self.system.cat('NODE.001_01', output='NODE.all')
673
+ self.system.cat("NODE.001_01", output="NODE.all")
504
674
 
505
675
  # prepares the next execution
506
676
  if len(eff_terms) > 1:
507
677
  self.system.mkdir(self._RUNSTORE)
508
678
  # Freeze the current output
509
- shelf_label = self.system.path.join(self._RUNSTORE, outnc(number=1, term=actualterm))
510
- self.system.move(outnc(number=1, term=Time(0)), shelf_label, fmt='fa')
679
+ shelf_label = self.system.path.join(
680
+ self._RUNSTORE, outnc(number=1, term=actualterm)
681
+ )
682
+ self.system.move(
683
+ outnc(number=1, term=Time(0)), shelf_label, fmt="fa"
684
+ )
511
685
  self._outputs_shelf.append(shelf_label)
512
686
  # Some cleaning
513
687
  for afile in wastebasket:
514
688
  self.system.remove(afile[0], fmt=afile[1])
515
- self.system.rmall('ncf927', 'dirlst')
689
+ self.system.rmall("ncf927", "dirlst")
516
690
  else:
517
691
  # We should not be here but whatever... some task are poorly written !
518
692
  super().execute(rh, opts)
519
693
 
520
694
  def postfix(self, rh, opts):
521
695
  """Post-processing cleaning."""
522
- self.system.title('Finalising the execution...')
696
+ self.system.title("Finalising the execution...")
523
697
  for afile in self._outputs_shelf:
524
698
  logger.info("Output found: %s", self.system.path.basename(afile))
525
- self.system.move(afile, self.system.path.basename(afile), fmt='fa')
699
+ self.system.move(afile, self.system.path.basename(afile), fmt="fa")
526
700
  super().postfix(rh, opts)
527
701
 
528
702
 
@@ -530,12 +704,14 @@ class IFSInflationFactor(IFSEdaEnsembleAbstractAlgo):
530
704
  """Compute an inflation factor based on individual members."""
531
705
 
532
706
  _footprint = dict(
533
- info='Compute an inflation factor based on individual members',
707
+ info="Compute an inflation factor based on individual members",
534
708
  attr=dict(
535
709
  kind=dict(
536
- values=['infl_factor', ],
710
+ values=[
711
+ "infl_factor",
712
+ ],
537
713
  ),
538
- )
714
+ ),
539
715
  )
540
716
 
541
717
 
@@ -546,15 +722,17 @@ class IFSInflationFactorLegacy(IFSInflationFactor):
546
722
  """
547
723
 
548
724
  _footprint = dict(
549
- info='Compute an inflation factor based on individual members',
725
+ info="Compute an inflation factor based on individual members",
550
726
  attr=dict(
551
727
  kind=dict(
552
- values=['infl', 'pert'],
728
+ values=["infl", "pert"],
553
729
  ),
554
730
  conf=dict(
555
- outcast=[701, ]
556
- )
557
- )
731
+ outcast=[
732
+ 701,
733
+ ]
734
+ ),
735
+ ),
558
736
  )
559
737
 
560
738
 
@@ -562,12 +740,14 @@ class IFSEnsembleMean(IFSEdaEnsembleAbstractAlgo):
562
740
  """Apply the inflation scheme on a given modelstate."""
563
741
 
564
742
  _footprint = dict(
565
- info='Operations around the background error covariance matrix',
743
+ info="Operations around the background error covariance matrix",
566
744
  attr=dict(
567
745
  kind=dict(
568
- values=['mean', ],
746
+ values=[
747
+ "mean",
748
+ ],
569
749
  ),
570
- )
750
+ ),
571
751
  )
572
752
 
573
753
 
@@ -575,20 +755,22 @@ class IFSCovB(IFSEdaLaggedEnsembleAbstractAlgo):
575
755
  """Operations around the background error covariance matrix."""
576
756
 
577
757
  _footprint = dict(
578
- info='Operations around the background error covariance matrix',
758
+ info="Operations around the background error covariance matrix",
579
759
  attr=dict(
580
760
  kind=dict(
581
- values=['covb', ],
761
+ values=[
762
+ "covb",
763
+ ],
582
764
  ),
583
- hybrid = dict(
584
- type = bool,
585
- optional = True,
586
- default = False,
765
+ hybrid=dict(
766
+ type=bool,
767
+ optional=True,
768
+ default=False,
587
769
  ),
588
- )
770
+ ),
589
771
  )
590
772
 
591
- _HYBRID_CLIM_ROLE = 'ClimatologicalModelState'
773
+ _HYBRID_CLIM_ROLE = "ClimatologicalModelState"
592
774
 
593
775
  @property
594
776
  def actual_totalnumber(self):
@@ -600,33 +782,59 @@ class IFSCovB(IFSEdaLaggedEnsembleAbstractAlgo):
600
782
  """Default pre-link for the initial condition file"""
601
783
  super().prepare(rh, opts)
602
784
  # Legacy...
603
- for num, sec in enumerate(sorted(self.context.sequence.effective_inputs(role='Rawfiles'),
604
- key=self._members_sorting_key),
605
- start=1):
785
+ for num, sec in enumerate(
786
+ sorted(
787
+ self.context.sequence.effective_inputs(role="Rawfiles"),
788
+ key=self._members_sorting_key,
789
+ ),
790
+ start=1,
791
+ ):
606
792
  repname = sec.rh.container.localpath()
607
- radical = repname.split('_')[0] + '_D{:03d}_L{:s}'
793
+ radical = repname.split("_")[0] + "_D{:03d}_L{:s}"
608
794
  for filename in self.system.listdir(repname):
609
- level = re.search(r'_L(\d+)$', filename)
795
+ level = re.search(r"_L(\d+)$", filename)
610
796
  if level is not None:
611
- self.system.softlink(self.system.path.join(repname, filename),
612
- radical.format(num, level.group(1)))
797
+ self.system.softlink(
798
+ self.system.path.join(repname, filename),
799
+ radical.format(num, level.group(1)),
800
+ )
613
801
  # Legacy...
614
- for num, sec in enumerate(sorted(self.context.sequence.effective_inputs(role='LaggedEnsemble'),
615
- key=self._members_sorting_key),
616
- start=1):
802
+ for num, sec in enumerate(
803
+ sorted(
804
+ self.context.sequence.effective_inputs(role="LaggedEnsemble"),
805
+ key=self._members_sorting_key,
806
+ ),
807
+ start=1,
808
+ ):
617
809
  repname = sec.rh.container.localpath()
618
- radical = repname.split('_')[0] + '_{:03d}'
810
+ radical = repname.split("_")[0] + "_{:03d}"
619
811
  self.system.softlink(repname, radical.format(num))
620
812
  # Requesting Hybrid computations ?
621
813
  if self.hybrid:
622
- hybstuff = self.context.sequence.effective_inputs(role=self._HYBRID_CLIM_ROLE)
814
+ hybstuff = self.context.sequence.effective_inputs(
815
+ role=self._HYBRID_CLIM_ROLE
816
+ )
623
817
  hybformat = hybstuff[0].rh.container.actualfmt
624
- totalnumber = self.actual_nbe * self.actual_nresx if self.padding else self.actual_nbe
625
- for i, tnum in enumerate(range(totalnumber + 1, 2 * totalnumber + 1)):
626
- innc = self.naming_convention(kind='edainput', variant=self.kind,
627
- totalnumber=self.actual_totalnumber,
628
- rh=rh, actualfmt=hybformat)
629
- logger.info("Soft-Linking %s to %s",
630
- hybstuff[i].rh.container.localpath(), innc(number=tnum))
631
- self.system.softlink(hybstuff[i].rh.container.localpath(),
632
- innc(number=tnum))
818
+ totalnumber = (
819
+ self.actual_nbe * self.actual_nresx
820
+ if self.padding
821
+ else self.actual_nbe
822
+ )
823
+ for i, tnum in enumerate(
824
+ range(totalnumber + 1, 2 * totalnumber + 1)
825
+ ):
826
+ innc = self.naming_convention(
827
+ kind="edainput",
828
+ variant=self.kind,
829
+ totalnumber=self.actual_totalnumber,
830
+ rh=rh,
831
+ actualfmt=hybformat,
832
+ )
833
+ logger.info(
834
+ "Soft-Linking %s to %s",
835
+ hybstuff[i].rh.container.localpath(),
836
+ innc(number=tnum),
837
+ )
838
+ self.system.softlink(
839
+ hybstuff[i].rh.container.localpath(), innc(number=tnum)
840
+ )