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 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)