The Problem class

An analytical problem is a problem where the mathematical formulation of the various objectives is known, as opposed to a data-driven problem, where one may need to train surrogate models to proceed with optimization.

The Problem class is the way to define optimization problems in the DESDEO framework. Once defined, the same Problem class instance can be used to solve optimization problems using various EAs from the desdeo-emo package, or the more traditional methods from the desdeo-mcdm package.

This notebook will help you understand how to instantiate a analytical problem object from scratch. The notebook will also go over other abstractions, namely classes for defining the decision variables, objectives, and the constraints, and will go over the functionalities provided by the abstractions.

Multiobjective Optimization Problem

Let’s say that we have the following minimization problem:

\begin{equation} \begin{aligned} & \underset{\mathbf x}{\text{min}} & & y_1, y_2, y_3\\ & & & y_1 = x_1 + x_2 + x_3 \\ & & & y_2 = x_1 * x_2 * x_3 \\ & & & y_3 = x_1 * x_2 + x_3 \\ & \text{s.t.} & & -2 \leq x_1 \leq 5 \\ & & & -1 \leq x_2 \leq 10 \\ & & & -0 \leq x_3 \leq 3 \\ & & & x_1 + x_2 + x_3 \leq 10 \\ & & & \mathbf{x} \; \in S, \\ \end{aligned} \end{equation}

Variables

Before instantiating the problem instance, we have to create object to define each of the variables, objectives, and constraints.

The variable objects can be created with the desdeo_problem.Variable.Variable class. This object stores the information related to the variable (such as, lower bound, upper bound, and an initial value). This information is used by the methods whenever required (such as when setting box constraints on searching algorithms or recombination operators) and for displaying results to the decision maker. Use this class to create variable objects, one variable at a time.

To define multiple Variable instances easily, use the desdeo_problem.Variable.variable_builder function. The function takes in all the necessary information for all the variables at once, and returns a List of Variable instances, one for each decision variable.

Use the help() function to know more about any function/class in the desdeo framework.

[1]:
from desdeo_problem import variable_builder

help(variable_builder)
Help on function variable_builder in module desdeo_problem.problem.Variable:

variable_builder(names: List[str], initial_values: Union[List[float], numpy.ndarray], lower_bounds: Union[List[float], numpy.ndarray] = None, upper_bounds: Union[List[float], numpy.ndarray] = None) -> List[desdeo_problem.problem.Variable.Variable]
    Automatically build all variable objects.

    Arguments:
        names (List[str]): Names of the variables
        initial_values (np.ndarray): Initial values taken by the variables.
        lower_bounds (Union[List[float], np.ndarray], optional): Lower bounds of the
            variables. If None, it defaults to negative infinity. Defaults to None.
        upper_bounds (Union[List[float], np.ndarray], optional): Upper bounds of the
            variables. If None, it defaults to positive infinity. Defaults to None.

    Raises:
        VariableError: Lengths of the input arrays are different.

    Returns:
        List[Variable]: List of variable objects

Let’s build the Variable objects

[2]:
var_names = ["a", "b", "c"]  #  Make sure that the variable names are meaningful to you.

initial_values = [1, 1, 1]
lower_bounds = [-2, -1, 0]
upper_bounds = [5, 10, 3]

variables = variable_builder(var_names, initial_values, lower_bounds, upper_bounds)
[3]:
print("Type of \"variables\": ", type(variables))
print("Length of \"variables\": ", len(variables))
print("Type of the contents of \"variables\": ", type(variables[0]))
Type of "variables":  <class 'list'>
Length of "variables":  3
Type of the contents of "variables":  <class 'desdeo_problem.problem.Variable.Variable'>

Objectives

Objectives are defined using tha various objective classes found within the module desdeo_problem.Objective. To define an objective class instance, one needs to pass the following:

  1. Objective name/s (Required): Name of the objective (or list of names, for multiple objective). This information will be used when displaying results to the user. Hence, these names must be understandable to the user.

  2. Evaluator (Required for analytical/simulation based objectives): An evaluator is a python Callable which takes in the decision variables as it’s input and returns the corresponding objective values. This python function can be used to connect to simulators outside the DESDEO framework.

  3. Lower bound (Not required): A lower bound for the objective. This information can be used to generate approximate ideal/nadir point during optimization.

  4. Upper bound (Not required): An upper bound for the objective. This information can be used to generate approximate ideal/nadir point during optimization.

  5. maximize (Not required): This is a boolean value that determines whether an objective is to be maximized or minimized. This is False by default (i.e. the objective is minimized).

The DESDEO framework has the following classification for objectives, based on the kind of evaluater to be used:

  1. “Scalar” objectives: If an evaluator/simulator evaluates only one objective, the objective is defined as a Scalar objective. Use the desdeo_problem.Objective._ScalarObjective class to handle such cases.

  2. “Vector” objectives: If an evaluator evaluates and returns more than one objective at once, the set of objectives is defined as Vector objective. Use the desdeo_problem.Objective.VectorObjective class to handle such cases.

Note:_ScalarObjective will be depreciated in the future, and all of it’s functionality will be handled by the VectorObjective class, which will be renamed to, simply, Objective.

To define a problem instance, the objectives may be defined as all Scalar objectives, all Vector objectives, or a mix of both, depending upon the case.

Let’s see how to define and use both kinds of Objective classes:

[4]:
from desdeo_problem import ScalarObjective, VectorObjective

import numpy as np

Define the evaluators for the objectives. These evaluators should be python functions that take in the decision variable values and give out the objective value/s. The arguments of these evaluators are 2-D Numpy arrays.

[5]:
def obj1_2(x):  #  This is a "simulator" that returns more than one objective at a time. Hence, use VectorObjective
    y1 = x[:, 0] + x[:, 1] + x[:, 2]
    y2 = x[:, 0] * x[:, 1] * x[:, 2]
    return (y1, y2)


def obj3(x):  #  This is a "simulator" that returns only one objective at a time. Hence, use ScalarObjective
    y3 = x[:, 0] * x[:, 1] + x[:, 2]
    return y3

Define the objectives. For this, you need the names of the objectives, and the evaluators defined above. If an evaluator returns multiple objective values, use the VectorObjective class to define those objectives. If an evaluator returns objective values for only one objective, either VectorObjective or ScalarObjective can be used.

If using VectorObjective, names should be provided in a list.

Additionaly, bounds of the objective values can also be provided.

[6]:
f1_2 = VectorObjective(["y1", "y2"], obj1_2)
f3 = ScalarObjective("y3", obj3, maximize=True)  # Note: f3 = VectorObjective(["y3"], obj3) will also work.

Constraints

Constraint may depend on the decision variable values, as well as the objective function.

The constraint should be defined so, that when evaluated, it should return a positive value, if the constraint is adhered to, and a negative, if the constraint is breached.

[7]:
from desdeo_problem import ScalarConstraint

const_func = lambda x, y: 10 - (x[:, 0] + x[:, 1] + x[:, 2])

# Args: name, number of variables, number of objectives, callable

cons1 = ScalarConstraint("c_1", 3, 3, const_func)

Creating the Problem object

Now that we have all the building blocks, we can create the problem object, using the desdeo_problem.Problem.MOProblem class.

Provide objectives, variables and constraints in lists.

[8]:
from desdeo_problem import MOProblem

prob = MOProblem(objectives=[f1_2, f3], variables=variables, constraints=[cons1])

The problem class provides abstractions such as the evaluate method. The method evaluates all the objective and constraint values for a given set of decision variables (in a numpy array), using the evaluators.

The abstraction also provides methods such as train and surrogate_evaluate for data driven problem. These will be tackled in the next notebook.

The output is a NamedTuple object. It contains the following elements:

  1. objectives: Contains the objective values

  2. fitness: Contains the fitness values. Fitness is either equal to the objective value, or equal to (-1 * objective value), depending upon whether the objective is to be minimized or maximized respectively. The optimization methods in the DESDEO framework internally use this value, rather than the values contained in output.objectives

  3. constraints: Contains constraint violation values.

  4. uncertainity: Contains the quantification of “uncertainity” of the evaluation

All of these values can be accessed in different ways, as shown below.

Note: Input as list of lists is not supported

[9]:
data = np.asarray([[1, -1, 0], [5, 5, 2]])
res= prob.evaluate(data)
[10]:
print(res)
# Note the sign reversal in the third objective and third fitness values because of maximization.
Evaluation Results Object
Objective values are:
[[ 0. 12. -1.]
 [ 0. 50. 27.]]
Constraint violation values are:
[[10.]
 [-2.]]
Fitness values are:
[[  0.  12.   1.]
 [  0.  50. -27.]]
Uncertainity values are:
[[nan nan nan]
 [nan nan nan]]

[11]:
print("The objective values for the given set of decision variables are: \n", res.objectives)
print("The constraint violation for the given set of decision variables are:\n", res.constraints)
The objective values for the given set of decision variables are:
 [[ 0. 12. -1.]
 [ 0. 50. 27.]]
The constraint violation for the given set of decision variables are:
 [[10.]
 [-2.]]
[12]:
res
[12]:
EvaluationResults(objectives=array([[ 0., 12., -1.],
       [ 0., 50., 27.]]), fitness=array([[  0.,  12.,   1.],
       [  0.,  50., -27.]]), constraints=array([[10.],
       [-2.]]), uncertainity=array([[nan, nan, nan],
       [nan, nan, nan]]))
[ ]: