Source code for idaes.models.unit_models.heat_exchanger_1D

#################################################################################
# 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.
#################################################################################
"""
Generic IDAES 1D Heat Exchanger Model with overall area and heat transfer coefficient
"""
# Import Pyomo libraries
from pyomo.environ import (
    Var,
    check_optimal_termination,
    value,
    units as pyunits,
)
from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool

# Import IDAES cores
from idaes.core import (
    ControlVolume1DBlock,
    UnitModelBlockData,
    declare_process_block_class,
    MaterialBalanceType,
    EnergyBalanceType,
    MomentumBalanceType,
    FlowDirection,
    useDefault,
)
from idaes.models.unit_models.heat_exchanger import (
    HeatExchangerFlowPattern,
    hx_process_config,
    add_hx_references,
)
from idaes.core.util.config import is_physical_parameter_block, DefaultBool
from idaes.core.util.misc import add_object_reference
from idaes.core.util.exceptions import ConfigurationError, InitializationError
from idaes.core.util.tables import create_stream_table_dataframe
from idaes.core.util import scaling as iscale
from idaes.core.solvers import get_solver

import idaes.logger as idaeslog


__author__ = "Jaffer Ghouse, Andrew Lee"

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


[docs]@declare_process_block_class("HeatExchanger1D") class HeatExchanger1DData(UnitModelBlockData): """Standard Heat Exchanger 1D Unit Model Class.""" CONFIG = UnitModelBlockData.CONFIG(implicit=True) # Template for config arguments for hot and cold side _SideTemplate = ConfigBlock() _SideTemplate.declare( "dynamic", ConfigValue( default=useDefault, domain=DefaultBool, description="Dynamic model flag", doc="""Indicates whether this model will be dynamic or not, **default** = useDefault. **Valid values:** { **useDefault** - get flag from parent (default = False), **True** - set as a dynamic model, **False** - set as a steady-state model.}""", ), ) _SideTemplate.declare( "has_holdup", ConfigValue( default=useDefault, domain=DefaultBool, description="Holdup construction flag", doc="""Indicates whether holdup terms should be constructed or not. Must be True if dynamic = True, **default** - False. **Valid values:** { **useDefault** - get flag from parent (default = False), **True** - construct holdup terms, **False** - do not construct holdup terms}""", ), ) _SideTemplate.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.}""", ), ) _SideTemplate.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.}""", ), ) _SideTemplate.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.}""", ), ) _SideTemplate.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.}""", ), ) _SideTemplate.declare( "has_phase_equilibrium", ConfigValue( default=False, domain=Bool, description="Phase equilibrium term construction flag", doc="""Argument to enable phase equilibrium. - True - include phase equilibrium term - False - do not include phase equilibrium term""", ), ) _SideTemplate.declare( "property_package", ConfigValue( default=None, domain=is_physical_parameter_block, description="Property package to use for control volume", doc="""Property parameter object used to define property calculations (default = 'use_parent_value') - 'use_parent_value' - get package from parent (default = None) - a ParameterBlock object""", ), ) _SideTemplate.declare( "property_package_args", ConfigValue( default={}, description="Arguments for constructing property package", doc="""A dict of arguments to be passed to the PropertyBlockData and used when constructing these (default = 'use_parent_value') - 'use_parent_value' - get package from parent (default = None) - a dict (see property package for documentation)""", ), ) # TODO : We should probably think about adding a consistency check for the # TODO : discretisation methods as well. _SideTemplate.declare( "transformation_method", ConfigValue( default=useDefault, description="Discretization method to use for DAE transformation", doc="""Discretization method to use for DAE transformation. See Pyomo documentation for supported transformations.""", ), ) _SideTemplate.declare( "transformation_scheme", ConfigValue( default=useDefault, description="Discretization scheme to use for DAE transformation", doc="""Discretization scheme to use when transformating domain. See Pyomo documentation for supported schemes.""", ), ) # Create individual config blocks for hot and cold side CONFIG.declare("hot_side", _SideTemplate(doc="hot side config arguments")) CONFIG.declare("cold_side", _SideTemplate(doc="cold side config arguments")) # Common config args for both sides CONFIG.declare( "finite_elements", ConfigValue( default=20, domain=int, description="Number of finite elements length domain", doc="""Number of finite elements to use when discretizing length domain (default=20)""", ), ) CONFIG.declare( "collocation_points", ConfigValue( default=5, domain=int, description="Number of collocation points per finite element", doc="""Number of collocation points to use per finite element when discretizing length domain (default=3)""", ), ) CONFIG.declare( "flow_type", ConfigValue( default=HeatExchangerFlowPattern.cocurrent, domain=In(HeatExchangerFlowPattern), description="Flow configuration of heat exchanger", doc="""Flow configuration of heat exchanger - HeatExchangerFlowPattern.cocurrent: hot and cold flows from 0 to 1 (default) - HeatExchangerFlowPattern.countercurrent: hot side flows from 0 to 1 cold side flows from 1 to 0""", ), ) CONFIG.declare( "hot_side_name", ConfigValue( default=None, domain=str, doc="Hot side name, sets control volume and inlet and outlet names. " "Default = None.", ), ) CONFIG.declare( "cold_side_name", ConfigValue( default=None, domain=str, doc="Cold side name, sets control volume and inlet and outlet names. " "Default = None.", ), )
[docs] def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() self._process_config() # Set flow directions for the control volume blocks and specify # dicretisation if not specified. if self.config.flow_type == HeatExchangerFlowPattern.cocurrent: set_direction_hot = FlowDirection.forward set_direction_cold = FlowDirection.forward if ( self.config.hot_side.transformation_method != self.config.cold_side.transformation_method ) or ( self.config.hot_side.transformation_scheme != self.config.cold_side.transformation_scheme ): raise ConfigurationError( "HeatExchanger1D only supports similar transformation " "schemes on the hot and cold side domains for " "both cocurrent and countercurrent flow patterns." ) if self.config.hot_side.transformation_method is useDefault: _log.warning( "Discretization method was " "not specified for the hot side of the " "co-current heat exchanger. " "Defaulting to finite " "difference method on the hot side." ) self.config.hot_side.transformation_method = "dae.finite_difference" if self.config.cold_side.transformation_method is useDefault: _log.warning( "Discretization method was " "not specified for the cold side of the " "co-current heat exchanger. " "Defaulting to finite " "difference method on the cold side." ) self.config.cold_side.transformation_method = "dae.finite_difference" if self.config.hot_side.transformation_scheme is useDefault: _log.warning( "Discretization scheme was " "not specified for the hot side of the " "co-current heat exchanger. " "Defaulting to backward finite " "difference on the hot side." ) self.config.hot_side.transformation_scheme = "BACKWARD" if self.config.cold_side.transformation_scheme is useDefault: _log.warning( "Discretization scheme was " "not specified for the cold side of the " "co-current heat exchanger. " "Defaulting to backward finite " "difference on the cold side." ) self.config.cold_side.transformation_scheme = "BACKWARD" elif self.config.flow_type == HeatExchangerFlowPattern.countercurrent: set_direction_hot = FlowDirection.forward set_direction_cold = FlowDirection.backward if self.config.hot_side.transformation_method is useDefault: _log.warning( "Discretization method was " "not specified for the hot side of the " "counter-current heat exchanger. " "Defaulting to finite " "difference method on the hot side." ) self.config.hot_side.transformation_method = "dae.finite_difference" if self.config.cold_side.transformation_method is useDefault: _log.warning( "Discretization method was " "not specified for the cold side of the " "counter-current heat exchanger. " "Defaulting to finite " "difference method on the cold side." ) self.config.cold_side.transformation_method = "dae.finite_difference" if self.config.hot_side.transformation_scheme is useDefault: _log.warning( "Discretization scheme was " "not specified for the hot side of the " "counter-current heat exchanger. " "Defaulting to backward finite " "difference on the hot side." ) self.config.hot_side.transformation_scheme = "BACKWARD" if self.config.cold_side.transformation_scheme is useDefault: _log.warning( "Discretization scheme was " "not specified for the cold side of the " "counter-current heat exchanger. " "Defaulting to forward finite " "difference on the cold side." ) self.config.cold_side.transformation_scheme = "BACKWARD" else: raise ConfigurationError( "{} HeatExchanger1D only supports cocurrent and " "countercurrent flow patterns, but flow_type configuration" " argument was set to {}.".format(self.name, self.config.flow_type) ) # Control volume 1D for hot self.hot_side = ControlVolume1DBlock( dynamic=self.config.hot_side.dynamic, has_holdup=self.config.hot_side.has_holdup, property_package=self.config.hot_side.property_package, property_package_args=self.config.hot_side.property_package_args, transformation_method=self.config.hot_side.transformation_method, transformation_scheme=self.config.hot_side.transformation_scheme, finite_elements=self.config.finite_elements, collocation_points=self.config.collocation_points, ) self.cold_side = ControlVolume1DBlock( dynamic=self.config.cold_side.dynamic, has_holdup=self.config.cold_side.has_holdup, property_package=self.config.cold_side.property_package, property_package_args=self.config.cold_side.property_package_args, transformation_method=self.config.cold_side.transformation_method, transformation_scheme=self.config.cold_side.transformation_scheme, finite_elements=self.config.finite_elements, collocation_points=self.config.collocation_points, ) self.hot_side.add_geometry(flow_direction=set_direction_hot) self.cold_side.add_geometry(flow_direction=set_direction_cold) self.hot_side.add_state_blocks( information_flow=set_direction_hot, has_phase_equilibrium=self.config.hot_side.has_phase_equilibrium, ) self.cold_side.add_state_blocks( information_flow=set_direction_cold, has_phase_equilibrium=self.config.cold_side.has_phase_equilibrium, ) # Populate hot self.hot_side.add_material_balances( balance_type=self.config.hot_side.material_balance_type, has_phase_equilibrium=self.config.hot_side.has_phase_equilibrium, ) 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, ) self.hot_side.apply_transformation() # Populate cold side self.cold_side.add_material_balances( balance_type=self.config.cold_side.material_balance_type, has_phase_equilibrium=self.config.cold_side.has_phase_equilibrium, ) 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, ) self.cold_side.apply_transformation() # Add Ports for hot side self.add_inlet_port(name="hot_side_inlet", block=self.hot_side) self.add_outlet_port(name="hot_side_outlet", block=self.hot_side) # Add Ports for cold side self.add_inlet_port(name="cold_side_inlet", block=self.cold_side) self.add_outlet_port(name="cold_side_outlet", block=self.cold_side) # Add references to the user provided aliases if applicable add_hx_references(self) self._make_geometry() self._make_performance() hot_side_units = ( self.config.hot_side.property_package.get_metadata().get_derived_units ) q_units = hot_side_units("power") / hot_side_units("length") @self.Constraint( self.flowsheet().time, self.hot_side.length_domain, doc="Heat conservation equality", ) def heat_conservation(self, t, x): return pyunits.convert(self.cold_side.heat[t, x], to_units=q_units) == -( self.hot_side.heat[t, x] )
def _process_config(self): hx_process_config(self) def _make_geometry(self): hot_side_units = ( self.config.hot_side.property_package.get_metadata().get_derived_units ) self.area = Var( initialize=1, units=hot_side_units("area"), doc="Heat transfer area" ) # Add reference to control volume geometry add_object_reference(self, "length", self.hot_side.length) # Equate hot and cold side lengths @self.Constraint(doc="Equating hot and cold side lengths") def length_equality(self): return ( pyunits.convert( self.cold_side.length, to_units=hot_side_units("length") ) == self.hot_side.length ) def _make_performance(self): """ Constraints for unit model. Args: None Returns: None """ hot_side_units = ( self.config.hot_side.property_package.get_metadata().get_derived_units ) # Performance variables self.heat_transfer_coefficient = Var( self.flowsheet().time, self.hot_side.length_domain, initialize=50, doc="Average heat transfer coefficient", units=hot_side_units("heat_transfer_coefficient"), ) @self.Constraint( self.flowsheet().time, self.hot_side.length_domain, doc="Heat transfer between hot_side and cold_side", ) def heat_transfer_eq(self, t, x): return self.hot_side.heat[t, x] == -( self.heat_transfer_coefficient[t, x] * self.area / self.length * ( self.hot_side.properties[t, x].temperature - pyunits.convert( self.cold_side.properties[t, x].temperature, to_units=hot_side_units("temperature"), ) ) )
[docs] def initialize_build( self, hot_side_state_args=None, cold_side_state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None, duty=None, ): """ Initialization routine for the unit. Keyword Arguments: state_args : a dict of arguments to be passed to the property package(s) to provide an initial state for initialization (see documentation of the specific property package) (default = {}). 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). A default value is calculated based on stream temperatures, the overall heat transfer coefficient, and exchanger area Returns: None """ init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") # Create solver opt = get_solver(solver, optarg) # --------------------------------------------------------------------- # Get length values if self.length.fixed: # Most likely case self.cold_side.length.set_value(self.length) elif self.cold_side.length.fixed: # This would be unusual, but check self.length.set_value(self.cold_side.length) else: # No fixed value for length - we will assume the user knows what they are doing pass # Initialize control volumes blocks Lfix = self.hot_side.length.fixed self.hot_side.length.fix() flags_hot_side = self.hot_side.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=hot_side_state_args, ) if not Lfix: self.hot_side.length.unfix() Lfix = self.cold_side.length.fixed self.cold_side.length.fix() flags_cold_side = self.cold_side.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=cold_side_state_args, ) if not Lfix: self.cold_side.length.unfix() init_log.info_high("Initialization Step 1 Complete.") # --------------------------------------------------------------------- # Solve unit with fixed heat duty # Guess heat duty based on 1/4 of maximum driving force hot_side_units = ( self.config.hot_side.property_package.get_metadata().get_derived_units ) cold_side_units = ( self.config.cold_side.property_package.get_metadata().get_derived_units ) if duty is None: duty = value( 0.25 * self.heat_transfer_coefficient[0, 0] * self.area * ( self.hot_side.properties[0, 0].temperature - pyunits.convert( self.cold_side.properties[0, 0].temperature, to_units=hot_side_units("temperature"), ) ) ) else: duty = pyunits.convert_value( duty[0], from_units=duty[1], to_units=hot_side_units("power") ) duty_per_length = value(duty / self.length) # Fix heat duties for v in self.hot_side.heat.values(): v.fix(-duty_per_length) for v in self.cold_side.heat.values(): v.fix( pyunits.convert_value( duty_per_length, to_units=cold_side_units("power") / cold_side_units("length"), from_units=hot_side_units("power") / hot_side_units("length"), ) ) # Deactivate heat duty constraints self.heat_transfer_eq.deactivate() self.heat_conservation.deactivate() 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))) # Unfix heat duty and reactivate constraints for v in self.hot_side.heat.values(): v.unfix() for v in self.cold_side.heat.values(): v.unfix() self.heat_transfer_eq.activate() self.heat_conservation.activate() 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))) self.hot_side.release_state(flags_hot_side) self.cold_side.release_state(flags_cold_side) if res is not None and not check_optimal_termination(res): raise InitializationError( f"{self.name} failed to initialize successfully. Please check " f"the output logs for more information." ) init_log.info("Initialization Complete.")
def _get_performance_contents(self, time_point=0): var_dict = {} var_dict["Area"] = self.area var_dict["Length"] = self.length return {"vars": var_dict} def _get_stream_table_contents(self, time_point=0): # Get names for hot and cold sides hot_name = self.config.hot_side_name if hot_name is None: hot_name = "Hot Side" cold_name = self.config.cold_side_name if cold_name is None: cold_name = "Cold Side" return create_stream_table_dataframe( { f"{hot_name} Inlet": self.hot_side_inlet, f"{hot_name} Outlet": self.hot_side_outlet, f"{cold_name} Inlet": self.cold_side_inlet, f"{cold_name} Outlet": self.cold_side_outlet, }, time_point=time_point, ) def calculate_scaling_factors(self): super().calculate_scaling_factors() for i, c in self.heat_transfer_eq.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.hot_side.heat[i], default=1, warning=True ), overwrite=False, ) for i, c in self.heat_conservation.items(): iscale.constraint_scaling_transform( c, iscale.get_scaling_factor( self.hot_side.heat[i], default=1, warning=True ), overwrite=False, )