[1]:
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
from dask.distributed import Client
import scipy

Working with AUS2200 data on model levels

Convert hybrid height coordinate to height above the surface, and interpolate to regular height intervals

[2]:
#From ~access/umdir/vn13.8/ctldata/vert/vertlevs_L70_50t_20s_80km

#This is a list of AUS2200 model levels and associated metadata, that will be used to reconstruct the hybrid height coordinate

z_top_of_model = 40000.00
first_constant_r_rho_level= 62
eta_theta=np.array([0.0000000E+00,   0.1250000E-03,   0.5416666E-03,   0.1125000E-02,   0.1875000E-02,
0.2791667E-02,   0.3875000E-02,   0.5125000E-02,   0.6541667E-02,   0.8125000E-02,
0.9875000E-02,   0.1179167E-01,   0.1387500E-01,   0.1612500E-01,   0.1854167E-01,
0.2112500E-01,   0.2387500E-01,   0.2679167E-01,   0.2987500E-01,   0.3312500E-01,
0.3654167E-01,   0.4012500E-01,   0.4387500E-01,   0.4779167E-01,   0.5187500E-01,
0.5612501E-01,   0.6054167E-01,   0.6512500E-01,   0.6987500E-01,   0.7479167E-01,
0.7987500E-01,   0.8512500E-01,   0.9054167E-01,   0.9612500E-01,   0.1018750E+00,
0.1077917E+00,   0.1138750E+00,   0.1201250E+00,   0.1265417E+00,   0.1331250E+00,
0.1398750E+00,   0.1467917E+00,   0.1538752E+00,   0.1611287E+00,   0.1685623E+00,
0.1761954E+00,   0.1840590E+00,   0.1921980E+00,   0.2006732E+00,   0.2095645E+00,
0.2189729E+00,   0.2290236E+00,   0.2398690E+00,   0.2516917E+00,   0.2647077E+00,
0.2791699E+00,   0.2953717E+00,   0.3136506E+00,   0.3343919E+00,   0.3580330E+00,
0.3850676E+00,   0.4160496E+00,   0.4515977E+00,   0.4924007E+00,   0.5392213E+00,
0.5929016E+00,   0.6543679E+00,   0.7246365E+00,   0.8048183E+00,   0.8961251E+00,
0.1000000E+01])
eta_rho=np.array([0.6249999E-04,   0.3333333E-03,   0.8333333E-03,   0.1500000E-02,   0.2333333E-02,
0.3333333E-02,   0.4500000E-02,   0.5833333E-02,   0.7333333E-02,   0.9000000E-02,
0.1083333E-01,   0.1283333E-01,   0.1500000E-01,   0.1733333E-01,   0.1983333E-01,
0.2250000E-01,   0.2533333E-01,   0.2833333E-01,   0.3150000E-01,   0.3483333E-01,
0.3833333E-01,   0.4200000E-01,   0.4583333E-01,   0.4983333E-01,   0.5400000E-01,
0.5833334E-01,   0.6283334E-01,   0.6750000E-01,   0.7233334E-01,   0.7733333E-01,
0.8250000E-01,   0.8783333E-01,   0.9333333E-01,   0.9900000E-01,   0.1048333E+00,
0.1108333E+00,   0.1170000E+00,   0.1233333E+00,   0.1298333E+00,   0.1365000E+00,
0.1433333E+00,   0.1503334E+00,   0.1575020E+00,   0.1648455E+00,   0.1723789E+00,
0.1801272E+00,   0.1881285E+00,   0.1964356E+00,   0.2051189E+00,   0.2142687E+00,
0.2239982E+00,   0.2344463E+00,   0.2457803E+00,   0.2581997E+00,   0.2719388E+00,
0.2872708E+00,   0.3045112E+00,   0.3240212E+00,   0.3462124E+00,   0.3715503E+00,
0.4005586E+00,   0.4338236E+00,   0.4719992E+00,   0.5158110E+00,   0.5660614E+00,
0.6236348E+00,   0.6895022E+00,   0.7647274E+00,   0.8504717E+00,   0.9480625E+00])

First check what the lev dimension actually represents

atmosphere_hybrid_height_coordinate is defined by: Z(i, j, k) = Zsea(k) + C(k) * orography(i, j)

Where Zsea is

Zsea(k) = eta(k) * top_level_height

And C is

C(k) = (1 - eta(k) / eta(first_constant_r_rho_level))**2 for k <= first_constant_r_rho_level

and

C(k) = 0 for k > first_constant_r_rho_level

(from http://climate-cms.wikis.unsw.edu.au/Vertical_Coordinates)

[3]:
#Calculate Zsea from the above equation using the actual model levels (rho levels here for wind data)
Zsea = (eta_rho * z_top_of_model)

#Load the atmosphere_hybrid_height_coordinate variable as levs
ua = xr.open_dataset(
    "/g/data/bs94/AUS2200/mjo-neutral2013/v1-0/1hr/ua/ua_AUS2200_mjo-neutral_1hrPt_201303011900-201303020000.nc",
    chunks={}
    )
levs = ua.lev.values
[4]:
#Compare the two
Zsea[0:66] == levs
[4]:
array([ True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True,  True,  True,  True,  True,  True,  True,
        True,  True,  True])

So the levs dimension represents Zsea.

Now we can use this to calculate the actual height coordinate, Z, but we first need to load orography and calculate C

[5]:
#Load orography
orog = xr.open_dataset("/g/data/bs94/AUS2200/mjo-neutral2013/v1-0/fx/orog/orog_AUS2200_mjo-neutral_fx.nc")
[6]:
#Calculate C from the model rho levels
C = (1 - eta_rho / eta_rho[first_constant_r_rho_level-1])**2
C[first_constant_r_rho_level:] = 0
[7]:
#Z will be of shape lat,lon,lev, so repeat C over lat/lon dimensions
C_repeated = np.repeat(
        np.repeat(
        C[np.newaxis,np.newaxis,0:len(levs)],
        orog.orog.shape[0],
        axis=0),
    orog.orog.shape[1],
    axis=1
)
[8]:
#And repeat orography over the level dimension
orog_repeated = np.repeat(
    orog.orog.values[:,:,np.newaxis],
    len(levs),
    axis=2)
[9]:
#Calculate Z
Z = levs + C_repeated * orog_repeated
[10]:
#Compare Z and levs for a point the ocean. They should be the same

plt.plot(Z[0,0,:],np.arange(66),marker="o",ms=5,label="Z")
plt.ylim([0,40])
plt.xlim([0,5000])

plt.plot(levs,np.arange(66),marker="x",label="lev")

plt.legend()
[10]:
<matplotlib.legend.Legend at 0x14acc6638a90>
../_images/Recipes_aus2200_vert_levels_12_1.png
[11]:
#Compare Z and levs over the land (for a point 233 m above sea level).
plt.plot(Z[1000,1000,:],np.arange(66),marker="o",ms=5,label="Z (above sea level)")
plt.ylim([0,40])
plt.xlim([0,5000])

plt.plot(Z[1000,1000,:] - orog_repeated[1000,1000,:],np.arange(66),marker="o",ms=5,label="Z (above surface)")
plt.ylim([0,40])
plt.xlim([0,5000])

plt.plot(levs,np.arange(66),marker="x",label="lev")

plt.legend()
[11]:
<matplotlib.legend.Legend at 0x14acc62e8590>
../_images/Recipes_aus2200_vert_levels_13_1.png
[20]:
#Plot the difference between Z and lev. Difference increases with altitude as the hybrid-height coordinates transition
# from closely following the terrain to being more uniform across the grid
plt.plot(
    (Z[1000,1000,:] - orog_repeated[1000,1000,:]) - levs,
    levs,marker="o")
plt.ylabel("Zsea")
[20]:
Text(0, 0.5, 'Zsea')
../_images/Recipes_aus2200_vert_levels_14_1.png
[12]:
def interp_scipy(x, xp, fp):
    f = scipy.interpolate.interp1d(xp, fp, kind="linear", fill_value="extrapolate")
    return f(x)

def interp_model_level_to_z(z_da,var_da,mdl_dim,heights):

    '''
    Linearly interpolate from model level data to height levels.
    If the requested height is below the lowest model level, data from the lowest model level is returned.
    If the requested height is above the highest model level, then NaNs are returned.

    Input
    z_da: xarray Dataarray of height
    var_da: xarray Dataarray of variable to interpolate
    mdl_dim: name of the model level dimension (e.g. hybrid). NOTE that model levels must be decreasing (so height is increasing)
    heights: numpy array of height levels
    '''

    #assert z_da[mdl_dim][0] > z_da[mdl_dim][-1], "Model levels should be decreasing"

    interp_da = xr.apply_ufunc(interp_scipy,
                heights,
                z_da,
                var_da,
                input_core_dims=[ ["height"], [mdl_dim], [mdl_dim]],
                output_core_dims=[["height"]],
                exclude_dims=set((mdl_dim,)),
                dask="parallelized",
                output_dtypes=[var_da.dtype],
                vectorize=True)
    interp_da["height"] = heights

    return interp_da
[13]:
client = Client()
client
/g/data/xp65/public/apps/med_conda/envs/analysis3-24.07/lib/python3.11/site-packages/distributed/node.py:187: UserWarning: Port 8787 is already in use.
Perhaps you already have a cluster running?
Hosting the HTTP server on port 33785 instead
  warnings.warn(
[13]:

Client

Client-f2225512-3794-11f0-8965-00000391fe80

Connection method: Cluster object Cluster type: distributed.LocalCluster
Dashboard: /proxy/33785/status

Cluster Info

[14]:
#Convert the height array to a chunked xarray dataarray
Z_agl_da = xr.DataArray(Z - orog_repeated,
             dims=["lat","lon","lev"],
            coords={"lat":ua.lat,"lon":ua.lon,"lev":ua.lev}).chunk({
    "lev":-1,
    "lon":ua.ua.chunksizes["lon"][0],
    "lat":ua.ua.chunksizes["lat"][0]})
[15]:
#Interpolate the U wind onto regular heights at 100 m intervals up to 4500 m
ua_interp = interp_model_level_to_z(
    Z_agl_da,
    ua.isel(time=0).ua.chunk({"lev":-1}),
    "lev",
    np.arange(0,4600,100)
)
[16]:
#Plot the interpolated winds

ua_interp.sel(height=3300).plot()
/g/data/xp65/public/apps/med_conda/envs/analysis3-24.07/lib/python3.11/site-packages/distributed/client.py:3371: UserWarning: Sending large graph of size 2.71 GiB.
This may cause some slowdown.
Consider loading the data with Dask directly
 or using futures or delayed objects to embed the data into the graph without repetition.
See also https://docs.dask.org/en/stable/best-practices.html#load-data-with-dask for more information.
  warnings.warn(
[16]:
<matplotlib.collections.QuadMesh at 0x14abac49ddd0>
../_images/Recipes_aus2200_vert_levels_19_2.png
[17]:
#Compared with the original data

ua.isel(lev=30,time=0).ua.plot()
[17]:
<matplotlib.collections.QuadMesh at 0x14abac3da250>
../_images/Recipes_aus2200_vert_levels_20_1.png
[21]:
#Compare vertical profiles for a point over the land

ua.isel(time=0,lat=1000,lon=1000).sel(lev=slice(0,4500)).ua.plot(y="lev",marker="o")
ua_interp.isel(lat=1000,lon=1000).plot(y="height",marker="o")
[21]:
[<matplotlib.lines.Line2D at 0x14ab4f90de50>]
../_images/Recipes_aus2200_vert_levels_21_1.png
[ ]: