5.4. Gasoline Blending#

** THIS WILL BE REVISED IN CLASS 3/22 (spill over to 3/24) **

The task is to determine the most profitable blend of gasoline products from given set of refinery streams.

import sys
if "google.colab" in sys.modules:
    !wget "https://raw.githubusercontent.com/ndcbe/CBE60499/main/notebooks/helper.py"
    import helper
    helper.install_idaes()
    helper.install_ipopt()
    helper.install_glpk()
    helper.install_cbc()
import pandas as pd
import pyomo.environ as pyo

5.4.1. Gasoline Product Specifications#

The gasoline products include regular and premium gasoline. In addition to the current price, the specifications include

  • octane the minimum road octane number. Road octane is the computed as the average of the Research Octane Number (RON) and Motor Octane Number (MON).

  • Reid Vapor Pressure Upper and lower limits are specified for the Reid vapor pressure. The Reid vapor pressure is the absolute pressure exerted by the liquid at 100°F.

  • benzene the maximum volume percentage of benzene allowed in the final product. Benzene helps to increase octane rating, but is also a treacherous environmental contaminant.

products = pd.DataFrame({
    'Regular'        : {'price': 2.75, 'octane': 87, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
    'Premium'        : {'price': 2.85, 'octane': 91, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
}).T

display(products)
price octane RVPmin RVPmax benzene
Regular 2.75 87.0 0.0 15.0 1.1
Premium 2.85 91.0 0.0 15.0 1.1

5.4.2. Stream Specifications#

A typical refinery produces many intermediate streams that can be incorporated in a blended gasoline product. Here we provide data on seven streams that include:

  • Butane n-butane is a C4 product stream produced from the light components of the crude being processed by the refinery. Butane is a highly volatile of gasoline.

  • LSR Light straight run naptha is a 90°F to 190°F cut from the crude distillation column primarily consisting of straight chain C5-C6 hydrocarbons.

  • Isomerate is the result of isomerizing LSR to produce branched molecules that results in higher octane number.

  • Reformate is result of catalytic reforming heavy straight run napthenes to produce a high octane blending component, as well by-product hydrogen used elsewhere in the refinery for hydro-treating.

  • Reformate LB is a is a low benzene variant of reformate.

  • FCC Naphta is the product of a fluidized catalytic cracking unit designed to produce gasoline blending components from long chain hydrocarbons present in the crude oil being processed by the refinery.

  • Alkylate The alkylation unit reacts iso-butane with low-molecular weight alkenes to produce a high octane blending component for gasoline.

The stream specifications include research octane and motor octane numbers for each blending component, the Reid vapor pressure, the benzene content, cost, and availability (in gallons per day). The road octane number is computed as the average of the RON and MON.

streams = pd.DataFrame({
    'Butane'       : {'RON': 93.0, 'MON': 92.0, 'RVP': 54.0, 'benzene': 0.00, 'cost': 0.85, 'avail': 30000},
    'LSR'          : {'RON': 78.0, 'MON': 76.0, 'RVP': 11.2, 'benzene': 0.73, 'cost': 2.05, 'avail': 35001},
    'Isomerate'    : {'RON': 83.0, 'MON': 81.1, 'RVP': 13.5, 'benzene': 0.00, 'cost': 2.20, 'avail': 0},
    'Reformate'    : {'RON':100.0, 'MON': 88.2, 'RVP':  3.2, 'benzene': 1.85, 'cost': 2.80, 'avail': 60000},
    'Reformate LB' : {'RON': 93.7, 'MON': 84.0, 'RVP':  2.8, 'benzene': 0.12, 'cost': 2.75, 'avail': 0},
    'FCC Naphtha'  : {'RON': 92.1, 'MON': 77.1, 'RVP':  1.4, 'benzene': 1.06, 'cost': 2.60, 'avail': 70000},
    'Alkylate'     : {'RON': 97.3, 'MON': 95.9, 'RVP':  4.6, 'benzene': 0.00, 'cost': 2.75, 'avail': 40000},
}).T

streams['octane'] = (streams['RON'] + streams['MON'])/2

display(streams)
RON MON RVP benzene cost avail octane
Butane 93.0 92.0 54.0 0.00 0.85 30000.0 92.50
LSR 78.0 76.0 11.2 0.73 2.05 35001.0 77.00
Isomerate 83.0 81.1 13.5 0.00 2.20 0.0 82.05
Reformate 100.0 88.2 3.2 1.85 2.80 60000.0 94.10
Reformate LB 93.7 84.0 2.8 0.12 2.75 0.0 88.85
FCC Naphtha 92.1 77.1 1.4 1.06 2.60 70000.0 84.60
Alkylate 97.3 95.9 4.6 0.00 2.75 40000.0 96.60

5.4.3. Questions we want to Answer#

  1. What is the maximum profit possible using the current product specifications and available streams?

  2. What are the marginal values of each blending stream? That is, how much would you be willing to pay for each additional gallon of the blending streams?

  3. A marketing team says there is an opportunity to create a mid-grade gasoline product with a road octane number of 89 that would sell for $2.82/gallon, and with all other specifications the same. Would an additional profit be created? What at what price point does the mid-grade product enhance profits?

  4. New environmental regulations have reduced the allowable benzene levels from 1.1 vol% to 0.62 vol%, and the maximum Reid vapor pressure from 15.0 to 9.0. What is the impact on profits?

5.4.4. Blending Model#

This simplified blending model assumes the product attributes can be computed as linear volume weighted averages of the component properties. Let the decision variable \(x_{s,p} \geq 0\) be the volume, in gallons, of blending component \(s \in S\) used in the final product \(p \in P\).

5.4.4.1. Objective#

The objective is maximize profit, which is the difference between product revenue and stream costs.

(5.1)#\[\begin{align} \mbox{profit} & = \max_{x_{s,p}}\left( \sum_{p\in P} \text{Price}_p \sum_{s\in S} x_{s,p} - \sum_{s\in S} \text{Cost}_s \sum_{p\in P} x_{s,p}\right) \\ & = \max_{x_{s,p}}\left(\sum_{p\in P}\sum_{s\in S}x_{s,p}(\text{Price}_p - \text{Cost}_s)\right) \end{align}\]

5.4.4.2. Raw Materials#

The first constraints in any blending problem are normally the limits on available raw materials. Letting $\text{

The blending constraint for octane can be written as

(5.2)#\[\begin{align} \frac{\sum_{s \in S} x_{s,p} \mbox{Octane}_s}{\sum_{s \in S} x_{s,p}} & \geq \mbox{Octane}_p & \forall p \in P \end{align}\]

where \(\mbox{Octane}_s\) refers to the octane rating of stream \(s\), whereas \(\mbox{Octane}_p\) refers to the octane rating of product \(p\). Multiplying through by the denominator, and consolidating terms gives

(5.3)#\[\begin{align} \sum_{s \in S} x_{s,p}\left(\mbox{Octane}_s - \mbox{Octane}_p\right) & \geq 0 & \forall p \in P \end{align}\]

The same assumptions and development apply to the benzene constraint

(5.4)#\[\begin{align} \sum_{s \in S} x_{s,p}\left(\mbox{Benzene}_s - \mbox{Benzene}_p\right) & \leq 0 & \forall p \in P \end{align}\]

Reid vapor pressure, however, follows a somewhat different mixing rule. For the Reid vapor pressure we have

(5.5)#\[\begin{align} \sum_{s \in S} x_{s,p}\left(\mbox{RVP}_s^{1.25} - \mbox{RVP}_{min,p}^{1.25}\right) & \geq 0 & \forall p \in P \\ \sum_{s \in S} x_{s,p}\left(\mbox{RVP}_s^{1.25} - \mbox{RVP}_{max,p}^{1.25}\right) & \leq 0 & \forall p \in P \end{align}\]

This model is implemented in the following cell.

import pyomo.environ as pyo

def gas_blending(products, streams):

    m = pyo.ConcreteModel("Gasoline Blending")
    
    m.PRODUCTS = pyo.Set(initialize=products.index)
    m.STREAMS = pyo.Set(initialize=streams.index)
    
    m.x = pyo.Var(m.STREAMS, m.PRODUCTS, domain=pyo.NonNegativeReals)
    
    @m.Objective(sense=pyo.maximize)
    def profit(m):
        return sum(sum(m.x[s, p]*(products.loc[p, 'price'] - streams.loc[s, 'cost']) for s in m.STREAMS) for p in m.PRODUCTS)
    
    @m.Constraint(m.STREAMS)
    def raw_material_available(m, s):
        return sum(m.x[s, p] for p in m.PRODUCTS) <= streams.loc[s, 'avail']
    
    @m.Constraint(m.PRODUCTS)
    def octane(m, p):
        return sum(m.x[s, p]*(streams.loc[s, 'octane'] - products.loc[p, 'octane']) for s in m.STREAMS) >= 0
    
    @m.Constraint(m.PRODUCTS)
    def benzene(m, p):
        return sum(m.x[s, p]*(streams.loc[s, 'benzene'] - products.loc[p, 'benzene']) for s in m.STREAMS) <= 0
   
    @m.Constraint(m.PRODUCTS)
    def min_reid_vapor_pressure(m, p):
        return sum(m.x[s, p]*(streams.loc[s, 'RVP']**1.25 - products.loc[p, 'RVPmin']**1.25) for s in m.STREAMS) >= 0
 
    @m.Constraint(m.PRODUCTS)
    def max_reid_vapor_pressure(m, p):
        return sum(m.x[s, p]*(streams.loc[s, 'RVP']**1.25 - products.loc[p, 'RVPmax']**1.25) for s in m.STREAMS) <= 0

    solver = pyo.SolverFactory('cbc')
    solver.solve(m)
    
    return m

m = gas_blending(products, streams)

    
soln = pd.DataFrame({s: {p: m.x[s,p]() for p in m.PRODUCTS} for s in m.STREAMS}).T
print("profit = ", m.profit())
display(soln)
profit =  100425.45002
Regular Premium
Butane 2.175503e+04 8244.9672
LSR 9.212971e+03 25788.0290
Isomerate 0.000000e+00 0.0000
Reformate 1.978550e+04 40214.5030
Reformate LB 0.000000e+00 0.0000
FCC Naphtha 7.000000e+04 0.0000
Alkylate -7.275958e-12 40000.0000
# create decision variables
S = streams.keys()
P = products.keys()
m.x = pyomo.Var(S,P, domain=pyomo.NonNegativeReals)
    
# objective
revenue = sum(sum(m.x[s,p]*products[p]['price'] for s in S) for p in P)
cost = sum(sum(m.x[s,p]*streams[s]['cost'] for s in S) for p in P)
m.profit = pyomo.Objective(expr = revenue - cost, sense=pyomo.maximize)

# constraints
m.cons = pyomo.ConstraintList()
for s in S:
    m.cons.add(sum(m.x[s,p] for p in P) <= streams[s]['avail'])
for p in P:
    m.cons.add(sum(m.x[s,p]*(feeds[s]['octane'] -    products[p]['octane'])       for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['RVP']**1.25 - products[p]['RVPmin']**1.25) for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['RVP']**1.25 - products[p]['RVPmax']**1.25) for s in S) <= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['benzene'] -   products[p]['benzene'])      for s in S) <= 0)

# solve
solver = pyomo.SolverFactory('glpk')
solver.solve(m)

# display results
vol = sum(m.x[s,p]() for s in S for p in P)
print("Total Volume =", round(vol, 1), "gallons.")
print("Total Profit =", round(m.profit(), 1), "dollars.")
print("Profit =", round(100*m.profit()/vol,1), "cents per gallon.")
Total Volume = 217971.0 gallons.
Total Profit = 60022.2 dollars.
Profit = 27.5 cents per gallon.

5.4.5. Display Results#

5.4.5.1. Results for each Stream#

stream_results = pd.DataFrame()
for s in S:
    for p in P:
        stream_results.loc[s,p] = round(m.x[s,p](), 1)
    stream_results.loc[s,'Total'] = round(sum(m.x[s,p]() for p in P), 1)
    stream_results.loc[s,'Available'] = streams[s]['avail']
    
stream_results['Unused (Slack)'] = stream_results['Available'] - stream_results['Total']
print(stream_results)
              Regular  Premium    Total  Available  Unused (Slack)
Butane        17924.9  12075.1  30000.0    30000.0             0.0
LSR           35000.0      0.0  35000.0    35000.0             0.0
Isomerate         0.0      0.0      0.0        0.0             0.0
Reformate     43599.3  16400.7  60000.0    60000.0             0.0
Reformate LB      0.0      0.0      0.0        0.0             0.0
FCC Naphtha   24225.8  45774.2  70000.0    70000.0             0.0
Alkylate          0.0  40000.0  40000.0    40000.0             0.0

5.4.5.2. Results for each Product#

product_results = pd.DataFrame()
for p in P:
    product_results.loc[p,'Volume'] = round(sum(m.x[s,p]() for s in S), 1)
    product_results.loc[p,'octane'] = round(sum(m.x[s,p]()*streams[s]['octane'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
    product_results.loc[p,'RVP'] = round((sum(m.x[s,p]()*streams[s]['RVP']**1.25 for s in S)
                                            /product_results.loc[p,'Volume'])**0.8, 1)
    product_results.loc[p,'benzene'] = round(sum(m.x[s,p]()*streams[s]['benzene'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
print(product_results)
           Volume  octane   RVP  benzene
Regular  120750.0    87.0  15.0      1.1
Premium  114250.0    91.0  10.6      0.7

5.4.6. Exercise 1.#

The marketing team says there is an opportunity to create a mid-grade gasoline product with a road octane number of 89 that would sell for $2.82/gallon, and with all other specifications the same. Could an additional profit be created?

Create a new cell (or cells) below to compute a solution to this exercise.

products = {
    'Regular'        : {'price': 2.75, 'octane': 87, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
    'Midgrade'       : {'price': 2.82, 'octane': 89, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
    'Premium'        : {'price': 2.85, 'octane': 91, 'RVPmin': 0.0, 'RVPmax': 15.0, 'benzene': 1.1},
}

print(pd.DataFrame.from_dict(products).T)

streams = {
    'Butane'       : {'RON': 93.0, 'MON': 92.0, 'RVP': 54.0, 'benzene': 0.00, 'cost': 0.85, 'avail': 30000},
    'LSR'          : {'RON': 78.0, 'MON': 76.0, 'RVP': 11.2, 'benzene': 0.73, 'cost': 2.05, 'avail': 35000},
    'Isomerate'    : {'RON': 83.0, 'MON': 81.1, 'RVP': 13.5, 'benzene': 0.00, 'cost': 2.20, 'avail': 0},
    'Reformate'    : {'RON':100.0, 'MON': 88.2, 'RVP':  3.2, 'benzene': 1.85, 'cost': 2.80, 'avail': 60000},
    'Reformate LB' : {'RON': 93.7, 'MON': 84.0, 'RVP':  2.8, 'benzene': 0.12, 'cost': 2.75, 'avail': 0},
    'FCC Naphtha'  : {'RON': 92.1, 'MON': 77.1, 'RVP':  1.4, 'benzene': 1.06, 'cost': 2.60, 'avail': 70000},
    'Alkylate'     : {'RON': 97.3, 'MON': 95.9, 'RVP':  4.6, 'benzene': 0.00, 'cost': 2.75, 'avail': 40000},
}

# calculate road octane as (R+M)/2
for s in streams.keys():
    streams[s]['octane'] = (streams[s]['RON'] + streams[s]['MON'])/2
    
# display feed information
print(pd.DataFrame.from_dict(streams).T)

# create model
m = pyomo.ConcreteModel()

# create decision variables
S = streams.keys()
P = products.keys()
m.x = pyomo.Var(S,P, domain=pyomo.NonNegativeReals)
    
# objective
revenue = sum(sum(m.x[s,p]*products[p]['price'] for s in S) for p in P)
cost = sum(sum(m.x[s,p]*streams[s]['cost'] for s in S) for p in P)
m.profit = pyomo.Objective(expr = revenue - cost, sense=pyomo.maximize)

# constraints
m.cons = pyomo.ConstraintList()
for s in S:
    m.cons.add(sum(m.x[s,p] for p in P) <= streams[s]['avail'])
for p in P:
    m.cons.add(sum(m.x[s,p]*(feeds[s]['octane'] -    products[p]['octane'])       for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['RVP']**1.25 - products[p]['RVPmin']**1.25) for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['RVP']**1.25 - products[p]['RVPmax']**1.25) for s in S) <= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['benzene'] -   products[p]['benzene'])      for s in S) <= 0)

# solve
solver = pyomo.SolverFactory('glpk')
solver.solve(m)
print("Profit = $", round(m.profit(),2))

stream_results = pd.DataFrame()
for s in S:
    for p in P:
        stream_results.loc[s,p] = round(m.x[s,p](), 1)
    stream_results.loc[s,'Total'] = round(sum(m.x[s,p]() for p in P), 1)
    stream_results.loc[s,'Available'] = streams[s]['avail']
    
stream_results['Unused (Slack)'] = stream_results['Available'] - stream_results['Total']
print(stream_results)

product_results = pd.DataFrame()
for p in P:
    product_results.loc[p,'Volume'] = round(sum(m.x[s,p]() for s in S), 1)
    product_results.loc[p,'octane'] = round(sum(m.x[s,p]()*streams[s]['octane'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
    product_results.loc[p,'RVP'] = round((sum(m.x[s,p]()*streams[s]['RVP']**1.25 for s in S)
                                            /product_results.loc[p,'Volume'])**0.8, 1)
    product_results.loc[p,'benzene'] = round(sum(m.x[s,p]()*streams[s]['benzene'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
print(product_results)

vol = sum(m.x[s,p]() for s in S for p in P)
print("Total Profit =", round(m.profit(), 1), "dollars.")
print("Total Volume =", round(vol, 1), "gallons.")
print("Profit =", round(100*m.profit()/vol,1), "cents per gallon.")
          RVPmax  RVPmin  benzene  octane  price
Midgrade    15.0     0.0      1.1    89.0   2.82
Premium     15.0     0.0      1.1    91.0   2.85
Regular     15.0     0.0      1.1    87.0   2.75
               MON    RON   RVP    avail  benzene  cost  octane
Alkylate      95.9   97.3   4.6  40000.0     0.00  2.75   96.60
Butane        92.0   93.0  54.0  30000.0     0.00  0.85   92.50
FCC Naphtha   77.1   92.1   1.4  70000.0     1.06  2.60   84.60
Isomerate     81.1   83.0  13.5      0.0     0.00  2.20   82.05
LSR           76.0   78.0  11.2  35000.0     0.73  2.05   77.00
Reformate     88.2  100.0   3.2  60000.0     1.85  2.80   94.10
Reformate LB  84.0   93.7   2.8      0.0     0.12  2.75   88.85
Profit = $ 104995.0
              Regular  Midgrade  Premium    Total  Available  Unused (Slack)
Butane            0.0   30000.0      0.0  30000.0    30000.0             0.0
LSR            2833.3   32166.7      0.0  35000.0    35000.0             0.0
Isomerate         0.0       0.0      0.0      0.0        0.0             0.0
Reformate      2746.9   57253.1      0.0  60000.0    60000.0             0.0
Reformate LB      0.0       0.0      0.0      0.0        0.0             0.0
FCC Naphtha       0.0   70000.0      0.0  70000.0    70000.0             0.0
Alkylate        919.8   39080.2      0.0  40000.0    40000.0             0.0
            Volume  octane   RVP  benzene
Regular     6500.0    87.0   7.2      1.1
Midgrade  228500.0    89.0  13.0      0.9
Premium        0.0     NaN   NaN      NaN
Total Profit = 104995.0 dollars.
Total Volume = 235000.0 gallons.
Profit = 44.7 cents per gallon.
/Users/jeff/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:68: RuntimeWarning: invalid value encountered in double_scalars
/Users/jeff/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:70: RuntimeWarning: invalid value encountered in double_scalars
/Users/jeff/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:72: RuntimeWarning: invalid value encountered in double_scalars

5.4.7. Exercise 2.#

New environmental regulations have reduced the allowable benzene levels from 1.1 vol% to 0.62 vol%, and the maximum Reid vapor pressure from 15.0 to 9.0.

Compared to the base case (i.e., without the midgrade product), how does this change profitability?

products = {
    'Regular'        : {'price': 2.75, 'octane': 87, 'RVPmin': 0.0, 'RVPmax': 9.0, 'benzene': 0.62},
    'Premium'        : {'price': 2.85, 'octane': 91, 'RVPmin': 0.0, 'RVPmax': 9.0, 'benzene': 0.62},
}

print(pd.DataFrame.from_dict(products).T)

streams = {
    'Butane'       : {'RON': 93.0, 'MON': 92.0, 'RVP': 54.0, 'benzene': 0.00, 'cost': 0.85, 'avail': 30000},
    'LSR'          : {'RON': 78.0, 'MON': 76.0, 'RVP': 11.2, 'benzene': 0.73, 'cost': 2.05, 'avail': 35000},
    'Isomerate'    : {'RON': 83.0, 'MON': 81.1, 'RVP': 13.5, 'benzene': 0.00, 'cost': 2.20, 'avail': 0},
    'Reformate'    : {'RON':100.0, 'MON': 88.2, 'RVP':  3.2, 'benzene': 1.85, 'cost': 2.80, 'avail': 60000},
    'Reformate LB' : {'RON': 93.7, 'MON': 84.0, 'RVP':  2.8, 'benzene': 0.12, 'cost': 2.75, 'avail': 0},
    'FCC Naphtha'  : {'RON': 92.1, 'MON': 77.1, 'RVP':  1.4, 'benzene': 1.06, 'cost': 2.60, 'avail': 70000},
    'Alkylate'     : {'RON': 97.3, 'MON': 95.9, 'RVP':  4.6, 'benzene': 0.00, 'cost': 2.75, 'avail': 40000},
}

# calculate road octane as (R+M)/2
for s in streams.keys():
    streams[s]['octane'] = (streams[s]['RON'] + streams[s]['MON'])/2
    
# display feed information
print(pd.DataFrame.from_dict(streams).T)

# create model
m = pyomo.ConcreteModel()

# create decision variables
S = streams.keys()
P = products.keys()
m.x = pyomo.Var(S,P, domain=pyomo.NonNegativeReals)
    
# objective
revenue = sum(sum(m.x[s,p]*products[p]['price'] for s in S) for p in P)
cost = sum(sum(m.x[s,p]*streams[s]['cost'] for s in S) for p in P)
m.profit = pyomo.Objective(expr = revenue - cost, sense=pyomo.maximize)

# constraints
m.cons = pyomo.ConstraintList()
for s in S:
    m.cons.add(sum(m.x[s,p] for p in P) <= streams[s]['avail'])
for p in P:
    m.cons.add(sum(m.x[s,p]*(feeds[s]['octane'] -    products[p]['octane'])       for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['RVP']**1.25 - products[p]['RVPmin']**1.25) for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['RVP']**1.25 - products[p]['RVPmax']**1.25) for s in S) <= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['benzene'] -   products[p]['benzene'])      for s in S) <= 0)

# solve
solver = pyomo.SolverFactory('glpk')
solver.solve(m)
print("Profit = $", round(m.profit(),2))

stream_results = pd.DataFrame()
for s in S:
    for p in P:
        stream_results.loc[s,p] = round(m.x[s,p](), 1)
    stream_results.loc[s,'Total'] = round(sum(m.x[s,p]() for p in P), 1)
    stream_results.loc[s,'Available'] = streams[s]['avail']
    
stream_results['Unused (Slack)'] = stream_results['Available'] - stream_results['Total']
print(stream_results)

product_results = pd.DataFrame()
for p in P:
    product_results.loc[p,'Volume'] = round(sum(m.x[s,p]() for s in S), 1)
    product_results.loc[p,'octane'] = round(sum(m.x[s,p]()*streams[s]['octane'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
    product_results.loc[p,'RVP'] = round((sum(m.x[s,p]()*streams[s]['RVP']**1.25 for s in S)
                                            /product_results.loc[p,'Volume'])**0.8, 1)
    product_results.loc[p,'benzene'] = round(sum(m.x[s,p]()*streams[s]['benzene'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
print(product_results)

vol = sum(m.x[s,p]() for s in S for p in P)
print("Total Profit =", round(m.profit(), 1), "dollars.")
print("Total Volume =", round(vol, 1), "gallons.")
print("Profit =", round(100*m.profit()/vol,1), "cents per gallon.")
         RVPmax  RVPmin  benzene  octane  price
Premium     9.0     0.0     0.62    91.0   2.85
Regular     9.0     0.0     0.62    87.0   2.75
               MON    RON   RVP    avail  benzene  cost  octane
Alkylate      95.9   97.3   4.6  40000.0     0.00  2.75   96.60
Butane        92.0   93.0  54.0  30000.0     0.00  0.85   92.50
FCC Naphtha   77.1   92.1   1.4  70000.0     1.06  2.60   84.60
Isomerate     81.1   83.0  13.5      0.0     0.00  2.20   82.05
LSR           76.0   78.0  11.2  35000.0     0.73  2.05   77.00
Reformate     88.2  100.0   3.2  60000.0     1.85  2.80   94.10
Reformate LB  84.0   93.7   2.8      0.0     0.12  2.75   88.85
Profit = $ 44493.62
              Regular  Premium    Total  Available  Unused (Slack)
Butane         8187.5      0.0   8187.5    30000.0         21812.5
LSR           28305.3      0.0  28305.3    35000.0          6694.7
Isomerate         0.0      0.0      0.0        0.0             0.0
Reformate         0.0      0.0      0.0    60000.0         60000.0
Reformate LB      0.0      0.0      0.0        0.0             0.0
FCC Naphtha   60824.3      0.0  60824.3    70000.0          9175.7
Alkylate      40000.0      0.0  40000.0    40000.0             0.0
           Volume  octane  RVP  benzene
Regular  137317.1    87.0  9.0      0.6
Premium       0.0     NaN  NaN      NaN
Total Profit = 44493.6 dollars.
Total Volume = 137317.1 gallons.
Profit = 32.4 cents per gallon.
/Users/jeff/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:67: RuntimeWarning: invalid value encountered in double_scalars
/Users/jeff/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:69: RuntimeWarning: invalid value encountered in double_scalars
/Users/jeff/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:71: RuntimeWarning: invalid value encountered in double_scalars

5.4.8. Exercise 3.#

Given the new product specifications in Exercise 2, let’s consider using different refinery streams. In place of Reformate, the refinery could produce Reformate LB. (That is, one or the other of the two streams could be 60000 gallons per day, but not both). Same for LSR and Reformate. How should the refinery be operated to maximize profitability?

products = {
    'Regular'        : {'price': 2.75, 'octane': 87, 'RVPmin': 0.0, 'RVPmax': 9.0, 'benzene': 0.62},
    'Premium'        : {'price': 2.85, 'octane': 91, 'RVPmin': 0.0, 'RVPmax': 9.0, 'benzene': 0.62},
}

print(pd.DataFrame.from_dict(products).T)

streams = {
    'Butane'       : {'RON': 93.0, 'MON': 92.0, 'RVP': 54.0, 'benzene': 0.00, 'cost': 0.85, 'avail': 30000},
    'LSR'          : {'RON': 78.0, 'MON': 76.0, 'RVP': 11.2, 'benzene': 0.73, 'cost': 2.05, 'avail': 35000},
    'Isomerate'    : {'RON': 83.0, 'MON': 81.1, 'RVP': 13.5, 'benzene': 0.00, 'cost': 2.20, 'avail': 0},
    'Reformate'    : {'RON':100.0, 'MON': 88.2, 'RVP':  3.2, 'benzene': 1.85, 'cost': 2.80, 'avail': 0},
    'Reformate LB' : {'RON': 93.7, 'MON': 84.0, 'RVP':  2.8, 'benzene': 0.12, 'cost': 2.75, 'avail': 60000},
    'FCC Naphtha'  : {'RON': 92.1, 'MON': 77.1, 'RVP':  1.4, 'benzene': 1.06, 'cost': 2.60, 'avail': 70000},
    'Alkylate'     : {'RON': 97.3, 'MON': 95.9, 'RVP':  4.6, 'benzene': 0.00, 'cost': 2.75, 'avail': 40000},
}

# calculate road octane as (R+M)/2
for s in streams.keys():
    streams[s]['octane'] = (streams[s]['RON'] + streams[s]['MON'])/2
    
# display feed information
print(pd.DataFrame.from_dict(streams).T)

# create model
m = pyomo.ConcreteModel()

# create decision variables
S = streams.keys()
P = products.keys()
m.x = pyomo.Var(S,P, domain=pyomo.NonNegativeReals)
    
# objective
revenue = sum(sum(m.x[s,p]*products[p]['price'] for s in S) for p in P)
cost = sum(sum(m.x[s,p]*streams[s]['cost'] for s in S) for p in P)
m.profit = pyomo.Objective(expr = revenue - cost, sense=pyomo.maximize)

# constraints
m.cons = pyomo.ConstraintList()
for s in S:
    m.cons.add(sum(m.x[s,p] for p in P) <= streams[s]['avail'])
for p in P:
    m.cons.add(sum(m.x[s,p]*(feeds[s]['octane'] -    products[p]['octane'])       for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['RVP']**1.25 - products[p]['RVPmin']**1.25) for s in S) >= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['RVP']**1.25 - products[p]['RVPmax']**1.25) for s in S) <= 0)
    m.cons.add(sum(m.x[s,p]*(feeds[s]['benzene'] -   products[p]['benzene'])      for s in S) <= 0)
    
# solve
solver = pyomo.SolverFactory('glpk')
solver.solve(m)
print("Profit = $", round(m.profit(),2))

stream_results = pd.DataFrame()
for s in S:
    for p in P:
        stream_results.loc[s,p] = round(m.x[s,p](), 1)
    stream_results.loc[s,'Total'] = round(sum(m.x[s,p]() for p in P), 1)
    stream_results.loc[s,'Available'] = streams[s]['avail']
    
stream_results['Unused (Slack)'] = stream_results['Available'] - stream_results['Total']
print(stream_results)

product_results = pd.DataFrame()
for p in P:
    product_results.loc[p,'Volume'] = round(sum(m.x[s,p]() for s in S), 1)
    product_results.loc[p,'octane'] = round(sum(m.x[s,p]()*streams[s]['octane'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
    product_results.loc[p,'RVP'] = round((sum(m.x[s,p]()*streams[s]['RVP']**1.25 for s in S)
                                            /product_results.loc[p,'Volume'])**0.8, 1)
    product_results.loc[p,'benzene'] = round(sum(m.x[s,p]()*streams[s]['benzene'] for s in S)
                                            /product_results.loc[p,'Volume'], 1)
print(product_results)

vol = sum(m.x[s,p]() for s in S for p in P)
print("Total Profit =", round(m.profit(), 1), "dollars.")
print("Total Volume =", round(vol, 1), "gallons.")
print("Profit =", round(100*m.profit()/vol,1), "cents per gallon.")
         RVPmax  RVPmin  benzene  octane  price
Premium     9.0     0.0     0.62    91.0   2.85
Regular     9.0     0.0     0.62    87.0   2.75
               MON    RON   RVP    avail  benzene  cost  octane
Alkylate      95.9   97.3   4.6  40000.0     0.00  2.75   96.60
Butane        92.0   93.0  54.0  30000.0     0.00  0.85   92.50
FCC Naphtha   77.1   92.1   1.4  70000.0     1.06  2.60   84.60
Isomerate     81.1   83.0  13.5      0.0     0.00  2.20   82.05
LSR           76.0   78.0  11.2  35000.0     0.73  2.05   77.00
Reformate     88.2  100.0   3.2      0.0     1.85  2.80   94.10
Reformate LB  84.0   93.7   2.8  60000.0     0.12  2.75   88.85
Profit = $ 63791.16
              Regular  Premium    Total  Available  Unused (Slack)
Butane        13290.3   1122.5  14412.8    30000.0         15587.2
LSR           35000.0      0.0  35000.0    35000.0             0.0
Isomerate         0.0      0.0      0.0        0.0             0.0
Reformate         0.0      0.0      0.0        0.0             0.0
Reformate LB  60000.0      0.0  60000.0    60000.0             0.0
FCC Naphtha   63818.6   6181.4  70000.0    70000.0             0.0
Alkylate      33236.2   6763.8  40000.0    40000.0             0.0
           Volume  octane  RVP  benzene
Regular  205345.2    87.0  9.0      0.5
Premium   14067.7    91.0  9.0      0.5
Total Profit = 63791.2 dollars.
Total Volume = 219412.8 gallons.
Profit = 29.1 cents per gallon.