Source code for optimagic.optimization.algorithm
import typing
import warnings
from abc import ABC, ABCMeta, abstractmethod
from dataclasses import dataclass, replace
from typing import Any
import numpy as np
import pydantic
from numpy.typing import NDArray
from typing_extensions import Self
from optimagic.exceptions import InvalidAlgoInfoError, InvalidAlgoOptionError
from optimagic.logging.types import StepStatus
from optimagic.optimization.history import History
from optimagic.optimization.internal_optimization_problem import (
InternalOptimizationProblem,
)
from optimagic.typing import (
STRICT_PYDANTIC_CONFIG,
AggregationLevel,
validated_dataclass,
)
def _algo_info_error(e: pydantic.ValidationError) -> Exception:
msg = f"The following arguments to AlgoInfo or `mark.minimizer` are invalid:\n\n{e}"
return InvalidAlgoInfoError(msg)
def _internal_optimize_result_error(e: pydantic.ValidationError) -> Exception:
msg = f"The following arguments to InternalOptimizeResult are invalid:\n\n{e}"
return TypeError(msg)
@validated_dataclass(config=STRICT_PYDANTIC_CONFIG, make_error=_algo_info_error)
@dataclass(frozen=True)
class AlgoInfo:
name: str
solver_type: AggregationLevel
is_available: bool
is_global: bool
needs_jac: bool
needs_hess: bool
needs_bounds: bool
supports_parallelism: bool
supports_bounds: bool
supports_infinite_bounds: bool
supports_linear_constraints: bool
supports_nonlinear_constraints: bool
disable_history: bool = False
experimental: bool = False
[docs]
@validated_dataclass(
config=STRICT_PYDANTIC_CONFIG, make_error=_internal_optimize_result_error
)
@dataclass(frozen=True)
class InternalOptimizeResult:
"""Internal representation of the result of an optimization problem.
Args:
x: The optimal parameters.
fun: The function value at the optimal parameters.
success: Whether the optimization was successful.
message: A message from the optimizer.
status: The status of the optimization.
n_fun_evals: The number of function evaluations.
n_jac_evals: The number of gradient or jacobian evaluations.
n_hess_evals: The number of Hessian evaluations.
n_iterations: The number of iterations.
jac: The Jacobian of the objective function at the optimal parameters.
hess: The Hessian of the objective function at the optimal parameters.
hess_inv: The inverse of the Hessian of the objective function at the optimal
parameters.
max_constraint_violation: The maximum constraint violation.
info: Additional information from the optimizer.
"""
x: NDArray[np.float64]
fun: float | NDArray[np.float64]
success: bool | None = None
message: str | None = None
status: int | None = None
n_fun_evals: int | None = None
n_jac_evals: int | None = None
n_hess_evals: int | None = None
n_iterations: int | None = None
jac: NDArray[np.float64] | None = None
hess: NDArray[np.float64] | None = None
hess_inv: NDArray[np.float64] | None = None
max_constraint_violation: float | None = None
info: dict[str, typing.Any] | None = None
history: History | None = None
multistart_info: dict[str, typing.Any] | None = None
class AlgorithmMeta(ABCMeta):
"""Metaclass to get repr, algo_info and name for classes, not just instances."""
def __repr__(self) -> str:
if hasattr(self, "__algo_info__") and self.__algo_info__ is not None:
out = f"om.algos.{self.__algo_info__.name}"
else:
out = self.__class__.__name__
return out
@property
def name(self) -> str:
if hasattr(self, "__algo_info__") and self.__algo_info__ is not None:
out = self.__algo_info__.name
else:
out = self.__class__.__name__
return out
@property
def algo_info(self) -> AlgoInfo:
if not hasattr(self, "__algo_info__") or self.__algo_info__ is None:
msg = (
f"The algorithm {self.name} does not have have the __algo_info__ "
"attribute. Use the `mark.minimizer` decorator to add this attribute."
)
raise AttributeError(msg)
return self.__algo_info__
[docs]
@dataclass(frozen=True)
class Algorithm(ABC, metaclass=AlgorithmMeta):
"""Base class for all optimization algorithms in optimagic.
To add an optimizer to optimagic you need to subclass Algorithm and overide the
``_solve_internal_problem`` method.
"""
@abstractmethod
def _solve_internal_problem(
self, problem: InternalOptimizationProblem, x0: NDArray[np.float64]
) -> InternalOptimizeResult:
pass
[docs]
def with_option(self, **kwargs: Any) -> Self:
"""Create a modified copy with the given options."""
valid_keys = set(self.__dataclass_fields__) - {"__algo_info__"}
invalid = set(kwargs) - valid_keys
if invalid:
raise InvalidAlgoOptionError(
f"The keyword arguments {invalid} are not valid options for "
f"the algorithm {self.name}"
)
return replace(self, **kwargs)
[docs]
def with_stopping(self, **kwargs: Any) -> Self:
"""Create a modified copy with the given stopping options."""
options = {}
for k, v in kwargs.items():
if k.startswith("stopping_"):
options[k] = v
else:
options[f"stopping_{k}"] = v
return self.with_option(**options)
[docs]
def with_convergence(self, **kwargs: Any) -> Self:
"""Create a modified copy with the given convergence options."""
options = {}
for k, v in kwargs.items():
if k.startswith("convergence_"):
options[k] = v
else:
options[f"convergence_{k}"] = v
return self.with_option(**options)
[docs]
def solve_internal_problem(
self,
problem: InternalOptimizationProblem,
x0: NDArray[np.float64],
step_id: int,
) -> InternalOptimizeResult:
"""Solve the internal optimization problem.
This method is called internally by `minimize` or `maximize` to solve the
internal optimization problem and process the results.
"""
problem = problem.with_new_history().with_step_id(step_id)
if problem.logger:
problem.logger.step_store.update(
step_id, {"status": str(StepStatus.RUNNING.value)}
)
result = self._solve_internal_problem(problem, x0)
if (not self.algo_info.disable_history) and (result.history is None):
result = replace(result, history=problem.history)
if problem.logger:
problem.logger.step_store.update(
step_id, {"status": str(StepStatus.COMPLETE.value)}
)
return result
def with_option_if_applicable(self, **kwargs: Any) -> Self:
"""Call with_option only with applicable keyword arguments."""
valid_keys = set(self.__dataclass_fields__) - {"__algo_info__"}
invalid = set(kwargs) - valid_keys
if invalid:
msg = (
"The following algo_options were ignored because they are not "
f"compatible with {self.name}:\n\n {invalid}"
)
warnings.warn(msg)
kwargs = {k: v for k, v in kwargs.items() if k in valid_keys}
return self.with_option(**kwargs)
@property
def name(self) -> str:
"""The name of the algorithm."""
# cannot call algo_info here because it would be an infinite recursion
if hasattr(self, "__algo_info__") and self.__algo_info__ is not None:
return self.__algo_info__.name
return self.__class__.__name__
@property
def algo_info(self) -> AlgoInfo:
"""Information about the algorithm."""
if not hasattr(self, "__algo_info__") or self.__algo_info__ is None:
msg = (
f"The algorithm {self.name} does not have have the __algo_info__ "
"attribute. Use the `mark.minimizer` decorator to add this attribute."
)
raise AttributeError(msg)
return self.__algo_info__