# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
#
# This module is under the UIUC open-source license. See
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
This module contains heat exchanger unit operations.
.. contents:: :local:
.. autoclass:: biosteam.units.heat_exchange.HX
.. autoclass:: biosteam.units.heat_exchange.HXutility
.. autoclass:: biosteam.units.heat_exchange.HXprocess
"""
from .. import Unit
from thermosteam._graphics import utility_heat_exchanger_graphics, process_heat_exchanger_graphics
from .design_tools.specification_factors import (
shell_and_tube_material_factor_coefficients,
compute_shell_and_tube_material_factor)
from ..utils import list_available_names
from .design_tools import heat_transfer as ht
import numpy as np
import biosteam as bst
from math import exp, log as ln
from typing import Optional
from numba import njit
from .._heat_utility import UtilityAgent
__all__ = ('HX', 'HXutility', 'HXutilities', 'HXprocess')
# Length factor
x = np.array((8, 13, 16, 20))
y = np.array((1.25, 1.12, 1.05, 1))
p2 = np.polyfit(x, y, 2)
# %% Purchase price
@njit(cache=True)
def compute_floating_head_purchase_cost(A, CE): # A - area ft**2
return exp(12.0310 - 0.8709*ln(A) + 0.09005 * ln(A)**2)*CE/567
@njit(cache=True)
def compute_fixed_head_purchase_cost(A, CE):
return exp(11.4185 - 0.9228*ln(A) + 0.09861 * ln(A)**2)*CE/567
@njit(cache=True)
def compute_u_tube_purchase_cost(A, CE):
return exp(11.5510 - 0.9186*ln(A) + 0.09790 * ln(A)**2)*CE/567
@njit(cache=True)
def compute_kettle_vaporizer_purchase_cost(A, CE):
return exp(12.3310 - 0.8709*ln(A) + 0.09005 * ln(A)**2)*CE/567
@njit(cache=True)
def compute_double_pipe_purchase_cost(A, CE):
return exp(7.2718 + 0.16*ln(A))*CE/567
@njit(cache=True)
def compute_furnace_purchase_cost(Q, CE): # Q - duty Btu/hr
return exp(-0.15241 + 0.785*ln(Q))*CE/567
# Purchase price
Cb_dict = {'Floating head': compute_floating_head_purchase_cost,
'Fixed head': compute_fixed_head_purchase_cost,
'U tube': compute_u_tube_purchase_cost,
'Kettle vaporizer': compute_kettle_vaporizer_purchase_cost,
'Double pipe': compute_double_pipe_purchase_cost,
'Furnace': compute_furnace_purchase_cost}
# %% Classes
[docs]
class HX(Unit, isabstract=True):
"""
Abstract class for counter current heat exchanger.
**Abstract methods**
get_streams()
Should return two inlet streams and two outlet streams that exchange
heat.
"""
line = 'Heat exchanger'
_units = {'Area': 'ft^2',
'Overall heat transfer coefficient': 'kW/m^2/K',
'Log-mean temperature difference': 'K',
'Tube side pressure drop': 'Pa',
'Shell side pressure drop': 'Pa',
'Operating pressure': 'psi',
'Total tube length': 'ft'}
_N_ins = 1
_N_outs = 1
_F_BM_default = {'Double pipe': 1.8,
'Floating head': 3.17,
'Fixed head': 3.17,
'U tube': 3.17,
'Kettle vaporizer': 3.17,
'Furnace': 2.19}
@property
def material(self):
"""Default 'Carbon steel/carbon steel'"""
return self._material
@material.setter
def material(self, material):
try:
self._F_Mab = shell_and_tube_material_factor_coefficients[material]
except KeyError:
raise AttributeError("material must be one of the following: "
f"{list_available_names(shell_and_tube_material_factor_coefficients)}")
self._material = material
@property
def heat_exchanger_type(self):
"""[str] Heat exchanger type. Purchase cost depends on this selection."""
return self._heat_exchanger_type
@heat_exchanger_type.setter
def heat_exchanger_type(self, heat_exchanger_type):
try:
self._Cb_func = Cb_dict[heat_exchanger_type]
except KeyError:
raise AttributeError("heat exchange type must be one of the following: "
f"{list_available_names(Cb_dict)}")
self._heat_exchanger_type = heat_exchanger_type
def _assert_compatible_property_package(self):
if self.owner is not self:
return
assert all([i.chemicals is j.chemicals for i, j in zip(self._ins, self._outs) if (i and j)]), (
"inlet and outlet stream chemicals are incompatible; "
"try using the `thermo` keyword argument to initialize the unit operation "
"with a compatible thermodynamic property package"
)
def _design(self):
# Get heat transfer (kW)
Q = self.total_heat_transfer
if Q <= 1e-12:
self.design_results.clear()
self.isfurnace = False
return
self.isfurnace = self.heat_utilities and self.heat_utilities[0].agent.isfuel
if self.isfurnace: return
Q = abs(Q) / 3600
### Use LMTD correction factor method ###
Design = self.design_results
# Get cold and hot inlet and outlet streams
ci, hi, co, ho = ht.order_streams(*self.get_streams())
# Get log mean temperature difference
Tci = ci.T
Thi = hi.T
Tco = co.T
Tho = ho.T
LMTD = ht.compute_LMTD(Thi, Tho, Tci, Tco)
# Get correction factor
ft = self.ft
if not ft:
N_shells = self.N_shells
ft = ht.compute_Fahkeri_LMTD_correction_factor(
Tci, Thi, Tco, Tho, N_shells)
# Get overall heat transfer coefficient
U = self.U or ht.heuristic_overall_heat_transfer_coefficient(
ci, hi, co, ho)
# TODO: Complete design of heat exchanger to find L
# For now assume lenght is 20 ft
L = 20
# Design pressure
P = max((ci.P, hi.P))
Design['Area'] = 10.763 * \
ht.compute_heat_transfer_area(abs(LMTD), U, Q, ft)
Design['Overall heat transfer coefficient'] = U
Design['Log-mean temperature difference'] = LMTD
Design['Fouling correction factor'] = ft
Design['Operating pressure'] = P * 14.7/101325 # psi
Design['Total tube length'] = L
if not self.neglect_pressure_drop:
Design['Tube side pressure drop'] = ho.P - hi.P
Design['Shell side pressure drop'] = co.P - ci.P
def _cost(self):
Design = self.design_results
if self.isfurnace:
Q = self.total_heat_transfer * 0.947817 # kJ / hr to Btu/hr
self.baseline_purchase_costs['Furnace'] = compute_furnace_purchase_cost(Q, bst.CE)
P = self.furnace_pressure
S = P / 500
self.F_P['Furnace'] = 0.986 - 0.0035 * S + 0.0175 * S * S
return
if not Design:
return
A = Design['Area']
L = Design['Total tube length']
P = Design['Operating pressure']
if A < 150: # Double pipe
P = P/600
F_p = 0.8510 + 0.1292*P + 0.0198*P**2
# Assume outer pipe carbon steel, inner pipe stainless steel
F_m = 2
A_min = 2.1718
if A < A_min:
F_l = A/A_min
A = A_min
else:
F_l = 1
heat_exchanger_type = 'Double pipe'
C_b = compute_double_pipe_purchase_cost(A, bst.CE)
else: # Shell and tube
F_m = compute_shell_and_tube_material_factor(A, *self._F_Mab)
F_l = 1 if L > 20 else np.polyval(p2, L)
P = P/100
F_p = 0.9803 + 0.018*P + 0.0017*P**2
heat_exchanger_type = self.heat_exchanger_type
C_b = self._Cb_func(A, bst.CE)
# Free on board purchase prize
self.F_M[heat_exchanger_type] = F_m
self.F_P[heat_exchanger_type] = F_p
self.F_D[heat_exchanger_type] = F_l
self.baseline_purchase_costs[heat_exchanger_type] = C_b
[docs]
class HXutility(HX):
"""
Create a heat exchanger that changes temperature of the
outlet stream using a heat utility.
Parameters
----------
ins :
Inlet.
outs :
Outlet.
T=None : float
Temperature of outlet stream [K].
V=None : float
Vapor fraction of outlet stream.
rigorous=False : bool
If true, calculate vapor liquid equilibrium
U=None : float, optional
Enforced overall heat transfer coefficent [kW/m^2/K].
heat_exchanger_type : str, optional
Heat exchanger type. Defaults to "Floating head".
N_shells : int, optional
Number of shells. Defaults to 2.
ft : float, optional
User imposed correction factor.
heat_only : bool, optional
If True, heat exchanger can only heat.
cool_only : bool, optional
If True, heat exchanger can only cool.
heat_transfer_efficiency : bool, optional
User enforced heat transfer efficiency. A value less than 1
means that a fraction of heat transfered is lost to the environment.
Defaults to the heat transfer efficiency of the utility agent.
Notes
-----
Must specify either `T` or `V` when creating a HXutility object.
Examples
--------
Run heat exchanger by temperature:
>>> from biosteam.units import HXutility
>>> from biosteam import Stream, settings
>>> settings.set_thermo(['Water', 'Ethanol'], cache=True)
>>> feed = Stream('feed', Water=200, Ethanol=200)
>>> hx = HXutility('hx', ins=feed, outs='product', T=50+273.15,
... rigorous=False) # Ignore VLE
>>> hx.simulate()
>>> hx.show()
HXutility: hx
ins...
[0] feed
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 200
Ethanol 200
outs...
[0] product
phase: 'l', T: 323.15 K, P: 101325 Pa
flow (kmol/hr): Water 200
Ethanol 200
>>> hx.results()
Heat exchanger Units hx
Low pressure steam Duty kJ/hr 1.01e+06
Flow kmol/hr 26.2
Cost USD/hr 6.22
Design Area ft^2 59.9
Overall heat transfer coefficient kW/m^2/K 0.5
Log-mean temperature difference K 101
Fouling correction factor 1
Operating pressure psi 50
Total tube length ft 20
Purchase cost Double pipe USD 4.78e+03
Total purchase cost USD 4.78e+03
Utility cost USD/hr 6.22
Run heat exchanger by vapor fraction:
>>> feed = Stream('feed', Water=200, Ethanol=200)
>>> hx = HXutility('hx', ins=feed, outs='product', V=1,
... rigorous=True) # Include VLE
>>> hx.simulate()
>>> hx.show()
HXutility: hx
ins...
[0] feed
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 200
Ethanol 200
outs...
[0] product
phase: 'g', T: 357.44 K, P: 101325 Pa
flow (kmol/hr): Water 200
Ethanol 200
>>> hx.results()
Heat exchanger Units hx
Low pressure steam Duty kJ/hr 1.94e+07
Flow kmol/hr 500
Cost USD/hr 119
Design Area ft^2 716
Overall heat transfer coefficient kW/m^2/K 1
Log-mean temperature difference K 80.8
Fouling correction factor 1
Operating pressure psi 50
Total tube length ft 20
Purchase cost Floating head USD 2.65e+04
Total purchase cost USD 2.65e+04
Utility cost USD/hr 119
We can also specify the heat transfer efficiency of the heat exchanger:
>>> hx.heat_transfer_efficiency = 1. # Originally 0.95 for low pressure steam
>>> hx.simulate()
>>> hx.results() # Notice how the duty, utility cost, and capital cost decreased
Heat exchanger Units hx
Low pressure steam Duty kJ/hr 1.84e+07
Flow kmol/hr 475
Cost USD/hr 113
Design Area ft^2 680
Overall heat transfer coefficient kW/m^2/K 1
Log-mean temperature difference K 80.8
Fouling correction factor 1
Operating pressure psi 50
Total tube length ft 20
Purchase cost Floating head USD 2.61e+04
Total purchase cost USD 2.61e+04
Utility cost USD/hr 113
Run heat exchanger by vapor fraction:
>>> feed = Stream('feed', Water=200, Ethanol=200)
>>> hx = HXutility('hx', ins=feed, outs='product', V=1,
... rigorous=True) # Include VLE
>>> hx.simulate()
>>> hx.show()
HXutility: hx
ins...
[0] feed
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Water 200
Ethanol 200
outs...
[0] product
phase: 'g', T: 357.44 K, P: 101325 Pa
flow (kmol/hr): Water 200
Ethanol 200
>>> hx.results()
Heat exchanger Units hx
Low pressure steam Duty kJ/hr 1.94e+07
Flow kmol/hr 500
Cost USD/hr 119
Design Area ft^2 716
Overall heat transfer coefficient kW/m^2/K 1
Log-mean temperature difference K 80.8
Fouling correction factor 1
Operating pressure psi 50
Total tube length ft 20
Purchase cost Floating head USD 2.65e+04
Total purchase cost USD 2.65e+04
Utility cost USD/hr 119
We can also specify the heat transfer efficiency of the heat exchanger:
>>> hx.heat_transfer_efficiency = 1. # Originally 0.95 for low pressure steam
>>> hx.simulate()
>>> hx.results() # Notice how the duty, utility cost, and capital cost decreased
Heat exchanger Units hx
Low pressure steam Duty kJ/hr 1.84e+07
Flow kmol/hr 475
Cost USD/hr 113
Design Area ft^2 680
Overall heat transfer coefficient kW/m^2/K 1
Log-mean temperature difference K 80.8
Fouling correction factor 1
Operating pressure psi 50
Total tube length ft 20
Purchase cost Floating head USD 2.61e+04
Total purchase cost USD 2.61e+04
Utility cost USD/hr 113
"""
line = 'Heat exchanger'
_graphics = utility_heat_exchanger_graphics
def _init(self,
T=None, V=None, rigorous=False, U=None, H=None,
heat_exchanger_type="Floating head",
material="Carbon steel/carbon steel",
N_shells=2,
ft=None,
heat_only=None,
cool_only=None,
heat_transfer_efficiency=None,
inner_fluid_pressure_drop=None,
outer_fluid_pressure_drop=None,
neglect_pressure_drop=True,
furnace_pressure=None, # [Pa] equivalent to 500 psig
):
self.T = T # : [float] Temperature of outlet stream (K).
self.V = V # : [float] Vapor fraction of outlet stream.
self.H = H # : [float] Enthalpy of outlet stream.
#: [bool] If true, calculate vapor liquid equilibrium
self.rigorous = rigorous
#: [float] Enforced overall heat transfer coefficent (kW/m^2/K)
self.U = U
#: [int] Number of shells for LMTD correction factor method.
self.N_shells = N_shells
#: [float] User imposed correction factor.
self.ft = ft
#: [bool] If True, heat exchanger can only heat.
self.heat_only = heat_only
#: [bool] If True, heat exchanger can only cool.
self.cool_only = cool_only
self.material = material
self.heat_exchanger_type = heat_exchanger_type
#: Optional[float] Pressure drop along the inner fluid.
self.inner_fluid_pressure_drop = inner_fluid_pressure_drop
#: Optional[float] Pressure drop along the outer fluid.
self.outer_fluid_pressure_drop = outer_fluid_pressure_drop
#: [bool] Whether to assume a negligible pressure drop.
self.neglect_pressure_drop = neglect_pressure_drop
#: [bool] User enforced heat transfer efficiency. A value less than 1
#: means that a fraction of heat transfered is lost to the environment.
#: If value is None, it defaults to the heat transfer efficiency of the
#: heat utility.
self.heat_transfer_efficiency = heat_transfer_efficiency
#: Optional[float] Internal pressure of combustion gas. Defaults
#: 500 psig (equivalent to 3548325.0 Pa)
self.furnace_pressure = 500 if furnace_pressure is None else furnace_pressure
@property
def total_heat_transfer(self):
"""[float] Heat transfer in kJ/hr, including environmental losses."""
return abs(self.net_duty)
Q = total_heat_transfer # Alias for backward compatibility
def simulate_as_auxiliary_exchanger(self,
ins, outs=None, duty=None, vle=True, scale=None, hxn_ok=True,
P_in=None, P_out=None, update=False,
):
inlet = self.ins[0]
outlet = self.outs[0]
if not inlet:
inlet = inlet.materialize_connection(None)
if not outlet:
outlet = outlet.materialize_connection(None)
inlet.mix_from(ins, conserve_phases=True)
if P_in is None:
P_in = inlet.P
else:
inlet.P = P_in
if vle:
inlet.vle(H=sum([i.H for i in ins]), P=P_in)
inlet.reduce_phases()
if outs is None:
if duty is None:
raise ValueError('must pass duty when no outlets are given')
outlet.copy_like(inlet)
if P_out is None:
P_out = outlet.P
else:
outlet.P = P_out
if vle:
outlet.vle(H=inlet.H + duty, P=P_out)
inlet.reduce_phases()
else:
outlet.Hnet = inlet.Hnet + duty
else:
outlet.mix_from(outs, conserve_phases=True)
if P_out is None:
P_out = outlet.P
else:
outlet.P = P_out
if duty is None:
duty = outlet.Hnet - inlet.Hnet
elif vle:
outlet.vle(H=inlet.H + duty, P=P_out)
inlet.reduce_phases()
else:
outlet.Hnet = inlet.Hnet + duty
if scale is not None:
duty *= scale
inlet.scale(scale)
outlet.scale(scale)
self.simulate(
run=False, # Do not run mass and energy balance
design_kwargs=dict(duty=duty),
)
for i in self.heat_utilities:
i.hxn_ok = hxn_ok
def _run(self):
feed = self.ins[0]
outlet = self.outs[0]
outlet.copy_flow(feed)
if outlet.isempty():
return
T = self.T
V = self.V
H = self.H
T_given = T is not None
V_given = V is not None
H_given = H is not None
N_given = T_given + V_given + H_given
if N_given == 0:
raise RuntimeError("no specification available; must define at either "
"temperature 'T', vapor fraction, 'V', or enthalpy 'H'")
if self.neglect_pressure_drop:
outlet.P = feed.P
else:
if T_given:
cooling = T > feed.T
elif V_given:
cooling = V < feed.vapor_fraction
elif H_given:
cooling = H < feed.H
else:
raise RuntimeError('unknown error')
if cooling:
if self.outer_fluid_pressure_drop:
outlet.P = feed.P - self.outer_fluid_pressure_drop
else:
estimate_pressure_drop = True
else:
if self.inner_fluid_pressure_drop:
outlet.P = feed.P - self.inner_fluid_pressure_drop
else:
estimate_pressure_drop = True
if estimate_pressure_drop:
self.neglect_pressure_drop = True
# Need to rerun to see find pressure drop.
# TODO: Make more efficient instead of rerunning.
try:
self._run()
finally:
self.neglect_pressure_drop = False
outlet.P = feed.P - 6894.76 * ht.heuristic_pressure_drop(
feed.vapor_fraction, outlet.vapor_fraction
)
if self.rigorous:
if N_given > 1:
raise RuntimeError("may only specify either temperature, 'T', "
"vapor fraction 'V', or enthalpy 'H', "
"in a rigorous simulation")
if V_given:
if V == 0:
outlet.phase = 'l'
outlet.T = outlet.bubble_point_at_P().T
elif V == 1:
outlet.phase = 'g'
outlet.T = outlet.dew_point_at_P().T
elif 0 < V < 1:
outlet.vle(V=V, P=outlet.P)
else:
raise RuntimeError("vapor fraction, 'V', must be a "
"positive fraction")
elif T_given:
if outlet.isempty():
outlet.T = T
else:
try:
outlet.vle(T=T, P=outlet.P)
except RuntimeError as e:
if len(outlet.phases) > 1:
raise e
T_bubble = outlet.bubble_point_at_P().T
if T <= T_bubble:
outlet.phase = 'l'
else:
T_dew = outlet.dew_point_at_P().T
if T_dew >= T:
outlet.phase = 'g'
else:
raise RuntimeError(
'outlet in vapor-liquid equilibrium, but stream is linked')
outlet.T = T
except ValueError:
outlet.vle(T=T, P=outlet.P)
else:
outlet.vle(H=H, P=outlet.P)
else:
if T_given and H_given:
raise RuntimeError("cannot specify both temperature, 'T' "
"and enthalpy 'H'")
if T_given:
outlet.T = T
else:
outlet.T = feed.T
if V_given:
if V == 0:
outlet.phase = 'l'
elif V == 1:
outlet.phase = 'g'
else:
raise RuntimeError("vapor fraction, 'V', must be either "
"0 or 1 in a non-rigorous simulation")
if V == 1 and feed.vapor_fraction < 1. and (outlet.T + 1e-6) < feed.T:
raise ValueError(
'outlet cannot be cooler than inlet if boiling')
if V == 0 and feed.vapor_fraction > 0. and outlet.T > feed.T + 1e-6:
raise ValueError(
'outlet cannot be hotter than inlet if condensing')
else:
phase = feed.phase
if len(phase) == 1:
outlet.phase = phase
if H_given:
outlet.H = H
if self.heat_only and outlet.H - feed.H < 0.:
outlet.copy_like(feed)
return
if self.cool_only and outlet.H - feed.H > 0.:
outlet.copy_like(feed)
return
def get_streams(self):
"""
Return inlet and outlet streams.
Returns
-------
in_a : Stream
Inlet a.
in_b : Stream
Inlet b.
out_a : Stream
Outlet a.
out_b : Stream
Outlet b.
"""
in_a = self.ins[0]
out_a = self.outs[0]
hu = self.heat_utilities[0]
in_b = hu.inlet_utility_stream
out_b = hu.outlet_utility_stream
return in_a, in_b, out_a, out_b
def _design(self, duty=None):
# Set duty and run heat utility
if duty is None:
duty = self.Hnet # Includes heat of formation
inlet = self.ins[0]
outlet = self.outs[0]
T_in = inlet.T
T_out = outlet.T
iscooling = duty < 0.
if iscooling: # Assume there is a pressure drop before the heat exchanger
if T_out > T_in:
T_in = T_out
else:
if T_out < T_in:
T_out = T_in
self.add_heat_utility(duty, T_in, T_out,
heat_transfer_efficiency=self.heat_transfer_efficiency,
hxn_ok=True)
super()._design()
class HXutilities(Unit):
auxiliary_unit_names = ('heat_exchangers',)
line = 'Heat exchanger'
_graphics = utility_heat_exchanger_graphics
_N_ins = _N_outs = 1
_init = HXutility._init
_run = HXutility._run
def _design(self):
feed = self.ins[0]
product = self.outs[0]
T_in = feed.T
T_out = product.T
H_out = product.H
H_in = feed.H
total_duty = H_out - H_in
self.heat_exchangers = []
if total_duty == 0: return
heating = total_duty > 0
agents = bst.settings.heating_agents if heating else bst.settings.cooling_agents
intermediate = feed
for i in agents:
T_intermediate = i.T - 10 if heating else i.T + 10
if T_in < T_intermediate if heating else T_in > T_intermediate:
T_in = T_intermediate
if T_in < T_out if heating else T_in > T_out:
hx = self.auxiliary(
'heat_exchangers', bst.HXutility,
ins=intermediate, T=T_intermediate,
rigorous=True
)
intermediate = hx.outs[0]
hx.simulate()
elif T_out <= T_in if heating else T_out > T_in:
hx = self.auxiliary(
'heat_exchangers', bst.HXutility,
ins=intermediate, outs=product, T=T_out,
rigorous=True
)
hx.simulate()
break
[docs]
class HXprocess(HX):
"""
Counter current heat exchanger for process fluids. Rigorously transfers
heat until the pinch temperature or a user set temperature limit is reached.
Parameters
----------
ins :
* [0] Inlet process fluid a
* [1] Inlet process fluid b
outs :
* [0] Outlet process fluid a
* [1] Outlet process fluid b
U=None : float, optional
Enforced overall heat transfer coefficent [kW/m^2/K].
dT=5. : float
Pinch temperature difference (i.e. dT = abs(outs[0].T - outs[1].T)).
T_lim0 : float, optional
Temperature limit of outlet stream at index 0.
T_lim1 : float, optional
Temperature limit of outlet stream at index 1.
heat_exchanger_type : str, optional
Heat exchanger type. Defaults to 'Floating head'.
N_shells=2 : int, optional
Number of shells.
ft=None : float, optional
User enforced correction factor.
phase0=None : 'l' or 'g', optional
User enforced phase of outlet stream at index 0.
phase1=None : 'l' or 'g', optional
User enforced phase of outlet stream at index 1.
H_lim0 : float, optional
Enthalpy limit of outlet stream at index 0.
H_lim1 : float, optional
Enthalpy limit of outlet stream at index 1.
Examples
--------
Rigorous heat exchange until pinch temperature is reached:
>>> from biosteam.units import HXprocess
>>> from biosteam import Stream, settings
>>> settings.set_thermo(['Water', 'Ethanol'])
>>> in_a = Stream('in_a', Ethanol=50, T=351.43, phase='g')
>>> in_b = Stream('in_b', Water=200)
>>> hx = HXprocess('hx', ins=(in_a, in_b), outs=('out_a', 'out_b'))
>>> hx.simulate()
>>> hx.show(T='degC:.2g')
HXprocess: hx
ins...
[0] in_a
phase: 'g', T: 78 degC, P: 101325 Pa
flow (kmol/hr): Ethanol 50
[1] in_b
phase: 'l', T: 25 degC, P: 101325 Pa
flow (kmol/hr): Water 200
outs...
[0] out_a
phases: ('g', 'l'), T: 78 degC, P: 101325 Pa
flow (kmol/hr): (g) Ethanol 31.4
(l) Ethanol 18.6
[1] out_b
phase: 'l', T: 73 degC, P: 101325 Pa
flow (kmol/hr): Water 200
>>> hx.results()
Heat exchanger Units hx
Design Area ft^2 213
Overall heat transfer coefficient kW/m^2/K 0.5
Log-mean temperature difference K 20.4
Fouling correction factor 1
Operating pressure psi 14.7
Total tube length ft 20
Purchase cost Floating head USD 2.06e+04
Total purchase cost USD 2.06e+04
Utility cost USD/hr 0
Sensible fluids case with user enfored outlet phases
(more computationally efficient):
>>> from biosteam.units import HXprocess
>>> from biosteam import Stream, settings
>>> settings.set_thermo(['Water', 'Ethanol'])
>>> in_a = Stream('in_a', Water=200, T=350)
>>> in_b = Stream('in_b', Ethanol=200)
>>> hx = HXprocess('hx', ins=(in_a, in_b), outs=('out_a', 'out_b'),
... phase0='l', phase1='l')
>>> hx.simulate()
>>> hx.show()
HXprocess: hx
ins...
[0] in_a
phase: 'l', T: 350 K, P: 101325 Pa
flow (kmol/hr): Water 200
[1] in_b
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (kmol/hr): Ethanol 200
outs...
[0] out_a
phase: 'l', T: 303.15 K, P: 101325 Pa
flow (kmol/hr): Water 200
[1] out_b
phase: 'l', T: 328.09 K, P: 101325 Pa
flow (kmol/hr): Ethanol 200
>>> hx.results()
Heat exchanger Units hx
Design Area ft^2 369
Overall heat transfer coefficient kW/m^2/K 0.5
Log-mean temperature difference K 11.4
Fouling correction factor 1
Operating pressure psi 14.7
Total tube length ft 20
Purchase cost Floating head USD 2.23e+04
Total purchase cost USD 2.23e+04
Utility cost USD/hr 0
>>> hx.results()
Heat exchanger Units hx
Design Area ft^2 369
Overall heat transfer coefficient kW/m^2/K 0.5
Log-mean temperature difference K 11.4
Fouling correction factor 1
Operating pressure psi 14.7
Total tube length ft 20
Purchase cost Floating head USD 2.23e+04
Total purchase cost USD 2.23e+04
Utility cost USD/hr 0
"""
line = 'Heat exchanger'
_interaction = True # For system/network path
_graphics = process_heat_exchanger_graphics
_N_ins = 2
_N_outs = 2
_energy_variable = 'T'
def _update_nonlinearities(self):
"""
Update phenomenological variables for phenomena-oriented simulation.
"""
pass
def _get_energy_departure_coefficient(self, stream):
"""
tuple[object, float] Return energy departure coefficient of a stream
for phenomena-oriented simulation.
"""
return (self, stream.C)
def _create_energy_departure_equations(self):
"""
list[tuple[dict, float]] Create energy departure equations for
phenomena-oriented simulation.
"""
coeff = {self: sum([i.C for i in self.outs])}
for i in self.ins: i._update_energy_departure_coefficient(coeff)
dH = self.H_in - self.H_out
return [(coeff, dH)]
def _update_energy_variable(self, departure):
"""
Update energy variable being solved in energy departure equations for
phenomena-oriented simulation.
"""
for i in self.outs: i.T += departure
def _create_material_balance_equations(self, composition_sensitive):
fresh_inlets, process_inlets, equations = self._begin_equations(composition_sensitive)
N = self.chemicals.size
ones = np.ones(N)
for i, j in zip(self.ins, self.outs):
if i in fresh_inlets:
rhs = i.mol
if len(j) > 1:
mol_total = j.mol
for n, s in enumerate(j):
split = s.mol / mol_total
eq_outs = {s: ones}
equations.append(
(eq_outs, split * rhs)
)
else:
eq_outs = {j: ones}
equations.append(
(eq_outs, rhs)
)
elif len(i) > 1: # process inlet
N = self.chemicals.size
rhs = np.zeros(N)
if len(j) > 1:
mol_total = j.mol
for n, s in enumerate(j):
split = s.mol / mol_total
minus_split = -split
eq_outs = {}
for ix in i: eq_outs[ix] = minus_split
eq_outs[s] = ones
equations.append(
(eq_outs, rhs)
)
else:
eq_outs = {j: ones}
for ix in i: eq_outs[ix] = -ones
equations.append(
(eq_outs, rhs)
)
else: # process inlet
N = self.chemicals.size
rhs = np.zeros(N)
if len(j) > 1:
mol_total = j.mol
for n, s in enumerate(j):
split = s.mol / mol_total
minus_split = -split
eq_outs = {}
eq_outs[i] = minus_split
eq_outs[s] = ones
equations.append(
(eq_outs, rhs)
)
else:
eq_outs = {}
eq_outs = {i: -ones,
j: ones}
equations.append(
(eq_outs, rhs)
)
return equations
def _init(self,
U=None, dT=5., T_lim0=None, T_lim1=None,
material="Carbon steel/carbon steel",
heat_exchanger_type="Floating head",
N_shells=2, ft=None,
phase0=None,
phase1=None,
H_lim0=None,
H_lim1=None,
inner_fluid_pressure_drop=None,
outer_fluid_pressure_drop=None,
neglect_pressure_drop=True,
):
#: [float] Enforced overall heat transfer coefficent (kW/m^2/K)
self.U = U
#: [float] Total heat transfered in kJ/hr (not including losses).
self.total_heat_transfer = None
#: Number of shells for LMTD correction factor method.
self.N_shells = N_shells
#: User imposed correction factor.
self.ft = ft
#: [float] Pinch temperature difference.
self.dT = dT
#: [float] Temperature limit of outlet stream at index 0.
self.T_lim0 = T_lim0
#: [float] Temperature limit of outlet stream at index 1.
self.T_lim1 = T_lim1
#: [float] Enthalpy limit of outlet stream at index 0.
self.H_lim0 = H_lim0
#: [float] Enthalpy limit of outlet stream at index 1.
self.H_lim1 = H_lim1
#: [str] Enforced phase of outlet at index 0
self.phase0 = phase0
#: [str] Enforced phase of outlet at index 1
self.phase1 = phase1
self.material = material
self.heat_exchanger_type = heat_exchanger_type
self.reset_streams_at_setup = False
#: Optional[float] Pressure drop along the inner fluid.
self.inner_fluid_pressure_drop = inner_fluid_pressure_drop
#: Optional[float] Pressure drop along the outer fluid.
self.outer_fluid_pressure_drop = outer_fluid_pressure_drop
#: [bool] Whether to assume a negligible pressure drop.
self.neglect_pressure_drop = neglect_pressure_drop
def get_streams(self):
s_in_a, s_in_b = self.ins
s_out_a, s_out_b = self.outs
return s_in_a, s_in_b, s_out_a, s_out_b
@property
def Q(self):
# Alias for backwards compatibility, not documented to avoid users
# from depending on it.
return self.total_heat_transfer
@Q.setter
def Q(self, Q):
self.total_heat_transfer = Q
def _setup(self):
super()._setup()
if self.reset_streams_at_setup:
for i in self._ins:
if i.source:
i.empty()
def _run(self):
s1_in, s2_in = self._ins
s1_out, s2_out = self._outs
if s1_in.isempty():
s1_out.empty()
s2_out.copy_like(s2_in)
self.total_heat_transfer = 0.
elif s2_in.isempty():
s2_out.empty()
s1_out.copy_like(s1_in)
self.total_heat_transfer = 0.
else:
s1_out.copy_like(s1_in)
s2_out.copy_like(s2_in)
self._run_counter_current_heat_exchange()
for s_out in (s1_out, s2_out):
if isinstance(s_out, bst.MultiStream):
phase = s_out.phase
if len(phase) == 1:
s_out.phase = phase
def _run_counter_current_heat_exchange(self):
#: TODO: Implement pressure drop
self.total_heat_transfer = ht.counter_current_heat_exchange(
*self._ins, *self._outs, self.dT, self.T_lim0, self.T_lim1,
self.phase0, self.phase1, self.H_lim0, self.H_lim1
)