diff --git a/HISTORY.rst b/HISTORY.rst index 07b4060..e8c2a1e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,7 @@ History 0.x.x (xxxx-xx-xx) ------------------ +* Add ``--cyclic`` option to regrid cli and services. (PR #108, @brews) * Add ``papermill``, ``intake-esm`` to Docker environment. (PR #106, @brews) diff --git a/dodola/cli.py b/dodola/cli.py index 39a6fb4..06c236c 100644 --- a/dodola/cli.py +++ b/dodola/cli.py @@ -233,7 +233,10 @@ def rechunk(x, chunk, out): help="Local path to existing regrid weights file", ) @click.option("--astype", "-t", default=None, help="Type to recast output to") -def regrid(x, out, method, domain_file, weightspath, astype): +@click.option( + "--cyclic", default=None, help="Add wrap-around values to dim before regridding" +) +def regrid(x, out, method, domain_file, weightspath, astype, cyclic): """Regrid a target climate dataset Note, the weightspath only accepts paths to NetCDF files on the local disk. See @@ -248,6 +251,7 @@ def regrid(x, out, method, domain_file, weightspath, astype): domain_file=domain_file, weights_path=weightspath, astype=astype, + add_cyclic=cyclic, ) diff --git a/dodola/core.py b/dodola/core.py index 684670d..c0a3df8 100644 --- a/dodola/core.py +++ b/dodola/core.py @@ -249,8 +249,22 @@ def build_xesmf_weights_file(x, domain, method, filename=None): return str(out.filename) -def xesmf_regrid(x, domain, method, weights_path=None, astype=None): +def _add_cyclic(ds, dim): """ + Adds wrap-around, appending first value to end of data for named dimension. + + Basically an xarray version of ``cartopy.util.add_cyclic_point()``. + """ + return ds.map( + lambda x, d: xr.concat([x, x.isel({d: 0})], dim=d), + keep_attrs=True, + d=str(dim), + ) + + +def xesmf_regrid(x, domain, method, weights_path=None, astype=None, add_cyclic=None): + """ + Regrid a Dataset. Parameters ---------- @@ -263,11 +277,18 @@ def xesmf_regrid(x, domain, method, weights_path=None, astype=None): Local path to netCDF file of pre-calculated XESMF regridding weights. astype : str, numpy.dtype, or None, optional Typecode or data-type to which the regridded output is cast. + add_cyclic : str, or None, optional + Add cyclic point (aka wrap-around pixel) to given dimension before + regridding. Useful for avoiding dateline artifacts along longitude + in global datasets. Returns ------- xr.Dataset """ + if add_cyclic: + x = _add_cyclic(x, add_cyclic) + regridder = xe.Regridder( x, domain, diff --git a/dodola/services.py b/dodola/services.py index 97959b6..7240e92 100644 --- a/dodola/services.py +++ b/dodola/services.py @@ -262,7 +262,9 @@ def rechunk(x, target_chunks, out): @log_service -def regrid(x, out, method, domain_file, weights_path=None, astype=None): +def regrid( + x, out, method, domain_file, weights_path=None, astype=None, add_cyclic=None +): """Regrid climate data Parameters @@ -279,9 +281,12 @@ def regrid(x, out, method, domain_file, weights_path=None, astype=None): Local file path name to write regridding weights file to. astype : str, numpy.dtype, or None, optional Typecode or data-type to which the regridded output is cast. + add_cyclic : str, or None, optional + Add cyclic (aka wrap-around values) to dimension before regridding. + Useful for avoiding dateline artifacts along longitude in global + datasets. """ ds = storage.read(x) - ds_domain = storage.read(domain_file) regridded_ds = xesmf_regrid( @@ -290,6 +295,7 @@ def regrid(x, out, method, domain_file, weights_path=None, astype=None): method=method, weights_path=weights_path, astype=astype, + add_cyclic=add_cyclic, ) storage.write(out, regridded_ds) diff --git a/dodola/tests/test_core.py b/dodola/tests/test_core.py index 2870597..eabe89d 100644 --- a/dodola/tests/test_core.py +++ b/dodola/tests/test_core.py @@ -5,6 +5,7 @@ from dodola.core import ( train_quantiledeltamapping, adjust_quantiledeltamapping_year, + _add_cyclic, ) @@ -181,3 +182,16 @@ def test_adjust_quantiledeltamapping_year_output_time(): assert max(adjusted_ds["time"].values) == cftime.DatetimeNoLeap( 2088, 12, 31, 0, 0, 0, 0 ) + + +def test_add_cyclic(): + """Test _add_cyclic adds wraparound values""" + in_da = xr.DataArray( + np.ones([5, 6]) * np.arange(6), + coords=[np.arange(5) + 1, np.arange(6) + 1], + dims=["lat", "lon"], + ) + out_ds = _add_cyclic(ds=in_da.to_dataset(name="fakevariable"), dim="lon") + assert all( + out_ds["fakevariable"].isel(lon=0) == out_ds["fakevariable"].isel(lon=-1) + )