Source code for idaes.power_generation.unit_models.boiler_heat_exchanger

#################################################################################
# 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), and is copyright (c) 2018-2021
# 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.
#################################################################################
"""
Power Plant IDAES heat exchanger model.

The boiler heat exchanger model consist of a cross flow shell and tube hx
that can be used for any of the boiler components, such as economizer,
reheater or superheaters (primary, secondary, etc).
The model includes shell and tube rigorous heat transfer calculations and
pressure drop calculations for shell side. Note that this model assumes no
phase transitions (if user requires phase transitions, they need a general
model)

The main config arguments:
    - delta T method: counter-current or co-current
    - tube_arrangement: in-line or staggered
    - has radiation: True if model is used as a reheater or superheater unit
        Gas emissivity calculated (Gas temperature above 700 K)

General assumtpions:
    - SI units (consistent with prop pack)
    - heat transfer calc U = f(Nu, Re, Pr)
    - Pressure drop tube and shell side (friction factor calc.)

"""

__author__ = "Boiler subsystem team (J Ma, M Zamarripa)"
__version__ = "1.0.0"

# Import Python libraries
import logging
from enum import Enum, EnumMeta

# Import Pyomo libraries
from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool

# Additional import for the unit operation
from pyomo.environ import (
    value,
    Var,
    Param,
    exp,
    sqrt,
    log,
    PositiveReals,
    NonNegativeReals,
    ExternalFunction,
    Reference,
    units as pyunits,
)

# Import IDAES cores
from idaes.core import (
    ControlVolume0DBlock,
    declare_process_block_class,
    MaterialBalanceType,
    EnergyBalanceType,
    MomentumBalanceType,
    UnitModelBlockData,
    useDefault,
)

from idaes.core.util.config import is_physical_parameter_block, DefaultBool
from idaes.core.util.misc import add_object_reference
from idaes.core.util.constants import Constants as c
from idaes.core.util import get_solver
from idaes.core.util.functions import functions_lib
import idaes.core.util.scaling as iscale
import idaes.logger as idaeslog
from idaes.core.util.exceptions import ConfigurationError, InitializationError

from idaes.generic_models.unit_models.heat_exchanger import (
    HeatExchangerData,
    delta_temperature_lmtd_callback,
    delta_temperature_lmtd2_callback,
    delta_temperature_lmtd3_callback,
    delta_temperature_amtd_callback,
    delta_temperature_underwood_callback,
    HeatExchangerFlowPattern,
)


# Set up logger
_log = logging.getLogger(__name__)


class TubeArrangement(Enum):
    inLine = 0
    staggered = 1


class _DeprecateDeltaTMethod(EnumMeta):
    # This is used to log a deprecation warning if someone uses DeltaTMethod
    def __getattribute__(cls, name):
        obj = super().__getattribute__(name)
        if isinstance(obj, Enum):
            _log.warning(
                "'DeltaTMethod' is deprecated use 'HeatExchangerFlowPattern' "
                "This will be removed in IDAES 3.0"
            )
        return obj


class DeltaTMethod(Enum, metaclass=_DeprecateDeltaTMethod):
    """DEPRECATED: use HeatExchangerFlowPattern instead"""

    counterCurrent = HeatExchangerFlowPattern.countercurrent
    coCurrent = HeatExchangerFlowPattern.cocurrent


def delta_temperature_underwood_tune_callback(b):
    """
    This is a callback for a temperature difference expression to calculate
    :math:`\Delta T` in the heat exchanger model using log-mean temperature
    difference (LMTD) approximation given by Underwood (1970).  It can be
    supplied to "delta_temperature_callback" HeatExchanger configuration option.
    This uses a cube root function that works with negative numbers returning
    the real negative root. This should always evaluate successfully.
    """
    _log.warning(
        "DEPRECATED: delta_temperature_underwood_tune_callback will be "
        "removed. Use another standard delta_temperature callback or "
        "provide a custom callback.  This will be removed in IDAES 3.0."
    )
    dT1 = b.delta_temperature_in
    dT2 = b.delta_temperature_out
    temp_units = pyunits.get_units(dT1[dT1.index_set().first()])
    b.lmtd_param_c1 = Var(initialize=0.3241)
    b.lmtd_param_c2 = Var(initialize=1.99996)
    b.lmtd_param_c1.fix()
    b.lmtd_param_c2.fix()
    c1 = b.lmtd_param_c1
    c2 = b.lmtd_param_c2

    @b.Expression(b.flowsheet().time)
    def delta_temperature(b, t):
        return (((dT1[t] / temp_units) ** c1 + (dT2[t] / temp_units) ** c1) / c2) ** (
            1 / c1
        ) * temp_units


[docs]@declare_process_block_class("BoilerHeatExchanger") class BoilerHeatExchangerData(HeatExchangerData): CONFIG = HeatExchangerData.CONFIG(implicit=True) CONFIG.declare( "side_1_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="DEPRECATED: Property package to use for control volume", doc="""DEPRECATED (will be removed in 3.0): Property parameter block, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", ), ) CONFIG.declare( "side_1_property_package_args", ConfigBlock( implicit=True, description="DEPRECATED: Arguments for constructing property packages", doc="""DEPRECATED (will be removed in 3.0): ConfigBlock to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) CONFIG.declare( "side_2_property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="DEPRECATED: Property package to use for control volume", doc="""DEPRECATED (will be removed in 3.0): Property parameter block, **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", ), ) CONFIG.declare( "side_2_property_package_args", ConfigBlock( implicit=True, description="DEPRECATED: Arguments for constructing property packages", doc="""DEPRECATED (will be removed in 3.0): ConfigBlock to be passed to a property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) CONFIG.declare( "material_balance_type", ConfigValue( default=None, domain=In(MaterialBalanceType), description="DEPRECATED: Material balance construction flag", doc="""DEPRECATED (will be removed in 3.0): Indicates type of material balance, **default** - MaterialBalanceType.componentPhase. **Valid values:** { **MaterialBalanceType.none** - exclude material balances, **MaterialBalanceType.componentPhase** - use phase component balances, **MaterialBalanceType.componentTotal** - use total component balances, **MaterialBalanceType.elementTotal** - use total element balances, **MaterialBalanceType.total** - use total material balance.}""", ), ) CONFIG.declare( "energy_balance_type", ConfigValue( default=None, domain=In(EnergyBalanceType), description="DEPRECATED: Energy balance construction flag", doc="""DEPRECATED(will be removed in 3.0): Type of energy balance, **default** - EnergyBalanceType.enthalpyTotal. **Valid values:** { **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material, **EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""", ), ) CONFIG.declare( "momentum_balance_type", ConfigValue( default=None, domain=In(MomentumBalanceType), description="DEPRECATED: Momentum balance construction flag", doc="""DEPRECATED (will be removed in 3.0): Type of momentum balance, **default** - MomentumBalanceType.pressureTotal. **Valid values:** { **MomentumBalanceType.none** - exclude momentum balances, **MomentumBalanceType.pressureTotal** - single pressure balance for material, **MomentumBalanceType.pressurePhase** - pressure balances for each phase, **MomentumBalanceType.momentumTotal** - single momentum balance for material, **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""", ), ) CONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=Bool, description="Pressure change term construction flag", doc="""Indicates whether pressure change terms should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""", ), ) CONFIG.declare( "delta_T_method", ConfigValue( default=None, description="DEPRECATED: Flow configuration in unit to compute delta T", doc="""DEPRECATED (will be removed in 3.0): Flag indicating flow arrangement to use for delta T **default** - DeltaTMethod.counterCurrent **Valid values:** { **DeltaTMethod.counterCurrent**}""", ), ) CONFIG.declare( "tube_arrangement", ConfigValue( default=TubeArrangement.inLine, domain=In(TubeArrangement), description="tube configuration", doc="Tube arrangement could be in-line and staggered", ), ) CONFIG.declare( "side_1_water_phase", ConfigValue( default="Liq", domain=In(["Liq", "Vap"]), description="side 1 water phase", doc="Define water phase for property calls", ), ) CONFIG.declare( "has_radiation", ConfigValue( default=False, domain=In([False, True]), description="Has side 2 gas radiation", doc="Define if side 2 gas radiation is to be considered", ), ) def _process_config(self): """Deal with old style config arguments by converting them to be consitent with the generic heat exchanger. Log a deprecation warning for old style args. """ super()._process_config() config = self.config if config.delta_T_method is not None: _log.warning( "Config item delta_T_method is deprecated use flow_pattern. Will be removed in IDAES 3.0." ) if isinstance(config.delta_T_method, DeltaTMethod): if config.delta_T_method == DeltaTMethod.coCurrent: config.flow_pattern = HeatExchangerFlowPattern.cocurrent else: config.flow_pattern = HeatExchangerFlowPattern.countercurrent else: config.flow_pattern = config.delta_T_method if config.flow_pattern == HeatExchangerFlowPattern.crossflow: raise ConfigurationError("Boiler heat exchanger does not support crossflow") if config.side_1_property_package is not None: _log.warning( "Config item side_1_property_package is deprecated. Will be removed in IDAES 3.0." ) # For this side 1 is tube/cold side config.cold_side_config.property_package = config.side_1_property_package if config.side_2_property_package is not None: _log.warning( "Config item side_2_property_package is deprecated. Will be removed in IDAES 3.0." ) # For this side 2 is shell/hot side config.hot_side_config.property_package = config.side_2_property_package if len(config.side_1_property_package_args) > 0: _log.warning( "Config item side_1_property_package_args is deprecated. Will be removed in IDAES 3.0." ) # For this side 1 is tube/cold side config.cold_side_config.property_package_args = ( config.side_1_property_package_args ) if len(config.side_2_property_package_args) > 0: _log.warning( "Config item side_2_property_package_args is deprecated. Will be removed in IDAES 3.0." ) # For this side 2 is shell/hot side config.hot_side_config.property_package_args = ( config.side_2_property_package_args ) if config.material_balance_type is not None: _log.warning( "Config item material_balance_type is deprecated. Will be removed in IDAES 3.0." ) config.cold_side_config.material_balance_type = config.material_balance_type config.hot_side_config.material_balance_type = config.material_balance_type if config.energy_balance_type is not None: _log.warning( "Config item energy_balance_type is deprecated. Will be removed in IDAES 3.0." ) config.cold_side_config.enrgy_balance_type = config.energy_balance_type config.hot_side_config.energy_balance_type = config.energy_balance_type if config.momentum_balance_type is not None: _log.warning( "Config item momentum_balance_type is deprecated. Will be removed in IDAES 3.0." ) config.cold_side_config.momentum_balance_type = config.momentum_balance_type config.hot_side_config.momentum_balance_type = config.momentum_balance_type if config.has_pressure_change is not None: config.cold_side_config.has_pressure_change = config.has_pressure_change config.hot_side_config.has_pressure_change = config.has_pressure_change config.side_1_is_hot = False
[docs] def build(self): """ Build method for Boiler heat exchanger model Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() self.deltaT_1 = Reference(self.delta_temperature_in) self.deltaT_2 = Reference(self.delta_temperature_out) self._set_geometry() self.side_1_fluid_phase = self.config.side_1_water_phase # Construct performance equations self._make_performance()
def _set_geometry(self): """ Define the geometry of the unit as necessary, and link to holdup volume Args: None Returns: None """ # Elevation difference (outlet - inlet) for static pressure calculation self.delta_elevation = Var( initialize=0, within=NonNegativeReals, doc="Elevation increase used for static pressure calculation", units=pyunits.m, ) # Number of tube columns in the cross section plane # perpendicular to shell side fluid flow (y direction) self.tube_ncol = Var( initialize=10.0, within=PositiveReals, doc="Number of tube columns" ) # Number of tube rows in the direction of shell side # fluid flow (x direction) self.tube_nrow = Var( initialize=10.0, within=PositiveReals, doc="Number of tube rows" ) # Number of inlet tube rows self.nrow_inlet = Var( initialize=1, within=PositiveReals, doc="Number of inlet tube rows" ) # Length of a tube in z direction for each path self.tube_length = Var( initialize=5.0, within=PositiveReals, doc="Tube length", units=pyunits.m ) # Inner diameter of tubes self.tube_di = Var( initialize=0.05, within=PositiveReals, doc="Inner diameter of tube", units=pyunits.m, ) # Thickness of tube self.tube_thickness = Var( initialize=0.005, within=PositiveReals, doc="Tube thickness", units=pyunits.m, ) # Pitch of tubes between two neighboring columns (in y direction). # Always greater than tube outside diameter self.pitch_y = Var( initialize=0.1, within=PositiveReals, doc="Pitch between two neighboring columns", units=pyunits.m, ) # Pitch of tubes between two neighboring rows (in x direction). # Always greater than tube outside diameter self.pitch_x = Var( initialize=0.1, within=PositiveReals, doc="Pitch between two neighboring rows", units=pyunits.m, ) # Tube outside diameter @self.Expression(doc="Outside diameter of tube") def do_tube(b): return b.tube_di + b.tube_thickness * 2.0 if self.config.has_radiation is True: # Mean beam length for radiation @self.Expression(doc="Mean beam length") def mbl(b): return 3.6 * ( b.pitch_x * b.pitch_y / c.pi / b.do_tube - b.do_tube / 4.0 ) # Mean beam length for radiation divided by sqrt(2) @self.Expression(doc="Mean beam length") def mbl_div2(b): return b.mbl / sqrt(2.0) # Mean beam length for radiation multiplied by sqrt(2) @self.Expression(doc="Mean beam length") def mbl_mul2(b): return b.mbl * sqrt(2.0) # Number of 180 degree bends for the tube @self.Expression(doc="Nbend_tube") def nbend_tube(b): return b.tube_nrow / b.nrow_inlet # Total flow area on tube side @self.Expression(doc="Total flow area on tube side") def area_flow_tube(b): return 0.25 * c.pi * b.tube_di ** 2.0 * b.tube_ncol * b.nrow_inlet # Total flow area on shell side @self.Expression(doc="Total flow area on shell side") def area_flow_shell(b): return b.tube_length * (b.pitch_y - b.do_tube) * b.tube_ncol # Total heat transfer area based on outside diameter @self.Constraint(doc="Total heat transfer area based on tube outside diameter") def area_eqn(b): return ( b.area == c.pi * b.do_tube * b.tube_length * b.tube_ncol * b.tube_nrow ) # Ratio of pitch_x/do_tube @self.Expression( doc="Ratio of pitch in x " "direction to tube outside diameter" ) def pitch_x_to_do(b): return b.pitch_x / b.do_tube # Ratio of pitch_y/do_tube @self.Expression( doc="Ratio of pitch in y " "direction to tube outside diameter" ) def pitch_y_to_do(b): return b.pitch_y / b.do_tube if self.config.has_holdup is True: add_object_reference(self, "volume_side_1", self.side_1.volume) add_object_reference(self, "volume_side_2", self.side_2.volume) # Total tube side valume self.Constraint(doc="Total tube side volume") def volume_side_1_eqn(b): return b.volumne_side_1 == ( 0.25 * c.pi * b.tube_di ** 2.0 * b.tube_length * b.tube_ncol * b.tube_nrow ) # Total shell side valume self.Constraint(doc="Total shell side volume") def volume_side_2_eqn(b): return ( b.volumne_side_2 == b.tube_ncol * b.pitch_y * b.tube_length * b.tube_nrow * b.pitch_x - 0.25 * c.pi * b.do_tube ** 2.0 * b.tube_length * b.tube_ncol * b.tube_nrow ) def _make_performance(self): """ Define constraints which describe the behaviour of the unit model. Args: None Returns: None """ # Set references to balance terms at unit level add_object_reference(self, "heat_duty", self.side_1.heat) if self.config.has_pressure_change is True: add_object_reference(self, "deltaP_tube", self.side_1.deltaP) add_object_reference(self, "deltaP_shell", self.side_2.deltaP) # Performance parameters and variables # Wall thermal conductivity self.therm_cond_wall = Param( initialize=43.0, within=PositiveReals, doc="Thermal conductivity of the wall", units=pyunits.W / pyunits.m / pyunits.K, ) # Loss coefficient for a 180 degree bend (u-turn), # usually related to radius to inside diameter ratio self.k_loss_uturn = Param( initialize=0.5, within=PositiveReals, mutable=True, doc="Loss coefficient of a tube u-turn", ) # Heat transfer resistance due to the fouling on tube side # (typical boiler hx) self.tube_r_fouling = Param( initialize=0.00017, within=NonNegativeReals, mutable=True, doc="Fouling resistance on tube side", units=pyunits.K * pyunits.m ** 2 * pyunits.W ** -1, ) # Heat transfer resistance due to the fouling on shell side self.shell_r_fouling = Param( initialize=0.0008, within=NonNegativeReals, mutable=True, doc="Fouling resistance on tube side", units=pyunits.K * pyunits.m ** 2 * pyunits.W ** -1, ) # Correction factor for overall heat transfer coefficient self.fcorrection_htc = Var( initialize=1.0, within=NonNegativeReals, doc="Correction factor for HTC" ) # Correction factor for tube side pressure drop due to friction self.fcorrection_dp_tube = Var( initialize=1.0, doc="Correction factor for tube side pressure drop" ) # Correction factor for shell side pressure drop due to friction self.fcorrection_dp_shell = Var( initialize=1.0, doc="Correction factor for shell side pressure drop" ) if self.config.has_radiation is True: # Shell side wall emissivity, converted from parameter to variable self.emissivity_wall = Var(initialize=0.7, doc="Shell side wall emissivity") # Gas emissivity at mbl self.gas_emissivity = Var( self.flowsheet().time, initialize=0.5, doc="Emissivity at given mean beam length", ) # Gas emissivity at mbl/sqrt(2) self.gas_emissivity_div2 = Var( self.flowsheet().time, initialize=0.4, doc="Emissivity at mean beam length divided by sqrt of 2", ) # Gas emissivity at mbl*sqrt(2) self.gas_emissivity_mul2 = Var( self.flowsheet().time, initialize=0.6, doc="Emissivity at mean beam length multiplied by sqrt of 2", ) # Gray fraction of gas in entire spectrum self.gas_gray_fraction = Var( self.flowsheet().time, initialize=0.5, doc="Gray fraction of gas in entire spectrum", ) # Gas-surface radiation exchange factor for shell side wall self.frad_gas_shell = Var( self.flowsheet().time, initialize=0.5, doc="Gas-surface radiation exchange " "factor for shell side wall", ) # Shell side equivalent convective heat transfer coefficient # due to radiation self.hconv_shell_rad = Var( self.flowsheet().time, initialize=100.0, doc="Shell convective heat transfer coefficient due to radiation", units=pyunits.W / pyunits.m ** 2 / pyunits.K, ) # Tube side convective heat transfer coefficient self.hconv_tube = Var( self.flowsheet().time, initialize=100.0, doc="Tube side convective heat transfer coefficient", units=pyunits.W / pyunits.m ** 2 / pyunits.K, ) # Shell side convective heat transfer coefficient due to convection self.hconv_shell_conv = Var( self.flowsheet().time, initialize=100.0, doc="Shell side convective heat transfer coefficient due to convection", units=pyunits.W / pyunits.m ** 2 / pyunits.K, ) # Total shell side convective heat transfer coefficient # including convection and radiation self.hconv_shell_total = Var( self.flowsheet().time, initialize=150.0, doc="Total shell side convective heat transfer coefficient", units=pyunits.W / pyunits.m ** 2 / pyunits.K, ) # Heat conduction resistance of tube wall self.rcond_wall = Var( initialize=1.0, doc="Heat conduction resistance of wall", units=pyunits.m ** 2 * pyunits.K / pyunits.W, ) if self.config.has_radiation is True: # Constraints for gas emissivity @self.Constraint(self.flowsheet().time, doc="Gas emissivity") def gas_emissivity_eqn(b, t): # This is a surrogate model, so need to do units manually X1 = ( ( b.side_2.properties_in[t].temperature + b.side_2.properties_out[t].temperature ) / 2 / pyunits.K ) X2 = b.mbl / pyunits.m X3 = b.side_2.properties_in[t].pressure / pyunits.Pa X4 = b.side_2.properties_in[t].mole_frac_comp["CO2"] X5 = b.side_2.properties_in[t].mole_frac_comp["H2O"] X6 = b.side_2.properties_in[t].mole_frac_comp["O2"] # Surrogate model fitted using rigorous calc. - 500 samples # Wide operating range: # X1: 700 – 1500 (Gas Temperature) # X2: 0.2 – 1 (Mean beam length) # X3: 79000-102000 (pressure in Pa) # X4: 0.12-0.16 (mol frac CO2) # X5: 0.075-0.15 (mol frac H2O) # X6: 0.01-0.07 (mol frac O2) return b.gas_emissivity[t] == ( -0.116916606892e-003 * X1 - 0.29111124038936179309056e-001 * X2 + 0.50509651230704191577346e-006 * X3 + 1.1844222822155641150488 * X4 - 0.64720757767102773949652e-001 * X5 - 0.35853593221454795048064e-001 * X6 + 0.12227919099126832724878 * log(X1) + 0.45102118316418124410738e-001 * log(X2) + 0.33111863480179408447679e-001 * log(X3) + 0.17674928397780117345084e-001 * log(X5) - 0.12541139396423576016226e-001 * exp(X2) - 0.90251708836308952577099 * exp(X4) + 0.32447078857791738538963e-002 * X2 ** 2 - 0.31332075610864829615706e-004 * X1 * X2 - 0.54639645449809960433102e-009 * X1 * X3 - 0.19721467902854980460033e-003 * X1 * X5 + 0.45275517692290622763507e-004 * X1 * X6 + 0.75458754990630776904396e-006 * X2 * X3 + 0.39691751689931338564765e-001 * X2 * X4 + 0.73169514231974708273754 * X2 * X5 - 0.35852614507684822664491e-001 * X2 * X6 + 0.39743672195685803976177e-005 * X3 * X5 + 0.58802879141883679897383e-008 * (X1 * X2) ** 2 - 1.2994610452829884472692 * (X2 * X5) ** 2 ) # Constraints for gas emissivity at mbl/sqrt(2) @self.Constraint( self.flowsheet().time, doc="Gas emissivity at a lower mean beam length" ) def gas_emissivity_div2_eqn(b, t): # This is a surrogate model, so need to do units manually X1 = ( ( b.side_2.properties_in[t].temperature + b.side_2.properties_out[t].temperature ) / 2 / pyunits.K ) X2 = b.mbl_div2 / pyunits.m X3 = b.side_2.properties_in[t].pressure / pyunits.Pa X4 = b.side_2.properties_in[t].mole_frac_comp["CO2"] X5 = b.side_2.properties_in[t].mole_frac_comp["H2O"] X6 = b.side_2.properties_in[t].mole_frac_comp["O2"] # Surrogate model fitted using rigorous calc. - 500 samples # Wide operating range: # X1: 700 – 1500 (Gas Temperature) # X2: 0.2 – 1 (Mean beam length) # X3: 79000-102000 (pressure in Pa) # X4: 0.12-0.16 (mol frac CO2) # X5: 0.075-0.15 (mol frac H2O) # X6: 0.01-0.07 (mol frac O2) return b.gas_emissivity_div2[t] == ( -0.116916606892e-003 * X1 - 0.29111124038936179309056e-001 * X2 + 0.50509651230704191577346e-006 * X3 + 1.1844222822155641150488 * X4 - 0.64720757767102773949652e-001 * X5 - 0.35853593221454795048064e-001 * X6 + 0.12227919099126832724878 * log(X1) + 0.45102118316418124410738e-001 * log(X2) + 0.33111863480179408447679e-001 * log(X3) + 0.17674928397780117345084e-001 * log(X5) - 0.12541139396423576016226e-001 * exp(X2) - 0.90251708836308952577099 * exp(X4) + 0.32447078857791738538963e-002 * X2 ** 2 - 0.31332075610864829615706e-004 * X1 * X2 - 0.54639645449809960433102e-009 * X1 * X3 - 0.19721467902854980460033e-003 * X1 * X5 + 0.45275517692290622763507e-004 * X1 * X6 + 0.75458754990630776904396e-006 * X2 * X3 + 0.39691751689931338564765e-001 * X2 * X4 + 0.73169514231974708273754 * X2 * X5 - 0.35852614507684822664491e-001 * X2 * X6 + 0.39743672195685803976177e-005 * X3 * X5 + 0.58802879141883679897383e-008 * (X1 * X2) ** 2 - 1.2994610452829884472692 * (X2 * X5) ** 2 ) # Constraints for gas emissivity at mbl*sqrt(2) @self.Constraint( self.flowsheet().time, doc="Gas emissivity at a higher mean beam length" ) def gas_emissivity_mul2_eqn(b, t): # This is a surrogate model, so need to do units manually X1 = ( ( b.side_2.properties_in[t].temperature + b.side_2.properties_out[t].temperature ) / 2 / pyunits.K ) X2 = b.mbl_mul2 / pyunits.m X3 = b.side_2.properties_in[t].pressure / pyunits.Pa X4 = b.side_2.properties_in[t].mole_frac_comp["CO2"] X5 = b.side_2.properties_in[t].mole_frac_comp["H2O"] X6 = b.side_2.properties_in[t].mole_frac_comp["O2"] # Surrogate model fitted using rigorous calc. 500 samples # Wide operating range: # X1: 700 – 1500 (Gas Temperature) # X2: 0.2 – 1 (Mean beam length) # X3: 79000-102000 (pressure in Pa) # X4: 0.12-0.16 (mol frac CO2) # X5: 0.075-0.15 (mol frac H2O) # X6: 0.01-0.07 (mol frac O2) return b.gas_emissivity_mul2[t] == ( -0.116916606892e-003 * X1 - 0.29111124038936179309056e-001 * X2 + 0.50509651230704191577346e-006 * X3 + 1.1844222822155641150488 * X4 - 0.64720757767102773949652e-001 * X5 - 0.35853593221454795048064e-001 * X6 + 0.12227919099126832724878 * log(X1) + 0.45102118316418124410738e-001 * log(X2) + 0.33111863480179408447679e-001 * log(X3) + 0.17674928397780117345084e-001 * log(X5) - 0.12541139396423576016226e-001 * exp(X2) - 0.90251708836308952577099 * exp(X4) + 0.32447078857791738538963e-002 * X2 ** 2 - 0.31332075610864829615706e-004 * X1 * X2 - 0.54639645449809960433102e-009 * X1 * X3 - 0.19721467902854980460033e-003 * X1 * X5 + 0.45275517692290622763507e-004 * X1 * X6 + 0.75458754990630776904396e-006 * X2 * X3 + 0.39691751689931338564765e-001 * X2 * X4 + 0.73169514231974708273754 * X2 * X5 - 0.35852614507684822664491e-001 * X2 * X6 + 0.39743672195685803976177e-005 * X3 * X5 + 0.58802879141883679897383e-008 * (X1 * X2) ** 2 - 1.2994610452829884472692 * (X2 * X5) ** 2 ) # fraction of gray gas spectrum @self.Constraint(self.flowsheet().time, doc="Fraction of gray gas spectrum") def gas_gray_fraction_eqn(b, t): return ( b.gas_gray_fraction[t] * (2 * b.gas_emissivity_div2[t] - b.gas_emissivity_mul2[t]) == b.gas_emissivity_div2[t] ** 2 ) # gas-surface radiation exchange factor # between gas and shell side wall @self.Constraint( self.flowsheet().time, doc="Gas-surface radiation exchange " "factor between gas and shell side wall", ) def frad_gas_shell_eqn(b, t): return ( b.frad_gas_shell[t] * ( (1 / b.emissivity_wall - 1) * b.gas_emissivity[t] + b.gas_gray_fraction[t] ) == b.gas_gray_fraction[t] * b.gas_emissivity[t] ) # equivalent convective heat transfer coefficent due to radiation @self.Constraint( self.flowsheet().time, doc="Equivalent convective heat transfer " "coefficent due to radiation", ) def hconv_shell_rad_eqn(b, t): return b.hconv_shell_rad[t] == c.stefan_constant * b.frad_gas_shell[ t ] * ( ( b.side_2.properties_in[t].temperature + b.side_2.properties_out[t].temperature ) / 2 + b.side_1.properties_in[t].temperature ) * ( ( ( b.side_2.properties_in[t].temperature + b.side_2.properties_out[t].temperature ) / 2 ) ** 2 + b.side_1.properties_in[t].temperature ** 2 ) # Tube side heat transfer coefficient and pressure drop # ----------------------------------------------------- # Velocity on tube side self.v_tube = Var( self.flowsheet().time, initialize=1.0, doc="Velocity on tube side", units=pyunits.m / pyunits.s, ) # Reynalds number on tube side self.N_Re_tube = Var( self.flowsheet().time, initialize=10000.0, doc="Reynolds number on tube side", ) if self.config.has_pressure_change is True: # Friction factor on tube side self.friction_factor_tube = Var( self.flowsheet().time, initialize=1.0, doc="Friction factor on tube side", ) # Pressure drop due to friction on tube side self.deltaP_tube_friction = Var( self.flowsheet().time, initialize=-10.0, doc="Pressure drop due to friction on tube side", units=pyunits.Pa, ) # Pressure drop due to 180 degree turn on tube side self.deltaP_tube_uturn = Var( self.flowsheet().time, initialize=-10.0, doc="Pressure drop due to u-turn on tube side", units=pyunits.Pa, ) # Prandtl number on tube side self.N_Pr_tube = Var( self.flowsheet().time, initialize=1, doc="Prandtl number on tube side" ) # Nusselt number on tube side self.N_Nu_tube = Var( self.flowsheet().time, initialize=1, doc="Nusselts number on tube side" ) # Velocity equation @self.Constraint(self.flowsheet().time, doc="Tube side velocity equation") def v_tube_eqn(b, t): return ( b.v_tube[t] * b.area_flow_tube * b.side_1.properties_in[t].dens_mol_phase[self.side_1_fluid_phase] == b.side_1.properties_in[t].flow_mol ) # Reynolds number @self.Constraint( self.flowsheet().time, doc="Reynolds number equation on tube side" ) def N_Re_tube_eqn(b, t): return ( b.N_Re_tube[t] * b.side_1.properties_in[t].visc_d_phase[self.side_1_fluid_phase] == b.tube_di * b.v_tube[t] * b.side_1.properties_in[t].dens_mass_phase[self.side_1_fluid_phase] ) if self.config.has_pressure_change is True: # Friction factor @self.Constraint( self.flowsheet().time, doc="Darcy friction factor on tube side" ) def friction_factor_tube_eqn(b, t): return ( b.friction_factor_tube[t] * b.N_Re_tube[t] ** 0.25 == 0.3164 * b.fcorrection_dp_tube ) # Pressure drop due to friction @self.Constraint( self.flowsheet().time, doc="Pressure drop due to friction on tube side" ) def deltaP_tube_friction_eqn(b, t): return ( b.deltaP_tube_friction[t] == -0.5 * b.side_1.properties_in[t].dens_mass_phase[self.side_1_fluid_phase] * b.v_tube[t] ** 2 * b.friction_factor_tube[t] * b.tube_length * b.tube_nrow / b.tube_di / b.nrow_inlet ) # Pressure drop due to u-turn @self.Constraint( self.flowsheet().time, doc="Pressure drop due to u-turn on tube side" ) def deltaP_tube_uturn_eqn(b, t): return ( b.deltaP_tube_uturn[t] == -0.5 * b.side_1.properties_in[t].dens_mass_phase[self.side_1_fluid_phase] * b.v_tube[t] ** 2 * b.k_loss_uturn ) # Total pressure drop on tube side @self.Constraint( self.flowsheet().time, doc="Total pressure drop on tube side" ) def deltaP_tube_eqn(b, t): return ( b.deltaP_tube[t] == b.deltaP_tube_friction[t] + b.deltaP_tube_uturn[t] - b.delta_elevation * c.acceleration_gravity * ( b.side_1.properties_in[t].dens_mass_phase[ self.side_1_fluid_phase ] + b.side_1.properties_out[t].dens_mass_phase[ self.side_1_fluid_phase ] ) / 2.0 ) # Prandtl number @self.Constraint( self.flowsheet().time, doc="Prandtl number equation on tube side" ) def N_Pr_tube_eqn(b, t): return ( b.N_Pr_tube[t] * b.side_1.properties_in[t].therm_cond_phase[self.side_1_fluid_phase] * b.side_1.properties_in[t].mw == b.side_1.properties_in[t].cp_mol_phase[self.side_1_fluid_phase] * b.side_1.properties_in[t].visc_d_phase[self.side_1_fluid_phase] ) # Nusselts number @self.Constraint( self.flowsheet().time, doc="Nusselts number equation on tube side" ) def N_Nu_tube_eqn(b, t): return ( b.N_Nu_tube[t] == 0.023 * b.N_Re_tube[t] ** 0.8 * abs(b.N_Pr_tube[t]) ** 0.4 ) # Heat transfer coefficient @self.Constraint( self.flowsheet().time, doc="Convective heat transfer " "coefficient equation on tube side", ) def hconv_tube_eqn(b, t): return ( b.hconv_tube[t] * self.tube_di / 1000 == b.N_Nu_tube[t] * b.side_1.properties_in[t].therm_cond_phase[self.side_1_fluid_phase] / 1000 ) # Pressure drop and heat transfer coefficient on shell side # ---------------------------------------------------------- # Tube arrangement factor if self.config.tube_arrangement == TubeArrangement.inLine: self.f_arrangement = Param( initialize=0.788, doc="In-line tube arrangement factor" ) elif self.config.tube_arrangement == TubeArrangement.staggered: self.f_arrangement = Param( initialize=1.0, doc="Staggered tube arrangement factor" ) else: raise Exception("tube arrangement type not supported") # Velocity on shell side self.v_shell = Var( self.flowsheet().time, initialize=1.0, doc="Velocity on shell side", units=pyunits.m / pyunits.s, ) # Reynalds number on shell side self.N_Re_shell = Var( self.flowsheet().time, initialize=10000.0, doc="Reynolds number on shell side", ) # Friction factor on shell side self.friction_factor_shell = Var( self.flowsheet().time, initialize=1.0, doc="Friction factor on shell side" ) # Prandtl number on shell side self.N_Pr_shell = Var( self.flowsheet().time, initialize=1, doc="Prandtl number on shell side" ) # Nusselt number on shell side self.N_Nu_shell = Var( self.flowsheet().time, initialize=1, doc="Nusselts number on shell side" ) # Velocity equation on shell side @self.Constraint(self.flowsheet().time, doc="Velocity on shell side") def v_shell_eqn(b, t): return b.v_shell[t] * b.side_2.properties_in[t].dens_mol_phase[ "Vap" ] * b.area_flow_shell == sum( b.side_2.properties_in[t].flow_mol_comp[j] for j in b.side_2.properties_in[t].params.component_list ) # Reynolds number @self.Constraint( self.flowsheet().time, doc="Reynolds number equation on shell side" ) def N_Re_shell_eqn(b, t): return b.N_Re_shell[t] * b.side_2.properties_in[ t ].visc_d == b.do_tube * b.v_shell[t] * b.side_2.properties_in[ t ].dens_mol_phase[ "Vap" ] * sum( b.side_2.properties_in[t].mw_comp[c] * b.side_2.properties_in[t].mole_frac_comp[c] for c in b.side_2.properties_in[t].params.component_list ) if self.config.has_pressure_change is True: # Friction factor on shell side if self.config.tube_arrangement == TubeArrangement.inLine: @self.Constraint( self.flowsheet().time, doc="In-line friction factor on shell side" ) def friction_factor_shell_eqn(b, t): return ( b.friction_factor_shell[t] * b.N_Re_shell[t] ** 0.15 == ( 0.044 + 0.08 * b.pitch_x_to_do / (b.pitch_y_to_do - 1.0) ** (0.43 + 1.13 / b.pitch_x_to_do) ) * b.fcorrection_dp_shell ) elif self.config.tube_arrangement == TubeArrangement.staggered: @self.Constraint( self.flowsheet().time, doc="Staggered friction factor on shell side" ) def friction_factor_shell_eqn(b, t): return ( b.friction_factor_shell[t] * b.N_Re_shell[t] ** 0.16 == (0.25 + 0.118 / (b.pitch_y_to_do - 1.0) ** 1.08) * b.fcorrection_dp_shell ) else: raise Exception("tube arrangement type not supported") # Pressure drop on shell side @self.Constraint(self.flowsheet().time, doc="Pressure change on shell side") def deltaP_shell_eqn(b, t): return ( b.deltaP_shell[t] == -1.4 * b.friction_factor_shell[t] * b.tube_nrow * b.side_2.properties_in[t].dens_mol_phase["Vap"] * sum( b.side_2.properties_in[t].mw_comp[c] * b.side_2.properties_in[t].mole_frac_comp[c] for c in b.side_2.properties_in[t].params.component_list ) * b.v_shell[t] ** 2 ) # Prandtl number @self.Constraint( self.flowsheet().time, doc="Prandtl number equation on shell side" ) def N_Pr_shell_eqn(b, t): return ( b.N_Pr_shell[t] * b.side_2.properties_in[t].therm_cond * sum( b.side_2.properties_in[t].mw_comp[c] * b.side_2.properties_in[t].mole_frac_comp[c] for c in b.side_2.properties_in[t].params.component_list ) == b.side_2.properties_in[t].cp_mol * b.side_2.properties_in[t].visc_d ) # Nusselt number, currently assume Re>300 @self.Constraint( self.flowsheet().time, doc="Nusselts number equation on shell side" ) def N_Nu_shell_eqn(b, t): return ( b.N_Nu_shell[t] == b.f_arrangement * 0.33 * b.N_Re_shell[t] ** 0.6 * b.N_Pr_shell[t] ** 0.333333 ) # Convective heat transfer coefficient on shell side due to convection @self.Constraint( self.flowsheet().time, doc="Convective heat transfer coefficient equation" "on shell side due to convection", ) def hconv_shell_conv_eqn(b, t): return ( b.hconv_shell_conv[t] * b.do_tube / 1000 == b.N_Nu_shell[t] * b.side_2.properties_in[t].therm_cond / 1000 ) # Total convective heat transfer coefficient on shell side @self.Constraint( self.flowsheet().time, doc="Total convective heat transfer " "coefficient equation on shell side", ) def hconv_shell_total_eqn(b, t): if self.config.has_radiation is True: return ( b.hconv_shell_total[t] == b.hconv_shell_conv[t] + b.hconv_shell_rad[t] ) else: return b.hconv_shell_total[t] == b.hconv_shell_conv[t] # Wall conduction heat transfer resistance # based on outside surface area @self.Constraint(doc="Wall conduction heat transfer resistance") def rcond_wall_eqn(b): return b.rcond_wall * b.therm_cond_wall == 0.5 * b.do_tube * log( b.do_tube / b.tube_di ) # Overall heat transfer coefficient @self.Constraint( self.flowsheet().time, doc="Wall conduction heat transfer resistance" ) def overall_heat_transfer_coefficient_eqn(b, t): return ( b.overall_heat_transfer_coefficient[t] * ( b.rcond_wall + b.tube_r_fouling + b.shell_r_fouling + 1.0 / b.hconv_shell_total[t] + b.do_tube / b.hconv_tube[t] / b.tube_di ) == b.fcorrection_htc )
[docs] def model_check(blk): """ Model checks for unit - calls model checks for both control volume Blocks. Args: None Returns: None """ # Run control volume block model checks blk.side_1.model_check() blk.side_2.model_check()
[docs] def initialize( blk, state_args_1=None, state_args_2=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None, ): """ General Heat Exchanger initialisation routine. Keyword Arguments: state_args_1 : a dict of arguments to be passed to the property package(s) for side 1 of the heat exchanger to provide an initial state for initialization (see documentation of the specific property package) (default = None). state_args_2 : a dict of arguments to be passed to the property package(s) for side 2 of the heat exchanger to provide an initial state for initialization (see documentation of the specific property package) (default = None). outlvl : sets output level of initialisation routine optarg : solver options dictionary object (default=None, use default solver options) solver : str indicating which solver to use during initialization (default = None, use default solver) Returns: None """ # Set solver options init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) # --------------------------------------------------------------------- # Initialize inlet property blocks flags1 = blk.side_1.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_1 ) flags2 = blk.side_2.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_2 ) init_log.info("{} Initialisation Step 1 Complete.".format(blk.name)) # --------------------------------------------------------------------- # Initialize temperature differentials p1_flags = {} p2_flags = {} h1_flags = {} t2_flags = {} for t in blk.flowsheet().time: p1_flags[t] = blk.side_1.properties_out[t].pressure.fixed if ( not blk.side_1.properties_out[t].pressure.fixed and blk.config.has_pressure_change ): blk.side_1.properties_out[t].pressure.fix( value(blk.side_1.properties_in[t].pressure) ) p2_flags[t] = blk.side_2.properties_out[t].pressure.fixed if ( not blk.side_2.properties_out[t].pressure.fixed and blk.config.has_pressure_change ): blk.side_2.properties_out[t].pressure.fix( value(blk.side_2.properties_in[t].pressure) ) h1_flags[t] = blk.side_1.properties_out[t].enth_mol.fixed if not blk.side_1.properties_out[t].enth_mol.fixed: blk.side_1.properties_out[t].enth_mol.fix( value(blk.side_1.properties_in[t].enth_mol) + 100.0 ) t2_flags[t] = blk.side_2.properties_out[t].temperature.fixed if not blk.side_2.properties_out[t].temperature.fixed: blk.side_2.properties_out[t].temperature.fix( value(blk.side_2.properties_in[t].temperature) - 5.0 ) # assuming Delta T min approach # Deactivate Constraints blk.heat_transfer_equation.deactivate() blk.unit_heat_balance.deactivate() if blk.config.has_pressure_change: blk.deltaP_tube_eqn.deactivate() blk.deltaP_shell_eqn.deactivate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res))) # Activate energy balance and driving force for t in blk.flowsheet().time: if not p1_flags[t]: blk.side_1.properties_out[t].pressure.unfix() if not p2_flags[t]: blk.side_2.properties_out[t].pressure.unfix() if not h1_flags[t]: blk.side_1.properties_out[t].enth_mol.unfix() if not t2_flags[t]: blk.side_2.properties_out[t].temperature.unfix() blk.heat_transfer_equation.activate() blk.unit_heat_balance.activate() if blk.config.has_pressure_change: blk.deltaP_tube_eqn.activate() blk.deltaP_shell_eqn.activate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state blk.side_1.release_state(flags1, outlvl) blk.side_2.release_state(flags2, outlvl) init_log.info("{} Initialisation Complete.".format(blk.name))
def calculate_scaling_factors(self): super().calculate_scaling_factors() # We have a pretty good idea that the delta Ts will be between about # 1 and 100 regardless of process of temperature units, so a default # should be fine, so don't warn. Guessing a typical delta t around 10 # the default scaling factor is set to 0.1 sf_dT1 = dict( zip( self.delta_temperature_in.keys(), [ iscale.get_scaling_factor(v, default=0.1) for v in self.delta_temperature_in.values() ], ) ) sf_dT2 = dict( zip( self.delta_temperature_out.keys(), [ iscale.get_scaling_factor(v, default=0.1) for v in self.delta_temperature_out.values() ], ) ) # U depends a lot on the process and units of measure so user should set # this one. sf_u = dict( zip( self.overall_heat_transfer_coefficient.keys(), [ iscale.get_scaling_factor(v, default=0.01, warning=True) for v in self.overall_heat_transfer_coefficient.values() ], ) ) # Since this depends on the process size this is another scaling factor # the user should always set. sf_a = iscale.get_scaling_factor(self.area, default=1e-4, warning=True) for t, c in self.v_shell_eqn.items(): s = iscale.min_scaling_factor( self.side_2.properties_in[t].flow_mol_comp, default=0, warning=False, hint=None, ) if s == 0: s = iscale.get_scaling_factor( self.side_2.properties_in[t].flow_mol, default=1, warning=True ) iscale.constraint_scaling_transform(c, s, overwrite=False) for t, c in self.v_tube_eqn.items(): s = iscale.min_scaling_factor( self.side_1.properties_in[t].flow_mol_comp, default=0, warning=False, hint=None, ) if s == 0: s = iscale.get_scaling_factor( self.side_1.properties_in[t].flow_mol, default=1, warning=True ) iscale.constraint_scaling_transform(c, s, overwrite=False) for t, c in self.N_Nu_tube_eqn.items(): s = iscale.get_scaling_factor(self.N_Nu_tube[t], default=1, warning=True) iscale.constraint_scaling_transform(c, s, overwrite=False) for t, c in self.N_Nu_shell_eqn.items(): s = iscale.get_scaling_factor(self.N_Nu_shell[t], default=1, warning=True) iscale.constraint_scaling_transform(c, s, overwrite=False) for t, c in self.hconv_shell_total_eqn.items(): s = iscale.get_scaling_factor( self.hconv_shell_total[t], default=1, warning=True ) iscale.constraint_scaling_transform(c, s, overwrite=False) if hasattr(self, "deltaP_shell_eqn"): for t, c in self.deltaP_shell_eqn.items(): s = iscale.get_scaling_factor( self.deltaP_shell[t], default=1, warning=True ) iscale.constraint_scaling_transform(c, s, overwrite=False) if hasattr(self, "deltaP_tube_eqn"): for t, c in self.deltaP_tube_eqn.items(): s = iscale.get_scaling_factor( self.deltaP_tube[t], default=1, warning=True ) iscale.constraint_scaling_transform(c, s, overwrite=False) if hasattr(self, "deltaP_tube_friction_eqn"): for t, c in self.deltaP_tube_friction_eqn.items(): s = iscale.get_scaling_factor( self.deltaP_tube_friction[t], default=1, warning=True ) iscale.constraint_scaling_transform(c, s, overwrite=False)