Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ocean data regridding to Gaussian grid functionality #8

Merged
merged 39 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7b7353f
Add ocean data regridding to Gaussian grid functionality
danielabdi-noaa Dec 15, 2023
0c75403
Apply suggestions from code review
danielabdi-noaa Dec 16, 2023
204670e
Update regrid/regrid.py
danielabdi-noaa Dec 16, 2023
e022469
Add option to read output grid from file.
danielabdi-noaa Dec 16, 2023
cbe48cd
Address some of reviewer suggestions.
danielabdi-noaa Dec 16, 2023
25c366d
Bug fix in wind field interpolation?
danielabdi-noaa Dec 17, 2023
f158f54
Integrate regridder to ufs2arco, still need to fix storing issue.
danielabdi-noaa Dec 17, 2023
807f3ea
Apply suggestions from code review
danielabdi-noaa Dec 18, 2023
a503b77
Minor fixes
danielabdi-noaa Dec 18, 2023
7fbecc6
Make periodic a config option.
danielabdi-noaa Dec 18, 2023
8f9b99c
Regrid all vars that have lat/lon coordinates.
danielabdi-noaa Dec 18, 2023
48f34a4
Fix for absent time coordinate.
danielabdi-noaa Dec 18, 2023
d972e47
Stat working on CICE6 regridding, also refactor.
danielabdi-noaa Dec 19, 2023
d0df16a
Update notebooks.
danielabdi-noaa Dec 19, 2023
c18f95b
add regrid objects to API Reference documentation
timothyas Dec 19, 2023
e758761
Merge pull request #1 from timothyas/feature/regrid-api
danielabdi-noaa Dec 20, 2023
48f2c40
Refactor regrid code.
danielabdi-noaa Dec 20, 2023
0adaced
Update notebooks.
danielabdi-noaa Dec 20, 2023
e1943e6
Apply suggestions from code review
danielabdi-noaa Dec 20, 2023
c99b190
Fix doc.
danielabdi-noaa Dec 20, 2023
ea52e43
Put regridding code under its own folder.
danielabdi-noaa Dec 20, 2023
7602748
Apply suggestions from code review
danielabdi-noaa Dec 20, 2023
26725df
Delete old files.
danielabdi-noaa Dec 20, 2023
842dee8
Apply suggestions from code review
danielabdi-noaa Dec 21, 2023
59a9999
Minor fixes.
danielabdi-noaa Dec 21, 2023
e6a828d
look for each weight file individually, not just t2t
timothyas Dec 21, 2023
507bcf2
rewrite helper function so that it does not update variables outside …
timothyas Dec 21, 2023
012415e
rather than overwrite the chunks, lets just specify them here
timothyas Dec 21, 2023
9deb256
name the default weights files with model component
timothyas Dec 21, 2023
928e2e4
revert CICE6 example to exclude regridding in order to show coordinat…
timothyas Dec 21, 2023
92333e3
need to have defaults for rg_ut, rg_vt
timothyas Dec 21, 2023
65d1409
this nc file does not exist in the repo
timothyas Dec 21, 2023
c57bd50
do not overwrite the chunks, instead rename dim names in config
timothyas Dec 21, 2023
b12d008
soft suggestion: rename classes as regridder
timothyas Dec 21, 2023
e514991
small detail for docs
timothyas Dec 21, 2023
f422e99
Merge pull request #2 from timothyas/feature/regrid-suggestions
danielabdi-noaa Dec 21, 2023
a6166b1
Update ufs2arco/regrid/ufsregridder.py
danielabdi-noaa Dec 27, 2023
ddbc3e7
Apply suggestions from code review
danielabdi-noaa Dec 27, 2023
ece3772
Merge branch 'main' into feature/regrid_ocean
timothyas Dec 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@ API Reference
#############


UFS Datasets
------------

.. autosummary::
:toctree: generated/

ufs2arco.FV3Dataset
ufs2arco.MOM6Dataset
ufs2arco.CICE6Dataset


Regridding
----------

.. autosummary::
:toctree: generated/

ufs2arco.RegridMOM6
ufs2arco.RegridCICE6
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
napoleon_custom_sections = [("Returns", "params_style"),
("Sets Attributes", "params_style"),
("Assumptions", "notes_style"),
("Required Fields in Config", "params_style"),
("Optional Fields in Config", "params_style"),
]


Expand Down
13 changes: 13 additions & 0 deletions docs/config-replay.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,16 @@ CICE6Dataset:
- hi
- Tsfc
- aice

RegridMOM6:
rotation_file: ocn_rotation_mx100.nc
weights_file_t2t:
weights_file_u2t:
weights_file_v2t:
periodic: True

RegridCICE6:
rotation_file:
weights_file_t2t:
weights_file_u2t:
periodic: True
15,375 changes: 13,692 additions & 1,683 deletions docs/example_replay_cice6.ipynb

Large diffs are not rendered by default.

3,703 changes: 3,257 additions & 446 deletions docs/example_replay_mom6.ipynb

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions environment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ channels:
dependencies:
- python=3.11
# Basics
- scipy
- numpy
- matplotlib
# xarray et al
Expand All @@ -18,6 +19,11 @@ dependencies:
- jupyter
- ipython
- ipykernel
# plotting
- cartopy
- shapely
# regrid
- xesmf
# Testing
- pytest
- coverage
Expand Down
2 changes: 2 additions & 0 deletions ufs2arco/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
from .cice6dataset import CICE6Dataset
from .fv3dataset import FV3Dataset
from .mom6dataset import MOM6Dataset
from .regrid_mom6 import RegridMOM6
from .regrid_cice6 import RegridCICE6
59 changes: 59 additions & 0 deletions ufs2arco/gaussian_grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Tools for working with Gaussian grids."""
from __future__ import absolute_import, division, print_function

import numpy as np
import numpy.linalg as la
from numpy.polynomial.legendre import legcompanion, legder, legval

def gaussian_latitudes(n):
"""Construct latitudes and latitude bounds for a Gaussian grid.

Args:
n (int): The Gaussian grid number (half the number of latitudes in the grid.
Returns:
latitudes (numpy.array): length ``n`` array of latitudes in degrees
bounds2d (numpy.array): ``(n, 2)`` array of grid bounds
"""
if abs(int(n)) != n:
raise ValueError("n must be a non-negative integer")
nlat = 2 * n
# Create the coefficients of the Legendre polynomial and construct the
# companion matrix:
cs = np.array([0] * nlat + [1], dtype=int)
cm = legcompanion(cs)
# Compute the eigenvalues of the companion matrix (the roots of the
# Legendre polynomial) taking advantage of the fact that the matrix is
# symmetric:
roots = la.eigvalsh(cm)
roots.sort()
# Improve the roots by one application of Newton's method, using the
# solved root as the initial guess:
fx = legval(roots, cs)
fpx = legval(roots, legder(cs))
roots -= fx / fpx
# The roots should exhibit symmetry, but with a sign change, so make sure
# this is the case:
roots = (roots - roots[::-1]) / 2.0
# Compute the Gaussian weights for each interval:
fm = legval(roots, cs[1:])
fm /= np.abs(fm).max()
fpx /= np.abs(fpx).max()
weights = 1.0 / (fm * fpx)
# Weights should be symmetric and sum to two (unit weighting over the
# interval [-1, 1]):
weights = (weights + weights[::-1]) / 2.0
weights *= 2.0 / weights.sum()
# Calculate the bounds from the weights, still on the interval [-1, 1]:
bounds1d = np.empty([nlat + 1])
bounds1d[0] = -1
bounds1d[1:-1] = -1 + weights[:-1].cumsum()
bounds1d[-1] = 1
# Convert the bounds to degrees of latitude on [-90, 90]:
bounds1d = np.rad2deg(np.arcsin(bounds1d))
bounds2d = np.empty([nlat, 2])
bounds2d[:, 0] = bounds1d[:-1]
bounds2d[:, 1] = bounds1d[1:]
# Convert the roots from the interval [-1, 1] to latitude values on the
# interval [-90, 90] degrees:
latitudes = np.rad2deg(np.arcsin(roots))
return latitudes, bounds2d
2 changes: 2 additions & 0 deletions ufs2arco/regrid/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .regrid_mom6 import RegridMOM6
from .regrid_cice6 import RegridCICE6
59 changes: 59 additions & 0 deletions ufs2arco/regrid/gaussian_grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Tools for working with Gaussian grids."""
from __future__ import absolute_import, division, print_function

import numpy as np
import numpy.linalg as la
from numpy.polynomial.legendre import legcompanion, legder, legval

def gaussian_latitudes(n):
"""Construct latitudes and latitude bounds for a Gaussian grid.

Args:
n (int): The Gaussian grid number (half the number of latitudes in the grid.
Returns:
latitudes (numpy.array): length ``n`` array of latitudes in degrees
bounds2d (numpy.array): ``(n, 2)`` array of grid bounds
"""
if abs(int(n)) != n:
raise ValueError("n must be a non-negative integer")
nlat = 2 * n
# Create the coefficients of the Legendre polynomial and construct the
# companion matrix:
cs = np.array([0] * nlat + [1], dtype=int)
cm = legcompanion(cs)
# Compute the eigenvalues of the companion matrix (the roots of the
# Legendre polynomial) taking advantage of the fact that the matrix is
# symmetric:
roots = la.eigvalsh(cm)
roots.sort()
# Improve the roots by one application of Newton's method, using the
# solved root as the initial guess:
fx = legval(roots, cs)
fpx = legval(roots, legder(cs))
roots -= fx / fpx
# The roots should exhibit symmetry, but with a sign change, so make sure
# this is the case:
roots = (roots - roots[::-1]) / 2.0
# Compute the Gaussian weights for each interval:
fm = legval(roots, cs[1:])
fm /= np.abs(fm).max()
fpx /= np.abs(fpx).max()
weights = 1.0 / (fm * fpx)
# Weights should be symmetric and sum to two (unit weighting over the
# interval [-1, 1]):
weights = (weights + weights[::-1]) / 2.0
weights *= 2.0 / weights.sum()
# Calculate the bounds from the weights, still on the interval [-1, 1]:
bounds1d = np.empty([nlat + 1])
bounds1d[0] = -1
bounds1d[1:-1] = -1 + weights[:-1].cumsum()
bounds1d[-1] = 1
# Convert the bounds to degrees of latitude on [-90, 90]:
bounds1d = np.rad2deg(np.arcsin(bounds1d))
bounds2d = np.empty([nlat, 2])
bounds2d[:, 0] = bounds1d[:-1]
bounds2d[:, 1] = bounds1d[1:]
# Convert the roots from the interval [-1, 1] to latitude values on the
# interval [-90, 90] degrees:
latitudes = np.rad2deg(np.arcsin(roots))
return latitudes, bounds2d
149 changes: 149 additions & 0 deletions ufs2arco/regrid/regrid_cice6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import os
import yaml
import warnings
import xarray as xr
import numpy as np

from .regrid_ufs import RegridUFS

try:
import xesmf as xe
except ImportError:
pass


class RegridCICE6(RegridUFS):
"""
Regrid cice dataset that is on a tripolar grid to a different grid (primarily Gaussian grid).


Optional fields in config:
rotation_file (str): path to file containing rotation fields "sin_rot" and "cos_rot", required for regridding vector fields
weights_file_t2t (str): path to t2t interpolation weights file
weights_file_u2t (str): path to u2t interpolation weights file
periodic (bool): Is the grid periodic in longitude?
"""

__doc__ = __doc__ + RegridUFS.__doc__

def __init__(
self,
lats1d_out: np.array,
lons1d_out: np.array,
ds_in: xr.Dataset,
config_filename: str,
interp_method: str = "bilinear",
) -> None:
super(RegridCICE6, self).__init__(
lats1d_out, lons1d_out, ds_in, config_filename, interp_method
)

def _create_regridder(self, ds_in: xr.Dataset) -> None:

# create rotation dataset
self.rotation_file = self.config.get("rotation_file", None)
self.ds_rot = None
if self.rotation_file is not None:
ds_rot = xr.open_dataset(self.rotation_file)
ds_rot = ds_rot[["cos_rot", "sin_rot"]]
self.ds_rot = ds_rot.rename({"xh": "lon", "yh": "lat"})
elif "ANGLE" in ds_in:
self.ds_rot = xr.Dataset()
self.ds_rot["cos_rot"] = np.cos(ds_in["ANGLE"])
self.ds_rot["sin_rot"] = np.sin(ds_in["ANGLE"])
else:
warnings.warn(
f"RegridMOM6.__init__: Could not find 'rotation_file' in configuration yaml. "
f"Vector fields will be silently ignored."
)

# create input dataset with t-/u- coordinates
ds_in_t = xr.Dataset()
ds_in_t["lon"] = ds_in["TLON"]
ds_in_t["lat"] = ds_in["TLAT"]
ds_in_u = xr.Dataset()
ds_in_u["lon"] = ds_in["ULON"]
ds_in_u["lat"] = ds_in["ULAT"]

# create output dataset
lons, lats = np.meshgrid(self.lons1d_out, self.lats1d_out)
grid_out = xr.Dataset()
grid_out["lon"] = xr.DataArray(lons, dims=["lat", "lon"])
grid_out["lat"] = xr.DataArray(lats, dims=["lat", "lon"])

# get nlon/nlat for input/output datsets
nlon_i = ds_in.sizes["ni"]
nlat_i = ds_in.sizes["nj"]
nlon_o = len(self.lons1d_out)
nlat_o = len(self.lats1d_out)
self.ires = f"{nlon_i}x{nlat_i}"
self.ores = f"{nlon_o}x{nlat_o}"

# paths to interpolation weights files
if "weights_file_t2t" in self.config.keys():
weights_file_t2t = self.config["weights_file_t2t"]
weights_file_u2t = self.config["weights_file_u2t"]
if weights_file_t2t is None:
weights_file_t2t = (
f"weights-{self.ires}.Ct.{self.ores}.Ct.{self.interp_method}.nc"
)
weights_file_u2t = (
f"weights-{self.ires}.Cu.{self.ires}.Ct.{self.interp_method}.nc"
)

# create regridding instances
periodic = self.config["periodic"]
reuse = os.path.exists(weights_file_t2t)
self.rg_tt = xe.Regridder(
ds_in_t,
grid_out,
self.interp_method,
periodic=periodic,
reuse_weights=reuse,
filename=weights_file_t2t,
)
reuse = os.path.exists(weights_file_u2t)
self.rg_ut = xe.Regridder(
ds_in_u,
ds_in_t,
self.interp_method,
periodic=periodic,
reuse_weights=reuse,
filename=weights_file_u2t,
)
danielabdi-noaa marked this conversation as resolved.
Show resolved Hide resolved

def regrid(self, ds_in: xr.Dataset) -> xr.Dataset:
ds_out = []

# CICE6 dataset specific variable and coordinate names
coords_xy = {"nj", "ni"}
variable_map = {
"uvel": ("vvel", "U"),
"vvel": (None, "skip"),
"strairx": ("strairy", "U"),
"strairy": (None, "skip"),
"strocnx": ("strocny", "U"),
"strocny": (None, "skip"),
"uvel_d": ("vvel_d", "U"),
"vvel_d": (None, "skip"),
"strairx_d": ("strairy_d", "U"),
"strairy_d": (None, "skip"),
"strocnx_d": ("strocny_d", "U"),
"strocny_d": (None, "skip"),
"uvel_h": ("vvel_h", "U"),
"vvel_h": (None, "skip"),
"strairx_h": ("strairy_h", "U"),
"strairy_h": (None, "skip"),
"strocnx_h": ("strocny_h", "U"),
"strocny_h": (None, "skip"),
}

return super(RegridCICE6, self).regrid_tripolar(
ds_in,
self.ds_rot,
self.rg_tt,
self.rg_ut,
self.rg_ut,
coords_xy,
variable_map,
)
Loading
Loading