##############################################################################
# Institute for the Design of Advanced Energy Systems Process Systems
# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018, 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.txt and LICENSE.txt for full copyright and
# license information, respectively. Both files are also available online
# at the URL "https://github.com/IDAES/idaes".
##############################################################################
"""
Standard IDAES heat exchanger model.
"""
from __future__ import division
# Import Python libraries
import logging
# Import Pyomo libraries
from pyomo.environ import log, SolverFactory, value, Var
from pyomo.opt import TerminationCondition
from pyomo.common.config import ConfigValue, In
# Import IDAES cores
from idaes.core import UnitBlockData, declare_process_block_class, \
Holdup0D, CONFIG_Base
from idaes.core.util.config import is_parameter_block, list_of_strings
from idaes.core.util.misc import add_object_ref
__author__ = "Andrew Lee"
# Add support for 1D Heat Exchanger model
# Set up logger
logger = logging.getLogger('idaes.unit_model')
[docs]@declare_process_block_class("HeatExchanger")
class HeatExchangerData(UnitBlockData):
"""
Standard Heat Exchanger Unit Model Class
"""
CONFIG = CONFIG_Base()
# Set default values of inherited attributes
CONFIG.get("has_phase_equilibrium")._default = True
CONFIG.get("has_heat_transfer")._default = True
# Add unit model attributes
CONFIG.declare("side_1_property_package", ConfigValue(
default=None,
domain=is_parameter_block,
description="Property package to use for side_1 holdup",
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"""))
CONFIG.declare("side_1_property_package_args", ConfigValue(
default={},
description="Arguments for constructing side_1 property package",
doc="""A dict of arguments to be passed to a property block
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)"""))
CONFIG.declare("side_2_property_package", ConfigValue(
default=None,
domain=is_parameter_block,
description="Property package to use for side_2 holdup",
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"""))
CONFIG.declare("side_2_property_package_args", ConfigValue(
default={},
description="Arguments for constructing side_2 property package",
doc="""A dict of arguments to be passed to a property block
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)"""))
CONFIG.declare("side_1_inlet_list", ConfigValue(
domain=list_of_strings,
description="List of inlet names for side_1 of heat exchanger",
doc="""A list containing names of inlets for side_1 of heat exchanger
(default = None)
- None - default single inlet
- list - a list of names for inlets"""))
CONFIG.declare("side_1_num_inlets", ConfigValue(
domain=int,
description="Number of inlets to side_1 of heat exchanger",
doc="""Argument indication number (int) of inlets to construct to
side_1 of heat exchanger (default = None). Not used if
side_1_inlet_list arg is provided.
- None - use side_1_inlet_list arg instead
- int - Inlets will be named with sequential numbers from 1
to num_inlets"""))
CONFIG.declare("side_1_outlet_list", ConfigValue(
domain=list_of_strings,
description="List of outlet names for side_1 of heat exchanger",
doc="""A list containing names of outlets for side_1 of heat exchanger
(default = None)
- None - default single outlet
- list - a list of names for outlets"""))
CONFIG.declare("side_1_num_outlets", ConfigValue(
domain=int,
description="Number of outlets to side_1 of heat exchanger",
doc="""Argument indication number (int) of outlets to construct to
side_1 of heat exchanger (default = None). Not used if
side_1_outlet_list arg is provided.
- None - use side_1_outlet_list arg instead
- int - Outlets will be named with sequential numbers from 1
to num_outlets"""))
CONFIG.declare("side_2_inlet_list", ConfigValue(
domain=list_of_strings,
description="List of inlet names for side_2 of heat exchanger",
doc="""A list containing names of inlets for side_2 of heat exchanger
(default = None)
- None - default single inlet
- list - a list of names for inlets"""))
CONFIG.declare("side_2_num_inlets", ConfigValue(
domain=int,
description="Number of inlets to side_2 of heat exchanger",
doc="""Argument indication number (int) of inlets to construct to
side_2 of heat exchanger (default = None). Not used if
side_2_inlet_list arg is provided.
- None - use side_2_inlet_list arg instead
- int - Inlets will be named with sequential numbers from 1
to num_inlets"""))
CONFIG.declare("side_2_outlet_list", ConfigValue(
domain=list_of_strings,
description="List of outlet names for side_2 of heat exchanger",
doc="""A list containing names of outlets for side_2 of heat exchanger
(default = None)
- None - default single outlet
- list - a list of names for outlets"""))
CONFIG.declare("side_2_num_outlets", ConfigValue(
domain=int,
description="Number of outlets to side_2 of heat exchanger",
doc="""Argument indication number (int) of outlets to construct to
side_2 of heat exchanger (default = None). Not used if
side_2_outlet_list arg is provided.
- None - use side_2_outlet_list arg instead
- int - Outlets will be named with sequential numbers from 2
to num_outlets"""))
CONFIG.declare("flow_type", ConfigValue(
default='counter-current',
domain=In(['counter-current']),
description="Flow configuration in unit",
doc="""Flag indicating type of flow arrangement to use for heat
exchanger (default = 'counter-current')
- 'counter-current' : counter-current flow arrangement"""))
[docs] def build(self):
"""
Begin building model (pre-DAE transformation)
Args:
None
Returns:
None
"""
# Call UnitModel.build to setup dynamics
super(HeatExchangerData, self).build()
# Build Holdup Block
self.side_1 = Holdup0D(
property_package=self.config.side_1_property_package,
property_package_args=self.config.side_1_property_package_args)
self.side_2 = Holdup0D(
property_package=self.config.side_2_property_package,
property_package_args=self.config.side_2_property_package_args)
# Set Unit Geometry and Holdup Volume
self._set_geometry()
# Construct performance equations
self._make_performance()
# Construct performance equations
if self.config.flow_type == "counter-current":
self._make_counter_current()
[docs] def post_transform_build(self):
"""
Continue model construction after DAE transformation
Args:
None
Returns:
None
"""
# Construct Inlets
self.build_inlets(holdup='side_1',
inlets=self.config.side_1_inlet_list,
num_inlets=self.config.side_1_num_inlets)
self.build_inlets(holdup='side_2',
inlets=self.config.side_2_inlet_list,
num_inlets=self.config.side_2_num_inlets)
# Build Outlets
self.build_outlets(holdup='side_1',
outlets=self.config.side_1_outlet_list,
num_outlets=self.config.side_1_num_outlets)
self.build_outlets(holdup='side_2',
outlets=self.config.side_2_outlet_list,
num_outlets=self.config.side_2_num_outlets)
def _set_geometry(self):
"""
Define the geometry of the unit as necessary, and link to holdup volume
Args:
None
Returns:
None
"""
# For this case, just create a reference to holdup volumes
if self.config.include_holdup is True:
add_object_ref(self, "volume_side_1", self.side_1.volume)
add_object_ref(self, "volume_side_2", self.side_2.volume)
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_ref(self, "heat", self.side_1.heat)
# Performance variables
self.heat_transfer_area = Var(initialize=1.0,
doc='Surface area of heat exchanger')
self.temperature_driving_force = Var(self.time,
initialize=1.0,
doc='Mean driving force for '
'heat exchange')
self.side_1_inlet_dT = Var(self.time,
initialize=1.0,
doc='Temperature difference at side 1 feed')
self.side_1_outlet_dT = Var(self.time,
initialize=1.0,
doc='Temperature difference at side 1 '
'discharge')
self.heat_transfer_coefficient = Var(initialize=1.0,
doc='Heat transfer coefficient')
# Energy balance equation
@self.Constraint(self.time, doc="Energy balance between two sides")
def energy_balance(b, t):
return b.side_1.heat[t] == -b.side_2.heat[t]
# Heat transfer correlation
@self.Constraint(self.time, doc="Heat transfer correlation")
def heat_transfer_correlation(b, t):
return b.heat[t] == -(b.heat_transfer_coefficient *
b.heat_transfer_area *
b.temperature_driving_force[t])
# Driving Force
@self.Constraint(self.time,
doc="Log mean temperature difference calculation")
def LMTD(b, t):
return (b.side_1_inlet_dT[t] - b.side_1_outlet_dT[t]) == (
b.temperature_driving_force[t] *
(log(b.side_1_inlet_dT[t]/b.side_1_outlet_dT[t])))
def _make_counter_current(self):
"""
Add temperature driving force Constraints for counter-current flow.
Args:
None
Returns:
None
"""
# Temperature Differences
@self.Constraint(self.time,
doc="Side 1 inlet temperature difference")
def temperature_difference_1(b, t):
return b.side_1_inlet_dT[t] == (
b.side_1.properties_in[t].temperature -
b.side_2.properties_out[t].temperature)
@self.Constraint(self.time,
doc="Side 1 outlet temperature difference")
def temperature_difference_2(b, t):
return b.side_1_outlet_dT[t] == (
b.side_1.properties_out[t].temperature -
b.side_2.properties_in[t].temperature)
[docs] def model_check(blk):
"""
Model checks for unit - calls model checks for both Holdup Blocks.
Args:
None
Returns:
None
"""
# Run holdup block model checks
blk.side_1.model_check()
blk.side_2.model_check()
[docs] def initialize(blk, state_args_1={}, state_args_2={},
outlvl=0, solver='ipopt', optarg={'tol': 1e-6}):
'''
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 = {}).
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 = {}).
outlvl : sets output level of initialisation routine
* 0 = no output (default)
* 1 = return solver state for each step in routine
* 2 = return solver state for each step in subroutines
* 3 = include solver output infomation (tee=True)
optarg : solver options dictionary object (default={'tol': 1e-6})
solver : str indicating whcih solver to use during
initialization (default = 'ipopt')
Returns:
None
'''
# Set solver options
if outlvl > 3:
stee = True
else:
stee = False
opt = SolverFactory(solver)
opt.options = optarg
# ---------------------------------------------------------------------
# Initialize inlet property blocks
flags1 = blk.side_1.initialize(outlvl=outlvl-1,
optarg=optarg,
solver=solver,
state_args=state_args_1)
flags2 = blk.side_2.initialize(outlvl=outlvl-1,
optarg=optarg,
solver=solver,
state_args=state_args_2)
if outlvl > 0:
logger.info('{} Initialisation Step 1 Complete.'.format(blk.name))
# ---------------------------------------------------------------------
# Initialize temperature differentials
# Fix variables
for t in blk.time:
T = (value(blk.side_1.properties_in[t].temperature) -
value(blk.side_2.properties_in[t].temperature))*0.1
blk.side_1.properties_out[t].temperature.fix(
value(blk.side_1.properties_in[t].temperature)-T)
blk.side_2.properties_out[t].temperature.fix(
value(blk.side_2.properties_in[t].temperature)+T)
# Deactivate Constraints
blk.energy_balance.deactivate()
blk.LMTD.deactivate()
blk.heat_transfer_correlation.deactivate()
results = opt.solve(blk, tee=stee)
if outlvl > 0:
if results.solver.termination_condition == \
TerminationCondition.optimal:
logger.info('{} Initialisation Step 2 Complete.'
.format(blk.name))
else:
logger.warning('{} Initialisation Step 2 Failed.'
.format(blk.name))
# ---------------------------------------------------------------------
# Estimate cross-over temperature and solve
for t in blk.time:
dh1 = (value(blk.side_1.heat[t]) /
(value(blk.side_1.properties_in[t].temperature) -
value(blk.side_1.properties_out[t].temperature)))
dh2 = (value(blk.side_2.heat[t]) /
(value(blk.side_2.properties_in[t].temperature) -
value(blk.side_2.properties_out[t].temperature)))
Tx = (dh1*value(blk.side_1.properties_in[t].temperature) +
dh2*value(blk.side_2.properties_in[t].temperature))/(dh1+dh2)
blk.side_1.properties_out[t].temperature.fix(Tx)
blk.side_2.properties_out[t].temperature.fix(Tx)
results = opt.solve(blk, tee=stee)
if outlvl > 0:
if results.solver.termination_condition == \
TerminationCondition.optimal:
logger.info('{} Initialisation Step 3 Complete.'
.format(blk.name))
else:
logger.warning('{} Initialisation Step 3 Failed.'
.format(blk.name))
# ---------------------------------------------------------------------
# Activate energy balance and driving force
for t in blk.time:
blk.side_1_inlet_dT[t] = (
value(blk.side_1.properties_in[t].temperature) -
value(blk.side_2.properties_out[t].temperature))
blk.side_1_outlet_dT[t] = (
value(blk.side_1.properties_out[t].temperature) -
value(blk.side_2.properties_in[t].temperature))
blk.temperature_driving_force[t] = (
(value(blk.side_1_inlet_dT[t]) -
value(blk.side_1_outlet_dT[t])) /
log(value(blk.side_1_inlet_dT[t]) /
value(blk.side_1_outlet_dT[t])))
blk.heat[t] = -(value(blk.heat_transfer_coefficient) *
value(blk.heat_transfer_area) *
value(blk.temperature_driving_force[t]))
blk.side_2.properties_out[t].temperature.unfix()
blk.side_1.properties_out[t].temperature.unfix()
blk.heat_transfer_correlation.activate()
blk.LMTD.activate()
blk.energy_balance.activate()
results = opt.solve(blk, tee=stee)
if outlvl > 0:
if results.solver.termination_condition == \
TerminationCondition.optimal:
logger.info('{} Initialisation Step 4 Complete.'
.format(blk.name))
else:
logger.warning('{} Initialisation Step 4 Failed.'
.format(blk.name))
# ---------------------------------------------------------------------
# Release Inlet state
blk.side_1.release_state(flags1, outlvl-1)
blk.side_2.release_state(flags2, outlvl-1)
if outlvl > 0:
logger.info('{} Initialisation Complete.'.format(blk.name))