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

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 

10 

11import numpy as np 

12import pandas as pd 

13from sympy import Basic, symbols 

14 

15from pyrc.core.components.templates import ( 

16 ObjectWithPorts, 

17 EquationItem, 

18 RCObjects, 

19 initial_rc_objects, 

20 RCSolution, 

21 solution_object, 

22) 

23 

24if TYPE_CHECKING: 

25 from pyrc.core.components.resistor import Resistor 

26 

27 

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. 

38 

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 

59 

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

61 self.initial_temperature = temperature 

62 self.temperature_derivative = temperature_derivative 

63 

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

65 

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

67 

68 # make space for results 

69 self.solutions: RCSolution = rc_solution 

70 

71 # Cashing 

72 self.__connected_mass_flow_nodes = None 

73 

74 # make space for results 

75 self.solutions: RCSolution = rc_solution 

76 

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. 

82 

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

84 

85 Returns 

86 ------- 

87 int : 

88 The index of the vector. 

89 """ 

90 pass 

91 

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 

97 

98 @property 

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

100 """ 

101 The result vector of one node as pandas Series. 

102 

103 Returns 

104 ------- 

105 pd.Series 

106 """ 

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

108 

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

114 

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) 

123 

124 @property 

125 def symbols(self) -> list: 

126 """ 

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

128 

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

130 

131 Returns 

132 ------- 

133 list : 

134 The list of sympy.symbols. 

135 """ 

136 return [self.temperature_symbol] 

137 

138 @property 

139 def values(self) -> list: 

140 """ 

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

142 

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

144 

145 Returns 

146 ------- 

147 list : 

148 The list of sympy.symbols. 

149 """ 

150 return [self.temperature] 

151 

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

153 """ 

154 Returns all resistors between self and node. 

155 

156 Parameters 

157 ---------- 

158 node : TemperatureNode 

159 The TemperatureNode to which the resistors should go. 

160 

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

171 

172 def filter_resistors_equivalent(self, resistors=None): 

173 """ 

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

175 

176 Used for equivalent resistances. 

177 

178 Parameters 

179 ---------- 

180 resistors : list[Resistor], optional 

181 The resistors to filter. 

182 

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) 

201 

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. 

205 

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. 

208 

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