Coverage for pyrc \ core \ components \ resistor.py: 84%

156 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-07 08:20 +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# ------------------------------------------------------------------------------ 

7 

8from __future__ import annotations 

9 

10from _warnings import warn 

11from typing import Any, TYPE_CHECKING 

12 

13import numpy as np 

14from sympy import symbols 

15 

16from pyrc.core.components.templates import ObjectWithPorts, SymbolMixin 

17from pyrc.tools.functions import contains_symbol 

18from pyrc.tools.science import round_valid 

19 

20if TYPE_CHECKING: 

21 from pyrc.core.nodes import Node 

22 from pyrc.core.components.node import TemperatureNode 

23 from pyrc.core.components.capacitor import Capacitor 

24 

25 

26class Resistor(ObjectWithPorts, SymbolMixin): 

27 resistor_counter = 0 

28 

29 def __init__(self, resistance=np.nan): 

30 """ 

31 (Thermal) resistor element of an RC network. 

32 

33 This is the base class of all Resistors and heat and mass transfer/transport elements calculating the 

34 resistance by their own. 

35 

36 Parameters 

37 ---------- 

38 resistance : float | int | np.number 

39 The (thermal) resistance of the `Resistor` in K/W. 

40 """ 

41 ObjectWithPorts.__init__(self) 

42 if not contains_symbol(resistance): 

43 resistance = np.float64(resistance) 

44 self._resistance = resistance 

45 Resistor.resistor_counter += 1 

46 self.id = Resistor.resistor_counter 

47 self.__direct_connected_node_templates = None 

48 

49 # Create sympy symbols to put in the equation(s) 

50 # Has to be at the end of init (after self.id) 

51 self.resistance_symbol = symbols(f"R_{self.id}") 

52 self._equivalent_resistance_symbol = None 

53 

54 @classmethod 

55 def reset_counter(cls): 

56 Resistor.resistor_counter = 0 

57 

58 def __str__(self): 

59 return self.__repr__() 

60 

61 def __repr__(self): 

62 return f"{self.__class__.__name__} {self.id}: 1/R={round_valid(1 / self._resistance, 3)}" 

63 

64 @property 

65 def nodes(self): 

66 """ 

67 Returns both connected nodes, no matter, if other resistors are between them. 

68 

69 Returns 

70 ------- 

71 list : 

72 The Nodes in a list. 

73 """ 

74 result = [] 

75 seen = {self} 

76 to_check = self.neighbours.copy() 

77 

78 from pyrc.core.components.node import TemperatureNode 

79 

80 while to_check and len(result) < 2: 

81 current = to_check.pop() 

82 if current in seen: 

83 continue 

84 seen.add(current) 

85 

86 if isinstance(current, TemperatureNode): 

87 result.append(current) 

88 else: 

89 to_check.extend(n for n in current.neighbours if n not in seen) 

90 assert len(result) <= 2, ( 

91 "Each Resistor should only be connected two a maximum of 2 TemperatureNodes.\nIf it is " 

92 "connected to more than 2 (also over other Resistors) you must insert a Node." 

93 ) 

94 return sorted(result, key=lambda node: node.id) 

95 

96 @property 

97 def direct_connected_node_templates(self): 

98 if self.__direct_connected_node_templates is None: 

99 from pyrc.core.components.node import TemperatureNode 

100 

101 self.__direct_connected_node_templates = [ 

102 neighbour for neighbour in self.neighbours if isinstance(neighbour, TemperatureNode) 

103 ] 

104 return self.__direct_connected_node_templates 

105 

106 @property 

107 def symbols(self) -> list: 

108 # return [self.resistance_symbol, self.equivalent_resistance_symbol] 

109 return [self.equivalent_resistance_symbol] 

110 

111 @property 

112 def values(self) -> list: 

113 # return [self.resistance, self.equivalent_resistance] 

114 return [self.equivalent_resistance] 

115 

116 @property 

117 def all_resistors_inbetween(self) -> list[Resistor]: 

118 """ 

119 Returns all resistors between both connected `TemperatureNode`\\s including self. 

120 

121 Returns 

122 ------- 

123 list[Resistor] : 

124 All Resistors that form the equivalent resistor between both connected `TemperatureNode`\\s. 

125 """ 

126 seen = {self} 

127 to_check = self.neighbours.copy() 

128 while to_check: 

129 current = to_check.pop() 

130 if current not in seen and isinstance(current, Resistor): 

131 seen.add(current) 

132 to_check.extend([n for n in current.neighbours if n not in seen]) 

133 return sorted(list(seen), key=lambda res: res.id) 

134 

135 @property 

136 def equivalent_resistance(self) -> np.float64: 

137 """ 

138 Returns the equivalent resistance from one node to the other. 

139 

140 This considers both serial and parallel Resistors between the same Nodes. 

141 

142 Returns 

143 ------- 

144 np.float64 : 

145 The equivalent resistance from one node to the other. 

146 """ 

147 nodes = self.direct_connected_node_templates 

148 assert len(nodes) > 0, ( 

149 "This property is meant to be executed only for resistors that are connected to one TemperatureNode." 

150 ) 

151 if len(nodes) == 2: 

152 return self.resistance 

153 

154 def run_through_resistors(start_resistor: Resistor, start_node: Capacitor): 

155 _equivalent_serial = start_resistor.resistance 

156 _parallel_resistors = [ 

157 neighbour for neighbour in start_resistor.neighbours if isinstance(neighbour, Resistor) 

158 ] 

159 _current = start_resistor 

160 while len(_parallel_resistors) == 1: 

161 _equivalent_serial += _parallel_resistors[0].resistance 

162 _parallel_resistors, _current = ( 

163 [ 

164 neighbour 

165 for neighbour in _parallel_resistors[0].neighbours 

166 if isinstance(neighbour, Resistor) 

167 and neighbour is not _current 

168 and neighbour.get_connected_node(_parallel_resistors[0]) is not start_node 

169 ], 

170 _parallel_resistors[0], 

171 ) 

172 return _equivalent_serial, _parallel_resistors, _current 

173 

174 # calculate the equivalent resistance 

175 equivalent_serial, parallel_resistors, current = run_through_resistors(self, nodes[0]) 

176 if len(parallel_resistors) > 1: 

177 equivalent_parallel = parallel_equivalent_resistance(current) 

178 else: 

179 # Doing the same thing just the other way round. 

180 # parallel resistors list is empty because the Resistor "current" at the end (right before a Node) 

181 # now you need to check if there are parallel Resistors if walking in the other direction 

182 equivalent_serial, parallel_resistors, current = run_through_resistors( 

183 current, current.direct_connected_node_templates[0] 

184 ) 

185 if len(parallel_resistors) > 1: 

186 equivalent_parallel = parallel_equivalent_resistance(current) 

187 else: 

188 # parallel_resistors is empty 

189 equivalent_parallel = 0 

190 

191 # serial resistance between both equivalents 

192 return equivalent_parallel + equivalent_serial 

193 

194 @property 

195 def equivalent_resistance_symbol(self): 

196 if self._equivalent_resistance_symbol is None: 

197 resistors = self.all_resistors_inbetween 

198 symbol = symbols("_".join([f"{r}" for r in ["R_eq", *[res.id for res in resistors]]])) 

199 

200 # set this symbol to all other resistors (including self) 

201 for r in resistors: 

202 r._equivalent_resistance_symbol = symbol 

203 return self._equivalent_resistance_symbol 

204 

205 # def get_equivalent_resistors(self) -> list: 

206 # """ 

207 # Returns a list with all resistors between the two nodes self is connected with. 

208 # 

209 # Returns 

210 # ------- 

211 # list : 

212 # """ 

213 # node1, node2 = self.nodes 

214 # result = set() 

215 # 

216 # neighbour: Resistor 

217 # for neighbour in node1.neighbours: 

218 # if neighbour.get_connected_node(node1) == node2: 

219 # result.add(neighbour) 

220 # connected_resistors 

221 # 

222 # def get_connected_resistors(self, include_self=False) -> list: 

223 # """ 

224 # Returns a list with all resistors that are connected to this resistor or the connected ones. 

225 # 

226 # Parameters 

227 # ---------- 

228 # include_self : bool, optional 

229 # If True, self is included in the result. 

230 # 

231 # Returns 

232 # ------- 

233 # list : 

234 # All connected resistors. 

235 # """ 

236 # result = [] 

237 # visited = set() 

238 # to_check = [neighbour for neighbour in self.neighbours if isinstance(neighbour, Resistor)] 

239 # 

240 # for resistor in to_check: 

241 # visited.add(resistor) 

242 # 

243 # while to_check: 

244 # resistor = to_check.pop() 

245 # result.append(resistor) 

246 # for neighbour in resistor.neighbours: 

247 # if isinstance(neighbour, Resistor) and neighbour not in visited: 

248 # visited.add(neighbour) 

249 # to_check.append(neighbour) 

250 # 

251 # if include_self: 

252 # result.append(self) 

253 # 

254 # return result 

255 

256 @property 

257 def resistance(self) -> float | int | np.nan: 

258 return self._resistance 

259 

260 def heat_flux(self, asking_node: Capacitor) -> float: 

261 """ 

262 Returns the accumulated heat flux of this node that is transferred into the ``asking_node``. 

263 

264 Parameters 

265 ---------- 

266 asking_node : Capacitor 

267 The Node asking for the heat flux. 

268 If the result is positive that heat flows into the asking_node. 

269 

270 Returns 

271 ------- 

272 float : 

273 The heat flux flowing into the asking_node in W. 

274 """ 

275 if self.neighbours[0] == asking_node: 

276 other_node = self.neighbours[1] 

277 else: 

278 other_node = self.neighbours[0] 

279 other_node: Capacitor 

280 

281 return 1 / self.resistance * (other_node.temperature - asking_node.temperature) 

282 

283 def connect(self, neighbour, direction: tuple | list | np.ndarray | Any = None, node_direction_points_to=None): 

284 super().connect(neighbour, direction, node_direction_points_to) 

285 # check, if direction is set manually if BoundaryCondition and Cell are involved 

286 from pyrc.core.components.templates import Cell 

287 

288 if len(self.nodes) == 2 and (isinstance(self, Cell) or isinstance(neighbour, Cell)): 

289 from pyrc.core.nodes import BoundaryCondition, Node 

290 from pyrc.core.components.templates import Geometric 

291 

292 for i, neighbour in enumerate(self.nodes): 

293 if isinstance(neighbour, BoundaryCondition) and not isinstance(neighbour, Geometric): 

294 # check, if manual_directions of other neighbour was set 

295 if isinstance(self.nodes[(i + 1) % 2], Node): 

296 if neighbour not in self.nodes[(i + 1) % 2].manual_directions: 

297 raise ValueError("Direction must be set manually if BoundaryCondition is involved.") 

298 else: 

299 raise NotImplementedError( 

300 "FAAIIILLL. What kind of object is connected here? Two " 

301 "BoundaryConditions? Or two Resistors???" 

302 ) 

303 

304 def get_connected(self, asking_node: Capacitor | Node | Resistor) -> list[TemperatureNode | Resistor]: 

305 """ 

306 Returns the neighbour that isn't the asking one. 

307 

308 Parameters 

309 ---------- 

310 asking_node : Capacitor | Node 

311 The asking Node. The method will return the other neighbour of ``self``. 

312 

313 Returns 

314 ------- 

315 TemperatureNode : 

316 The other neighbour of ``self``. 

317 """ 

318 assert asking_node in self.neighbours, "The asking_node is not connected to self." 

319 return [n for n in self.neighbours if n is not asking_node] 

320 

321 def get_connected_node(self, asking_node: TemperatureNode | Resistor) -> TemperatureNode: 

322 """ 

323 Returns the connected TemperatureNode that isn't the asking one. It is routed through all connected Resistors to 

324 the TemperatureNode. 

325 

326 Parameters 

327 ---------- 

328 asking_node : Capacitor | Resistor 

329 The asking Node. The method will return the other node connected (over other `Resistor`\\s) to self. 

330 

331 Returns 

332 ------- 

333 TemperatureNode : 

334 The other node connected to ``self``. 

335 """ 

336 assert asking_node in self.neighbours, "The asking_node is not connected to self." 

337 if isinstance(asking_node, Resistor) and len(self.neighbours) > 2: 

338 warn("It is not defined, which node is returned. The first one found is returned.") 

339 seen_nodes = {asking_node, self} 

340 to_check = [n for n in self.neighbours if n is not asking_node] 

341 

342 from pyrc.core.components.node import TemperatureNode 

343 

344 while to_check: 

345 current = to_check.pop() 

346 if current in seen_nodes: 

347 continue 

348 seen_nodes.add(current) 

349 

350 if isinstance(current, TemperatureNode): 

351 return current 

352 

353 to_check.extend(n for n in current.neighbours if n not in seen_nodes) 

354 raise ValueError(f"No connected TemperatureNode found: {self}") 

355 

356 

357def parallel_equivalent_resistance(start_resistor: Resistor, ignore_resistor: Resistor = None) -> np.float64: 

358 """ 

359 Calculates the equivalent resistance of the connected Resistors. 

360 

361 It also checks if the connected Resistors are connected to other Resistors in serial and consider this, too. 

362 

363 This only works, if no other parallel resistors occur. But this should be okay, because this doesn't make any sense 

364 thermo-physically. 

365 

366 Parameters 

367 ---------- 

368 start_resistor : Resistor 

369 The start Resistor, where in one "direction" parallel resistors are connected to (can also be just one). 

370 ignore_resistor : Resistor, optional 

371 If given this Resistor is excluded from the neighbours list of start_resistor. 

372 This is needed to determine some kind of "direction" and only calculate the equivalent resistance for the 

373 Resistors that are actually parallel. 

374 

375 Returns 

376 ------- 

377 np.float64 : 

378 The equivalent resistance of the connected Resistors without the resistance of the start_resistor. 

379 """ 

380 parallel_resistors = [neighbour for neighbour in start_resistor.neighbours if isinstance(neighbour, Resistor)] 

381 if ignore_resistor is not None: 

382 try: 

383 parallel_resistors.remove(ignore_resistor) 

384 except ValueError: 

385 # ignore_resistor not in neighbours of start_resistor 

386 pass 

387 parallel_resistance_list = [] 

388 for resistor in parallel_resistors: 

389 parallel_resistance = resistor.resistance 

390 current = resistor 

391 connected: list = current.get_connected(start_resistor) 

392 assert len(connected) == 1, "Currently in parallel circuits no other nested parallel circuits are allowed." 

393 connected = connected[0] 

394 while isinstance(connected, Resistor): 

395 assert len(connected.neighbours) == 2, ( 

396 "No series of parallel Resistors are allowed. Just one Resistor " 

397 "that is connected to multiple others in parallel\n" 

398 "which themselves can be serial connected to others, but not parallel!" 

399 ) 

400 parallel_resistance += connected.resistance 

401 connected, current = connected.get_connected(current), connected 

402 assert len(connected) == 1, "Currently in parallel circuits no other nested parallel circuits are allowed." 

403 connected = connected[0] 

404 parallel_resistance_list.append(parallel_resistance) 

405 return 1 / np.float64(sum([1 / np.float64(r) for r in parallel_resistance_list]))