-
Notifications
You must be signed in to change notification settings - Fork 168
Description
Hi
This issue is continuing from a discussion my student and I started in issue #849, which is related to the new Advection + Diffusion kernels implemented in Parcels version 2.2.
As noted earlier, we are wanting to run a simulation with 3D advection + 3D spatially-varying horizontal diffusion (i.e. Kh varies with lat/lon and depth). I am very familiar with both RK4 and Milstein methods, and since the current Parcels code only works for 2D, I am developing a custom kernel that extends what you did to also advect in the vertical, and to use the local Kh values at the particle's current depth. Below I list 3 points related to this effort. I am very happy to continue interacting with you to address these points, as well as aiding with future improvements to Parcels numerical methods.
-
I discovered an errror in the formula you used for Milstein. Specifically, in your terms involving dKdx and dKdy, your term in parentheses is wrong. It should be the random number ^2 + 1 and then all of that multiplied by delta t/2, which is not the same as what you have. The correct formula is show in the code below using your notation.
-
Below is my draft of the custom kernel for the 3D Advection with the Milstein method for 2D diffusion at each depth. I welcome you feedback about this draft code. I should note that I am a Python newbie (my expertise is MATLAB) and am struggling with figuring out Python's syntax and data-types in order to make things readable and efficient. This is NOT how I would write this in MATLAB.
-
I am confused about whether my units are correct. For example, it appears that in Parcels, velocities are already converted to degrees per second in fieldset, so we can use u,v,w directly when calculating displacements in lat lon in the custom kernel. In contrast, it appears that in the fieldset, kH remains in m^2/ second, so the units need to be adjusted in the custom kernel. This is not obvious, given that when I run your 2D Adv-Dif with the builtin Milstein (and your earlier built-in kernels BrownianMotion2D and SpatiallyVaryingBrownianMotion), it appears that Kh has been converted to degrees^2 / second. Can you please confirm this?
Best wishes
Wendy
from parcels import FieldSet, Field, ParticleSet, JITParticle, ParticleFile, ErrorCode
import numpy as np
from datetime import timedelta as delta
import math
from netCDF4 import Dataset
from parcels import rng as random
def DeleteParticle(particle, fieldset, time):
particle.delete()
def AdvectionRK43D_DiffusionM12D(particle, fieldset, time):
# Kernel for 3D advection-solved using fourth order Runge-Kutta (RK4)
# and 3D Horizontal diffusion -- solved using the Milstein scheme at first order (M1).
# Note that this approach does not consider the interaction of advection and diffusion within the time step
# Milstein requires computation of the local gradient in Kh in both directions
# These gradients are estimated using a centered finite different approach of O(h^2)
# Where h = dres (set in the main code). this should be at least an order of magnitude
# less than the typical grid resolution, but not so small as to introduce catestrophic cancellation
# Parcels 2.2 has used a uniform random number to determine the random kick
# Mathematically, a normal distribution is more accurate for a given time step as noted in Parcels 2.2
# Parcels 2.2 argue that uniform is more efficient, which is generally true when comparing individual calls
# However, since the uniform only converges on the correct distribution after about 5 steps,
# it is not overall more efficient than a normal distribution.
# Parcel 2.2. also argue that the random increments remain bounded.
# It is not immediately apparent to me why "boundedness" is an advantage.
# Despite that,I have kept the code below using a uniform distribution
# in part b/c horizontal diffusion is already very approximate (our diffusivities are not precise!)
# There is also a long history of Ocean Modelers using a uniformly distributed random number (e.g. Visser, Ross & Sharples etc)
# Current space-time location of particle
lonp = particle.lon
latp = particle.lat
depthp = particle.depth
dt = particle.dt
# RK4 for Advection
# Changed variable naming from Parcels 2.2 to
# (1) improve efficiency as we were instructed that it is faster to rename variables that are "." than to recall and
# (2) to make the code easier to read
# Note: I have followed Parcels 2.2. and left the RK4 to be computing slopes within the time step instead of displacements
# Both approaches are used in the literature
#start at current space-time point using those values to get slope
(u1, v1, w1) = fieldset.UVW[time, depthp, latp, lonp]
#Get estimate for slope at midpoint using previous estimate for slope
lon1, lat1, dep1 = (lonp + u1*.5*dt, latp + v1*.5*dt, depthp + w1*.5*dt)
(u2, v2, w2) = fieldset.UVW[time + .5*dt, dep1, lat1, lon1]
#Get improved estimate for slope at midpoint using previous estimate for slope
lon2, lat2, dep2 = (lonp + u2*.5*dt, latp + v2 *.5*dt, depthp + w2*.5*dt)
(u3, v3,w3) = fieldset.UVW[time + .5 * dt, dep2, lat2, lon2]
#Get estimate for slope at endppoint using previous estimate for slope at midpoint
lon3, lat3, dep3 = (lonp + u3*dt, latp + v3*dt, depthp+w3*dt)
(u4, v4, w4) = fieldset.UVW[time + dt, dep3, lat3, lon3]
#Calculate particle displacement due to local advection
#This assumes that fieldset has already converted u,v,w to degrees
Advect_lon = ((u1 + 2 * u2 + 2 * u3 + u4) / 6.) * dt
Advect_lat = ((v1 + 2 * v2 + 2 * v3 + v4) / 6.) * dt
Advect_dep = ((w1 + 2 * w2 + 2 * w3 + w4) / 6.) * dt
#Milstein for Horizontal Diffusion
Khdifferencedist = fieldset.dres #in degrees
#Note Kh is in m^2/s here, unlike built-in that has already converted to degrees
#To save on repeated conversions, only done after compute displace ent
#Sample random number (Wiener increment) with zero mean and std of sqrt(dt) from uniform distribution
dWx = random.uniform(-1., 1.) * math.sqrt(math.fabs(dt) * 3)
dWy = random.uniform(-1., 1.) * math.sqrt(math.fabs(dt) * 3)
# dWx = random.normalvariate(0., 1.) * math.sqrt(math.fabs(dt))
# dWy = random.normalvariate(0., 1.) * math.sqrt(math.fabs(dt))
#Get estimate of random kick in x-direction based on local diffuvisity (neglects spatial variation)
bx = math.sqrt(2* fieldset.Kh_zonal[time, depthp, latp, lonp])
#Get estimate of randome kick in y-direction based on local diffusivity (neglects spatial variation)
by = math.sqrt(2 * fieldset.Kh_meridional[time, depthp, latp, lonp])
#Get estimate of zonal diffusivity gradient at current location using finite centered finite differenece
#This derivative is used to correct basic random kick due to variable diffusivity
Kxp1 = fieldset.Kh_zonal[time, depthp, latp, lonp + Khdifferencedist]
Kxm1 = fieldset.Kh_zonal[time, depthp, latp, lonp - Khdifferencedist]
dKdx = (Kxp1 - Kxm1) / (2 * Khdifferencedist)
#Get estimate of meridional gradient at current location using fininte centered difference
#This derivative is used to correct basic random kick due to variable diffusivity
Kyp1 = fieldset.Kh_meridional[time, depthp, latp + Khdifferencedist, lonp]
Kym1 = fieldset.Kh_meridional[time, depthp, latp - Khdifferencedist, lonp]
dKdy = (Kyp1 - Kym1) / (2 * Khdifferencedist)
#Calculate particle horizontal displacement due to local diffusiona and diffusivity gradient
DiffH_lon = bx*dWx + 0.5 * dKdx * (dWx**2 + 1)*dt #Wonky units
DiffH_lat = by*dWy + 0.5 * dKdy * (dWy**2 + 1)*dt #Wonky units
to_lat = 1/1000./1.852/60.
to_lon = to_lat/math.cos(particle.lat*math.pi/180)
DiffH_lon=DiffH_lon*to_lon
DiffH_lat=DiffH_lat*to_lat
#Particle positions are updated only after evaluating all terms (i.e. Advection + Diffusion simulataneous)
#Note this approach does not consider the interaction of adv and diff on estimated particle displacements within the time step)
particle.lon += Advect_lon + DiffH_lon
particle.lat += Advect_lat + DiffH_lat
particle.depth += Advect_dep
#Read in physical fields
data_path = '../..//BNAM/PhysicalFields/'
ufiles = data_path + '3D_U_'+ 'September' +'_BNAM.nc'
vfiles = data_path + '3D_V_'+ 'September' +'_BNAM.nc'
wfiles = data_path + '3D_W_'+ 'September' +'_BNAM.nc'
mesh_mask = data_path + '3D_Mask_'+ 'September' +'_BNAM.nc'
filenames = {'U': {'lon': mesh_mask, 'lat': mesh_mask, 'depth': wfiles, 'data': ufiles},
'V': {'lon': mesh_mask, 'lat': mesh_mask, 'depth': wfiles, 'data': vfiles},
'W': {'lon': mesh_mask, 'lat': mesh_mask, 'depth': wfiles, 'data': wfiles}}
variables = {'U': 'U',
'V': 'V',
'W': 'W'}
dimensions = {'U': {'lon': 'glamf', 'lat': 'gphif', 'depth': 'deptht', 'time': 'time_counter'},
'V': {'lon': 'glamf', 'lat': 'gphif', 'depth': 'deptht', 'time': 'time_counter'},
'W': {'lon': 'glamf', 'lat': 'gphif', 'depth': 'deptht', 'time': 'time_counter'}}
fieldset = FieldSet.from_nemo(filenames, variables, dimensions, allow_time_extrapolation=True)
dataset = Dataset(data_path +"Mixing3D_BNAM.nc")
kh_zonal = dataset.variables['kh_zonal'][:]
kh_meridional = dataset.variables['kh_meridional'][:]
fieldset.add_field(Field('Kh_zonal', kh_zonal, grid=fieldset.U.grid))
fieldset.add_field(Field('Kh_meridional', kh_meridional, grid=fieldset.U.grid))
fieldset.add_constant('dres', 0.001) #Changed to avoid potential for catestrophic cancellation. Our grid resolution is ??
# Set coordinates of partiles
a = np.loadtxt('../../SCALLOPS/SCALLOP DATA/ICs/'+ 'ICs1.txt')
#Lon and lat coordinates respectively
lonp = a[:,0]
latp = a[:,1]
depthp = np.ones(len(lonp))
pset = ParticleSet.from_list(fieldset, JITParticle, lon=lonp, lat=latp, depth=depthp)
kernels = pset.Kernel(AdvectionRK43D_DiffusionM12D)
pset.execute(kernels, runtime=delta(days=45), dt=delta(hours=1),
output_file = ParticleFile("Particles_Testing", pset, outputdt=delta(days=1)),
recovery={ErrorCode.ErrorOutOfBounds: DeleteParticle})