Coverage for backend/pinch_service/OpenPinch/src/classes/stream.py: 75%

178 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-11-06 23:27 +0000

1from typing import Optional 

2from ..lib.enums import * 

3from .value import Value 

4 

5 

6class Stream(): 

7 """Class representing a generic stream (process or utility).""" 

8 

9 def __init__( 

10 self, 

11 name: str = "Stream", 

12 t_supply: Optional[float] = None, 

13 t_target: Optional[float] = None, 

14 dt_cont: float = 0.0, 

15 heat_flow: float = 0.0, 

16 htc: float = 1.0, 

17 price: float = 0.0, 

18 is_process_stream: bool = True, 

19 ): 

20 self._name: str = name 

21 self._type: str = None 

22 self._t_supply: Optional[float] = t_supply 

23 self._t_target: Optional[float] = t_target 

24 self._dt_cont: float = dt_cont 

25 self._heat_flow: float = heat_flow 

26 self._htc: float = htc if htc != 0.0 else 1.0 

27 self._htr: float = 1 / self._htc 

28 self._price: float = price 

29 self._is_process_stream: bool = is_process_stream 

30 self._active = True 

31 self._update_attributes() 

32 

33 # === Core Properties === 

34 

35 @property 

36 def name(self) -> str: 

37 """Stream name.""" 

38 return self._name 

39 @name.setter 

40 def name(self, value: str): 

41 self._name = value 

42 

43 @property 

44 def is_process_stream(self) -> bool: 

45 """Process or utility stream.""" 

46 return self._is_process_stream 

47 @is_process_stream.setter 

48 def is_process_stream(self, value: bool): 

49 self._is_process_stream = value 

50 

51 @property 

52 def type(self) -> Optional[str]: 

53 """Stream type (Hot, Cold, Both).""" 

54 return self._type 

55 @type.setter 

56 def type(self, value: str): 

57 self._type = value 

58 

59 @property 

60 def t_supply(self) -> Optional[float]: 

61 """Supply temperature.""" 

62 return self._t_supply 

63 @t_supply.setter 

64 def t_supply(self, value: float): 

65 self._t_supply = value 

66 self._update_attributes() 

67 

68 @property 

69 def t_target(self) -> Optional[float]: 

70 """Target temperature.""" 

71 return self._t_target 

72 @t_target.setter 

73 def t_target(self, value: float): 

74 self._t_target = value 

75 self._update_attributes() 

76 

77 @property 

78 def dt_cont(self) -> float: 

79 """Delta T minimum (approach temperature).""" 

80 return self._dt_cont 

81 @dt_cont.setter 

82 def dt_cont(self, value: float): 

83 self._dt_cont = value 

84 self._update_attributes() 

85 

86 @property 

87 def heat_flow(self) -> float: 

88 """Stream heat flow (kW).""" 

89 return self._heat_flow 

90 @heat_flow.setter 

91 def heat_flow(self, value: float): 

92 self._heat_flow = value 

93 self._update_attributes() 

94 

95 @property 

96 def htc(self) -> float: 

97 """Heat transfer coefficient.""" 

98 return self._htc 

99 @htc.setter 

100 def htc(self, value: float): 

101 self._htc = value 

102 self._update_attributes() 

103 

104 @property 

105 def htr(self) -> float: 

106 """Heat transfer coefficient.""" 

107 return self._htr 

108 @htr.setter 

109 def htr(self, value: float): 

110 self._htr = value 

111 

112 @property 

113 def price(self) -> float: 

114 """Unit energy price ($/MWh or similar).""" 

115 return self._price 

116 @price.setter 

117 def price(self, value: float): 

118 self._price = value 

119 

120 @property 

121 def ut_cost(self) -> float: 

122 """Utility cost contribution (if relevant).""" 

123 return self._ut_cost 

124 @ut_cost.setter 

125 def ut_cost(self, value: float): 

126 self._ut_cost = value 

127 

128 @property 

129 def CP(self) -> float: 

130 """Heat capacity flowrate (kW/K).""" 

131 return self._CP 

132 @CP.setter 

133 def CP(self, value: float): 

134 self._CP = value 

135 

136 @property 

137 def rCP(self) -> Optional[float]: 

138 """Resistance-capacity product (1/heat transfer rate).""" 

139 return self._RCP_prod 

140 @rCP.setter 

141 def rCP(self, value: float): 

142 self._RCP_prod = value 

143 

144 @property 

145 def active(self) -> bool: 

146 """Whether the stream is active in analysis.""" 

147 return self._active 

148 @active.setter 

149 def active(self, value: bool): 

150 self._active = value 

151 

152 # === Computed Temperature Bounds === 

153 

154 @property 

155 def t_min(self) -> Optional[float]: 

156 """Minimum temperature (supply or target depending on hot/cold).""" 

157 return self._t_min 

158 @t_min.setter 

159 def t_min(self, value: float): 

160 self._t_min = value 

161 

162 @property 

163 def t_max(self) -> Optional[float]: 

164 """Maximum temperature (supply or target depending on hot/cold).""" 

165 return self._t_max 

166 @t_max.setter 

167 def t_max(self, value: float): 

168 self._t_max = value 

169 

170 @property 

171 def t_min_star(self) -> Optional[float]: 

172 """Adjusted minimum temperature (accounting for DTmin).""" 

173 return self._t_min_star 

174 @t_min_star.setter 

175 def t_min_star(self, value: float): 

176 self._t_min_star = value 

177 

178 @property 

179 def t_max_star(self) -> Optional[float]: 

180 """Adjusted maximum temperature (accounting for DTmin).""" 

181 return self._t_max_star 

182 @t_max_star.setter 

183 def t_max_star(self, value: float): 

184 self._t_max_star = value 

185 

186 

187 # === Methods === 

188 

189 def _update_attributes(self) -> None: 

190 """Calculates key stream attributes based on temperatures.""" 

191 if self._t_supply is None or self._t_target is None or self._htc is None: 191 ↛ 192line 191 didn't jump to line 192 because the condition on line 191 was never true

192 return 

193 

194 if self._t_supply > self._t_target: 

195 # Hot stream 

196 self._set_hot_stream_min_max_temperatures() 

197 elif self._t_supply < self._t_target: 197 ↛ 201line 197 didn't jump to line 201 because the condition on line 197 was always true

198 # Cold stream 

199 self._set_cold_stream_min_max_temperatures() 

200 else: 

201 if isinstance(self._heat_flow, float | int): 

202 if self._heat_flow > 0.0: 

203 # Cold stream 

204 self._t_target = self._t_supply + 0.01 

205 self._set_cold_stream_min_max_temperatures() 

206 elif self._heat_flow < 0.0: 

207 # Hot stream 

208 self._t_target = self._t_supply - 0.01 

209 self._set_hot_stream_min_max_temperatures() 

210 

211 if isinstance(self._heat_flow, float | int): 211 ↛ 213line 211 didn't jump to line 213 because the condition on line 211 was always true

212 self._CP = self._heat_flow / (self._t_max - self._t_min) 

213 elif isinstance(self._CP, float | int): 

214 self._heat_flow = self._CP * (self._t_max - self._t_min) 

215 

216 self._calc_utility_cost() 

217 self._calc_htr_and_cp_product() 

218 

219 

220 def set_heat_flow(self, value: float, units: str = "kW") -> None: 

221 """Sets the heat flow and updates CP and utility cost.""" 

222 self._heat_flow = value 

223 self._calc_utility_cost() 

224 if self._t_supply is not None and self._t_target is not None and abs(self._t_supply - self._t_target) > 0: 224 ↛ exitline 224 didn't return from function 'set_heat_flow' because the condition on line 224 was always true

225 self._CP = value / abs(self._t_supply - self._t_target) 

226 self._RCP_prod = self._htr * self._CP 

227 

228 

229 def _calc_utility_cost(self): 

230 if isinstance(self._heat_flow, float | int) and isinstance(self._price, float | int): 230 ↛ exitline 230 didn't return from function '_calc_utility_cost' because the condition on line 230 was always true

231 self._ut_cost = (self._heat_flow / 1000) * self._price 

232 

233 

234 def _calc_htr_and_cp_product(self): 

235 if isinstance(self._heat_flow, float | int) and isinstance(self._price, float | int): 235 ↛ exitline 235 didn't return from function '_calc_htr_and_cp_product' because the condition on line 235 was always true

236 if self._htc != 0.0: 236 ↛ exitline 236 didn't return from function '_calc_htr_and_cp_product' because the condition on line 236 was always true

237 self._htr = 1 / self._htc 

238 self._RCP_prod = self._CP * self._htr if self._htc > 0.0 else 0.0 

239 

240 

241 def _set_hot_stream_min_max_temperatures(self): 

242 self._t_min = self._t_target 

243 self._t_max = self._t_supply 

244 self._t_min_star = self._t_min - self._dt_cont 

245 self._t_max_star = self._t_max - self._dt_cont 

246 if self._type is None: 

247 self._type = StreamType.Hot.value 

248 

249 

250 def _set_cold_stream_min_max_temperatures(self): 

251 self._t_min = self._t_supply 

252 self._t_max = self._t_target 

253 self._t_min_star = self._t_min + self._dt_cont 

254 self._t_max_star = self._t_max + self._dt_cont 

255 if self._type is None: 

256 self._type = StreamType.Cold.value