"""Input validation for NLSQ optimization (T079).
Extracted from wrapper.py as part of architecture refactoring.
Enhanced with structured logging for T027.
"""
from __future__ import annotations
from typing import Any
import numpy as np
from homodyne.utils.logging import get_logger
logger = get_logger(__name__)
[docs]
def validate_array_dimensions(xdata: np.ndarray, ydata: np.ndarray) -> bool:
"""Validate that xdata and ydata have compatible dimensions.
Parameters
----------
xdata : np.ndarray
Independent variable data
ydata : np.ndarray
Dependent variable data
Returns
-------
bool
True if dimensions are compatible
"""
if len(xdata) == 0:
logger.warning("xdata is empty")
return False
if len(ydata) == 0:
logger.warning("ydata is empty")
return False
if len(xdata) != len(ydata):
logger.warning(f"Array length mismatch: xdata={len(xdata)}, ydata={len(ydata)}")
return False
return True
[docs]
def validate_no_nan_inf(
arr: np.ndarray,
name: str,
iteration: int | None = None,
context: dict[str, Any] | None = None,
) -> bool:
"""Validate that array contains no NaN or Inf values (T027).
Parameters
----------
arr : np.ndarray
Array to validate
name : str
Name for logging
iteration : int, optional
Current iteration number for context
context : dict, optional
Additional context for logging
Returns
-------
bool
True if array contains only finite values
"""
if not np.all(np.isfinite(arr)):
nan_count = int(np.sum(np.isnan(arr)))
inf_count = int(np.sum(np.isinf(arr)))
# Find indices of problematic values for debugging
nan_indices = np.where(np.isnan(arr))[0][:10] # First 10 NaN indices
inf_indices = np.where(np.isinf(arr))[0][:10] # First 10 Inf indices
# T027: Enhanced logging with context
context_str = ""
if iteration is not None:
context_str += f" [iteration={iteration}]"
if context:
context_str += f" [context={context}]"
logger.warning(
f"{name} contains numerical issues{context_str}:\n"
f" NaN count: {nan_count}, first indices: {nan_indices.tolist()}\n"
f" Inf count: {inf_count}, first indices: {inf_indices.tolist()}\n"
f" Array shape: {arr.shape}, dtype: {arr.dtype}\n"
f" Array range: [{np.nanmin(arr):.4g}, {np.nanmax(arr):.4g}]"
)
return False
return True
[docs]
def validate_bounds_consistency(
bounds: tuple[np.ndarray, np.ndarray],
initial_params: np.ndarray,
) -> bool:
"""Validate that bounds are consistent.
Parameters
----------
bounds : tuple[np.ndarray, np.ndarray]
(lower, upper) bounds arrays
initial_params : np.ndarray
Initial parameter values
Returns
-------
bool
True if bounds are consistent
"""
lower, upper = bounds
# Check bounds arrays have same length as params
if len(lower) != len(initial_params):
logger.warning(
f"Lower bounds length {len(lower)} != params length {len(initial_params)}"
)
return False
if len(upper) != len(initial_params):
logger.warning(
f"Upper bounds length {len(upper)} != params length {len(initial_params)}"
)
return False
# Check lower <= upper
if not np.all(lower <= upper):
violations = np.where(lower > upper)[0]
logger.warning(f"Lower > upper at indices: {violations}")
return False
return True
[docs]
def validate_initial_params(
initial_params: np.ndarray,
bounds: tuple[np.ndarray, np.ndarray] | None,
) -> bool:
"""Validate that initial parameters are within bounds.
Parameters
----------
initial_params : np.ndarray
Initial parameter values
bounds : tuple[np.ndarray, np.ndarray] | None
(lower, upper) bounds arrays, or None for unbounded
Returns
-------
bool
True if params are within bounds
"""
if bounds is None:
return True
lower, upper = bounds
# Check within bounds
below_lower = initial_params < lower
above_upper = initial_params > upper
if np.any(below_lower):
indices = np.where(below_lower)[0]
logger.warning(f"Params below lower bound at indices: {indices}")
return False
if np.any(above_upper):
indices = np.where(above_upper)[0]
logger.warning(f"Params above upper bound at indices: {indices}")
return False
return True
__all__ = [
"InputValidator",
"validate_array_dimensions",
"validate_bounds_consistency",
"validate_initial_params",
"validate_no_nan_inf",
]