GSIBEC: calculate rotated lon/lat parameters for a mpas grid#

Developed by Junjun Hu

get the pyDAmonitor_ROOT env variable#

%%time 

# autoload external python modules if they changed
%load_ext autoreload
%autoreload 2

import sys, os
pyDAmonitor_ROOT=os.getenv("pyDAmonitor_ROOT")
if pyDAmonitor_ROOT is None:
    print("!!! pyDAmonitor_ROOT is NOT set. Run `source ush/load_pyDAmonitor.sh`")
else:
    print(f"pyDAmonitor_ROOT={pyDAmonitor_ROOT}\n")
sys.path.insert(0, pyDAmonitor_ROOT)
print(f"{pyDAmonitor_ROOT}")
pyDAmonitor_ROOT=/home/Guoqing.Ge/pyDAmonitor

/home/Guoqing.Ge/pyDAmonitor
CPU times: user 5.91 ms, sys: 73.7 ms, total: 79.6 ms
Wall time: 118 ms

define grid resolution and grid_ratio_regional#

from netCDF4 import Dataset
import numpy as np

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature

############ Constants ####################
deg2rad = np.pi / 180.0
rad2deg = 180.0 / np.pi
quarter = 0.25
one = 1.0

###########################################
grid_ratio_regional = 2.0 # =1.0, the analysis grid resolution is the same as background grid resolution; >1.0, analysis is performed on a coaser grid

# conus 12km
dlat = 0.115 # background grid resolution
dlon = 0.115 # background grid resolution

# conus 3km
#dlat = 0.027 
#dlon = 0.027 

# south 3.5km
#dlat = 0.0314 
#dlon = 0.0314  

# ncar 5km
#dlat = 0.0449
#dlon = 0.0449 

print(f"grid_ratio_regional : {np.min(grid_ratio_regional):.1f}")
grid_ratio_regional : 2.0

read MPAS grid#

# MPAS grid
mgrid = f"{pyDAmonitor_ROOT}/data/mpasjedi/conus12km.grid.nc"
#mgrid = f"{pyDAmonitor_ROOT}/data/mpasjedi/conus3km.grid.nc"
#mgrid = f"{pyDAmonitor_ROOT}/data/mpasjedi/south3.5km.grid.nc"
#mgrid = f"{pyDAmonitor_ROOT}/data/mpasjedi/ea5km.grid.nc"

# Read lon/lat from *.grid.nc
nc_g = Dataset(mgrid, mode='r')
latCell_rad = nc_g.variables["latCell"][:]
lonCell_rad = nc_g.variables["lonCell"][:]
latCell_deg = latCell_rad*rad2deg
lonCell_deg = lonCell_rad*rad2deg

calculate rotated lon/lat from mpas grid and print out the GSIBEC parameters#

# rotate mpas grid
# create xc, yc, zc for the cell centers.
nc = np.size(latCell_rad) # number of cells

xc = np.cos(latCell_rad) * np.cos(lonCell_rad)
yc = np.cos(latCell_rad) * np.sin(lonCell_rad)
zc = np.sin(latCell_rad)

#  compute center as average x,y,z coordinates of all cells --
xcent = np.mean(xc)
ycent = np.mean(yc)
zcent = np.mean(zc)

rnorm = one / np.sqrt(xcent**2 + ycent**2 + zcent**2)
xcent *= rnorm
ycent *= rnorm
zcent *= rnorm

centlat = np.arcsin(zcent) * rad2deg
centlon = np.arctan2(ycent, xcent) * rad2deg

north_pole_lat = 90.0 - centlat
north_pole_lon = centlon + 180.0

# compute new lats, lons in the rotated lon-lat
rlon0 = centlon
rlat0 = centlat

#print(f"rlon0 : {rlon0:.6f}°, rlat0 : {rlat0:.6f}°")

x = np.cos(latCell_rad) * np.cos(lonCell_rad)
y = np.cos(latCell_rad) * np.sin(lonCell_rad)
z = np.sin(latCell_rad)

rlon0_rad = rlon0 * deg2rad
xt =  x * np.cos(rlon0_rad) + y * np.sin(rlon0_rad)
yt = -x * np.sin(rlon0_rad) + y * np.cos(rlon0_rad)
zt =  z

rlat0_rad = rlat0 * deg2rad
xtt =  xt * np.cos(rlat0_rad) + zt * np.sin(rlat0_rad)
ytt =  yt
ztt = -xt * np.sin(rlat0_rad) + zt * np.cos(rlat0_rad)

gcrlat = np.arcsin(ztt) * rad2deg
gcrlon = np.arctan2(ytt, xtt) * rad2deg

#####################################################
## compute analysis A-grid  lats, lons
#####################################################

# obtain analysis grid spacing
#print(f"gcrlat : min = {np.min(gcrlat):.6f}°, max = {np.max(gcrlat):.6f}°")
#print(f"gcrlon : min = {np.min(gcrlon):.6f}°, max = {np.max(gcrlon):.6f}°")
ny = np.ceil((np.max(gcrlat) - np.min(gcrlat)) / dlat) + 1
nx = np.ceil((np.max(gcrlon) - np.min(gcrlon)) / dlon) + 1

adlat = dlat * grid_ratio_regional
adlon = dlon * grid_ratio_regional

# rotated domain should be slightly larger than the orginal one, so add 10 grids on each boundary 
ny = int(np.ceil(ny/grid_ratio_regional)+10)
nx = int(np.ceil(nx/grid_ratio_regional)+10)

# setup analysis A-grid; find center of the domain
nlonh = nx // 2
nlath = ny // 2

if nx % 2 == 0:
    clon = adlon / 2.0
    cx = 0.5
else:
    clon = adlon
    cx = 1.0

if ny % 2 == 0:
    clat = adlat / 2.0
    cy = 0.5
else:
    clat = adlat
    cy = 1.0

# setup analysis A-grid from center of the domain
j_idx = np.arange(1, nx + 1)
i_idx = np.arange(1, ny + 1)

J, I = np.meshgrid(j_idx, i_idx)


lon_rotated = (J - nlonh) * adlon - clon
lat_rotated = (I - nlath) * adlat - clat

print(f"export GSIBEC_NLAT={ny}")
print(f"export GSIBEC_NLON={nx}")
print(f"export GSIBEC_NORTH_POLE_LAT={north_pole_lat:.6f}")
print(f"export GSIBEC_NORTH_POLE_LON={north_pole_lon:.6f}")
print(f"export GSIBEC_LON_START={np.min(lon_rotated):.6f}")
print(f"export GSIBEC_LON_END={np.max(lon_rotated):.6f}")
print(f"export GSIBEC_LAT_START={np.min(lat_rotated):.6f}")
print(f"export GSIBEC_LAT_END={np.max(lat_rotated):.6f}")
export GSIBEC_NLAT=179
export GSIBEC_NLON=288
export GSIBEC_NORTH_POLE_LAT=48.554305
export GSIBEC_NORTH_POLE_LON=82.649802
export GSIBEC_LON_START=-33.005000
export GSIBEC_LON_END=33.005000
export GSIBEC_LAT_START=-20.470000
export GSIBEC_LAT_END=20.470000

calculate the earth relative lon/lat from rotated lon/lat#

def unrotate2deg(rlon_out,rlat_out,north_pole_lon,north_pole_lat):
    rlon0 = north_pole_lon - 180.0;
    rlat0 = 90.0 - north_pole_lat;
    xtt = np.cos(rlat_out * deg2rad) * np.cos(rlon_out * deg2rad)
    ytt = np.cos(rlat_out * deg2rad) * np.sin(rlon_out * deg2rad)
    ztt = np.sin(rlat_out * deg2rad)

    xt = xtt * np.cos(rlat0 * deg2rad) - ztt * np.sin(rlat0 * deg2rad)
    yt = ytt
    zt = xtt * np.sin(rlat0 * deg2rad) + ztt * np.cos(rlat0 * deg2rad)

    x = xt * np.cos(rlon0 * deg2rad) - yt * np.sin(rlon0 * deg2rad)
    y = xt * np.sin(rlon0 * deg2rad) + yt * np.cos(rlon0 * deg2rad)
    z = zt

    rlat_in = np.asin(z) * rad2deg
    rlon_in = np.atan2(y,x) * rad2deg
    return rlat_in,rlon_in

plot the domain coverage, red: the original domain, blue: the rotated domain (i.e. GSIBEC recursive filter domain)#

# --- Plot the domain coverage, before and after rotate ---
fig = plt.figure(figsize=(10, 6))
ax = plt.axes(projection=ccrs.PlateCarree())
ax.set_title("MPAS Domain Coverage [ R:orig, B:rotated ]", fontsize=14)

# Add geographic features
ax.coastlines()
ax.add_feature(cfeature.BORDERS, linewidth=0.5)
ax.add_feature(cfeature.STATES, linewidth=0.5)  # U.S. state borders
ax.gridlines(draw_labels=True)

# scatter full grid points, rotated
rlat_in,rlon_in = unrotate2deg(lon_rotated,lat_rotated,north_pole_lon,north_pole_lat)
ax.scatter(rlon_in, rlat_in, s=5, color='blue', alpha=0.3, transform=ccrs.PlateCarree())

# scatter full grid points, original
ax.scatter(lonCell_deg, latCell_deg, s=5, color='red', alpha=0.3, transform=ccrs.PlateCarree())

plt.tight_layout()
plt.show()
../_images/2d95a4b79ed4446557c85ed1bdc2af6d0f569a63e6e77bcd089297e1ddbfab10.png