Source code for nlsq.global_optimization.method_selector

"""Method selector for global optimization.

Selects between CMA-ES and multi-start optimization based on problem
characteristics and available dependencies.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Literal

import numpy as np

from nlsq.global_optimization.cmaes_config import is_evosax_available

if TYPE_CHECKING:
    from numpy.typing import ArrayLike

__all__ = ["MethodSelector"]

logger = logging.getLogger(__name__)

# Default threshold for scale ratio to prefer CMA-ES
# 1000x (3 orders of magnitude) difference in parameter scales
DEFAULT_SCALE_THRESHOLD = 1000.0

MethodType = Literal["cmaes", "multi-start", "auto"] | None


[docs] class MethodSelector: """Select optimization method based on problem characteristics. The selector analyzes the parameter bounds to compute a scale ratio, which indicates how many orders of magnitude separate the parameter scales. CMA-ES is preferred for multi-scale problems (high scale ratio) when evosax is available. Parameters ---------- scale_threshold : float, optional Threshold for scale ratio above which CMA-ES is preferred. Default is 1000.0 (3 orders of magnitude). Attributes ---------- scale_threshold : float The scale ratio threshold for CMA-ES preference. Examples -------- >>> from nlsq.global_optimization import MethodSelector >>> import numpy as np >>> >>> selector = MethodSelector() >>> lower = np.array([1e2, 1e-5, 0.5]) >>> upper = np.array([1e6, 1e-1, 3.0]) >>> >>> method = selector.select('auto', lower, upper) >>> print(f"Selected method: {method}") """
[docs] def __init__(self, scale_threshold: float = DEFAULT_SCALE_THRESHOLD) -> None: """Initialize MethodSelector. Parameters ---------- scale_threshold : float, optional Threshold for scale ratio above which CMA-ES is preferred. """ self.scale_threshold = scale_threshold
[docs] def compute_scale_ratio( self, lower_bounds: ArrayLike, upper_bounds: ArrayLike ) -> float: """Compute the scale ratio from parameter bounds. The scale ratio is the ratio of the largest parameter range to the smallest, indicating how many orders of magnitude separate the parameter scales. Parameters ---------- lower_bounds : ArrayLike Lower bounds for parameters. upper_bounds : ArrayLike Upper bounds for parameters. Returns ------- float Scale ratio (>= 1.0). Higher values indicate more diverse scales. """ lower = np.asarray(lower_bounds) upper = np.asarray(upper_bounds) # Compute ranges ranges = upper - lower # Filter out zero-width ranges (fixed parameters) nonzero_ranges = ranges[ranges > 0] if len(nonzero_ranges) == 0: return 1.0 # Ratio of max to min range max_range = np.max(nonzero_ranges) min_range = np.min(nonzero_ranges) if min_range == 0: return float("inf") return float(max_range / min_range)
[docs] def select( self, requested_method: MethodType, lower_bounds: ArrayLike, upper_bounds: ArrayLike, ) -> Literal["cmaes", "multi-start"]: """Select the optimization method to use. Parameters ---------- requested_method : {'cmaes', 'multi-start', 'auto'} | None Requested method. If 'auto' or None, selection is based on scale ratio and evosax availability. lower_bounds : ArrayLike Lower bounds for parameters. upper_bounds : ArrayLike Upper bounds for parameters. Returns ------- Literal['cmaes', 'multi-start'] The selected optimization method. Notes ----- Selection logic: 1. If 'cmaes' requested and evosax available -> 'cmaes' 2. If 'cmaes' requested but evosax unavailable -> 'multi-start' (with warning) 3. If 'multi-start' requested -> 'multi-start' 4. If 'auto' or None: - If scale_ratio > threshold and evosax available -> 'cmaes' - Otherwise -> 'multi-start' """ evosax_available = is_evosax_available() # Explicit CMA-ES request if requested_method == "cmaes": if evosax_available: logger.debug("CMA-ES requested and evosax available") return "cmaes" else: logger.info( "CMA-ES requested but evosax not installed. " "Falling back to multi-start optimization. " "Install evosax with: pip install 'nlsq[global]'" ) return "multi-start" # Explicit multi-start request if requested_method == "multi-start": logger.debug("Multi-start explicitly requested") return "multi-start" # Auto selection (or None) scale_ratio = self.compute_scale_ratio(lower_bounds, upper_bounds) logger.debug(f"Auto method selection: scale_ratio={scale_ratio:.2f}") if scale_ratio > self.scale_threshold: if evosax_available: logger.info( f"Scale ratio {scale_ratio:.0f}x exceeds threshold " f"({self.scale_threshold:.0f}x). Using CMA-ES for " "multi-scale optimization." ) return "cmaes" else: logger.info( f"Scale ratio {scale_ratio:.0f}x suggests CMA-ES would be " "beneficial, but evosax not installed. Using multi-start. " "Install evosax with: pip install 'nlsq[global]'" ) return "multi-start" else: logger.debug( f"Scale ratio {scale_ratio:.0f}x below threshold " f"({self.scale_threshold:.0f}x). Using multi-start." ) return "multi-start"