-
Notifications
You must be signed in to change notification settings - Fork 73
Add function to project 2D gridded data #246
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
Changes from all commits
7d81603
f85dc88
6b29a53
07f690d
cdca126
c28201a
53ebb71
b806320
761c127
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| """ | ||
| Projection of gridded data | ||
| ========================== | ||
|
|
||
| Sometimes gridded data products need to be projected before they can be used. | ||
| For example, you might want to project the topography of Antarctica from | ||
| geographic latitude and longitude to a planar polar stereographic projection | ||
| before doing your analysis. When projecting, the data points will likely not | ||
| fall on a regular grid anymore and must be interpolated (re-sampled) onto a new | ||
| grid. | ||
|
|
||
| The :func:`verde.project_grid` function automates this process using the | ||
| interpolation methods available in Verde. An input grid | ||
| (:class:`xarray.DataArray`) is interpolated onto a new grid in the given | ||
| `pyproj <https://jswhit.github.io/pyproj/>`__ projection. The function takes | ||
| care of choosing a default grid spacing and region, running a blocked mean to | ||
| avoid spatial aliasing (using :class:`~verde.BlockReduce`), and masking the | ||
| points in the new grid that aren't constrained by the original data (using | ||
| :func:`~verde.convexhull_mask`). | ||
|
|
||
| In this example, we'll generate a synthetic geographic grid with a checkerboard | ||
| pattern around the South pole. We'll project the grid to South Polar | ||
| Stereographic, revealing the checkered pattern of the data. | ||
|
|
||
| .. note:: | ||
|
|
||
| The interpolation methods are limited to what is available in Verde and | ||
| there is only support for single 2D grids. For more sophisticated use | ||
| cases, you might want to try | ||
| `pyresample <https://github.com/pytroll/pyresample>`__ instead. | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a useful note with link to pyresample |
||
| """ | ||
| import numpy as np | ||
| import matplotlib.pyplot as plt | ||
| import pyproj | ||
| import verde as vd | ||
|
|
||
|
|
||
| # We'll use synthetic data near the South pole to highlight the effects of the | ||
| # projection. EPSG 3031 is a South Polar Stereographic projection. | ||
| projection = pyproj.Proj("epsg:3031") | ||
|
|
||
| # Create a synthetic geographic grid using a checkerboard pattern | ||
| region = (0, 360, -90, -60) | ||
| spacing = 0.25 | ||
| wavelength = 10 * 1e5 # The size of the cells in the checkerboard | ||
| checkerboard = vd.datasets.CheckerBoard( | ||
| region=vd.project_region(region, projection), w_east=wavelength, w_north=wavelength | ||
| ) | ||
| data = checkerboard.grid( | ||
| region=region, | ||
| spacing=spacing, | ||
| projection=projection, | ||
| data_names=["checkerboard"], | ||
| dims=("latitude", "longitude"), | ||
| ) | ||
| print("Geographic grid:") | ||
| print(data) | ||
|
|
||
| # Do the projection while setting the output grid spacing (in projected meters). Set | ||
| # the coordinates names to x and y since they aren't really "northing" or | ||
| # "easting". | ||
| polar_data = vd.project_grid( | ||
| data.checkerboard, projection, spacing=0.5 * 1e5, dims=("y", "x") | ||
| ) | ||
| print("\nProjected grid:") | ||
| print(polar_data) | ||
|
|
||
| # Plot the original and projected grids | ||
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 6)) | ||
| data.checkerboard.plot( | ||
| ax=ax1, cbar_kwargs=dict(orientation="horizontal", aspect=50, pad=0.1) | ||
| ) | ||
| ax1.set_title("Geographic Grid") | ||
| polar_data.plot(ax=ax2, cbar_kwargs=dict(orientation="horizontal", aspect=50, pad=0.1)) | ||
| ax2.set_title("Polar Stereographic Grid") | ||
| plt.tight_layout() | ||
| plt.show() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only thing that I could see differently is adding a plot of the data for creating the geographic grid earlier on in the example. But the more I think about it, the more I like it down here where I can visually compare the geographic and projected coordinates. I think it looks great the way it is.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be better in a tutorial. But since the plots in the gallery appear all at the top, it wouldn't make much difference. It took me a while to think of a way to show this function in action (we don't have any grids in Verde as sample data). |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| """ | ||
| Operations with projections for grids, regions, etc. | ||
| """ | ||
| import numpy as np | ||
|
|
||
| from .coordinates import grid_coordinates, get_region, shape_to_spacing, check_region | ||
| from .utils import grid_to_table | ||
| from .scipygridder import ScipyGridder | ||
| from .blockreduce import BlockReduce | ||
| from .chain import Chain | ||
| from .mask import convexhull_mask | ||
|
|
||
|
|
||
| def project_region(region, projection): | ||
| """ | ||
| Calculate the bounding box of a region in projected coordinates. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| region : list = [W, E, S, N] | ||
| The boundaries of a given region in Cartesian or geographic | ||
| coordinates. | ||
| projection : callable | ||
| Should be a callable object (like a function) ``projection(easting, | ||
| northing) -> (proj_easting, proj_northing)`` that takes in easting and | ||
| northing coordinate arrays and returns projected northing and easting | ||
| coordinate arrays. | ||
|
|
||
| Returns | ||
| ------- | ||
| proj_region : list = [W, E, S, N] | ||
| The bounding box of the projected region. | ||
|
|
||
| Examples | ||
| -------- | ||
|
|
||
| >>> def projection(x, y): | ||
| ... return (2*x, -1*y) | ||
| >>> project_region((3, 5, -9, -4), projection) | ||
| (6.0, 10.0, 4.0, 9.0) | ||
|
|
||
| """ | ||
| east, north = grid_coordinates(region, shape=(101, 101)) | ||
| east, north = projection(east.ravel(), north.ravel()) | ||
| return (east.min(), east.max(), north.min(), north.max()) | ||
|
|
||
|
|
||
| def project_grid(grid, projection, method="linear", antialias=True, **kwargs): | ||
| """ | ||
| Apply the given map projection to a grid and re-sample it. | ||
|
|
||
| Creates a new grid in the projected coordinates by interpolating the | ||
| original values using the chosen *method* (linear by default). Before | ||
| interpolation, apply a blocked mean operation (:class:`~verde.BlockReduce`) | ||
| to avoid aliasing when the projected coordinates become oversampled in some | ||
| regions (which would cause the interpolation to down-sample the original | ||
| data). For example, applying a polar projection results in oversampled data | ||
| close to the pole. | ||
|
|
||
| Points that fall outside the convex hull of the original data will be | ||
| masked (see :func:`~verde.convexhull_mask`) since they are not constrained | ||
| by any data points. | ||
|
|
||
| Any arguments that can be passed to the | ||
| :meth:`~verde.base.BaseGridder.grid` method of Verde gridders can be passed | ||
| to this function as well. Use this to set a region and spacing (or shape) | ||
| for the projected grid. The region and spacing must be in *projected | ||
| coordinates*. | ||
|
|
||
| If no region is provided, the bounding box of the projected data will be | ||
| used. If no spacing or shape is provided, the shape of the input *grid* | ||
| will be used for the projected grid. | ||
|
|
||
| By default, the ``data_names`` argument will be set to the name of the data | ||
| variable of the input *grid* (if it has been set). | ||
|
|
||
| .. note:: | ||
|
|
||
| The interpolation methods are limited to what is available in Verde and | ||
| there is only support for single 2D grids. For more sophisticated use | ||
| cases, you might want to try | ||
| `pyresample <https://github.com/pytroll/pyresample>`__ instead. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| grid : :class:`xarray.DataArray` | ||
| A single 2D grid of values. The first dimension is assumed to be the | ||
| northing/latitude dimension while the second is assumed to be the | ||
| easting/longitude dimension. | ||
| projection : callable | ||
| Should be a callable object (like a function) ``projection(easting, | ||
| northing) -> (proj_easting, proj_northing)`` that takes in easting and | ||
| northing coordinate arrays and returns projected northing and easting | ||
| coordinate arrays. | ||
| method : string or Verde gridder | ||
| If a string, will use it to create a :class:`~verde.ScipyGridder` with | ||
| the corresponding method (nearest, linear, or cubic). Otherwise, should | ||
| be a gridder/estimator object, like :class:`~verde.Spline`. Default is | ||
| ``"linear"``. | ||
| antialias : bool | ||
| If True, will run a :class:`~verde.BlockReduce` with a mean function to | ||
| avoid aliasing when the projection results in oversampling of the data | ||
| in some areas (for example, in polar projections). If False, will not | ||
| run the blocked mean. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is great to have antialiasing in here
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree 🙂 |
||
|
|
||
| Returns | ||
| ------- | ||
| projected_grid : :class:`xarray.DataArray` | ||
| The projected grid, interpolated with the given parameters. | ||
|
|
||
| """ | ||
| if hasattr(grid, "data_vars"): | ||
| raise ValueError( | ||
| "Projecting xarray.Dataset is not currently supported. " | ||
| "Please provide a DataArray instead." | ||
| ) | ||
| if len(grid.dims) != 2: | ||
| raise ValueError( | ||
| "Projecting grids with number of dimensions other than 2 is not " | ||
| "currently supported (dimensions of the given DataArray: {}).".format( | ||
| len(grid.dims) | ||
| ) | ||
| ) | ||
|
|
||
| # Can be set to None for some data arrays depending on how they are created | ||
| # so we can't just rely on the default value for getattr. | ||
| name = getattr(grid, "name", None) | ||
| if name is None: | ||
| name = "scalars" | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Data checks look good and print useful and meaningful error messages |
||
| data = grid_to_table(grid).dropna() | ||
| coordinates = projection(data[grid.dims[1]].values, data[grid.dims[0]].values) | ||
| data_region = get_region(coordinates) | ||
|
|
||
| region = kwargs.pop("region", data_region) | ||
| shape = kwargs.pop("shape", grid.shape) | ||
| spacing = kwargs.pop("spacing", shape_to_spacing(region, shape)) | ||
|
|
||
| check_region(region) | ||
|
|
||
| steps = [] | ||
| if antialias: | ||
| steps.append( | ||
| ("mean", BlockReduce(np.mean, spacing=spacing, region=data_region)) | ||
| ) | ||
| if isinstance(method, str): | ||
| steps.append(("spline", ScipyGridder(method))) | ||
| else: | ||
| steps.append(("spline", method)) | ||
| interpolator = Chain(steps) | ||
| interpolator.fit(coordinates, data[name]) | ||
|
|
||
| projected = interpolator.grid( | ||
| region=region, | ||
| spacing=spacing, | ||
| data_names=kwargs.pop("data_names", [name]), | ||
| **kwargs | ||
| ) | ||
| if method not in ["linear", "cubic"]: | ||
| projected = convexhull_mask(coordinates, grid=projected) | ||
| return projected[name] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great description of how it works and how the function deals with the original data. I dig it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I did mostly because I had to dig into the GMT code in order to see what they did. It's fancier than this but Verde is much easier to install :)