8.6. Multi-Objective Optimisation (MODO): Bridge Design Problem#

Note

Press the rocket symbol on the top right of the page to make the page interactive and play with the code!

In reality, engineering design almost always involves multiple stakeholders with conflicting objectives. Therefore, multi-objective optimisation (MODO) is the more general and fundamental approach in preference-based engineering design.

SODO can be seen as a special case of MODO, obtained by assigning all weight to a single objective.

The bridge design problem is extended here to illustrate MODO. Instead of optimising each stakeholder’s objective separately, we evaluate trade-offs and find compromise solutions.

A priori optimisation

The single-objective optimisation in Section 8.5 considered each stakeholder separately and evaluated only the corner points of the design space. However, this is no guarantee that the true optimal solution is found. In fact, for the bridge design problem the optimal solution is not located at any of the corners.

This illustrates that single-objective optimisation is only a stepping stone. To identify true compromise solutions between conflicting objectives, a full multi-objective optimisation is required.

Step 1: Define the preference functions

For each stakeholder, we define preference functions that map objective values to preference scores.

  • Municipality (costs): preference decreases linearly with increasing construction costs.

  • Waterway users (waiting time): preference decreases with increasing waiting time.

For example, costs of 75 yield a relatively high preference, whereas costs of 228 result in a very low preference. Similarly, waiting times near 71 seconds are rated highly, whereas values above 108 seconds are strongly penalised.

Step 2: Aggregate stakeholder preferences

To combine stakeholder preferences into a single evaluation, we use an aggregation method. Here, the a-fine aggregator is applied with equal weights for both stakeholders:

\(𝑤1= 𝑤2 = 0.5\)

This reflects the assumption that both the municipality and waterway users have equal influence in the decision-making process.

Step 3: Corner point evaluation

Before running a full optimisation, the feasible set’s corner points can be evaluated. This illustrates how preferences trade off against each other:

\(x_1\)

\(x_2\)

Aggregated preference

1.0

3.0

36.68

1.0

8.0

0.00

5.0

3.0

100.00

5.0

8.0

63.32

These results already show that compromise solutions may exist, but again, optimality is not guaranteed at corners.

Step 4: Multi-objective optimisation with a Genetic Algorithm

To find the true optimal solution, a Genetic Algorithm (GA) is applied over the full design space. This algorithm searches iteratively for design solutions that maximise the aggregated preference score.

Parameters used:

  • Aggregation: a-fine

  • Population size: 120

  • Iterations: 1000 (stopping earlier if no improvement)

  • Crossover rate: 0.9

  • Variables: 𝑥1 (span) and 𝑥2 (clearance height)

The GA result for the bridge problem is:

\(𝑥1 ≈ 4.98\), \(𝑥2 ≈ 3.02\)

This solution balances both cost and waiting time, achieving a higher aggregated preference than the single-objective results.

Step 5: Visualisation of results

The figure below shows the design space, the feasible region, and the optimal compromise solution identified by the GA.

Python code for bridge design problem#

Type of algorithm used for single objective optimisation: Scipy minimize and MILP
Type of algorithm used for multi objective optimisation: Genetic Algorithm (tetra)
Number of design variables: 2
Number of objective functions: 2
Bounds: yes
Constraints: no
Type of problem: linear

import micropip
await micropip.install("urllib3 ")
await micropip.install("requests")

import numpy as np
import pandas as pd
from scipy.optimize import minimize, LinearConstraint, milp
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon

from genetic_algorithm_pfm.a_fine_aggregator_main import a_fine_aggregator

%matplotlib inline
plt.rcParams['figure.dpi'] = 400
plt.rcParams.update({'font.size': 14})

MILP and Minimise#

Scipy has two algorithms that can easily be used for optimisation: MILP and Minimize. The former is especially useful for mixed-integer problems: problems where one or more of the variables is an integer variable. The latter is, however, more straightforward in its use.

To show their applicability, the same problem is worked out with both methods in this notebook.

# define constants
c1 = 3   # costs per material
c2 = 4   # material used per metre bridge span
c3 = 7   # material used per metre air draft
c4 = 1.2   # relation between waiting time and traffic flow
c5 = 1.7   # traffic flow per metre bridge span
c6 = 1.9   # traffic flow per metre air draft

WT0 = 100   # minimal waiting time

MILP#

# first, define the objective function. Since it is linear, we can just provide the coefficients with which x1 and x2
# are multiplied. Note the -1: we need to maximize, however, milp is a minimization algorithm!
c_costs = 1 * np.array([c1 * c2, c1 * c3])
c_wait_time = -1 * np.array([c4 * c5, c4 * c6])

# next, define the constraints. For this we first provide a matrix A with all the coefficients x1 and x2 are multiplied.
A = np.array([[1, 0], [0, 1]])

# next we determine the upper bounds as vectors
b_u = np.array([5, 8])

# finally, we need to define the lower bound. In our case, these are taken as 0
b_l = np.array([1, 3])

# we can now define the LinearConstraint
constraints = LinearConstraint(A, b_l, b_u)

# the integrality array will tell the algorithm what type of variables (0 = continuous; 1 = integer) there are
integrality = np.zeros_like(c_costs)

# Run the optimization
result1 = milp(c=c_costs, constraints=constraints, integrality=integrality)
result2 = milp(c=c_wait_time, constraints=constraints, integrality=integrality)

print('Results MILP')
print(f'Objective 1 is minimal for x1 = {result1.x[0]} and x2 = {result1.x[1]}. The costs are then {result1.fun}.')
print(f'Objective 2 is minimal for x1 = {result2.x[0]} and x2 = {result2.x[1]}. '
      f'The wait time is then {result2.fun + WT0}.')
Results MILP
Objective 1 is minimal for x1 = 1.0 and x2 = 3.0. The costs are then 75.0.
Objective 2 is minimal for x1 = 5.0 and x2 = 8.0. The wait time is then 71.56.

Minimize#

# define objectives
def objective_costs(x):
    x1, x2 = x

    F1 = c2 * x1 + c3 * x2

    return c1 * F1


def objective_wait_time(x):
    x1, x2 = x

    F2 = c5 * x1 + c6 * x2

    return -1 * c4 * F2 + WT0


# define bounds for two variables
bounds = ((1, 5), (3, 8))

# initiate optimization
result1 = minimize(objective_costs, x0=np.array([1, 1]), bounds=bounds)
result2 = minimize(objective_wait_time, x0=np.array([1, 1]), bounds=bounds)

optimal_result_O1 = result1.fun
optimal_result_O2 = result2.fun

# print results
print('Results Minimize')
print(f'Objective 1 is minimal for x1 = {result1.x[0]} and x2 = {result1.x[1]}. The costs are then {result1.fun}.')
print(f'Objective 2 is minimal for x1 = {result2.x[0]} and x2 = {result2.x[1]}. The wait time is then {result2.fun}.')
print()
Results Minimize
Objective 1 is minimal for x1 = 1.0 and x2 = 3.0. The costs are then 75.0.
Objective 2 is minimal for x1 = 5.0 and x2 = 8.0. The wait time is then 71.56.

Plot the results#

After optimising using the minimise algorithm the solution space and the objective function are plotted.

# plot graphical solution
fig, ax = plt.subplots(figsize=(8, 6))

# Draw constraint lines
ax.vlines(3, -1, 7)
ax.vlines(8, -1, 7)
ax.hlines(1, 1, 10)
ax.hlines(5, 1, 10)

# Draw the feasible region
feasible_set = Polygon(np.array([[3, 1],
                                 [8, 1],
                                 [8, 5],
                                 [3, 5]]),
                       color="lightgrey")
ax.add_patch(feasible_set)

ax.set_xlabel('X2')
ax.set_ylabel('X1')

# Draw the objective function
x2 = np.linspace(1, 6)
ax.plot(x2, (optimal_result_O1 - c1 * c3 * x2) / (c1 * c2), color="purple", label='Costs')
x2 = np.linspace(6, 10)
ax.plot(x2, (WT0 - c4 * c6 * x2 - optimal_result_O2) / (c4 * c5), color="orange", label='Wait time')

ax.scatter([3,3,8,8], [1,5,1,5], marker='*', color='red', label='corner points', s=150)

ax.legend()
ax.grid();
../_images/fc06945f0cf300e148e5d534b3440c4eb64282bc634415b2a0c7d887f88de9b1.png

Corner point evaluation#

Now the corner points of the solution space can be evaluated using the Tetra solver.

# corner point evaluation with Tetra
def preference_P1(variables):
    x1 = variables[:, 0]
    x2 = variables[:, 1]

    costs = objective_costs([x1, x2])
    return 7600 / 51 - 100 * costs / 153

def preference_P2(variables):
    x1 = variables[:, 0]
    x2 = variables[:, 1]

    wait_time = objective_wait_time([x1, x2])
    return 291.747 - 2.67953 * wait_time


alternatives = np.array([
    [1, 3],
    [1, 8],
    [5, 3],
    [5, 8]
])

P1 = preference_P1(alternatives)
P2 = preference_P2(alternatives)

w = [0.5, 0.5] # weights are equal here
p = [P1, P2]

ret = a_fine_aggregator(w, p)

# add results to DataFrame and print it
results = np.zeros((4, 3))
results[:, 0] = alternatives[:, 0]
results[:, 1] = alternatives[:, 1]
results[:, 2] = np.multiply(-1, ret)

df = pd.DataFrame(np.round_(results, 2), columns=['x1', 'x2', 'Aggregated preference'])
print(df)
    x1   x2  Aggregated preference
0  1.0  3.0                  36.68
1  1.0  8.0                   0.00
2  5.0  3.0                 100.00
3  5.0  8.0                  63.32
constants_1 = np.linspace(75, 228)
constants_2 = np.linspace(71.56, 108.88)

pref_1 = 7600 / 51 - 100 * constants_1 / 153
pref_2 = 291.747 - 2.67953 * constants_2

fig = plt.figure()
ax1 = fig.add_subplot(1, 1, 1)
ax1.plot(constants_1, pref_1)
ax1.set_xlim((70, 235))
ax1.set_ylim((0, 100))
ax1.set_title('Costs')
ax1.set_xlabel('Costs [€]')
ax1.set_ylabel('Preference score')
ax1.grid()

fig = plt.figure()
ax2 = fig.add_subplot(1, 1, 1)
ax2.plot(constants_2, pref_2)
ax2.set_xlim((65, 115))
ax2.set_ylim((0, 100))
ax2.set_title('Waiting time')
ax2.set_xlabel('Waiting time [s]')
ax2.set_ylabel('Preference score')
ax2.grid()
../_images/1a5de20b068ecbb3707fca33c272aa34fe1fd488625560f1c765698e60725a18.png ../_images/0c315ce65991bd2f5142b1e79dc74dcdc42c1dadbaf0dc82955a8e663f32a01d.png

A priori optimisation#

The optimisation above initially considers only the single stakeholders and evaluates only the corner points. However, as you will in other examples, this is no guarantee that the optimal solution is found. Here, the optimal point is not on any of the corners. To find the optimal solutions in these cases, a multi-objective optimisation is performed.

The same is showed below for the bridge problem, so you can see how the single-objective optimisations, the multi-objective evaluation, and the multi-objective optimisation relate to each other.

from genetic_algorithm_pfm import GeneticAlgorithm

weights = [0.5, 0.5]

def objective(variables):
    p1 = preference_P1(variables)
    p2 = preference_P2(variables)

    return weights, [p1, p2]

bounds = [[1, 5], [3, 8]]
options = {
    'aggregation': 'a_fine',
    "n_pop": 120,
    "max_stall": 60,
    "n_iter": 1000,
    "n_bits": 8,
    "r_cross": 0.9
}

# run the GA and print its result
ga = GeneticAlgorithm(objective=objective, constraints=[], bounds=bounds, options=options)
score, design_variables, _ = ga.run()

print()
print('Results multi-objective optimisation')
print(f'x1 = {design_variables[0]} and x2 = {design_variables[1]}.')
The type of aggregation is set to a_fine
Generation   Best score   Mean             Max stall    Diversity    Number of non-feasible results
No initial starting point for the optimization with the a-fine-aggregator is given. A random population is generated.
0            -100.0       -34.3212         1            0.017        0           
1            -100.0       -79.5951         2            0.188        0           
2            -100.0       -79.7827         1            0.342        0           
3            -100.0       -91.3552         2            0.221        0           
4            -100.0       -91.6844         1            0.429        0           
5            -100.0       -92.7016         2            0.412        0           
6            -100.0       -89.0771         3            0.392        0           
7            -100.0       -91.1093         4            0.421        0           
8            -100.0       -95.6549         5            0.446        0           
9            -100.0       -94.761          6            0.442        0           
10           -100.0       -92.6198         7            0.425        0           
11           -100.0       -84.5174         8            0.425        0           
12           -100.0       -90.3866         9            0.35         0           
13           -100.0       -92.1431         10           0.421        0           
14           -100.0       -94.5978         11           0.429        0           
15           -100.0       -91.9916         12           0.425        0           
16           -100.0       -89.8685         13           0.438        0           
17           -100.0       -92.6939         14           0.417        0           
18           -100.0       -91.4171         15           0.421        0           
19           -100.0       -94.0053         16           0.417        0           
20           -100.0       -90.9118         17           0.438        0           
21           -100.0       -87.0313         18           0.433        0           
22           -100.0       -68.89           19           0.421        0           
23           -100.0       -94.6577         20           0.442        0           
24           -100.0       -95.8959         21           0.45         0           
25           -100.0       -90.9943         22           0.408        0           
26           -100.0       -89.2141         23           0.425        0           
27           -100.0       -87.5114         24           0.421        0           
28           -100.0       -93.4957         25           0.421        0           
29           -100.0       -94.2854         26           0.438        0           
30           -100.0       -85.2189         27           0.412        0           
31           -100.0       -89.1417         28           0.417        0           
32           -100.0       -86.0954         29           0.412        0           
33           -100.0       -91.0166         30           0.433        0           
34           -100.0       -91.7523         31           0.433        0           
35           -100.0       -94.0363         32           0.425        0           
36           -100.0       -94.1904         33           0.421        0           
37           -100.0       -93.8514         34           0.442        0           
38           -100.0       -91.0599         35           0.446        0           
39           -100.0       -93.6419         36           0.454        0           
40           -100.0       -91.6437         37           0.425        0           
41           -100.0       -94.4822         38           0.421        0           
42           -100.0       -92.8724         39           0.442        0           
43           -100.0       -90.2641         40           0.425        0           
44           -100.0       -91.014          41           0.438        0           
45           -100.0       -93.9772         42           0.442        0           
46           -100.0       -90.7165         43           0.412        0           
47           -100.0       -87.7539         44           0.417        0           
48           -100.0       -76.3443         45           0.438        0           
49           -100.0       -91.6208         46           0.425        0           
50           -100.0       -83.5065         47           0.4          0           
51           -100.0       -86.4593         48           0.392        0           
52           -100.0       -88.8302         49           0.396        0           
53           -100.0       -88.6736         50           0.421        0           
54           -100.0       -87.5807         51           0.446        0           
55           -100.0       -91.1472         52           0.392        0           
56           -100.0       -87.014          53           0.417        0           
57           -100.0       -86.288          54           0.408        0           
58           -100.0       -94.611          55           0.429        0           
59           -100.0       -93.8426         56           0.429        0           
60           -100.0       -91.8967         57           0.4          0           
61           -100.0       -90.7563         58           0.425        0           
62           -100.0       -93.5012         59           0.433        0           
63           -100.0       -94.9854         60           0.442        0           
Stopped at gen 63
The number of generations is terribly close to the number of max stall iterations. This suggests a too fast convergence and wrong results.
Please be careful in using these results and assume they are wrong unless proven otherwise!
Execution time was 0.2041 seconds

Results multi-objective optimisation
x1 = 4.984375 and x2 = 3.01953125.
# create figure that shows the results in the design space
plt.rcParams.update({'font.size': 14})
fig, ax = plt.subplots(figsize=(7, 7))
ax.set_xlim((2, 9))
ax.set_ylim((0, 6))
ax.set_xlabel(r'$x_2$ = bridge height')
ax.set_ylabel(r'$x_1$ = bridge span')
ax.set_title('Design space')

# define corner points of design space
x_fill = [3, 8, 8, 3]
y_fill = [1, 1, 5, 5]

ax.fill_between(x_fill, y_fill, color='#539ecd', label='Design space')
ax.scatter(design_variables[1], design_variables[0], label='Optimal solution IMAP', color='tab:purple')

ax.grid()  # show grid
ax.legend();  # show legend
../_images/137b16f8f5172943dfb1267fb4407a4848f501a987c4687b6947708493176f88.png