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

166 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-07 08:20 +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 :class:`MassFlowNode` s. 

39 

40 The resistance is calculated automatically. 

41 

42 Be aware that this :class:`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 

129 :class:`~pyrc.core.components.templates.Cell` or at least :class:`Geometric`\\. 

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

131 thermal conduction. If you connect a :class:`~pyrc.core.inputs.BoundaryCondition` (not inheriting from 

132 Cell/Geometric) you still can use this class but must define the direction to this BoundaryCondition 

133 manually using ``manual_directions`` and ``set_direction()`` of :class:`TemperatureNode` ( 

134 Capacitor/BoundaryCondition). 

135 

136 Parameters 

137 ---------- 

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

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

140 instead). 

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

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

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

144 heat_conduction : bool, optional 

145 Switch on/off heat conductivity. 

146 heat_transfer : bool, optional 

147 Switch on/off heat transfer. 

148 

149 See Also 

150 -------- 

151 Resistor : The basic Resistor class without the automatics. 

152 """ 

153 super().__init__(resistance) 

154 self.heat_conduction: bool = heat_conduction 

155 self.heat_transfer: bool = heat_transfer 

156 self.__htc = htc 

157 

158 @property 

159 def htc(self): 

160 if not is_set(self.__htc): 

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

162 self.__htc = 5 

163 return self.__htc 

164 

165 @property 

166 def heat_transfer_coefficient(self): 

167 return self.htc 

168 

169 @property 

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

171 """ 

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

173 

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

175 

176 Returns 

177 ------- 

178 

179 """ 

180 if is_set(self._resistance): 

181 return self._resistance 

182 resistance = np.float64(0) 

183 

184 from pyrc.core.nodes import Node, ChannelNode 

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

186 

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

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

189 # no BoundaryCondition 

190 if ( 

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

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

193 and self.heat_conduction 

194 ): 

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

196 node: Node 

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

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

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

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

201 ) 

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

203 # ChannelNode to Solid: HeatTransfer AND HeatConduction 

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

205 assert isinstance(node.material, Solid) 

206 if self.heat_transfer: 

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

208 resistance += resistance_channel_heat_transfer(channel_node, node) 

209 if self.heat_conduction: 

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

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

212 ) 

213 else: 

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

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

216 if self.heat_transfer: 

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

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

219 if self.heat_conduction: 

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

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

222 ) 

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

224 # 1 BC and 1 Node 

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

226 # 1 FluidBC and 1 Node 

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

228 if isinstance(node.material, Solid): 

229 if self.heat_transfer: 

230 resistance += resistance_bc_heat_transfer(bc, node) 

231 if self.heat_conduction: 

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

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

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

235 ) 

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

237 # 1 SolidBC and 1 Node 

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

239 effective_area = node.get_area(bc) 

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

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

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

243 elif self.heat_conduction: 

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

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

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

247 ) 

248 else: 

249 # BC is undefined -> resistance should be given 

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

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

252 ) 

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

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

255 if not is_set(bc.htc): 

256 htc = self.htc 

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

258 else: 

259 htc = bc.htc 

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

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

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

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

264 else: 

265 raise ValueError( 

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

267 ) 

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

269 # Only connected to other resistors: 

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

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

272 else: 

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

274 # resistance! 

275 node: Capacitor = self.direct_connected_node_templates[0] 

276 other_node: Capacitor = self.get_connected_node(node) 

277 if isinstance(node, BoundaryCondition): 

278 assert isinstance(other_node, Node) 

279 if isinstance(node, FluidBoundaryCondition): 

280 if isinstance(other_node.material, Fluid): 

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

282 resistance += np.float64(0) 

283 elif self.heat_transfer: 

284 # FluidBC to Solid Node -> HeatTransfer 

285 resistance += resistance_bc_heat_transfer(node, other_node) 

286 elif isinstance(node, SolidBoundaryCondition): 

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

288 # SolidBC to Fluid -> HeatTransfer 

289 resistance += resistance_bc_heat_transfer(node, other_node) 

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

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

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

293 # HeatTransfer in a round channel 

294 resistance += resistance_channel_heat_transfer(node, other_node) 

295 else: 

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

297 node: Node 

298 if self.heat_conduction and ( 

299 isinstance(node.material, Solid) 

300 or ( 

301 isinstance(node.material, Fluid) 

302 and ( 

303 isinstance(other_node, FluidBoundaryCondition) 

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

305 ) 

306 ) 

307 ): 

308 resistance = node.get_conduction_length(other_node) / ( 

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

310 ) 

311 elif self.heat_transfer: 

312 # self.material is Fluid and other_node is solid 

313 # Heat transfer 

314 if isinstance(other_node, SolidBoundaryCondition): 

315 resistance += resistance_bc_heat_transfer(other_node, node) 

316 else: 

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

318 return resistance 

319 

320 

321class HeatConduction(CombinedResistor): 

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

323 """ 

324 Represents the resistance caused by heat conduction. 

325 

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

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

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

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

330 

331 Parameters 

332 ---------- 

333 resistance : float, optional 

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

335 """ 

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

337 

338 @property 

339 def htc(self): 

340 return np.nan 

341 

342 

343class HeatTransfer(CombinedResistor): 

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

345 """ 

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

347 

348 Parameters 

349 ---------- 

350 resistance : float, optional 

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

352 """ 

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

354 

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

356 # """ 

357 # Returns the resistance using the passed node. 

358 # 

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

360 # 

361 # Returns 

362 # ------- 

363 # np.float64 : 

364 # The resistance. 

365 # """ 

366 # if isinstance(node, Node): 

367 # assert isinstance(node.material, Fluid) 

368 # 

369 # else: 

370 # assert isinstance(node, FluidBoundaryCondition) 

371 

372 

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

374 """ 

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

376 

377 Parameters 

378 ---------- 

379 bc : BoundaryCondition 

380 node : Node 

381 

382 Returns 

383 ------- 

384 np.float64 

385 """ 

386 effective_area = node.get_area(bc) 

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

388 return 1 / (bc.heat_transfer_coefficient * effective_area) 

389 

390 

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

392 effective_area = channel_node.get_pipe_area(node) 

393 

394 # calculate the alpha using Gnielinski 

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

396 return 1 / (alpha * effective_area)