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
« 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# ------------------------------------------------------------------------------
8from __future__ import annotations
10import warnings
11from typing import Any
12from typing import TYPE_CHECKING
14import numpy as np
15from sympy import Basic
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
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 )
32np.seterr(divide="ignore")
35class MassTransport(Resistor):
36 def __init__(self):
37 """
38 Represents the resistance caused by mass transfer between two `MassFlowNode` s.
40 The resistance is calculated automatically.
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
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
56 @source.setter
57 def source(self, value: MassFlowNode):
58 self.__source = value
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
66 @sink.setter
67 def sink(self, value: MassFlowNode):
68 self.__sink = value
70 @property
71 def guess_volume_flow(self):
72 return self._volume_flow
74 @property
75 def volume_flow(self):
76 from pyrc.core.nodes import MassFlowNode
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
86 @property
87 def resistance(self) -> float | int:
88 from pyrc.core.nodes import FlowBoundaryCondition
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)
100 self._resistance = resistance
101 return self._resistance
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!")
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.
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).
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
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
159 @property
160 def heat_transfer_coefficient(self):
161 return self.htc
163 @property
164 def resistance(self) -> float | int:
165 """
166 Determines the resistance accordingly to the nodes the resistor is connected to.
168 To get this, look at the pictures of Joel Kimmich from 13.8.2025
170 Returns
171 -------
173 """
174 if is_set(self._resistance):
175 return self._resistance
176 resistance = np.float64(0)
178 from pyrc.core.nodes import Node, ChannelNode
179 from pyrc.core.inputs import BoundaryCondition, FluidBoundaryCondition, SolidBoundaryCondition
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
315class HeatConduction(CombinedResistor):
316 def __init__(self, resistance=np.nan):
317 """
318 Represents the resistance caused by heat conduction.
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!
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)
332 @property
333 def htc(self):
334 return np.nan
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.
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)
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)
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.
371 Parameters
372 ----------
373 bc : BoundaryCondition
374 node : Node
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)
385def resistance_channel_heat_transfer(channel_node: ChannelNode, node: Node):
386 effective_area = channel_node.get_pipe_area(node)
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)