Spaces:
Sleeping
Sleeping
| """ | |
| HVAC Component Selection Module | |
| Provides UI for selecting building components in the HVAC Load Calculator. | |
| All dependencies are included within this file for standalone operation. | |
| Updated 2025-04-28: Added perimeter field to Floor class for F-factor calculations. | |
| Updated 2025-04-29: Fixed Floors table headings, added insulated field for dynamic F-factor. | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import json | |
| import uuid | |
| from dataclasses import dataclass, field | |
| from enum import Enum | |
| from typing import Dict, List, Any, Optional | |
| import io | |
| # --- Enums --- | |
| class Orientation(Enum): | |
| 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): | |
| WALL = "Wall" | |
| ROOF = "Roof" | |
| FLOOR = "Floor" | |
| WINDOW = "Window" | |
| DOOR = "Door" | |
| # --- Data Models --- | |
| class MaterialLayer: | |
| name: str | |
| thickness: float # in mm | |
| conductivity: float # W/(m·K) | |
| class BuildingComponent: | |
| id: str = field(default_factory=lambda: str(uuid.uuid4())) | |
| name: str = "Unnamed Component" | |
| component_type: ComponentType = ComponentType.WALL | |
| u_value: float = 0.0 # W/(m²·K) | |
| area: float = 0.0 # m² | |
| orientation: Orientation = Orientation.NOT_APPLICABLE | |
| def __post_init__(self): | |
| if self.area <= 0: | |
| raise ValueError("Area must be greater than zero") | |
| if self.u_value <= 0: | |
| raise ValueError("U-value must be greater than zero") | |
| def to_dict(self) -> dict: | |
| 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 | |
| } | |
| class Wall(BuildingComponent): | |
| wall_type: str = "Brick" | |
| wall_group: str = "A" # ASHRAE group | |
| absorptivity: float = 0.6 | |
| shading_coefficient: float = 1.0 | |
| infiltration_rate_cfm: float = 0.0 | |
| def __post_init__(self): | |
| super().__post_init__() | |
| self.component_type = ComponentType.WALL | |
| if not 0 <= self.absorptivity <= 1: | |
| raise ValueError("Absorptivity must be between 0 and 1") | |
| if not 0 <= self.shading_coefficient <= 1: | |
| raise ValueError("Shading coefficient must be between 0 and 1") | |
| if self.infiltration_rate_cfm < 0: | |
| raise ValueError("Infiltration rate cannot be negative") | |
| VALID_WALL_GROUPS = {"A", "B", "C", "D", "E", "F", "G", "H"} | |
| if self.wall_group not in VALID_WALL_GROUPS: | |
| st.warning(f"Invalid wall_group '{self.wall_group}' for wall '{self.name}'. Defaulting to 'A'.") | |
| self.wall_group = "A" | |
| def to_dict(self) -> dict: | |
| base_dict = super().to_dict() | |
| base_dict.update({ | |
| "wall_type": self.wall_type, "wall_group": self.wall_group, "absorptivity": self.absorptivity, | |
| "shading_coefficient": self.shading_coefficient, "infiltration_rate_cfm": self.infiltration_rate_cfm | |
| }) | |
| return base_dict | |
| class Roof(BuildingComponent): | |
| roof_type: str = "Concrete" | |
| roof_group: str = "A" # ASHRAE group | |
| slope: str = "Flat" | |
| absorptivity: float = 0.6 | |
| def __post_init__(self): | |
| super().__post_init__() | |
| self.component_type = ComponentType.ROOF | |
| if not self.orientation == Orientation.HORIZONTAL: | |
| self.orientation = Orientation.HORIZONTAL | |
| if not 0 <= self.absorptivity <= 1: | |
| raise ValueError("Absorptivity must be between 0 and 1") | |
| VALID_ROOF_GROUPS = {"A", "B", "C", "D", "E", "F", "G"} | |
| if self.roof_group not in VALID_ROOF_GROUPS: | |
| st.warning(f"Invalid roof_group '{self.roof_group}' for roof '{self.name}'. Defaulting to 'A'.") | |
| self.roof_group = "A" | |
| def to_dict(self) -> dict: | |
| base_dict = super().to_dict() | |
| base_dict.update({ | |
| "roof_type": self.roof_type, "roof_group": self.roof_group, "slope": self.slope, "absorptivity": self.absorptivity | |
| }) | |
| return base_dict | |
| class Floor(BuildingComponent): | |
| floor_type: str = "Concrete" | |
| ground_contact: bool = True | |
| ground_temperature_c: float = 25.0 | |
| perimeter: float = 0.0 | |
| insulated: bool = False # NEW: For dynamic F-factor | |
| def __post_init__(self): | |
| super().__post_init__() | |
| self.component_type = ComponentType.FLOOR | |
| self.orientation = Orientation.NOT_APPLICABLE | |
| if self.perimeter < 0: | |
| raise ValueError("Perimeter cannot be negative") | |
| if self.ground_contact and not (-10 <= self.ground_temperature_c <= 40): | |
| raise ValueError("Ground temperature must be between -10°C and 40°C for ground-contact floors") | |
| def to_dict(self) -> dict: | |
| base_dict = super().to_dict() | |
| base_dict.update({ | |
| "floor_type": self.floor_type, "ground_contact": self.ground_contact, | |
| "ground_temperature_c": self.ground_temperature_c, "perimeter": self.perimeter, | |
| "insulated": self.insulated | |
| }) | |
| return base_dict | |
| class Window(BuildingComponent): | |
| shgc: float = 0.7 | |
| shading_device: str = "None" | |
| shading_coefficient: float = 1.0 | |
| frame_type: str = "Aluminum" | |
| frame_percentage: float = 20.0 | |
| infiltration_rate_cfm: float = 0.0 | |
| def __post_init__(self): | |
| super().__post_init__() | |
| self.component_type = ComponentType.WINDOW | |
| if not 0 <= self.shgc <= 1: | |
| raise ValueError("SHGC must be between 0 and 1") | |
| if not 0 <= self.shading_coefficient <= 1: | |
| raise ValueError("Shading coefficient must be between 0 and 1") | |
| if not 0 <= self.frame_percentage <= 30: | |
| raise ValueError("Frame percentage must be between 0 and 30") | |
| if self.infiltration_rate_cfm < 0: | |
| raise ValueError("Infiltration rate cannot be negative") | |
| def to_dict(self) -> dict: | |
| base_dict = super().to_dict() | |
| base_dict.update({ | |
| "shgc": self.shgc, "shading_device": self.shading_device, "shading_coefficient": self.shading_coefficient, | |
| "frame_type": self.frame_type, "frame_percentage": self.frame_percentage, "infiltration_rate_cfm": self.infiltration_rate_cfm | |
| }) | |
| return base_dict | |
| class Door(BuildingComponent): | |
| door_type: str = "Solid Wood" | |
| infiltration_rate_cfm: float = 0.0 | |
| def __post_init__(self): | |
| super().__post_init__() | |
| self.component_type = ComponentType.DOOR | |
| if self.infiltration_rate_cfm < 0: | |
| raise ValueError("Infiltration rate cannot be negative") | |
| def to_dict(self) -> dict: | |
| base_dict = super().to_dict() | |
| base_dict.update({"door_type": self.door_type, "infiltration_rate_cfm": self.infiltration_rate_cfm}) | |
| return base_dict | |
| # --- Reference Data --- | |
| class ReferenceData: | |
| def __init__(self): | |
| self.data = { | |
| "materials": { | |
| "Concrete": {"conductivity": 1.4}, | |
| "Insulation": {"conductivity": 0.04}, | |
| "Brick": {"conductivity": 0.8}, | |
| "Glass": {"conductivity": 1.0}, | |
| "Wood": {"conductivity": 0.15} | |
| }, | |
| "wall_types": { | |
| "Brick Wall": {"u_value": 2.0, "absorptivity": 0.6, "wall_group": "A"}, | |
| "Insulated Brick": {"u_value": 0.5, "absorptivity": 0.6, "wall_group": "B"}, | |
| "Concrete Block": {"u_value": 1.8, "absorptivity": 0.6, "wall_group": "C"}, | |
| "Insulated Concrete": {"u_value": 0.4, "absorptivity": 0.6, "wall_group": "D"}, | |
| "Timber Frame": {"u_value": 0.3, "absorptivity": 0.6, "wall_group": "E"}, | |
| "Cavity Brick": {"u_value": 0.6, "absorptivity": 0.6, "wall_group": "F"}, | |
| "Lightweight Panel": {"u_value": 1.0, "absorptivity": 0.6, "wall_group": "G"}, | |
| "Reinforced Concrete": {"u_value": 1.5, "absorptivity": 0.6, "wall_group": "H"}, | |
| "SIP": {"u_value": 0.25, "absorptivity": 0.6, "wall_group": "A"}, | |
| "Custom": {"u_value": 0.5, "absorptivity": 0.6, "wall_group": "A"} | |
| }, | |
| "roof_types": { | |
| "Concrete Roof": {"u_value": 0.3, "absorptivity": 0.6, "group": "A"}, | |
| "Metal Roof": {"u_value": 1.0, "absorptivity": 0.75, "group": "B"} | |
| }, | |
| "roof_ventilation_methods": { | |
| "No Ventilation": 0.0, | |
| "Natural Low": 0.1, | |
| "Natural High": 0.5, | |
| "Mechanical": 1.0 | |
| }, | |
| "floor_types": { | |
| "Concrete Slab": {"u_value": 0.4, "ground_contact": True}, | |
| "Wood Floor": {"u_value": 0.8, "ground_contact": False} | |
| }, | |
| "window_types": { | |
| "Double Glazed": {"u_value": 2.8, "shgc": 0.7, "frame_type": "Aluminum"}, | |
| "Single Glazed": {"u_value": 5.0, "shgc": 0.9, "frame_type": "Wood"} | |
| }, | |
| "shading_devices": { | |
| "None": 1.0, | |
| "Venetian Blinds": 0.6, | |
| "Overhang": 0.4, | |
| "Roller Shades": 0.5, | |
| "Drapes": 0.7 | |
| }, | |
| "door_types": { | |
| "Solid Wood": {"u_value": 2.0}, | |
| "Glass Door": {"u_value": 3.5} | |
| } | |
| } | |
| def get_materials(self) -> List[Dict[str, Any]]: | |
| return [{"name": k, "conductivity": v["conductivity"]} for k, v in self.data["materials"].items()] | |
| reference_data = ReferenceData() | |
| # --- Component Library --- | |
| class ComponentLibrary: | |
| def __init__(self): | |
| self.components = {} | |
| def add_component(self, component: BuildingComponent): | |
| self.components[component.id] = component | |
| def remove_component(self, component_id: str): | |
| if not component_id.startswith("preset_") and component_id in self.components: | |
| del self.components[component_id] | |
| component_library = ComponentLibrary() | |
| # --- U-Value Calculator --- | |
| class UValueCalculator: | |
| def __init__(self): | |
| self.materials = reference_data.get_materials() | |
| def calculate_u_value(self, layers: List[Dict[str, float]], outside_resistance: float, inside_resistance: float) -> float: | |
| r_layers = sum(layer["thickness"] / 1000 / layer["conductivity"] for layer in layers) | |
| r_total = outside_resistance + r_layers + inside_resistance | |
| return 1 / r_total if r_total > 0 else 0 | |
| u_value_calculator = UValueCalculator() | |
| # --- Component Selection Interface --- | |
| class ComponentSelectionInterface: | |
| def __init__(self): | |
| self.component_library = component_library | |
| self.u_value_calculator = u_value_calculator | |
| self.reference_data = reference_data | |
| def display_component_selection(self, session_state: Any) -> None: | |
| st.title("Building Components") | |
| if 'components' not in session_state: | |
| session_state.components = {'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': []} | |
| if 'roof_air_volume_m3' not in session_state: | |
| session_state.roof_air_volume_m3 = 0.0 | |
| if 'roof_ventilation_ach' not in session_state: | |
| session_state.roof_ventilation_ach = 0.0 | |
| tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "U-Value Calculator"]) | |
| with tabs[0]: | |
| self._display_component_tab(session_state, ComponentType.WALL) | |
| with tabs[1]: | |
| self._display_component_tab(session_state, ComponentType.ROOF) | |
| with tabs[2]: | |
| self._display_component_tab(session_state, ComponentType.FLOOR) | |
| with tabs[3]: | |
| self._display_component_tab(session_state, ComponentType.WINDOW) | |
| with tabs[4]: | |
| self._display_component_tab(session_state, ComponentType.DOOR) | |
| with tabs[5]: | |
| self._display_u_value_calculator_tab(session_state) | |
| if st.button("Save Components"): | |
| self._save_components(session_state) | |
| def _display_component_tab(self, session_state: Any, component_type: ComponentType) -> None: | |
| type_name = component_type.value.lower() | |
| st.subheader(f"{type_name.capitalize()} Components") | |
| with st.expander(f"Add {type_name.capitalize()}", expanded=True): | |
| if component_type == ComponentType.WALL: | |
| self._display_add_wall_form(session_state) | |
| elif component_type == ComponentType.ROOF: | |
| self._display_add_roof_form(session_state) | |
| elif component_type == ComponentType.FLOOR: | |
| self._display_add_floor_form(session_state) | |
| elif component_type == ComponentType.WINDOW: | |
| self._display_add_window_form(session_state) | |
| elif component_type == ComponentType.DOOR: | |
| self._display_add_door_form(session_state) | |
| components = session_state.components.get(type_name + 's', []) | |
| if components or component_type == ComponentType.ROOF: | |
| st.subheader(f"Existing {type_name.capitalize()} Components") | |
| self._display_components_table(session_state, component_type, components) | |
| def _display_add_wall_form(self, session_state: Any) -> None: | |
| st.write("Add walls manually or upload a file.") | |
| method = st.radio("Add Wall Method", ["Manual Entry", "File Upload"]) | |
| if "add_wall_submitted" not in session_state: | |
| session_state.add_wall_submitted = False | |
| if method == "Manual Entry": | |
| with st.form("add_wall_form", clear_on_submit=True): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| name = st.text_input("Name", "New Wall") | |
| area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1) | |
| orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0) | |
| with col2: | |
| wall_options = self.reference_data.data["wall_types"] | |
| selected_wall = st.selectbox("Wall Type", options=list(wall_options.keys())) | |
| u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(wall_options[selected_wall]["u_value"]), step=0.01) | |
| wall_group = st.selectbox("Wall Group (ASHRAE)", ["A", "B", "C", "D", "E", "F", "G", "H"], index=0) | |
| absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"], index=2) | |
| shading_coefficient = st.number_input("Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05) | |
| infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1) | |
| submitted = st.form_submit_button("Add Wall") | |
| if submitted and not session_state.add_wall_submitted: | |
| try: | |
| absorptivity_value = float(absorptivity.split("(")[1].strip(")")) | |
| new_wall = Wall( | |
| name=name, u_value=u_value, area=area, orientation=Orientation(orientation), | |
| wall_type=selected_wall, wall_group=wall_group, absorptivity=absorptivity_value, | |
| shading_coefficient=shading_coefficient, infiltration_rate_cfm=infiltration_rate | |
| ) | |
| self.component_library.add_component(new_wall) | |
| session_state.components['walls'].append(new_wall) | |
| st.success(f"Added {new_wall.name}") | |
| session_state.add_wall_submitted = True | |
| st.rerun() | |
| except ValueError as e: | |
| st.error(f"Error: {str(e)}") | |
| if session_state.add_wall_submitted: | |
| session_state.add_wall_submitted = False | |
| elif method == "File Upload": | |
| uploaded_file = st.file_uploader("Upload Walls File", type=["csv", "xlsx"], key="wall_upload") | |
| required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)"] | |
| template_data = pd.DataFrame(columns=required_cols) | |
| template_data.loc[0] = ["Example Wall", 10.0, 2.0, "North", "Brick Wall", "A", 0.6, 1.0, 0.0] | |
| st.download_button(label="Download Wall Template", data=template_data.to_csv(index=False), file_name="wall_template.csv", mime="text/csv") | |
| if uploaded_file: | |
| df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file) | |
| if all(col in df.columns for col in required_cols): | |
| valid_wall_groups = {"A", "B", "C", "D", "E", "F", "G", "H"} | |
| for _, row in df.iterrows(): | |
| try: | |
| wall_group = str(row["Wall Group"]) | |
| if wall_group not in valid_wall_groups: | |
| st.warning(f"Invalid Wall Group '{wall_group}' in row '{row['Name']}'. Defaulting to 'A'.") | |
| wall_group = "A" | |
| new_wall = Wall( | |
| name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]), | |
| orientation=Orientation(row["Orientation"]), wall_type=str(row["Wall Type"]), | |
| wall_group=wall_group, absorptivity=float(row["Absorptivity"]), | |
| shading_coefficient=float(row["Shading Coefficient"]), infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"]) | |
| ) | |
| self.component_library.add_component(new_wall) | |
| session_state.components['walls'].append(new_wall) | |
| except ValueError as e: | |
| st.error(f"Error in row {row['Name']}: {str(e)}") | |
| st.success("Walls uploaded successfully!") | |
| st.rerun() | |
| else: | |
| st.error(f"File must contain: {', '.join(required_cols)}") | |
| def _display_add_roof_form(self, session_state: Any) -> None: | |
| st.write("Add roofs manually or upload a file.") | |
| method = st.radio("Add Roof Method", ["Manual Entry", "File Upload"]) | |
| if "add_roof_submitted" not in session_state: | |
| session_state.add_roof_submitted = False | |
| st.subheader("Roof System Ventilation") | |
| air_volume = st.number_input("Air Volume (m³)", min_value=0.0, value=session_state.roof_air_volume_m3, step=1.0, help="Total volume between roof and ceiling") | |
| vent_options = {f"{k} (ACH={v})": v for k, v in self.reference_data.data["roof_ventilation_methods"].items()} | |
| vent_options["Custom"] = None | |
| ventilation_method = st.selectbox("Ventilation Method", options=list(vent_options.keys()), index=0, help="Applies to entire roof system") | |
| ventilation_ach = st.number_input("Custom Ventilation Rate (ACH)", min_value=0.0, max_value=10.0, value=0.0, step=0.1) if ventilation_method == "Custom" else vent_options[ventilation_method] | |
| session_state.roof_air_volume_m3 = air_volume | |
| session_state.roof_ventilation_ach = ventilation_ach | |
| if method == "Manual Entry": | |
| with st.form("add_roof_form", clear_on_submit=True): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| name = st.text_input("Name", "New Roof") | |
| area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1) | |
| orientation = Orientation.HORIZONTAL.value | |
| with col2: | |
| roof_options = self.reference_data.data["roof_types"] | |
| selected_roof = st.selectbox("Roof Type", options=list(roof_options.keys())) | |
| u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(roof_options[selected_roof]["u_value"]), step=0.01) | |
| roof_group = st.selectbox("Roof Group (ASHRAE)", ["A", "B", "C", "D", "E", "F", "G"], index=0) | |
| slope = st.selectbox("Slope", ["Flat", "Pitched"], index=0) | |
| absorptivity = st.selectbox("Solar Absorptivity", ["Light (0.3)", "Light to Medium (0.45)", "Medium (0.6)", "Medium to Dark (0.75)", "Dark (0.9)"], index=2) | |
| submitted = st.form_submit_button("Add Roof") | |
| if submitted and not session_state.add_roof_submitted: | |
| try: | |
| absorptivity_value = float(absorptivity.split("(")[1].strip(")")) | |
| new_roof = Roof( | |
| name=name, u_value=u_value, area=area, orientation=Orientation(orientation), | |
| roof_type=selected_roof, roof_group=roof_group, slope=slope, absorptivity=absorptivity_value | |
| ) | |
| self.component_library.add_component(new_roof) | |
| session_state.components['roofs'].append(new_roof) | |
| st.success(f"Added {new_roof.name}") | |
| session_state.add_roof_submitted = True | |
| st.rerun() | |
| except ValueError as e: | |
| st.error(f"Error: {str(e)}") | |
| if session_state.add_roof_submitted: | |
| session_state.add_roof_submitted = False | |
| elif method == "File Upload": | |
| uploaded_file = st.file_uploader("Upload Roofs File", type=["csv", "xlsx"], key="roof_upload") | |
| required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Roof Type", "Roof Group", "Slope", "Absorptivity"] | |
| template_data = pd.DataFrame(columns=required_cols) | |
| template_data.loc[0] = ["Example Roof", 10.0, 0.3, "Horizontal", "Concrete Roof", "A", "Flat", 0.6] | |
| st.download_button(label="Download Roof Template", data=template_data.to_csv(index=False), file_name="roof_template.csv", mime="text/csv") | |
| if uploaded_file: | |
| df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file) | |
| if all(col in df.columns for col in required_cols): | |
| valid_roof_groups = {"A", "B", "C", "D", "E", "F", "G"} | |
| for _, row in df.iterrows(): | |
| try: | |
| roof_group = str(row["Roof Group"]) | |
| if roof_group not in valid_roof_groups: | |
| st.warning(f"Invalid Roof Group '{roof_group}' in row '{row['Name']}'. Defaulting to 'A'.") | |
| roof_group = "A" | |
| new_roof = Roof( | |
| name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]), | |
| orientation=Orientation(row["Orientation"]), roof_type=str(row["Roof Type"]), | |
| roof_group=roof_group, slope=str(row["Slope"]), absorptivity=float(row["Absorptivity"]) | |
| ) | |
| self.component_library.add_component(new_roof) | |
| session_state.components['roofs'].append(new_roof) | |
| except ValueError as e: | |
| st.error(f"Error in row {row['Name']}: {str(e)}") | |
| st.success("Roofs uploaded successfully!") | |
| st.rerun() | |
| else: | |
| st.error(f"File must contain: {', '.join(required_cols)}") | |
| def _display_add_floor_form(self, session_state: Any) -> None: | |
| st.write("Add floors manually or upload a file.") | |
| method = st.radio("Add Floor Method", ["Manual Entry", "File Upload"]) | |
| if "add_floor_submitted" not in session_state: | |
| session_state.add_floor_submitted = False | |
| if method == "Manual Entry": | |
| with st.form("add_floor_form", clear_on_submit=True): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| name = st.text_input("Name", "New Floor") | |
| area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1) | |
| perimeter = st.number_input("Perimeter (m)", min_value=0.0, value=0.0, step=0.1) | |
| with col2: | |
| floor_options = self.reference_data.data["floor_types"] | |
| selected_floor = st.selectbox("Floor Type", options=list(floor_options.keys())) | |
| u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(floor_options[selected_floor]["u_value"]), step=0.01) | |
| ground_contact = st.selectbox("Ground Contact", ["Yes", "No"], index=0 if floor_options[selected_floor]["ground_contact"] else 1) | |
| ground_temp = st.number_input("Ground Temperature (°C)", min_value=-10.0, max_value=40.0, value=25.0, step=0.1) if ground_contact == "Yes" else 25.0 | |
| insulated = st.checkbox("Insulated Floor (e.g., R-10)", value=False) # NEW: Insulation option | |
| submitted = st.form_submit_button("Add Floor") | |
| if submitted and not session_state.add_floor_submitted: | |
| try: | |
| new_floor = Floor( | |
| name=name, u_value=u_value, area=area, floor_type=selected_floor, | |
| ground_contact=(ground_contact == "Yes"), ground_temperature_c=ground_temp, | |
| perimeter=perimeter, insulated=insulated | |
| ) | |
| self.component_library.add_component(new_floor) | |
| session_state.components['floors'].append(new_floor) | |
| st.success(f"Added {new_floor.name}") | |
| session_state.add_floor_submitted = True | |
| st.rerun() | |
| except ValueError as e: | |
| st.error(f"Error: {str(e)}") | |
| if session_state.add_floor_submitted: | |
| session_state.add_floor_submitted = False | |
| elif method == "File Upload": | |
| uploaded_file = st.file_uploader("Upload Floors File", type=["csv", "xlsx"], key="floor_upload") | |
| required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact", "Ground Temperature (°C)", "Perimeter (m)", "Insulated"] # NEW: Added Insulated | |
| template_data = pd.DataFrame(columns=required_cols) | |
| template_data.loc[0] = ["Example Floor", 10.0, 0.4, "Concrete Slab", "Yes", 25.0, 12.0, "No"] | |
| st.download_button(label="Download Floor Template", data=template_data.to_csv(index=False), file_name="floor_template.csv", mime="text/csv") | |
| if uploaded_file: | |
| df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file) | |
| if all(col in df.columns for col in required_cols): | |
| for _, row in df.iterrows(): | |
| try: | |
| insulated = str(row["Insulated"]).lower() in ["yes", "true", "1"] | |
| new_floor = Floor( | |
| name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]), | |
| floor_type=str(row["Floor Type"]), ground_contact=(str(row["Ground Contact"]).lower() == "yes"), | |
| ground_temperature_c=float(row["Ground Temperature (°C)"]), perimeter=float(row["Perimeter (m)"]), | |
| insulated=insulated | |
| ) | |
| self.component_library.add_component(new_floor) | |
| session_state.components['floors'].append(new_floor) | |
| except ValueError as e: | |
| st.error(f"Error in row {row['Name']}: {str(e)}") | |
| st.success("Floors uploaded successfully!") | |
| st.rerun() | |
| else: | |
| st.error(f"File must contain: {', '.join(required_cols)}") | |
| def _display_add_window_form(self, session_state: Any) -> None: | |
| st.write("Add windows manually or upload a file.") | |
| method = st.radio("Add Window Method", ["Manual Entry", "File Upload"]) | |
| if "add_window_submitted" not in session_state: | |
| session_state.add_window_submitted = False | |
| if method == "Manual Entry": | |
| with st.form("add_window_form", clear_on_submit=True): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| name = st.text_input("Name", "New Window") | |
| area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1) | |
| orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0) | |
| shgc = st.number_input("SHGC", min_value=0.0, max_value=1.0, value=0.7, step=0.01) | |
| with col2: | |
| window_options = self.reference_data.data["window_types"] | |
| selected_window = st.selectbox("Window Type", options=list(window_options.keys())) | |
| u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(window_options[selected_window]["u_value"]), step=0.01) | |
| shading_options = {f"{k} (SC={v})": k for k, v in self.reference_data.data["shading_devices"].items()} | |
| shading_options["Custom"] = "Custom" | |
| shading_device = st.selectbox("Shading Device", options=list(shading_options.keys()), index=0) | |
| shading_coefficient = st.number_input("Custom Shading Coefficient", min_value=0.0, max_value=1.0, value=1.0, step=0.05) if shading_device == "Custom" else self.reference_data.data["shading_devices"][shading_options[shading_device]] | |
| frame_type = st.selectbox("Frame Type", ["Aluminum", "Wood", "Vinyl"], index=0) | |
| frame_percentage = st.slider("Frame Percentage (%)", min_value=0.0, max_value=30.0, value=20.0) | |
| infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1) | |
| submitted = st.form_submit_button("Add Window") | |
| if submitted and not session_state.add_window_submitted: | |
| try: | |
| new_window = Window( | |
| name=name, u_value=u_value, area=area, orientation=Orientation(orientation), | |
| shgc=shgc, shading_device=shading_options[shading_device], shading_coefficient=shading_coefficient, | |
| frame_type=frame_type, frame_percentage=frame_percentage, infiltration_rate_cfm=infiltration_rate | |
| ) | |
| self.component_library.add_component(new_window) | |
| session_state.components['windows'].append(new_window) | |
| st.success(f"Added {new_window.name}") | |
| session_state.add_window_submitted = True | |
| st.rerun() | |
| except ValueError as e: | |
| st.error(f"Error: {str(e)}") | |
| if session_state.add_window_submitted: | |
| session_state.add_window_submitted = False | |
| elif method == "File Upload": | |
| uploaded_file = st.file_uploader("Upload Windows File", type=["csv", "xlsx"], key="window_upload") | |
| required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "SHGC", "Shading Device", "Shading Coefficient", "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)"] | |
| st.download_button(label="Download Window Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="window_template.csv", mime="text/csv") | |
| if uploaded_file: | |
| df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file) | |
| if all(col in df.columns for col in required_cols): | |
| for _, row in df.iterrows(): | |
| try: | |
| new_window = Window( | |
| name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]), | |
| orientation=Orientation(row["Orientation"]), shgc=float(row["SHGC"]), | |
| shading_device=str(row["Shading Device"]), shading_coefficient=float(row["Shading Coefficient"]), | |
| frame_type=str(row["Frame Type"]), frame_percentage=float(row["Frame Percentage"]), | |
| infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"]) | |
| ) | |
| self.component_library.add_component(new_window) | |
| session_state.components['windows'].append(new_window) | |
| except ValueError as e: | |
| st.error(f"Error in row {row['Name']}: {str(e)}") | |
| st.success("Windows uploaded successfully!") | |
| st.rerun() | |
| else: | |
| st.error(f"File must contain: {', '.join(required_cols)}") | |
| def _display_add_door_form(self, session_state: Any) -> None: | |
| st.write("Add doors manually or upload a file.") | |
| method = st.radio("Add Door Method", ["Manual Entry", "File Upload"]) | |
| if "add_door_submitted" not in session_state: | |
| session_state.add_door_submitted = False | |
| if method == "Manual Entry": | |
| with st.form("add_door_form", clear_on_submit=True): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| name = st.text_input("Name", "New Door") | |
| area = st.number_input("Area (m²)", min_value=0.0, value=1.0, step=0.1) | |
| orientation = st.selectbox("Orientation", [o.value for o in Orientation if o != Orientation.HORIZONTAL and o != Orientation.NOT_APPLICABLE], index=0) | |
| with col2: | |
| door_options = self.reference_data.data["door_types"] | |
| selected_door = st.selectbox("Door Type", options=list(door_options.keys())) | |
| u_value = st.number_input("U-Value (W/m²·K)", min_value=0.0, value=float(door_options[selected_door]["u_value"]), step=0.01) | |
| infiltration_rate = st.number_input("Infiltration Rate (CFM)", min_value=0.0, value=0.0, step=0.1) | |
| submitted = st.form_submit_button("Add Door") | |
| if submitted and not session_state.add_door_submitted: | |
| try: | |
| new_door = Door( | |
| name=name, u_value=u_value, area=area, orientation=Orientation(orientation), | |
| door_type=selected_door, infiltration_rate_cfm=infiltration_rate | |
| ) | |
| self.component_library.add_component(new_door) | |
| session_state.components['doors'].append(new_door) | |
| st.success(f"Added {new_door.name}") | |
| session_state.add_door_submitted = True | |
| st.rerun() | |
| except ValueError as e: | |
| st.error(f"Error: {str(e)}") | |
| if session_state.add_door_submitted: | |
| session_state.add_door_submitted = False | |
| elif method == "File Upload": | |
| uploaded_file = st.file_uploader("Upload Doors File", type=["csv", "xlsx"], key="door_upload") | |
| required_cols = ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type", "Infiltration Rate (CFM)"] | |
| st.download_button(label="Download Door Template", data=pd.DataFrame(columns=required_cols).to_csv(index=False), file_name="door_template.csv", mime="text/csv") | |
| if uploaded_file: | |
| df = pd.read_csv(uploaded_file) if uploaded_file.name.endswith('.csv') else pd.read_excel(uploaded_file) | |
| if all(col in df.columns for col in required_cols): | |
| for _, row in df.iterrows(): | |
| try: | |
| new_door = Door( | |
| name=str(row["Name"]), u_value=float(row["U-Value (W/m²·K)"]), area=float(row["Area (m²)"]), | |
| orientation=Orientation(row["Orientation"]), door_type=str(row["Door Type"]), | |
| infiltration_rate_cfm=float(row["Infiltration Rate (CFM)"]) | |
| ) | |
| self.component_library.add_component(new_door) | |
| session_state.components['doors'].append(new_door) | |
| except ValueError as e: | |
| st.error(f"Error in row {row['Name']}: {str(e)}") | |
| st.success("Doors uploaded successfully!") | |
| st.rerun() | |
| else: | |
| st.error(f"File must contain: {', '.join(required_cols)}") | |
| def _display_components_table(self, session_state: Any, component_type: ComponentType, components: List[BuildingComponent]) -> None: | |
| type_name = component_type.value.lower() | |
| if component_type == ComponentType.ROOF: | |
| st.write(f"Roof Air Volume: {session_state.roof_air_volume_m3} m³, Ventilation Rate: {session_state.roof_ventilation_ach} ACH") | |
| if components: | |
| headers = { | |
| ComponentType.WALL: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Wall Type", "Wall Group", "Absorptivity", "Shading Coefficient", "Infiltration Rate (CFM)", "Delete"], | |
| ComponentType.ROOF: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Roof Type", "Roof Group", "Slope", "Absorptivity", "Delete"], | |
| ComponentType.FLOOR: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Floor Type", "Ground Contact", "Ground Temperature (°C)", "Perimeter (m)", "Insulated", "Delete"], # NEW: Added Insulated | |
| ComponentType.WINDOW: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "SHGC", "Shading Device", "Shading Coefficient", "Frame Type", "Frame Percentage", "Infiltration Rate (CFM)", "Delete"], | |
| ComponentType.DOOR: ["Name", "Area (m²)", "U-Value (W/m²·K)", "Orientation", "Door Type", "Infiltration Rate (CFM)", "Delete"] | |
| }[component_type] | |
| cols = st.columns([1] * len(headers)) | |
| for i, header in enumerate(headers): | |
| cols[i].write(f"**{header}**") | |
| for comp in components: | |
| cols = st.columns([1] * len(headers)) | |
| cols[0].write(comp.name) | |
| cols[1].write(comp.area) | |
| cols[2].write(comp.u_value) | |
| cols[3].write(comp.orientation.value) | |
| if component_type == ComponentType.WALL: | |
| cols[4].write(comp.wall_type) | |
| cols[5].write(comp.wall_group) | |
| cols[6].write(comp.absorptivity) | |
| cols[7].write(comp.shading_coefficient) | |
| cols[8].write(comp.infiltration_rate_cfm) | |
| elif component_type == ComponentType.ROOF: | |
| cols[4].write(comp.roof_type) | |
| cols[5].write(comp.roof_group) | |
| cols[6].write(comp.slope) | |
| cols[7].write(comp.absorptivity) | |
| elif component_type == ComponentType.FLOOR: | |
| cols[4].write(comp.floor_type) | |
| cols[5].write("Yes" if comp.ground_contact else "No") | |
| cols[6].write(comp.ground_temperature_c if comp.ground_contact else "N/A") | |
| cols[7].write(comp.perimeter) | |
| cols[8].write("Yes" if comp.insulated else "No") # NEW: Display insulated | |
| elif component_type == ComponentType.WINDOW: | |
| cols[4].write(comp.shgc) | |
| cols[5].write(comp.shading_device) | |
| cols[6].write(comp.shading_coefficient) | |
| cols[7].write(comp.frame_type) | |
| cols[8].write(comp.frame_percentage) | |
| cols[9].write(comp.infiltration_rate_cfm) | |
| elif component_type == ComponentType.DOOR: | |
| cols[4].write(comp.door_type) | |
| cols[5].write(comp.infiltration_rate_cfm) | |
| if cols[-1].button("Delete", key=f"delete_{comp.id}"): | |
| self.component_library.remove_component(comp.id) | |
| session_state.components[type_name + 's'] = [c for c in components if c.id != comp.id] | |
| st.success(f"Deleted {comp.name}") | |
| st.rerun() | |
| def _display_u_value_calculator_tab(self, session_state: Any) -> None: | |
| st.subheader("U-Value Calculator (Standalone)") | |
| if "u_value_layers" not in session_state: | |
| session_state.u_value_layers = [] | |
| if session_state.u_value_layers: | |
| st.write("Material Layers (Outside to Inside):") | |
| layer_data = [{"Layer": i+1, "Material": l["name"], "Thickness (mm)": l["thickness"], | |
| "Conductivity (W/m·K)": l["conductivity"], "R-Value (m²·K/W)": l["thickness"] / 1000 / l["conductivity"]} | |
| for i, l in enumerate(session_state.u_value_layers)] | |
| st.dataframe(pd.DataFrame(layer_data)) | |
| outside_resistance = st.selectbox("Outside Resistance (m²·K/W)", ["Summer (0.04)", "Winter (0.03)", "Custom"], index=0) | |
| outside_r = float(st.number_input("Custom Outside Resistance", min_value=0.0, value=0.04, step=0.01)) if outside_resistance == "Custom" else (0.04 if outside_resistance.startswith("Summer") else 0.03) | |
| inside_r = st.number_input("Inside Resistance (m²·K/W)", min_value=0.0, value=0.13, step=0.01) | |
| u_value = self.u_value_calculator.calculate_u_value(session_state.u_value_layers, outside_r, inside_r) | |
| st.metric("U-Value", f"{u_value:.3f} W/m²·K") | |
| with st.form("u_value_form"): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| material_options = {m["name"]: m["conductivity"] for m in self.u_value_calculator.materials} | |
| material_name = st.selectbox("Material", options=list(material_options.keys())) | |
| conductivity = st.number_input("Conductivity (W/m·K)", min_value=0.0, value=material_options[material_name], step=0.01) | |
| with col2: | |
| thickness = st.number_input("Thickness (mm)", min_value=0.0, value=100.0, step=1.0) | |
| submitted = st.form_submit_button("Add Layer") | |
| if submitted: | |
| session_state.u_value_layers.append({"name": material_name, "thickness": thickness, "conductivity": conductivity}) | |
| st.rerun() | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("Remove Last Layer"): | |
| if session_state.u_value_layers: | |
| session_state.u_value_layers.pop() | |
| st.rerun() | |
| with col2: | |
| if st.button("Reset"): | |
| session_state.u_value_layers = [] | |
| st.rerun() | |
| def _save_components(self, session_state: Any) -> None: | |
| components_dict = { | |
| "walls": [c.to_dict() for c in session_state.components["walls"]], | |
| "roofs": [c.to_dict() for c in session_state.components["roofs"]], | |
| "floors": [c.to_dict() for c in session_state.components["floors"]], | |
| "windows": [c.to_dict() for c in session_state.components["windows"]], | |
| "doors": [c.to_dict() for c in session_state.components["doors"]], | |
| "roof_air_volume_m3": session_state.roof_air_volume_m3, | |
| "roof_ventilation_ach": session_state.roof_ventilation_ach | |
| } | |
| file_path = "components_export.json" | |
| with open(file_path, 'w') as f: | |
| json.dump(components_dict, f, indent=4) | |
| with open(file_path, 'r') as f: | |
| st.download_button(label="Download Components", data=f, file_name="components.json", mime="application/json") | |
| st.success("Components saved successfully.") | |
| # --- Main Execution --- | |
| if __name__ == "__main__": | |
| interface = ComponentSelectionInterface() | |
| interface.display_component_selection(st.session_state) |