Engine power vs emissions#
Click –> Live Code to activate live coding on this page!
Problem#
In this problem we’re trying to optimize a diesel engine for maximum power and minimum CO2 emissions. Depending on the optimal engine speed, the power and CO2 emissions change.
The following data is available:
Rotations per minute |
CO2 Emissions |
Power |
---|---|---|
800 |
708 |
161.141 |
1000 |
696.889 |
263.243 |
1200 |
688.247 |
330.51 |
1400 |
682.897 |
381.561 |
1700 |
684.955 |
391.17 |
1800 |
697.3 |
370 |
This data is interpolated to obtain a continuous relation:
Show code cell source
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
%config InlineBackend.figure_formats = ['svg']
RPM = np.array([800, 1000, 1200, 1400, 1700, 1800])
CO2 = np.array([708, 696.889, 688.247, 682.897, 684.955, 697.3 ])
POW = np.array([161.141, 263.243, 330.51 , 381.561, 391.17, 370 ])
def CO2func(s):
return sp.interpolate.pchip_interpolate(RPM,CO2,s)
def POWfunc(s):
return sp.interpolate.pchip_interpolate(RPM,POW,s)
RPM_continuous = np.linspace(800,1800,100)
plt.figure()
plt.subplot(2,1,1)
plt.plot(RPM, CO2, 'x', label='Original Data')
plt.plot(RPM_continuous, CO2func(RPM_continuous), label='Interpolated Data')
plt.xlabel('RPM')
plt.ylabel('CO$_2$ (g/kWh)')
ax = plt.gca()
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.tight_layout()
plt.legend()
plt.subplot(2,1,2)
plt.plot(RPM, POW, 'x', label='Original Data')
plt.plot(RPM_continuous, POWfunc(RPM_continuous), label='Interpolated Data')
plt.xlabel('RPM')
plt.ylabel('Power (kW)')
ax = plt.gca()
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.tight_layout()
plt.legend();
Model#
We’ll define the model as follows:
Design variables: scalar value representing the rotations per minute (\(\text{RPM}\))
Objective function: weighted sum of the power and CO2 emissions, max of difference with goals or pareto front
Bounds: only interpolation so between \(800\) and \(1800\) \(\text{RPM}\)
Design variables#
Let’s start with our design variables. In this case a logical choice could be the width, height and depth of our block
Objective function#
Let’s explore all three possible objective functions as defined in (4.2), (4.3) and (4.4):
Weighted objective function#
We can define the weighted objective function as defined by (4.2) with some predefined weights. For example a weight of \(\cfrac{1}{3}\) for the power value and \(\cfrac{2}{3}\) for the CO2 emissions.
Please note that we need a minus for the power objective because it’s an maximization objective and we now apply the weights to the non-normalized functions while they have different units. Therefore, it might be wise to apply normalization as defined by (4.5).
Goal attainment#
If we define two fails for the power and CO2 emissions, we can apply goal attainment. Those goals could be \(460 \text{ kW}\) for the generated power and \(640 \text{ g/kWh}\) for the CO2 emissions. Now the objective function can as in (4.3) as:
Pareto front#
Finally, we can find the pareto front by defining the problem as in (4.4):
For normalization, we could take the value from our plot, but for more real-life complex problems the dimensions are higher and you cannot find the maximum and minimum of each single objective function. Therefore, let’s estimate the maxima and minima as:
CO2 Emissions |
Power |
|
---|---|---|
Minimum value |
680 |
150 |
Maximum value |
710 |
400 |
This gives:
To find the full pareto front this optimization model has to be solved for a large set of \(\delta_i\) and \(\delta_j\)
Bounds#
Let’s limit our solution to within our interpolation domain. Therefore, the bound can be defined as:
Find best solution manually#
Try and adjust the values for \(x\). Can you find the optimal solution? How does it change for different models?
Method#
Now let’s solve this problem using an optimization method. The interpolated data is stored in CO2func
and POWfunc
.
Import libraries#
import scipy as sp
import numpy as np
import matplotlib.pyplot as plt
import scipy as sp
import numpy as np
import matplotlib.pyplot as plt
%config InlineBackend.figure_formats = ['svg']
Define variables#
As before, we don’t need to specify our variable \(x\) itself as defined in (3.2). However, this optimization method requires an initial guess. An arbitrary value is chosen here:
RPM = np.array([800, 1000, 1200, 1400, 1700, 1800])
CO2 = np.array([708, 696.889, 688.247, 682.897, 684.955, 697.3 ])
POW = np.array([161.141, 263.243, 330.51 , 381.561, 391.17, 370 ])
def CO2func(s):
return sp.interpolate.pchip_interpolate(RPM,CO2,s)
def POWfunc(s):
return sp.interpolate.pchip_interpolate(RPM,POW,s);
x0 = np.array(1200)
Define objective function#
Let’s define the objective function for each of the three models
Weighted objective function#
The objective function was defined in (4.7), which gives:
def weighted_obj(x):
delta_p = 1/3
delta_c = 1 - delta_p
return -delta_p * POWfunc(x) + delta_c * CO2func(x)
Goal attainment#
The objective function was defined in (4.8), which gives:
def goal_attainment(x):
Pt = 460
Ct = 640
return max(Pt - POWfunc(x),CO2func(x)-Ct)
Pareto front#
For the pareto front, the objective functions needed to be normalized as defiend by (4.10):
def POWfunc_normalized(x):
return (POWfunc(x) - 150)/(400 - 150)
def CO2func_normalized(x):
return (CO2func(x) - 680)/(710 - 680)
The objective function was defined in (4.9), which gives:
def weighted_obj_pareto(x):
return -delta_p * POWfunc_normalized(x) + delta_c * CO2func_normalized(x)
in which delta_p
and delta_c
are defined when solving this problem
Define bounds#
The one single bound was defined in (4.11) and results in:
bounds = [[800,1800]]
Solve the problem#
Now let’s solve the problem for each of the three models. We’re gonna time them to see how long the analysis takes. Please note that when running this in the browser, the CPU times
gives invalid values.
Weighted objective function#
%%time
result = sp.optimize.minimize(fun = weighted_obj, x0 = x0, bounds = bounds)
print(result)
message: CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL
success: True
status: 0
fun: 325.81425292950235
x: [ 1.613e+03]
nit: 7
jac: [-5.684e-06]
nfev: 26
njev: 13
hess_inv: <1x1 LbfgsInvHessProduct with dtype=float64>
CPU times: total: 15.6 ms
Wall time: 16.1 ms
Goal attainment#
%%time
result2 = sp.optimize.minimize(fun = goal_attainment, x0 = x0, bounds=bounds)
print(result2)
message: CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL
success: True
status: 0
fun: 68.83000008096866
x: [ 1.700e+03]
nit: 12
jac: [-5.684e-06]
nfev: 50
njev: 25
hess_inv: <1x1 LbfgsInvHessProduct with dtype=float64>
CPU times: total: 15.6 ms
Wall time: 22.9 ms
Pareto front#
For the pareto front we need to solve this problem for a collection of weights. The results are stored in a list
%%time
x_pareto_opt =[]
delta_p_list = np.linspace(0,1,101)
delta_c_list = 1 - delta_p_list
for i in range(101):
delta_p = delta_p_list[i]
delta_c = delta_c_list[i]
result_i = sp.optimize.minimize(fun = weighted_obj_pareto,x0=x0,bounds=bounds)
x_pareto_opt.append(result_i.x[0])
CPU times: total: 844 ms
Wall time: 840 ms
Now the pareto front can be plotted by evaluating the objectives functions for our collection of optimum values for \(x\):
P_pareto_opt = POWfunc(x_pareto_opt)
C_pareto_opt = CO2func(x_pareto_opt)
plt.figure()
plt.plot(C_pareto_opt,P_pareto_opt,'x')
for i in range(101):
if i%10 == 0:
plt.annotate(f"($\delta_i,\delta_j$)=({round(delta_p_list[i],1)},{round(delta_c_list[i],1)})", (C_pareto_opt[i], P_pareto_opt[i]), ha='left',va = 'bottom')
plt.ylabel('Power (kW)')
plt.xlabel('CO$_2$ (g/kWh)')
plt.title('Pareto Front')
ax = plt.gca()
ax.invert_yaxis()
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
Exercise#
Click –> Live Code and answer the following question: