Source code for scalarization.EpsilonConstraintMethod

import numpy as np

from desdeo_tools.scalarization import Scalarizer
from desdeo_tools.solver.ScalarSolver import ScalarMinimizer
from typing import Optional, Callable, Union


[docs]class ECMError(Exception): """Raised when an error related to the Epsilon Constraint Method is encountered. """
[docs]class EpsilonConstraintMethod: """A class to represent a class for scalarizing MOO problems using the epsilon constraint method. Attributes: objectives (Callable): Objective functions. to_be_minimized (int): Integer representing which objective function should be minimized. epsilons (np.ndarray): Upper bounds chosen by the decison maker. Epsilon constraint functions are defined in a following form: f_i(x) <= eps_i If the constraint function is of form f_i(x) >= eps_i Remember to multiply the epsilon value with -1! constraints (Optional[Callable]): Function that returns definitions of other constraints, if existing. """ def __init__( self, objectives: Callable, to_be_minimized: int, epsilons: np.ndarray, constraints: Optional[Callable] ): self.objectives = objectives self._to_be_minimized = to_be_minimized self.epsilons = epsilons self.constraints = constraints
[docs] def evaluate_constraints(self, xs) -> np.ndarray: """ Returns values of constraints with given decison variables. Args: xs (np.ndarray): Decision variables. Returns: Values of constraint functions (both "original" constraints as well as epsilon constraints) in a vector. """ xs = np.atleast_2d(xs) # evaluate epsilon constraint function "left-side" values with given decision variables epsilon_left_side = np.array( [val for nrow, row in enumerate(self.objectives(xs)) for ival, val in enumerate(row) if ival != self._to_be_minimized ]) if len(epsilon_left_side) != len(self.epsilons): msg = ("The lenght of the epsilons array ({}) must match the total number of objectives - 1 ({})." ).format(len(self.epsilons), len(self.objectives(xs)) - 1) raise ECMError(msg) # evaluate values of epsilon constraint functions e: np.ndarray = np.array([-(f - v) for f, v in zip(epsilon_left_side, self.epsilons)]) if self.constraints(xs) is not None: c = self.constraints(xs) return np.concatenate([c, e], axis=None) # does it work with multiple constraints? else: return e
[docs] def __call__(self, objective_vector: np.ndarray) -> Union[float, np.ndarray]: """ Returns the value of objective function to be minimized. Args: objective_vector (np.ndarray): Values of objective functions. Returns: Value of objective function to be minimized. """ if np.shape(objective_vector)[0] > 1: # more rows than one return np.array([objective_vector[i][self._to_be_minimized] for i, _ in enumerate(objective_vector)]) else: return objective_vector[0][self._to_be_minimized]
# Testing the method if __name__ == "__main__": # 1. Define objective functions, bounds and constraints
[docs] def volume(r, h): return np.pi * r ** 2 * h
def area(r, h): return 2 * np.pi ** 2 + np.pi * r * h # add third objective def weight(v): return 0.01 * v def objective(xs): # xs is a 2d array like, which has different values for r and h on its first and second columns respectively. xs = np.atleast_2d(xs) return np.stack((volume(xs[:, 0], xs[:, 1]), -area(xs[:, 0], xs[:, 1]), weight(volume(xs[:, 0], xs[:, 1])))).T # bounds for decision variables r_bounds = np.array([2.5, 15]) h_bounds = np.array([10, 50]) bounds = np.stack((r_bounds, h_bounds)) # constraints def con_golden(xs): # constraints are defined in DESDEO in a way were a positive value indicates an agreement with a constraint, and # a negative one a disagreement. xs = np.atleast_2d(xs) return -(xs[:, 0] / xs[:, 1] - 1.618) # 2. Apply Epsilon contraint method # index of which objective function to minimize obj_min = 2 # set upper bound(s) for the other objectives, in the same order than which corresponding objective functions # are defined epsil = np.array([2000, -100]) # multiply the epsilons with -1, if the constraint is of form f_i(x) >= e_i # create an instance of EpsilonConstraintMethod-class for given problem eps = EpsilonConstraintMethod(objective, obj_min, epsil, constraints=con_golden) # constraint evaluator, used by the solver cons_evaluate = eps.evaluate_constraints # scalarize scalarized_objective = Scalarizer(objective, eps) print(scalarized_objective) # 3. Solve # starting point x0 = np.array([2, 11]) minimizer = ScalarMinimizer(scalarized_objective, bounds, constraint_evaluator=cons_evaluate, method=None) # minimize res = minimizer.minimize(x0) final_r, final_h = res["x"][0], res["x"][1] final_obj = objective(res["x"]).squeeze() final_V, final_A, final_W = final_obj[0], final_obj[1], final_obj[2] print(f"Final cake specs: radius: {final_r}cm, height: {final_h}cm.") print(f"Final cake dimensions: volume: {final_V}, area: {-final_A}, weight: {final_W}.") print(final_r / final_h) print(res)