Skip to content

Commit 86a40e4

Browse files
authored
Merge pull request #1637 from knutfrode/dev
[run-ex] Conditional mechanism now also used to decide if previous va…
2 parents a5f3ce3 + 4ba4247 commit 86a40e4

3 files changed

Lines changed: 122 additions & 37 deletions

File tree

opendrift/elements/elements.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,18 @@ class LagrangianArray:
7373
'standard_name': 'longitude',
7474
'long_name': 'longitude',
7575
'seed': False,
76+
'store_previous_if': (
77+
('general:coastline_action', 'in', ['stranding', 'previous']), 'or',
78+
('general:seafloor_action', 'in', ['previous'])),
7679
'axis': 'X'}),
7780
('lat', {'dtype': np.float32,
7881
'units': 'degrees_north',
7982
'standard_name': 'latitude',
8083
'long_name': 'latitude',
8184
'seed': False,
85+
'store_previous_if': (
86+
('general:coastline_action', 'in', ['stranding', 'previous']), 'or',
87+
('general:seafloor_action', 'in', ['previous'])),
8288
'axis': 'Y'}),
8389
('z', {'dtype': np.float32,
8490
'units': 'm',

opendrift/models/basemodel/__init__.py

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,8 @@ def __init__(self,
472472

473473
# Add default element properties to config
474474
c = {}
475-
for p in self.ElementType.variables:
476-
v = self.ElementType.variables[p]
475+
for p in self.elements.variables:
476+
v = self.elements.variables[p]
477477
if 'seed' in v and v['seed'] is False:
478478
continue # Properties which may not be provided by user
479479
c['seed:%s' % p] = {
@@ -1717,11 +1717,22 @@ def run(self,
17171717
###################################################################################
17181718
for vn, var in self.required_variables.copy().items():
17191719
if 'skip_if' in var:
1720-
skip = self.evaluate_conditional(*var['skip_if'])
1720+
skip = evaluate_conditional(*var['skip_if'], self)
17211721
if skip is True:
17221722
logger.info(f'Skipping environment variable {vn} because of condition {var["skip_if"]}')
17231723
self.required_variables.pop(vn)
17241724

1725+
####################################################################################
1726+
# Evaluate conditionals to determine if previous element properties shall be stored
1727+
####################################################################################
1728+
for en, prop in self.elements.variables.copy().items():
1729+
if 'store_previous_if' in prop:
1730+
store = evaluate_conditional(*prop['store_previous_if'], self)
1731+
if store is True:
1732+
logger.info(f'Storing previous values of element property {en} because of condition {prop["store_previous_if"]}')
1733+
self.elements.variables[en]['store_previous'] = True
1734+
del self.elements.variables[en]['store_previous_if'] # To avoid writing this to netCDF metadata
1735+
17251736
########################
17261737
# Simulation time step
17271738
########################
@@ -1878,7 +1889,7 @@ def run(self,
18781889

18791890
element_vars = {}
18801891
default_dtype = np.float32 # Allows NaN (in contrast to np.int)
1881-
for varname, attrs in self.ElementType.variables.items():
1892+
for varname, attrs in self.elements.variables.items():
18821893
if varname == 'ID' or (self.export_variables is not None and
18831894
varname not in self.export_variables):
18841895
continue
@@ -1923,7 +1934,7 @@ def run(self,
19231934

19241935
if self.origin_marker is not None and 'origin_marker' in self.result.data_vars:
19251936
self.result['origin_marker'] = self.result.origin_marker.assign_attrs(
1926-
{'flag_values': np.arange(len(self.origin_marker)).astype(self.ElementType.variables['origin_marker']['dtype']),
1937+
{'flag_values': np.arange(len(self.origin_marker)).astype(self.elements.variables['origin_marker']['dtype']),
19271938
'flag_meanings': " ".join(self.origin_marker.values())
19281939
})
19291940

@@ -1943,11 +1954,6 @@ def run(self,
19431954
self.elements_scheduled.lat)
19441955
self.timer_end('preparing main loop:moving elements to ocean')
19451956

1946-
# TODO: adjust store_previous according to config, later with upcming conditional mechanism
1947-
if self.get_config('general:coastline_action', None) in ['stranding', 'previous']:
1948-
logger.info('Storing previous position of elements for coastline interaction')
1949-
self.elements.variables['lon']['store_previous'] = True
1950-
self.elements.variables['lat']['store_previous'] = True
19511957

19521958
# Make Xarray datasets to store environment variables and element properties from previous time step
19531959
environment_previous = [vn for vn, v in self.required_variables.items()
@@ -2211,7 +2217,7 @@ def state_to_buffer(self, final=False):
22112217
logger.debug(f'Truncating buffer from {numtimes_before} to {numtimes_after} times')
22122218

22132219
# Final update some variable attributes
2214-
status_dtype = self.ElementType.variables['status']['dtype']
2220+
status_dtype = self.elements.variables['status']['dtype']
22152221
self.result['status'] = self.result.status.assign_attrs(
22162222
{'valid_range': np.array((0, len(self.status_categories) - 1)).astype(status_dtype),
22172223
'flag_values': np.array(np.arange(len(self.status_categories))).astype(status_dtype),
@@ -4676,33 +4682,47 @@ def gui_postproc(self):
46764682
'''To be overloaded by subclasses'''
46774683
pass
46784684

4679-
def evaluate_conditional(self, key, operator, value):
4680-
"""Evaluate a condition as True or False
4685+
def _evaluate_key(self, key):
4686+
# Presently assuming that key is a config key
4687+
return self.get_config(key, 'not_implemented')
46814688

4682-
This can be used to:
4683-
- skip required_variables that are not required, based on config setting
4684-
- store previous value of element property or environment variable, based on config setting
4685-
- disable a config setting based on another setting (for dynamic menus)
4689+
def evaluate_conditional(key, operator, value, self=None):
4690+
"""Evaluate a condition as True or False
46864691
4687-
key: config key string
4688-
operator: one from operator_map below
4689-
value: the provided value to be matched with operator against actual config setting
4692+
This can be used to:
4693+
- skip required_variables that are not required, based on config setting
4694+
- store previous value of element property or environment variable, based on config setting
4695+
- disable a config setting based on another setting (for dynamic menus)
46904696
4691-
Returns: True or False
4692-
"""
4697+
key: e.g. config key/setting as string
4698+
operator: relation between key and value, one from operator_map below
4699+
value: the provided value to be matched with operator against actual key
46934700
4694-
operator_map = {
4695-
'==': lambda x, y: x == y,
4696-
'!=': lambda x, y: x != y,
4697-
'<': lambda x, y: x < y,
4698-
'<=': lambda x, y: x <= y,
4699-
'>': lambda x, y: x > y,
4700-
'>=': lambda x, y: x >= y,
4701-
'is': lambda x, y: x is y,
4702-
'is not': lambda x, y: x is not y,
4703-
}
4704-
4705-
# Presently assuming that key is a config key
4706-
key = self.get_config(key)
4701+
Returns: True or False
4702+
"""
47074703

4708-
return operator_map[operator](key, value)
4704+
operator_map = {
4705+
'==': lambda x, y: x == y,
4706+
'!=': lambda x, y: x != y,
4707+
'<': lambda x, y: x < y,
4708+
'<=': lambda x, y: x <= y,
4709+
'>': lambda x, y: x > y,
4710+
'>=': lambda x, y: x >= y,
4711+
'in': lambda x, y: x in y,
4712+
'is': lambda x, y: x is y,
4713+
'is not': lambda x, y: x is not y,
4714+
'or': lambda x, y: x or y,
4715+
'and': lambda x, y: x and y,
4716+
}
4717+
4718+
if isinstance(key, tuple):
4719+
key = evaluate_conditional(key[0], key[1], key[2], self)
4720+
4721+
if self is not None and not isinstance(key, bool):
4722+
# If a OpenDrift instance is provided, this will evaluate the key, e.g. with get_config
4723+
key = self._evaluate_key(key)
4724+
4725+
if isinstance(value, tuple):
4726+
value = evaluate_conditional(value[0], value[1], value[2], self)
4727+
4728+
return operator_map[operator](key, value)

tests/models/test_basemodel.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
def test_logging(tmpdir, capsys):
1111
# Accepting small variations in log output,
1212
# depending on machine, and from which folder test is run
13-
accepted = (219, 222)
13+
accepted = (220, 223)
1414

1515
# Logging to console
1616
logfile = None
@@ -171,3 +171,62 @@ def test_clone():
171171
time=datetime(2015, 1, 1))
172172
c.run(steps=2, time_step=-10*3600, time_step_output=-10*3600)
173173

174+
def test_conditionals():
175+
from opendrift.models.basemodel import evaluate_conditional
176+
assert evaluate_conditional(True, 'is', True) is True
177+
assert evaluate_conditional(1, '==', 1) is True
178+
assert evaluate_conditional(1, '==', 2) is False
179+
assert evaluate_conditional('previous', '==', 'previous') is True
180+
assert evaluate_conditional('previous', '==', 'stranding') is False
181+
assert evaluate_conditional(1, '>', 0) is True
182+
assert evaluate_conditional(1, '<', 0) is False
183+
# Nested conditionals
184+
assert evaluate_conditional((1, '==', 1), 'is', True) is True
185+
assert evaluate_conditional((1, '==', 1), 'is', False) is False
186+
assert evaluate_conditional((1, '==', 0), 'is', False) is True
187+
assert evaluate_conditional((1, '==', 1), 'and', (1, '==', 1)) is True
188+
assert evaluate_conditional((1, '==', 1), 'and', (0, '==', 1)) is False
189+
assert evaluate_conditional((1, '==', 1), 'or', (0, '==', 1)) is True
190+
# Double nesting
191+
assert evaluate_conditional((1, '==', 1), 'or', ((1, '==', 1), 'is', (0, '==', 0))) is True
192+
193+
# Testing with a model instance
194+
o = OceanDrift(loglevel=50)
195+
o.set_config('general:coastline_action', 'stranding')
196+
o.set_config('general:seafloor_action', 'previous')
197+
assert evaluate_conditional('general:coastline_action', '==', 'stranding', o) is True
198+
assert evaluate_conditional('general:coastline_action', '==', 'previous', o) is False
199+
assert evaluate_conditional(('general:coastline_action', '==', 'stranding'), 'and',
200+
('general:seafloor_action', '==', 'previous'), o) is True
201+
assert evaluate_conditional(('general:coastline_action', '==', 'stranding'), 'and',
202+
('general:seafloor_action', '==', 'wrong'), o) is False
203+
assert evaluate_conditional(('general:coastline_action', '==', 'stranding'), 'or',
204+
('general:seafloor_action', '==', 'wrong'), o) is True
205+
assert evaluate_conditional(('general:coastline_action', '==', 'wrong'), 'or',
206+
('general:seafloor_action', '==', 'wrong'), o) is False
207+
208+
# Perform a simulation, to check if previous position is stored, based on given conditionals
209+
o = OceanDrift(loglevel=50)
210+
o.set_config('general:coastline_action', 'stranding')
211+
o.set_config('general:seafloor_action', 'previous')
212+
o.set_config('environment:constant:land_binary_mask', 0)
213+
o.seed_elements(lon=4, lat=60, number=5, time=datetime.now())
214+
o.run(steps=1)
215+
assert 'lon' in o.elements_previous
216+
217+
# Check that either coastline or seafloor actions require storing previous positions
218+
o = OceanDrift(loglevel=50)
219+
o.set_config('general:coastline_action', 'none') # does not require storing previous positions
220+
o.set_config('general:seafloor_action', 'previous') # requires storing previous positions
221+
o.set_config('environment:constant:land_binary_mask', 0)
222+
o.seed_elements(lon=4, lat=60, number=5, time=datetime.now())
223+
o.run(steps=1)
224+
assert 'lon' in o.elements_previous
225+
226+
o = OceanDrift(loglevel=50)
227+
o.set_config('general:coastline_action', 'none')
228+
o.set_config('general:seafloor_action', 'deactivate')
229+
o.set_config('environment:constant:land_binary_mask', 0)
230+
o.seed_elements(lon=4, lat=60, number=5, time=datetime.now())
231+
o.run(steps=1)
232+
assert o.elements_previous is None

0 commit comments

Comments
 (0)