Source code for hvacpy.loads._components

"""Component dataclasses for heat load calculations.

Data containers only — no calculations happen here. These describe
the building envelope and internal heat sources for a room or zone.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING

from hvacpy.units import Q_

if TYPE_CHECKING:
    from pint import Quantity
    from hvacpy.assembly import Assembly


# ── Orientation Enum ────────────────────────────────────────────────

[docs] class Orientation(str, Enum): """Cardinal / inter-cardinal directions for building surfaces.""" N = "N" NE = "NE" E = "E" SE = "SE" S = "S" SW = "SW" W = "W" NW = "NW" HORIZONTAL = "H" # for roofs and skylights
# Valid wall groups per ASHRAE 1997 HOF Ch.28 _VALID_WALL_GROUPS = frozenset('ABCDEFG') # Valid gain types _VALID_GAIN_TYPES = frozenset({'people', 'lighting', 'equipment'}) # Valid activity types — keys of PEOPLE_SENSIBLE_W / PEOPLE_LATENT_W _VALID_ACTIVITIES = frozenset({ 'seated_quiet', 'office_work', 'standing_light', 'walking', 'light_bench_work', 'retail_banking', 'restaurant', 'dancing', 'heavy_work', }) # ── WallComponent ──────────────────────────────────────────────────
[docs] @dataclass class WallComponent: """An opaque wall or roof surface contributing to cooling/heating load. Args: name: Human label e.g. 'South facade'. assembly: hvacpy Assembly — provides U-value. area: Net wall area (excluding windows/doors) as Quantity (m² or ft²). orientation: Orientation enum value. wall_group: ASHRAE CLTD wall group 'A' through 'G'. Default 'D'. See _cltd_tables.py for group descriptions. is_roof: True if this is a roof/ceiling surface. Default False. """ name: str assembly: 'Assembly' area: 'Quantity' orientation: Orientation wall_group: str = 'D' is_roof: bool = False def __post_init__(self) -> None: # Validate orientation is an Orientation enum member if not isinstance(self.orientation, Orientation): raise ValueError( f"orientation must be an Orientation enum value, " f"got {self.orientation!r}. Use e.g. Orientation.S" ) # Validate wall_group wg = self.wall_group.upper() if wg not in _VALID_WALL_GROUPS: raise ValueError( f"wall_group must be one of {sorted(_VALID_WALL_GROUPS)} " f"('A' through 'G'), got {self.wall_group!r}" ) self.wall_group = wg
# ── WindowComponent ────────────────────────────────────────────────
[docs] @dataclass class WindowComponent: """A glazed opening contributing solar and conductive cooling load. Args: name: Human label e.g. 'South glazing'. area: Gross glazed area as Quantity (m² or ft²). orientation: Orientation enum. u_factor: Overall window U-factor as Quantity W/(m²K). Typical double-glazed = 2.8, triple = 1.8. shgc: Solar Heat Gain Coefficient 0.0–1.0. Clear single = 0.87, Low-e double = 0.25–0.40. has_interior_shading: True if blinds/curtains present. Default False. Affects CLF table selection. frame_fraction: Fraction of area that is frame (0.0–1.0). Default 0.15. """ name: str area: 'Quantity' orientation: Orientation u_factor: 'Quantity' shgc: float has_interior_shading: bool = False frame_fraction: float = 0.15 def __post_init__(self) -> None: # Validate orientation if not isinstance(self.orientation, Orientation): raise ValueError( f"orientation must be an Orientation enum value, " f"got {self.orientation!r}. Use e.g. Orientation.S" ) # Validate SHGC if not (0.0 <= self.shgc <= 1.0): raise ValueError( f"shgc must be between 0.0 and 1.0, got {self.shgc}" ) # Validate frame_fraction if not (0.0 <= self.frame_fraction <= 0.5): raise ValueError( f"frame_fraction must be between 0.0 and 0.5, " f"got {self.frame_fraction}" )
# ── InternalGain ───────────────────────────────────────────────────
[docs] @dataclass class InternalGain: """A heat source inside the space. For people: specify count and activity. For lighting: specify watts_per_m2 or total_watts. For equipment: specify watts_per_m2 or total_watts. """ gain_type: str # 'people' | 'lighting' | 'equipment' # People fields count: int = 0 activity: str = 'office_work' # Lighting / equipment fields watts_per_m2: float = 0.0 # W/m² — used if total_watts is 0 total_watts: float = 0.0 # W — takes priority over watts_per_m2 # Diversity factor diversity: float = 1.0 # fraction of gain actually present (0.0–1.0) # CLF — cooling load factor accounting for thermal storage clf: float = 1.0 # use 1.0 (conservative) unless specified def __post_init__(self) -> None: # Validate gain_type if self.gain_type not in _VALID_GAIN_TYPES: raise ValueError( f"gain_type must be one of {sorted(_VALID_GAIN_TYPES)}, " f"got {self.gain_type!r}" ) # Validate activity for people if self.gain_type == 'people' and self.activity not in _VALID_ACTIVITIES: raise ValueError( f"Invalid activity {self.activity!r}. " f"Valid activities: {sorted(_VALID_ACTIVITIES)}" ) # Validate diversity if not (0.0 <= self.diversity <= 1.0): raise ValueError( f"diversity must be between 0.0 and 1.0, " f"got {self.diversity}" )