Coverage for pyrc \ core \ resistors.py: 51%

166 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-13 16:59 +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# ------------------------------------------------------------------------------ 

7 

8from __future__ import annotations 

9 

10import warnings 

11from typing import Any 

12from typing import TYPE_CHECKING 

13 

14import numpy as np 

15from sympy import Basic 

16 

17from pyrc.core.components.resistor import Resistor 

18from pyrc.core.components.capacitor import Capacitor 

19from pyrc.core.heat_transfer import alpha_forced_convection_in_pipe 

20from pyrc.core.components.templates import Solid, Fluid 

21from pyrc.tools.functions import is_set, check_type, return_type 

22 

23if TYPE_CHECKING: 

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

25 from pyrc.core.inputs import ( 

26 BoundaryCondition, 

27 FluidBoundaryCondition, 

28 FlowBoundaryCondition, 

29 SolidBoundaryCondition, 

30 ) 

31 

32np.seterr(divide="ignore") 

33 

34 

35class MassTransport(Resistor): 

36 def __init__(self): 

37 """ 

38 Represents the resistance caused by mass transfer between two `MassFlowNode` s. 

39 

40 The resistance is calculated automatically. 

41 

42 Be aware that this `Resistor` doesn't care about Courant number at all. This has to be checked in the Handler 

43 that starts the simulation. 

44 """ 

45 super().__init__(resistance=np.nan) 

46 self.__source: MassFlowNode | Any = None 

47 self.__sink: MassFlowNode | Any = None 

48 self._volume_flow: float | Any = None 

49 

50 @property 

51 def source(self) -> MassFlowNode: 

52 if self.__source is None: 

53 raise AttributeError("Source node has not been set yet.") 

54 return self.__source 

55 

56 @source.setter 

57 def source(self, value: MassFlowNode): 

58 self.__source = value 

59 

60 @property 

61 def sink(self) -> MassFlowNode: 

62 if self.__sink is None: 

63 raise AttributeError("Sink node has not been set yet.") 

64 return self.__sink 

65 

66 @sink.setter 

67 def sink(self, value: MassFlowNode): 

68 self.__sink = value 

69 

70 @property 

71 def guess_volume_flow(self): 

72 return self._volume_flow 

73 

74 @property 

75 def volume_flow(self): 

76 from pyrc.core.nodes import MassFlowNode 

77 

78 if self._volume_flow is None: 

79 # create volume flow using one connected MassFlowNode 

80 for node in self.neighbours: 

81 if isinstance(node, MassFlowNode): 

82 node.propagate_flow() 

83 break 

84 return self._volume_flow 

85 

86 @property 

87 def resistance(self) -> float | int: 

88 from pyrc.core.nodes import FlowBoundaryCondition 

89 

90 if not is_set(self._resistance): 

91 # Get volume flow using source and sink of neighbour nodes 

92 nodes: list = self.nodes 

93 assert len(nodes) == 2 

94 if isinstance(nodes[0], FlowBoundaryCondition): 

95 mfn = nodes[1] 

96 else: 

97 mfn = nodes[0] 

98 resistance = 1 / np.float64(self.volume_flow * mfn.material.density * mfn.material.heat_capacity) 

99 

100 self._resistance = resistance 

101 return self._resistance 

102 

103 # def make_velocity_direction(self): 

104 # """ 

105 # Trigger the process in a `MassFlowNode` neighbour of ``self``. 

106 # """ 

107 # from pyrc.core.nodes import MassFlowNode 

108 # for neighbour in self.neighbours: 

109 # if isinstance(neighbour, MassFlowNode): 

110 # neighbour.make_velocity_direction() 

111 # return None # break out of the method 

112 # # raise error otherwise 

113 # raise ConnectionError(f"The {type(self)} isn't connected to any MassFlowNode. Damn!") 

114 

115 

116class CombinedResistor(Resistor): 

117 def __init__( 

118 self, 

119 resistance: float | int | np.number | Basic = np.nan, 

120 htc: float | int | np.number | Basic = np.nan, 

121 heat_conduction=True, 

122 heat_transfer=True, 

123 ): 

124 """ 

125 Automated version of Resistor, calculating its resistance based on connected objects. 

126 

127 The algorithms of this class rely on geometric representations and assume a thermal problem. For most of the 

128 algorithms the connected capacities and boundary conditions must inherit from `Cell` or at least `Geometric`\. 

129 Using the geometric information let the algorithm figure out which areas/lengths influence the heat transfer and 

130 thermal conduction. If you connect a `BoundaryCondition` (not inheriting from Cell/Geometric) you still can use 

131 this class but must define the direction to this BoundaryCondition manually using ``manual_directions`` and 

132 ``set_direction()`` of `TemperatureNode` (Capacitor/BoundaryCondition). 

133 

134 Parameters 

135 ---------- 

136 resistance : float | int | np.number | sympy.Basic, optional 

137 The resistance of self. If set, no algorithm is used (and it would be preferable to use Resistor class 

138 instead). 

139 htc : float | int | np.number | sympy.Basic, optional 

140 Heat transfer coefficient that is used, if no other HTC was found (in boundary conditions). 

141 If not set, an initial value of 5 is used (raising a warning). 

142 heat_conduction : bool, optional 

143 Switch on/off heat conductivity. 

144 heat_transfer : bool, optional 

145 Switch on/off heat transfer. 

146 """ 

147 super().__init__(resistance) 

148 self.heat_conduction: bool = heat_conduction 

149 self.heat_transfer: bool = heat_transfer 

150 self.__htc = htc 

151 

152 @property 

153 def htc(self): 

154 if not is_set(self.__htc): 

155 warnings.warn("Warning: Initial HTC value of 5 is used.") 

156 self.__htc = 5 

157 return self.__htc 

158 

159 @property 

160 def heat_transfer_coefficient(self): 

161 return self.htc 

162 

163 @property 

164 def resistance(self) -> float | int: 

165 """ 

166 Determines the resistance accordingly to the nodes the resistor is connected to. 

167 

168 To get this, look at the pictures of Joel Kimmich from 13.8.2025 

169 

170 Returns 

171 ------- 

172 

173 """ 

174 if is_set(self._resistance): 

175 return self._resistance 

176 resistance = np.float64(0) 

177 

178 from pyrc.core.nodes import Node, ChannelNode 

179 from pyrc.core.inputs import BoundaryCondition, FluidBoundaryCondition, SolidBoundaryCondition 

180 

181 if len(self.direct_connected_node_templates) == 2: 

182 if all(isinstance(neighbour, Node) for neighbour in self.neighbours): 

183 # no BoundaryCondition 

184 if ( 

185 all(isinstance(neighbour.material, Solid) for neighbour in self.neighbours) 

186 or all(isinstance(neighbour.material, Fluid) for neighbour in self.neighbours) 

187 and self.heat_conduction 

188 ): 

189 # heat conduction in both nodes (also for two ChannelNodes) 

190 node: Node 

191 for i, node in enumerate(self.neighbours): 

192 other_node = self.neighbours[(i + 1) % 2] 

193 resistance += node.get_conduction_length(other_node) / ( 

194 node.material.thermal_conductivity * node.get_area(other_node) 

195 ) 

196 elif check_type(self.neighbours, ChannelNode, Node): 

197 # ChannelNode to Solid: HeatTransfer AND HeatConduction 

198 channel_node, node = return_type(self.neighbours, ChannelNode) 

199 assert isinstance(node.material, Solid) 

200 if self.heat_transfer: 

201 # calculate heat transfer in pipe using Nusselt correlation(s) 

202 resistance += resistance_channel_heat_transfer(channel_node, node) 

203 if self.heat_conduction: 

204 resistance += node.get_conduction_length(channel_node) / ( 

205 node.material.thermal_conductivity * node.get_area(channel_node) 

206 ) 

207 else: 

208 # solid to fluid: heat transfer in fluid and conduction in solid 

209 solid, fluid = return_type(self.neighbours, Solid, [n.material for n in self.neighbours]) 

210 if self.heat_transfer: 

211 effective_area = min(solid.get_area(fluid), fluid.get_area(solid)) 

212 resistance += 1 / (self.htc * effective_area) 

213 if self.heat_conduction: 

214 resistance += solid.get_conduction_length(fluid) / ( 

215 solid.material.thermal_conductivity * solid.get_area(fluid) 

216 ) 

217 elif check_type(self.neighbours, BoundaryCondition, Node): 

218 # 1 BC and 1 Node 

219 if check_type(self.neighbours, FluidBoundaryCondition, Node): 

220 # 1 FluidBC and 1 Node 

221 bc, node = return_type(self.neighbours, FluidBoundaryCondition) 

222 if isinstance(node.material, Solid): 

223 if self.heat_transfer: 

224 resistance += resistance_bc_heat_transfer(bc, node) 

225 if self.heat_conduction: 

226 # for both: (both Fluid) and (Fluid BC and Solid Node) 

227 resistance += node.get_conduction_length(bc) / ( 

228 node.material.thermal_conductivity * node.get_area(bc) 

229 ) 

230 elif check_type(self.neighbours, SolidBoundaryCondition, Node): 

231 # 1 SolidBC and 1 Node 

232 node, bc = return_type(self.neighbours, Node) 

233 effective_area = node.get_area(bc) 

234 if isinstance(node.material, Fluid) and self.heat_transfer: 

235 resistance += 1 / (self.htc * effective_area) 

236 # no heat conduction if node is Fluid (is already included in heat transfer) 

237 elif self.heat_conduction: 

238 # like two solids, but only node is used (so conduction in BC is infinite) 

239 resistance += node.get_conduction_length(bc) / ( 

240 node.material.thermal_conductivity * node.get_area(bc) 

241 ) 

242 else: 

243 # BC is undefined -> resistance should be given 

244 assert check_type(self.neighbours, BoundaryCondition, Node), ( 

245 "You must use FluidBoundaryCondition or SolidBoundaryCondition or set a resistance/htc value." 

246 ) 

247 bc, node = return_type(self.neighbours, BoundaryCondition) 

248 # TODO: Maybe a BC shouldn't get an HTC value because it depends on the cell / the cell AND the BC. 

249 if not is_set(bc.htc): 

250 htc = self.htc 

251 warnings.info("Resistor.htc was used instead of BoundaryCondition.htc.") 

252 else: 

253 htc = bc.htc 

254 warnings.warn("You might consider using FluidBoundaryCondition instead of BoundaryCondition.") 

255 resistance += 1 / (htc * node.get_area(bc)) 

256 elif all(isinstance(neighbour, BoundaryCondition) for neighbour in self.neighbours): 

257 raise ValueError("Two BoundaryConditions cannot be connected directly to each other.") 

258 else: 

259 raise ValueError( 

260 f"This combination isn't implemented: {self.neighbours}\n{[type(n) for n in self.neighbours]}" 

261 ) 

262 elif len(self.direct_connected_node_templates) == 0: 

263 # Only connected to other resistors: 

264 # the resistance must be given. But this was already checked, so raise an Error 

265 raise ValueError("If a Resistor is between two others, its resistance must be given!") 

266 else: 

267 # one Node, one/multiple Resistor/s -> only the resistance of this Resistor is returned, no equivalent 

268 # resistance! 

269 node: Capacitor = self.direct_connected_node_templates[0] 

270 other_node: Capacitor = self.get_connected_node(node) 

271 if isinstance(node, BoundaryCondition): 

272 assert isinstance(other_node, Node) 

273 if isinstance(node, FluidBoundaryCondition): 

274 if isinstance(other_node.material, Fluid): 

275 # FluidBC to Fluid Node -> Own resistance is zero, because BC don't have mass/volume 

276 resistance += np.float64(0) 

277 elif self.heat_transfer: 

278 # FluidBC to Solid Node -> HeatTransfer 

279 resistance += resistance_bc_heat_transfer(node, other_node) 

280 elif isinstance(node, SolidBoundaryCondition): 

281 if isinstance(other_node.material, Fluid) and self.heat_transfer: 

282 # SolidBC to Fluid -> HeatTransfer 

283 resistance += resistance_bc_heat_transfer(node, other_node) 

284 elif isinstance(node, ChannelNode) and self.heat_transfer: 

285 # ChannelNode should only be connected to a Solid Node over a HeatConduction Resistor 

286 assert isinstance(other_node, Node) and isinstance(other_node.material, Solid) 

287 # HeatTransfer in a round channel 

288 resistance += resistance_channel_heat_transfer(node, other_node) 

289 else: 

290 # one Node (no BC) and one/multiple resistors -> only heat mechanism of self is calculated 

291 node: Node 

292 if self.heat_conduction and ( 

293 isinstance(node.material, Solid) 

294 or ( 

295 isinstance(node.material, Fluid) 

296 and ( 

297 isinstance(other_node, FluidBoundaryCondition) 

298 or (isinstance(other_node, Node) and isinstance(other_node.material, Fluid)) 

299 ) 

300 ) 

301 ): 

302 resistance = node.get_conduction_length(other_node) / ( 

303 node.material.thermal_conductivity * node.get_area(other_node) 

304 ) 

305 elif self.heat_transfer: 

306 # self.material is Fluid and other_node is solid 

307 # Heat transfer 

308 if isinstance(other_node, SolidBoundaryCondition): 

309 resistance += resistance_bc_heat_transfer(other_node, node) 

310 else: 

311 resistance += 1 / (self.htc * node.get_area(other_node)) 

312 return resistance 

313 

314 

315class HeatConduction(CombinedResistor): 

316 def __init__(self, resistance=np.nan): 

317 """ 

318 Represents the resistance caused by heat conduction. 

319 

320 If the nodes, where the heat conduction takes place, differ in their material (Solid and Fluid) the heat 

321 conduction is set to 0 (the resistance is set to np.inf), because the heat conduction is included in 

322 HeatTransfer. So calculating it also in HeatConduction it would be taken into account twice. 

323 So: Do not forget to create a `HeatTransfer` `Resistor` between such nodes! 

324 

325 Parameters 

326 ---------- 

327 resistance : float, optional 

328 The resistance. If set, it will not be calculated. 

329 """ 

330 super().__init__(resistance, heat_conduction=True, heat_transfer=False) 

331 

332 @property 

333 def htc(self): 

334 return np.nan 

335 

336 

337class HeatTransfer(CombinedResistor): 

338 def __init__(self, resistance=np.nan, htc=np.nan): 

339 """ 

340 Represents the resistance caused by heat transfer between a solid and a fluid. 

341 

342 Parameters 

343 ---------- 

344 resistance : float, optional 

345 The resistance. If set, it will not be calculated. 

346 """ 

347 super().__init__(resistance, htc=htc, heat_transfer=True, heat_conduction=False) 

348 

349 # def single_resistance(self, node: FluidBoundaryCondition | Node): 

350 # """ 

351 # Returns the resistance using the passed node. 

352 # 

353 # The passed node must have a Fluid as material or must be a FluidBoundaryCondition. 

354 # 

355 # Returns 

356 # ------- 

357 # np.float64 : 

358 # The resistance. 

359 # """ 

360 # if isinstance(node, Node): 

361 # assert isinstance(node.material, Fluid) 

362 # 

363 # else: 

364 # assert isinstance(node, FluidBoundaryCondition) 

365 

366 

367def resistance_bc_heat_transfer(bc: BoundaryCondition, node: Node): 

368 """ 

369 Returns the resistance of a heat transfer between FluidBC-Solid Node or SolidBC-Fluid Node. 

370 

371 Parameters 

372 ---------- 

373 bc : BoundaryCondition 

374 node : Node 

375 

376 Returns 

377 ------- 

378 np.float64 

379 """ 

380 effective_area = node.get_area(bc) 

381 assert is_set(bc.heat_transfer_coefficient), "FluidBoundaryCondition has to get a heat transfer coefficient." 

382 return 1 / (bc.heat_transfer_coefficient * effective_area) 

383 

384 

385def resistance_channel_heat_transfer(channel_node: ChannelNode, node: Node): 

386 effective_area = channel_node.get_pipe_area(node) 

387 

388 # calculate the alpha using Gnielinski 

389 alpha = alpha_forced_convection_in_pipe(channel_node.diameter, channel_node.velocity, channel_node.material) 

390 return 1 / (alpha * effective_area)