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