Skip to content
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

12 support non latlon grib grids #16

Merged
merged 6 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading