Skip to content

Commit 0a3044f

Browse files
authored
Add CDO-based regridding (#5)
Add a Regridder class that can interpolate Xarray data onto a new grid
1 parent e94fd6c commit 0a3044f

15 files changed

+966
-3
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*.swp
2+
*.egg-info
3+
4+
/doc/_build
5+
.pytest_cache
6+
.cache
7+
__pycache__

conda/dev-environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies:
99
- python
1010
- pytest
1111
- sphinx
12+
- cdo
1213
- mule
1314
- cfunits
1415
- dask

conda/meta.yaml

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@ requirements:
1414
- python
1515
run:
1616
- python
17+
- cdo
1718
- cfunits
19+
- dask
1820
- mule
21+
- netcdf4
22+
- numpy
23+
- scipy
24+
- sparse
1925
- xarray
20-
- dask
2126

2227
test:
2328
requires:

doc/api/grid.rst

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Grids
2+
=====
3+
4+
Grid objects are used to hold layout information used by regridders. They can
5+
output the grid in various formats, e.g. so they can be read by CDO.
6+
7+
.. autofunction:: coecms.grid.identify_grid
8+
9+
.. autoclass:: coecms.grid.Grid
10+
:members:
11+
12+
.. autoclass:: coecms.grid.LonLatGrid
13+
:members:

doc/api/regrid.rst

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Regridding
2+
==========
3+
4+
The coecms regridder makes use of CDO to generate its regridding weights.
5+
6+
Effectively behind the scenes it runs::
7+
8+
cdo genbil,gridspec sourcegrid.nc weights.nc
9+
cdo remap,gridspec,weights.nc source.nc dest.nc
10+
11+
with some added Dask magic.
12+
13+
There are two ways to run the regridder. You can create a
14+
:class:`coecms.regrid.Regridder`, which stores the weights for re-use, or you
15+
can call :func:`coecms.regrid.regrid` to quickly regrid a single dataset.
16+
17+
.. autoclass:: coecms.regrid.Regridder
18+
:members:
19+
20+
.. autofunction:: coecms.regrid.regrid

doc/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
'sphinx.ext.autodoc',
4545
'sphinx.ext.doctest',
4646
'sphinx.ext.coverage',
47+
'sphinx.ext.napoleon',
4748
]
4849

4950
# Add any paths that contain templates here, relative to this directory.

doc/index.rst

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Welcome to CLEX CMS Utility Library's documentation!
1010
:maxdepth: 2
1111
:caption: Contents:
1212

13+
api/grid.rst
14+
api/regrid.rst
1315
api/dimension.rst
1416

1517
api/um.rst

notebooks/regridding.ipynb

+211
Large diffs are not rendered by default.

setup.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212

1313
install_requires = [
1414
'cfunits',
15-
'mule',
16-
'xarray',
1715
'dask[array]',
16+
'mule',
1817
'netCDF4',
18+
'numpy',
19+
'pytest',
20+
'scipy',
21+
'sparse',
22+
'xarray',
1923
],
2024
entry_points = {
2125
'console_scripts': [

src/coecms/dimension.py

+19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,25 @@
1616
from __future__ import print_function
1717

1818
from cfunits import Units
19+
import numpy
20+
21+
22+
def remove_degenerate_axes(coord):
23+
"""
24+
Remove any degenerate axes from the coordinate, where all the values along a dimension are identical
25+
26+
Args:
27+
coord (xarray.DataArray): Co-ordinate to operate on
28+
29+
Returns:
30+
xarray.DataArray with degenerate axes removed
31+
"""
32+
33+
for d in coord.dims:
34+
if numpy.allclose(coord.isel({d: 0}), coord.mean(dim=d)):
35+
coord = coord.mean(dim=d)
36+
37+
return coord
1938

2039

2140
def identify_lat_lon(dataarray):

src/coecms/grid.py

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/usr/bin/env python
2+
# Copyright 2018 ARC Centre of Excellence for Climate Extremes
3+
# author: Scott Wales <[email protected]>
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
from __future__ import print_function
17+
from abc import ABCMeta, abstractmethod
18+
19+
import six
20+
import xarray
21+
import numpy
22+
23+
"""
24+
Different grid types
25+
"""
26+
27+
28+
def identify_grid(dataset):
29+
"""
30+
Identify the grid used by a Dataset. Returns the appropriate :class:`Grid`
31+
object
32+
33+
Args:
34+
dataset (xarray.DataArray): Input dataset
35+
36+
Returns:
37+
Grid: Grid for that dataset
38+
"""
39+
40+
if isinstance(dataset, Grid):
41+
return dataset
42+
43+
try:
44+
if dataset.attrs['conventions'] == 'SCRIP':
45+
return ScripGrid(dataset)
46+
except KeyError:
47+
pass
48+
except AttributeError:
49+
pass
50+
51+
try:
52+
if dataset.lon.ndim == 1 and dataset.lat.ndim == 1:
53+
return LonLatGrid(lons=dataset.lon, lats=dataset.lat)
54+
except AttributeError:
55+
pass
56+
57+
raise NotImplementedError
58+
59+
60+
@six.add_metaclass(ABCMeta)
61+
class Grid(object):
62+
"""Abstract base class for grids"""
63+
64+
@abstractmethod
65+
def to_cdo_grid(self, outfile):
66+
"""
67+
Write the grid to a format readable by CDO's regridder (either text or
68+
SCRIP format)
69+
70+
Args:
71+
outfile: File-like object to write to
72+
"""
73+
74+
@abstractmethod
75+
def to_netcdf(self, outfile):
76+
"""
77+
Create a netCDF file using the grid
78+
79+
Args:
80+
outfile: Path or File-like object to write to
81+
82+
Note that if `outfile` is a file object it will be closed
83+
automatically.
84+
"""
85+
86+
def to_scrip(self, outfile):
87+
"""
88+
Create a SCRIP file using the grid
89+
90+
Args:
91+
outfile: Path or File-like object to write to
92+
93+
Note that if `outfile` is a file object it will be closed
94+
automatically.
95+
"""
96+
raise NotImplementedError
97+
98+
99+
class LonLatGrid(Grid):
100+
"""
101+
A cartesian grid, with lats and lons one dimensional arrays
102+
"""
103+
104+
def __init__(self, lats, lons):
105+
"""
106+
Args:
107+
lats (numpy.array): Grid latitudes
108+
lons (numpy.array): Grid longitude
109+
"""
110+
111+
self.lats = lats
112+
self.lons = lons
113+
114+
if self.lats.ndim != 1 or self.lons.ndim != 1:
115+
raise Exception("Lons and Lats must be 1D")
116+
117+
def to_cdo_grid(self, outfile):
118+
outfile.write('gridtype = lonlat\n'.encode())
119+
120+
outfile.write(('xsize = %d\n' % len(self.lons)).encode())
121+
outfile.write(('xvals = %s\n' % (','.join(['%f' % x for x in self.lons]))).encode())
122+
123+
outfile.write(('ysize = %d\n' % len(self.lats)).encode())
124+
outfile.write(('yvals = %s\n' % (','.join(['%f' % x for x in self.lats]))).encode())
125+
126+
outfile.flush()
127+
128+
def to_netcdf(self, outfile):
129+
ds = xarray.DataArray(data=numpy.zeros((len(self.lats), len(self.lons))),
130+
coords=[('lat', self.lats), ('lon', self.lons)])
131+
ds.lat.attrs['units'] = 'degrees_north'
132+
ds.lon.attrs['units'] = 'degrees_east'
133+
ds.to_netcdf(outfile)
134+
135+
def to_scrip(self, outfile):
136+
lat = self.lats
137+
lon = self.lons % 360
138+
139+
top = (lat.shift(lat=-1) + lat) / 2.0
140+
top[-1] = 90
141+
142+
bot = (lat.shift(lat=1) + lat) / 2.0
143+
bot[0] = -90
144+
145+
left = ((lon - (lon - lon.roll(lon=1).values) % 360) / 2.0) % 360
146+
right = (lon + ((lon.roll(lon=-1).values - lon) % 360) / 2.0) % 360
147+
148+
center_lon, center_lat = numpy.meshgrid(lon, lat)
149+
150+
corner_lon0, corner_lat0 = numpy.meshgrid(left, top)
151+
corner_lon1, corner_lat1 = numpy.meshgrid(left, bot)
152+
corner_lon2, corner_lat2 = numpy.meshgrid(right, bot)
153+
corner_lon3, corner_lat3 = numpy.meshgrid(right, top)
154+
155+
corner_lat = numpy.array([x.reshape(-1) for x in [corner_lat0, corner_lat1, corner_lat2, corner_lat3]])
156+
corner_lon = numpy.array([x.reshape(-1) for x in [corner_lon0, corner_lon1, corner_lon2, corner_lon3]])
157+
158+
scrip = xarray.Dataset(
159+
coords={
160+
'grid_dims': (['grid_rank'], [lon.size, lat.size]),
161+
'grid_center_lat': (['grid_size'], center_lat.reshape(-1)),
162+
'grid_center_lon': (['grid_size'], center_lon.reshape(-1)),
163+
'grid_imask': (['grid_size'], numpy.ones(center_lat.size)),
164+
'grid_corner_lat': (['grid_size', 'grid_corners'], corner_lat.T),
165+
'grid_corner_lon': (['grid_size', 'grid_corners'], corner_lon.T),
166+
})
167+
168+
scrip.grid_center_lat.attrs['units'] = 'degrees_north'
169+
scrip.grid_center_lon.attrs['units'] = 'degrees_east'
170+
scrip.grid_corner_lat.attrs['units'] = 'degrees_north'
171+
scrip.grid_corner_lon.attrs['units'] = 'degrees_east'
172+
173+
scrip.to_netcdf(outfile)
174+
175+
176+
class ScripGrid(Grid):
177+
def __init__(self, grid):
178+
self._grid = grid
179+
180+
def to_cdo_grid(self, outfile):
181+
self._grid.to_netcdf(outfile)
182+
183+
def to_netcdf(self, outfile):
184+
self._grid.to_netcdf(outfile)
185+
186+
def to_scrip(self, outfile):
187+
self._grid.to_netcdf(outfile)

0 commit comments

Comments
 (0)