| """ |
| Building component data models for HVAC Load Calculator. |
| This module defines the data structures for walls, roofs, floors, windows, doors, and other building components. |
| """ |
|
|
| from dataclasses import dataclass, field |
| from enum import Enum |
| from typing import List, Dict, Optional, Union |
| import numpy as np |
|
|
|
|
| class Orientation(Enum): |
| """Enumeration for building component orientations.""" |
| NORTH = "North" |
| NORTHEAST = "Northeast" |
| EAST = "East" |
| SOUTHEAST = "Southeast" |
| SOUTH = "South" |
| SOUTHWEST = "Southwest" |
| WEST = "West" |
| NORTHWEST = "Northwest" |
| HORIZONTAL = "Horizontal" |
| NOT_APPLICABLE = "N/A" |
|
|
|
|
| class ComponentType(Enum): |
| """Enumeration for building component types.""" |
| WALL = "Wall" |
| ROOF = "Roof" |
| FLOOR = "Floor" |
| WINDOW = "Window" |
| DOOR = "Door" |
| SKYLIGHT = "Skylight" |
|
|
|
|
| class MaterialLayer: |
| """Class representing a single material layer in a building component.""" |
| |
| def __init__(self, name: str, thickness: float, conductivity: float, |
| density: float = None, specific_heat: float = None): |
| """ |
| Initialize a material layer. |
| |
| Args: |
| name: Name of the material |
| thickness: Thickness of the layer in meters |
| conductivity: Thermal conductivity in W/(m·K) |
| density: Density in kg/m³ (optional) |
| specific_heat: Specific heat capacity in J/(kg·K) (optional) |
| """ |
| self.name = name |
| self.thickness = thickness |
| self.conductivity = conductivity |
| self.density = density |
| self.specific_heat = specific_heat |
| |
| @property |
| def r_value(self) -> float: |
| """Calculate the thermal resistance (R-value) of the layer in m²·K/W.""" |
| if self.conductivity == 0: |
| return float('inf') |
| return self.thickness / self.conductivity |
| |
| @property |
| def thermal_mass(self) -> Optional[float]: |
| """Calculate the thermal mass of the layer in J/(m²·K).""" |
| if self.density is None or self.specific_heat is None: |
| return None |
| return self.thickness * self.density * self.specific_heat |
| |
| def to_dict(self) -> Dict: |
| """Convert the material layer to a dictionary.""" |
| return { |
| "name": self.name, |
| "thickness": self.thickness, |
| "conductivity": self.conductivity, |
| "density": self.density, |
| "specific_heat": self.specific_heat, |
| "r_value": self.r_value, |
| "thermal_mass": self.thermal_mass |
| } |
|
|
|
|
| @dataclass |
| class BuildingComponent: |
| """Base class for all building components.""" |
| |
| id: str |
| name: str |
| component_type: ComponentType |
| u_value: float |
| area: float |
| orientation: Orientation = Orientation.NOT_APPLICABLE |
| color: str = "Medium" |
| material_layers: List[MaterialLayer] = field(default_factory=list) |
| |
| def __post_init__(self): |
| """Validate component data after initialization.""" |
| if self.area <= 0: |
| raise ValueError("Area must be greater than zero") |
| if self.u_value < 0: |
| raise ValueError("U-value cannot be negative") |
| |
| @property |
| def r_value(self) -> float: |
| """Calculate the total thermal resistance (R-value) in m²·K/W.""" |
| return 1 / self.u_value if self.u_value > 0 else float('inf') |
| |
| @property |
| def total_r_value_from_layers(self) -> Optional[float]: |
| """Calculate the total R-value from material layers if available.""" |
| if not self.material_layers: |
| return None |
| |
| |
| r_si = 0.13 |
| r_se = 0.04 |
| |
| |
| r_layers = sum(layer.r_value for layer in self.material_layers) |
| |
| return r_si + r_layers + r_se |
| |
| @property |
| def calculated_u_value(self) -> Optional[float]: |
| """Calculate U-value from material layers if available.""" |
| total_r = self.total_r_value_from_layers |
| if total_r is None or total_r == 0: |
| return None |
| return 1 / total_r |
| |
| def heat_transfer_rate(self, delta_t: float) -> float: |
| """ |
| Calculate heat transfer rate through the component. |
| |
| Args: |
| delta_t: Temperature difference across the component in K or °C |
| |
| Returns: |
| Heat transfer rate in Watts |
| """ |
| return self.u_value * self.area * delta_t |
| |
| def to_dict(self) -> Dict: |
| """Convert the building component to a dictionary.""" |
| return { |
| "id": self.id, |
| "name": self.name, |
| "component_type": self.component_type.value, |
| "u_value": self.u_value, |
| "area": self.area, |
| "orientation": self.orientation.value, |
| "color": self.color, |
| "r_value": self.r_value, |
| "material_layers": [layer.to_dict() for layer in self.material_layers], |
| "calculated_u_value": self.calculated_u_value, |
| "total_r_value_from_layers": self.total_r_value_from_layers |
| } |
|
|
|
|
| @dataclass |
| class Wall(BuildingComponent): |
| """Class representing a wall component.""" |
| |
| has_sun_exposure: bool = True |
| wall_type: str = "Custom" |
| wall_group: str = "A" |
| gross_area: float = None |
| net_area: float = None |
| windows: List[str] = field(default_factory=list) |
| doors: List[str] = field(default_factory=list) |
| |
| def __post_init__(self): |
| """Initialize wall-specific attributes.""" |
| super().__post_init__() |
| self.component_type = ComponentType.WALL |
| |
| |
| if self.net_area is None: |
| self.net_area = self.area |
| |
| |
| if self.gross_area is None: |
| self.gross_area = self.net_area |
| |
| def update_net_area(self, window_areas: Dict[str, float], door_areas: Dict[str, float]): |
| """ |
| Update the net wall area by subtracting windows and doors. |
| |
| Args: |
| window_areas: Dictionary mapping window IDs to areas |
| door_areas: Dictionary mapping door IDs to areas |
| """ |
| total_window_area = sum(window_areas.get(window_id, 0) for window_id in self.windows) |
| total_door_area = sum(door_areas.get(door_id, 0) for door_id in self.doors) |
| |
| self.net_area = self.gross_area - total_window_area - total_door_area |
| self.area = self.net_area |
| |
| if self.net_area <= 0: |
| raise ValueError("Net wall area cannot be negative or zero") |
| |
| def to_dict(self) -> Dict: |
| """Convert the wall to a dictionary.""" |
| wall_dict = super().to_dict() |
| wall_dict.update({ |
| "has_sun_exposure": self.has_sun_exposure, |
| "wall_type": self.wall_type, |
| "wall_group": self.wall_group, |
| "gross_area": self.gross_area, |
| "net_area": self.net_area, |
| "windows": self.windows, |
| "doors": self.doors |
| }) |
| return wall_dict |
|
|
|
|
| @dataclass |
| class Roof(BuildingComponent): |
| """Class representing a roof component.""" |
| |
| roof_type: str = "Custom" |
| roof_group: str = "A" |
| pitch: float = 0.0 |
| has_suspended_ceiling: bool = False |
| ceiling_plenum_height: float = 0.0 |
| |
| def __post_init__(self): |
| """Initialize roof-specific attributes.""" |
| super().__post_init__() |
| self.component_type = ComponentType.ROOF |
| self.orientation = Orientation.HORIZONTAL |
| |
| def to_dict(self) -> Dict: |
| """Convert the roof to a dictionary.""" |
| roof_dict = super().to_dict() |
| roof_dict.update({ |
| "roof_type": self.roof_type, |
| "roof_group": self.roof_group, |
| "pitch": self.pitch, |
| "has_suspended_ceiling": self.has_suspended_ceiling, |
| "ceiling_plenum_height": self.ceiling_plenum_height |
| }) |
| return roof_dict |
|
|
|
|
| @dataclass |
| class Floor(BuildingComponent): |
| """Class representing a floor component.""" |
| |
| floor_type: str = "Custom" |
| is_ground_contact: bool = False |
| perimeter_length: float = 0.0 |
| |
| def __post_init__(self): |
| """Initialize floor-specific attributes.""" |
| super().__post_init__() |
| self.component_type = ComponentType.FLOOR |
| self.orientation = Orientation.HORIZONTAL |
| |
| def to_dict(self) -> Dict: |
| """Convert the floor to a dictionary.""" |
| floor_dict = super().to_dict() |
| floor_dict.update({ |
| "floor_type": self.floor_type, |
| "is_ground_contact": self.is_ground_contact, |
| "perimeter_length": self.perimeter_length |
| }) |
| return floor_dict |
|
|
|
|
| @dataclass |
| class Fenestration(BuildingComponent): |
| """Base class for fenestration components (windows, doors, skylights).""" |
| |
| shgc: float = 0.7 |
| vt: float = 0.7 |
| frame_type: str = "Aluminum" |
| frame_width: float = 0.05 |
| has_shading: bool = False |
| shading_type: str = None |
| shading_coefficient: float = 1.0 |
| |
| def __post_init__(self): |
| """Initialize fenestration-specific attributes.""" |
| super().__post_init__() |
| |
| if self.shgc < 0 or self.shgc > 1: |
| raise ValueError("SHGC must be between 0 and 1") |
| if self.vt < 0 or self.vt > 1: |
| raise ValueError("VT must be between 0 and 1") |
| if self.shading_coefficient < 0 or self.shading_coefficient > 1: |
| raise ValueError("Shading coefficient must be between 0 and 1") |
| |
| @property |
| def effective_shgc(self) -> float: |
| """Calculate the effective SHGC considering shading.""" |
| return self.shgc * self.shading_coefficient |
| |
| def to_dict(self) -> Dict: |
| """Convert the fenestration to a dictionary.""" |
| fenestration_dict = super().to_dict() |
| fenestration_dict.update({ |
| "shgc": self.shgc, |
| "vt": self.vt, |
| "frame_type": self.frame_type, |
| "frame_width": self.frame_width, |
| "has_shading": self.has_shading, |
| "shading_type": self.shading_type, |
| "shading_coefficient": self.shading_coefficient, |
| "effective_shgc": self.effective_shgc |
| }) |
| return fenestration_dict |
|
|
|
|
| @dataclass |
| class Window(Fenestration): |
| """Class representing a window component.""" |
| |
| window_type: str = "Custom" |
| glazing_layers: int = 2 |
| gas_fill: str = "Air" |
| low_e_coating: bool = False |
| width: float = 1.0 |
| height: float = 1.0 |
| wall_id: str = None |
| |
| def __post_init__(self): |
| """Initialize window-specific attributes.""" |
| super().__post_init__() |
| self.component_type = ComponentType.WINDOW |
| |
| |
| if self.area <= 0 and self.width > 0 and self.height > 0: |
| self.area = self.width * self.height |
| |
| def to_dict(self) -> Dict: |
| """Convert the window to a dictionary.""" |
| window_dict = super().to_dict() |
| window_dict.update({ |
| "window_type": self.window_type, |
| "glazing_layers": self.glazing_layers, |
| "gas_fill": self.gas_fill, |
| "low_e_coating": self.low_e_coating, |
| "width": self.width, |
| "height": self.height, |
| "wall_id": self.wall_id |
| }) |
| return window_dict |
|
|
|
|
| @dataclass |
| class Door(Fenestration): |
| """Class representing a door component.""" |
| |
| door_type: str = "Custom" |
| glazing_percentage: float = 0.0 |
| width: float = 0.9 |
| height: float = 2.1 |
| wall_id: str = None |
| |
| def __post_init__(self): |
| """Initialize door-specific attributes.""" |
| super().__post_init__() |
| self.component_type = ComponentType.DOOR |
| |
| |
| if self.area <= 0 and self.width > 0 and self.height > 0: |
| self.area = self.width * self.height |
| |
| if self.glazing_percentage < 0 or self.glazing_percentage > 100: |
| raise ValueError("Glazing percentage must be between 0 and 100") |
| |
| @property |
| def glazing_area(self) -> float: |
| """Calculate the glazed area of the door in m².""" |
| return self.area * (self.glazing_percentage / 100) |
| |
| @property |
| def opaque_area(self) -> float: |
| """Calculate the opaque area of the door in m².""" |
| return self.area - self.glazing_area |
| |
| def to_dict(self) -> Dict: |
| """Convert the door to a dictionary.""" |
| door_dict = super().to_dict() |
| door_dict.update({ |
| "door_type": self.door_type, |
| "glazing_percentage": self.glazing_percentage, |
| "width": self.width, |
| "height": self.height, |
| "wall_id": self.wall_id, |
| "glazing_area": self.glazing_area, |
| "opaque_area": self.opaque_area |
| }) |
| return door_dict |
|
|
|
|
| @dataclass |
| class Skylight(Fenestration): |
| """Class representing a skylight component.""" |
| |
| skylight_type: str = "Custom" |
| glazing_layers: int = 2 |
| gas_fill: str = "Air" |
| low_e_coating: bool = False |
| width: float = 1.0 |
| length: float = 1.0 |
| roof_id: str = None |
| |
| def __post_init__(self): |
| """Initialize skylight-specific attributes.""" |
| super().__post_init__() |
| self.component_type = ComponentType.SKYLIGHT |
| self.orientation = Orientation.HORIZONTAL |
| |
| |
| if self.area <= 0 and self.width > 0 and self.length > 0: |
| self.area = self.width * self.length |
| |
| def to_dict(self) -> Dict: |
| """Convert the skylight to a dictionary.""" |
| skylight_dict = super().to_dict() |
| skylight_dict.update({ |
| "skylight_type": self.skylight_type, |
| "glazing_layers": self.glazing_layers, |
| "gas_fill": self.gas_fill, |
| "low_e_coating": self.low_e_coating, |
| "width": self.width, |
| "length": self.length, |
| "roof_id": self.roof_id |
| }) |
| return skylight_dict |
|
|
|
|
| class BuildingComponentFactory: |
| """Factory class for creating building components.""" |
| |
| @staticmethod |
| def create_component(component_data: Dict) -> BuildingComponent: |
| """ |
| Create a building component from a dictionary of data. |
| |
| Args: |
| component_data: Dictionary containing component data |
| |
| Returns: |
| A BuildingComponent object of the appropriate type |
| """ |
| component_type = component_data.get("component_type") |
| |
| if component_type == ComponentType.WALL.value: |
| return Wall(**component_data) |
| elif component_type == ComponentType.ROOF.value: |
| return Roof(**component_data) |
| elif component_type == ComponentType.FLOOR.value: |
| return Floor(**component_data) |
| elif component_type == ComponentType.WINDOW.value: |
| return Window(**component_data) |
| elif component_type == ComponentType.DOOR.value: |
| return Door(**component_data) |
| elif component_type == ComponentType.SKYLIGHT.value: |
| return Skylight(**component_data) |
| else: |
| raise ValueError(f"Unknown component type: {component_type}") |
|
|