Coverage for pyrc\core\components\node.py: 73%
84 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# ------------------------------------------------------------------------------
7from __future__ import annotations
9from abc import abstractmethod
10from typing import TYPE_CHECKING, Any
12import numpy as np
13import pandas as pd
14from sympy import Basic, symbols
16from pyrc.core.components.templates import (
17 EquationItem,
18 ObjectWithPorts,
19 RCObjects,
20 RCSolution,
21 initial_rc_objects,
22 solution_object,
23)
25if TYPE_CHECKING:
26 from pyrc.core.components.resistor import Resistor
29class TemperatureNode(ObjectWithPorts, EquationItem):
30 def __init__(
31 self,
32 temperature: float | int | np.number,
33 rc_objects: RCObjects = None,
34 temperature_derivative=0,
35 rc_solution: RCSolution = None,
36 ):
37 """
38 Capacitor and BoundaryCondition building part, holding temperature symbol, rc_solution and rc_objects.
40 Parameters
41 ----------
42 temperature : float | int | np.number
43 The temperature of the node.
44 temperature_derivative : float | int
45 The temperature derivative of the node.
46 rc_objects : RCObjects, optional
47 An `RCObjects` object to store all building parts (`Capacitor`\\s, `Resistor`\\s, ...)
48 If None, an initial object will be used.
49 rc_solution : RCSolution, optional
50 An `RCSolution` object where the solution is stored.
51 If None, an initial solution will be used.
52 """
53 if rc_objects is None:
54 rc_objects: RCObjects = initial_rc_objects
55 if rc_solution is None:
56 rc_solution: RCSolution = solution_object
57 ObjectWithPorts.__init__(self)
58 EquationItem.__init__(self)
59 self.rc_objects: RCObjects = rc_objects
61 assert not (isinstance(temperature, Basic) and temperature.free_symbols)
62 self.initial_temperature = temperature
63 self.temperature_derivative = temperature_derivative
65 self.temperature_symbol = symbols(f"theta_{self.id}")
67 self.manual_directions = {} # store manual set directions. Used for connected BoundaryConnections
69 # make space for results
70 self.solutions: RCSolution = rc_solution
72 # Cashing
73 self.__connected_mass_flow_nodes = None
75 # make space for results
76 self.solutions: RCSolution = rc_solution
78 @property
79 @abstractmethod
80 def index(self) -> int:
81 """
82 Returns the position of self within the vector where the temperature is stored in.
84 Is currently defined by its subclasses using the result object.
86 Returns
87 -------
88 int :
89 The index of the vector.
90 """
91 pass
93 @property
94 def temperature(self) -> float | np.number | int | Any:
95 if self.solutions.exist:
96 return self.solutions.temperature_vectors[-1, self.index]
97 return self.initial_temperature
99 @property
100 def temperature_vector_pandas(self) -> pd.Series:
101 """
102 The result vector of one node as pandas Series.
104 Returns
105 -------
106 pd.Series
107 """
108 return self.solutions.temperature_vectors_pandas.iloc[:, self.index]
110 @property
111 def temperature_vector(self) -> np.ndarray:
112 if self.solutions.exist:
113 return self.solutions.temperature_vectors[:, self.index]
114 return np.array([self.initial_temperature])
116 def temperature_at_time(self, time_step: list | float | int) -> np.float64 | list:
117 if isinstance(time_step, list):
118 result = []
119 for step in time_step:
120 result.append(np.interp(step, self.solutions.time_steps, self.temperature_vector))
121 return result
122 else:
123 return np.interp(time_step, self.solutions.time_steps, self.temperature_vector)
125 @property
126 def symbols(self) -> list:
127 """
128 Returns a list of all sympy.symbols of the object, except time dependent symbols.
130 Must be in the same order as self.values.
132 Returns
133 -------
134 list :
135 The list of sympy.symbols.
136 """
137 return [self.temperature_symbol]
139 @property
140 def values(self) -> list:
141 """
142 Returns a list of all values of all object symbols, except of time dependent symbols.
144 Must be in the same order as self.symbols.
146 Returns
147 -------
148 list :
149 The list of sympy.symbols.
150 """
151 return [self.temperature]
153 def get_resistors_between(self, node) -> list[Resistor]:
154 """
155 Returns all resistors between self and node.
157 Parameters
158 ----------
159 node : TemperatureNode
160 The TemperatureNode to which the resistors should go.
162 Returns
163 -------
164 list[Resistor] :
165 A list with all resistors between self and node.
166 """
167 resistor: Resistor
168 for resistor in self.neighbours:
169 if resistor.get_connected_node(self) == node:
170 return resistor.all_resistors_inbetween
171 raise ValueError("No resistors between two TemperatureNodes. This is not allowed.")
173 def filter_resistors_equivalent(self, resistors=None):
174 """
175 Returns all neighbours without the ones that are connected to the same node.
177 Used for equivalent resistances.
179 Parameters
180 ----------
181 resistors : list[Resistor], optional
182 The resistors to filter.
184 Returns
185 -------
186 list[Resistor] :
187 The filtered list.
188 """
189 if resistors is None:
190 resistors = self.neighbours
191 else:
192 for r in resistors:
193 assert r in self.neighbours
194 result = []
195 seen_nodes = set()
196 for resistor in resistors:
197 node = resistor.get_connected_node(self)
198 if node not in seen_nodes:
199 result.append(resistor)
200 seen_nodes.add(node)
201 return sorted(result, key=lambda obj: obj.id)
203 def set_direction(self, node_direction_points_to: "TemperatureNode", direction: np.ndarray):
204 """
205 Adding a manual direction from self to the given node.
207 The direction has to be a np.ndarray like:
208 [1,0,0] or [0,1,0] or [0,0,1] or negative ones of this.
210 Parameters
211 ----------
212 node_direction_points_to : TemperatureNode
213 The node where the direction points to.
214 direction : np.ndarray
215 The direction to the ``node_direction_points_to``\\. Has to be a np.ndarray.
216 """
217 direction = np.array(direction).ravel()
218 if len(direction) == 1:
219 np.append(direction, np.array([0, 0]))
220 elif len(direction) == 2:
221 np.append(direction, np.array([0]))
222 elif len(direction) != 3:
223 raise ValueError(f"direction must have 1,2, or 3 values, got {direction.shape}")
224 valid = np.array([[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]])
225 if not any(np.array_equal(direction, v) for v in valid):
226 raise ValueError(f"direction must be one of ±[1,0,0], ±[0,1,0], ±[0,0,1], got {direction}")
227 self.manual_directions[node_direction_points_to] = direction