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:
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.
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.Lower bound (Not required): A lower bound for the objective. This information can be used to generate approximate ideal/nadir point during optimization.
Upper bound (Not required): An upper bound for the objective. This information can be used to generate approximate ideal/nadir point during optimization.
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:
“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.“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:
objectives
: Contains the objective valuesfitness
: 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.objectivesconstraints
: Contains constraint violation values.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]]))
[ ]: