Source code for idaes.core.util.utility_minimization

# -*- coding: UTF-8 -*-
#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2024 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved.  Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
"""
General purpose heat integration block for IDAES models
"""
# TODO: Missing docstrings
# pylint: disable=missing-function-docstring

# Import Plotting and Numpy libraries for composite curves
import matplotlib.pyplot as plt
import numpy as np

# Import Pyomo libraries
from pyomo.environ import Constraint, sqrt, Expression, value, Var

from pyomo.environ import units as pyunits

__author__ = "Alejandro Garciadiego, Alexander Dowling"

# Temperature Epsilon to add or subtract 1 degree to avoid division over
# 0 in equipment not changing temperature
EpsT = 1


[docs] def min_utility(blk, heating, cooling, DTmin, eps=1e-6, DG_units=pyunits.Mwatt): """ Function for Duran-Grossmann for minimization of utilities This function adds variables and constraints to the flowsheet to minimize utilities Args: blk: flowsheet heating: Equipment that requires Heat cooling: Equipment from which heat is being removed DTmin: HRAT (Heat Recovery Approximation Temperature) eps: Epsilon for smoothing operation Returns: Constraint and variable object representing the calculation for hot utility and cold utility """ # Generate lists out of strings from Args exchanger_list = heating + cooling pinch_streams = exchanger_list # Generate dictionaries out of strings to convert to pyomo objects pinch_streamsdict = {} coolingdict = {} heatingdict = {} exchangerdict = {} for i, el in enumerate(exchanger_list): pinch_streamsdict[str(el)] = el for i, el in enumerate(cooling): coolingdict[str(el)] = el for i, el in enumerate(heating): heatingdict[str(el)] = el for i, el in enumerate(exchanger_list): exchangerdict[str(el)] = el Q = {} for i in exchangerdict: Q[i] = pyunits.convert(pinch_streamsdict[i].heat_duty[0], to_units=DG_units) # Run function to fill exchanger data for pinch calculations for # initialization of variables exchangeData = heat_data(blk, heating, cooling, DG_units) # Call pinch calculation for initialization of variables in PD class PD = pinch_calc(heating, cooling, exchangeData, DTmin, eps) # Define dictionary for addition of DTmin to heating equipment dT = {} for i, el in enumerate(cooling): dT[str(el)] = 0 for i, el in enumerate(heating): dT[str(el)] = DTmin def T_in(blk, i): return pinch_streamsdict[i].control_volume.properties_in[0].temperature blk.Tin = Expression( pinch_streamsdict.keys(), rule=T_in, doc="Inlet temperature in exchangers" ) def T_out(blk, i): return pinch_streamsdict[i].control_volume.properties_out[0].temperature blk.Tout = Expression( pinch_streamsdict.keys(), rule=T_out, doc="Outlet temperature in exchangers" ) # Expression for cp of equipment with heat exchange def Theta_(blk, i): if i in heatingdict: return Q[i] / ( 0.5 * ( (blk.Tout[i] - blk.Tin[i] + (EpsT)) + sqrt((blk.Tout[i] - blk.Tin[i] - (EpsT)) ** 2 + eps) ) ) else: return Q[i] / ( -0.5 * ( (-(blk.Tout[i] - blk.Tin[i]) - (-EpsT)) + sqrt(((-EpsT) - (blk.Tout[i] - blk.Tin[i])) ** 2 + eps) ) ) blk.Theta = Expression( pinch_streamsdict.keys(), rule=Theta_, doc="FCp in exchangers" ) # Define expression for pinch candidate temperature def T_(blk, i): return pinch_streamsdict[i].control_volume.properties_in[0].temperature + dT[i] blk.T_ = Expression( pinch_streamsdict.keys(), rule=T_, doc="Pinch candidate temperature" ) # Define variable for heat content above the pinch point blk.QAh = Var( pinch_streamsdict.keys(), initialize=PD.initQAh, bounds=(1e-8, None), doc="Heat content above pinch", units=pyunits.get_units(Q[str(pinch_streams[0])]), ) # Define a constraint to calculate the variable QAh def rule_heat_above_pinch(blk, p): return blk.QAh[p] == sum( blk.Theta[i] * ( 0.5 * ((blk.Tin[i] - blk.T_[p]) + sqrt((blk.Tin[i] - blk.T_[p]) ** 2 + eps)) - 0.5 * ( (blk.Tout[i] - blk.T_[p]) + sqrt((blk.Tout[i] - blk.T_[p]) ** 2 + eps) ) ) for i in coolingdict ) blk.heat_above_pinch = Constraint( pinch_streamsdict.keys(), rule=rule_heat_above_pinch ) # Define variable for heat content below the pinch point blk.QAc = Var( pinch_streamsdict.keys(), initialize=PD.initQAc, bounds=(1e-8, None), doc="Heat content bellow pinch", units=pyunits.get_units(Q[str(pinch_streams[0])]), ) # Define a constraint to calculate the variable QAc def rule_heat_below_pinch(blk, p): return blk.QAc[p] == sum( blk.Theta[i] * ( 0.5 * ( (blk.Tout[i] - blk.T_[p] + DTmin) + sqrt((blk.Tout[i] - blk.T_[p] + DTmin) ** 2 + eps) ) - 0.5 * ( (blk.Tin[i] - blk.T_[p] + DTmin) + sqrt((blk.Tin[i] - blk.T_[p] + DTmin) ** 2 + eps) ) ) for i in heatingdict ) blk.heat_below_pinch = Constraint( pinch_streamsdict.keys(), rule=rule_heat_below_pinch ) # Define variable for Heat of hot utility blk.Qs = Var( initialize=PD.initQs, bounds=(1e-8, None), doc="Heating utilities", units=pyunits.get_units(Q[str(pinch_streams[0])]), ) # Define a constraint to solve for Qs # Where 1E-6 is added to both sides of the constraint as a scaling factor def rule_heating_utility(blk, p): return blk.Qs >= (blk.QAc[p] - blk.QAh[p]) blk.heating_utility = Constraint( pinch_streamsdict.keys(), rule=rule_heating_utility ) # Define variable for Heat of cold utility blk.Qw = Var( initialize=PD.initQw, bounds=(1e-8, None), doc="Cooling utilities", units=pyunits.get_units(Q[str(pinch_streams[0])]), ) # Define a constraint to solve for Qw # Where 1E-6 is added to both sides of the constraint as a scaling factor def rule_cooling_utility(blk): return blk.Qw == -sum(Q[i] for i in exchangerdict) + blk.Qs blk.cooling_utility = Constraint(rule=rule_cooling_utility)
def heat_data(blk, heating, cooling, DG_units=pyunits.Mwatt): """ Function for generating necessary data for Duran-Grossmann initialization Allows the calculation of reactors Args: blk: flowsheet heating: Equipment that requires Heat cooling: Equipment from which heat is being removed Returns: Dictionary for the equipment exchanging heat containing: Tin Tout FCp Q """ # Generate lists out of strings from Args exchanger_list = heating + cooling # Generate dictionaries out of strings to convert to pyomo objects pinch_streamsdict = {} coolingdict = {} heatingdict = {} exchangerdict = {} for i, el in enumerate(exchanger_list): pinch_streamsdict[str(el)] = el for i, el in enumerate(cooling): coolingdict[str(el)] = el for i, el in enumerate(heating): heatingdict[str(el)] = el for i, el in enumerate(exchanger_list): exchangerdict[str(el)] = el # Generate dictionaries for information for heat data Q = {} T_in = {} T_out = {} FCp_ = {} # Defining inlet temperature for inlet from equipment control volume for i in heatingdict: T_in[i] = value( pinch_streamsdict[i].control_volume.properties_in[0].temperature ) T_out[i] = ( value(pinch_streamsdict[i].control_volume.properties_out[0].temperature) + EpsT ) # Defining inlet temperature for utlet from equipment control volume for i in coolingdict: T_in[i] = ( value(pinch_streamsdict[i].control_volume.properties_in[0].temperature) + EpsT ) T_out[i] = value( pinch_streamsdict[i].control_volume.properties_out[0].temperature ) # Calculating FCp out of heat in control volume # Obtains Q from equipment's control volume heat for i, v in pinch_streamsdict.items(): Q[i] = value(pyunits.convert(v.heat_duty[0], to_units=DG_units)) FCp_[i] = Q[i] / (T_out[i] - T_in[i]) # Generate a large dictionary containing all the data obtained # from the equipment exchangeData = {} for i in pinch_streamsdict: exchangeData[i] = { "T_in": T_in[i], "T_out": T_out[i], "FCp_": FCp_[i], "Q": Q[i], } return exchangeData def pinch_calc(heating, cooling, exchangeData, DTmin, eps): """ Function for calculating heat data for Duran-Grossmann initialization Args: heating: Equipment that requires Heat cooling: Equipment from which heat is being removed exchangeData: Dictionary containing Tin, Tout, FCp and Q for each equipment in lists DTmin: HRAT (Heat Recovery Approximation Temperature) eps: Epsilon for smoothing operation Returns: PD (Dictionary containing initialized values for QAh, QAc, Qs and Qw) """ # Generate lists out of strings from Args exchanger_list = heating + cooling # Generate dictionaries out of strings to convert to pyomo objects pinch_streamsdict = {} coolingdict = {} heatingdict = {} exchangerdict = {} for i, el in enumerate(exchanger_list): pinch_streamsdict[str(el)] = el for i, el in enumerate(cooling): coolingdict[str(el)] = el for i, el in enumerate(heating): heatingdict[str(el)] = el for i, el in enumerate(exchanger_list): exchangerdict[str(el)] = el # Generate dictionaries to contain initialized data T_ = {} initQAh = {} initQAc = {} b = [] # Define dictionary for addition of DTmin to heating equipment dT = {} for i, el in enumerate(cooling): dT[str(el)] = 0 for i, el in enumerate(heating): dT[str(el)] = DTmin # Calculate pinch temperature candidate # Calculate QAh and QAc for i in pinch_streamsdict: T_[i] = exchangeData[i]["T_in"] + dT[i] initQAh[i] = sum( exchangeData[j]["FCp_"] * ( 0.5 * ( (exchangeData[j]["T_in"] - T_[i]) + sqrt((exchangeData[j]["T_in"] - T_[i]) ** 2 + eps) ) - 0.5 * ( (exchangeData[j]["T_out"] - T_[i]) + sqrt((exchangeData[j]["T_out"] - T_[i]) ** 2 + eps) ) ) for j in coolingdict ) initQAc[i] = sum( exchangeData[j]["FCp_"] * ( 0.5 * ( (exchangeData[j]["T_out"] - T_[i] + DTmin) + sqrt((exchangeData[j]["T_out"] - T_[i] + DTmin) ** 2 + eps) ) - 0.5 * ( (exchangeData[j]["T_in"] - T_[i] + DTmin) + sqrt((exchangeData[j]["T_in"] - T_[i] + DTmin) ** 2 + eps) ) ) for j in heatingdict ) # Generate array with all possible QS for i in exchangerdict: b.append(initQAc[i] - initQAh[i]) # Define largest value of QS c = max([max(b), 0.0]) initQs = c # Calculate Qw from largest value of Qs initQw = -sum(value(exchangeData[i]["Q"]) for i in exchangerdict) + initQs initQw = max([initQw, 0.0]) # Fill Class with all the data to initialize Duran-Grossmann variables PD = PinchDataClass(initQs, initQw) PD.initQAh = initQAh PD.initQAc = initQAc return PD
[docs] def generate_curves(CD): """ Function for plotting composite curves Args: CD: Class containing curve data Returns: Composite curves """ # Transform information from CD class into arrays for plotting function # Transforms cooling equipment data CTin = CD.Cooling_Tin CTout = CD.Cooling_Tout CQ = CD.Cooling_Q CTin, CTout, CQ = np.array((CTin, CTout, CQ)) # Transforms heating equipment data HTin = CD.Heating_Tin HTout = CD.Heating_Tout HQ = CD.Heating_Q HTin, HTout, HQ = np.array((HTin, HTout, HQ)) Thot, Qhot = gen_curves(CTin, CTout, CQ) Tcold, Qcold = gen_curves(HTin, HTout, -HQ) # Brings Qw scalar into scope out of the class Qw = CD.Qw plt.figure(figsize=(10, 10)) # Plot values for Hot streams # Negative sign corrects for Q < 0 for hot streams plt.plot(-Qhot, Thot, color="r", label="Hot Streams / Streams that are Cooled") # Need to shift curve to account for the cooling utility Qcold = Qcold + sum(HQ) # Plot values for cold streams plt.plot( (Qcold + value(Qw)), Tcold, color="b", label="Cold Streams / Streams that are Heated", ) plt.xlabel( "Cumulative Process-Wide Heat Exchange [" + str(CD.Q_unit) + "]", fontsize=18 ) plt.ylabel("Temperature [" + str(CD.T_unit) + "]", fontsize=18) plt.title("Composite Curves for minimum utilities", size=20) plt.legend(loc="upper left", fontsize=14) plt.grid() plt.show() return
[docs] def heat_ex_data(blk, heating, cooling): """ Function for turning IDAES heat exchanging equipment into a class for use in plotting Args: blk: Flowsheet heating: Equipment that heats streams cooling: Equipment that cools streams Returns: CD: Class with heating and cooling equipment as arrays """ # Generate dictionaries out of strings to convert to pyomo objects exchanger_list = heating + cooling pinch_streams = exchanger_list pinch_streamsdict = {} coolingdict = {} heatingdict = {} exchangerdict = {} T_units = pyunits.get_units( pinch_streams[0].control_volume.properties_in[0].temperature ) DG_units = pyunits.get_units(blk.Qw) for el in exchanger_list: pinch_streamsdict[str(el)] = el exchangerdict[str(el)] = el for el in cooling: coolingdict[str(el)] = el for el in heating: heatingdict[str(el)] = el # Generate zero arrays from length of the cooling list CHX = len(coolingdict) CTin = np.zeros(CHX) CTout = np.zeros(CHX) CQ = np.zeros(CHX) # Convert Pyomo model values into arrays j = 0 for v in coolingdict.values(): CTin[j] = value(v.control_volume.properties_in[0].temperature) + 1 CTout[j] = value(v.control_volume.properties_out[0].temperature) CQ[j] = value(pyunits.convert(v.control_volume.heat[0], to_units=DG_units)) j += 1 # Generate zero arrays from length of the heating list HHX = len(heatingdict) HTin = np.zeros(HHX) HTout = np.zeros(HHX) HQ = np.zeros(HHX) # Convert Pyomo model values into arrays j = 0 for v in heatingdict.values(): HTin[j] = value(v.control_volume.properties_in[0].temperature) HTout[j] = value(v.control_volume.properties_out[0].temperature) + 1 HQ[j] = value(pyunits.convert(v.control_volume.heat[0], to_units=DG_units)) j += 1 # Fill class with values and arrays Qw = value(blk.Qw) CD = CurveData(Qw, T_units, DG_units) CD.Cooling_Tin = CTin CD.Cooling_Tout = CTout CD.Cooling_Q = CQ CD.Heating_Tin = HTin CD.Heating_Tout = HTout CD.Heating_Q = HQ return CD
def gen_curves(Tin, Tout, Q): """ Function to do add cumulative heat arrays Args: Tin: Inlet temperatures of cooling/heating equipment Tout: Outlet temperatures of cooling/heating equipment Q: Heat of cooling/heating equipment Returns: Tstar: Cumulative Temperature array NQstar: Cumulative heat array """ # Q < 0 for hot streams = heat removing = cooling units # Q > 0 for cold streams = heat added = heating units # Ignoring edge cases for phase changes # Shaping Temperature array # Calls unique function to avoid repeating Tstart = unique(np.vstack([Tin, Tout]).reshape((1, -1))) Tstar = np.sort(Tstart[0]) # Generate vector of Tin size and Tunique nTin = len(Tin) nTstar = len(Tstar) Qmat = np.zeros((nTin, nTstar)) # Generate cumulative arrays for temperature and heat for i in range(nTin): for j in range(nTstar): Qmat[i, j] = linear_interpolation([Tin[i], Tout[i]], [0, Q[i]], Tstar[j]) Qstar = sum(Qmat, 0) NQstar = Qstar.reshape(nTstar) return Tstar, NQstar def linear_interpolation(x, y, t): """ Function to do Linear interpolation with nearest neighbor extrapolation Args: x: Inlet and Outlet Temperature values y: 0 and Heat value t: Unique inlet and outlet temperatures Returns: Qstar: New array of Q Values """ # Set the upper or lower value if x[0] < x[1]: lo = 0 hi = 1 else: lo = 1 hi = 0 # do Linear interpolation with nearest neighbor extrapolation alpha = (x[hi] - t) / (x[hi] - x[lo]) alpha = max([0, alpha]) alpha = min([alpha, 1]) Qsta = alpha * (y[hi] - y[lo]) + y[lo] return Qsta def print_HX_results(blk, exchanger_list): """ Function to print results of Heat Exchangers Args: blk: flowsheet of the equipment exchanger_list: List of equipment to print data Returns: Printed List """ # Initialize dictionary and fill with exchanger list exchangerdict = {} for i, el in enumerate(exchanger_list): exchangerdict[str(el)] = el # initialize null dictionaries for data to be printed Tin_ = {} Tout_ = {} f_ = {} Q_ = {} # Loop over heat exchangers for i, v in exchangerdict.items(): Tin_[i] = value(v.control_volume.properties_in[0].temperature) Tout_[i] = value(v.control_volume.properties_out[0].temperature) f_[i] = value(v.control_volume.properties_out[0].flow_mol) Q_[i] = value(v.control_volume.heat[0]) T_units = pyunits.get_units(v.control_volume.properties_in[0].temperature) DG_units = pyunits.get_units(v.heat_duty[0]) # Print the header print("Heat Exchanger Summary: ") # Print Inlet Temperature, Outlet Temperature and Heat for i, v in exchangerdict.items(): print("Heat exchanger: ", v) print(f'Inlet T: {" "*3} {Tin_[i] : 0.3f} {T_units}') print(f'Outlet T: {" "*2} {Tout_[i] : 0.3f} {T_units}') print(f'Q : {" "*9} {Q_[i]: 0.3f} {DG_units}') def unique(list1): """ Function to remove not unique elements of a list Args: list1: List from where to obtain only unique values Returns: unique_list """ # initialize a null list unique_list = [] # traverse for all elements for i in list1: # check if exists in unique_list or not if i not in unique_list: unique_list.append(i) return unique_list class PinchDataClass: """ Class containing all the initialized values for all heat Variables """ def __init__(self, initQs, initQw): """ Args: initQs: Initial value of heat of hot utility initQw: Initial value of heat to be removed by cold utility Returns: None """ self.initQs = initQs self.initQw = initQw self.initQAh = {} self.initQAc = {} def HeatAbove(self, data_dic): """ Args: data_dic: Initial value of heat above pinch Returns: None """ self.initQAh = data_dic def HeatBellow(self, data_dic2): """ Args: data_dic2: Initial value of heat below pinch Returns: None """ self.initQAc = data_dic2 class CurveData: """ Class containing necessary data to generate composite curves """ def __init__(self, Qw, T_unit, Q_unit): self.Qw = Qw self.T_unit = T_unit self.Q_unit = Q_unit def Cooling_Tin(self, _list): self.Cooling_Tin = _list def Cooling_Tout(self, _list): self.Cooling_Tout = _list def Cooling_Q(self, _list): self.Cooling_Q = _list def Heating_Tin(self, _list): self.Heating_Tin = _list def Heating_Tout(self, _list): self.Heating_Tout = _list def Heating_Q(self, _list): self.Heating_Q = _list