Coverage for backend/ahuora-builder/src/ahuora_builder/custom/custom_heat_exchanger.py: 74%
96 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
3# Import Pyomo libraries
4from pyomo.environ import (
5 Block,
6 Var,
7 Param,
8 log,
9 Reference,
10 PositiveReals,
11 ExternalFunction,
12 units as pyunits,
13 check_optimal_termination,
14 value,
15)
16from pyomo.common.config import ConfigBlock, ConfigValue, In
18# Import IDAES cores
19from idaes.core import (
20 declare_process_block_class,
21 UnitModelBlockData,
22)
24import idaes.logger as idaeslog
25from idaes.core.util.functions import functions_lib
26from idaes.core.util.tables import create_stream_table_dataframe
27from idaes.models.unit_models.heater import (
28 _make_heater_config_block,
29 _make_heater_control_volume,
30)
32from idaes.core.util.misc import add_object_reference
33from idaes.core.util import scaling as iscale
34from idaes.core.solvers import get_solver
35from idaes.core.util.exceptions import ConfigurationError, InitializationError
36from idaes.core.initialization import SingleControlVolumeUnitInitializer
37from idaes.models.unit_models.heat_exchanger import HX0DInitializer, _make_heat_exchanger_config, HeatExchangerData, delta_temperature_underwood_callback
38from .inverted import add_inverted, initialise_inverted
39_log = idaeslog.getLogger(__name__)
42@declare_process_block_class("CustomHeatExchanger", doc="Simple 0D heat exchanger model.")
43class CustomHeatExchangerData(HeatExchangerData):
45 CONFIG = HeatExchangerData.CONFIG()
46 CONFIG.pop("delta_temperature_callback")
47 CONFIG.declare(
48 "delta_temperature_callback",
49 ConfigValue(
50 default=delta_temperature_underwood_callback,
51 description="Callback for for temperature difference calculations",
52 ),
53 )
55 def build(self,*args,**kwargs) -> None:
56 """
57 Begin building model.
58 """
59 super().build(*args,**kwargs)
60 # Add an inverted DeltaP
61 add_inverted(self.hot_side, "deltaP")
62 add_inverted(self.cold_side, "deltaP")
64 def initialize_build(
65 self,
66 state_args_1=None,
67 state_args_2=None,
68 outlvl=idaeslog.NOTSET,
69 solver=None,
70 optarg=None,
71 duty=None,
72 ):
73 """
74 Heat exchanger initialization method.
76 Args:
77 state_args_1 : a dict of arguments to be passed to the property
78 initialization for the hot side (see documentation of the specific
79 property package) (default = {}).
80 state_args_2 : a dict of arguments to be passed to the property
81 initialization for the cold side (see documentation of the specific
82 property package) (default = {}).
83 outlvl : sets output level of initialization routine
84 optarg : solver options dictionary object (default=None, use
85 default solver options)
86 solver : str indicating which solver to use during
87 initialization (default = None, use default solver)
88 duty : an initial guess for the amount of heat transferred. This
89 should be a tuple in the form (value, units), (default
90 = (1000 J/s))
92 Returns:
93 None
95 """
96 # So, when solving with a correct area, there can be problems
97 # That's because if the area's even slightly too large, it becomes infeasible
98 if not self.area.fixed:
99 self.area.value = self.area.value * 0.8
101 initialise_inverted(self.hot_side, "deltaP")
102 initialise_inverted(self.cold_side, "deltaP")
104 # Set solver options
105 init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
106 solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")
108 # Create solver
109 opt = get_solver(solver, optarg)
111 flags1 = self.hot_side.initialize(
112 outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_1
113 )
115 init_log.info_high("Initialization Step 1a (hot side) Complete.")
117 flags2 = self.cold_side.initialize(
118 outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_2
119 )
120 init_log.info_high("Initialization Step 1b (cold side) Complete.")
121 # ---------------------------------------------------------------------
122 # Solve unit without heat transfer equation
123 self.heat_transfer_equation.deactivate()
124 if hasattr( self.cold_side.properties_out[0], "_deactivate_additional_constraints"): 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true
125 self.cold_side.properties_out[0]._deactivate_additional_constraints()
126 if hasattr( self.hot_side.properties_out[0], "_deactivate_additional_constraints"): 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true
127 self.hot_side.properties_out[0]._deactivate_additional_constraints()
129 # Get side 1 and side 2 heat units, and convert duty as needed
130 s1_units = self.hot_side.heat.get_units()
131 s2_units = self.cold_side.heat.get_units()
133 # Check to see if heat duty is fixed
134 # WE will assume that if the first point is fixed, it is fixed at all points
135 if not self.cold_side.heat[self.flowsheet().time.first()].fixed: 135 ↛ 163line 135 didn't jump to line 163 because the condition on line 135 was always true
136 cs_fixed = False
137 if duty is None: 137 ↛ 152line 137 didn't jump to line 152 because the condition on line 137 was always true
138 # Assume 1000 J/s and check for unitless properties
139 if s1_units is None and s2_units is None: 139 ↛ 141line 139 didn't jump to line 141 because the condition on line 139 was never true
140 # Backwards compatibility for unitless properties
141 s1_duty = -1000
142 s2_duty = 1000
143 else:
144 s1_duty = pyunits.convert_value(
145 -1000, from_units=pyunits.W, to_units=s1_units
146 )
147 s2_duty = pyunits.convert_value(
148 1000, from_units=pyunits.W, to_units=s2_units
149 )
150 else:
151 # Duty provided with explicit units
152 s1_duty = -pyunits.convert_value(
153 duty[0], from_units=duty[1], to_units=s1_units
154 )
155 s2_duty = pyunits.convert_value(
156 duty[0], from_units=duty[1], to_units=s2_units
157 )
159 self.cold_side.heat.fix(s2_duty)
160 for i in self.hot_side.heat:
161 self.hot_side.heat[i].value = s1_duty
162 else:
163 cs_fixed = True
164 for i in self.hot_side.heat:
165 self.hot_side.heat[i].set_value(self.cold_side.heat[i])
166 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
167 res = opt.solve(self, tee=slc.tee)
168 init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res)))
169 if not cs_fixed: 169 ↛ 171line 169 didn't jump to line 171 because the condition on line 169 was always true
170 self.cold_side.heat.unfix()
171 if hasattr( self.cold_side.properties_out[0], "_reactivate_additional_constraints"): 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true
172 self.cold_side.properties_out[0]._reactivate_additional_constraints()
173 if hasattr( self.hot_side.properties_out[0], "_reactivate_additional_constraints"): 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true
174 self.hot_side.properties_out[0]._reactivate_additional_constraints()
175 self.heat_transfer_equation.activate()
177 # ---------------------------------------------------------------------
178 # Solve unit
179 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
180 res = opt.solve(self, tee=slc.tee)
181 init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res)))
182 # ---------------------------------------------------------------------
184 # Release Inlet state
185 self.hot_side.release_state(flags1, outlvl=outlvl)
186 self.cold_side.release_state(flags2, outlvl=outlvl)
188 init_log.info("Initialization Completed, {}".format(idaeslog.condition(res)))
190 if not check_optimal_termination(res):
191 raise InitializationError(
192 f"{self.name} failed to initialize successfully. Please check "
193 f"the output logs for more information."
194 )
197 def diagnose(self) -> list[tuple[Component, str]]:
198 """
199 Test a few common issues with the heat exchanger model and provide hints to the user.
200 returns a list with the variable the it is most relevant to and a message describing the issue
201 """
202 # if flow rates are drastically different and enthalpy rates are drastically different,
203 # this might be a problem.
204 problems = []
205 mass_flow_difference = value(self.hot_side.properties_in[0].flow_mass)/value(self.cold_side.properties_in[0].flow_mass)
206 if mass_flow_difference > 100: 206 ↛ 213line 206 didn't jump to line 213 because the condition on line 206 was always true
207 problems.append(
208 (
209 self.hot_side.properties_in[0].flow_mass,
210 f"Mass flow rate on hot side is {mass_flow_difference:.2f} times higher than cold side. This may cause convergence issues. Consider adjusting the model or providing better initial guesses.",
211 )
212 )
213 elif mass_flow_difference < 0.01:
214 problems.append(
215 (
216 self.cold_side.properties_in[0].flow_mass,
217 f"Mass flow rate on cold side is {1/mass_flow_difference:.2f} times higher than hot side. This may cause convergence issues. Consider adjusting the model or providing better initial guesses.",
218 )
219 )
220 hsi_temp = value(self.hot_side.properties_in[0].temperature) or 0
221 csi_temp = value(self.cold_side.properties_in[0].temperature) or 0
222 hso_temp = value(self.hot_side.properties_out[0].temperature) or 0
223 cso_temp = value(self.cold_side.properties_out[0].temperature) or 0
224 if hsi_temp < csi_temp: 224 ↛ 226line 224 didn't jump to line 226 because the condition on line 224 was never true
225 # switch hot and cold side temps so hot side is always hotter.
226 hsi_temp, csi_temp = csi_temp, hsi_temp
227 hso_temp, cso_temp = cso_temp, hso_temp
229 if hsi_temp - cso_temp < 1e-1: 229 ↛ 236line 229 didn't jump to line 236 because the condition on line 229 was always true
230 problems.append(
231 (
232 self.overall_heat_transfer_coefficient,
233 f"The heat exchanger has used all the heat available in the hot side. Temperature difference between HS inlet and CS outlet is ({hsi_temp - cso_temp:.2f} K). There needs to be a temperature difference to drive heat transfer. Perhaps there is insufficient energy in one of the streams.",
234 )
235 )
236 elif hso_temp - csi_temp < 1e-1:
237 problems.append(
238 (
239 self.overall_heat_transfer_coefficient,
240 f"The heat exchanger has used all the cooling available in the cold side. Temperature difference between HS outlet and CS inlet is ({hso_temp - csi_temp:.2f} K). There needs to be a temperature difference to drive heat transfer. Perhaps there is insufficient cooling in the cold side or too much heat in the hot side.",
241 )
242 )
243 return problems