Coverage for pyrc \ model \ facade.py: 69%

157 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 typing import Any 

9 

10import numpy as np 

11 

12from pyrc.core.inputs import Radiation, ExteriorBoundaryConditionGeometric, InteriorBoundaryConditionGeometric 

13from pyrc.core.materials import Air, Plaster, SandLimeBrick, EPS032 

14from pyrc.core.network import RCNetwork 

15from pyrc.core.nodes import Node, MassFlowNode 

16from pyrc.core.resistors import HeatConduction, MassTransport, HeatTransfer 

17from pyrc.tools.science import celsius_to_kelvin 

18 

19 

20class Facade(RCNetwork): 

21 

22 def __init__(self, 

23 *args, 

24 height=0.5, 

25 length=11, 

26 nodes_in_length=1, 

27 nodes_in_height=1, 

28 one_y_node_z_layer_indexes: list | Any = None, 

29 ambient_htc: float | int = 7, 

30 interior_htc: float | int = 10, 

31 specific_power_in_w_per_meter_squared: float | int = 200, 

32 **kwargs): 

33 """ 

34 A Facade RC network to simulate different wall layers. 

35 

36 #TODO: update, because a lot of new algorithms make the build up easier and faster since this model was created 

37 

38 Parameters 

39 ---------- 

40 args : list 

41 The arguments for `RCNetwork` . 

42 one_y_node_z_layer_indexes : list | Any, optional 

43 A list with z layer indexes where only one y node should be applied. 

44 The numbers must match the indexes of the z_list (layers list). 

45 This is used to minimize the number of nodes in the network where it is not necessary to resolve in this 

46 detail. 

47 Currently, it is only working when the region is at the end of the layer (and maybe at the beginning). 

48 So, using [n-2, n-1, n] is working, but not [n-3, n-2, n-1] because the last layer is not chosen. 

49 To implement it properly the connection between the layers must be modified. 

50 ambient_htc : float | int, optional 

51 The heat transfer coefficient on the outer wall surface. 

52 interior_htc : float | int, optional 

53 The heat transfer coefficient on the inner wall surface. 

54 specific_power_in_w_per_meter_squared : float | int, optional 

55 The specific power in W/m^2 squared of all Radiation objects. 

56 Only used when weather data is not used. 

57 kwargs 

58 """ 

59 super().__init__(*args, **kwargs) 

60 self.air = Air() 

61 self.plaster = Plaster() 

62 self.eps = EPS032() 

63 sand_lime_brick = SandLimeBrick() 

64 

65 # At these layers only one node is used for the y direction. 

66 # Used to minimize the number of nodes in the network and fasten calculation. 

67 if one_y_node_z_layer_indexes is None: 

68 one_y_node_z_layer_indexes = [] 

69 self.one_y_node_z_layer_indexes = one_y_node_z_layer_indexes 

70 self.layers = [ 

71 0.001, 

72 0.003, 

73 0.1, 

74 0.1, 

75 0.1, 

76 0.05, 

77 0.05, 

78 0.075, 

79 0.016 

80 ] 

81 self.materials = [ 

82 self.plaster, 

83 self.plaster, 

84 self.eps, 

85 self.eps, 

86 self.eps, 

87 sand_lime_brick, 

88 sand_lime_brick, 

89 sand_lime_brick, 

90 self.plaster 

91 ] 

92 

93 self.height = height 

94 self.length = length 

95 self.nodes_in_length = nodes_in_length 

96 self.nodes_in_height = nodes_in_height 

97 

98 self.ambient_htc = ambient_htc # W/m/m/K 

99 self.interior_htc = interior_htc # W/m/m/K 

100 

101 # initial values, only used if weather data is not used 

102 self.ambient_temperature = celsius_to_kelvin(-11) 

103 self.interior_temperature = celsius_to_kelvin(20) 

104 self.initial_solid_temperature = celsius_to_kelvin(11) 

105 self.initial_fluid_temperature = celsius_to_kelvin(-11) 

106 self.specific_power_in_w_per_meter_squared = specific_power_in_w_per_meter_squared 

107 

108 self.epsilon_short = 0.7 

109 

110 def _create_network(self): 

111 # define thicknesses 

112 layers = self.layers 

113 materials = self.materials 

114 

115 ambient_temperature = self.ambient_temperature 

116 

117 interior_temperature = self.interior_temperature 

118 

119 initial_solid_temperature = self.initial_solid_temperature 

120 

121 # make lengths lists for subgroup 

122 x_list = [self.length / self.nodes_in_length] * self.nodes_in_length 

123 y_list = [self.height / self.nodes_in_height] * self.nodes_in_height 

124 z_list = layers 

125 

126 y_height = self.height 

127 

128 # build up the network, starting with the upper right corner as 0,0,0 

129 # place cells 

130 subgroup_nodes: list = [[[None for _ in range(len(z_list))] for _ in range(len(y_list))] for _ in 

131 range(len(x_list))] 

132 

133 # first cell is positioned on 0, so the middle is shifted by the first height/2 

134 y_pos = y_height / 2 - y_list[0] / 2 

135 for zi, z in enumerate(z_list): 

136 if zi == 0: 

137 if 0 in self.one_y_node_z_layer_indexes: 

138 # Radiation to every outer node 

139 for xi, x in enumerate(x_list): 

140 subgroup_nodes[xi][0][zi] = Node( 

141 material=materials[zi], 

142 temperature=initial_solid_temperature, 

143 internal_heat_source=True, 

144 internal_heat_source_type=Radiation, 

145 position=np.array([ 

146 lengths_to_position(x_list, xi), 

147 y_pos, 

148 lengths_to_position(z_list, zi) 

149 ]), 

150 delta=(x, y_height, z), 

151 specific_power_in_w_per_meter_squared=self.specific_power_in_w_per_meter_squared, 

152 area_direction=np.array([0, 0, -1]), 

153 settings=self.settings, 

154 rc_objects=self.rc_objects, 

155 rc_solution=self.rc_solution, 

156 epsilon_short=self.epsilon_short 

157 ) 

158 else: 

159 for yi, y in enumerate(y_list): 

160 # Radiation to every outer node 

161 for xi, x in enumerate(x_list): 

162 subgroup_nodes[xi][yi][zi] = Node( 

163 material=materials[zi], 

164 temperature=initial_solid_temperature, 

165 internal_heat_source=True, 

166 internal_heat_source_type=Radiation, 

167 position=np.array([ 

168 lengths_to_position(x_list, xi), 

169 lengths_to_position(y_list, yi), 

170 lengths_to_position(z_list, zi) 

171 ]), 

172 delta=(x, y, z), 

173 specific_power_in_w_per_meter_squared=self.specific_power_in_w_per_meter_squared, 

174 area_direction=np.array([0, 0, -1]), 

175 settings=self.settings, 

176 rc_objects=self.rc_objects, 

177 rc_solution=self.rc_solution, 

178 epsilon_short=self.epsilon_short 

179 ) 

180 elif zi not in self.one_y_node_z_layer_indexes: 

181 for yi, y in enumerate(y_list): 

182 for xi, x in enumerate(x_list): 

183 subgroup_nodes[xi][yi][zi] = Node( 

184 material=materials[zi], 

185 temperature=initial_solid_temperature, 

186 position=np.array([ 

187 lengths_to_position(x_list, xi), 

188 lengths_to_position(y_list, yi), 

189 lengths_to_position(z_list, zi) 

190 ]), 

191 delta=(x, y, z), 

192 rc_objects=self.rc_objects, 

193 rc_solution=self.rc_solution, 

194 ) 

195 else: # only one node over y direction 

196 for xi, x in enumerate(x_list): 

197 subgroup_nodes[xi][0][zi] = Node( 

198 material=materials[zi], 

199 temperature=initial_solid_temperature, 

200 position=np.array([ 

201 lengths_to_position(x_list, xi), 

202 y_pos, 

203 lengths_to_position(z_list, zi) 

204 ]), 

205 delta=(x, y_height, z), 

206 rc_objects=self.rc_objects, 

207 rc_solution=self.rc_solution, 

208 ) 

209 

210 nodes_list = [ 

211 x 

212 for xss in subgroup_nodes 

213 for xs in xss 

214 for x in xs 

215 if x is not None 

216 ] 

217 boundaries_list: list = [] 

218 

219 distance_bc_to_wall = 0.3 

220 # add boundary conditions 

221 bc_outer_position = 0.5 * ( 

222 subgroup_nodes[-1][0][0].position - get_last_y_node(subgroup_nodes, 0, -1, 0).position) + np.array( 

223 [0, 0, -distance_bc_to_wall]) + get_last_y_node(subgroup_nodes, 0, -1, 0).position 

224 bc_outer = ExteriorBoundaryConditionGeometric( 

225 temperature=ambient_temperature, 

226 position=bc_outer_position, 

227 is_mass_flow_start=False, 

228 heat_transfer_coefficient=self.ambient_htc, 

229 temperature_delta=10, 

230 middle_temperature=celsius_to_kelvin(-5), 

231 settings=self.settings, 

232 rc_objects=self.rc_objects, 

233 rc_solution=self.rc_solution, 

234 ) 

235 boundaries_list.append(bc_outer) 

236 

237 bc_inner_position = ( 

238 0.5 * (subgroup_nodes[-1][0][-1].position - get_last_y_node(subgroup_nodes, 0, -1, 

239 -1).position) + np.array( 

240 [0, 0, distance_bc_to_wall]) + get_last_y_node(subgroup_nodes, 0, -1, -1).position) 

241 bc_inner = InteriorBoundaryConditionGeometric( 

242 temperature=interior_temperature, 

243 position=bc_inner_position, 

244 is_mass_flow_start=False, 

245 heat_transfer_coefficient=self.interior_htc, 

246 settings=self.settings, 

247 rc_objects=self.rc_objects, 

248 rc_solution=self.rc_solution, 

249 ) 

250 boundaries_list.append(bc_inner) 

251 

252 # region Create Resistors and connect them to the nodes. 

253 

254 # initialize lists for resistors 

255 resistors = [] 

256 # connect all nodes 

257 for x in range(len(x_list) - 1): 

258 for y in range(len(y_list)): 

259 for z in range(len(z_list)): 

260 node1 = subgroup_nodes[x][y][z] 

261 node2 = subgroup_nodes[x + 1][y][z] 

262 if node1 is None or node2 is None: 

263 continue 

264 resistors.append(HeatConduction()) 

265 

266 # connect it to the nodes 

267 resistors[-1].connect(node1) 

268 resistors[-1].connect(node2) 

269 

270 # check if also other resistors must be added. 

271 if (isinstance(node2, MassFlowNode) and 

272 isinstance(node1, MassFlowNode)): 

273 resistors.append(MassTransport()) 

274 

275 # connect it to the nodes 

276 resistors[-1].connect(node1) 

277 resistors[-1].connect(node2) 

278 resistors[-1].source = node1 

279 resistors[-1].sink = node2 

280 

281 # walk over y-axis 

282 for y in range(len(y_list) - 1): 

283 for x in range(len(x_list)): 

284 for z in range(len(z_list)): 

285 node1 = subgroup_nodes[x][y][z] 

286 node2 = subgroup_nodes[x][y + 1][z] 

287 if node1 is None or node2 is None: 

288 continue 

289 resistors.append(HeatConduction()) 

290 

291 # connect it to the nodes 

292 resistors[-1].connect(node1) 

293 resistors[-1].connect(node2) 

294 

295 # check if also other resistors must be added. 

296 if (isinstance(node2, MassFlowNode) or 

297 isinstance(node1, MassFlowNode)): 

298 resistors.append(HeatTransfer()) 

299 

300 # connect it to the nodes 

301 resistors[-1].connect(node1) 

302 resistors[-1].connect(node2) 

303 

304 # walk over z-axis 

305 for z in range(len(z_list) - 1): 

306 for x in range(len(x_list)): 

307 for y in range(len(y_list)): 

308 node1 = subgroup_nodes[x][y][z] 

309 node2 = subgroup_nodes[x][y][z + 1] 

310 if node1 is None and node2 is None: 

311 continue 

312 elif node1 is None: 

313 # only node1 is None 

314 node1 = get_last_y_node(subgroup_nodes, x, y, z) 

315 elif node2 is None: 

316 node2 = get_last_y_node(subgroup_nodes, x, y, z + 1) 

317 resistors.append(HeatConduction()) 

318 

319 # connect it to the nodes 

320 resistors[-1].connect(node1) 

321 resistors[-1].connect(node2) 

322 

323 # check if also other resistors must be added. 

324 if (isinstance(node2, MassFlowNode) or 

325 isinstance(node1, MassFlowNode)): 

326 resistors.append(HeatTransfer()) 

327 

328 # connect it to the nodes 

329 resistors[-1].connect(node1) 

330 resistors[-1].connect(node2) 

331 

332 # connect front/back nodes (to bc outer/inner) 

333 for x, y in zip(*[range(len(x_list))] * 2): 

334 for z, bc, direction in [(0, bc_outer, (0, 0, -1)), (-1, bc_inner, (0, 0, 1))]: 

335 node: Node = subgroup_nodes[x][y][z] 

336 if node is None: 

337 continue 

338 resistors.append(HeatTransfer()) 

339 resistors[-1].connect(node, direction=direction, node_direction_points_to=bc) 

340 resistors[-1].connect(bc) 

341 

342 # endregion 

343 

344 self.rc_objects.set_lists(capacitors=nodes_list, resistors=resistors, boundaries=boundaries_list) 

345 

346 # create some groups to use them later for postprocessing 

347 self.groups[f"layers"] = [ 

348 [[item for b in subgroup_nodes[l] for item in b if item is not None] for l in range(len(x_list))], 

349 [[item for a in subgroup_nodes for item in a[l] if item is not None] for l in range(len(y_list))], 

350 [[item[l] for a in subgroup_nodes for item in a if (item is not None and item[l] is not None)] for l in 

351 range( 

352 len(z_list))], 

353 ] 

354 self.groups["outer_layer"] = [z_list[0] for y_list in subgroup_nodes for z_list in y_list if 

355 z_list[0] is not None] 

356 self.groups["inner_layer"] = [z_list[-1] for y_list in subgroup_nodes for z_list in y_list if 

357 z_list[-1] is not None] 

358 self.groups["flux_bcs"] = [bc_outer, bc_inner] 

359 self.groups["internal_heat_sources"] = [ihc.internal_heat_source for ihc in nodes_list if 

360 ihc is not None and 

361 ihc.internal_heat_source is not None] 

362 

363 print(f"Number nodes: {len(nodes_list)}") 

364 print(f"Number resistors: {len(resistors)}") 

365 

366 

367def get_last_y_object(nodes: list, x, z) -> Node: 

368 y = len(nodes) - 1 

369 last_element = nodes[x][y][z] 

370 while last_element is None and y > 0: 

371 y -= 1 

372 last_element = nodes[x][y][z] 

373 return last_element 

374 

375 

376def get_last_y_node(nodes: list, x, y, z) -> Node: 

377 """ 

378 Returns the node that is the last of the y direction. 

379 

380 E.g. if only one node is created in y direction but other x-z-layers have more y-nodes, the single node is stored in 

381 index 0. It is returned instead of the None value in the nodes list. 

382 

383 Correcter way would be to check, which node is the neighbour by using position and delta values, but it is 

384 heavier to calculate. 

385 

386 Parameters 

387 ---------- 

388 nodes 

389 x 

390 y 

391 z 

392 

393 Returns 

394 ------- 

395 

396 """ 

397 y = y + len(nodes[x]) if y <= 0 else y 

398 node = nodes[x][y][z] 

399 y_counter = y 

400 while node is None and y_counter > 0: 

401 y_counter -= 1 

402 node = nodes[x][y_counter][z] 

403 while node is None and y < len(nodes) - 1: 

404 # if not working, search in positive direction for a node and take it. 

405 y += 1 

406 node = nodes[x][y][z] 

407 return node 

408 

409 

410def lengths_to_position(the_list: list, index: int): 

411 if index == 0: 

412 return 0 

413 

414 length = the_list[index] / 2 + the_list[0] / 2 

415 

416 # sum all lengths up that lay between 0 and index: 

417 for i in range(1, index): 

418 length += the_list[i] 

419 

420 return length