Coverage for pyrc \ core \ components \ capacitor.py: 54%
175 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 typing import TYPE_CHECKING, Any
12import numpy as np
13from sympy import symbols
15from pyrc.core.components.node import TemperatureNode
16from pyrc.core.components.templates import (
17 initial_rc_objects,
18 solution_object,
19 ConnectedFlowObject,
20 Cell,
21)
23if TYPE_CHECKING:
24 from pyrc.core.components.resistor import Resistor
25 from pyrc.core.nodes import Node
26 from pyrc.core.resistors import MassTransport
27 from pyrc.core.inputs import InternalHeatSource
28 from pyrc.core.nodes import MassFlowNode
29 from pyrc.core.components.templates import RCObjects, RCSolution
32class Capacitor(TemperatureNode):
33 def __init__(
34 self,
35 capacity: float,
36 temperature,
37 rc_objects: RCObjects = initial_rc_objects,
38 temperature_derivative=0,
39 internal_heat_source: InternalHeatSource = None,
40 rc_solution: RCSolution = solution_object,
41 ):
42 """
43 Capacitor building part, currently designed as thermal capacitor.
45 Parameters
46 ----------
47 capacity : float
48 The capacity of the capacitor.
49 temperature : float | int
50 The temperature of the node.
51 temperature_derivative : float | int
52 The temperature derivative of the node.
53 """
54 super().__init__(
55 temperature=temperature,
56 rc_objects=rc_objects,
57 temperature_derivative=temperature_derivative,
58 rc_solution=rc_solution,
59 )
60 if type(self) is Capacitor and (capacity is None or capacity == np.nan or capacity == 0):
61 # subclasses are allowed to set capacity to None (see Node)
62 raise TypeError("Capacitor requires 'capacity'")
63 self._capacity = capacity
65 self.__internal_heat_source: InternalHeatSource | Any = internal_heat_source
67 # Cashing
68 self.__connected_mass_flow_nodes = None
70 # Create sympy symbols to put in the equation(s)
71 # Has to be at the end of init (after self.id)
72 self.capacity_symbol = symbols(f"C_{self.id}")
74 @property
75 def index(self):
76 if not self._index:
77 self._index = self.rc_objects.nodes.index(self)
78 return self._index
80 @property
81 def internal_heat_source(self) -> InternalHeatSource:
82 return self.__internal_heat_source
84 def make_internal_heat_source(self, heat_source_type, **kwargs):
85 self.__internal_heat_source = heat_source_type(node=self, **kwargs)
87 def add_internal_heat_source(self, heat_source: InternalHeatSource):
88 assert heat_source.node == self
89 self.__internal_heat_source = heat_source
91 @property
92 def resistors_without_parallel(self):
93 """
94 Returns every resistor on the node that isn't connected to the same as another.
96 If multiple resistors are in self.neighbours and are connected to the same node the first one is taken (more
97 or less random).
99 Returns
100 -------
102 """
103 resistors: Resistor = self.neighbours
104 seen_nodes = set()
105 result = []
106 for resistor in resistors:
107 connected_node = resistor.get_connected_node(self)
108 if connected_node not in seen_nodes:
109 seen_nodes.add(connected_node)
110 result.append(resistor)
111 return result
113 @property
114 def symbols(self) -> list:
115 """
116 Returns a list of all sympy.symbols of the object, except time dependent symbols.
118 Must be in the same order as self.values.
120 Returns
121 -------
122 list :
123 The list of sympy.symbols.
124 """
125 result = [*super().symbols, self.capacity_symbol]
126 if self.internal_heat_source:
127 result.append(self.internal_heat_source.symbol)
128 return result
130 @property
131 def values(self) -> list:
132 """
133 Returns a list of all values of all object symbols, except of time dependent symbols.
135 Must be in the same order as self.symbols.
137 Returns
138 -------
139 list :
140 The list of sympy.symbols.
141 """
142 result = [*self.values_without_capacity, self._capacity]
143 return result
145 @property
146 def values_without_capacity(self) -> list:
147 """
148 Returns a list of all values of all object symbols expect the capacity and the time dependent symbols.
150 This is used in some subclasses that calculate their capacity on their own.
152 Must be in the same order as self.symbols.
154 Returns
155 -------
156 list :
157 The list of sympy.symbols.
158 """
159 result = super().values
160 if self.internal_heat_source:
161 result.append(self.internal_heat_source.power)
162 return result
164 @property
165 def mass_flow_connections(self) -> int:
166 """
167 Returns the number of `MassFlowNode` s connected to ``self``.
169 Returns
170 -------
171 int :
172 The number of connected `MassFlowNode` s.
173 """
174 return len(self.connected_mass_flow_nodes)
176 @property
177 def connected_mass_flow_nodes(self) -> list[MassFlowNode]:
178 if self.__connected_mass_flow_nodes is None:
179 self.__connected_mass_flow_nodes = self.get_connected_mass_flow_nodes()
180 return self.__connected_mass_flow_nodes
182 def temperature_derivative_term(self) -> tuple:
183 """
184 Create the `sympy` expression for the temperature derivative (right side of heat flux balance equation).
186 The sign convention is:
187 Heat flux pointing into the node is positive.
189 Returns
190 -------
191 tuple[sympy expression, set] :
192 The term and a set with all involved temperature symbols.
193 """
194 from pyrc.core.resistors import MassTransport
196 result_equation = 0
197 temperature_symbols = set()
198 all_symbols = set()
200 # loop over ports/connections
201 resistor: Resistor
202 for resistor in self.filter_resistors_equivalent():
203 connected_node: TemperatureNode = resistor.get_connected_node(self)
204 rc_term = 1 / (resistor.equivalent_resistance_symbol * self.capacity_symbol)
205 if isinstance(resistor, MassTransport):
206 if (resistor.source == self and resistor.volume_flow >= 0) or (
207 resistor.sink == self and resistor.volume_flow < 0
208 ):
209 result_equation -= rc_term * self.temperature_symbol
210 else:
211 # self is sink
212 result_equation += rc_term * connected_node.temperature_symbol
213 temperature_symbols.add(connected_node.temperature_symbol)
214 else:
215 # resistor is a resistor, so we have to get the other node connected to it.
216 result_equation += rc_term * (connected_node.temperature_symbol - self.temperature_symbol)
217 temperature_symbols.add(connected_node.temperature_symbol)
218 temperature_symbols.add(self.temperature_symbol)
220 # add internal heat source terms
221 if self.internal_heat_source:
222 result_equation += self.internal_heat_source.symbol / self.capacity_symbol
223 all_symbols.add(self.internal_heat_source.symbol)
225 all_symbols.update(temperature_symbols)
226 return result_equation, temperature_symbols, all_symbols
228 def get_mass_transport_to_node(self, target_node: ConnectedFlowObject):
229 """
230 Returns the `ConnectedFlowObject` `Resistor` lying inbetween self and target_node.
232 Parameters
233 ----------
234 target_node : ConnectedFlowObject
235 The `ConnectedFlowObject` to which the MassTransport Resistor is requested for.
237 Returns
238 -------
239 MassTransport :
240 The MassTransport Resistor inbetween self and target_node.
241 """
242 for resistor in self.get_connected_mass_transport_resistors():
243 if resistor.get_connected_node(self) == target_node:
244 return resistor
246 def reset_properties(self):
247 self.__connected_mass_flow_nodes = None
249 def get_neighbours(self, variant: type = None) -> list:
250 """
251 Returns a list of connected objects with given variant.
253 If variant is ``None``, all objects are returned.
255 Parameters
256 ----------
257 variant : None | type
258 The type (of `Resistors`) to return.
259 Example: ``variant=MassTransport`` only returns all `MassTransport` resistors.
261 Returns
262 -------
263 list :
264 A list with all requested objects.
265 """
266 if variant is None:
267 return self.neighbours
268 else:
269 result = []
270 for resistor in self.neighbours:
271 if isinstance(resistor, variant):
272 result.append(resistor)
273 return result
275 def get_connected_mass_transport_resistors(self, except_this: MassTransport | list = None) -> list:
276 """
277 Returns a list of connected `MassTransport` resistors except the given ones.
279 Parameters
280 ----------
281 except_this : MassTransport | list
282 The given `MassTransport` resistors to exclude from the result.
284 Returns
285 -------
286 list :
287 The list of connected `MassTransport` resistors without all given ones.
288 """
289 from pyrc.core.resistors import MassTransport
291 result = self.get_neighbours(variant=MassTransport)
292 if isinstance(except_this, MassTransport):
293 except_this = [except_this]
295 if except_this:
296 for except_resistor in except_this:
297 if except_resistor in result:
298 result.remove(except_resistor)
300 return result
302 @property
303 def connected_mass_transport_resistors(self) -> list:
304 return self.get_connected_mass_transport_resistors()
306 def get_connected_nodes(self, variant: type = None) -> list:
307 """
308 Returns a list of by `Resistor` s connected `TemperatureNode` s.
310 Duplicates of connected nodes (due to multiple resistors between two unique nodes) are not returned twice.
312 Parameters
313 ----------
314 variant : None | type
315 The type (of `TemperatureNode`) to return.
316 If ``None``, all connected nodes are returned.
318 Returns
319 -------
320 list :
321 The list of connected `TemperatureNode` s.
322 """
323 result = []
325 if variant is None:
326 neighbour: Resistor
327 for neighbour in self.neighbours:
328 connected: list = neighbour.get_connected(self)
329 for c in connected:
330 if c not in result:
331 result.append(c)
332 else:
333 neighbour: Resistor
334 for neighbour in self.neighbours:
335 connected: list = neighbour.get_connected(self)
336 for c in connected:
337 if isinstance(c, variant) and c not in result:
338 result.append(c)
339 return result
341 def get_connected_mass_flow_nodes(self) -> list[MassFlowNode]:
342 """
343 Returns a list of connected `MassFlowNode` s.
345 Returns
346 -------
347 list :
348 The list of connected `MassFlowNode` s.
349 """
350 from pyrc.core.nodes import MassFlowNode
352 return self.get_connected_nodes(MassFlowNode)
354 def get_next_air_nodes(self, asking_node: Capacitor) -> list[MassFlowNode]:
355 """
356 Returns the next air nodes as a list.
358 Parameters
359 ----------
360 asking_node : Capacitor
361 The Node that asks for the connected `MassFlowNode` s.
363 Returns
364 -------
365 list[MassFlowNode] :
366 The connected `MassFlowNode` s of self but without ``asking_node``.
367 """
368 result = []
370 for node in self.connected_mass_flow_nodes:
371 if not node == asking_node:
372 result.append(node)
373 return result
375 def resistors_in_direction(
376 self, direction: np.ndarray | str, except_resistor_types: list[type] = None
377 ) -> list[Resistor]:
378 """
379 Returns all `Resistor` s connected to nodes in the given direction.
381 Parameters
382 ----------
383 direction : np.ndarray | str
384 The direction to get the `Resistor` s from.
385 If an array, it has to be parallel to the coordinate axes.
386 If a string, it should be of the following:
387 +x,+y,+z,-x,-y,-z,x,y,z
388 except_resistor_types : list, optional
389 If not None, these `Resistor` types will not be added to the result.
391 Returns
392 -------
393 list[Resistor] :
394 The `Resistor` s in the requested direction.
395 """
396 result = []
397 if except_resistor_types is None:
398 except_resistor_types = []
399 if not isinstance(except_resistor_types, list):
400 except_resistor_types = [except_resistor_types]
402 if isinstance(direction, str):
403 if len(direction) == 1 or direction[0] == "+":
404 sign = 1
405 else:
406 sign = -1
407 match direction[-1].lower():
408 case "x":
409 direction = sign * np.ndarray((1, 0, 0))
410 case "y":
411 direction = sign * np.ndarray((0, 1, 0))
412 case "z":
413 direction = sign * np.ndarray((0, 0, 1))
414 case _:
415 raise ValueError("Invalid direction string. This algorithm only works for rectangular meshes.")
417 neighbour_resistors = [
418 r for r in self.neighbours if not any(isinstance(r, r_type) for r_type in except_resistor_types)
419 ]
420 for resistor in neighbour_resistors:
421 other_node: Capacitor = resistor.get_connected_node(self)
422 neighbour_dir = self.get_direction(other_node)
423 if np.allclose((neighbour_dir - direction), 0):
424 result.append(resistor)
425 return result
427 def resistors_in_direction_filtered(
428 self, direction: np.ndarray | str, except_resistor_types: list = None
429 ) -> list[Resistor]:
430 """
431 Like resistors_in_direction but only one resistor is returned for parallel resistors.
433 Parameters
434 ----------
435 direction : np.ndarray | str
436 The direction to get the `Resistor` s from.
437 If an array, it has to be parallel to the coordinate axes.
438 If a string, it should be of the following:
439 +x,+y,+z,-x,-y,-z,x,y,z
440 except_resistor_types : list, optional
441 If not None, these `Resistor` types will not be added to the result.
443 Returns
444 -------
445 list[Resistor] :
446 The `Resistor` s in the requested direction.
447 """
448 resistors = self.resistors_in_direction(direction, except_resistor_types)
449 return self.filter_resistors_equivalent(resistors)
451 def get_direction(self, asking_node: Cell | Capacitor) -> np.array:
452 """
453 Returns the direction (x,y,z direction) to the asking node. Is returned as normalized vector.
455 Parameters
456 ----------
457 asking_node : Cell
458 The Node asking for the direction to it.
460 Returns
461 -------
462 np.ndarray :
463 The direction from ``self`` to the asking node.
464 """
466 # compare the boundaries to get the matching one and determine its direction
467 if asking_node in self.manual_directions:
468 return np.array(self.manual_directions[asking_node])
469 assert isinstance(asking_node, Cell), "Directions have to be set manually if the asking_node is no Cell."
470 self: Node
471 asking_boundaries = asking_node.boundaries
472 self_boundaries = self.boundaries
474 diff = []
475 for i, element in enumerate(asking_boundaries):
476 unit = -1 + 2 * ((i + 1) % 2) # switch that is -1 if i is odd and +1 if i is even.
477 diff.append(element - self_boundaries[i + unit])
479 directions = np.array([
480 [1, 0, 0], [-1, 0, 0],
481 [0, 1, 0], [0, -1, 0],
482 [0, 0, 1], [0, 0, -1]
483 ])
485 # Take the difference that is closest to 0 (without floating point error it will exactly has one 0 in it).
486 return directions[np.argmin(np.abs(diff))]