Optimized Allocation of Streams to Heat Exchangers — Framing a Minimization Problem using Python’s PuLP module

Prathamesh Mohite
Analytics Vidhya
Published in
5 min readDec 28, 2020

--

Introduction

If you’ve spent a considerable amount of time working in an oil refinery, chances are you might have come across these systematically arranged gigantic structures like the ones above. In mechanical terms, this system is that of a ‘shell-and-tube’ type heat exchangers primarily deployed for the purpose of transferring or exchanging heat from a stream of high temperature fluid to another stream of low temperature fluid or vice versa.

An optimized arrangement of heat exchangers can enable an efficient channel for industrial fluids to be re-directed in other processes or ensure proper discharge in adjacent water bodies without causing significant environmental impact. Through this article, we will try to formulate a linear programming problem using Python’s PuLP module in systematically assigning streams of fluid outlets from multiple sources to a set of heat exchangers ensuring minimal wastage of raw materials and preventing intermixing of fluids with an objective to minimize the overall installation cost.

Problem definition

Importing required libraries

import pandas as pd
import numpy as np
import itertools
from pulp import *
import warnings
warnings.filterwarnings("ignore")

All the data and code used for this article can be accessed via my GitHub repo: https://github.com/mohiteprathamesh1996/Heat-Exchanger-Assignment-

Now let’s have a look at the data available:

df = pd.read_excel("Heat Exchanger Assignment Optimization.xlsx", sheet_name="Installation Costs")df.set_index("Streams", inplace=True)df

The above table shows the various costs (in USD $) associated with assigning outputs from 10 fluid streams to 10 different heat exchangers. The actual cost of assigning stream i where i ∈ (1,…,10) to heat exchanger j where j (1,…,10) may vary depending on several factors like the length of pipe used, distance from exchanger stations, raw materials and manpower required, etc. Our objective now is “to assign each of these streams to their respective exchangers at the least possible cost such that each stream goes to exactly 1 exchanger and conversely each exchanger receives its input from exactly 1 stream.”

Decision Variables

In this problem we define the binary decision variable, X(i, j) such that

X (i, j) = 1 if stream ‘i’ is assigned for exchanger ‘j’ else 0 for all i∈ (1,..10)and j ∈ (1,…10)

# Create a list of stream indexes
streams = df.index.to_list()
print("Streams : {} \n".format(streams))
# Create a list of indexes for exchangers
exchanger_stations = df.columns.to_list()
print("Heat Exchangers : {} \n".format(exchanger_stations))
# Create a dictionary of binary type decision variables
var_dict = LpVariable.dicts(
name="assign",
indexs=[(i, j) for i in streams for j in exchanger_stations],
lowBound=0,
cat="Binary")
# Display first 5 decision variables
[var_dict[(i,j)] for i in streams for j in exchanger_stations][:5]

Defining the Objective Function

Framing the above LP as a minimization problem, we can define out objective function as :

Minimize, Z = 949*X(1,1) + 1176*X(1,2)+….+1055*X(10,9) + 1175*X(10,10)

Mathematically,

Minimize, Z = Σ C(i,j)*X(i,j) where C (i, j) is the cost of assigning stream ‘i’ to exchanger ‘j’, for all i ∈ (1,…,10) and j (1,…,10) as seen in the above table.

# Define the LP Minimization problem
model = LpProblem("Minimize Installation Costs", LpMinimize)
# Adding the objective function
model += lpSum([df.loc[(i,j)]*var_dict[(i,j)] \
for i in streams \
for j in exchanger_stations])

Constraints

As discussed earlier, the LP is constrained such that the outlet of each stream goes to exactly 1 exchanger and conversely each exchanger receives its input from exactly 1 stream.

First, the constraint on the streams. Here we treat the stream index ‘i’ as a constant and iterate over every exchanger station. Mathematically, Σ X(j)=1

# Each stream must be assigned to exactly 1 heat exchanger
for i in streams:
model += lpSum([var_dict[(i, j)] \
for j in exchanger_stations]) == 1

Next, the constraint on the exchangers. Here we treat the exchanger index ‘j’ as a constant and iterate over the stream outlets. Mathematically, Σ X(i)=1

# Every heat exchanger must receive input from exactly 1 stream    
for j in exchanger_stations:
model += lpSum([var_dict[(i, j)] \
for i in streams]) == 1

Optimal Solution

In this last step, we solve the above LP and obtain the optimal pattern of stream assignment.

# Solve the mode
model.solve()
# Saving the optimal solution results
if LpStatus[model.status]=="Optimal":
optimal_soln = pd.DataFrame(
[(v.name, v.varValue) for v in model.variables() if v.varValue==1],
columns=["Assignment", "Status"])
print("Total installation cost : $ {}".format(value(model.objective)))optimal_soln
pd.DataFrame(
[(optimal_soln[optimal_soln[c]==1].index.values[0], c)\
for c in optimal_soln.columns],
columns = ["ConnectionFrom", "ConnectionTo"])

Finally we have achieved our objective in obtaining the appropriate assignment pattern of streams to their respective heat exchangers at the lowest possible installation cost. The above sequence of code can be reused in case the dimensions of the above dataset change.

References

  1. Edgar, T.F., Himmelblau, D.M. and Lasdon, L.S., 2001. Optimization of chemical processes. McGraw-Hill,.
  2. Mitchell, S., OSullivan, M. and Dunning, I., 2011. PuLP: a linear programming toolkit for python. The University of Auckland, Auckland, New Zealand.
  3. https://www.explainthatstuff.com/how-heat-exchangers-work.html

PS: Feel free to comment in case you want me to make any edits or any in general suggestions. Thanks for your time!

Let’s connect on LinkedIn: https://www.linkedin.com/in/prathameshmohite96/

--

--

Prathamesh Mohite
Analytics Vidhya

Passionate about addressing real world problems through an analytics driven approach.