# This file is part of OpenDrift.
#
# OpenDrift is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 2
#
# OpenDrift is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OpenDrift. If not, see <https://www.gnu.org/licenses/>.
#
# Copyright 2015, Knut-Frode Dagestad, MET Norway
from datetime import datetime, timedelta
import numpy as np
from scipy.interpolate import interp1d
import logging; logger = logging.getLogger(__name__)
from opendrift.models.basemodel import OpenDriftSimulation
from opendrift.elements import LagrangianArray
from opendrift.models.physics_methods import verticaldiffusivity_Large1994, verticaldiffusivity_Sundby1983, gls_tke, skillscore_liu_weissberg
from opendrift.config import CONFIG_LEVEL_ESSENTIAL, CONFIG_LEVEL_BASIC, CONFIG_LEVEL_ADVANCED
# Defining the oil element properties
[docs]
class Lagrangian3DArray(LagrangianArray):
"""Extending LagrangianArray for elements moving in 3 dimensions
The Particle may be buoyant and/or subject to vertical mixing
buoyant bahaviour is described by terminal velocity
"""
variables = LagrangianArray.add_variables([
('wind_drift_factor', {'dtype': np.float32,
'units': '1',
'description': 'Elements at surface are moved with this '
'fraction of the vind vector, in addition to currents '
'and Stokes drift',
'default': 0.02}),
('current_drift_factor', {'dtype': np.float32,
'units': '1',
'description': 'Elements are moved with this fraction of the '
'current vector, in addition to currents '
'and Stokes drift',
'default': 1}),
('terminal_velocity', {'dtype': np.float32,
'units': 'm/s',
'description': 'Terminal rise/sinking velocity (buoyancy) '
'in the ocean column',
'default': 0.})])
[docs]
class OceanDrift(OpenDriftSimulation):
"""Open source buoyant particle trajectory model based on OpenDrift.
Developed at MET Norway
Generic module for particles that move in 3 dimensions
and may be to vertical turbulent mixing
with the possibility for positive or negative buoyancy
Particles could be e.g. oil droplets, plankton, nutrients or sediments,
Model may be subclassed for more specific behaviour.
"""
ElementType = Lagrangian3DArray
required_variables = {
'x_sea_water_velocity': {'fallback': 0},
'y_sea_water_velocity': {'fallback': 0},
'sea_surface_height': {'fallback': 0},
'x_wind': {'fallback': 0},
'y_wind': {'fallback': 0},
'upward_sea_water_velocity': {'fallback': 0},
'ocean_vertical_diffusivity': {'fallback': 0,
'profiles': True},
'sea_surface_wave_significant_height': {'fallback': 0},
'sea_surface_wave_stokes_drift_x_velocity': {'fallback': 0},
'sea_surface_wave_stokes_drift_y_velocity': {'fallback': 0},
'sea_surface_wave_period_at_variance_spectral_density_maximum':
{'fallback': 0},
'sea_surface_wave_mean_period_from_variance_spectral_density_second_frequency_moment': {'fallback': 0},
'sea_surface_swell_wave_to_direction': {'fallback': 0, 'important': False},
'sea_surface_swell_wave_peak_period_from_variance_spectral_density': {'fallback': 0, 'important': False},
'sea_surface_swell_wave_significant_height': {'fallback': 0, 'important': False},
'sea_surface_wind_wave_to_direction': {'fallback': 0, 'important': False},
'sea_surface_wind_wave_mean_period': {'fallback': 0, 'important': False},
'sea_surface_wind_wave_significant_height': {'fallback': 0, 'important': False},
'surface_downward_x_stress': {'fallback': 0},
'surface_downward_y_stress': {'fallback': 0},
'turbulent_kinetic_energy': {'fallback': 0},
'turbulent_generic_length_scale': {'fallback': 0},
'ocean_mixed_layer_thickness': {'fallback': 50},
'sea_floor_depth_below_sea_level': {'fallback': 10000},
'land_binary_mask': {'fallback': None},
}
def __init__(self, *args, **kwargs):
if 'machine_learning_dict' in kwargs:
logger.debug('Machine learning correction supplied.')
mld = kwargs['machine_learning_dict']
del kwargs['machine_learning_dict']
import pickle
from tensorflow import keras
from tools.normalizations import generate_one_normalized_predictor, generate_one_denormalized_prediction
self.generate_one_normalized_predictor = generate_one_normalized_predictor
self.generate_one_denormalized_prediction = generate_one_denormalized_prediction
self.trained_model = keras.models.load_model(mld['trained_model'])
self.dict_normalization_params = pickle.load(open(mld['dict_normalization_params'], 'rb'))
self.ml_predictors = [self.dict_normalization_params['predictors'][i]['content'] for i in self.dict_normalization_params['predictors']]
else:
logger.debug('No machine learning correction available.')
if hasattr(self, 'required_profiles_z_range'):
raise ValueError('self.required_profiles_z_range is obsolete, and replaced by config setting drift:profile_depth. Please update your model.')
# Calling general constructor of parent class
super(OceanDrift, self).__init__(*args, **kwargs)
self._add_config({
'drift:vertical_advection': {'type': 'bool', 'default': True, 'description':
'Advect elements with vertical component of ocean current.',
'level': CONFIG_LEVEL_BASIC},
'drift:vertical_mixing': {'type': 'bool', 'default': False, 'level': CONFIG_LEVEL_BASIC,
'description': 'Activate vertical mixing scheme with inner loop'},
'vertical_mixing:timestep': {'type': 'float', 'min': 0.1, 'max': 3600, 'default': 60,
'level': CONFIG_LEVEL_ADVANCED, 'units': 'seconds', 'description':
'Time step used for inner loop of vertical mixing.'},
'vertical_mixing:diffusivitymodel': {'type': 'enum', 'default': 'environment',
'enum': ['environment', 'stepfunction', 'windspeed_Sundby1983',
'windspeed_Large1994', 'gls_tke','constant'], 'level': CONFIG_LEVEL_ADVANCED,
'units': 'seconds', 'description': 'Algorithm/source used for profile of vertical diffusivity. Environment means that diffusivity is aquired from readers or environment constants/fallback.'},
'vertical_mixing:background_diffusivity': {'type': 'float', 'min': 0, 'max': 1, 'default': 1.2e-5,
'level': CONFIG_LEVEL_ADVANCED, 'units': 'm2s-1', 'description':
'Background diffusivity used below mixed layer for wind parameterisations.'},
'vertical_mixing:TSprofiles': {'type': 'bool', 'default': False, 'level':
CONFIG_LEVEL_ADVANCED,
'description': 'Update T and S profiles within inner loop of vertical mixing. This takes more time, but may be slightly more accurate.'},
'drift:wind_drift_depth': {'type': 'float', 'default': 0.1,
'min': 0, 'max': 10, 'units': 'meters',
'description': 'The direct wind drift (windage) is linearly decreasing from the surface value (wind_drift_factor) until 0 at this depth.',
'level': CONFIG_LEVEL_ADVANCED},
'drift:stokes_drift': {'type': 'bool', 'default': True,
'description': 'Advection elements with Stokes drift (wave orbital motion).',
'level': CONFIG_LEVEL_ADVANCED},
'drift:stokes_drift_profile': {'type': 'enum', 'default': 'Phillips',
'enum': ['monochromatic', 'exponential', 'Phillips', 'windsea_swell'],
'description': 'Algorithm to calculate Stokes drift at depth from surface value',
'level': CONFIG_LEVEL_ADVANCED},
'drift:use_tabularised_stokes_drift': {'type': 'bool', 'default': False,
'description': 'If True, Stokes drift is estimated from wind based on look-up-tables for given fetch (drift:tabularised_stokes_drift_fetch).',
'level': CONFIG_LEVEL_ADVANCED},
'drift:tabularised_stokes_drift_fetch': {'type': 'enum', 'enum': ['5000', '25000', '50000'], 'default': '25000',
'level': CONFIG_LEVEL_ADVANCED, 'description':
'The fetch length when using tabularised Stokes drift.'},
'general:seafloor_action': {'type': 'enum', 'default': 'lift_to_seafloor',
'enum': ['none', 'lift_to_seafloor', 'deactivate', 'previous'],
'description': '"deactivate": elements are deactivated; "lift_to_seafloor": elements are lifted to seafloor level; "previous": elements are moved back to previous position; "none"; seafloor is ignored.',
'level': CONFIG_LEVEL_ADVANCED},
'drift:truncate_ocean_model_below_m': {'type': 'float', 'default': None,
'min': 0, 'max': 10000, 'units': 'm',
'description': 'Ocean model data are only read down to at most this depth, and extrapolated below. May be specified to read less data to improve performance.',
'level': CONFIG_LEVEL_ADVANCED},
'seed:z': {'type': 'float', 'default': 0,
'min': -10000, 'max': 0, 'units': 'm',
'description': 'Depth below sea level where elements are released. This depth is neglected if seafloor seeding is set selected.',
'level': CONFIG_LEVEL_ESSENTIAL},
'seed:seafloor': {'type': 'bool', 'default': False,
'description': 'Elements are seeded at seafloor, and seeding depth (z) is neglected.',
'level': CONFIG_LEVEL_ESSENTIAL},
})
self._set_config_default('drift:max_speed', 2)
[docs]
def update(self):
"""Update positions and properties of elements."""
# Simply move particles with ambient current
self.advect_ocean_current()
# Advect particles due to surface wind drag,
# according to element property wind_drift_factor
self.advect_wind()
# Stokes drift
self.stokes_drift()
# Turbulent Mixing
self.update_terminal_velocity()
if self.get_config('drift:vertical_mixing') is True:
self.vertical_mixing()
else: # Buoyancy
self.vertical_buoyancy()
# Vertical advection
self.vertical_advection()
# Optional machine learning correction
self.machine_learning_correction()
[docs]
def simulate_trajectories(self, outfile, trajectories, number=1,
wind_drift_factors=None, current_drift_factors=None,
time_step=None, time_step_output=None,
simulation_duration=None, simulation_interval=None):
import pandas as pd
time_step_output = pd.Timedelta(time_step_output)
simulation_interval = pd.Timedelta(simulation_interval)
# Interpolate trajectories to output time step
#trajectories = trajectories.traj.gridtime(time_step_output)
# Find all seed combinations: position, time, wdf, cdf
# Loop også over trajektorier -> origin_marker
step = int(simulation_interval / time_step_output)
tind = np.arange(0, trajectories.sizes['time'], step)
start_lons = trajectories.isel(time=tind).isel(trajectory=0).lon.values
start_lats = trajectories.isel(time=tind).isel(trajectory=0).lat.values
start_times = trajectories.time[tind]
self.set_config('drift:max_age_seconds', simulation_duration.total_seconds())
kwargs = {'wind_drift_factor': wind_drift_factors[0],
'current_drift_factor': current_drift_factors[0]}
for (lo,la,ti) in zip(start_lons, start_lats, start_times):
if np.isnan(lo):
continue
ti = pd.Timestamp(ti.values).to_pydatetime()
self.seed_elements(lon=lo, lat=la, time=ti, number=number, **kwargs)
print(self)
self.run(outfile=outfile, end_time=pd.Timestamp(start_times[-1].values).to_pydatetime()+simulation_duration)
# Simulate and save to file
[docs]
def wind_drift_factor_from_trajectory_lw(self, drifters, wind_drift_factors,
simulation_length, simulation_interval):
"""Perform simulations and use skillscore to optimize wind_drift_factor
drifters: list of dictionaries with numpy arrays of 'lon' and 'lat'
and list of datetimes
wind_drift_factors: the wind_drift_factors to use for simulations/optimalizations
"""
output = [] # List with one dictionary per trajectory
for d in drifters:
time = np.array(d['time'])
ti = time[1::] - time[0:-1]
if ti.min() == ti.max():
trajectory_interval = ti[0]
else:
raise ValueError('Trajectory interval is not constant')
if 'tiv' not in locals():
tiv = trajectory_interval
index_interval = simulation_interval.total_seconds()/trajectory_interval.total_seconds()
if not index_interval.is_integer():
raise ValueError('Simulation interval must be a multiple of trajectory time step')
else:
if trajectory_interval != tiv:
raise ValueError('Trajectories do not have the same time step')
last_index = (time[-1]-time[0]-simulation_length).total_seconds()/trajectory_interval.total_seconds()
seed_indices = np.arange(0, last_index, index_interval).astype(int)
do = {'lon': d['lon'][seed_indices],
'lat': d['lat'][seed_indices],
'time': np.array(time[seed_indices]),
'trajectory_start_index': seed_indices}
output.append(do)
if not 'last_seed_time' in locals():
last_seed_time = do['time'][-1]
else:
last_seed_time = np.maximum(do['time'][-1], last_seed_time)
# Seed for all starting positions in this trajectory
for lo,la,ti in zip(do['lon'], do['lat'], do['time']):
ow = np.ones(len(wind_drift_factors))
self.seed_elements(lon=lo*ow, lat=la*ow, time=ti,
wind_drift_factor=wind_drift_factors)
self.set_config('drift:max_age_seconds', simulation_length.total_seconds()+1)
self.run(end_time=last_seed_time+simulation_length)
index_of_first, index_of_last = \
self.index_of_activation_and_deactivation()
times_model = np.array(self.get_time_array()[0])
#import matplotlib.pyplot as plt
i = 0 # element number in simulation
for d, o in zip(drifters, output):
o['segments'] = {}
o['wind_drift_factor'] = np.ones(len(o['trajectory_start_index']))
o['skillscore'] = np.ones(len(o['trajectory_start_index']))
for segnum, tsi in enumerate(o['trajectory_start_index']):
o['segments'][segnum] = {'skillscore': ow}
lon_obs = d['lon'][tsi:tsi+int(index_interval)]
lat_obs = d['lat'][tsi:tsi+int(index_interval)]
#plt.plot(lon_obs, lat_obs, 'r')
for wnum, wdf in enumerate(wind_drift_factors):
lon_model = self.history['lon'][i, index_of_first[i]:index_of_last[i]]
lat_model = self.history['lat'][i, index_of_first[i]:index_of_last[i]]
i = i + 1
ss = skillscore_liu_weissberg(lon_obs, lat_obs, lon_model, lat_model)
o['segments'][segnum]['skillscore'][wnum] = ss
#plt.plot(lon_model, lat_model, label='Skillscore: %s' % ss)
print(o['segments'][segnum]['skillscore'], 'SS')
o['wind_drift_factor'][segnum] = \
wind_drift_factors[np.argmax(o['segments'][segnum]['skillscore'])]
o['skillscore'][segnum] = o['segments'][segnum]['skillscore'].max()
#plt.legend()
#plt.show()
#stop
#import xarray as xr
#da = xr.DataArray(
# data=skillscore,
# dims=["wind_drift_factor", "time"],
# coords=dict(
# wind_drift_factor=wind_drift_factors,
# time=time
# ),
# attrs=dict(
# description="Liu Weissberg skillscore",
# ),
#)
return output
[docs]
def machine_learning_correction(self):
if not hasattr(self, 'trained_model'):
return # No machine learning correction available
# ML shall only be applied every 1 hour
if not hasattr(self, 'ml_timesteps'):
self.ml_timesteps = 3600/self.time_step.total_seconds()
if self.steps_calculation % self.ml_timesteps != 0:
logger.warning('ML not applied at time step %s' % self.steps_calculation)
return
# Only apply every 1 hour
logger.warning('Machine learning')
x_vel = np.ones(self.num_elements_active())*np.NaN
y_vel = np.ones(self.num_elements_active())*np.NaN
st = datetime.now()
list_predictors_normalized = []
list_predictors_denormalized = []
# Prepare predictor dicts
for n in range(self.num_elements_active()):
dict_input = {s: getattr(self.environment, s)[n] for s in self.ml_predictors}
predictor_normalized = self.generate_one_normalized_predictor(
dict_input, self.dict_normalization_params)
list_predictors_normalized.append(predictor_normalized)
# Normalize
all_predictors_normalized = np.squeeze(np.array(list_predictors_normalized))
# perform a prediction from normalized predictor to normalized label
logger.warning('Calculating ML correction...')
predicted_normalized_residual_correction = self.trained_model.predict(all_predictors_normalized)
# get the "native units" residual correction
logger.warning('Denormalization...')
for n in range(self.num_elements_active()):
native_units_correction = self.generate_one_denormalized_prediction(
predicted_normalized_residual_correction[n, :], self.dict_normalization_params)
x_vel[n] = native_units_correction['residual_displacement_x']*1000/3600
y_vel[n] = native_units_correction['residual_displacement_y']*1000/3600
# Apply correction
logger.warning('Applying ML correction: %s to %s m/s eastwards, %s to %s m/s northwards' %
(x_vel.min(), x_vel.max(), y_vel.min(), y_vel.max()))
#logger.warning('%s particles, %s' % (self.num_elements_active(), datetime.now()-st))
self.update_positions(x_vel, y_vel)
[docs]
def disable_vertical_motion(self):
"""Deactivate any vertical processes/advection"""
conf = {
'drift:vertical_advection': False,
'drift:vertical_mixing': False}
for co, va in conf.items():
logger.info('Setting config: %s -> %s' % (co, va))
self.set_config(co, va)
[docs]
def update_terminal_velocity(self, Tprofiles=None, Sprofiles=None,
z_index=None):
"""Calculate terminal velocity due to bouyancy from own properties
and environmental variables. Sub-modules should overload
this method for particle-specific behaviour
"""
pass
[docs]
def prepare_vertical_mixing(self):
pass # To be implemented by subclasses as needed
[docs]
def prepare_run(self):
super(OceanDrift, self).prepare_run()
[docs]
def vertical_advection(self):
"""Move particles vertically according to vertical ocean current
Vertical advection by ocean currents is normally small
compared to termical velocity
"""
if self.get_config('drift:vertical_advection') is False:
logger.debug('Vertical advection deactivated')
return
in_ocean = np.where(self.elements.z<0)[0]
if len(in_ocean) > 0:
w = self.environment.upward_sea_water_velocity[in_ocean]
self.elements.z[in_ocean] = np.minimum(0,
self.elements.z[in_ocean] + self.elements.moving[in_ocean] * w * self.time_step.total_seconds())
else:
logger.debug('No vertical advection for elements at surface')
[docs]
def vertical_buoyancy(self):
"""Move particles vertically according to their buoyancy"""
in_ocean = np.where(self.elements.z<0)[0]
if len(in_ocean) > 0:
self.elements.z[in_ocean] = np.minimum(0,
self.elements.z[in_ocean] + self.elements.terminal_velocity[in_ocean] * self.time_step.total_seconds())
# check for minimum height/maximum depth for each particle accouting also for
# the sea surface height
Zmin = -1.*(self.environment.sea_floor_depth_below_sea_level + self.environment.sea_surface_height)
# Let particles stick to bottom
bottom = np.where(self.elements.z < Zmin)
if len(bottom[0]) > 0:
logger.debug('%s elements reached seafloor, interacting with bottom' % len(bottom[0]))
self.interact_with_seafloor()
self.bottom_interaction(Zmin)
[docs]
def surface_stick(self):
'''To be overloaded by subclasses, e.g. downward mixing of oil'''
# keep particle just below the surface
surface = np.where(self.elements.z >= 0)
if len(surface[0]) > 0:
self.elements.z[surface] = -0.01
[docs]
def bottom_interaction(self, Zmin=None):
'''To be overloaded by subclasses, e.g. radionuclides in sediments'''
pass
[docs]
def surface_wave_mixing(self, time_step_seconds):
'''To be overloaded by subclasses, e.g. downward mixing of oil'''
pass
[docs]
def get_diffusivity_profile(self, model, depths):
wind, depth = np.meshgrid(self.wind_speed(), depths)
MLD = self.environment.ocean_mixed_layer_thickness
background_diffusivity = self.get_config('vertical_mixing:background_diffusivity')
if model == 'windspeed_Large1994':
return verticaldiffusivity_Large1994(wind, depth, MLD, background_diffusivity)
elif model == 'windspeed_Sundby1983':
return verticaldiffusivity_Sundby1983(wind, depth, MLD, background_diffusivity)
elif model == 'gls_tke':
if not hasattr(self, 'gls_parameters'):
logger.info('Searching readers for GLS parameters...')
for reader_name, reader in self.readers.items():
if hasattr(reader, 'gls_parameters'):
self.gls_parameters = reader.gls_parameters
logger.info('Found gls-parameters in ' + reader_name)
break # Success
if not hasattr(self, 'gls_parameters'):
logger.info('Did not find gls-parameters in any readers.')
self.gls_parameters = None
windstress = np.sqrt(self.environment.surface_downward_x_stress**2 +
self.environment.surface_downward_y_stress**2)
return gls_tke(windstress, depth, self.sea_water_density(),
self.environment.turbulent_kinetic_energy,
self.environment.turbulent_generic_length_scale,
gls_parameters)
else:
raise ValueError('Unknown diffusivity model: ' + model)
[docs]
def vertical_mixing(self, store_depths=False):
"""Mix particles vertically according to eddy diffusivity and buoyancy
Buoyancy is expressed as terminal velocity, which is the
steady-state vertical velocity due to positive or negative
buoyant behaviour. It is usually a function of particle density,
diameter, and shape.
Vertical particle displacemend du to turbulent mixing is
calculated using a random walk scheme" (Visser et al. 1996)
"""
if self.get_config('drift:vertical_mixing') is False:
logger.debug('Turbulent mixing deactivated')
return
self.timer_start('main loop:updating elements:vertical mixing')
dt_mix = self.get_config('vertical_mixing:timestep')
# minimum height/maximum depth for each particle accouting also for
# the sea surface height
Zmin = -1.*(self.environment.sea_floor_depth_below_sea_level + self.environment.sea_surface_height)
# Eventual model specific preparions
self.prepare_vertical_mixing()
# get profile of eddy diffusivity
# get vertical eddy diffusivity from environment or specific model
diffusivity_model = self.get_config('vertical_mixing:diffusivitymodel')
diffusivity_fallback = self.get_config('environment:fallback:ocean_vertical_diffusivity')
# Mixing levels to be used if analytical expression:
mixing_z_analytical = -np.arange(0, self.environment.ocean_mixed_layer_thickness.max() + 2)
if diffusivity_model == 'environment':
if 'ocean_vertical_diffusivity' in self.environment_profiles and not (
self.environment_profiles['ocean_vertical_diffusivity'].min() ==
diffusivity_fallback and
self.environment_profiles['ocean_vertical_diffusivity'].max() ==
diffusivity_fallback):
Kprofiles = self.environment_profiles[
'ocean_vertical_diffusivity']
mixing_z = self.environment_profiles['z'].copy()
logger.debug('Using diffusivity from ocean model')
else:
logger.debug('Using diffusivity from Large1994 since model diffusivities not available')
mixing_z = mixing_z_analytical
Kprofiles = self.get_diffusivity_profile('windspeed_Large1994', np.abs(mixing_z))
elif diffusivity_model == 'constant':
logger.debug('Using constant diffusivity specified by fallback_values[''ocean_vertical_diffusivity''] = %s m2.s-1' % (diffusivity_fallback))
mixing_z = self.environment_profiles['z'].copy()
Kprofiles = diffusivity_fallback*np.ones(
self.environment_profiles['ocean_vertical_diffusivity'].shape) # keep constant value for ocean_vertical_diffusivity
else:
logger.debug('Using functional expression for diffusivity')
# Note: although analytical functions, z is discretised
mixing_z = mixing_z_analytical
Kprofiles = self.get_diffusivity_profile(diffusivity_model, np.abs(mixing_z))
logger.debug('Diffusivities are in range %s to %s' %
(Kprofiles.min(), Kprofiles.max()))
# get profiles of salinity and temperature
# (to save interpolation time in the inner loop)
if (self.get_config('vertical_mixing:TSprofiles') is True
and 'sea_water_salinity' in self.required_variables):
Sprofiles = self.environment_profiles['sea_water_salinity']
Tprofiles = \
self.environment_profiles['sea_water_temperature']
if (self.get_config('environment:fallback:sea_water_salinity') is not None and
Sprofiles.min() == Sprofiles.max()):
logger.debug('Salinity and temperature are fallback'
'values, skipping TSprofile')
Sprofiles = None
Tprofiles = None
else:
logger.debug('Using TSprofiles for vertical mixing')
else:
logger.debug('TSprofiles deactivated for vertical mixing')
Sprofiles = None
Tprofiles = None
# prepare vertical interpolation coordinates
z_i = range(mixing_z.shape[0])
if len(z_i) == 1:
z_index = 0
else:
z_index = interp1d(-mixing_z, z_i, bounds_error=False,
fill_value=(0,len(z_i)-1)) # Extrapolation
# Internal loop for fast time step of vertical mixing model.
# Random walk needs faster time step than horizontal advection.
logger.debug('Vertical mixing module:' +
self.get_config('vertical_mixing:diffusivitymodel'))
ntimes_mix = np.abs(int(self.time_step.total_seconds()/dt_mix))
logger.debug('Turbulent diffusion with random walk '
'scheme using ' + str(ntimes_mix) +
' fast time steps of dt=' + str(dt_mix) + 's')
if store_depths is not False:
depths = np.zeros((ntimes_mix, self.num_elements_active()))
depths[0, :] = self.elements.z
# Calculating dK/dz for all profiles before the loop
gradK = -np.gradient(Kprofiles, mixing_z, axis=0)
gradK[np.abs(gradK)<1e-10] = 0
for i in range(0, ntimes_mix):
#remember which particles belong to the exact surface
surface = self.elements.z == 0
# Update the terminal velocity of particles
self.update_terminal_velocity(Tprofiles=Tprofiles, Sprofiles=Sprofiles, z_index=z_index)
w = self.elements.terminal_velocity
# Diffusivity and its gradient at z
zi = np.round(z_index(-self.elements.z)).astype(np.uint16)
Kz = Kprofiles[zi, range(Kprofiles.shape[1])]
dKdz = gradK[zi, range(Kprofiles.shape[1])]
# Visser et al. 1997 random walk mixing
# requires an inner loop time step dt such that
# dt << (d2K/dz2)^-1, e.g. typically dt << 15min
#
# NB: In the last term Kz is evaluated in zi, while
# it should be evaluated in (self.elements.z - dKdz*dt_mix)
# This is not expected have large impact on the result
R = 2*np.random.random(self.num_elements_active()) - 1
r = 1.0/3
# New position = old position - up_K_flux + random walk
self.elements.z = self.elements.z - self.elements.moving*(
dKdz*dt_mix - R*np.sqrt((Kz*dt_mix*2/r)))
# Reflect from surface
reflect = np.where(self.elements.z >= 0)
if len(reflect[0]) > 0:
self.elements.z[reflect] = -self.elements.z[reflect]
# Reflect elements going below seafloor
# Zmin accounts for sea_surface_height
bottom = np.where(np.logical_and(self.elements.z < Zmin, self.elements.moving == 1))
if len(bottom[0]) > 0:
logger.debug('%s elements penetrated seafloor, lifting up' % len(bottom[0]))
self.elements.z[bottom] = 2*Zmin[bottom] - self.elements.z[bottom]
# Advect due to buoyancy
self.elements.z = self.elements.z + w*dt_mix*self.elements.moving
# Put the particles that belonged to the surface slick
# (if present) back to the surface
self.elements.z[surface] = 0.
# Formation of slick and wave mixing for surfaced particles
# if implemented for this class
self.surface_stick()
self.surface_wave_mixing(dt_mix)
# Let particles stick to bottom
bottom = np.where(self.elements.z < Zmin)
if len(bottom[0]) > 0:
logger.debug('%s elements reached seafloor, interacting with bottom' % len(bottom[0]))
self.interact_with_seafloor()
self.bottom_interaction(Zmin)
if store_depths is not False:
depths[i, :] = self.elements.z
self.timer_end('main loop:updating elements:vertical mixing')
if store_depths is not False:
return depths
else:
return None
[docs]
def animate_vertical_distribution(self, depths=None, maxdepth=None, bins=50, filename=None, subsamplingstep=1):
"""Function to animate vertical distribution of particles
bins: number of bins in the histogram
maxdepth: maximum depth
subsamplingstep: speed-up the generation of the animation reducing the number of output frames
fasterwriter: speed-up the writing to outpute file
"""
import matplotlib.pyplot as plt
import matplotlib.animation as animation
start_time = datetime.now()
#from timeit import default_timer as timer
#start=timer()
fig, (axk, axn) = plt.subplots(1, 2, gridspec_kw={'width_ratios': [1, 3]})
if depths is not None: # Debug mode, output from one cycle has been provided
z = depths
time_step = self.get_config('vertical_mixing:timestep')
times = [self.time + i*timedelta(seconds=time_step) for i in range(z.shape[0])]
else:
z = self.get_property('z')[0]
z = np.ma.filled(z, np.nan)
K = self.get_property('ocean_vertical_diffusivity')[0]
time_step = self.time_step.total_seconds()
times = self.get_time_array()[0]
if maxdepth is None:
maxdepth = np.nanmin(z)
if maxdepth > 0:
maxdepth = -maxdepth # negative z
if depths is not None:
axk.plot(np.nanmean(self.environment_profiles['ocean_vertical_diffusivity'], 1), self.environment_profiles['z'])
xmax = self.environment_profiles['ocean_vertical_diffusivity'].max()
else:
axk.plot(K, z, 'k.')
xmax = np.nanmax(K)
axk.set_ylim([maxdepth, 0])
axk.set_xlim([0, xmax*1.1])
axk.set_ylabel('Depth [m]')
axk.set_xlabel('Vertical diffusivity [$m^2/s$]')
subsamplingstep=max(1,subsamplingstep)
times=times[0:-1:subsamplingstep]
z=z[0:-1:subsamplingstep,:]
hist_series = np.zeros((bins, len(times)))
bin_series = np.zeros((bins+1, len(times)))
for i in range(len(times)):
hist_series[:,i], bin_series[:,i] = np.histogram(z[i,:][np.isfinite(z[i,:])], bins=bins, range=(maxdepth,z[np.isfinite(z)].max()))
maxnum = hist_series.max()
axn.clear()
bc=axn.barh(bin_series[0:-1,i], hist_series[:,i], height=-maxdepth/bins, align='edge')
axn.set_ylim([maxdepth, 0])
axn.set_xlim([0, maxnum])
title=axn.set_title('%s UTC' % times[i])
axn.set_xlabel('Number of particles')
def update_histogram(i):
for rect, y in zip(bc, hist_series[:,i]):
rect.set_width(y)
title.set_text('%s UTC' % times[i])
self.__save_or_plot_animation__(plt.gcf(),
update_histogram,
filename,
frames = len(times),
fps = 10,
interval=50,
blit=False)
logger.info('Time to make animation: %s' %
(datetime.now() - start_time))
[docs]
def plot_vertical_distribution(self, maxdepth=None, bins=None, maxnum=None):
"""Function to plot vertical distribution of particles
maxdepth: maximum depth considered for the profile
bins: number of bins between surface and maxdepth
maxnum: range of bars in histogram is [0,maxnum]
"""
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
fig = plt.figure()
mainplot = fig.add_axes([.15, .3, .8, .5])
sliderax = fig.add_axes([.15, .08, .75, .05])
tslider = Slider(sliderax, 'Timestep', 0, self.steps_output-1,
valinit=self.steps_output-1, valfmt='%0.0f')
try:
dz = self.get_config('vertical_mixing:verticalresolution')
except:
dz = 1.
maxrange = -100
# overwrite default values if input arguments are provided
if maxdepth is not None:
maxrange = maxdepth
if maxrange > 0:
maxrange = -maxrange # negative z
if bins is not None:
dz = -maxrange/bins
# using get_property instead of history to exclude elements thare are not yet seeded
z = self.get_property('z')[0]
z = np.ma.filled(z, np.nan)
if maxnum is None:
# Precalculatig histograms to find maxnum
hist_series = np.zeros((int(-maxrange/dz), self.steps_output-1))
bin_series = np.zeros((int(-maxrange/dz)+1, self.steps_output-1))
for i in range(self.steps_output-1):
hist_series[:,i], bin_series[:,i] = np.histogram(z[i,:][np.isfinite(z[i,:])], bins=int(-maxrange/dz), range=[maxrange, 0])
maxnum = hist_series.max()
def update(val):
tindex = int(tslider.val)
mainplot.cla()
mainplot.grid()
mainplot.hist(z[tindex, :], bins=int(-maxrange/dz),
range=[maxrange, 0], orientation='horizontal')
mainplot.set_ylim([maxrange, 0])
mainplot.set_xlim([0, maxnum])
mainplot.set_xlabel('number of particles')
mainplot.set_ylabel('depth [m]')
x_wind = self.history['x_wind'].T[tindex, :]
y_wind = self.history['y_wind'].T[tindex, :]
windspeed = np.mean(np.sqrt(x_wind**2 + y_wind**2))
mainplot.set_title(str(self.get_time_array()[0][tindex]) +
#' Percent at surface: %.1f %' % percent_at_surface)
' Mean windspeed: %.1f m/s' % windspeed)
fig.canvas.draw_idle()
update(0) # Plot initial distribution
tslider.on_changed(update)
plt.show()
# returning objects prevents unresponsiveness when moving the Slider
# https://github.com/matplotlib/matplotlib/issues/3105#issuecomment-44855888
return fig,mainplot,sliderax,tslider
[docs]
def plotter_vertical_distribution_time(self, ax=None, mask=None,
dz=1., maxrange=-100, bins=None, step=1):
"""Function to plot vertical distribution of particles.
Use mask to plot any selection of particles.
"""
from pylab import axes, draw
from matplotlib import dates, pyplot
if ax is None:
fig = pyplot.figure()
ax = fig.gca()
show = True
else:
show = False
if mask is None: # create a mask that is True for all particles
mask = self.history['z'].T[0] == self.history['z'].T[0]
if bins is None:
bins=int(-maxrange/dz)
ax.hist(self.history['z'].T[step,mask], bins=bins,
range=[maxrange, 0], orientation='horizontal')
ax.set_ylim([maxrange, 0])
ax.grid()
#ax.set_xlim([0, mask.sum()*.15])
ax.set_xlabel('Number of particles')
ax.set_ylabel('Depth [m]')
x_wind = self.history['x_wind'].T[step, :]
y_wind = self.history['x_wind'].T[step, :]
windspeed = np.mean(np.sqrt(x_wind**2 + y_wind**2))
ax.set_title(str(self.get_time_array()[0][step]) +
' Mean windspeed: %.1f m/s' % windspeed)
if show is True:
pyplot.show()