Scheduled Downtime
On Friday 21 April 2023 @ 5pm MT, this website will be down for maintenance and expected to return online the morning of 24 April 2023 at the latest

Air mass conservation in WRF

Rostislav

New member
Hi,

I would need a way to get vertical mass fluxes (in kg/m2/s) of air through w-levels of WRF, consistent with mass conservation.

I try to figure out how well WRF output fields satisfy the continuity equation for air-mass.

I have took a WRF output from AMPS (any other WRF output would do):
Code:
ncks -v U,V,XLAT,XLONG,PSFC,P_TOP,ZNW,P_STRAT http://amrdcdata.ssec.wisc.edu/thredds/dodsC/yoppshwrfnetcdf/202206/2022060400/wrfout_d01_2022060400_f066.nc wrfout_d01_2022060400_f066.nc

And run the following code that integrates the continuity equation along vertical to get air flux divergence across half of the domain, and across full vertical of the domain.

Python:
#!/usr/bin/env python3                                                                                                                                                                                                                
 
import matplotlib as mpl
mpl.use('Agg')
import matplotlib.pyplot as plt
 
import netCDF4 as nc4
import datetime as dt
import numpy as np
import os
import sys
 
def cmap_and_norm_clevs(cmapname, clevs):
   # Input -- name of mpl colormap
   #          np.array of cleca
    #Generates colormap and norm with given clevs
    # As in grads
    basecm=mpl.cm.get_cmap(cmapname)
    nlevs=len(clevs)
    cols=basecm(np.linspace(0, 1, nlevs+1))
    norm = mpl.colors.BoundaryNorm(clevs, nlevs-1)
    #cm_out = pylab.cm.from_list("%s_discr"%cmapname, cols[1:-1], nlevs-1)
    cm_out = mpl.colors.ListedColormap(cols[1:-1], name="%s_discr"%cmapname)
    cm_out.set_under(cols[0])
    cm_out.set_over(cols[-1])
    cm_out.set_bad([0,0,0,0]) #Transparent
    return cm_out, norm
 
infname="wrfout_d01_2022060400_f066.nc"
 
with nc4.Dataset(infname) as inf:
    inf.set_auto_mask(False) ## No masked arrays, please!
 
    nz = inf.dimensions['bottom_top'].size
 
    it = 0
 
    dx = inf.DX
    dy = inf.DY                                                                                                                                                                                                                      
    u = inf.variables['U'][it,:,:,:]
    v = inf.variables['V'][it,:,:,:]
    eta  = inf.variables['ZNW'][it,:]
    ptop  = inf.variables['P_TOP'][it] #P_TOP:description = "PRESSURE TOP OF THE MODEL" ;
    psfc = inf.variables['PSFC'][it,:,:] # PSFC:description = "SFC PRESSURE" ;
 
    # zeroth-level skipped
    psfc0 = (psfc[np.newaxis,:,:] - ptop)
    psfcu = np.zeros(u.shape[1:])
    psfcv = np.zeros(v.shape[1:])
    # remap surface pressure to U and V
    psfcu[:,0] = psfc[:,0]
    psfcu[:,-1] = psfc[:,-1]
    psfcu[:,1:-1] = 0.5*(psfc[:,:-1] + psfc[:,1:])
    psfcv[0,:] = psfc[0,:]
    psfcv[-1,:] = psfc[-1,:]
    psfcv[1:-1,:] = 0.5*(psfc[:-1,:] + psfc[1:,:])
 
    u *= psfcu / 9.81  ## kg/m2/s
    v *= psfcv / 9.81  ## kg/m2/s
    div = (u[:,:,:-1] - u[:,:,1:] +
           v[:,:-1,:] - v[:,1:,:]) / dx  
    deta = eta[1:] - eta[:-1]
    div *= deta[:,np.newaxis,np.newaxis] # rho*dz
     #grid%pb(i,k,j) = grid%znu(k)*(p_surf - p_top) + p_top
 
fig =  plt.figure()
 
clevs = [-2, -1, -.5, -.2, -.1, -.05, .05, .1, .2, .5, 1, 2]
cmap,norm = cmap_and_norm_clevs("jet", clevs)
 
for iLev in [ nz//2, nz-1 ]:
    ax = fig.add_axes((.1,.1,.8,.8))  #(left, bottom, width, height)
    ax.imshow(np.sum(div[0:(iLev+1),:,:], axis=0),  cmap=cmap, norm=norm)
    ax.set_title("vertical mass flux at w level %d, kg/m2/s"%(iLev))
   
    axcb = fig.add_axes((.85,.1,.03,.8))
    cb = mpl.colorbar.ColorbarBase(axcb, norm=norm, cmap=cmap,
                        orientation='vertical', extend='both')
    axcb.set_yticks(clevs)
    figname = "div_lev_0-%02d.png"%(iLev)
    fig.savefig(figname, dpi=600)
    fig.clf()
The half-domain flux divergence ( or mass flux across the w level in the middle of the domain) looks like
div_lev_0-30.png
For the whole domain, I would expect it to vanish, but it stays well beyond the numerical errors.
div_lev_0-59.png
I wonder if it is some bug in my script to calculate it, my misunderstanding of continuity in WRF, my overoptimism about numerical errors, or some issue with conserving air mass in the model?
Any help would be appreciated.

UPD 2024-05-31: Corrected the magic to download the file
 
Last edited:
Thank you! The doc did help a bit, together with some magic from `registry.hyb_coord` file from the source code.
Calculating of divergence needs few more variabes:
Code:
ncks -v U,V,XLAT,XLONG,PSFC,P_TOP,ZNU,ZNW,BF,C1F,C2F,C3F,C4F http://amrdcdata.ssec.wisc.edu/thredds/dodsC/yoppshwrfnetcdf/202206/2022060400/wrfout_d01_2022060400_f066.nc wrfout_d01_2022060400_f066.nc

In the end I came up with the following code:
Python:
!/usr/bin/env python3                                                                                                                                                                                                                         
 
 
import matplotlib as mpl
mpl.use('Agg')
import matplotlib.pyplot as plt
 
import netCDF4 as nc4
import datetime as dt
import numpy as np
import os
import sys
 
 
def cmap_and_norm_clevs(cmapname, clevs):
   # Input -- name of mpl colormap
   #          np.array of cleca
    #Generates colormap and norm with given clevs
    # As in grads
    basecm=mpl.cm.get_cmap(cmapname)
    nlevs=len(clevs)
    cols=basecm(np.linspace(0, 1, nlevs+1))
    norm = mpl.colors.BoundaryNorm(clevs, nlevs-1)
    #cm_out = pylab.cm.from_list("%s_discr"%cmapname, cols[1:-1], nlevs-1)
    cm_out = mpl.colors.ListedColormap(cols[1:-1], name="%s_discr"%cmapname)
    cm_out.set_under(cols[0])
    cm_out.set_over(cols[-1])
    cm_out.set_bad([0,0,0,0]) #Transparent
    return cm_out, norm
 
infname="wrfout_d01_2022060400_f066.nc"

with nc4.Dataset(infname) as inf:
    inf.set_auto_mask(False) ## No masked arrays, please!
 
    nz = inf.dimensions['bottom_top'].size
 
    it = 0
 
    dx = inf.DX                                                                                                                                                                                                                                 
    dy = inf.DY
    u = inf.variables['U'][it,:,:,:]
    v = inf.variables['V'][it,:,:,:]
#    eta  = inf.variables['ZNW'][it,:]
    c3f = inf.variables['C3F'][it,:]
    c4f = inf.variables['C4F'][it,:]
    ptop  = inf.variables['P_TOP'][it] #P_TOP:description = "PRESSURE TOP OF THE MODEL" ;
    psfc = inf.variables['PSFC'][it,:,:] # PSFC:description = "SFC PRESSURE" ;
 
 
## Get surface pressure for U and V grids
psfcu = np.zeros(u.shape[1:])
psfcv = np.zeros(v.shape[1:])
psfcu[:,0] = psfc[:,0]
psfcu[:,-1] = psfc[:,-1]
psfcu[:,1:-1] = 0.5*(psfc[:,:-1] + psfc[:,1:])
psfcv[0,:] = psfc[0,:]
psfcv[-1,:] = psfc[-1,:]
psfcv[1:-1,:] = 0.5*(psfc[:-1,:] + psfc[1:,:])
 
# From registry.hyb_coord
#Pd = BF ( Pds - Pt ) + ( eta - BF ) ( P0 - Pt ) + Pt
#    = C3 * ( Pds - Pt ) +  C4  + Pt
 
#dPd = dC3 * ( Pds - Pt ) + dC4
dC3 = c3f[:-1] -  c3f[1:]
dC4 = c4f[:-1] -  c4f[1:]
 
# mass divergence
casename = "c3_c4-ptop"
u *=  (dC3[:,None,None] * (psfcu[None,:,:] - ptop ) + dC4[:,None,None])  / 9.81  ## kg/m/s
v *=  (dC3[:,None,None] * (psfcv[None,:,:] - ptop ) + dC4[:,None,None])  / 9.81  ## kg/m/s
div = (u[:,:,  :-1] - u[:,:, 1:] +
       v[:,:-1,:  ] - v[:,1:,: ]) / dx  ## kg/m2s

fig =  plt.figure()
 
clevs = [-3, -1,  -.1, -.03,  -.01, -.001,  .001, .01, .03, .1, .3 , 1, 3]
cmap,norm = cmap_and_norm_clevs("jet", clevs)
 
for iLev in range(nz+1):
    ax = fig.add_axes((.1,.1,.8,.8))  #(left, bottom, width, height)
    ax.imshow(np.sum(div[0:(iLev+1),:,:], axis=0),  cmap=cmap, norm=norm)
 
    ax.set_title("mass flux divergence below w level %d"%(iLev+1))
  
 
    axcb = fig.add_axes((.8,.1,.03,.8))
    axcb.set_title('kg/m2/s')
    cb = mpl.colorbar.ColorbarBase(axcb, norm=norm, cmap=cmap,
                        orientation='vertical', extend='both')
    axcb.set_yticks(clevs)
    figname = "div_%s_lev_0-%02d.png"%(casename, iLev)
    fig.savefig(figname, dpi=600)
    fig.clf()
div_c3_c4-ptop_lev_0-60.png
The continuity got notably better, but still not perfect. There are clear outliers along the antarctic coast. The typical over ocean divergence is ~0.01 kg/m2/s which corresponds to pressure change of 0.1 Pa/s = 3.6 mBar/hour, which seems to be a a bit faster than normal pressure change rate..

So is it a bug in my script, my misunderstanding, violation of mass conservation in WRF or some issue with numerical precision?
Also I wonder about a frame around Antarctica...


Thank you!
 
Last edited:
Just got another WRF output: 3-km simulation around South China sea, and plotted it with pretty much the same script as above. This case seems to be a better illustration for the problem.

Here is mass-flux divergence at the lowest level (should be equal to vertical air-mass flux at it's top).

div_c3_c4-ptop_lev_0-00.png
And here is the mass-flux divergence integrated over full vertical extent of the domain

div_c3_c4-ptop_lev_0-39.png
No nesting here, so no fine-scale artifacts. So what is the reason for the vertically-integrated flux divergence?
 
Top