Skip to content

Commit

Permalink
12 support non latlon grib grids (#16)
Browse files Browse the repository at this point in the history
* Adds support for non-lat-lon GRIB data points

* Re-enable automatic legend titles with proper fallback if no metadata found

* QA tweaks

* Rollback version of micromamba used with unit tests to v12

* Testing latest micromamba

* Migrate to new setup-micromamba
  • Loading branch information
JamesVarndell authored Oct 7, 2024
1 parent 9b33c73 commit 7873725
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 55 deletions.
16 changes: 7 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,12 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Install Conda environment with Micromamba
uses: mamba-org/provision-with-micromamba@v14
uses: mamba-org/setup-micromamba@v1
with:
environment-file: environment.yml
environment-name: DEVELOP
channels: conda-forge
cache-env: true
extra-specs: |
cache-environment: true
create-args: >-
python=3.10
- name: Install package
run: |
Expand Down Expand Up @@ -140,14 +139,13 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Install Conda environment with Micromamba
uses: mamba-org/provision-with-micromamba@v12
uses: mamba-org/setup-micromamba@v1
with:
environment-file: environment${{ matrix.extra }}.yml
environment-name: DEVELOP${{ matrix.extra }}
channels: conda-forge
cache-env: true
cache-env-key: ubuntu-latest-${{ matrix.python-version }}${{ matrix.extra }}.
extra-specs: |
cache-environment: true
cache-environment-key: ubuntu-latest-${{ matrix.python-version }}${{ matrix.extra }}.
create-args: >-
python=${{matrix.python-version }}
- name: Install package
run: |
Expand Down
2 changes: 1 addition & 1 deletion src/earthkit/plots/components/subplots.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,7 @@ def legend(self, style=None, location=None, **kwargs):
dummy = [[1, 2], [3, 4]]
mappable = self.contourf(x=dummy, y=dummy, z=dummy, style=style)
layer = Layer(single.SingleSource(), mappable, self, style)
legend = layer.style.legend(layer, label=kwargs.pop("label", ""), **kwargs)
legend = layer.style.legend(layer, **kwargs)
legends.append(legend)
else:
for i, layer in enumerate(self.distinct_legend_layers):
Expand Down
130 changes: 90 additions & 40 deletions src/earthkit/plots/geo/grids.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,59 +21,109 @@
_NO_SCIPY = True


def is_structured(lat, lon, tol=1e-5):
def is_structured(x, y, tol=1e-5):
"""
Determines whether the x and y points form a structured grid.
This function checks if the x and y coordinate arrays represent a structured
grid, i.e., a grid with consistent spacing between points. The function supports
1D arrays (representing coordinates of a grid) and 2D arrays (representing the
actual grid coordinates) of x and y.
Parameters
----------
x : array_like
A 1D or 2D array of x-coordinates. For example, this can be longitude or
the x-coordinate in a Cartesian grid.
y : array_like
A 1D or 2D array of y-coordinates. For example, this can be latitude or
the y-coordinate in a Cartesian grid.
tol : float, optional
Tolerance for floating-point comparison to account for numerical precision
errors when checking spacing consistency. The default is 1e-5.
Returns
-------
bool
True if the data represents a structured grid, i.e., the spacing between
consecutive points in both x and y is consistent. False otherwise.
"""
Determines whether the latitude and longitude points form a structured grid.

Parameters:
- lat: A 1D or 2D array of latitude points.
- lon: A 1D or 2D array of longitude points.
- tol: Tolerance for floating-point comparison (default 1e-5).
x = np.asarray(x)
y = np.asarray(y)

Returns:
- True if the data is structured (grid), False if it's unstructured.
"""
# If both x and y are 1D arrays, ensure they can form a grid
if x.ndim == 1 and y.ndim == 1:
# Check if the number of points match (can form a meshgrid)
if len(x) * len(y) != x.size * y.size:
return False

# Check consistent spacing in x and y
x_diff = np.diff(x)
y_diff = np.diff(y)

lat = np.asarray(lat)
lon = np.asarray(lon)
x_spacing_consistent = np.all(np.abs(x_diff - x_diff[0]) < tol)
y_spacing_consistent = np.all(np.abs(y_diff - y_diff[0]) < tol)

# Check if there are consistent spacing in latitudes and longitudes
unique_lat = np.unique(lat)
unique_lon = np.unique(lon)
return x_spacing_consistent and y_spacing_consistent

# Structured grid condition: the number of unique lat/lon values should multiply to the number of total points
if len(unique_lat) * len(unique_lon) == len(lat) * len(lon):
# Now check if the spacing is consistent
lat_diff = np.diff(unique_lat)
lon_diff = np.diff(unique_lon)
# If x and y are 2D arrays, verify they are structured as a grid
elif x.ndim == 2 and y.ndim == 2:
# Check if rows of x and y have consistent spacing along the grid lines
# x should vary only along one axis, y along the other axis

# Check if lat/lon differences are consistent
lat_spacing_consistent = np.all(np.abs(lat_diff - lat_diff[0]) < tol)
lon_spacing_consistent = np.all(np.abs(lon_diff - lon_diff[0]) < tol)
x_rows_consistent = np.all(
np.abs(np.diff(x, axis=1) - np.diff(x, axis=1)[:, 0:1]) < tol
)
y_columns_consistent = np.all(
np.abs(np.diff(y, axis=0) - np.diff(y, axis=0)[0:1, :]) < tol
)

return lat_spacing_consistent and lon_spacing_consistent
return x_rows_consistent and y_columns_consistent

# If the product of unique lat/lon values doesn't match total points, it's unstructured
return False
else:
# Invalid input, dimensions of x and y must match (either both 1D or both 2D)
return False


def interpolate_unstructured(x, y, z, resolution=1000, method="linear"):
"""
Interpolates unstructured data to a structured grid, handling NaNs in z-values
and preventing interpolation across large gaps.
Parameters:
- x: 1D array of x-coordinates.
- y: 1D array of y-coordinates.
- z: 1D array of z values.
- resolution: The number of points along each axis for the structured grid.
- method: Interpolation method ('linear', 'nearest', 'cubic').
- gap_threshold: The distance threshold beyond which interpolation is not performed (set to NaN).
Returns:
- grid_x: 2D grid of x-coordinates.
- grid_y: 2D grid of y-coordinates.
- grid_z: 2D grid of interpolated z-values, with NaNs in large gap regions.
Interpolate unstructured data to a structured grid.
This function takes unstructured (scattered) data points and interpolates them
to a structured grid, handling NaN values in `z` and providing options for
different interpolation methods. It creates a regular grid based on the given
resolution and interpolates the z-values from the unstructured points onto this grid.
Parameters
----------
x : array_like
1D array of x-coordinates.
y : array_like
1D array of y-coordinates.
z : array_like
1D array of z-values at each (x, y) point.
resolution : int, optional
The number of points along each axis for the structured grid.
Default is 1000.
method : {'linear', 'nearest', 'cubic'}, optional
The interpolation method to use. Default is 'linear'.
The methods supported are:
- 'linear': Linear interpolation between points.
- 'nearest': Nearest-neighbor interpolation.
- 'cubic': Cubic interpolation, which may produce smoother results.
Returns
-------
grid_x : ndarray
2D array representing the x-coordinates of the structured grid.
grid_y : ndarray
2D array representing the y-coordinates of the structured grid.
grid_z : ndarray
2D array of interpolated z-values at the grid points. NaNs may be
present in regions where interpolation was not possible (e.g., due to
large gaps in the data).
"""
if _NO_SCIPY:
raise ImportError(
Expand Down
14 changes: 11 additions & 3 deletions src/earthkit/plots/sources/earthkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,17 @@ def extract_xy(self):
)
points = get_points(1)
else:
points = self.data.to_points(flatten=False)
x = points["x"]
y = points["y"]
try:
points = self.data.to_points(flatten=False)
x = points["x"]
y = points["y"]
except ValueError:
latlon = self.data.to_latlon(flatten=False)
lat = latlon["lat"]
lon = latlon["lon"]
transformed = self.crs.transform_points(ccrs.PlateCarree(), lon, lat)
x = transformed[:, :, 0]
y = transformed[:, :, 1]
return x, y

def extract_x(self):
Expand Down
7 changes: 5 additions & 2 deletions src/earthkit/plots/styles/legends.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.


DEFAULT_LEGEND_LABEL = "" # {variable_name} ({units})"
DEFAULT_LEGEND_LABEL = "{variable_name} ({units})"

_DISJOINT_LEGEND_LOCATIONS = {
"bottom": {
Expand Down Expand Up @@ -55,7 +55,10 @@ def colorbar(layer, *args, shrink=0.8, aspect=35, ax=None, **kwargs):
Any keyword arguments accepted by `matplotlib.figures.Figure.colorbar`.
"""
label = kwargs.pop("label", DEFAULT_LEGEND_LABEL)
label = layer.format_string(label)
try:
label = layer.format_string(label)
except (AttributeError, ValueError, KeyError):
label = ""

kwargs = {**layer.style._legend_kwargs, **kwargs}
kwargs.setdefault("format", lambda x, _: f"{x:g}")
Expand Down

0 comments on commit 7873725

Please sign in to comment.