|
| 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