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

# 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

- Edgar, T.F., Himmelblau, D.M. and Lasdon, L.S., 2001.
*Optimization of chemical processes*. McGraw-Hill,. - Mitchell, S., OSullivan, M. and Dunning, I., 2011. PuLP: a linear programming toolkit for python.
*The University of Auckland, Auckland, New Zealand*. - 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/