Heat Exchanger 0D Unit Model with Two Property Packages¶
Problem Statement: In this example, we will be heating a benzene-toluene mixture using steam.
Tube Side Inlet
Flow Rate = 250 mol/s
Mole fraction (Benzene) = 0.4
Mole fraction (Toluene) = 0.6
Pressure = 101325 Pa
Temperature = 350 K
Shell Side Inlet
Flow Rate = 100 mol/s
Mole fraction (Steam) = 1
Pressure = 101325 Pa
Temperature = 450 K
This example will demonstrate the simulation of the 0D heat exchanger by fixing any 2 of the following degrees of freedom: - heat transfer area - overall heat transfer coefficient - minimum approach temperature
IDAES documentation reference for heat exchanger 0D model: https://idaes-pse.readthedocs.io/en/latest/reference_guides/model_libraries/generic/unit_models/heat_exchanger.html
The IDAES library contains a more advanced
HeatExchangerLumpedCapacitance
model supporting a wall temperature
and heat holdup for transient simulations; more details on the lumped
capacitance heat exchanger may be found
here.
Importing necessary tools¶
First, import the required IDAES and Pyomo modules. Note that the hotside (shell) and coldside (tube) properties leverage separate property packages:
# Import pyomo package
from pyomo.environ import ConcreteModel, Constraint, value, units
# Import idaes logger to set output levels
import idaes.logger as idaeslog
# Import the main FlowsheetBlock from IDAES. The flowsheet block will contain the unit model
from idaes.core import FlowsheetBlock
# Import the IAPWS property package to create a properties block for steam in the flowsheet
from idaes.models.properties import iapws95
from idaes.models.properties.iapws95 import htpx
from idaes.models.properties.modular_properties import GenericParameterBlock
# Import the BT_ideal property package to create a properties block for the tube side in the flowsheet
from idaes.models.properties.modular_properties.examples.BT_ideal \
import configuration
# Import the degrees_of_freedom function from the idaes.core.util.model_statistics package
from idaes.core.util.model_statistics import degrees_of_freedom
# Import the default IPOPT solver
from idaes.core.solvers import get_solver
# Import a heat exchanger unit
from idaes.models.unit_models.heat_exchanger import HeatExchanger, delta_temperature_amtd_callback
Setting up the flowsheet¶
Then, we will create the ConcreteModel
foundation, attach the steady
state flowsheet, and declare the property parameter block that will used
for the shell and tube sides.
More information on this general workflow can be found here: https://idaes-pse.readthedocs.io/en/stable/how_to_guides/workflow/general.html
m = ConcreteModel()
m.fs = FlowsheetBlock(dynamic=False)
m.fs.properties_shell = iapws95.Iapws95ParameterBlock()
m.fs.properties_tube = GenericParameterBlock(**configuration)
Then, import and define the HeatExchanger
unit, add it to the
flowsheet, and determine the initial degrees of freedom associated with
the heat exchanger. As mentioned above, we will designate the shell as
the hot side and the tube as the cold side. The
delta_temperature_callback
option controls how $
:raw-latex:`\Delta `T$ is calculated, in this case an average mean
temperature difference (AMTD) approach; see the
`Callbacks <https://idaes-pse.readthedocs.io/en/latest/reference_guides/model_libraries/generic/unit_models/heat_exchanger.html#callbacks>`__
section of the documentation for more information on available callback
options.
The unit is created below:
m.fs.heat_exchanger = HeatExchanger(
delta_temperature_callback=delta_temperature_amtd_callback,
hot_side_name="shell",
cold_side_name="tube",
shell={"property_package": m.fs.properties_shell},
tube={"property_package": m.fs.properties_tube})
DOF_initial = degrees_of_freedom(m)
print("The initial DOF is {0}".format(DOF_initial))
The initial DOF is 10
assert DOF_initial == 10
Fixing input specifications¶
For this problem, we will fix the inlet conditions, re-evaluate the degrees of freedom to ensure the problem is square (i.e. DOF=0), and run two different options for unit specifications:
h = htpx(450*units.K, P = 101325*units.Pa) # calculate IAPWS enthalpy
m.fs.heat_exchanger.shell_inlet.flow_mol.fix(100) # mol/s
m.fs.heat_exchanger.shell_inlet.pressure.fix(101325) # Pa
m.fs.heat_exchanger.shell_inlet.enth_mol.fix(h) # J/mol
DOF_initial = degrees_of_freedom(m)
print("The DOF is {0}".format(DOF_initial))
The DOF is 7
m.fs.heat_exchanger.tube_inlet.flow_mol.fix(250) # mol/s
m.fs.heat_exchanger.tube_inlet.mole_frac_comp[0, "benzene"].fix(0.4)
m.fs.heat_exchanger.tube_inlet.mole_frac_comp[0, "toluene"].fix(0.6)
m.fs.heat_exchanger.tube_inlet.pressure.fix(101325) # Pa
m.fs.heat_exchanger.tube_inlet.temperature[0].fix(350) # K
DOF_final = degrees_of_freedom(m)
print("The DOF is {0}".format(DOF_final))
The DOF is 2
Option 1: Fix overall heat transfer coefficient (HTC) and the heat transfer area¶
Below, we fix the heat exchanger area and heat transfer coefficient, which yields a fully defined problem with zero degrees of freedom that may be initialized and solved:
m.fs.heat_exchanger.area.fix(50) # m2
m.fs.heat_exchanger.overall_heat_transfer_coefficient[0].fix(500) # W/m2/K
DOF_final = degrees_of_freedom(m)
print("The DOF is {0}".format(DOF_final))
The DOF is 0
assert DOF_final == 0
Now that the problem is square (zero degress of freedom), we can initialize and solve the full model:
# Initialize the flowsheet, and set the output at INFO
m.fs.heat_exchanger.initialize(outlvl=idaeslog.INFO)
# Solve the simulation using ipopt
# Note: If the degrees of freedom = 0, we have a square problem
opt = get_solver()
solve_status = opt.solve(m)
# Display a readable report
m.fs.heat_exchanger.report()
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.hot_side: Initialization Complete
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_in: Starting initialization
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_in: Dew and bubble point initialization: optimal - Optimal Solution Found.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_in: Equilibrium temperature initialization completed.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_in: State variable initialization completed.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_in: Phase equilibrium initialization: optimal - Optimal Solution Found.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_in: Property initialization: optimal - Optimal Solution Found.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_out: Starting initialization
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_out: Dew and bubble point initialization: optimal - Optimal Solution Found.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_out: Equilibrium temperature initialization completed.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_out: State variable initialization completed.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_out: Phase equilibrium initialization: optimal - Optimal Solution Found.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side.properties_out: Property initialization: optimal - Optimal Solution Found.
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger.cold_side: Initialization Complete
2023-03-04 01:47:55 [INFO] idaes.init.fs.heat_exchanger: Initialization Completed, optimal - Optimal Solution Found
====================================================================================
Unit : fs.heat_exchanger Time: 0.0
------------------------------------------------------------------------------------
Unit Performance
Variables:
Key : Value : Units : Fixed : Bounds
HX Area : 50.000 : meter ** 2 : True : (0, None)
HX Coefficient : 500.00 : kilogram / kelvin / second ** 3 : True : (0, None)
Heat Duty : 1.2985e+06 : watt : False : (None, None)
Expressions:
Key : Value : Units
Delta T Driving : 51.940 : kelvin
Delta T In : 80.757 : kelvin
Delta T Out : 23.124 : kelvin
------------------------------------------------------------------------------------
Stream Table
Units shell Inlet shell Outlet tube Inlet tube Outlet
Molar Flow mole / second 100 100.00 - -
Mass Flow kilogram / second 1.8015 1.8015 - -
T kelvin 450.00 373.12 - -
P pascal 101325 1.0132e+05 - -
Vapor Fraction dimensionless 1.0000 0.74888 - -
Molar Enthalpy joule / mole 50977. 37992. - -
Total Molar Flowrate mole / second - - 250 250.00
Total Mole Fraction benzene dimensionless - - 0.40000 0.40000
Total Mole Fraction toluene dimensionless - - 0.60000 0.60000
Temperature kelvin - - 350 369.24
Pressure pascal - - 1.0132e+05 1.0132e+05
====================================================================================
from pyomo.environ import assert_optimal_termination
import pytest
# Check if termination condition is optimal
assert_optimal_termination(solve_status)
assert value(m.fs.heat_exchanger.shell.properties_out[0].temperature) == pytest.approx(373.13, abs=1e-2)
assert value(m.fs.heat_exchanger.tube.properties_out[0].temperature) == pytest.approx(369.24, abs=1e-2)
Option 2: Unfix area and fix shell side outlet temperature¶
In the previous case, we fixed the heat exchanger area and overall heat transfer coefficient. However, given that the models in IDAES are equation oriented, we can fix the outlet variables. For example, we can fix the outlet temperature for the shell side and solve for the heat exchanger area that will satisfy that condition.
m.fs.heat_exchanger.area.unfix()
m.fs.heat_exchanger.shell_outlet.enth_mol.fix(htpx(360*units.K, P = 101325*units.Pa))
# Call the degrees_of_freedom function, get final DOF
DOF_final = degrees_of_freedom(m)
print("The DOF is {0}".format(DOF_final))
The DOF is 0
result = opt.solve(m)
print(result)
# Display a readable report
m.fs.heat_exchanger.report()
Problem: - Lower bound: -inf Upper bound: inf Number of objectives: 1 Number of constraints: 44 Number of variables: 44 Sense: unknown Solver: - Status: ok Message: Ipopt 3.13.2x3a Optimal Solution Found Termination condition: optimal Id: 0 Error rc: 0 Time: 0.024888992309570312 Solution: - number of solutions: 0 number of solutions displayed: 0 ==================================================================================== Unit : fs.heat_exchanger Time: 0.0 ------------------------------------------------------------------------------------ Unit Performance Variables: Key : Value : Units : Fixed : Bounds HX Area : 200.26 : meter ** 2 : False : (0, None) HX Coefficient : 500.00 : kilogram / kelvin / second ** 3 : True : (0, None) Heat Duty : 4.4423e+06 : watt : False : (None, None) Expressions: Key : Value : Units Delta T Driving : 44.365 : kelvin Delta T In : 78.730 : kelvin Delta T Out : 10.000 : kelvin ------------------------------------------------------------------------------------ Stream Table Units shell Inlet shell Outlet tube Inlet tube Outlet Molar Flow mole / second 100 100.00 - - Mass Flow kilogram / second 1.8015 1.8015 - - T kelvin 450.00 360.00 - - P pascal 101325 1.0132e+05 - - Vapor Fraction dimensionless 1.0000 0.0000 - - Molar Enthalpy joule / mole 50977. 6554.3 - - Total Molar Flowrate mole / second - - 250 250.00 Total Mole Fraction benzene dimensionless - - 0.40000 0.40000 Total Mole Fraction toluene dimensionless - - 0.60000 0.60000 Temperature kelvin - - 350 371.27 Pressure pascal - - 1.0132e+05 1.0132e+05 ====================================================================================
# Check if termination condition is optimal
assert_optimal_termination(result)
assert value(m.fs.heat_exchanger.area) == pytest.approx(200.26, abs=1e-2)
assert value(m.fs.heat_exchanger.tube.properties_out[0].temperature) == pytest.approx(371.27, abs=1e-2)