From f9f40e23117ee9bbe54921cd614b7bdb2c160560 Mon Sep 17 00:00:00 2001 From: James Varndell Date: Mon, 7 Oct 2024 18:02:14 +0100 Subject: [PATCH 1/6] Adds support for non-lat-lon GRIB data points --- src/earthkit/plots/geo/grids.py | 129 +++++++++++++++++-------- src/earthkit/plots/sources/earthkit.py | 14 ++- 2 files changed, 99 insertions(+), 44 deletions(-) diff --git a/src/earthkit/plots/geo/grids.py b/src/earthkit/plots/geo/grids.py index 1db379d..644d067 100644 --- a/src/earthkit/plots/geo/grids.py +++ b/src/earthkit/plots/geo/grids.py @@ -21,59 +21,106 @@ _NO_SCIPY = True -def is_structured(lat, lon, tol=1e-5): +def is_structured(x, y, tol=1e-5): """ - 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). - - Returns: - - True if the data is structured (grid), False if it's unstructured. + 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. """ - lat = np.asarray(lat) - lon = np.asarray(lon) + x = np.asarray(x) + y = np.asarray(y) - # Check if there are consistent spacing in latitudes and longitudes - unique_lat = np.unique(lat) - unique_lon = np.unique(lon) + # 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) + + 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) - # 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) + return x_spacing_consistent and y_spacing_consistent - # 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) + # 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 + + 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 x_rows_consistent and y_columns_consistent - return lat_spacing_consistent and lon_spacing_consistent + else: + # Invalid input, dimensions of x and y must match (either both 1D or both 2D) + return False - # If the product of unique lat/lon values doesn't match total points, it's unstructured - 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( diff --git a/src/earthkit/plots/sources/earthkit.py b/src/earthkit/plots/sources/earthkit.py index 913f11b..b4001ce 100644 --- a/src/earthkit/plots/sources/earthkit.py +++ b/src/earthkit/plots/sources/earthkit.py @@ -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 as e: + 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): From fbb7b9e054a4d081976f587ed4a5935f6a8af8cf Mon Sep 17 00:00:00 2001 From: James Varndell Date: Mon, 7 Oct 2024 18:02:57 +0100 Subject: [PATCH 2/6] Re-enable automatic legend titles with proper fallback if no metadata found --- src/earthkit/plots/components/subplots.py | 2 +- src/earthkit/plots/styles/legends.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/earthkit/plots/components/subplots.py b/src/earthkit/plots/components/subplots.py index 376f2ae..397d7dc 100644 --- a/src/earthkit/plots/components/subplots.py +++ b/src/earthkit/plots/components/subplots.py @@ -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): diff --git a/src/earthkit/plots/styles/legends.py b/src/earthkit/plots/styles/legends.py index e5a0106..19bef4a 100644 --- a/src/earthkit/plots/styles/legends.py +++ b/src/earthkit/plots/styles/legends.py @@ -13,7 +13,7 @@ # limitations under the License. -DEFAULT_LEGEND_LABEL = "" # {variable_name} ({units})" +DEFAULT_LEGEND_LABEL = "{variable_name} ({units})" _DISJOINT_LEGEND_LOCATIONS = { "bottom": { @@ -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}") From f09de82e11559e9ba407d0f92c2d4731f8476f0b Mon Sep 17 00:00:00 2001 From: James Varndell Date: Mon, 7 Oct 2024 18:04:31 +0100 Subject: [PATCH 3/6] QA tweaks --- src/earthkit/plots/geo/grids.py | 33 ++++++++++++++------------ src/earthkit/plots/sources/earthkit.py | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/earthkit/plots/geo/grids.py b/src/earthkit/plots/geo/grids.py index 644d067..85f0ef7 100644 --- a/src/earthkit/plots/geo/grids.py +++ b/src/earthkit/plots/geo/grids.py @@ -33,19 +33,19 @@ def is_structured(x, y, tol=1e-5): Parameters ---------- x : array_like - A 1D or 2D array of x-coordinates. For example, this can be longitude or + 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 + 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 + 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 + True if the data represents a structured grid, i.e., the spacing between consecutive points in both x and y is consistent. False otherwise. """ @@ -57,11 +57,11 @@ def is_structured(x, y, tol=1e-5): # 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) - + 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) @@ -71,10 +71,14 @@ def is_structured(x, y, tol=1e-5): 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 - - 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) - + + 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 x_rows_consistent and y_columns_consistent else: @@ -82,14 +86,13 @@ def is_structured(x, y, tol=1e-5): return False - def interpolate_unstructured(x, y, z, resolution=1000, method="linear"): """ 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 + 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 @@ -106,7 +109,7 @@ def interpolate_unstructured(x, y, z, resolution=1000, method="linear"): 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. @@ -118,7 +121,7 @@ def interpolate_unstructured(x, y, z, resolution=1000, method="linear"): 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 + 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). """ diff --git a/src/earthkit/plots/sources/earthkit.py b/src/earthkit/plots/sources/earthkit.py index b4001ce..e63916e 100644 --- a/src/earthkit/plots/sources/earthkit.py +++ b/src/earthkit/plots/sources/earthkit.py @@ -137,7 +137,7 @@ def extract_xy(self): points = self.data.to_points(flatten=False) x = points["x"] y = points["y"] - except ValueError as e: + except ValueError: latlon = self.data.to_latlon(flatten=False) lat = latlon["lat"] lon = latlon["lon"] From 693969e9313dbb2af4da5b4b01400e0cd950006c Mon Sep 17 00:00:00 2001 From: James Varndell Date: Mon, 7 Oct 2024 19:00:46 +0100 Subject: [PATCH 4/6] Rollback version of micromamba used with unit tests to v12 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 376c4b9..cbb27d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ 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/provision-with-micromamba@v12 with: environment-file: environment.yml environment-name: DEVELOP From 771b0470f1a957a4f5b21726250780413a2d458a Mon Sep 17 00:00:00 2001 From: James Varndell Date: Mon, 7 Oct 2024 19:03:29 +0100 Subject: [PATCH 5/6] Testing latest micromamba --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbb27d9..3402388 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ 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/provision-with-micromamba@v16 with: environment-file: environment.yml environment-name: DEVELOP @@ -140,7 +140,7 @@ 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/provision-with-micromamba@v16 with: environment-file: environment${{ matrix.extra }}.yml environment-name: DEVELOP${{ matrix.extra }} From e2953768b40f191a31bcd4b2a34db96bbb17417a Mon Sep 17 00:00:00 2001 From: James Varndell Date: Mon, 7 Oct 2024 19:09:41 +0100 Subject: [PATCH 6/6] Migrate to new setup-micromamba --- .github/workflows/ci.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3402388..e649de2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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@v16 + 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: | @@ -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@v16 + 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: |