Source code for autolyap.utils.validation

# SPDX-FileCopyrightText: 2025-2026 AutoLyap contributors
# SPDX-License-Identifier: GPL-3.0-only

"""Validation helpers shared across algorithms and problem classes."""

from numbers import Integral, Real
from typing import Any, Iterable, List, Optional, Sequence, Union, cast

import numpy as np

_INDEX_LIST_TYPES = (list, tuple, np.ndarray)
IndexContainer = Union[List[Any], tuple[Any, ...], np.ndarray]
RealScalar = Union[int, float, np.integer, np.floating]


def _ensure_index_container(
    values: Optional[IndexContainer],
    name: str,
    error_message: str,
) -> Sequence[Any]:
    r"""
    Ensure index-like input is a concrete finite-length sequence container.

    Accepted container types are ``list``, ``tuple``, and ``numpy.ndarray``.

    **Parameters**

    - `values`: Candidate container to validate.
    - `name`: Logical input name (reserved for caller-side error context).
    - `error_message`: Error text raised when validation fails.

    **Returns**

    - A sequence view of `values` suitable for indexed iteration.
    """
    if values is None or not isinstance(values, _INDEX_LIST_TYPES):
        raise ValueError(error_message)
    return cast(Sequence[Any], values)


[docs] def ensure_integral(value: RealScalar, name: str, minimum: Optional[int] = None, maximum: Optional[int] = None) -> int: r""" Validate and normalize an integer-valued scalar. Rejects booleans explicitly and optionally enforces lower/upper bounds. """ # Reject bools explicitly; they are subclasses of int in Python. if isinstance(value, bool) or not isinstance(value, Integral): raise ValueError(f"{name} must be an integer.") value = int(value) # Bounds checks are optional; use when caller cares about valid ranges. if minimum is not None and value < minimum: raise ValueError(f"{name} must be >= {minimum}.") if maximum is not None and value > maximum: raise ValueError(f"{name} must be <= {maximum}.") return value
[docs] def ensure_real_number(value: RealScalar, name: str, finite: bool = False, minimum: Optional[float] = None, maximum: Optional[float] = None) -> float: r""" Validate and normalize a real-valued scalar. Rejects booleans and NaNs explicitly. Optional flags enforce finiteness and lower/upper bounds. """ # Reject bools explicitly; they are subclasses of int/float in Python. if isinstance(value, bool) or not isinstance(value, Real): raise ValueError(f"{name} must be a real number.") value = float(value) if np.isnan(value): raise ValueError(f"{name} must be a real number.") if finite and not np.isfinite(value): raise ValueError(f"{name} must be finite.") if minimum is not None and value < minimum: raise ValueError(f"{name} must be >= {minimum}.") if maximum is not None and value > maximum: raise ValueError(f"{name} must be <= {maximum}.") return value
[docs] def ensure_finite_array(array: np.ndarray, name: str) -> None: r"""Raise if `array` contains non-finite entries (NaN or inf).""" # Centralized finite check used across matrix/vector inputs. try: if not np.all(np.isfinite(array)): raise ValueError(f"{name} must contain only finite entries.") except TypeError as exc: raise ValueError(f"{name} must contain only finite entries.") from exc
[docs] def ensure_index_list(values: Iterable[RealScalar], name: str, m: int) -> List[int]: r""" Validate a sorted, duplicate-free component-index list. The returned list is guaranteed to contain integers in ``[1, m]`` in strictly increasing order. """ values = _ensure_index_container(values, name, f"{name} must be a list of integers.") items: List[int] = [] for v in values: if isinstance(v, bool) or not isinstance(v, Integral): raise ValueError(f"{name} must contain only integers.") v_int = int(v) if v_int < 1 or v_int > m: raise ValueError(f"{name} entries must be in [1, {m}].") items.append(v_int) if len(set(items)) != len(items): raise ValueError(f"{name} must not contain duplicates.") if items != sorted(items): raise ValueError(f"{name} must be in increasing order.") return items
[docs] def ensure_m_bar_list(values: Iterable[RealScalar], m: int) -> List[int]: r""" Validate per-component evaluation counts. Ensures the sequence length equals ``m`` and every entry is an integer strictly greater than zero. """ values = _ensure_index_container(values, "m_bar_is", "m_bar_is must be a list of positive integers.") if len(values) != m: raise ValueError("m must equal the length of m_bar_is") out: List[int] = [] for idx, v in enumerate(values, start=1): if isinstance(v, bool) or not isinstance(v, Integral): raise ValueError("m_bar_is must contain only integers.") v_int = int(v) if v_int <= 0: raise ValueError(f"m_bar_is entries must be > 0. Got {v_int} at index {idx}.") out.append(v_int) return out