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

155 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# ------------------------------------------------------------------------------ 

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 

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

292 if isinstance(neighbour, BoundaryCondition): 

293 # check, if manual_directions of other neighbour was set 

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

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

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

297 else: 

298 raise NotImplementedError( 

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

300 "BoundaryConditions? Or two Resistors???" 

301 ) 

302 

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

304 """ 

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

306 

307 Parameters 

308 ---------- 

309 asking_node : Capacitor | Node 

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

311 

312 Returns 

313 ------- 

314 TemperatureNode : 

315 The other neighbour of ``self``. 

316 """ 

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

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

319 

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

321 """ 

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

323 the TemperatureNode. 

324 

325 Parameters 

326 ---------- 

327 asking_node : Capacitor | Resistor 

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

329 

330 Returns 

331 ------- 

332 TemperatureNode : 

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

334 """ 

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

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

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

338 seen_nodes = {asking_node, self} 

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

340 

341 from pyrc.core.components.node import TemperatureNode 

342 

343 while to_check: 

344 current = to_check.pop() 

345 if current in seen_nodes: 

346 continue 

347 seen_nodes.add(current) 

348 

349 if isinstance(current, TemperatureNode): 

350 return current 

351 

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

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

354 

355 

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

357 """ 

358 Calculates the equivalent resistance of the connected Resistors. 

359 

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

361 

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

363 thermo-physically. 

364 

365 Parameters 

366 ---------- 

367 start_resistor : Resistor 

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

369 ignore_resistor : Resistor, optional 

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

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

372 Resistors that are actually parallel. 

373 

374 Returns 

375 ------- 

376 np.float64 : 

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

378 """ 

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

380 if ignore_resistor is not None: 

381 try: 

382 parallel_resistors.remove(ignore_resistor) 

383 except ValueError: 

384 # ignore_resistor not in neighbours of start_resistor 

385 pass 

386 parallel_resistance_list = [] 

387 for resistor in parallel_resistors: 

388 parallel_resistance = resistor.resistance 

389 current = resistor 

390 connected: list = current.get_connected(start_resistor) 

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

392 connected = connected[0] 

393 while isinstance(connected, Resistor): 

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

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

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

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

398 ) 

399 parallel_resistance += connected.resistance 

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

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

402 connected = connected[0] 

403 parallel_resistance_list.append(parallel_resistance) 

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