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
« 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# ------------------------------------------------------------------------------
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 :class:`MassFlowNode` s.
40 The resistance is calculated automatically.
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
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
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).
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.
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
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
165 @property
166 def heat_transfer_coefficient(self):
167 return self.htc
169 @property
170 def resistance(self) -> float | int:
171 """
172 Determines the resistance accordingly to the nodes the resistor is connected to.
174 To get this, look at the pictures of Joel Kimmich from 13.8.2025
176 Returns
177 -------
179 """
180 if is_set(self._resistance):
181 return self._resistance
182 resistance = np.float64(0)
184 from pyrc.core.nodes import Node, ChannelNode
185 from pyrc.core.inputs import BoundaryCondition, FluidBoundaryCondition, SolidBoundaryCondition
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
321class HeatConduction(CombinedResistor):
322 def __init__(self, resistance=np.nan):
323 """
324 Represents the resistance caused by heat conduction.
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!
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)
338 @property
339 def htc(self):
340 return np.nan
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.
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)
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)
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.
377 Parameters
378 ----------
379 bc : BoundaryCondition
380 node : Node
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)
391def resistance_channel_heat_transfer(channel_node: ChannelNode, node: Node):
392 effective_area = channel_node.get_pipe_area(node)
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)