Heat Integrated Distillation Sequences - MINLP (MINLPHIX)#

minlphix.py

"""
## GAMSSOURCE: https://www.gams.com/latest/gamslib_ml/libhtml/gamslib_minlphix.html
## LICENSETYPE: Demo
## MODELTYPE: MINLP
## KEYWORDS: mixed integer nonlinear programming, chemical engineering, distillation sequences, heat integrated distillation


Heat Integrated Distillation Sequences - MINLP (MINLPHIX)

This is a direct MINLP formulation of the model MINLPHI.


Morari, M, and Grossmann, I E, Eds, Chemical Engineering
Optimization Models with GAMS. Computer Aids for Chemical
Engineering Corporation, 1991.

Floudas, C A, and Paules IV, G E, A Mixed-Integer Nonlinear
Programming Formulation for the Synthesis of Heat Integrated
Distillation Sequence. Computers and Chemical Engineering 12,
6 (1988), 531-546.


This formulation provides the Optimal Heat Integrated
Distillation Sequence with Pressure as a continuous variable
for a three component separation.

           Components:     a == Hexane
                           b == Benzene
                           c == Heptane

total feed to superstructure == 396 kgmol/hr

multicomponent feed composition: a = 0.80
                                 b = 0.10
                                 c = 0.10


A Superstructure of the form ...

                        _______               _______
                       _|_    |              _|_    |
                      /   \  ( )            /   \  ( )
                      |   |___|__ A         |   |___|___ B
                      |   |                 |   |
            |---------| 1 |                 | 3 |
            |         |   |       ----------|   |
            |         |   |       |         |   |
            |         |   |_______|         |   |
            |         \___/  |  BC          \___/_______ C
     F      |           |   ( )               |     |
   -------->|           |____|                |----( )
   (ABC)    |
            |           _______               _______
            |          _|_    |              _|_    |
            |         /   \  ( )            /   \  ( )
            |         |   |___| AB          |   |___|___ A
            |         |   |   |_____________|   |
            |---------| 2 |                 | 4 |
                      |   |                 |   |
                      |   |                 |   |
                      |   |______ C         |   |_______ B
                      \___/  |              \___/   |
                        |   ( )               |    ( )
                        |____|                |_____|


is used with binary variables representing:
   a_  the existence of columns in the sequence.
   b_  the selection of heat exchangers for heat integration.
   c_  the selection of hot and cold utilities.


Associated Reference:

"A Mixed-Integer Nonlinear Programming formulation for the
 synthesis of Heat-Integrated Distillation Sequences"

 C.A. Floudas and G.E. Paules IV,  1988.
 Computers and Chemical Engineering vol. 12 no. 6 pp. 531-546
"""

from __future__ import annotations

import os

import numpy as np
import pandas as pd
from gamspy import (
    Alias,
    Container,
    Domain,
    Equation,
    Model,
    Options,
    Ord,
    Parameter,
    Sense,
    Set,
    Sum,
    Variable,
)
from gamspy.math import sqrt


def main():
    cont = Container(
        system_directory=os.getenv("SYSTEM_DIRECTORY", None),
    )

    # Set
    i = Set(
        cont,
        name="i",
        records=[f"c-{i}" for i in range(1, 5)],
        description="condensers-columns",
    )
    j = Set(
        cont,
        name="j",
        records=[f"r-{i}" for i in range(1, 5)],
        description="reboilers",
    )
    hu = Set(
        cont, name="hu", records=["lp", "ex"], description="hot utilities"
    )
    cu = Set(cont, name="cu", records=["cw"], description="cold utilities")
    n = Set(cont, name="n", records=["a", "b"], description="index")
    m = Set(cont, name="m", records=["ab", "bc"], description="intermediates")
    pm = Set(
        cont,
        name="pm",
        domain=[i, m],
        records=[("c-1", "bc"), ("c-2", "ab")],
        description="products",
    )
    fm = Set(
        cont,
        name="fm",
        domain=[i, m],
        records=[("c-3", "bc"), ("c-4", "ab")],
        description="feeds",
    )

    ip = Alias(cont, name="ip", alias_with=i)
    jp = Alias(cont, name="jp", alias_with=j)

    # ====================================================================
    # Definition of "z" sets for conditional control of model
    # used to map permissible matches between condensers and reboilers
    # and the position of columns in the superstructure
    # =====================================================================

    # Set
    zlead = Set(
        cont,
        name="zlead",
        domain=i,
        records=["c-1", "c-2"],
        description="leading columns in superstructure",
    )
    zcrhx = Set(
        cont,
        name="zcrhx",
        domain=[i, j],
        records=[
            ("c-1", "r-3"),
            ("c-2", "r-4"),
            ("c-3", "r-1"),
            ("c-4", "r-2"),
        ],
        description="condenser to reboiler allowable matches",
    )
    zlim = Set(
        cont,
        name="zlim",
        domain=[i, j],
        description="direction of heat integration",
    )
    zcr = Set(
        cont, name="zcr", domain=[i, j], description="reboiler-condenser pairs"
    )

    zlim[i, j] = zcrhx[i, j] & (Ord(i) < Ord(j))
    zcr[i, j] = Ord(i) == Ord(j)

    # Parameter
    spltfrc = Parameter(
        cont,
        name="spltfrc",
        domain=[i, m],
        records=pd.DataFrame([["c-1", "bc", 0.20], ["c-2", "ab", 0.90]]),
        description="split fraction of distillation columns",
    )

    tcmin = Parameter(
        cont,
        name="tcmin",
        domain=i,
        records=np.array([341.92, 343.01, 353.54, 341.92]),
        description="minimum condenser temperatures",
    )
    trmax = Parameter(
        cont,
        name="trmax",
        domain=j,
        description="maximum reboiler temperatures",
    )
    trmax[j] = 1000

    # ====================================================================
    # scaled cost coefficients for distillation column fits
    # nonlinear fixed-charge cost model
    #   cost = fc*y + vc*flow*temp
    # scaling factor = 1000
    # ====================================================================

    # Parameter
    fc = Parameter(
        cont,
        name="fc",
        domain=i,
        records=np.array([151.125, 180.003, 4.2286, 213.42]),
        description="fixed charge for distillation columns",
    )
    vc = Parameter(
        cont,
        name="vc",
        domain=i,
        records=np.array([0.003375, 0.000893, 0.004458, 0.003176]),
        description="variable charge for distillation columns",
    )
    thu = Parameter(
        cont,
        name="thu",
        domain=hu,
        records=np.array([421.0, 373.0]),
        description="hot utility temperatures",
    )

    # hot utility cost coeff - gives cost in thousands of dollars per year
    # ucost = q(10e+6 kj/hr)*costhu(hu)

    costhu = Parameter(
        cont,
        name="costhu",
        domain=hu,
        records=np.array([24.908, 9.139]),
        description="hot utility cost coefficients",
    )

    kf = Parameter(
        cont,
        name="kf",
        domain=[i, n],
        records=np.array(
            [
                [32.4, 0.0225],
                [25.0, 0.0130],
                [3.76, 0.0043],
                [35.1, 0.0156],
            ]
        ),
        description="coeff. for heat duty temperature fits",
    )

    af = Parameter(
        cont,
        name="af",
        domain=[i, n],
        records=np.array(
            [
                [9.541, 1.028],
                [12.24, 1.050],
                [8.756, 1.029],
                [9.181, 1.005],
            ]
        ),
        description="coeff. for column temperature fits",
    )

    # Scalar
    totflow = Parameter(
        cont,
        name="totflow",
        records=396,
        description="total flow to superstructure",
    )
    fchx = Parameter(
        cont,
        name="fchx",
        records=3.392,
        description="fixed charge for heat exchangers scaled",
    )
    vchx = Parameter(
        cont,
        name="vchx",
        records=0.0893,
        description="variable charge for heat exchangers scaled",
    )
    htc = Parameter(
        cont,
        name="htc",
        records=0.0028,
        description="overall heat transfer coefficient",
    )
    dtmin = Parameter(
        cont,
        name="dtmin",
        records=10.0,
        description="minimum temperature approach",
    )
    tcin = Parameter(
        cont,
        name="tcin",
        records=305.0,
        description="inlet temperature of cold water",
    )
    tcout = Parameter(
        cont,
        name="tcout",
        records=325.0,
        description="outlet temperature of cold water",
    )
    costcw = Parameter(
        cont,
        name="costcw",
        records=4.65,
        description="cooling water cost coefficient",
    )
    beta = Parameter(
        cont,
        name="beta",
        records=0.52,
        description="income tax correction factor",
    )
    alpha = Parameter(
        cont,
        name="alpha",
        records=0.40,
        description="one over payout time factor in years",
    )
    u = Parameter(
        cont,
        name="u",
        records=1500,
        description="large number for logical constraints",
    )
    uint = Parameter(
        cont,
        name="uint",
        records=20,
        description="upper bound for integer logical",
    )

    # Positive Variables
    f = Variable(
        cont,
        name="f",
        type="positive",
        domain=i,
        description="flowrates to columns",
    )
    qr = Variable(
        cont,
        name="qr",
        type="positive",
        domain=j,
        description="reboiler duties for column with reboiler j",
    )
    qc = Variable(
        cont,
        name="qc",
        type="positive",
        domain=i,
        description="condenser duties for column i",
    )
    qcr = Variable(
        cont,
        name="qcr",
        type="positive",
        domain=[i, j],
        description="heat integration heat transfer",
    )
    qhu = Variable(
        cont,
        name="qhu",
        type="positive",
        domain=[hu, j],
        description="hot utility heat transfer",
    )
    qcu = Variable(
        cont,
        name="qcu",
        type="positive",
        domain=[i, cu],
        description="cold utility heat transfer",
    )
    tc = Variable(
        cont,
        name="tc",
        type="positive",
        domain=i,
        description="condenser temperature for column with cond. i",
    )
    tr = Variable(
        cont,
        name="tr",
        type="positive",
        domain=j,
        description="reboiler temperature for column with reb. j",
    )
    lmtd = Variable(
        cont,
        name="lmtd",
        type="positive",
        domain=i,
        description="lmtd for cooling water exchanges",
    )
    sl1 = Variable(
        cont,
        name="sl1",
        type="positive",
        domain=i,
        description="artificial slack variable for lmtd equalities",
    )
    sl2 = Variable(
        cont,
        name="sl2",
        type="positive",
        domain=i,
        description="artificial slack variable for lmtd equalities",
    )
    s1 = Variable(
        cont,
        name="s1",
        type="positive",
        domain=i,
        description="artificial slack variable for reb-con equalities",
    )
    s2 = Variable(
        cont,
        name="s2",
        type="positive",
        domain=i,
        description="artificial slack variable for reb-con equalities",
    )
    s3 = Variable(
        cont,
        name="s3",
        type="positive",
        domain=i,
        description="artificial slack variable for duty equalities",
    )
    s4 = Variable(
        cont,
        name="s4",
        type="positive",
        domain=i,
        description="artificial slack variable for duty equalities",
    )

    # Binary Variable
    yhx = Variable(
        cont,
        name="yhx",
        type="binary",
        domain=[i, j],
        description="heat integration matches condenser i reboiler j",
    )
    yhu = Variable(
        cont,
        name="yhu",
        type="binary",
        domain=[hu, j],
        description="hot utility matches hot utility hu reboiler j",
    )
    ycu = Variable(
        cont,
        name="ycu",
        type="binary",
        domain=[i, cu],
        description="cold utility matches condenser i cold util cu",
    )
    ycol = Variable(
        cont,
        name="ycol",
        type="binary",
        domain=i,
        description="columns in superstructure",
    )

    # Equation
    tctrlo = Equation(
        cont,
        name="tctrlo",
        domain=[i, j],
        description="prevent division by 0 in the objective",
    )
    lmtdlo = Equation(
        cont,
        name="lmtdlo",
        domain=i,
        description="prevent division by 0 in the objective",
    )
    lmtdsn = Equation(
        cont,
        name="lmtdsn",
        domain=i,
        description="nonlinear form of lmtd definition",
    )
    tempset = Equation(
        cont,
        name="tempset",
        domain=i,
        description="sets temperatures of inactive columns to 0 (milp)",
    )
    artrex1 = Equation(
        cont,
        name="artrex1",
        domain=i,
        description="relaxes artificial slack variables (nlp)",
    )
    artrex2 = Equation(
        cont,
        name="artrex2",
        domain=i,
        description="relaxes artificial slack variables (nlp)",
    )
    material = Equation(
        cont,
        name="material",
        domain=m,
        description="material balances for each intermediate product",
    )
    feed = Equation(cont, name="feed", description="feed to superstructure")
    matlog = Equation(
        cont,
        name="matlog",
        domain=i,
        description="material balance logical constraints",
    )
    duty = Equation(
        cont,
        name="duty",
        domain=i,
        description="heat duty definition of condenser i",
    )
    rebcon = Equation(
        cont,
        name="rebcon",
        domain=[i, j],
        description="equates condenser and reboiler duties",
    )
    conheat = Equation(
        cont, name="conheat", domain=i, description="condenser heat balances"
    )
    rebheat = Equation(
        cont, name="rebheat", domain=j, description="reboiler heat balances"
    )
    dtminlp = Equation(
        cont,
        name="dtminlp",
        domain=j,
        description="minimum temp approach for low pressure steam",
    )
    dtminc = Equation(
        cont,
        name="dtminc",
        domain=i,
        description="minimum temp allowable for each condenser",
    )
    trtcdef = Equation(
        cont,
        name="trtcdef",
        domain=[i, j],
        description="relates reboiler and condenser temps of columns",
    )
    dtmincr = Equation(
        cont,
        name="dtmincr",
        domain=[i, j],
        description="minimum temp approach for heat integration",
    )
    dtminex = Equation(
        cont,
        name="dtminex",
        domain=j,
        description="minimum temp approach for exhaust steam",
    )
    hxclog = Equation(
        cont,
        name="hxclog",
        domain=[i, j],
        description="logical constraint for heat balances",
    )
    hxhulog = Equation(
        cont,
        name="hxhulog",
        domain=[hu, j],
        description="logical constraint for heat balances",
    )
    hxculog = Equation(
        cont,
        name="hxculog",
        domain=[i, cu],
        description="logical constraint for heat balances",
    )
    qcqrlog = Equation(
        cont,
        name="qcqrlog",
        domain=i,
        description="logical constraint for con-reb duties",
    )

    # these are the pure binary constraints of the minlp
    sequen = Equation(
        cont,
        name="sequen",
        domain=m,
        description="restricts superstructure to a single sequence",
    )
    lead = Equation(cont, name="lead", description="sequence control")
    limutil = Equation(
        cont,
        name="limutil",
        domain=j,
        description="limits columns to have a single hot utility",
    )
    hidirect = Equation(
        cont,
        name="hidirect",
        domain=[i, j],
        description="requires a single direction of heat integration",
    )
    heat = Equation(
        cont, name="heat", domain=i, description="logical integer constraint"
    )

    # nlp subproblems objective
    zoau = alpha * (
        Sum(i, fc[i] * ycol[i] + vc[i] * (tc[i] - tcmin[i]) * f[i])
        + Sum(
            zcrhx[i, j],
            fchx * yhx[i, j]
            + (vchx / htc) * (qcr[i, j] / (tc[i] - tr[j] + 1 - ycol[i])),
        )
        + Sum(
            [i, cu],
            fchx * ycu[i, cu]
            + (vchx / htc) * (qcu[i, cu] / (lmtd[i] + 1 - ycol[i])),
        )
        + Sum(
            [hu, j],
            fchx * yhu[hu, j]
            + (vchx / htc) * (qhu[hu, j] / (thu[hu] - tr[j])),
        )
    ) + beta * (
        Sum([i, cu], costcw * qcu[i, cu])
        + Sum([hu, j], costhu[hu] * qhu[hu, j])
    )

    # limit the denominator in the second line of the objective away from zero
    tctrlo[zcrhx[i, j]] = tc[i] - tr[j] + 1 - ycol[i] >= 1

    # lmtd and ycol from being 0 and 1 at the same time to prevent divding
    # by 0 in the objective
    lmtdlo[i] = lmtd[i] >= 2 * ycol[i]

    lmtdsn[i] = lmtd[i] == (
        (2 / 3) * sqrt((tc[i] - tcin) * (tc[i] - tcout))
        + (1 / 6) * ((tc[i] - tcin) + (tc[i] - tcout))
        + sl1[i]
        - sl2[i]
    )

    artrex1[i] = s1[i] + s2[i] + sl1[i] <= u * (1 - ycol[i])

    artrex2[i] = s3[i] + s4[i] + sl2[i] <= u * (1 - ycol[i])

    material[m] = Sum(pm[i, m], spltfrc[i, m] * f[i]) == Sum(fm[i, m], f[i])

    feed[...] = Sum(zlead[i], f[i]) == totflow

    duty[i] = (
        qc[i] == (kf[i, "a"] + kf[i, "b"] * (tc[i] - tcmin[i])) + s3[i] - s4[i]
    )

    rebcon[zcr[i, j]] = qr[j] == qc[i]

    conheat[i] = qc[i] == Sum(zcrhx[i, j], qcr[i, j]) + Sum(cu, qcu[i, cu])

    rebheat[j] = qr[j] == Sum(zcrhx[i, j], qcr[i, j]) + Sum(hu, qhu[hu, j])

    trtcdef[zcr[i, j]] = (
        tr[j] == (af[i, "a"] + af[i, "b"] * (tc[i] - tcmin[i])) + s1[i] - s2[i]
    )

    dtminlp[j] = dtmin - (thu["lp"] - tr[j]) <= 0

    dtminex[j] = dtmin - (thu["ex"] - tr[j]) - u * (1 - yhu["ex", j]) <= 0

    tempset[i] = tc[i] + lmtd[i] + Sum(zcr[i, j], tr[j]) <= u * ycol[i]

    matlog[i] = f[i] <= u * ycol[i]

    dtminc[i] = tcmin[i] - tc[i] <= u * (1 - ycol[i])

    dtmincr[zcrhx[i, j]] = tr[j] - tc[i] - u * (1 - yhx[i, j]) + dtmin <= 0

    hxclog[zcrhx[i, j]] = qcr[i, j] <= u * yhx[i, j]

    hxhulog[hu, j] = qhu[hu, j] <= u * yhu[hu, j]

    hxculog[i, cu] = qcu[i, cu] <= u * ycu[i, cu]

    qcqrlog[i] = qc[i] + Sum(j.where[zcr[i, j]], qr[j]) <= u * ycol[i]

    sequen[m] = Sum(pm[i, m], ycol[i]) == Sum(fm[i, m], ycol[i])

    lead[...] = Sum(zlead[i], ycol[i]) == 1

    limutil[j] = Sum(hu, yhu[hu, j]) <= 1

    # only one of the mutual heat integration binaries can be 1
    hidirect[zlim[i, j]] = (
        yhx[i, j]
        + Sum(
            Domain(ip, jp).where[(Ord(ip) == Ord(j)) & (Ord(jp) == Ord(i))],
            yhx[ip, jp],
        )
        <= 1
    )

    # if a column doesn't exist then all binary variables associated
    # with it must also be set to zero
    heat[i] = (
        Sum(
            zcrhx[i, j],
            yhx[i, j]
            + Sum(
                Domain(ip, jp).where[
                    (Ord(ip) == Ord(j)) & (Ord(jp) == Ord(i))
                ],
                yhx[ip, jp],
            ),
        )
        + Sum((hu, zcr[i, j]), yhu[hu, j])
        + Sum(cu, ycu[i, cu])
    ) <= uint * ycol[i]

    tc.lo["c-1"] = tcout + 1
    tc.up["c-2"] = tcin - 1
    tc.lo["c-3"] = tcout + 1
    tc.up["c-4"] = tcin - 1
    tr.up[j] = trmax[j]

    skip = Model(
        cont,
        name="skip",
        equations=cont.getEquations(),
        problem="minlp",
        sense=Sense.MIN,
        objective=zoau,
    )

    skip.solve(options=Options(domain_violation_limit=100))

    import math

    assert math.isclose(skip.objective_value, 316.6927, rel_tol=0.001)

    print("Best integer solution found:", skip.objective_value)


if __name__ == "__main__":
    main()