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
« 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# ------------------------------------------------------------------------------
8from typing import Any
10import numpy as np
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
20class Facade(RCNetwork):
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.
36 #TODO: update, because a lot of new algorithms make the build up easier and faster since this model was created
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()
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 ]
93 self.height = height
94 self.length = length
95 self.nodes_in_length = nodes_in_length
96 self.nodes_in_height = nodes_in_height
98 self.ambient_htc = ambient_htc # W/m/m/K
99 self.interior_htc = interior_htc # W/m/m/K
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
108 self.epsilon_short = 0.7
110 def _create_network(self):
111 # define thicknesses
112 layers = self.layers
113 materials = self.materials
115 ambient_temperature = self.ambient_temperature
117 interior_temperature = self.interior_temperature
119 initial_solid_temperature = self.initial_solid_temperature
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
126 y_height = self.height
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))]
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 )
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 = []
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)
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)
252 # region Create Resistors and connect them to the nodes.
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())
266 # connect it to the nodes
267 resistors[-1].connect(node1)
268 resistors[-1].connect(node2)
270 # check if also other resistors must be added.
271 if (isinstance(node2, MassFlowNode) and
272 isinstance(node1, MassFlowNode)):
273 resistors.append(MassTransport())
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
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())
291 # connect it to the nodes
292 resistors[-1].connect(node1)
293 resistors[-1].connect(node2)
295 # check if also other resistors must be added.
296 if (isinstance(node2, MassFlowNode) or
297 isinstance(node1, MassFlowNode)):
298 resistors.append(HeatTransfer())
300 # connect it to the nodes
301 resistors[-1].connect(node1)
302 resistors[-1].connect(node2)
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())
319 # connect it to the nodes
320 resistors[-1].connect(node1)
321 resistors[-1].connect(node2)
323 # check if also other resistors must be added.
324 if (isinstance(node2, MassFlowNode) or
325 isinstance(node1, MassFlowNode)):
326 resistors.append(HeatTransfer())
328 # connect it to the nodes
329 resistors[-1].connect(node1)
330 resistors[-1].connect(node2)
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)
342 # endregion
344 self.rc_objects.set_lists(capacitors=nodes_list, resistors=resistors, boundaries=boundaries_list)
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]
363 print(f"Number nodes: {len(nodes_list)}")
364 print(f"Number resistors: {len(resistors)}")
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
376def get_last_y_node(nodes: list, x, y, z) -> Node:
377 """
378 Returns the node that is the last of the y direction.
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.
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.
386 Parameters
387 ----------
388 nodes
389 x
390 y
391 z
393 Returns
394 -------
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
410def lengths_to_position(the_list: list, index: int):
411 if index == 0:
412 return 0
414 length = the_list[index] / 2 + the_list[0] / 2
416 # sum all lengths up that lay between 0 and index:
417 for i in range(1, index):
418 length += the_list[i]
420 return length