Source code for nlsq.core.factories
"""Factory functions for composing curve fitting configurations.
This module provides factory functions that enable runtime composition
of curve fitting features like global optimization and diagnostics.
These factories follow the Builder pattern to provide a clean API for
configuring optimization pipelines.
The factories decouple feature composition from the core curve_fit
implementation, reducing the dependency count in minpack.py.
Examples
--------
>>> from nlsq.core.factories import create_optimizer, configure_curve_fit
>>>
>>> # Configure curve_fit with custom settings
>>> curve_fit = configure_curve_fit(
... enable_diagnostics=True,
... enable_recovery=True,
... )
>>> popt, pcov = curve_fit(model, xdata, ydata)
"""
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
import numpy as np
if TYPE_CHECKING:
from nlsq.caching.unified_cache import UnifiedCache
from nlsq.diagnostics.types import DiagnosticsConfig
from nlsq.stability.guard import NumericalStabilityGuard
[docs]
@dataclass
class OptimizerConfig:
"""Configuration for optimizer creation.
Attributes
----------
enable_global : bool
Enable global optimization with multi-start.
enable_diagnostics : bool
Enable diagnostic reporting.
enable_recovery : bool
Enable automatic recovery from numerical issues.
n_starts : int
Number of starts for global optimization.
"""
enable_global: bool = False
enable_diagnostics: bool = False
enable_recovery: bool = True
n_starts: int = 10
extra_kwargs: dict[str, Any] = field(default_factory=dict)
[docs]
def create_optimizer(
*,
global_optimization: bool = False,
diagnostics: bool = False,
recovery: bool = True,
n_starts: int = 10,
cache: "UnifiedCache | None" = None,
stability_guard: "NumericalStabilityGuard | None" = None,
diagnostics_config: "DiagnosticsConfig | None" = None,
**kwargs: Any,
) -> "ConfiguredOptimizer":
"""Create a configured optimizer with specified features.
This factory function composes various optimization features
(global optimization, diagnostics) into a single
optimizer instance.
Parameters
----------
global_optimization : bool, default=False
Enable global optimization with multi-start.
diagnostics : bool, default=False
Enable diagnostic reporting.
recovery : bool, default=True
Enable automatic recovery from numerical issues.
n_starts : int, default=10
Number of starts for global optimization.
cache : UnifiedCache | None, default=None
Optional cache for JIT compilation.
stability_guard : NumericalStabilityGuard | None, default=None
Optional numerical stability guard.
diagnostics_config : DiagnosticsConfig | None, default=None
Optional diagnostics configuration.
**kwargs : Any
Additional keyword arguments passed to curve_fit.
Returns
-------
ConfiguredOptimizer
A configured optimizer ready for use.
Examples
--------
>>> # Standard optimizer
>>> optimizer = create_optimizer()
>>> popt, pcov = optimizer.fit(model, xdata, ydata)
>>>
>>> # Global optimizer with diagnostics
>>> optimizer = create_optimizer(global_optimization=True, diagnostics=True)
"""
config = OptimizerConfig(
enable_global=global_optimization,
enable_diagnostics=diagnostics,
enable_recovery=recovery,
n_starts=n_starts,
extra_kwargs=kwargs,
)
return ConfiguredOptimizer(
config=config,
cache=cache,
stability_guard=stability_guard,
diagnostics_config=diagnostics_config,
)
[docs]
class ConfiguredOptimizer:
"""An optimizer configured with specific features.
This class encapsulates the configuration for curve fitting
and provides a clean interface for performing fits.
Parameters
----------
config : OptimizerConfig
The optimizer configuration.
cache : UnifiedCache | None
Optional cache for JIT compilation.
stability_guard : NumericalStabilityGuard | None
Optional numerical stability guard.
diagnostics_config : DiagnosticsConfig | None
Optional diagnostics configuration.
"""
__slots__ = ("_cache", "_config", "_diagnostics_config", "_stability_guard")
[docs]
def __init__(
self,
config: OptimizerConfig,
cache: "UnifiedCache | None" = None,
stability_guard: "NumericalStabilityGuard | None" = None,
diagnostics_config: "DiagnosticsConfig | None" = None,
) -> None:
"""Initialize the configured optimizer."""
self._config = config
self._cache = cache
self._stability_guard = stability_guard
self._diagnostics_config = diagnostics_config
[docs]
def fit(
self,
f: Callable[..., np.ndarray],
xdata: np.ndarray,
ydata: np.ndarray,
p0: np.ndarray | None = None,
sigma: np.ndarray | None = None,
bounds: tuple[np.ndarray, np.ndarray] | None = None,
**kwargs: Any,
) -> tuple[np.ndarray, np.ndarray]:
"""Fit a function to data using the configured optimizer.
Parameters
----------
f : Callable
Model function ``f(x, *params) -> y``.
xdata : np.ndarray
Independent variable data.
ydata : np.ndarray
Dependent variable data.
p0 : np.ndarray or None
Initial parameter guess.
sigma : np.ndarray or None
Uncertainty in ydata.
bounds : tuple or None
(lower, upper) bounds for parameters.
**kwargs : Any
Additional keyword arguments.
Returns
-------
tuple[np.ndarray, np.ndarray]
(popt, pcov) - optimal parameters and covariance matrix.
"""
# Merge config kwargs with call kwargs
merged_kwargs = {**self._config.extra_kwargs, **kwargs}
# Handle global optimization
if self._config.enable_global:
return self._fit_global(f, xdata, ydata, p0, sigma, bounds, **merged_kwargs)
# Standard fit
return self._fit_standard(f, xdata, ydata, p0, sigma, bounds, **merged_kwargs)
def _fit_standard(
self,
f: Callable[..., np.ndarray],
xdata: np.ndarray,
ydata: np.ndarray,
p0: np.ndarray | None,
sigma: np.ndarray | None,
bounds: tuple[np.ndarray, np.ndarray] | None,
**kwargs: Any,
) -> tuple[np.ndarray, np.ndarray] | Any:
"""Perform standard curve fitting."""
from nlsq.core.minpack import curve_fit
return curve_fit(
f,
xdata,
ydata,
p0=p0,
sigma=sigma,
bounds=bounds,
**kwargs,
)
def _fit_global(
self,
f: Callable[..., np.ndarray],
xdata: np.ndarray,
ydata: np.ndarray,
p0: np.ndarray | None,
sigma: np.ndarray | None,
bounds: tuple[np.ndarray, np.ndarray] | None,
**kwargs: Any,
) -> Any:
"""Perform global optimization with multi-start."""
from nlsq.global_optimization.config import GlobalOptimizationConfig
from nlsq.global_optimization.multi_start import MultiStartOrchestrator
config = GlobalOptimizationConfig(n_starts=self._config.n_starts)
optimizer = MultiStartOrchestrator(config=config)
# Infer p0 shape from function signature if not provided
if p0 is None:
from inspect import signature
sig = signature(f)
# Subtract 1 for the x parameter
n_params = max(len(sig.parameters) - 1, 1)
p0 = np.ones(n_params)
# MultiStartOrchestrator.fit() returns dict, not tuple
return optimizer.fit(
f,
xdata,
ydata,
p0=p0,
**kwargs,
)
[docs]
def configure_curve_fit(
*,
enable_diagnostics: bool = False,
enable_recovery: bool = True,
enable_caching: bool = True,
**default_kwargs: Any,
) -> Callable[..., tuple[np.ndarray, np.ndarray] | Any]:
"""Configure a curve_fit function with default settings.
Returns a callable that wraps curve_fit with pre-configured
defaults, allowing for consistent settings across an application.
Parameters
----------
enable_diagnostics : bool, default=False
Enable diagnostic reporting by default.
enable_recovery : bool, default=True
Enable automatic recovery by default.
enable_caching : bool, default=True
Enable JIT caching by default.
**default_kwargs : Any
Additional default keyword arguments.
Returns
-------
Callable
A configured curve_fit function.
Examples
--------
>>> curve_fit = configure_curve_fit(enable_diagnostics=True)
>>> popt, pcov = curve_fit(model, xdata, ydata)
"""
def configured_fit(
f: Callable[..., np.ndarray],
xdata: np.ndarray,
ydata: np.ndarray,
p0: np.ndarray | None = None,
sigma: np.ndarray | None = None,
bounds: tuple[np.ndarray, np.ndarray] | None = None,
**kwargs: Any,
) -> tuple[np.ndarray, np.ndarray] | Any:
"""Configured curve_fit with preset defaults."""
from nlsq.core.minpack import curve_fit
# Merge defaults with call kwargs
merged = {**default_kwargs, **kwargs}
# Apply configuration
if enable_diagnostics and "diagnostics" not in merged:
from nlsq.diagnostics.types import DiagnosticsConfig
merged["diagnostics"] = DiagnosticsConfig()
return curve_fit(
f,
xdata,
ydata,
p0=p0,
sigma=sigma,
bounds=bounds,
**merged,
)
return configured_fit