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