Source code for gemseo_pymoo.post.compromise

# Copyright 2022 Airbus SAS
# 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:
#    INITIAL AUTHORS - initial API and implementation and/or initial
#                           documentation
#        :author: Gabriel Max DE MENDONÇA ABRANTES
"""Compromise points for multi-criteria decision-making."""
from __future__ import annotations

import logging
from typing import Any

from numpy import atleast_2d
from numpy import ndarray
from numpy import vstack
from pymoo.factory import get_decomposition

from gemseo_pymoo.algos.opt_result_mo import Pareto
from gemseo_pymoo.post.scatter_pareto import ScatterPareto

LOGGER = logging.getLogger(__name__)


[docs]class Compromise(ScatterPareto): """Scatter plot with pareto front and compromise points. See `Compromise Programming <https://pymoo.org/mcdm/index.html#Compromise-Programming>`_. """ fig_title = "Compromise Points" fig_name_prefix = "compromise" SCALARIZATION_FUNCTIONS: dict[str, str] = { "weighted-sum": "Weighted Sum", "tchebi": "Tchebysheff", "pbi": "PBI", "asf": "Achievement Scalarization Function", "aasf": "Augmented Achievement Scalarization Function", "perp_dist": "Perpendicular Distance", } """The alias and the corresponding name of the scalarization functions.""" def _plot( self, scalar_name: str = "weighted-sum", weights: ndarray | None = None, plot_extra: bool = True, plot_legend: bool = True, plot_arrow: bool = False, **scalar_options: Any, ) -> None: """Scatter plot of the pareto front along with the compromise points. The compromise points are calculated using a `scalarization function <https://pymoo.org/misc/decomposition.html>`_). Args: scalar_name: The name of the scalarization function to use. weights: The weights for the scalarization function. If None, a normalized array is used, e.g. [1./n, 1./n, ..., 1./n] for an optimization problem with n-objectives. plot_extra: Whether to plot the extra pareto related points, i.e. ``utopia``, ``nadir`` and ``anchor`` points. plot_legend: Whether to show the legend. plot_arrow: Whether to plot arrows connecting the utopia point to the compromise points. The arrows are annotated with the ``2-norm`` ( `Euclidian distance <https://en.wikipedia.org/wiki/Euclidean_distance>`_ ) of the vector represented by the arrow. **scalar_options: The keyword arguments for the scalarization function. Raises: ValueError: Either if the scalarization function name is unknown or if the number of weights does not match the number of objectives. """ if scalar_name not in self.SCALARIZATION_FUNCTIONS: raise ValueError( "The scalarization function name must be one of the " f"following: {self.SCALARIZATION_FUNCTIONS}" ) # Objectives. n_obj = self.opt_problem.objective.dim # Default weights. if weights is None: weights = [1.0 / n_obj] * n_obj # Ensure correct dimension and type. weights = atleast_2d(weights).astype(float) # Check weight's dimension. if weights.shape[1] != n_obj: raise ValueError( "You must provide exactly one weight for each objective function!" ) # Create Pareto object. pareto = Pareto(self.opt_problem) # Initialize decomposition function. decomposition = get_decomposition(scalar_name, **scalar_options) # Prepare points to plot. points = [] # Points' coordinates. point_labels = [] # Points' labels. for weight in weights: # Apply decomposition. d_res = decomposition.do( pareto.front, weight, utopian_point=pareto.utopia, nadir_point=pareto.anti_utopia, **scalar_options, ) # Best value according to the scalarization function. d_min = d_res.min() # Index where the minimum value is located (at the pareto front). d_idx = d_res.argmin() # Point's coordinates. points.append(pareto.front[d_idx]) # Point's label. float_format = ".2e" if abs(d_min) > 1e3 else ".2f" point_labels.append(f"s({weight}) = {d_min:{float_format}}") # Concatenate points to plot. points = vstack(points) # Extra figure options. self.fig_title = ( f"{self.fig_title}\n(s = {self.SCALARIZATION_FUNCTIONS[scalar_name]})" ) # Update name's prefix with current decomposition function's name. self.fig_name_prefix += f"_{scalar_name}" super()._plot(points, point_labels, plot_extra, plot_legend, plot_arrow)