From 5782077fa2b6396e910a05ab7246190e5943843e Mon Sep 17 00:00:00 2001 From: Doug A Date: Wed, 21 Feb 2024 17:23:17 -0500 Subject: [PATCH 01/38] add unit models --- .../heat_exchanger_1D_cross_flow.py | 765 ++++++++++++ .../unit_models/heat_exchanger_common.py | 1101 +++++++++++++++++ .../power_generation/unit_models/heater_1D.py | 624 ++++++++++ 3 files changed, 2490 insertions(+) create mode 100644 idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py create mode 100644 idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py create mode 100644 idaes/models_extra/power_generation/unit_models/heater_1D.py diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py new file mode 100644 index 0000000000..24e7355d90 --- /dev/null +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py @@ -0,0 +1,765 @@ +############################################################################## +# Institute for the Design of Advanced Energy Systems Process Systems +# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018-2019, 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-pse". +############################################################################## +""" +1-D Cross Flow Heat Exchanger Model With Wall Temperatures + +Discretization based on tube rows +""" +from __future__ import division + +# Import Python libraries +import math + +import pyomo.common.config +import pyomo.opt + +# Import Pyomo libraries +from pyomo.environ import ( + SolverFactory, + Var, + Param, + Constraint, + value, + TerminationCondition, + exp, + sqrt, + log, + sin, + cos, + SolverStatus, +) +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, + UnitModelBlockData, + useDefault, +) +from idaes.core.util.constants import Constants as const +import idaes.core.util.scaling as iscale +from pyomo.dae import DerivativeVar +from pyomo.network import Port +from idaes.core.solvers import get_solver +from idaes.core.util.config import is_physical_parameter_block +from idaes.core.util.misc import add_object_reference +import idaes.logger as idaeslog +from idaes.core.util.tables import create_stream_table_dataframe + +from idaes.models.unit_models.heater import _make_heater_config_block +from idaes.models.unit_models.heat_exchanger import ( + HeatExchangerFlowPattern, + hx_process_config, + add_hx_references, +) +from idaes.models.unit_models.heat_exchanger_1D import HeatExchanger1DData +from idaes.models.unit_models.shell_and_tube_1d import ShellAndTube1DData +import heat_exchanger_common + +__author__ = "Jinliang Ma, Douglas Allan" + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("HeatExchangerCrossFlow1D") +class HeatExchangerCrossFlow1DData(HeatExchanger1DData): + """Standard Heat Exchanger Cross Flow Unit Model Class.""" + + CONFIG = HeatExchanger1DData.CONFIG() + 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.""", + ), + ) + CONFIG.declare( + "tube_arrangement", + ConfigValue( + default="in-line", + domain=In(["in-line", "staggered"]), + description="tube configuration", + doc="tube arrangement could be in-line or staggered", + ), + ) + CONFIG.declare( + "has_radiation", + ConfigValue( + default=False, + domain=Bool, + description="Has side 2 gas radiation", + doc="define if shell side gas radiation is to be considered", + ), + ) + + def _process_config(self): + # Copy and pasted from ShellAndTube1D + 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 build(self): + """ + Begin building model (pre-DAE transformation). + + Args: + None + + Returns: + None + """ + # Call HeatExchanger1DData build to make common components + super().build() + + # The HeatExchanger1DData equates the heat lost by the hot side and heat gained by the cold side. + # That equation is deleted here because heat can accumulate in the wall. + self.del_component(self.heat_conservation) + + # Create aliases for ports + if self.config.shell_is_hot: + self.shell_inlet = Port(extends=self.hot_side_inlet) + self.shell_outlet = Port(extends=self.hot_side_outlet) + self.tube_inlet = Port(extends=self.cold_side_inlet) + self.tube_outlet = Port(extends=self.cold_side_outlet) + else: + self.shell_inlet = Port(extends=self.cold_side_inlet) + self.shell_outlet = Port(extends=self.cold_side_outlet) + self.tube_inlet = Port(extends=self.hot_side_inlet) + self.tube_outlet = Port(extends=self.hot_side_outlet) + + def _make_geometry(self): + """ + Constraints for unit model. + + Args: + None + + Returns: + None + """ + if self.config.shell_is_hot: + shell = self.hot_side + tube = self.cold_side + shell_units = ( + self.config.hot_side.property_package.get_metadata().derived_units + ) + else: + shell = self.cold_side + tube = self.hot_side + shell_units = ( + self.config.cold_side.property_package.get_metadata().derived_units + ) + # Add reference to control volume geometry + add_object_reference(self, "area_flow_shell", shell.area) + add_object_reference(self, "length_flow_shell", shell.length) + add_object_reference(self, "area_flow_tube", tube.area) + # total tube length of flow path + add_object_reference(self, "length_flow_tube", tube.length) + heat_exchanger_common._make_geometry_common(self, shell_units=shell_units) + heat_exchanger_common._make_geometry_tube(self, shell_units=shell_units) + + def _make_performance(self): + """ + Constraints for unit model. + + Args: + None + + Returns: + None + """ + if self.config.shell_is_hot: + shell = self.hot_side + tube = self.cold_side + shell_units = ( + self.config.hot_side.property_package.get_metadata().derived_units + ) + tube_units = ( + self.config.cold_side.property_package.get_metadata().derived_units + ) + else: + shell = self.cold_side + tube = self.hot_side + shell_units = ( + self.config.cold_side.property_package.get_metadata().derived_units + ) + tube_units = ( + self.config.hot_side.property_package.get_metadata().derived_units + ) + # Reference + add_object_reference(self, "heat_tube", tube.heat) + add_object_reference(self, "heat_shell", shell.heat) + + shell_has_pressure_change = False + tube_has_pressure_change = False + + if self.config.cold_side.has_pressure_change: + if self.config.shell_is_hot: + add_object_reference(self, "deltaP_tube", tube.deltaP) + tube_has_pressure_change = True + else: + add_object_reference(self, "deltaP_shell", shell.deltaP) + shell_has_pressure_change = True + if self.config.hot_side.has_pressure_change: + if self.config.shell_is_hot: + add_object_reference(self, "deltaP_shell", shell.deltaP) + shell_has_pressure_change = True + else: + add_object_reference(self, "deltaP_tube", tube.deltaP) + tube_has_pressure_change = True + + heat_exchanger_common._make_performance_common( + self, + shell=shell, + shell_units=shell_units, + shell_has_pressure_change=shell_has_pressure_change, + make_reynolds=True, + make_nusselt=True, + ) + heat_exchanger_common._make_performance_tube( + self, + tube=tube, + tube_units=tube_units, + tube_has_pressure_change=tube_has_pressure_change, + make_reynolds=True, + make_nusselt=True, + ) + + def heat_accumulation_term(b, t, x): + return b.heat_accumulation[t, x] if b.config.dynamic else 0 + + # Nusselts number + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="Nusselts number equation on tube side", + ) + def N_Nu_tube_eqn(b, t, x): + return ( + b.N_Nu_tube[t, x] + == 0.023 + * b.N_Re_tube[t, x] ** 0.8 + * tube.properties[t, x].prandtl_number_phase["Vap"] ** 0.4 + ) + + @self.Constraint( + self.flowsheet().config.time, + shell.length_domain, + doc="Nusselts number equation on shell side", + ) + def N_Nu_shell_eqn(b, t, x): + return ( + b.N_Nu_shell[t, x] + == b.f_arrangement + * 0.33 + * b.N_Re_shell[t, x] ** 0.6 + * shell.properties[t, x].prandtl_number_phase["Vap"] ** 0.333333 + ) + + # Energy balance with tube wall + # ------------------------------------ + # Heat to wall per length on tube side + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="heat per length on tube side", + ) + def heat_tube_eqn(b, t, x): + return b.heat_tube[t, x] == b.hconv_tube[ + t, x + ] * const.pi * b.di_tube * b.nrow_inlet * b.ncol_tube * ( + b.temp_wall_tube[t, x] - tube.properties[t, x].temperature + ) + + # Heat to wall per length on shell side + @self.Constraint( + self.flowsheet().config.time, + shell.length_domain, + doc="heat per length on shell side", + ) + def heat_shell_eqn(b, t, x): + return b.heat_shell[ + t, x + ] * b.length_flow_shell == b.length_flow_tube * b.hconv_shell_total[ + t, x + ] * const.pi * b.do_tube * b.nrow_inlet * b.ncol_tube * ( + b.temp_wall_shell[t, x] - shell.properties[t, x].temperature + ) + + # Tube side wall temperature + # FIXME replace with deviation variables + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="tube side wall temperature", + ) + def temp_wall_tube_eqn(b, t, x): + return ( + b.hconv_tube[t, x] + * (tube.properties[t, x].temperature - b.temp_wall_tube[t, x]) + * (b.thickness_tube / 2 / b.therm_cond_wall + b.rfouling_tube) + == b.temp_wall_tube[t, x] - b.temp_wall_center[t, x] + ) + + # Shell side wall temperature + @self.Constraint( + self.flowsheet().config.time, + shell.length_domain, + doc="shell side wall temperature", + ) + def temp_wall_shell_eqn(b, t, x): + return ( + b.hconv_shell_total[t, x] + * (shell.properties[t, x].temperature - b.temp_wall_shell[t, x]) + * (b.thickness_tube / 2 / b.therm_cond_wall + b.rfouling_shell) + == b.temp_wall_shell[t, x] - b.temp_wall_center[t, x] + ) + + # Center point wall temperature based on energy balance for tube wall heat holdup + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="center point wall temperature", + ) + def temp_wall_center_eqn(b, t, x): + return -heat_accumulation_term(b, t, x) == ( + b.heat_shell[t, x] * b.length_flow_shell / b.length_flow_tube + + b.heat_tube[t, x] + ) + + if not self.config.dynamic: + z0 = shell.length_domain.first() + z1 = shell.length_domain.last() + + @self.Expression(self.flowsheet().config.time) + def total_heat_duty(b, t): + if self.config.flow_type == HeatExchangerFlowPattern.cocurrent: + enth_in = shell.properties[t, z0].enth_mol + enth_out = shell.properties[t, z1].enth_mol + else: + enth_out = shell.properties[t, z0].enth_mol + enth_in = shell.properties[t, z1].enth_mol + + return (enth_out - enth_in) * shell.properties[t, z0].flow_mol + + @self.Expression(self.flowsheet().config.time) + def log_mean_delta_temperature(b, t): + dT0 = ( + b.hot_side.properties[t, z0].temperature + - b.cold_side.properties[t, z0].temperature + ) + dT1 = ( + b.hot_side.properties[t, z1].temperature + - b.cold_side.properties[t, z1].temperature + ) + return (dT0 - dT1) / log(dT0 / dT1) + + @self.Expression(self.flowsheet().config.time) + def overall_heat_transfer_coefficient(b, t): + return b.total_heat_duty[t] / ( + b.total_heat_transfer_area * b.log_mean_delta_temperature[t] + ) + + def set_initial_condition(self): + if self.config.dynamic is True: + self.heat_accumulation[:, :].value = 0 + self.heat_accumulation[0, :].fix(0) + # no accumulation term for fluid side models to avoid pressure waves + + def initialize_build( + blk, + shell_state_args=None, + tube_state_args=None, + outlvl=0, + solver="ipopt", + optarg=None, + ): + """ + HeatExchangerCrossFlow1D initialization routine + + 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 = None). + outlvl : sets output level of initialization 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 information (tee=True) + + optarg : solver options dictionary object (default={'tol': 1e-6}) + solver : str indicating which solver to use during + initialization (default = 'ipopt') + + Returns: + None + """ + init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") + solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") + + if optarg is None: + optarg = {} + opt = get_solver(solver, optarg) + + if blk.config.shell_is_hot: + shell = blk.hot_side + tube = blk.cold_side + shell_has_pressure_change = blk.config.hot_side.has_pressure_change + tube_has_pressure_change = blk.config.cold_side.has_pressure_change + else: + shell = blk.cold_side + tube = blk.hot_side + shell_has_pressure_change = blk.config.cold_side.has_pressure_change + tube_has_pressure_change = blk.config.hot_side.has_pressure_change + + # --------------------------------------------------------------------- + # Initialize shell block + + flags_tube = tube.initialize( + outlvl=0, optarg=optarg, solver=solver, state_args=tube_state_args + ) + + flags_shell = shell.initialize( + outlvl=0, optarg=optarg, solver=solver, state_args=shell_state_args + ) + + init_log.info_high("Initialization Step 1 Complete.") + + # Set tube thermal conductivity to a small value to avoid IPOPT unable to solve initially + therm_cond_wall_save = blk.therm_cond_wall.value + blk.therm_cond_wall = 0.05 + # In Step 2, fix tube metal temperatures fix fluid state variables (enthalpy/temperature and pressure) + # calculate maximum heat duty assuming infinite area and use half of the maximum duty as initial guess to calculate outlet temperature + if blk.config.flow_type == HeatExchangerFlowPattern.cocurrent: + mcp_shell = value( + shell.properties[0, 0].flow_mol * shell.properties[0, 0].cp_mol + ) + mcp_tube = value( + tube.properties[0, 0].flow_mol * tube.properties[0, 0].cp_mol + ) + tout_max = ( + mcp_tube * value(tube.properties[0, 0].temperature) + + mcp_shell * value(shell.properties[0, 0].temperature) + ) / (mcp_tube + mcp_shell) + q_guess = ( + mcp_tube + * value(tout_max - value(tube.properties[0, 0].temperature)) + / 2 + ) + temp_out_tube_guess = ( + value(tube.properties[0, 0].temperature) + q_guess / mcp_tube + ) + temp_out_shell_guess = ( + value(shell.properties[0, 0].temperature) - q_guess / mcp_shell + ) + else: + mcp_shell = value( + shell.properties[0, 0].flow_mol * shell.properties[0, 0].cp_mol + ) + mcp_tube = value( + tube.properties[0, 1].flow_mol * tube.properties[0, 1].cp_mol + ) + print("mcp_shell=", mcp_shell) + print("mcp_tube=", mcp_tube) + if mcp_tube < mcp_shell: + q_guess = ( + mcp_tube + * value( + shell.properties[0, 0].temperature + - tube.properties[0, 1].temperature + ) + / 2 + ) + else: + q_guess = ( + mcp_shell + * value( + shell.properties[0, 0].temperature + - tube.properties[0, 1].temperature + ) + / 2 + ) + temp_out_tube_guess = ( + value(tube.properties[0, 1].temperature) + q_guess / mcp_tube + ) + temp_out_shell_guess = ( + value(shell.properties[0, 0].temperature) - q_guess / mcp_shell + ) + + for t in blk.flowsheet().config.time: + for z in tube.length_domain: + if blk.config.flow_type == "co_current": + blk.temp_wall_center[t, z].fix( + value( + 0.5 + * ( + (1 - z) * shell.properties[0, 0].temperature + + z * temp_out_shell_guess + ) + + 0.5 + * ( + (1 - z) * tube.properties[0, 0].temperature + + z * temp_out_tube_guess + ) + ) + ) + else: + blk.temp_wall_center[t, z].fix( + value( + 0.5 + * ( + (1 - z) * shell.properties[0, 0].temperature + + z * temp_out_shell_guess + ) + + 0.5 + * ( + (1 - z) * temp_out_tube_guess + + z * tube.properties[0, 1].temperature + ) + ) + ) + blk.temp_wall_shell[t, z].fix(blk.temp_wall_center[t, z].value) + blk.temp_wall_tube[t, z].fix(blk.temp_wall_center[t, z].value) + blk.temp_wall_shell[t, z].unfix() + blk.temp_wall_tube[t, z].unfix() + + for t in blk.flowsheet().config.time: + for z in tube.length_domain: + tube.properties[t, z].temperature.fix( + value(tube.properties[t, 0].temperature) + ) + if tube_has_pressure_change: + tube.properties[t, z].pressure.fix( + value(tube.properties[t, 0].pressure) + ) + + for t in blk.flowsheet().config.time: + for z in shell.length_domain: + shell.properties[t, z].temperature.fix( + value(shell.properties[t, 0].temperature) + ) + if shell_has_pressure_change: + shell.properties[t, z].pressure.fix( + value(shell.properties[t, 0].pressure) + ) + + blk.temp_wall_center_eqn.deactivate() + if tube_has_pressure_change == True: + blk.deltaP_tube_eqn.deactivate() + if shell_has_pressure_change == True: + blk.deltaP_shell_eqn.deactivate() + blk.heat_tube_eqn.deactivate() + blk.heat_shell_eqn.deactivate() + + # import pdb; pdb.set_trace() + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(blk, tee=slc.tee) + pyomo.opt.assert_optimal_termination(res) + init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res))) + + # In Step 3, unfix fluid state variables (enthalpy/temperature and pressure) + # keep the inlet state variables fixed, otherwise, the degree of freedom > 0 + for t in blk.flowsheet().config.time: + for z in tube.length_domain: + tube.properties[t, z].temperature.unfix() + tube.properties[t, z].pressure.unfix() + if blk.config.flow_type == "co_current": + tube.properties[t, 0].temperature.fix( + value(blk.tube_inlet.temperature[0]) + ) + tube.properties[t, 0].pressure.fix(value(blk.tube_inlet.pressure[0])) + else: + tube.properties[t, 1].temperature.fix( + value(blk.tube_inlet.temperature[0]) + ) + tube.properties[t, 1].pressure.fix(value(blk.tube_inlet.pressure[0])) + + for t in blk.flowsheet().config.time: + for z in shell.length_domain: + shell.properties[t, z].temperature.unfix() + shell.properties[t, z].pressure.unfix() + shell.properties[t, 0].temperature.fix( + value(blk.shell_inlet.temperature[0]) + ) + shell.properties[t, 0].pressure.fix(value(blk.shell_inlet.pressure[0])) + + if tube_has_pressure_change == True: + blk.deltaP_tube_eqn.activate() + if shell_has_pressure_change == True: + blk.deltaP_shell_eqn.activate() + blk.heat_tube_eqn.activate() + blk.heat_shell_eqn.activate() + + # return + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(blk, tee=slc.tee) + pyomo.opt.assert_optimal_termination(res) + + init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) + + blk.temp_wall_center[:, :].unfix() + blk.temp_wall_center_eqn.activate() + + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(blk, tee=slc.tee) + pyomo.opt.assert_optimal_termination(res) + + init_log.info_high("Initialization Step 4 {}.".format(idaeslog.condition(res))) + + # set the wall thermal conductivity back to the user specified value + blk.therm_cond_wall = therm_cond_wall_save + + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(blk, tee=slc.tee) + init_log.info_high("Initialization Step 5 {}.".format(idaeslog.condition(res))) + tube.release_state(flags_tube) + shell.release_state(flags_shell) + init_log.info("Initialization Complete.") + + def calculate_scaling_factors(self): + def gsf(obj): + return iscale.get_scaling_factor(obj, default=1, warning=True) + + def ssf(obj, sf): + iscale.set_scaling_factor(obj, sf, overwrite=False) + + def cst(con, sf): + iscale.constraint_scaling_transform(con, sf, overwrite=False) + + sgsf = iscale.set_and_get_scaling_factor + + if self.config.shell_is_hot: + shell = self.hot_side + tube = self.cold_side + else: + shell = self.cold_side + tube = self.hot_side + + tube_has_pressure_change = hasattr(self, "deltaP_tube") + shell_has_pressure_change = hasattr(self, "deltaP_shell") + + heat_exchanger_common._scale_common( + self, + shell, + shell_has_pressure_change, + make_reynolds=True, + make_nusselt=True + ) + heat_exchanger_common._scale_tube( + self, + tube, + tube_has_pressure_change, + make_reynolds=True, + make_nusselt=True + ) + + for t in self.flowsheet().time: + for z in shell.length_domain: + # FIXME try to do this rigorously later on + sf_area_per_length_tube = 1 / value( + const.pi * self.di_tube * self.nrow_inlet * self.ncol_tube + ) + sf_T_tube = gsf(tube.properties[t, z].temperature) + ssf(self.temp_wall_tube[t, z], sf_T_tube) + cst(self.temp_wall_tube_eqn[t, z], sf_T_tube) + + sf_hconv_tube = gsf(self.hconv_tube[t, z]) + sf_Q_tube = sgsf( + tube.heat[t, z], + sf_hconv_tube * sf_area_per_length_tube * sf_T_tube, + ) + cst(self.heat_tube_eqn[t, z], sf_Q_tube) + + sf_T_shell = gsf(shell.properties[t, z].temperature) + ssf(self.temp_wall_shell[t, z], sf_T_shell) + cst(self.temp_wall_shell_eqn[t, z], sf_T_shell) + + sf_area_per_length_shell = value( + self.length_flow_shell + / ( + self.length_flow_tube + * const.pi + * self.do_tube + * self.nrow_inlet + * self.ncol_tube + ) + ) + sf_hconv_shell_conv = gsf(self.hconv_shell_conv[t, z]) + if self.config.has_radiation: + sf_hconv_shell_rad = 1 # FIXME Placeholder + sf_hconv_shell_total = 1 / ( + 1 / sf_hconv_shell_conv + 1 / sf_hconv_shell_rad + ) + else: + sf_hconv_shell_total = sf_hconv_shell_conv + s_Q_shell = sgsf( + shell.heat[t, z], + sf_hconv_shell_total * sf_area_per_length_shell * sf_T_shell, + ) + cst(self.heat_shell_eqn[t, z], s_Q_shell * value(self.length_flow_shell)) + # Geometric mean is overkill for most reasonable cases, but it mitigates + # damage done when one stream has an unset scaling factor + ssf(self.temp_wall_center[t, z], (sf_T_shell * sf_T_tube)**0.5) + cst(self.temp_wall_center_eqn[t, z], (sf_Q_tube * s_Q_shell) ** 0.5) + + def _get_performance_contents(self, time_point=0): + var_dict = {} + # var_dict = { + # "HX Coefficient": self.overall_heat_transfer_coefficient[time_point] + # } + # var_dict["HX Area"] = self.area + # var_dict["Heat Duty"] = self.heat_duty[time_point] + # if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: + # var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]} + + expr_dict = {} + expr_dict["HX Area"] = self.total_heat_transfer_area + expr_dict["Delta T Driving"] = self.log_mean_delta_temperature[time_point] + expr_dict["Total Heat Duty"] = self.total_heat_duty[time_point] + expr_dict["HX Coefficient"] = self.overall_heat_transfer_coefficient[time_point] + + return {"vars": var_dict, "exprs": expr_dict} + + def _get_stream_table_contents(self, time_point=0): + return create_stream_table_dataframe( + { + "Hot Inlet": self.shell_inlet, + "Hot Outlet": self.shell_outlet, + "Cold Inlet": self.tube_inlet, + "Cold Outlet": self.tube_outlet, + }, + time_point=time_point, + ) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py new file mode 100644 index 0000000000..8f5c6444c8 --- /dev/null +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -0,0 +1,1101 @@ +############################################################################## +# Institute for the Design of Advanced Energy Systems Process Systems +# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018-2019, 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-pse". +############################################################################## +""" +1-D Cross Flow Heat Exchanger Model With Wall Temperatures + +Discretization based on tube rows +""" +from __future__ import division + +# Import Python libraries +import math + +# Import Pyomo libraries +from pyomo.environ import ( + SolverFactory, + Var, + Param, + Constraint, + value, + TerminationCondition, + exp, + sqrt, + log, + sin, + cos, + SolverStatus, +) +from pyomo.common.config import ConfigBlock, ConfigValue, In +from pyomo.util.calc_var_value import calculate_variable_from_constraint + +# Import IDAES cores +from idaes.core import ( + ControlVolume1DBlock, + UnitModelBlockData, + declare_process_block_class, + MaterialBalanceType, + EnergyBalanceType, + MomentumBalanceType, + FlowDirection, + UnitModelBlockData, + useDefault, +) +from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.constants import Constants as const +import idaes.core.util.scaling as iscale +from pyomo.dae import DerivativeVar +from pyomo.environ import units as pyunits +from idaes.core.solvers import get_solver +from idaes.core.util.config import is_physical_parameter_block +from idaes.core.util.misc import add_object_reference +import idaes.logger as idaeslog +from idaes.core.util.tables import create_stream_table_dataframe + +__author__ = "Jinliang Ma, Douglas Allan" + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +def _make_geometry_common(blk, shell_units): + # Number of tube columns in the cross section plane perpendicular to shell side fluid flow (y direction) + blk.ncol_tube = Var( + initialize=10.0, doc="number of tube columns", units=pyunits.dimensionless + ) + + # Number of segments of tube bundles + blk.nseg_tube = Var( + initialize=10.0, doc="number of tube segments", units=pyunits.dimensionless + ) + + # Number of inlet tube rows + blk.nrow_inlet = Var( + initialize=1, doc="number of inlet tube rows", units=pyunits.dimensionless + ) + + # Inner diameter of tubes + blk.di_tube = Var( + initialize=0.05, doc="inner diameter of tube", units=shell_units["length"] + ) + + # Thickness of tube + blk.thickness_tube = Var( + initialize=0.005, doc="tube thickness", units=shell_units["length"] + ) + + # Pitch of tubes between two neighboring columns (in y direction). Always greater than tube outside diameter + blk.pitch_y = Var( + initialize=0.1, + doc="pitch between two neighboring columns", + units=shell_units["length"], + ) + + # Pitch of tubes between two neighboring rows (in x direction). Always greater than tube outside diameter + blk.pitch_x = Var( + initialize=0.1, + doc="pitch between two neighboring rows", + units=shell_units["length"], + ) + + # Length of tube per segment in z direction + blk.length_tube_seg = Var( + initialize=1.0, doc="length of tube per segment", units=shell_units["length"] + ) + + # Minimum cross-sectional area on shell side + blk.area_flow_shell_min = Var( + initialize=1.0, doc="minimum flow area on shell side", units=shell_units["area"] + ) + + # total number of tube rows + @blk.Expression(doc="total number of tube rows") + def nrow_tube(b): + return b.nseg_tube * b.nrow_inlet + + # Tube outside diameter + @blk.Expression(doc="outside diameter of tube") + def do_tube(b): + return b.di_tube + b.thickness_tube * 2.0 + + # Mean beam length for radiation + if blk.config.has_radiation: + + @blk.Expression(doc="mean bean length") + def mbl(b): + return 3.6 * ( + b.pitch_x * b.pitch_y / const.pi / b.do_tube - b.do_tube / 4.0 + ) + + # Mean beam length for radiation divided by sqrt(2) + @blk.Expression(doc="sqrt(1/2) of mean bean length") + def mbl_div2(b): + return b.mbl / sqrt(2.0) + + # Mean beam length for radiation multiplied by sqrt(2) + @blk.Expression(doc="sqrt(2) of mean bean length") + def mbl_mul2(b): + return b.mbl * sqrt(2.0) + + # Ratio of pitch_x/do_tube + @blk.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 + @blk.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 + + # Total cross-sectional area of tube metal per segment + @blk.Expression(doc="total cross section area of tube metal per segment") + def area_wall_seg(b): + return ( + 0.25 + * const.pi + * (b.do_tube**2 - b.di_tube**2) + * b.ncol_tube + * b.nrow_inlet + ) + + # Length of shell side flow + @blk.Constraint(doc="Length of shell side flow") + def length_flow_shell_eqn(b): + return b.length_flow_shell == b.nrow_tube * b.pitch_x + + # Average flow area on shell side + @blk.Constraint(doc="Average cross section area of shell side flow") + def area_flow_shell_eqn(b): + return ( + b.length_flow_shell * b.area_flow_shell + == b.length_tube_seg * b.length_flow_shell * b.pitch_y * b.ncol_tube + - b.ncol_tube + * b.nrow_tube + * 0.25 + * const.pi + * b.do_tube**2 + * b.length_tube_seg + ) + + # Minimum flow area on shell side + @blk.Constraint(doc="Minimum flow area on shell side") + def area_flow_shell_min_eqn(b): + return ( + b.area_flow_shell_min + == b.length_tube_seg * (b.pitch_y - b.do_tube) * b.ncol_tube + ) + + @blk.Expression() + def total_heat_transfer_area(b): + return ( + const.pi + * b.do_tube + * b.nrow_inlet + * b.ncol_tube + * b.nseg_tube + * b.length_tube_seg + ) + + +def _make_geometry_tube(blk, shell_units): + # Elevation difference (outlet - inlet) for static pressure calculation + blk.delta_elevation = Var( + initialize=0.0, + units=shell_units["length"], + doc="Elevation increase used for static pressure calculation", + ) + + # Length of tube side flow + @blk.Constraint(doc="Length of tube side flow") + def length_flow_tube_eqn(b): + return ( + pyunits.convert(b.length_flow_tube, to_units=shell_units["length"]) + == b.nseg_tube * b.length_tube_seg + ) + + # Total flow area on tube side + @blk.Constraint(doc="Total area of tube flow") + def area_flow_tube_eqn(b): + return ( + b.area_flow_tube + == 0.25 * const.pi * b.di_tube**2.0 * b.ncol_tube * b.nrow_inlet + ) + + +def _make_performance_common( + blk, shell, shell_units, shell_has_pressure_change, make_reynolds, make_nusselt +): + # We need the Reynolds number for pressure change, even if we don't need it for heat transfer + if shell_has_pressure_change: + make_reynolds = True + + add_object_reference(blk, "heat_shell", shell.heat) + + if shell_has_pressure_change: + add_object_reference(blk, "deltaP_shell", shell.deltaP) + + # Parameters + if blk.config.has_radiation: + # tube wall emissivity, converted from parameter to variable + # TODO is this dimensionless? + blk.emissivity_wall = Var(initialize=0.7, doc="shell side wall emissivity") + + # Wall thermal conductivity + blk.therm_cond_wall = Param( + initialize=1.0, + mutable=True, + units=shell_units["thermal_conductivity"], + doc="Thermal conductivity of tube wall", + ) + + # Wall heat capacity + blk.cp_wall = Param( + initialize=502.4, + mutable=True, + units=shell_units["heat_capacity_mass"], + doc="Tube wall heat capacity", + ) + + # Wall density + blk.density_wall = Param( + initialize=7800.0, + mutable=True, + units=shell_units["density_mass"], + doc="Tube wall density", + ) + + # Heat transfer resistance due to the fouling on shell side + blk.rfouling_shell = Param( + initialize=0.0001, mutable=True, doc="Fouling resistance on tube side" + ) + + # Correction factor for convective heat transfer coefficient on shell side + blk.fcorrection_htc_shell = Var( + initialize=1.0, doc="Correction factor for convective HTC on shell" + ) + + # Correction factor for shell side pressure drop due to friction + if shell_has_pressure_change: + blk.fcorrection_dp_shell = Var( + initialize=1.0, doc="Correction factor for shell side pressure drop" + ) + + # Performance variables + if blk.config.has_radiation: + # Gas emissivity at mbl + blk.gas_emissivity = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=0.5, + doc="emissivity at given mean beam length", + ) + + # Gas emissivity at mbl/sqrt(2) + blk.gas_emissivity_div2 = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=0.4, + doc="emissivity at mean beam length divided by sqrt of 2", + ) + + # Gas emissivity at mbl*sqrt(2) + blk.gas_emissivity_mul2 = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=0.6, + doc="emissivity at mean beam length multiplied by sqrt of 2", + ) + + # Gray fraction of gas in entire spectrum + blk.gas_gray_fraction = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=0.5, + doc="gray fraction of gas in entire spectrum", + ) + + # Gas-surface radiation exchange factor for shell side wall + blk.frad_gas_shell = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=0.5, + doc="gas-surface radiation exchange factor for shell side wall", + ) + + # Shell side equivalent convective heat transfer coefficient due to radiation + blk.hconv_shell_rad = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=100.0, + bounds=(0, None), + units=shell_units["heat_transfer_coefficient"], + doc="shell side convective heat transfer coefficient due to radiation", + ) + + # Shell side convective heat transfer coefficient due to convection only + blk.hconv_shell_conv = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=100.0, + bounds=(0, None), + units=shell_units["heat_transfer_coefficient"], + doc="shell side convective heat transfer coefficient due to convection", + ) + + # Boundary wall temperature on shell side + blk.temp_wall_shell = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=500, + units=shell_units["temperature"], + doc="boundary wall temperature on shell side", + ) + + # Central wall temperature of tube metal, used to calculate energy contained by tube metal + blk.temp_wall_center = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=500, + units=shell_units["temperature"], + doc="tube wall temperature at center", + ) + + # Tube wall heat holdup per length of shell + if blk.config.has_holdup: + blk.heat_holdup = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=1e6, + units=shell_units["energy"] / shell_units["length"], + doc="tube wall heat holdup per length of shell", + ) + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="heat holdup of tube metal", + ) + def heat_holdup_eqn(b, t, x): + return ( + b.heat_holdup[t, x] + == b.cp_wall + * b.density_wall + * b.area_wall_seg + * pyunits.convert(b.length_flow_tube, to_units=shell_units["length"]) + / b.length_flow_shell + * b.temp_wall_center[t, x] + ) + + # Tube wall heat accumulation term + if blk.config.dynamic: + blk.heat_accumulation = DerivativeVar( + blk.heat_holdup, + initialize=0, + wrt=blk.flowsheet().config.time, + units=shell_units["energy"] / shell_units["length"] / shell_units["time"], + doc="Tube wall heat accumulation per unit length of shell", + ) + + if blk.config.has_radiation: + # TODO Make units consistent for radiation + # Constraints for gas emissivity + @blk.Constraint( + blk.flowsheet().config.time, shell.length_domain, doc="Gas emissivity" + ) + def gas_emissivity_eqn(b, t, x): + X1 = shell.properties[t, x].temperature + X2 = b.mbl + X3 = shell.properties[t, x].pressure + try: + X4 = shell.properties[t, x].mole_frac_comp["CO2"] + except KeyError: + X4 = 0 + try: + X5 = shell.properties[t, x].mole_frac_comp["H2O"] + except KeyError: + X5 = 0 + try: + X6 = shell.properties[t, x].mole_frac_comp["O2"] + except KeyError: + X6 = 0 + return ( + b.gas_emissivity[t, x] + == -0.000116906 * X1 + + 1.02113 * X2 + + 4.81687e-07 * X3 + + 0.922679 * X4 + - 0.0708822 * X5 + - 0.0368321 * X6 + + 0.121843 * log(X1) + + 0.0353343 * log(X2) + + 0.0346181 * log(X3) + + 0.0180859 * log(X5) + - 0.256274 * exp(X2) + - 0.674791 * exp(X4) + - 0.724802 * sin(X2) + - 0.0206726 * cos(X2) + - 9.01012e-05 * cos(X3) + - 3.09283e-05 * X1 * X2 + - 5.44339e-10 * X1 * X3 + - 0.000196134 * X1 * X5 + + 4.54838e-05 * X1 * X6 + + 7.57411e-07 * X2 * X3 + + 0.0395456 * X2 * X4 + + 0.726625 * X2 * X5 + - 0.034842 * X2 * X6 + + 4.00056e-06 * X3 * X5 + + 5.71519e-09 * (X1 * X2) ** 2 + - 1.27853 * (X2 * X5) ** 2 + ) + + # Constraints for gas emissivity at mbl/sqrt(2) + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="Gas emissivity at a lower mean beam length", + ) + def gas_emissivity_div2_eqn(b, t, x): + X1 = shell.properties[t, x].temperature + X2 = b.mbl_div2 + X3 = shell.properties[t, x].pressure + try: + X4 = shell.properties[t, x].mole_frac_comp["CO2"] + except KeyError: + X4 = 0 + try: + X5 = shell.properties[t, x].mole_frac_comp["H2O"] + except KeyError: + X5 = 0 + try: + X6 = shell.properties[t, x].mole_frac_comp["O2"] + except KeyError: + X6 = 0 + return ( + b.gas_emissivity_div2[t, x] + == -0.000116906 * X1 + + 1.02113 * X2 + + 4.81687e-07 * X3 + + 0.922679 * X4 + - 0.0708822 * X5 + - 0.0368321 * X6 + + 0.121843 * log(X1) + + 0.0353343 * log(X2) + + 0.0346181 * log(X3) + + 0.0180859 * log(X5) + - 0.256274 * exp(X2) + - 0.674791 * exp(X4) + - 0.724802 * sin(X2) + - 0.0206726 * cos(X2) + - 9.01012e-05 * cos(X3) + - 3.09283e-05 * X1 * X2 + - 5.44339e-10 * X1 * X3 + - 0.000196134 * X1 * X5 + + 4.54838e-05 * X1 * X6 + + 7.57411e-07 * X2 * X3 + + 0.0395456 * X2 * X4 + + 0.726625 * X2 * X5 + - 0.034842 * X2 * X6 + + 4.00056e-06 * X3 * X5 + + 5.71519e-09 * (X1 * X2) ** 2 + - 1.27853 * (X2 * X5) ** 2 + ) + + # Constraints for gas emissivity at mbl*sqrt(2) + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="Gas emissivity at a higher mean beam length", + ) + def gas_emissivity_mul2_eqn(b, t, x): + X1 = shell.properties[t, x].temperature + X2 = b.mbl_mul2 + X3 = shell.properties[t, x].pressure + try: + X4 = shell.properties[t, x].mole_frac_comp["CO2"] + except KeyError: + X4 = 0 + try: + X5 = shell.properties[t, x].mole_frac_comp["H2O"] + except KeyError: + X5 = 0 + try: + X6 = shell.properties[t, x].mole_frac_comp["O2"] + except KeyError: + X6 = 0 + return ( + b.gas_emissivity_mul2[t, x] + == -0.000116906 * X1 + + 1.02113 * X2 + + 4.81687e-07 * X3 + + 0.922679 * X4 + - 0.0708822 * X5 + - 0.0368321 * X6 + + 0.121843 * log(X1) + + 0.0353343 * log(X2) + + 0.0346181 * log(X3) + + 0.0180859 * log(X5) + - 0.256274 * exp(X2) + - 0.674791 * exp(X4) + - 0.724802 * sin(X2) + - 0.0206726 * cos(X2) + - 9.01012e-05 * cos(X3) + - 3.09283e-05 * X1 * X2 + - 5.44339e-10 * X1 * X3 + - 0.000196134 * X1 * X5 + + 4.54838e-05 * X1 * X6 + + 7.57411e-07 * X2 * X3 + + 0.0395456 * X2 * X4 + + 0.726625 * X2 * X5 + - 0.034842 * X2 * X6 + + 4.00056e-06 * X3 * X5 + + 5.71519e-09 * (X1 * X2) ** 2 + - 1.27853 * (X2 * X5) ** 2 + ) + + # fraction of gray gas spectrum + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="fraction of gray gas spectrum", + ) + def gas_gray_fraction_eqn(b, t, x): + return ( + b.gas_gray_fraction[t, x] + * (2 * b.gas_emissivity_div2[t, x] - b.gas_emissivity_mul2[t, x]) + == b.gas_emissivity_div2[t, x] ** 2 + ) + + # gas-surface radiation exchange factor between gas and shell side wall + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="gas-surface radiation exchange factor between gas and shell side wall", + ) + def frad_gas_shell_eqn(b, t, x): + return ( + b.frad_gas_shell[t, x] + * ( + (1 / b.emissivity_wall - 1) * b.gas_emissivity[t, x] + + b.gas_gray_fraction[t, x] + ) + == b.gas_gray_fraction[t, x] * b.gas_emissivity[t, x] + ) + + # equivalent convective heat transfer coefficient due to radiation + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="equivalent convective heat transfer coefficient due to radiation", + ) + def hconv_shell_rad_eqn(b, t, x): + return b.hconv_shell_rad[t, x] == ( + pyunits.convert( + const.stefan_constant, + to_units=shell_units["power"] + / shell_units["length"] ** 2 + / shell_units["temperature"] ** 4, + ) + * b.frad_gas_shell[t, x] + * (shell.properties[t, x].temperature + b.temp_wall_shell[t, x]) + * ( + shell.properties[t, x].temperature ** 2 + + b.temp_wall_shell[t, x] ** 2 + ) + ) + + # Pressure drop and heat transfer coefficient on shell side + # ---------------------------------------------------------- + # Tube arrangement factor + if blk.config.tube_arrangement == "in-line": + blk.f_arrangement = Param( + initialize=0.788, doc="in-line tube arrangement factor" + ) + elif blk.config.tube_arrangement == "staggered": + blk.f_arrangement = Param( + initialize=1.0, doc="staggered tube arrangement factor" + ) + else: + raise ConfigurationError() + + if make_reynolds: + # Velocity on shell side + blk.v_shell = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=1.0, + units=shell_units["velocity"], + doc="velocity on shell side", + ) + + # Reynalds number on shell side + blk.N_Re_shell = Var( + blk.flowsheet().config.time, + shell.length_domain, + bounds=(1e-7, None), + initialize=10000.0, + units=pyunits.dimensionless, + doc="Reynolds number on shell side", + ) + + if shell_has_pressure_change: + # Friction factor on shell side + # TODO does this have units? + blk.friction_factor_shell = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=1.0, + doc="friction factor on shell side", + ) + + if make_nusselt: + # Nusselt number on shell side + blk.N_Nu_shell = Var( + blk.flowsheet().config.time, + shell.length_domain, + initialize=1, + units=pyunits.dimensionless, + doc="Nusselts number on shell side", + bounds=(1e-7, None), + ) + + if make_reynolds: + # Velocity equation on shell side + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="velocity on shell side", + ) + def v_shell_eqn(b, t, x): + return ( + b.v_shell[t, x] + * shell.properties[t, x].dens_mol_phase["Vap"] + * b.area_flow_shell_min + == shell.properties[t, x].flow_mol + ) + + # Reynolds number + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="Reynolds number equation on shell side", + ) + def N_Re_shell_eqn(b, t, x): + return ( + b.N_Re_shell[t, x] * shell.properties[t, x].visc_d_phase["Vap"] + == b.do_tube + * b.v_shell[t, x] + * shell.properties[t, x].dens_mol_phase["Vap"] + * shell.properties[t, x].mw + ) + # return b.N_Re_shell[t,x] * shell.properties[t,x].visc_d * b.area_flow_shell_min == \ + # b.do_tube * shell.properties[t,x].dens_mol_phase["Vap"]**2 * sum( + # shell.properties[t,x].flow_mol_comp[j]*shell.properties[t,x].mw_comp[j] + # for j in shell.properties[t,x].component_list) + + if shell_has_pressure_change == True: + # Friction factor on shell side + if blk.config.tube_arrangement == "in-line": + + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="in-line friction factor on shell side", + ) + def friction_factor_shell_eqn(b, t, x): + return ( + b.friction_factor_shell[t, x] * b.N_Re_shell[t, x] ** 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 blk.config.tube_arrangement == "staggered": + + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="staggered friction factor on shell side", + ) + def friction_factor_shell_eqn(b, t, x): + return ( + b.friction_factor_shell[t, x] * b.N_Re_shell[t, x] ** 0.16 + == (0.25 + 0.118 / (b.pitch_y_to_do - 1.0) ** 1.08) + * b.fcorrection_dp_shell + ) + + else: + raise ConfigurationError() + + # Pressure drop on shell side + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="pressure change on shell side", + ) + def deltaP_shell_eqn(b, t, x): + return ( + b.deltaP_shell[t, x] * b.pitch_x + == -1.4 + * b.friction_factor_shell[t, x] + * shell.properties[t, x].dens_mol_phase["Vap"] + * shell.properties[t, x].mw + * b.v_shell[t, x] ** 2 + ) + + if make_nusselt: + # The actual Nusselt number correlation needs to be made by the particular heat exchanger + @blk.Constraint( + blk.flowsheet().config.time, + shell.length_domain, + doc="Convective heat transfer coefficient equation on shell side due to convection", + ) + def hconv_shell_conv_eqn(b, t, x): + return ( + b.hconv_shell_conv[t, x] * b.do_tube + == b.N_Nu_shell[t, x] + * shell.properties[t, x].therm_cond_phase["Vap"] + * b.fcorrection_htc_shell + ) + + # Total convective heat transfer coefficient on shell side + @blk.Expression( + blk.flowsheet().config.time, + shell.length_domain, + doc="Total convective heat transfer coefficient on shell side", + ) + def hconv_shell_total(b, t, x): + if blk.config.has_radiation: + return b.hconv_shell_conv[t, x] + b.hconv_shell_rad[t, x] + else: + return b.hconv_shell_conv[t, x] + + +def _make_performance_tube( + blk, tube, tube_units, tube_has_pressure_change, make_reynolds, make_nusselt +): + + # Need Reynolds number for pressure drop, even if we don't need it for heat transfer + if tube_has_pressure_change: + make_reynolds = True + + blk.hconv_tube = Var( + blk.flowsheet().config.time, + tube.length_domain, + initialize=100.0, + doc="tube side convective heat transfer coefficient", + ) + + # Loss coefficient for a 180 degree bend (u-turn), usually related to radius to inside diameter ratio + blk.kloss_uturn = Param( + initialize=0.5, mutable=True, doc="loss coefficient of a tube u-turn" + ) + + # Heat transfer resistance due to the fouling on tube side + blk.rfouling_tube = Param( + initialize=0.0, mutable=True, doc="fouling resistance on tube side" + ) + # Correction factor for convective heat transfer coefficient on tube side + blk.fcorrection_htc_tube = Var( + initialize=1.0, doc="correction factor for convective HTC on tube side" + ) + # Correction factor for tube side pressure drop due to friction + if tube_has_pressure_change: + blk.fcorrection_dp_tube = Var( + initialize=1.0, doc="correction factor for tube side pressure drop" + ) + + # Boundary wall temperature on tube side + blk.temp_wall_tube = Var( + blk.flowsheet().config.time, + tube.length_domain, + initialize=500, + units=tube_units["temperature"], + doc="boundary wall temperature on tube side", + ) + if make_reynolds: + # Tube side heat transfer coefficient and pressure drop + # ----------------------------------------------------- + # Velocity on tube side + blk.v_tube = Var( + blk.flowsheet().config.time, + tube.length_domain, + initialize=1.0, + units=tube_units["velocity"], + doc="velocity on tube side", + ) + + # Reynalds number on tube side + blk.N_Re_tube = Var( + blk.flowsheet().config.time, + tube.length_domain, + initialize=10000.0, + units=pyunits.dimensionless, + doc="Reynolds number on tube side", + bounds=(1e-7, None), + ) + if tube_has_pressure_change == True: + # Friction factor on tube side + # TODO does this have units? + blk.friction_factor_tube = Var( + blk.flowsheet().config.time, + tube.length_domain, + initialize=1.0, + doc="friction factor on tube side", + ) + + # Pressure drop due to friction on tube side + blk.deltaP_tube_friction = Var( + blk.flowsheet().config.time, + tube.length_domain, + initialize=-10.0, + units=tube_units["pressure"], + doc="pressure drop due to friction on tube side", + ) + + # Pressure drop due to 180 degree turn on tube side + blk.deltaP_tube_uturn = Var( + blk.flowsheet().config.time, + tube.length_domain, + initialize=-10.0, + units=tube_units["pressure"], + doc="pressure drop due to u-turn on tube side", + ) + if make_nusselt: + # Nusselt number on tube side + blk.N_Nu_tube = Var( + blk.flowsheet().config.time, + tube.length_domain, + initialize=1, + units=pyunits.dimensionless, + doc="Nusselts number on tube side", + bounds=(1e-7, None), + ) + + if make_reynolds: + # Velocity equation + @blk.Constraint( + blk.flowsheet().config.time, + tube.length_domain, + doc="tube side velocity equation", + ) + def v_tube_eqn(b, t, x): + return ( + b.v_tube[t, x] + * pyunits.convert(b.area_flow_tube, to_units=tube_units["area"]) + * tube.properties[t, x].dens_mol_phase["Vap"] + == tube.properties[t, x].flow_mol + ) + + # Reynolds number + @blk.Constraint( + blk.flowsheet().config.time, + tube.length_domain, + doc="Reynolds number equation on tube side", + ) + def N_Re_tube_eqn(b, t, x): + return ( + b.N_Re_tube[t, x] * tube.properties[t, x].visc_d_phase["Vap"] + == pyunits.convert(b.di_tube, to_units=tube_units["length"]) + * b.v_tube[t, x] + * tube.properties[t, x].dens_mol_phase["Vap"] + * tube.properties[t, x].mw + ) + # return b.N_Re_tube[t,x] * tube.properties[t,x].visc_d * b.area_flow_tube == \ + # b.di_tube * tube.properties[t,x].dens_mol_phase["Vap"]**2 * sum( + # tube.properties[t,x].flow_mol_comp[j]*tube.properties[t,x].mw_comp[j] + # for j in tube.properties[t,x].component_list) + + if tube_has_pressure_change: + # Friction factor + @blk.Constraint( + blk.flowsheet().config.time, + tube.length_domain, + doc="Darcy friction factor on tube side", + ) + def friction_factor_tube_eqn(b, t, x): + return ( + b.friction_factor_tube[t, x] * b.N_Re_tube[t, x] ** 0.25 + == 0.3164 * b.fcorrection_dp_tube + ) + + # Pressure drop due to friction per tube length + @blk.Constraint( + blk.flowsheet().config.time, + tube.length_domain, + doc="pressure drop due to friction per tube length", + ) + def deltaP_tube_friction_eqn(b, t, x): + return ( + b.deltaP_tube_friction[t, x] + * pyunits.convert(b.di_tube, to_units=tube_units["length"]) + == -0.5 + * tube.properties[t, x].dens_mol_phase["Vap"] + * tube.properties[t, x].mw + * b.v_tube[t, x] ** 2 + * b.friction_factor_tube[t, x] + ) + + # Pressure drop due to u-turn + @blk.Constraint( + blk.flowsheet().config.time, + tube.length_domain, + doc="pressure drop due to u-turn on tube side", + ) + def deltaP_tube_uturn_eqn(b, t, x): + return ( + b.deltaP_tube_uturn[t, x] + * pyunits.convert(b.length_tube_seg, to_units=tube_units["length"]) + == -0.5 + * tube.properties[t, x].dens_mol_phase["Vap"] + * tube.properties[t, x].mw + * b.v_tube[t, x] ** 2 + * b.kloss_uturn + ) + + # Total pressure drop on tube side + @blk.Constraint( + blk.flowsheet().config.time, + tube.length_domain, + doc="total pressure drop on tube side", + ) + def deltaP_tube_eqn(b, t, x): + return b.deltaP_tube[t, x] == ( + b.deltaP_tube_friction[t, x] + + b.deltaP_tube_uturn[t, x] + - pyunits.convert(b.delta_elevation, to_units=tube_units["length"]) + / b.nseg_tube + * pyunits.convert( + const.acceleration_gravity, to_units=tube_units["acceleration"] + ) + * tube.properties[t, x].dens_mol_phase["Vap"] + * tube.properties[t, x].mw + / pyunits.convert(b.length_tube_seg, to_units=tube_units["length"]) + ) + + if make_nusselt: + @blk.Constraint( + blk.flowsheet().config.time, + tube.length_domain, + doc="convective heat transfer coefficient equation on tube side", + ) + def hconv_tube_eqn(b, t, x): + return ( + b.hconv_tube[t, x] * b.di_tube + == b.N_Nu_tube[t, x] + * tube.properties[t, x].therm_cond_phase["Vap"] + * b.fcorrection_htc_tube + ) +def _scale_common(blk, shell, shell_has_pressure_change, make_reynolds, make_nusselt): + def gsf(obj): + return iscale.get_scaling_factor(obj, default=1, warning=True) + def ssf(obj, sf): + iscale.set_scaling_factor(obj, sf, overwrite=False) + def cst(con, sf): + iscale.constraint_scaling_transform(con, sf, overwrite=False) + sgsf = iscale.set_and_get_scaling_factor + + sf_do_tube = iscale.get_scaling_factor( + blk.do_tube, default=1 / value(blk.do_tube) + ) + + sf_di_tube = iscale.get_scaling_factor( + blk.do_tube, default=1 / value(blk.di_tube) + ) + calculate_variable_from_constraint( + blk.area_flow_shell_min, + blk.area_flow_shell_min_eqn + ) + sf_area_flow_shell_min = iscale.get_scaling_factor( + blk.area_flow_shell_min, default=1/value(blk.area_flow_shell_min) + ) + for t in blk.flowsheet().time: + for z in shell.length_domain: + sf_flow_mol_shell = gsf(shell.properties[t, z].flow_mol) + + if make_reynolds: + # FIXME get better scaling later + ssf(blk.v_shell[t, z], 1/10) + cst(blk.v_shell_eqn[t, z], sf_flow_mol_shell) + + #FIXME should get scaling of N_Re from defining eqn + sf_N_Re_shell = sgsf(blk.N_Re_shell[t, z], 1e-4) + + sf_visc_d_shell = gsf(shell.properties[t, z].visc_d_phase["Vap"]) + cst(blk.N_Re_shell_eqn[t, z], sf_N_Re_shell * sf_visc_d_shell) + if make_nusselt: + sf_k_shell = gsf(shell.properties[t, z].therm_cond_phase["Vap"]) + + sf_N_Nu_shell = sgsf( + blk.N_Nu_shell[t, z], 1 / 0.33 * sf_N_Re_shell ** 0.6 + ) + cst(blk.N_Nu_shell_eqn[t, z], sf_N_Nu_shell) + + sf_hconv_shell_conv = sgsf( + blk.hconv_shell_conv[t, z], sf_N_Nu_shell * sf_k_shell / sf_do_tube + ) + cst(blk.hconv_shell_conv_eqn[t, z], sf_hconv_shell_conv * sf_do_tube) + + + + # FIXME estimate from parameters + if blk.config.has_holdup: + s_U_holdup = gsf(blk.heat_holdup[t, z]) + cst(blk.heat_holdup_eqn[t, z], s_U_holdup) + +def _scale_tube(blk, tube, tube_has_presure_change, make_reynolds, make_nusselt): + def gsf(obj): + return iscale.get_scaling_factor(obj, default=1, warning=True) + + def ssf(obj, sf): + iscale.set_scaling_factor(obj, sf, overwrite=False) + + def cst(con, sf): + iscale.constraint_scaling_transform(con, sf, overwrite=False) + + sgsf = iscale.set_and_get_scaling_factor + + sf_di_tube = iscale.get_scaling_factor( + blk.do_tube, default=1 / value(blk.di_tube) + ) + sf_do_tube = iscale.get_scaling_factor( + blk.do_tube, default=1 / value(blk.do_tube) + ) + + for t in blk.flowsheet().time: + for z in tube.length_domain: + if make_reynolds: + # FIXME get better scaling later + ssf(blk.v_tube[t, z], 1/20) + sf_flow_mol_tube = gsf(tube.properties[t, z].flow_mol) + + cst(blk.v_tube_eqn[t, z], sf_flow_mol_tube) + + # FIXME should get scaling of N_Re from defining eqn + sf_N_Re_tube = sgsf(blk.N_Re_tube[t, z], 1e-4) + + sf_visc_d_tube = gsf(tube.properties[t, z].visc_d_phase["Vap"]) + cst(blk.N_Re_tube_eqn[t, z], sf_N_Re_tube * sf_visc_d_tube) + if make_nusselt: + sf_k_tube = gsf(tube.properties[t, z].therm_cond_phase["Vap"]) + + sf_N_Nu_tube = sgsf( + blk.N_Nu_tube[t, z], 1 / 0.023 * sf_N_Re_tube ** 0.8 + ) + cst(blk.N_Nu_tube_eqn[t, z], sf_N_Nu_tube) + + sf_hconv_tube = sgsf( + blk.hconv_tube[t, z], sf_N_Nu_tube * sf_k_tube / sf_di_tube + ) + cst(blk.hconv_tube_eqn[t, z], sf_hconv_tube * sf_di_tube) \ No newline at end of file diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py new file mode 100644 index 0000000000..2e791d032d --- /dev/null +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -0,0 +1,624 @@ +############################################################################## +# Institute for the Design of Advanced Energy Systems Process Systems +# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018-2019, 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-pse". +############################################################################## +""" +1-D Cross Flow Heat Exchanger Model With Wall Temperatures + +Discretization based on tube rows +""" +from __future__ import division + +# Import Python libraries +import math + +# Import Pyomo libraries +from pyomo.environ import ( + SolverFactory, + Var, + Param, + Constraint, + value, + TerminationCondition, + exp, + sqrt, + log, + sin, + cos, + SolverStatus, + units as pyunits, +) +from pyomo.common.config import ConfigBlock, ConfigValue, In + +# Import IDAES cores +from idaes.core import ( + ControlVolume1DBlock, + UnitModelBlockData, + declare_process_block_class, + MaterialBalanceType, + EnergyBalanceType, + MomentumBalanceType, + FlowDirection, + UnitModelBlockData, + useDefault, +) +from idaes.core.util.constants import Constants as const +import idaes.core.util.scaling as iscale +from pyomo.dae import DerivativeVar +from idaes.core.solvers import get_solver +from pyomo.util.calc_var_value import calculate_variable_from_constraint +from idaes.core.util.config import is_physical_parameter_block +from idaes.core.util.misc import add_object_reference +import idaes.logger as idaeslog +from idaes.core.util.tables import create_stream_table_dataframe +from idaes.core.util.model_statistics import degrees_of_freedom + +from heat_exchanger_common import _make_geometry_common, _make_performance_common, _scale_common + +__author__ = "Jinliang Ma, Douglas Allan" + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("Heater1D") +class Heater1DData(UnitModelBlockData): + """Standard Heat Exchanger Cross Flow Unit Model Class.""" + + # Template for config arguments for shell and tube side + _SideTemplate = ConfigBlock() + _SideTemplate.declare( + "dynamic", + ConfigValue( + default=useDefault, + domain=In([useDefault, True, False]), + 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=False, + domain=In([True, False]), + description="Holdup construction flag", + doc="""Indicates whether holdup terms should be constructed or not. +Must be True if dynamic = True, +**default** - False. +**Valid values:** { +**True** - construct holdup terms, +**False** - do not construct holdup terms}""", + ), + ) + _SideTemplate.declare( + "has_fluid_holdup", + ConfigValue( + default=False, + domain=In([True, False]), + description="Holdup construction flag", + doc="""Indicates whether holdup terms for the fluid should be constructed or not. + **default** - False. + **Valid values:** { + **True** - construct holdup terms, + **False** - do not construct holdup terms}""", + ), + ) + _SideTemplate.declare( + "material_balance_type", + ConfigValue( + default=MaterialBalanceType.componentTotal, + domain=In(MaterialBalanceType), + description="Material balance construction flag", + doc="""Indicates what type of mass balance should be constructed, +**default** - MaterialBalanceType.componentTotal. +**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.}""", + ), + ) + _SideTemplate.declare( + "energy_balance_type", + ConfigValue( + default=EnergyBalanceType.enthalpyTotal, + domain=In(EnergyBalanceType), + description="Energy balance construction flag", + doc="""Indicates what type of energy balance should be constructed, +**default** - EnergyBalanceType.enthalpyTotal. +**Valid values:** { +**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=In([True, False]), + 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=In([True, False]), + description="Phase equilibrium term construction flag", + doc="""Argument to enable phase equilibrium on the shell side. +- True - include phase equilibrium term +- False - do not include phase equilibrium term""", + ), + ) + _SideTemplate.declare( + "property_package", + ConfigValue( + 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=None, + description="Arguments for constructing shell 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 : discretization 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 transforming domain. See Pyomo +documentation for supported schemes.""", + ), + ) + + CONFIG = _SideTemplate + + # Common config args for both sides + CONFIG.declare( + "finite_elements", + ConfigValue( + default=5, + domain=int, + description="Number of finite elements length domain", + doc="""Number of finite elements to use when discretizing length +domain (default=5). Should set to the number of tube rows""", + ), + ) + CONFIG.declare( + "collocation_points", + ConfigValue( + default=3, + 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( + "tube_arrangement", + ConfigValue( + default="in-line", + domain=In(["in-line", "staggered"]), + description="tube configuration", + doc="tube arrangement could be in-line or staggered", + ), + ) + CONFIG.declare( + "has_radiation", + ConfigValue( + default=False, + domain=In([False, True]), + description="Has side 2 gas radiation", + doc="define if shell side gas radiation is to be considered", + ), + ) + + def build(self): + """ + Begin building model (pre-DAE transformation). + + Args: + None + + Returns: + None + """ + # Call UnitModel.build to setup dynamics + super(Heater1DData, self).build() + + # Set flow directions for the control volume blocks and specify + # dicretization if not specified. + set_direction_shell = FlowDirection.forward + if self.config.transformation_method is useDefault: + self.config.transformation_method = "dae.finite_difference" + if self.config.transformation_scheme is useDefault: + self.config.transformation_scheme = "FORWARD" + + if self.config.property_package_args is None: + self.config.property_package_args = {} + + # Control volume 1D for shell, set to steady-state for fluid + self.control_volume = ControlVolume1DBlock( + dynamic=self.config.dynamic and self.config.has_fluid_holdup, + has_holdup=self.config.has_fluid_holdup, + property_package=self.config.property_package, + property_package_args=self.config.property_package_args, + transformation_method=self.config.transformation_method, + transformation_scheme=self.config.transformation_scheme, + finite_elements=self.config.finite_elements, + collocation_points=self.config.collocation_points, + ) + + self.control_volume.add_geometry(flow_direction=set_direction_shell) + + self.control_volume.add_state_blocks( + information_flow=set_direction_shell, + has_phase_equilibrium=self.config.has_phase_equilibrium, + ) + + # Populate shell + self.control_volume.add_material_balances( + balance_type=self.config.material_balance_type, + has_phase_equilibrium=self.config.has_phase_equilibrium, + ) + + self.control_volume.add_energy_balances( + balance_type=self.config.energy_balance_type, has_heat_transfer=True + ) + + self.control_volume.add_momentum_balances( + balance_type=self.config.momentum_balance_type, + has_pressure_change=self.config.has_pressure_change, + ) + + self.control_volume.apply_transformation() + + # Populate tube + + # Add Ports for shell side + self.add_inlet_port(name="inlet", block=self.control_volume) + self.add_outlet_port(name="outlet", block=self.control_volume) + + self._make_geometry() + + self._make_performance() + + def _make_geometry(self): + """ + Constraints for unit model. + + Args: + None + + Returns: + None + """ + units = self.config.property_package.get_metadata().derived_units + # Add reference to control volume geometry + add_object_reference(self, "area_flow_shell", self.control_volume.area) + add_object_reference(self, "length_flow_shell", self.control_volume.length) + _make_geometry_common(self, shell_units=units) + @self.Expression( + doc="Common performance equations expect this expression to be here" + ) + def length_flow_tube(b): + return b.nseg_tube * b.length_tube_seg + + + def _make_performance(self): + """ + Constraints for unit model. + + Args: + None + + Returns: + None + """ + self.electric_heat_duty = Var( + self.flowsheet().config.time, + initialize=1e6, + units=pyunits.W, + doc="Heat duty provided to heater " "through resistive heating", + ) + units = self.config.property_package.get_metadata().derived_units + _make_performance_common( + self, + shell=self.control_volume, + shell_units=units, + shell_has_pressure_change=self.config.has_pressure_change, + make_reynolds=True, + make_nusselt=True, + ) + + def heat_accumulation_term(b, t, x): + return b.heat_accumulation[t, x] if b.config.dynamic else 0 + + # Nusselt number, currently assume Re>300 + @self.Constraint( + self.flowsheet().config.time, + self.control_volume.length_domain, + doc="Nusselts number equation", + ) + def N_Nu_shell_eqn(b, t, x): + return ( + b.N_Nu_shell[t, x] + == b.f_arrangement + * 0.33 + * b.N_Re_shell[t, x] ** 0.6 + * b.control_volume.properties[t, x].prandtl_number_phase["Vap"] + ** 0.333333 + ) + + # Energy balance with tube wall + # ------------------------------------ + # Heat to wall per length + @self.Constraint( + self.flowsheet().config.time, + self.control_volume.length_domain, + doc="heat per length", + ) + def heat_shell_eqn(b, t, x): + return b.control_volume.heat[t, x] * b.length_flow_shell == ( + b.hconv_shell_total[t, x] + * b.total_heat_transfer_area + * (b.temp_wall_shell[t, x] - b.control_volume.properties[t, x].temperature) + ) + + # Shell side wall temperature + @self.Constraint( + self.flowsheet().config.time, + self.control_volume.length_domain, + doc="shell side wall temperature", + ) + def temp_wall_shell_eqn(b, t, x): + return ( + b.hconv_shell_total[t, x] + * (b.control_volume.properties[t, x].temperature - b.temp_wall_shell[t, x]) + * (b.thickness_tube / b.therm_cond_wall + b.rfouling_shell) + == b.temp_wall_shell[t, x] - b.temp_wall_center[t, x] + ) + + @self.Constraint( + self.flowsheet().config.time, + self.control_volume.length_domain, + doc="wall temperature", + ) + def temp_wall_center_eqn(b, t, x): + return heat_accumulation_term(b, t, x) == ( + -b.control_volume.heat[t, x] + b.electric_heat_duty[t] / b.length_flow_shell + ) + + def set_initial_condition(self): + if self.config.dynamic is True: + self.heat_accumulation[:, :].value = 0 + self.heat_accumulation[0, :].fix(0) + # no accumulation term for fluid side models to avoid pressure waves + + def initialize_build(blk, state_args=None, outlvl=0, solver="ipopt", optarg=None): + """ + HeatExchangerCrossFlow1D initialization routine + + 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 = None). + outlvl : sets output level of initialization 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 information (tee=True) + + optarg : solver options dictionary object (default={'tol': 1e-6}) + solver : str indicating which solver to use during + initialization (default = 'ipopt') + + Returns: + None + """ + init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") + solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") + + if optarg is None: + optarg = {} + opt = get_solver(solver, optarg) + + # --------------------------------------------------------------------- + # Initialize shell block + + flags = blk.control_volume.initialize( + outlvl=0, optarg=optarg, solver=solver, state_args=state_args + ) + + init_log.info_high("Initialization Step 1 Complete.") + + # mcp = value(blk.control_volume.properties[0,0].flow_mol * blk.control_volume.properties[0,0].cp_mol) + # tout_guess = value(blk.tube.properties[0,0].temperature) + value(blk.electric_heat_duty[0]/blk.length_flow) + calc_var = calculate_variable_from_constraint + + calc_var(blk.length_flow_shell, blk.length_flow_shell_eqn) + calc_var(blk.area_flow_shell, blk.area_flow_shell_eqn) + calc_var(blk.area_flow_shell_min, blk.area_flow_shell_min_eqn) + + for t in blk.flowsheet().config.time: + for x in blk.control_volume.length_domain: + blk.control_volume.heat[t, x].fix( + value(blk.electric_heat_duty[t] / blk.length_flow_shell) + ) + blk.control_volume.length.fix() + assert degrees_of_freedom(blk.control_volume) == 0 + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(blk.control_volume, tee=slc.tee) + + assert res.solver.termination_condition == TerminationCondition.optimal + assert res.solver.status == SolverStatus.ok + + init_log.info_high("Initialization Step 2 Complete.") + blk.control_volume.length.unfix() + blk.control_volume.heat.unfix() + + for t in blk.flowsheet().config.time: + for x in blk.control_volume.length_domain: + blk.temp_wall_center[t, x].fix( + value(blk.control_volume.properties[t, x].temperature) + 10 + ) + calc_var(blk.heat_holdup[t, x], blk.heat_holdup_eqn[t, x]) + blk.temp_wall_center[t, x].unfix() + + # fixed = blk.control_volume.temperature[t,x].fixed + # blk.control_volume.temperature[t,x].fix() + # calc_var() + + assert degrees_of_freedom(blk) == 0 + + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = opt.solve(blk, tee=slc.tee) + + assert res.solver.termination_condition == TerminationCondition.optimal + assert res.solver.status == SolverStatus.ok + + init_log.info_high("Initialization Step 3 Complete.") + + blk.control_volume.release_state(flags) + + def calculate_scaling_factors(self): + def gsf(obj): + return iscale.get_scaling_factor(obj, default=1, warning=True) + + def ssf(obj, sf): + iscale.set_scaling_factor(obj, sf, overwrite=False) + + def cst(con, sf): + iscale.constraint_scaling_transform(con, sf, overwrite=False) + + sgsf = iscale.set_and_get_scaling_factor + + _scale_common( + self, + self.control_volume, + self.config.has_pressure_change, + make_reynolds=True, + make_nusselt=True + ) + + sf_d_tube = iscale.get_scaling_factor( + self.do_tube, default=1 / value(self.do_tube) + ) + + for t in self.flowsheet().time: + for z in self.control_volume.length_domain: + sf_hconv_conv = gsf(self.hconv_shell_conv[t, z]) + cst(self.hconv_shell_conv_eqn[t, z], sf_hconv_conv * sf_d_tube) + + if self.config.has_radiation: + sf_hconv_rad = 1 # FIXME Placeholder + sf_hconv_total = 1 / (1 / sf_hconv_conv + 1 / sf_hconv_rad) + else: + sf_hconv_total = sf_hconv_conv + + # FIXME try to do this rigorously later on + sf_T = gsf(self.control_volume.properties[t, z].temperature) + ssf(self.temp_wall_shell[t, z], sf_T) + ssf(self.temp_wall_center[t, z], sf_T) + + sf_area_per_length = value( + self.length_flow_shell / self.total_heat_transfer_area + ) + s_Q = sgsf( + self.control_volume.heat[t, z], + sf_hconv_total * sf_area_per_length * sf_T, + ) + ssf(self.electric_heat_duty[t], s_Q / value(self.length_flow_shell)) + cst(self.heat_shell_eqn[t, z], s_Q * value(self.length_flow_shell)) + ssf(self.temp_wall_center[t, z], sf_T) + cst(self.temp_wall_shell_eqn[t, z], sf_T) + cst(self.temp_wall_center_eqn[t, z], s_Q) + + def _get_performance_contents(self, time_point=0): + var_dict = {} + # var_dict = { + # "HX Coefficient": self.overall_heat_transfer_coefficient[time_point] + # } + # var_dict["HX Area"] = self.area + # var_dict["Heat Duty"] = self.heat_duty[time_point] + # if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: + # var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]} + + expr_dict = {} + expr_dict["HX Area"] = self.total_heat_transfer_area + expr_dict["Electric Heat Duty"] = self.electric_heat_duty[time_point] + + return {"vars": var_dict, "exprs": expr_dict} + + def _get_stream_table_contents(self, time_point=0): + return create_stream_table_dataframe( + { + "Inlet": self.inlet, + "Outlet": self.outlet, + }, + time_point=time_point, + ) From db315527fe4c0899ec38ab77624215c48d080cec Mon Sep 17 00:00:00 2001 From: Doug A Date: Thu, 22 Feb 2024 15:58:05 -0500 Subject: [PATCH 02/38] edits --- .../unit_models/heat_exchanger_common.py | 320 ++---------------- 1 file changed, 19 insertions(+), 301 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 8f5c6444c8..8ae88497bb 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -70,94 +70,75 @@ def _make_geometry_common(blk, shell_units): # Number of tube columns in the cross section plane perpendicular to shell side fluid flow (y direction) blk.ncol_tube = Var( - initialize=10.0, doc="number of tube columns", units=pyunits.dimensionless + initialize=10.0, doc="Number of tube columns", units=pyunits.dimensionless ) # Number of segments of tube bundles blk.nseg_tube = Var( - initialize=10.0, doc="number of tube segments", units=pyunits.dimensionless + initialize=10.0, doc="Number of segments of tube bundles", units=pyunits.dimensionless ) # Number of inlet tube rows blk.nrow_inlet = Var( - initialize=1, doc="number of inlet tube rows", units=pyunits.dimensionless + initialize=1, doc="Number of inlet tube rows", units=pyunits.dimensionless ) # Inner diameter of tubes blk.di_tube = Var( - initialize=0.05, doc="inner diameter of tube", units=shell_units["length"] + initialize=0.05, doc="Inner diameter of tube", units=shell_units["length"] ) # Thickness of tube blk.thickness_tube = Var( - initialize=0.005, doc="tube thickness", units=shell_units["length"] + initialize=0.005, doc="Tube thickness", units=shell_units["length"] ) # Pitch of tubes between two neighboring columns (in y direction). Always greater than tube outside diameter blk.pitch_y = Var( initialize=0.1, - doc="pitch between two neighboring columns", + doc="Pitch between two neighboring columns", units=shell_units["length"], ) # Pitch of tubes between two neighboring rows (in x direction). Always greater than tube outside diameter blk.pitch_x = Var( initialize=0.1, - doc="pitch between two neighboring rows", + doc="Pitch between two neighboring rows", units=shell_units["length"], ) # Length of tube per segment in z direction blk.length_tube_seg = Var( - initialize=1.0, doc="length of tube per segment", units=shell_units["length"] + initialize=1.0, doc="Length of tube per segment", units=shell_units["length"] ) # Minimum cross-sectional area on shell side blk.area_flow_shell_min = Var( - initialize=1.0, doc="minimum flow area on shell side", units=shell_units["area"] + initialize=1.0, doc="Minimum flow area on shell side", units=shell_units["area"] ) # total number of tube rows - @blk.Expression(doc="total number of tube rows") + @blk.Expression(doc="Total number of tube rows") def nrow_tube(b): return b.nseg_tube * b.nrow_inlet # Tube outside diameter - @blk.Expression(doc="outside diameter of tube") + @blk.Expression(doc="Outside diameter of tube") def do_tube(b): return b.di_tube + b.thickness_tube * 2.0 - # Mean beam length for radiation - if blk.config.has_radiation: - - @blk.Expression(doc="mean bean length") - def mbl(b): - return 3.6 * ( - b.pitch_x * b.pitch_y / const.pi / b.do_tube - b.do_tube / 4.0 - ) - - # Mean beam length for radiation divided by sqrt(2) - @blk.Expression(doc="sqrt(1/2) of mean bean length") - def mbl_div2(b): - return b.mbl / sqrt(2.0) - - # Mean beam length for radiation multiplied by sqrt(2) - @blk.Expression(doc="sqrt(2) of mean bean length") - def mbl_mul2(b): - return b.mbl * sqrt(2.0) - # Ratio of pitch_x/do_tube - @blk.Expression(doc="ratio of pitch in x direction to tube outside diameter") + @blk.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 - @blk.Expression(doc="ratio of pitch in y direction to tube outside diameter") + @blk.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 # Total cross-sectional area of tube metal per segment - @blk.Expression(doc="total cross section area of tube metal per segment") + @blk.Expression(doc="Total cross section area of tube metal per segment") def area_wall_seg(b): return ( 0.25 @@ -244,11 +225,6 @@ def _make_performance_common( add_object_reference(blk, "deltaP_shell", shell.deltaP) # Parameters - if blk.config.has_radiation: - # tube wall emissivity, converted from parameter to variable - # TODO is this dimensionless? - blk.emissivity_wall = Var(initialize=0.7, doc="shell side wall emissivity") - # Wall thermal conductivity blk.therm_cond_wall = Param( initialize=1.0, @@ -290,57 +266,6 @@ def _make_performance_common( ) # Performance variables - if blk.config.has_radiation: - # Gas emissivity at mbl - blk.gas_emissivity = Var( - blk.flowsheet().config.time, - shell.length_domain, - initialize=0.5, - doc="emissivity at given mean beam length", - ) - - # Gas emissivity at mbl/sqrt(2) - blk.gas_emissivity_div2 = Var( - blk.flowsheet().config.time, - shell.length_domain, - initialize=0.4, - doc="emissivity at mean beam length divided by sqrt of 2", - ) - - # Gas emissivity at mbl*sqrt(2) - blk.gas_emissivity_mul2 = Var( - blk.flowsheet().config.time, - shell.length_domain, - initialize=0.6, - doc="emissivity at mean beam length multiplied by sqrt of 2", - ) - - # Gray fraction of gas in entire spectrum - blk.gas_gray_fraction = Var( - blk.flowsheet().config.time, - shell.length_domain, - initialize=0.5, - doc="gray fraction of gas in entire spectrum", - ) - - # Gas-surface radiation exchange factor for shell side wall - blk.frad_gas_shell = Var( - blk.flowsheet().config.time, - shell.length_domain, - initialize=0.5, - doc="gas-surface radiation exchange factor for shell side wall", - ) - - # Shell side equivalent convective heat transfer coefficient due to radiation - blk.hconv_shell_rad = Var( - blk.flowsheet().config.time, - shell.length_domain, - initialize=100.0, - bounds=(0, None), - units=shell_units["heat_transfer_coefficient"], - doc="shell side convective heat transfer coefficient due to radiation", - ) - # Shell side convective heat transfer coefficient due to convection only blk.hconv_shell_conv = Var( blk.flowsheet().config.time, @@ -348,7 +273,7 @@ def _make_performance_common( initialize=100.0, bounds=(0, None), units=shell_units["heat_transfer_coefficient"], - doc="shell side convective heat transfer coefficient due to convection", + doc="Shell side convective heat transfer coefficient due to convection", ) # Boundary wall temperature on shell side @@ -357,7 +282,7 @@ def _make_performance_common( shell.length_domain, initialize=500, units=shell_units["temperature"], - doc="boundary wall temperature on shell side", + doc="Boundary wall temperature on shell side", ) # Central wall temperature of tube metal, used to calculate energy contained by tube metal @@ -366,7 +291,7 @@ def _make_performance_common( shell.length_domain, initialize=500, units=shell_units["temperature"], - doc="tube wall temperature at center", + doc="Tube wall temperature at center", ) # Tube wall heat holdup per length of shell @@ -376,12 +301,12 @@ def _make_performance_common( shell.length_domain, initialize=1e6, units=shell_units["energy"] / shell_units["length"], - doc="tube wall heat holdup per length of shell", + doc="Tube wall heat holdup per length of shell", ) @blk.Constraint( blk.flowsheet().config.time, shell.length_domain, - doc="heat holdup of tube metal", + doc="Heat holdup of tube metal", ) def heat_holdup_eqn(b, t, x): return ( @@ -404,213 +329,6 @@ def heat_holdup_eqn(b, t, x): doc="Tube wall heat accumulation per unit length of shell", ) - if blk.config.has_radiation: - # TODO Make units consistent for radiation - # Constraints for gas emissivity - @blk.Constraint( - blk.flowsheet().config.time, shell.length_domain, doc="Gas emissivity" - ) - def gas_emissivity_eqn(b, t, x): - X1 = shell.properties[t, x].temperature - X2 = b.mbl - X3 = shell.properties[t, x].pressure - try: - X4 = shell.properties[t, x].mole_frac_comp["CO2"] - except KeyError: - X4 = 0 - try: - X5 = shell.properties[t, x].mole_frac_comp["H2O"] - except KeyError: - X5 = 0 - try: - X6 = shell.properties[t, x].mole_frac_comp["O2"] - except KeyError: - X6 = 0 - return ( - b.gas_emissivity[t, x] - == -0.000116906 * X1 - + 1.02113 * X2 - + 4.81687e-07 * X3 - + 0.922679 * X4 - - 0.0708822 * X5 - - 0.0368321 * X6 - + 0.121843 * log(X1) - + 0.0353343 * log(X2) - + 0.0346181 * log(X3) - + 0.0180859 * log(X5) - - 0.256274 * exp(X2) - - 0.674791 * exp(X4) - - 0.724802 * sin(X2) - - 0.0206726 * cos(X2) - - 9.01012e-05 * cos(X3) - - 3.09283e-05 * X1 * X2 - - 5.44339e-10 * X1 * X3 - - 0.000196134 * X1 * X5 - + 4.54838e-05 * X1 * X6 - + 7.57411e-07 * X2 * X3 - + 0.0395456 * X2 * X4 - + 0.726625 * X2 * X5 - - 0.034842 * X2 * X6 - + 4.00056e-06 * X3 * X5 - + 5.71519e-09 * (X1 * X2) ** 2 - - 1.27853 * (X2 * X5) ** 2 - ) - - # Constraints for gas emissivity at mbl/sqrt(2) - @blk.Constraint( - blk.flowsheet().config.time, - shell.length_domain, - doc="Gas emissivity at a lower mean beam length", - ) - def gas_emissivity_div2_eqn(b, t, x): - X1 = shell.properties[t, x].temperature - X2 = b.mbl_div2 - X3 = shell.properties[t, x].pressure - try: - X4 = shell.properties[t, x].mole_frac_comp["CO2"] - except KeyError: - X4 = 0 - try: - X5 = shell.properties[t, x].mole_frac_comp["H2O"] - except KeyError: - X5 = 0 - try: - X6 = shell.properties[t, x].mole_frac_comp["O2"] - except KeyError: - X6 = 0 - return ( - b.gas_emissivity_div2[t, x] - == -0.000116906 * X1 - + 1.02113 * X2 - + 4.81687e-07 * X3 - + 0.922679 * X4 - - 0.0708822 * X5 - - 0.0368321 * X6 - + 0.121843 * log(X1) - + 0.0353343 * log(X2) - + 0.0346181 * log(X3) - + 0.0180859 * log(X5) - - 0.256274 * exp(X2) - - 0.674791 * exp(X4) - - 0.724802 * sin(X2) - - 0.0206726 * cos(X2) - - 9.01012e-05 * cos(X3) - - 3.09283e-05 * X1 * X2 - - 5.44339e-10 * X1 * X3 - - 0.000196134 * X1 * X5 - + 4.54838e-05 * X1 * X6 - + 7.57411e-07 * X2 * X3 - + 0.0395456 * X2 * X4 - + 0.726625 * X2 * X5 - - 0.034842 * X2 * X6 - + 4.00056e-06 * X3 * X5 - + 5.71519e-09 * (X1 * X2) ** 2 - - 1.27853 * (X2 * X5) ** 2 - ) - - # Constraints for gas emissivity at mbl*sqrt(2) - @blk.Constraint( - blk.flowsheet().config.time, - shell.length_domain, - doc="Gas emissivity at a higher mean beam length", - ) - def gas_emissivity_mul2_eqn(b, t, x): - X1 = shell.properties[t, x].temperature - X2 = b.mbl_mul2 - X3 = shell.properties[t, x].pressure - try: - X4 = shell.properties[t, x].mole_frac_comp["CO2"] - except KeyError: - X4 = 0 - try: - X5 = shell.properties[t, x].mole_frac_comp["H2O"] - except KeyError: - X5 = 0 - try: - X6 = shell.properties[t, x].mole_frac_comp["O2"] - except KeyError: - X6 = 0 - return ( - b.gas_emissivity_mul2[t, x] - == -0.000116906 * X1 - + 1.02113 * X2 - + 4.81687e-07 * X3 - + 0.922679 * X4 - - 0.0708822 * X5 - - 0.0368321 * X6 - + 0.121843 * log(X1) - + 0.0353343 * log(X2) - + 0.0346181 * log(X3) - + 0.0180859 * log(X5) - - 0.256274 * exp(X2) - - 0.674791 * exp(X4) - - 0.724802 * sin(X2) - - 0.0206726 * cos(X2) - - 9.01012e-05 * cos(X3) - - 3.09283e-05 * X1 * X2 - - 5.44339e-10 * X1 * X3 - - 0.000196134 * X1 * X5 - + 4.54838e-05 * X1 * X6 - + 7.57411e-07 * X2 * X3 - + 0.0395456 * X2 * X4 - + 0.726625 * X2 * X5 - - 0.034842 * X2 * X6 - + 4.00056e-06 * X3 * X5 - + 5.71519e-09 * (X1 * X2) ** 2 - - 1.27853 * (X2 * X5) ** 2 - ) - - # fraction of gray gas spectrum - @blk.Constraint( - blk.flowsheet().config.time, - shell.length_domain, - doc="fraction of gray gas spectrum", - ) - def gas_gray_fraction_eqn(b, t, x): - return ( - b.gas_gray_fraction[t, x] - * (2 * b.gas_emissivity_div2[t, x] - b.gas_emissivity_mul2[t, x]) - == b.gas_emissivity_div2[t, x] ** 2 - ) - - # gas-surface radiation exchange factor between gas and shell side wall - @blk.Constraint( - blk.flowsheet().config.time, - shell.length_domain, - doc="gas-surface radiation exchange factor between gas and shell side wall", - ) - def frad_gas_shell_eqn(b, t, x): - return ( - b.frad_gas_shell[t, x] - * ( - (1 / b.emissivity_wall - 1) * b.gas_emissivity[t, x] - + b.gas_gray_fraction[t, x] - ) - == b.gas_gray_fraction[t, x] * b.gas_emissivity[t, x] - ) - - # equivalent convective heat transfer coefficient due to radiation - @blk.Constraint( - blk.flowsheet().config.time, - shell.length_domain, - doc="equivalent convective heat transfer coefficient due to radiation", - ) - def hconv_shell_rad_eqn(b, t, x): - return b.hconv_shell_rad[t, x] == ( - pyunits.convert( - const.stefan_constant, - to_units=shell_units["power"] - / shell_units["length"] ** 2 - / shell_units["temperature"] ** 4, - ) - * b.frad_gas_shell[t, x] - * (shell.properties[t, x].temperature + b.temp_wall_shell[t, x]) - * ( - shell.properties[t, x].temperature ** 2 - + b.temp_wall_shell[t, x] ** 2 - ) - ) - # Pressure drop and heat transfer coefficient on shell side # ---------------------------------------------------------- # Tube arrangement factor From cde84b749f8f35ec150019d6e5ee973457497a91 Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 8 Mar 2024 12:34:46 -0500 Subject: [PATCH 03/38] changes --- .../unit_models/heat_exchanger_1D_cross_flow.py | 15 ++------------- .../unit_models/heat_exchanger_common.py | 9 --------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py index 24e7355d90..4fd34cb073 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py @@ -696,11 +696,7 @@ def cst(con, sf): ssf(self.temp_wall_tube[t, z], sf_T_tube) cst(self.temp_wall_tube_eqn[t, z], sf_T_tube) - sf_hconv_tube = gsf(self.hconv_tube[t, z]) - sf_Q_tube = sgsf( - tube.heat[t, z], - sf_hconv_tube * sf_area_per_length_tube * sf_T_tube, - ) + sf_Q_tube = gsf(tube.heat[t, z]) cst(self.heat_tube_eqn[t, z], sf_Q_tube) sf_T_shell = gsf(shell.properties[t, z].temperature) @@ -718,16 +714,9 @@ def cst(con, sf): ) ) sf_hconv_shell_conv = gsf(self.hconv_shell_conv[t, z]) - if self.config.has_radiation: - sf_hconv_shell_rad = 1 # FIXME Placeholder - sf_hconv_shell_total = 1 / ( - 1 / sf_hconv_shell_conv + 1 / sf_hconv_shell_rad - ) - else: - sf_hconv_shell_total = sf_hconv_shell_conv s_Q_shell = sgsf( shell.heat[t, z], - sf_hconv_shell_total * sf_area_per_length_shell * sf_T_shell, + sf_hconv_shell_conv * sf_area_per_length_shell * sf_T_shell, ) cst(self.heat_shell_eqn[t, z], s_Q_shell * value(self.length_flow_shell)) # Geometric mean is overkill for most reasonable cases, but it mitigates diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 8ae88497bb..f920149a4d 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -413,10 +413,6 @@ def N_Re_shell_eqn(b, t, x): * shell.properties[t, x].dens_mol_phase["Vap"] * shell.properties[t, x].mw ) - # return b.N_Re_shell[t,x] * shell.properties[t,x].visc_d * b.area_flow_shell_min == \ - # b.do_tube * shell.properties[t,x].dens_mol_phase["Vap"]**2 * sum( - # shell.properties[t,x].flow_mol_comp[j]*shell.properties[t,x].mw_comp[j] - # for j in shell.properties[t,x].component_list) if shell_has_pressure_change == True: # Friction factor on shell side @@ -630,10 +626,6 @@ def N_Re_tube_eqn(b, t, x): * tube.properties[t, x].dens_mol_phase["Vap"] * tube.properties[t, x].mw ) - # return b.N_Re_tube[t,x] * tube.properties[t,x].visc_d * b.area_flow_tube == \ - # b.di_tube * tube.properties[t,x].dens_mol_phase["Vap"]**2 * sum( - # tube.properties[t,x].flow_mol_comp[j]*tube.properties[t,x].mw_comp[j] - # for j in tube.properties[t,x].component_list) if tube_has_pressure_change: # Friction factor @@ -766,7 +758,6 @@ def cst(con, sf): cst(blk.hconv_shell_conv_eqn[t, z], sf_hconv_shell_conv * sf_do_tube) - # FIXME estimate from parameters if blk.config.has_holdup: s_U_holdup = gsf(blk.heat_holdup[t, z]) From 1bc0ea71e1c04b6d2138032c671c8d6233b90823 Mon Sep 17 00:00:00 2001 From: Doug A Date: Tue, 12 Mar 2024 13:30:53 -0400 Subject: [PATCH 04/38] Code cleaning --- idaes/models/unit_models/heat_exchanger_1D.py | 97 +++--- .../heat_exchanger_1D_cross_flow.py | 278 +++++++++--------- 2 files changed, 194 insertions(+), 181 deletions(-) diff --git a/idaes/models/unit_models/heat_exchanger_1D.py b/idaes/models/unit_models/heat_exchanger_1D.py index aa059aee74..31fb1d19a2 100644 --- a/idaes/models/unit_models/heat_exchanger_1D.py +++ b/idaes/models/unit_models/heat_exchanger_1D.py @@ -430,39 +430,63 @@ def build(self): # Set flow directions for the control volume blocks and specify # discretization 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 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_method + != self.config.cold_side.transformation_method + ): + raise ConfigurationError( + "HeatExchanger1D only supports similar transformation " + "methods on the hot and cold side domains for " + "both cocurrent and countercurrent flow patterns. " + f"Found method {self.config.hot_side.transformation_method} " + f"on hot side and method {self.config.cold_side.transformation_method} " + "on cold side." + ) + if self.config.hot_side.transformation_method == "dae.collocation": + if ( + self.config.hot_side.transformation_scheme is useDefault + or self.config.cold_side.transformation_scheme is useDefault + ): + raise ConfigurationError( + "If a collocation method is used for HeatExchanger1D, the user " + "must specify the transformation scheme they want to use." + ) 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( + "If a collocation method is used, " "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." + "schemes on the hot and cold side domains. Found " + f"{self.config.hot_side.transformation_scheme} scheme on " + f"hot side and {self.config.cold_side.transformation_scheme} " + "scheme on cold side." ) - self.config.cold_side.transformation_method = "dae.finite_difference" + + + if self.config.flow_type == HeatExchangerFlowPattern.cocurrent: + set_direction_hot = FlowDirection.forward + set_direction_cold = FlowDirection.forward if self.config.hot_side.transformation_scheme is useDefault: _log.warning( "Discretization scheme was " @@ -484,24 +508,6 @@ def build(self): 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 " @@ -519,7 +525,7 @@ def build(self): "Defaulting to forward finite " "difference on the cold side." ) - self.config.cold_side.transformation_scheme = "BACKWARD" + self.config.cold_side.transformation_scheme = "FORWARD" else: raise ConfigurationError( "{} HeatExchanger1D only supports cocurrent and " @@ -776,6 +782,7 @@ def initialize_build( cold_side_units = ( self.cold_side.config.property_package.get_metadata().get_derived_units ) + # TODO What if there is more than one time point? What if t0 != 0? if duty is None: duty = value( 0.25 diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py index 4fd34cb073..06ded01249 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py @@ -59,6 +59,7 @@ from idaes.core.solvers import get_solver from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.misc import add_object_reference +from idaes.core.util.exceptions import ConfigurationError, BurntToast import idaes.logger as idaeslog from idaes.core.util.tables import create_stream_table_dataframe @@ -217,6 +218,29 @@ def _make_performance(self): tube_units = ( self.config.hot_side.property_package.get_metadata().derived_units ) + if ( + len(self.config.hot_side.property_package.phase_list) != 1 + or len(self.config.cold_side.property_package.phase_list) != 1 + ): + raise ConfigurationError( + "The HeatExchangerCrossFlow1D model is valid only for property packages " + f"with a single phase. Found {len(self.config.hot_side.property_package.phase_list)} " + f"phases on the hot side and {len(self.config.cold_side.property_package.phase_list)} " + "phases on the cold side." + ) + elif not self.config.hot_side.property_package.phase_list[0].is_vapor_phase(): + raise ConfigurationError( + "The HeatExchangerCrossFlow1D model is valid only for property packages " + "whose single phase is a vapor phase. The hot side phase is not a vapor phase." + ) + elif not self.config.cold_side.property_package.phase_list[0].is_vapor_phase(): + raise ConfigurationError( + "The HeatExchangerCrossFlow1D model is valid only for property packages " + "whose single phase is a vapor phase. The cold side phase is not a vapor phase." + ) + else: + p_hot = self.config.hot_side.property_package.phase_list[0] + p_cold = self.config.cold_side.property_package.phase_list[0] # Reference add_object_reference(self, "heat_tube", tube.heat) add_object_reference(self, "heat_shell", shell.heat) @@ -353,6 +377,9 @@ def temp_wall_shell_eqn(b, t, x): doc="center point wall temperature", ) def temp_wall_center_eqn(b, t, x): + # heat_shell and heat_tube are positive when heat flows into those + # control volumes (and out of the wall), hence the negative sign + # on heat_accumulation_term return -heat_accumulation_term(b, t, x) == ( b.heat_shell[t, x] * b.length_flow_shell / b.length_flow_tube + b.heat_tube[t, x] @@ -364,14 +391,10 @@ def temp_wall_center_eqn(b, t, x): @self.Expression(self.flowsheet().config.time) def total_heat_duty(b, t): - if self.config.flow_type == HeatExchangerFlowPattern.cocurrent: - enth_in = shell.properties[t, z0].enth_mol - enth_out = shell.properties[t, z1].enth_mol - else: - enth_out = shell.properties[t, z0].enth_mol - enth_in = shell.properties[t, z1].enth_mol + enth_in = b.hot_side.properties[t, z0].get_enthalpy_flow_terms(p_hot) + enth_out = b.hot_side.properties[t, z1].get_enthalpy_flow_terms(p_hot) - return (enth_out - enth_in) * shell.properties[t, z0].flow_mol + return enth_out - enth_in @self.Expression(self.flowsheet().config.time) def log_mean_delta_temperature(b, t): @@ -401,7 +424,7 @@ def initialize_build( blk, shell_state_args=None, tube_state_args=None, - outlvl=0, + outlvl=idaeslog.NOTSET, solver="ipopt", optarg=None, ): @@ -434,6 +457,20 @@ def initialize_build( optarg = {} opt = get_solver(solver, optarg) + hot_side = blk.hot_side + cold_side = blk.cold_side + + if not ( + "temperature" in blk.config.hot_side.property_package.define_state_vars().keys() + and "temperature" in blk.config.cold_side.property_package.define_state_vars().keys() + ): + raise NotImplementedError( + "Presently, initialization of the HeatExchangerCrossFlow1D requires " + "temperature to be a state variable of both hot side and cold side " + "property packages. Extension to enth_mol or enth_mass as state variables " + "is straightforward---feel free to open a pull request implementing it." + ) + if blk.config.shell_is_hot: shell = blk.hot_side tube = blk.cold_side @@ -449,11 +486,11 @@ def initialize_build( # Initialize shell block flags_tube = tube.initialize( - outlvl=0, optarg=optarg, solver=solver, state_args=tube_state_args + outlvl=outlvl, optarg=optarg, solver=solver, state_args=tube_state_args ) flags_shell = shell.initialize( - outlvl=0, optarg=optarg, solver=solver, state_args=shell_state_args + outlvl=outlvl, optarg=optarg, solver=solver, state_args=shell_state_args ) init_log.info_high("Initialization Step 1 Complete.") @@ -463,118 +500,93 @@ def initialize_build( blk.therm_cond_wall = 0.05 # In Step 2, fix tube metal temperatures fix fluid state variables (enthalpy/temperature and pressure) # calculate maximum heat duty assuming infinite area and use half of the maximum duty as initial guess to calculate outlet temperature - if blk.config.flow_type == HeatExchangerFlowPattern.cocurrent: - mcp_shell = value( - shell.properties[0, 0].flow_mol * shell.properties[0, 0].cp_mol - ) - mcp_tube = value( - tube.properties[0, 0].flow_mol * tube.properties[0, 0].cp_mol - ) - tout_max = ( - mcp_tube * value(tube.properties[0, 0].temperature) - + mcp_shell * value(shell.properties[0, 0].temperature) - ) / (mcp_tube + mcp_shell) - q_guess = ( - mcp_tube - * value(tout_max - value(tube.properties[0, 0].temperature)) - / 2 - ) - temp_out_tube_guess = ( - value(tube.properties[0, 0].temperature) + q_guess / mcp_tube - ) - temp_out_shell_guess = ( - value(shell.properties[0, 0].temperature) - q_guess / mcp_shell - ) - else: - mcp_shell = value( - shell.properties[0, 0].flow_mol * shell.properties[0, 0].cp_mol - ) - mcp_tube = value( - tube.properties[0, 1].flow_mol * tube.properties[0, 1].cp_mol + + for t in blk.flowsheet().config.time: + # TODO we first access cp during initialization. That could pose a problem if it is + # converted to a Var-Constraint pair instead of being a giant Expression like it is + # presently. + mcp_hot_side = value( + hot_side.properties[t, 0].flow_mol * hot_side.properties[t, 0].cp_mol ) - print("mcp_shell=", mcp_shell) - print("mcp_tube=", mcp_tube) - if mcp_tube < mcp_shell: - q_guess = ( - mcp_tube - * value( - shell.properties[0, 0].temperature - - tube.properties[0, 1].temperature - ) - / 2 + T_in_hot_side = value(hot_side.properties[t, 0].temperature) + P_in_hot_side = value(hot_side.properties[t, 0].pressure) + if blk.config.flow_type == HeatExchangerFlowPattern.cocurrent: + mcp_cold_side = value( + cold_side.properties[t, 0].flow_mol * cold_side.properties[t, 0].cp_mol ) - else: - q_guess = ( - mcp_shell - * value( - shell.properties[0, 0].temperature - - tube.properties[0, 1].temperature - ) - / 2 + T_in_cold_side = value(cold_side.properties[t, 0].temperature) + P_in_cold_side = value(cold_side.properties[t, 0].pressure) + + T_out_max = ( + mcp_cold_side * T_in_cold_side + + mcp_hot_side * T_in_hot_side + ) / (mcp_cold_side + mcp_hot_side) + + q_guess = mcp_cold_side * (T_out_max - T_in_cold_side) / 2 + + temp_out_cold_side_guess = ( + T_in_cold_side + q_guess / mcp_cold_side ) - temp_out_tube_guess = ( - value(tube.properties[0, 1].temperature) + q_guess / mcp_tube - ) - temp_out_shell_guess = ( - value(shell.properties[0, 0].temperature) - q_guess / mcp_shell - ) + cold_side.properties[t, 1].temperature.fix(temp_out_cold_side_guess) - for t in blk.flowsheet().config.time: - for z in tube.length_domain: - if blk.config.flow_type == "co_current": - blk.temp_wall_center[t, z].fix( - value( - 0.5 - * ( - (1 - z) * shell.properties[0, 0].temperature - + z * temp_out_shell_guess - ) - + 0.5 - * ( - (1 - z) * tube.properties[0, 0].temperature - + z * temp_out_tube_guess - ) - ) - ) + temp_out_hot_side_guess = ( + T_in_cold_side - q_guess / mcp_hot_side + ) + hot_side.properties[t, 1].temperature.fix(temp_out_hot_side_guess) + + elif blk.config.flow_type == HeatExchangerFlowPattern.countercurrent: + mcp_cold_side = value( + cold_side.properties[t, 1].flow_mol * cold_side.properties[t, 1].cp_mol + ) + T_in_cold_side = value(cold_side.properties[t, 1].temperature) + P_in_cold_side = value(cold_side.properties[t, 1].pressure) + + if mcp_cold_side < mcp_hot_side: + q_guess = mcp_cold_side * (T_in_hot_side - T_in_cold_side) / 2 else: - blk.temp_wall_center[t, z].fix( - value( - 0.5 - * ( - (1 - z) * shell.properties[0, 0].temperature - + z * temp_out_shell_guess - ) - + 0.5 - * ( - (1 - z) * temp_out_tube_guess - + z * tube.properties[0, 1].temperature - ) - ) - ) - blk.temp_wall_shell[t, z].fix(blk.temp_wall_center[t, z].value) - blk.temp_wall_tube[t, z].fix(blk.temp_wall_center[t, z].value) - blk.temp_wall_shell[t, z].unfix() - blk.temp_wall_tube[t, z].unfix() + q_guess = mcp_hot_side * (T_in_hot_side - T_in_cold_side) / 2 + + temp_out_cold_side_guess = ( + T_in_cold_side + q_guess / mcp_cold_side + ) + cold_side.properties[t, 0].temperature.fix(temp_out_cold_side_guess) - for t in blk.flowsheet().config.time: - for z in tube.length_domain: - tube.properties[t, z].temperature.fix( - value(tube.properties[t, 0].temperature) + temp_out_hot_side_guess = ( + T_in_hot_side - q_guess / mcp_hot_side ) - if tube_has_pressure_change: - tube.properties[t, z].pressure.fix( - value(tube.properties[t, 0].pressure) - ) + hot_side.properties[t, 1].temperature.fix(temp_out_hot_side_guess) - for t in blk.flowsheet().config.time: - for z in shell.length_domain: - shell.properties[t, z].temperature.fix( - value(shell.properties[t, 0].temperature) + else: + raise BurntToast( + "HeatExchangerFlowPattern should be limited to cocurrent " + "or countercurrent flow by parent model. Please open an " + "issue on the IDAES Github so this error can be fixed." + ) + + for z in cold_side.length_domain: + hot_side.temperature[t, z].fix( + value( + (1 - z) * hot_side.properties[t, 0].temperature + + z * hot_side.properties[t, 1].temperature + ) ) - if shell_has_pressure_change: - shell.properties[t, z].pressure.fix( - value(shell.properties[t, 0].pressure) + cold_side.temperature[t, z].fix( + value( + (1 - z) * cold_side.properties[t, 0].temperature + + z * cold_side.properties[t, 1].temperature ) + ) + blk.temp_wall_center[t, z].fix( + value(hot_side.temperature[t, z] + cold_side.temperature[t, z]) / 2 + ) + + blk.temp_wall_hot_side[t, z].set_value(blk.temp_wall_center[t, z].value) + blk.temp_wall_cold_side[t, z].set_value(blk.temp_wall_center[t, z].value) + + if blk.config.cold_side.has_pressure_change: + cold_side[t, z].pressure.fix(P_in_cold_side) + if blk.config.hot_side.has_pressure_change: + hot_side[t, z].pressure.fix(P_in_hot_side) blk.temp_wall_center_eqn.deactivate() if tube_has_pressure_change == True: @@ -584,7 +596,6 @@ def initialize_build( blk.heat_tube_eqn.deactivate() blk.heat_shell_eqn.deactivate() - # import pdb; pdb.set_trace() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) pyomo.opt.assert_optimal_termination(res) @@ -592,29 +603,25 @@ def initialize_build( # In Step 3, unfix fluid state variables (enthalpy/temperature and pressure) # keep the inlet state variables fixed, otherwise, the degree of freedom > 0 - for t in blk.flowsheet().config.time: - for z in tube.length_domain: - tube.properties[t, z].temperature.unfix() - tube.properties[t, z].pressure.unfix() - if blk.config.flow_type == "co_current": - tube.properties[t, 0].temperature.fix( - value(blk.tube_inlet.temperature[0]) - ) - tube.properties[t, 0].pressure.fix(value(blk.tube_inlet.pressure[0])) - else: - tube.properties[t, 1].temperature.fix( - value(blk.tube_inlet.temperature[0]) - ) - tube.properties[t, 1].pressure.fix(value(blk.tube_inlet.pressure[0])) + hot_side.properties.temperature.unfix() + hot_side.properties.pressure.unfix() + hot_side.properties.temperature[:, 0].fix() + hot_side.properties.pressure[:, 0].fix() - for t in blk.flowsheet().config.time: - for z in shell.length_domain: - shell.properties[t, z].temperature.unfix() - shell.properties[t, z].pressure.unfix() - shell.properties[t, 0].temperature.fix( - value(blk.shell_inlet.temperature[0]) + cold_side.properties.temperature.unfix() + cold_side.properties.pressure.unfix() + if blk.config.flow_type == HeatExchangerFlowPattern.cocurrent: + cold_side.properties[:, 0].temperature.fix() + cold_side.properties[:, 0].pressure.fix() + elif blk.config.flow_type == HeatExchangerFlowPattern.countercurrent: + cold_side.properties[:, 1].temperature.fix() + cold_side.properties[:, 1].pressure.fix() + else: + raise BurntToast( + "HeatExchangerFlowPattern should be limited to cocurrent " + "or countercurrent flow by parent model. Please open an " + "issue on the IDAES Github so this error can be fixed." ) - shell.properties[t, 0].pressure.fix(value(blk.shell_inlet.pressure[0])) if tube_has_pressure_change == True: blk.deltaP_tube_eqn.activate() @@ -623,14 +630,13 @@ def initialize_build( blk.heat_tube_eqn.activate() blk.heat_shell_eqn.activate() - # return with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) pyomo.opt.assert_optimal_termination(res) init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) - blk.temp_wall_center[:, :].unfix() + blk.temp_wall_center.unfix() blk.temp_wall_center_eqn.activate() with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: From 65b3863e7e7469277e8e6944e4da709316ec2977 Mon Sep 17 00:00:00 2001 From: Doug A Date: Tue, 12 Mar 2024 14:03:51 -0400 Subject: [PATCH 05/38] Fix initialization --- .../heat_exchanger_1D_cross_flow.py | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py index 06ded01249..cce0c1b0a5 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py @@ -228,19 +228,22 @@ def _make_performance(self): f"phases on the hot side and {len(self.config.cold_side.property_package.phase_list)} " "phases on the cold side." ) - elif not self.config.hot_side.property_package.phase_list[0].is_vapor_phase(): + + p_hot = self.config.hot_side.property_package.phase_list.at(1) + pobj_hot = self.config.hot_side.property_package.get_phase(p_hot) + p_cold = self.config.cold_side.property_package.phase_list.at(1) + pobj_cold = self.config.cold_side.property_package.get_phase(p_cold) + if not pobj_hot.is_vapor_phase(): raise ConfigurationError( "The HeatExchangerCrossFlow1D model is valid only for property packages " "whose single phase is a vapor phase. The hot side phase is not a vapor phase." ) - elif not self.config.cold_side.property_package.phase_list[0].is_vapor_phase(): + if not pobj_cold.is_vapor_phase(): raise ConfigurationError( "The HeatExchangerCrossFlow1D model is valid only for property packages " "whose single phase is a vapor phase. The cold side phase is not a vapor phase." ) - else: - p_hot = self.config.hot_side.property_package.phase_list[0] - p_cold = self.config.cold_side.property_package.phase_list[0] + # Reference add_object_reference(self, "heat_tube", tube.heat) add_object_reference(self, "heat_shell", shell.heat) @@ -459,10 +462,10 @@ def initialize_build( hot_side = blk.hot_side cold_side = blk.cold_side - + t0 = blk.flowsheet().time.first() if not ( - "temperature" in blk.config.hot_side.property_package.define_state_vars().keys() - and "temperature" in blk.config.cold_side.property_package.define_state_vars().keys() + "temperature" in hot_side.properties[t0, 0].define_state_vars().keys() + and "temperature" in cold_side.properties[t0, 0].define_state_vars().keys() ): raise NotImplementedError( "Presently, initialization of the HeatExchangerCrossFlow1D requires " @@ -564,29 +567,29 @@ def initialize_build( ) for z in cold_side.length_domain: - hot_side.temperature[t, z].fix( + hot_side.properties[t, z].temperature.fix( value( (1 - z) * hot_side.properties[t, 0].temperature + z * hot_side.properties[t, 1].temperature ) ) - cold_side.temperature[t, z].fix( + cold_side.properties[t, z].temperature.fix( value( (1 - z) * cold_side.properties[t, 0].temperature + z * cold_side.properties[t, 1].temperature ) ) blk.temp_wall_center[t, z].fix( - value(hot_side.temperature[t, z] + cold_side.temperature[t, z]) / 2 + value(hot_side.properties[t, z].temperature + cold_side.properties[t, z].temperature) / 2 ) - blk.temp_wall_hot_side[t, z].set_value(blk.temp_wall_center[t, z].value) - blk.temp_wall_cold_side[t, z].set_value(blk.temp_wall_center[t, z].value) + blk.temp_wall_shell[t, z].set_value(blk.temp_wall_center[t, z].value) + blk.temp_wall_tube[t, z].set_value(blk.temp_wall_center[t, z].value) if blk.config.cold_side.has_pressure_change: - cold_side[t, z].pressure.fix(P_in_cold_side) + cold_side.properties[t, z].pressure.fix(P_in_cold_side) if blk.config.hot_side.has_pressure_change: - hot_side[t, z].pressure.fix(P_in_hot_side) + hot_side.properties[t, z].pressure.fix(P_in_hot_side) blk.temp_wall_center_eqn.deactivate() if tube_has_pressure_change == True: @@ -603,13 +606,13 @@ def initialize_build( # In Step 3, unfix fluid state variables (enthalpy/temperature and pressure) # keep the inlet state variables fixed, otherwise, the degree of freedom > 0 - hot_side.properties.temperature.unfix() - hot_side.properties.pressure.unfix() - hot_side.properties.temperature[:, 0].fix() - hot_side.properties.pressure[:, 0].fix() + hot_side.properties[:, :].temperature.unfix() + hot_side.properties[:, :].pressure.unfix() + hot_side.properties[:, 0].temperature.fix() + hot_side.properties[:, 0].pressure.fix() - cold_side.properties.temperature.unfix() - cold_side.properties.pressure.unfix() + cold_side.properties[:, :].temperature.unfix() + cold_side.properties[:, :].pressure.unfix() if blk.config.flow_type == HeatExchangerFlowPattern.cocurrent: cold_side.properties[:, 0].temperature.fix() cold_side.properties[:, 0].pressure.fix() From d9405f836ae7d05d2b57aa13bd9be5496be10997 Mon Sep 17 00:00:00 2001 From: Doug A Date: Tue, 12 Mar 2024 17:37:36 -0400 Subject: [PATCH 06/38] Begin adding tests --- .../power_generation/unit_models/__init__.py | 1 + ...low.py => cross_flow_heat_exchanger_1D.py} | 11 +- .../test_cross_flow_heat_exchanger_1D.py | 100 ++++++++++++++++++ 3 files changed, 102 insertions(+), 10 deletions(-) rename idaes/models_extra/power_generation/unit_models/{heat_exchanger_1D_cross_flow.py => cross_flow_heat_exchanger_1D.py} (98%) create mode 100644 idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py diff --git a/idaes/models_extra/power_generation/unit_models/__init__.py b/idaes/models_extra/power_generation/unit_models/__init__.py index abd6fbb5d6..97ff59fc85 100644 --- a/idaes/models_extra/power_generation/unit_models/__init__.py +++ b/idaes/models_extra/power_generation/unit_models/__init__.py @@ -20,6 +20,7 @@ from .drum1D import Drum1D from .feedwater_heater_0D_dynamic import FWH0DDynamic from .heat_exchanger_3streams import HeatExchangerWith3Streams +from .cross_flow_heat_exchanger_1D import CrossFlowHeatExchanger1D from .steamheater import SteamHeater from .waterpipe import WaterPipe from .watertank import WaterTank diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py similarity index 98% rename from idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py rename to idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index cce0c1b0a5..e79602a743 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_1D_cross_flow.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -80,7 +80,7 @@ @declare_process_block_class("HeatExchangerCrossFlow1D") -class HeatExchangerCrossFlow1DData(HeatExchanger1DData): +class CrossFlowHeatExchanger1DData(HeatExchanger1DData): """Standard Heat Exchanger Cross Flow Unit Model Class.""" CONFIG = HeatExchanger1DData.CONFIG() @@ -103,15 +103,6 @@ class HeatExchangerCrossFlow1DData(HeatExchanger1DData): doc="tube arrangement could be in-line or staggered", ), ) - CONFIG.declare( - "has_radiation", - ConfigValue( - default=False, - domain=Bool, - description="Has side 2 gas radiation", - doc="define if shell side gas radiation is to be considered", - ), - ) def _process_config(self): # Copy and pasted from ShellAndTube1D diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py new file mode 100644 index 0000000000..0e667d4c54 --- /dev/null +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -0,0 +1,100 @@ +################################################################################# +# 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-2023 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. +################################################################################# + +import pytest +import pyomo.environ as pyo +from pyomo.util.check_units import assert_units_consistent + +from idaes.core import FlowsheetBlock +from idaes.models.properties.modular_properties import GenericParameterBlock +from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop, EosType +from idaes.models_extra.power_generation.unit_models import CrossFlowHeatExchanger1D +from idaes.core.util.model_statistics import degrees_of_freedom +from idaes.core.solvers import get_solver + +# Set up solver +solver = get_solver() + +@pytest.fixture +def model(): + m = pyo.ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.h2_side_prop_params = GenericParameterBlock( + **get_prop(["H2", "H2O", "Ar", "N2"], {"Vap"}, eos=EosType.IDEAL), + doc="H2O + H2 gas property parameters", + ) + m.fs.heater = CrossFlowHeatExchanger1D( + property_package=m.fs.h2_side_prop_params, + has_holdup=True, + has_fluid_holdup=False, + has_pressure_change=False, + finite_elements=2, + tube_arrangement="in-line" + ) + + +@pytest.mark.skipif(not helmholtz_available(), reason="General Helmholtz not available") +@pytest.mark.unit +def test_fwh_model(): + model = pyo.ConcreteModel() + model.fs = FlowsheetBlock( + dynamic=False, default_property_package=iapws95.Iapws95ParameterBlock() + ) + model.fs.properties = model.fs.config.default_property_package + model.fs.fwh = FWH0D( + has_desuperheat=True, + has_drain_cooling=True, + has_drain_mixer=True, + property_package=model.fs.properties, + ) + + model.fs.fwh.desuperheat.hot_side_inlet.flow_mol[:].set_value(100) + model.fs.fwh.desuperheat.hot_side_inlet.pressure.fix(201325) + model.fs.fwh.desuperheat.hot_side_inlet.enth_mol.fix(60000) + model.fs.fwh.drain_mix.drain.flow_mol.fix(1) + model.fs.fwh.drain_mix.drain.pressure.fix(201325) + model.fs.fwh.drain_mix.drain.enth_mol.fix(20000) + model.fs.fwh.cooling.cold_side_inlet.flow_mol.fix(400) + model.fs.fwh.cooling.cold_side_inlet.pressure.fix(101325) + model.fs.fwh.cooling.cold_side_inlet.enth_mol.fix(3000) + model.fs.fwh.condense.area.fix(1000) + model.fs.fwh.condense.overall_heat_transfer_coefficient.fix(100) + model.fs.fwh.desuperheat.area.fix(1000) + model.fs.fwh.desuperheat.overall_heat_transfer_coefficient.fix(10) + model.fs.fwh.cooling.area.fix(1000) + model.fs.fwh.cooling.overall_heat_transfer_coefficient.fix(10) + model.fs.fwh.initialize(optarg={"max_iter": 50}) + + assert degrees_of_freedom(model) == 0 + assert ( + abs(pyo.value(model.fs.fwh.desuperheat.hot_side_inlet.flow_mol[0]) - 98.335) + < 0.01 + ) + + +@pytest.mark.skipif(not helmholtz_available(), reason="General Helmholtz not available") +@pytest.mark.integration +def test_fwh_units(): + model = pyo.ConcreteModel() + model.fs = FlowsheetBlock( + dynamic=False, default_property_package=iapws95.Iapws95ParameterBlock() + ) + model.fs.properties = model.fs.config.default_property_package + model.fs.fwh = FWH0D( + has_desuperheat=True, + has_drain_cooling=True, + has_drain_mixer=True, + property_package=model.fs.properties, + ) + + assert_units_consistent(model) From 693305f952e315c9e6b907802db3babb7bc48566 Mon Sep 17 00:00:00 2001 From: Doug A Date: Tue, 12 Mar 2024 18:02:26 -0400 Subject: [PATCH 07/38] changes --- .../unit_models/cross_flow_heat_exchanger_1D.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index e79602a743..7893999c41 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -79,7 +79,7 @@ _log = idaeslog.getLogger(__name__) -@declare_process_block_class("HeatExchangerCrossFlow1D") +@declare_process_block_class("CrossFlowHeatExchanger1D") class CrossFlowHeatExchanger1DData(HeatExchanger1DData): """Standard Heat Exchanger Cross Flow Unit Model Class.""" @@ -214,7 +214,7 @@ def _make_performance(self): or len(self.config.cold_side.property_package.phase_list) != 1 ): raise ConfigurationError( - "The HeatExchangerCrossFlow1D model is valid only for property packages " + "The CrossFlowHeatExchanger1D model is valid only for property packages " f"with a single phase. Found {len(self.config.hot_side.property_package.phase_list)} " f"phases on the hot side and {len(self.config.cold_side.property_package.phase_list)} " "phases on the cold side." @@ -226,12 +226,12 @@ def _make_performance(self): pobj_cold = self.config.cold_side.property_package.get_phase(p_cold) if not pobj_hot.is_vapor_phase(): raise ConfigurationError( - "The HeatExchangerCrossFlow1D model is valid only for property packages " + "The CrossFlowHeatExchanger1D model is valid only for property packages " "whose single phase is a vapor phase. The hot side phase is not a vapor phase." ) if not pobj_cold.is_vapor_phase(): raise ConfigurationError( - "The HeatExchangerCrossFlow1D model is valid only for property packages " + "The CrossFlowHeatExchanger1D model is valid only for property packages " "whose single phase is a vapor phase. The cold side phase is not a vapor phase." ) @@ -423,7 +423,7 @@ def initialize_build( optarg=None, ): """ - HeatExchangerCrossFlow1D initialization routine + CrossFlowHeatExchanger1D initialization routine Keyword Arguments: state_args : a dict of arguments to be passed to the property @@ -459,7 +459,7 @@ def initialize_build( and "temperature" in cold_side.properties[t0, 0].define_state_vars().keys() ): raise NotImplementedError( - "Presently, initialization of the HeatExchangerCrossFlow1D requires " + "Presently, initialization of the CrossFlowHeatExchanger1D requires " "temperature to be a state variable of both hot side and cold side " "property packages. Extension to enth_mol or enth_mass as state variables " "is straightforward---feel free to open a pull request implementing it." From a2fe413f912a07c1d93e7fbc5f28400b4fb64eff Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 15 Mar 2024 11:47:17 -0400 Subject: [PATCH 08/38] test units --- .../cross_flow_heat_exchanger_1D.py | 200 ++++++++++--- .../unit_models/heat_exchanger_common.py | 24 +- .../test_cross_flow_heat_exchanger_1D.py | 265 ++++++++++++++---- 3 files changed, 382 insertions(+), 107 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 7893999c41..b6f00e2aad 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -37,6 +37,7 @@ sin, cos, SolverStatus, + units as pyunits, ) from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool @@ -70,8 +71,7 @@ add_hx_references, ) from idaes.models.unit_models.heat_exchanger_1D import HeatExchanger1DData -from idaes.models.unit_models.shell_and_tube_1d import ShellAndTube1DData -import heat_exchanger_common +from idaes.models_extra.power_generation.unit_models import heat_exchanger_common __author__ = "Jinliang Ma, Douglas Allan" @@ -314,10 +314,14 @@ def N_Nu_shell_eqn(b, t, x): doc="heat per length on tube side", ) def heat_tube_eqn(b, t, x): - return b.heat_tube[t, x] == b.hconv_tube[ - t, x - ] * const.pi * b.di_tube * b.nrow_inlet * b.ncol_tube * ( - b.temp_wall_tube[t, x] - tube.properties[t, x].temperature + return b.heat_tube[t, x] == ( + b.hconv_tube[t, x] + * const.pi + * pyunits.convert(b.di_tube, to_units=tube_units["length"]) + * b.nrow_inlet + * b.ncol_tube * ( + b.temp_wall_tube[t, x] - tube.properties[t, x].temperature + ) ) # Heat to wall per length on shell side @@ -329,7 +333,10 @@ def heat_tube_eqn(b, t, x): def heat_shell_eqn(b, t, x): return b.heat_shell[ t, x - ] * b.length_flow_shell == b.length_flow_tube * b.hconv_shell_total[ + ] * b.length_flow_shell == pyunits.convert( + b.length_flow_tube, + to_units=shell_units["length"] + ) * b.hconv_shell_total[ t, x ] * const.pi * b.do_tube * b.nrow_inlet * b.ncol_tube * ( b.temp_wall_shell[t, x] - shell.properties[t, x].temperature @@ -346,8 +353,16 @@ def temp_wall_tube_eqn(b, t, x): return ( b.hconv_tube[t, x] * (tube.properties[t, x].temperature - b.temp_wall_tube[t, x]) - * (b.thickness_tube / 2 / b.therm_cond_wall + b.rfouling_tube) - == b.temp_wall_tube[t, x] - b.temp_wall_center[t, x] + * ( + pyunits.convert( + b.thickness_tube / 2 / b.therm_cond_wall, + to_units=1 / tube_units["heat_transfer_coefficient"] + ) + b.rfouling_tube + ) + == b.temp_wall_tube[t, x] - pyunits.convert( + b.temp_wall_center[t, x], + to_units=tube_units["temperature"] + ) ) # Shell side wall temperature @@ -375,8 +390,14 @@ def temp_wall_center_eqn(b, t, x): # control volumes (and out of the wall), hence the negative sign # on heat_accumulation_term return -heat_accumulation_term(b, t, x) == ( - b.heat_shell[t, x] * b.length_flow_shell / b.length_flow_tube - + b.heat_tube[t, x] + b.heat_shell[t, x] * b.length_flow_shell / pyunits.convert( + b.length_flow_tube, + to_units=shell_units["length"] + ) + + pyunits.convert( + b.heat_tube[t, x], + to_units=shell_units["power"]/shell_units["length"] + ) ) if not self.config.dynamic: @@ -388,17 +409,26 @@ def total_heat_duty(b, t): enth_in = b.hot_side.properties[t, z0].get_enthalpy_flow_terms(p_hot) enth_out = b.hot_side.properties[t, z1].get_enthalpy_flow_terms(p_hot) - return enth_out - enth_in + return pyunits.convert( + enth_in - enth_out, # Hot side loses enthalpy + to_units=shell_units["power"] # Hot side isn't always the shell + ) @self.Expression(self.flowsheet().config.time) def log_mean_delta_temperature(b, t): dT0 = ( b.hot_side.properties[t, z0].temperature - - b.cold_side.properties[t, z0].temperature + - pyunits.convert( + b.cold_side.properties[t, z0].temperature, + to_units=shell_units["temperature"] + ) ) dT1 = ( b.hot_side.properties[t, z1].temperature - - b.cold_side.properties[t, z1].temperature + - pyunits.convert( + b.cold_side.properties[t, z1].temperature, + to_units=shell_units["temperature"] + ) ) return (dT0 - dT1) / log(dT0 / dT1) @@ -465,17 +495,31 @@ def initialize_build( "is straightforward---feel free to open a pull request implementing it." ) + hot_units = blk.config.hot_side.property_package.get_metadata().derived_units + cold_units = blk.config.cold_side.property_package.get_metadata().derived_units + if blk.config.shell_is_hot: shell = blk.hot_side tube = blk.cold_side shell_has_pressure_change = blk.config.hot_side.has_pressure_change tube_has_pressure_change = blk.config.cold_side.has_pressure_change + shell_units = ( + blk.config.hot_side.property_package.get_metadata().derived_units + ) + tube_units = ( + blk.config.cold_side.property_package.get_metadata().derived_units + ) else: shell = blk.cold_side tube = blk.hot_side shell_has_pressure_change = blk.config.cold_side.has_pressure_change tube_has_pressure_change = blk.config.hot_side.has_pressure_change - + shell_units = ( + blk.config.cold_side.property_package.get_metadata().derived_units + ) + tube_units = ( + blk.config.hot_side.property_package.get_metadata().derived_units + ) # --------------------------------------------------------------------- # Initialize shell block @@ -500,15 +544,31 @@ def initialize_build( # converted to a Var-Constraint pair instead of being a giant Expression like it is # presently. mcp_hot_side = value( - hot_side.properties[t, 0].flow_mol * hot_side.properties[t, 0].cp_mol + pyunits.convert( + hot_side.properties[t, 0].flow_mol * hot_side.properties[t, 0].cp_mol, + to_units=shell_units["power"]/shell_units["temperature"] + ) + ) + T_in_hot_side = value( + pyunits.convert( + hot_side.properties[t, 0].temperature, + to_units=shell_units["temperature"] + ) ) - T_in_hot_side = value(hot_side.properties[t, 0].temperature) P_in_hot_side = value(hot_side.properties[t, 0].pressure) if blk.config.flow_type == HeatExchangerFlowPattern.cocurrent: mcp_cold_side = value( - cold_side.properties[t, 0].flow_mol * cold_side.properties[t, 0].cp_mol + pyunits.convert( + cold_side.properties[t, 0].flow_mol * cold_side.properties[t, 0].cp_mol, + to_units=shell_units["power"]/shell_units["temperature"] + ) + ) + T_in_cold_side = value( + pyunits.convert( + cold_side.properties[t, 0].temperature, + to_units=shell_units["temperature"] + ) ) - T_in_cold_side = value(cold_side.properties[t, 0].temperature) P_in_cold_side = value(cold_side.properties[t, 0].pressure) T_out_max = ( @@ -521,18 +581,39 @@ def initialize_build( temp_out_cold_side_guess = ( T_in_cold_side + q_guess / mcp_cold_side ) - cold_side.properties[t, 1].temperature.fix(temp_out_cold_side_guess) + + cold_side.properties[t, 1].temperature.fix( + pyunits.convert_value( + temp_out_cold_side_guess, + from_units=shell_units["temperature"], + to_units=cold_units["temperature"] + ) + ) temp_out_hot_side_guess = ( T_in_cold_side - q_guess / mcp_hot_side ) - hot_side.properties[t, 1].temperature.fix(temp_out_hot_side_guess) + hot_side.properties[t, 1].temperature.fix( + pyunits.convert_value( + temp_out_hot_side_guess, + from_units=shell_units["temperature"], + to_units=hot_units["temperature"] + ) + ) elif blk.config.flow_type == HeatExchangerFlowPattern.countercurrent: mcp_cold_side = value( - cold_side.properties[t, 1].flow_mol * cold_side.properties[t, 1].cp_mol + pyunits.convert( + cold_side.properties[t, 1].flow_mol * cold_side.properties[t, 1].cp_mol, + to_units=shell_units["power"]/shell_units["temperature"] + ) + ) + T_in_cold_side = value( + pyunits.convert( + cold_side.properties[t, 1].temperature, + to_units=shell_units["temperature"] + ) ) - T_in_cold_side = value(cold_side.properties[t, 1].temperature) P_in_cold_side = value(cold_side.properties[t, 1].pressure) if mcp_cold_side < mcp_hot_side: @@ -543,12 +624,24 @@ def initialize_build( temp_out_cold_side_guess = ( T_in_cold_side + q_guess / mcp_cold_side ) - cold_side.properties[t, 0].temperature.fix(temp_out_cold_side_guess) + cold_side.properties[t, 0].temperature.fix( + pyunits.convert_value( + temp_out_cold_side_guess, + from_units=shell_units["temperature"], + to_units=cold_units["temperature"] + ) + ) temp_out_hot_side_guess = ( T_in_hot_side - q_guess / mcp_hot_side ) - hot_side.properties[t, 1].temperature.fix(temp_out_hot_side_guess) + hot_side.properties[t, 1].temperature.fix( + pyunits.convert_value( + temp_out_hot_side_guess, + from_units=shell_units["temperature"], + to_units=hot_units["temperature"] + ) + ) else: raise BurntToast( @@ -571,11 +664,26 @@ def initialize_build( ) ) blk.temp_wall_center[t, z].fix( - value(hot_side.properties[t, z].temperature + cold_side.properties[t, z].temperature) / 2 + value( + pyunits.convert( + hot_side.properties[t, z].temperature, + to_units=shell_units["temperature"] + ) + + pyunits.convert( + cold_side.properties[t, z].temperature, + to_units=shell_units["temperature"] + ) + ) / 2 ) blk.temp_wall_shell[t, z].set_value(blk.temp_wall_center[t, z].value) - blk.temp_wall_tube[t, z].set_value(blk.temp_wall_center[t, z].value) + blk.temp_wall_tube[t, z].set_value( + pyunits.convert_value( + blk.temp_wall_center[t, z].value, + from_units=shell_units["temperature"], + to_units=tube_units["temperature"] + ) + ) if blk.config.cold_side.has_pressure_change: cold_side.properties[t, z].pressure.fix(P_in_cold_side) @@ -660,13 +768,18 @@ def cst(con, sf): iscale.constraint_scaling_transform(con, sf, overwrite=False) sgsf = iscale.set_and_get_scaling_factor - if self.config.shell_is_hot: shell = self.hot_side tube = self.cold_side + shell_units = ( + self.config.hot_side.property_package.get_metadata().derived_units + ) else: shell = self.cold_side tube = self.hot_side + shell_units = ( + self.config.cold_side.property_package.get_metadata().derived_units + ) tube_has_pressure_change = hasattr(self, "deltaP_tube") shell_has_pressure_change = hasattr(self, "deltaP_shell") @@ -686,12 +799,22 @@ def cst(con, sf): make_nusselt=True ) + sf_area_per_length_shell = value( + self.length_flow_shell + / ( + pyunits.convert( + self.length_flow_tube, + to_units=shell_units["length"] + ) + * const.pi + * self.do_tube + * self.nrow_inlet + * self.ncol_tube + ) + ) + for t in self.flowsheet().time: for z in shell.length_domain: - # FIXME try to do this rigorously later on - sf_area_per_length_tube = 1 / value( - const.pi * self.di_tube * self.nrow_inlet * self.ncol_tube - ) sf_T_tube = gsf(tube.properties[t, z].temperature) ssf(self.temp_wall_tube[t, z], sf_T_tube) cst(self.temp_wall_tube_eqn[t, z], sf_T_tube) @@ -703,16 +826,7 @@ def cst(con, sf): ssf(self.temp_wall_shell[t, z], sf_T_shell) cst(self.temp_wall_shell_eqn[t, z], sf_T_shell) - sf_area_per_length_shell = value( - self.length_flow_shell - / ( - self.length_flow_tube - * const.pi - * self.do_tube - * self.nrow_inlet - * self.ncol_tube - ) - ) + sf_hconv_shell_conv = gsf(self.hconv_shell_conv[t, z]) s_Q_shell = sgsf( shell.heat[t, z], @@ -738,7 +852,7 @@ def _get_performance_contents(self, time_point=0): expr_dict["HX Area"] = self.total_heat_transfer_area expr_dict["Delta T Driving"] = self.log_mean_delta_temperature[time_point] expr_dict["Total Heat Duty"] = self.total_heat_duty[time_point] - expr_dict["HX Coefficient"] = self.overall_heat_transfer_coefficient[time_point] + expr_dict["Average HX Coefficient"] = self.overall_heat_transfer_coefficient[time_point] return {"vars": var_dict, "exprs": expr_dict} diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index f920149a4d..d1a01cfcd1 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -34,10 +34,13 @@ sin, cos, SolverStatus, + units as pyunits, ) from pyomo.common.config import ConfigBlock, ConfigValue, In from pyomo.util.calc_var_value import calculate_variable_from_constraint +from pyomo.dae import DerivativeVar + # Import IDAES cores from idaes.core import ( ControlVolume1DBlock, @@ -53,8 +56,6 @@ from idaes.core.util.exceptions import ConfigurationError from idaes.core.util.constants import Constants as const import idaes.core.util.scaling as iscale -from pyomo.dae import DerivativeVar -from pyomo.environ import units as pyunits from idaes.core.solvers import get_solver from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.misc import add_object_reference @@ -251,7 +252,10 @@ def _make_performance_common( # Heat transfer resistance due to the fouling on shell side blk.rfouling_shell = Param( - initialize=0.0001, mutable=True, doc="Fouling resistance on tube side" + units=1/shell_units["heat_transfer_coefficient"], + initialize=0.0001, + mutable=True, + doc="Fouling resistance on tube side" ) # Correction factor for convective heat transfer coefficient on shell side @@ -490,10 +494,8 @@ def hconv_shell_conv_eqn(b, t, x): doc="Total convective heat transfer coefficient on shell side", ) def hconv_shell_total(b, t, x): - if blk.config.has_radiation: - return b.hconv_shell_conv[t, x] + b.hconv_shell_rad[t, x] - else: - return b.hconv_shell_conv[t, x] + # Retain in case we add back radiation + return b.hconv_shell_conv[t, x] def _make_performance_tube( @@ -508,6 +510,7 @@ def _make_performance_tube( blk.flowsheet().config.time, tube.length_domain, initialize=100.0, + units=tube_units["heat_transfer_coefficient"], doc="tube side convective heat transfer coefficient", ) @@ -518,7 +521,10 @@ def _make_performance_tube( # Heat transfer resistance due to the fouling on tube side blk.rfouling_tube = Param( - initialize=0.0, mutable=True, doc="fouling resistance on tube side" + initialize=0.0, + mutable=True, + units=1/tube_units["heat_transfer_coefficient"], + doc="fouling resistance on tube side" ) # Correction factor for convective heat transfer coefficient on tube side blk.fcorrection_htc_tube = Var( @@ -535,7 +541,7 @@ def _make_performance_tube( blk.flowsheet().config.time, tube.length_domain, initialize=500, - units=tube_units["temperature"], + units=tube_units["temperature"], # Want to be in shell units for consistency in equations doc="boundary wall temperature on tube side", ) if make_reynolds: diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index 0e667d4c54..39e9896a1f 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -16,85 +16,240 @@ from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock +import idaes.core.util.scaling as iscale +from idaes.models.unit_models import HeatExchangerFlowPattern from idaes.models.properties.modular_properties import GenericParameterBlock from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop, EosType from idaes.models_extra.power_generation.unit_models import CrossFlowHeatExchanger1D +import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.solvers import get_solver # Set up solver -solver = get_solver() +optarg={ + # 'bound_push' : 1e-6, + 'constr_viol_tol': 1e-8, + 'nlp_scaling_method': 'user-scaling', + 'linear_solver': 'ma57', + 'OF_ma57_automatic_scaling': 'yes', + 'max_iter': 350, + 'tol': 1e-8, + 'halt_on_ampl_error': 'no', +} +solver = get_solver("ipopt", options=optarg) -@pytest.fixture -def model(): +def _create_model(pressure_drop): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) m.fs.h2_side_prop_params = GenericParameterBlock( **get_prop(["H2", "H2O", "Ar", "N2"], {"Vap"}, eos=EosType.IDEAL), doc="H2O + H2 gas property parameters", ) - m.fs.heater = CrossFlowHeatExchanger1D( - property_package=m.fs.h2_side_prop_params, + m.fs.heat_exchanger = CrossFlowHeatExchanger1D( has_holdup=True, - has_fluid_holdup=False, - has_pressure_change=False, - finite_elements=2, - tube_arrangement="in-line" + dynamic=False, + cold_side={ + "property_package": m.fs.h2_side_prop_params, + "has_holdup": False, + "dynamic": False, + "has_pressure_change": pressure_drop, + "transformation_method": "dae.finite_difference", + "transformation_scheme": "FORWARD", + }, + hot_side={ + "property_package": m.fs.h2_side_prop_params, + "has_holdup": False, + "dynamic": False, + "has_pressure_change": pressure_drop, + "transformation_method": "dae.finite_difference", + "transformation_scheme": "BACKWARD", + }, + shell_is_hot=True, + flow_type=HeatExchangerFlowPattern.countercurrent, + finite_elements=12, + tube_arrangement="staggered", ) + + hx = m.fs.heat_exchanger + + hx.hot_side_inlet.flow_mol.fix(2619.7) + hx.hot_side_inlet.temperature.fix(971.6) + hx.hot_side_inlet.pressure.fix(1.2e5) + hx.hot_side_inlet.mole_frac_comp[0, "H2"].fix(0.79715) + hx.hot_side_inlet.mole_frac_comp[0, "H2O"].fix(0.20177) + hx.hot_side_inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) + hx.hot_side_inlet.mole_frac_comp[0, "N2"].fix(0.00021589) + + hx.cold_side_inlet.flow_mol.fix(2619.7) + hx.cold_side_inlet.temperature.fix(446.21) + hx.cold_side_inlet.pressure.fix(1.2e5) + hx.cold_side_inlet.mole_frac_comp[0, "H2"].fix(0.36203) + hx.cold_side_inlet.mole_frac_comp[0, "H2O"].fix(0.63689) + hx.cold_side_inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) + hx.cold_side_inlet.mole_frac_comp[0, "N2"].fix(0.00021589) + + hx.di_tube.fix(0.0525018) + hx.thickness_tube.fix(0.0039116) + hx.length_tube_seg.fix(4.3) + hx.nseg_tube.fix(12) + hx.ncol_tube.fix(50) + hx.nrow_inlet.fix(25) + + hx.pitch_x.fix(0.1) + hx.pitch_y.fix(0.1) + hx.delta_elevation.fix(0) + hx.therm_cond_wall = 43.0 + hx.rfouling_tube = 0.0001 + hx.rfouling_shell = 0.0001 + hx.fcorrection_htc_tube.fix(1) + hx.fcorrection_htc_shell.fix(1) + if pressure_drop: + hx.fcorrection_dp_tube.fix(1) + hx.fcorrection_dp_shell.fix(1) + + hx.cp_wall.value = 502.4 + + pp = m.fs.h2_side_prop_params + pp.set_default_scaling("enth_mol_phase", 1e-3) + pp.set_default_scaling("pressure", 1e-5) + pp.set_default_scaling("temperature", 1e-2) + pp.set_default_scaling("flow_mol", 1e-3) + + _mf_scale = { + "H2": 1, + "H2O": 1, + "N2": 10, + "Ar": 10, + } + for comp, s in _mf_scale.items(): + pp.set_default_scaling("mole_frac_comp", s, index=comp) + pp.set_default_scaling("mole_frac_phase_comp", s, index=("Vap", comp)) + pp.set_default_scaling("flow_mol_phase_comp", s * 1e-3, index=("Vap", comp)) + shell = hx.hot_side + tube = hx.cold_side + iscale.set_scaling_factor(shell.area, 1e-1) + # ssf(hx.shell.heat, 1e-6) + iscale.set_scaling_factor(tube.area, 1) + # ssf(hx.tube.heat, 1e-6) + iscale.set_scaling_factor(shell._enthalpy_flow, 1e-8) + iscale.set_scaling_factor(tube._enthalpy_flow, 1e-8) + iscale.set_scaling_factor(shell.enthalpy_flow_dx, 1e-7) + iscale.set_scaling_factor(tube.enthalpy_flow_dx, 1e-7) + iscale.set_scaling_factor(hx.heat_holdup, 1e-8) -@pytest.mark.skipif(not helmholtz_available(), reason="General Helmholtz not available") -@pytest.mark.unit -def test_fwh_model(): - model = pyo.ConcreteModel() - model.fs = FlowsheetBlock( - dynamic=False, default_property_package=iapws95.Iapws95ParameterBlock() - ) - model.fs.properties = model.fs.config.default_property_package - model.fs.fwh = FWH0D( - has_desuperheat=True, - has_drain_cooling=True, - has_drain_mixer=True, - property_package=model.fs.properties, - ) + iscale.calculate_scaling_factors(m) + + return m + +def _check_model_statistics(m): + fixed_unused_var_set = { + "fs.h2_side_prop_params.H2.omega", + "fs.h2_side_prop_params.H2.pressure_crit", + "fs.h2_side_prop_params.H2.temperature_crit", + "fs.h2_side_prop_params.H2.cp_mol_ig_comp_coeff_G", + "fs.h2_side_prop_params.H2.cp_mol_ig_comp_coeff_H", + "fs.h2_side_prop_params.H2O.omega", + "fs.h2_side_prop_params.H2O.pressure_crit", + "fs.h2_side_prop_params.H2O.temperature_crit", + "fs.h2_side_prop_params.H2O.cp_mol_ig_comp_coeff_G", + "fs.h2_side_prop_params.H2O.cp_mol_ig_comp_coeff_H", + "fs.h2_side_prop_params.H2O.pressure_sat_comp_coeff_A", + "fs.h2_side_prop_params.H2O.pressure_sat_comp_coeff_B", + "fs.h2_side_prop_params.H2O.pressure_sat_comp_coeff_C", + "fs.h2_side_prop_params.Ar.omega", + "fs.h2_side_prop_params.Ar.pressure_crit", + "fs.h2_side_prop_params.Ar.temperature_crit", + "fs.h2_side_prop_params.Ar.cp_mol_ig_comp_coeff_G", + "fs.h2_side_prop_params.Ar.cp_mol_ig_comp_coeff_H", + "fs.h2_side_prop_params.N2.omega", + "fs.h2_side_prop_params.N2.pressure_crit", + "fs.h2_side_prop_params.N2.temperature_crit", + "fs.h2_side_prop_params.N2.cp_mol_ig_comp_coeff_G", + "fs.h2_side_prop_params.N2.cp_mol_ig_comp_coeff_H", + } + for var in mstat.fixed_unused_variables_set(m): + assert var.name in fixed_unused_var_set + + unfixed_unused_var_set = { + "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,H2]", + "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,H2O]", + "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,Ar]", + "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,N2]", + "fs.heat_exchanger.hot_side.enthalpy_flow_dx[0.0,0.0,Vap]", + "fs.heat_exchanger.hot_side.pressure_dx[0.0,0.0]", + "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,H2]", + "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,H2O]", + "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,Ar]", + "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,N2]", + "fs.heat_exchanger.cold_side.enthalpy_flow_dx[0.0,1.0,Vap]", + "fs.heat_exchanger.cold_side.pressure_dx[0.0,1.0]", + } + + for var in mstat.unused_variables_set(m) - mstat.fixed_unused_variables_set(m): + assert var.name in unfixed_unused_var_set + + assert len(mstat.deactivated_constraints_set(m)) == 0 + +@pytest.fixture +def model_no_dP(): + m = _create_model(pressure_drop=False) + return m - model.fs.fwh.desuperheat.hot_side_inlet.flow_mol[:].set_value(100) - model.fs.fwh.desuperheat.hot_side_inlet.pressure.fix(201325) - model.fs.fwh.desuperheat.hot_side_inlet.enth_mol.fix(60000) - model.fs.fwh.drain_mix.drain.flow_mol.fix(1) - model.fs.fwh.drain_mix.drain.pressure.fix(201325) - model.fs.fwh.drain_mix.drain.enth_mol.fix(20000) - model.fs.fwh.cooling.cold_side_inlet.flow_mol.fix(400) - model.fs.fwh.cooling.cold_side_inlet.pressure.fix(101325) - model.fs.fwh.cooling.cold_side_inlet.enth_mol.fix(3000) - model.fs.fwh.condense.area.fix(1000) - model.fs.fwh.condense.overall_heat_transfer_coefficient.fix(100) - model.fs.fwh.desuperheat.area.fix(1000) - model.fs.fwh.desuperheat.overall_heat_transfer_coefficient.fix(10) - model.fs.fwh.cooling.area.fix(1000) - model.fs.fwh.cooling.overall_heat_transfer_coefficient.fix(10) - model.fs.fwh.initialize(optarg={"max_iter": 50}) - - assert degrees_of_freedom(model) == 0 +@pytest.mark.component +def test_initialization(model_no_dP): + m = model_no_dP + + assert degrees_of_freedom(m) == 0 + _check_model_statistics(m) + + m.fs.heat_exchanger.initialize_build(optarg=optarg) + + assert degrees_of_freedom(m) == 0 + _check_model_statistics(m) + assert ( + pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) + == pytest.approx(485.34, abs=1e-1) + ) assert ( - abs(pyo.value(model.fs.fwh.desuperheat.hot_side_inlet.flow_mol[0]) - 98.335) - < 0.01 + pyo.value(m.fs.heat_exchanger.cold_side_outlet.temperature[0]) + == pytest.approx(911.47, abs=1e-1) ) -@pytest.mark.skipif(not helmholtz_available(), reason="General Helmholtz not available") @pytest.mark.integration -def test_fwh_units(): - model = pyo.ConcreteModel() - model.fs = FlowsheetBlock( - dynamic=False, default_property_package=iapws95.Iapws95ParameterBlock() +def test_units(model_no_dP): + assert_units_consistent(model_no_dP.fs.heat_exchanger) + + + +@pytest.fixture +def model_dP(): + m = _create_model(pressure_drop=True) + return m + +@pytest.mark.component +def test_initialization(model_dP): + m = model_dP + + assert degrees_of_freedom(m) == 0 + _check_model_statistics(m) + + m.fs.heat_exchanger.initialize_build(optarg=optarg) + + assert degrees_of_freedom(m) == 0 + _check_model_statistics(m) + + assert ( + pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) + == pytest.approx(485.34, abs=1e-1) ) - model.fs.properties = model.fs.config.default_property_package - model.fs.fwh = FWH0D( - has_desuperheat=True, - has_drain_cooling=True, - has_drain_mixer=True, - property_package=model.fs.properties, + assert ( + pyo.value(m.fs.heat_exchanger.cold_side_outlet.temperature[0]) + == pytest.approx(911.47, abs=1e-1) ) - assert_units_consistent(model) + +@pytest.mark.integration +def test_units(model_dP): + assert_units_consistent(model_dP.fs.heat_exchanger) From a30edf69d21a425abe891c282231ebd78be2e49e Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 15 Mar 2024 12:01:37 -0400 Subject: [PATCH 09/38] fix pressure drop unit issue --- .../unit_models/heat_exchanger_common.py | 5 ++-- .../test_cross_flow_heat_exchanger_1D.py | 25 +++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index d1a01cfcd1..f60ca2196a 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -463,6 +463,7 @@ def friction_factor_shell_eqn(b, t, x): doc="pressure change on shell side", ) def deltaP_shell_eqn(b, t, x): + # FIXME this equation doesn't have elevation change---is this right? return ( b.deltaP_shell[t, x] * b.pitch_x == -1.4 @@ -580,7 +581,7 @@ def _make_performance_tube( blk.flowsheet().config.time, tube.length_domain, initialize=-10.0, - units=tube_units["pressure"], + units=tube_units["pressure"] / tube_units["length"], doc="pressure drop due to friction on tube side", ) @@ -589,7 +590,7 @@ def _make_performance_tube( blk.flowsheet().config.time, tube.length_domain, initialize=-10.0, - units=tube_units["pressure"], + units=tube_units["pressure"] / tube_units["length"], doc="pressure drop due to u-turn on tube side", ) if make_nusselt: diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index 39e9896a1f..ce2780f193 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -142,7 +142,7 @@ def _create_model(pressure_drop): return m -def _check_model_statistics(m): +def _check_model_statistics(m, deltaP): fixed_unused_var_set = { "fs.h2_side_prop_params.H2.omega", "fs.h2_side_prop_params.H2.pressure_crit", @@ -168,6 +168,9 @@ def _check_model_statistics(m): "fs.h2_side_prop_params.N2.cp_mol_ig_comp_coeff_G", "fs.h2_side_prop_params.N2.cp_mol_ig_comp_coeff_H", } + if not deltaP: + fixed_unused_var_set.add("fs.heat_exchanger.delta_elevation") + for var in mstat.fixed_unused_variables_set(m): assert var.name in fixed_unused_var_set @@ -201,12 +204,12 @@ def test_initialization(model_no_dP): m = model_no_dP assert degrees_of_freedom(m) == 0 - _check_model_statistics(m) + _check_model_statistics(m, deltaP=False) m.fs.heat_exchanger.initialize_build(optarg=optarg) assert degrees_of_freedom(m) == 0 - _check_model_statistics(m) + _check_model_statistics(m, deltaP=False) assert ( pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) == pytest.approx(485.34, abs=1e-1) @@ -229,16 +232,16 @@ def model_dP(): return m @pytest.mark.component -def test_initialization(model_dP): +def test_initialization_dP(model_dP): m = model_dP assert degrees_of_freedom(m) == 0 - _check_model_statistics(m) + _check_model_statistics(m, deltaP=True) m.fs.heat_exchanger.initialize_build(optarg=optarg) assert degrees_of_freedom(m) == 0 - _check_model_statistics(m) + _check_model_statistics(m, deltaP=True) assert ( pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) @@ -248,8 +251,16 @@ def test_initialization(model_dP): pyo.value(m.fs.heat_exchanger.cold_side_outlet.temperature[0]) == pytest.approx(911.47, abs=1e-1) ) + assert ( + pyo.value(m.fs.heat_exchanger.hot_side_outlet.pressure[0]) + == pytest.approx(118870.08569, abs=1) + ) + assert ( + pyo.value(m.fs.heat_exchanger.cold_side_outlet.pressure[0]) + == pytest.approx(111418.71399, abs=1) + ) @pytest.mark.integration -def test_units(model_dP): +def test_units_dP(model_dP): assert_units_consistent(model_dP.fs.heat_exchanger) From 1c03d2310ce139cda9dcaa17120cc3cad1a11338 Mon Sep 17 00:00:00 2001 From: Doug A Date: Tue, 19 Mar 2024 16:07:50 -0400 Subject: [PATCH 10/38] Moving towards heater testing --- .../power_generation/unit_models/__init__.py | 3 +- .../unit_models/tests/test_heater_1D.py | 253 ++++++++++++++++++ 2 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py diff --git a/idaes/models_extra/power_generation/unit_models/__init__.py b/idaes/models_extra/power_generation/unit_models/__init__.py index 97ff59fc85..af8d39973b 100644 --- a/idaes/models_extra/power_generation/unit_models/__init__.py +++ b/idaes/models_extra/power_generation/unit_models/__init__.py @@ -15,12 +15,13 @@ from .boiler_fireside import BoilerFireside from .boiler_heat_exchanger import BoilerHeatExchanger from .boiler_heat_exchanger_2D import HeatExchangerCrossFlow2D_Header +from .cross_flow_heat_exchanger_1D import CrossFlowHeatExchanger1D from .downcomer import Downcomer from .drum import Drum from .drum1D import Drum1D from .feedwater_heater_0D_dynamic import FWH0DDynamic +from .heater_1D import Heater1D from .heat_exchanger_3streams import HeatExchangerWith3Streams -from .cross_flow_heat_exchanger_1D import CrossFlowHeatExchanger1D from .steamheater import SteamHeater from .waterpipe import WaterPipe from .watertank import WaterTank diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py new file mode 100644 index 0000000000..a5388031e7 --- /dev/null +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -0,0 +1,253 @@ +################################################################################# +# 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-2023 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. +################################################################################# + +import pytest +import pyomo.environ as pyo +from pyomo.util.check_units import assert_units_consistent + +from idaes.core import FlowsheetBlock +import idaes.core.util.scaling as iscale +from idaes.models.unit_models import HeatExchangerFlowPattern +from idaes.models.properties.modular_properties import GenericParameterBlock +from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop, EosType +from idaes.models_extra.power_generation.unit_models import Heater1D +import idaes.core.util.model_statistics as mstat +from idaes.core.util.model_statistics import degrees_of_freedom +from idaes.core.solvers import get_solver + +# Set up solver +optarg={ + # 'bound_push' : 1e-6, + 'constr_viol_tol': 1e-8, + 'nlp_scaling_method': 'user-scaling', + 'linear_solver': 'ma57', + 'OF_ma57_automatic_scaling': 'yes', + 'max_iter': 350, + 'tol': 1e-8, + 'halt_on_ampl_error': 'no', +} +solver = get_solver("ipopt", options=optarg) + +def _create_model(pressure_drop): + m = pyo.ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.h2_side_prop_params = GenericParameterBlock( + **get_prop(["H2", "H2O", "Ar", "N2"], {"Vap"}, eos=EosType.IDEAL), + doc="H2O + H2 gas property parameters", + ) + m.fs.feed_heater = Heater1D( + property_package=m.fs.h2_side_prop_params, + has_holdup=True, + dynamic=False, + has_fluid_holdup=False, + has_pressure_change=pressure_drop, + finite_elements=4, + tube_arrangement="in-line", + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + ) + + hx = m.fs.heat_exchanger + + hx.hot_side_inlet.flow_mol.fix(2619.7) + hx.hot_side_inlet.temperature.fix(971.6) + hx.hot_side_inlet.pressure.fix(1.2e5) + hx.hot_side_inlet.mole_frac_comp[0, "H2"].fix(0.79715) + hx.hot_side_inlet.mole_frac_comp[0, "H2O"].fix(0.20177) + hx.hot_side_inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) + hx.hot_side_inlet.mole_frac_comp[0, "N2"].fix(0.00021589) + + hx.cold_side_inlet.flow_mol.fix(2619.7) + hx.cold_side_inlet.temperature.fix(446.21) + hx.cold_side_inlet.pressure.fix(1.2e5) + hx.cold_side_inlet.mole_frac_comp[0, "H2"].fix(0.36203) + hx.cold_side_inlet.mole_frac_comp[0, "H2O"].fix(0.63689) + hx.cold_side_inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) + hx.cold_side_inlet.mole_frac_comp[0, "N2"].fix(0.00021589) + + hx.di_tube.fix(0.0525018) + hx.thickness_tube.fix(0.0039116) + hx.length_tube_seg.fix(4.3) + hx.nseg_tube.fix(12) + hx.ncol_tube.fix(50) + hx.nrow_inlet.fix(25) + + hx.pitch_x.fix(0.1) + hx.pitch_y.fix(0.1) + hx.delta_elevation.fix(0) + hx.therm_cond_wall = 43.0 + hx.rfouling_tube = 0.0001 + hx.rfouling_shell = 0.0001 + hx.fcorrection_htc_tube.fix(1) + hx.fcorrection_htc_shell.fix(1) + if pressure_drop: + hx.fcorrection_dp_tube.fix(1) + hx.fcorrection_dp_shell.fix(1) + + hx.cp_wall.value = 502.4 + + pp = m.fs.h2_side_prop_params + pp.set_default_scaling("enth_mol_phase", 1e-3) + pp.set_default_scaling("pressure", 1e-5) + pp.set_default_scaling("temperature", 1e-2) + pp.set_default_scaling("flow_mol", 1e-3) + + _mf_scale = { + "H2": 1, + "H2O": 1, + "N2": 10, + "Ar": 10, + } + for comp, s in _mf_scale.items(): + pp.set_default_scaling("mole_frac_comp", s, index=comp) + pp.set_default_scaling("mole_frac_phase_comp", s, index=("Vap", comp)) + pp.set_default_scaling("flow_mol_phase_comp", s * 1e-3, index=("Vap", comp)) + + shell = hx.hot_side + tube = hx.cold_side + iscale.set_scaling_factor(shell.area, 1e-1) + # ssf(hx.shell.heat, 1e-6) + iscale.set_scaling_factor(tube.area, 1) + # ssf(hx.tube.heat, 1e-6) + iscale.set_scaling_factor(shell._enthalpy_flow, 1e-8) + iscale.set_scaling_factor(tube._enthalpy_flow, 1e-8) + iscale.set_scaling_factor(shell.enthalpy_flow_dx, 1e-7) + iscale.set_scaling_factor(tube.enthalpy_flow_dx, 1e-7) + iscale.set_scaling_factor(hx.heat_holdup, 1e-8) + + iscale.calculate_scaling_factors(m) + + return m + +def _check_model_statistics(m, deltaP): + fixed_unused_var_set = { + "fs.h2_side_prop_params.H2.omega", + "fs.h2_side_prop_params.H2.pressure_crit", + "fs.h2_side_prop_params.H2.temperature_crit", + "fs.h2_side_prop_params.H2.cp_mol_ig_comp_coeff_G", + "fs.h2_side_prop_params.H2.cp_mol_ig_comp_coeff_H", + "fs.h2_side_prop_params.H2O.omega", + "fs.h2_side_prop_params.H2O.pressure_crit", + "fs.h2_side_prop_params.H2O.temperature_crit", + "fs.h2_side_prop_params.H2O.cp_mol_ig_comp_coeff_G", + "fs.h2_side_prop_params.H2O.cp_mol_ig_comp_coeff_H", + "fs.h2_side_prop_params.H2O.pressure_sat_comp_coeff_A", + "fs.h2_side_prop_params.H2O.pressure_sat_comp_coeff_B", + "fs.h2_side_prop_params.H2O.pressure_sat_comp_coeff_C", + "fs.h2_side_prop_params.Ar.omega", + "fs.h2_side_prop_params.Ar.pressure_crit", + "fs.h2_side_prop_params.Ar.temperature_crit", + "fs.h2_side_prop_params.Ar.cp_mol_ig_comp_coeff_G", + "fs.h2_side_prop_params.Ar.cp_mol_ig_comp_coeff_H", + "fs.h2_side_prop_params.N2.omega", + "fs.h2_side_prop_params.N2.pressure_crit", + "fs.h2_side_prop_params.N2.temperature_crit", + "fs.h2_side_prop_params.N2.cp_mol_ig_comp_coeff_G", + "fs.h2_side_prop_params.N2.cp_mol_ig_comp_coeff_H", + } + if not deltaP: + fixed_unused_var_set.add("fs.heat_exchanger.delta_elevation") + + for var in mstat.fixed_unused_variables_set(m): + assert var.name in fixed_unused_var_set + + unfixed_unused_var_set = { + "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,H2]", + "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,H2O]", + "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,Ar]", + "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,N2]", + "fs.heat_exchanger.hot_side.enthalpy_flow_dx[0.0,0.0,Vap]", + "fs.heat_exchanger.hot_side.pressure_dx[0.0,0.0]", + "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,H2]", + "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,H2O]", + "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,Ar]", + "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,N2]", + "fs.heat_exchanger.cold_side.enthalpy_flow_dx[0.0,1.0,Vap]", + "fs.heat_exchanger.cold_side.pressure_dx[0.0,1.0]", + } + + for var in mstat.unused_variables_set(m) - mstat.fixed_unused_variables_set(m): + assert var.name in unfixed_unused_var_set + + assert len(mstat.deactivated_constraints_set(m)) == 0 + +@pytest.fixture +def model_no_dP(): + m = _create_model(pressure_drop=False) + return m + +@pytest.mark.component +def test_initialization(model_no_dP): + m = model_no_dP + + assert degrees_of_freedom(m) == 0 + _check_model_statistics(m, deltaP=False) + + m.fs.heat_exchanger.initialize_build(optarg=optarg) + + assert degrees_of_freedom(m) == 0 + _check_model_statistics(m, deltaP=False) + assert ( + pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) + == pytest.approx(485.34, abs=1e-1) + ) + assert ( + pyo.value(m.fs.heat_exchanger.cold_side_outlet.temperature[0]) + == pytest.approx(911.47, abs=1e-1) + ) + + +@pytest.mark.integration +def test_units(model_no_dP): + assert_units_consistent(model_no_dP.fs.heat_exchanger) + + + +@pytest.fixture +def model_dP(): + m = _create_model(pressure_drop=True) + return m + +@pytest.mark.component +def test_initialization_dP(model_dP): + m = model_dP + + assert degrees_of_freedom(m) == 0 + _check_model_statistics(m, deltaP=True) + + m.fs.heat_exchanger.initialize_build(optarg=optarg) + + assert degrees_of_freedom(m) == 0 + _check_model_statistics(m, deltaP=True) + + assert ( + pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) + == pytest.approx(485.34, abs=1e-1) + ) + assert ( + pyo.value(m.fs.heat_exchanger.cold_side_outlet.temperature[0]) + == pytest.approx(911.47, abs=1e-1) + ) + assert ( + pyo.value(m.fs.heat_exchanger.hot_side_outlet.pressure[0]) + == pytest.approx(118870.08569, abs=1) + ) + assert ( + pyo.value(m.fs.heat_exchanger.cold_side_outlet.pressure[0]) + == pytest.approx(111418.71399, abs=1) + ) + + +@pytest.mark.integration +def test_units_dP(model_dP): + assert_units_consistent(model_dP.fs.heat_exchanger) From 599ae34c5123db7fedb52224be3150df5021a3d8 Mon Sep 17 00:00:00 2001 From: Doug A Date: Wed, 20 Mar 2024 14:50:50 -0400 Subject: [PATCH 11/38] move changes from other branch --- .../cross_flow_heat_exchanger_1D.py | 213 +++++++----------- .../unit_models/heat_exchanger_common.py | 101 +++------ .../power_generation/unit_models/heater_1D.py | 160 +++++-------- .../test_cross_flow_heat_exchanger_1D.py | 73 +++--- .../unit_models/tests/test_heater_1D.py | 177 ++++++--------- 5 files changed, 284 insertions(+), 440 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index b6f00e2aad..e1a8dcc2cd 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -15,60 +15,30 @@ Discretization based on tube rows """ -from __future__ import division -# Import Python libraries -import math - -import pyomo.common.config -import pyomo.opt # Import Pyomo libraries from pyomo.environ import ( - SolverFactory, - Var, - Param, - Constraint, value, - TerminationCondition, - exp, - sqrt, log, - sin, - cos, - SolverStatus, units as pyunits, ) -from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool +import pyomo.common.config +import pyomo.opt +from pyomo.common.config import ConfigValue, In, Bool +from pyomo.network import Port # Import IDAES cores -from idaes.core import ( - ControlVolume1DBlock, - UnitModelBlockData, - declare_process_block_class, - MaterialBalanceType, - EnergyBalanceType, - MomentumBalanceType, - FlowDirection, - UnitModelBlockData, - useDefault, -) +from idaes.core import declare_process_block_class from idaes.core.util.constants import Constants as const import idaes.core.util.scaling as iscale -from pyomo.dae import DerivativeVar -from pyomo.network import Port from idaes.core.solvers import get_solver -from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.misc import add_object_reference from idaes.core.util.exceptions import ConfigurationError, BurntToast import idaes.logger as idaeslog from idaes.core.util.tables import create_stream_table_dataframe - -from idaes.models.unit_models.heater import _make_heater_config_block from idaes.models.unit_models.heat_exchanger import ( HeatExchangerFlowPattern, - hx_process_config, - add_hx_references, ) from idaes.models.unit_models.heat_exchanger_1D import HeatExchanger1DData from idaes.models_extra.power_generation.unit_models import heat_exchanger_common @@ -178,8 +148,12 @@ def _make_geometry(self): add_object_reference(self, "area_flow_tube", tube.area) # total tube length of flow path add_object_reference(self, "length_flow_tube", tube.length) - heat_exchanger_common._make_geometry_common(self, shell_units=shell_units) - heat_exchanger_common._make_geometry_tube(self, shell_units=shell_units) + heat_exchanger_common._make_geometry_common( + self, shell_units=shell_units + ) # pylint: disable=W0212 + heat_exchanger_common._make_geometry_tube( + self, shell_units=shell_units + ) # pylint: disable=W0212 def _make_performance(self): """ @@ -219,7 +193,7 @@ def _make_performance(self): f"phases on the hot side and {len(self.config.cold_side.property_package.phase_list)} " "phases on the cold side." ) - + p_hot = self.config.hot_side.property_package.phase_list.at(1) pobj_hot = self.config.hot_side.property_package.get_phase(p_hot) p_cold = self.config.cold_side.property_package.phase_list.at(1) @@ -257,7 +231,7 @@ def _make_performance(self): add_object_reference(self, "deltaP_tube", tube.deltaP) tube_has_pressure_change = True - heat_exchanger_common._make_performance_common( + heat_exchanger_common._make_performance_common( # pylint: disable=W0212 self, shell=shell, shell_units=shell_units, @@ -265,7 +239,7 @@ def _make_performance(self): make_reynolds=True, make_nusselt=True, ) - heat_exchanger_common._make_performance_tube( + heat_exchanger_common._make_performance_tube( # pylint: disable=W0212 self, tube=tube, tube_units=tube_units, @@ -316,12 +290,11 @@ def N_Nu_shell_eqn(b, t, x): def heat_tube_eqn(b, t, x): return b.heat_tube[t, x] == ( b.hconv_tube[t, x] - * const.pi + * const.pi * pyunits.convert(b.di_tube, to_units=tube_units["length"]) - * b.nrow_inlet - * b.ncol_tube * ( - b.temp_wall_tube[t, x] - tube.properties[t, x].temperature - ) + * b.nrow_inlet + * b.ncol_tube + * (b.temp_wall_tube[t, x] - tube.properties[t, x].temperature) ) # Heat to wall per length on shell side @@ -331,11 +304,8 @@ def heat_tube_eqn(b, t, x): doc="heat per length on shell side", ) def heat_shell_eqn(b, t, x): - return b.heat_shell[ - t, x - ] * b.length_flow_shell == pyunits.convert( - b.length_flow_tube, - to_units=shell_units["length"] + return b.heat_shell[t, x] * b.length_flow_shell == pyunits.convert( + b.length_flow_tube, to_units=shell_units["length"] ) * b.hconv_shell_total[ t, x ] * const.pi * b.do_tube * b.nrow_inlet * b.ncol_tube * ( @@ -350,19 +320,18 @@ def heat_shell_eqn(b, t, x): doc="tube side wall temperature", ) def temp_wall_tube_eqn(b, t, x): - return ( - b.hconv_tube[t, x] - * (tube.properties[t, x].temperature - b.temp_wall_tube[t, x]) - * ( - pyunits.convert( - b.thickness_tube / 2 / b.therm_cond_wall, - to_units=1 / tube_units["heat_transfer_coefficient"] - ) + b.rfouling_tube - ) - == b.temp_wall_tube[t, x] - pyunits.convert( - b.temp_wall_center[t, x], - to_units=tube_units["temperature"] + return b.hconv_tube[t, x] * ( + tube.properties[t, x].temperature - b.temp_wall_tube[t, x] + ) * ( + pyunits.convert( + b.thickness_tube / 2 / b.therm_cond_wall, + to_units=1 / tube_units["heat_transfer_coefficient"], ) + + b.rfouling_tube + ) == b.temp_wall_tube[ + t, x + ] - pyunits.convert( + b.temp_wall_center[t, x], to_units=tube_units["temperature"] ) # Shell side wall temperature @@ -390,13 +359,12 @@ def temp_wall_center_eqn(b, t, x): # control volumes (and out of the wall), hence the negative sign # on heat_accumulation_term return -heat_accumulation_term(b, t, x) == ( - b.heat_shell[t, x] * b.length_flow_shell / pyunits.convert( - b.length_flow_tube, - to_units=shell_units["length"] - ) + b.heat_shell[t, x] + * b.length_flow_shell + / pyunits.convert(b.length_flow_tube, to_units=shell_units["length"]) + pyunits.convert( b.heat_tube[t, x], - to_units=shell_units["power"]/shell_units["length"] + to_units=shell_units["power"] / shell_units["length"], ) ) @@ -410,25 +378,19 @@ def total_heat_duty(b, t): enth_out = b.hot_side.properties[t, z1].get_enthalpy_flow_terms(p_hot) return pyunits.convert( - enth_in - enth_out, # Hot side loses enthalpy - to_units=shell_units["power"] # Hot side isn't always the shell + enth_in - enth_out, # Hot side loses enthalpy + to_units=shell_units["power"], # Hot side isn't always the shell ) @self.Expression(self.flowsheet().config.time) def log_mean_delta_temperature(b, t): - dT0 = ( - b.hot_side.properties[t, z0].temperature - - pyunits.convert( - b.cold_side.properties[t, z0].temperature, - to_units=shell_units["temperature"] - ) + dT0 = b.hot_side.properties[t, z0].temperature - pyunits.convert( + b.cold_side.properties[t, z0].temperature, + to_units=shell_units["temperature"], ) - dT1 = ( - b.hot_side.properties[t, z1].temperature - - pyunits.convert( - b.cold_side.properties[t, z1].temperature, - to_units=shell_units["temperature"] - ) + dT1 = b.hot_side.properties[t, z1].temperature - pyunits.convert( + b.cold_side.properties[t, z1].temperature, + to_units=shell_units["temperature"], ) return (dT0 - dT1) / log(dT0 / dT1) @@ -535,83 +497,81 @@ def initialize_build( # Set tube thermal conductivity to a small value to avoid IPOPT unable to solve initially therm_cond_wall_save = blk.therm_cond_wall.value - blk.therm_cond_wall = 0.05 + blk.therm_cond_wall.set_value(0.05) # In Step 2, fix tube metal temperatures fix fluid state variables (enthalpy/temperature and pressure) # calculate maximum heat duty assuming infinite area and use half of the maximum duty as initial guess to calculate outlet temperature for t in blk.flowsheet().config.time: - # TODO we first access cp during initialization. That could pose a problem if it is + # TODO we first access cp during initialization. That could pose a problem if it is # converted to a Var-Constraint pair instead of being a giant Expression like it is # presently. mcp_hot_side = value( pyunits.convert( - hot_side.properties[t, 0].flow_mol * hot_side.properties[t, 0].cp_mol, - to_units=shell_units["power"]/shell_units["temperature"] + hot_side.properties[t, 0].flow_mol + * hot_side.properties[t, 0].cp_mol, + to_units=shell_units["power"] / shell_units["temperature"], ) ) T_in_hot_side = value( pyunits.convert( hot_side.properties[t, 0].temperature, - to_units=shell_units["temperature"] + to_units=shell_units["temperature"], ) ) P_in_hot_side = value(hot_side.properties[t, 0].pressure) if blk.config.flow_type == HeatExchangerFlowPattern.cocurrent: mcp_cold_side = value( pyunits.convert( - cold_side.properties[t, 0].flow_mol * cold_side.properties[t, 0].cp_mol, - to_units=shell_units["power"]/shell_units["temperature"] + cold_side.properties[t, 0].flow_mol + * cold_side.properties[t, 0].cp_mol, + to_units=shell_units["power"] / shell_units["temperature"], ) ) T_in_cold_side = value( pyunits.convert( cold_side.properties[t, 0].temperature, - to_units=shell_units["temperature"] + to_units=shell_units["temperature"], ) ) P_in_cold_side = value(cold_side.properties[t, 0].pressure) T_out_max = ( - mcp_cold_side * T_in_cold_side - + mcp_hot_side * T_in_hot_side + mcp_cold_side * T_in_cold_side + mcp_hot_side * T_in_hot_side ) / (mcp_cold_side + mcp_hot_side) q_guess = mcp_cold_side * (T_out_max - T_in_cold_side) / 2 - temp_out_cold_side_guess = ( - T_in_cold_side + q_guess / mcp_cold_side - ) + temp_out_cold_side_guess = T_in_cold_side + q_guess / mcp_cold_side cold_side.properties[t, 1].temperature.fix( pyunits.convert_value( temp_out_cold_side_guess, from_units=shell_units["temperature"], - to_units=cold_units["temperature"] + to_units=cold_units["temperature"], ) ) - temp_out_hot_side_guess = ( - T_in_cold_side - q_guess / mcp_hot_side - ) + temp_out_hot_side_guess = T_in_cold_side - q_guess / mcp_hot_side hot_side.properties[t, 1].temperature.fix( pyunits.convert_value( temp_out_hot_side_guess, from_units=shell_units["temperature"], - to_units=hot_units["temperature"] + to_units=hot_units["temperature"], ) ) elif blk.config.flow_type == HeatExchangerFlowPattern.countercurrent: mcp_cold_side = value( pyunits.convert( - cold_side.properties[t, 1].flow_mol * cold_side.properties[t, 1].cp_mol, - to_units=shell_units["power"]/shell_units["temperature"] + cold_side.properties[t, 1].flow_mol + * cold_side.properties[t, 1].cp_mol, + to_units=shell_units["power"] / shell_units["temperature"], ) ) T_in_cold_side = value( pyunits.convert( cold_side.properties[t, 1].temperature, - to_units=shell_units["temperature"] + to_units=shell_units["temperature"], ) ) P_in_cold_side = value(cold_side.properties[t, 1].pressure) @@ -620,26 +580,22 @@ def initialize_build( q_guess = mcp_cold_side * (T_in_hot_side - T_in_cold_side) / 2 else: q_guess = mcp_hot_side * (T_in_hot_side - T_in_cold_side) / 2 - - temp_out_cold_side_guess = ( - T_in_cold_side + q_guess / mcp_cold_side - ) + + temp_out_cold_side_guess = T_in_cold_side + q_guess / mcp_cold_side cold_side.properties[t, 0].temperature.fix( pyunits.convert_value( temp_out_cold_side_guess, from_units=shell_units["temperature"], - to_units=cold_units["temperature"] + to_units=cold_units["temperature"], ) ) - temp_out_hot_side_guess = ( - T_in_hot_side - q_guess / mcp_hot_side - ) + temp_out_hot_side_guess = T_in_hot_side - q_guess / mcp_hot_side hot_side.properties[t, 1].temperature.fix( pyunits.convert_value( temp_out_hot_side_guess, from_units=shell_units["temperature"], - to_units=hot_units["temperature"] + to_units=hot_units["temperature"], ) ) @@ -649,7 +605,7 @@ def initialize_build( "or countercurrent flow by parent model. Please open an " "issue on the IDAES Github so this error can be fixed." ) - + for z in cold_side.length_domain: hot_side.properties[t, z].temperature.fix( value( @@ -667,13 +623,14 @@ def initialize_build( value( pyunits.convert( hot_side.properties[t, z].temperature, - to_units=shell_units["temperature"] + to_units=shell_units["temperature"], ) + pyunits.convert( cold_side.properties[t, z].temperature, - to_units=shell_units["temperature"] + to_units=shell_units["temperature"], ) - ) / 2 + ) + / 2 ) blk.temp_wall_shell[t, z].set_value(blk.temp_wall_center[t, z].value) @@ -681,7 +638,7 @@ def initialize_build( pyunits.convert_value( blk.temp_wall_center[t, z].value, from_units=shell_units["temperature"], - to_units=tube_units["temperature"] + to_units=tube_units["temperature"], ) ) @@ -789,23 +746,16 @@ def cst(con, sf): shell, shell_has_pressure_change, make_reynolds=True, - make_nusselt=True + make_nusselt=True, ) heat_exchanger_common._scale_tube( - self, - tube, - tube_has_pressure_change, - make_reynolds=True, - make_nusselt=True + self, tube, tube_has_pressure_change, make_reynolds=True, make_nusselt=True ) sf_area_per_length_shell = value( self.length_flow_shell / ( - pyunits.convert( - self.length_flow_tube, - to_units=shell_units["length"] - ) + pyunits.convert(self.length_flow_tube, to_units=shell_units["length"]) * const.pi * self.do_tube * self.nrow_inlet @@ -826,16 +776,17 @@ def cst(con, sf): ssf(self.temp_wall_shell[t, z], sf_T_shell) cst(self.temp_wall_shell_eqn[t, z], sf_T_shell) - sf_hconv_shell_conv = gsf(self.hconv_shell_conv[t, z]) s_Q_shell = sgsf( shell.heat[t, z], sf_hconv_shell_conv * sf_area_per_length_shell * sf_T_shell, ) - cst(self.heat_shell_eqn[t, z], s_Q_shell * value(self.length_flow_shell)) + cst( + self.heat_shell_eqn[t, z], s_Q_shell * value(self.length_flow_shell) + ) # Geometric mean is overkill for most reasonable cases, but it mitigates # damage done when one stream has an unset scaling factor - ssf(self.temp_wall_center[t, z], (sf_T_shell * sf_T_tube)**0.5) + ssf(self.temp_wall_center[t, z], (sf_T_shell * sf_T_tube) ** 0.5) cst(self.temp_wall_center_eqn[t, z], (sf_Q_tube * s_Q_shell) ** 0.5) def _get_performance_contents(self, time_point=0): @@ -852,7 +803,9 @@ def _get_performance_contents(self, time_point=0): expr_dict["HX Area"] = self.total_heat_transfer_area expr_dict["Delta T Driving"] = self.log_mean_delta_temperature[time_point] expr_dict["Total Heat Duty"] = self.total_heat_duty[time_point] - expr_dict["Average HX Coefficient"] = self.overall_heat_transfer_coefficient[time_point] + expr_dict["Average HX Coefficient"] = self.overall_heat_transfer_coefficient[ + time_point + ] return {"vars": var_dict, "exprs": expr_dict} diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index f60ca2196a..c29b44bc9d 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -15,58 +15,26 @@ Discretization based on tube rows """ -from __future__ import division - -# Import Python libraries -import math # Import Pyomo libraries from pyomo.environ import ( - SolverFactory, Var, Param, - Constraint, value, - TerminationCondition, - exp, - sqrt, - log, - sin, - cos, - SolverStatus, units as pyunits, ) -from pyomo.common.config import ConfigBlock, ConfigValue, In from pyomo.util.calc_var_value import calculate_variable_from_constraint from pyomo.dae import DerivativeVar # Import IDAES cores -from idaes.core import ( - ControlVolume1DBlock, - UnitModelBlockData, - declare_process_block_class, - MaterialBalanceType, - EnergyBalanceType, - MomentumBalanceType, - FlowDirection, - UnitModelBlockData, - useDefault, -) from idaes.core.util.exceptions import ConfigurationError from idaes.core.util.constants import Constants as const import idaes.core.util.scaling as iscale -from idaes.core.solvers import get_solver -from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.misc import add_object_reference -import idaes.logger as idaeslog -from idaes.core.util.tables import create_stream_table_dataframe __author__ = "Jinliang Ma, Douglas Allan" -# Set up logger -_log = idaeslog.getLogger(__name__) - def _make_geometry_common(blk, shell_units): # Number of tube columns in the cross section plane perpendicular to shell side fluid flow (y direction) @@ -76,7 +44,9 @@ def _make_geometry_common(blk, shell_units): # Number of segments of tube bundles blk.nseg_tube = Var( - initialize=10.0, doc="Number of segments of tube bundles", units=pyunits.dimensionless + initialize=10.0, + doc="Number of segments of tube bundles", + units=pyunits.dimensionless, ) # Number of inlet tube rows @@ -252,10 +222,10 @@ def _make_performance_common( # Heat transfer resistance due to the fouling on shell side blk.rfouling_shell = Param( - units=1/shell_units["heat_transfer_coefficient"], + units=1 / shell_units["heat_transfer_coefficient"], initialize=0.0001, mutable=True, - doc="Fouling resistance on tube side" + doc="Fouling resistance on tube side", ) # Correction factor for convective heat transfer coefficient on shell side @@ -307,6 +277,7 @@ def _make_performance_common( units=shell_units["energy"] / shell_units["length"], doc="Tube wall heat holdup per length of shell", ) + @blk.Constraint( blk.flowsheet().config.time, shell.length_domain, @@ -369,7 +340,6 @@ def heat_holdup_eqn(b, t, x): if shell_has_pressure_change: # Friction factor on shell side - # TODO does this have units? blk.friction_factor_shell = Var( blk.flowsheet().config.time, shell.length_domain, @@ -418,7 +388,7 @@ def N_Re_shell_eqn(b, t, x): * shell.properties[t, x].mw ) - if shell_has_pressure_change == True: + if shell_has_pressure_change: # Friction factor on shell side if blk.config.tube_arrangement == "in-line": @@ -495,7 +465,7 @@ def hconv_shell_conv_eqn(b, t, x): doc="Total convective heat transfer coefficient on shell side", ) def hconv_shell_total(b, t, x): - # Retain in case we add back radiation + # Retain in case we add back radiation return b.hconv_shell_conv[t, x] @@ -524,8 +494,8 @@ def _make_performance_tube( blk.rfouling_tube = Param( initialize=0.0, mutable=True, - units=1/tube_units["heat_transfer_coefficient"], - doc="fouling resistance on tube side" + units=1 / tube_units["heat_transfer_coefficient"], + doc="fouling resistance on tube side", ) # Correction factor for convective heat transfer coefficient on tube side blk.fcorrection_htc_tube = Var( @@ -542,7 +512,9 @@ def _make_performance_tube( blk.flowsheet().config.time, tube.length_domain, initialize=500, - units=tube_units["temperature"], # Want to be in shell units for consistency in equations + units=tube_units[ + "temperature" + ], # Want to be in shell units for consistency in equations doc="boundary wall temperature on tube side", ) if make_reynolds: @@ -566,9 +538,8 @@ def _make_performance_tube( doc="Reynolds number on tube side", bounds=(1e-7, None), ) - if tube_has_pressure_change == True: + if tube_has_pressure_change: # Friction factor on tube side - # TODO does this have units? blk.friction_factor_tube = Var( blk.flowsheet().config.time, tube.length_domain, @@ -702,6 +673,7 @@ def deltaP_tube_eqn(b, t, x): ) if make_nusselt: + @blk.Constraint( blk.flowsheet().config.time, tube.length_domain, @@ -714,28 +686,28 @@ def hconv_tube_eqn(b, t, x): * tube.properties[t, x].therm_cond_phase["Vap"] * b.fcorrection_htc_tube ) + + def _scale_common(blk, shell, shell_has_pressure_change, make_reynolds, make_nusselt): def gsf(obj): return iscale.get_scaling_factor(obj, default=1, warning=True) + def ssf(obj, sf): iscale.set_scaling_factor(obj, sf, overwrite=False) + def cst(con, sf): iscale.constraint_scaling_transform(con, sf, overwrite=False) + sgsf = iscale.set_and_get_scaling_factor - sf_do_tube = iscale.get_scaling_factor( - blk.do_tube, default=1 / value(blk.do_tube) - ) + sf_do_tube = iscale.get_scaling_factor(blk.do_tube, default=1 / value(blk.do_tube)) - sf_di_tube = iscale.get_scaling_factor( - blk.do_tube, default=1 / value(blk.di_tube) - ) + sf_di_tube = iscale.get_scaling_factor(blk.do_tube, default=1 / value(blk.di_tube)) calculate_variable_from_constraint( - blk.area_flow_shell_min, - blk.area_flow_shell_min_eqn + blk.area_flow_shell_min, blk.area_flow_shell_min_eqn ) sf_area_flow_shell_min = iscale.get_scaling_factor( - blk.area_flow_shell_min, default=1/value(blk.area_flow_shell_min) + blk.area_flow_shell_min, default=1 / value(blk.area_flow_shell_min) ) for t in blk.flowsheet().time: for z in shell.length_domain: @@ -743,10 +715,10 @@ def cst(con, sf): if make_reynolds: # FIXME get better scaling later - ssf(blk.v_shell[t, z], 1/10) + ssf(blk.v_shell[t, z], 1 / 10) cst(blk.v_shell_eqn[t, z], sf_flow_mol_shell) - #FIXME should get scaling of N_Re from defining eqn + # FIXME should get scaling of N_Re from defining eqn sf_N_Re_shell = sgsf(blk.N_Re_shell[t, z], 1e-4) sf_visc_d_shell = gsf(shell.properties[t, z].visc_d_phase["Vap"]) @@ -755,7 +727,7 @@ def cst(con, sf): sf_k_shell = gsf(shell.properties[t, z].therm_cond_phase["Vap"]) sf_N_Nu_shell = sgsf( - blk.N_Nu_shell[t, z], 1 / 0.33 * sf_N_Re_shell ** 0.6 + blk.N_Nu_shell[t, z], 1 / 0.33 * sf_N_Re_shell**0.6 ) cst(blk.N_Nu_shell_eqn[t, z], sf_N_Nu_shell) @@ -764,13 +736,15 @@ def cst(con, sf): ) cst(blk.hconv_shell_conv_eqn[t, z], sf_hconv_shell_conv * sf_do_tube) - # FIXME estimate from parameters if blk.config.has_holdup: s_U_holdup = gsf(blk.heat_holdup[t, z]) cst(blk.heat_holdup_eqn[t, z], s_U_holdup) -def _scale_tube(blk, tube, tube_has_presure_change, make_reynolds, make_nusselt): + +def _scale_tube( + blk, tube, tube_has_presure_change, make_reynolds, make_nusselt +): # pylint: disable=W0613 def gsf(obj): return iscale.get_scaling_factor(obj, default=1, warning=True) @@ -782,18 +756,13 @@ def cst(con, sf): sgsf = iscale.set_and_get_scaling_factor - sf_di_tube = iscale.get_scaling_factor( - blk.do_tube, default=1 / value(blk.di_tube) - ) - sf_do_tube = iscale.get_scaling_factor( - blk.do_tube, default=1 / value(blk.do_tube) - ) + sf_di_tube = iscale.get_scaling_factor(blk.do_tube, default=1 / value(blk.di_tube)) for t in blk.flowsheet().time: for z in tube.length_domain: if make_reynolds: # FIXME get better scaling later - ssf(blk.v_tube[t, z], 1/20) + ssf(blk.v_tube[t, z], 1 / 20) sf_flow_mol_tube = gsf(tube.properties[t, z].flow_mol) cst(blk.v_tube_eqn[t, z], sf_flow_mol_tube) @@ -807,11 +776,11 @@ def cst(con, sf): sf_k_tube = gsf(tube.properties[t, z].therm_cond_phase["Vap"]) sf_N_Nu_tube = sgsf( - blk.N_Nu_tube[t, z], 1 / 0.023 * sf_N_Re_tube ** 0.8 + blk.N_Nu_tube[t, z], 1 / 0.023 * sf_N_Re_tube**0.8 ) cst(blk.N_Nu_tube_eqn[t, z], sf_N_Nu_tube) sf_hconv_tube = sgsf( blk.hconv_tube[t, z], sf_N_Nu_tube * sf_k_tube / sf_di_tube ) - cst(blk.hconv_tube_eqn[t, z], sf_hconv_tube * sf_di_tube) \ No newline at end of file + cst(blk.hconv_tube_eqn[t, z], sf_hconv_tube * sf_di_tube) diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 2e791d032d..344687e9c5 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -15,33 +15,19 @@ Discretization based on tube rows """ -from __future__ import division - -# Import Python libraries -import math - # Import Pyomo libraries from pyomo.environ import ( - SolverFactory, + assert_optimal_termination, Var, - Param, - Constraint, value, - TerminationCondition, - exp, - sqrt, - log, - sin, - cos, - SolverStatus, units as pyunits, ) from pyomo.common.config import ConfigBlock, ConfigValue, In +from pyomo.util.calc_var_value import calculate_variable_from_constraint # Import IDAES cores from idaes.core import ( ControlVolume1DBlock, - UnitModelBlockData, declare_process_block_class, MaterialBalanceType, EnergyBalanceType, @@ -50,32 +36,29 @@ UnitModelBlockData, useDefault, ) -from idaes.core.util.constants import Constants as const import idaes.core.util.scaling as iscale -from pyomo.dae import DerivativeVar from idaes.core.solvers import get_solver -from pyomo.util.calc_var_value import calculate_variable_from_constraint from idaes.core.util.config import is_physical_parameter_block from idaes.core.util.misc import add_object_reference import idaes.logger as idaeslog from idaes.core.util.tables import create_stream_table_dataframe from idaes.core.util.model_statistics import degrees_of_freedom - -from heat_exchanger_common import _make_geometry_common, _make_performance_common, _scale_common +from idaes.models_extra.power_generation.unit_models.heat_exchanger_common import ( + _make_geometry_common, # pylint: disable=W0212 + _make_performance_common, # pylint: disable=W0212 + _scale_common, # pylint: disable=W0212 +) __author__ = "Jinliang Ma, Douglas Allan" -# Set up logger -_log = idaeslog.getLogger(__name__) - @declare_process_block_class("Heater1D") class Heater1DData(UnitModelBlockData): """Standard Heat Exchanger Cross Flow Unit Model Class.""" # Template for config arguments for shell and tube side - _SideTemplate = ConfigBlock() - _SideTemplate.declare( + CONFIG = ConfigBlock() + CONFIG.declare( "dynamic", ConfigValue( default=useDefault, @@ -89,7 +72,7 @@ class Heater1DData(UnitModelBlockData): **False** - set as a steady-state model.}""", ), ) - _SideTemplate.declare( + CONFIG.declare( "has_holdup", ConfigValue( default=False, @@ -103,20 +86,19 @@ class Heater1DData(UnitModelBlockData): **False** - do not construct holdup terms}""", ), ) - _SideTemplate.declare( + CONFIG.declare( "has_fluid_holdup", ConfigValue( default=False, - domain=In([True, False]), + domain=In([False]), description="Holdup construction flag", doc="""Indicates whether holdup terms for the fluid should be constructed or not. **default** - False. **Valid values:** { - **True** - construct holdup terms, **False** - do not construct holdup terms}""", ), ) - _SideTemplate.declare( + CONFIG.declare( "material_balance_type", ConfigValue( default=MaterialBalanceType.componentTotal, @@ -132,7 +114,7 @@ class Heater1DData(UnitModelBlockData): **MaterialBalanceType.total** - use total material balance.}""", ), ) - _SideTemplate.declare( + CONFIG.declare( "energy_balance_type", ConfigValue( default=EnergyBalanceType.enthalpyTotal, @@ -148,7 +130,7 @@ class Heater1DData(UnitModelBlockData): **EnergyBalanceType.energyPhase** - energy balances for each phase.}""", ), ) - _SideTemplate.declare( + CONFIG.declare( "momentum_balance_type", ConfigValue( default=MomentumBalanceType.pressureTotal, @@ -164,7 +146,7 @@ class Heater1DData(UnitModelBlockData): **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""", ), ) - _SideTemplate.declare( + CONFIG.declare( "has_pressure_change", ConfigValue( default=False, @@ -178,18 +160,7 @@ class Heater1DData(UnitModelBlockData): **False** - exclude pressure change terms.}""", ), ) - _SideTemplate.declare( - "has_phase_equilibrium", - ConfigValue( - default=False, - domain=In([True, False]), - description="Phase equilibrium term construction flag", - doc="""Argument to enable phase equilibrium on the shell side. -- True - include phase equilibrium term -- False - do not include phase equilibrium term""", - ), - ) - _SideTemplate.declare( + CONFIG.declare( "property_package", ConfigValue( domain=is_physical_parameter_block, @@ -200,7 +171,7 @@ class Heater1DData(UnitModelBlockData): - a ParameterBlock object""", ), ) - _SideTemplate.declare( + CONFIG.declare( "property_package_args", ConfigValue( default=None, @@ -212,9 +183,7 @@ class Heater1DData(UnitModelBlockData): - a dict (see property package for documentation)""", ), ) - # TODO : We should probably think about adding a consistency check for the - # TODO : discretization methods as well. - _SideTemplate.declare( + CONFIG.declare( "transformation_method", ConfigValue( default=useDefault, @@ -223,7 +192,7 @@ class Heater1DData(UnitModelBlockData): documentation for supported transformations.""", ), ) - _SideTemplate.declare( + CONFIG.declare( "transformation_scheme", ConfigValue( default=useDefault, @@ -233,7 +202,7 @@ class Heater1DData(UnitModelBlockData): ), ) - CONFIG = _SideTemplate + CONFIG = CONFIG # Common config args for both sides CONFIG.declare( @@ -252,8 +221,8 @@ class Heater1DData(UnitModelBlockData): default=3, 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)""", + doc="""If using collocation, number of collocation points to use + per finite element when discretizing length domain (default=3)""", ), ) CONFIG.declare( @@ -265,15 +234,6 @@ class Heater1DData(UnitModelBlockData): doc="tube arrangement could be in-line or staggered", ), ) - CONFIG.declare( - "has_radiation", - ConfigValue( - default=False, - domain=In([False, True]), - description="Has side 2 gas radiation", - doc="define if shell side gas radiation is to be considered", - ), - ) def build(self): """ @@ -286,7 +246,7 @@ def build(self): None """ # Call UnitModel.build to setup dynamics - super(Heater1DData, self).build() + super().build() # Set flow directions for the control volume blocks and specify # dicretization if not specified. @@ -294,7 +254,7 @@ def build(self): if self.config.transformation_method is useDefault: self.config.transformation_method = "dae.finite_difference" if self.config.transformation_scheme is useDefault: - self.config.transformation_scheme = "FORWARD" + self.config.transformation_scheme = "BACKWARD" if self.config.property_package_args is None: self.config.property_package_args = {} @@ -315,13 +275,13 @@ def build(self): self.control_volume.add_state_blocks( information_flow=set_direction_shell, - has_phase_equilibrium=self.config.has_phase_equilibrium, + has_phase_equilibrium=False, ) # Populate shell self.control_volume.add_material_balances( balance_type=self.config.material_balance_type, - has_phase_equilibrium=self.config.has_phase_equilibrium, + has_phase_equilibrium=False, ) self.control_volume.add_energy_balances( @@ -360,13 +320,13 @@ def _make_geometry(self): add_object_reference(self, "area_flow_shell", self.control_volume.area) add_object_reference(self, "length_flow_shell", self.control_volume.length) _make_geometry_common(self, shell_units=units) + @self.Expression( doc="Common performance equations expect this expression to be here" ) def length_flow_tube(b): return b.nseg_tube * b.length_tube_seg - def _make_performance(self): """ Constraints for unit model. @@ -381,7 +341,7 @@ def _make_performance(self): self.flowsheet().config.time, initialize=1e6, units=pyunits.W, - doc="Heat duty provided to heater " "through resistive heating", + doc="Heat duty provided to heater through resistive heating", ) units = self.config.property_package.get_metadata().derived_units _make_performance_common( @@ -424,7 +384,10 @@ def heat_shell_eqn(b, t, x): return b.control_volume.heat[t, x] * b.length_flow_shell == ( b.hconv_shell_total[t, x] * b.total_heat_transfer_area - * (b.temp_wall_shell[t, x] - b.control_volume.properties[t, x].temperature) + * ( + b.temp_wall_shell[t, x] + - b.control_volume.properties[t, x].temperature + ) ) # Shell side wall temperature @@ -436,8 +399,13 @@ def heat_shell_eqn(b, t, x): def temp_wall_shell_eqn(b, t, x): return ( b.hconv_shell_total[t, x] - * (b.control_volume.properties[t, x].temperature - b.temp_wall_shell[t, x]) - * (b.thickness_tube / b.therm_cond_wall + b.rfouling_shell) + * ( + b.control_volume.properties[t, x].temperature + - b.temp_wall_shell[t, x] + ) + # Divide thickness by 2 in order to represent center of hollow tube instead of + # interior edge of hollow tube + * (b.thickness_tube / (2 * b.therm_cond_wall) + b.rfouling_shell) == b.temp_wall_shell[t, x] - b.temp_wall_center[t, x] ) @@ -448,10 +416,12 @@ def temp_wall_shell_eqn(b, t, x): ) def temp_wall_center_eqn(b, t, x): return heat_accumulation_term(b, t, x) == ( - -b.control_volume.heat[t, x] + b.electric_heat_duty[t] / b.length_flow_shell + -b.control_volume.heat[t, x] + + b.electric_heat_duty[t] / b.length_flow_shell ) def set_initial_condition(self): + # TODO how to deal with holdup for fluid side? if self.config.dynamic is True: self.heat_accumulation[:, :].value = 0 self.heat_accumulation[0, :].fix(0) @@ -509,13 +479,16 @@ def initialize_build(blk, state_args=None, outlvl=0, solver="ipopt", optarg=None blk.control_volume.heat[t, x].fix( value(blk.electric_heat_duty[t] / blk.length_flow_shell) ) + + if blk.config.has_pressure_change: + blk.control_volume.pressure.fix() + blk.control_volume.length.fix() assert degrees_of_freedom(blk.control_volume) == 0 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk.control_volume, tee=slc.tee) - assert res.solver.termination_condition == TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert_optimal_termination(res) init_log.info_high("Initialization Step 2 Complete.") blk.control_volume.length.unfix() @@ -529,17 +502,16 @@ def initialize_build(blk, state_args=None, outlvl=0, solver="ipopt", optarg=None calc_var(blk.heat_holdup[t, x], blk.heat_holdup_eqn[t, x]) blk.temp_wall_center[t, x].unfix() - # fixed = blk.control_volume.temperature[t,x].fixed - # blk.control_volume.temperature[t,x].fix() - # calc_var() + if blk.config.has_pressure_change: + blk.control_volume.pressure.unfix() + blk.control_volume.pressure[:, 0].fix() assert degrees_of_freedom(blk) == 0 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) - assert res.solver.termination_condition == TerminationCondition.optimal - assert res.solver.status == SolverStatus.ok + assert_optimal_termination(res) init_log.info_high("Initialization Step 3 Complete.") @@ -555,14 +527,12 @@ def ssf(obj, sf): def cst(con, sf): iscale.constraint_scaling_transform(con, sf, overwrite=False) - sgsf = iscale.set_and_get_scaling_factor - _scale_common( self, self.control_volume, self.config.has_pressure_change, make_reynolds=True, - make_nusselt=True + make_nusselt=True, ) sf_d_tube = iscale.get_scaling_factor( @@ -574,24 +544,11 @@ def cst(con, sf): sf_hconv_conv = gsf(self.hconv_shell_conv[t, z]) cst(self.hconv_shell_conv_eqn[t, z], sf_hconv_conv * sf_d_tube) - if self.config.has_radiation: - sf_hconv_rad = 1 # FIXME Placeholder - sf_hconv_total = 1 / (1 / sf_hconv_conv + 1 / sf_hconv_rad) - else: - sf_hconv_total = sf_hconv_conv - - # FIXME try to do this rigorously later on sf_T = gsf(self.control_volume.properties[t, z].temperature) ssf(self.temp_wall_shell[t, z], sf_T) ssf(self.temp_wall_center[t, z], sf_T) - sf_area_per_length = value( - self.length_flow_shell / self.total_heat_transfer_area - ) - s_Q = sgsf( - self.control_volume.heat[t, z], - sf_hconv_total * sf_area_per_length * sf_T, - ) + s_Q = gsf(self.control_volume.heat[t, z]) ssf(self.electric_heat_duty[t], s_Q / value(self.length_flow_shell)) cst(self.heat_shell_eqn[t, z], s_Q * value(self.length_flow_shell)) ssf(self.temp_wall_center[t, z], sf_T) @@ -600,17 +557,10 @@ def cst(con, sf): def _get_performance_contents(self, time_point=0): var_dict = {} - # var_dict = { - # "HX Coefficient": self.overall_heat_transfer_coefficient[time_point] - # } - # var_dict["HX Area"] = self.area - # var_dict["Heat Duty"] = self.heat_duty[time_point] - # if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: - # var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]} + var_dict["Electric Heat Duty"] = self.electric_heat_duty[time_point] expr_dict = {} expr_dict["HX Area"] = self.total_heat_transfer_area - expr_dict["Electric Heat Duty"] = self.electric_heat_duty[time_point] return {"vars": var_dict, "exprs": expr_dict} diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index ce2780f193..2dbcb26c92 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -19,25 +19,29 @@ import idaes.core.util.scaling as iscale from idaes.models.unit_models import HeatExchangerFlowPattern from idaes.models.properties.modular_properties import GenericParameterBlock -from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop, EosType +from idaes.models_extra.power_generation.properties.natural_gas_PR import ( + get_prop, + EosType, +) from idaes.models_extra.power_generation.unit_models import CrossFlowHeatExchanger1D import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.solvers import get_solver # Set up solver -optarg={ +optarg = { # 'bound_push' : 1e-6, - 'constr_viol_tol': 1e-8, - 'nlp_scaling_method': 'user-scaling', - 'linear_solver': 'ma57', - 'OF_ma57_automatic_scaling': 'yes', - 'max_iter': 350, - 'tol': 1e-8, - 'halt_on_ampl_error': 'no', + "constr_viol_tol": 1e-8, + "nlp_scaling_method": "user-scaling", + "linear_solver": "ma57", + "OF_ma57_automatic_scaling": "yes", + "max_iter": 350, + "tol": 1e-8, + "halt_on_ampl_error": "no", } solver = get_solver("ipopt", options=optarg) + def _create_model(pressure_drop): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) @@ -125,15 +129,15 @@ def _create_model(pressure_drop): pp.set_default_scaling("mole_frac_comp", s, index=comp) pp.set_default_scaling("mole_frac_phase_comp", s, index=("Vap", comp)) pp.set_default_scaling("flow_mol_phase_comp", s * 1e-3, index=("Vap", comp)) - + shell = hx.hot_side tube = hx.cold_side iscale.set_scaling_factor(shell.area, 1e-1) # ssf(hx.shell.heat, 1e-6) iscale.set_scaling_factor(tube.area, 1) # ssf(hx.tube.heat, 1e-6) - iscale.set_scaling_factor(shell._enthalpy_flow, 1e-8) - iscale.set_scaling_factor(tube._enthalpy_flow, 1e-8) + iscale.set_scaling_factor(shell._enthalpy_flow, 1e-8) # pylint: disable=W0212 + iscale.set_scaling_factor(tube._enthalpy_flow, 1e-8) # pylint: disable=W0212 iscale.set_scaling_factor(shell.enthalpy_flow_dx, 1e-7) iscale.set_scaling_factor(tube.enthalpy_flow_dx, 1e-7) iscale.set_scaling_factor(hx.heat_holdup, 1e-8) @@ -142,6 +146,7 @@ def _create_model(pressure_drop): return m + def _check_model_statistics(m, deltaP): fixed_unused_var_set = { "fs.h2_side_prop_params.H2.omega", @@ -170,7 +175,7 @@ def _check_model_statistics(m, deltaP): } if not deltaP: fixed_unused_var_set.add("fs.heat_exchanger.delta_elevation") - + for var in mstat.fixed_unused_variables_set(m): assert var.name in fixed_unused_var_set @@ -194,11 +199,13 @@ def _check_model_statistics(m, deltaP): assert len(mstat.deactivated_constraints_set(m)) == 0 + @pytest.fixture def model_no_dP(): m = _create_model(pressure_drop=False) return m + @pytest.mark.component def test_initialization(model_no_dP): m = model_no_dP @@ -210,14 +217,12 @@ def test_initialization(model_no_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) - assert ( - pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) - == pytest.approx(485.34, abs=1e-1) - ) - assert ( - pyo.value(m.fs.heat_exchanger.cold_side_outlet.temperature[0]) - == pytest.approx(911.47, abs=1e-1) - ) + assert pyo.value( + m.fs.heat_exchanger.hot_side_outlet.temperature[0] + ) == pytest.approx(485.34, abs=1e-1) + assert pyo.value( + m.fs.heat_exchanger.cold_side_outlet.temperature[0] + ) == pytest.approx(911.47, abs=1e-1) @pytest.mark.integration @@ -225,12 +230,12 @@ def test_units(model_no_dP): assert_units_consistent(model_no_dP.fs.heat_exchanger) - @pytest.fixture def model_dP(): m = _create_model(pressure_drop=True) return m + @pytest.mark.component def test_initialization_dP(model_dP): m = model_dP @@ -243,21 +248,17 @@ def test_initialization_dP(model_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) - assert ( - pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) - == pytest.approx(485.34, abs=1e-1) - ) - assert ( - pyo.value(m.fs.heat_exchanger.cold_side_outlet.temperature[0]) - == pytest.approx(911.47, abs=1e-1) - ) - assert ( - pyo.value(m.fs.heat_exchanger.hot_side_outlet.pressure[0]) - == pytest.approx(118870.08569, abs=1) + assert pyo.value( + m.fs.heat_exchanger.hot_side_outlet.temperature[0] + ) == pytest.approx(485.34, abs=1e-1) + assert pyo.value( + m.fs.heat_exchanger.cold_side_outlet.temperature[0] + ) == pytest.approx(911.47, abs=1e-1) + assert pyo.value(m.fs.heat_exchanger.hot_side_outlet.pressure[0]) == pytest.approx( + 118870.08569, abs=1 ) - assert ( - pyo.value(m.fs.heat_exchanger.cold_side_outlet.pressure[0]) - == pytest.approx(111418.71399, abs=1) + assert pyo.value(m.fs.heat_exchanger.cold_side_outlet.pressure[0]) == pytest.approx( + 111418.71399, abs=1 ) diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py index a5388031e7..72c5229253 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -17,27 +17,30 @@ from idaes.core import FlowsheetBlock import idaes.core.util.scaling as iscale -from idaes.models.unit_models import HeatExchangerFlowPattern from idaes.models.properties.modular_properties import GenericParameterBlock -from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop, EosType +from idaes.models_extra.power_generation.properties.natural_gas_PR import ( + get_prop, + EosType, +) from idaes.models_extra.power_generation.unit_models import Heater1D import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.solvers import get_solver # Set up solver -optarg={ +optarg = { # 'bound_push' : 1e-6, - 'constr_viol_tol': 1e-8, - 'nlp_scaling_method': 'user-scaling', - 'linear_solver': 'ma57', - 'OF_ma57_automatic_scaling': 'yes', - 'max_iter': 350, - 'tol': 1e-8, - 'halt_on_ampl_error': 'no', + "constr_viol_tol": 1e-8, + "nlp_scaling_method": "user-scaling", + "linear_solver": "ma57", + "OF_ma57_automatic_scaling": "yes", + "max_iter": 350, + "tol": 1e-8, + "halt_on_ampl_error": "no", } solver = get_solver("ipopt", options=optarg) + def _create_model(pressure_drop): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) @@ -45,56 +48,43 @@ def _create_model(pressure_drop): **get_prop(["H2", "H2O", "Ar", "N2"], {"Vap"}, eos=EosType.IDEAL), doc="H2O + H2 gas property parameters", ) - m.fs.feed_heater = Heater1D( - property_package=m.fs.h2_side_prop_params, - has_holdup=True, - dynamic=False, - has_fluid_holdup=False, - has_pressure_change=pressure_drop, - finite_elements=4, - tube_arrangement="in-line", - transformation_method="dae.finite_difference", - transformation_scheme="BACKWARD", - ) - - hx = m.fs.heat_exchanger - - hx.hot_side_inlet.flow_mol.fix(2619.7) - hx.hot_side_inlet.temperature.fix(971.6) - hx.hot_side_inlet.pressure.fix(1.2e5) - hx.hot_side_inlet.mole_frac_comp[0, "H2"].fix(0.79715) - hx.hot_side_inlet.mole_frac_comp[0, "H2O"].fix(0.20177) - hx.hot_side_inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) - hx.hot_side_inlet.mole_frac_comp[0, "N2"].fix(0.00021589) - - hx.cold_side_inlet.flow_mol.fix(2619.7) - hx.cold_side_inlet.temperature.fix(446.21) - hx.cold_side_inlet.pressure.fix(1.2e5) - hx.cold_side_inlet.mole_frac_comp[0, "H2"].fix(0.36203) - hx.cold_side_inlet.mole_frac_comp[0, "H2O"].fix(0.63689) - hx.cold_side_inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) - hx.cold_side_inlet.mole_frac_comp[0, "N2"].fix(0.00021589) - - hx.di_tube.fix(0.0525018) - hx.thickness_tube.fix(0.0039116) - hx.length_tube_seg.fix(4.3) - hx.nseg_tube.fix(12) - hx.ncol_tube.fix(50) - hx.nrow_inlet.fix(25) - - hx.pitch_x.fix(0.1) - hx.pitch_y.fix(0.1) - hx.delta_elevation.fix(0) - hx.therm_cond_wall = 43.0 - hx.rfouling_tube = 0.0001 - hx.rfouling_shell = 0.0001 - hx.fcorrection_htc_tube.fix(1) - hx.fcorrection_htc_shell.fix(1) + m.fs.heater = Heater1D( + property_package=m.fs.h2_side_prop_params, + has_holdup=True, + dynamic=False, + has_fluid_holdup=False, + has_pressure_change=pressure_drop, + finite_elements=4, + tube_arrangement="in-line", + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + ) + + heater = m.fs.heater + + heater.inlet.flow_mol.fix(5102.5) + heater.inlet.temperature.fix(938.83) + heater.inlet.pressure.fix(1.2e5) + heater.inlet.mole_frac_comp[0, "H2"].fix(0.57375) + heater.inlet.mole_frac_comp[0, "H2O"].fix(0.42517) + heater.inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) + heater.inlet.mole_frac_comp[0, "N2"].fix(0.00021589) + + heater.di_tube.fix(0.0525018) + heater.thickness_tube.fix(0.0039116) + heater.pitch_x.fix(0.1) + heater.pitch_y.fix(0.1) + heater.length_tube_seg.fix(10) + heater.nseg_tube.fix(1) + heater.rfouling = 0.0001 + heater.fcorrection_htc_shell.fix(1) + heater.cp_wall = 502.4 if pressure_drop: - hx.fcorrection_dp_tube.fix(1) - hx.fcorrection_dp_shell.fix(1) + heater.fcorrection_dp_shell.fix(1) - hx.cp_wall.value = 502.4 + heater.ncol_tube.fix(40) + heater.nrow_inlet.fix(40) + heater.electric_heat_duty.fix(3.6504e06) pp = m.fs.h2_side_prop_params pp.set_default_scaling("enth_mol_phase", 1e-3) @@ -112,23 +102,19 @@ def _create_model(pressure_drop): pp.set_default_scaling("mole_frac_comp", s, index=comp) pp.set_default_scaling("mole_frac_phase_comp", s, index=("Vap", comp)) pp.set_default_scaling("flow_mol_phase_comp", s * 1e-3, index=("Vap", comp)) - - shell = hx.hot_side - tube = hx.cold_side + + shell = heater.control_volume iscale.set_scaling_factor(shell.area, 1e-1) - # ssf(hx.shell.heat, 1e-6) - iscale.set_scaling_factor(tube.area, 1) - # ssf(hx.tube.heat, 1e-6) - iscale.set_scaling_factor(shell._enthalpy_flow, 1e-8) - iscale.set_scaling_factor(tube._enthalpy_flow, 1e-8) + iscale.set_scaling_factor(shell.heat, 1e-6) + iscale.set_scaling_factor(shell._enthalpy_flow, 1e-8) # pylint: disable=W0212 iscale.set_scaling_factor(shell.enthalpy_flow_dx, 1e-7) - iscale.set_scaling_factor(tube.enthalpy_flow_dx, 1e-7) - iscale.set_scaling_factor(hx.heat_holdup, 1e-8) + iscale.set_scaling_factor(heater.heat_holdup, 1e-8) iscale.calculate_scaling_factors(m) return m + def _check_model_statistics(m, deltaP): fixed_unused_var_set = { "fs.h2_side_prop_params.H2.omega", @@ -156,18 +142,18 @@ def _check_model_statistics(m, deltaP): "fs.h2_side_prop_params.N2.cp_mol_ig_comp_coeff_H", } if not deltaP: - fixed_unused_var_set.add("fs.heat_exchanger.delta_elevation") - + fixed_unused_var_set.add("fs.heater.delta_elevation") + for var in mstat.fixed_unused_variables_set(m): assert var.name in fixed_unused_var_set unfixed_unused_var_set = { - "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,H2]", - "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,H2O]", - "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,Ar]", - "fs.heat_exchanger.hot_side.material_flow_dx[0.0,0.0,Vap,N2]", - "fs.heat_exchanger.hot_side.enthalpy_flow_dx[0.0,0.0,Vap]", - "fs.heat_exchanger.hot_side.pressure_dx[0.0,0.0]", + "fs.heater.control_volume.material_flow_dx[0.0,0.0,Vap,H2]", + "fs.heater.control_volume.material_flow_dx[0.0,0.0,Vap,H2O]", + "fs.heater.control_volume.material_flow_dx[0.0,0.0,Vap,Ar]", + "fs.heater.control_volume.material_flow_dx[0.0,0.0,Vap,N2]", + "fs.heater.control_volume.enthalpy_flow_dx[0.0,0.0,Vap]", + "fs.heater.control_volume.pressure_dx[0.0,0.0]", "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,H2]", "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,H2O]", "fs.heat_exchanger.cold_side.material_flow_dx[0.0,1.0,Vap,Ar]", @@ -181,11 +167,13 @@ def _check_model_statistics(m, deltaP): assert len(mstat.deactivated_constraints_set(m)) == 0 + @pytest.fixture def model_no_dP(): m = _create_model(pressure_drop=False) return m + @pytest.mark.component def test_initialization(model_no_dP): m = model_no_dP @@ -193,24 +181,18 @@ def test_initialization(model_no_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) - m.fs.heat_exchanger.initialize_build(optarg=optarg) + m.fs.heater.initialize_build(optarg=optarg) assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) - assert ( - pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) - == pytest.approx(485.34, abs=1e-1) - ) - assert ( - pyo.value(m.fs.heat_exchanger.cold_side_outlet.temperature[0]) - == pytest.approx(911.47, abs=1e-1) + assert pyo.value(m.fs.heater.outlet.temperature[0]) == pytest.approx( + 959.55, abs=1e-1 ) @pytest.mark.integration def test_units(model_no_dP): - assert_units_consistent(model_no_dP.fs.heat_exchanger) - + assert_units_consistent(model_no_dP.fs.heater) @pytest.fixture @@ -218,6 +200,7 @@ def model_dP(): m = _create_model(pressure_drop=True) return m + @pytest.mark.component def test_initialization_dP(model_dP): m = model_dP @@ -225,29 +208,17 @@ def test_initialization_dP(model_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) - m.fs.heat_exchanger.initialize_build(optarg=optarg) + m.fs.heater.initialize_build(optarg=optarg) assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) - assert ( - pyo.value(m.fs.heat_exchanger.hot_side_outlet.temperature[0]) - == pytest.approx(485.34, abs=1e-1) - ) - assert ( - pyo.value(m.fs.heat_exchanger.cold_side_outlet.temperature[0]) - == pytest.approx(911.47, abs=1e-1) - ) - assert ( - pyo.value(m.fs.heat_exchanger.hot_side_outlet.pressure[0]) - == pytest.approx(118870.08569, abs=1) - ) - assert ( - pyo.value(m.fs.heat_exchanger.cold_side_outlet.pressure[0]) - == pytest.approx(111418.71399, abs=1) + assert pyo.value(m.fs.heater.outlet.temperature[0]) == pytest.approx( + 959.55, abs=1e-1 ) + assert pyo.value(m.fs.heater.outlet.pressure[0]) == pytest.approx(119762.3, abs=1) @pytest.mark.integration def test_units_dP(model_dP): - assert_units_consistent(model_dP.fs.heat_exchanger) + assert_units_consistent(model_dP.fs.heater) From 50d5fd9a666e7e1e7affbcf0ae636df920f604a7 Mon Sep 17 00:00:00 2001 From: Doug A Date: Wed, 20 Mar 2024 14:55:35 -0400 Subject: [PATCH 12/38] format remaining file --- idaes/models/unit_models/heat_exchanger_1D.py | 1 - 1 file changed, 1 deletion(-) diff --git a/idaes/models/unit_models/heat_exchanger_1D.py b/idaes/models/unit_models/heat_exchanger_1D.py index 31fb1d19a2..bd1aedddbb 100644 --- a/idaes/models/unit_models/heat_exchanger_1D.py +++ b/idaes/models/unit_models/heat_exchanger_1D.py @@ -483,7 +483,6 @@ def build(self): "scheme on cold side." ) - if self.config.flow_type == HeatExchangerFlowPattern.cocurrent: set_direction_hot = FlowDirection.forward set_direction_cold = FlowDirection.forward From 52e67b6a9b4f7b3e290d82d8cd390fea8e28bcb7 Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 22 Mar 2024 14:21:51 -0400 Subject: [PATCH 13/38] separate 1d hx changes --- idaes/models/unit_models/heat_exchanger_1D.py | 96 ++++--- .../tests/test_heat_exchanger_1D.py | 238 +++++++++--------- 2 files changed, 169 insertions(+), 165 deletions(-) diff --git a/idaes/models/unit_models/heat_exchanger_1D.py b/idaes/models/unit_models/heat_exchanger_1D.py index bd1aedddbb..aa059aee74 100644 --- a/idaes/models/unit_models/heat_exchanger_1D.py +++ b/idaes/models/unit_models/heat_exchanger_1D.py @@ -430,62 +430,39 @@ def build(self): # Set flow directions for the control volume blocks and specify # discretization if not specified. - 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_method - != self.config.cold_side.transformation_method - ): - raise ConfigurationError( - "HeatExchanger1D only supports similar transformation " - "methods on the hot and cold side domains for " - "both cocurrent and countercurrent flow patterns. " - f"Found method {self.config.hot_side.transformation_method} " - f"on hot side and method {self.config.cold_side.transformation_method} " - "on cold side." - ) - if self.config.hot_side.transformation_method == "dae.collocation": - if ( - self.config.hot_side.transformation_scheme is useDefault - or self.config.cold_side.transformation_scheme is useDefault - ): - raise ConfigurationError( - "If a collocation method is used for HeatExchanger1D, the user " - "must specify the transformation scheme they want to use." - ) + 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( - "If a collocation method is used, " "HeatExchanger1D only supports similar transformation " - "schemes on the hot and cold side domains. Found " - f"{self.config.hot_side.transformation_scheme} scheme on " - f"hot side and {self.config.cold_side.transformation_scheme} " - "scheme on cold side." + "schemes on the hot and cold side domains for " + "both cocurrent and countercurrent flow patterns." ) - - if self.config.flow_type == HeatExchangerFlowPattern.cocurrent: - set_direction_hot = FlowDirection.forward - set_direction_cold = FlowDirection.forward + 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 " @@ -507,6 +484,24 @@ def build(self): 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 " @@ -524,7 +519,7 @@ def build(self): "Defaulting to forward finite " "difference on the cold side." ) - self.config.cold_side.transformation_scheme = "FORWARD" + self.config.cold_side.transformation_scheme = "BACKWARD" else: raise ConfigurationError( "{} HeatExchanger1D only supports cocurrent and " @@ -781,7 +776,6 @@ def initialize_build( cold_side_units = ( self.cold_side.config.property_package.get_metadata().get_derived_units ) - # TODO What if there is more than one time point? What if t0 != 0? if duty is None: duty = value( 0.25 diff --git a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py index eea2293e18..8c376eefc0 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py @@ -68,6 +68,7 @@ BlockTriangularizationInitializer, InitializationStatus, ) +from idaes.core.util import DiagnosticsToolbox # Imports to assemble BT-PR with different units from idaes.core import LiquidPhase, VaporPhase, Component @@ -384,15 +385,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 10 @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.area, pyunits.m**2) - assert_units_equivalent(btx.fs.unit.length, pyunits.m) - - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -529,6 +524,13 @@ def test_conservation(self, btx): ) assert abs(hot_side - cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- class TestBTX_countercurrent(object): @@ -606,19 +608,9 @@ def test_build(self, btx): assert number_unused_variables(btx) == 10 @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.area, pyunits.m**2) - assert_units_equivalent(btx.fs.unit.length, pyunits.m) - assert_units_equivalent( - btx.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(btx) - - @pytest.mark.unit - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -764,6 +756,13 @@ def test_conservation(self, btx): ) assert abs(hot_side - cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + # ----------------------------------------------------------------------------- def build_model(): @@ -846,19 +845,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 12 @pytest.mark.integration - def test_units(self, iapws): - assert_units_equivalent(iapws.fs.unit.area, pyunits.m**2) - assert_units_equivalent(iapws.fs.unit.length, pyunits.m) - assert_units_equivalent( - iapws.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -997,6 +986,13 @@ def test_conservation(self, iapws): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + # # ----------------------------------------------------------------------------- @pytest.mark.iapws @@ -1066,19 +1062,9 @@ def test_build(self, iapws): assert number_unused_variables(iapws) == 12 @pytest.mark.integration - def test_units(self, iapws): - assert_units_equivalent(iapws.fs.unit.area, pyunits.m**2) - assert_units_equivalent(iapws.fs.unit.length, pyunits.m) - assert_units_equivalent( - iapws.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(iapws) - - @pytest.mark.unit - def test_dof(self, iapws): - assert degrees_of_freedom(iapws) == 0 + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1217,6 +1203,13 @@ def test_conservation(self, iapws): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_numerical_warnings() + # # ----------------------------------------------------------------------------- class TestSaponification_cocurrent(object): @@ -1243,8 +1236,8 @@ def sapon(self): m.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.cold_side_inlet.flow_vol[0].fix(1e-3) m.fs.unit.cold_side_inlet.temperature[0].fix(300) @@ -1252,8 +1245,8 @@ def sapon(self): m.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) return m @@ -1294,19 +1287,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 16 @pytest.mark.integration - def test_units(self, sapon): - assert_units_equivalent(sapon.fs.unit.area, pyunits.m**2) - assert_units_equivalent(sapon.fs.unit.length, pyunits.m) - assert_units_equivalent( - sapon.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1409,25 +1392,37 @@ def test_solution(self, sapon): sapon.fs.unit.cold_side_outlet.flow_vol[0] ) - assert 55388.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"]) - assert 100.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"]) - assert 100.0 == value( + assert pytest.approx(55388.0, rel=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "EthylAcetate"] ) - assert 0.0 == value( + assert pytest.approx(0.0, abs=1e-5) == value( sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"] ) - assert 0.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"]) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"] + ) - assert 55388.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"]) - assert 100.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"]) - assert 100.0 == value( + assert pytest.approx(55388.0, rel=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "EthylAcetate"] ) - assert 0.0 == value( + assert pytest.approx(0.0, abs=1e-5) == value( sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"] ) - assert 0.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"]) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"] + ) assert pytest.approx(318.873, rel=1e-5) == value( sapon.fs.unit.hot_side_outlet.temperature[0] @@ -1466,6 +1461,13 @@ def test_conservation(self, sapon): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + # # ----------------------------------------------------------------------------- class TestSaponification_countercurrent(object): @@ -1492,8 +1494,8 @@ def sapon(self): m.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.hot_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) m.fs.unit.cold_side_inlet.flow_vol[0].fix(1e-3) m.fs.unit.cold_side_inlet.temperature[0].fix(300) @@ -1501,8 +1503,8 @@ def sapon(self): m.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"].fix(55388.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"].fix(100.0) m.fs.unit.cold_side_inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(0.0) - m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(0.0) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) return m @@ -1543,19 +1545,9 @@ def test_build(self, sapon): assert number_unused_variables(sapon) == 16 @pytest.mark.integration - def test_units(self, sapon): - assert_units_equivalent(sapon.fs.unit.area, pyunits.m**2) - assert_units_equivalent(sapon.fs.unit.length, pyunits.m) - assert_units_equivalent( - sapon.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, - ) - - assert_units_consistent(sapon) - - @pytest.mark.unit - def test_dof(self, sapon): - assert degrees_of_freedom(sapon) == 0 + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() @pytest.mark.ui @pytest.mark.unit @@ -1658,25 +1650,37 @@ def test_solution(self, sapon): sapon.fs.unit.cold_side_outlet.flow_vol[0] ) - assert 55388.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"]) - assert 100.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"]) - assert 100.0 == value( + assert pytest.approx(55388.0, rel=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "H2O"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "NaOH"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "EthylAcetate"] ) - assert 0.0 == value( + assert pytest.approx(0.0, abs=1e-5) == value( sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "SodiumAcetate"] ) - assert 0.0 == value(sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"]) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.hot_side_inlet.conc_mol_comp[0, "Ethanol"] + ) - assert 55388.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"]) - assert 100.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"]) - assert 100.0 == value( + assert pytest.approx(55388.0, rel=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "H2O"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "NaOH"] + ) + assert pytest.approx(100.0, rel=1e-5) == value( sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "EthylAcetate"] ) - assert 0.0 == value( + assert pytest.approx(0.0, abs=1e-5) == value( sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "SodiumAcetate"] ) - assert 0.0 == value(sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"]) + assert pytest.approx(0.0, abs=1e-5) == value( + sapon.fs.unit.cold_side_inlet.conc_mol_comp[0, "Ethanol"] + ) assert pytest.approx(318.869, rel=1e-5) == value( sapon.fs.unit.hot_side_outlet.temperature[0] @@ -1715,6 +1719,13 @@ def test_conservation(self, sapon): ) assert abs(hot_side + cold_side) <= 1e-6 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_numerical_warnings() + # # ----------------------------------------------------------------------------- @pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") @@ -1908,20 +1919,12 @@ def test_build(self, btx): assert number_unused_variables(btx) == 36 @pytest.mark.integration - def test_units(self, btx): - assert_units_equivalent(btx.fs.unit.area, pyunits.m**2) - assert_units_equivalent(btx.fs.unit.length, pyunits.m) - assert_units_equivalent( - btx.fs.unit.heat_transfer_coefficient, - pyunits.W / pyunits.m**2 / pyunits.K, + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings( + ignore_evaluation_errors=True, ) - assert_units_consistent(btx) - - @pytest.mark.component - def test_dof(self, btx): - assert degrees_of_freedom(btx) == 0 - @pytest.mark.ui @pytest.mark.unit def test_get_performance_contents(self, btx): @@ -2061,6 +2064,13 @@ def test_conservation(self, btx): ) assert abs((hot_side - cold_side) / hot_side) <= 3e-4 + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.integration + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + @pytest.mark.component def test_initialization_error(self, btx): btx.fs.unit.hot_side_outlet.flow_mol[0].fix(20) From c9b86da234dc758faf586489db66573f3b0de2f3 Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 22 Mar 2024 16:06:49 -0400 Subject: [PATCH 14/38] Fix pylint errors --- .../cross_flow_heat_exchanger_1D.py | 24 +++++++++---------- .../unit_models/heat_exchanger_common.py | 11 +++------ .../power_generation/unit_models/heater_1D.py | 5 ++-- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index e1a8dcc2cd..135d3aa281 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -51,7 +51,7 @@ @declare_process_block_class("CrossFlowHeatExchanger1D") class CrossFlowHeatExchanger1DData(HeatExchanger1DData): - """Standard Heat Exchanger Cross Flow Unit Model Class.""" + """Standard Cross Flow Heat Exchanger Unit Model Class.""" CONFIG = HeatExchanger1DData.CONFIG() CONFIG.declare( @@ -148,12 +148,10 @@ def _make_geometry(self): add_object_reference(self, "area_flow_tube", tube.area) # total tube length of flow path add_object_reference(self, "length_flow_tube", tube.length) - heat_exchanger_common._make_geometry_common( - self, shell_units=shell_units - ) # pylint: disable=W0212 - heat_exchanger_common._make_geometry_tube( - self, shell_units=shell_units - ) # pylint: disable=W0212 + # pylint: disable-next=W0212 + heat_exchanger_common._make_geometry_common(self, shell_units=shell_units) + # pylint: disable-next=W0212 + heat_exchanger_common._make_geometry_tube(self, shell_units=shell_units) def _make_performance(self): """ @@ -648,9 +646,9 @@ def initialize_build( hot_side.properties[t, z].pressure.fix(P_in_hot_side) blk.temp_wall_center_eqn.deactivate() - if tube_has_pressure_change == True: + if tube_has_pressure_change: blk.deltaP_tube_eqn.deactivate() - if shell_has_pressure_change == True: + if shell_has_pressure_change: blk.deltaP_shell_eqn.deactivate() blk.heat_tube_eqn.deactivate() blk.heat_shell_eqn.deactivate() @@ -682,9 +680,9 @@ def initialize_build( "issue on the IDAES Github so this error can be fixed." ) - if tube_has_pressure_change == True: + if tube_has_pressure_change: blk.deltaP_tube_eqn.activate() - if shell_has_pressure_change == True: + if shell_has_pressure_change: blk.deltaP_shell_eqn.activate() blk.heat_tube_eqn.activate() blk.heat_shell_eqn.activate() @@ -705,7 +703,7 @@ def initialize_build( init_log.info_high("Initialization Step 4 {}.".format(idaeslog.condition(res))) # set the wall thermal conductivity back to the user specified value - blk.therm_cond_wall = therm_cond_wall_save + blk.therm_cond_wall.set_value(therm_cond_wall_save) with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(blk, tee=slc.tee) @@ -741,6 +739,7 @@ def cst(con, sf): tube_has_pressure_change = hasattr(self, "deltaP_tube") shell_has_pressure_change = hasattr(self, "deltaP_shell") + # pylint: disable-next=W0212 heat_exchanger_common._scale_common( self, shell, @@ -748,6 +747,7 @@ def cst(con, sf): make_reynolds=True, make_nusselt=True, ) + # pylint: disable-next=W0212 heat_exchanger_common._scale_tube( self, tube, tube_has_pressure_change, make_reynolds=True, make_nusselt=True ) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index c29b44bc9d..48dee80fc0 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -11,10 +11,10 @@ # at the URL "https://github.com/IDAES/idaes-pse". ############################################################################## """ -1-D Cross Flow Heat Exchanger Model With Wall Temperatures - -Discretization based on tube rows +Methods shared by the CrossFlowHeatExchanger1D and Heater1D models. """ +# Presently the tube methods are not shared. I'm not sure why I chose to extract +# them here, but I don't want to change things this late into development. --Doug # Import Pyomo libraries from pyomo.environ import ( @@ -701,14 +701,9 @@ def cst(con, sf): sgsf = iscale.set_and_get_scaling_factor sf_do_tube = iscale.get_scaling_factor(blk.do_tube, default=1 / value(blk.do_tube)) - - sf_di_tube = iscale.get_scaling_factor(blk.do_tube, default=1 / value(blk.di_tube)) calculate_variable_from_constraint( blk.area_flow_shell_min, blk.area_flow_shell_min_eqn ) - sf_area_flow_shell_min = iscale.get_scaling_factor( - blk.area_flow_shell_min, default=1 / value(blk.area_flow_shell_min) - ) for t in blk.flowsheet().time: for z in shell.length_domain: sf_flow_mol_shell = gsf(shell.properties[t, z].flow_mol) diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 344687e9c5..aad23b97e6 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -11,7 +11,7 @@ # at the URL "https://github.com/IDAES/idaes-pse". ############################################################################## """ -1-D Cross Flow Heat Exchanger Model With Wall Temperatures +1-D Electric Trim Heater Model With Wall Temperatures Discretization based on tube rows """ @@ -54,9 +54,8 @@ @declare_process_block_class("Heater1D") class Heater1DData(UnitModelBlockData): - """Standard Heat Exchanger Cross Flow Unit Model Class.""" + """Standard Trim Heater Model Class Class.""" - # Template for config arguments for shell and tube side CONFIG = ConfigBlock() CONFIG.declare( "dynamic", From 4a0f9aaa6c4787aaffa43a3175f1041ca9c1bc5a Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 22 Mar 2024 18:44:54 -0400 Subject: [PATCH 15/38] more pylint issues --- .../unit_models/cross_flow_heat_exchanger_1D.py | 6 ------ .../power_generation/unit_models/heater_1D.py | 9 +-------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 135d3aa281..9f9659702e 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -398,12 +398,6 @@ def overall_heat_transfer_coefficient(b, t): b.total_heat_transfer_area * b.log_mean_delta_temperature[t] ) - def set_initial_condition(self): - if self.config.dynamic is True: - self.heat_accumulation[:, :].value = 0 - self.heat_accumulation[0, :].fix(0) - # no accumulation term for fluid side models to avoid pressure waves - def initialize_build( blk, shell_state_args=None, diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index aad23b97e6..f6289c9b91 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -220,7 +220,7 @@ class Heater1DData(UnitModelBlockData): default=3, domain=int, description="Number of collocation points per finite element", - doc="""If using collocation, number of collocation points to use + doc="""If using collocation, number of collocation points to use per finite element when discretizing length domain (default=3)""", ), ) @@ -419,13 +419,6 @@ def temp_wall_center_eqn(b, t, x): + b.electric_heat_duty[t] / b.length_flow_shell ) - def set_initial_condition(self): - # TODO how to deal with holdup for fluid side? - if self.config.dynamic is True: - self.heat_accumulation[:, :].value = 0 - self.heat_accumulation[0, :].fix(0) - # no accumulation term for fluid side models to avoid pressure waves - def initialize_build(blk, state_args=None, outlvl=0, solver="ipopt", optarg=None): """ HeatExchangerCrossFlow1D initialization routine From e440cc5999ef8261457259298c9f3259f85bfc9b Mon Sep 17 00:00:00 2001 From: Doug A Date: Mon, 1 Apr 2024 10:11:07 -0400 Subject: [PATCH 16/38] remove elevation change --- .../unit_models/heat_exchanger_common.py | 16 ---------------- .../tests/test_cross_flow_heat_exchanger_1D.py | 1 - .../unit_models/tests/test_heater_1D.py | 2 -- 3 files changed, 19 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 48dee80fc0..3116e9d1d2 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -159,13 +159,6 @@ def total_heat_transfer_area(b): def _make_geometry_tube(blk, shell_units): - # Elevation difference (outlet - inlet) for static pressure calculation - blk.delta_elevation = Var( - initialize=0.0, - units=shell_units["length"], - doc="Elevation increase used for static pressure calculation", - ) - # Length of tube side flow @blk.Constraint(doc="Length of tube side flow") def length_flow_tube_eqn(b): @@ -433,7 +426,6 @@ def friction_factor_shell_eqn(b, t, x): doc="pressure change on shell side", ) def deltaP_shell_eqn(b, t, x): - # FIXME this equation doesn't have elevation change---is this right? return ( b.deltaP_shell[t, x] * b.pitch_x == -1.4 @@ -662,14 +654,6 @@ def deltaP_tube_eqn(b, t, x): return b.deltaP_tube[t, x] == ( b.deltaP_tube_friction[t, x] + b.deltaP_tube_uturn[t, x] - - pyunits.convert(b.delta_elevation, to_units=tube_units["length"]) - / b.nseg_tube - * pyunits.convert( - const.acceleration_gravity, to_units=tube_units["acceleration"] - ) - * tube.properties[t, x].dens_mol_phase["Vap"] - * tube.properties[t, x].mw - / pyunits.convert(b.length_tube_seg, to_units=tube_units["length"]) ) if make_nusselt: diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index 2dbcb26c92..8c79ed24a4 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -101,7 +101,6 @@ def _create_model(pressure_drop): hx.pitch_x.fix(0.1) hx.pitch_y.fix(0.1) - hx.delta_elevation.fix(0) hx.therm_cond_wall = 43.0 hx.rfouling_tube = 0.0001 hx.rfouling_shell = 0.0001 diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py index 72c5229253..63397ab91c 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -141,8 +141,6 @@ def _check_model_statistics(m, deltaP): "fs.h2_side_prop_params.N2.cp_mol_ig_comp_coeff_G", "fs.h2_side_prop_params.N2.cp_mol_ig_comp_coeff_H", } - if not deltaP: - fixed_unused_var_set.add("fs.heater.delta_elevation") for var in mstat.fixed_unused_variables_set(m): assert var.name in fixed_unused_var_set From 57d1e8749c4576b0f9bde1753279cab1031cbc87 Mon Sep 17 00:00:00 2001 From: Doug A Date: Mon, 1 Apr 2024 10:42:34 -0400 Subject: [PATCH 17/38] run black --- .../power_generation/unit_models/heat_exchanger_common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 3116e9d1d2..358511cc43 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -652,8 +652,7 @@ def deltaP_tube_uturn_eqn(b, t, x): ) def deltaP_tube_eqn(b, t, x): return b.deltaP_tube[t, x] == ( - b.deltaP_tube_friction[t, x] - + b.deltaP_tube_uturn[t, x] + b.deltaP_tube_friction[t, x] + b.deltaP_tube_uturn[t, x] ) if make_nusselt: From 659de32af49eb0cd1a1a9466e3f2c9a8acc3ebd4 Mon Sep 17 00:00:00 2001 From: Doug A Date: Wed, 3 Apr 2024 17:15:07 -0400 Subject: [PATCH 18/38] get rid of commented code --- .../unit_models/cross_flow_heat_exchanger_1D.py | 7 ------- .../models_extra/power_generation/unit_models/heater_1D.py | 2 -- .../unit_models/tests/test_cross_flow_heat_exchanger_1D.py | 5 ++--- .../power_generation/unit_models/tests/test_heater_1D.py | 1 - 4 files changed, 2 insertions(+), 13 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 9f9659702e..539326d7cd 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -785,13 +785,6 @@ def cst(con, sf): def _get_performance_contents(self, time_point=0): var_dict = {} - # var_dict = { - # "HX Coefficient": self.overall_heat_transfer_coefficient[time_point] - # } - # var_dict["HX Area"] = self.area - # var_dict["Heat Duty"] = self.heat_duty[time_point] - # if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow: - # var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]} expr_dict = {} expr_dict["HX Area"] = self.total_heat_transfer_area diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index f6289c9b91..17581e005c 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -458,8 +458,6 @@ def initialize_build(blk, state_args=None, outlvl=0, solver="ipopt", optarg=None init_log.info_high("Initialization Step 1 Complete.") - # mcp = value(blk.control_volume.properties[0,0].flow_mol * blk.control_volume.properties[0,0].cp_mol) - # tout_guess = value(blk.tube.properties[0,0].temperature) + value(blk.electric_heat_duty[0]/blk.length_flow) calc_var = calculate_variable_from_constraint calc_var(blk.length_flow_shell, blk.length_flow_shell_eqn) diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index 8c79ed24a4..925ced596d 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -30,7 +30,6 @@ # Set up solver optarg = { - # 'bound_push' : 1e-6, "constr_viol_tol": 1e-8, "nlp_scaling_method": "user-scaling", "linear_solver": "ma57", @@ -132,9 +131,9 @@ def _create_model(pressure_drop): shell = hx.hot_side tube = hx.cold_side iscale.set_scaling_factor(shell.area, 1e-1) - # ssf(hx.shell.heat, 1e-6) + iscale.set_scaling_factor(shell.heat, 1e-6) iscale.set_scaling_factor(tube.area, 1) - # ssf(hx.tube.heat, 1e-6) + iscale.set_scaling_factor(tube.heat, 1e-6) iscale.set_scaling_factor(shell._enthalpy_flow, 1e-8) # pylint: disable=W0212 iscale.set_scaling_factor(tube._enthalpy_flow, 1e-8) # pylint: disable=W0212 iscale.set_scaling_factor(shell.enthalpy_flow_dx, 1e-7) diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py index 63397ab91c..a44915c183 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -29,7 +29,6 @@ # Set up solver optarg = { - # 'bound_push' : 1e-6, "constr_viol_tol": 1e-8, "nlp_scaling_method": "user-scaling", "linear_solver": "ma57", From bcde9a3d9efcd1f165d1685fd03b095ff9f26e13 Mon Sep 17 00:00:00 2001 From: Doug A Date: Thu, 4 Apr 2024 17:12:06 -0400 Subject: [PATCH 19/38] Fix typo --- .../power_generation/unit_models/heat_exchanger_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 358511cc43..7e5ecf1c3c 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -721,7 +721,7 @@ def cst(con, sf): def _scale_tube( - blk, tube, tube_has_presure_change, make_reynolds, make_nusselt + blk, tube, tube_has_pressure_change, make_reynolds, make_nusselt ): # pylint: disable=W0613 def gsf(obj): return iscale.get_scaling_factor(obj, default=1, warning=True) From 37d3edf6d3b39f7547e316687b9d1eff9e74920d Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 12 Apr 2024 09:55:33 -0400 Subject: [PATCH 20/38] At Andrew's insistence, make methods public --- .../unit_models/cross_flow_heat_exchanger_1D.py | 12 ++++++------ .../unit_models/heat_exchanger_common.py | 12 ++++++------ .../power_generation/unit_models/heater_1D.py | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 539326d7cd..3345708cc0 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -149,9 +149,9 @@ def _make_geometry(self): # total tube length of flow path add_object_reference(self, "length_flow_tube", tube.length) # pylint: disable-next=W0212 - heat_exchanger_common._make_geometry_common(self, shell_units=shell_units) + heat_exchanger_common.make_geometry_common(self, shell_units=shell_units) # pylint: disable-next=W0212 - heat_exchanger_common._make_geometry_tube(self, shell_units=shell_units) + heat_exchanger_common.make_geometry_tube(self, shell_units=shell_units) def _make_performance(self): """ @@ -229,7 +229,7 @@ def _make_performance(self): add_object_reference(self, "deltaP_tube", tube.deltaP) tube_has_pressure_change = True - heat_exchanger_common._make_performance_common( # pylint: disable=W0212 + heat_exchanger_common.make_performance_common( # pylint: disable=W0212 self, shell=shell, shell_units=shell_units, @@ -237,7 +237,7 @@ def _make_performance(self): make_reynolds=True, make_nusselt=True, ) - heat_exchanger_common._make_performance_tube( # pylint: disable=W0212 + heat_exchanger_common.make_performance_tube( # pylint: disable=W0212 self, tube=tube, tube_units=tube_units, @@ -734,7 +734,7 @@ def cst(con, sf): shell_has_pressure_change = hasattr(self, "deltaP_shell") # pylint: disable-next=W0212 - heat_exchanger_common._scale_common( + heat_exchanger_common.scale_common( self, shell, shell_has_pressure_change, @@ -742,7 +742,7 @@ def cst(con, sf): make_nusselt=True, ) # pylint: disable-next=W0212 - heat_exchanger_common._scale_tube( + heat_exchanger_common.scale_tube( self, tube, tube_has_pressure_change, make_reynolds=True, make_nusselt=True ) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 7e5ecf1c3c..1df74f84d5 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -36,7 +36,7 @@ __author__ = "Jinliang Ma, Douglas Allan" -def _make_geometry_common(blk, shell_units): +def make_geometry_common(blk, shell_units): # Number of tube columns in the cross section plane perpendicular to shell side fluid flow (y direction) blk.ncol_tube = Var( initialize=10.0, doc="Number of tube columns", units=pyunits.dimensionless @@ -158,7 +158,7 @@ def total_heat_transfer_area(b): ) -def _make_geometry_tube(blk, shell_units): +def make_geometry_tube(blk, shell_units): # Length of tube side flow @blk.Constraint(doc="Length of tube side flow") def length_flow_tube_eqn(b): @@ -176,7 +176,7 @@ def area_flow_tube_eqn(b): ) -def _make_performance_common( +def make_performance_common( blk, shell, shell_units, shell_has_pressure_change, make_reynolds, make_nusselt ): # We need the Reynolds number for pressure change, even if we don't need it for heat transfer @@ -461,7 +461,7 @@ def hconv_shell_total(b, t, x): return b.hconv_shell_conv[t, x] -def _make_performance_tube( +def make_performance_tube( blk, tube, tube_units, tube_has_pressure_change, make_reynolds, make_nusselt ): @@ -671,7 +671,7 @@ def hconv_tube_eqn(b, t, x): ) -def _scale_common(blk, shell, shell_has_pressure_change, make_reynolds, make_nusselt): +def scale_common(blk, shell, shell_has_pressure_change, make_reynolds, make_nusselt): def gsf(obj): return iscale.get_scaling_factor(obj, default=1, warning=True) @@ -720,7 +720,7 @@ def cst(con, sf): cst(blk.heat_holdup_eqn[t, z], s_U_holdup) -def _scale_tube( +def scale_tube( blk, tube, tube_has_pressure_change, make_reynolds, make_nusselt ): # pylint: disable=W0613 def gsf(obj): diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 17581e005c..8af8e95385 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -44,9 +44,9 @@ from idaes.core.util.tables import create_stream_table_dataframe from idaes.core.util.model_statistics import degrees_of_freedom from idaes.models_extra.power_generation.unit_models.heat_exchanger_common import ( - _make_geometry_common, # pylint: disable=W0212 - _make_performance_common, # pylint: disable=W0212 - _scale_common, # pylint: disable=W0212 + make_geometry_common, # pylint: disable=W0212 + make_performance_common, # pylint: disable=W0212 + scale_common, # pylint: disable=W0212 ) __author__ = "Jinliang Ma, Douglas Allan" @@ -318,7 +318,7 @@ def _make_geometry(self): # Add reference to control volume geometry add_object_reference(self, "area_flow_shell", self.control_volume.area) add_object_reference(self, "length_flow_shell", self.control_volume.length) - _make_geometry_common(self, shell_units=units) + make_geometry_common(self, shell_units=units) @self.Expression( doc="Common performance equations expect this expression to be here" @@ -343,7 +343,7 @@ def _make_performance(self): doc="Heat duty provided to heater through resistive heating", ) units = self.config.property_package.get_metadata().derived_units - _make_performance_common( + make_performance_common( self, shell=self.control_volume, shell_units=units, @@ -517,7 +517,7 @@ def ssf(obj, sf): def cst(con, sf): iscale.constraint_scaling_transform(con, sf, overwrite=False) - _scale_common( + scale_common( self, self.control_volume, self.config.has_pressure_change, From 622762a1246258572bf70dcb24e6d991c3f79f03 Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 12 Apr 2024 11:14:19 -0400 Subject: [PATCH 21/38] address more of Andrew's comments --- .../cross_flow_heat_exchanger_1D.py | 37 ++++++++----------- .../unit_models/heat_exchanger_common.py | 2 +- .../power_generation/unit_models/heater_1D.py | 34 ++--------------- 3 files changed, 20 insertions(+), 53 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 3345708cc0..9eb4e4be1b 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -21,9 +21,9 @@ from pyomo.environ import ( value, log, + Reference, units as pyunits, ) -import pyomo.common.config import pyomo.opt from pyomo.common.config import ConfigValue, In, Bool from pyomo.network import Port @@ -142,15 +142,13 @@ def _make_geometry(self): shell_units = ( self.config.cold_side.property_package.get_metadata().derived_units ) - # Add reference to control volume geometry + # Use add_object_reference for scalar vars add_object_reference(self, "area_flow_shell", shell.area) - add_object_reference(self, "length_flow_shell", shell.length) add_object_reference(self, "area_flow_tube", tube.area) - # total tube length of flow path + add_object_reference(self, "length_flow_shell", shell.length) add_object_reference(self, "length_flow_tube", tube.length) - # pylint: disable-next=W0212 - heat_exchanger_common.make_geometry_common(self, shell_units=shell_units) - # pylint: disable-next=W0212 + + heat_exchanger_common.make_geometry_common(self, shell=shell, shell_units=shell_units) heat_exchanger_common.make_geometry_tube(self, shell_units=shell_units) def _make_performance(self): @@ -207,29 +205,28 @@ def _make_performance(self): "whose single phase is a vapor phase. The cold side phase is not a vapor phase." ) - # Reference - add_object_reference(self, "heat_tube", tube.heat) - add_object_reference(self, "heat_shell", shell.heat) + self.heat_tube = Reference(tube.heat) + self.heat_shell = Reference(shell.heat) shell_has_pressure_change = False tube_has_pressure_change = False if self.config.cold_side.has_pressure_change: if self.config.shell_is_hot: - add_object_reference(self, "deltaP_tube", tube.deltaP) + self.deltaP_tube = Reference(tube.deltaP) tube_has_pressure_change = True else: - add_object_reference(self, "deltaP_shell", shell.deltaP) + self.deltaP_shell = Reference(shell.deltaP) shell_has_pressure_change = True if self.config.hot_side.has_pressure_change: if self.config.shell_is_hot: - add_object_reference(self, "deltaP_shell", shell.deltaP) + self.deltaP_shell = Reference(shell.deltaP) shell_has_pressure_change = True else: - add_object_reference(self, "deltaP_tube", tube.deltaP) + self.deltaP_tube = Reference(tube.deltaP) tube_has_pressure_change = True - heat_exchanger_common.make_performance_common( # pylint: disable=W0212 + heat_exchanger_common.make_performance_common( self, shell=shell, shell_units=shell_units, @@ -237,7 +234,7 @@ def _make_performance(self): make_reynolds=True, make_nusselt=True, ) - heat_exchanger_common.make_performance_tube( # pylint: disable=W0212 + heat_exchanger_common.make_performance_tube( self, tube=tube, tube_units=tube_units, @@ -246,9 +243,6 @@ def _make_performance(self): make_nusselt=True, ) - def heat_accumulation_term(b, t, x): - return b.heat_accumulation[t, x] if b.config.dynamic else 0 - # Nusselts number @self.Constraint( self.flowsheet().config.time, @@ -346,6 +340,9 @@ def temp_wall_shell_eqn(b, t, x): == b.temp_wall_shell[t, x] - b.temp_wall_center[t, x] ) + def heat_accumulation_term(b, t, x): + return b.heat_accumulation[t, x] if b.config.dynamic else 0 + # Center point wall temperature based on energy balance for tube wall heat holdup @self.Constraint( self.flowsheet().config.time, @@ -733,7 +730,6 @@ def cst(con, sf): tube_has_pressure_change = hasattr(self, "deltaP_tube") shell_has_pressure_change = hasattr(self, "deltaP_shell") - # pylint: disable-next=W0212 heat_exchanger_common.scale_common( self, shell, @@ -741,7 +737,6 @@ def cst(con, sf): make_reynolds=True, make_nusselt=True, ) - # pylint: disable-next=W0212 heat_exchanger_common.scale_tube( self, tube, tube_has_pressure_change, make_reynolds=True, make_nusselt=True ) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 1df74f84d5..c0e6efdadf 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -36,7 +36,7 @@ __author__ = "Jinliang Ma, Douglas Allan" -def make_geometry_common(blk, shell_units): +def make_geometry_common(blk, shell, shell_units): # Number of tube columns in the cross section plane perpendicular to shell side fluid flow (y direction) blk.ncol_tube = Var( initialize=10.0, doc="Number of tube columns", units=pyunits.dimensionless diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 8af8e95385..bb16243506 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -22,7 +22,7 @@ value, units as pyunits, ) -from pyomo.common.config import ConfigBlock, ConfigValue, In +from pyomo.common.config import ConfigValue, In from pyomo.util.calc_var_value import calculate_variable_from_constraint # Import IDAES cores @@ -56,35 +56,7 @@ class Heater1DData(UnitModelBlockData): """Standard Trim Heater Model Class Class.""" - CONFIG = ConfigBlock() - CONFIG.declare( - "dynamic", - ConfigValue( - default=useDefault, - domain=In([useDefault, True, False]), - 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.}""", - ), - ) - CONFIG.declare( - "has_holdup", - ConfigValue( - default=False, - domain=In([True, False]), - description="Holdup construction flag", - doc="""Indicates whether holdup terms should be constructed or not. -Must be True if dynamic = True, -**default** - False. -**Valid values:** { -**True** - construct holdup terms, -**False** - do not construct holdup terms}""", - ), - ) + CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "has_fluid_holdup", ConfigValue( @@ -318,7 +290,7 @@ def _make_geometry(self): # Add reference to control volume geometry add_object_reference(self, "area_flow_shell", self.control_volume.area) add_object_reference(self, "length_flow_shell", self.control_volume.length) - make_geometry_common(self, shell_units=units) + make_geometry_common(self, shell=self.control_volume, shell_units=units) @self.Expression( doc="Common performance equations expect this expression to be here" From 2514cfb85d1d6264c37f7d95443f66366603c86c Mon Sep 17 00:00:00 2001 From: Doug A Date: Mon, 15 Apr 2024 17:57:51 -0400 Subject: [PATCH 22/38] Update initialization to new form --- .../power_generation/unit_models/__init__.py | 4 +- .../cross_flow_heat_exchanger_1D.py | 617 +++++++++--------- .../power_generation/unit_models/heater_1D.py | 191 +++--- .../test_cross_flow_heat_exchanger_1D.py | 17 +- .../unit_models/tests/test_heater_1D.py | 14 +- 5 files changed, 433 insertions(+), 410 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/__init__.py b/idaes/models_extra/power_generation/unit_models/__init__.py index af8d39973b..d1fadcbf62 100644 --- a/idaes/models_extra/power_generation/unit_models/__init__.py +++ b/idaes/models_extra/power_generation/unit_models/__init__.py @@ -15,12 +15,12 @@ from .boiler_fireside import BoilerFireside from .boiler_heat_exchanger import BoilerHeatExchanger from .boiler_heat_exchanger_2D import HeatExchangerCrossFlow2D_Header -from .cross_flow_heat_exchanger_1D import CrossFlowHeatExchanger1D +from .cross_flow_heat_exchanger_1D import CrossFlowHeatExchanger1D, CrossFlowHeatExchanger1DInitializer from .downcomer import Downcomer from .drum import Drum from .drum1D import Drum1D from .feedwater_heater_0D_dynamic import FWH0DDynamic -from .heater_1D import Heater1D +from .heater_1D import Heater1D, Heater1DInitializer from .heat_exchanger_3streams import HeatExchangerWith3Streams from .steamheater import SteamHeater from .waterpipe import WaterPipe diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 9eb4e4be1b..0a9d8608be 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -19,6 +19,7 @@ # Import Pyomo libraries from pyomo.environ import ( + Block, value, log, Reference, @@ -42,12 +43,316 @@ ) from idaes.models.unit_models.heat_exchanger_1D import HeatExchanger1DData from idaes.models_extra.power_generation.unit_models import heat_exchanger_common +from idaes.core.util.exceptions import InitializationError +import idaes.logger as idaeslog +from idaes.core.initialization import SingleControlVolumeUnitInitializer + __author__ = "Jinliang Ma, Douglas Allan" -# Set up logger -_log = idaeslog.getLogger(__name__) +class CrossFlowHeatExchanger1DInitializer(SingleControlVolumeUnitInitializer): + """ + Initializer for Cross Flow Heat Exchanger 1D units. + """ + + def initialize_main_model( + self, + model: Block, + copy_inlet_state: bool = False, + ): + """ + Initialization routine for the main Cross Flow Heat Exchanger 1D model + (as opposed to submodels like costing, which presently do not exist). + + 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. + + """ + # 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" + ) + + solver_obj = get_solver(self.config.solver, self.config.solver_options) + + hot_side = model.hot_side + cold_side = model.cold_side + t0 = model.flowsheet().time.first() + if not ( + "temperature" in hot_side.properties[t0, 0].define_state_vars().keys() + and "temperature" in cold_side.properties[t0, 0].define_state_vars().keys() + ): + raise NotImplementedError( + "Presently, initialization of the CrossFlowHeatExchanger1D requires " + "temperature to be a state variable of both hot side and cold side " + "property packages. Extension to enth_mol or enth_mass as state variables " + "is straightforward---feel free to open a pull request implementing it." + ) + + hot_units = model.config.hot_side.property_package.get_metadata().derived_units + cold_units = model.config.cold_side.property_package.get_metadata().derived_units + + if model.config.shell_is_hot: + shell = model.hot_side + tube = model.cold_side + shell_has_pressure_change = model.config.hot_side.has_pressure_change + tube_has_pressure_change = model.config.cold_side.has_pressure_change + shell_units = ( + model.config.hot_side.property_package.get_metadata().derived_units + ) + tube_units = ( + model.config.cold_side.property_package.get_metadata().derived_units + ) + else: + shell = model.cold_side + tube = model.hot_side + shell_has_pressure_change = model.config.cold_side.has_pressure_change + tube_has_pressure_change = model.config.hot_side.has_pressure_change + shell_units = ( + model.config.cold_side.property_package.get_metadata().derived_units + ) + tube_units = ( + model.config.hot_side.property_package.get_metadata().derived_units + ) + + # Trigger creation of cp for use in future initialization + # Important to do before initializing property packages in + # case it is implemented as Var-Constraint pair instead of + # an Expression + value(hot_side.properties[t0, 0].cp_mol) + value(cold_side.properties[t0, 0].cp_mol) + + # --------------------------------------------------------------------- + # Initialize shell block + self.initialize_control_volume(tube, copy_inlet_state) + self.initialize_control_volume(shell, copy_inlet_state) + + init_log.info_high("Initialization Step 1 Complete.") + + # Set tube thermal conductivity to a small value to avoid IPOPT unable to solve initially + therm_cond_wall_save = model.therm_cond_wall.value + model.therm_cond_wall.set_value(0.05) + # In Step 2, fix tube metal temperatures fix fluid state variables (enthalpy/temperature and pressure) + # calculate maximum heat duty assuming infinite area and use half of the maximum duty as initial guess to calculate outlet temperature + + for t in model.flowsheet().config.time: + mcp_hot_side = value( + pyunits.convert( + hot_side.properties[t, 0].flow_mol + * hot_side.properties[t, 0].cp_mol, + to_units=shell_units["power"] / shell_units["temperature"], + ) + ) + T_in_hot_side = value( + pyunits.convert( + hot_side.properties[t, 0].temperature, + to_units=shell_units["temperature"], + ) + ) + P_in_hot_side = value(hot_side.properties[t, 0].pressure) + if model.config.flow_type == HeatExchangerFlowPattern.cocurrent: + mcp_cold_side = value( + pyunits.convert( + cold_side.properties[t, 0].flow_mol + * cold_side.properties[t, 0].cp_mol, + to_units=shell_units["power"] / shell_units["temperature"], + ) + ) + T_in_cold_side = value( + pyunits.convert( + cold_side.properties[t, 0].temperature, + to_units=shell_units["temperature"], + ) + ) + P_in_cold_side = value(cold_side.properties[t, 0].pressure) + + T_out_max = ( + mcp_cold_side * T_in_cold_side + mcp_hot_side * T_in_hot_side + ) / (mcp_cold_side + mcp_hot_side) + + q_guess = mcp_cold_side * (T_out_max - T_in_cold_side) / 2 + + temp_out_cold_side_guess = T_in_cold_side + q_guess / mcp_cold_side + + cold_side.properties[t, 1].temperature.fix( + pyunits.convert_value( + temp_out_cold_side_guess, + from_units=shell_units["temperature"], + to_units=cold_units["temperature"], + ) + ) + + temp_out_hot_side_guess = T_in_cold_side - q_guess / mcp_hot_side + hot_side.properties[t, 1].temperature.fix( + pyunits.convert_value( + temp_out_hot_side_guess, + from_units=shell_units["temperature"], + to_units=hot_units["temperature"], + ) + ) + + elif model.config.flow_type == HeatExchangerFlowPattern.countercurrent: + mcp_cold_side = value( + pyunits.convert( + cold_side.properties[t, 1].flow_mol + * cold_side.properties[t, 1].cp_mol, + to_units=shell_units["power"] / shell_units["temperature"], + ) + ) + T_in_cold_side = value( + pyunits.convert( + cold_side.properties[t, 1].temperature, + to_units=shell_units["temperature"], + ) + ) + P_in_cold_side = value(cold_side.properties[t, 1].pressure) + + if mcp_cold_side < mcp_hot_side: + q_guess = mcp_cold_side * (T_in_hot_side - T_in_cold_side) / 2 + else: + q_guess = mcp_hot_side * (T_in_hot_side - T_in_cold_side) / 2 + temp_out_cold_side_guess = T_in_cold_side + q_guess / mcp_cold_side + cold_side.properties[t, 0].temperature.fix( + pyunits.convert_value( + temp_out_cold_side_guess, + from_units=shell_units["temperature"], + to_units=cold_units["temperature"], + ) + ) + + temp_out_hot_side_guess = T_in_hot_side - q_guess / mcp_hot_side + hot_side.properties[t, 1].temperature.fix( + pyunits.convert_value( + temp_out_hot_side_guess, + from_units=shell_units["temperature"], + to_units=hot_units["temperature"], + ) + ) + + else: + raise BurntToast( + "HeatExchangerFlowPattern should be limited to cocurrent " + "or countercurrent flow by parent model. Please open an " + "issue on the IDAES Github so this error can be fixed." + ) + + for z in cold_side.length_domain: + hot_side.properties[t, z].temperature.fix( + value( + (1 - z) * hot_side.properties[t, 0].temperature + + z * hot_side.properties[t, 1].temperature + ) + ) + cold_side.properties[t, z].temperature.fix( + value( + (1 - z) * cold_side.properties[t, 0].temperature + + z * cold_side.properties[t, 1].temperature + ) + ) + model.temp_wall_center[t, z].fix( + value( + pyunits.convert( + hot_side.properties[t, z].temperature, + to_units=shell_units["temperature"], + ) + + pyunits.convert( + cold_side.properties[t, z].temperature, + to_units=shell_units["temperature"], + ) + ) + / 2 + ) + + model.temp_wall_shell[t, z].set_value(model.temp_wall_center[t, z].value) + model.temp_wall_tube[t, z].set_value( + pyunits.convert_value( + model.temp_wall_center[t, z].value, + from_units=shell_units["temperature"], + to_units=tube_units["temperature"], + ) + ) + + if model.config.cold_side.has_pressure_change: + cold_side.properties[t, z].pressure.fix(P_in_cold_side) + if model.config.hot_side.has_pressure_change: + hot_side.properties[t, z].pressure.fix(P_in_hot_side) + + model.temp_wall_center_eqn.deactivate() + if tube_has_pressure_change: + model.deltaP_tube_eqn.deactivate() + if shell_has_pressure_change: + model.deltaP_shell_eqn.deactivate() + model.heat_tube_eqn.deactivate() + model.heat_shell_eqn.deactivate() + + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = solver_obj.solve(model, tee=slc.tee) + pyomo.opt.assert_optimal_termination(res) + init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res))) + + # In Step 3, unfix fluid state variables (enthalpy/temperature and pressure) + # keep the inlet state variables fixed, otherwise, the degree of freedom > 0 + hot_side.properties[:, :].temperature.unfix() + hot_side.properties[:, :].pressure.unfix() + hot_side.properties[:, 0].temperature.fix() + hot_side.properties[:, 0].pressure.fix() + + cold_side.properties[:, :].temperature.unfix() + cold_side.properties[:, :].pressure.unfix() + if model.config.flow_type == HeatExchangerFlowPattern.cocurrent: + cold_side.properties[:, 0].temperature.fix() + cold_side.properties[:, 0].pressure.fix() + elif model.config.flow_type == HeatExchangerFlowPattern.countercurrent: + cold_side.properties[:, 1].temperature.fix() + cold_side.properties[:, 1].pressure.fix() + else: + raise BurntToast( + "HeatExchangerFlowPattern should be limited to cocurrent " + "or countercurrent flow by parent model. Please open an " + "issue on the IDAES Github so this error can be fixed." + ) + + if tube_has_pressure_change: + model.deltaP_tube_eqn.activate() + if shell_has_pressure_change: + model.deltaP_shell_eqn.activate() + model.heat_tube_eqn.activate() + model.heat_shell_eqn.activate() + + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = solver_obj.solve(model, tee=slc.tee) + pyomo.opt.assert_optimal_termination(res) + + init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) + + model.temp_wall_center.unfix() + model.temp_wall_center_eqn.activate() + + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = solver_obj.solve(model, tee=slc.tee) + pyomo.opt.assert_optimal_termination(res) + + init_log.info_high("Initialization Step 4 {}.".format(idaeslog.condition(res))) + + # set the wall thermal conductivity back to the user specified value + model.therm_cond_wall.set_value(therm_cond_wall_save) + + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = solver_obj.solve(model, tee=slc.tee) + init_log.info_high("Initialization Step 5 {}.".format(idaeslog.condition(res))) + init_log.info("Initialization Complete.") + return res @declare_process_block_class("CrossFlowHeatExchanger1D") class CrossFlowHeatExchanger1DData(HeatExchanger1DData): @@ -395,314 +700,6 @@ def overall_heat_transfer_coefficient(b, t): b.total_heat_transfer_area * b.log_mean_delta_temperature[t] ) - def initialize_build( - blk, - shell_state_args=None, - tube_state_args=None, - outlvl=idaeslog.NOTSET, - solver="ipopt", - optarg=None, - ): - """ - CrossFlowHeatExchanger1D initialization routine - - 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 = None). - outlvl : sets output level of initialization 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 information (tee=True) - - optarg : solver options dictionary object (default={'tol': 1e-6}) - solver : str indicating which solver to use during - initialization (default = 'ipopt') - - Returns: - None - """ - init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") - solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") - - if optarg is None: - optarg = {} - opt = get_solver(solver, optarg) - - hot_side = blk.hot_side - cold_side = blk.cold_side - t0 = blk.flowsheet().time.first() - if not ( - "temperature" in hot_side.properties[t0, 0].define_state_vars().keys() - and "temperature" in cold_side.properties[t0, 0].define_state_vars().keys() - ): - raise NotImplementedError( - "Presently, initialization of the CrossFlowHeatExchanger1D requires " - "temperature to be a state variable of both hot side and cold side " - "property packages. Extension to enth_mol or enth_mass as state variables " - "is straightforward---feel free to open a pull request implementing it." - ) - - hot_units = blk.config.hot_side.property_package.get_metadata().derived_units - cold_units = blk.config.cold_side.property_package.get_metadata().derived_units - - if blk.config.shell_is_hot: - shell = blk.hot_side - tube = blk.cold_side - shell_has_pressure_change = blk.config.hot_side.has_pressure_change - tube_has_pressure_change = blk.config.cold_side.has_pressure_change - shell_units = ( - blk.config.hot_side.property_package.get_metadata().derived_units - ) - tube_units = ( - blk.config.cold_side.property_package.get_metadata().derived_units - ) - else: - shell = blk.cold_side - tube = blk.hot_side - shell_has_pressure_change = blk.config.cold_side.has_pressure_change - tube_has_pressure_change = blk.config.hot_side.has_pressure_change - shell_units = ( - blk.config.cold_side.property_package.get_metadata().derived_units - ) - tube_units = ( - blk.config.hot_side.property_package.get_metadata().derived_units - ) - # --------------------------------------------------------------------- - # Initialize shell block - - flags_tube = tube.initialize( - outlvl=outlvl, optarg=optarg, solver=solver, state_args=tube_state_args - ) - - flags_shell = shell.initialize( - outlvl=outlvl, optarg=optarg, solver=solver, state_args=shell_state_args - ) - - init_log.info_high("Initialization Step 1 Complete.") - - # Set tube thermal conductivity to a small value to avoid IPOPT unable to solve initially - therm_cond_wall_save = blk.therm_cond_wall.value - blk.therm_cond_wall.set_value(0.05) - # In Step 2, fix tube metal temperatures fix fluid state variables (enthalpy/temperature and pressure) - # calculate maximum heat duty assuming infinite area and use half of the maximum duty as initial guess to calculate outlet temperature - - for t in blk.flowsheet().config.time: - # TODO we first access cp during initialization. That could pose a problem if it is - # converted to a Var-Constraint pair instead of being a giant Expression like it is - # presently. - mcp_hot_side = value( - pyunits.convert( - hot_side.properties[t, 0].flow_mol - * hot_side.properties[t, 0].cp_mol, - to_units=shell_units["power"] / shell_units["temperature"], - ) - ) - T_in_hot_side = value( - pyunits.convert( - hot_side.properties[t, 0].temperature, - to_units=shell_units["temperature"], - ) - ) - P_in_hot_side = value(hot_side.properties[t, 0].pressure) - if blk.config.flow_type == HeatExchangerFlowPattern.cocurrent: - mcp_cold_side = value( - pyunits.convert( - cold_side.properties[t, 0].flow_mol - * cold_side.properties[t, 0].cp_mol, - to_units=shell_units["power"] / shell_units["temperature"], - ) - ) - T_in_cold_side = value( - pyunits.convert( - cold_side.properties[t, 0].temperature, - to_units=shell_units["temperature"], - ) - ) - P_in_cold_side = value(cold_side.properties[t, 0].pressure) - - T_out_max = ( - mcp_cold_side * T_in_cold_side + mcp_hot_side * T_in_hot_side - ) / (mcp_cold_side + mcp_hot_side) - - q_guess = mcp_cold_side * (T_out_max - T_in_cold_side) / 2 - - temp_out_cold_side_guess = T_in_cold_side + q_guess / mcp_cold_side - - cold_side.properties[t, 1].temperature.fix( - pyunits.convert_value( - temp_out_cold_side_guess, - from_units=shell_units["temperature"], - to_units=cold_units["temperature"], - ) - ) - - temp_out_hot_side_guess = T_in_cold_side - q_guess / mcp_hot_side - hot_side.properties[t, 1].temperature.fix( - pyunits.convert_value( - temp_out_hot_side_guess, - from_units=shell_units["temperature"], - to_units=hot_units["temperature"], - ) - ) - - elif blk.config.flow_type == HeatExchangerFlowPattern.countercurrent: - mcp_cold_side = value( - pyunits.convert( - cold_side.properties[t, 1].flow_mol - * cold_side.properties[t, 1].cp_mol, - to_units=shell_units["power"] / shell_units["temperature"], - ) - ) - T_in_cold_side = value( - pyunits.convert( - cold_side.properties[t, 1].temperature, - to_units=shell_units["temperature"], - ) - ) - P_in_cold_side = value(cold_side.properties[t, 1].pressure) - - if mcp_cold_side < mcp_hot_side: - q_guess = mcp_cold_side * (T_in_hot_side - T_in_cold_side) / 2 - else: - q_guess = mcp_hot_side * (T_in_hot_side - T_in_cold_side) / 2 - - temp_out_cold_side_guess = T_in_cold_side + q_guess / mcp_cold_side - cold_side.properties[t, 0].temperature.fix( - pyunits.convert_value( - temp_out_cold_side_guess, - from_units=shell_units["temperature"], - to_units=cold_units["temperature"], - ) - ) - - temp_out_hot_side_guess = T_in_hot_side - q_guess / mcp_hot_side - hot_side.properties[t, 1].temperature.fix( - pyunits.convert_value( - temp_out_hot_side_guess, - from_units=shell_units["temperature"], - to_units=hot_units["temperature"], - ) - ) - - else: - raise BurntToast( - "HeatExchangerFlowPattern should be limited to cocurrent " - "or countercurrent flow by parent model. Please open an " - "issue on the IDAES Github so this error can be fixed." - ) - - for z in cold_side.length_domain: - hot_side.properties[t, z].temperature.fix( - value( - (1 - z) * hot_side.properties[t, 0].temperature - + z * hot_side.properties[t, 1].temperature - ) - ) - cold_side.properties[t, z].temperature.fix( - value( - (1 - z) * cold_side.properties[t, 0].temperature - + z * cold_side.properties[t, 1].temperature - ) - ) - blk.temp_wall_center[t, z].fix( - value( - pyunits.convert( - hot_side.properties[t, z].temperature, - to_units=shell_units["temperature"], - ) - + pyunits.convert( - cold_side.properties[t, z].temperature, - to_units=shell_units["temperature"], - ) - ) - / 2 - ) - - blk.temp_wall_shell[t, z].set_value(blk.temp_wall_center[t, z].value) - blk.temp_wall_tube[t, z].set_value( - pyunits.convert_value( - blk.temp_wall_center[t, z].value, - from_units=shell_units["temperature"], - to_units=tube_units["temperature"], - ) - ) - - if blk.config.cold_side.has_pressure_change: - cold_side.properties[t, z].pressure.fix(P_in_cold_side) - if blk.config.hot_side.has_pressure_change: - hot_side.properties[t, z].pressure.fix(P_in_hot_side) - - blk.temp_wall_center_eqn.deactivate() - if tube_has_pressure_change: - blk.deltaP_tube_eqn.deactivate() - if shell_has_pressure_change: - blk.deltaP_shell_eqn.deactivate() - blk.heat_tube_eqn.deactivate() - blk.heat_shell_eqn.deactivate() - - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - res = opt.solve(blk, tee=slc.tee) - pyomo.opt.assert_optimal_termination(res) - init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res))) - - # In Step 3, unfix fluid state variables (enthalpy/temperature and pressure) - # keep the inlet state variables fixed, otherwise, the degree of freedom > 0 - hot_side.properties[:, :].temperature.unfix() - hot_side.properties[:, :].pressure.unfix() - hot_side.properties[:, 0].temperature.fix() - hot_side.properties[:, 0].pressure.fix() - - cold_side.properties[:, :].temperature.unfix() - cold_side.properties[:, :].pressure.unfix() - if blk.config.flow_type == HeatExchangerFlowPattern.cocurrent: - cold_side.properties[:, 0].temperature.fix() - cold_side.properties[:, 0].pressure.fix() - elif blk.config.flow_type == HeatExchangerFlowPattern.countercurrent: - cold_side.properties[:, 1].temperature.fix() - cold_side.properties[:, 1].pressure.fix() - else: - raise BurntToast( - "HeatExchangerFlowPattern should be limited to cocurrent " - "or countercurrent flow by parent model. Please open an " - "issue on the IDAES Github so this error can be fixed." - ) - - if tube_has_pressure_change: - blk.deltaP_tube_eqn.activate() - if shell_has_pressure_change: - blk.deltaP_shell_eqn.activate() - blk.heat_tube_eqn.activate() - blk.heat_shell_eqn.activate() - - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - res = opt.solve(blk, tee=slc.tee) - pyomo.opt.assert_optimal_termination(res) - - init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) - - blk.temp_wall_center.unfix() - blk.temp_wall_center_eqn.activate() - - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - res = opt.solve(blk, tee=slc.tee) - pyomo.opt.assert_optimal_termination(res) - - init_log.info_high("Initialization Step 4 {}.".format(idaeslog.condition(res))) - - # set the wall thermal conductivity back to the user specified value - blk.therm_cond_wall.set_value(therm_cond_wall_save) - - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - res = opt.solve(blk, tee=slc.tee) - init_log.info_high("Initialization Step 5 {}.".format(idaeslog.condition(res))) - tube.release_state(flags_tube) - shell.release_state(flags_shell) - init_log.info("Initialization Complete.") - def calculate_scaling_factors(self): def gsf(obj): return iscale.get_scaling_factor(obj, default=1, warning=True) diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index bb16243506..711bc60d2e 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -18,9 +18,10 @@ # Import Pyomo libraries from pyomo.environ import ( assert_optimal_termination, + Block, + units as pyunits, Var, value, - units as pyunits, ) from pyomo.common.config import ConfigValue, In from pyomo.util.calc_var_value import calculate_variable_from_constraint @@ -44,13 +45,107 @@ from idaes.core.util.tables import create_stream_table_dataframe from idaes.core.util.model_statistics import degrees_of_freedom from idaes.models_extra.power_generation.unit_models.heat_exchanger_common import ( - make_geometry_common, # pylint: disable=W0212 - make_performance_common, # pylint: disable=W0212 - scale_common, # pylint: disable=W0212 + make_geometry_common, + make_performance_common, + scale_common, ) +from idaes.core.initialization import SingleControlVolumeUnitInitializer __author__ = "Jinliang Ma, Douglas Allan" +class Heater1DInitializer(SingleControlVolumeUnitInitializer): + """ + Initializer for Heater 1D units. + """ + def initialize_main_model( + self, + model: Block, + copy_inlet_state: bool = False, + duty=1000 * pyunits.W, + ): + """ + Initialization routine for the main Heater 1D model + (as opposed to submodels like costing, which presently do not exist). + + 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_obj = get_solver(self.config.solver, self.config.solver_options) + + # --------------------------------------------------------------------- + # Initialize shell block + + self.initialize_control_volume(model.control_volume, copy_inlet_state) + + init_log.info_high("Initialization Step 1 Complete.") + + calc_var = calculate_variable_from_constraint + + calc_var(model.length_flow_shell, model.length_flow_shell_eqn) + calc_var(model.area_flow_shell, model.area_flow_shell_eqn) + calc_var(model.area_flow_shell_min, model.area_flow_shell_min_eqn) + + for t in model.flowsheet().config.time: + for x in model.control_volume.length_domain: + model.control_volume.heat[t, x].fix( + value(model.electric_heat_duty[t] / model.length_flow_shell) + ) + + if model.config.has_pressure_change: + model.control_volume.pressure.fix() + + model.control_volume.length.fix() + assert degrees_of_freedom(model.control_volume) == 0 + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = solver_obj.solve(model.control_volume, tee=slc.tee) + + assert_optimal_termination(res) + + init_log.info_high("Initialization Step 2 Complete.") + model.control_volume.length.unfix() + model.control_volume.heat.unfix() + + for t in model.flowsheet().config.time: + for x in model.control_volume.length_domain: + model.temp_wall_center[t, x].fix( + value(model.control_volume.properties[t, x].temperature) + 10 + ) + calc_var(model.heat_holdup[t, x], model.heat_holdup_eqn[t, x]) + model.temp_wall_center[t, x].unfix() + + if model.config.has_pressure_change: + model.control_volume.pressure.unfix() + model.control_volume.pressure[:, 0].fix() + + assert degrees_of_freedom(model) == 0 + + with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: + res = solver_obj.solve(model, tee=slc.tee) + + assert_optimal_termination(res) + + init_log.info_high("Initialization Step 3 Complete.") + + return res @declare_process_block_class("Heater1D") class Heater1DData(UnitModelBlockData): @@ -391,94 +486,6 @@ def temp_wall_center_eqn(b, t, x): + b.electric_heat_duty[t] / b.length_flow_shell ) - def initialize_build(blk, state_args=None, outlvl=0, solver="ipopt", optarg=None): - """ - HeatExchangerCrossFlow1D initialization routine - - 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 = None). - outlvl : sets output level of initialization 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 information (tee=True) - - optarg : solver options dictionary object (default={'tol': 1e-6}) - solver : str indicating which solver to use during - initialization (default = 'ipopt') - - Returns: - None - """ - init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") - solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") - - if optarg is None: - optarg = {} - opt = get_solver(solver, optarg) - - # --------------------------------------------------------------------- - # Initialize shell block - - flags = blk.control_volume.initialize( - outlvl=0, optarg=optarg, solver=solver, state_args=state_args - ) - - init_log.info_high("Initialization Step 1 Complete.") - - calc_var = calculate_variable_from_constraint - - calc_var(blk.length_flow_shell, blk.length_flow_shell_eqn) - calc_var(blk.area_flow_shell, blk.area_flow_shell_eqn) - calc_var(blk.area_flow_shell_min, blk.area_flow_shell_min_eqn) - - for t in blk.flowsheet().config.time: - for x in blk.control_volume.length_domain: - blk.control_volume.heat[t, x].fix( - value(blk.electric_heat_duty[t] / blk.length_flow_shell) - ) - - if blk.config.has_pressure_change: - blk.control_volume.pressure.fix() - - blk.control_volume.length.fix() - assert degrees_of_freedom(blk.control_volume) == 0 - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - res = opt.solve(blk.control_volume, tee=slc.tee) - - assert_optimal_termination(res) - - init_log.info_high("Initialization Step 2 Complete.") - blk.control_volume.length.unfix() - blk.control_volume.heat.unfix() - - for t in blk.flowsheet().config.time: - for x in blk.control_volume.length_domain: - blk.temp_wall_center[t, x].fix( - value(blk.control_volume.properties[t, x].temperature) + 10 - ) - calc_var(blk.heat_holdup[t, x], blk.heat_holdup_eqn[t, x]) - blk.temp_wall_center[t, x].unfix() - - if blk.config.has_pressure_change: - blk.control_volume.pressure.unfix() - blk.control_volume.pressure[:, 0].fix() - - assert degrees_of_freedom(blk) == 0 - - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: - res = opt.solve(blk, tee=slc.tee) - - assert_optimal_termination(res) - - init_log.info_high("Initialization Step 3 Complete.") - - blk.control_volume.release_state(flags) - def calculate_scaling_factors(self): def gsf(obj): return iscale.get_scaling_factor(obj, default=1, warning=True) diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index 925ced596d..4d85664588 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -23,7 +23,10 @@ get_prop, EosType, ) -from idaes.models_extra.power_generation.unit_models import CrossFlowHeatExchanger1D +from idaes.models_extra.power_generation.unit_models import ( + CrossFlowHeatExchanger1D, + CrossFlowHeatExchanger1DInitializer +) import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.solvers import get_solver @@ -211,7 +214,11 @@ def test_initialization(model_no_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) - m.fs.heat_exchanger.initialize_build(optarg=optarg) + initializer = CrossFlowHeatExchanger1DInitializer( + solver="ipopt", + solver_options=optarg + ) + initializer.initialize(model=m.fs.heat_exchanger) assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) @@ -241,7 +248,11 @@ def test_initialization_dP(model_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) - m.fs.heat_exchanger.initialize_build(optarg=optarg) + initializer = CrossFlowHeatExchanger1DInitializer( + solver="ipopt", + solver_options=optarg + ) + initializer.initialize(m.fs.heat_exchanger) assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py index a44915c183..3c287cbd00 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -22,7 +22,7 @@ get_prop, EosType, ) -from idaes.models_extra.power_generation.unit_models import Heater1D +from idaes.models_extra.power_generation.unit_models import Heater1D, Heater1DInitializer import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.solvers import get_solver @@ -178,7 +178,11 @@ def test_initialization(model_no_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) - m.fs.heater.initialize_build(optarg=optarg) + initializer = Heater1DInitializer( + solver="ipopt", + solver_options=optarg + ) + initializer.initialize(model=m.fs.heater) assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) @@ -205,7 +209,11 @@ def test_initialization_dP(model_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) - m.fs.heater.initialize_build(optarg=optarg) + initializer = Heater1DInitializer( + solver="ipopt", + solver_options=optarg + ) + initializer.initialize(model=m.fs.heater) assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) From c721eb514a6dac26eea87953bcb5d714f66a5194 Mon Sep 17 00:00:00 2001 From: Doug A Date: Tue, 16 Apr 2024 12:19:17 -0400 Subject: [PATCH 23/38] documentation --- .../cross_flow_heat_exchanger_1D.rst | 241 ++++++++++++++++++ .../cross_flow_heat_exchanger_1D.py | 10 +- .../unit_models/heat_exchanger_common.py | 28 +- .../power_generation/unit_models/heater_1D.py | 2 +- .../test_cross_flow_heat_exchanger_1D.py | 8 +- .../unit_models/tests/test_heater_1D.py | 6 +- 6 files changed, 268 insertions(+), 27 deletions(-) create mode 100644 docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst diff --git a/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst new file mode 100644 index 0000000000..aa2427644c --- /dev/null +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst @@ -0,0 +1,241 @@ +======================== +CrossFlowHeatExchanger1D +======================== + +.. index:: + pair: idaes.models_extra.power_generation.unit_models.cross_flow_heat_exchanger_1D;CrossFlowHeatExchanger1D + +.. module:: idaes.models_extra.power_generation.unit_models.cross_flow_heat_exchanger_1D + +This model is for a cross flow heat exchanger between two gases. + +Example +======= + +.. code-block:: python + + import pyomo.environ as pyo + + from idaes.core import FlowsheetBlock + import idaes.core.util.scaling as iscale + from idaes.models.unit_models import HeatExchangerFlowPattern + from idaes.models.properties.modular_properties import GenericParameterBlock + from idaes.models_extra.power_generation.properties.natural_gas_PR import ( + get_prop, + EosType, + ) + from idaes.models_extra.power_generation.unit_models import CrossFlowHeatExchanger1D + import idaes.core.util.model_statistics as mstat + from idaes.core.util.model_statistics import degrees_of_freedom + from idaes.core.solvers import get_solver + + + # Set up solver + optarg = { + "constr_viol_tol": 1e-8, + "nlp_scaling_method": "user-scaling", + "linear_solver": "ma57", + "OF_ma57_automatic_scaling": "yes", + "max_iter": 350, + "tol": 1e-8, + "halt_on_ampl_error": "no", + } + solver = get_solver("ipopt", options=optarg) + + m = pyo.ConcreteModel() + m.fs = pyo.FlowsheetBlock(dynamic=False) + m.fs.properties = GenericParameterBlock( + **get_prop(["H2", "H2O", "Ar", "N2"], {"Vap"}, eos=EosType.IDEAL), + doc="H2O + H2 gas property parameters", + ) + m.fs.heat_exchanger = CrossFlowHeatExchanger1D( + has_holdup=True, + dynamic=False, + cold_side={ + "property_package": m.fs.h2_side_prop_params, + "has_holdup": False, + "dynamic": False, + "has_pressure_change": pressure_drop, + "transformation_method": "dae.finite_difference", + "transformation_scheme": "FORWARD", + }, + hot_side={ + "property_package": m.fs.h2_side_prop_params, + "has_holdup": False, + "dynamic": False, + "has_pressure_change": pressure_drop, + "transformation_method": "dae.finite_difference", + "transformation_scheme": "BACKWARD", + }, + shell_is_hot=True, + flow_type=HeatExchangerFlowPattern.countercurrent, + finite_elements=12, + tube_arrangement="staggered", + ) + + hx = m.fs.heat_exchanger + + hx.hot_side_inlet.flow_mol.fix(2619.7) + hx.hot_side_inlet.temperature.fix(971.6) + hx.hot_side_inlet.pressure.fix(1.2e5) + hx.hot_side_inlet.mole_frac_comp[0, "H2"].fix(0.79715) + hx.hot_side_inlet.mole_frac_comp[0, "H2O"].fix(0.20177) + hx.hot_side_inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) + hx.hot_side_inlet.mole_frac_comp[0, "N2"].fix(0.00021589) + + hx.cold_side_inlet.flow_mol.fix(2619.7) + hx.cold_side_inlet.temperature.fix(446.21) + hx.cold_side_inlet.pressure.fix(1.2e5) + hx.cold_side_inlet.mole_frac_comp[0, "H2"].fix(0.36203) + hx.cold_side_inlet.mole_frac_comp[0, "H2O"].fix(0.63689) + hx.cold_side_inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) + hx.cold_side_inlet.mole_frac_comp[0, "N2"].fix(0.00021589) + + hx.di_tube.fix(0.0525018) + hx.thickness_tube.fix(0.0039116) + hx.length_tube_seg.fix(4.3) + hx.nseg_tube.fix(12) + hx.ncol_tube.fix(50) + hx.nrow_inlet.fix(25) + + hx.pitch_x.fix(0.1) + hx.pitch_y.fix(0.1) + hx.therm_cond_wall = 43.0 + hx.rfouling_tube = 0.0001 + hx.rfouling_shell = 0.0001 + hx.fcorrection_htc_tube.fix(1) + hx.fcorrection_htc_shell.fix(1) + if pressure_drop: + hx.fcorrection_dp_tube.fix(1) + hx.fcorrection_dp_shell.fix(1) + + hx.cp_wall.value = 502.4 + + pp = m.fs.properties + pp.set_default_scaling("enth_mol_phase", 1e-3) + pp.set_default_scaling("pressure", 1e-5) + pp.set_default_scaling("temperature", 1e-2) + pp.set_default_scaling("flow_mol", 1e-3) + + _mf_scale = { + "H2": 1, + "H2O": 1, + "N2": 10, + "Ar": 10, + } + for comp, s in _mf_scale.items(): + pp.set_default_scaling("mole_frac_comp", s, index=comp) + pp.set_default_scaling("mole_frac_phase_comp", s, index=("Vap", comp)) + pp.set_default_scaling("flow_mol_phase_comp", s * 1e-3, index=("Vap", comp)) + + shell = hx.hot_side + tube = hx.cold_side + iscale.set_scaling_factor(shell.area, 1e-1) + iscale.set_scaling_factor(shell.heat, 1e-6) + iscale.set_scaling_factor(tube.area, 1) + iscale.set_scaling_factor(tube.heat, 1e-6) + iscale.set_scaling_factor(shell._enthalpy_flow, 1e-8) # pylint: disable=W0212 + iscale.set_scaling_factor(tube._enthalpy_flow, 1e-8) # pylint: disable=W0212 + iscale.set_scaling_factor(shell.enthalpy_flow_dx, 1e-7) + iscale.set_scaling_factor(tube.enthalpy_flow_dx, 1e-7) + iscale.set_scaling_factor(hx.heat_holdup, 1e-8) + + iscale.calculate_scaling_factors(m) + + initializer = CrossFlowHeatExchanger1DInitializer( + solver="ipopt", + solver_options=optarg + ) + initializer.initialize(m.fs.heat_exchanger) + + + + +Heat Exchanger Geometry +======================= + +Variables +--------- + +This model features many variables describing the geometry of the heat exchanger in +order to accurately calculate heat transfer coefficients, pressure drop, and heat holdup. + +=========================== =========== ================================================================================= +Variable Index Sets Doc +=========================== =========== ================================================================================= +``number_columns_per_pass`` None Number of columns of tube per pass +``number_rows_per_pass`` None Number of rows of tube per pass +``number_passes`` None Number of tube banks of `nrow_tube * ncol_inlet` tubes +``pitch_x`` None Distance between columns (TODO rows?) of tubes, measured from center-of-tube to center-of-tube +``pitch_y`` None Distance between rows (TODO columns?) of tubes, measured from center-of-tube to center-of-tube +``length_tube_seg`` None Length of tube segment perpendicular to flow in each pass +``area_flow_shell_min`` None Minimum flow area on shell side +``di_tube`` None Inner diameter of tubes +``thickness_tube`` None Thickness of tube wall. +=========================== =========== ================================================================================= + + +Expressions +----------- + +=========================== =========== =========================================================================== +Expression Index Sets Doc +=========================== =========== =========================================================================== +``nrow_tube`` None Total number of rows of tube +``do_tube`` None Outer diameter of tube (equal to `di_tube+2*thickness_tube`) +``pitch_x_to_do`` None Ratio of `pitch_x` to `do_tube` +``pitch_y_to_do`` None Ratio of `pitch_y` to `do_tube` +``area_wall_seg`` None Total cross-sectional area of tube per pass +=========================== =========== =========================================================================== + +Constraints +----------- + +=========================== =========== ================================================================================================= +Constraint Index Sets Doc +=========================== =========== ================================================================================================= +``length_flow_shell_eqn`` None Constrains shell flow length from control volume to equal value implied by geometry +``area_flow_shell_eqn`` None Constrains shell flow cross-sectional area from control volume to equal value implied by geometry + +=========================== =========== ================================================================================================= + + +=============== + +Constraints +----------- + +The pressure flow relation is added to the inherited constraints from the :ref:`PressureChanger model +`. + +If the ``phase`` option is set to ``"Liq"`` the following equation describes the pressure-flow relation. + +.. math:: + + \frac{1}{s_f^2}F^2 = \frac{1}{s_f^2}C_v^2\left(P_{in} - P_{out}\right)f(x)^2 + +If the ``phase`` option is set to ``"Vap"`` the following equation describes the pressure-flow relation. + +.. math:: + + \frac{1}{s_f^2}F^2 = \frac{1}{s_f^2}C_v^2\left(P_{in}^2 - P_{out}^2\right)f(x)^2 + + +Initialization +-------------- + +This just calls the initialization routine from PressureChanger, but it is wrapped in +a function to ensure the state after initialization is the same as before initialization. +The arguments to the initialization method are the same as PressureChanger. + +HelmValve Class +---------------- + +.. autoclass:: HelmValve + :members: + +HelmValveData Class +--------------------- + +.. autoclass:: HelmValveData + :members: diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 0a9d8608be..782751acac 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -589,8 +589,8 @@ def heat_tube_eqn(b, t, x): b.hconv_tube[t, x] * const.pi * pyunits.convert(b.di_tube, to_units=tube_units["length"]) - * b.nrow_inlet - * b.ncol_tube + * b.number_rows_per_pass + * b.number_columns_per_pass * (b.temp_wall_tube[t, x] - tube.properties[t, x].temperature) ) @@ -605,7 +605,7 @@ def heat_shell_eqn(b, t, x): b.length_flow_tube, to_units=shell_units["length"] ) * b.hconv_shell_total[ t, x - ] * const.pi * b.do_tube * b.nrow_inlet * b.ncol_tube * ( + ] * const.pi * b.do_tube * b.number_rows_per_pass * b.number_columns_per_pass * ( b.temp_wall_shell[t, x] - shell.properties[t, x].temperature ) @@ -744,8 +744,8 @@ def cst(con, sf): pyunits.convert(self.length_flow_tube, to_units=shell_units["length"]) * const.pi * self.do_tube - * self.nrow_inlet - * self.ncol_tube + * self.number_rows_per_pass + * self.number_columns_per_pass ) ) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index c0e6efdadf..62cd915f07 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -38,19 +38,19 @@ def make_geometry_common(blk, shell, shell_units): # Number of tube columns in the cross section plane perpendicular to shell side fluid flow (y direction) - blk.ncol_tube = Var( + blk.number_columns_per_pass = Var( initialize=10.0, doc="Number of tube columns", units=pyunits.dimensionless ) # Number of segments of tube bundles - blk.nseg_tube = Var( + blk.number_passes = Var( initialize=10.0, doc="Number of segments of tube bundles", units=pyunits.dimensionless, ) # Number of inlet tube rows - blk.nrow_inlet = Var( + blk.number_rows_per_pass = Var( initialize=1, doc="Number of inlet tube rows", units=pyunits.dimensionless ) @@ -91,7 +91,7 @@ def make_geometry_common(blk, shell, shell_units): # total number of tube rows @blk.Expression(doc="Total number of tube rows") def nrow_tube(b): - return b.nseg_tube * b.nrow_inlet + return b.number_passes * b.number_rows_per_pass # Tube outside diameter @blk.Expression(doc="Outside diameter of tube") @@ -115,8 +115,8 @@ def area_wall_seg(b): 0.25 * const.pi * (b.do_tube**2 - b.di_tube**2) - * b.ncol_tube - * b.nrow_inlet + * b.number_columns_per_pass + * b.number_rows_per_pass ) # Length of shell side flow @@ -129,8 +129,8 @@ def length_flow_shell_eqn(b): def area_flow_shell_eqn(b): return ( b.length_flow_shell * b.area_flow_shell - == b.length_tube_seg * b.length_flow_shell * b.pitch_y * b.ncol_tube - - b.ncol_tube + == b.length_tube_seg * b.length_flow_shell * b.pitch_y * b.number_columns_per_pass + - b.number_columns_per_pass * b.nrow_tube * 0.25 * const.pi @@ -143,7 +143,7 @@ def area_flow_shell_eqn(b): def area_flow_shell_min_eqn(b): return ( b.area_flow_shell_min - == b.length_tube_seg * (b.pitch_y - b.do_tube) * b.ncol_tube + == b.length_tube_seg * (b.pitch_y - b.do_tube) * b.number_columns_per_pass ) @blk.Expression() @@ -151,9 +151,9 @@ def total_heat_transfer_area(b): return ( const.pi * b.do_tube - * b.nrow_inlet - * b.ncol_tube - * b.nseg_tube + * b.number_rows_per_pass + * b.number_columns_per_pass + * b.number_passes * b.length_tube_seg ) @@ -164,7 +164,7 @@ def make_geometry_tube(blk, shell_units): def length_flow_tube_eqn(b): return ( pyunits.convert(b.length_flow_tube, to_units=shell_units["length"]) - == b.nseg_tube * b.length_tube_seg + == b.number_passes * b.length_tube_seg ) # Total flow area on tube side @@ -172,7 +172,7 @@ def length_flow_tube_eqn(b): def area_flow_tube_eqn(b): return ( b.area_flow_tube - == 0.25 * const.pi * b.di_tube**2.0 * b.ncol_tube * b.nrow_inlet + == 0.25 * const.pi * b.di_tube**2.0 * b.number_columns_per_pass * b.number_rows_per_pass ) diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 711bc60d2e..177d6a56fa 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -391,7 +391,7 @@ def _make_geometry(self): doc="Common performance equations expect this expression to be here" ) def length_flow_tube(b): - return b.nseg_tube * b.length_tube_seg + return b.number_passes * b.length_tube_seg def _make_performance(self): """ diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index 4d85664588..50c738aa2c 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -97,9 +97,9 @@ def _create_model(pressure_drop): hx.di_tube.fix(0.0525018) hx.thickness_tube.fix(0.0039116) hx.length_tube_seg.fix(4.3) - hx.nseg_tube.fix(12) - hx.ncol_tube.fix(50) - hx.nrow_inlet.fix(25) + hx.number_passes.fix(12) + hx.number_columns_per_pass.fix(50) + hx.number_rows_per_pass.fix(25) hx.pitch_x.fix(0.1) hx.pitch_y.fix(0.1) @@ -253,7 +253,7 @@ def test_initialization_dP(model_dP): solver_options=optarg ) initializer.initialize(m.fs.heat_exchanger) - + assert False assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py index 3c287cbd00..d24da90214 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -74,15 +74,15 @@ def _create_model(pressure_drop): heater.pitch_x.fix(0.1) heater.pitch_y.fix(0.1) heater.length_tube_seg.fix(10) - heater.nseg_tube.fix(1) + heater.number_passes.fix(1) heater.rfouling = 0.0001 heater.fcorrection_htc_shell.fix(1) heater.cp_wall = 502.4 if pressure_drop: heater.fcorrection_dp_shell.fix(1) - heater.ncol_tube.fix(40) - heater.nrow_inlet.fix(40) + heater.number_columns_per_pass.fix(40) + heater.number_rows_per_pass.fix(40) heater.electric_heat_duty.fix(3.6504e06) pp = m.fs.h2_side_prop_params From 04153e59fb0ce80cfed9756d83a3bf0ac3dab243 Mon Sep 17 00:00:00 2001 From: Doug A Date: Tue, 16 Apr 2024 17:55:54 -0400 Subject: [PATCH 24/38] Documentation in progress --- .../cross_flow_heat_exchanger_1D.rst | 159 ++++++++++++++---- .../cross_flow_heat_exchanger_1D.py | 12 +- .../unit_models/heat_exchanger_common.py | 71 ++++---- .../power_generation/unit_models/heater_1D.py | 8 +- 4 files changed, 169 insertions(+), 81 deletions(-) diff --git a/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst index aa2427644c..34bddbd5e1 100644 --- a/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst @@ -1,4 +1,3 @@ -======================== CrossFlowHeatExchanger1D ======================== @@ -10,7 +9,7 @@ CrossFlowHeatExchanger1D This model is for a cross flow heat exchanger between two gases. Example -======= +------- .. code-block:: python @@ -152,14 +151,7 @@ Example Heat Exchanger Geometry -======================= - -Variables ---------- - -This model features many variables describing the geometry of the heat exchanger in -order to accurately calculate heat transfer coefficients, pressure drop, and heat holdup. - +----------------------- =========================== =========== ================================================================================= Variable Index Sets Doc =========================== =========== ================================================================================= @@ -174,33 +166,131 @@ Variable Index Sets Doc ``thickness_tube`` None Thickness of tube wall. =========================== =========== ================================================================================= - -Expressions ------------ - -=========================== =========== =========================================================================== -Expression Index Sets Doc -=========================== =========== =========================================================================== -``nrow_tube`` None Total number of rows of tube -``do_tube`` None Outer diameter of tube (equal to `di_tube+2*thickness_tube`) -``pitch_x_to_do`` None Ratio of `pitch_x` to `do_tube` -``pitch_y_to_do`` None Ratio of `pitch_y` to `do_tube` -``area_wall_seg`` None Total cross-sectional area of tube per pass -=========================== =========== =========================================================================== - -Constraints ------------ +============================ =========== =========================================================================== +Expression Index Sets Doc +============================ =========== =========================================================================== +``nrow_tube`` None Total number of rows of tube +``do_tube`` None Outer diameter of tube (equal to `di_tube+2*thickness_tube`) +``pitch_x_to_do`` None Ratio of `pitch_x` to `do_tube` +``pitch_y_to_do`` None Ratio of `pitch_y` to `do_tube` +``area_wall_seg`` None Total cross-sectional area of tube per pass +``total_heat_transfer_area`` None Total heat transfer area, as measured on outer surface of tubes +============================ =========== =========================================================================== =========================== =========== ================================================================================================= Constraint Index Sets Doc =========================== =========== ================================================================================================= ``length_flow_shell_eqn`` None Constrains shell flow length from control volume to equal value implied by geometry ``area_flow_shell_eqn`` None Constrains shell flow cross-sectional area from control volume to equal value implied by geometry - +``area_flow_shell_min_eqn`` None Constraints `area_flow_shell_min` to equal value determined by geometry +``length_flow_tube_eqn`` None Constrains tube flow length from control volume to equal value implied by geometry +``area_flow_tube_eqn`` None Constrains tube flow cross-sectional area from control volume to equal value implied by geometry =========================== =========== ================================================================================================= +Performance Equations +----------------------- + +================================== ============ ================================================================================= +Variable Index Sets Doc +================================== ============ ================================================================================= +``fcorrection_htc_shell`` time, length Correction factor for shell convective heat transfer +``conv_heat_transfer_coeff_shell`` time, length Shell-side convective heat transfer coefficient +``temp_wall_shell`` time, length Shell-side wall temperature of tube +``temp_wall_center`` time, length Temperature at center of tube wall +``v_shell`` time, length Flow velocity on shell side through minimum area +``N_Re_shell`` time, length Reynolds number on shell side +``N_Nu_shell`` time, length Nusselt number on shell side +``heat_transfer_coeff_tube`` time, length Tube-side heat transfer coefficient +``temp_wall_tube`` time, length Tube-side wall temperature of tube +``v_tube`` time, length Flow velocity of gas in tube +``N_Re_tube`` time, length Reynolds number in tube +``N_Nu_tube`` time, length Nusselt number on tube side +================================== ============ ================================================================================= + +=========================== =========== ================================================================================= +Parameter Index Sets Doc +=========================== =========== ================================================================================= +``therm_cond_wall`` None Thermal conductivity of tube wall +``density_wall`` None Mass density of tube wall metal +``cp_wall`` None Tube wall heat capacity (mass basis) +``rfouling_shell`` None Fouling resistance on shell side +``f_arrangement`` None Adjustment factor depending on `tube_arrangement` in config +=========================== =========== ================================================================================= + +====================================== ============ ================================================================================= +Constraint Index Sets Doc +====================================== ============ ================================================================================= +``v_shell_eqn`` time, length Calculates velocity of flow through shell using `area_flow_shell_min` +``N_Re_shell_eqn`` time, length Calculates the shell-side Reynolds number +``conv_heat_transfer_coeff_shell_eqn`` time, length Calculates the shell-side convective heat transfer coefficient +``v_tube_eqn`` time, length Calculates gas velocity in tube +``N_Re_tube_eqn`` time, length Calculates the tube-side Reynolds number +``heat_transfer_coeff_tube_eqn`` time, length Calcualtes the tube-side heat transfer coefficient +====================================== ============ ================================================================================= + +====================================== ============ =================================================================================== +Expression Index Sets Doc +====================================== ============ =================================================================================== +``total_heat_transfer_coeff_shell`` time, length Returns ``conv_heat_transfer_coeff_shell``. Could be extended to include radiation. +====================================== ============ =================================================================================== + +Pressure Change Equations +------------------------- + +=========================== ============ ================================================================================= +Parameter Index Sets Doc +=========================== ============ ================================================================================= +``fcorrection_dp_shell`` None Correction factor for shell side pressure drop +``kloss_uturn`` None Loss coefficient of a tube u-turn +``fcorrection_dp_tube`` None Correction factor for tube side pressure drop +=========================== ============ ================================================================================= + +=========================== ============ ================================================================================= +Variable Index Sets Doc +=========================== ============ ================================================================================= +``fcorrection_dp_shell`` None Correction factor for shell side pressure drop +``friction_factor_shell`` time, length Friction factor on shell side +``friction_factor_tube`` time, length Friction factor on tube side +``deltaP_tube_friction`` time, length Change of pressure in tube due to friction +``deltaP_tube_uturn`` time, length Change of pressure in tube due to U turns +=========================== ============ ================================================================================= + +================================== ============ ================================================================================= +Constraint Index Sets Doc +================================== ============ ================================================================================= +``friction_factor_shell_eqn`` time, length Calculates the shell-side friction factor +``deltaP_shell_eqn`` time, length Sets `deltaP_shell` based on the friction factor and shell properties +``friction_factor_tube_eqn`` time, length Calculates the tube-side friction factor +``deltaP_tube_friction_eqn`` time, length Sets `deltaP_tube_friction` based on friction factor +``deltaP_tube_uturn_eqn`` time, length Sets `deltaP_tube_uturn` based on `kloss_uturn` +``deltaP_tube_eqn`` time, length Sets `deltaP_tube` by summing `deltaP_tube_friction` and `deltaP_tube_uturn` +================================== ============ ================================================================================= + + +Holdup Equations +---------------- +Created when `has_holdup=True` in the config. +=========================== ============ ================================================================================= +Variable Index Sets Doc +=========================== ============ ================================================================================= +``heat_holdup`` time, length Energy holdup per unit length of shell flow path +=========================== ============ ================================================================================= + +=========================== ============ ================================================================================= +Constraint Index Sets Doc +=========================== ============ ================================================================================= +``heat_holdup_eqn`` time, length Defines heat holdup in terms of geometry and physical properties +=========================== ============ ================================================================================= + +Dynamic Equations +----------------- +Created when `dynamic=True` in the config. +=========================== ============ ================================================================================= +Derivative Variable Index Sets Doc +=========================== ============ ================================================================================= +``heat_accumulation`` time, length Energy accumulation in tube wall per unit length of shell flow path per unit time +=========================== ============ ================================================================================= -=============== Constraints ----------- @@ -223,19 +313,22 @@ If the ``phase`` option is set to ``"Vap"`` the following equation describes the Initialization -------------- +First, the shell and tube control volumes are initialized without heat transfer. Next +the total possible heat transfer between streams is estimated based on heat capacity, +flow rate, and inlet/outlet temperatures. The actual temperature change is set to be +half the theoretical maximum, and the shell and tube are initalized with linear +temperature profiles. Finally, temperatures besides the inlets are unfixed and +the performance equations are activated before a full solve of the system model. -This just calls the initialization routine from PressureChanger, but it is wrapped in -a function to ensure the state after initialization is the same as before initialization. -The arguments to the initialization method are the same as PressureChanger. HelmValve Class ---------------- -.. autoclass:: HelmValve +.. autoclass:: CrossFlowHeatExchanger1D :members: HelmValveData Class --------------------- -.. autoclass:: HelmValveData +.. autoclass:: CrossFlowHeatExchanger1DData :members: diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 782751acac..43af3f8444 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -586,7 +586,7 @@ def N_Nu_shell_eqn(b, t, x): ) def heat_tube_eqn(b, t, x): return b.heat_tube[t, x] == ( - b.hconv_tube[t, x] + b.heat_transfer_coeff_tube[t, x] * const.pi * pyunits.convert(b.di_tube, to_units=tube_units["length"]) * b.number_rows_per_pass @@ -603,7 +603,7 @@ def heat_tube_eqn(b, t, x): def heat_shell_eqn(b, t, x): return b.heat_shell[t, x] * b.length_flow_shell == pyunits.convert( b.length_flow_tube, to_units=shell_units["length"] - ) * b.hconv_shell_total[ + ) * b.total_heat_transfer_coeff_shell[ t, x ] * const.pi * b.do_tube * b.number_rows_per_pass * b.number_columns_per_pass * ( b.temp_wall_shell[t, x] - shell.properties[t, x].temperature @@ -617,7 +617,7 @@ def heat_shell_eqn(b, t, x): doc="tube side wall temperature", ) def temp_wall_tube_eqn(b, t, x): - return b.hconv_tube[t, x] * ( + return b.heat_transfer_coeff_tube[t, x] * ( tube.properties[t, x].temperature - b.temp_wall_tube[t, x] ) * ( pyunits.convert( @@ -639,7 +639,7 @@ def temp_wall_tube_eqn(b, t, x): ) def temp_wall_shell_eqn(b, t, x): return ( - b.hconv_shell_total[t, x] + b.total_heat_transfer_coeff_shell[t, x] * (shell.properties[t, x].temperature - b.temp_wall_shell[t, x]) * (b.thickness_tube / 2 / b.therm_cond_wall + b.rfouling_shell) == b.temp_wall_shell[t, x] - b.temp_wall_center[t, x] @@ -762,10 +762,10 @@ def cst(con, sf): ssf(self.temp_wall_shell[t, z], sf_T_shell) cst(self.temp_wall_shell_eqn[t, z], sf_T_shell) - sf_hconv_shell_conv = gsf(self.hconv_shell_conv[t, z]) + sf_conv_heat_transfer_coeff_shell = gsf(self.conv_heat_transfer_coeff_shell[t, z]) s_Q_shell = sgsf( shell.heat[t, z], - sf_hconv_shell_conv * sf_area_per_length_shell * sf_T_shell, + sf_conv_heat_transfer_coeff_shell * sf_area_per_length_shell * sf_T_shell, ) cst( self.heat_shell_eqn[t, z], s_Q_shell * value(self.length_flow_shell) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 62cd915f07..ff37f00cbf 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -119,6 +119,17 @@ def area_wall_seg(b): * b.number_rows_per_pass ) + @blk.Expression(doc="Total heat transfer area on outer surface of tubes") + def total_heat_transfer_area(b): + return ( + const.pi + * b.do_tube + * b.number_rows_per_pass + * b.number_columns_per_pass + * b.number_passes + * b.length_tube_seg + ) + # Length of shell side flow @blk.Constraint(doc="Length of shell side flow") def length_flow_shell_eqn(b): @@ -146,17 +157,6 @@ def area_flow_shell_min_eqn(b): == b.length_tube_seg * (b.pitch_y - b.do_tube) * b.number_columns_per_pass ) - @blk.Expression() - def total_heat_transfer_area(b): - return ( - const.pi - * b.do_tube - * b.number_rows_per_pass - * b.number_columns_per_pass - * b.number_passes - * b.length_tube_seg - ) - def make_geometry_tube(blk, shell_units): # Length of tube side flow @@ -234,7 +234,7 @@ def make_performance_common( # Performance variables # Shell side convective heat transfer coefficient due to convection only - blk.hconv_shell_conv = Var( + blk.conv_heat_transfer_coeff_shell = Var( blk.flowsheet().config.time, shell.length_domain, initialize=100.0, @@ -377,8 +377,7 @@ def N_Re_shell_eqn(b, t, x): b.N_Re_shell[t, x] * shell.properties[t, x].visc_d_phase["Vap"] == b.do_tube * b.v_shell[t, x] - * shell.properties[t, x].dens_mol_phase["Vap"] - * shell.properties[t, x].mw + * shell.properties[t, x].dens_mass_phase["Vap"] ) if shell_has_pressure_change: @@ -430,8 +429,7 @@ def deltaP_shell_eqn(b, t, x): b.deltaP_shell[t, x] * b.pitch_x == -1.4 * b.friction_factor_shell[t, x] - * shell.properties[t, x].dens_mol_phase["Vap"] - * shell.properties[t, x].mw + * shell.properties[t, x].dens_mass_phase["Vap"] * b.v_shell[t, x] ** 2 ) @@ -442,9 +440,9 @@ def deltaP_shell_eqn(b, t, x): shell.length_domain, doc="Convective heat transfer coefficient equation on shell side due to convection", ) - def hconv_shell_conv_eqn(b, t, x): + def conv_heat_transfer_coeff_shell_eqn(b, t, x): return ( - b.hconv_shell_conv[t, x] * b.do_tube + b.conv_heat_transfer_coeff_shell[t, x] * b.do_tube == b.N_Nu_shell[t, x] * shell.properties[t, x].therm_cond_phase["Vap"] * b.fcorrection_htc_shell @@ -456,9 +454,9 @@ def hconv_shell_conv_eqn(b, t, x): shell.length_domain, doc="Total convective heat transfer coefficient on shell side", ) - def hconv_shell_total(b, t, x): + def total_heat_transfer_coeff_shell(b, t, x): # Retain in case we add back radiation - return b.hconv_shell_conv[t, x] + return b.conv_heat_transfer_coeff_shell[t, x] def make_performance_tube( @@ -469,7 +467,7 @@ def make_performance_tube( if tube_has_pressure_change: make_reynolds = True - blk.hconv_tube = Var( + blk.heat_transfer_coeff_tube = Var( blk.flowsheet().config.time, tube.length_domain, initialize=100.0, @@ -477,11 +475,6 @@ def make_performance_tube( doc="tube side convective heat transfer coefficient", ) - # Loss coefficient for a 180 degree bend (u-turn), usually related to radius to inside diameter ratio - blk.kloss_uturn = Param( - initialize=0.5, mutable=True, doc="loss coefficient of a tube u-turn" - ) - # Heat transfer resistance due to the fouling on tube side blk.rfouling_tube = Param( initialize=0.0, @@ -495,6 +488,10 @@ def make_performance_tube( ) # Correction factor for tube side pressure drop due to friction if tube_has_pressure_change: + # Loss coefficient for a 180 degree bend (u-turn), usually related to radius to inside diameter ratio + blk.kloss_uturn = Param( + initialize=0.5, mutable=True, doc="loss coefficient of a tube u-turn" + ) blk.fcorrection_dp_tube = Var( initialize=1.0, doc="correction factor for tube side pressure drop" ) @@ -621,8 +618,7 @@ def deltaP_tube_friction_eqn(b, t, x): b.deltaP_tube_friction[t, x] * pyunits.convert(b.di_tube, to_units=tube_units["length"]) == -0.5 - * tube.properties[t, x].dens_mol_phase["Vap"] - * tube.properties[t, x].mw + * tube.properties[t, x].dens_mass_phase["Vap"] * b.v_tube[t, x] ** 2 * b.friction_factor_tube[t, x] ) @@ -638,8 +634,7 @@ def deltaP_tube_uturn_eqn(b, t, x): b.deltaP_tube_uturn[t, x] * pyunits.convert(b.length_tube_seg, to_units=tube_units["length"]) == -0.5 - * tube.properties[t, x].dens_mol_phase["Vap"] - * tube.properties[t, x].mw + * tube.properties[t, x].dens_mass_phase["Vap"] * b.v_tube[t, x] ** 2 * b.kloss_uturn ) @@ -662,9 +657,9 @@ def deltaP_tube_eqn(b, t, x): tube.length_domain, doc="convective heat transfer coefficient equation on tube side", ) - def hconv_tube_eqn(b, t, x): + def heat_transfer_coeff_tube_eqn(b, t, x): return ( - b.hconv_tube[t, x] * b.di_tube + b.heat_transfer_coeff_tube[t, x] * b.di_tube == b.N_Nu_tube[t, x] * tube.properties[t, x].therm_cond_phase["Vap"] * b.fcorrection_htc_tube @@ -709,10 +704,10 @@ def cst(con, sf): ) cst(blk.N_Nu_shell_eqn[t, z], sf_N_Nu_shell) - sf_hconv_shell_conv = sgsf( - blk.hconv_shell_conv[t, z], sf_N_Nu_shell * sf_k_shell / sf_do_tube + sf_conv_heat_transfer_coeff_shell = sgsf( + blk.conv_heat_transfer_coeff_shell[t, z], sf_N_Nu_shell * sf_k_shell / sf_do_tube ) - cst(blk.hconv_shell_conv_eqn[t, z], sf_hconv_shell_conv * sf_do_tube) + cst(blk.conv_heat_transfer_coeff_shell_eqn[t, z], sf_conv_heat_transfer_coeff_shell * sf_do_tube) # FIXME estimate from parameters if blk.config.has_holdup: @@ -758,7 +753,7 @@ def cst(con, sf): ) cst(blk.N_Nu_tube_eqn[t, z], sf_N_Nu_tube) - sf_hconv_tube = sgsf( - blk.hconv_tube[t, z], sf_N_Nu_tube * sf_k_tube / sf_di_tube + sf_heat_transfer_coeff_tube = sgsf( + blk.heat_transfer_coeff_tube[t, z], sf_N_Nu_tube * sf_k_tube / sf_di_tube ) - cst(blk.hconv_tube_eqn[t, z], sf_hconv_tube * sf_di_tube) + cst(blk.heat_transfer_coeff_tube_eqn[t, z], sf_heat_transfer_coeff_tube * sf_di_tube) diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 177d6a56fa..68c788d664 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -448,7 +448,7 @@ def N_Nu_shell_eqn(b, t, x): ) def heat_shell_eqn(b, t, x): return b.control_volume.heat[t, x] * b.length_flow_shell == ( - b.hconv_shell_total[t, x] + b.total_heat_transfer_coeff_shell[t, x] * b.total_heat_transfer_area * ( b.temp_wall_shell[t, x] @@ -464,7 +464,7 @@ def heat_shell_eqn(b, t, x): ) def temp_wall_shell_eqn(b, t, x): return ( - b.hconv_shell_total[t, x] + b.total_heat_transfer_coeff_shell[t, x] * ( b.control_volume.properties[t, x].temperature - b.temp_wall_shell[t, x] @@ -510,8 +510,8 @@ def cst(con, sf): for t in self.flowsheet().time: for z in self.control_volume.length_domain: - sf_hconv_conv = gsf(self.hconv_shell_conv[t, z]) - cst(self.hconv_shell_conv_eqn[t, z], sf_hconv_conv * sf_d_tube) + sf_hconv_conv = gsf(self.conv_heat_transfer_coeff_shell[t, z]) + cst(self.conv_heat_transfer_coeff_shell_eqn[t, z], sf_hconv_conv * sf_d_tube) sf_T = gsf(self.control_volume.properties[t, z].temperature) ssf(self.temp_wall_shell[t, z], sf_T) From 53c384957659487dd7963de34ccbc21205f1e488 Mon Sep 17 00:00:00 2001 From: Doug A Date: Tue, 16 Apr 2024 17:57:47 -0400 Subject: [PATCH 25/38] remove debugging --- .../unit_models/tests/test_cross_flow_heat_exchanger_1D.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index 50c738aa2c..d132f99157 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -253,7 +253,7 @@ def test_initialization_dP(model_dP): solver_options=optarg ) initializer.initialize(m.fs.heat_exchanger) - assert False + assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) From e8068b7cf204920d44b4a54ca03e0aabe3a1ce3f Mon Sep 17 00:00:00 2001 From: Doug A Date: Wed, 17 Apr 2024 17:24:39 -0400 Subject: [PATCH 26/38] Documentation --- .../cross_flow_heat_exchanger_1D.rst | 99 +++---- .../unit_models/heater_1D.rst | 268 ++++++++++++++++++ .../power_generation/unit_models/index.rst | 2 + .../cross_flow_heat_exchanger_1D.py | 2 + .../power_generation/unit_models/heater_1D.py | 5 +- .../test_cross_flow_heat_exchanger_1D.py | 6 +- .../unit_models/tests/test_heater_1D.py | 6 +- 7 files changed, 328 insertions(+), 60 deletions(-) create mode 100644 docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst diff --git a/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst index 34bddbd5e1..d695698545 100644 --- a/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst @@ -6,7 +6,8 @@ CrossFlowHeatExchanger1D .. module:: idaes.models_extra.power_generation.unit_models.cross_flow_heat_exchanger_1D -This model is for a cross flow heat exchanger between two gases. +This model is for a cross flow heat exchanger between two gases. The gas in the shell has a straight path, +while the gas in the tubes snakes back and forth across the shell's path. Example ------- @@ -24,12 +25,8 @@ Example EosType, ) from idaes.models_extra.power_generation.unit_models import CrossFlowHeatExchanger1D - import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom - from idaes.core.solvers import get_solver - - # Set up solver optarg = { "constr_viol_tol": 1e-8, "nlp_scaling_method": "user-scaling", @@ -39,7 +36,6 @@ Example "tol": 1e-8, "halt_on_ampl_error": "no", } - solver = get_solver("ipopt", options=optarg) m = pyo.ConcreteModel() m.fs = pyo.FlowsheetBlock(dynamic=False) @@ -93,9 +89,9 @@ Example hx.di_tube.fix(0.0525018) hx.thickness_tube.fix(0.0039116) hx.length_tube_seg.fix(4.3) - hx.nseg_tube.fix(12) - hx.ncol_tube.fix(50) - hx.nrow_inlet.fix(25) + hx.number_passes.fix(12) + hx.number_columns_per_pass.fix(50) + hx.number_rows_per_pass.fix(25) hx.pitch_x.fix(0.1) hx.pitch_y.fix(0.1) @@ -104,9 +100,6 @@ Example hx.rfouling_shell = 0.0001 hx.fcorrection_htc_tube.fix(1) hx.fcorrection_htc_shell.fix(1) - if pressure_drop: - hx.fcorrection_dp_tube.fix(1) - hx.fcorrection_dp_shell.fix(1) hx.cp_wall.value = 502.4 @@ -141,7 +134,7 @@ Example iscale.calculate_scaling_factors(m) - initializer = CrossFlowHeatExchanger1DInitializer( + initializer = m.fs.heat_exchanger.default_initializer( solver="ipopt", solver_options=optarg ) @@ -157,22 +150,26 @@ Variable Index Sets Doc =========================== =========== ================================================================================= ``number_columns_per_pass`` None Number of columns of tube per pass ``number_rows_per_pass`` None Number of rows of tube per pass -``number_passes`` None Number of tube banks of `nrow_tube * ncol_inlet` tubes +``number_passes`` None Number of tube banks of ``nrow_tube * ncol_inlet`` tubes ``pitch_x`` None Distance between columns (TODO rows?) of tubes, measured from center-of-tube to center-of-tube ``pitch_y`` None Distance between rows (TODO columns?) of tubes, measured from center-of-tube to center-of-tube ``length_tube_seg`` None Length of tube segment perpendicular to flow in each pass +``area_flow_shell`` None Reference to flow area on shell control volume +``length_flow_shell`` None Reference to flow length on shell control volume ``area_flow_shell_min`` None Minimum flow area on shell side ``di_tube`` None Inner diameter of tubes ``thickness_tube`` None Thickness of tube wall. +``area_flow_tube`` None Reference to flow area on tube control volume +``length_flow_tube`` None Reference to flow length on tube control volume =========================== =========== ================================================================================= ============================ =========== =========================================================================== Expression Index Sets Doc ============================ =========== =========================================================================== ``nrow_tube`` None Total number of rows of tube -``do_tube`` None Outer diameter of tube (equal to `di_tube+2*thickness_tube`) -``pitch_x_to_do`` None Ratio of `pitch_x` to `do_tube` -``pitch_y_to_do`` None Ratio of `pitch_y` to `do_tube` +``do_tube`` None Outer diameter of tube (equal to ``di_tube+2*thickness_tube``) +``pitch_x_to_do`` None Ratio of ``pitch_x`` to ``do_tube`` +``pitch_y_to_do`` None Ratio of ``pitch_y`` to ``do_tube`` ``area_wall_seg`` None Total cross-sectional area of tube per pass ``total_heat_transfer_area`` None Total heat transfer area, as measured on outer surface of tubes ============================ =========== =========================================================================== @@ -182,7 +179,7 @@ Constraint Index Sets Doc =========================== =========== ================================================================================================= ``length_flow_shell_eqn`` None Constrains shell flow length from control volume to equal value implied by geometry ``area_flow_shell_eqn`` None Constrains shell flow cross-sectional area from control volume to equal value implied by geometry -``area_flow_shell_min_eqn`` None Constraints `area_flow_shell_min` to equal value determined by geometry +``area_flow_shell_min_eqn`` None Constraints ``area_flow_shell_min`` to equal value determined by geometry ``length_flow_tube_eqn`` None Constrains tube flow length from control volume to equal value implied by geometry ``area_flow_tube_eqn`` None Constrains tube flow cross-sectional area from control volume to equal value implied by geometry =========================== =========== ================================================================================================= @@ -214,24 +211,34 @@ Parameter Index Sets Doc ``density_wall`` None Mass density of tube wall metal ``cp_wall`` None Tube wall heat capacity (mass basis) ``rfouling_shell`` None Fouling resistance on shell side -``f_arrangement`` None Adjustment factor depending on `tube_arrangement` in config +``f_arrangement`` None Adjustment factor depending on ``tube_arrangement`` in config =========================== =========== ================================================================================= ====================================== ============ ================================================================================= Constraint Index Sets Doc ====================================== ============ ================================================================================= -``v_shell_eqn`` time, length Calculates velocity of flow through shell using `area_flow_shell_min` +``v_shell_eqn`` time, length Calculates velocity of flow through shell using ``area_flow_shell_min`` ``N_Re_shell_eqn`` time, length Calculates the shell-side Reynolds number ``conv_heat_transfer_coeff_shell_eqn`` time, length Calculates the shell-side convective heat transfer coefficient ``v_tube_eqn`` time, length Calculates gas velocity in tube ``N_Re_tube_eqn`` time, length Calculates the tube-side Reynolds number ``heat_transfer_coeff_tube_eqn`` time, length Calcualtes the tube-side heat transfer coefficient +``N_Nu_shell_eqn`` time, length Calculate the shell-side Nusselt number +``N_Nu_tube_eqn`` time, length Calculate the tube-side Nusselt number +``heat_tube_eqn`` time, length Calculates heat transfer per unit length +``heat_shell_eqn`` time, length Calculates heat transfer per unit length +``temp_wall_tube_eqn`` time, length Calculate the wall temperature of the inner tube +``temp_wall_shell_eqn`` time, length Calculate the wall temperature of the outer tube +``temp_wall_center_eqn`` time, length Overall energy balance on tube metal ====================================== ============ ================================================================================= ====================================== ============ =================================================================================== Expression Index Sets Doc ====================================== ============ =================================================================================== -``total_heat_transfer_coeff_shell`` time, length Returns ``conv_heat_transfer_coeff_shell``. Could be extended to include radiation. +``total_heat_transfer_coeff_shell`` time Returns ``conv_heat_transfer_coeff_shell``. Could be extended to include radiation. +``total_heat_duty`` time Created only if not ``dynamic``. Gives total heat transferred over entire exchanger +``log_mean_delta_temperature`` time Created only if not ``dynamic``. Gives the log mean temperature difference (LMTD). +``overall_heat_transfer_coefficient`` time Created only if not ``dynamic``. Calculated from total heat transfer, area, and LMTD. ====================================== ============ =================================================================================== Pressure Change Equations @@ -255,21 +262,23 @@ Variable Index Sets Doc ``deltaP_tube_uturn`` time, length Change of pressure in tube due to U turns =========================== ============ ================================================================================= -================================== ============ ================================================================================= +================================== ============ ================================================================================== Constraint Index Sets Doc -================================== ============ ================================================================================= +================================== ============ ================================================================================== ``friction_factor_shell_eqn`` time, length Calculates the shell-side friction factor -``deltaP_shell_eqn`` time, length Sets `deltaP_shell` based on the friction factor and shell properties +``deltaP_shell_eqn`` time, length Sets ``deltaP_shell`` based on the friction factor and shell properties ``friction_factor_tube_eqn`` time, length Calculates the tube-side friction factor -``deltaP_tube_friction_eqn`` time, length Sets `deltaP_tube_friction` based on friction factor -``deltaP_tube_uturn_eqn`` time, length Sets `deltaP_tube_uturn` based on `kloss_uturn` -``deltaP_tube_eqn`` time, length Sets `deltaP_tube` by summing `deltaP_tube_friction` and `deltaP_tube_uturn` -================================== ============ ================================================================================= +``deltaP_tube_friction_eqn`` time, length Sets ``deltaP_tube_friction`` based on friction factor +``deltaP_tube_uturn_eqn`` time, length Sets ``deltaP_tube_uturn`` based on ``kloss_uturn`` +``deltaP_tube_eqn`` time, length Sets ``deltaP_tube`` by summing ``deltaP_tube_friction`` and ``deltaP_tube_uturn`` +================================== ============ ================================================================================== Holdup Equations ---------------- -Created when `has_holdup=True` in the config. + +Created when ``has_holdup=True`` in the config. + =========================== ============ ================================================================================= Variable Index Sets Doc =========================== ============ ================================================================================= @@ -284,7 +293,9 @@ Constraint Index Sets Doc Dynamic Equations ----------------- -Created when `dynamic=True` in the config. + +Created when ``dynamic=True`` in the config. + =========================== ============ ================================================================================= Derivative Variable Index Sets Doc =========================== ============ ================================================================================= @@ -292,27 +303,9 @@ Derivative Variable Index Sets Doc =========================== ============ ================================================================================= -Constraints ------------ - -The pressure flow relation is added to the inherited constraints from the :ref:`PressureChanger model -`. - -If the ``phase`` option is set to ``"Liq"`` the following equation describes the pressure-flow relation. - -.. math:: - - \frac{1}{s_f^2}F^2 = \frac{1}{s_f^2}C_v^2\left(P_{in} - P_{out}\right)f(x)^2 - -If the ``phase`` option is set to ``"Vap"`` the following equation describes the pressure-flow relation. - -.. math:: - - \frac{1}{s_f^2}F^2 = \frac{1}{s_f^2}C_v^2\left(P_{in}^2 - P_{out}^2\right)f(x)^2 - - Initialization -------------- + First, the shell and tube control volumes are initialized without heat transfer. Next the total possible heat transfer between streams is estimated based on heat capacity, flow rate, and inlet/outlet temperatures. The actual temperature change is set to be @@ -321,14 +314,14 @@ temperature profiles. Finally, temperatures besides the inlets are unfixed and the performance equations are activated before a full solve of the system model. -HelmValve Class ----------------- +CrossFlowHeatExchanger1D Class +------------------------------ .. autoclass:: CrossFlowHeatExchanger1D :members: -HelmValveData Class ---------------------- +CrossFlowHeatExchanger1DData Class +---------------------------------- .. autoclass:: CrossFlowHeatExchanger1DData :members: diff --git a/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst new file mode 100644 index 0000000000..914459c743 --- /dev/null +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst @@ -0,0 +1,268 @@ +Heater1D +======== + +.. index:: + pair: idaes.models_extra.power_generation.unit_models.heater_1D;Heater1D + +.. module:: idaes.models_extra.power_generation.unit_models.heater_1D + +This model is for a gas trim heater modeled as gas being blown perpendicularly across banks of hollow tubes, +which are heated by resistive heating. + +Example +------- + +.. code-block:: python + + import pyomo.environ as pyo + + from idaes.core import FlowsheetBlock + import idaes.core.util.scaling as iscale + from idaes.models.properties.modular_properties import GenericParameterBlock + from idaes.models_extra.power_generation.properties.natural_gas_PR import ( + get_prop, + EosType, + ) + from idaes.models_extra.power_generation.unit_models import Heater1D + from idaes.core.util.model_statistics import degrees_of_freedom + + optarg = { + "constr_viol_tol": 1e-8, + "nlp_scaling_method": "user-scaling", + "linear_solver": "ma57", + "OF_ma57_automatic_scaling": "yes", + "max_iter": 350, + "tol": 1e-8, + "halt_on_ampl_error": "no", + } + + m = pyo.ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.h2_side_prop_params = GenericParameterBlock( + **get_prop(["H2", "H2O", "Ar", "N2"], {"Vap"}, eos=EosType.IDEAL), + doc="H2O + H2 gas property parameters", + ) + m.fs.heater = Heater1D( + property_package=m.fs.h2_side_prop_params, + has_holdup=True, + dynamic=False, + has_fluid_holdup=False, + has_pressure_change=pressure_drop, + finite_elements=4, + tube_arrangement="in-line", + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + ) + + heater = m.fs.heater + + heater.inlet.flow_mol.fix(5102.5) + heater.inlet.temperature.fix(938.83) + heater.inlet.pressure.fix(1.2e5) + heater.inlet.mole_frac_comp[0, "H2"].fix(0.57375) + heater.inlet.mole_frac_comp[0, "H2O"].fix(0.42517) + heater.inlet.mole_frac_comp[0, "Ar"].fix(0.00086358) + heater.inlet.mole_frac_comp[0, "N2"].fix(0.00021589) + + heater.di_tube.fix(0.0525018) + heater.thickness_tube.fix(0.0039116) + heater.pitch_x.fix(0.1) + heater.pitch_y.fix(0.1) + heater.length_tube_seg.fix(10) + heater.number_passes.fix(1) + heater.rfouling = 0.0001 + heater.fcorrection_htc_shell.fix(1) + heater.cp_wall = 502.4 + if pressure_drop: + heater.fcorrection_dp_shell.fix(1) + + heater.number_columns_per_pass.fix(40) + heater.number_rows_per_pass.fix(40) + heater.electric_heat_duty.fix(3.6504e06) + + pp = m.fs.h2_side_prop_params + pp.set_default_scaling("enth_mol_phase", 1e-3) + pp.set_default_scaling("pressure", 1e-5) + pp.set_default_scaling("temperature", 1e-2) + pp.set_default_scaling("flow_mol", 1e-3) + + _mf_scale = { + "H2": 1, + "H2O": 1, + "N2": 10, + "Ar": 10, + } + for comp, s in _mf_scale.items(): + pp.set_default_scaling("mole_frac_comp", s, index=comp) + pp.set_default_scaling("mole_frac_phase_comp", s, index=("Vap", comp)) + pp.set_default_scaling("flow_mol_phase_comp", s * 1e-3, index=("Vap", comp)) + + shell = heater.control_volume + iscale.set_scaling_factor(shell.area, 1e-1) + iscale.set_scaling_factor(shell.heat, 1e-6) + iscale.set_scaling_factor(shell.enthalpy_flow_dx, 1e-7) + iscale.set_scaling_factor(heater.heat_holdup, 1e-8) + + iscale.calculate_scaling_factors(m) + + initializer = m.fs.heat_exchanger.default_initializer( + solver="ipopt", + solver_options=optarg + ) + initializer.initialize(m.fs.heat_exchanger) + + + + +Heater Geometry +--------------- +=========================== =========== ================================================================================= +Variable Index Sets Doc +=========================== =========== ================================================================================= +``number_columns_per_pass`` None Number of columns of tube per pass +``number_rows_per_pass`` None Number of rows of tube per pass +``number_passes`` None Number of tube banks of ``nrow_tube * ncol_inlet`` tubes +``pitch_x`` None Distance between columns (TODO rows?) of tubes, measured from center-of-tube to center-of-tube +``pitch_y`` None Distance between rows (TODO columns?) of tubes, measured from center-of-tube to center-of-tube +``length_tube_seg`` None Length of tube segment perpendicular to flow in each pass +``area_flow_shell`` None Reference to flow area on control volume +``length_flow_shell`` None Reference to flow length on control volume +``area_flow_shell_min`` None Minimum flow area on shell side +``di_tube`` None Inner diameter of tubes +``thickness_tube`` None Thickness of tube wall. +=========================== =========== ================================================================================= + +============================ =========== =========================================================================== +Expression Index Sets Doc +============================ =========== =========================================================================== +``nrow_tube`` None Total number of rows of tube +``do_tube`` None Outer diameter of tube (equal to ``di_tube+2*thickness_tube``) +``pitch_x_to_do`` None Ratio of ``pitch_x`` to ``do_tube`` +``pitch_y_to_do`` None Ratio of ``pitch_y`` to ``do_tube`` +``area_wall_seg`` None Total cross-sectional area of tube per pass +``total_heat_transfer_area`` None Total heat transfer area, as measured on outer surface of tubes +============================ =========== =========================================================================== + +=========================== =========== ================================================================================================= +Constraint Index Sets Doc +=========================== =========== ================================================================================================= +``length_flow_shell_eqn`` None Constrains flow length from control volume to equal value implied by geometry +``area_flow_shell_eqn`` None Constrains flow cross-sectional area from control volume to equal value implied by geometry +``area_flow_shell_min_eqn`` None Constraints ``area_flow_shell_min`` to equal value determined by geometry +=========================== =========== ================================================================================================= + +Performance Equations +----------------------- + +================================== ============ ================================================================================= +Variable Index Sets Doc +================================== ============ ================================================================================= +``electric_heat_duty`` time Electric heat duty supplied to entire heater unit +``fcorrection_htc_shell`` time, length Correction factor for convective heat transfer +``conv_heat_transfer_coeff_shell`` time, length Convective heat transfer coefficient +``temp_wall_shell`` time, length Wall temperature of tube +``temp_wall_center`` time, length Temperature at center of tube wall +``v_shell`` time, length Flow velocity through minimum area +``N_Re_shell`` time, length Reynolds number +``N_Nu_shell`` time, length Nusselt number +================================== ============ ================================================================================= + +=========================== =========== ================================================================================= +Parameter Index Sets Doc +=========================== =========== ================================================================================= +``therm_cond_wall`` None Thermal conductivity of tube wall +``density_wall`` None Mass density of tube wall metal +``cp_wall`` None Tube wall heat capacity (mass basis) +``rfouling_shell`` None Fouling resistance on shell side +``f_arrangement`` None Adjustment factor depending on ``tube_arrangement`` in config +=========================== =========== ================================================================================= + +====================================== ============ ================================================================================= +Constraint Index Sets Doc +====================================== ============ ================================================================================= +``v_shell_eqn`` time, length Calculates velocity of flow through shell using ``area_flow_shell_min`` +``N_Re_shell_eqn`` time, length Calculates the Reynolds number +``conv_heat_transfer_coeff_shell_eqn`` time, length Calculates the convective heat transfer coefficient +``N_Nu_shell_eqn`` time, length Calculate the Nusselt number +``heat_shell_eqn`` time, length Calculates heat transfer per unit length +``temp_wall_shell_eqn`` time, length Calculate the wall temperature of the outer tube +``temp_wall_center_eqn`` time, length Overall energy balance on tube metal +====================================== ============ ================================================================================= + +====================================== ============ =================================================================================== +Expression Index Sets Doc +====================================== ============ =================================================================================== +``total_heat_transfer_coeff_shell`` time Returns ``conv_heat_transfer_coeff_shell``. Could be extended to include radiation. +====================================== ============ =================================================================================== + +Pressure Change Equations +------------------------- + +=========================== ============ ================================================================================= +Parameter Index Sets Doc +=========================== ============ ================================================================================= +``fcorrection_dp_shell`` None Correction factor for pressure drop +=========================== ============ ================================================================================= + +=========================== ============ ================================================================================= +Variable Index Sets Doc +=========================== ============ ================================================================================= +``fcorrection_dp_shell`` None Correction factor for pressure drop +``friction_factor_shell`` time, length Friction factor +=========================== ============ ================================================================================= + +================================== ============ ================================================================================= +Constraint Index Sets Doc +================================== ============ ================================================================================= +``friction_factor_shell_eqn`` time, length Calculates the friction factor +``deltaP_shell_eqn`` time, length Sets ``deltaP_shell`` based on the friction factor and physical properties +================================== ============ ================================================================================= + + +Holdup Equations +---------------- + +Created when ``has_holdup=True`` in the config. + +=========================== ============ ================================================================================= +Variable Index Sets Doc +=========================== ============ ================================================================================= +``heat_holdup`` time, length Energy holdup per unit length of flow path +=========================== ============ ================================================================================= + +=========================== ============ ================================================================================= +Constraint Index Sets Doc +=========================== ============ ================================================================================= +``heat_holdup_eqn`` time, length Defines heat holdup in terms of geometry and physical properties +=========================== ============ ================================================================================= + +Dynamic Equations +----------------- + +Created when ``dynamic=True`` in the config. + +=========================== ============ ================================================================================= +Derivative Variable Index Sets Doc +=========================== ============ ================================================================================= +``heat_accumulation`` time, length Energy accumulation in tube wall per unit length of shell flow path per unit time +=========================== ============ ================================================================================= + + +Initialization +-------------- + +A simple initialization method that first initializes the control volume without heat transfer, +then adds heat transfer in and solves it again, then finally solves the entire model. + + +Heater1D Class +-------------- + +.. autoclass:: Heater1D + :members: + +Heater1DData Class +------------------ + +.. autoclass:: Heater1DData + :members: diff --git a/docs/reference_guides/model_libraries/power_generation/unit_models/index.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/index.rst index 1a10eb0952..b226b2817a 100644 --- a/docs/reference_guides/model_libraries/power_generation/unit_models/index.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/index.rst @@ -26,3 +26,5 @@ Unit Models waterpipe boiler_heat_exchanger_3streams feedwater_heater_0D_dynamic + cross_flow_heat_exchanger_1D + heater_1D diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 43af3f8444..8f4a9c3e5b 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -358,6 +358,8 @@ def initialize_main_model( class CrossFlowHeatExchanger1DData(HeatExchanger1DData): """Standard Cross Flow Heat Exchanger Unit Model Class.""" + default_initializer = CrossFlowHeatExchanger1DInitializer + CONFIG = HeatExchanger1DData.CONFIG() CONFIG.declare( "shell_is_hot", diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 68c788d664..61923e2610 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -61,7 +61,6 @@ def initialize_main_model( self, model: Block, copy_inlet_state: bool = False, - duty=1000 * pyunits.W, ): """ Initialization routine for the main Heater 1D model @@ -78,8 +77,6 @@ def initialize_main_model( 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" @@ -151,6 +148,8 @@ def initialize_main_model( class Heater1DData(UnitModelBlockData): """Standard Trim Heater Model Class Class.""" + default_initializer = Heater1DInitializer + CONFIG = UnitModelBlockData.CONFIG() CONFIG.declare( "has_fluid_holdup", diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index d132f99157..edb0b2b357 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -214,10 +214,11 @@ def test_initialization(model_no_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) - initializer = CrossFlowHeatExchanger1DInitializer( + initializer = m.fs.heat_exchanger.default_initializer( solver="ipopt", solver_options=optarg ) + assert isinstance(initializer, CrossFlowHeatExchanger1DInitializer) initializer.initialize(model=m.fs.heat_exchanger) assert degrees_of_freedom(m) == 0 @@ -248,10 +249,11 @@ def test_initialization_dP(model_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) - initializer = CrossFlowHeatExchanger1DInitializer( + initializer = m.fs.heat_exchanger.default_initializer( solver="ipopt", solver_options=optarg ) + assert isinstance(initializer, CrossFlowHeatExchanger1DInitializer) initializer.initialize(m.fs.heat_exchanger) assert degrees_of_freedom(m) == 0 diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py index d24da90214..0052381b4c 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -178,10 +178,11 @@ def test_initialization(model_no_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) - initializer = Heater1DInitializer( + initializer = m.fs.heater.default_initializer( solver="ipopt", solver_options=optarg ) + assert isinstance(initializer, Heater1DInitializer) initializer.initialize(model=m.fs.heater) assert degrees_of_freedom(m) == 0 @@ -209,10 +210,11 @@ def test_initialization_dP(model_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) - initializer = Heater1DInitializer( + initializer = m.fs.heater.default_initializer( solver="ipopt", solver_options=optarg ) + assert isinstance(initializer, Heater1DInitializer) initializer.initialize(model=m.fs.heater) assert degrees_of_freedom(m) == 0 From b2af3c9ecb7358e70c292ab284384b4db2db5872 Mon Sep 17 00:00:00 2001 From: Doug A Date: Thu, 18 Apr 2024 09:03:01 -0400 Subject: [PATCH 27/38] pitch --- .../power_generation/unit_models/heat_exchanger_common.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index ff37f00cbf..67ca7ad9fe 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -59,22 +59,19 @@ def make_geometry_common(blk, shell, shell_units): initialize=0.05, doc="Inner diameter of tube", units=shell_units["length"] ) - # Thickness of tube blk.thickness_tube = Var( initialize=0.005, doc="Tube thickness", units=shell_units["length"] ) - # Pitch of tubes between two neighboring columns (in y direction). Always greater than tube outside diameter blk.pitch_y = Var( initialize=0.1, - doc="Pitch between two neighboring columns", + doc="Pitch between tubes perpendicular to direction of flow", units=shell_units["length"], ) - # Pitch of tubes between two neighboring rows (in x direction). Always greater than tube outside diameter blk.pitch_x = Var( initialize=0.1, - doc="Pitch between two neighboring rows", + doc="Pitch between tubes in line with direction of flow", units=shell_units["length"], ) From ad4dddfdb978ad0884900ab2a993962678661b9f Mon Sep 17 00:00:00 2001 From: Doug A Date: Thu, 18 Apr 2024 11:19:11 -0400 Subject: [PATCH 28/38] polishing step --- .../cross_flow_heat_exchanger_1D.rst | 20 +++++++------- .../unit_models/heater_1D.rst | 14 +++++----- .../cross_flow_heat_exchanger_1D.py | 22 +++++++++++---- .../unit_models/heat_exchanger_common.py | 27 ++++++++++++++----- .../power_generation/unit_models/heater_1D.py | 8 +++++- .../test_cross_flow_heat_exchanger_1D.py | 8 +++--- .../unit_models/tests/test_heater_1D.py | 15 +++++------ 7 files changed, 73 insertions(+), 41 deletions(-) diff --git a/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst index d695698545..6408b09652 100644 --- a/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst @@ -6,8 +6,10 @@ CrossFlowHeatExchanger1D .. module:: idaes.models_extra.power_generation.unit_models.cross_flow_heat_exchanger_1D -This model is for a cross flow heat exchanger between two gases. The gas in the shell has a straight path, -while the gas in the tubes snakes back and forth across the shell's path. +This model is for a cross flow heat exchanger between two gases. The gas in the shell has a straight path, +while the gas in the tubes snakes back and forth across the shell's path. Note that the ``finite_elements`` +option in the shell and tube control volume configs should be set to an integer factor of ``number_passes`` +in order for the discretization equations to make sense as a cross-flow heat exchanger. Example ------- @@ -145,14 +147,14 @@ Example Heat Exchanger Geometry ----------------------- -=========================== =========== ================================================================================= +=========================== =========== =================================================================================================== Variable Index Sets Doc -=========================== =========== ================================================================================= +=========================== =========== =================================================================================================== ``number_columns_per_pass`` None Number of columns of tube per pass ``number_rows_per_pass`` None Number of rows of tube per pass ``number_passes`` None Number of tube banks of ``nrow_tube * ncol_inlet`` tubes -``pitch_x`` None Distance between columns (TODO rows?) of tubes, measured from center-of-tube to center-of-tube -``pitch_y`` None Distance between rows (TODO columns?) of tubes, measured from center-of-tube to center-of-tube +``pitch_x`` None Distance between tubes parallel to shell flow, measured from center-of-tube to center-of-tube +``pitch_y`` None Distance between tubes perpendicular to shell flow, measured from center-of-tube to center-of-tube ``length_tube_seg`` None Length of tube segment perpendicular to flow in each pass ``area_flow_shell`` None Reference to flow area on shell control volume ``length_flow_shell`` None Reference to flow length on shell control volume @@ -161,7 +163,7 @@ Variable Index Sets Doc ``thickness_tube`` None Thickness of tube wall. ``area_flow_tube`` None Reference to flow area on tube control volume ``length_flow_tube`` None Reference to flow length on tube control volume -=========================== =========== ================================================================================= +=========================== =========== =================================================================================================== ============================ =========== =========================================================================== Expression Index Sets Doc @@ -222,7 +224,7 @@ Constraint Index Sets Doc ``conv_heat_transfer_coeff_shell_eqn`` time, length Calculates the shell-side convective heat transfer coefficient ``v_tube_eqn`` time, length Calculates gas velocity in tube ``N_Re_tube_eqn`` time, length Calculates the tube-side Reynolds number -``heat_transfer_coeff_tube_eqn`` time, length Calcualtes the tube-side heat transfer coefficient +``heat_transfer_coeff_tube_eqn`` time, length Calculates the tube-side heat transfer coefficient ``N_Nu_shell_eqn`` time, length Calculate the shell-side Nusselt number ``N_Nu_tube_eqn`` time, length Calculate the tube-side Nusselt number ``heat_tube_eqn`` time, length Calculates heat transfer per unit length @@ -309,7 +311,7 @@ Initialization First, the shell and tube control volumes are initialized without heat transfer. Next the total possible heat transfer between streams is estimated based on heat capacity, flow rate, and inlet/outlet temperatures. The actual temperature change is set to be -half the theoretical maximum, and the shell and tube are initalized with linear +half the theoretical maximum, and the shell and tube are initialized with linear temperature profiles. Finally, temperatures besides the inlets are unfixed and the performance equations are activated before a full solve of the system model. diff --git a/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst index 914459c743..988e6a8a22 100644 --- a/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst @@ -7,7 +7,9 @@ Heater1D .. module:: idaes.models_extra.power_generation.unit_models.heater_1D This model is for a gas trim heater modeled as gas being blown perpendicularly across banks of hollow tubes, -which are heated by resistive heating. +which are heated by resistive heating. Note that the ``finite_elements`` option in the control +volume config should be set to an integer factor of ``number_passes`` in order for the +discretization equations to make sense as a cross-flow heat exchanger. Example ------- @@ -116,21 +118,21 @@ Example Heater Geometry --------------- -=========================== =========== ================================================================================= +=========================== =========== ============================================================================================= Variable Index Sets Doc -=========================== =========== ================================================================================= +=========================== =========== ============================================================================================= ``number_columns_per_pass`` None Number of columns of tube per pass ``number_rows_per_pass`` None Number of rows of tube per pass ``number_passes`` None Number of tube banks of ``nrow_tube * ncol_inlet`` tubes -``pitch_x`` None Distance between columns (TODO rows?) of tubes, measured from center-of-tube to center-of-tube -``pitch_y`` None Distance between rows (TODO columns?) of tubes, measured from center-of-tube to center-of-tube +``pitch_x`` None Distance between tubes parallel to flow, measured from center-of-tube to center-of-tube +``pitch_y`` None Distance between tubes perpendicular to flow, measured from center-of-tube to center-of-tube ``length_tube_seg`` None Length of tube segment perpendicular to flow in each pass ``area_flow_shell`` None Reference to flow area on control volume ``length_flow_shell`` None Reference to flow length on control volume ``area_flow_shell_min`` None Minimum flow area on shell side ``di_tube`` None Inner diameter of tubes ``thickness_tube`` None Thickness of tube wall. -=========================== =========== ================================================================================= +=========================== =========== ============================================================================================= ============================ =========== =========================================================================== Expression Index Sets Doc diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 8f4a9c3e5b..7ee1802c46 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -50,6 +50,7 @@ __author__ = "Jinliang Ma, Douglas Allan" + class CrossFlowHeatExchanger1DInitializer(SingleControlVolumeUnitInitializer): """ Initializer for Cross Flow Heat Exchanger 1D units. @@ -100,7 +101,9 @@ def initialize_main_model( ) hot_units = model.config.hot_side.property_package.get_metadata().derived_units - cold_units = model.config.cold_side.property_package.get_metadata().derived_units + cold_units = ( + model.config.cold_side.property_package.get_metadata().derived_units + ) if model.config.shell_is_hot: shell = model.hot_side @@ -274,7 +277,9 @@ def initialize_main_model( / 2 ) - model.temp_wall_shell[t, z].set_value(model.temp_wall_center[t, z].value) + model.temp_wall_shell[t, z].set_value( + model.temp_wall_center[t, z].value + ) model.temp_wall_tube[t, z].set_value( pyunits.convert_value( model.temp_wall_center[t, z].value, @@ -354,6 +359,7 @@ def initialize_main_model( init_log.info("Initialization Complete.") return res + @declare_process_block_class("CrossFlowHeatExchanger1D") class CrossFlowHeatExchanger1DData(HeatExchanger1DData): """Standard Cross Flow Heat Exchanger Unit Model Class.""" @@ -455,7 +461,9 @@ def _make_geometry(self): add_object_reference(self, "length_flow_shell", shell.length) add_object_reference(self, "length_flow_tube", tube.length) - heat_exchanger_common.make_geometry_common(self, shell=shell, shell_units=shell_units) + heat_exchanger_common.make_geometry_common( + self, shell=shell, shell_units=shell_units + ) heat_exchanger_common.make_geometry_tube(self, shell_units=shell_units) def _make_performance(self): @@ -764,10 +772,14 @@ def cst(con, sf): ssf(self.temp_wall_shell[t, z], sf_T_shell) cst(self.temp_wall_shell_eqn[t, z], sf_T_shell) - sf_conv_heat_transfer_coeff_shell = gsf(self.conv_heat_transfer_coeff_shell[t, z]) + sf_conv_heat_transfer_coeff_shell = gsf( + self.conv_heat_transfer_coeff_shell[t, z] + ) s_Q_shell = sgsf( shell.heat[t, z], - sf_conv_heat_transfer_coeff_shell * sf_area_per_length_shell * sf_T_shell, + sf_conv_heat_transfer_coeff_shell + * sf_area_per_length_shell + * sf_T_shell, ) cst( self.heat_shell_eqn[t, z], s_Q_shell * value(self.length_flow_shell) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 67ca7ad9fe..dc3665c636 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -137,7 +137,10 @@ def length_flow_shell_eqn(b): def area_flow_shell_eqn(b): return ( b.length_flow_shell * b.area_flow_shell - == b.length_tube_seg * b.length_flow_shell * b.pitch_y * b.number_columns_per_pass + == b.length_tube_seg + * b.length_flow_shell + * b.pitch_y + * b.number_columns_per_pass - b.number_columns_per_pass * b.nrow_tube * 0.25 @@ -169,7 +172,11 @@ def length_flow_tube_eqn(b): def area_flow_tube_eqn(b): return ( b.area_flow_tube - == 0.25 * const.pi * b.di_tube**2.0 * b.number_columns_per_pass * b.number_rows_per_pass + == 0.25 + * const.pi + * b.di_tube**2.0 + * b.number_columns_per_pass + * b.number_rows_per_pass ) @@ -702,9 +709,13 @@ def cst(con, sf): cst(blk.N_Nu_shell_eqn[t, z], sf_N_Nu_shell) sf_conv_heat_transfer_coeff_shell = sgsf( - blk.conv_heat_transfer_coeff_shell[t, z], sf_N_Nu_shell * sf_k_shell / sf_do_tube + blk.conv_heat_transfer_coeff_shell[t, z], + sf_N_Nu_shell * sf_k_shell / sf_do_tube, + ) + cst( + blk.conv_heat_transfer_coeff_shell_eqn[t, z], + sf_conv_heat_transfer_coeff_shell * sf_do_tube, ) - cst(blk.conv_heat_transfer_coeff_shell_eqn[t, z], sf_conv_heat_transfer_coeff_shell * sf_do_tube) # FIXME estimate from parameters if blk.config.has_holdup: @@ -751,6 +762,10 @@ def cst(con, sf): cst(blk.N_Nu_tube_eqn[t, z], sf_N_Nu_tube) sf_heat_transfer_coeff_tube = sgsf( - blk.heat_transfer_coeff_tube[t, z], sf_N_Nu_tube * sf_k_tube / sf_di_tube + blk.heat_transfer_coeff_tube[t, z], + sf_N_Nu_tube * sf_k_tube / sf_di_tube, + ) + cst( + blk.heat_transfer_coeff_tube_eqn[t, z], + sf_heat_transfer_coeff_tube * sf_di_tube, ) - cst(blk.heat_transfer_coeff_tube_eqn[t, z], sf_heat_transfer_coeff_tube * sf_di_tube) diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 61923e2610..04f2ad4b43 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -53,10 +53,12 @@ __author__ = "Jinliang Ma, Douglas Allan" + class Heater1DInitializer(SingleControlVolumeUnitInitializer): """ Initializer for Heater 1D units. """ + def initialize_main_model( self, model: Block, @@ -144,6 +146,7 @@ def initialize_main_model( return res + @declare_process_block_class("Heater1D") class Heater1DData(UnitModelBlockData): """Standard Trim Heater Model Class Class.""" @@ -510,7 +513,10 @@ def cst(con, sf): for t in self.flowsheet().time: for z in self.control_volume.length_domain: sf_hconv_conv = gsf(self.conv_heat_transfer_coeff_shell[t, z]) - cst(self.conv_heat_transfer_coeff_shell_eqn[t, z], sf_hconv_conv * sf_d_tube) + cst( + self.conv_heat_transfer_coeff_shell_eqn[t, z], + sf_hconv_conv * sf_d_tube, + ) sf_T = gsf(self.control_volume.properties[t, z].temperature) ssf(self.temp_wall_shell[t, z], sf_T) diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index edb0b2b357..b9b89139b2 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -25,7 +25,7 @@ ) from idaes.models_extra.power_generation.unit_models import ( CrossFlowHeatExchanger1D, - CrossFlowHeatExchanger1DInitializer + CrossFlowHeatExchanger1DInitializer, ) import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom @@ -215,8 +215,7 @@ def test_initialization(model_no_dP): _check_model_statistics(m, deltaP=False) initializer = m.fs.heat_exchanger.default_initializer( - solver="ipopt", - solver_options=optarg + solver="ipopt", solver_options=optarg ) assert isinstance(initializer, CrossFlowHeatExchanger1DInitializer) initializer.initialize(model=m.fs.heat_exchanger) @@ -250,8 +249,7 @@ def test_initialization_dP(model_dP): _check_model_statistics(m, deltaP=True) initializer = m.fs.heat_exchanger.default_initializer( - solver="ipopt", - solver_options=optarg + solver="ipopt", solver_options=optarg ) assert isinstance(initializer, CrossFlowHeatExchanger1DInitializer) initializer.initialize(m.fs.heat_exchanger) diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py index 0052381b4c..d0b70849ae 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -22,7 +22,10 @@ get_prop, EosType, ) -from idaes.models_extra.power_generation.unit_models import Heater1D, Heater1DInitializer +from idaes.models_extra.power_generation.unit_models import ( + Heater1D, + Heater1DInitializer, +) import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.solvers import get_solver @@ -178,10 +181,7 @@ def test_initialization(model_no_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=False) - initializer = m.fs.heater.default_initializer( - solver="ipopt", - solver_options=optarg - ) + initializer = m.fs.heater.default_initializer(solver="ipopt", solver_options=optarg) assert isinstance(initializer, Heater1DInitializer) initializer.initialize(model=m.fs.heater) @@ -210,10 +210,7 @@ def test_initialization_dP(model_dP): assert degrees_of_freedom(m) == 0 _check_model_statistics(m, deltaP=True) - initializer = m.fs.heater.default_initializer( - solver="ipopt", - solver_options=optarg - ) + initializer = m.fs.heater.default_initializer(solver="ipopt", solver_options=optarg) assert isinstance(initializer, Heater1DInitializer) initializer.initialize(model=m.fs.heater) From 74c5e737c0e9d010c6031cc57de4f9df057e0727 Mon Sep 17 00:00:00 2001 From: Doug A Date: Thu, 18 Apr 2024 11:37:20 -0400 Subject: [PATCH 29/38] run black --- idaes/models_extra/power_generation/unit_models/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/idaes/models_extra/power_generation/unit_models/__init__.py b/idaes/models_extra/power_generation/unit_models/__init__.py index d1fadcbf62..92cdde53a4 100644 --- a/idaes/models_extra/power_generation/unit_models/__init__.py +++ b/idaes/models_extra/power_generation/unit_models/__init__.py @@ -15,7 +15,10 @@ from .boiler_fireside import BoilerFireside from .boiler_heat_exchanger import BoilerHeatExchanger from .boiler_heat_exchanger_2D import HeatExchangerCrossFlow2D_Header -from .cross_flow_heat_exchanger_1D import CrossFlowHeatExchanger1D, CrossFlowHeatExchanger1DInitializer +from .cross_flow_heat_exchanger_1D import ( + CrossFlowHeatExchanger1D, + CrossFlowHeatExchanger1DInitializer, +) from .downcomer import Downcomer from .drum import Drum from .drum1D import Drum1D From dd3afc7a009539ec877c55c4f34e23dfb11295e0 Mon Sep 17 00:00:00 2001 From: Doug A Date: Thu, 18 Apr 2024 12:03:21 -0400 Subject: [PATCH 30/38] new black version --- .../power_generation/unit_models/heat_exchanger_common.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index dc3665c636..99d923fbac 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -756,9 +756,7 @@ def cst(con, sf): if make_nusselt: sf_k_tube = gsf(tube.properties[t, z].therm_cond_phase["Vap"]) - sf_N_Nu_tube = sgsf( - blk.N_Nu_tube[t, z], 1 / 0.023 * sf_N_Re_tube**0.8 - ) + sf_N_Nu_tube = sgsf(blk.N_Nu_tube[t, z], 1 / 0.023 * sf_N_Re_tube**0.8) cst(blk.N_Nu_tube_eqn[t, z], sf_N_Nu_tube) sf_heat_transfer_coeff_tube = sgsf( From ac648693cb196604df7bb81d7639587e99cb7699 Mon Sep 17 00:00:00 2001 From: Doug A Date: Thu, 18 Apr 2024 17:32:01 -0400 Subject: [PATCH 31/38] culling common --- .../cross_flow_heat_exchanger_1D.py | 220 +++++++++++++++++- .../unit_models/heat_exchanger_common.py | 57 +++-- .../power_generation/unit_models/heater_1D.py | 2 +- 3 files changed, 244 insertions(+), 35 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 7ee1802c46..160b422bf1 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -21,7 +21,9 @@ from pyomo.environ import ( Block, value, + Var, log, + Param, Reference, units as pyunits, ) @@ -462,9 +464,26 @@ def _make_geometry(self): add_object_reference(self, "length_flow_tube", tube.length) heat_exchanger_common.make_geometry_common( - self, shell=shell, shell_units=shell_units + self, shell_units=shell_units ) - heat_exchanger_common.make_geometry_tube(self, shell_units=shell_units) + # Important that these values about tube geometry are in shell units! + @self.Constraint(doc="Length of tube side flow") + def length_flow_tube_eqn(b): + return ( + pyunits.convert(b.length_flow_tube, to_units=shell_units["length"]) + == b.number_passes * b.length_tube_seg + ) + + @self.Constraint(doc="Total area of tube flow") + def area_flow_tube_eqn(b): + return ( + b.area_flow_tube + == 0.25 + * const.pi + * b.di_tube**2.0 + * b.number_columns_per_pass + * b.number_rows_per_pass + ) def _make_performance(self): """ @@ -549,15 +568,198 @@ def _make_performance(self): make_reynolds=True, make_nusselt=True, ) - heat_exchanger_common.make_performance_tube( - self, - tube=tube, - tube_units=tube_units, - tube_has_pressure_change=tube_has_pressure_change, - make_reynolds=True, - make_nusselt=True, + + self.heat_transfer_coeff_tube = Var( + self.flowsheet().config.time, + tube.length_domain, + initialize=100.0, + units=tube_units["heat_transfer_coefficient"], + doc="tube side convective heat transfer coefficient", + ) + + # Heat transfer resistance due to the fouling on tube side + self.rfouling_tube = Param( + initialize=0.0, + mutable=True, + units=1 / tube_units["heat_transfer_coefficient"], + doc="fouling resistance on tube side", + ) + # Correction factor for convective heat transfer coefficient on tube side + self.fcorrection_htc_tube = Var( + initialize=1.0, doc="correction factor for convective HTC on tube side" + ) + # Correction factor for tube side pressure drop due to friction + if tube_has_pressure_change: + # Loss coefficient for a 180 degree bend (u-turn), usually related to radius to inside diameter ratio + self.kloss_uturn = Param( + initialize=0.5, mutable=True, doc="loss coefficient of a tube u-turn" + ) + self.fcorrection_dp_tube = Var( + initialize=1.0, doc="correction factor for tube side pressure drop" + ) + + # Boundary wall temperature on tube side + self.temp_wall_tube = Var( + self.flowsheet().config.time, + tube.length_domain, + initialize=500, + units=tube_units[ + "temperature" + ], # Want to be in shell units for consistency in equations + doc="boundary wall temperature on tube side", + ) + # Tube side heat transfer coefficient and pressure drop + # ----------------------------------------------------- + # Velocity on tube side + self.v_tube = Var( + self.flowsheet().config.time, + tube.length_domain, + initialize=1.0, + units=tube_units["velocity"], + doc="velocity on tube side", + ) + + # Reynalds number on tube side + self.N_Re_tube = Var( + self.flowsheet().config.time, + tube.length_domain, + initialize=10000.0, + units=pyunits.dimensionless, + doc="Reynolds number on tube side", + bounds=(1e-7, None), + ) + if tube_has_pressure_change: + # Friction factor on tube side + self.friction_factor_tube = Var( + self.flowsheet().config.time, + tube.length_domain, + initialize=1.0, + doc="friction factor on tube side", + ) + + # Pressure drop due to friction on tube side + self.deltaP_tube_friction = Var( + self.flowsheet().config.time, + tube.length_domain, + initialize=-10.0, + units=tube_units["pressure"] / tube_units["length"], + doc="pressure drop due to friction on tube side", + ) + + # Pressure drop due to 180 degree turn on tube side + self.deltaP_tube_uturn = Var( + self.flowsheet().config.time, + tube.length_domain, + initialize=-10.0, + units=tube_units["pressure"] / tube_units["length"], + doc="pressure drop due to u-turn on tube side", + ) + # Nusselt number on tube side + self.N_Nu_tube = Var( + self.flowsheet().config.time, + tube.length_domain, + initialize=1, + units=pyunits.dimensionless, + doc="Nusselts number on tube side", + bounds=(1e-7, None), + ) + + # Velocity equation + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="tube side velocity equation", ) + def v_tube_eqn(b, t, x): + return ( + b.v_tube[t, x] + * pyunits.convert(b.area_flow_tube, to_units=tube_units["area"]) + * tube.properties[t, x].dens_mol_phase["Vap"] + == tube.properties[t, x].flow_mol + ) + + # Reynolds number + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="Reynolds number equation on tube side", + ) + def N_Re_tube_eqn(b, t, x): + return ( + b.N_Re_tube[t, x] * tube.properties[t, x].visc_d_phase["Vap"] + == pyunits.convert(b.di_tube, to_units=tube_units["length"]) + * b.v_tube[t, x] + * tube.properties[t, x].dens_mol_phase["Vap"] + * tube.properties[t, x].mw + ) + if tube_has_pressure_change: + # Friction factor + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="Darcy friction factor on tube side", + ) + def friction_factor_tube_eqn(b, t, x): + return ( + b.friction_factor_tube[t, x] * b.N_Re_tube[t, x] ** 0.25 + == 0.3164 * b.fcorrection_dp_tube + ) + + # Pressure drop due to friction per tube length + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="pressure drop due to friction per tube length", + ) + def deltaP_tube_friction_eqn(b, t, x): + return ( + b.deltaP_tube_friction[t, x] + * pyunits.convert(b.di_tube, to_units=tube_units["length"]) + == -0.5 + * tube.properties[t, x].dens_mass_phase["Vap"] + * b.v_tube[t, x] ** 2 + * b.friction_factor_tube[t, x] + ) + + # Pressure drop due to u-turn + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="pressure drop due to u-turn on tube side", + ) + def deltaP_tube_uturn_eqn(b, t, x): + return ( + b.deltaP_tube_uturn[t, x] + * pyunits.convert(b.length_tube_seg, to_units=tube_units["length"]) + == -0.5 + * tube.properties[t, x].dens_mass_phase["Vap"] + * b.v_tube[t, x] ** 2 + * b.kloss_uturn + ) + + # Total pressure drop on tube side + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="total pressure drop on tube side", + ) + def deltaP_tube_eqn(b, t, x): + return b.deltaP_tube[t, x] == ( + b.deltaP_tube_friction[t, x] + b.deltaP_tube_uturn[t, x] + ) + @self.Constraint( + self.flowsheet().config.time, + tube.length_domain, + doc="convective heat transfer coefficient equation on tube side", + ) + def heat_transfer_coeff_tube_eqn(b, t, x): + return ( + b.heat_transfer_coeff_tube[t, x] * b.di_tube + == b.N_Nu_tube[t, x] + * tube.properties[t, x].therm_cond_phase["Vap"] + * b.fcorrection_htc_tube + ) # Nusselts number @self.Constraint( self.flowsheet().config.time, diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 99d923fbac..fe97423f80 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -36,7 +36,17 @@ __author__ = "Jinliang Ma, Douglas Allan" -def make_geometry_common(blk, shell, shell_units): +def make_geometry_common(blk, shell_units): + """Function to create variables, constraints, and expressions regarding + unit geometry shared between the CrossFlowHeatExchanger1D and Heater1D models. + + Args: + blk : unit model on which components are being generated + shell_units : derived units for property package of shell control volume + + Returns: + None + """ # Number of tube columns in the cross section plane perpendicular to shell side fluid flow (y direction) blk.number_columns_per_pass = Var( initialize=10.0, doc="Number of tube columns", units=pyunits.dimensionless @@ -157,32 +167,29 @@ def area_flow_shell_min_eqn(b): == b.length_tube_seg * (b.pitch_y - b.do_tube) * b.number_columns_per_pass ) - -def make_geometry_tube(blk, shell_units): - # Length of tube side flow - @blk.Constraint(doc="Length of tube side flow") - def length_flow_tube_eqn(b): - return ( - pyunits.convert(b.length_flow_tube, to_units=shell_units["length"]) - == b.number_passes * b.length_tube_seg - ) - - # Total flow area on tube side - @blk.Constraint(doc="Total area of tube flow") - def area_flow_tube_eqn(b): - return ( - b.area_flow_tube - == 0.25 - * const.pi - * b.di_tube**2.0 - * b.number_columns_per_pass - * b.number_rows_per_pass - ) - - def make_performance_common( - blk, shell, shell_units, shell_has_pressure_change, make_reynolds, make_nusselt + blk, + shell, + shell_units, + shell_has_pressure_change: bool, + make_reynolds: bool, + make_nusselt: bool, ): + """Function to create variables, constraints, and expressions regarding + performance constraints shared between the CrossFlowHeatExchanger1D and + Heater1D models. + + Args: + blk : unit model on which components are being generated + shell: shell control volume + shell_units : derived units for property package of shell control volume + shell_has_pressure_change: bool about whether to make pressure change components + make_reynolds: bool about whether to create the Reynolds numebr + make_nusselt: bool about whether to create Nusselt number + + Returns: + None + """ # We need the Reynolds number for pressure change, even if we don't need it for heat transfer if shell_has_pressure_change: make_reynolds = True diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 04f2ad4b43..af6c40bad1 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -387,7 +387,7 @@ def _make_geometry(self): # Add reference to control volume geometry add_object_reference(self, "area_flow_shell", self.control_volume.area) add_object_reference(self, "length_flow_shell", self.control_volume.length) - make_geometry_common(self, shell=self.control_volume, shell_units=units) + make_geometry_common(self, shell_units=units) @self.Expression( doc="Common performance equations expect this expression to be here" From bf82efd8819f192a6b3059fac8603b0c06f9db9f Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 19 Apr 2024 09:29:26 -0400 Subject: [PATCH 32/38] Docstrings and merges --- .../cross_flow_heat_exchanger_1D.py | 38 ++- .../unit_models/heat_exchanger_common.py | 265 +----------------- 2 files changed, 47 insertions(+), 256 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 160b422bf1..d8351fc893 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -463,9 +463,8 @@ def _make_geometry(self): add_object_reference(self, "length_flow_shell", shell.length) add_object_reference(self, "length_flow_tube", tube.length) - heat_exchanger_common.make_geometry_common( - self, shell_units=shell_units - ) + heat_exchanger_common.make_geometry_common(self, shell_units=shell_units) + # Important that these values about tube geometry are in shell units! @self.Constraint(doc="Length of tube side flow") def length_flow_tube_eqn(b): @@ -748,6 +747,7 @@ def deltaP_tube_eqn(b, t, x): return b.deltaP_tube[t, x] == ( b.deltaP_tube_friction[t, x] + b.deltaP_tube_uturn[t, x] ) + @self.Constraint( self.flowsheet().config.time, tube.length_domain, @@ -760,6 +760,7 @@ def heat_transfer_coeff_tube_eqn(b, t, x): * tube.properties[t, x].therm_cond_phase["Vap"] * b.fcorrection_htc_tube ) + # Nusselts number @self.Constraint( self.flowsheet().config.time, @@ -946,8 +947,9 @@ def cst(con, sf): make_reynolds=True, make_nusselt=True, ) - heat_exchanger_common.scale_tube( - self, tube, tube_has_pressure_change, make_reynolds=True, make_nusselt=True + + sf_di_tube = iscale.get_scaling_factor( + self.do_tube, default=1 / value(self.di_tube) ) sf_area_per_length_shell = value( @@ -963,6 +965,32 @@ def cst(con, sf): for t in self.flowsheet().time: for z in shell.length_domain: + # FIXME get better scaling later + ssf(self.v_tube[t, z], 1 / 20) + sf_flow_mol_tube = gsf(tube.properties[t, z].flow_mol) + + cst(self.v_tube_eqn[t, z], sf_flow_mol_tube) + + # FIXME should get scaling of N_Re from defining eqn + sf_N_Re_tube = sgsf(self.N_Re_tube[t, z], 1e-4) + + sf_visc_d_tube = gsf(tube.properties[t, z].visc_d_phase["Vap"]) + cst(self.N_Re_tube_eqn[t, z], sf_N_Re_tube * sf_visc_d_tube) + + sf_k_tube = gsf(tube.properties[t, z].therm_cond_phase["Vap"]) + + sf_N_Nu_tube = sgsf(self.N_Nu_tube[t, z], 1 / 0.023 * sf_N_Re_tube**0.8) + cst(self.N_Nu_tube_eqn[t, z], sf_N_Nu_tube) + + sf_heat_transfer_coeff_tube = sgsf( + self.heat_transfer_coeff_tube[t, z], + sf_N_Nu_tube * sf_k_tube / sf_di_tube, + ) + cst( + self.heat_transfer_coeff_tube_eqn[t, z], + sf_heat_transfer_coeff_tube * sf_di_tube, + ) + sf_T_tube = gsf(tube.properties[t, z].temperature) ssf(self.temp_wall_tube[t, z], sf_T_tube) cst(self.temp_wall_tube_eqn[t, z], sf_T_tube) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index fe97423f80..5ebecf48db 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -167,6 +167,7 @@ def area_flow_shell_min_eqn(b): == b.length_tube_seg * (b.pitch_y - b.do_tube) * b.number_columns_per_pass ) + def make_performance_common( blk, shell, @@ -470,214 +471,22 @@ def total_heat_transfer_coeff_shell(b, t, x): return b.conv_heat_transfer_coeff_shell[t, x] -def make_performance_tube( - blk, tube, tube_units, tube_has_pressure_change, make_reynolds, make_nusselt -): - - # Need Reynolds number for pressure drop, even if we don't need it for heat transfer - if tube_has_pressure_change: - make_reynolds = True - - blk.heat_transfer_coeff_tube = Var( - blk.flowsheet().config.time, - tube.length_domain, - initialize=100.0, - units=tube_units["heat_transfer_coefficient"], - doc="tube side convective heat transfer coefficient", - ) - - # Heat transfer resistance due to the fouling on tube side - blk.rfouling_tube = Param( - initialize=0.0, - mutable=True, - units=1 / tube_units["heat_transfer_coefficient"], - doc="fouling resistance on tube side", - ) - # Correction factor for convective heat transfer coefficient on tube side - blk.fcorrection_htc_tube = Var( - initialize=1.0, doc="correction factor for convective HTC on tube side" - ) - # Correction factor for tube side pressure drop due to friction - if tube_has_pressure_change: - # Loss coefficient for a 180 degree bend (u-turn), usually related to radius to inside diameter ratio - blk.kloss_uturn = Param( - initialize=0.5, mutable=True, doc="loss coefficient of a tube u-turn" - ) - blk.fcorrection_dp_tube = Var( - initialize=1.0, doc="correction factor for tube side pressure drop" - ) - - # Boundary wall temperature on tube side - blk.temp_wall_tube = Var( - blk.flowsheet().config.time, - tube.length_domain, - initialize=500, - units=tube_units[ - "temperature" - ], # Want to be in shell units for consistency in equations - doc="boundary wall temperature on tube side", - ) - if make_reynolds: - # Tube side heat transfer coefficient and pressure drop - # ----------------------------------------------------- - # Velocity on tube side - blk.v_tube = Var( - blk.flowsheet().config.time, - tube.length_domain, - initialize=1.0, - units=tube_units["velocity"], - doc="velocity on tube side", - ) - - # Reynalds number on tube side - blk.N_Re_tube = Var( - blk.flowsheet().config.time, - tube.length_domain, - initialize=10000.0, - units=pyunits.dimensionless, - doc="Reynolds number on tube side", - bounds=(1e-7, None), - ) - if tube_has_pressure_change: - # Friction factor on tube side - blk.friction_factor_tube = Var( - blk.flowsheet().config.time, - tube.length_domain, - initialize=1.0, - doc="friction factor on tube side", - ) - - # Pressure drop due to friction on tube side - blk.deltaP_tube_friction = Var( - blk.flowsheet().config.time, - tube.length_domain, - initialize=-10.0, - units=tube_units["pressure"] / tube_units["length"], - doc="pressure drop due to friction on tube side", - ) - - # Pressure drop due to 180 degree turn on tube side - blk.deltaP_tube_uturn = Var( - blk.flowsheet().config.time, - tube.length_domain, - initialize=-10.0, - units=tube_units["pressure"] / tube_units["length"], - doc="pressure drop due to u-turn on tube side", - ) - if make_nusselt: - # Nusselt number on tube side - blk.N_Nu_tube = Var( - blk.flowsheet().config.time, - tube.length_domain, - initialize=1, - units=pyunits.dimensionless, - doc="Nusselts number on tube side", - bounds=(1e-7, None), - ) - - if make_reynolds: - # Velocity equation - @blk.Constraint( - blk.flowsheet().config.time, - tube.length_domain, - doc="tube side velocity equation", - ) - def v_tube_eqn(b, t, x): - return ( - b.v_tube[t, x] - * pyunits.convert(b.area_flow_tube, to_units=tube_units["area"]) - * tube.properties[t, x].dens_mol_phase["Vap"] - == tube.properties[t, x].flow_mol - ) - - # Reynolds number - @blk.Constraint( - blk.flowsheet().config.time, - tube.length_domain, - doc="Reynolds number equation on tube side", - ) - def N_Re_tube_eqn(b, t, x): - return ( - b.N_Re_tube[t, x] * tube.properties[t, x].visc_d_phase["Vap"] - == pyunits.convert(b.di_tube, to_units=tube_units["length"]) - * b.v_tube[t, x] - * tube.properties[t, x].dens_mol_phase["Vap"] - * tube.properties[t, x].mw - ) - - if tube_has_pressure_change: - # Friction factor - @blk.Constraint( - blk.flowsheet().config.time, - tube.length_domain, - doc="Darcy friction factor on tube side", - ) - def friction_factor_tube_eqn(b, t, x): - return ( - b.friction_factor_tube[t, x] * b.N_Re_tube[t, x] ** 0.25 - == 0.3164 * b.fcorrection_dp_tube - ) - - # Pressure drop due to friction per tube length - @blk.Constraint( - blk.flowsheet().config.time, - tube.length_domain, - doc="pressure drop due to friction per tube length", - ) - def deltaP_tube_friction_eqn(b, t, x): - return ( - b.deltaP_tube_friction[t, x] - * pyunits.convert(b.di_tube, to_units=tube_units["length"]) - == -0.5 - * tube.properties[t, x].dens_mass_phase["Vap"] - * b.v_tube[t, x] ** 2 - * b.friction_factor_tube[t, x] - ) - - # Pressure drop due to u-turn - @blk.Constraint( - blk.flowsheet().config.time, - tube.length_domain, - doc="pressure drop due to u-turn on tube side", - ) - def deltaP_tube_uturn_eqn(b, t, x): - return ( - b.deltaP_tube_uturn[t, x] - * pyunits.convert(b.length_tube_seg, to_units=tube_units["length"]) - == -0.5 - * tube.properties[t, x].dens_mass_phase["Vap"] - * b.v_tube[t, x] ** 2 - * b.kloss_uturn - ) - - # Total pressure drop on tube side - @blk.Constraint( - blk.flowsheet().config.time, - tube.length_domain, - doc="total pressure drop on tube side", - ) - def deltaP_tube_eqn(b, t, x): - return b.deltaP_tube[t, x] == ( - b.deltaP_tube_friction[t, x] + b.deltaP_tube_uturn[t, x] - ) - - if make_nusselt: +def scale_common(blk, shell, shell_has_pressure_change, make_reynolds, make_nusselt): + """Function to scale variables and constraints shared between + the CrossFlowHeatExchanger1D and Heater1D models. - @blk.Constraint( - blk.flowsheet().config.time, - tube.length_domain, - doc="convective heat transfer coefficient equation on tube side", - ) - def heat_transfer_coeff_tube_eqn(b, t, x): - return ( - b.heat_transfer_coeff_tube[t, x] * b.di_tube - == b.N_Nu_tube[t, x] - * tube.properties[t, x].therm_cond_phase["Vap"] - * b.fcorrection_htc_tube - ) + Args: + blk : unit model on which components are being generated + shell: shell control volume + shell_units : derived units for property package of shell control volume + shell_has_pressure_change: bool about whether to make pressure change components + make_reynolds: bool about whether to create the Reynolds numebr + make_nusselt: bool about whether to create Nusselt number + Returns: + None + """ -def scale_common(blk, shell, shell_has_pressure_change, make_reynolds, make_nusselt): def gsf(obj): return iscale.get_scaling_factor(obj, default=1, warning=True) @@ -728,49 +537,3 @@ def cst(con, sf): if blk.config.has_holdup: s_U_holdup = gsf(blk.heat_holdup[t, z]) cst(blk.heat_holdup_eqn[t, z], s_U_holdup) - - -def scale_tube( - blk, tube, tube_has_pressure_change, make_reynolds, make_nusselt -): # pylint: disable=W0613 - def gsf(obj): - return iscale.get_scaling_factor(obj, default=1, warning=True) - - def ssf(obj, sf): - iscale.set_scaling_factor(obj, sf, overwrite=False) - - def cst(con, sf): - iscale.constraint_scaling_transform(con, sf, overwrite=False) - - sgsf = iscale.set_and_get_scaling_factor - - sf_di_tube = iscale.get_scaling_factor(blk.do_tube, default=1 / value(blk.di_tube)) - - for t in blk.flowsheet().time: - for z in tube.length_domain: - if make_reynolds: - # FIXME get better scaling later - ssf(blk.v_tube[t, z], 1 / 20) - sf_flow_mol_tube = gsf(tube.properties[t, z].flow_mol) - - cst(blk.v_tube_eqn[t, z], sf_flow_mol_tube) - - # FIXME should get scaling of N_Re from defining eqn - sf_N_Re_tube = sgsf(blk.N_Re_tube[t, z], 1e-4) - - sf_visc_d_tube = gsf(tube.properties[t, z].visc_d_phase["Vap"]) - cst(blk.N_Re_tube_eqn[t, z], sf_N_Re_tube * sf_visc_d_tube) - if make_nusselt: - sf_k_tube = gsf(tube.properties[t, z].therm_cond_phase["Vap"]) - - sf_N_Nu_tube = sgsf(blk.N_Nu_tube[t, z], 1 / 0.023 * sf_N_Re_tube**0.8) - cst(blk.N_Nu_tube_eqn[t, z], sf_N_Nu_tube) - - sf_heat_transfer_coeff_tube = sgsf( - blk.heat_transfer_coeff_tube[t, z], - sf_N_Nu_tube * sf_k_tube / sf_di_tube, - ) - cst( - blk.heat_transfer_coeff_tube_eqn[t, z], - sf_heat_transfer_coeff_tube * sf_di_tube, - ) From 3c34674c91cc6d15cc23f91bbce2cb0f499e4dfb Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 19 Apr 2024 09:31:21 -0400 Subject: [PATCH 33/38] spelling --- .../power_generation/unit_models/heat_exchanger_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py index 5ebecf48db..fd2932853b 100644 --- a/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py +++ b/idaes/models_extra/power_generation/unit_models/heat_exchanger_common.py @@ -185,7 +185,7 @@ def make_performance_common( shell: shell control volume shell_units : derived units for property package of shell control volume shell_has_pressure_change: bool about whether to make pressure change components - make_reynolds: bool about whether to create the Reynolds numebr + make_reynolds: bool about whether to create the Reynolds number make_nusselt: bool about whether to create Nusselt number Returns: @@ -480,7 +480,7 @@ def scale_common(blk, shell, shell_has_pressure_change, make_reynolds, make_nuss shell: shell control volume shell_units : derived units for property package of shell control volume shell_has_pressure_change: bool about whether to make pressure change components - make_reynolds: bool about whether to create the Reynolds numebr + make_reynolds: bool about whether to create the Reynolds number make_nusselt: bool about whether to create Nusselt number Returns: From 864695ff9f5c35a576dac8b3b25b3b71e7571e43 Mon Sep 17 00:00:00 2001 From: Doug A Date: Fri, 19 Apr 2024 09:41:44 -0400 Subject: [PATCH 34/38] pylint --- .../unit_models/cross_flow_heat_exchanger_1D.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index d8351fc893..d1984c6426 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -45,8 +45,6 @@ ) from idaes.models.unit_models.heat_exchanger_1D import HeatExchanger1DData from idaes.models_extra.power_generation.unit_models import heat_exchanger_common -from idaes.core.util.exceptions import InitializationError -import idaes.logger as idaeslog from idaes.core.initialization import SingleControlVolumeUnitInitializer @@ -937,7 +935,6 @@ def cst(con, sf): self.config.cold_side.property_package.get_metadata().derived_units ) - tube_has_pressure_change = hasattr(self, "deltaP_tube") shell_has_pressure_change = hasattr(self, "deltaP_shell") heat_exchanger_common.scale_common( From 4ffcc528dce92d5dbda86ac32bde5d41a87e3da8 Mon Sep 17 00:00:00 2001 From: Doug A Date: Mon, 22 Apr 2024 10:34:20 -0400 Subject: [PATCH 35/38] more Andrew suggestions --- .../cross_flow_heat_exchanger_1D.rst | 18 +++----- .../unit_models/heater_1D.rst | 6 +++ .../cross_flow_heat_exchanger_1D.py | 29 ++++++++++--- .../power_generation/unit_models/heater_1D.py | 21 ++++++---- .../test_cross_flow_heat_exchanger_1D.py | 42 +++++++++++++++---- .../unit_models/tests/test_heater_1D.py | 37 ++++++++++++---- 6 files changed, 113 insertions(+), 40 deletions(-) diff --git a/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst index 6408b09652..ed995a84c8 100644 --- a/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/cross_flow_heat_exchanger_1D.rst @@ -304,18 +304,6 @@ Derivative Variable Index Sets Doc ``heat_accumulation`` time, length Energy accumulation in tube wall per unit length of shell flow path per unit time =========================== ============ ================================================================================= - -Initialization --------------- - -First, the shell and tube control volumes are initialized without heat transfer. Next -the total possible heat transfer between streams is estimated based on heat capacity, -flow rate, and inlet/outlet temperatures. The actual temperature change is set to be -half the theoretical maximum, and the shell and tube are initialized with linear -temperature profiles. Finally, temperatures besides the inlets are unfixed and -the performance equations are activated before a full solve of the system model. - - CrossFlowHeatExchanger1D Class ------------------------------ @@ -327,3 +315,9 @@ CrossFlowHeatExchanger1DData Class .. autoclass:: CrossFlowHeatExchanger1DData :members: + +CrossFlowHeatExchanger1DInitializer Class +----------------------------------------- + +.. autoclass:: CrossFlowHeatExchanger1DInitializer + :members: \ No newline at end of file diff --git a/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst b/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst index 988e6a8a22..89b757ffc7 100644 --- a/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst +++ b/docs/reference_guides/model_libraries/power_generation/unit_models/heater_1D.rst @@ -268,3 +268,9 @@ Heater1DData Class .. autoclass:: Heater1DData :members: + +Heater1DInitializer Class +------------------------- + +.. autoclass:: Heater1DInitializer + :members: \ No newline at end of file diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index d1984c6426..8d2ff86ba3 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -19,6 +19,7 @@ # Import Pyomo libraries from pyomo.environ import ( + assert_optimal_termination, Block, value, Var, @@ -27,7 +28,6 @@ Reference, units as pyunits, ) -import pyomo.opt from pyomo.common.config import ConfigValue, In, Bool from pyomo.network import Port @@ -36,6 +36,7 @@ from idaes.core.util.constants import Constants as const import idaes.core.util.scaling as iscale from idaes.core.solvers import get_solver +from idaes.core.util.exceptions import InitializationError from idaes.core.util.misc import add_object_reference from idaes.core.util.exceptions import ConfigurationError, BurntToast import idaes.logger as idaeslog @@ -54,6 +55,13 @@ class CrossFlowHeatExchanger1DInitializer(SingleControlVolumeUnitInitializer): """ Initializer for Cross Flow Heat Exchanger 1D units. + + First, the shell and tube control volumes are initialized without heat transfer. Next + the total possible heat transfer between streams is estimated based on heat capacity, + flow rate, and inlet/outlet temperatures. The actual temperature change is set to be + half the theoretical maximum, and the shell and tube are initialized with linear + temperature profiles. Finally, temperatures besides the inlets are unfixed and + the performance equations are activated before a full solve of the system model. """ def initialize_main_model( @@ -132,8 +140,8 @@ def initialize_main_model( # Important to do before initializing property packages in # case it is implemented as Var-Constraint pair instead of # an Expression - value(hot_side.properties[t0, 0].cp_mol) - value(cold_side.properties[t0, 0].cp_mol) + hot_side.properties[t0, 0].cp_mol + cold_side.properties[t0, 0].cp_mol # --------------------------------------------------------------------- # Initialize shell block @@ -303,7 +311,10 @@ def initialize_main_model( with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver_obj.solve(model, tee=slc.tee) - pyomo.opt.assert_optimal_termination(res) + try: + assert_optimal_termination(res) + except AssertionError: + raise InitializationError("Initialization solve failed.") init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res))) # In Step 3, unfix fluid state variables (enthalpy/temperature and pressure) @@ -337,7 +348,10 @@ def initialize_main_model( with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver_obj.solve(model, tee=slc.tee) - pyomo.opt.assert_optimal_termination(res) + try: + assert_optimal_termination(res) + except AssertionError: + raise InitializationError("Initialization solve failed.") init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) @@ -346,7 +360,10 @@ def initialize_main_model( with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver_obj.solve(model, tee=slc.tee) - pyomo.opt.assert_optimal_termination(res) + try: + assert_optimal_termination(res) + except AssertionError: + raise InitializationError("Initialization solve failed.") init_log.info_high("Initialization Step 4 {}.".format(idaeslog.condition(res))) diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index af6c40bad1..0d4ac17ced 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -40,6 +40,7 @@ import idaes.core.util.scaling as iscale from idaes.core.solvers import get_solver from idaes.core.util.config import is_physical_parameter_block +from idaes.core.util.exceptions import InitializationError from idaes.core.util.misc import add_object_reference import idaes.logger as idaeslog from idaes.core.util.tables import create_stream_table_dataframe @@ -57,6 +58,10 @@ class Heater1DInitializer(SingleControlVolumeUnitInitializer): """ Initializer for Heater 1D units. + + A simple initialization method that first initializes the control volume + without heat transfer, then adds heat transfer in and solves it again, + then finally solves the entire model. """ def initialize_main_model( @@ -68,6 +73,7 @@ def initialize_main_model( Initialization routine for the main Heater 1D model (as opposed to submodels like costing, which presently do not exist). + Args: model: Pyomo Block to be initialized. copy_inlet_state: bool (default=False). Whether to copy inlet state to other states or not @@ -113,11 +119,13 @@ def initialize_main_model( model.control_volume.pressure.fix() model.control_volume.length.fix() - assert degrees_of_freedom(model.control_volume) == 0 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver_obj.solve(model.control_volume, tee=slc.tee) - assert_optimal_termination(res) + try: + assert_optimal_termination(res) + except AssertionError: + raise InitializationError("Initialization solve failed.") init_log.info_high("Initialization Step 2 Complete.") model.control_volume.length.unfix() @@ -135,12 +143,13 @@ def initialize_main_model( model.control_volume.pressure.unfix() model.control_volume.pressure[:, 0].fix() - assert degrees_of_freedom(model) == 0 - with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = solver_obj.solve(model, tee=slc.tee) - assert_optimal_termination(res) + try: + assert_optimal_termination(res) + except AssertionError: + raise InitializationError("Initialization solve failed.") init_log.info_high("Initialization Step 3 Complete.") @@ -270,8 +279,6 @@ class Heater1DData(UnitModelBlockData): ), ) - CONFIG = CONFIG - # Common config args for both sides CONFIG.declare( "finite_elements", diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index b9b89139b2..84d5b53bc6 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -29,7 +29,7 @@ ) import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom -from idaes.core.solvers import get_solver +from idaes.core.util.model_diagnostics import DiagnosticsToolbox # Set up solver optarg = { @@ -41,7 +41,6 @@ "tol": 1e-8, "halt_on_ampl_error": "no", } -solver = get_solver("ipopt", options=optarg) def _create_model(pressure_drop): @@ -137,8 +136,6 @@ def _create_model(pressure_drop): iscale.set_scaling_factor(shell.heat, 1e-6) iscale.set_scaling_factor(tube.area, 1) iscale.set_scaling_factor(tube.heat, 1e-6) - iscale.set_scaling_factor(shell._enthalpy_flow, 1e-8) # pylint: disable=W0212 - iscale.set_scaling_factor(tube._enthalpy_flow, 1e-8) # pylint: disable=W0212 iscale.set_scaling_factor(shell.enthalpy_flow_dx, 1e-7) iscale.set_scaling_factor(tube.enthalpy_flow_dx, 1e-7) iscale.set_scaling_factor(hx.heat_holdup, 1e-8) @@ -231,9 +228,23 @@ def test_initialization(model_no_dP): @pytest.mark.integration -def test_units(model_no_dP): - assert_units_consistent(model_no_dP.fs.heat_exchanger) +def test_structural_issues_no_dP(model_no_dP): + dt = DiagnosticsToolbox(model_no_dP) + dt.assert_no_structural_warnings(ignore_evaluation_errors=True) +@pytest.mark.integration +def test_numerical_issues_no_dP(model_no_dP): + # Model will already be initialized if the component test is run, + # but reinitialize in case integration tests are run alone + initializer = model_no_dP.fs.heat_exchanger.default_initializer( + solver="ipopt", solver_options=optarg + ) + initializer.initialize(model=model_no_dP.fs.heat_exchanger) + + m_scaled = pyo.TransformationFactory('core.scale_model').create_using(model_no_dP, rename=False) + + dt = DiagnosticsToolbox(m_scaled) + dt.assert_no_numerical_warnings() @pytest.fixture def model_dP(): @@ -272,5 +283,20 @@ def test_initialization_dP(model_dP): @pytest.mark.integration -def test_units_dP(model_dP): - assert_units_consistent(model_dP.fs.heat_exchanger) +def test_structural_issues_dP(model_dP): + dt = DiagnosticsToolbox(model_dP) + dt.assert_no_structural_warnings(ignore_evaluation_errors=True) + +@pytest.mark.integration +def test_numerical_issues_dP(model_dP): + # Model will already be initialized if the component test is run, + # but reinitialize in case integration tests are run alone + initializer = model_dP.fs.heat_exchanger.default_initializer( + solver="ipopt", solver_options=optarg + ) + initializer.initialize(model=model_dP.fs.heat_exchanger) + + m_scaled = pyo.TransformationFactory('core.scale_model').create_using(model_dP, rename=False) + + dt = DiagnosticsToolbox(m_scaled) + dt.assert_no_numerical_warnings() \ No newline at end of file diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py index d0b70849ae..1f6fa3098b 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -13,7 +13,6 @@ import pytest import pyomo.environ as pyo -from pyomo.util.check_units import assert_units_consistent from idaes.core import FlowsheetBlock import idaes.core.util.scaling as iscale @@ -28,7 +27,7 @@ ) import idaes.core.util.model_statistics as mstat from idaes.core.util.model_statistics import degrees_of_freedom -from idaes.core.solvers import get_solver +from idaes.core.util.model_diagnostics import DiagnosticsToolbox # Set up solver optarg = { @@ -40,7 +39,6 @@ "tol": 1e-8, "halt_on_ampl_error": "no", } -solver = get_solver("ipopt", options=optarg) def _create_model(pressure_drop): @@ -193,9 +191,21 @@ def test_initialization(model_no_dP): @pytest.mark.integration -def test_units(model_no_dP): - assert_units_consistent(model_no_dP.fs.heater) +def test_structural_issues_no_dP(model_no_dP): + dt = DiagnosticsToolbox(model_no_dP) + dt.assert_no_structural_warnings(ignore_evaluation_errors=True) +@pytest.mark.integration +def test_numerical_issues_no_dP(model_no_dP): + # Model will already be initialized if the component test is run, + # but reinitialize in case integration tests are run alone + initializer = model_no_dP.fs.heater.default_initializer(solver="ipopt", solver_options=optarg) + initializer.initialize(model=model_no_dP.fs.heater) + + m_scaled = pyo.TransformationFactory('core.scale_model').create_using(model_no_dP, rename=False) + + dt = DiagnosticsToolbox(m_scaled) + dt.assert_no_numerical_warnings() @pytest.fixture def model_dP(): @@ -224,5 +234,18 @@ def test_initialization_dP(model_dP): @pytest.mark.integration -def test_units_dP(model_dP): - assert_units_consistent(model_dP.fs.heater) +def test_structural_issues_dP(model_dP): + dt = DiagnosticsToolbox(model_dP) + dt.assert_no_structural_warnings(ignore_evaluation_errors=True) + +@pytest.mark.integration +def test_numerical_issues_dP(model_dP): + # Model will already be initialized if the component test is run, + # but reinitialize in case integration tests are run alone + initializer = model_dP.fs.heater.default_initializer(solver="ipopt", solver_options=optarg) + initializer.initialize(model=model_dP.fs.heater) + + m_scaled = pyo.TransformationFactory('core.scale_model').create_using(model_dP, rename=False) + + dt = DiagnosticsToolbox(m_scaled) + dt.assert_no_numerical_warnings() From 10e46fac73d6102a2963bdd026942b68f586f8d4 Mon Sep 17 00:00:00 2001 From: Doug A Date: Mon, 22 Apr 2024 10:35:16 -0400 Subject: [PATCH 36/38] run Black --- .../cross_flow_heat_exchanger_1D.py | 8 +++---- .../power_generation/unit_models/heater_1D.py | 2 +- .../test_cross_flow_heat_exchanger_1D.py | 17 ++++++++++---- .../unit_models/tests/test_heater_1D.py | 23 ++++++++++++++----- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 8d2ff86ba3..27983e09f0 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -57,10 +57,10 @@ class CrossFlowHeatExchanger1DInitializer(SingleControlVolumeUnitInitializer): Initializer for Cross Flow Heat Exchanger 1D units. First, the shell and tube control volumes are initialized without heat transfer. Next - the total possible heat transfer between streams is estimated based on heat capacity, - flow rate, and inlet/outlet temperatures. The actual temperature change is set to be - half the theoretical maximum, and the shell and tube are initialized with linear - temperature profiles. Finally, temperatures besides the inlets are unfixed and + the total possible heat transfer between streams is estimated based on heat capacity, + flow rate, and inlet/outlet temperatures. The actual temperature change is set to be + half the theoretical maximum, and the shell and tube are initialized with linear + temperature profiles. Finally, temperatures besides the inlets are unfixed and the performance equations are activated before a full solve of the system model. """ diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 0d4ac17ced..9eff84d1c6 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -60,7 +60,7 @@ class Heater1DInitializer(SingleControlVolumeUnitInitializer): Initializer for Heater 1D units. A simple initialization method that first initializes the control volume - without heat transfer, then adds heat transfer in and solves it again, + without heat transfer, then adds heat transfer in and solves it again, then finally solves the entire model. """ diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py index 84d5b53bc6..9141f350e6 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_cross_flow_heat_exchanger_1D.py @@ -232,6 +232,7 @@ def test_structural_issues_no_dP(model_no_dP): dt = DiagnosticsToolbox(model_no_dP) dt.assert_no_structural_warnings(ignore_evaluation_errors=True) + @pytest.mark.integration def test_numerical_issues_no_dP(model_no_dP): # Model will already be initialized if the component test is run, @@ -241,11 +242,14 @@ def test_numerical_issues_no_dP(model_no_dP): ) initializer.initialize(model=model_no_dP.fs.heat_exchanger) - m_scaled = pyo.TransformationFactory('core.scale_model').create_using(model_no_dP, rename=False) - + m_scaled = pyo.TransformationFactory("core.scale_model").create_using( + model_no_dP, rename=False + ) + dt = DiagnosticsToolbox(m_scaled) dt.assert_no_numerical_warnings() + @pytest.fixture def model_dP(): m = _create_model(pressure_drop=True) @@ -287,6 +291,7 @@ def test_structural_issues_dP(model_dP): dt = DiagnosticsToolbox(model_dP) dt.assert_no_structural_warnings(ignore_evaluation_errors=True) + @pytest.mark.integration def test_numerical_issues_dP(model_dP): # Model will already be initialized if the component test is run, @@ -296,7 +301,9 @@ def test_numerical_issues_dP(model_dP): ) initializer.initialize(model=model_dP.fs.heat_exchanger) - m_scaled = pyo.TransformationFactory('core.scale_model').create_using(model_dP, rename=False) - + m_scaled = pyo.TransformationFactory("core.scale_model").create_using( + model_dP, rename=False + ) + dt = DiagnosticsToolbox(m_scaled) - dt.assert_no_numerical_warnings() \ No newline at end of file + dt.assert_no_numerical_warnings() diff --git a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py index 1f6fa3098b..91c34d2a26 100644 --- a/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/tests/test_heater_1D.py @@ -195,18 +195,24 @@ def test_structural_issues_no_dP(model_no_dP): dt = DiagnosticsToolbox(model_no_dP) dt.assert_no_structural_warnings(ignore_evaluation_errors=True) + @pytest.mark.integration def test_numerical_issues_no_dP(model_no_dP): # Model will already be initialized if the component test is run, # but reinitialize in case integration tests are run alone - initializer = model_no_dP.fs.heater.default_initializer(solver="ipopt", solver_options=optarg) + initializer = model_no_dP.fs.heater.default_initializer( + solver="ipopt", solver_options=optarg + ) initializer.initialize(model=model_no_dP.fs.heater) - m_scaled = pyo.TransformationFactory('core.scale_model').create_using(model_no_dP, rename=False) - + m_scaled = pyo.TransformationFactory("core.scale_model").create_using( + model_no_dP, rename=False + ) + dt = DiagnosticsToolbox(m_scaled) dt.assert_no_numerical_warnings() + @pytest.fixture def model_dP(): m = _create_model(pressure_drop=True) @@ -238,14 +244,19 @@ def test_structural_issues_dP(model_dP): dt = DiagnosticsToolbox(model_dP) dt.assert_no_structural_warnings(ignore_evaluation_errors=True) + @pytest.mark.integration def test_numerical_issues_dP(model_dP): # Model will already be initialized if the component test is run, # but reinitialize in case integration tests are run alone - initializer = model_dP.fs.heater.default_initializer(solver="ipopt", solver_options=optarg) + initializer = model_dP.fs.heater.default_initializer( + solver="ipopt", solver_options=optarg + ) initializer.initialize(model=model_dP.fs.heater) - m_scaled = pyo.TransformationFactory('core.scale_model').create_using(model_dP, rename=False) - + m_scaled = pyo.TransformationFactory("core.scale_model").create_using( + model_dP, rename=False + ) + dt = DiagnosticsToolbox(m_scaled) dt.assert_no_numerical_warnings() From 18a7f3686c335276c05213f29bf02bf7d14d00ef Mon Sep 17 00:00:00 2001 From: Doug A Date: Mon, 22 Apr 2024 11:25:27 -0400 Subject: [PATCH 37/38] pylint --- .../unit_models/cross_flow_heat_exchanger_1D.py | 4 ++-- idaes/models_extra/power_generation/unit_models/heater_1D.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 27983e09f0..963a186cb3 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -140,8 +140,8 @@ def initialize_main_model( # Important to do before initializing property packages in # case it is implemented as Var-Constraint pair instead of # an Expression - hot_side.properties[t0, 0].cp_mol - cold_side.properties[t0, 0].cp_mol + value(hot_side.properties[t0, 0].cp_mol) + value(cold_side.properties[t0, 0].cp_mol) # --------------------------------------------------------------------- # Initialize shell block diff --git a/idaes/models_extra/power_generation/unit_models/heater_1D.py b/idaes/models_extra/power_generation/unit_models/heater_1D.py index 9eff84d1c6..e52d5d3d7c 100644 --- a/idaes/models_extra/power_generation/unit_models/heater_1D.py +++ b/idaes/models_extra/power_generation/unit_models/heater_1D.py @@ -44,7 +44,6 @@ from idaes.core.util.misc import add_object_reference import idaes.logger as idaeslog from idaes.core.util.tables import create_stream_table_dataframe -from idaes.core.util.model_statistics import degrees_of_freedom from idaes.models_extra.power_generation.unit_models.heat_exchanger_common import ( make_geometry_common, make_performance_common, From 29c4a88556bd574b7e957dae427f694f170189a3 Mon Sep 17 00:00:00 2001 From: Doug A Date: Mon, 22 Apr 2024 11:31:12 -0400 Subject: [PATCH 38/38] no more values --- .../unit_models/cross_flow_heat_exchanger_1D.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py index 963a186cb3..d4615698d5 100644 --- a/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py +++ b/idaes/models_extra/power_generation/unit_models/cross_flow_heat_exchanger_1D.py @@ -140,8 +140,8 @@ def initialize_main_model( # Important to do before initializing property packages in # case it is implemented as Var-Constraint pair instead of # an Expression - value(hot_side.properties[t0, 0].cp_mol) - value(cold_side.properties[t0, 0].cp_mol) + hot_side.properties[t0, 0].cp_mol # pylint: disable=pointless-statement + cold_side.properties[t0, 0].cp_mol # pylint: disable=pointless-statement # --------------------------------------------------------------------- # Initialize shell block