Source code for gemseo.algos.opt_result

# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
# Contributors:
#    Francois Gallard
#    Matthias De Lozzo
"""Optimization result."""

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import field
from dataclasses import fields
from typing import TYPE_CHECKING
from typing import Any
from typing import ClassVar
from typing import Union

from numpy import ndarray

from gemseo.utils.metaclasses import ABCGoogleDocstringInheritanceMeta
from gemseo.utils.string_tools import MultiLineString

if TYPE_CHECKING:
    from collections.abc import Mapping

    from gemseo.algos.opt_problem import OptimizationProblem

Value = Union[str, int, bool, ndarray]


# TODO: API: Rename the module to optimization_result.
[docs] @dataclass class OptimizationResult(metaclass=ABCGoogleDocstringInheritanceMeta): """The result of an optimization.""" x_0: ndarray | None = None """The initial values of the design variables.""" x_0_as_dict: dict[str, ndarray] = field(default_factory=dict) """The design variable names bound to the initial design values.""" x_opt: ndarray | None = None """The optimal values of the design variables, called the *optimum*.""" x_opt_as_dict: dict[str, ndarray] = field(default_factory=dict) """The design variable names bound to the optimal design values.""" f_opt: ndarray | None = None """The value of the objective function at the optimum.""" objective_name: str = "" """The name of the objective function.""" status: int | None = None """The status of the optimization.""" optimizer_name: str | None = None """The name of the optimizer.""" message: str | None = None """The message returned by the optimizer.""" n_obj_call: int | None = None """The number of calls to the objective function.""" n_grad_call: int | None = None """The number of calls to the gradient function.""" n_constr_call: int | None = None """The number of calls to the constraints function.""" is_feasible: bool = False """Whether the solution is feasible.""" optimum_index: int | None = None """The zero-based position of the optimum in the optimization history.""" constraint_values: Mapping[str, ndarray] | None = None """The values of the constraints at the optimum.""" constraints_grad: Mapping[str, ndarray | None] | None = None """The values of the gradients of the constraints at the optimum.""" __CGRAD_TAG = "constr_grad:" __CGRAD_TAG_LEN = len(__CGRAD_TAG) __C_TAG = "constr:" __C_TAG_LEN = len(__C_TAG) __CONSTRAINTS_VALUES = "constraint_values" __CONSTRAINTS_GRAD = "constraints_grad" __NOT_DICT_KEYS: ClassVar[list[str]] = [__CONSTRAINTS_VALUES, __CONSTRAINTS_GRAD] @property def __string_representation(self) -> MultiLineString: """The string representation of the optimization result.""" mls = MultiLineString() mls.add("Optimization result:") mls.indent() mls.add("Design variables: {}", self.x_opt) mls.add("Objective function: {}", self.f_opt) mls.add("Feasible solution: {}", self.is_feasible) return mls def __repr__(self) -> str: return str(self.__string_representation) def _repr_html_(self) -> str: return self.__string_representation._repr_html_() @property def _strings(self) -> list[MultiLineString]: """The 3 multi-line strings used by __str__ and for logging. The second one can be logged with either an INFO or a WARNING level according to the feasibility of the solution. """ strings = [] msg = MultiLineString() msg.add("Optimization result:") msg.indent() msg.add("Optimizer info:") msg.indent() msg.add("Status: {}", self.status) msg.add("Message: {}", self.message) if self.n_obj_call is not None: msg.add( "Number of calls to the objective function by the optimizer: {}", self.n_obj_call, ) msg.dedent() msg.add("Solution:") msg.indent() strings.append(msg) msg = MultiLineString() if self.constraint_values: not_ = "" if self.is_feasible else "not " msg.indent() msg.indent() msg.add("The solution is {}feasible.", not_) strings.append(msg) msg = MultiLineString() msg.indent() msg.indent() msg.add("Objective: {}", self.f_opt) if self.constraint_values and len(self.constraint_values) < 20: msg.add("Standardized constraints:") msg.indent() for name, value in sorted(self.constraint_values.items()): msg.add("{} = {}", name, value) strings.append(msg) return strings def __str__(self) -> str: return str(self._strings[0] + self._strings[1] + self._strings[2])
[docs] def to_dict(self) -> dict[str, Value]: """Convert the optimization result to a dictionary. The keys are the names of the optimization result fields, except for the constraint values and gradients. The key ``"constr:y"`` maps to ``result.constraint_values["y"]`` while ``"constr_grad:y"`` maps to ``result.constraints_grad["y"]``. Returns: A dictionary representation of the optimization result. """ dict_ = { k: v for k, v in self.__dict__.items() if k not in self.__NOT_DICT_KEYS } for mapping, prefix in [ (self.constraint_values, self.__C_TAG), (self.constraints_grad, self.__CGRAD_TAG), ]: if mapping is not None: for key, value in mapping.items(): dict_[f"{prefix}{key}"] = value return dict_
[docs] @classmethod def from_dict(cls, dict_: Mapping[str, Value]) -> OptimizationResult: """Create an optimization result from a dictionary. Args: dict_: The dictionary representation of the optimization result. The keys are the names of the optimization result fields, except for the constraint values and gradients. The value associated with the key ``"constr:y"`` will be stored in ``result.constraint_values["y"]`` while the value associated with the key ``"constr_grad:y"`` will be stored in ``result.constraints_grad["y"]``. Returns: An optimization result. """ cstr = {} cstr_grad = {} for key, value in dict_.items(): if key.startswith(cls.__C_TAG): cstr[key[cls.__C_TAG_LEN :]] = value if key.startswith(cls.__CGRAD_TAG): cstr_grad[key[cls.__CGRAD_TAG_LEN :]] = value optimization_result = { key.name: dict_[key.name] for key in fields(cls) if key.name in dict_ } optimization_result.update({ cls.__CONSTRAINTS_VALUES: cstr or None, cls.__CONSTRAINTS_GRAD: cstr_grad or None, }) return cls(**optimization_result)
[docs] @classmethod def from_optimization_problem( cls, problem: OptimizationProblem, **fields_ ) -> OptimizationResult: """Create an optimization result from an optimization problem. Args: problem: The optimization problem. **fields_: The fields of the :class:`.OptimizationResult` that cannot be deduced from the optimization problem; e.g. ``"optimizer_name"``. Returns: The optimization result associated with the optimization problem. """ if not problem.database: return cls(n_obj_call=0, **fields_) x_0 = problem.database.get_x_vect(1) # compute the best feasible or infeasible point f_opt, x_opt, is_feas, c_opt, c_opt_grad = problem.get_optimum() if ( f_opt is not None and not problem.minimize_objective and not problem.use_standardized_objective ): f_opt = -f_opt objective_name = problem.objective.original_name else: objective_name = problem.objective.name if x_opt is None: optimum_index = None else: optimum_index = problem.database.get_iteration(x_opt) - 1 fields_["objective_name"] = objective_name fields_.update(cls._get_additional_fields(problem)) return cls( x_0=x_0, x_0_as_dict=problem.design_space.array_to_dict(x_0), x_opt=x_opt, x_opt_as_dict=problem.design_space.array_to_dict(x_opt), f_opt=f_opt, n_obj_call=problem.objective.n_calls, is_feasible=is_feas, constraint_values=c_opt, constraints_grad=c_opt_grad, optimum_index=optimum_index, **fields_, )
@classmethod def _get_additional_fields(cls, problem: OptimizationProblem) -> dict[str, Any]: """Return the names and values of the additional fields. Args: problem: The optimization problem used to get the additional fields. Returns: The names and values of the additional fields. """ return {}