Skip to content

New Advection Diffusion Algorithms (Bug in Milstein + template for 3D Advection) #858

@ElizaJayne11

Description

@ElizaJayne11

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.

  1. 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.

  2. 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.

  3. 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})

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions