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

1 

2 

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 

17 

18# Import IDAES cores 

19from idaes.core import ( 

20 declare_process_block_class, 

21 UnitModelBlockData, 

22) 

23 

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) 

31 

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__) 

40 

41 

42@declare_process_block_class("CustomHeatExchanger", doc="Simple 0D heat exchanger model.") 

43class CustomHeatExchangerData(HeatExchangerData): 

44 

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 ) 

54 

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") 

63 

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. 

75 

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)) 

91 

92 Returns: 

93 None 

94 

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 

100 

101 initialise_inverted(self.hot_side, "deltaP") 

102 initialise_inverted(self.cold_side, "deltaP") 

103 

104 # Set solver options 

105 init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") 

106 solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") 

107 

108 # Create solver 

109 opt = get_solver(solver, optarg) 

110 

111 flags1 = self.hot_side.initialize( 

112 outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_1 

113 ) 

114 

115 init_log.info_high("Initialization Step 1a (hot side) Complete.") 

116 

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() 

128 

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() 

132 

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 ) 

158 

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() 

176 

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 # --------------------------------------------------------------------- 

183 

184 # Release Inlet state 

185 self.hot_side.release_state(flags1, outlvl=outlvl) 

186 self.cold_side.release_state(flags2, outlvl=outlvl) 

187 

188 init_log.info("Initialization Completed, {}".format(idaeslog.condition(res))) 

189 

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 ) 

195 

196 

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 

228 

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