iso52016 0.0.1__tar.gz
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-0.0.1/LICENSE +29 -0
- iso52016-0.0.1/PKG-INFO +109 -0
- iso52016-0.0.1/README.md +73 -0
- iso52016-0.0.1/iso52016/__init__.py +16 -0
- iso52016-0.0.1/iso52016/building.py +314 -0
- iso52016-0.0.1/iso52016/door.py +23 -0
- iso52016-0.0.1/iso52016/functions.py +241 -0
- iso52016-0.0.1/iso52016/generals.py +38 -0
- iso52016-0.0.1/iso52016/ground_temperature.py +262 -0
- iso52016-0.0.1/iso52016/layer.py +25 -0
- iso52016-0.0.1/iso52016/material.py +20 -0
- iso52016-0.0.1/iso52016/node.py +25 -0
- iso52016-0.0.1/iso52016/opaque_element.py +242 -0
- iso52016-0.0.1/iso52016/orientation.py +14 -0
- iso52016-0.0.1/iso52016/thermal_bridges.py +11 -0
- iso52016-0.0.1/iso52016/ventilation.py +19 -0
- iso52016-0.0.1/iso52016/weather.py +153 -0
- iso52016-0.0.1/iso52016/window.py +78 -0
- iso52016-0.0.1/iso52016/zone.py +101 -0
- iso52016-0.0.1/iso52016.egg-info/PKG-INFO +109 -0
- iso52016-0.0.1/iso52016.egg-info/SOURCES.txt +38 -0
- iso52016-0.0.1/iso52016.egg-info/dependency_links.txt +1 -0
- iso52016-0.0.1/iso52016.egg-info/requires.txt +16 -0
- iso52016-0.0.1/iso52016.egg-info/top_level.txt +1 -0
- iso52016-0.0.1/pyproject.toml +79 -0
- iso52016-0.0.1/setup.cfg +59 -0
- iso52016-0.0.1/setup.py +4 -0
- iso52016-0.0.1/tests/test_ASHRAE140_600.py +95 -0
- iso52016-0.0.1/tests/test_determine_hci.py +10 -0
- iso52016-0.0.1/tests/test_functions.py +125 -0
- iso52016-0.0.1/tests/test_ground_temperature.py +60 -0
- iso52016-0.0.1/tests/test_industrial.py +127 -0
- iso52016-0.0.1/tests/test_iso52016_heavy_example.py +293 -0
- iso52016-0.0.1/tests/test_iso52016_light_example.py +283 -0
- iso52016-0.0.1/tests/test_layer.py +104 -0
- iso52016-0.0.1/tests/test_multizone.py +60 -0
- iso52016-0.0.1/tests/test_simple_cases.py +694 -0
- iso52016-0.0.1/tests/test_validation.py +69 -0
- iso52016-0.0.1/tests/test_weather.py +116 -0
iso52016-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, Tobias Blanke
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
iso52016-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iso52016
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python package for a ISO 52016 calculation
|
|
5
|
+
Home-page: https://git.fh-aachen.de/tb5152e/din-en-iso-52016
|
|
6
|
+
Author: Tobias Blanke
|
|
7
|
+
Author-email: blanke@fh-aachen.de
|
|
8
|
+
License: BSD-3-Clause
|
|
9
|
+
Keywords: iso52016,building,energy,thermal,hvac,heating,cooling
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy>=1.24.4
|
|
21
|
+
Requires-Dist: pandas>=2.0.3
|
|
22
|
+
Requires-Dist: pvlib>=0.10.1
|
|
23
|
+
Requires-Dist: scipy>=1.10.1
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: flake8>=4.0.1; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest>=7.1.2; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=0.961; extra == "dev"
|
|
29
|
+
Requires-Dist: bandit>=1.7.4; extra == "dev"
|
|
30
|
+
Requires-Dist: pylint>=2.14.5; extra == "dev"
|
|
31
|
+
Requires-Dist: isort>=5.10.1; extra == "dev"
|
|
32
|
+
Requires-Dist: black>=22.6.0; extra == "dev"
|
|
33
|
+
Requires-Dist: anybadge==1.9.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pandas-stubs>=2.1.1; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# DIN EN ISO 52016
|
|
38
|
+
|
|
39
|
+
[](https://git.fh-aachen.de/tb5152e/din-en-iso-52016/-/commits/main)
|
|
40
|
+
[](https://git.fh-aachen.de/tb5152e/din-en-iso-52016/-/commits/main)
|
|
41
|
+
[](https://din-en-iso-52016.readthedocs.io/en/latest/?badge=latest)
|
|
42
|
+
[](https://github.com/psf/black)
|
|
43
|
+
|
|
44
|
+
Python implementation of the **DIN EN ISO 52016** standard for calculating the energy performance of buildings. The package computes hourly heating and cooling loads for thermal zones using a 5-node thermal-network model per opaque element, solar irradiation via `pvlib`, and ground-temperature coupling per ISO 13370.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or for development:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e ".[dev]"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import numpy as np
|
|
62
|
+
from iso52016 import (
|
|
63
|
+
Building, Layer, Material, OpaqueElement, Orientation,
|
|
64
|
+
Ventilation, Weather, Window, Zone,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Create materials and layers
|
|
68
|
+
concrete = Material(density_kg_m3=2300, thermal_capacity_J_kgK=880, conductivity_W_mK=1.7)
|
|
69
|
+
insulation = Material(density_kg_m3=20, thermal_capacity_J_kgK=1450, conductivity_W_mK=0.035)
|
|
70
|
+
wall_layers = [Layer(0.2, concrete), Layer(0.1, insulation)]
|
|
71
|
+
|
|
72
|
+
# Set up weather (8760 hourly values)
|
|
73
|
+
weather = Weather(air_temperature=t_air, wind=wind)
|
|
74
|
+
|
|
75
|
+
# Define building envelope
|
|
76
|
+
wall = OpaqueElement(area=50, orientation=Orientation(90, 0), layers=wall_layers)
|
|
77
|
+
# ... add more walls, roof, floor
|
|
78
|
+
|
|
79
|
+
zone = Zone(
|
|
80
|
+
opaque_elements=[wall, ...],
|
|
81
|
+
usable_floor_area=100,
|
|
82
|
+
internal_gains=[np.zeros(8760)],
|
|
83
|
+
lower_temperature_limit=20,
|
|
84
|
+
upper_temperature_limit=26,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
building = Building(zones=zone, weather=weather)
|
|
88
|
+
building.calculate()
|
|
89
|
+
|
|
90
|
+
print(f"Heating: {zone.heat_load.sum() / 1000:.0f} kWh")
|
|
91
|
+
print(f"Cooling: {zone.cool_load.sum() / 1000:.0f} kWh")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
See `examples/easy_example.py` for a complete working example.
|
|
95
|
+
|
|
96
|
+
## Features
|
|
97
|
+
|
|
98
|
+
- Hourly heating and cooling load calculation per ISO 52016
|
|
99
|
+
- 5-node thermal network for opaque elements, 2-node for windows
|
|
100
|
+
- Solar irradiation via `pvlib` (any orientation/tilt)
|
|
101
|
+
- Ground-temperature coupling per ISO 13370
|
|
102
|
+
- Multi-zone support
|
|
103
|
+
- Ventilation with heat recovery
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
BSD 3-Clause.
|
|
108
|
+
|
|
109
|
+
Independent implementation of the calculation method described in DIN EN ISO 52016-1. This project is not affiliated with, authorized by, or endorsed by ISO, CEN, or DIN, and does not reproduce the text of the standard.
|
iso52016-0.0.1/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# DIN EN ISO 52016
|
|
2
|
+
|
|
3
|
+
[](https://git.fh-aachen.de/tb5152e/din-en-iso-52016/-/commits/main)
|
|
4
|
+
[](https://git.fh-aachen.de/tb5152e/din-en-iso-52016/-/commits/main)
|
|
5
|
+
[](https://din-en-iso-52016.readthedocs.io/en/latest/?badge=latest)
|
|
6
|
+
[](https://github.com/psf/black)
|
|
7
|
+
|
|
8
|
+
Python implementation of the **DIN EN ISO 52016** standard for calculating the energy performance of buildings. The package computes hourly heating and cooling loads for thermal zones using a 5-node thermal-network model per opaque element, solar irradiation via `pvlib`, and ground-temperature coupling per ISO 13370.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or for development:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install -e ".[dev]"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import numpy as np
|
|
26
|
+
from iso52016 import (
|
|
27
|
+
Building, Layer, Material, OpaqueElement, Orientation,
|
|
28
|
+
Ventilation, Weather, Window, Zone,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Create materials and layers
|
|
32
|
+
concrete = Material(density_kg_m3=2300, thermal_capacity_J_kgK=880, conductivity_W_mK=1.7)
|
|
33
|
+
insulation = Material(density_kg_m3=20, thermal_capacity_J_kgK=1450, conductivity_W_mK=0.035)
|
|
34
|
+
wall_layers = [Layer(0.2, concrete), Layer(0.1, insulation)]
|
|
35
|
+
|
|
36
|
+
# Set up weather (8760 hourly values)
|
|
37
|
+
weather = Weather(air_temperature=t_air, wind=wind)
|
|
38
|
+
|
|
39
|
+
# Define building envelope
|
|
40
|
+
wall = OpaqueElement(area=50, orientation=Orientation(90, 0), layers=wall_layers)
|
|
41
|
+
# ... add more walls, roof, floor
|
|
42
|
+
|
|
43
|
+
zone = Zone(
|
|
44
|
+
opaque_elements=[wall, ...],
|
|
45
|
+
usable_floor_area=100,
|
|
46
|
+
internal_gains=[np.zeros(8760)],
|
|
47
|
+
lower_temperature_limit=20,
|
|
48
|
+
upper_temperature_limit=26,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
building = Building(zones=zone, weather=weather)
|
|
52
|
+
building.calculate()
|
|
53
|
+
|
|
54
|
+
print(f"Heating: {zone.heat_load.sum() / 1000:.0f} kWh")
|
|
55
|
+
print(f"Cooling: {zone.cool_load.sum() / 1000:.0f} kWh")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
See `examples/easy_example.py` for a complete working example.
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- Hourly heating and cooling load calculation per ISO 52016
|
|
63
|
+
- 5-node thermal network for opaque elements, 2-node for windows
|
|
64
|
+
- Solar irradiation via `pvlib` (any orientation/tilt)
|
|
65
|
+
- Ground-temperature coupling per ISO 13370
|
|
66
|
+
- Multi-zone support
|
|
67
|
+
- Ventilation with heat recovery
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
BSD 3-Clause.
|
|
72
|
+
|
|
73
|
+
Independent implementation of the calculation method described in DIN EN ISO 52016-1. This project is not affiliated with, authorized by, or endorsed by ISO, CEN, or DIN, and does not reproduce the text of the standard.
|
|
@@ -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
|
|
@@ -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]
|
|
@@ -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)
|