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

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 

8 

9from abc import abstractmethod 

10from typing import TYPE_CHECKING, Any 

11 

12import numpy as np 

13import pandas as pd 

14from sympy import Basic, symbols 

15 

16from pyrc.core.components.templates import ( 

17 EquationItem, 

18 ObjectWithPorts, 

19 RCObjects, 

20 RCSolution, 

21 initial_rc_objects, 

22 solution_object, 

23) 

24 

25if TYPE_CHECKING: 

26 from pyrc.core.components.resistor import Resistor 

27 

28 

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. 

39 

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 

60 

61 assert not (isinstance(temperature, Basic) and temperature.free_symbols) 

62 self.initial_temperature = temperature 

63 self.temperature_derivative = temperature_derivative 

64 

65 self.temperature_symbol = symbols(f"theta_{self.id}") 

66 

67 self.manual_directions = {} # store manual set directions. Used for connected BoundaryConnections 

68 

69 # make space for results 

70 self.solutions: RCSolution = rc_solution 

71 

72 # Cashing 

73 self.__connected_mass_flow_nodes = None 

74 

75 # make space for results 

76 self.solutions: RCSolution = rc_solution 

77 

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. 

83 

84 Is currently defined by its subclasses using the result object. 

85 

86 Returns 

87 ------- 

88 int : 

89 The index of the vector. 

90 """ 

91 pass 

92 

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 

98 

99 @property 

100 def temperature_vector_pandas(self) -> pd.Series: 

101 """ 

102 The result vector of one node as pandas Series. 

103 

104 Returns 

105 ------- 

106 pd.Series 

107 """ 

108 return self.solutions.temperature_vectors_pandas.iloc[:, self.index] 

109 

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]) 

115 

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) 

124 

125 @property 

126 def symbols(self) -> list: 

127 """ 

128 Returns a list of all sympy.symbols of the object, except time dependent symbols. 

129 

130 Must be in the same order as self.values. 

131 

132 Returns 

133 ------- 

134 list : 

135 The list of sympy.symbols. 

136 """ 

137 return [self.temperature_symbol] 

138 

139 @property 

140 def values(self) -> list: 

141 """ 

142 Returns a list of all values of all object symbols, except of time dependent symbols. 

143 

144 Must be in the same order as self.symbols. 

145 

146 Returns 

147 ------- 

148 list : 

149 The list of sympy.symbols. 

150 """ 

151 return [self.temperature] 

152 

153 def get_resistors_between(self, node) -> list[Resistor]: 

154 """ 

155 Returns all resistors between self and node. 

156 

157 Parameters 

158 ---------- 

159 node : TemperatureNode 

160 The TemperatureNode to which the resistors should go. 

161 

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.") 

172 

173 def filter_resistors_equivalent(self, resistors=None): 

174 """ 

175 Returns all neighbours without the ones that are connected to the same node. 

176 

177 Used for equivalent resistances. 

178 

179 Parameters 

180 ---------- 

181 resistors : list[Resistor], optional 

182 The resistors to filter. 

183 

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) 

202 

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. 

206 

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. 

209 

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