|
|
""" |
|
|
Component library module for HVAC Load Calculator. |
|
|
This module implements the preset component database and component selection interface. |
|
|
""" |
|
|
|
|
|
from typing import Dict, List, Any, Optional, Tuple |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import os |
|
|
import json |
|
|
import uuid |
|
|
from dataclasses import asdict |
|
|
|
|
|
|
|
|
from data.building_components import ( |
|
|
BuildingComponent, Wall, Roof, Floor, Window, Door, Skylight, |
|
|
MaterialLayer, Orientation, ComponentType, BuildingComponentFactory |
|
|
) |
|
|
from data.reference_data import reference_data |
|
|
|
|
|
|
|
|
DATA_DIR = os.path.dirname(os.path.abspath(__file__)) |
|
|
|
|
|
|
|
|
class ComponentLibrary: |
|
|
"""Class for managing building component library.""" |
|
|
|
|
|
def __init__(self): |
|
|
"""Initialize component library.""" |
|
|
self.components = {} |
|
|
self.load_preset_components() |
|
|
|
|
|
def load_preset_components(self): |
|
|
"""Load preset components from reference data.""" |
|
|
|
|
|
for wall_id, wall_data in reference_data.wall_types.items(): |
|
|
|
|
|
material_layers = [] |
|
|
for layer_data in wall_data.get("layers", []): |
|
|
material_id = layer_data.get("material") |
|
|
thickness = layer_data.get("thickness") |
|
|
|
|
|
material = reference_data.get_material(material_id) |
|
|
if material: |
|
|
layer = MaterialLayer( |
|
|
name=material["name"], |
|
|
thickness=thickness, |
|
|
conductivity=material["conductivity"], |
|
|
density=material.get("density"), |
|
|
specific_heat=material.get("specific_heat") |
|
|
) |
|
|
material_layers.append(layer) |
|
|
|
|
|
|
|
|
component_id = f"preset_wall_{wall_id}" |
|
|
wall = Wall( |
|
|
id=component_id, |
|
|
name=wall_data["name"], |
|
|
component_type=ComponentType.WALL, |
|
|
u_value=wall_data["u_value"], |
|
|
area=1.0, |
|
|
orientation=Orientation.NOT_APPLICABLE, |
|
|
wall_type=wall_data["name"], |
|
|
wall_group=wall_data["wall_group"], |
|
|
material_layers=material_layers |
|
|
) |
|
|
|
|
|
self.components[component_id] = wall |
|
|
|
|
|
|
|
|
for roof_id, roof_data in reference_data.roof_types.items(): |
|
|
|
|
|
material_layers = [] |
|
|
for layer_data in roof_data.get("layers", []): |
|
|
material_id = layer_data.get("material") |
|
|
thickness = layer_data.get("thickness") |
|
|
|
|
|
material = reference_data.get_material(material_id) |
|
|
if material: |
|
|
layer = MaterialLayer( |
|
|
name=material["name"], |
|
|
thickness=thickness, |
|
|
conductivity=material["conductivity"], |
|
|
density=material.get("density"), |
|
|
specific_heat=material.get("specific_heat") |
|
|
) |
|
|
material_layers.append(layer) |
|
|
|
|
|
|
|
|
component_id = f"preset_roof_{roof_id}" |
|
|
roof = Roof( |
|
|
id=component_id, |
|
|
name=roof_data["name"], |
|
|
component_type=ComponentType.ROOF, |
|
|
u_value=roof_data["u_value"], |
|
|
area=1.0, |
|
|
orientation=Orientation.HORIZONTAL, |
|
|
roof_type=roof_data["name"], |
|
|
roof_group=roof_data["roof_group"], |
|
|
material_layers=material_layers |
|
|
) |
|
|
|
|
|
self.components[component_id] = roof |
|
|
|
|
|
|
|
|
for floor_id, floor_data in reference_data.floor_types.items(): |
|
|
|
|
|
material_layers = [] |
|
|
for layer_data in floor_data.get("layers", []): |
|
|
material_id = layer_data.get("material") |
|
|
thickness = layer_data.get("thickness") |
|
|
|
|
|
material = reference_data.get_material(material_id) |
|
|
if material: |
|
|
layer = MaterialLayer( |
|
|
name=material["name"], |
|
|
thickness=thickness, |
|
|
conductivity=material["conductivity"], |
|
|
density=material.get("density"), |
|
|
specific_heat=material.get("specific_heat") |
|
|
) |
|
|
material_layers.append(layer) |
|
|
|
|
|
|
|
|
component_id = f"preset_floor_{floor_id}" |
|
|
floor = Floor( |
|
|
id=component_id, |
|
|
name=floor_data["name"], |
|
|
component_type=ComponentType.FLOOR, |
|
|
u_value=floor_data["u_value"], |
|
|
area=1.0, |
|
|
orientation=Orientation.HORIZONTAL, |
|
|
floor_type=floor_data["name"], |
|
|
is_ground_contact=floor_data["is_ground_contact"], |
|
|
material_layers=material_layers |
|
|
) |
|
|
|
|
|
self.components[component_id] = floor |
|
|
|
|
|
|
|
|
for window_id, window_data in reference_data.window_types.items(): |
|
|
|
|
|
component_id = f"preset_window_{window_id}" |
|
|
window = Window( |
|
|
id=component_id, |
|
|
name=window_data["name"], |
|
|
component_type=ComponentType.WINDOW, |
|
|
u_value=window_data["u_value"], |
|
|
area=1.0, |
|
|
orientation=Orientation.NOT_APPLICABLE, |
|
|
shgc=window_data["shgc"], |
|
|
vt=window_data["vt"], |
|
|
window_type=window_data["name"], |
|
|
glazing_layers=window_data["glazing_layers"], |
|
|
gas_fill=window_data["gas_fill"], |
|
|
low_e_coating=window_data["low_e_coating"] |
|
|
) |
|
|
|
|
|
self.components[component_id] = window |
|
|
|
|
|
|
|
|
for door_id, door_data in reference_data.door_types.items(): |
|
|
|
|
|
component_id = f"preset_door_{door_id}" |
|
|
door = Door( |
|
|
id=component_id, |
|
|
name=door_data["name"], |
|
|
component_type=ComponentType.DOOR, |
|
|
u_value=door_data["u_value"], |
|
|
area=1.0, |
|
|
orientation=Orientation.NOT_APPLICABLE, |
|
|
door_type=door_data["door_type"], |
|
|
glazing_percentage=door_data["glazing_percentage"], |
|
|
shgc=door_data.get("shgc", 0.0), |
|
|
vt=door_data.get("vt", 0.0) |
|
|
) |
|
|
|
|
|
self.components[component_id] = door |
|
|
|
|
|
def get_component(self, component_id: str) -> Optional[BuildingComponent]: |
|
|
""" |
|
|
Get a component by ID. |
|
|
|
|
|
Args: |
|
|
component_id: Component identifier |
|
|
|
|
|
Returns: |
|
|
BuildingComponent object or None if not found |
|
|
""" |
|
|
return self.components.get(component_id) |
|
|
|
|
|
def get_components_by_type(self, component_type: ComponentType) -> List[BuildingComponent]: |
|
|
""" |
|
|
Get all components of a specific type. |
|
|
|
|
|
Args: |
|
|
component_type: Component type |
|
|
|
|
|
Returns: |
|
|
List of BuildingComponent objects |
|
|
""" |
|
|
return [comp for comp in self.components.values() if comp.component_type == component_type] |
|
|
|
|
|
def get_preset_components_by_type(self, component_type: ComponentType) -> List[BuildingComponent]: |
|
|
""" |
|
|
Get all preset components of a specific type. |
|
|
|
|
|
Args: |
|
|
component_type: Component type |
|
|
|
|
|
Returns: |
|
|
List of BuildingComponent objects |
|
|
""" |
|
|
return [comp for comp in self.components.values() |
|
|
if comp.component_type == component_type and comp.id.startswith("preset_")] |
|
|
|
|
|
def get_custom_components_by_type(self, component_type: ComponentType) -> List[BuildingComponent]: |
|
|
""" |
|
|
Get all custom components of a specific type. |
|
|
|
|
|
Args: |
|
|
component_type: Component type |
|
|
|
|
|
Returns: |
|
|
List of BuildingComponent objects |
|
|
""" |
|
|
return [comp for comp in self.components.values() |
|
|
if comp.component_type == component_type and comp.id.startswith("custom_")] |
|
|
|
|
|
def add_component(self, component: BuildingComponent) -> str: |
|
|
""" |
|
|
Add a component to the library. |
|
|
|
|
|
Args: |
|
|
component: BuildingComponent object |
|
|
|
|
|
Returns: |
|
|
Component ID |
|
|
""" |
|
|
if component.id in self.components: |
|
|
|
|
|
component.id = f"custom_{component.component_type.value.lower()}_{str(uuid.uuid4())[:8]}" |
|
|
|
|
|
self.components[component.id] = component |
|
|
return component.id |
|
|
|
|
|
def update_component(self, component_id: str, component: BuildingComponent) -> bool: |
|
|
""" |
|
|
Update a component in the library. |
|
|
|
|
|
Args: |
|
|
component_id: ID of the component to update |
|
|
component: Updated BuildingComponent object |
|
|
|
|
|
Returns: |
|
|
True if the component was updated, False otherwise |
|
|
""" |
|
|
if component_id not in self.components: |
|
|
return False |
|
|
|
|
|
|
|
|
component.id = component_id |
|
|
self.components[component_id] = component |
|
|
return True |
|
|
|
|
|
def remove_component(self, component_id: str) -> bool: |
|
|
""" |
|
|
Remove a component from the library. |
|
|
|
|
|
Args: |
|
|
component_id: ID of the component to remove |
|
|
|
|
|
Returns: |
|
|
True if the component was removed, False otherwise |
|
|
""" |
|
|
if component_id not in self.components: |
|
|
return False |
|
|
|
|
|
|
|
|
if component_id.startswith("preset_"): |
|
|
return False |
|
|
|
|
|
del self.components[component_id] |
|
|
return True |
|
|
|
|
|
def clone_component(self, component_id: str, new_name: str = None) -> Optional[str]: |
|
|
""" |
|
|
Clone a component in the library. |
|
|
|
|
|
Args: |
|
|
component_id: ID of the component to clone |
|
|
new_name: Name for the cloned component (optional) |
|
|
|
|
|
Returns: |
|
|
ID of the cloned component, or None if the original component was not found |
|
|
""" |
|
|
component = self.get_component(component_id) |
|
|
if not component: |
|
|
return None |
|
|
|
|
|
|
|
|
component_dict = asdict(component) |
|
|
|
|
|
|
|
|
component_dict["id"] = f"custom_{component.component_type.value.lower()}_{str(uuid.uuid4())[:8]}" |
|
|
|
|
|
|
|
|
if new_name: |
|
|
component_dict["name"] = new_name |
|
|
else: |
|
|
component_dict["name"] = f"Copy of {component.name}" |
|
|
|
|
|
|
|
|
new_component = BuildingComponentFactory.create_component( |
|
|
component_type=component.component_type, |
|
|
**component_dict |
|
|
) |
|
|
|
|
|
|
|
|
return self.add_component(new_component) |
|
|
|
|
|
def export_to_json(self, filepath: str) -> bool: |
|
|
""" |
|
|
Export the component library to a JSON file. |
|
|
|
|
|
Args: |
|
|
filepath: Path to the output JSON file |
|
|
|
|
|
Returns: |
|
|
True if the export was successful, False otherwise |
|
|
""" |
|
|
try: |
|
|
|
|
|
components_dict = {comp_id: comp.to_dict() for comp_id, comp in self.components.items()} |
|
|
|
|
|
|
|
|
with open(filepath, 'w') as f: |
|
|
json.dump(components_dict, f, indent=2) |
|
|
|
|
|
return True |
|
|
except Exception as e: |
|
|
print(f"Error exporting component library: {e}") |
|
|
return False |
|
|
|
|
|
def import_from_json(self, filepath: str) -> bool: |
|
|
""" |
|
|
Import components from a JSON file. |
|
|
|
|
|
Args: |
|
|
filepath: Path to the input JSON file |
|
|
|
|
|
Returns: |
|
|
True if the import was successful, False otherwise |
|
|
""" |
|
|
try: |
|
|
|
|
|
with open(filepath, 'r') as f: |
|
|
components_dict = json.load(f) |
|
|
|
|
|
|
|
|
for comp_id, comp_dict in components_dict.items(): |
|
|
component_type = ComponentType(comp_dict["component_type"]) |
|
|
component = BuildingComponentFactory.create_component( |
|
|
component_type=component_type, |
|
|
**comp_dict |
|
|
) |
|
|
self.components[comp_id] = component |
|
|
|
|
|
return True |
|
|
except Exception as e: |
|
|
print(f"Error importing component library: {e}") |
|
|
return False |
|
|
|
|
|
|
|
|
class BuildingComponentFactory: |
|
|
"""Factory class for creating building components.""" |
|
|
|
|
|
@staticmethod |
|
|
def create_component(component_type: ComponentType, **kwargs) -> BuildingComponent: |
|
|
""" |
|
|
Create a building component of the specified type. |
|
|
|
|
|
Args: |
|
|
component_type: Type of component to create |
|
|
**kwargs: Component properties |
|
|
|
|
|
Returns: |
|
|
BuildingComponent object |
|
|
""" |
|
|
if component_type == ComponentType.WALL: |
|
|
return Wall(**kwargs) |
|
|
elif component_type == ComponentType.ROOF: |
|
|
return Roof(**kwargs) |
|
|
elif component_type == ComponentType.FLOOR: |
|
|
return Floor(**kwargs) |
|
|
elif component_type == ComponentType.WINDOW: |
|
|
return Window(**kwargs) |
|
|
elif component_type == ComponentType.DOOR: |
|
|
return Door(**kwargs) |
|
|
elif component_type == ComponentType.SKYLIGHT: |
|
|
return Skylight(**kwargs) |
|
|
else: |
|
|
raise ValueError(f"Unknown component type: {component_type}") |
|
|
|
|
|
|
|
|
|
|
|
component_library = ComponentLibrary() |
|
|
|