Coverage for pyrc\postprocessing\heat.py: 13%

150 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-08-03 16:07 +0200

1# ------------------------------------------------------------------------------- 

2# Copyright (C) 2026 Joel Kimmich, Tim Jourdan 

3# ------------------------------------------------------------------------------ 

4# License 

5# This file is part of PyRC, distributed under GPL-3.0-or-later. 

6# ------------------------------------------------------------------------------ 

7from __future__ import annotations 

8 

9from datetime import datetime 

10from typing import TYPE_CHECKING 

11 

12import numpy as np 

13from matplotlib import pyplot as plt 

14 

15from pyrc.core.components.templates import solution_object 

16from pyrc.tools.functions import seconds_to_dates 

17from leanplot import TimePlot 

18 

19if TYPE_CHECKING: 

20 from pyrc.core.nodes import Node, MassFlowNode, ChannelNode 

21 from pyrc.core.components.capacitor import Capacitor 

22 from pyrc.core.components.resistor import Resistor 

23 from pyrc.core.inputs import BoundaryCondition, InternalHeatSource 

24 from pyrc.core.components.templates import RCSolution 

25 

26 

27def get_accumulated_heat_flux(node: Capacitor): 

28 """ 

29 Sums up the heat flux of all connected resistors. 

30 

31 Used to get the heat flux that is brought in through `BoundaryCondition` s. 

32 

33 Parameters 

34 ---------- 

35 node : NoteTemplate 

36 The Node of which the heat flux is being calculated. 

37 For this, all connected resistors are summed up. 

38 

39 Returns 

40 ------- 

41 float : 

42 The heat flux of all connected resistors. 

43 """ 

44 resistors: Resistor = node.neighbours 

45 seen_nodes = set() 

46 resistors_without_parallel = [] 

47 for resistor in resistors: 

48 connected_node = resistor.get_connected_node(node) 

49 if connected_node not in seen_nodes: 

50 seen_nodes.add(connected_node) 

51 resistors_without_parallel.append(resistor) 

52 

53 heat_flux = [r.heat_flux(node) for r in resistors_without_parallel] 

54 

55 return sum(heat_flux) 

56 

57 

58class HeatFlux: 

59 def __init__(self, rc_solution: RCSolution = solution_object): 

60 self.rc_solution = rc_solution 

61 

62 def parse_time_step_index(self, time_step_index): 

63 if time_step_index is None: 

64 time_step_index = list(range(len(self.rc_solution.time_steps))) 

65 elif ( 

66 not isinstance(time_step_index, list) 

67 and not isinstance(time_step_index, slice) 

68 and not isinstance(time_step_index, np.ndarray) 

69 ): 

70 time_step_index = [time_step_index] 

71 return time_step_index 

72 

73 def boundary(self, boundary: BoundaryCondition, time_step_index=None): 

74 resistors: Resistor = boundary.neighbours 

75 

76 time_step_index = self.parse_time_step_index(time_step_index) 

77 

78 bc_temp = self.rc_solution.input_vectors[time_step_index, boundary.index] 

79 heat_flux_sum = np.zeros_like(bc_temp) 

80 

81 for resistor in resistors: 

82 other_node_index = resistor.get_connected_node(boundary).index 

83 node_temp = self.rc_solution.result_vectors[time_step_index, other_node_index] 

84 heat_flux_sum += 1 / resistor.resistance * (bc_temp - node_temp) 

85 return self.rc_solution.time_steps[time_step_index], heat_flux_sum 

86 

87 def preheat(self, distributor: MassFlowNode, collector: MassFlowNode): 

88 """ 

89 Returns the preheat in Kelvin. 

90 

91 Returns 

92 ------- 

93 np.ndarray 

94 """ 

95 time_step_index = self.parse_time_step_index(None) 

96 temp_dist: np.ndarray = self.rc_solution.result_vectors[time_step_index, distributor.index] 

97 temp_col: np.ndarray = self.rc_solution.result_vectors[time_step_index, collector.index] 

98 return temp_col - temp_dist 

99 

100 def calculate_heat_flux(self, node, resistors: list | Resistor, time_step_index=None) -> np.ndarray | float: 

101 """ 

102 Returns the heat flux going into the node by the passed resistors. 

103 

104 Parameters 

105 ---------- 

106 node : NoteTemplate 

107 The Node of which the heat flux is being calculated. 

108 resistors : list | Resistor 

109 The resistors used to calculate the heat flow. 

110 time_step_index : int | slice | list, optional 

111 The time steps that should be calculated. 

112 If None, all are calculated. 

113 

114 Returns 

115 ------- 

116 float | np.ndarray : 

117 The heat fluxes for every time step. 

118 """ 

119 from pyrc.core.components.resistor import Resistor 

120 

121 if isinstance(resistors, Resistor): 

122 resistors = [resistors] 

123 time_step_index = self.parse_time_step_index(time_step_index) 

124 

125 result = 0 

126 node_temp = self.rc_solution.result_vectors[time_step_index, node.index] 

127 

128 from pyrc.core.components.input import Input 

129 

130 for resistor in resistors: 

131 other_node = resistor.get_connected_node(node) 

132 if isinstance(other_node, Input): 

133 result += ( 

134 1 

135 / resistor.equivalent_resistance 

136 * (self.rc_solution.input_vectors[time_step_index, other_node.index] - node_temp) 

137 ) 

138 else: 

139 result += ( 

140 1 

141 / resistor.equivalent_resistance 

142 * (self.rc_solution.result_vectors[time_step_index, other_node.index] - node_temp) 

143 ) 

144 return result 

145 

146 def internal_heat_source(self, ihc: InternalHeatSource | list, time_step_index=None): 

147 if not isinstance(ihc, list): 

148 ihc = [ihc] 

149 ihc_index = [i.index for i in ihc] 

150 time_step_index = self.parse_time_step_index(time_step_index) 

151 ihc_values = self.rc_solution.input_vectors[time_step_index, :][:, ihc_index] 

152 return self.rc_solution.time_steps[time_step_index], ihc_values 

153 

154 def heat_flux_directions( 

155 self, 

156 nodes: list[Node], 

157 directions: list | np.ndarray = np.array([0, 0, -1]), 

158 except_resistor_types=None, 

159 time_step_index=None, 

160 ) -> tuple | np.ndarray: 

161 """ 

162 Returns the heat flux through the layer in the desired direction of all nodes in the given list. 

163 

164 E.g. if the direction is (0,0,1) the heat flux in positive z direction is calculated. The heat flux is 

165 positive if going in the same direction as the desired one. 

166 

167 Parameters 

168 ---------- 

169 nodes : list[Node] 

170 The Nodes of which the heat flux is being calculated. 

171 directions : list | np.ndarray, optional 

172 The direction(s) in which the heat flux is calculated. 

173 Can be a list then each direction is calculated and returned. 

174 except_resistor_types 

175 time_step_index 

176 

177 Returns 

178 ------- 

179 np.ndarray | tuple : 

180 The result of one direction as np.ndarray or the result of all directions as tuple. 

181 """ 

182 if except_resistor_types is None: 

183 except_resistor_types = [] 

184 if isinstance(directions, np.ndarray): 

185 directions = [directions] 

186 results = [] 

187 for direction in directions: 

188 nodes_and_resistors = [ 

189 (node, node.resistors_in_direction_filtered(direction, except_resistor_types=except_resistor_types)) 

190 for node in nodes 

191 if node is not None 

192 ] 

193 

194 result = 0 

195 for node, resistors in nodes_and_resistors: 

196 result += self.calculate_heat_flux(node, resistors, time_step_index) 

197 results.append(result) 

198 

199 if len(results) > 1: 

200 return tuple(results) 

201 return results[0] 

202 

203 

204def fluxes_data( 

205 boundaries: list = None, 

206 boundaries_sum: list = None, 

207 time_step_index=None, 

208 balance: bool = False, 

209 rc_solution: RCSolution = None, 

210): 

211 if rc_solution is not None: 

212 heat_flux = HeatFlux(rc_solution=rc_solution) 

213 else: 

214 heat_flux = HeatFlux() 

215 

216 results = [] 

217 labels = [] 

218 

219 sum_vector = None 

220 time_steps = None 

221 

222 def add_to_sum(result: np.ndarray, _sum_vector): 

223 if _sum_vector is None: 

224 _sum_vector = np.zeros_like(result) 

225 return _sum_vector + result 

226 

227 if boundaries is not None: 

228 for bc in boundaries: 

229 time_steps, y = heat_flux.boundary(bc, time_step_index) 

230 results.append(y) 

231 labels.append(bc.__class__.__name__) 

232 sum_vector = add_to_sum(y, sum_vector) 

233 if boundaries_sum is not None: 

234 if not isinstance(boundaries_sum[0], list): 

235 boundaries_sum = [boundaries_sum] 

236 for i, bc_groups in enumerate(boundaries_sum): 

237 time_steps, bc_sum = heat_flux.internal_heat_source(bc_groups[0], time_step_index) 

238 for bc in bc_groups[1:]: 

239 _, bc_add_on = heat_flux.internal_heat_source(bc, time_step_index) 

240 bc_sum += bc_add_on 

241 results.append(bc_sum) 

242 labels.append(f"{bc_groups[0].__class__.__name__}s: {i}") 

243 sum_vector = add_to_sum(bc_sum.reshape(-1, 1), sum_vector.reshape(-1, 1)) 

244 if balance and time_steps is not None and sum_vector is not None: 

245 results.append(sum_vector) 

246 labels.append("Balance") 

247 return time_steps, results, labels 

248 

249 

250def plot_channel_balance( 

251 channel_nodes: list[Node] | ChannelNode, 

252 distributor: MassFlowNode, 

253 collector: MassFlowNode, 

254 time_step_index=None, 

255 start_date=datetime(2023, 1, 1), 

256 y_scale=1, 

257): 

258 """ 

259 Plots the balance of the given channel nodes. 

260 

261 Parameters 

262 ---------- 

263 channel_nodes : list[ChannelNode] | ChannelNode 

264 The channel nodes to plot the sum of. 

265 distributor : MassFlowNode 

266 The distributor before the channel nodes. 

267 Used to calculate the heat flux that goes into the mass flow within the channel nodes. 

268 collector : MassFlowNode 

269 The collector after the channel nodes. 

270 Used to calculate the heat flux that goes into the mass flow within the channel nodes. 

271 time_step_index : int | slice | list, optional 

272 The time steps that should be calculated. 

273 

274 """ 

275 from pyrc.core.nodes import ChannelNode 

276 

277 if isinstance(channel_nodes, ChannelNode): 

278 channel_nodes = [channel_nodes] 

279 heat_flux = HeatFlux() 

280 time_step_index = heat_flux.parse_time_step_index(time_step_index) 

281 exterior_direction = np.array([0, 0, -1]) 

282 interior_direction = np.array([0, 0, 1]) 

283 upper_direction = np.array([0, -1, 0]) 

284 lower_direction = np.array([0, 1, 0]) 

285 ex, interior, up, lo = heat_flux.heat_flux_directions( 

286 channel_nodes, 

287 directions=[exterior_direction, interior_direction, upper_direction, lower_direction], 

288 time_step_index=time_step_index, 

289 ) 

290 # side_heat_flux_dist = heat_flux.heat_flux_directions([distributor], 

291 # directions=[np.array([1, 0, 0])], 

292 # time_step_index=time_step_index, 

293 # except_resistor_types=[MassTransport] 

294 # ) 

295 # side_heat_flux_col = heat_flux.heat_flux_directions([collector], 

296 # directions=[np.array([-1, 0, 0])], 

297 # time_step_index=time_step_index, 

298 # except_resistor_types=[MassTransport] 

299 # ) 

300 temp_dist = heat_flux.rc_solution.result_vectors[time_step_index, distributor.index] 

301 temp_col = heat_flux.rc_solution.result_vectors[time_step_index, collector.index] 

302 mass_flow = distributor.mass_flow 

303 spec_capacity = distributor.material.heat_capacity 

304 channel_heat_flux = mass_flow * spec_capacity * (temp_col - temp_dist) 

305 

306 x = heat_flux.rc_solution.time_steps[time_step_index] 

307 x = seconds_to_dates(x, start_date) 

308 

309 time_plot = TimePlot( 

310 x=x, 

311 ys=[ex, interior, (up + lo)], 

312 labels=["exterior", "interior", "inbetween"], 

313 y_scale=y_scale, 

314 y_title="Heat Flux / W", 

315 ) 

316 time_plot.plot_stack() 

317 time_plot.ax.plot( 

318 time_plot.x, 

319 channel_heat_flux * time_plot.y_scale, 

320 label=["channel"], 

321 color="black", 

322 linewidth=time_plot.line_width, 

323 ) 

324 time_plot.format() 

325 time_plot.show() 

326 

327 

328def plot_fluxes_accumulated( 

329 boundaries: list = None, boundaries_sum: list = None, time_step_index=None, plot_balance: bool = False 

330): 

331 x, ys, labels = fluxes_data(boundaries, boundaries_sum, time_step_index, plot_balance) 

332 

333 fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1, sharex=True) 

334 

335 for i, y in enumerate(ys): 

336 if labels[i] == "Balance": 

337 pass 

338 ax1.plot(x, y, labels[i]) 

339 

340 fig.tight_layout() 

341 ax1.set_xlabel("Time") 

342 ax1.set_ylabel("Positive Heat flux / W") 

343 ax2.set_ylabel("Negative Heat flux / W") 

344 for ax in (ax1, ax2): 

345 ax.grid(True) 

346 ax.legend() 

347 

348 plt.show()