Source code for grid_process.grid_interp_utils

# Copyright (c) 2026 MeteoSwiss, contributors listed in AUTHORS
#
# Distributed under the terms of the BSD 3-Clause License.
#
# SPDX-License-Identifier: BSD-3-Clause

"""
Module with utilities for spatial interpolation of gridded datasets
"""

from typing import Any

import numpy as np
from pyproj import CRS
import xarray as xr

from grid_process.helpers import reproject
from grid_process import data_model as dm


Y_DIM, X_DIM = dm.YX[0]
LAT, LON = dm.LATLON[0]


[docs] def create_dst_grid( # pylint: disable=too-many-locals dst_grid_crs: Any, bottomleft: tuple[float, float], topright: tuple[float, float] | None = None, nrowcol: tuple[int, int] | None = None, resolution: float | None = None, add_latlon: bool = False, dtype: np.dtype = np.dtype("float32"), ) -> xr.Dataset: """ Create a destination grid on which data can be interpolated. The output can be directly used with :func:`interp2grid`. Two out of the three optional input arguments ``topright``, ``nrowcol``, ``resolution`` must be provided. Parameters ---------- dst_grid_crs : string, int, dict or a CRS object Anything accepted by pyproj.crs.CRS.from_user_input: https://pyproj4.github.io/pyproj/dev/api/crs/crs.html#pyproj.crs.CRS.from_user_input (e.g. EPSG:4326) bottomleft : tuple of floats (y, x) coordinates of the center of the bottom left corner pixel topright : tuple of floats, optional (y, x) coordinates of the center of the top right corner pixel. If provided with ``nrowcol``, this is the exact coordinates. If provided with ``resolution`` this is a closed upper bound. The exact coordinates of the top right corner pixel will then be a multiple of the resolution with respect to the bottom left corner. nrowcol : tuple of int, optional number of rows (y) and columns (x) respectively resolution : float, optional resolution of the destination grid in the units of the ``grid_mapping`` add_latlon : bool If True, add latitude and longitude 2d arrays as coordinates dtype : numpy.dtype The dtype in which spatial coordinates are returned Returns ------- dst_grid : xarray.Dataset dataset with spatial horizontal coordinates and an associated crs attribute See also -------- :func:`interp2grid` """ start_y, start_x = bottomleft if topright is not None and nrowcol is not None: end_y, end_x = topright nrows, ncols = nrowcol y_coords = np.linspace(start_y, end_y, nrows, dtype=dtype) x_coords = np.linspace(start_x, end_x, ncols, dtype=dtype) elif topright is not None and resolution is not None: end_y, end_x = topright y_coords = np.linspace(start_y, end_y, num=int((end_y - start_y) / resolution) + 1, dtype=dtype) x_coords = np.linspace(start_x, end_x, num=int((end_x - start_x) / resolution) + 1, dtype=dtype) elif nrowcol is not None and resolution is not None: nrows, ncols = nrowcol end_y = start_y + (nrows - 1) * resolution end_x = start_x + (ncols - 1) * resolution y_coords = np.linspace(start_y, end_y, nrows, dtype=dtype) x_coords = np.linspace(start_x, end_x, ncols, dtype=dtype) else: raise ValueError("At least two of 'topright', 'nrowcol', 'resolution' arguments must be provided") dst_grid = xr.Dataset( coords={X_DIM: x_coords, Y_DIM: y_coords}, attrs={"crs": dst_grid_crs}, ) if add_latlon: x_2d, y_2d = np.meshgrid(x_coords, y_coords) dst_crs = CRS.from_user_input(dst_grid_crs) longitude, latitude = reproject(x_2d, y_2d, dst_crs, "EPSG:4326") dst_grid = dst_grid.assign_coords( { LON: ((Y_DIM, X_DIM), longitude.astype(dtype, copy=False)), LAT: ((Y_DIM, X_DIM), latitude.astype(dtype, copy=False)), } ) return dst_grid
[docs] def create_dst_grid_from_src( # pylint: disable=too-many-locals src_grid: xr.Dataset, dst_grid_crs: Any, add_latlon: bool = False, ) -> xr.Dataset: """ Creates a destination grid on which data can be interpolated from a source grid in a different Coordinate Reference System. Domain and sizes are preserved. The output can be directly used with :func:`interp2grid`. Parameters ---------- src_grid : xarray Dataset The source grid from which a new destination grid with same sizes and domain is derived. Supported data models include: - CF-convention grids (see https://cfconventions.org/) - internal MCH models - Unstructured grids Typical spatial dimension and coordinate names are supported, and can be extended via environment variables: - Y coordinate: export GRID_PROCESS__data_model__grid__y (e.g., '["y", "latitude", "lat", "dummy_y"]') - X coordinate: export GRID_PROCESS__data_model__grid__x (e.g., '["x", "longitude", "lon", "dummy_x"]') - Unstructured dimension: export GRID_PROCESS__data_model__grid__unstructured_dim (e.g., '["dummy_cell"]') - When the spatial coordinates variables have a CF compliant `standard_name` attribute, arbitrary names can be used directly. Spatial CRS must be referenced via one of: - Global attribute 'crs' (e.g., "EPSG:2056" or anything accepted by :meth:`pyproj.CRS.from_user_input`) - Global attribute 'grid_mapping' (internal grid database convention) - CF-convention grid mapping variable - If coordinates are named 'latitude'/'longitude' or 'lat'/'lon', WGS84 (EPSG:4326) is assumed. dst_grid_crs : string, int, dict or a CRS object Anything accepted by pyproj.crs.CRS.from_user_input: https://pyproj4.github.io/pyproj/dev/api/crs/crs.html#pyproj.crs.CRS.from_user_input (e.g. EPSG:4326) add_latlon : bool If True, add latitude and longitude 2d arrays as coordinates Returns ------- dst_grid : xarray.Dataset dataset with spatial horizontal coordinates and an associated crs attribute See also -------- :func:`interp2grid` """ spatial_dims = dm.get_spatial_dims(src_grid) src_grid = dm.set_grid_mapping(src_grid) src_y, src_x, src_crs = dm.get_coords_name_crs(src_grid, spatial_dims) dst_crs = CRS.from_user_input(dst_grid_crs) x_min, x_max = src_grid[src_x].values.min(), src_grid[src_x].values.max() y_min, y_max = src_grid[src_y].values.min(), src_grid[src_y].values.max() x_bnds = (x_min, x_min, x_max, x_max) y_bnds = (y_min, y_max, y_min, y_max) x_bnds, y_bnds = reproject(x_bnds, y_bnds, src_crs, dst_crs) x_bnds, y_bnds = (min(x_bnds), max(x_bnds)), (min(y_bnds), max(y_bnds)) if len(spatial_dims) == 1: # need to infer expected sizes for x and y dims xy_ratio = (x_bnds[1] - x_bnds[0]) / (y_bnds[1] - y_bnds[0]) dst_x_size = round(np.sqrt(src_grid.sizes[spatial_dims[0]] * xy_ratio)) dst_y_size = round(np.sqrt(src_grid.sizes[spatial_dims[0]] / xy_ratio)) bool_decrease = False else: dst_x_size, dst_y_size = ( src_grid.sizes[spatial_dims[1]], src_grid.sizes[spatial_dims[0]], ) bool_decrease = dm.is_decreasing(src_grid[src_y], spatial_dims[0]) src_x_dtype, src_y_dtype = src_grid[src_x].dtype, src_grid[src_y].dtype dst_x_dtype = src_x_dtype if src_x_dtype == np.float64 else np.float32 dst_y_dtype = src_y_dtype if src_y_dtype == np.float64 else np.float32 dst_x_coords = np.linspace(x_bnds[0], x_bnds[1], dst_x_size, dtype=dst_x_dtype) dst_y_coords = np.linspace(y_bnds[0], y_bnds[1], dst_y_size, dtype=dst_y_dtype) if bool_decrease: dst_y_coords = dst_y_coords[::-1] dst_grid = xr.Dataset( coords={X_DIM: dst_x_coords, Y_DIM: dst_y_coords}, attrs={"crs": dst_grid_crs}, ) if add_latlon: x_2d, y_2d = np.meshgrid(dst_x_coords, dst_y_coords) longitude, latitude = reproject(x_2d, y_2d, dst_crs, "EPSG:4326") dst_grid = dst_grid.assign_coords( { LON: ((Y_DIM, X_DIM), longitude.astype(dst_x_dtype, copy=False)), LAT: ((Y_DIM, X_DIM), latitude.astype(dst_y_dtype, copy=False)), } ) return dst_grid