Coverage for backend/idaes_service/solver/custom/custom_heat_exchanger.py: 79%

74 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-11-06 23:27 +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) 

15from pyomo.common.config import ConfigBlock, ConfigValue, In 

16 

17# Import IDAES cores 

18from idaes.core import ( 

19 declare_process_block_class, 

20 UnitModelBlockData, 

21) 

22 

23import idaes.logger as idaeslog 

24from idaes.core.util.functions import functions_lib 

25from idaes.core.util.tables import create_stream_table_dataframe 

26from idaes.models.unit_models.heater import ( 

27 _make_heater_config_block, 

28 _make_heater_control_volume, 

29) 

30 

31from idaes.core.util.misc import add_object_reference 

32from idaes.core.util import scaling as iscale 

33from idaes.core.solvers import get_solver 

34from idaes.core.util.exceptions import ConfigurationError, InitializationError 

35from idaes.core.initialization import SingleControlVolumeUnitInitializer 

36from idaes.models.unit_models.heat_exchanger import HX0DInitializer, _make_heat_exchanger_config, HeatExchangerData 

37from .inverted import add_inverted, initialise_inverted 

38_log = idaeslog.getLogger(__name__) 

39 

40 

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

42class CustomHeatExchangerData(HeatExchangerData): 

43 

44 def build(self,*args,**kwargs) -> None: 

45 """ 

46 Begin building model. 

47 """ 

48 super().build(*args,**kwargs) 

49 # Add an inverted DeltaP 

50 add_inverted(self.hot_side, "deltaP") 

51 add_inverted(self.cold_side, "deltaP") 

52 

53 def initialize_build( 

54 self, 

55 state_args_1=None, 

56 state_args_2=None, 

57 outlvl=idaeslog.NOTSET, 

58 solver=None, 

59 optarg=None, 

60 duty=None, 

61 ): 

62 """ 

63 Heat exchanger initialization method. 

64 

65 Args: 

66 state_args_1 : a dict of arguments to be passed to the property 

67 initialization for the hot side (see documentation of the specific 

68 property package) (default = {}). 

69 state_args_2 : a dict of arguments to be passed to the property 

70 initialization for the cold side (see documentation of the specific 

71 property package) (default = {}). 

72 outlvl : sets output level of initialization routine 

73 optarg : solver options dictionary object (default=None, use 

74 default solver options) 

75 solver : str indicating which solver to use during 

76 initialization (default = None, use default solver) 

77 duty : an initial guess for the amount of heat transferred. This 

78 should be a tuple in the form (value, units), (default 

79 = (1000 J/s)) 

80 

81 Returns: 

82 None 

83 

84 """ 

85 # So, when solving with a correct area, there can be problems 

86 # That's because if the area's even slightly too large, it becomes infeasible 

87 if not self.area.fixed: 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true

88 self.area.value = self.area.value * 0.8 

89 

90 initialise_inverted(self.hot_side, "deltaP") 

91 initialise_inverted(self.cold_side, "deltaP") 

92 

93 # Set solver options 

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

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

96 

97 # Create solver 

98 opt = get_solver(solver, optarg) 

99 

100 flags1 = self.hot_side.initialize( 

101 outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_1 

102 ) 

103 

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

105 

106 flags2 = self.cold_side.initialize( 

107 outlvl=outlvl, optarg=optarg, solver=solver, state_args=state_args_2 

108 ) 

109 init_log.info_high("Initialization Step 1b (cold side) Complete.") 

110 # --------------------------------------------------------------------- 

111 # Solve unit without heat transfer equation 

112 self.heat_transfer_equation.deactivate() 

113 if hasattr( self.cold_side.properties_out[0], "constraints"): 113 ↛ 115line 113 didn't jump to line 115 because the condition on line 113 was always true

114 self.cold_side.properties_out[0].constraints.deactivate() 

115 if hasattr( self.hot_side.properties_out[0], "constraints"): 115 ↛ 119line 115 didn't jump to line 119 because the condition on line 115 was always true

116 self.hot_side.properties_out[0].constraints.deactivate() 

117 

118 # Get side 1 and side 2 heat units, and convert duty as needed 

119 s1_units = self.hot_side.heat.get_units() 

120 s2_units = self.cold_side.heat.get_units() 

121 

122 # Check to see if heat duty is fixed 

123 # WE will assume that if the first point is fixed, it is fixed at all points 

124 if not self.cold_side.heat[self.flowsheet().time.first()].fixed: 124 ↛ 152line 124 didn't jump to line 152 because the condition on line 124 was always true

125 cs_fixed = False 

126 if duty is None: 126 ↛ 141line 126 didn't jump to line 141 because the condition on line 126 was always true

127 # Assume 1000 J/s and check for unitless properties 

128 if s1_units is None and s2_units is None: 128 ↛ 130line 128 didn't jump to line 130 because the condition on line 128 was never true

129 # Backwards compatibility for unitless properties 

130 s1_duty = -1000 

131 s2_duty = 1000 

132 else: 

133 s1_duty = pyunits.convert_value( 

134 -1000, from_units=pyunits.W, to_units=s1_units 

135 ) 

136 s2_duty = pyunits.convert_value( 

137 1000, from_units=pyunits.W, to_units=s2_units 

138 ) 

139 else: 

140 # Duty provided with explicit units 

141 s1_duty = -pyunits.convert_value( 

142 duty[0], from_units=duty[1], to_units=s1_units 

143 ) 

144 s2_duty = pyunits.convert_value( 

145 duty[0], from_units=duty[1], to_units=s2_units 

146 ) 

147 

148 self.cold_side.heat.fix(s2_duty) 

149 for i in self.hot_side.heat: 

150 self.hot_side.heat[i].value = s1_duty 

151 else: 

152 cs_fixed = True 

153 for i in self.hot_side.heat: 

154 self.hot_side.heat[i].set_value(self.cold_side.heat[i]) 

155 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: 

156 res = opt.solve(self, tee=slc.tee) 

157 init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res))) 

158 if not cs_fixed: 158 ↛ 160line 158 didn't jump to line 160 because the condition on line 158 was always true

159 self.cold_side.heat.unfix() 

160 if hasattr( self.cold_side.properties_out[0], "constraints"): 160 ↛ 162line 160 didn't jump to line 162 because the condition on line 160 was always true

161 self.cold_side.properties_out[0].constraints.activate() 

162 if hasattr( self.hot_side.properties_out[0], "constraints"): 162 ↛ 164line 162 didn't jump to line 164 because the condition on line 162 was always true

163 self.hot_side.properties_out[0].constraints.activate() 

164 self.heat_transfer_equation.activate() 

165 

166 # --------------------------------------------------------------------- 

167 # Solve unit 

168 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: 

169 res = opt.solve(self, tee=slc.tee) 

170 init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res))) 

171 # --------------------------------------------------------------------- 

172 

173 # Release Inlet state 

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

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

176 

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

178 

179 if not check_optimal_termination(res): 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true

180 raise InitializationError( 

181 f"{self.name} failed to initialize successfully. Please check " 

182 f"the output logs for more information." 

183 )