hvacpy 0.4.0__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.
- hvacpy-0.4.0/LICENSE +21 -0
- hvacpy-0.4.0/PKG-INFO +159 -0
- hvacpy-0.4.0/README.md +124 -0
- hvacpy-0.4.0/hvacpy/__init__.py +70 -0
- hvacpy-0.4.0/hvacpy/assembly.py +249 -0
- hvacpy-0.4.0/hvacpy/equipment/__init__.py +47 -0
- hvacpy-0.4.0/hvacpy/equipment/_airflow.py +108 -0
- hvacpy-0.4.0/hvacpy/equipment/_cooling.py +355 -0
- hvacpy-0.4.0/hvacpy/equipment/_duct.py +263 -0
- hvacpy-0.4.0/hvacpy/equipment/_heatpump.py +203 -0
- hvacpy-0.4.0/hvacpy/equipment/_nominal_sizes.py +55 -0
- hvacpy-0.4.0/hvacpy/equipment/_ventilation.py +116 -0
- hvacpy-0.4.0/hvacpy/exceptions.py +41 -0
- hvacpy-0.4.0/hvacpy/loads/__init__.py +40 -0
- hvacpy-0.4.0/hvacpy/loads/_cltd_tables.py +328 -0
- hvacpy-0.4.0/hvacpy/loads/_components.py +174 -0
- hvacpy-0.4.0/hvacpy/loads/_cooling.py +412 -0
- hvacpy-0.4.0/hvacpy/loads/_heating.py +170 -0
- hvacpy-0.4.0/hvacpy/loads/_infiltration.py +94 -0
- hvacpy-0.4.0/hvacpy/loads/_internal.py +130 -0
- hvacpy-0.4.0/hvacpy/loads/_room.py +68 -0
- hvacpy-0.4.0/hvacpy/materials/__init__.py +218 -0
- hvacpy-0.4.0/hvacpy/materials/_database.py +260 -0
- hvacpy-0.4.0/hvacpy/psychrometrics/__init__.py +626 -0
- hvacpy-0.4.0/hvacpy/psychrometrics/_chart.py +215 -0
- hvacpy-0.4.0/hvacpy/psychrometrics/_equations.py +288 -0
- hvacpy-0.4.0/hvacpy/psychrometrics/_process.py +168 -0
- hvacpy-0.4.0/hvacpy/units.py +52 -0
- hvacpy-0.4.0/hvacpy.egg-info/PKG-INFO +159 -0
- hvacpy-0.4.0/hvacpy.egg-info/SOURCES.txt +46 -0
- hvacpy-0.4.0/hvacpy.egg-info/dependency_links.txt +1 -0
- hvacpy-0.4.0/hvacpy.egg-info/requires.txt +9 -0
- hvacpy-0.4.0/hvacpy.egg-info/top_level.txt +1 -0
- hvacpy-0.4.0/pyproject.toml +55 -0
- hvacpy-0.4.0/setup.cfg +4 -0
- hvacpy-0.4.0/tests/test_assembly.py +245 -0
- hvacpy-0.4.0/tests/test_cooling_loads.py +536 -0
- hvacpy-0.4.0/tests/test_duct_sizing.py +147 -0
- hvacpy-0.4.0/tests/test_equipment_cooling.py +250 -0
- hvacpy-0.4.0/tests/test_equipment_heatpump.py +162 -0
- hvacpy-0.4.0/tests/test_heating_loads.py +170 -0
- hvacpy-0.4.0/tests/test_internal_gains.py +141 -0
- hvacpy-0.4.0/tests/test_loads_components.py +214 -0
- hvacpy-0.4.0/tests/test_materials.py +103 -0
- hvacpy-0.4.0/tests/test_psychrometric_chart.py +62 -0
- hvacpy-0.4.0/tests/test_psychrometrics.py +377 -0
- hvacpy-0.4.0/tests/test_units.py +45 -0
- hvacpy-0.4.0/tests/test_ventilation.py +143 -0
hvacpy-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hvacpy contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hvacpy-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hvacpy
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: HVAC and building energy calculations for engineers
|
|
5
|
+
Author: hvacpy contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/anirudhsankar/hvacpy
|
|
8
|
+
Project-URL: Repository, https://github.com/anirudhsankar/hvacpy
|
|
9
|
+
Project-URL: Issues, https://github.com/anirudhsankar/hvacpy/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/anirudhsankar/hvacpy/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: hvac,building-energy,engineering,ashrae,u-value,heat-load,psychrometrics,cooling-load,equipment-sizing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: pint>=0.23
|
|
27
|
+
Requires-Dist: psychrolib>=2.5.0
|
|
28
|
+
Requires-Dist: matplotlib>=3.7
|
|
29
|
+
Requires-Dist: numpy>=1.24
|
|
30
|
+
Requires-Dist: scipy>=1.10
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# hvacpy
|
|
37
|
+
|
|
38
|
+
**HVAC and building energy calculations for engineers.**
|
|
39
|
+
|
|
40
|
+
Free, open, practitioner-first Python tooling that replaces expensive
|
|
41
|
+
proprietary software and inherited Excel spreadsheets for everyday
|
|
42
|
+
HVAC engineering calculations.
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/hvacpy/)
|
|
45
|
+
[](https://pypi.org/project/hvacpy/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
```bash
|
|
50
|
+
pip install hvacpy
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## What It Does
|
|
54
|
+
|
|
55
|
+
| Module | What you can calculate |
|
|
56
|
+
|--------|----------------------|
|
|
57
|
+
| **Assembly** | U-values and R-values for any wall, roof, or floor construction |
|
|
58
|
+
| **Psychrometrics** | All moist air properties from any two known conditions |
|
|
59
|
+
| **Heat Loads** | Cooling and heating loads for rooms and zones (CLTD/CLF method) |
|
|
60
|
+
| **Equipment Sizing** | Split systems, RTUs, FCUs, chillers, heat pumps, duct sizing, ventilation |
|
|
61
|
+
|
|
62
|
+
## Quick Examples
|
|
63
|
+
|
|
64
|
+
**Wall U-value:**
|
|
65
|
+
```python
|
|
66
|
+
from hvacpy import Q_, Assembly
|
|
67
|
+
|
|
68
|
+
wall = Assembly("Brick Cavity Wall")
|
|
69
|
+
wall.add_layer("brick_common", Q_(110, "mm"))
|
|
70
|
+
wall.add_layer("mineral_wool_batt", Q_(75, "mm"))
|
|
71
|
+
wall.add_layer("plasterboard_std", Q_(12.5, "mm"))
|
|
72
|
+
|
|
73
|
+
print(wall.u_value) # 0.347 W/(m²·K)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Moist air properties:**
|
|
77
|
+
```python
|
|
78
|
+
from hvacpy import Q_, AirState
|
|
79
|
+
|
|
80
|
+
air = AirState(dry_bulb=Q_(25, "degC"), rh=0.60)
|
|
81
|
+
print(air.wet_bulb) # 19.47 °C
|
|
82
|
+
print(air.dew_point) # 16.70 °C
|
|
83
|
+
print(air.enthalpy) # 55.45 kJ/kg
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Cooling load:**
|
|
87
|
+
```python
|
|
88
|
+
from hvacpy import (
|
|
89
|
+
Q_, Room, WallComponent, InternalGain, CoolingLoad, Orientation
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
room = Room(name="Office", floor_area=Q_(50, "m**2"),
|
|
93
|
+
ceiling_height=Q_(3, "m"))
|
|
94
|
+
room.walls.append(WallComponent(
|
|
95
|
+
name="South Wall", assembly=wall,
|
|
96
|
+
area=Q_(20, "m**2"), orientation=Orientation.S,
|
|
97
|
+
))
|
|
98
|
+
room.internal_gains.append(
|
|
99
|
+
InternalGain(gain_type="people", count=8, activity="office_work")
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
load = CoolingLoad(room, city="london")
|
|
103
|
+
print(f"Peak cooling: {load.peak_total.to('kW'):.2f}")
|
|
104
|
+
print(load.breakdown())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Equipment sizing (v0.4):**
|
|
108
|
+
```python
|
|
109
|
+
from hvacpy import Q_, SplitSystem, DuctSizer, VentilationCheck
|
|
110
|
+
|
|
111
|
+
# Size a split system from the cooling load
|
|
112
|
+
ss = SplitSystem(load, cop_rated=3.5)
|
|
113
|
+
print(ss.summary()) # box-format sizing report
|
|
114
|
+
print(ss.nominal_capacity) # e.g. 10.0 kW
|
|
115
|
+
print(ss.oversizing_warning) # None / 'WARNING' / 'CRITICAL'
|
|
116
|
+
|
|
117
|
+
# Size a main supply duct — equal friction method
|
|
118
|
+
ds = DuctSizer(Q_(0.5, "m**3/s"), method="equal_friction")
|
|
119
|
+
print(ds.diameter) # e.g. 400 mm standard size
|
|
120
|
+
print(ds.velocity) # actual air velocity
|
|
121
|
+
print(ds.summary()) # Dia400mm - 3.98m/s - 0.45Pa/m - or 600x400mm rect
|
|
122
|
+
|
|
123
|
+
# Check ventilation compliance (ASHRAE 62.1-2022)
|
|
124
|
+
vc = VentilationCheck(room, supply_airflow=Q_(0.5, "m**3/s"), space_type="office")
|
|
125
|
+
print(vc.compliant) # True / False
|
|
126
|
+
print(vc.summary())
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Design Principles
|
|
130
|
+
|
|
131
|
+
- **Correct before fast** — all equations trace to ASHRAE and ISO sources
|
|
132
|
+
- **Units everywhere** — every value carries its unit, no silent conversions
|
|
133
|
+
- **Practitioner language** — APIs use terms engineers actually use
|
|
134
|
+
- **The engineer always decides** — hvacpy calculates and warns; it never refuses
|
|
135
|
+
|
|
136
|
+
## Standards Referenced
|
|
137
|
+
|
|
138
|
+
| Standard | Used in |
|
|
139
|
+
|---|---|
|
|
140
|
+
| ASHRAE HOF 2021 Ch.28 | Cooling loads (CLTD/CLF) |
|
|
141
|
+
| ASHRAE HOF 2021 Ch.18 | Heating loads |
|
|
142
|
+
| ASHRAE HOF 2021 Ch.14 | Psychrometrics |
|
|
143
|
+
| ASHRAE HOF 2021 Ch.21 | Duct sizing |
|
|
144
|
+
| ASHRAE HSE 2020 | Equipment sizing |
|
|
145
|
+
| ASHRAE 62.1-2022 | Ventilation compliance |
|
|
146
|
+
|
|
147
|
+
## Test Coverage
|
|
148
|
+
|
|
149
|
+
185 tests · 92% equipment coverage · all verified against reference values
|
|
150
|
+
|
|
151
|
+
## Roadmap
|
|
152
|
+
|
|
153
|
+
- v0.5 — Weather data (EPW files, degree days, ASHRAE design conditions)
|
|
154
|
+
- v0.6 — Data centre loads (IT load, PUE, WUE, economiser analysis)
|
|
155
|
+
- v1.0 — Annual energy estimation and carbon footprint reporting
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
hvacpy-0.4.0/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# hvacpy
|
|
2
|
+
|
|
3
|
+
**HVAC and building energy calculations for engineers.**
|
|
4
|
+
|
|
5
|
+
Free, open, practitioner-first Python tooling that replaces expensive
|
|
6
|
+
proprietary software and inherited Excel spreadsheets for everyday
|
|
7
|
+
HVAC engineering calculations.
|
|
8
|
+
|
|
9
|
+
[](https://pypi.org/project/hvacpy/)
|
|
10
|
+
[](https://pypi.org/project/hvacpy/)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
```bash
|
|
15
|
+
pip install hvacpy
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What It Does
|
|
19
|
+
|
|
20
|
+
| Module | What you can calculate |
|
|
21
|
+
|--------|----------------------|
|
|
22
|
+
| **Assembly** | U-values and R-values for any wall, roof, or floor construction |
|
|
23
|
+
| **Psychrometrics** | All moist air properties from any two known conditions |
|
|
24
|
+
| **Heat Loads** | Cooling and heating loads for rooms and zones (CLTD/CLF method) |
|
|
25
|
+
| **Equipment Sizing** | Split systems, RTUs, FCUs, chillers, heat pumps, duct sizing, ventilation |
|
|
26
|
+
|
|
27
|
+
## Quick Examples
|
|
28
|
+
|
|
29
|
+
**Wall U-value:**
|
|
30
|
+
```python
|
|
31
|
+
from hvacpy import Q_, Assembly
|
|
32
|
+
|
|
33
|
+
wall = Assembly("Brick Cavity Wall")
|
|
34
|
+
wall.add_layer("brick_common", Q_(110, "mm"))
|
|
35
|
+
wall.add_layer("mineral_wool_batt", Q_(75, "mm"))
|
|
36
|
+
wall.add_layer("plasterboard_std", Q_(12.5, "mm"))
|
|
37
|
+
|
|
38
|
+
print(wall.u_value) # 0.347 W/(m²·K)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Moist air properties:**
|
|
42
|
+
```python
|
|
43
|
+
from hvacpy import Q_, AirState
|
|
44
|
+
|
|
45
|
+
air = AirState(dry_bulb=Q_(25, "degC"), rh=0.60)
|
|
46
|
+
print(air.wet_bulb) # 19.47 °C
|
|
47
|
+
print(air.dew_point) # 16.70 °C
|
|
48
|
+
print(air.enthalpy) # 55.45 kJ/kg
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Cooling load:**
|
|
52
|
+
```python
|
|
53
|
+
from hvacpy import (
|
|
54
|
+
Q_, Room, WallComponent, InternalGain, CoolingLoad, Orientation
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
room = Room(name="Office", floor_area=Q_(50, "m**2"),
|
|
58
|
+
ceiling_height=Q_(3, "m"))
|
|
59
|
+
room.walls.append(WallComponent(
|
|
60
|
+
name="South Wall", assembly=wall,
|
|
61
|
+
area=Q_(20, "m**2"), orientation=Orientation.S,
|
|
62
|
+
))
|
|
63
|
+
room.internal_gains.append(
|
|
64
|
+
InternalGain(gain_type="people", count=8, activity="office_work")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
load = CoolingLoad(room, city="london")
|
|
68
|
+
print(f"Peak cooling: {load.peak_total.to('kW'):.2f}")
|
|
69
|
+
print(load.breakdown())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Equipment sizing (v0.4):**
|
|
73
|
+
```python
|
|
74
|
+
from hvacpy import Q_, SplitSystem, DuctSizer, VentilationCheck
|
|
75
|
+
|
|
76
|
+
# Size a split system from the cooling load
|
|
77
|
+
ss = SplitSystem(load, cop_rated=3.5)
|
|
78
|
+
print(ss.summary()) # box-format sizing report
|
|
79
|
+
print(ss.nominal_capacity) # e.g. 10.0 kW
|
|
80
|
+
print(ss.oversizing_warning) # None / 'WARNING' / 'CRITICAL'
|
|
81
|
+
|
|
82
|
+
# Size a main supply duct — equal friction method
|
|
83
|
+
ds = DuctSizer(Q_(0.5, "m**3/s"), method="equal_friction")
|
|
84
|
+
print(ds.diameter) # e.g. 400 mm standard size
|
|
85
|
+
print(ds.velocity) # actual air velocity
|
|
86
|
+
print(ds.summary()) # Dia400mm - 3.98m/s - 0.45Pa/m - or 600x400mm rect
|
|
87
|
+
|
|
88
|
+
# Check ventilation compliance (ASHRAE 62.1-2022)
|
|
89
|
+
vc = VentilationCheck(room, supply_airflow=Q_(0.5, "m**3/s"), space_type="office")
|
|
90
|
+
print(vc.compliant) # True / False
|
|
91
|
+
print(vc.summary())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Design Principles
|
|
95
|
+
|
|
96
|
+
- **Correct before fast** — all equations trace to ASHRAE and ISO sources
|
|
97
|
+
- **Units everywhere** — every value carries its unit, no silent conversions
|
|
98
|
+
- **Practitioner language** — APIs use terms engineers actually use
|
|
99
|
+
- **The engineer always decides** — hvacpy calculates and warns; it never refuses
|
|
100
|
+
|
|
101
|
+
## Standards Referenced
|
|
102
|
+
|
|
103
|
+
| Standard | Used in |
|
|
104
|
+
|---|---|
|
|
105
|
+
| ASHRAE HOF 2021 Ch.28 | Cooling loads (CLTD/CLF) |
|
|
106
|
+
| ASHRAE HOF 2021 Ch.18 | Heating loads |
|
|
107
|
+
| ASHRAE HOF 2021 Ch.14 | Psychrometrics |
|
|
108
|
+
| ASHRAE HOF 2021 Ch.21 | Duct sizing |
|
|
109
|
+
| ASHRAE HSE 2020 | Equipment sizing |
|
|
110
|
+
| ASHRAE 62.1-2022 | Ventilation compliance |
|
|
111
|
+
|
|
112
|
+
## Test Coverage
|
|
113
|
+
|
|
114
|
+
185 tests · 92% equipment coverage · all verified against reference values
|
|
115
|
+
|
|
116
|
+
## Roadmap
|
|
117
|
+
|
|
118
|
+
- v0.5 — Weather data (EPW files, degree days, ASHRAE design conditions)
|
|
119
|
+
- v0.6 — Data centre loads (IT load, PUE, WUE, economiser analysis)
|
|
120
|
+
- v1.0 — Annual energy estimation and carbon footprint reporting
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""hvacpy — HVAC and building energy calculations for engineers.
|
|
2
|
+
|
|
3
|
+
This package provides tools for building envelope thermal analysis,
|
|
4
|
+
including material properties, assembly U-value/R-value calculations,
|
|
5
|
+
and unit-aware engineering quantities.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from hvacpy import Q_, db, Assembly
|
|
9
|
+
>>> wall = Assembly('My Wall')
|
|
10
|
+
>>> wall.add_layer('brick_common', Q_(110, 'mm'))
|
|
11
|
+
>>> wall.add_layer('mineral_wool_batt', Q_(100, 'mm'))
|
|
12
|
+
>>> wall.add_layer('plasterboard_std', Q_(12.5, 'mm'))
|
|
13
|
+
>>> print(wall.u_value)
|
|
14
|
+
0.298... W / K / m²
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from hvacpy.units import Q_, ureg, validate_unit
|
|
18
|
+
from hvacpy.exceptions import UnitError, MaterialNotFoundError
|
|
19
|
+
from hvacpy.exceptions import PsychrometricInputError
|
|
20
|
+
from hvacpy.exceptions import LoadCalculationError, DesignConditionsNotFoundError
|
|
21
|
+
from hvacpy.exceptions import EquipmentSizingError, AirflowCalculationError, DuctSizingError
|
|
22
|
+
from hvacpy.materials import Material, MaterialsDB, _DB as db
|
|
23
|
+
from hvacpy.assembly import Assembly
|
|
24
|
+
from hvacpy.psychrometrics import AirState, PsychChart, AirProcess
|
|
25
|
+
from hvacpy.psychrometrics import (
|
|
26
|
+
dry_bulb_from_wet_bulb,
|
|
27
|
+
humidity_ratio_from_rh,
|
|
28
|
+
dew_point_from_humidity_ratio,
|
|
29
|
+
)
|
|
30
|
+
from hvacpy.loads import CoolingLoad, HeatingLoad, Room, Zone
|
|
31
|
+
from hvacpy.loads import (
|
|
32
|
+
WallComponent as Wall,
|
|
33
|
+
WallComponent,
|
|
34
|
+
WindowComponent as Window,
|
|
35
|
+
WindowComponent,
|
|
36
|
+
InternalGain,
|
|
37
|
+
Orientation,
|
|
38
|
+
)
|
|
39
|
+
from hvacpy.equipment import (
|
|
40
|
+
SplitSystem, PackagedRTU, FanCoilUnit, Chiller,
|
|
41
|
+
AirSourceHeatPump, DuctSizer, VentilationCheck,
|
|
42
|
+
size_cooling_equipment, size_heat_pump,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Aliases for spec compatibility
|
|
46
|
+
Roof = WallComponent # Roof is a WallComponent with is_roof=True
|
|
47
|
+
Floor = WallComponent # Floor is a WallComponent (simplified for v0.3)
|
|
48
|
+
Wall = WallComponent
|
|
49
|
+
Window = WindowComponent
|
|
50
|
+
|
|
51
|
+
__version__ = '0.4.0'
|
|
52
|
+
__all__ = [
|
|
53
|
+
'Q_', 'ureg', 'validate_unit',
|
|
54
|
+
'UnitError', 'MaterialNotFoundError', 'PsychrometricInputError',
|
|
55
|
+
'LoadCalculationError', 'DesignConditionsNotFoundError',
|
|
56
|
+
'EquipmentSizingError', 'AirflowCalculationError', 'DuctSizingError',
|
|
57
|
+
'Material', 'MaterialsDB', 'db',
|
|
58
|
+
'Assembly',
|
|
59
|
+
'AirState', 'PsychChart', 'AirProcess',
|
|
60
|
+
'dry_bulb_from_wet_bulb',
|
|
61
|
+
'humidity_ratio_from_rh',
|
|
62
|
+
'dew_point_from_humidity_ratio',
|
|
63
|
+
'CoolingLoad', 'HeatingLoad', 'Room', 'Zone',
|
|
64
|
+
'WallComponent', 'WindowComponent', 'InternalGain',
|
|
65
|
+
'Orientation', 'Wall', 'Window', 'Roof', 'Floor',
|
|
66
|
+
'SplitSystem', 'PackagedRTU', 'FanCoilUnit', 'Chiller',
|
|
67
|
+
'AirSourceHeatPump', 'DuctSizer', 'VentilationCheck',
|
|
68
|
+
'size_cooling_equipment', 'size_heat_pump',
|
|
69
|
+
'__version__',
|
|
70
|
+
]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Assembly module — layered building envelope elements.
|
|
2
|
+
|
|
3
|
+
An Assembly represents a wall, roof, floor, or ceiling made of
|
|
4
|
+
stacked material layers. It calculates total thermal resistance
|
|
5
|
+
(R-value) and thermal transmittance (U-value) using the series
|
|
6
|
+
resistance method from ISO 6946:2017.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from hvacpy import Q_, db, Assembly
|
|
10
|
+
>>> wall = Assembly('My Wall')
|
|
11
|
+
>>> wall.add_layer('brick_common', Q_(110, 'mm'))
|
|
12
|
+
>>> wall.add_layer('mineral_wool_batt', Q_(100, 'mm'))
|
|
13
|
+
>>> wall.add_layer('plasterboard_std', Q_(12.5, 'mm'))
|
|
14
|
+
>>> print(wall.u_value)
|
|
15
|
+
0.298... W / K / m²
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pint import Quantity
|
|
21
|
+
|
|
22
|
+
from hvacpy.exceptions import UnitError
|
|
23
|
+
from hvacpy.materials import Material, _DB
|
|
24
|
+
from hvacpy.units import Q_, validate_unit
|
|
25
|
+
|
|
26
|
+
# Surface resistance values — ISO 6946:2017 Table 1
|
|
27
|
+
# Named constants per spec Section 9.2, no magic numbers.
|
|
28
|
+
R_SI_WALL = 0.13 # m²K/W — horizontal heat flow
|
|
29
|
+
R_SI_ROOF = 0.10 # m²K/W — upward heat flow
|
|
30
|
+
R_SI_FLOOR = 0.17 # m²K/W — downward heat flow
|
|
31
|
+
R_SE = 0.04 # m²K/W — all orientations
|
|
32
|
+
|
|
33
|
+
_R_SI_MAP: dict[str, float] = {
|
|
34
|
+
"wall": R_SI_WALL,
|
|
35
|
+
"roof": R_SI_ROOF,
|
|
36
|
+
"floor": R_SI_FLOOR,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_VALID_ORIENTATIONS = frozenset(_R_SI_MAP.keys())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Assembly:
|
|
43
|
+
"""A layered building envelope element.
|
|
44
|
+
|
|
45
|
+
Layers are ordered outside-to-inside. Surface resistances are
|
|
46
|
+
added automatically based on orientation.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
name: Human-readable description e.g. 'Brick Cavity Wall'.
|
|
50
|
+
orientation: 'wall' | 'roof' | 'floor'. Defaults to 'wall'.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If orientation is not 'wall', 'roof', or 'floor'.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, name: str, orientation: str = "wall") -> None:
|
|
57
|
+
if orientation not in _VALID_ORIENTATIONS:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"orientation must be 'wall', 'roof', or 'floor', "
|
|
60
|
+
f"got '{orientation}'"
|
|
61
|
+
)
|
|
62
|
+
self._name = name
|
|
63
|
+
self._orientation = orientation
|
|
64
|
+
self._r_si: float = _R_SI_MAP[orientation]
|
|
65
|
+
self._r_se: float = R_SE
|
|
66
|
+
# Internal layer storage — plain floats in SI units.
|
|
67
|
+
self._layers: list[dict] = []
|
|
68
|
+
|
|
69
|
+
def add_layer(
|
|
70
|
+
self,
|
|
71
|
+
material: str | Material,
|
|
72
|
+
thickness: Quantity,
|
|
73
|
+
) -> "Assembly":
|
|
74
|
+
"""Add a material layer to this assembly.
|
|
75
|
+
|
|
76
|
+
Layers are ordered as added, outside to inside.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
material: Material key string (e.g. 'brick_common') or a
|
|
80
|
+
Material instance. String keys are case-insensitive.
|
|
81
|
+
thickness: Layer thickness as a pint Quantity with length
|
|
82
|
+
units. E.g. Q_(110, 'mm') or Q_(0.11, 'm').
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
self, enabling method chaining.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
TypeError: If thickness is not a pint Quantity.
|
|
89
|
+
UnitError: If thickness does not have length dimensions.
|
|
90
|
+
ValueError: If thickness is not positive.
|
|
91
|
+
MaterialNotFoundError: If material is a string key not
|
|
92
|
+
in DB.
|
|
93
|
+
"""
|
|
94
|
+
# Validate thickness type.
|
|
95
|
+
if not isinstance(thickness, Quantity):
|
|
96
|
+
raise TypeError(
|
|
97
|
+
f"thickness must be a pint Quantity, got "
|
|
98
|
+
f"{type(thickness).__name__}. "
|
|
99
|
+
f'Use Q_(value, unit) e.g. Q_(110, "mm")'
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Validate thickness dimensionality.
|
|
103
|
+
validate_unit(thickness, "[length]", "thickness")
|
|
104
|
+
|
|
105
|
+
# Convert to metres (SI).
|
|
106
|
+
thickness_m: float = thickness.to("m").magnitude
|
|
107
|
+
|
|
108
|
+
# Validate positive.
|
|
109
|
+
if thickness_m <= 0:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"thickness must be positive, got {thickness}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Resolve material.
|
|
115
|
+
db_key: str | None = None
|
|
116
|
+
if isinstance(material, str):
|
|
117
|
+
db_key = material.lower()
|
|
118
|
+
mat = _DB.get(material)
|
|
119
|
+
elif isinstance(material, Material):
|
|
120
|
+
mat = material
|
|
121
|
+
else:
|
|
122
|
+
raise TypeError(
|
|
123
|
+
f"material must be a str or Material, got "
|
|
124
|
+
f"{type(material).__name__}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
conductivity: float = mat.conductivity.to("W/(m*K)").magnitude
|
|
128
|
+
r_layer: float = thickness_m / conductivity
|
|
129
|
+
|
|
130
|
+
self._layers.append({
|
|
131
|
+
"name": mat.name,
|
|
132
|
+
"key": db_key,
|
|
133
|
+
"thickness_m": thickness_m,
|
|
134
|
+
"conductivity": conductivity,
|
|
135
|
+
"r_layer": r_layer,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def r_value(self) -> Quantity:
|
|
142
|
+
"""Total thermal resistance including surface resistances.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
R-value as a pint Quantity in m²·K/W.
|
|
146
|
+
"""
|
|
147
|
+
r_total = self._r_si + self._r_se
|
|
148
|
+
for layer in self._layers:
|
|
149
|
+
r_total += layer["r_layer"]
|
|
150
|
+
return Q_(r_total, "m²*K/W")
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def u_value(self) -> Quantity:
|
|
154
|
+
"""Thermal transmittance (reciprocal of R-value).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
U-value as a pint Quantity in W/(m²·K).
|
|
158
|
+
"""
|
|
159
|
+
r_total = self.r_value.magnitude
|
|
160
|
+
return Q_(1.0 / r_total, "W/(m²*K)")
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def layers(self) -> list[dict]:
|
|
164
|
+
"""Layer information as list of dicts.
|
|
165
|
+
|
|
166
|
+
Each dict contains:
|
|
167
|
+
- name (str): Material name.
|
|
168
|
+
- key (str | None): DB key if looked up, None if passed
|
|
169
|
+
as Material instance.
|
|
170
|
+
- thickness_m (float): Thickness in metres.
|
|
171
|
+
- conductivity (float): W/(m·K).
|
|
172
|
+
- r_layer (float): Layer resistance in m²K/W.
|
|
173
|
+
- r_fraction (float): Layer R as fraction of total layer
|
|
174
|
+
R (excludes surface resistances), 0.0–1.0.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
List of layer dicts ordered outside-to-inside.
|
|
178
|
+
"""
|
|
179
|
+
r_layers_total = sum(
|
|
180
|
+
layer["r_layer"] for layer in self._layers
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
result: list[dict] = []
|
|
184
|
+
for layer in self._layers:
|
|
185
|
+
fraction = (
|
|
186
|
+
layer["r_layer"] / r_layers_total
|
|
187
|
+
if r_layers_total > 0
|
|
188
|
+
else 0.0
|
|
189
|
+
)
|
|
190
|
+
result.append({
|
|
191
|
+
"name": layer["name"],
|
|
192
|
+
"key": layer["key"],
|
|
193
|
+
"thickness_m": layer["thickness_m"],
|
|
194
|
+
"conductivity": layer["conductivity"],
|
|
195
|
+
"r_layer": layer["r_layer"],
|
|
196
|
+
"r_fraction": fraction,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
def breakdown(self) -> str:
|
|
202
|
+
"""Human-readable breakdown of the assembly.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Formatted string showing all layers, their resistances,
|
|
206
|
+
surface resistances, and total R/U values.
|
|
207
|
+
"""
|
|
208
|
+
r_total = self.r_value.magnitude
|
|
209
|
+
u_val = self.u_value.magnitude
|
|
210
|
+
|
|
211
|
+
lines: list[str] = []
|
|
212
|
+
lines.append(
|
|
213
|
+
f"Wall: {self._name} "
|
|
214
|
+
f"(orientation: {self._orientation})"
|
|
215
|
+
)
|
|
216
|
+
lines.append("━" * 47)
|
|
217
|
+
lines.append(" [outside]")
|
|
218
|
+
lines.append(
|
|
219
|
+
f" Surface resistance (R_se)"
|
|
220
|
+
f" {self._r_se:.3f} m²K/W"
|
|
221
|
+
)
|
|
222
|
+
lines.append(" " + "─" * 45)
|
|
223
|
+
|
|
224
|
+
for layer in self._layers:
|
|
225
|
+
thickness_mm = layer["thickness_m"] * 1000
|
|
226
|
+
# Format thickness — show decimal only if needed.
|
|
227
|
+
if thickness_mm == int(thickness_mm):
|
|
228
|
+
t_str = f"{int(thickness_mm)} mm"
|
|
229
|
+
else:
|
|
230
|
+
t_str = f"{thickness_mm:.1f} mm"
|
|
231
|
+
|
|
232
|
+
# Use key if available, otherwise name.
|
|
233
|
+
label = layer["key"] if layer["key"] else layer["name"]
|
|
234
|
+
lines.append(
|
|
235
|
+
f" {label:<16s}{t_str:>8s}"
|
|
236
|
+
f" {layer['r_layer']:.3f} m²K/W"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
lines.append(" " + "─" * 45)
|
|
240
|
+
lines.append(
|
|
241
|
+
f" Surface resistance (R_si)"
|
|
242
|
+
f" {self._r_si:.3f} m²K/W"
|
|
243
|
+
)
|
|
244
|
+
lines.append(" [inside]")
|
|
245
|
+
lines.append("━" * 47)
|
|
246
|
+
lines.append(f" R_total = {r_total:.3f} m²K/W")
|
|
247
|
+
lines.append(f" U_value = {u_val:.3f} W/(m²K)")
|
|
248
|
+
|
|
249
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""hvacpy.equipment — Equipment Sizing (v0.4).
|
|
2
|
+
|
|
3
|
+
ASHRAE HSE 2020 equipment sizing, ASHRAE HOF 2021 Ch.21 duct sizing,
|
|
4
|
+
ASHRAE 62.1-2022 ventilation compliance.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
SplitSystem, PackagedRTU, FanCoilUnit, Chiller
|
|
8
|
+
AirSourceHeatPump
|
|
9
|
+
DuctSizer
|
|
10
|
+
VentilationCheck
|
|
11
|
+
size_cooling_equipment, size_heat_pump (convenience functions)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from hvacpy.equipment._cooling import (
|
|
15
|
+
SplitSystem, PackagedRTU, FanCoilUnit, Chiller,
|
|
16
|
+
)
|
|
17
|
+
from hvacpy.equipment._heatpump import AirSourceHeatPump
|
|
18
|
+
from hvacpy.equipment._duct import DuctSizer
|
|
19
|
+
from hvacpy.equipment._ventilation import VentilationCheck
|
|
20
|
+
from hvacpy.equipment._airflow import (
|
|
21
|
+
supply_airflow_cooling,
|
|
22
|
+
supply_airflow_heating,
|
|
23
|
+
airflow_from_cooling_load,
|
|
24
|
+
)
|
|
25
|
+
from hvacpy.equipment._nominal_sizes import next_size_up, NOMINAL_SIZES
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def size_cooling_equipment(cooling_load, equipment_class=SplitSystem, **kwargs):
|
|
29
|
+
"""Convenience function to size cooling equipment from a CoolingLoad."""
|
|
30
|
+
return equipment_class(cooling_load, **kwargs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def size_heat_pump(cooling_load, heating_load, **kwargs):
|
|
34
|
+
"""Convenience function to size an air-source heat pump."""
|
|
35
|
+
return AirSourceHeatPump(cooling_load, heating_load, **kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
'SplitSystem', 'PackagedRTU', 'FanCoilUnit', 'Chiller',
|
|
40
|
+
'AirSourceHeatPump',
|
|
41
|
+
'DuctSizer',
|
|
42
|
+
'VentilationCheck',
|
|
43
|
+
'size_cooling_equipment', 'size_heat_pump',
|
|
44
|
+
'supply_airflow_cooling', 'supply_airflow_heating',
|
|
45
|
+
'airflow_from_cooling_load',
|
|
46
|
+
'next_size_up', 'NOMINAL_SIZES',
|
|
47
|
+
]
|