|
|
""" |
|
|
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}") |
|
|
|