Skip to content

Commit 18aadce

Browse files
authored
Merge pull request #1627 from knutfrode/dev
[run-ex] Environment variables and element properties with attribute *store_previous=True* are now available as self.environment_previos.<var> and self.elements_previous.<var>. This replaces hardcoded solution for lon and lat only.
2 parents 2db5710 + 34bf455 commit 18aadce

File tree

2 files changed

+74
-81
lines changed

2 files changed

+74
-81
lines changed

opendrift/models/basemodel/__init__.py

Lines changed: 73 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ def __init__(self,
262262
self.steps_calculation = 0 # Increase for each simulation step
263263
self.elements_deactivated = self.ElementType() # Empty array
264264
self.elements = self.ElementType() # Empty array
265+
self._elements_previous = None
266+
self._environment_previous = None
265267

266268
# Set up logging
267269
logformat = '%(asctime)s %(levelname)-7s %(name)s:%(lineno)d: %(message)s'
@@ -570,55 +572,24 @@ def prepare_run(self):
570572
def post_run(self):
571573
pass
572574

573-
def store_present_positions(self, IDs=None, lons=None, lats=None):
574-
"""Store present element positions, in case they shall be moved back"""
575-
if self.get_config('general:coastline_action') in ['previous', 'stranding'] or (
576-
'general:seafloor_action' in self._config
577-
and self.get_config('general:seafloor_action') == 'previous'):
578-
if not hasattr(self, 'previous_lon'):
579-
self.previous_lon = np.ma.masked_all(self.num_elements_total())
580-
self.previous_lat = np.ma.masked_all(self.num_elements_total())
581-
if IDs is None:
582-
IDs = self.elements.ID
583-
lons = self.elements.lon
584-
lats = self.elements.lat
585-
self.newly_seeded_IDs = None
586-
else:
587-
# to check if seeded on land
588-
if len(IDs) > 0:
589-
self.newly_seeded_IDs = np.copy(IDs)
590-
else:
591-
self.newly_seeded_IDs = None
592-
self.previous_lon[IDs] = np.copy(lons)
593-
self.previous_lat[IDs] = np.copy(lats)
594-
595-
def store_previous_variables(self):
596-
"""Store some environment variables, for access at next time step"""
597-
598-
if not hasattr(self, 'store_previous'):
599-
return
600-
if not hasattr(self, 'variables_previous'):
601-
# Create ndarray to store previous variables
602-
dtype = [(var, np.float32) for var in self.store_previous]
603-
self.variables_previous = np.array(np.full(
604-
self.num_elements_total(), np.nan),
605-
dtype=dtype)
606-
607-
# Copying variables_previous to environment_previous
608-
self.environment_previous = self.variables_previous[self.elements.ID]
609-
610-
# Use new values for new elements which have no previous value
611-
for var in self.store_previous:
612-
undefined = np.isnan(self.environment_previous[var])
613-
self.environment_previous[var][undefined] = getattr(
614-
self.environment, var)[undefined]
615-
616-
self.environment_previous = self.environment_previous.view(np.recarray)
617-
618-
for var in self.store_previous:
619-
self.variables_previous[var][self.elements.ID] = getattr(
620-
self.environment, var)
621-
575+
def update_previous_state(self):
576+
"""Store some properties and variables, for access at next time step"""
577+
578+
# Retrieve previous from storage
579+
if self._environment_previous is not None:
580+
# Update previous environment
581+
self.environment_previous = self._environment_previous.isel(trajectory=self.elements.ID)
582+
# Store present environment variables
583+
for var in self._environment_previous:
584+
self._environment_previous[var][self.elements.ID] = self.environment[var]
585+
586+
if self._elements_previous is not None:
587+
# Update previous elements
588+
self.elements_previous = self._elements_previous.isel(trajectory=self.elements.ID)
589+
# Store present element properties
590+
for var in self._elements_previous:
591+
self._elements_previous[var][self.elements.ID] = getattr(self.elements, var)
592+
622593
def interact_with_coastline(self, final=False):
623594
"""Coastline interaction according to configuration setting"""
624595

@@ -661,23 +632,24 @@ def interact_with_coastline(self, final=False):
661632
for on_land_id, on_land_prev_id in zip(on_land, self.elements.ID[on_land]):
662633
lon = self.elements.lon[on_land_id]
663634
lat = self.elements.lat[on_land_id]
664-
prev_lon = self.previous_lon[on_land_prev_id]
665-
prev_lat = self.previous_lat[on_land_prev_id]
666-
635+
prev_lon = self._elements_previous.lon[on_land_prev_id].data
636+
prev_lat = self._elements_previous.lat[on_land_prev_id].data
667637
step_degrees = float(coastline_approximation_precision)
668638

669639
x_degree_diff = np.abs(prev_lon - lon)
640+
y_degree_diff = np.abs(prev_lat - lat)
641+
if x_degree_diff == 0 and y_degree_diff == 0:
642+
continue
670643
x_samples = np.floor(x_degree_diff / step_degrees).astype(np.int64) if x_degree_diff > step_degrees else 1
671644
x = np.linspace(prev_lon, lon, x_samples)
672645

673-
y_degree_diff = np.abs(prev_lat - lat)
674646
y_samples = np.floor(y_degree_diff/ step_degrees).astype(np.int64) if y_degree_diff > step_degrees else 1
675647
y = np.linspace(prev_lat, lat, y_samples)
676648

677649
xx, yy = np.meshgrid(x,y)
678650
xx, yy = xx.ravel(), yy.ravel()
679651

680-
rl_mask = rl.contains_many(xx.ravel(), yy.ravel())
652+
rl_mask = rl.contains_many(xx, yy)
681653
if np.any(rl_mask):
682654
index = np.argmax(rl_mask)
683655
new_lon = xx[index]
@@ -702,9 +674,9 @@ def interact_with_coastline(self, final=False):
702674
'moving back to water' % len(on_land))
703675
on_land_ID = self.elements.ID[on_land]
704676
self.elements.lon[on_land] = \
705-
np.copy(self.previous_lon[on_land_ID])
677+
self._elements_previous.lon[on_land_ID]
706678
self.elements.lat[on_land] = \
707-
np.copy(self.previous_lat[on_land_ID])
679+
self._elements_previous.lat[on_land_ID]
708680
self.environment.land_binary_mask[on_land] = 0
709681

710682
def interact_with_seafloor(self):
@@ -741,10 +713,8 @@ def interact_with_seafloor(self):
741713
logger.warning('%s elements hit seafloor, '
742714
'moving back ' % len(below))
743715
below_ID = self.elements.ID[below]
744-
self.elements.lon[below] = \
745-
np.copy(self.previous_lon[below_ID])
746-
self.elements.lat[below] = \
747-
np.copy(self.previous_lat[below_ID])
716+
self.elements.lon[below] = self._elements_previous.lon[below_ID].data
717+
self.elements.lat[below] = self._elements_previous.lat[below_ID].data
748718

749719
@abstractmethod
750720
def update(self):
@@ -873,6 +843,7 @@ def release_elements(self):
873843
'to be seeded: %s, already seeded %s' %
874844
(len(self.elements_scheduled), self.num_elements_activated()))
875845
if len(self.elements_scheduled) == 0:
846+
self.newly_seeded_IDs = None
876847
return
877848
if self.time_step.days >= 0:
878849
indices = (self.elements_scheduled_time >= self.time) & \
@@ -882,9 +853,12 @@ def release_elements(self):
882853
indices = (self.elements_scheduled_time <= self.time) & \
883854
(self.elements_scheduled_time >
884855
self.time + self.time_step)
885-
self.store_present_positions(self.elements_scheduled.ID[indices],
886-
self.elements_scheduled.lon[indices],
887-
self.elements_scheduled.lat[indices])
856+
857+
self.newly_seeded_IDs = self.elements_scheduled.ID[indices] # may be deactivated if on land
858+
if self._elements_previous is not None and 'lon' in self._elements_previous:
859+
# store position of newly seeded elements
860+
self._elements_previous.lon[self.newly_seeded_IDs] = self.elements_scheduled.lon[indices]
861+
self._elements_previous.lat[self.newly_seeded_IDs] = self.elements_scheduled.lat[indices]
888862
self.elements_scheduled.move_elements(self.elements, indices)
889863
self.elements_scheduled_time = self.elements_scheduled_time[~indices]
890864
logger.debug('Released %i new elements.' % np.sum(indices))
@@ -915,10 +889,10 @@ def closest_ocean_points(self, lon, lat):
915889

916890
if isinstance(land_reader, ShapeReader):
917891
# can do this better
918-
is_inside = land_reader.__on_land__(lon, lat)
919-
lon[is_inside], lat[is_inside], _ = land_reader.get_nearest_outside(
920-
lon[is_inside],
921-
lat[is_inside],
892+
land_indices = land_reader.__on_land__(lon, lat)
893+
lon[land_indices], lat[land_indices], _ = land_reader.get_nearest_outside(
894+
lon[land_indices],
895+
lat[land_indices],
922896
buffer_distance=land_reader._land_kdtree_buffer_distance,
923897
)
924898

@@ -938,11 +912,12 @@ def closest_ocean_points(self, lon, lat):
938912
time=land_reader.start_time)[0]['land_binary_mask']
939913
if land.max() == 0:
940914
logger.info('All points are in ocean')
941-
return lon, lat
915+
return lon, lat, None
942916
logger.info('Moving %i out of %i points from land to water' %
943917
(np.sum(land != 0), len(lon)))
944-
landlons = lon[land != 0]
945-
landlats = lat[land != 0]
918+
land_indices = np.where(land != 0)[0]
919+
landlons = lon[land_indices]
920+
landlats = lat[land_indices]
946921
longrid = np.arange(lonmin, lonmax, deltalon)
947922
latgrid = np.arange(latmin, latmax, deltalat)
948923
longrid, latgrid = np.meshgrid(longrid, latgrid)
@@ -960,10 +935,10 @@ def closest_ocean_points(self, lon, lat):
960935
if landgrid.size == 0:
961936
# Need to catch this before trying .min() on it...
962937
logger.warning('Land grid has zero size, cannot move elements.')
963-
return lon, lat
938+
return lon, lat, land_indices
964939
if landgrid.min() == 1 or np.isnan(landgrid.min()):
965940
logger.warning('No ocean pixels nearby, cannot move elements.')
966-
return lon, lat
941+
return lon, lat, land_indices
967942

968943
oceangridlons = longrid[landgrid == 0]
969944
oceangridlats = latgrid[landgrid == 0]
@@ -972,10 +947,10 @@ def closest_ocean_points(self, lon, lat):
972947
landpoints = np.dstack([landlons, landlats])
973948
_dist, indices = tree.query(landpoints)
974949
indices = indices.ravel()
975-
lon[land != 0] = oceangridlons[indices]
976-
lat[land != 0] = oceangridlats[indices]
950+
lon[land_indices] = oceangridlons[indices]
951+
lat[land_indices] = oceangridlats[indices]
977952

978-
return lon, lat
953+
return lon, lat, land_indices
979954

980955
@require_mode(mode=Mode.Ready)
981956
def seed_elements(self,
@@ -1992,7 +1967,7 @@ def run(self,
19921967
if varname == 'ID' or (self.export_variables is not None and
19931968
varname not in self.export_variables):
19941969
continue
1995-
attrs = {k:v for k,v in attrs.copy().items() if k not in ['seed', 'default']}
1970+
attrs = {k:v for k,v in attrs.copy().items() if k not in ['seed', 'default', 'store_previous']}
19961971
element_vars[varname] = (dims,
19971972
np.full(shape=shape, fill_value=np.nan, dtype=default_dtype),
19981973
attrs)
@@ -2048,11 +2023,30 @@ def run(self,
20482023
('land_binary_mask' in self.required_variables):
20492024
#('land_binary_mask' not in self.fallback_values) and \
20502025
self.timer_start('preparing main loop:moving elements to ocean')
2051-
self.elements_scheduled.lon, self.elements_scheduled.lat = \
2026+
self.elements_scheduled.lon, self.elements_scheduled.lat, land_indices = \
20522027
self.closest_ocean_points(self.elements_scheduled.lon,
20532028
self.elements_scheduled.lat)
20542029
self.timer_end('preparing main loop:moving elements to ocean')
20552030

2031+
# TODO: adjust store_previous according to config, later with upcming conditional mechanism
2032+
if self.get_config('general:coastline_action') in ['previous', 'stranding'] or (
2033+
'general:coastline_action' in self._config and
2034+
self.get_config('general:coastline_action') == 'previous'):
2035+
logger.info('Storing previous position of elements for coastline interaction')
2036+
self.elements.variables['lon']['store_previous'] = True
2037+
self.elements.variables['lat']['store_previous'] = True
2038+
2039+
# Make Xarray datasets to store environment variables and element properties from previous time step
2040+
environment_previous = [vn for vn, v in self.required_variables.items()
2041+
if v.get('store_previous', False) is True]
2042+
elements_previous = [vn for vn, v in self.elements.variables.items()
2043+
if v.get('store_previous', False) is True]
2044+
2045+
if len(environment_previous) > 0:
2046+
self._environment_previous = self.result[[*environment_previous]].isel(time=0, drop=True).copy(deep=True)
2047+
if len(elements_previous) > 0:
2048+
self._elements_previous = self.result[[*elements_previous]].isel(time=0, drop=True).copy(deep=True)
2049+
20562050
#############################
20572051
# Check validity domain
20582052
#############################
@@ -2147,9 +2141,8 @@ def run(self,
21472141

21482142
self.remove_deactivated_elements()
21492143

2150-
# Store location, in case elements shall be moved back
2151-
self.store_present_positions()
2152-
2144+
self.update_previous_state() # Store location, in case elements shall be moved back
2145+
21532146
if self.num_elements_active() > 0:
21542147
########################################
21552148
# Calling module specific update method

tests/models/test_basemodel.py

Lines changed: 1 addition & 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 = (285, 288, 313)
13+
accepted = (286, 289, 314)
1414

1515
# Logging to console
1616
logfile = None

0 commit comments

Comments
 (0)