Skip to content

Commit efb268d

Browse files
committed
add refinery_optimization exercise
1 parent 8610eb6 commit efb268d

File tree

1 file changed

+289
-0
lines changed

1 file changed

+289
-0
lines changed
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
# Refinery Optimization Exercise
2+
3+
4+
## Problem description
5+
6+
The figure above shows a simplified schema of a refinery with three units (a, b, and c). Each unit consumes crude oil from the same storage (S).
7+
The different units and storage have the following characteristics
8+
9+
### Storage:
10+
- Capacity (Cs): 500 barrels
11+
- Initial volume (Vs): 300 barrels
12+
13+
### Units:
14+
- Unit A capacity (Ca): 200 barrels
15+
- Unit B capacity (Cb): 300 barrels
16+
- Unit C capacity (Cc): 100 barrels
17+
18+
The volumes of crude oil consumed by units are:
19+
- $V_a$: Volume consumed by Unit A
20+
- $V_b$: Volume consumed by Unit B
21+
- $V_c$: Volume consumed by Unit C
22+
23+
Your goal is to model the allocation of crude oil from storage to the processing units to ensure constraints are satisfied and optimize certain objectives.
24+
25+
First, let's define problem decision variables.
26+
27+
---
28+
29+
30+
```python
31+
# install pulp if needed
32+
# %pip install pulp
33+
```
34+
35+
36+
```python
37+
# Define decision variables
38+
# We can use an optimization library like PuLP
39+
from pulp import LpProblem, LpVariable, LpMinimize, LpStatusOptimal, LpStatus, LpConstraint, LpAffineExpression
40+
41+
# Create a problem instance
42+
problem = LpProblem("Refinery_Optimization", LpMinimize)
43+
44+
# Define decision variables
45+
V_a = LpVariable("V_a", 0, 200) # Volume for Unit A
46+
V_b = LpVariable("V_b", 0, 300) # Volume for Unit B
47+
V_c = LpVariable("V_c", 0, 100) # Volume for Unit C
48+
```
49+
50+
## Exercise Questions
51+
52+
### Step 1: Write constraints modeling the crude oil flow from Storage (S) to the different units
53+
54+
55+
```python
56+
# Constraints can be added to the problem using natural syntax: problem += x + y <= 1, "{cstr_name}"
57+
58+
problem += ..., "Total_Consumption_Limit"
59+
```
60+
61+
Refinery units (a,b and c) operate with a certain cost. For each barrel of crude oil processed, each unit incurs a cost ($c_a$, $b_c$ and $c_c$). Let's consider following operating costs:
62+
- $c_a$ = 3
63+
- $c_b$ = 4
64+
- $c_c$=1
65+
66+
67+
```python
68+
c_a, c_b, c_c = (3, 4, 1)
69+
```
70+
71+
### Step 2: Define an objective function minimizing total operation cost of all units?
72+
73+
74+
```python
75+
# Define the objective function (fill the missing part)
76+
problem += ..., "Total_Cost"
77+
```
78+
79+
Let's now solve the problem. Can you guess what would be the optimal solution?
80+
81+
Use the solve function to solve the LP and confirm wether the solution is coherent.
82+
83+
84+
```python
85+
def solve(problem: LpProblem) -> LpStatus:
86+
problem.solve()
87+
status = LpStatus[problem.status]
88+
if problem.status != LpStatusOptimal:
89+
print("Problem is infeasible")
90+
return LpStatus[problem.status]
91+
print(f"Problem solver. Optimal value = {problem.objective.value()}")
92+
for var in problem.variables():
93+
print(f"{var.name} = {var.varValue}")
94+
return LpStatus[problem.status]
95+
96+
solve(problem)
97+
```
98+
99+
In the real world, refieneries operate on a 1-week schedule. Each unit has a daily processing capacity:
100+
- Unit A: 20 barrels/day
101+
- Unit B: 30 barrels/day
102+
- Unit C: 20 barrels/day
103+
104+
The refinery needs to process all volume in storage unit within a week.
105+
106+
Before going any futhure, let's define a function that builds and solves a model given a set of constraints and an objective function.
107+
108+
109+
```python
110+
def solve_problem(problem_name: str, constraints: list[LpConstraint], objective_function: LpAffineExpression) -> LpStatus:
111+
problem = LpProblem(problem_name, LpMinimize)
112+
113+
for cstr in constraints:
114+
problem += cstr
115+
problem += objective_function
116+
problem.solve()
117+
status = LpStatus[problem.status]
118+
if problem.status != LpStatusOptimal:
119+
print("Problem is infeasible")
120+
else:
121+
print(f"Problem solver. Optimal value = {problem.objective.value()}")
122+
return LpStatus[problem.status]
123+
124+
def display_results(V_a: list[LpVariable], V_b: list[LpVariable], V_c: list[LpVariable], V_s: list[LpVariable]):
125+
for t in range(T):
126+
print(f"Day {t + 1}:")
127+
print(" V_a:", V_a[t].varValue)
128+
print(" V_b:", V_b[t].varValue)
129+
print(" V_c:", V_c[t].varValue)
130+
print(" V_s:", V_s[t].varValue)
131+
```
132+
133+
134+
### Step 3:
135+
instructions:
136+
- extend the model to include daily time steps
137+
- ensure appropriate storage volume daily-evolution
138+
- ensure daily processing capacities are not exceeded
139+
- ensure that volume storage $V_s$ starts at initial value and is entirely consummed at the end of time horizon T
140+
- formulate objective function representing total operational cost over the tme horizon
141+
142+
Costs of operating units are considered constant on the whole time horizon.
143+
144+
145+
```python
146+
# Extend model for time dimension
147+
T = 7 # Days horizon
148+
# Redefine variables
149+
V_a_t = [... for t in range(T)]
150+
V_b_t = [... for t in range(T)]
151+
V_c_t = [... for t in range(T)]
152+
V_s_t = [... for t in range(T)]
153+
154+
# initialize constraints list
155+
constraints = []
156+
# add storage evolution constraints
157+
for t in range(T):
158+
if t < T-1:
159+
constraints.append(V_s[t+1] == V_s[t] + ...)
160+
161+
# storage volume initial value
162+
constraints.append(...)
163+
# storage volume end value
164+
constraints.append(...)
165+
166+
# define objective function
167+
total_operational_cost = sum(... for t in range(T))
168+
```
169+
170+
### **Step 4: Run Optimization and Explore Solutions**
171+
#### Instructions:
172+
- Solve the extended problem.
173+
- Display the optimal solution over the time horizon.
174+
175+
176+
```python
177+
solve_problem("refinery_optimization", constraints=constraints, objective_function=total_operational_cost)
178+
display_results(V_a, V_b, V_c, V_s)
179+
```
180+
181+
### **Step 5: Add priority Constraints**
182+
#### Instructions:
183+
- Add constraints ensuring that unit c is only used after unit a runs at full capabity.
184+
- Use binary variables defined below 'MAX_CAPA_a'
185+
186+
187+
```python
188+
MAX_CAPA_a = [LpVariable(f"MAX_CAPA_a_{t}", 0, 1, cat='Binary') for t in range(T)]
189+
190+
# add priority evolution
191+
for t in range(T):
192+
constraints.append(...)
193+
constraints.append(...)
194+
```
195+
196+
- Solve and display the results.
197+
198+
199+
```python
200+
solve_problem("refinery_optimization", constraints=constraints, objective_function=total_operational_cost)
201+
display_results(V_a, V_b, V_c, V_s)
202+
```
203+
204+
### **Step 6: Add Outage Constraints**
205+
#### Instructions:
206+
- Introduce necessary outages for maintenance.
207+
- Each unit must proceed in an outage at least one time per 7 days
208+
- outage lasts 1 day
209+
210+
We defined turn-on status binary variables 'Y_a, Y_b, Yc' to be used to enforce outages.
211+
212+
213+
```python
214+
# Define binary variables for turn-on status
215+
Y_a = [LpVariable(f"Y_a_{t}", 0, 1, cat="Binary") for t in range(T)]
216+
Y_b = [LpVariable(f"Y_b_{t}", 0, 1, cat="Binary") for t in range(T)]
217+
Y_c = [LpVariable(f"Y_c_{t}", 0, 1, cat="Binary") for t in range(T)]
218+
219+
# Link turn-on status to volumes processed
220+
for t in range(T):
221+
constraints.append(...)
222+
constraints.append(...)
223+
constraints.append(...)
224+
225+
# ensure 1-day outage per week
226+
constraints.append(...)
227+
constraints.append(...)
228+
constraints.append(...)
229+
```
230+
231+
- solve and display results
232+
233+
234+
```python
235+
solve_problem("refinery_optimization", constraints=constraints, objective_function=total_operational_cost)
236+
display_results(V_a, V_b, V_c, V_s)
237+
```
238+
239+
### Step 7: furthure extend the model with refined products
240+
In the reality, processing units a,b and c will produce refined products: $product_1, product_2$. Each barrel of a refined product can be sold at a correspondin price: $p_1$ and $p_2$. Furthuremore, each unit converts crude oil to refined products with a specific ratios $r_{unit, product}$. Per example, unit a, after processing $x$ barrels of crude oil will generate $r_{a,1}x$, $r_{a,2}x$ barrels of refined products. We assume that crude ol and refined products have the same density, thus $r_{a,1} + r_{a,2} = 1$.
241+
242+
Convertion ratios are define for each unit:
243+
- unit a: [$r_{a,1}, r_{a,2}$] = [0.7, 0.3]
244+
- unit a: [$r_{b,1}, r_{b,2}$] = [0.6, 0.4]
245+
- unit a: [$r_{c,1}, r_{c,2}$] = [0.8, 0.2]
246+
247+
a barrel of $product_1$ generates a revenue of $p_1=4$ and $p_2=7$.
248+
249+
instructions:
250+
- Extend the model to include refined products
251+
- Include refined products revenue in the objective function
252+
253+
254+
```python
255+
# Define binary variables for starting each unit
256+
r = {"a": [0.7, 0.3],
257+
"b": [0.6, 0.4],
258+
"c": [0.8, 0.2]}
259+
p_1, p_2 = 4, 7
260+
261+
# Define refined products variables
262+
V_product_1 = [... for t in range(T)]
263+
V_product_2 = [... for t in range(T)]
264+
265+
# Add refined products production cstr (from crude oil)
266+
for t in range(T):
267+
# product_1
268+
constraints.append(...)
269+
# product_2
270+
constraints.append(...)
271+
272+
# Add refined products revenue to the objective function
273+
total_operational_cost = sum(... for t in range(T))
274+
refined_products_revenue = sum(... for t in range(T))
275+
objective_function = total_operational_cost + refined_products_revenue
276+
```
277+
278+
- solve problem and display results
279+
280+
281+
```python
282+
solve_problem("refinery_optimization", constraints=constraints, objective_function=total_operational_cost)
283+
display_results(V_a, V_b, V_c, V_s)
284+
```
285+
286+
287+
```python
288+
289+
```

0 commit comments

Comments
 (0)