Skip to content

Commit 0ec5a35

Browse files
Merge pull request #13 from ecmwf/feature/unstructured
Adds basic implementation of visualising unstructured grids
2 parents b37cece + 2b59d74 commit 0ec5a35

File tree

5 files changed

+337
-3
lines changed

5 files changed

+337
-3
lines changed

docs/examples/gallery/gallery.ipynb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"* [Model orography](gridded-data/model-orography.ipynb)\n",
5454
"* [Temperature and pressure](gridded-data/temperature-and-pressure.ipynb)\n",
5555
"* [Time zones](gridded-data/time-zones.ipynb)\n",
56-
"* [Weather forecast steps](gridded-data/weather-forecast-steps.ipynb)"
56+
"* [Weather forecast steps](gridded-data/weather-forecast-steps.ipynb)\n",
57+
"* [Unstructured grids](gridded-data/unstructured-grids.ipynb)"
5758
]
5859
},
5960
{

docs/examples/gallery/gridded-data/unstructured-grids.ipynb

Lines changed: 220 additions & 0 deletions
Large diffs are not rendered by default.

src/earthkit/plots/components/subplots.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import warnings
1516
from itertools import cycle
1617

1718
import earthkit.data
@@ -21,6 +22,7 @@
2122

2223
from earthkit.plots import identifiers
2324
from earthkit.plots.components.layers import Layer
25+
from earthkit.plots.geo import grids
2426
from earthkit.plots.metadata.formatters import (
2527
LayerFormatter,
2628
SourceFormatter,
@@ -338,7 +340,8 @@ def _extract_plottables(
338340
style = auto.guess_style(
339341
source, units=units or source.units, **override_kwargs
340342
)
341-
if (data is None and z is None) or (z is not None and not z):
343+
344+
if data is None and z is None:
342345
z_values = None
343346
else:
344347
z_values = style.convert_units(source.z_values, source.units)
@@ -371,6 +374,19 @@ def _extract_plottables(
371374
x_values = source.x_values
372375
y_values = source.y_values
373376

377+
if method_name in (
378+
"contour",
379+
"contourf",
380+
"pcolormesh",
381+
) and not grids.is_structured(x_values, y_values):
382+
x_values, y_values, z_values = grids.interpolate_unstructured(
383+
x_values,
384+
y_values,
385+
z_values,
386+
method=kwargs.pop("interpolation_method", "linear"),
387+
)
388+
extract_domain = False
389+
374390
if every is not None:
375391
x_values = x_values[::every]
376392
y_values = y_values[::every]
@@ -384,6 +400,11 @@ def _extract_plottables(
384400
z_values,
385401
source_crs=source.crs,
386402
)
403+
if "interpolation_method" in kwargs:
404+
kwargs.pop("interpolation_method")
405+
warnings.warn(
406+
"The 'interpolation_method' argument is only valid for unstructured data."
407+
)
387408
mappable = getattr(style, method_name)(
388409
self.ax, x_values, y_values, z_values, **kwargs
389410
)

src/earthkit/plots/geo/grids.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright 2024, European Centre for Medium Range Weather Forecasts.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import numpy as np
16+
from scipy.interpolate import griddata
17+
18+
19+
def is_structured(lat, lon, tol=1e-5):
20+
"""
21+
Determines whether the latitude and longitude points form a structured grid.
22+
23+
Parameters:
24+
- lat: A 1D or 2D array of latitude points.
25+
- lon: A 1D or 2D array of longitude points.
26+
- tol: Tolerance for floating-point comparison (default 1e-5).
27+
28+
Returns:
29+
- True if the data is structured (grid), False if it's unstructured.
30+
"""
31+
32+
lat = np.asarray(lat)
33+
lon = np.asarray(lon)
34+
35+
# Check if there are consistent spacing in latitudes and longitudes
36+
unique_lat = np.unique(lat)
37+
unique_lon = np.unique(lon)
38+
39+
# Structured grid condition: the number of unique lat/lon values should multiply to the number of total points
40+
if len(unique_lat) * len(unique_lon) == len(lat) * len(lon):
41+
# Now check if the spacing is consistent
42+
lat_diff = np.diff(unique_lat)
43+
lon_diff = np.diff(unique_lon)
44+
45+
# Check if lat/lon differences are consistent
46+
lat_spacing_consistent = np.all(np.abs(lat_diff - lat_diff[0]) < tol)
47+
lon_spacing_consistent = np.all(np.abs(lon_diff - lon_diff[0]) < tol)
48+
49+
return lat_spacing_consistent and lon_spacing_consistent
50+
51+
# If the product of unique lat/lon values doesn't match total points, it's unstructured
52+
return False
53+
54+
55+
def interpolate_unstructured(x, y, z, resolution=1000, method="linear"):
56+
"""
57+
Interpolates unstructured data to a structured grid, handling NaNs in z-values
58+
and preventing interpolation across large gaps.
59+
60+
Parameters:
61+
- x: 1D array of x-coordinates.
62+
- y: 1D array of y-coordinates.
63+
- z: 1D array of z values.
64+
- resolution: The number of points along each axis for the structured grid.
65+
- method: Interpolation method ('linear', 'nearest', 'cubic').
66+
- gap_threshold: The distance threshold beyond which interpolation is not performed (set to NaN).
67+
68+
Returns:
69+
- grid_x: 2D grid of x-coordinates.
70+
- grid_y: 2D grid of y-coordinates.
71+
- grid_z: 2D grid of interpolated z-values, with NaNs in large gap regions.
72+
"""
73+
# Filter out NaN values from z and corresponding x, y
74+
mask = ~np.isnan(z)
75+
x_filtered = x[mask]
76+
y_filtered = y[mask]
77+
z_filtered = z[mask]
78+
79+
# Create a structured grid
80+
grid_x, grid_y = np.mgrid[
81+
x.min() : x.max() : resolution * 1j, y.min() : y.max() : resolution * 1j
82+
]
83+
84+
# Interpolate the filtered data onto the structured grid
85+
grid_z = griddata(
86+
np.column_stack((x_filtered, y_filtered)),
87+
z_filtered,
88+
(grid_x, grid_y),
89+
method=method,
90+
)
91+
92+
return grid_x, grid_y, grid_z

src/earthkit/plots/styles/legends.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515

16-
DEFAULT_LEGEND_LABEL = "{variable_name} ({units})"
16+
DEFAULT_LEGEND_LABEL = "" # {variable_name} ({units})"
1717

1818
_DISJOINT_LEGEND_LOCATIONS = {
1919
"bottom": {

0 commit comments

Comments
 (0)