Coverage for pyrc \ core \ components \ resistor.py: 85%
155 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
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
291 for i, neighbour in enumerate(self.nodes):
292 if isinstance(neighbour, BoundaryCondition):
293 # check, if manual_directions of other neighbour was set
294 if isinstance(self.nodes[(i + 1) % 2], Node):
295 if neighbour not in self.nodes[(i + 1) % 2].manual_directions:
296 raise ValueError("Direction must be set manually if BoundaryCondition is involved.")
297 else:
298 raise NotImplementedError(
299 "FAAIIILLL. What kind of object is connected here? Two "
300 "BoundaryConditions? Or two Resistors???"
301 )
303 def get_connected(self, asking_node: Capacitor | Node | Resistor) -> list[TemperatureNode | Resistor]:
304 """
305 Returns the neighbour that isn't the asking one.
307 Parameters
308 ----------
309 asking_node : Capacitor | Node
310 The asking Node. The method will return the other neighbour of ``self``.
312 Returns
313 -------
314 TemperatureNode :
315 The other neighbour of ``self``.
316 """
317 assert asking_node in self.neighbours, "The asking_node is not connected to self."
318 return [n for n in self.neighbours if n is not asking_node]
320 def get_connected_node(self, asking_node: TemperatureNode | Resistor) -> TemperatureNode:
321 """
322 Returns the connected TemperatureNode that isn't the asking one. It is routed through all connected Resistors to
323 the TemperatureNode.
325 Parameters
326 ----------
327 asking_node : Capacitor | Resistor
328 The asking Node. The method will return the other node connected (over other `Resistor`\s) to self.
330 Returns
331 -------
332 TemperatureNode :
333 The other node connected to ``self``.
334 """
335 assert asking_node in self.neighbours, "The asking_node is not connected to self."
336 if isinstance(asking_node, Resistor) and len(self.neighbours) > 2:
337 warn("It is not defined, which node is returned. The first one found is returned.")
338 seen_nodes = {asking_node, self}
339 to_check = [n for n in self.neighbours if n is not asking_node]
341 from pyrc.core.components.node import TemperatureNode
343 while to_check:
344 current = to_check.pop()
345 if current in seen_nodes:
346 continue
347 seen_nodes.add(current)
349 if isinstance(current, TemperatureNode):
350 return current
352 to_check.extend(n for n in current.neighbours if n not in seen_nodes)
353 raise ValueError(f"No connected TemperatureNode found: {self}")
356def parallel_equivalent_resistance(start_resistor: Resistor, ignore_resistor: Resistor = None) -> np.float64:
357 """
358 Calculates the equivalent resistance of the connected Resistors.
360 It also checks if the connected Resistors are connected to other Resistors in serial and consider this, too.
362 This only works, if no other parallel resistors occur. But this should be okay, because this doesn't make any sense
363 thermo-physically.
365 Parameters
366 ----------
367 start_resistor : Resistor
368 The start Resistor, where in one "direction" parallel resistors are connected to (can also be just one).
369 ignore_resistor : Resistor, optional
370 If given this Resistor is excluded from the neighbours list of start_resistor.
371 This is needed to determine some kind of "direction" and only calculate the equivalent resistance for the
372 Resistors that are actually parallel.
374 Returns
375 -------
376 np.float64 :
377 The equivalent resistance of the connected Resistors without the resistance of the start_resistor.
378 """
379 parallel_resistors = [neighbour for neighbour in start_resistor.neighbours if isinstance(neighbour, Resistor)]
380 if ignore_resistor is not None:
381 try:
382 parallel_resistors.remove(ignore_resistor)
383 except ValueError:
384 # ignore_resistor not in neighbours of start_resistor
385 pass
386 parallel_resistance_list = []
387 for resistor in parallel_resistors:
388 parallel_resistance = resistor.resistance
389 current = resistor
390 connected: list = current.get_connected(start_resistor)
391 assert len(connected) == 1, "Currently in parallel circuits no other nested parallel circuits are allowed."
392 connected = connected[0]
393 while isinstance(connected, Resistor):
394 assert len(connected.neighbours) == 2, (
395 "No series of parallel Resistors are allowed. Just one Resistor "
396 "that is connected to multiple others in parallel\n"
397 "which themselves can be serial connected to others, but not parallel!"
398 )
399 parallel_resistance += connected.resistance
400 connected, current = connected.get_connected(current), connected
401 assert len(connected) == 1, "Currently in parallel circuits no other nested parallel circuits are allowed."
402 connected = connected[0]
403 parallel_resistance_list.append(parallel_resistance)
404 return 1 / np.float64(sum([1 / np.float64(r) for r in parallel_resistance_list]))