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