Coverage for pyrc\core\components\resistor.py: 84%
158 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-29 15:57 +0200
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-29 15:57 +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
10from _warnings import warn
11from typing import TYPE_CHECKING, Any
13import numpy as np
14from sympy import Expr, symbols
16from pyrc.core.components.templates import ObjectWithPorts, SymbolMixin
17from pyrc.tools.functions import contains_symbol
18from pyrc.tools.science import round_valid
20if TYPE_CHECKING:
21 from pyrc.core.components.capacitor import Capacitor
22 from pyrc.core.components.node import TemperatureNode
25class Resistor(ObjectWithPorts, SymbolMixin):
26 resistor_counter = 0
28 def __init__(self, resistance: float | int | Expr = np.nan):
29 """
30 (Thermal) resistor element of an RC network.
32 This is the base class of all Resistors and heat and mass transfer/transport elements calculating the
33 resistance by their own.
35 Parameters
36 ----------
37 resistance : float | int | Expr
38 The (thermal) resistance of the `Resistor` in K/W.
39 """
40 ObjectWithPorts.__init__(self)
41 if not contains_symbol(resistance):
42 resistance = np.float64(resistance)
43 self._resistance = resistance
44 Resistor.resistor_counter += 1
45 self.id = Resistor.resistor_counter
46 self.__direct_connected_node_templates = None
48 # Create sympy symbols to put in the equation(s)
49 # Has to be at the end of init (after self.id)
50 self.resistance_symbol = symbols(f"R_{self.id}")
51 self._equivalent_resistance_symbol = None
53 @classmethod
54 def reset_counter(cls):
55 Resistor.resistor_counter = 0
57 def __str__(self):
58 return self.__repr__()
60 def __repr__(self):
61 if isinstance(self._resistance, Expr):
62 return f"{self.__class__.__name__} {self.id}: R={str(self._resistance)}"
63 return f"{self.__class__.__name__} {self.id}: 1/R={round_valid(1 / self._resistance, 3)}"
65 @property
66 def nodes(self):
67 """
68 Returns both connected nodes, no matter, if other resistors are between them.
70 Returns
71 -------
72 list :
73 The Nodes in a list.
74 """
75 result = []
76 seen = {self}
77 to_check = self.neighbours.copy()
79 from pyrc.core.components.node import TemperatureNode
81 while to_check and len(result) < 2:
82 current = to_check.pop()
83 if current in seen:
84 continue
85 seen.add(current)
87 if isinstance(current, TemperatureNode):
88 result.append(current)
89 else:
90 to_check.extend(n for n in current.neighbours if n not in seen)
91 assert len(result) <= 2, (
92 "Each Resistor should only be connected two a maximum of 2 TemperatureNodes.\nIf it is "
93 "connected to more than 2 (also over other Resistors) you must insert a Node."
94 )
95 return sorted(result, key=lambda node: node.id)
97 @property
98 def direct_connected_node_templates(self):
99 if self.__direct_connected_node_templates is None:
100 from pyrc.core.components.node import TemperatureNode
102 self.__direct_connected_node_templates = [
103 neighbour for neighbour in self.neighbours if isinstance(neighbour, TemperatureNode)
104 ]
105 return self.__direct_connected_node_templates
107 @property
108 def symbols(self) -> list:
109 # return [self.resistance_symbol, self.equivalent_resistance_symbol]
110 return [self.equivalent_resistance_symbol]
112 @property
113 def values(self) -> list:
114 # return [self.resistance, self.equivalent_resistance]
115 return [self.equivalent_resistance]
117 @property
118 def all_resistors_inbetween(self) -> list[Resistor]:
119 """
120 Returns all resistors between both connected `TemperatureNode`\\s including self.
122 Returns
123 -------
124 list[Resistor] :
125 All Resistors that form the equivalent resistor between both connected `TemperatureNode`\\s.
126 """
127 seen = {self}
128 to_check = self.neighbours.copy()
129 while to_check:
130 current = to_check.pop()
131 if current not in seen and isinstance(current, Resistor):
132 seen.add(current)
133 to_check.extend([n for n in current.neighbours if n not in seen])
134 return sorted(list(seen), key=lambda res: res.id)
136 @property
137 def equivalent_resistance(self) -> np.float64 | float | int | Expr:
138 """
139 Returns the equivalent resistance from one node to the other.
141 This considers both serial and parallel Resistors between the same Nodes.
143 Returns
144 -------
145 np.float64 :
146 The equivalent resistance from one node to the other.
147 """
148 nodes = self.direct_connected_node_templates
149 assert len(nodes) > 0, (
150 "This property is meant to be executed only for resistors that are connected to one TemperatureNode."
151 )
152 if len(nodes) == 2:
153 return self.resistance
155 def run_through_resistors(start_resistor: Resistor, start_node: TemperatureNode):
156 _equivalent_serial = start_resistor.resistance
157 _parallel_resistors = [
158 neighbour for neighbour in start_resistor.neighbours if isinstance(neighbour, Resistor)
159 ]
160 _current = start_resistor
161 while len(_parallel_resistors) == 1:
162 _equivalent_serial += _parallel_resistors[0].resistance
163 _parallel_resistors, _current = (
164 [
165 neighbour
166 for neighbour in _parallel_resistors[0].neighbours
167 if isinstance(neighbour, Resistor)
168 and neighbour is not _current
169 and neighbour.get_connected_node(_parallel_resistors[0]) is not start_node
170 ],
171 _parallel_resistors[0],
172 )
173 return _equivalent_serial, _parallel_resistors, _current
175 # calculate the equivalent resistance
176 equivalent_serial, parallel_resistors, current = run_through_resistors(self, nodes[0])
177 if len(parallel_resistors) > 1:
178 equivalent_parallel = parallel_equivalent_resistance(current)
179 else:
180 # Doing the same thing just the other way round.
181 # parallel resistors list is empty because the Resistor "current" at the end (right before a Node)
182 # now you need to check if there are parallel Resistors if walking in the other direction
183 equivalent_serial, parallel_resistors, current = run_through_resistors(
184 current, current.direct_connected_node_templates[0]
185 )
186 if len(parallel_resistors) > 1:
187 equivalent_parallel = parallel_equivalent_resistance(current)
188 else:
189 # parallel_resistors is empty
190 equivalent_parallel = 0
192 # serial resistance between both equivalents
193 return equivalent_parallel + equivalent_serial
195 @property
196 def equivalent_resistance_symbol(self):
197 if self._equivalent_resistance_symbol is None:
198 resistors = self.all_resistors_inbetween
199 symbol = symbols("_".join([f"{r}" for r in ["R_eq", *[res.id for res in resistors]]]))
201 # set this symbol to all other resistors (including self)
202 for r in resistors:
203 r._equivalent_resistance_symbol = symbol
204 return self._equivalent_resistance_symbol
206 # def get_equivalent_resistors(self) -> list:
207 # """
208 # Returns a list with all resistors between the two nodes self is connected with.
209 #
210 # Returns
211 # -------
212 # list :
213 # """
214 # node1, node2 = self.nodes
215 # result = set()
216 #
217 # neighbour: Resistor
218 # for neighbour in node1.neighbours:
219 # if neighbour.get_connected_node(node1) == node2:
220 # result.add(neighbour)
221 # connected_resistors
222 #
223 # def get_connected_resistors(self, include_self=False) -> list:
224 # """
225 # Returns a list with all resistors that are connected to this resistor or the connected ones.
226 #
227 # Parameters
228 # ----------
229 # include_self : bool, optional
230 # If True, self is included in the result.
231 #
232 # Returns
233 # -------
234 # list :
235 # All connected resistors.
236 # """
237 # result = []
238 # visited = set()
239 # to_check = [neighbour for neighbour in self.neighbours if isinstance(neighbour, Resistor)]
240 #
241 # for resistor in to_check:
242 # visited.add(resistor)
243 #
244 # while to_check:
245 # resistor = to_check.pop()
246 # result.append(resistor)
247 # for neighbour in resistor.neighbours:
248 # if isinstance(neighbour, Resistor) and neighbour not in visited:
249 # visited.add(neighbour)
250 # to_check.append(neighbour)
251 #
252 # if include_self:
253 # result.append(self)
254 #
255 # return result
257 @property
258 def resistance(self) -> float | int | Expr:
259 return self._resistance
261 def heat_flux(self, asking_node: Capacitor) -> float:
262 """
263 Returns the accumulated heat flux of this node that is transferred into the ``asking_node``.
265 Parameters
266 ----------
267 asking_node : Capacitor
268 The Node asking for the heat flux.
269 If the result is positive that heat flows into the asking_node.
271 Returns
272 -------
273 float :
274 The heat flux flowing into the asking_node in W.
275 """
276 if self.neighbours[0] == asking_node:
277 other_node = self.neighbours[1]
278 else:
279 other_node = self.neighbours[0]
280 other_node: Capacitor
282 return 1 / self.resistance * (other_node.temperature - asking_node.temperature)
284 def connect(self, neighbour, direction: tuple | list | np.ndarray | Any = None, node_direction_points_to=None):
285 """
286 Overloading for further checking and warning.
288 Parameters
289 ----------
290 neighbour
291 direction
292 node_direction_points_to
293 """
294 from pyrc.core.components.templates import Cell
296 if direction is not None and node_direction_points_to is None and len(self.nodes) == 1:
297 node_direction_points_to = self.nodes[0]
299 super().connect(neighbour, direction, node_direction_points_to)
300 # check, if direction is set manually if BoundaryCondition and Cell are involved
302 if len(self.nodes) == 2 and isinstance(neighbour, Cell):
303 from pyrc.core.nodes import BoundaryCondition
305 for i, resistor_neighbour in enumerate(self.nodes):
306 if isinstance(resistor_neighbour, BoundaryCondition) and not isinstance(resistor_neighbour, Cell):
307 # check, if manual_directions of other resistor neighbour was set, otherwise the cells automatics
308 # are broken
309 if isinstance(self.nodes[(i + 1) % 2], Cell):
310 if resistor_neighbour not in self.nodes[(i + 1) % 2].manual_directions:
311 raise ValueError("Direction must be set manually if BoundaryCondition is involved.")
313 def get_connected(self, asking_node: ObjectWithPorts) -> list[ObjectWithPorts]:
314 """
315 Returns the neighbour that isn't the asking one.
317 Parameters
318 ----------
319 asking_node : ObjectWithPorts
320 The asking Node. The method will return the other neighbour of ``self``.
322 Returns
323 -------
324 TemperatureNode :
325 The other neighbour of ``self``.
326 """
327 assert asking_node in self.neighbours, "The asking_node is not connected to self."
328 return [n for n in self.neighbours if n is not asking_node]
330 def get_connected_node(self, asking_node: TemperatureNode | Resistor) -> TemperatureNode:
331 """
332 Returns the connected TemperatureNode that isn't the asking one. It is routed through all connected Resistors to
333 the TemperatureNode.
335 Parameters
336 ----------
337 asking_node : TemperatureNode | Resistor
338 The asking Node. The method will return the other node connected (over other `Resistor`\\s) to self.
340 Returns
341 -------
342 TemperatureNode :
343 The other node connected to ``self``.
344 """
345 assert asking_node in self.neighbours, "The asking_node is not connected to self."
346 if isinstance(asking_node, Resistor) and len(self.neighbours) > 2:
347 warn("It is not defined, which node is returned. The first one found is returned.")
348 seen_nodes = {asking_node, self}
349 to_check = [n for n in self.neighbours if n is not asking_node]
351 from pyrc.core.components.node import TemperatureNode
353 while to_check:
354 current = to_check.pop()
355 if current in seen_nodes:
356 continue
357 seen_nodes.add(current)
359 if isinstance(current, TemperatureNode):
360 return current
362 to_check.extend(n for n in current.neighbours if n not in seen_nodes)
363 raise ValueError(f"No connected TemperatureNode found: {self}")
366def parallel_equivalent_resistance(start_resistor: Resistor, ignore_resistor: Resistor = None) -> np.float64:
367 """
368 Calculates the equivalent resistance of the connected Resistors.
370 It also checks if the connected Resistors are connected to other Resistors in serial and consider this, too.
372 This only works, if no other parallel resistors occur. But this should be okay, because this doesn't make any sense
373 thermo-physically.
375 Parameters
376 ----------
377 start_resistor : Resistor
378 The start Resistor, where in one "direction" parallel resistors are connected to (can also be just one).
379 ignore_resistor : Resistor, optional
380 If given this Resistor is excluded from the neighbours list of start_resistor.
381 This is needed to determine some kind of "direction" and only calculate the equivalent resistance for the
382 Resistors that are actually parallel.
384 Returns
385 -------
386 np.float64 :
387 The equivalent resistance of the connected Resistors without the resistance of the start_resistor.
388 """
389 parallel_resistors = [neighbour for neighbour in start_resistor.neighbours if isinstance(neighbour, Resistor)]
390 if ignore_resistor is not None:
391 try:
392 parallel_resistors.remove(ignore_resistor)
393 except ValueError:
394 # ignore_resistor not in neighbours of start_resistor
395 pass
396 parallel_resistance_list = []
397 for resistor in parallel_resistors:
398 parallel_resistance = resistor.resistance
399 current = resistor
400 connected: list = current.get_connected(start_resistor)
401 assert len(connected) == 1, "Currently in parallel circuits no other nested parallel circuits are allowed."
402 connected = connected[0]
403 while isinstance(connected, Resistor):
404 assert len(connected.neighbours) == 2, (
405 "No series of parallel Resistors are allowed. Just one Resistor "
406 "that is connected to multiple others in parallel\n"
407 "which themselves can be serial connected to others, but not parallel!"
408 )
409 parallel_resistance += connected.resistance
410 connected, current = connected.get_connected(current), connected
411 assert len(connected) == 1, "Currently in parallel circuits no other nested parallel circuits are allowed."
412 connected = connected[0]
413 parallel_resistance_list.append(parallel_resistance)
414 return 1 / np.float64(sum([1 / np.float64(r) for r in parallel_resistance_list]))