Source code for idaes.models.unit_models.heat_exchanger_ntu

#################################################################################
# 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.
#################################################################################
"""
IDAES heat exchanger model using effectiveness-NTU method

Assumptions:
    * No phase equilibrium or reactions occur within unit

"""

# Import Pyomo libraries
from pyomo.environ import (
    Block,
    check_optimal_termination,
    Constraint,
    Expression,
    Param,
    PositiveReals,
    Reference,
    units as pyunits,
    Var,
)
from pyomo.common.config import Bool, ConfigBlock, ConfigValue, In

# Import IDAES cores
from idaes.core import (
    ControlVolume0DBlock,
    declare_process_block_class,
    MaterialBalanceType,
    EnergyBalanceType,
    MomentumBalanceType,
    UnitModelBlockData,
    useDefault,
)
from idaes.models.unit_models.heat_exchanger import hx_process_config, add_hx_references
from idaes.core.util.config import is_physical_parameter_block
from idaes.core.util.tables import create_stream_table_dataframe
from idaes.core.util.math import smooth_min, smooth_max
from idaes.core.solvers import get_solver
from idaes.core.util.exceptions import InitializationError
import idaes.logger as idaeslog
from idaes.core.initialization import SingleControlVolumeUnitInitializer

__author__ = "Paul Akula, Andrew Lee"


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


[docs] class HXNTUInitializer(SingleControlVolumeUnitInitializer): """ Initializer for NTU Heat Exchanger units. """
[docs] def initialization_routine( self, model: Block, plugin_initializer_args: dict = None, copy_inlet_state: bool = False, duty=1000 * pyunits.W, ): """ Common initialization routine for NTU Heat Exchangers. This routine starts by initializing the hot and cold side properties. Next, the heat transfer between the two sides is fixed to an initial guess for the heat duty (provided by the duty argument), the associated constraint deactivated, and the model is then solved. Finally, the heat duty is unfixed and the heat transfer constraint reactivated followed by a final solve of the model. Args: model: Pyomo Block to be initialized plugin_initializer_args: dict-of-dicts containing arguments to be passed to plug-in Initializers. Keys should be submodel components. copy_inlet_state: bool (default=False). Whether to copy inlet state to other states or not (0-D control volumes only). Copying will generally be faster, but inlet states may not contain all properties required elsewhere. duty: initial guess for heat duty to assist with initialization. Can be a Pyomo expression with units. Returns: Pyomo solver results object """ return super(SingleControlVolumeUnitInitializer, self).initialization_routine( model=model, plugin_initializer_args=plugin_initializer_args, copy_inlet_state=copy_inlet_state, duty=duty, )
def initialize_main_model( self, model: Block, copy_inlet_state: bool = False, duty=1000 * pyunits.W, ): """ Initialization routine for main NTU HX models. Args: model: Pyomo Block to be initialized. copy_inlet_state: bool (default=False). Whether to copy inlet state to other states or not (0-D control volumes only). Copying will generally be faster, but inlet states may not contain all properties required elsewhere. duty: initial guess for heat duty to assist with initialization. Can be a Pyomo expression with units. Returns: Pyomo solver results object. """ # TODO: Aside from one differences in constraint names, this is # identical to the Initializer for the 0D HX unit. # Set solver options init_log = idaeslog.getInitLogger( model.name, self.get_output_level(), tag="unit" ) solve_log = idaeslog.getSolveLogger( model.name, self.get_output_level(), tag="unit" ) # Create solver solver = self._get_solver() self.initialize_control_volume(model.hot_side, copy_inlet_state) init_log.info_high("Initialization Step 1a (hot side) Complete.") self.initialize_control_volume(model.cold_side, copy_inlet_state) init_log.info_high("Initialization Step 1b (cold side) Complete.") # --------------------------------------------------------------------- # Solve unit without heat transfer equation model.energy_balance_constraint.deactivate() model.cold_side.heat.fix(duty) for i in model.hot_side.heat: model.hot_side.heat[i].set_value(-duty) with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(model, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res))) model.cold_side.heat.unfix() model.energy_balance_constraint.activate() # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver.solve(model, tee=slc.tee) init_log.info("Initialization Completed, {}".format(idaeslog.condition(res))) return res
[docs] @declare_process_block_class("HeatExchangerNTU") class HeatExchangerNTUData(UnitModelBlockData): """Heat Exchanger Unit Model using NTU method.""" default_initializer = HXNTUInitializer CONFIG = UnitModelBlockData.CONFIG(implicit=True) # Configuration template for fluid specific arguments _SideCONFIG = ConfigBlock() _SideCONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.useDefault, domain=In(MaterialBalanceType), description="Material balance construction flag", doc="""Indicates what type of mass balance should be constructed, **default** - MaterialBalanceType.useDefault. **Valid values:** { **MaterialBalanceType.useDefault - refer to property package for default balance type **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.}""", ), ) _SideCONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.useDefault, domain=In(EnergyBalanceType), description="Energy balance construction flag", doc="""Indicates what type of energy balance should be constructed, **default** - EnergyBalanceType.useDefault. **Valid values:** { **EnergyBalanceType.useDefault - refer to property package for default balance type **EnergyBalanceType.none** - exclude energy balances, **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material, **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase, **EnergyBalanceType.energyTotal** - single energy balance for material, **EnergyBalanceType.energyPhase** - energy balances for each phase.}""", ), ) _SideCONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, domain=In(MomentumBalanceType), description="Momentum balance construction flag", doc="""Indicates what type of momentum balance should be constructed, **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.}""", ), ) _SideCONFIG.declare( "has_pressure_change", ConfigValue( default=False, domain=Bool, description="Pressure change term construction flag", doc="""Indicates whether terms for pressure change should be constructed, **default** - False. **Valid values:** { **True** - include pressure change terms, **False** - exclude pressure change terms.}""", ), ) _SideCONFIG.declare( "property_package", ConfigValue( default=useDefault, domain=is_physical_parameter_block, description="Property package to use ", doc="""Property parameter object used to define property calculations **default** - useDefault. **Valid values:** { **useDefault** - use default package from parent model or flowsheet, **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", ), ) _SideCONFIG.declare( "property_package_args", ConfigBlock( implicit=True, description="Arguments to use for constructing property package", doc="""A ConfigBlock with arguments to be passed to property block(s) and used when constructing these, **default** - None. **Valid values:** { see property package for documentation.}""", ), ) # Create individual config blocks for hot and cold sides CONFIG.declare("hot_side", _SideCONFIG(doc="Hot fluid config arguments")) CONFIG.declare("cold_side", _SideCONFIG(doc="Cold fluid config arguments")) CONFIG.declare( "hot_side_name", ConfigValue( default=None, domain=str, doc="Hot side name, sets control volume and inlet and outlet names", ), ) CONFIG.declare( "cold_side_name", ConfigValue( default=None, domain=str, doc="Cold side name, sets control volume and inlet and outlet names", ), )
[docs] def build(self): # Call UnitModel.build to setup model super().build() hx_process_config(self) # --------------------------------------------------------------------- # Build hot-side control volume self.hot_side = ControlVolume0DBlock( dynamic=self.config.dynamic, has_holdup=self.config.has_holdup, property_package=self.config.hot_side.property_package, property_package_args=self.config.hot_side.property_package_args, ) # TODO : Add support for phase equilibrium? self.hot_side.add_state_blocks(has_phase_equilibrium=False) self.hot_side.add_material_balances( balance_type=self.config.hot_side.material_balance_type, has_phase_equilibrium=False, ) self.hot_side.add_energy_balances( balance_type=self.config.hot_side.energy_balance_type, has_heat_transfer=True, ) self.hot_side.add_momentum_balances( balance_type=self.config.hot_side.momentum_balance_type, has_pressure_change=self.config.hot_side.has_pressure_change, ) # --------------------------------------------------------------------- # Build cold-side control volume self.cold_side = ControlVolume0DBlock( dynamic=self.config.dynamic, has_holdup=self.config.has_holdup, property_package=self.config.cold_side.property_package, property_package_args=self.config.cold_side.property_package_args, ) self.cold_side.add_state_blocks(has_phase_equilibrium=False) self.cold_side.add_material_balances( balance_type=self.config.cold_side.material_balance_type, has_phase_equilibrium=False, ) self.cold_side.add_energy_balances( balance_type=self.config.cold_side.energy_balance_type, has_heat_transfer=True, ) self.cold_side.add_momentum_balances( balance_type=self.config.cold_side.momentum_balance_type, has_pressure_change=self.config.cold_side.has_pressure_change, ) # --------------------------------------------------------------------- # Add Ports to control volumes self.add_inlet_port( name="hot_side_inlet", block=self.hot_side, doc="Hot side inlet port" ) self.add_outlet_port( name="hot_side_outlet", block=self.hot_side, doc="Hot side outlet port" ) self.add_inlet_port( name="cold_side_inlet", block=self.cold_side, doc="Cold side inlet port" ) self.add_outlet_port( name="cold_side_outlet", block=self.cold_side, doc="Cold side outlet port" ) # --------------------------------------------------------------------- # Add unit level References # Set references to balance terms at unit level self.heat_duty = Reference(self.cold_side.heat[:]) # Add references to the user provided aliases (if applicable). add_hx_references(self) # --------------------------------------------------------------------- # Add performance equations # All units of measurement will be based on hot side hunits = self.hot_side.config.property_package.get_metadata().get_derived_units # Common heat exchanger variables self.area = Var( initialize=1, units=hunits("area"), domain=PositiveReals, doc="Heat transfer area", ) self.heat_transfer_coefficient = Var( self.flowsheet().time, initialize=1, units=hunits("heat_transfer_coefficient"), domain=PositiveReals, doc="Overall heat transfer coefficient", ) # Overall energy balance def rule_energy_balance(blk, t): return blk.hot_side.heat[t] == -pyunits.convert( blk.cold_side.heat[t], to_units=hunits("power") ) self.energy_balance_constraint = Constraint( self.flowsheet().time, rule=rule_energy_balance ) # Add e-NTU variables self.effectiveness = Var( self.flowsheet().time, initialize=1, units=pyunits.dimensionless, domain=PositiveReals, doc="Effectiveness factor for NTU method", ) # Minimum heat capacitance ratio for e-NTU method self.eps_cmin = Param( initialize=1e-3, mutable=True, units=hunits("power") / hunits("temperature"), doc="Epsilon parameter for smooth Cmin and Cmax", ) # TODO : Support both mass and mole based flows def rule_Cmin(blk, t): caph = ( blk.hot_side.properties_in[t].flow_mol * blk.hot_side.properties_in[t].cp_mol ) capc = pyunits.convert( blk.cold_side.properties_in[t].flow_mol * blk.cold_side.properties_in[t].cp_mol, to_units=hunits("power") / hunits("temperature"), ) return smooth_min(caph, capc, eps=blk.eps_cmin) self.Cmin = Expression( self.flowsheet().time, rule=rule_Cmin, doc="Minimum heat capacitance rate" ) def rule_Cmax(blk, t): caph = ( blk.hot_side.properties_in[t].flow_mol * blk.hot_side.properties_in[t].cp_mol ) capc = pyunits.convert( blk.cold_side.properties_in[t].flow_mol * blk.cold_side.properties_in[t].cp_mol, to_units=hunits("power") / hunits("temperature"), ) return smooth_max(caph, capc, eps=blk.eps_cmin) self.Cmax = Expression( self.flowsheet().time, rule=rule_Cmax, doc="Maximum heat capacitance rate" ) # Heat capacitance ratio def rule_Cratio(blk, t): return blk.Cmin[t] / blk.Cmax[t] self.Cratio = Expression( self.flowsheet().time, rule=rule_Cratio, doc="Heat capacitance ratio" ) def rule_NTU(blk, t): return blk.heat_transfer_coefficient[t] * blk.area / blk.Cmin[t] self.NTU = Expression( self.flowsheet().time, rule=rule_NTU, doc="Number of heat transfer units" ) # Heat transfer by e-NTU method def rule_entu(blk, t): return blk.hot_side.heat[t] == -( blk.effectiveness[t] * blk.Cmin[t] * ( blk.hot_side.properties_in[t].temperature - pyunits.convert( blk.cold_side.properties_in[t].temperature, to_units=hunits("temperature"), ) ) ) self.heat_duty_constraint = Constraint(self.flowsheet().time, rule=rule_entu)
# TODO : Add scaling methods
[docs] def initialize_build( self, hot_side_state_args=None, cold_side_state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None, duty=None, ): """ Heat exchanger initialization method. Args: hot_side_state_args : a dict of arguments to be passed to the property initialization for the hot side (see documentation of the specific property package) (default = None). cold_side_state_args : a dict of arguments to be passed to the property initialization for the cold side (see documentation of the specific property package) (default = None). outlvl : sets output level of initialization 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) duty : an initial guess for the amount of heat transferred. This should be a tuple in the form (value, units), (default = (1000 J/s)) Returns: None """ # Set solver options init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") hot_side = self.hot_side cold_side = self.cold_side # Create solver opt = get_solver(solver, optarg) flags1 = hot_side.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=hot_side_state_args ) init_log.info_high("Initialization Step 1a (hot side) Complete.") flags2 = cold_side.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=cold_side_state_args ) init_log.info_high("Initialization Step 1b (cold side) Complete.") # --------------------------------------------------------------------- # Solve unit without heat transfer equation self.energy_balance_constraint.deactivate() # Get side 1 and side 2 heat units, and convert duty as needed s1_units = hot_side.heat.get_units() s2_units = cold_side.heat.get_units() if duty is None: # Assume 1000 J/s and check for unitless properties if s1_units is None and s2_units is None: # Backwards compatibility for unitless properties s1_duty = -1000 s2_duty = 1000 else: s1_duty = pyunits.convert_value( -1000, from_units=pyunits.W, to_units=s1_units ) s2_duty = pyunits.convert_value( 1000, from_units=pyunits.W, to_units=s2_units ) else: # Duty provided with explicit units s1_duty = -pyunits.convert_value( duty[0], from_units=duty[1], to_units=s1_units ) s2_duty = pyunits.convert_value( duty[0], from_units=duty[1], to_units=s2_units ) cold_side.heat.fix(s2_duty) for i in hot_side.heat: hot_side.heat[i].value = s1_duty with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res))) cold_side.heat.unfix() self.energy_balance_constraint.activate() # --------------------------------------------------------------------- # Solve unit with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) # --------------------------------------------------------------------- # Release Inlet state hot_side.release_state(flags1, outlvl=outlvl) cold_side.release_state(flags2, outlvl=outlvl) init_log.info("Initialization Completed, {}".format(idaeslog.condition(res))) if not check_optimal_termination(res): raise InitializationError( f"{self.name} failed to initialize successfully. Please check " f"the output logs for more information." ) return res
def _get_stream_table_contents(self, time_point=0): return create_stream_table_dataframe( { "Hot Inlet": self.hot_side_inlet, "Hot Outlet": self.hot_side_outlet, "Cold Inlet": self.cold_side_inlet, "Cold Outlet": self.cold_side_outlet, }, time_point=time_point, )