Source code for pycif.plugins.transforms.basic.regrid.utils.conservative.weights

from __future__ import annotations

import warnings
from functools import wraps

import numpy as np
import pandas as pd
from numpy.typing import NDArray
from scipy.sparse import coo_array

from .......utils.geometry.equal_area_proj import WorldEqualArea, optimize_proj_bins
from .......utils.geometry.polygons import vertices_to_coords
from .......utils.geometry.utils import standardize_lon
from .......utils.check.errclass import CifValueError

try:
    imported_njit = True  # pylint: disable=invalid-name
    from numba import njit
except ImportError:
    imported_njit = False  # pylint: disable=invalid-name

    def njit(func):
        return wraps(func)


@njit
def _get_sparse_coords_kernel(
    n_out: int,
    ind_out: NDArray[np.signedinteger],
) -> NDArray[np.signedinteger]:
    n = ind_out.shape[0]

    coords = np.zeros((2, n), dtype=np.int64)
    counts = np.zeros(n_out, dtype=np.int64)

    for i in range(n):
        coords[0, i] = ind_out[i]
        coords[1, i] = counts[ind_out[i]]
        counts[ind_out[i]] += 1

    return coords


def _extract_indices_weights_numba(
    n_cells: int,
    intersection: pd.DataFrame,
) -> tuple[NDArray[np.signedinteger], NDArray[np.floating]]:
    n_out = n_cells
    n_in = intersection.i_out.value_counts().max()

    i_out = intersection.i_out.to_numpy()
    i_in = intersection.i_in.to_numpy()
    weight = intersection.weight.to_numpy()

    coords = _get_sparse_coords_kernel(n_out, i_out)

    indices = coo_array((i_in, coords), shape=(n_out, n_in), dtype=np.int64)
    weights = coo_array((weight, coords), shape=(n_out, n_in), dtype=np.float64)

    return indices.toarray(), weights.toarray()  # type: ignore


def _extract_indices_weights_python(
    n_cells: int,
    intersection: pd.DataFrame,
) -> tuple[NDArray[np.signedinteger], NDArray[np.floating]]:
    n_out = n_cells
    n_in = intersection.i_out.value_counts().max()

    indices = np.full((n_out, n_in), -1, dtype=int)
    weights = np.full((n_out, n_in), np.nan, dtype=float)

    def fill_data(df: pd.DataFrame) -> None:
        i_out = df.i_out.iat[0]
        n_in = df.shape[0]
        indices[i_out, :n_in] = df.i_in.to_numpy()
        weights[i_out, :n_in] = df.weight.to_numpy()

    intersection.groupby("i_out")[["i_out", "i_in", "weight"]].apply(
        fill_data)  # type: ignore
    return indices, weights


def _extract_indices_weights(
    n_cells: int,
    intersection: pd.DataFrame,
    use_numba: bool = True,
) -> tuple[NDArray[np.signedinteger], NDArray[np.floating]]:
    if use_numba and imported_njit:
        return _extract_indices_weights_numba(n_cells, intersection)

    else:
        warnings.warn(
            "Numba is not installed, using slow Python implementation",
            UserWarning,
        )
        return _extract_indices_weights_python(n_cells, intersection)


[docs] def compute_weights_unstructured( lon_vertices_out: NDArray[np.floating], lat_vertices_out: NDArray[np.floating], cell_area_out: NDArray[np.floating] | None, lon_vertices_in: NDArray[np.floating], lat_vertices_in: NDArray[np.floating], cell_area_in: NDArray[np.floating] | None, chunk_size: int | None = None, processes: int | None = None, use_numba: bool = True, ) -> tuple[NDArray[np.signedinteger], NDArray[np.floating]]: """Compute indices and weigths for conservative interpolation between unstructured grids. Args: lon_vertices_out (2D array (M1, N1)): Output grid longitudes of the vertices of each cell. lat_vertices_out (2D array (M1, N1)): Output grid latitudes of the vertices of each cell. cell_area_out (1D array (M1) or None): Precomputed areas of each cell of the output grid. lon_vertices_in (2D array (M2, N2)): Input grid longitudes of the vertices of each cell. lat_vertices_in (2D array (M2, N2)): Input grid latitudes of the vertices of each cell. cell_area_in (1D array (M2) or None): Precomputed areas of each cell of the input grid. chunk_size (int, optional): Chunk size in the latitude bins. Defaults to None. processes (int, optional): Number of processes. Defaults to None. use_numba (bool, optional): Use Numba if available, only used for unit tests. Defaults to True. Returns: 2D array of int (M1, K), 2D array of float (M1, K): Indices and weights for interpolation. """ if lon_vertices_in.shape != lat_vertices_in.shape: raise CifValueError( "input longitude and latitude vertices have different shapes") if lon_vertices_out.shape != lat_vertices_out.shape: raise CifValueError( "output longitude and latitude vertices have different shapes") n_out, _ = lon_vertices_out.shape # Get projection bins mid_split, poles_split = optimize_proj_bins(lat_vertices_out, lat_vertices_in) # Get output projections lon_vertices_out = standardize_lon(lon_vertices_out) geom_out = WorldEqualArea( lon_vertices_out, lat_vertices_out, cell_area=cell_area_out, index_name="i_out", mid_split=mid_split, poles_split=poles_split, chunk_size=chunk_size, processes=processes ) # Get input projections lon_vertices_in = standardize_lon(lon_vertices_in) coords_in = vertices_to_coords(lon_vertices_in, lat_vertices_in) unique, unique_indices = np.unique(coords_in, return_index=True, axis=0) lon_vertices_in = unique[:, :, 0] lat_vertices_in = unique[:, :, 1] geom_in = WorldEqualArea( lon_vertices_in, lat_vertices_in, indices=unique_indices, cell_area=cell_area_in.flatten() if cell_area_in is not None else None, index_name="i_in", mid_split=mid_split, poles_split=poles_split, chunk_size=chunk_size, processes=processes ) # Compute intersection area intersection = geom_out.intersection_area(geom_in) if intersection.empty: raise CifValueError("Input and output domains are not overlapping") # Intersection area to weights ind_out = intersection[geom_out.index_name] intersection["weight"] = ( intersection["area"] / geom_out.area.loc[ind_out].to_numpy() ) # Format indices and weights indices, weights = _extract_indices_weights(n_out, intersection, use_numba) return indices, weights