iso52016 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iso52016/__init__.py +16 -0
- iso52016/building.py +314 -0
- iso52016/door.py +23 -0
- iso52016/functions.py +241 -0
- iso52016/generals.py +38 -0
- iso52016/ground_temperature.py +262 -0
- iso52016/layer.py +25 -0
- iso52016/material.py +20 -0
- iso52016/node.py +25 -0
- iso52016/opaque_element.py +242 -0
- iso52016/orientation.py +14 -0
- iso52016/thermal_bridges.py +11 -0
- iso52016/ventilation.py +19 -0
- iso52016/weather.py +153 -0
- iso52016/window.py +78 -0
- iso52016/zone.py +101 -0
- iso52016-0.0.1.dist-info/METADATA +109 -0
- iso52016-0.0.1.dist-info/RECORD +21 -0
- iso52016-0.0.1.dist-info/WHEEL +5 -0
- iso52016-0.0.1.dist-info/licenses/LICENSE +29 -0
- iso52016-0.0.1.dist-info/top_level.txt +1 -0
iso52016/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
iso 52016 base instance
|
|
3
|
+
"""
|
|
4
|
+
from .building import Building
|
|
5
|
+
from .door import Door
|
|
6
|
+
from .functions import ThermalCase
|
|
7
|
+
from .generals import general
|
|
8
|
+
from .layer import Layer
|
|
9
|
+
from .material import Material
|
|
10
|
+
from .opaque_element import OpaqueElement
|
|
11
|
+
from .orientation import Orientation
|
|
12
|
+
from .thermal_bridges import ThermalBridge
|
|
13
|
+
from .ventilation import Ventilation
|
|
14
|
+
from .weather import Weather
|
|
15
|
+
from .window import Window
|
|
16
|
+
from .zone import Zone
|
iso52016/building.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
iso 52016 classes
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy import linalg # type: ignore
|
|
10
|
+
|
|
11
|
+
from .functions import ThermalCase, determine_case
|
|
12
|
+
from .generals import SECONDS_PER_HOUR, general
|
|
13
|
+
|
|
14
|
+
if typing.TYPE_CHECKING:
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
|
|
17
|
+
from .weather import Weather
|
|
18
|
+
from .zone import Zone
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Building:
|
|
22
|
+
"""
|
|
23
|
+
Building class for including the zones and weather and performing the calculation
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, weather: Weather, zones: list[Zone] | Zone):
|
|
27
|
+
"""
|
|
28
|
+
init building class
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
weather: Weather
|
|
32
|
+
the weather in which the building is placed
|
|
33
|
+
zones: list[Zone]
|
|
34
|
+
list of all zones in the building
|
|
35
|
+
"""
|
|
36
|
+
self.weather: Weather = weather
|
|
37
|
+
self.zones: list[Zone] = [zones] if not isinstance(zones, list) else zones
|
|
38
|
+
self._basic_matrix: NDArray[np.float64] = np.empty(0)
|
|
39
|
+
|
|
40
|
+
self.res_multiplication: NDArray[np.float64] = np.empty(0)
|
|
41
|
+
self.res_constant: NDArray[np.float64] = np.empty(0)
|
|
42
|
+
|
|
43
|
+
def set_node_indexes(self) -> None:
|
|
44
|
+
"""
|
|
45
|
+
determines all nodes indexes for the matrix calculation
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
None
|
|
49
|
+
"""
|
|
50
|
+
number = 0
|
|
51
|
+
for zone in self.zones:
|
|
52
|
+
zone.operative_temperature.idx = number # + 1
|
|
53
|
+
number += 1
|
|
54
|
+
for area in zone.opaque_elements:
|
|
55
|
+
for node in area.nodes:
|
|
56
|
+
node.idx = number
|
|
57
|
+
number += 1
|
|
58
|
+
for window in area.windows:
|
|
59
|
+
for node in window.nodes:
|
|
60
|
+
node.idx = number
|
|
61
|
+
number += 1
|
|
62
|
+
|
|
63
|
+
zone.air_temperature.idx = number
|
|
64
|
+
zone.heat_load_var.idx = number + 1
|
|
65
|
+
zone.cool_load_var.idx = number + 2
|
|
66
|
+
number += 3
|
|
67
|
+
|
|
68
|
+
def calculate(self) -> None:
|
|
69
|
+
"""
|
|
70
|
+
calculate the loads for all zones in this building
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
None
|
|
75
|
+
"""
|
|
76
|
+
nodes: int = sum(zone.nodes for zone in self.zones)
|
|
77
|
+
for zone in self.zones:
|
|
78
|
+
zone.update_values(self.weather)
|
|
79
|
+
_ = [element.set_default_external_temperature(self.weather.t_air) for element in zone.opaque_elements]
|
|
80
|
+
|
|
81
|
+
self.set_node_indexes()
|
|
82
|
+
self.create_basic_matrix(nodes)
|
|
83
|
+
self.create_results_matrices(nodes)
|
|
84
|
+
temps_old = np.ones(nodes) * 20
|
|
85
|
+
for time_step in range(general.simulation_length):
|
|
86
|
+
temps = self.determine_load(time_step, [ThermalCase.FREE_FLOAT] * len(self.zones), temps_old)
|
|
87
|
+
cases = [determine_case(temps, zone, time_step) for zone in self.zones]
|
|
88
|
+
if any(case != ThermalCase.FREE_FLOAT for case in cases):
|
|
89
|
+
temps = self.determine_load(time_step, cases, temps_old)
|
|
90
|
+
cases_new = [determine_case(temps, zone, time_step) for zone in self.zones]
|
|
91
|
+
if any(case_old != cases_new for case_old, case_new in zip(cases, cases_new)):
|
|
92
|
+
temps = self.determine_load(time_step, cases_new, temps_old)
|
|
93
|
+
|
|
94
|
+
temps_old = temps
|
|
95
|
+
for zone in self.zones:
|
|
96
|
+
zone.air_temperature[time_step] = temps[zone.air_temperature.idx]
|
|
97
|
+
zone.operative_temperature[time_step] = temps[zone.operative_temperature.idx]
|
|
98
|
+
zone.heat_load_var[time_step] = temps[zone.heat_load_var.idx]
|
|
99
|
+
zone.cool_load_var[time_step] = temps[zone.cool_load_var.idx]
|
|
100
|
+
for elem in zone.opaque_elements:
|
|
101
|
+
for node in elem.nodes:
|
|
102
|
+
node[time_step] = temps[node.idx]
|
|
103
|
+
for window in elem.windows:
|
|
104
|
+
for node in window.nodes:
|
|
105
|
+
node[time_step] = temps[node.idx]
|
|
106
|
+
|
|
107
|
+
def determine_load(self, time_step: int, cases: list[ThermalCase], temps_old: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
108
|
+
"""
|
|
109
|
+
solves the matrix
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
time_step: int
|
|
114
|
+
time step
|
|
115
|
+
cases: list[ThermalCase]
|
|
116
|
+
list of thermal cases for the different zones
|
|
117
|
+
temps_old: NDArray[np.float64]
|
|
118
|
+
old temperature results array
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
NDArray[np.float64]:
|
|
122
|
+
solved nodes matrix
|
|
123
|
+
"""
|
|
124
|
+
mat = self._basic_matrix.copy()
|
|
125
|
+
res = self.set_general_result_vector_values(temps_old, time_step)
|
|
126
|
+
for zone, case in zip(self.zones, cases):
|
|
127
|
+
mat[zone.air_temperature.idx, zone.air_temperature.idx] += zone.h_vent[time_step]
|
|
128
|
+
if case == ThermalCase.FREE_FLOAT:
|
|
129
|
+
mat[zone.heat_load_var.idx, zone.heat_load_var.idx] = 1
|
|
130
|
+
mat[zone.cool_load_var.idx, zone.cool_load_var.idx] = 1
|
|
131
|
+
continue
|
|
132
|
+
if case == ThermalCase.HEATING:
|
|
133
|
+
mat[zone.heat_load_var.idx, zone.reference_temp.idx] = 1
|
|
134
|
+
mat[zone.cool_load_var.idx, zone.cool_load_var.idx] = 1
|
|
135
|
+
res[zone.heat_load_var.idx] = zone.t_min[time_step]
|
|
136
|
+
continue
|
|
137
|
+
if case == ThermalCase.MAX_HEATING:
|
|
138
|
+
mat[zone.heat_load_var.idx, zone.heat_load_var.idx] = 1
|
|
139
|
+
mat[zone.cool_load_var.idx, zone.cool_load_var.idx] = 1
|
|
140
|
+
res[zone.heat_load_var.idx] = zone.max_heat
|
|
141
|
+
continue
|
|
142
|
+
if case == ThermalCase.MAX_COOLING:
|
|
143
|
+
mat[zone.heat_load_var.idx, zone.heat_load_var.idx] = 1
|
|
144
|
+
mat[zone.cool_load_var.idx, zone.cool_load_var.idx] = 1
|
|
145
|
+
res[zone.cool_load_var.idx] = zone.max_cool
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
mat[zone.cool_load_var.idx, zone.reference_temp.idx] = 1
|
|
149
|
+
mat[zone.heat_load_var.idx, zone.heat_load_var.idx] = 1
|
|
150
|
+
res[zone.cool_load_var.idx] = zone.t_max[time_step]
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
temps = linalg.lu_solve(
|
|
154
|
+
linalg.lu_factor(mat[: res.size, : res.size], overwrite_a=False, check_finite=False), res, overwrite_b=False, check_finite=False
|
|
155
|
+
)
|
|
156
|
+
except linalg.LinAlgError as exc:
|
|
157
|
+
raise linalg.LinAlgError(
|
|
158
|
+
f"Singular matrix at time step {time_step}. "
|
|
159
|
+
"Check that all building elements have valid thermal properties (non-zero area, conductivity, etc.)."
|
|
160
|
+
) from exc
|
|
161
|
+
return temps
|
|
162
|
+
|
|
163
|
+
def create_basic_matrix(self, nodes: int) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Create the basic matrix to use it later.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
nodes: int
|
|
170
|
+
number of nodes
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
None
|
|
175
|
+
"""
|
|
176
|
+
matrix = np.zeros((nodes, nodes))
|
|
177
|
+
for zone in self.zones:
|
|
178
|
+
self._fill_zone_rows(matrix, zone)
|
|
179
|
+
for elem in zone.opaque_elements:
|
|
180
|
+
self._fill_opaque_element_rows(matrix, zone, elem)
|
|
181
|
+
for window in elem.windows:
|
|
182
|
+
self._fill_window_rows(matrix, zone, elem, window)
|
|
183
|
+
self._basic_matrix = matrix
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _other_surfaces(zone: Zone, exclude_parent: object) -> list:
|
|
187
|
+
"""Collect all surfaces in the zone except *exclude_parent* and its own windows."""
|
|
188
|
+
return zone.opaque_elements + [w for e in zone.opaque_elements for w in e.windows if e != exclude_parent]
|
|
189
|
+
|
|
190
|
+
def _fill_zone_rows(self, matrix: NDArray[np.float64], zone: Zone) -> None:
|
|
191
|
+
"""Fill the air-temperature and operative-temperature rows for a zone."""
|
|
192
|
+
all_surfaces = zone.opaque_elements + [w for e in zone.opaque_elements for w in e.windows]
|
|
193
|
+
matrix[zone.air_temperature.idx, zone.air_temperature.idx] = (
|
|
194
|
+
zone.c_internal / SECONDS_PER_HOUR + sum(s.area * s.h_ci for s in all_surfaces) + zone.h_th_bridge
|
|
195
|
+
)
|
|
196
|
+
matrix[zone.air_temperature.idx, zone.heat_load_var.idx] = -zone.f_heat_cool
|
|
197
|
+
matrix[zone.air_temperature.idx, zone.cool_load_var.idx] = zone.f_heat_cool
|
|
198
|
+
matrix[zone.operative_temperature.idx, zone.operative_temperature.idx] = -2
|
|
199
|
+
matrix[zone.operative_temperature.idx, zone.air_temperature.idx] = 1
|
|
200
|
+
|
|
201
|
+
def _fill_opaque_element_rows(self, matrix: NDArray[np.float64], zone: Zone, elem: object) -> None:
|
|
202
|
+
"""Fill matrix rows for an opaque element's 5 nodes."""
|
|
203
|
+
# zone-level coupling
|
|
204
|
+
matrix[zone.air_temperature.idx, elem.nodes[4].idx] = -1 * elem.area * elem.h_ci
|
|
205
|
+
matrix[zone.operative_temperature.idx, elem.nodes[4].idx] = elem.area / zone.total_area
|
|
206
|
+
# internal surface node (node 4)
|
|
207
|
+
matrix[elem.nodes[4].idx, zone.air_temperature.idx] = -1 * elem.h_ci
|
|
208
|
+
for window in elem.windows:
|
|
209
|
+
matrix[window.nodes[1].idx, zone.air_temperature.idx] = -1 * window.h_ci
|
|
210
|
+
matrix[elem.nodes[4].idx, elem.nodes[3].idx] = -1 * elem.heat_coefficient[3]
|
|
211
|
+
others = [s for s in self._other_surfaces(zone, elem) if s != elem]
|
|
212
|
+
matrix[elem.nodes[4].idx, elem.nodes[4].idx] = (
|
|
213
|
+
elem.heat_coefficient[3]
|
|
214
|
+
+ elem.h_ci
|
|
215
|
+
+ elem.capacities[4] / SECONDS_PER_HOUR
|
|
216
|
+
+ elem.h_ri / zone.total_area * sum(s.area for s in others)
|
|
217
|
+
)
|
|
218
|
+
matrix[elem.nodes[4].idx, zone.heat_load_var.idx] = -1 * (1 - zone.f_heat_cool) / zone.total_area
|
|
219
|
+
matrix[elem.nodes[4].idx, zone.cool_load_var.idx] = (1 - zone.f_heat_cool) / zone.total_area
|
|
220
|
+
# radiative exchange with other surfaces
|
|
221
|
+
for elem_i in zone.opaque_elements:
|
|
222
|
+
if elem_i == elem:
|
|
223
|
+
continue
|
|
224
|
+
matrix[elem.nodes[4].idx, elem_i.nodes[4].idx] = -1 * elem.h_ri * elem_i.area / zone.total_area
|
|
225
|
+
for window_i in elem_i.windows:
|
|
226
|
+
matrix[elem.nodes[4].idx, window_i.nodes[1].idx] = -1 * elem.h_ri * window_i.area / zone.total_area
|
|
227
|
+
# intermediate nodes (1-3)
|
|
228
|
+
for i in range(1, 4):
|
|
229
|
+
matrix[elem.nodes[i].idx, elem.nodes[i - 1].idx] = -1 * elem.heat_coefficient[i - 1]
|
|
230
|
+
matrix[elem.nodes[i].idx, elem.nodes[i].idx] = elem.capacities[i] / SECONDS_PER_HOUR + elem.heat_coefficient[i] + elem.heat_coefficient[i - 1]
|
|
231
|
+
matrix[elem.nodes[i].idx, elem.nodes[i + 1].idx] = -1 * elem.heat_coefficient[i]
|
|
232
|
+
# external node (node 0)
|
|
233
|
+
matrix[elem.nodes[0].idx, elem.nodes[0].idx] = elem.capacities[0] / SECONDS_PER_HOUR + elem.h_ce + elem.h_re + elem.heat_coefficient[0]
|
|
234
|
+
matrix[elem.nodes[0].idx, elem.nodes[1].idx] = -1 * elem.heat_coefficient[0]
|
|
235
|
+
|
|
236
|
+
def _fill_window_rows(self, matrix: NDArray[np.float64], zone: Zone, parent_elem: object, window: object) -> None:
|
|
237
|
+
"""Fill matrix rows for a window's 2 nodes."""
|
|
238
|
+
matrix[zone.air_temperature.idx, window.nodes[1].idx] = -1 * window.area * window.h_ci
|
|
239
|
+
matrix[zone.operative_temperature.idx, window.nodes[1].idx] = window.area / zone.total_area
|
|
240
|
+
# external node
|
|
241
|
+
matrix[window.nodes[0].idx, window.nodes[0].idx] = window.h_ce + window.h_re + window.heat_coefficient
|
|
242
|
+
matrix[window.nodes[0].idx, window.nodes[1].idx] = -1 * window.heat_coefficient
|
|
243
|
+
# internal node
|
|
244
|
+
matrix[window.nodes[1].idx, window.nodes[0].idx] = -1 * window.heat_coefficient
|
|
245
|
+
others = [s for s in self._other_surfaces(zone, parent_elem) if s != parent_elem]
|
|
246
|
+
matrix[window.nodes[1].idx, window.nodes[1].idx] = (
|
|
247
|
+
window.heat_coefficient + window.h_ci + window.h_ri / zone.total_area * sum(s.area for s in others)
|
|
248
|
+
)
|
|
249
|
+
matrix[window.nodes[1].idx, zone.heat_load_var.idx] = -1 * (1 - zone.f_heat_cool) / zone.total_area
|
|
250
|
+
matrix[window.nodes[1].idx, zone.cool_load_var.idx] = (1 - zone.f_heat_cool) / zone.total_area
|
|
251
|
+
# radiative exchange with other surfaces
|
|
252
|
+
for elem_i in zone.opaque_elements:
|
|
253
|
+
if parent_elem == elem_i:
|
|
254
|
+
continue
|
|
255
|
+
matrix[window.nodes[1].idx, elem_i.nodes[4].idx] = -1 * window.h_ri * elem_i.area / zone.total_area
|
|
256
|
+
for window_i in elem_i.windows:
|
|
257
|
+
matrix[window.nodes[1].idx, window_i.nodes[1].idx] = -1 * window.h_ri * window_i.area / zone.total_area
|
|
258
|
+
|
|
259
|
+
def create_results_matrices(self, nodes: int) -> None:
|
|
260
|
+
"""
|
|
261
|
+
create the results matrix
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
nodes: int
|
|
266
|
+
number of nodes
|
|
267
|
+
|
|
268
|
+
Returns
|
|
269
|
+
-------
|
|
270
|
+
None
|
|
271
|
+
"""
|
|
272
|
+
self.res_multiplication = np.zeros((nodes, nodes))
|
|
273
|
+
self.res_constant = np.zeros((nodes, general.simulation_length))
|
|
274
|
+
|
|
275
|
+
for zone in self.zones:
|
|
276
|
+
self.res_multiplication[zone.air_temperature.idx, zone.air_temperature.idx] = zone.c_internal / SECONDS_PER_HOUR
|
|
277
|
+
self.res_constant[zone.air_temperature.idx, :] = (
|
|
278
|
+
sum((vent.h_air * vent.t_supply) for vent in zone.ventilation)
|
|
279
|
+
+ zone.h_th_bridge * self.weather.t_air
|
|
280
|
+
+ zone.internal_gains * zone.f_internal
|
|
281
|
+
+ zone.f_sol * zone.solar_gains
|
|
282
|
+
)
|
|
283
|
+
for elem in zone.opaque_elements:
|
|
284
|
+
self.res_multiplication[elem.nodes[4].idx, elem.nodes[4].idx] = elem.capacities[4] / SECONDS_PER_HOUR
|
|
285
|
+
self.res_constant[elem.nodes[4].idx, :] = ((1 - zone.f_internal) * zone.internal_gains + (1 - zone.f_sol) * zone.solar_gains) / zone.total_area
|
|
286
|
+
for i in range(1, 4):
|
|
287
|
+
self.res_multiplication[elem.nodes[i].idx, elem.nodes[i].idx] = elem.capacities[i] / SECONDS_PER_HOUR
|
|
288
|
+
self.res_multiplication[elem.nodes[0].idx, elem.nodes[0].idx] = elem.capacities[0] / SECONDS_PER_HOUR
|
|
289
|
+
self.res_constant[elem.nodes[0].idx, :] = (
|
|
290
|
+
(elem.h_ce + elem.h_re) * elem.external_temperature + elem.a_sol * (elem.diffuse_irradiation + elem.direct_irradiation) - elem.sky_heat_flux
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
for window in elem.windows:
|
|
294
|
+
self.res_constant[window.nodes[0].idx, :] = (window.h_ce + window.h_re) * window.external_temperature - window.sky_heat_flux
|
|
295
|
+
self.res_constant[window.nodes[1].idx, :] = (
|
|
296
|
+
(1 - zone.f_internal) * zone.internal_gains + (1 - zone.f_sol) * zone.solar_gains
|
|
297
|
+
) / zone.total_area
|
|
298
|
+
|
|
299
|
+
def set_general_result_vector_values(self, temps_old: NDArray[np.float64], time_step: int) -> NDArray[np.float64]:
|
|
300
|
+
"""
|
|
301
|
+
update results vector with new temperatures.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
temps_old: NDArray[np.float64]
|
|
306
|
+
old temperature values
|
|
307
|
+
time_step: int
|
|
308
|
+
current time step
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
NDArray[np.float64]
|
|
313
|
+
"""
|
|
314
|
+
return self.res_multiplication.dot(temps_old) + self.res_constant[:, time_step]
|
iso52016/door.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
script for door class
|
|
3
|
+
"""
|
|
4
|
+
from iso52016.window import Window
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Door(Window):
|
|
8
|
+
"""
|
|
9
|
+
Door data class
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, *, area: float, u_value: float):
|
|
13
|
+
"""
|
|
14
|
+
init window
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
area: float
|
|
19
|
+
window area in m²
|
|
20
|
+
u_value: float
|
|
21
|
+
U value of window in W/m²K
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(area=area, u_value=u_value, g_value=0, frame_fraction=1)
|
iso52016/functions.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
script for different functions
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.interpolate import interp1d
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from numpy.typing import NDArray
|
|
14
|
+
|
|
15
|
+
from iso52016.layer import Layer
|
|
16
|
+
from iso52016.zone import Zone
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ThermalCase(IntEnum):
|
|
20
|
+
"""Thermal calculation cases per ISO 52016."""
|
|
21
|
+
|
|
22
|
+
FREE_FLOAT = 0
|
|
23
|
+
HEATING = 1
|
|
24
|
+
COOLING = 2
|
|
25
|
+
MAX_HEATING = 3
|
|
26
|
+
MAX_COOLING = 4
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Internal convective heat transfer coefficient interpolation anchors [W/(m2K)]
|
|
30
|
+
# per ISO 52016 Table B.12
|
|
31
|
+
H_CI_DOWNWARD = 0.7 # angle 0 deg (heat flow downward)
|
|
32
|
+
H_CI_HORIZONTAL = 2.5 # angle 90 deg (heat flow horizontal)
|
|
33
|
+
H_CI_UPWARD = 5.0 # angle 180 deg (heat flow upward)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def moving_average(vector: NDArray[np.float64], n: int) -> NDArray[np.float64]:
|
|
37
|
+
"""Compute a circular moving average of *vector* with window size *n*."""
|
|
38
|
+
vector = np.append(vector, vector[: n - 1])
|
|
39
|
+
ret = np.cumsum(vector, dtype=float)
|
|
40
|
+
ret[n:] = ret[n:] - ret[:-n]
|
|
41
|
+
return ret[n - 1 :] / n
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def determine_capacity_class(layers: list[Layer]) -> str:
|
|
45
|
+
"""
|
|
46
|
+
determines the capacity class
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
layers: list[Layer]
|
|
51
|
+
layers list
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
str
|
|
56
|
+
"""
|
|
57
|
+
total_capacity = sum(lay.thermal_capacity for lay in layers)
|
|
58
|
+
capacity_class = (
|
|
59
|
+
"IE"
|
|
60
|
+
if layers[0].thermal_capacity > total_capacity * 0.45 and layers[-1].thermal_capacity > total_capacity * 0.45
|
|
61
|
+
else "I"
|
|
62
|
+
if layers[0].thermal_capacity > 2 / 3 * total_capacity
|
|
63
|
+
else "E"
|
|
64
|
+
if layers[-1].thermal_capacity > 2 / 3 * total_capacity
|
|
65
|
+
else "D"
|
|
66
|
+
)
|
|
67
|
+
return capacity_class
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def determine_capacities(layers: list[Layer]) -> list[float]:
|
|
71
|
+
total_capacity = sum(lay.thermal_capacity for lay in layers)
|
|
72
|
+
capacity_class = determine_capacity_class(layers)
|
|
73
|
+
capacities: list[float] = (
|
|
74
|
+
[0, 0, 0, 0, total_capacity]
|
|
75
|
+
if capacity_class == "I"
|
|
76
|
+
else [total_capacity, 0, 0, 0, 0]
|
|
77
|
+
if capacity_class == "E"
|
|
78
|
+
else [total_capacity / 2, 0, 0, 0, total_capacity / 2]
|
|
79
|
+
if capacity_class == "IE"
|
|
80
|
+
else ([total_capacity / 8, total_capacity / 4, total_capacity / 4, total_capacity / 4, total_capacity / 8])
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return capacities
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def determine_capacities_real(layers: list[Layer]) -> list[float]:
|
|
87
|
+
length_n = sum(lay.thickness for lay in layers) / 5
|
|
88
|
+
x_pos = np.cumsum([lay.thickness for lay in layers])
|
|
89
|
+
y_val = np.array([lay.material.density_kg_m3 * lay.material.thermal_capacity_J_kgK for lay in layers])
|
|
90
|
+
x_pos = np.append([0.0], x_pos)
|
|
91
|
+
y_val = np.append(y_val, y_val[-1])
|
|
92
|
+
f = interp1d(x_pos, y_val, kind="previous", fill_value="extrapolate")
|
|
93
|
+
res = [np.mean(f(np.arange(length_n * i, length_n * (i + 1), 0.0001).round(4))) * length_n for i in range(5)]
|
|
94
|
+
return res
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def determine_capacities_in_earth_contact(layers: list[Layer], capacity_ground: float) -> list[float]:
|
|
98
|
+
total_capacity = sum(lay.thermal_capacity for lay in layers)
|
|
99
|
+
capacity_class = determine_capacity_class(layers)
|
|
100
|
+
capacities: list[float] = (
|
|
101
|
+
[0, capacity_ground, 0, 0, total_capacity]
|
|
102
|
+
if capacity_class == "I"
|
|
103
|
+
else [0, capacity_ground, total_capacity, 0, 0]
|
|
104
|
+
if capacity_class == "E"
|
|
105
|
+
else [0, capacity_ground, total_capacity / 2, 0, total_capacity / 2]
|
|
106
|
+
if capacity_class == "IE"
|
|
107
|
+
else ([0, capacity_ground, total_capacity / 4, total_capacity / 2, total_capacity / 4])
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return capacities
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def determine_heat_resistance(layers: list[Layer]) -> list[float]:
|
|
114
|
+
r_tot = sum(lay.thermal_resistance for lay in layers)
|
|
115
|
+
return [6 / r_tot, 3 / r_tot, 3 / r_tot, 6 / r_tot]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def determine_heat_resistance_real(layers: list[Layer]) -> list[float]:
|
|
119
|
+
"""
|
|
120
|
+
determines equally distributed heat resistances
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
layers: list[Layer]
|
|
125
|
+
list of layers
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
list[float]
|
|
130
|
+
"""
|
|
131
|
+
# Calculate the length per quarter
|
|
132
|
+
total_length = sum(layer.thickness for layer in layers)
|
|
133
|
+
length_per_quarter = total_length / 4
|
|
134
|
+
|
|
135
|
+
# Calculate positions and resistance values
|
|
136
|
+
positions = np.cumsum([layer.thickness for layer in layers])
|
|
137
|
+
resistance_values = np.array([0.000_01 / layer.material.conductivity_W_mK for layer in layers])
|
|
138
|
+
|
|
139
|
+
# Ensure the arrays have appropriate lengths for interpolation
|
|
140
|
+
positions = np.append([0.0], positions)
|
|
141
|
+
resistance_values = np.append(resistance_values, resistance_values[-1])
|
|
142
|
+
|
|
143
|
+
# Interpolate the resistance values
|
|
144
|
+
interpolated_function = interp1d(positions, resistance_values, kind="previous", fill_value="extrapolate")
|
|
145
|
+
|
|
146
|
+
# Calculate resistances at quarter lengths
|
|
147
|
+
resistances = [np.sum(interpolated_function(np.arange(length_per_quarter * i, length_per_quarter * (i + 1), 0.000_01).round(5))) for i in range(4)]
|
|
148
|
+
resistances = [1 / val for val in resistances]
|
|
149
|
+
return resistances
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def determine_heat_resistances_in_earth_contact(layers: list[Layer], r_g: float) -> list[float]:
|
|
153
|
+
r_tot = sum(lay.thermal_resistance for lay in layers)
|
|
154
|
+
return [4 / r_tot, 2 / r_tot, 1 / (r_tot / 4 + r_g / 2), 2 / r_g]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def determine_hci(angle: float | int) -> float:
|
|
158
|
+
angle = angle if 0 <= angle <= 180 else -1 * angle if angle < 0 else (360 - angle) # noqa: PLR2004
|
|
159
|
+
if angle < 90: # noqa: PLR2004
|
|
160
|
+
h_ci = H_CI_DOWNWARD + (H_CI_HORIZONTAL - H_CI_DOWNWARD) * angle / 90
|
|
161
|
+
else:
|
|
162
|
+
h_ci = H_CI_HORIZONTAL + (H_CI_UPWARD - H_CI_HORIZONTAL) * (angle - 90) / 90
|
|
163
|
+
return h_ci
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def determine_case(temps: NDArray[np.float64], zone: Zone, time_step: int) -> ThermalCase:
|
|
167
|
+
"""
|
|
168
|
+
Determines the thermal calculation case for the zone.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
temps: NDArray[np.float64]
|
|
173
|
+
previous results
|
|
174
|
+
zone: Zone
|
|
175
|
+
zone to determine case for
|
|
176
|
+
time_step: int
|
|
177
|
+
time step
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
ThermalCase
|
|
182
|
+
"""
|
|
183
|
+
if temps[zone.heat_load_var.idx] > zone.max_heat:
|
|
184
|
+
return ThermalCase.MAX_HEATING
|
|
185
|
+
if temps[zone.cool_load_var.idx] > zone.max_cool:
|
|
186
|
+
return ThermalCase.MAX_COOLING
|
|
187
|
+
if temps[zone.reference_temp.idx] <= zone.t_min[time_step] * 1.000_01:
|
|
188
|
+
return ThermalCase.HEATING
|
|
189
|
+
if temps[zone.reference_temp.idx] >= zone.t_max[time_step] * 0.999_99:
|
|
190
|
+
return ThermalCase.COOLING
|
|
191
|
+
|
|
192
|
+
return ThermalCase.FREE_FLOAT
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def calculate_mean_temperature(
|
|
196
|
+
annual_mean: float, amplitude_int: float, minimal_temperature_month: float | int, month: float | NDArray[np.float64]
|
|
197
|
+
) -> float | NDArray[np.float64]:
|
|
198
|
+
"""
|
|
199
|
+
Calculates the monthly mean temperature for the given month number
|
|
200
|
+
|
|
201
|
+
:param annual_mean: float, annual mean of yearly temperature, in °C
|
|
202
|
+
:param amplitude_int: float, amplitude of fluctuations of the monthly mean of yearly temperature, in K
|
|
203
|
+
:param minimal_temperature_month: int, the month number with the lowest exterior temperature
|
|
204
|
+
:param month: int, the number of the month
|
|
205
|
+
:return: float, the calculated monthly mean of indoor temperature for the given month number
|
|
206
|
+
"""
|
|
207
|
+
return annual_mean - amplitude_int * np.cos(2 * np.pi * (month - minimal_temperature_month) / 12)
|
|
208
|
+
|
|
209
|
+
def are_months_close(month1: float, month2: float) -> bool:
|
|
210
|
+
# Ensure months are within the range of 1 to 12
|
|
211
|
+
month1 = (month1 - 1) % 12 + 1
|
|
212
|
+
month2 = (month2 - 1) % 12 + 1
|
|
213
|
+
|
|
214
|
+
# Calculate the difference in months considering the circular nature of months
|
|
215
|
+
month_difference = min((month2 - month1) % 12, (month1 - month2) % 12)
|
|
216
|
+
|
|
217
|
+
# Check if the months are close (within 1 months difference, considering circularity)
|
|
218
|
+
return month_difference <= 1
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_yearly_mean_amplitude_min_temp_month(temperature: NDArray[np.float64]) -> tuple[float, float, float]:
|
|
222
|
+
"""
|
|
223
|
+
determines the yearly mean temperature, the month amplitude and the minimal temperate month.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
temperature: NDArray[np.float64]
|
|
228
|
+
hourly temperature profile
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
yearly mean, monthly amplitude, minimal temperature month
|
|
233
|
+
"""
|
|
234
|
+
t_mean: float = np.mean(temperature)
|
|
235
|
+
month_hours: int = int(len(temperature) / 12)
|
|
236
|
+
t_month_mean: NDArray[np.float64] = np.array([np.mean(temperature[t : t + month_hours]) for t in range(0, len(temperature), month_hours)])
|
|
237
|
+
n_min_month = float(t_month_mean.argmin())
|
|
238
|
+
n_min_hourly = temperature.argmin() / len(temperature) * 12
|
|
239
|
+
n_min = n_min_hourly if are_months_close(n_min_month, n_min_hourly) else n_min_month
|
|
240
|
+
t_amplitude = np.max(np.abs(t_mean - t_month_mean))
|
|
241
|
+
return t_mean, t_amplitude, n_min
|
iso52016/generals.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
script for general properties
|
|
3
|
+
"""
|
|
4
|
+
import dataclasses
|
|
5
|
+
|
|
6
|
+
from iso52016.material import Material
|
|
7
|
+
|
|
8
|
+
# Default heat transfer coefficients per ISO 52016 / ISO 6946 [W/(m2K)]
|
|
9
|
+
H_CE_DEFAULT = 20.0 # external convective
|
|
10
|
+
H_RE_DEFAULT = 4.14 # external radiative
|
|
11
|
+
H_RI_DEFAULT = 5.13 # internal radiative
|
|
12
|
+
|
|
13
|
+
# Default solar absorption factor for opaque surfaces [-]
|
|
14
|
+
A_SOL_DEFAULT = 0.6
|
|
15
|
+
|
|
16
|
+
# Zone distribution factors per ISO 52016 Table B.11 [-]
|
|
17
|
+
F_INTERNAL_DEFAULT = 0.4 # fraction of internal gains to air node
|
|
18
|
+
F_HEAT_COOL_DEFAULT = 0.4 # fraction of heating/cooling to air node
|
|
19
|
+
F_SOL_DEFAULT = 0.1 # fraction of solar gains to air node
|
|
20
|
+
|
|
21
|
+
# Maximum load limiter [W/m2]
|
|
22
|
+
MAX_LOAD_PER_M2 = 100_000
|
|
23
|
+
|
|
24
|
+
# Seconds per hour (for capacity / time-step conversion)
|
|
25
|
+
SECONDS_PER_HOUR = 3600
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclasses.dataclass(slots=True)
|
|
29
|
+
class General:
|
|
30
|
+
"""
|
|
31
|
+
Dataclass for general simulation properties
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
air: Material
|
|
35
|
+
simulation_length: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
general = General(Material(1.204, 1006, 0.02), 8760)
|