A full example: from problem definition to solutions using interactive multiobjective optimization methods¶
In this example, we will see how we can define a simple problem and how to solve it using an interactive multiobjective optimization method.
Defining a multiobjective optimization problem¶
As an example, consider the following multiobjective optimization problem:
$$ \begin{align*} \min f_1(\mathbf{x}) &= x_1^2 - c_1\sin(x_2) \\ \min f_2(\mathbf{x}) &= x_2^2 - \cos(3x_1) \\ \text{s.t.}\quad & g_1(\mathbf{x}) = x_1 + x_2 \leq 10, \\ & -5 \leq x_1 \leq 5, \\ & -5 \leq x_2 \leq 5, \\ & c_1 = 1.5. \end{align*} $$
We see that we have two objective functions, $f_1$ and $f_2$, two decision variables, $\mathbf{x} = (x_1$, $x_2),$ a constant, $c_1 = 2.5$, and a constraint $g_1$. The values of the decision variables are also bound to be between $-5$ and $5$.
To begin, we will need to import relevant code from DESDEO first:
# These are to just suppress warnings in the outputs of the example
import warnings
from desdeo.problem import Constant, Constraint, ConstraintTypeEnum, Objective, Problem, Variable, VariableTypeEnum
warnings.filterwarnings("ignore")
Defining constants and variables¶
Next, we will define the constants and variables. With constants and variables, the attribute symbol is very important, as it will be used later in function definitions.
variable_x_1 = Variable(
name="The first variable, x_1",
symbol="x_1",
variable_type=VariableTypeEnum.real,
lowerbound=-5.0,
upperbound=5.0,
initial_value=1.0,
)
variable_x_2 = Variable(
name="The first variable, x_2",
symbol="x_2",
variable_type=VariableTypeEnum.real,
lowerbound=-5.0,
upperbound=5.0,
initial_value=1.0,
)
We have defined the variables to be a real numbers by setting the attribute variable_type=VariableTypeEnum.real, and we have bound their values by setting the lowerbound and upperbound attributes. The initial_value of the variables have also been set. Notice that the name of the variable is only important in providing information about the variable.
Similar to variables, we can define our constant $c_1$:
constant_c_1 = Constant(name="The constant c_1", symbol="c_1", value=1.5)
A constant has not bounds since its value, by definition, is not going to change.
Defining constraints and objective functions¶
We can now proceed to defining objective and constraint functions:
objective_f_1 = Objective(
name="Objective f_1",
symbol="f_1",
func="x_1**2 - c_1*Sin(x_2)",
maximize=False,
is_convex=False,
is_linear=False,
is_twice_differentiable=True,
)
objective_f_2 = Objective(
name="Objective f_2",
symbol="f_2",
func="x_2**2 - Cos(3*x_1)",
maximize=False,
is_convex=False,
is_linear=False,
is_twice_differentiable=True,
)
Similar to variables and constants, we have a name and a symbol attribute. We now also have a func attribute, which is the mathematical representation of the objective function. Notice how we have utilized
the symbols we defined earlier for the variables and constants in the func attribute. Since we are minimizing both objective functions, we have set maximize=False. Lastly, we have the attributes is_linear, is_convex,
and is_twice_differentiable, which tell us whether the objective function is convex, linear, or differentiable, respectively. Since our objective functions are neither convex or linear, the first two of these attributes
are set to False, while the last one is set to True, because the functions are (twice) differentiable in $x_1$ and $x_2$.
Our constraint $g_1$ is defined similarly to objective functions:
constraint_g_1 = Constraint(
name="Constraint g_1",
symbol="g_1",
func="x_1 + x_2 - 10",
cons_type=ConstraintTypeEnum.LTE,
is_linear=True,
is_convex=True,
is_twice_differentiable=True,
)
One might notice that the func of the constraint is not exactly the same as in our problem definition. This is because in DESDEO, constraints are expected in a standard form, where inequality constraints $g$ and equality
constraints $h$ are defines as $g \leq 0$ and $h = 0$. We readily see that if we define $g_1 = x_1 + x_2 - 10$ it is now congruent with this standard form and equivalent to the original constraint in our problem.
To express whether we are dealing with an equality or inequality constraint, we provide the attribute const_type, which we set to be ConstraintTypeEnum.LTE to express a constraint of type "less than or equal" (for an quality constraint, we would set the attribute to ConstraintTypeEnum.EQ instead.) The rest of the attributes are the same as found when defining an instance of Objective.
Putting it all together¶
We can now define our multiobjective optimization problem as an instance of the Problem class, which is used to represent all kinds of multiobjective optimization problems in DESDEO:
problem = Problem(
name="Example problem",
description="This problem is a simple example on how to define problems in DESDEO.",
constants=[constant_c_1],
variables=[variable_x_1, variable_x_2],
objectives=[objective_f_1, objective_f_2],
constraints=[constraint_g_1],
)
And that is it! We are now ready to do all kinds of interesting things with our problem. We will begin by calculating its ideal point, and approximating its nadir point.
Ideal and nadir points¶
Because the scalarization functions required by the interactive method we are going to apply require an ideal point and an approximation of the nadir point, we need to calculate them next. Luckily, this is very straightforward if we utilize the payoff-table method, which is well suited for the purpose of this example:
from desdeo.tools import payoff_table_method
ideal, nadir = payoff_table_method(problem)
We can then update our problem with the new ideal and nadir point values:
problem = problem.update_ideal_and_nadir(new_ideal=ideal, new_nadir=nadir)
Solving the problem using the reference point method¶
We are now ready to solve the problem utilizing an interactive multiobjective optimization method found in DESDEO. We will be utilizing the reference point method.
Because the reference point method requires a reference point, it might be a good idea to inspect the ideal and nadir points we just calculated to get an idea of the ranges for the two objective functions $f_1$ and $f_2$:
print(f"Ideal values: {problem.get_ideal_point()}")
print(f"Nadir values: {problem.get_nadir_point()} (approximations!)")
Ideal values: {'f_1': -1.5, 'f_2': -1.0}
Nadir values: {'f_1': 1.8793571533137906e-10, 'f_2': 1.4674010989178496} (approximations!)
We can safely assume the nadir value for $f_1$ to be (very near) zero.
Next, we can define an initial reference point and solve the problem using the reference point method:
from desdeo.mcdm.reference_point_method import rpm_solve_solutions
reference_point = {"f_1": -0.75, "f_2": 1.2}
results = rpm_solve_solutions(problem, reference_point=reference_point)
Let us then inspect the results:
for i, result in enumerate(results):
print(f"Solution {i + 1}:")
print(f"Objective function values \t\t {result.optimal_objectives}")
print(f"Decision variable values \t\t {result.optimal_variables}")
print(f"Constraint values \t\t\t {result.constraint_values}")
print("---")
Solution 1:
Objective function values {'f_1': -1.3418453187349122, 'f_2': 0.2264537186251374}
Decision variable values {'x_1': -1.3418806470832773e-10, 'x_2': 1.1074537094728327, '_alpha': -0.3945632889855735}
Constraint values {'g_1': -8.892546290661356, 'f_1_con': 6.2539222134283534e-09, 'f_2_con': 2.4414633092995075e-09}
---
Solution 2:
Objective function values {'f_1': -0.7709617885583611, 'f_2': -0.7086032659774049}
Decision variable values {'x_1': -9.232688904165525e-11, 'x_2': 0.5398117579514132, '_alpha': -0.77352746119992}
Constraint values {'g_1': -9.460188242140912, 'f_1_con': 2.557184797247203e-09, 'f_2_con': 6.224309068159073e-09}
---
Solution 3:
Objective function values {'f_1': -1.4902309480370677, 'f_2': 1.121699318261173}
Decision variable values {'x_1': -1.9339132868848427e-10, 'x_2': 1.456605409251652, '_alpha': -0.4934869770258314}
Constraint values {'g_1': -8.54339459094174, 'f_1_con': 7.387595202246189e-09, 'f_2_con': -2.10404455525115e-08}
---
We can readily inspect the objective function values and decision variable values in the results. We have three solutions, because the reference point method returns $k+1$ solutions, where $k$ is the number of objective functions. We also notice some new acquittances in the results, namely _alpha, f_1_con, and f_2_con. These are the symbols auxiliary variables and constraints that have been added to the problem automatically when it has been scalarized by the reference point method. These can be safely ignored for the purpose of this example.
If we are not happy with the solutions, we can try to change the reference point. We can try to be more demanding (we are minimizing both objective functions, thus, less is more!):
reference_point = {"f_1": -1.3, "f_2": -1.2}
results = rpm_solve_solutions(problem, reference_point=reference_point)
for i, result in enumerate(results):
print(f"Solution {i + 1}:")
print(f"Objective function values \t\t {result.optimal_objectives}")
print(f"Decision variable values \t\t {result.optimal_variables}")
print(f"Constraint values \t\t\t {result.constraint_values}")
print("---")
Solution 1:
Objective function values {'f_1': -0.9160487221718046, 'f_2': -0.5684256234762243}
Decision variable values {'x_1': -9.859845582535822e-11, 'x_2': 0.6569432064674813, '_alpha': 0.25596734413127403}
Constraint values {'g_1': -9.343056793631117, 'f_1_con': 3.743887477813956e-09, 'f_2_con': 5.819822623820414e-09}
---
Solution 2:
Objective function values {'f_1': -0.3959162742606164, 'f_2': -0.9286527164391796}
Decision variable values {'x_1': -7.96552693118994e-11, 'x_2': 0.26710912294569883, '_alpha': 0.10997286032311548}
Constraint values {'g_1': -9.732890877133956, 'f_1_con': -3.894020744743543e-09, 'f_2_con': 6.96099176911158e-09}
---
Solution 3:
Objective function values {'f_1': -1.156514524606623, 'f_2': -0.22485158558614837}
Decision variable values {'x_1': -1.133237138290914e-10, 'x_2': 0.8804251327704427, '_alpha': 0.09565691455471291}
Constraint values {'g_1': -9.119574867342882, 'f_1_con': 5.257606974784501e-09, 'f_2_con': 4.686439319945279e-09}
---
Perhaps unsurprisingly, we got different results after changing the reference point. We can keep iterating the method by changing the reference point until we find something we are satisfied with (or the decision maker is!).
Try changing the reference point again, or try modifying the problem! You can add a third objective functions, or you might come up with a completely new problem. The sky is the limit!
Conclusions¶
In this example, we have seen how to define a multiobjective optimization problem, how to find out its nadir and (approximate) ideal points, and how to solve the problem utilizing the reference point method.
DESDEO has support for many kinds of multiobjective optimization problems. Keep exploring the framework to find out what it has to offer!
Please note: DESDEO 2.0 is still under heavy development. The documentation will be updated in due time with more examples like this.