Source code for pyinterp.grid

# Copyright (c) 2025 CNES
#
# All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
"""Regular grids."""
from __future__ import annotations

from typing import TYPE_CHECKING, Any

import numpy

from . import Axis, TemporalAxis, core, interface

if TYPE_CHECKING:
    from .typing import NDArray2D, NDArray3D, NDArray4D

#: Two dimensional type variable
NUM_DIMS_2 = 2

#: Three dimensional type variable
NUM_DIMS_3 = 3

#: Four dimensional type variable
NUM_DIMS_4 = 4


def _configure_grid_class(
    self: Grid2D | Grid3D | Grid4D,
    *args: Any,  # noqa: ANN401
    increasing_axes: str | None = None,
) -> None:
    """Initialize a Grid instance."""
    prefix = ''
    for item in args:
        if isinstance(item, core.TemporalAxis):
            prefix = 'Temporal'
            break
    _class = f'{prefix}Grid{self._DIMENSIONS}D' + \
        interface._core_class_suffix(args[-1], handle_integer=True)
    if increasing_axes is not None:
        if increasing_axes not in ['inplace', 'copy']:
            raise ValueError('increasing_axes '
                             f'{increasing_axes!r} is not defined')
        inplace = increasing_axes == 'inplace'
        # Tuple does not support item assignment
        args = list(args)  # type: ignore[assignment]
        for idx, item in enumerate(args):
            if isinstance(item, (
                    core.Axis,
                    core.TemporalAxis,
            )) and not item.is_ascending():
                args[idx] = item.flip(  # type: ignore[index]
                    inplace=inplace)
                args[-1] = numpy.flip(  # type: ignore[index]
                    args[-1], axis=idx)
    self._instance = getattr(core, _class)(*args)
    self._prefix = prefix


def _format_instance(self: Grid2D | Grid3D | Grid4D) -> str:
    """Get the string representation of this instance."""

    def pad(string: str, length: int) -> str:
        """Pad a string to a given length."""
        return '\n'.join([(' ' * length if ix else '') + line
                          for ix, line in enumerate(string.split('\n'))])

    result = [
        f'<{self.__module__}.{self.__class__.__name__}>',
        repr(self.array),
    ]
    result.append('Axis:')
    for item in dir(self):
        attr = getattr(self, item)
        if isinstance(attr, (core.Axis, core.TemporalAxis)):
            prefix = f'* {item}: '
            result.append(f' {prefix}{pad(repr(attr), len(prefix))}')
    return '\n'.join(result)


[docs] class Grid2D: """2D Cartesian Grid. Args: x: X-Axis. y: Y-Axis. array: Discrete representation of a continuous function on a uniform 2-dimensional grid. increasing_axes: Optional string indicating how to ensure that the grid axes are increasing. If axes are decreasing, the axes and grid provided will be flipped in place or copied before being flipped. By default, the decreasing axes are not modified. Examples: >>> import numpy as np >>> import pyinterp >>> x_axis = pyinterp.Axis( >>> np.arange(-180.0, 180.0, 1.0), >>> is_circle=True, >>> ) >>> y_axis = pyinterp.Axis( >>> np.arange(-80.0, 80.0, 1.0), >>> is_circle=False, >>> ) >>> array = np.zeros((len(x_axis), len(y_axis))) >>> grid = pyinterp.Grid2D(x_axis, y_axis, array) <pyinterp.grid.Grid2D> array([[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]], shape=(360, 160)) Axis: * x: <pyinterp.core.Axis> min_value: -180 max_value: 179 step : 1 is_circle: true * y: <pyinterp.core.Axis> min_value: -80 max_value: 79 step : 1 is_circle: false """ #: The number of grid dimensions handled by this object. _DIMENSIONS = NUM_DIMS_2 def __init__( self, x: Axis, y: Axis, array: NDArray2D, increasing_axes: str | None = None, ) -> None: """Initialize a Grid2D instance.""" self._instance: core.Grid2DFloat32 | core.Grid2DFloat64 self._prefix: str _configure_grid_class( self, x, y, array, increasing_axes=increasing_axes, )
[docs] def __repr__(self) -> str: """Get the string representation of this instance.""" return _format_instance(self)
@property def x(self) -> core.Axis: """Gets the X-Axis handled by this instance. Returns: X-Axis. """ return self._instance.x @property def y(self) -> core.Axis: """Gets the Y-Axis handled by this instance. Returns: Y-Axis. """ return self._instance.y @property def array(self) -> NDArray2D: """Gets the values handled by this instance. Returns: numpy.ndarray: values. """ return self._instance.array
[docs] class Grid3D: """3D Cartesian Grid. Args: x: X-Axis. y: Y-Axis. z: Z-Axis. array: Discrete representation of a continuous function on a uniform 3-dimensional grid. increasing_axes: Ensure that the axes of the grid are increasing. If this is not the case, the axes and grid provided will be flipped. Default to False. Notes: If the Z axis is a :py:class:`temporal axis <pyinterp.TemporalAxis>`, the grid will handle this axis during interpolations as a time axis. Examples: >>> import numpy as np >>> import pyinterp >>> >>> x_axis = pyinterp.Axis( >>> np.arange(-180.0, 180.0, 1.0), >>> is_circle=True, >>> ) >>> y_axis = pyinterp.Axis( >>> np.arange(-80.0, 80.0, 1.0), >>> is_circle=False, >>> ) >>> z_axis = pyinterp.TemporalAxis( >>> np.array( >>> ['2000-01-01'], >>> dtype="datetime64[s]", >>> )) >>> array = np.zeros((len(x_axis), len(y_axis), len(z_axis))) >>> grid = pyinterp.Grid3D(x_axis, y_axis, z_axis, array) <pyinterp.grid.Grid3D> array([[[0.], [0.], [0.], ..., [0.], [0.], [0.]], [[0.], [0.], [0.], ..., [0.], [0.], [0.]], [[0.], [0.], [0.], ..., [0.], [0.], [0.]], ..., [[0.], [0.], [0.], ..., [0.], [0.], [0.]], [[0.], [0.], [0.], ..., [0.], [0.], [0.]], [[0.], [0.], [0.], ..., [0.], [0.], [0.]]],shape=(360, 160, 1)) Axis: * x: <pyinterp.core.Axis> min_value: -180 max_value: 179 step : 1 is_circle: true * y: <pyinterp.core.Axis> min_value: -80 max_value: 79 step : 1 is_circle: false * z: <pyinterp.core.TemporalAxis> values : ['2000-01-01T00:00:00'] """ _DIMENSIONS = NUM_DIMS_3 def __init__( self, x: Axis, y: Axis, z: Axis | TemporalAxis, array: NDArray3D, increasing_axes: str | None = None, ) -> None: """Initialize a Grid3D instance.""" self._instance: (core.Grid3DInt8 | core.Grid3DUInt8 | core.Grid3DFloat32 | core.Grid3DFloat64 | core.TemporalGrid3DFloat32 | core.TemporalGrid3DFloat64) self._prefix: str self._dtype = None if isinstance(y, core.Axis) else y.dtype() _configure_grid_class( self, x, y, z, array, increasing_axes=increasing_axes, )
[docs] def __repr__(self) -> str: """Get the string representation of this instance.""" return _format_instance(self)
@property def x(self) -> core.Axis: """Gets the X-Axis handled by this instance. Returns: X-Axis. """ return self._instance.x @property def y(self) -> core.Axis: """Gets the Y-Axis handled by this instance. Returns: Y-Axis. """ return self._instance.y @property def z(self) -> core.Axis | core.TemporalAxis: """Gets the Z-Axis handled by this instance. Returns: Z-Axis. """ return self._instance.z # type: ignore[return-value] @property def array(self) -> NDArray3D: """Gets the values handled by this instance. Returns: numpy.ndarray: values. """ return self._instance.array
[docs] class Grid4D: """4D Cartesian Grid. Args: x: X-Axis. y: Y-Axis. z: Z-Axis. u: U-Axis. array: Discrete representation of a continuous function on a uniform 4-dimensional grid. increasing_axes: Ensure that the axes of the grid are increasing. If this is not the case, the axes and grid provided will be flipped. Default to False. Notes: If the Z axis is a temporal axis, the grid will handle this axis during interpolations as a time axis. """ _DIMENSIONS = NUM_DIMS_4 def __init__( self, x: Axis, y: Axis, z: Axis | TemporalAxis, u: Axis, array: NDArray4D, increasing_axes: str | None = None, ) -> None: """Initialize a Grid4D instance.""" self._instance: (core.Grid4DInt8 | core.Grid4DUInt8 | core.Grid4DFloat32 | core.Grid4DFloat64 | core.TemporalGrid4DFloat32 | core.TemporalGrid4DFloat64) self._prefix: str self._dtype = None if isinstance(y, core.Axis) else y.dtype() _configure_grid_class( self, x, y, z, u, array, increasing_axes=increasing_axes, )
[docs] def __repr__(self) -> str: """Get the string representation of this instance.""" return _format_instance(self)
@property def x(self) -> core.Axis: """Gets the X-Axis handled by this instance. Returns: X-Axis. """ return self._instance.x @property def y(self) -> core.Axis: """Gets the Y-Axis handled by this instance. Returns: Y-Axis. """ return self._instance.y @property def z(self) -> core.Axis | core.TemporalAxis: """Gets the Z-Axis handled by this instance. Returns: Z-Axis. """ return self._instance.z # type: ignore[return-value] @property def u(self) -> core.Axis: """Gets the U-Axis handled by this instance. Returns: U-Axis. """ return self._instance.u @property def array(self) -> NDArray4D: """Gets the values handled by this instance. Returns: numpy.ndarray: values. """ return self._instance.array
def _core_variate_interpolator( instance: Grid2D | Grid3D | Grid4D, interpolator: str, **kwargs: Any, # noqa: ANN401 ) -> Any: # noqa: ANN401 """Obtain the interpolator from the string provided.""" dimensions = instance._DIMENSIONS # 4D interpolation uses the 3D interpolator if dimensions > NUM_DIMS_3: dimensions -= 1 prefix = instance._prefix if interpolator == 'bilinear': return getattr(core, f'{prefix}Bilinear{dimensions}D')(**kwargs) if interpolator == 'nearest': return getattr(core, f'{prefix}Nearest{dimensions}D')(**kwargs) if interpolator == 'inverse_distance_weighting': return getattr( core, f'{prefix}InverseDistanceWeighting{dimensions}D')(**kwargs) raise ValueError(f'interpolator {interpolator!r} is not defined')