Source code for gemseo.post.pareto_front

# -*- coding: utf-8 -*-
# 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 - API and implementation and/or documentation
#        :author: Francois Gallard
#        :author: Damien Guenot
#    OTHER AUTHORS   - MACROSCOPIC CHANGES
"""A Pareto Front."""
from __future__ import division, unicode_literals

import logging
from typing import List, Optional, Sequence, Tuple

from numpy import full, ndarray

from gemseo.algos.pareto_front import generate_pareto_plots
from gemseo.post.opt_post_processor import OptPostProcessor

LOGGER = logging.getLogger(__name__)


[docs]class ParetoFront(OptPostProcessor): """Compute the Pareto front Search for all non dominated points. For each point, check if it exists ``j`` such that there is no lower value for ``obj_values[:,j]`` that does not degrade at least one other objective ``obj_values[:,i]``. Generate a plot or a matrix of plots if there are more than 2 objectives. Plot in red the locally non dominated points for the currrent two objectives. Plot in green the globally (all objectives) Pareto optimal points. """ def _plot( self, objectives=None, # type: Optional[Sequence[str]] objectives_labels=None, # type: Optional[Sequence[str]] figsize_x=10.0, # type: float figsize_y=10.0, # type: float show_non_feasible=True, # type: bool ): # type: (...) -> None """ Args: objectives: The functions names or design variables to plot. If None, use the objective function (may be a vector). objectives_labels: The labels of the objective components. If None, use the objective name suffixed by an index. figsize_x: The size of figure in the horizontal direction (inches). figsize_y: The size of figure in the vertical direction (inches). show_non_feasible: If True, show the non feasible points in the plot. Raises: ValueError: If the numbers of objectives and objectives labels are different. """ if objectives is None: objectives = [self.opt_problem.objective.name] all_funcs = self.opt_problem.get_all_functions_names() all_dv_names = self.opt_problem.design_space.variables_names vals, vname = self.__compute_names_and_values( all_dv_names, all_funcs, objectives ) non_feasible_samples = self.__compute_non_feasible_samples(vals) if objectives_labels is not None: if len(vname) != len(objectives_labels): raise ValueError( "objective_labels shall have the same dimension as vname." ) vname = objectives_labels fig = generate_pareto_plots( vals, vname, figsize=(figsize_x, figsize_y), non_feasible_samples=non_feasible_samples, show_non_feasible=show_non_feasible, ) self._add_figure(fig) def __compute_names_and_values( self, all_dv_names, # type: Sequence[str] all_funcs, # type: Sequence[str] objectives, # type: Sequence[str] ): # type: (...) -> Tuple[ndarray,List[str]] """Compute the names and values of the objective and design variables. Args: add_dv_names: The design variables names. all_funcs: The function names. objectives: The objective names. Returns: The sample values and the sample names. """ # TODO: Those lines are not covered by the tests. It has to be # investigated to see if it's dead code. if not objectives: # The function list only contains design variables vals = self.database.get_x_history() vname = self.database.set_dv_names(vals[0].shape[0]) else: design_variables = [] for func in list(objectives): self.__check_objective_name(all_dv_names, all_funcs, func, objectives) self.__move_objective_to_design_variable( design_variables, func, objectives ) if not design_variables: design_variables = None add_dv = False else: add_dv = True vals, vname, _ = self.database.get_history_array( objectives, design_variables, add_dv=add_dv ) return vals, vname def __check_objective_name( self, all_dv_names, # type: Sequence[str] all_funcs, # type: Sequence[str] func, # type: str objectives, # type: Sequence[str] ): # type: (...) -> None """Check that the objective name is valid. Args: add_dv_names: The design variables names. all_funcs: The function names. func: The function name. objectives: The objectives names. Raises: ValueError: If the objective name is not valid. """ if func not in all_funcs and func not in all_dv_names: min_f = "-" + func == self.opt_problem.objective.name if min_f and not self.opt_problem.minimize_objective: objectives[objectives.index(func)] = "-" + func else: msg = ( "Cannot build Pareto front," " Function {} is neither among" " optimization problem functions: " "{} nor design variables: {}." ) msg = msg.format(func, str(all_funcs), str(all_dv_names)) raise ValueError(msg) def __move_objective_to_design_variable( self, design_variables, # type: Sequence[str] func, # type: str objectives, # type: Sequence[str] ): # type: (...) -> None """Move an objective to a design variable. If the given function is a design variable, then move it from the objectives to the design_variables. Args: design_variables: The design variables. func: The function name. objectives: The objectives names. """ if func in self.opt_problem.design_space.variables_names: objectives.remove(func) design_variables.append(func) def __compute_non_feasible_samples( self, vals # type: ndarray ): # type: (...) -> ndarray """Compute the non-feasible indexes. Args: vals: The sample values. Returns: An array of size ``n_samples``, True if the point is non feasible """ x_feasible, _ = self.opt_problem.get_feasible_points() feasible_indexes = [self.database.get_index_of(x) for x in x_feasible] is_non_feasible = full(vals.shape[0], True) is_non_feasible[feasible_indexes] = False return is_non_feasible