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.
Files changed (48) hide show
  1. hvacpy-0.4.0/LICENSE +21 -0
  2. hvacpy-0.4.0/PKG-INFO +159 -0
  3. hvacpy-0.4.0/README.md +124 -0
  4. hvacpy-0.4.0/hvacpy/__init__.py +70 -0
  5. hvacpy-0.4.0/hvacpy/assembly.py +249 -0
  6. hvacpy-0.4.0/hvacpy/equipment/__init__.py +47 -0
  7. hvacpy-0.4.0/hvacpy/equipment/_airflow.py +108 -0
  8. hvacpy-0.4.0/hvacpy/equipment/_cooling.py +355 -0
  9. hvacpy-0.4.0/hvacpy/equipment/_duct.py +263 -0
  10. hvacpy-0.4.0/hvacpy/equipment/_heatpump.py +203 -0
  11. hvacpy-0.4.0/hvacpy/equipment/_nominal_sizes.py +55 -0
  12. hvacpy-0.4.0/hvacpy/equipment/_ventilation.py +116 -0
  13. hvacpy-0.4.0/hvacpy/exceptions.py +41 -0
  14. hvacpy-0.4.0/hvacpy/loads/__init__.py +40 -0
  15. hvacpy-0.4.0/hvacpy/loads/_cltd_tables.py +328 -0
  16. hvacpy-0.4.0/hvacpy/loads/_components.py +174 -0
  17. hvacpy-0.4.0/hvacpy/loads/_cooling.py +412 -0
  18. hvacpy-0.4.0/hvacpy/loads/_heating.py +170 -0
  19. hvacpy-0.4.0/hvacpy/loads/_infiltration.py +94 -0
  20. hvacpy-0.4.0/hvacpy/loads/_internal.py +130 -0
  21. hvacpy-0.4.0/hvacpy/loads/_room.py +68 -0
  22. hvacpy-0.4.0/hvacpy/materials/__init__.py +218 -0
  23. hvacpy-0.4.0/hvacpy/materials/_database.py +260 -0
  24. hvacpy-0.4.0/hvacpy/psychrometrics/__init__.py +626 -0
  25. hvacpy-0.4.0/hvacpy/psychrometrics/_chart.py +215 -0
  26. hvacpy-0.4.0/hvacpy/psychrometrics/_equations.py +288 -0
  27. hvacpy-0.4.0/hvacpy/psychrometrics/_process.py +168 -0
  28. hvacpy-0.4.0/hvacpy/units.py +52 -0
  29. hvacpy-0.4.0/hvacpy.egg-info/PKG-INFO +159 -0
  30. hvacpy-0.4.0/hvacpy.egg-info/SOURCES.txt +46 -0
  31. hvacpy-0.4.0/hvacpy.egg-info/dependency_links.txt +1 -0
  32. hvacpy-0.4.0/hvacpy.egg-info/requires.txt +9 -0
  33. hvacpy-0.4.0/hvacpy.egg-info/top_level.txt +1 -0
  34. hvacpy-0.4.0/pyproject.toml +55 -0
  35. hvacpy-0.4.0/setup.cfg +4 -0
  36. hvacpy-0.4.0/tests/test_assembly.py +245 -0
  37. hvacpy-0.4.0/tests/test_cooling_loads.py +536 -0
  38. hvacpy-0.4.0/tests/test_duct_sizing.py +147 -0
  39. hvacpy-0.4.0/tests/test_equipment_cooling.py +250 -0
  40. hvacpy-0.4.0/tests/test_equipment_heatpump.py +162 -0
  41. hvacpy-0.4.0/tests/test_heating_loads.py +170 -0
  42. hvacpy-0.4.0/tests/test_internal_gains.py +141 -0
  43. hvacpy-0.4.0/tests/test_loads_components.py +214 -0
  44. hvacpy-0.4.0/tests/test_materials.py +103 -0
  45. hvacpy-0.4.0/tests/test_psychrometric_chart.py +62 -0
  46. hvacpy-0.4.0/tests/test_psychrometrics.py +377 -0
  47. hvacpy-0.4.0/tests/test_units.py +45 -0
  48. 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
+ [![PyPI](https://img.shields.io/pypi/v/hvacpy)](https://pypi.org/project/hvacpy/)
45
+ [![Python](https://img.shields.io/pypi/pyversions/hvacpy)](https://pypi.org/project/hvacpy/)
46
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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
+ [![PyPI](https://img.shields.io/pypi/v/hvacpy)](https://pypi.org/project/hvacpy/)
10
+ [![Python](https://img.shields.io/pypi/pyversions/hvacpy)](https://pypi.org/project/hvacpy/)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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
+ ]