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.
- vortex/__init__.py +135 -0
- vortex/algo/__init__.py +12 -0
- vortex/algo/components.py +2136 -0
- vortex/algo/mpitools.py +1648 -0
- vortex/algo/mpitools_templates/envelope_wrapper_default.tpl +27 -0
- vortex/algo/mpitools_templates/envelope_wrapper_mpiauto.tpl +29 -0
- vortex/algo/mpitools_templates/wrapstd_wrapper_default.tpl +18 -0
- vortex/algo/serversynctools.py +170 -0
- vortex/config.py +115 -0
- vortex/data/__init__.py +13 -0
- vortex/data/abstractstores.py +1572 -0
- vortex/data/containers.py +780 -0
- vortex/data/contents.py +596 -0
- vortex/data/executables.py +284 -0
- vortex/data/flow.py +113 -0
- vortex/data/geometries.ini +2689 -0
- vortex/data/geometries.py +703 -0
- vortex/data/handlers.py +1021 -0
- vortex/data/outflow.py +67 -0
- vortex/data/providers.py +465 -0
- vortex/data/resources.py +201 -0
- vortex/data/stores.py +1271 -0
- vortex/gloves.py +282 -0
- vortex/layout/__init__.py +27 -0
- vortex/layout/appconf.py +109 -0
- vortex/layout/contexts.py +511 -0
- vortex/layout/dataflow.py +1069 -0
- vortex/layout/jobs.py +1276 -0
- vortex/layout/monitor.py +833 -0
- vortex/layout/nodes.py +1424 -0
- vortex/layout/subjobs.py +464 -0
- vortex/nwp/__init__.py +11 -0
- vortex/nwp/algo/__init__.py +12 -0
- vortex/nwp/algo/assim.py +483 -0
- vortex/nwp/algo/clim.py +920 -0
- vortex/nwp/algo/coupling.py +609 -0
- vortex/nwp/algo/eda.py +632 -0
- vortex/nwp/algo/eps.py +613 -0
- vortex/nwp/algo/forecasts.py +745 -0
- vortex/nwp/algo/fpserver.py +927 -0
- vortex/nwp/algo/ifsnaming.py +403 -0
- vortex/nwp/algo/ifsroot.py +311 -0
- vortex/nwp/algo/monitoring.py +202 -0
- vortex/nwp/algo/mpitools.py +554 -0
- vortex/nwp/algo/odbtools.py +974 -0
- vortex/nwp/algo/oopsroot.py +735 -0
- vortex/nwp/algo/oopstests.py +186 -0
- vortex/nwp/algo/request.py +579 -0
- vortex/nwp/algo/stdpost.py +1285 -0
- vortex/nwp/data/__init__.py +12 -0
- vortex/nwp/data/assim.py +392 -0
- vortex/nwp/data/boundaries.py +261 -0
- vortex/nwp/data/climfiles.py +539 -0
- vortex/nwp/data/configfiles.py +149 -0
- vortex/nwp/data/consts.py +929 -0
- vortex/nwp/data/ctpini.py +133 -0
- vortex/nwp/data/diagnostics.py +181 -0
- vortex/nwp/data/eda.py +148 -0
- vortex/nwp/data/eps.py +383 -0
- vortex/nwp/data/executables.py +1039 -0
- vortex/nwp/data/fields.py +96 -0
- vortex/nwp/data/gridfiles.py +308 -0
- vortex/nwp/data/logs.py +551 -0
- vortex/nwp/data/modelstates.py +334 -0
- vortex/nwp/data/monitoring.py +220 -0
- vortex/nwp/data/namelists.py +644 -0
- vortex/nwp/data/obs.py +748 -0
- vortex/nwp/data/oopsexec.py +72 -0
- vortex/nwp/data/providers.py +182 -0
- vortex/nwp/data/query.py +217 -0
- vortex/nwp/data/stores.py +147 -0
- vortex/nwp/data/surfex.py +338 -0
- vortex/nwp/syntax/__init__.py +9 -0
- vortex/nwp/syntax/stdattrs.py +375 -0
- vortex/nwp/tools/__init__.py +10 -0
- vortex/nwp/tools/addons.py +35 -0
- vortex/nwp/tools/agt.py +55 -0
- vortex/nwp/tools/bdap.py +48 -0
- vortex/nwp/tools/bdcp.py +38 -0
- vortex/nwp/tools/bdm.py +21 -0
- vortex/nwp/tools/bdmp.py +49 -0
- vortex/nwp/tools/conftools.py +1311 -0
- vortex/nwp/tools/drhook.py +62 -0
- vortex/nwp/tools/grib.py +268 -0
- vortex/nwp/tools/gribdiff.py +99 -0
- vortex/nwp/tools/ifstools.py +163 -0
- vortex/nwp/tools/igastuff.py +249 -0
- vortex/nwp/tools/mars.py +56 -0
- vortex/nwp/tools/odb.py +548 -0
- vortex/nwp/tools/partitioning.py +234 -0
- vortex/nwp/tools/satrad.py +56 -0
- vortex/nwp/util/__init__.py +6 -0
- vortex/nwp/util/async.py +184 -0
- vortex/nwp/util/beacon.py +40 -0
- vortex/nwp/util/diffpygram.py +359 -0
- vortex/nwp/util/ens.py +198 -0
- vortex/nwp/util/hooks.py +128 -0
- vortex/nwp/util/taskdeco.py +81 -0
- vortex/nwp/util/usepygram.py +591 -0
- vortex/nwp/util/usetnt.py +87 -0
- vortex/proxy.py +6 -0
- vortex/sessions.py +341 -0
- vortex/syntax/__init__.py +9 -0
- vortex/syntax/stdattrs.py +628 -0
- vortex/syntax/stddeco.py +176 -0
- vortex/toolbox.py +982 -0
- vortex/tools/__init__.py +11 -0
- vortex/tools/actions.py +457 -0
- vortex/tools/addons.py +297 -0
- vortex/tools/arm.py +76 -0
- vortex/tools/compression.py +322 -0
- vortex/tools/date.py +20 -0
- vortex/tools/ddhpack.py +10 -0
- vortex/tools/delayedactions.py +672 -0
- vortex/tools/env.py +513 -0
- vortex/tools/folder.py +663 -0
- vortex/tools/grib.py +559 -0
- vortex/tools/lfi.py +746 -0
- vortex/tools/listings.py +354 -0
- vortex/tools/names.py +575 -0
- vortex/tools/net.py +1790 -0
- vortex/tools/odb.py +10 -0
- vortex/tools/parallelism.py +336 -0
- vortex/tools/prestaging.py +186 -0
- vortex/tools/rawfiles.py +10 -0
- vortex/tools/schedulers.py +413 -0
- vortex/tools/services.py +871 -0
- vortex/tools/storage.py +1061 -0
- vortex/tools/surfex.py +61 -0
- vortex/tools/systems.py +3396 -0
- vortex/tools/targets.py +384 -0
- vortex/util/__init__.py +9 -0
- vortex/util/config.py +1071 -0
- vortex/util/empty.py +24 -0
- vortex/util/helpers.py +184 -0
- vortex/util/introspection.py +63 -0
- vortex/util/iosponge.py +76 -0
- vortex/util/roles.py +51 -0
- vortex/util/storefunctions.py +103 -0
- vortex/util/structs.py +26 -0
- vortex/util/worker.py +150 -0
- vortex_nwp-2.0.0b1.dist-info/LICENSE +517 -0
- vortex_nwp-2.0.0b1.dist-info/METADATA +50 -0
- vortex_nwp-2.0.0b1.dist-info/RECORD +146 -0
- vortex_nwp-2.0.0b1.dist-info/WHEEL +5 -0
- vortex_nwp-2.0.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AlgoComponents for OOPS.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import itertools
|
|
6
|
+
from collections import OrderedDict, defaultdict, namedtuple
|
|
7
|
+
import functools
|
|
8
|
+
|
|
9
|
+
import footprints
|
|
10
|
+
from bronx.fancies.dump import lightdump, fulldump
|
|
11
|
+
from bronx.stdtypes.date import Date, Time, Period
|
|
12
|
+
from bronx.compat.functools import cached_property
|
|
13
|
+
|
|
14
|
+
from vortex.algo.components import AlgoComponentError, AlgoComponentDecoMixin, Parallel
|
|
15
|
+
from vortex.algo.components import algo_component_deco_mixin_autodoc
|
|
16
|
+
from vortex.data import geometries
|
|
17
|
+
from vortex.tools import grib
|
|
18
|
+
from ..syntax.stdattrs import ArpIfsSimplifiedCycle as IfsCycle
|
|
19
|
+
from ..syntax.stdattrs import algo_member, oops_members_terms_lists
|
|
20
|
+
from ..tools import drhook, odb, satrad
|
|
21
|
+
|
|
22
|
+
#: No automatic export
|
|
23
|
+
__all__ = []
|
|
24
|
+
|
|
25
|
+
logger = footprints.loggers.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
OOPSMemberInfos = namedtuple('OOPSMemberInfos', ('member', 'date'))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EnsSizeAlgoComponentError(AlgoComponentError):
|
|
32
|
+
"""Exception raised when the ensemble is too small."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, nominal_ens_size, actual_ens_size, min_ens_size):
|
|
35
|
+
self.nominal_ens_size = nominal_ens_size
|
|
36
|
+
self.actual_ens_size = actual_ens_size
|
|
37
|
+
self.min_ens_size = min_ens_size
|
|
38
|
+
super().__init__('{:d} found ({:d} required)'.format(actual_ens_size, min_ens_size))
|
|
39
|
+
|
|
40
|
+
def __reduce__(self):
|
|
41
|
+
red = list(super().__reduce__())
|
|
42
|
+
red[1] = (self.nominal_ens_size, self.actual_ens_size, self.min_ens_size)
|
|
43
|
+
return tuple(red)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@algo_component_deco_mixin_autodoc
|
|
47
|
+
class OOPSMemberDecoMixin(AlgoComponentDecoMixin):
|
|
48
|
+
"""Add a member footprints' attribute and use it in the configuration files."""
|
|
49
|
+
|
|
50
|
+
_MIXIN_EXTRA_FOOTPRINTS = (algo_member, )
|
|
51
|
+
|
|
52
|
+
def _algo_member_deco_setup(self, rh, opts): # @UnusedVariable
|
|
53
|
+
"""Update the configuration files."""
|
|
54
|
+
if self.member is not None:
|
|
55
|
+
self._generic_config_subs['member'] = self.member
|
|
56
|
+
for namrh in self.updatable_namelists:
|
|
57
|
+
namrh.contents.setmacro('MEMBER', self.member)
|
|
58
|
+
namrh.contents.setmacro('PERTURB', self.member)
|
|
59
|
+
|
|
60
|
+
_MIXIN_PREPARE_HOOKS = (_algo_member_deco_setup, )
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@algo_component_deco_mixin_autodoc
|
|
64
|
+
class OOPSMembersTermsDetectDecoMixin(AlgoComponentDecoMixin):
|
|
65
|
+
"""Tries to detect a members/terms list using the sequence's inputs
|
|
66
|
+
|
|
67
|
+
This mixin class is intended to be used with AlgoComponent classes. It will
|
|
68
|
+
automatically add footprints' attributes related to this feature, crawl into
|
|
69
|
+
the sequence's input after the ``prepare`` step and, depending on the result
|
|
70
|
+
of the members/terms detection add ``members`` and ``effterms`` entries into
|
|
71
|
+
the configuration file substitutions dictionary ``_generic_config_subs``.
|
|
72
|
+
|
|
73
|
+
:note: Effective terms are considered (i.e term - (current_date - resource_date))
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
_membersdetect_roles = tuple(p + r
|
|
77
|
+
for p in ('', 'Ensemble')
|
|
78
|
+
for r in ('ModelState',
|
|
79
|
+
'Guess',
|
|
80
|
+
'InitialCondition',
|
|
81
|
+
'Background',
|
|
82
|
+
'SurfaceModelState',
|
|
83
|
+
'SurfaceGuess',
|
|
84
|
+
'SurfaceInitialCondition',
|
|
85
|
+
'SurfaceBackground',)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
_MIXIN_EXTRA_FOOTPRINTS = (footprints.Footprint(
|
|
89
|
+
info="Abstract mbdetect footprint",
|
|
90
|
+
attr=dict(
|
|
91
|
+
ens_minsize=dict(
|
|
92
|
+
info="For a multi-member algocomponent, the minimum of the ensemble.",
|
|
93
|
+
optional=True,
|
|
94
|
+
type=int
|
|
95
|
+
),
|
|
96
|
+
ens_failure_conf_objects=dict(
|
|
97
|
+
info="For a multi-member algocomponent, alternative config file when the ensemble is too small.",
|
|
98
|
+
optional=True,
|
|
99
|
+
),
|
|
100
|
+
strict_mbdetect=dict(
|
|
101
|
+
info="Performs a strict members/terms detection",
|
|
102
|
+
type=bool,
|
|
103
|
+
optional=True,
|
|
104
|
+
default=True,
|
|
105
|
+
doc_zorder=-60,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
),)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _stateless_members_detect(smap, basedate, section_check_cb, ensminsize=None, utest=False):
|
|
112
|
+
"""
|
|
113
|
+
This method does not really need to be static but this way it allows for
|
|
114
|
+
unit-testing (see ``tests.tests_algo.test_oopspara.py``).
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
# Look for members
|
|
118
|
+
# The ensemble is possibly lagged... be careful
|
|
119
|
+
allmembers = defaultdict(
|
|
120
|
+
functools.partial(defaultdict, functools.partial(defaultdict, set))
|
|
121
|
+
)
|
|
122
|
+
members = set()
|
|
123
|
+
r_members = []
|
|
124
|
+
for arole, srole in smap.items():
|
|
125
|
+
# Gather data
|
|
126
|
+
for s in srole:
|
|
127
|
+
minfo = OOPSMemberInfos(getattr(s.rh.provider, 'member', None),
|
|
128
|
+
getattr(s.rh.resource, 'date', None))
|
|
129
|
+
allmembers[arole][minfo][getattr(s.rh.resource, 'term', None)].add(s)
|
|
130
|
+
# Sanity checks and filtering
|
|
131
|
+
role_members_info = set(allmembers[arole].keys())
|
|
132
|
+
if None in {a_member.member for a_member in role_members_info}:
|
|
133
|
+
# Ignore sections when some sections have no members defined
|
|
134
|
+
if len(role_members_info) > 1:
|
|
135
|
+
logger.warning('Role: %s. Only some sections have a member number.', arole)
|
|
136
|
+
role_members_info = set()
|
|
137
|
+
if len(role_members_info) > 1:
|
|
138
|
+
if not members:
|
|
139
|
+
members = role_members_info
|
|
140
|
+
else:
|
|
141
|
+
# Consistency check on members numbering
|
|
142
|
+
if members != role_members_info:
|
|
143
|
+
raise AlgoComponentError('Inconsistent members numbering')
|
|
144
|
+
else:
|
|
145
|
+
# If there is only one member, ignore it: it's not really an ensemble!
|
|
146
|
+
del allmembers[arole]
|
|
147
|
+
|
|
148
|
+
lagged = False
|
|
149
|
+
if members:
|
|
150
|
+
# Is it a lagged ensemble ?
|
|
151
|
+
members_by_date = defaultdict(set)
|
|
152
|
+
for m in members:
|
|
153
|
+
members_by_date[m.date].add(m.member)
|
|
154
|
+
lagged = len(members_by_date.keys()) > 1
|
|
155
|
+
# Be verbose...
|
|
156
|
+
if lagged:
|
|
157
|
+
for a_date, a_mset in members_by_date.items():
|
|
158
|
+
logger.info('Members detected from date=%s: %s',
|
|
159
|
+
a_date, ','.join(sorted(str(m) for m in a_mset)))
|
|
160
|
+
else:
|
|
161
|
+
logger.info('Members detected: %s',
|
|
162
|
+
','.join(sorted(str(m[0]) for m in members)))
|
|
163
|
+
logger.info('Total number of detected members: %d', len(members))
|
|
164
|
+
r_members = sorted(allmembers.keys())
|
|
165
|
+
logger.info('Members roles: %s', ','.join(r_members))
|
|
166
|
+
|
|
167
|
+
# Look for effective terms
|
|
168
|
+
alleffterms = dict()
|
|
169
|
+
for arole, srole in allmembers.items():
|
|
170
|
+
first_effterms = None
|
|
171
|
+
for minfo, mterms in srole.items():
|
|
172
|
+
effterms = set()
|
|
173
|
+
for term in mterms.keys():
|
|
174
|
+
effterms.add(term - (basedate - minfo.date)
|
|
175
|
+
if term is not None and minfo.date is not None
|
|
176
|
+
else None)
|
|
177
|
+
# Intra-role consistency
|
|
178
|
+
if first_effterms is None:
|
|
179
|
+
first_effterms = effterms
|
|
180
|
+
else:
|
|
181
|
+
# Consistency check on members numbering
|
|
182
|
+
if effterms != first_effterms:
|
|
183
|
+
raise AlgoComponentError('Inconsistent effective terms between members sets (role={:s})'
|
|
184
|
+
.format(arole))
|
|
185
|
+
# If there are more than one term, consider it
|
|
186
|
+
if len(first_effterms) > 1:
|
|
187
|
+
# Check that there is no None in the way
|
|
188
|
+
if None in first_effterms:
|
|
189
|
+
raise AlgoComponentError(
|
|
190
|
+
'For a given role, all of the resources or none of then should have a term (role={:s})'
|
|
191
|
+
.format(arole)
|
|
192
|
+
)
|
|
193
|
+
# Remove Nones
|
|
194
|
+
first_effterms = {e for e in first_effterms if e is not None}
|
|
195
|
+
if len(first_effterms):
|
|
196
|
+
alleffterms[arole] = first_effterms
|
|
197
|
+
|
|
198
|
+
# Check consistency and be verbose
|
|
199
|
+
r_effterms = []
|
|
200
|
+
l_effterms = []
|
|
201
|
+
if alleffterms:
|
|
202
|
+
# Hard check only when multiple effetive terms are found
|
|
203
|
+
multieffterms = {r: ets for r, ets in alleffterms.items() if len(ets) > 1}
|
|
204
|
+
if multieffterms:
|
|
205
|
+
if sum(1 for _ in itertools.groupby(multieffterms.values())) > 1:
|
|
206
|
+
raise AlgoComponentError('Inconsistent effective terms between relevant roles')
|
|
207
|
+
r_effterms = sorted(multieffterms.keys())
|
|
208
|
+
_, l_effterms = multieffterms.popitem()
|
|
209
|
+
else:
|
|
210
|
+
if sum(1 for _ in itertools.groupby(alleffterms.values())) == 1:
|
|
211
|
+
r_effterms = sorted(alleffterms.keys())
|
|
212
|
+
_, l_effterms = alleffterms.popitem()
|
|
213
|
+
l_effterms = sorted(l_effterms)
|
|
214
|
+
logger.info('Effective terms detected: %s', ','.join([str(t) for t in effterms]))
|
|
215
|
+
logger.info('Terms roles: %s', ','.join(sorted(alleffterms.keys())))
|
|
216
|
+
|
|
217
|
+
# Theoretical ensemble size
|
|
218
|
+
nominal_ens_size = len(members)
|
|
219
|
+
if nominal_ens_size:
|
|
220
|
+
eff_members = set()
|
|
221
|
+
for mb in members:
|
|
222
|
+
# Look for missing resources in the various relevant roles
|
|
223
|
+
broken = list()
|
|
224
|
+
for arole in allmembers.keys():
|
|
225
|
+
broken.extend([(s, arole)
|
|
226
|
+
for t, slist in allmembers[arole][mb].items()
|
|
227
|
+
for s in slist
|
|
228
|
+
if not section_check_cb(s)])
|
|
229
|
+
for s, arole in broken:
|
|
230
|
+
if not utest:
|
|
231
|
+
logger.warning('Missing items: %s (role: %s).',
|
|
232
|
+
s.rh.container.localpath(), arole)
|
|
233
|
+
if broken:
|
|
234
|
+
logger.warning('Throwing away member: %s', mb)
|
|
235
|
+
else:
|
|
236
|
+
eff_members.add(mb)
|
|
237
|
+
# Sanity checks depending on ensminsize
|
|
238
|
+
if ensminsize is None and len(eff_members) != nominal_ens_size:
|
|
239
|
+
raise EnsSizeAlgoComponentError(nominal_ens_size, len(eff_members), nominal_ens_size)
|
|
240
|
+
elif ensminsize is not None and len(eff_members) < ensminsize:
|
|
241
|
+
raise EnsSizeAlgoComponentError(nominal_ens_size, len(eff_members), ensminsize)
|
|
242
|
+
|
|
243
|
+
members = eff_members
|
|
244
|
+
|
|
245
|
+
l_members = [m.member for m in sorted(members)]
|
|
246
|
+
l_members_d = [m.date for m in sorted(members)]
|
|
247
|
+
l_members_o = [None if m.date is None else (basedate - m.date)
|
|
248
|
+
for m in sorted(members)]
|
|
249
|
+
|
|
250
|
+
return (l_members, l_members_d, l_members_o, l_effterms,
|
|
251
|
+
lagged, nominal_ens_size, r_members, r_effterms)
|
|
252
|
+
|
|
253
|
+
def members_detect(self):
|
|
254
|
+
"""Detect the members/terms list and update the substitution dictionary."""
|
|
255
|
+
sectionsmap = {r: self.context.sequence.filtered_inputs(role=r, no_alternates=True)
|
|
256
|
+
for r in self._membersdetect_roles}
|
|
257
|
+
try:
|
|
258
|
+
(self._ens_members_num,
|
|
259
|
+
self._ens_members_date,
|
|
260
|
+
self._ens_members_offset,
|
|
261
|
+
self._ens_effterms,
|
|
262
|
+
self._ens_is_lagged,
|
|
263
|
+
self._ens_nominal_size,
|
|
264
|
+
_, _) = self._stateless_members_detect(sectionsmap,
|
|
265
|
+
self.date,
|
|
266
|
+
self.context.sequence.is_somehow_viable,
|
|
267
|
+
self.ens_minsize)
|
|
268
|
+
except EnsSizeAlgoComponentError as e:
|
|
269
|
+
if self.strict_mbdetect and self.ens_failure_conf_objects is None:
|
|
270
|
+
raise
|
|
271
|
+
else:
|
|
272
|
+
logger.warning("Members detection failed: %s", str(e))
|
|
273
|
+
logger.info("'strict_mbdetect' is False... going on with empty lists.")
|
|
274
|
+
self._ens_members_num = []
|
|
275
|
+
self._ens_members_date = []
|
|
276
|
+
self._ens_members_offset = []
|
|
277
|
+
self._ens_is_lagged = False
|
|
278
|
+
self._ens_effterms = []
|
|
279
|
+
self._ens_nominal_size = e.nominal_ens_size
|
|
280
|
+
if self.ens_failure_conf_objects:
|
|
281
|
+
# Find the new configuration object
|
|
282
|
+
main_conf = None
|
|
283
|
+
for sconf, ssub in self._individual_config_subs.items():
|
|
284
|
+
if getattr(sconf.rh.resource, 'objects', '') == self.ens_failure_conf_objects:
|
|
285
|
+
main_conf = sconf
|
|
286
|
+
main_conf_sub = ssub
|
|
287
|
+
break
|
|
288
|
+
if main_conf is None:
|
|
289
|
+
raise AlgoComponentError('Alternative configuration file was not found')
|
|
290
|
+
# Update the config ordered dictionary
|
|
291
|
+
del self._individual_config_subs[main_conf]
|
|
292
|
+
new_individual_config_subs = OrderedDict()
|
|
293
|
+
new_individual_config_subs[main_conf] = main_conf_sub
|
|
294
|
+
for sconf, ssub in self._individual_config_subs.items():
|
|
295
|
+
new_individual_config_subs[sconf] = ssub
|
|
296
|
+
self._individual_config_subs = new_individual_config_subs
|
|
297
|
+
logger.info("Using an alternative configuration file (objects=%s, role=%s)",
|
|
298
|
+
self.ens_failure_conf_objects, main_conf.role)
|
|
299
|
+
|
|
300
|
+
self._generic_config_subs['ens_members_num'] = self._ens_members_num
|
|
301
|
+
self._generic_config_subs['ens_members_date'] = self._ens_members_date
|
|
302
|
+
self._generic_config_subs['ens_members_offset'] = self._ens_members_offset
|
|
303
|
+
self._generic_config_subs['ens_is_lagged'] = self._ens_is_lagged
|
|
304
|
+
# Legacy:
|
|
305
|
+
self._generic_config_subs['members'] = self._ens_members_num
|
|
306
|
+
# Namelist stuff
|
|
307
|
+
for namrh in self.updatable_namelists:
|
|
308
|
+
namrh.contents.setmacro('ENS_MEMBERS', len(self._ens_members_num))
|
|
309
|
+
if self._ens_members_num:
|
|
310
|
+
namrh.contents.setmacro('ENS_AUTO_NSTRIN', len(self._ens_members_num))
|
|
311
|
+
else:
|
|
312
|
+
namrh.contents.setmacro('ENS_AUTO_NSTRIN', self._ens_nominal_size)
|
|
313
|
+
self._generic_config_subs['ens_effterms'] = self._ens_effterms
|
|
314
|
+
# Legacy:
|
|
315
|
+
self._generic_config_subs['effterms'] = self._ens_effterms
|
|
316
|
+
|
|
317
|
+
def _membersd_setup(self, rh, opts): # @UnusedVariable
|
|
318
|
+
"""Set up the members/terms detection."""
|
|
319
|
+
self.members_detect()
|
|
320
|
+
|
|
321
|
+
_MIXIN_PREPARE_HOOKS = (_membersd_setup, )
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@algo_component_deco_mixin_autodoc
|
|
325
|
+
class OOPSMembersTermsDecoMixin(AlgoComponentDecoMixin):
|
|
326
|
+
"""Adds members/terms footprints' attributes and use them in configuration files.
|
|
327
|
+
|
|
328
|
+
This mixin class is intended to be used with AlgoComponent classes. It will
|
|
329
|
+
automatically add footprints' attributes ``members`` and ``terms`` and add
|
|
330
|
+
the corresponding ``members`` and ``effterms`` entries into
|
|
331
|
+
the configuration file substitutions dictionary ``_generic_config_subs``.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
_MIXIN_EXTRA_FOOTPRINTS = (oops_members_terms_lists, )
|
|
335
|
+
|
|
336
|
+
def _membersterms_deco_setup(self, rh, opts): # @UnusedVariable
|
|
337
|
+
"""Setup the configuration file."""
|
|
338
|
+
actualmembers = [m if isinstance(m, int) else int(m)
|
|
339
|
+
for m in self.members]
|
|
340
|
+
actualterms = [t if isinstance(t, Time) else Time(t)
|
|
341
|
+
for t in self.terms]
|
|
342
|
+
self._generic_config_subs['members'] = actualmembers
|
|
343
|
+
self._generic_config_subs['effterms'] = actualterms
|
|
344
|
+
|
|
345
|
+
_MIXIN_PREPARE_HOOKS = (_membersterms_deco_setup, )
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@algo_component_deco_mixin_autodoc
|
|
349
|
+
class OOPSTimestepDecoMixin(AlgoComponentDecoMixin):
|
|
350
|
+
"""Add a timsestep attribute and handle substitutions."""
|
|
351
|
+
|
|
352
|
+
_MIXIN_EXTRA_FOOTPRINTS = (footprints.Footprint(
|
|
353
|
+
info="Abstract timestep footprint",
|
|
354
|
+
attr=dict(
|
|
355
|
+
timestep=dict(
|
|
356
|
+
info="A possible model timestep (in seconds).",
|
|
357
|
+
optional=True,
|
|
358
|
+
type=float
|
|
359
|
+
),
|
|
360
|
+
),
|
|
361
|
+
),)
|
|
362
|
+
|
|
363
|
+
def _timestep_deco_setup(self, rh, opts): # @UnusedVariable
|
|
364
|
+
"""Set up the timestep in config and namelists."""
|
|
365
|
+
if self.timestep is not None:
|
|
366
|
+
self._generic_config_subs['timestep'] = Period(seconds=self.timestep)
|
|
367
|
+
logger.info("Set macro TIMESTEP=%f in namelists.", self.timestep)
|
|
368
|
+
for namrh in self.updatable_namelists:
|
|
369
|
+
namrh.contents.setmacro('TIMESTEP', self.timestep)
|
|
370
|
+
|
|
371
|
+
_MIXIN_PREPARE_HOOKS = (_timestep_deco_setup, )
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@algo_component_deco_mixin_autodoc
|
|
375
|
+
class OOPSIncrementalDecoMixin(AlgoComponentDecoMixin):
|
|
376
|
+
"""Add incremental attributes and handle substitutions."""
|
|
377
|
+
|
|
378
|
+
_MIXIN_EXTRA_FOOTPRINTS = (footprints.Footprint(
|
|
379
|
+
info="Abstract incremental_* footprint",
|
|
380
|
+
attr=dict(
|
|
381
|
+
incremental_tsteps=dict(
|
|
382
|
+
info="Timestep for each of the outer loop iteration (in seconds).",
|
|
383
|
+
optional=True,
|
|
384
|
+
type=footprints.FPList,
|
|
385
|
+
default=footprints.FPList(),
|
|
386
|
+
),
|
|
387
|
+
incremental_niters=dict(
|
|
388
|
+
info="Inner loop size for each of the outer loop iteration.",
|
|
389
|
+
optional=True,
|
|
390
|
+
type=footprints.FPList,
|
|
391
|
+
default=footprints.FPList(),
|
|
392
|
+
),
|
|
393
|
+
incremental_geos=dict(
|
|
394
|
+
info="Geometry for each of the outer loop iteration.",
|
|
395
|
+
optional=True,
|
|
396
|
+
type=footprints.FPList,
|
|
397
|
+
default=footprints.FPList(),
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
),)
|
|
401
|
+
|
|
402
|
+
def _incremental_deco_setup(self, rh, opts): # @UnusedVariable
|
|
403
|
+
"""Set up the incremental DA settings in config and namelists."""
|
|
404
|
+
if self.incremental_tsteps or self.incremental_niters:
|
|
405
|
+
sizes = {len(t) for t in [self.incremental_tsteps,
|
|
406
|
+
self.incremental_niters,
|
|
407
|
+
self.incremental_geos] if t}
|
|
408
|
+
if len(sizes) != 1:
|
|
409
|
+
raise ValueError('Inconsistent sizes between incr_tsteps and incr_niters')
|
|
410
|
+
actual_tsteps = [float(t) for t in (self.incremental_tsteps or ())]
|
|
411
|
+
actual_tsteps_p = [Period(seconds=t) for t in actual_tsteps]
|
|
412
|
+
actual_niters = [int(t) for t in (self.incremental_niters or ())]
|
|
413
|
+
actual_geos = [g if isinstance(g, geometries.Geometry) else geometries.get(tag=g)
|
|
414
|
+
for g in (self.incremental_geos or ())]
|
|
415
|
+
if actual_tsteps:
|
|
416
|
+
self._generic_config_subs['incremental_tsteps'] = actual_tsteps_p
|
|
417
|
+
for upd_i, tstep in enumerate(actual_tsteps, start=1):
|
|
418
|
+
logger.info("Set macro UPD%d_TIMESTEP=%f macro in namelists.",
|
|
419
|
+
upd_i, tstep)
|
|
420
|
+
for namrh in self.updatable_namelists:
|
|
421
|
+
namrh.contents.setmacro('UPD{:d}_TIMESTEP'.format(upd_i), tstep)
|
|
422
|
+
if actual_niters:
|
|
423
|
+
self._generic_config_subs['incremental_niters'] = actual_niters
|
|
424
|
+
for upd_i, niter in enumerate(actual_niters, start=1):
|
|
425
|
+
logger.info("Set macro UPD%d_NITER=%d macro in namelists.",
|
|
426
|
+
upd_i, niter)
|
|
427
|
+
for namrh in self.updatable_namelists:
|
|
428
|
+
namrh.contents.setmacro('UPD{:d}_NITER'.format(upd_i), niter)
|
|
429
|
+
if actual_geos:
|
|
430
|
+
self._generic_config_subs['incremental_geos'] = actual_geos
|
|
431
|
+
|
|
432
|
+
_MIXIN_PREPARE_HOOKS = (_incremental_deco_setup,)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class OOPSParallel(Parallel,
|
|
436
|
+
drhook.DrHookDecoMixin,
|
|
437
|
+
grib.EcGribDecoMixin,
|
|
438
|
+
satrad.SatRadDecoMixin):
|
|
439
|
+
"""Abstract AlgoComponent for any OOPS run."""
|
|
440
|
+
|
|
441
|
+
_abstract = True
|
|
442
|
+
_footprint = dict(
|
|
443
|
+
info = "Any OOPS Run (abstract).",
|
|
444
|
+
attr = dict(
|
|
445
|
+
kind = dict(
|
|
446
|
+
values = ['oorun'],
|
|
447
|
+
),
|
|
448
|
+
date = dict(
|
|
449
|
+
info = 'The current run date.',
|
|
450
|
+
access = 'rwx',
|
|
451
|
+
type = Date,
|
|
452
|
+
doc_zorder = -50,
|
|
453
|
+
),
|
|
454
|
+
config_subs = dict(
|
|
455
|
+
info = "Substitutions to be performed in the config file (before run)",
|
|
456
|
+
optional = True,
|
|
457
|
+
type = footprints.FPDict,
|
|
458
|
+
default = footprints.FPDict(),
|
|
459
|
+
doc_zorder = -60,
|
|
460
|
+
),
|
|
461
|
+
binarysingle=dict(
|
|
462
|
+
default = 'basicnwp',
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def __init__(self, *kargs, **kwargs):
|
|
468
|
+
"""Declare some hidden attributes for a later use."""
|
|
469
|
+
super().__init__(*kargs, **kwargs)
|
|
470
|
+
self._oops_cycle = None
|
|
471
|
+
self._generic_config_subs = dict()
|
|
472
|
+
self._individual_config_subs = OrderedDict()
|
|
473
|
+
self._last_l_subs = dict()
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def oops_cycle(self):
|
|
477
|
+
"""The binary's cycle number."""
|
|
478
|
+
return self._oops_cycle
|
|
479
|
+
|
|
480
|
+
def valid_executable(self, rh):
|
|
481
|
+
"""Be sure that the specified executable has a cycle attribute."""
|
|
482
|
+
valid = super().valid_executable(rh)
|
|
483
|
+
if hasattr(rh.resource, 'cycle'):
|
|
484
|
+
self._oops_cycle = rh.resource.cycle
|
|
485
|
+
return valid
|
|
486
|
+
else:
|
|
487
|
+
logger.error('The binary < %s > has no cycle attribute', repr(rh))
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
def prepare(self, rh, opts):
|
|
491
|
+
"""Preliminary setups."""
|
|
492
|
+
super().prepare(rh, opts)
|
|
493
|
+
# Look for channels namelists and set appropriate links
|
|
494
|
+
self.setchannels()
|
|
495
|
+
# Register all of the config files
|
|
496
|
+
self.set_config_rendering()
|
|
497
|
+
# Looking for low-level-libs defaults...
|
|
498
|
+
self.boost_defaults()
|
|
499
|
+
self.eckit_defaults()
|
|
500
|
+
|
|
501
|
+
def spawn_hook(self):
|
|
502
|
+
"""Perform configuration file rendering before executing the binary."""
|
|
503
|
+
self.do_config_rendering()
|
|
504
|
+
self.do_namelist_rendering()
|
|
505
|
+
super().spawn_hook()
|
|
506
|
+
|
|
507
|
+
def spawn_command_options(self):
|
|
508
|
+
"""Prepare options for the binary's command line."""
|
|
509
|
+
mconfig = list(self._individual_config_subs.keys())[0]
|
|
510
|
+
configfile = mconfig.rh.container.localpath()
|
|
511
|
+
options = {'configfile': configfile}
|
|
512
|
+
return options
|
|
513
|
+
|
|
514
|
+
@cached_property
|
|
515
|
+
def updatable_namelists(self):
|
|
516
|
+
return [s.rh for s in self.context.sequence.effective_inputs(role='Namelist')]
|
|
517
|
+
|
|
518
|
+
def set_config_rendering(self):
|
|
519
|
+
"""
|
|
520
|
+
Look into effective inputs for configuration files and register them for
|
|
521
|
+
a later rendering using bronx' templating system.
|
|
522
|
+
"""
|
|
523
|
+
mconfig = self.context.sequence.effective_inputs(role='MainConfig')
|
|
524
|
+
gconfig = self.context.sequence.effective_inputs(role='Config')
|
|
525
|
+
if len(mconfig) > 1:
|
|
526
|
+
raise AlgoComponentError("Only one Main Config section may be provided.")
|
|
527
|
+
if len(mconfig) == 0 and len(gconfig) != 1:
|
|
528
|
+
raise AlgoComponentError("Please provide a Main Config section or a unique Config section.")
|
|
529
|
+
if len(mconfig) == 1:
|
|
530
|
+
gconfig.insert(0, mconfig[0])
|
|
531
|
+
self._individual_config_subs = {sconf: dict() for sconf in gconfig}
|
|
532
|
+
|
|
533
|
+
def do_config_rendering(self):
|
|
534
|
+
"""Render registered configuration files using the bronx' templating system."""
|
|
535
|
+
l_first = True
|
|
536
|
+
for sconf, sdict in self._individual_config_subs.items():
|
|
537
|
+
self.system.subtitle('Configuration file rendering for: {:s}'
|
|
538
|
+
.format(sconf.rh.container.localpath()))
|
|
539
|
+
l_subs = dict(now=self.date, date=self.date)
|
|
540
|
+
l_subs.update(self._generic_config_subs)
|
|
541
|
+
l_subs.update(sdict)
|
|
542
|
+
l_subs.update(self.config_subs)
|
|
543
|
+
if l_subs != self._last_l_subs.get(sconf, dict()):
|
|
544
|
+
if not hasattr(sconf.rh.contents, 'bronx_tpl_render'):
|
|
545
|
+
logger.error('The < %s > content object has no "bronx_tpl_render" method. Skipping it.',
|
|
546
|
+
repr(sconf.rh.contents))
|
|
547
|
+
continue
|
|
548
|
+
try:
|
|
549
|
+
sconf.rh.contents.bronx_tpl_render(** l_subs)
|
|
550
|
+
except Exception:
|
|
551
|
+
logger.error('The config file rendering failed. The substitution dict was: \n%s',
|
|
552
|
+
lightdump(l_subs))
|
|
553
|
+
raise
|
|
554
|
+
self._last_l_subs[sconf] = l_subs
|
|
555
|
+
if l_first:
|
|
556
|
+
print(fulldump(sconf.rh.contents.data))
|
|
557
|
+
sconf.rh.save()
|
|
558
|
+
else:
|
|
559
|
+
logger.info("It's not necessary to update the file (no changes).")
|
|
560
|
+
l_first = False
|
|
561
|
+
|
|
562
|
+
def do_namelist_rendering(self):
|
|
563
|
+
todo = [r for r in self.updatable_namelists if r.contents.dumps_needs_update]
|
|
564
|
+
self.system.subtitle('Updating namelists')
|
|
565
|
+
if todo:
|
|
566
|
+
for namrh in todo:
|
|
567
|
+
logger.info('Rewriting %s.', namrh.container.localpath())
|
|
568
|
+
namrh.save()
|
|
569
|
+
else:
|
|
570
|
+
logger.info('None of the namelists need to be rewritten.')
|
|
571
|
+
|
|
572
|
+
def boost_defaults(self):
|
|
573
|
+
"""Set defaults for BOOST environment variables.
|
|
574
|
+
|
|
575
|
+
Do not overwrite pre-initialised ones. The default list of variables
|
|
576
|
+
depends on the code's cycle number.
|
|
577
|
+
"""
|
|
578
|
+
defaults = {
|
|
579
|
+
IfsCycle('cy1'): {
|
|
580
|
+
'BOOST_TEST_CATCH_SYSTEM_ERRORS': 'no',
|
|
581
|
+
'BOOST_TEST_DETECT_FP_EXCEPTIONS': 'no',
|
|
582
|
+
'BOOST_TEST_LOG_FORMAT': 'XML',
|
|
583
|
+
'BOOST_TEST_LOG_LEVEL': 'message',
|
|
584
|
+
'BOOST_TEST_OUTPUT_FORMAT': 'XML',
|
|
585
|
+
'BOOST_TEST_REPORT_FORMAT': 'XML',
|
|
586
|
+
'BOOST_TEST_RESULT_CODE': 'yes'
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
cydefaults = None
|
|
590
|
+
for k, defdict in sorted(defaults.items(), reverse=True):
|
|
591
|
+
if k < self.oops_cycle:
|
|
592
|
+
cydefaults = defdict
|
|
593
|
+
break
|
|
594
|
+
self.algoassert(cydefaults is not None,
|
|
595
|
+
'BOOST defaults not found for cycle: {!s}'.format(self.oops_cycle))
|
|
596
|
+
logger.info('Setting up BOOST defaults:%s', lightdump(cydefaults))
|
|
597
|
+
self.env.default(**cydefaults)
|
|
598
|
+
|
|
599
|
+
def eckit_defaults(self):
|
|
600
|
+
"""Set defaults for eckit environment variables.
|
|
601
|
+
|
|
602
|
+
Do not overwrite pre-initialised ones. The default list of variables
|
|
603
|
+
depends on the code's cycle number.
|
|
604
|
+
"""
|
|
605
|
+
defaults = {
|
|
606
|
+
IfsCycle('cy1'): {
|
|
607
|
+
'ECKIT_MPI_INIT_THREAD': ('MPI_THREAD_MULTIPLE'
|
|
608
|
+
if int(self.env.get('OMP_NUM_THREADS', '1')) > 1
|
|
609
|
+
else 'MPI_THREAD_SINGLE'),
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
cydefaults = None
|
|
613
|
+
for k, defdict in sorted(defaults.items(), reverse=True):
|
|
614
|
+
if k < self.oops_cycle:
|
|
615
|
+
cydefaults = defdict
|
|
616
|
+
break
|
|
617
|
+
self.algoassert(cydefaults is not None,
|
|
618
|
+
'eckit defaults not found for cycle: {!s}'.format(self.oops_cycle))
|
|
619
|
+
logger.info('Setting up eckit defaults:%s', lightdump(cydefaults))
|
|
620
|
+
self.env.default(**cydefaults)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class OOPSODB(OOPSParallel, odb.OdbComponentDecoMixin):
|
|
624
|
+
"""Abstract AlgoComponent for any OOPS run requiring ODB databases."""
|
|
625
|
+
|
|
626
|
+
_abstract = True
|
|
627
|
+
_footprint = dict(
|
|
628
|
+
info = "OOPS ObsOperator Test run.",
|
|
629
|
+
attr = dict(
|
|
630
|
+
kind = dict(
|
|
631
|
+
values = ['oorunodb'],
|
|
632
|
+
),
|
|
633
|
+
binarysingle = dict(
|
|
634
|
+
default = 'basicnwpobsort',
|
|
635
|
+
),
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
#: If ``True``, an empty CCMA database will be created before the run and
|
|
640
|
+
#: necessary environment variables will be added in order for the executable
|
|
641
|
+
#: to populate this database at the end of the run.
|
|
642
|
+
_OOPSODB_CCMA_DIRECT = False
|
|
643
|
+
|
|
644
|
+
def prepare(self, rh, opts):
|
|
645
|
+
"""Setup ODB stuff."""
|
|
646
|
+
super().prepare(rh, opts)
|
|
647
|
+
sh = self.system
|
|
648
|
+
|
|
649
|
+
# Looking for input observations
|
|
650
|
+
allodb = self.lookupodb()
|
|
651
|
+
allcma = [x for x in allodb if x.rh.resource.layout.lower() == self.virtualdb]
|
|
652
|
+
if self.virtualdb.lower() == 'ccma':
|
|
653
|
+
self.algoassert(len(allcma) == 1, 'A unique CCMA database is to be provided.')
|
|
654
|
+
self.algoassert(not self._OOPSODB_CCMA_DIRECT,
|
|
655
|
+
'_OOPSODB_CCMA_DIRECT needs to be False if virtualdb=ccma.')
|
|
656
|
+
cma = allcma[0]
|
|
657
|
+
cma_path = sh.path.abspath(cma.rh.container.localpath())
|
|
658
|
+
else:
|
|
659
|
+
cma_path = self.odb_merge_if_needed(allcma)
|
|
660
|
+
if self._OOPSODB_CCMA_DIRECT:
|
|
661
|
+
ccma_path = self.odb_create_db(layout='CCMA')
|
|
662
|
+
self.odb.fix_db_path('CCMA', ccma_path)
|
|
663
|
+
|
|
664
|
+
# Set ODB environment
|
|
665
|
+
self.odb.fix_db_path(self.virtualdb, cma_path)
|
|
666
|
+
|
|
667
|
+
if self._OOPSODB_CCMA_DIRECT:
|
|
668
|
+
self.odb.ioassign_gather(cma_path, ccma_path)
|
|
669
|
+
else:
|
|
670
|
+
self.odb.ioassign_gather(cma_path)
|
|
671
|
+
|
|
672
|
+
if self.virtualdb.lower() != 'ccma':
|
|
673
|
+
self.odb.create_poolmask(self.virtualdb, cma_path)
|
|
674
|
+
self.odb.shuffle_setup(self.slots,
|
|
675
|
+
mergedirect=True,
|
|
676
|
+
ccmadirect=self._OOPSODB_CCMA_DIRECT)
|
|
677
|
+
|
|
678
|
+
# Fix the input databases intent
|
|
679
|
+
self.odb_rw_or_overwrite_method(* allcma)
|
|
680
|
+
|
|
681
|
+
# Look for extras ODB raw
|
|
682
|
+
self.odb_handle_raw_dbs()
|
|
683
|
+
|
|
684
|
+
# Allow assimilation window / timeslots configuration
|
|
685
|
+
self._generic_config_subs['window_length'] = self.slots.window
|
|
686
|
+
self._generic_config_subs['window_lmargin'] = Period(- self.slots.start)
|
|
687
|
+
self._generic_config_subs['window_rmargin'] = self.slots.window + self.slots.start
|
|
688
|
+
self._generic_config_subs['timeslot_length'] = self.slots.chunk
|
|
689
|
+
self._generic_config_subs['timeslot_centered'] = self.slots.center
|
|
690
|
+
self._generic_config_subs['timeslot_centers'] = self.slots.as_centers_fromstart()
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class OOPSAnalysis(OOPSODB,
|
|
694
|
+
OOPSTimestepDecoMixin,
|
|
695
|
+
OOPSIncrementalDecoMixin,
|
|
696
|
+
OOPSMemberDecoMixin,
|
|
697
|
+
OOPSMembersTermsDetectDecoMixin):
|
|
698
|
+
"""Any kind of OOPS analysis (screening/thining step excluded)."""
|
|
699
|
+
|
|
700
|
+
_footprint = dict(
|
|
701
|
+
info = "OOPS minimisation.",
|
|
702
|
+
attr = dict(
|
|
703
|
+
kind = dict(
|
|
704
|
+
values = ['ooanalysis', 'oominim'],
|
|
705
|
+
remap = dict(autoremap='first'),
|
|
706
|
+
),
|
|
707
|
+
virtualdb = dict(
|
|
708
|
+
default = 'ccma',
|
|
709
|
+
),
|
|
710
|
+
withscreening=dict(
|
|
711
|
+
values = [False, ],
|
|
712
|
+
type = bool,
|
|
713
|
+
optional = True,
|
|
714
|
+
default = False,
|
|
715
|
+
),
|
|
716
|
+
)
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
class OOPSAnalysisWithScreening(OOPSAnalysis):
|
|
721
|
+
"""Any kind of OOPS analysis with screening/thining step."""
|
|
722
|
+
|
|
723
|
+
_OOPSODB_CCMA_DIRECT = True
|
|
724
|
+
|
|
725
|
+
_footprint = dict(
|
|
726
|
+
attr = dict(
|
|
727
|
+
virtualdb = dict(
|
|
728
|
+
default = 'ecma',
|
|
729
|
+
),
|
|
730
|
+
withscreening = dict(
|
|
731
|
+
values = [True, ],
|
|
732
|
+
optional = False,
|
|
733
|
+
),
|
|
734
|
+
)
|
|
735
|
+
)
|