Source code for idaes.models.unit_models.shell_and_tube_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.
#################################################################################
"""
1D Single pass shell and tube HX model with 0D wall conduction model.

This model derives from the HeatExchanger1D unit model.
"""
# Import Pyomo libraries
from pyomo.environ import (
    Var,
    check_optimal_termination,
    Constraint,
    value,
    units as pyunits,
)
from pyomo.common.config import ConfigValue, Bool

# Import IDAES cores
from idaes.core import declare_process_block_class, UnitModelBlockData
from idaes.models.unit_models.heat_exchanger_1D import (
    HeatExchanger1DData,
)
from idaes.core.util.misc import add_object_reference
from idaes.core.util.exceptions import InitializationError
from idaes.core.util.constants import Constants as c
from idaes.core.util import scaling as iscale
from idaes.core.util.tables import create_stream_table_dataframe
from idaes.core.solvers import get_solver

import idaes.logger as idaeslog


__author__ = "Jaffer Ghouse"

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


[docs]@declare_process_block_class("ShellAndTube1D") class ShellAndTube1DData(HeatExchanger1DData): """1D Shell and Tube HX Unit Model Class.""" CONFIG = HeatExchanger1DData.CONFIG(implicit=True) CONFIG.declare( "shell_is_hot", ConfigValue( default=True, domain=Bool, description="Shell side contains hot fluid", doc="""Boolean flag indicating whether shell side contains hot fluid (default=True). If True, shell side will be the hot_side, if False shell side will be cold_side.""", ), )
[docs] def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build()
def _process_config(self): super()._process_config() # Check for custom names, and if not present assign defaults if self.config.hot_side_name is None: if self.config.shell_is_hot: self.config.hot_side_name = "Shell" else: self.config.hot_side_name = "Tube" if self.config.cold_side_name is None: if self.config.shell_is_hot: self.config.cold_side_name = "Tube" else: self.config.cold_side_name = "Shell" def _make_geometry(self): # Add reference to control volume geometry add_object_reference(self, "hot_side_area", self.hot_side.area) add_object_reference(self, "length", self.hot_side.length) # Equate hot and cold side geometries 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 ) @self.Constraint( self.flowsheet().time, doc="Equating hot and cold side lengths" ) def length_equality(self, t): return ( pyunits.convert( self.cold_side.length, to_units=hot_side_units("length") ) == self.hot_side.length ) # Get hot and cold sides if self.config.shell_is_hot: shell = self.hot_side tube = self.cold_side else: shell = self.cold_side tube = self.hot_side # New Unit model variables and constraints self.shell_diameter = Var( initialize=0.011, doc="Diameter of shell", units=hot_side_units("length"), ) self.tube_outer_diameter = Var( initialize=1, doc="Outer diameter of tubes", units=hot_side_units("length"), ) self.tube_inner_diameter = Var( initialize=0.010, doc="Inner diameter of tubes", units=hot_side_units("length"), ) self.number_of_tubes = Var( initialize=1, doc="Number of tubes", units=pyunits.dimensionless ) # Calculate cross-sectional area of control volumes self.tube_side_xsec_area_calc = Constraint( expr=4 * tube.area == self.number_of_tubes * c.pi * pyunits.convert( self.tube_inner_diameter, to_units=cold_side_units("length") ) ** 2, doc="Tube side cross-sectional area", ) # Need to account for area occupied by tubes self.shell_side_xsec_area_calc = Constraint( expr=4 * shell.area == c.pi * ( self.shell_diameter**2 - self.number_of_tubes * self.tube_outer_diameter**2 ), doc="Shell side cross-sectional area", ) def _make_performance(self): hot_side_units = ( self.config.hot_side.property_package.get_metadata().get_derived_units ) # Performance variables self.hot_side_heat_transfer_coefficient = Var( self.flowsheet().time, self.hot_side.length_domain, initialize=50, doc="Hot side heat transfer coefficient", units=hot_side_units("heat_transfer_coefficient"), ) self.cold_side_heat_transfer_coefficient = Var( self.flowsheet().time, self.cold_side.length_domain, initialize=50, doc="Cold side heat transfer coefficient", units=hot_side_units("heat_transfer_coefficient"), ) self.temperature_wall = Var( self.flowsheet().time, self.hot_side.length_domain, initialize=298.15, units=hot_side_units("temperature"), ) @self.Constraint( self.flowsheet().time, self.hot_side.length_domain, doc="Heat transfer between hot_side and wall", ) def hot_side_heat_transfer_eq(self, t, x): return self.hot_side.heat[t, x] == -( self.hot_side_heat_transfer_coefficient[t, x] * self.number_of_tubes * c.pi * self.tube_outer_diameter * ( self.hot_side.properties[t, x].temperature - self.temperature_wall[t, x] ) ) @self.Constraint( self.flowsheet().time, self.cold_side.length_domain, doc="Heat transfer between cold_side and wall", ) def cold_side_heat_transfer_eq(self, t, x): return pyunits.convert( self.cold_side.heat[t, x], to_units=hot_side_units("power") / hot_side_units("length"), ) == ( self.cold_side_heat_transfer_coefficient[t, x] * self.number_of_tubes * c.pi * self.tube_inner_diameter * ( self.temperature_wall[t, x] - 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, ): """ 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) 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) # --------------------------------------------------------------------- # Initialize hot_side block flags_hot_side = self.hot_side.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=hot_side_state_args, ) flags_cold_side = self.cold_side.initialize( outlvl=outlvl, optarg=optarg, solver=solver, state_args=cold_side_state_args, ) init_log.info_high("Initialization Step 1 Complete.") # --------------------------------------------------------------------- # Solve unit hot_side_units = ( self.config.hot_side.property_package.get_metadata().get_derived_units ) for t in self.flowsheet().time: for z in self.hot_side.length_domain: self.temperature_wall[t, z].fix( value( 0.5 * ( self.hot_side.properties[t, 0].temperature + pyunits.convert( self.cold_side.properties[t, 0].temperature, to_units=hot_side_units("temperature"), ) ) ) ) self.cold_side.deactivate() self.cold_side_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))) self.cold_side.activate() self.cold_side_heat_transfer_eq.activate() self.heat_conservation.activate() self.temperature_wall.unfix() 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["Shell Diameter"] = self.shell_diameter var_dict["Length"] = self.length var_dict["Tube Outer Diameter"] = self.tube_outer_diameter var_dict["Tube Inner Diameter"] = self.tube_inner_diameter var_dict["Number of Tubes"] = self.number_of_tubes 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 cold_name = self.config.cold_side_name 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(UnitModelBlockData, self).calculate_scaling_factors() for i, c in self.hot_side_heat_transfer_eq.items(): print( iscale.get_scaling_factor( self.hot_side.heat[i], default=1, warning=False ) ) 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.cold_side_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, )