Skip to content

Commit 795c4af

Browse files
authored
Merge pull request #9 from stephenworsley/regridding_first_draft
Add regridding section
2 parents ea7363c + ca50ee9 commit 795c4af

File tree

3 files changed

+2570
-1
lines changed

3 files changed

+2570
-1
lines changed
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
---
2+
jupyter:
3+
jupytext:
4+
text_representation:
5+
extension: .md
6+
format_name: markdown
7+
format_version: '1.3'
8+
jupytext_version: 1.14.4
9+
kernelspec:
10+
display_name: with_esmf_regridders
11+
language: python
12+
name: with_esmf_regridders
13+
---
14+
15+
# Section 5: Regridding
16+
17+
## Set up
18+
19+
```python
20+
from pathlib import Path
21+
import numpy as np
22+
23+
from esmf_regrid.experimental.unstructured_scheme import MeshToGridESMFRegridder, GridToMeshESMFRegridder
24+
import iris
25+
from iris import load, load_cube
26+
from iris.coords import DimCoord
27+
from iris.cube import Cube
28+
```
29+
30+
## Example: Regridding LFRic data
31+
32+
33+
Suppose we need to compare data located on two different kinds of grids. One is located on a UM style "latlon" _grid_ and one is located on an LFRic style cubed sphere UGRID _mesh_. Data can be translated from the grid to the mesh and vice versa via _regridding_. We will demonstrate with the following files:
34+
35+
```python
36+
from testdata_fetching import lfric_orography, um_orography
37+
mesh_cube = lfric_orography()
38+
mesh_cube
39+
```
40+
41+
```python
42+
grid_cube = um_orography()
43+
grid_cube
44+
```
45+
46+
Regridding unstructured data is more complex than the regridders contained in Iris and requires making use of powerful libraries (`ESMF`). The `iris-esmf-regrid` package provides a bridge from iris to esmf with objects that interact directly with iris cubes. The `MeshToGridESMFRegridder` class allows the regridding of (LFRic style) mesh cubes onto (UM style) latlon grid cubes.
47+
48+
First we initialise the regridder object with a source mesh cube and target grid cube...
49+
50+
```python
51+
# Initialise the regridder.
52+
# This object can be re-used and also saved/re-loaded.
53+
# Note: it may take a few seconds to initialise the regridder.
54+
regridder = MeshToGridESMFRegridder(mesh_cube, grid_cube)
55+
```
56+
57+
...Then we use that regridder object to translate the data onto the grid of the target cube.
58+
59+
```python
60+
# Regrid the mesh cube.
61+
result = regridder(mesh_cube)
62+
result
63+
```
64+
65+
The reason this is done in two steps is because initialising a regridder is potentially quite expensive if the grids or meshes involved are large. Once initialised, a regridder can regrid many source cubes (defined on the same source grid/mesh) onto the same target. We can demonstrate this by regridding a different cube using the same regridder.
66+
67+
```python
68+
# Load an air temperature cube.
69+
from testdata_fetching import lfric_temp
70+
mesh_temp = lfric_temp()
71+
72+
# We can check that this cube shares the same mesh.
73+
assert mesh_temp.mesh == mesh_cube.mesh
74+
75+
mesh_temp
76+
```
77+
78+
```python
79+
# Regrid the new mesh cube using the same regridder.
80+
# Note how the time coordinate is also transposed in the result.
81+
result_2 = regridder(mesh_temp)
82+
result_2
83+
```
84+
85+
We can save time in future runs by saving and loading a regridder with `save_regridder` and `load_regridder`.
86+
87+
*Note:* The information for the regridder is saved as a NetCDF file so the file name must have a `.nc` extension.
88+
89+
```python
90+
# This code is commented for the time being to avoid generating files.
91+
92+
# from esmf_regrid.experimental.io import load_regridder, save_regridder
93+
94+
# save_regridder(regridder, "lf_to_um_regridder.nc")
95+
# loaded_regridder = load_regridder("lf_to_um_regridder.nc")
96+
```
97+
98+
We can compare the regridded file to an equivalent file from the UM.
99+
100+
```python
101+
import iris.quickplot as iqplt
102+
import matplotlib.pyplot as plt
103+
104+
from testdata_fetching import um_temp
105+
grid_temp = um_temp()
106+
107+
iqplt.pcolormesh(grid_temp[0, 0])
108+
plt.gca().coastlines()
109+
plt.show()
110+
111+
iqplt.pcolormesh(result_2[0, 0])
112+
plt.gca().coastlines()
113+
plt.show()
114+
```
115+
116+
We can then plot the difference between the UM data and the data regridded from LFRic. Since our data is now on a latlon grid we can do this with matplotlib as normal.
117+
118+
```python
119+
temp_diff = result_2 - grid_temp
120+
121+
iqplt.pcolormesh(temp_diff[0, 0], vmin=-4,vmax=4, cmap="seismic")
122+
plt.gca().coastlines()
123+
plt.show()
124+
```
125+
126+
We can also regrid from latlon grids to LFRic style meshes using `GridToMeshESMFRegridder`.
127+
128+
```python
129+
# Initialise the regridder.
130+
g2m_regridder = GridToMeshESMFRegridder(grid_cube, mesh_cube)
131+
# Regrid the grid cube.
132+
result_3 = g2m_regridder(grid_cube)
133+
result_3
134+
```
135+
136+
```python
137+
# Bonus task:
138+
# Use %%timeit to investigate how much time it takes to initialise a regridder vs applying the regridder.
139+
```
140+
141+
## Exercise 1: Comparing regridding methods
142+
143+
By default, regridding uses the area weighted `conservative` method. We can also use the bilinear regridding method.
144+
145+
**Step 1:** Use the `method="bilinear"` keyword to initialise a bilinear `MeshToGridESMFRegridder` with arguments `mesh_cube` and `grid_cube`.
146+
147+
```python
148+
bilinear_regridder = MeshToGridESMFRegridder(mesh_cube, grid_cube, method="bilinear")
149+
```
150+
151+
**Step 2:** Use this regridder to regrid `mesh_cube`.
152+
153+
```python
154+
bilinear_result = bilinear_regridder(mesh_cube)
155+
```
156+
157+
**Step 3:** Compare this result with the result from the default area weighted conservative regridder.
158+
159+
```python
160+
bilinear_diff = bilinear_result - result
161+
```
162+
163+
**Step 4:** Plot the results and the difference using `iris.quickplot` and `matplotlib`.
164+
165+
```python
166+
import iris.quickplot as iqplt
167+
import matplotlib.pyplot as plt
168+
169+
iqplt.pcolormesh(result, cmap="terrain")
170+
plt.show()
171+
iqplt.pcolormesh(bilinear_result, cmap="terrain")
172+
plt.show()
173+
iqplt.pcolormesh(bilinear_diff, vmin=-400,vmax=400, cmap="seismic")
174+
plt.show()
175+
```
176+
177+
```python
178+
# Bonus Exercises:
179+
# - calculate the difference between methods for the GridToMeshESMFRegridder.
180+
# - calculate the difference between raw data and data which has been round tripped.
181+
# (e.g. regrid from mesh to grid then from grid to mesh)
182+
# - demonstrate that the data in the grid file was probably a result of regridding from the mesh file.
183+
```
184+
185+
## Exercise 2: Zonal means
186+
187+
For a latlon cube, a common operation is to collapse over longitude by taking an average. This is not possible for an LFRic style mesh cube since there is no independent longitude dimension to collapse. While it is possible to regrid to a latlon cube and then collapse, this introduces an additional step to the process. Instead, it is possible to simplify this into a single step by considering this as a regridding operation where the target cube contains multiple latitude bands.
188+
189+
A zonal mean is the area weighted average over a defined region or sequence of regions. e.g. a band of latitude/longitude.
190+
Calculating zonal means can be done as a regridding operation where the zone is defined by the target cube. This can involve a target cube with a single cell or, as in this example, a number of cells along the latitude dimension.
191+
192+
**Step 1:** Define a latitude coordinate whose bounds are `[[-90, -60], [-60, -30], [-30, 0], [0, 30], [30, 60], [60, 90]]`. Remember to set the standard name to be `"latitude"` and the units to be `"degrees"`
193+
194+
```python
195+
lat_bands = DimCoord(
196+
[-75, -45, -15, 15, 45, 75],
197+
bounds=[[-90, -60], [-60, -30], [-30, 0], [0, 30], [30, 60], [60, 90]],
198+
standard_name="latitude",
199+
units="degrees"
200+
)
201+
```
202+
203+
**Step 2:** Define a longitude coordinate whose bounds are `[[-180, 180]]`. Remember to set the standard name to be `"longitude"` and the units to be `"degrees"`
204+
205+
```python
206+
lon_full = DimCoord(0, bounds=[[-180, 180]], standard_name="longitude", units="degrees")
207+
```
208+
209+
**Step 3:** Create a single celled cube (i.e. `Cube([[0]])`) and attach the latitude and longitude coordinates to it.
210+
211+
```python
212+
lat_band_cube = Cube(np.zeros((1,) + lat_bands.shape))
213+
lat_band_cube.add_dim_coord(lat_bands, 1)
214+
lat_band_cube.add_dim_coord(lon_full, 0)
215+
lat_band_cube
216+
```
217+
218+
**Step 4:** Create a regridder from `mesh_cube` to the single celled cube you created.
219+
220+
*Note:* ESMF represents all lines as sections of great circles rather than lines of constant latitude. This means that `MeshToGridESMFRegridder` would fail to properly handle such a large cell. We can solve this problem by using the `resolution` keyword. By providing a `resolution`, we divide each cell into as many sub-cells each bounded by the same latitude bounds.
221+
222+
If we initialise a regridder with `MeshToGridESMFRegridder(src_mesh, tgt_grid, resolution=10)`, then the lines of latitude bounding each of the cells in `tgt_grid` will be *approximated* by 10 great circle sections.
223+
224+
Initialise a `MeshToGridESMFRegridder` with `mesh_cube` and your single celled cube as its arguments and with a `resolution=10` keyword.
225+
226+
```python
227+
lat_band_mean_calculator_10 = MeshToGridESMFRegridder(mesh_cube, lat_band_cube, resolution=10)
228+
```
229+
230+
**Step 5:** Apply this regridder to `mesh_cube` and print the data from this result (i.e. `print(result_cube.data)`).
231+
232+
```python
233+
lat_band_mean_10 = lat_band_mean_calculator_10(mesh_cube)
234+
print(lat_band_mean_10.data)
235+
iqplt.pcolormesh(lat_band_mean_10)
236+
plt.gca().coastlines()
237+
plt.show()
238+
```
239+
240+
**Step 6:** Repeat step 4 and 5 for `resolution=100`.
241+
242+
Note the difference in value. Also note that it takes more time to initialise a regridder with higher resolution.
243+
244+
```python
245+
lat_band_mean_calculator_100 = MeshToGridESMFRegridder(mesh_cube, lat_band_cube, resolution=100)
246+
lat_band_mean_100 = lat_band_mean_calculator_100(mesh_cube)
247+
print(lat_band_mean_100.data)
248+
249+
iqplt.pcolormesh(lat_band_mean_100)
250+
plt.gca().coastlines()
251+
plt.show()
252+
```
253+
254+
**Step 7:** Repeat steps 1 - 6 for latitude bounds `[[-90, 90]]`, longitude bounds `[[-40, 40]]` and resolutions 2 and 10.
255+
256+
*Note:* Unlike lines of constant latitude, lines of constant longitude are already great circle arcs.This might suggest that the `resolution` argument is unnnecessary, however these arcs are 180 degrees which ESMF is unable to represent so we still need a `resolution` of at least 2. In this case, an increase in resolution will not affect the accuracy since a resolution of 2 will already have maximum accuracy. Note how the results are the equal.
257+
258+
```python
259+
lat_full = DimCoord(0, bounds=[[-90, 90]], standard_name="latitude", units="degrees")
260+
lon_band = DimCoord(0, bounds=[[-40, 40]], standard_name="longitude", units="degrees")
261+
262+
lon_band_cube = Cube([[0]])
263+
lon_band_cube.add_dim_coord(lat_full, 0)
264+
lon_band_cube.add_dim_coord(lon_band, 1)
265+
266+
lon_band_mean_calculator_2 = MeshToGridESMFRegridder(mesh_cube, lon_band_cube, resolution=2)
267+
lon_band_mean_2 = lon_band_mean_calculator_2(mesh_cube)
268+
print(lon_band_mean_2.data)
269+
270+
lon_band_mean_calculator_10 = MeshToGridESMFRegridder(mesh_cube, lon_band_cube, resolution=10)
271+
lon_band_mean_10 = lon_band_mean_calculator_10(mesh_cube)
272+
print(lon_band_mean_10.data)
273+
```
274+
275+
## Exercise 3: Hovmoller plots
276+
277+
If we have data on aditional dimensions, we can use the same approach as exercise 2 to produce a Hovmoller diagram. That is, if we have data that varies along time we can take the area weighted mean over latitude bands and plot the data aginst longitude and time (or similarly, we can plot against latitude and time).
278+
279+
**Step 1:** Load a temperature cube using the `testdata_fetching` function `lfric_temp`. Extract a single pressure slice using the `cube.extract` method with a constraint `iris.Constraint(pressure=850)` as the argument (we choose this level because it has noticable details).
280+
281+
282+
from testdata_fetching import fric_temp
283+
mesh_temp = lfric_temp()
284+
285+
temp_slice = mesh_temp.extract(iris.Constraint(pressure=850))
286+
temp_slice
287+
288+
289+
**Step 2:** Create a target cube whose longitude coordinate is derived from the UM cube loaded from `um_orography` and whose latitude coordinate has bounds `[[-180, 180]]`. This can be done by slicing a cube derived from `um_orography` (using the slice `[:1]` so that this dimension isnt collapsed), removing the latitude coordinate and adding a latitude coordinate with bounds `[[-180, 180]]` (you can reuse the coordinate from exercise 2).
290+
291+
```python
292+
target_cube_lons = grid_cube[:1]
293+
target_cube_lons.remove_coord("latitude")
294+
target_cube_lons.add_dim_coord(lat_full, 0)
295+
target_cube_lons
296+
```
297+
298+
```python
299+
# We also can do the same thing for bands of constant latitude.
300+
301+
# target_cube_lats = grid_cube[:,:1]
302+
# target_cube_lats.remove_coord("longitude")
303+
# target_cube_lats.add_dim_coord(lon_full, 1)
304+
# target_cube_lats
305+
```
306+
307+
**Step 3:** Create a `MeshToGridESMFRegridder` regridder from the slice of the temperature cube onto the target cube. Set the resolution keyword to 2 (this should be sufficient since these are bands of constant longitude). Use this regridder to create a resulting cube.
308+
309+
```python
310+
um_lon_band_mean_calculator = MeshToGridESMFRegridder(temp_slice, target_cube_lons, resolution=2)
311+
um_lon_bound_means = um_lon_band_mean_calculator(temp_slice)
312+
um_lon_bound_means
313+
```
314+
315+
```python
316+
# um_lat_band_mean_calculator = MeshToGridESMFRegridder(temp_slice, target_cube, resolution=500)
317+
# um_lat_bound_means = um_lat_band_mean_calculator(temp_slice)
318+
# um_lat_bound_means
319+
```
320+
321+
**Step 4:** Plot the data in the resulting cube. This can be done with `iqplt.pcolormesh`. Note that the resulting cube will have an unnecessary dimension which will have to be sliced (using `[:, 0]`). Note that extra steps can be taken to format the dates for this plot.
322+
323+
```python
324+
import matplotlib.dates as mdates
325+
326+
iqplt.pcolormesh(um_lon_bound_means[:, 0, :])
327+
plt.gca().yaxis.set_major_formatter(mdates.DateFormatter("%D"))
328+
plt.show()
329+
```
330+
331+
```python
332+
# import matplotlib.dates as mdates
333+
# iqplt.pcolormesh(um_lat_bound_means[:, :, 0])
334+
# plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%D"))
335+
# plt.show()
336+
```
337+
338+
```python
339+
# Bonus Exercise:
340+
341+
# Create a regridder onto a single celled cube which represents the whole earth.
342+
# Use this regridder to compare how well bilinear regridding and area weighted
343+
# regridding preserve area weighted mean after round tripping.
344+
```
345+
346+
```python
347+
348+
```

0 commit comments

Comments
 (0)