Source code for pyinterp.fill

# Copyright (c) 2026 CNES.
#
# All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
"""Fill Methods Wrapper.

This module provides convenient wrapper functions around pyinterp.core.fill
methods that accept both configuration objects and simplified keyword
arguments.

For most use cases, you can simply pass keyword arguments:
>>> iterations, residual = gauss_seidel(grid, max_iterations=100, epsilon=1e-5)

For advanced configuration, pass a config object:
>>> from pyinterp.config import fill
>>> config = fill.GaussSeidel().with_max_iterations(100).with_epsilon(1e-5)
>>> iterations, residual = gauss_seidel(grid, config)
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast

from . import core
from .core.config import fill


if TYPE_CHECKING:
    from .type_hints import NDArray2DFloat32, NDArray2DFloat64

__all__ = [
    "fft_inpaint",
    "gauss_seidel",
    "loess",
    "matrix",
    "multigrid",
    "vector",
]

#: First guess initialization methods
FirstGuessMethod = Literal["zero", "zonal_average"]

#: Loess value type options
LoessValueType = Literal["all", "defined", "undefined"]

# First guess mapping
_FIRST_GUESS_MAP = {
    "zero": fill.FirstGuess.ZERO,
    "zonal_average": fill.FirstGuess.ZONAL_AVERAGE,
}

# Loess value type mapping
_LOESS_VALUE_TYPE_MAP = {
    "all": fill.LoessValueType.ALL,
    "defined": fill.LoessValueType.DEFINED,
    "undefined": fill.LoessValueType.UNDEFINED,
}

# TypeVars for generic config creation
_FillConfig = TypeVar(
    "_FillConfig",
    fill.FFTInpaint,
    fill.GaussSeidel,
    fill.Loess,
    fill.Multigrid,
)

if TYPE_CHECKING:
    _FloatArrayT = TypeVar(
        "_FloatArrayT",
        NDArray2DFloat32,
        NDArray2DFloat64,
    )
else:
    _FloatArrayT = TypeVar("_FloatArrayT")


def _build_config(
    config_class: type[_FillConfig],
    method_map: dict[str, tuple[str, dict | None]] | None = None,
    **kwargs: Any,  # noqa: ANN401
) -> _FillConfig:
    """Build a configuration object from keyword arguments.

    Args:
        config_class: The configuration class to instantiate
        method_map: Dict mapping parameter names to method names and optional
            value maps
        **kwargs: Keyword arguments to apply

    Returns:
        Configured object

    """
    cfg = config_class()
    method_map = method_map or {}

    for param_name, value in kwargs.items():
        if value is None or param_name not in method_map:
            continue

        method_name, value_map = method_map[param_name]
        method = getattr(cfg, method_name)
        cfg = method(value_map[value] if value_map else value)

    return cast("_FillConfig", cfg)


[docs] def fft_inpaint( grid: _FloatArrayT, config: fill.FFTInpaint | None = None, *, max_iterations: int | None = None, epsilon: float | None = None, sigma: float | None = None, first_guess: FirstGuessMethod | None = None, is_periodic: bool | None = None, num_threads: int | None = None, ) -> tuple[int, float]: """Fill missing values using FFT-based inpainting. Args: grid: 2D grid with missing values (NaN) config: Configuration object (if provided, keyword args are ignored) max_iterations: Maximum number of iterations epsilon: Convergence criterion sigma: Smoothing parameter for Gaussian filter first_guess: Initial guess method is_periodic: Whether to assume periodic boundaries num_threads: Number of threads (0 = auto) Returns: Tuple of (number of iterations, final residual) Examples: Simple usage: >>> iterations, residual = fft_inpaint( ... grid, max_iterations=100, epsilon=1e-5 ... ) Advanced usage with config object: >>> from pyinterp.core.config import fill >>> config = ( ... fill.FFTInpaint() ... .with_max_iterations(100) ... .with_epsilon(1e-5) ... .with_sigma(0.5) ... ) >>> iterations, residual = fft_inpaint(grid, config) """ # If config is provided, use it directly if config is not None: return core.fill.fft_inpaint(grid, config) # Create config from keyword arguments method_map = { "max_iterations": ("with_max_iterations", None), "epsilon": ("with_epsilon", None), "sigma": ("with_sigma", None), "first_guess": ("with_first_guess", _FIRST_GUESS_MAP), "is_periodic": ("with_is_periodic", None), "num_threads": ("with_num_threads", None), } cfg = _build_config( fill.FFTInpaint, method_map, max_iterations=max_iterations, epsilon=epsilon, sigma=sigma, first_guess=first_guess, is_periodic=is_periodic, num_threads=num_threads, ) return core.fill.fft_inpaint(grid, cfg)
[docs] def gauss_seidel( grid: _FloatArrayT, config: fill.GaussSeidel | None = None, *, max_iterations: int | None = None, epsilon: float | None = None, relaxation: float | None = None, first_guess: FirstGuessMethod | None = None, is_periodic: bool | None = None, num_threads: int | None = None, ) -> tuple[int, float]: """Fill missing values using Gauss-Seidel relaxation. Args: grid: 2D grid with missing values (NaN) config: Configuration object (if provided, keyword args are ignored) max_iterations: Maximum number of iterations epsilon: Convergence criterion relaxation: Relaxation parameter (0 < relaxation <= 2) first_guess: Initial guess method is_periodic: Whether to assume periodic boundaries num_threads: Number of threads (0 = auto) Returns: Tuple of (number of iterations, final residual) Examples: >>> iterations, residual = gauss_seidel( ... grid, max_iterations=1000, epsilon=1e-4, relaxation=1.5 ... ) """ # If config is provided, use it directly if config is not None: return core.fill.gauss_seidel(grid, config) # Create config from keyword arguments method_map = { "max_iterations": ("with_max_iterations", None), "epsilon": ("with_epsilon", None), "relaxation": ("with_relaxation", None), "first_guess": ("with_first_guess", _FIRST_GUESS_MAP), "is_periodic": ("with_is_periodic", None), "num_threads": ("with_num_threads", None), } cfg = _build_config( fill.GaussSeidel, method_map, max_iterations=max_iterations, epsilon=epsilon, relaxation=relaxation, first_guess=first_guess, is_periodic=is_periodic, num_threads=num_threads, ) return core.fill.gauss_seidel(grid, cfg)
[docs] def loess( data: _FloatArrayT, config: fill.Loess | None = None, *, nx: int | None = None, ny: int | None = None, max_iterations: int | None = None, epsilon: float | None = None, value_type: LoessValueType | None = None, first_guess: FirstGuessMethod | None = None, is_periodic: bool | None = None, num_threads: int | None = None, ) -> _FloatArrayT: """Fill missing values using LOESS (locally weighted regression). Args: data: 2D grid with missing values (NaN) config: Configuration object (if provided, keyword args are ignored) nx: Window size in X direction ny: Window size in Y direction max_iterations: Maximum number of iterations. If the value is 1, a single-pass LOESS is performed, the first guess option is ignored. epsilon: Convergence criterion value_type: Which values to use in regression first_guess: Initial guess method is_periodic: Whether to assume periodic boundaries num_threads: Number of threads (0 = auto) Returns: Filled grid Examples: >>> filled_grid = loess( ... data, nx=5, ny=5, max_iterations=10, value_type="defined" ... ) """ # If config is provided, use it directly if config is not None: return core.fill.loess(data, config) # Create config from keyword arguments method_map = { "nx": ("with_nx", None), "ny": ("with_ny", None), "max_iterations": ("with_max_iterations", None), "epsilon": ("with_epsilon", None), "value_type": ("with_value_type", _LOESS_VALUE_TYPE_MAP), "first_guess": ("with_first_guess", _FIRST_GUESS_MAP), "is_periodic": ("with_is_periodic", None), "num_threads": ("with_num_threads", None), } cfg = _build_config( fill.Loess, method_map, nx=nx, ny=ny, max_iterations=max_iterations, epsilon=epsilon, value_type=value_type, first_guess=first_guess, is_periodic=is_periodic, num_threads=num_threads, ) return core.fill.loess(data, cfg)
[docs] def multigrid( grid: _FloatArrayT, config: fill.Multigrid | None = None, *, max_iterations: int | None = None, epsilon: float | None = None, pre_smooth: int | None = None, post_smooth: int | None = None, first_guess: FirstGuessMethod | None = None, is_periodic: bool | None = None, num_threads: int | None = None, ) -> tuple[int, float]: """Fill missing values using multigrid method. Args: grid: 2D grid with missing values (NaN) config: Configuration object (if provided, keyword args are ignored) max_iterations: Maximum number of iterations epsilon: Convergence criterion pre_smooth: Number of pre-smoothing iterations post_smooth: Number of post-smoothing iterations first_guess: Initial guess method is_periodic: Whether to assume periodic boundaries num_threads: Number of threads (0 = auto) Returns: Tuple of (number of iterations, final residual) Examples: >>> iterations, residual = multigrid( ... grid, ... max_iterations=100, ... epsilon=1e-5, ... pre_smooth=2, ... post_smooth=2, ... ) """ # If config is provided, use it directly if config is not None: return core.fill.multigrid(grid, config) # Create config from keyword arguments method_map = { "max_iterations": ("with_max_iterations", None), "epsilon": ("with_epsilon", None), "pre_smooth": ("with_pre_smooth", None), "post_smooth": ("with_post_smooth", None), "first_guess": ("with_first_guess", _FIRST_GUESS_MAP), "is_periodic": ("with_is_periodic", None), "num_threads": ("with_num_threads", None), } cfg = _build_config( fill.Multigrid, method_map, max_iterations=max_iterations, epsilon=epsilon, pre_smooth=pre_smooth, post_smooth=post_smooth, first_guess=first_guess, is_periodic=is_periodic, num_threads=num_threads, ) return core.fill.multigrid(grid, cfg)
[docs] def matrix(grid: _FloatArrayT, fill_value: float | None = None) -> None: """Fill a 2D array by linear interpolation. Args: grid: 2D array to fill fill_value: Value to use to determine missing values (if None, use NaN) Note: This function modifies the grid in-place. Examples: >>> import numpy as np >>> grid = np.arange(100, dtype=np.float32).reshape(10, 10) >>> grid[::2, ::2] = np.nan >>> matrix(grid) """ core.fill.matrix( grid, fill_value if fill_value is not None else float("nan") )
[docs] def vector(array: _FloatArrayT, fill_value: float | None = None) -> None: """Fill a 1D array by linear interpolation. Args: array: 1D array to fill fill_value: Value to use to determine missing values (if None, use NaN) Note: This function modifies the array in-place. Examples: >>> import numpy as np >>> arr = np.arange(100, dtype=np.float32) >>> arr[10:20] = np.nan >>> vector(arr) """ core.fill.vector( array, fill_value if fill_value is not None else float("nan") )