# Heat Integrated Distillation Sequences (MINLPHI)#

`minlphi.py`

```"""
## GAMSSOURCE: https://www.gams.com/latest/gamslib_ml/libhtml/gamslib_minlphi.html
## MODELTYPE: MIP, NLP
## KEYWORDS: mixed integer linear programming, nonlinear programming, chemical engineering, distillation sequences, heat integrated distillation

Heat Integrated Distillation Sequences (MINLPHI)

This problem describes a formulation and algorithmic procedure
for obtaining heat-integrated distillation sequences for the separation
of a given multi component feed stream into its pure components products.

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.

======================================================================

A MATHEMATICAL PROGRAMMING FORMULATION FOR PROCESS SYNTHESIS

===================================================================

copyright    G.E. PAULES IV & C.A. FLOUDAS

*** Dept. of Chemical Engineering ***
*** Princeton University ***
May 23, 1987

Algorithm:  The Outer Approximation with Equality Relaxation
Full Solution with Starting Point from FIXDT

======================================================================

This formulation provides the Optimal Heat Integrated
Distillation Sequence with Pressure as a continuous variable
for a three component separation.
The Outer Approximation with Equality Relaxation algorithm is
used in the automatic solution procedure using GAMS

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
from sys import float_info

import gamspy.math as gams_math
import numpy as np
from gamspy import (
Alias,
Container,
Domain,
Equation,
Model,
Number,
Options,
Ord,
Parameter,
Set,
Sum,
Variable,
)
from gamspy.math import sqr

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

# SETS #
# the set of all columns and their condensers in the superstructure
i = Set(
cont,
name="i",
records=[f"c-{i}" for i in range(1, 5)],
description="condensers-columns",
)
# the set of all reboilers in the superstructure
j = Set(
cont,
name="j",
records=[f"r-{i}" for i in range(1, 5)],
description="reboilers",
)
# the set of all hot utilities available
hu = Set(
cont, name="hu", records=["lp", "ex"], description="hot utilities"
)
# the set of all cold utilities available
cu = Set(cont, name="cu", records=["cw"], description="cold utilities")
# an index for linear fit coefficients
n = Set(cont, name="n", records=["a", "b"], description="index")
# the set of all intermediate products in superstructure
m = Set(cont, name="m", records=["ab", "bc"], description="intermediates")
# this set maps columns to produced intermediate products
pm = Set(
cont,
name="pm",
domain=[i, m],
records=[("c-1", "bc"), ("c-2", "ab")],
description="products",
)
# this set maps columns to intermediate product feeds
fm = Set(
cont,
name="fm",
domain=[i, m],
records=[("c-3", "bc"), ("c-4", "ab")],
description="feeds",
)
# these sets are for dynamic control of solution algorithm
km = Set(
cont,
name="km",
records=[f"k-{i}" for i in range(1, 101)],
description="static iterations",
)
k = Set(cont, name="k", domain=km, description="dynamic iterations")
kiter = Set(cont, name="kiter", domain=km, description="dynamic counter")
kdynmax = Set(
cont, name="kdynmax", domain=km, description="dynamic loop control"
)

# alias sets for condensers and reboilers
ip = Alias(cont, name="ip", alias_with=i)
jp = Alias(cont, name="jp", alias_with=j)

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

# PARAMETERS #
# defines the set of leading columns in the superstructure
cont,
domain=i,
records=[["c-1", 1], ["c-2", 1]],
)

# defines allowable matches of heat integration for superstructure
# only permits heat integration between columns in the same sequence
zcrhx = Parameter(
cont,
name="zcrhx",
domain=[i, j],
records=[
["c-1", "r-3", 1],
["c-2", "r-4", 1],
["c-3", "r-1", 1],
["c-4", "r-2", 1],
],
description="condenser to reboiler allowable matches",
)

# Parameter used in pure integer constraint to permit only one
# direction of heat integration between two columns
# this would yield an infeasible solution but the constraint
# is included explicitly to reduce milp solution time
zlim = Parameter(
cont,
name="zlim",
domain=[i, j],
description="direction of heat integration",
)
zlim[i, j] = Number(1).where[(zcrhx[i, j]) & (Ord(i) < Ord(j))]

# relates appropriate reboiler to the condenser of same column
# (preferably should use an alias rather than a different set)
zcr = Parameter(
cont, name="zcr", domain=[i, j], description="reboiler-condenser pairs"
)
zcr[i, j] = Number(1).where[Ord(i) == Ord(j)]

# =====================================================================
# Binary variables are divided into 4 classes and variable/parameter
# names starting with "y"
#     ycol - column selection
#     yhx  - heat integration exchanger matches
#     yhu  - hot utility matches
#     ycup - cold utiltiy matches

# These parameters store first guess combination of binary variables
# used to initialize minlp algorithm and parameterize the minlp
# primal problem throughout the rest of the iterations
# =====================================================================
yhxp = Parameter(
cont,
name="yhxp",
domain=[i, j],
records=[["c-1", "r-3", 1]],
description="current proposal for heat integration matches",
)
yhup = Parameter(
cont,
name="yhup",
domain=[hu, j],
records=[["lp", "r-1", 1]],
description="current binary proposal for hot utility matches",
)
ycup = Parameter(
cont,
name="ycup",
domain=[i, cu],
records=[["c-1", "cw", 1], ["c-3", "cw", 1]],
description="current binary proposal for cold utility matches",
)
ycolp = Parameter(
cont,
name="ycolp",
domain=i,
records=[["c-1", 1], ["c-3", 1]],
description="current storage for columns in superstructure",
)

# =====================================================================
# These parameters store the values of the binary proposals
# for all the iterations performed for use in integer cuts
# and recovering optimal solution
# =====================================================================
yhxk = Parameter(
cont,
name="yhxk",
domain=[i, j, km],
description="binary storage parameter yhx",
)
yhuk = Parameter(
cont,
name="yhuk",
domain=[hu, j, km],
description="binary storage parameter yhu",
)
ycuk = Parameter(
cont,
name="ycuk",
domain=[i, cu, km],
description="binary storage parameter ycu",
)
ycolk = Parameter(
cont,
name="ycolk",
domain=[i, km],
description="binary storage parameter ycol",
)

# =====================================================================
# Declaration of parameters for rest of model
# =====================================================================
# mass balances for each sharp separator
spltfrc = Parameter(
cont,
name="spltfrc",
domain=[i, m],
records=[["c-1", "bc", 0.20], ["c-2", "ab", 0.90]],
description="split fraction of distillation columns",
)
# minimum condenser temperatures obtained from simulation data
tcmin = Parameter(
cont,
name="tcmin",
domain=i,
records=np.array([341.92, 343.01, 353.54, 341.92]),
description="minimum condenser temperatures",
)
# either hottest hot utility-dtmin or for individual separations
# 2*dtmin below critical temperature of bottoms product
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
# ====================================================================
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",
)

# =====================================================================
# define scalar quantities for rest of model
# =====================================================================
totflow = Parameter(
cont,
name="totflow",
records=396,
description="total flow to superstructure",
)
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",
)
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",
)

# =====================================================================
# The parameters declared here are assigned throughout the
# algorithmic procedures.
# They perform the following tasks in the algorithm
#     1) transfer of solution data between master and subproblem
#     2) storage of solution data
#     3) control of upper and lower bounds in milp master
#     4) storage of optimal solution
# =====================================================================

# Storage of variable levels for each iteration
# Identifier derived from name of variable with letter "k" appended
fk = Parameter(
cont, name="fk", domain=[i, km], description="storage of flowrates"
)
qrk = Parameter(
cont,
name="qrk",
domain=[j, km],
description="storage of reboiler duties",
)
qck = Parameter(
cont,
name="qck",
domain=[i, km],
description="storage of condenser duties",
)
qcrk = Parameter(
cont,
name="qcrk",
domain=[i, j, km],
description="storage of heat integrated exchanges",
)
qhuk = Parameter(
cont,
name="qhuk",
domain=[hu, j, km],
description="storage of hot utility usage",
)
qcuk = Parameter(
cont,
name="qcuk",
domain=[i, cu, km],
description="storage of cold utility usage",
)
tck = Parameter(
cont,
name="tck",
domain=[i, km],
description="storage of condenser temperatures",
)
trk = Parameter(
cont,
name="trk",
domain=[j, km],
description="storage of reboiler temperatures",
)
lmtdk = Parameter(
cont, name="lmtdk", domain=[i, km], description="storage of lmtds"
)

zoaup = Parameter(
cont,
name="zoaup",
records=np.inf,
description="single value storage of upper bound",
)

# storage of optimal binary variable combination
# continuous variable levels are not stored separately as they
# can be obtained from the xxxk storage parameters above
yhxopt = Parameter(
cont,
name="yhxopt",
domain=[i, j],
description="optimal heat integration",
)
yhuopt = Parameter(
cont,
name="yhuopt",
domain=[hu, j],
description="optimal hot utility match",
)
ycuopt = Parameter(
cont,
name="ycuopt",
domain=[i, cu],
description="optimal cold utility match",
)
ycolopt = Parameter(
cont, name="ycolopt", domain=i, description="optimal superstructure"
)

kopt = Parameter(
cont,
name="kopt",
description="iteration at which optimal solution was found",
)

# storage of sign() of Lagrange multiplier from nonlinear equalities
lmtdmar = Parameter(
cont,
name="lmtdmar",
domain=[i, km],
description="direction matrix for nonlinear equalities",
)

# VARIABLES #

# Free Variables
zoau = Variable(
cont,
name="zoau",
description="objective function value of nlp subproblem",
)
zoal = Variable(
cont,
name="zoal",
description="objective function value of milp masters",
)
vqcr = Variable(
cont,
name="vqcr",
domain=km,
description="heat integration contribution to milpcon",
)
vqhu = Variable(
cont,
name="vqhu",
domain=km,
description="hot utility exchange contribution to milpcon",
)
vqcu = Variable(
cont,
name="vqcu",
domain=km,
description="cold utility exchange contribution to milpcon",
)

# 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 Variables
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",
)

# =====================================================================
# declaration of equations
# for solution of the nlp subproblems:
# early versions of GAMS did not permit binary variables to appear
# in the constraints of a nonlinear programming problem even if
# they appeared in linear constraints and were fixed at a bound
# therefore -
# constraints that contain the binary variables are duplicated:
# one form contains the declared binary variable and the  other
# substitutes a parameter that is assigned the current level of
# the binary variable.  constraints that are duplicated and are to
# appear in the nlp subproblem model have the letter "n" prepended
# to the equation name.
# =====================================================================
# EQUATIONS #
nlpobj = Equation(
cont,
name="nlpobj",
type="regular",
description="nlp subproblems objective",
)
milpcon = Equation(
cont,
name="milpcon",
type="regular",
domain=km,
description="nonlinear contribution to milp objective",
)
evqcr = Equation(
cont,
name="evqcr",
type="regular",
domain=km,
description="heat integration contribution to milpcon",
)
evqhu = Equation(
cont,
name="evqhu",
type="regular",
domain=km,
description="hot utility exchange contribution to milpcon",
)
evqcu = Equation(
cont,
name="evqcu",
type="regular",
domain=km,
description="cold utility exchange contribution to milpcon",
)
lmtdsn = Equation(
cont,
name="lmtdsn",
type="regular",
domain=i,
description="nonlinear form of lmtd definition",
)
lmtdsm = Equation(
cont,
name="lmtdsm",
type="regular",
domain=[i, km],
description="linearization of lmtdsn(i) in milp masters",
)
ntempset = Equation(
cont,
name="ntempset",
type="regular",
domain=i,
description="sets temperatures of inactive columns to 0 (nlp)",
)
tempset = Equation(
cont,
name="tempset",
type="regular",
domain=i,
description="sets temperatures of inactive columns to 0 (milp)",
)
nartrex1 = Equation(
cont,
name="nartrex1",
type="regular",
domain=i,
description="relaxes artificial slack variables (nlp)",
)
artrex1 = Equation(
cont,
name="artrex1",
type="regular",
domain=i,
description="relaxes artificial slack variables (milp)",
)
nartrex2 = Equation(
cont,
name="nartrex2",
type="regular",
domain=i,
description="relaxes artificial slack variables (nlp)",
)
artrex2 = Equation(
cont,
name="artrex2",
type="regular",
domain=i,
description="relaxes artificial slack variables (milp)",
)
material = Equation(
cont,
name="material",
type="regular",
domain=m,
description="material balances for each intermediate product",
)
feed = Equation(
cont, name="feed", type="regular", description="feed to superstructure"
)
nmatlog = Equation(
cont,
name="nmatlog",
type="regular",
domain=i,
description="material balance logical constraints (nlp)",
)
matlog = Equation(
cont,
name="matlog",
type="regular",
domain=i,
description="material balance logical constraints",
)
duty = Equation(
cont,
name="duty",
type="regular",
domain=i,
description="heat duty definition of condenser i",
)
rebcon = Equation(
cont,
name="rebcon",
type="regular",
domain=[i, j],
description="equates condenser and reboiler duties",
)
conheat = Equation(
cont,
name="conheat",
type="regular",
domain=i,
description="condenser heat balances",
)
rebheat = Equation(
cont,
name="rebheat",
type="regular",
domain=j,
description="reboiler heat balances",
)
dtminlp = Equation(
cont,
name="dtminlp",
type="regular",
domain=j,
description="minimum temp approach for low pressure steam",
)
ndtminc = Equation(
cont,
name="ndtminc",
type="regular",
domain=i,
description="minimum temp allowable for each condenser (nlp)",
)
dtminc = Equation(
cont,
name="dtminc",
type="regular",
domain=i,
description="minimum temp allowable for each condenser",
)
trtcdef = Equation(
cont,
name="trtcdef",
type="regular",
domain=[i, j],
description="relates reboiler and condenser temps of columns",
)
ndtmincr = Equation(
cont,
name="ndtmincr",
type="regular",
domain=[i, j],
description="minimum temp approach for heat integration (nlp)",
)
ndtminex = Equation(
cont,
name="ndtminex",
type="regular",
domain=j,
description="minimum temp approach for exhaust steam (nlp)",
)
nhxclog = Equation(
cont,
name="nhxclog",
type="regular",
domain=[i, j],
description="logical constraint for heat balances (nlp)",
)
nhxhulog = Equation(
cont,
name="nhxhulog",
type="regular",
domain=[hu, j],
description="logical constraint for heat balances (nlp)",
)
nhxculog = Equation(
cont,
name="nhxculog",
type="regular",
domain=[i, cu],
description="logical constraint for heat balances (nlp)",
)
nqcqrlog = Equation(
cont,
name="nqcqrlog",
type="regular",
domain=i,
description="logical constraint for con-reb duties (nlp)",
)
dtmincr = Equation(
cont,
name="dtmincr",
type="regular",
domain=[i, j],
description="minimum temp approach for heat integration",
)
dtminex = Equation(
cont,
name="dtminex",
type="regular",
domain=j,
description="minimum temp approach for exhaust steam",
)
hxclog = Equation(
cont,
name="hxclog",
type="regular",
domain=[i, j],
description="logical constraint for heat balances",
)
hxhulog = Equation(
cont,
name="hxhulog",
type="regular",
domain=[hu, j],
description="logical constraint for heat balances",
)
hxculog = Equation(
cont,
name="hxculog",
type="regular",
domain=[i, cu],
description="logical constraint for heat balances",
)
qcqrlog = Equation(
cont,
name="qcqrlog",
type="regular",
domain=i,
description="logical constraint for con-reb duties",
)

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

# =====================================================================
# equations for nlp subproblems
# note that some equations are duplicated in structure but
# given different names in the nlp and milp. these equations
# involve both continuous and binary variables. In older
# versions of GAMS, it was not permissible to pose nonlinear
# models with discrete variables present, even when their values
# were held fixed (rmidnlp). This required two forms of the equation
# two be declared: one with the discrete variables present (milp)
# and one with binary variables replaced by parameters that have
# been assigned the current levels of their associated binary
# in the nlp subproblems.
# =====================================================================
#                         capital costs

nlpobj[...] = (
zoau
== alpha
* (
Sum(i, fc[i] * ycolp[i] + vc[i] * (tc[i] - tcmin[i]) * f[i])
+ Sum(
Domain(i, j).where[zcrhx[i, j]],
fchx * yhxp[i, j]
+ (vchx / htc) * (qcr[i, j] / (tc[i] - tr[j] + 1 - ycolp[i])),
)
+ Sum(
[i, cu],
fchx * ycup[i, cu]
+ (vchx / htc) * (qcu[i, cu] / (lmtd[i] + 1 - ycolp[i])),
)
+ Sum(
[hu, j],
fchx * yhup[hu, j]
+ (vchx / htc) * (qhu[hu, j] / (thu[hu] - tr[j])),
)
)
# operating costs
+ beta
* (
(costcw * Sum([i, cu], qcu[i, cu]))
+ Sum([hu, j], costhu[hu] * qhu[hu, j])
)
)

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

nartrex1[i] = s1[i] + s2[i] + sl1[i] - u * (1 - ycolp[i]) <= 0

nartrex2[i] = s3[i] + s4[i] + sl2[i] - u * (1 - ycolp[i]) <= 0

ntempset[i] = (
tc[i] + lmtd[i] + Sum(j.where[zcr[i, j]], tr[j]) - u * ycolp[i] <= 0
)

material[m] = (
Sum(i.where[pm[i, m]], spltfrc[i, m] * f[i])
- Sum(i.where[fm[i, m]], f[i])
== 0
)

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

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

rebcon[i, j].where[zcr[i, j]] = qr[j] - qc[i] == 0

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

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

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

nmatlog[i] = f[i] - u * ycolp[i] <= 0

ndtminc[i] = (tcmin[i] - tc[i] - u * (1 - ycolp[i])) <= 0

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

ndtmincr[i, j].where[zcrhx[i, j]] = (
tr[j] - tc[i] - u * (1 - yhxp[i, j]) + dtmin <= 0
)

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

nhxclog[i, j].where[zcrhx[i, j]] = qcr[i, j] <= u * yhxp[i, j]

nhxhulog[hu, j] = qhu[hu, j] <= u * yhup[hu, j]

nhxculog[i, cu] = qcu[i, cu] <= u * ycup[i, cu]

nqcqrlog[i] = qc[i] + Sum(j.where[zcr[i, j]], qr[j]) - u * ycolp[i] <= 0

nlpsub = Model(
cont,
name="nlpsub",
equations=[
nlpobj,
lmtdsn,
nartrex1,
nartrex2,
ntempset,
material,
feed,
nmatlog,
duty,
rebcon,
conheat,
rebheat,
ndtminc,
dtminlp,
trtcdef,
ndtmincr,
ndtminex,
nhxclog,
nhxhulog,
nhxculog,
nqcqrlog,
],
problem="nlp",
sense="min",
objective=zoau,
)

# ======================================================================
# Define equations for milp master problems
# Note: the nonlinear parts of the objective function related
#       to heat exchanger area have been broken out into separate
#       constraints to perform their linearizations, only a
#       contribution term appears in the linearized objective
#       function milpcon.
# ======================================================================
milpcon[k] = zoal >= alpha * (
Sum(i, fc[i] * ycol[i])
+ fchx
* (
Sum(Domain(i, j).where[zcrhx[i, j]], yhx[i, j])
+ Sum([hu, j], yhu[hu, j])
+ Sum([i, cu], ycu[i, cu])
)
+ Sum(
i,
(
vc[i]
* (
(tck[i, k] - tcmin[i]) * (f[i] - fk[i, k])
+ fk[i, k] * (tc[i] - tcmin[i])
)
),
)
+ (vchx / htc) * (vqcr[k] + vqhu[k] + vqcu[k])
) + beta * (
(costcw * Sum([i, cu], qcu[i, cu]))
+ Sum([hu, j], costhu[hu] * qhu[hu, j])
)

# ==========================================================================
# these are the linearized contributions to the objective related
# to heat exchange.  the appearance of the binary variable storage
# parameters in the denominator of some of the expressions is done
# to prevent division by zero during model generation for linearizations
# done at points where the temperatures were set to zero for unused
# columns.  the numerator is zero then also and no error is introduced.
# ==========================================================================
evqcr[k] = vqcr[k] == Sum(
Domain(i, j).where[zcrhx[i, j]],
(
(qcrk[i, j, k] / (tck[i, k] - trk[j, k] + 1 - ycolk[i, k]))
+ (
(1 / (tck[i, k] - trk[j, k] + 1 - ycolk[i, k]))
* (qcr[i, j] - qcrk[i, j, k])
)
* ycolk[i, k]
+ (
(
qcrk[i, j, k]
/ (sqr(tck[i, k] - trk[j, k]) + 1 - ycolk[i, k])
)
* ((tr[j] - trk[j, k]) - (tc[i] - tck[i, k]))
)
),
)

evqhu[k] = vqhu[k] == Sum(
[hu, j],
(
(qhuk[hu, j, k] / (thu[hu] - trk[j, k]))
+ ((1 / (thu[hu] - trk[j, k])) * (qhu[hu, j] - qhuk[hu, j, k]))
* Sum(i.where[zcr[i, j]], ycolk[i, k])
+ (
(qhuk[hu, j, k] / sqr(thu[hu] - trk[j, k]))
* (tr[j] - trk[j, k])
)
),
)

evqcu[k] = vqcu[k] == Sum(
[i, cu],
(
(qcuk[i, cu, k] / (lmtdk[i, k] + 1 - ycolk[i, k]))
+ (
(1 / (lmtdk[i, k] + 1 - ycolk[i, k]))
* (qcu[i, cu] - qcuk[i, cu, k])
)
* ycolk[i, k]
- (
(qcuk[i, cu, k] / (sqr(lmtdk[i, k]) + 1 - ycolk[i, k]))
* (lmtd[i] - lmtdk[i, k])
)
),
)

lmtdsm[i, k] = (
lmtdmar[i, k]
* (
lmtd[i]
- (2 / 3)
* gams_math.sqrt((tck[i, k] - tcin) * (tck[i, k] - tcout))
- (1 / 6) * ((tck[i, k] - tcin) + (tck[i, k] - tcout))
- (
(1 / 3)
* (
(
(2 * tck[i, k] - (tcin + tcout))
/ gams_math.sqrt(
sqr(tck[i, k])
- (tcin + tcout) * tck[i, k]
+ (tcin * tcout)
)
)
+ 1
)
)
* (tc[i] - tck[i, k])
- (sl1[i] - sl2[i])
)
<= 0
)

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

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

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

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

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

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

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

hxclog[i, j].where[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] <= 0

# pure binary constraints
# material balances determine sequence
sequen[m] = (
Sum(i.where[pm[i, m]], ycol[i]) - Sum(i.where[fm[i, m]], ycol[i]) == 0
)

# select 1 sequence

# limit choice of hot utility to 1
limutil[j] = Sum(hu, yhu[hu, j]) <= 1

# only one of the mutual heat integration binaries can be 1
hidirect[i, j].where[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(
j.where[zcrhx[i, j]],
yhx[i, j]
+ Sum(
Domain(ip, jp).where[
(Ord(ip) == Ord(j)) & (Ord(jp) == Ord(i))
],
yhx[ip, jp],
),
)
+ Sum([hu, j], yhu[hu, j].where[zcr[i, j]])
+ Sum(cu, ycu[i, cu])
- uint * ycol[i]
<= 0
)

# integer cuts
cuts[k] = (
Sum(i, gams_math.sign(ycolk[i, k] - 0.5) * ycol[i])
+ Sum(
Domain(i, j).where[zcrhx[i, j]],
gams_math.sign(yhxk[i, j, k] - 0.5) * yhx[i, j],
)
+ Sum([hu, j], gams_math.sign(yhuk[hu, j, k] - 0.5) * yhu[hu, j])
+ Sum([i, cu], gams_math.sign(ycuk[i, cu, k] - 0.5) * ycu[i, cu])
<= Sum(i, ycolk[i, k])
+ Sum(Domain(i, j).where[zcrhx[i, j]], yhxk[i, j, k])
+ Sum([hu, j], yhuk[hu, j, k])
+ Sum([i, cu], ycuk[i, cu, k])
- 1
)

# ======================================================================
# declare the milp master problem
# ======================================================================
master = Model(
cont,
name="master",
equations=[
milpcon,
evqcr,
evqhu,
evqcu,
lmtdsm,
artrex1,
artrex2,
tempset,
material,
feed,
matlog,
duty,
rebcon,
conheat,
rebheat,
dtminc,
dtminlp,
trtcdef,
dtmincr,
dtminex,
hxclog,
hxhulog,
hxculog,
qcqrlog,
sequen,
limutil,
hidirect,
heat,
cuts,
],
problem="mip",
sense="min",
objective=zoal,
)

# =====================================================================
# all declarations made, start algorithmic procedures

# initialize the optimal storage parameters to 1st guess
# =====================================================================
yhxopt[i, j] = yhxp[i, j]
yhuopt[hu, j] = yhup[hu, j]
ycuopt[i, cu] = ycup[i, cu]
ycolopt[i] = ycolp[i]
kopt[...] = 1

# ======================================================================
# assign the initial configuration to the binary proposal parameter
# ======================================================================
kiter["k-1"] = True

yhxk[i, j, kiter] = yhxp[i, j]
yhuk[hu, j, kiter] = yhup[hu, j]
ycuk[i, cu, kiter] = ycup[i, cu]
ycolk[i, kiter] = ycolp[i]
yhx.l[i, j] = yhxp[i, j]
yhu.l[hu, j] = yhup[hu, j]
ycu.l[i, cu] = ycup[i, cu]
ycol.l[i] = ycolp[i]

# set an arbitrary initial lower bound
zoal.l[...] = -10e6

# ======================================================================
# give the continuous variables a starting point for 1st nlp
# ======================================================================
tr.l["r-1"] = 410
tc.l["c-1"] = 390
tc.l["c-3"] = 360
tr.l["r-3"] = 380
tc.l["c-2"] = 0
tr.l["r-2"] = 0
tc.l["c-4"] = 0
tr.l["r-4"] = 0
f.l["c-1"] = totflow
lmtd.l["c-1"] = 75
lmtd.l["c-3"] = 25
lmtd.l["c-2"] = 0
lmtd.l["c-4"] = 0
qr.l["r-2"] = 0
qc.l["c-2"] = 0
qr.l["r-4"] = 0
qc.l["c-4"] = 0

# ======================================================================
# add bounds on tc. A sqrt in equation lmtdsn is defined for tc > tcout
# and for tc < tcin. The relevant interval is determined for each
# element of tc based on the initial values given above.
# ======================================================================
tc.lo["c-1"] = tcout + 1
tc.up["c-2"] = tcin - 1
tc.lo["c-3"] = tcout + 1
tc.up["c-4"] = tcin - 1

# ======================================================================
# bound the reboiler temperatures by their maximum allowable
# ======================================================================
tr.up[j] = trmax[j]

# ======================================================================
# initialize the dynamic sets for algorithm control
# ======================================================================
k[km] = False
kiter[km] = False
kdynmax[km] = True

# ======================================================================
# major driving loop of algorithm
# ======================================================================

for idx, _ in enumerate(kdynmax.toList()):
#  update the dynamic iteration sets
# -set kiter to contain only the current iteration element
# -add to k the current iteration element
kiter[km] = Number(1).where[Ord(km) == idx + 1]
k[kiter] = True

#  store the current binary combination
yhxk[i, j, kiter] = yhx.l[i, j]
yhuk[hu, j, kiter] = yhu.l[hu, j]
ycuk[i, cu, kiter] = ycu.l[i, cu]
ycolk[i, kiter] = ycol.l[i]

#  set the current combination parameters that appear in the nlp constraints
yhxp[i, j] = yhx.l[i, j]
yhup[hu, j] = yhu.l[hu, j]
ycup[i, cu] = ycu.l[i, cu]
ycolp[i] = ycol.l[i]
zoal.lo[...] = zoal.l

# ======================================================================
# the current levels of the lmtds are moved away from zero
# to prevent evaluation errors in the next nlp subproblem
# ======================================================================
lmtd.l[i] = lmtd.l[i] + 1

#  solve the nlp subproblem
nlpsub.solve(
options=Options(
basis_detection_threshold=1,
domain_violation_limit=1000,
relative_optimality_gap=0,
time_limit=15,
)
)

#  resolve with Conopt to get marginals for lmtdsn, if not provided by used NLP solver *****
if nlpsub.marginals == 0:
nlpsub.solve(options=(Options(nlp="conopt")))

# ======================================================================
# update the optimal solution storage parameters if new nlp
# objective function value is less than the incumbent
# ======================================================================
if zoau.toValue() < zoaup.toValue():
yhxopt[i, j] = yhx.l[i, j]
yhuopt[hu, j] = yhu.l[hu, j]
ycuopt[i, cu] = ycu.l[i, cu]
ycolopt[i] = ycol.l[i]
kopt[...] = idx + 1

# ======================================================================
# assign the solution levels of the variables that appear in the
# nonlinear equations to their corresponding storage parameters
# ======================================================================
fk[i, kiter] = f.l[i]
qrk[j, kiter] = qr.l[j]
qck[i, kiter] = qc.l[i]
qcrk[i, j, kiter] = qcr.l[i, j]
qhuk[hu, j, kiter] = qhu.l[hu, j]
qcuk[i, cu, kiter] = qcu.l[i, cu]
tck[i, kiter] = tc.l[i]
trk[j, kiter] = tr.l[j]
lmtdk[i, kiter] = lmtd.l[i]

# ======================================================================
# assign the sign of marginal values of the nonlinear equalities
# to the storage parameter lmtdmar
# ======================================================================
lmtdmar[i, kiter] = (
Number(-1)
* gams_math.sign(lmtdsn.m[i]).where[
lmtdsn.m[i] != float_info.epsilon
]
)

# ======================================================================
# store the smallest nlp objective value for upper bound on master
# ======================================================================
zoaup[...] = gams_math.Min(zoaup, zoau.l)
zoal.up[...] = zoaup
#  protect against numerical errors introduced by the solver
zoal.lo[...] = gams_math.Min(zoal.lo, zoal.up)

#  now solve the milp master problem
master.solve()

print(
"new binary combination: \n\n",
f"ycol: {ycol.toDict()}\n\n",
f"yhx: {yhx.toDict()}\n\n",
f"yhu: {yhu.toDict()}\n\n",
f"ycu: {ycu.toDict()}\n\n",
)

# ======================================================================
# check stopping criterion:
# master problem integer infeasible
# ======================================================================
if master.status in [4.0, 10.0, 19.0]:
kdynmax[km] = False
print(
"stopping criterion met: \n\n",
f"zoaup: {round(zoaup.toValue(),3)}\n\n",
f"yhxopt: {yhxopt.toDict()}\n\n",
f"yhuopt: {yhuopt.toDict()}\n\n",
f"ycuopt: {ycuopt.toDict()}\n\n",
f"ycolopt: {ycolopt.toDict()}\n\n",
f"kopt: {kopt.toValue()}\n\n",
)
break

if __name__ == "__main__":
main()
```