Spaces:
Sleeping
Sleeping
| """ | |
| HVAC Calculator Code Documentation | |
| Developed by: Dr Majed Abuseif, Deakin University | |
| © 2025 | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| import json | |
| import uuid | |
| import time | |
| import io | |
| import logging | |
| from typing import Dict, List, Any, Optional, Tuple | |
| from enum import Enum | |
| import scipy.linalg as linalg | |
| from data.material_library import MaterialLibrary, MaterialCategory, Construction, Material, GlazingMaterial, DoorMaterial | |
| from data.climate_data import ClimateData, ClimateLocation | |
| import streamlit.components.v1 as components | |
| from data.calculation import TFMCalculations, ComponentType | |
| from data.internal_loads import BUILDING_TYPES | |
| # Set up logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| class ComponentType(Enum): | |
| WALL = "Wall" | |
| ROOF = "Roof" | |
| FLOOR = "Floor" | |
| WINDOW = "Window" | |
| DOOR = "Door" | |
| SKYLIGHT = "Skylight" | |
| class GlazingMaterial: | |
| def __init__(self, name: str, shgc: float, u_value: float, h_o: float, is_library: bool = True): | |
| self.name = name | |
| self.shgc = shgc | |
| self.u_value = u_value | |
| self.h_o = h_o | |
| self.is_library = is_library | |
| class DoorMaterial: | |
| def __init__(self, name: str, u_value: float, solar_absorption: float, is_library: bool = True): | |
| self.name = name | |
| self.u_value = u_value | |
| self.solar_absorption = solar_absorption | |
| self.is_library = is_library | |
| class GlazingMaterial: | |
| def __init__(self, name: str, shgc: float, u_value: float, h_o: float, is_library: bool = True): | |
| self.name = name | |
| self.shgc = shgc | |
| self.u_value = u_value | |
| self.h_o = h_o | |
| self.is_library = is_library | |
| class DoorMaterial: | |
| def __init__(self, name: str, u_value: float, solar_absorption: float, is_library: bool = True): | |
| self.name = name | |
| self.u_value = u_value | |
| self.solar_absorption = solar_absorption | |
| self.is_library = is_library | |
| class Component: | |
| def __init__(self, name: str, component_type: ComponentType, area: float, elevation: Optional[str] = None, | |
| orientation: Optional[float] = None, rotation: float = 0.0, tilt: float = None, | |
| construction: Optional[Construction] = None, glazing_material: Optional[GlazingMaterial] = None, | |
| door_material: Optional[DoorMaterial] = None, layers: List[Dict] = None, u_value: float = None, | |
| shgc: float = None): | |
| self.name = name | |
| self.component_type = component_type | |
| self.area = area | |
| self.elevation = elevation # A, B, C, D for walls, windows, doors | |
| self.orientation = orientation # 0–360° for roofs, skylights | |
| self.rotation = rotation # -45° to 45° for walls, windows, doors | |
| self.tilt = tilt # 0–180° for walls/windows/doors, 0–90° for roofs/skylights | |
| self.construction = construction | |
| self.glazing_material = glazing_material | |
| self.door_material = door_material | |
| self.layers = layers if layers else construction.layers if construction else [] | |
| if u_value is not None: | |
| self.u_value = u_value | |
| elif glazing_material: | |
| self.u_value = glazing_material.u_value | |
| elif door_material: | |
| self.u_value = door_material.u_value | |
| else: | |
| self.u_value = construction.u_value if construction else self.calculate_u_value() | |
| self.solar_absorptivity = (door_material.solar_absorption if door_material else | |
| construction.solar_absorption if construction else 0.6) | |
| self.shgc = shgc if shgc is not None else (glazing_material.shgc if glazing_material else None) | |
| # Compute orientation_angle | |
| if component_type in [ComponentType.WALL, ComponentType.WINDOW, ComponentType.DOOR]: | |
| if elevation not in ['A', 'B', 'C', 'D']: | |
| raise ValueError("Elevation must be one of A, B, C, or D") | |
| orientation_map = {'A': 0.0, 'B': 180.0, 'C': 90.0, 'D': 270.0} | |
| base_angle = orientation_map[elevation] | |
| building_orientation = st.session_state.building_info.get("orientation_angle", 0.0) | |
| self.orientation_angle = (base_angle + building_orientation + rotation) % 360 | |
| else: # Roofs, Skylights, Floors | |
| self.orientation_angle = orientation if orientation is not None else 0.0 | |
| self.ctf = self.calculate_ctf() | |
| def calculate_u_value(self) -> float: | |
| if not self.layers: | |
| return 0.1 | |
| r_total = sum(layer["thickness"] / layer["material"].conductivity for layer in self.layers) | |
| return 1 / r_total if r_total > 0 else 0.1 | |
| def calculate_ctf(self) -> List[float]: | |
| num_layers = len(self.layers) | |
| if num_layers == 0: | |
| return [1.0] | |
| A = np.zeros((num_layers, num_layers)) | |
| for i in range(num_layers): | |
| A[i, i] = self.layers[i]["material"].conductivity | |
| b = np.ones(num_layers) | |
| try: | |
| ctf = linalg.solve(A, b) | |
| return ctf.tolist() | |
| except: | |
| return [1.0] * num_layers | |
| class HVACCalculator: | |
| def __init__(self): | |
| st.set_page_config( | |
| page_title="HVAC Load Calculator", | |
| page_icon="🌡️", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| if 'page' not in st.session_state: | |
| st.session_state.page = 'Building Information' | |
| if 'building_info' not in st.session_state: | |
| st.session_state.building_info = { | |
| "project_name": "", | |
| "floor_area": 100.0, | |
| "building_height": 3.0, | |
| "indoor_design_temp": 24.0, | |
| "indoor_design_rh": 50.0, | |
| "ventilation_rate": 0.1, | |
| "orientation_angle": 0.0, | |
| "operation_hours": 8, | |
| "building_type": "Office" | |
| } | |
| if 'climate_data' not in st.session_state: | |
| st.session_state.climate_data = {} | |
| if 'components' not in st.session_state: | |
| st.session_state.components = { | |
| 'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': [], 'skylights': [] | |
| } | |
| if 'internal_loads' not in st.session_state: | |
| st.session_state.internal_loads = { | |
| 'people': [], 'lighting': [], 'equipment': [] | |
| } | |
| if 'calculation_results' not in st.session_state: | |
| st.session_state.calculation_results = { | |
| 'cooling': {}, 'heating': {} | |
| } | |
| if 'sim_period' not in st.session_state: | |
| st.session_state.sim_period = {"type": "Full Year"} | |
| if 'indoor_conditions' not in st.session_state: | |
| st.session_state.indoor_conditions = { | |
| "type": "Fixed", | |
| "temperature": st.session_state.building_info["indoor_design_temp"], | |
| "rh": st.session_state.building_info["indoor_design_rh"] | |
| } | |
| if 'hvac_settings' not in st.session_state: | |
| st.session_state.hvac_settings = { | |
| "cop": 3.5, | |
| "operating_hours": [{"start": 8, "end": 18}] | |
| } | |
| if 'debug_mode' not in st.session_state: | |
| st.session_state.debug_mode = False | |
| if 'project_materials' not in st.session_state: | |
| st.session_state.project_materials = {} | |
| if 'project_constructions' not in st.session_state: | |
| st.session_state.project_constructions = {} | |
| if 'project_glazing_materials' not in st.session_state: | |
| st.session_state.project_glazing_materials = {} | |
| if 'project_door_materials' not in st.session_state: | |
| st.session_state.project_door_materials = {} | |
| if 'material_filter' not in st.session_state: | |
| st.session_state.material_filter = "All" | |
| if 'construction_filter' not in st.session_state: | |
| st.session_state.construction_filter = "All" | |
| if 'glazing_filter' not in st.session_state: | |
| st.session_state.glazing_filter = "All" | |
| if 'door_filter' not in st.session_state: | |
| st.session_state.door_filter = "All" | |
| if 'active_tab' not in st.session_state: | |
| st.session_state.active_tab = "Materials" | |
| if 'form_submitted' not in st.session_state: | |
| st.session_state.form_submitted = False | |
| self.tfm = TFMCalculations() | |
| self.climate_data = ClimateData() | |
| self.material_library = MaterialLibrary() | |
| self.setup_layout() | |
| def setup_layout(self): | |
| st.sidebar.title("HVAC Load Calculator") | |
| st.sidebar.markdown("---") | |
| st.sidebar.subheader("Navigation") | |
| pages = [ | |
| "Building Information", | |
| "Climate Data and Design Requirements", | |
| "Material Library", | |
| "Building Components", | |
| "Internal Loads", | |
| "Calculation Results", | |
| "Export Data" | |
| ] | |
| selected_page = st.sidebar.radio("Go to", pages, index=pages.index(st.session_state.page)) | |
| if selected_page != st.session_state.page: | |
| st.session_state.page = selected_page | |
| self.display_page(st.session_state.page) | |
| st.sidebar.markdown("---") | |
| st.sidebar.info( | |
| "HVAC Load Calculator v2.0.0\n\n" | |
| "Based on ASHRAE CTF/TFM methods\n\n" | |
| "Developed by: Dr Majed Abuseif\n\n" | |
| "School of Architecture and Built Environment\n\n" | |
| "Deakin University\n\n" | |
| "© 2025" | |
| ) | |
| st.sidebar.markdown("### Help") | |
| st.sidebar.write("Learn about CTF/TFM methods:") | |
| st.sidebar.link_button("ASHRAE Handbook", "https://www.ashrae.org/technical-resources/bookstore/handbook") | |
| def display_page(self, page: str): | |
| if page == "Building Information": | |
| self.display_building_info() | |
| elif page == "Climate Data and Design Requirements": | |
| self.display_climate_data() | |
| elif page == "Material Library": | |
| self.display_material_library() | |
| elif page == "Building Components": | |
| self.display_building_components() | |
| elif page =="Internal Loads": | |
| self.display_internal_loads() | |
| elif page == "Calculation Results": | |
| self.display_calculation_results() | |
| elif page == "Export Data": | |
| self.display_export_data() | |
| def display_building_info(self): | |
| st.title("Building Information") | |
| with st.form("building_info_form"): | |
| project_name = st.text_input( | |
| "Project Name", | |
| value=st.session_state.building_info["project_name"], | |
| help="Unique identifier for the project." | |
| ) | |
| floor_area = st.number_input( | |
| "Floor Area (square meters)", | |
| min_value=1.0, | |
| value=st.session_state.building_info["floor_area"], | |
| help="Total floor area of the building in square meters." | |
| ) | |
| building_height = st.number_input( | |
| "Building Height (meters)", | |
| min_value=1.0, | |
| max_value=100.0, | |
| value=st.session_state.building_info["building_height"], | |
| help="Average height of the building in meters." | |
| ) | |
| building_type = st.selectbox( | |
| "Building Type", | |
| BUILDING_TYPES, | |
| index=BUILDING_TYPES.index(st.session_state.building_info["building_type"]), | |
| help="Primary use of the building, setting ASHRAE-compliant ventilation and load parameters." | |
| ) | |
| indoor_design_temp = st.number_input( | |
| "Indoor Design Temperature (°C)", | |
| min_value=15.0, | |
| max_value=30.0, | |
| value=st.session_state.building_info["indoor_design_temp"], | |
| help="Target indoor temperature for cooling season comfort (15–30°C)." | |
| ) | |
| indoor_design_rh = st.number_input( | |
| "Indoor Design Relative Humidity (%)", | |
| min_value=0.0, | |
| max_value=100.0, | |
| value=st.session_state.building_info["indoor_design_rh"], | |
| help="Target indoor relative humidity for comfort (0–100%)." | |
| ) | |
| orientation_angle = st.slider( | |
| "Orientation Angle (°)", | |
| min_value=-180, | |
| max_value=180, | |
| value=int(st.session_state.building_info["orientation_angle"]), | |
| step=1, | |
| help="Sets the building’s rotation angle relative to north (0°). Component orientations are defined as A (North), B (South), C (East), D (West) at 0°. Adjusting this angle rotates all component orientations accordingly." | |
| ) | |
| operation_hours = st.slider( | |
| "Operation Hours (hours/day)", | |
| min_value=0, | |
| max_value=24, | |
| value=st.session_state.building_info["operation_hours"], | |
| help="Daily hours the building is occupied or operational, affecting internal loads (0–24 hours)." | |
| ) | |
| if st.form_submit_button("Save"): | |
| if not project_name: | |
| st.error("Project Name is required.") | |
| elif not building_type: | |
| st.error("Building Type is required.") | |
| else: | |
| st.session_state.building_info.update({ | |
| "project_name": project_name, | |
| "floor_area": floor_area, | |
| "building_height": building_height, | |
| "building_type": building_type, | |
| "indoor_design_temp": indoor_design_temp, | |
| "indoor_design_rh": indoor_design_rh, | |
| "orientation_angle": float(orientation_angle), | |
| "operation_hours": operation_hours | |
| }) | |
| st.success("Building information saved!") | |
| st.button("Continue to Climate Data and Design Requirements", key="building_to_climate", | |
| on_click=lambda: setattr(st.session_state, "page", "Climate Data and Design Requirements")) | |
| def display_climate_data(self): | |
| """Display the climate data page.""" | |
| logger.info("Entering display_climate_data") | |
| st.title("Climate Data and Design Requirements") | |
| if "climate_data" in st.session_state and st.session_state["climate_data"]: | |
| logger.info("Climate data found in session_state: %s", st.session_state["climate_data"].get("id", "Unknown")) | |
| else: | |
| logger.info("No climate data in session_state") | |
| self.climate_data.display_climate_input(st.session_state) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.button("Back to Building Information", key="climate_back_to_building", | |
| on_click=lambda: setattr(st.session_state, "page", "Building Information")) | |
| with col2: | |
| st.button("Continue to Material Library", key="climate_to_material_library", | |
| on_click=lambda: setattr(st.session_state, "page", "Material Library")) | |
| def display_page(self, page: str): | |
| """Display the selected page based on session state.""" | |
| logger.info(f"Displaying page: {page}") | |
| if page == "Building Information": | |
| self.display_building_info() | |
| elif page == "Climate Data and Design Requirements": | |
| self.display_climate_data() | |
| elif page == "Material Library": | |
| self.display_material_library() | |
| elif page == "Building Components": | |
| self.display_building_components() | |
| elif page == "Internal Loads": | |
| self.display_internal_loads() | |
| elif page == "Calculation Results": | |
| self.display_calculation_results() | |
| elif page == "Export Data": | |
| self.display_export_data() | |
| else: | |
| logger.warning(f"Unknown page: {page}, defaulting to Building Information") | |
| st.session_state.page = "Building Information" | |
| self.display_building_info() | |
| def display_material_library(self): | |
| st.title("Material Library") | |
| st.write("Manage materials, constructions, glazing, and doors based on ASHRAE 2005 Handbook of Fundamentals.") | |
| # CSS for box heights, scrolling, and visual appeal | |
| st.markdown(""" | |
| <style> | |
| .box-container { | |
| border: 1px solid #e0e0e0; | |
| border-radius: 8px; | |
| padding: 10px; | |
| background-color: #f9f9f9; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| overflow-y: auto; | |
| scrollbar-width: thin; | |
| } | |
| .library-box { | |
| max-height: 250px; | |
| } | |
| .project-box { | |
| max-height: 250px; | |
| } | |
| .editor-box { | |
| min-height: 540px; | |
| } | |
| .stButton>button { | |
| width: 100%; | |
| font-size: 12px; | |
| padding: 5px; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Check for rerun trigger | |
| if st.session_state.get("rerun_pending", False): | |
| st.session_state.rerun_pending = False | |
| st.rerun() | |
| # Initialize session state | |
| if 'active_tab' not in st.session_state: | |
| st.session_state.active_tab = "Materials" | |
| if 'material_action' not in st.session_state: | |
| st.session_state.material_action = {"action": None, "id": None} | |
| if 'construction_action' not in st.session_state: | |
| st.session_state.construction_action = {"action": None, "id": None} | |
| if 'glazing_action' not in st.session_state: | |
| st.session_state.glazing_action = {"action": None, "id": None} | |
| if 'door_action' not in st.session_state: | |
| st.session_state.door_action = {"action": None, "id": None} | |
| if 'rerun_pending' not in st.session_state: | |
| st.session_state.rerun_pending = False | |
| if 'material_form_state' not in st.session_state: | |
| st.session_state.material_form_state = {} | |
| if 'construction_form_state' not in st.session_state: | |
| st.session_state.construction_form_state = {} | |
| if 'glazing_form_state' not in st.session_state: | |
| st.session_state.glazing_form_state = {} | |
| if 'door_form_state' not in st.session_state: | |
| st.session_state.door_form_state = {} | |
| # Create tabs and set active tab | |
| tab1, tab2, tab3, tab4 = st.tabs(["Materials", "Constructions", "Glazing", "Doors"]) | |
| active_tab = st.session_state.active_tab | |
| with tab1: | |
| if active_tab == "Materials": | |
| st.session_state.active_tab = "Materials" | |
| with tab2: | |
| if active_tab == "Constructions": | |
| st.session_state.active_tab = "Constructions" | |
| with tab3: | |
| if active_tab == "Glazing": | |
| st.session_state.active_tab = "Glazing" | |
| with tab4: | |
| if active_tab == "Doors": | |
| st.session_state.active_tab = "Doors" | |
| with tab1: | |
| st.subheader("Materials") | |
| col1, col2 = st.columns([3, 2]) | |
| with col1: | |
| # Category filter | |
| filter_options = ["All", "None"] + [c.value for c in MaterialCategory] | |
| category = st.selectbox("Filter by Category", filter_options, key="material_filter") | |
| st.subheader("Library Materials") | |
| with st.container(): | |
| library_materials = list(self.material_library.library_materials.values()) | |
| if category == "None": | |
| library_materials = [] | |
| elif category != "All": | |
| library_materials = [m for m in library_materials if m.category.value == category] | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**Thermal Mass**") | |
| cols[2].write("**U-Value (W/m²·K)**") | |
| cols[3].write("**Preview**") | |
| cols[4].write("**Copy**") | |
| for material in library_materials: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(material.name) | |
| cols[1].write(material.get_thermal_mass().value) | |
| cols[2].write(f"{material.get_u_value():.3f}") | |
| if cols[3].button("Preview", key=f"preview_lib_mat_{material.name}"): | |
| if st.session_state.get("rerun_trigger") != f"preview_mat_{material.name}": | |
| st.session_state.rerun_trigger = f"preview_mat_{material.name}" | |
| st.session_state.material_editor = { | |
| "name": material.name, | |
| "category": material.category.value, | |
| "conductivity": material.conductivity, | |
| "density": material.density, | |
| "specific_heat": material.specific_heat, | |
| "default_thickness": material.default_thickness, | |
| "embodied_carbon": material.embodied_carbon, | |
| "solar_absorption": material.solar_absorption, | |
| "price": material.price, | |
| "emissivity": material.emissivity, | |
| "is_edit": False | |
| } | |
| st.session_state.material_form_state = { | |
| "name": material.name, | |
| "category": material.category.value, | |
| "conductivity": material.conductivity, | |
| "density": material.density, | |
| "specific_heat": material.specific_heat, | |
| "default_thickness": material.default_thickness, | |
| "embodied_carbon": material.embodied_carbon, | |
| "solar_absorption": material.solar_absorption, | |
| "price": material.price, | |
| "emissivity": material.emissivity | |
| } | |
| st.session_state.active_tab = "Materials" | |
| if cols[4].button("Copy", key=f"copy_lib_mat_{material.name}"): | |
| new_name = f"{material.name}_Project" | |
| counter = 1 | |
| while new_name in st.session_state.project_materials or new_name in self.material_library.library_materials: | |
| new_name = f"{material.name}_Project_{counter}" | |
| counter += 1 | |
| new_material = Material( | |
| name=new_name, | |
| category=material.category, | |
| conductivity=material.conductivity, | |
| density=material.density, | |
| specific_heat=material.specific_heat, | |
| default_thickness=material.default_thickness, | |
| embodied_carbon=material.embodied_carbon, | |
| solar_absorption=material.solar_absorption, | |
| price=material.price, | |
| emissivity=material.emissivity, | |
| is_library=False | |
| ) | |
| try: | |
| success, message = self.material_library.add_project_material(new_material, st.session_state.project_materials) | |
| if success: | |
| st.success(message) | |
| st.session_state.rerun_pending = True | |
| else: | |
| st.error(message) | |
| except Exception as e: | |
| st.error(f"Error copying material: {str(e)}") | |
| st.subheader("Project Materials") | |
| with st.container(): | |
| if st.session_state.project_materials: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**Thermal Mass**") | |
| cols[2].write("**U-Value (W/m²·K)**") | |
| cols[3].write("**Edit**") | |
| cols[4].write("**Delete**") | |
| for material in st.session_state.project_materials.values(): | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(material.name) | |
| cols[1].write(material.get_thermal_mass().value) | |
| cols[2].write(f"{material.get_u_value():.3f}") | |
| if cols[3].button("Edit", key=f"edit_proj_mat_{material.name}"): | |
| if st.session_state.get("rerun_trigger") != f"edit_mat_{material.name}": | |
| st.session_state.rerun_trigger = f"edit_mat_{material.name}" | |
| st.session_state.material_editor = { | |
| "name": material.name, | |
| "category": material.category.value, | |
| "conductivity": material.conductivity, | |
| "density": material.density, | |
| "specific_heat": material.specific_heat, | |
| "default_thickness": material.default_thickness, | |
| "embodied_carbon": material.embodied_carbon, | |
| "solar_absorption": material.solar_absorption, | |
| "price": material.price, | |
| "emissivity": material.emissivity, | |
| "is_edit": True, | |
| "original_name": material.name | |
| } | |
| st.session_state.material_form_state = { | |
| "name": material.name, | |
| "category": material.category.value, | |
| "conductivity": material.conductivity, | |
| "density": material.density, | |
| "specific_heat": material.specific_heat, | |
| "default_thickness": material.default_thickness, | |
| "embodied_carbon": material.embodied_carbon, | |
| "solar_absorption": material.solar_absorption, | |
| "price": material.price, | |
| "emissivity": material.emissivity | |
| } | |
| st.session_state.active_tab = "Materials" | |
| if cols[4].button("Delete", key=f"delete_proj_mat_{material.name}"): | |
| success, message = self.material_library.delete_project_material( | |
| material.name, st.session_state.project_materials, st.session_state.components | |
| ) | |
| if success: | |
| st.success(message) | |
| st.session_state.rerun_pending = True | |
| else: | |
| st.error(message) | |
| else: | |
| st.write("No project materials added.") | |
| with col2: | |
| st.subheader("Material Editor/Creator") | |
| with st.container(): | |
| with st.form("material_editor_form", clear_on_submit=False): | |
| editor_state = st.session_state.get("material_editor", {}) | |
| form_state = st.session_state.get("material_form_state", { | |
| "name": "", | |
| "category": "Insulation", | |
| "conductivity": 0.1, | |
| "density": 1000.0, | |
| "specific_heat": 1000.0, | |
| "default_thickness": 0.1, | |
| "embodied_carbon": 0.5, | |
| "solar_absorption": 0.6, | |
| "price": 50.0, | |
| "emissivity": 0.925 | |
| }) | |
| is_edit = editor_state.get("is_edit", False) | |
| original_name = editor_state.get("original_name", "") | |
| name = st.text_input( | |
| "Material Name", | |
| value=form_state.get("name", editor_state.get("name", "")), | |
| help="Unique material identifier", | |
| key="material_name_input" | |
| ) | |
| filter_category = st.session_state.get("material_filter", "All") | |
| default_category = (filter_category if filter_category in [c.value for c in MaterialCategory] | |
| else editor_state.get("category", "Insulation")) | |
| category_index = ([c.value for c in MaterialCategory].index(default_category) | |
| if default_category in [c.value for c in MaterialCategory] else 0) | |
| category = st.selectbox( | |
| "Category", | |
| [c.value for c in MaterialCategory], | |
| index=category_index, | |
| help="Material type classification", | |
| key="material_category_input" | |
| ) | |
| conductivity = st.number_input( | |
| "Thermal Conductivity (W/m·K)", | |
| min_value=0.01, | |
| value=form_state.get("conductivity", editor_state.get("conductivity", 0.1)), | |
| help="Heat flow ease", | |
| key="material_conductivity_input" | |
| ) | |
| density = st.number_input( | |
| "Density (kg/m³)", | |
| min_value=1.0, | |
| value=form_state.get("density", editor_state.get("density", 1000.0)), | |
| help="Mass per volume", | |
| key="material_density_input" | |
| ) | |
| specific_heat = st.number_input( | |
| "Specific Heat (J/kg·K)", | |
| min_value=100.0, | |
| value=form_state.get("specific_heat", editor_state.get("specific_heat", 1000.0)), | |
| help="Heat storage capacity", | |
| key="material_specific_heat_input" | |
| ) | |
| default_thickness = st.number_input( | |
| "Default Thickness (m)", | |
| min_value=0.001, | |
| value=form_state.get("default_thickness", editor_state.get("default_thickness", 0.1)), | |
| help="Typical layer thickness", | |
| key="material_default_thickness_input" | |
| ) | |
| embodied_carbon = st.number_input( | |
| "Embodied Carbon (kgCO₂e/kg)", | |
| min_value=0.0, | |
| value=form_state.get("embodied_carbon", editor_state.get("embodied_carbon", 0.5)), | |
| help="Production carbon emissions", | |
| key="material_embodied_carbon_input" | |
| ) | |
| solar_absorption = st.number_input( | |
| "Solar Absorption", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=form_state.get("solar_absorption", editor_state.get("solar_absorption", 0.6)), | |
| help="Solar radiation absorbed", | |
| key="material_solar_absorption_input" | |
| ) | |
| price = st.number_input( | |
| "Price (USD/m²)", | |
| min_value=0.0, | |
| value=form_state.get("price", editor_state.get("price", 50.0)), | |
| help="Cost per area", | |
| key="material_price_input" | |
| ) | |
| emissivity = st.number_input( | |
| "Emissivity", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=form_state.get("emissivity", editor_state.get("emissivity", 0.925)), | |
| help="Ratio of radiation emitted by the material (0.0 to 1.0)", | |
| key="material_emissivity_input" | |
| ) | |
| if st.form_submit_button("Save Material"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.material_action.get("id") != action_id: | |
| st.session_state.material_action = {"action": "save", "id": action_id} | |
| st.session_state.material_form_state = { | |
| "name": name, | |
| "category": category, | |
| "conductivity": conductivity, | |
| "density": density, | |
| "specific_heat": specific_heat, | |
| "default_thickness": default_thickness, | |
| "embodied_carbon": embodied_carbon, | |
| "solar_absorption": solar_absorption, | |
| "price": price, | |
| "emissivity": emissivity | |
| } | |
| if not name or not name.strip(): | |
| st.error("Material name cannot be empty.") | |
| elif (name in st.session_state.project_materials or name in self.material_library.library_materials) and (not is_edit or name != original_name): | |
| st.error(f"Material '{name}' already exists.") | |
| else: | |
| try: | |
| save_as_new = is_edit and name != original_name | |
| new_material = Material( | |
| name=name, | |
| category=MaterialCategory(category), | |
| conductivity=conductivity, | |
| density=density, | |
| specific_heat=specific_heat, | |
| default_thickness=default_thickness, | |
| embodied_carbon=embodied_carbon, | |
| solar_absorption=solar_absorption, | |
| price=price, | |
| emissivity=emissivity, | |
| is_library=False | |
| ) | |
| if is_edit and not save_as_new: | |
| success, message = self.material_library.edit_project_material( | |
| original_name, new_material, st.session_state.project_materials, st.session_state.components | |
| ) | |
| else: | |
| success, message = self.material_library.add_project_material(new_material, st.session_state.project_materials) | |
| if success: | |
| st.success(message) | |
| st.session_state.material_editor = {} | |
| st.session_state.material_form_state = { | |
| "name": "", | |
| "category": "Insulation", | |
| "conductivity": 0.1, | |
| "density": 1000.0, | |
| "specific_heat": 1000.0, | |
| "default_thickness": 0.1, | |
| "embodied_carbon": 0.5, | |
| "solar_absorption": 0.6, | |
| "price": 50.0, | |
| "emissivity": 0.925 | |
| } | |
| st.session_state.material_action = {"action": None, "id": None} | |
| st.session_state.rerun_trigger = None | |
| st.session_state.rerun_pending = True | |
| else: | |
| st.error(f"Failed to save material: {message}") | |
| except Exception as e: | |
| st.error(f"Error saving material: {str(e)}") | |
| st.subheader("Project Materials") | |
| try: | |
| material_df = self.material_library.to_dataframe("materials", | |
| project_materials=st.session_state.project_materials, | |
| only_project=True) | |
| if not material_df.empty: | |
| # Ensure emissivity is included in the DataFrame | |
| material_df['Emissivity'] = [m.emissivity for m in st.session_state.project_materials.values()] | |
| st.dataframe(material_df, use_container_width=True) | |
| else: | |
| st.write("No project materials to display.") | |
| except Exception as e: | |
| st.error(f"Error displaying project materials: {str(e)}") | |
| st.write("No project materials to display.") | |
| with tab2: | |
| st.subheader("Constructions") | |
| col1, col2 = st.columns([3, 2]) | |
| with col1: | |
| filter_options = ["All", "Wall", "Roof", "Floor"] | |
| construction_filter = st.selectbox("Filter by Component Type", filter_options, key="construction_filter") | |
| st.subheader("Library Constructions") | |
| with st.container(): | |
| library_constructions = list(self.material_library.library_constructions.values()) | |
| if construction_filter != "All": | |
| library_constructions = [c for c in library_constructions if c.component_type == construction_filter] | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**Thermal Mass**") | |
| cols[2].write("**U-Value (W/m²·K)**") | |
| cols[3].write("**Preview**") | |
| cols[4].write("**Copy**") | |
| for construction in library_constructions: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(construction.name) | |
| cols[1].write(construction.get_thermal_mass().value) | |
| cols[2].write(f"{construction.u_value:.3f}") | |
| if cols[3].button("Preview", key=f"preview_lib_cons_{construction.name}"): | |
| if st.session_state.get("rerun_trigger") != f"preview_cons_{construction.name}": | |
| st.session_state.rerun_trigger = f"preview_cons_{construction.name}" | |
| st.session_state.construction_editor = { | |
| "name": construction.name, | |
| "component_type": construction.component_type, | |
| "layers": [{"material_name": layer["material"].name, "thickness": layer["thickness"]} | |
| for layer in construction.layers], | |
| "is_edit": False | |
| } | |
| st.session_state.construction_form_state = { | |
| "name": construction.name, | |
| "component_type": construction.component_type, | |
| "num_layers": len(construction.layers), | |
| "layers": [{"material_name": layer["material"].name, "thickness": layer["thickness"]} | |
| for layer in construction.layers] | |
| } | |
| st.session_state.active_tab = "Constructions" | |
| if cols[4].button("Copy", key=f"copy_lib_cons_{construction.name}"): | |
| new_name = f"{construction.name}_Project" | |
| counter = 1 | |
| while new_name in st.session_state.project_constructions or new_name in self.material_library.library_constructions: | |
| new_name = f"{construction.name}_Project_{counter}" | |
| counter += 1 | |
| new_layers = [] | |
| for layer in construction.layers: | |
| material = layer["material"] | |
| if material.is_library: | |
| mat_name = f"{material.name}_Project" | |
| counter = 1 | |
| while mat_name in st.session_state.project_materials or mat_name in self.material_library.library_materials: | |
| mat_name = f"{material.name}_Project_{counter}" | |
| counter += 1 | |
| new_material = Material( | |
| name=mat_name, | |
| category=material.category, | |
| conductivity=material.conductivity, | |
| density=material.density, | |
| specific_heat=material.specific_heat, | |
| default_thickness=material.default_thickness, | |
| embodied_carbon=material.embodied_carbon, | |
| solar_absorption=material.solar_absorption, | |
| price=material.price, | |
| emissivity=material.emissivity, | |
| is_library=False | |
| ) | |
| success, message = self.material_library.add_project_material(new_material, st.session_state.project_materials) | |
| if not success: | |
| st.error(f"Failed to copy material '{material.name}': {message}") | |
| break | |
| material = new_material | |
| new_layers.append({"material": material, "thickness": layer["thickness"]}) | |
| else: | |
| new_construction = Construction( | |
| name=new_name, | |
| component_type=construction.component_type, | |
| layers=new_layers, | |
| is_library=False | |
| ) | |
| success, message = self.material_library.add_project_construction(new_construction, st.session_state.project_constructions) | |
| if success: | |
| st.success(message) | |
| st.session_state.rerun_pending = True | |
| else: | |
| st.error(message) | |
| st.subheader("Project Constructions") | |
| with st.container(): | |
| if st.session_state.project_constructions: | |
| project_constructions = list(st.session_state.project_constructions.values()) | |
| if construction_filter != "All": | |
| project_constructions = [c for c in project_constructions if c.component_type == construction_filter] | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**Thermal Mass**") | |
| cols[2].write("**U-Value (W/m²·K)**") | |
| cols[3].write("**Edit**") | |
| cols[4].write("**Delete**") | |
| for construction in project_constructions: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(construction.name) | |
| cols[1].write(construction.get_thermal_mass().value) | |
| cols[2].write(f"{construction.u_value:.3f}") | |
| if cols[3].button("Edit", key=f"edit_proj_cons_{construction.name}"): | |
| if st.session_state.get("rerun_trigger") != f"edit_cons_{construction.name}": | |
| st.session_state.rerun_trigger = f"edit_cons_{construction.name}" | |
| st.session_state.construction_editor = { | |
| "name": construction.name, | |
| "component_type": construction.component_type, | |
| "layers": [{"material_name": layer["material"].name, "thickness": layer["thickness"]} | |
| for layer in construction.layers], | |
| "is_edit": True, | |
| "original_name": construction.name | |
| } | |
| st.session_state.construction_form_state = { | |
| "name": construction.name, | |
| "component_type": construction.component_type, | |
| "num_layers": len(construction.layers), | |
| "layers": [{"material_name": layer["material"].name, "thickness": layer["thickness"]} | |
| for layer in construction.layers] | |
| } | |
| st.session_state.active_tab = "Constructions" | |
| if cols[4].button("Delete", key=f"delete_proj_cons_{construction.name}"): | |
| success, message = self.material_library.delete_project_construction( | |
| construction.name, st.session_state.project_constructions, st.session_state.components | |
| ) | |
| if success: | |
| st.success(message) | |
| st.session_state.rerun_pending = True | |
| else: | |
| st.error(message) | |
| else: | |
| st.write("No project constructions added.") | |
| with col2: | |
| st.subheader("Construction Editor/Creator") | |
| with st.container(): | |
| is_preview = st.session_state.get("rerun_trigger", "") and st.session_state.get("rerun_trigger", "").startswith("preview_cons_") | |
| materials = (list(self.material_library.library_materials.values()) if is_preview | |
| else list(st.session_state.project_materials.values())) | |
| if not materials and not is_preview: | |
| st.error("No project materials available. Please create materials in the Materials tab first.") | |
| st.button("Go to Materials", key="go_to_materials", on_click=lambda: setattr(st.session_state, "page", "Material Library")) | |
| st.stop() | |
| with st.form("construction_editor_form", clear_on_submit=False): | |
| editor_state = st.session_state.get("construction_editor", {}) | |
| form_state = st.session_state.get("construction_form_state", { | |
| "name": "", | |
| "component_type": "Wall", | |
| "num_layers": 1, | |
| "layers": [{"material_name": "", "thickness": 0.1}] | |
| }) | |
| is_edit = editor_state.get("is_edit", False) | |
| original_name = editor_state.get("original_name", "") | |
| name = st.text_input( | |
| "Construction Name", | |
| value=form_state.get("name", editor_state.get("name", "")), | |
| help="Unique construction identifier", | |
| key="construction_name_input" | |
| ) | |
| component_type = st.selectbox( | |
| "Component Type", | |
| ["Wall", "Roof", "Floor"], | |
| index=["Wall", "Roof", "Floor"].index(form_state.get("component_type", editor_state.get("component_type", "Wall"))), | |
| help="Building component type", | |
| key="construction_component_type_input" | |
| ) | |
| num_layers = st.number_input( | |
| "Number of Layers", | |
| min_value=1, | |
| max_value=10, | |
| value=form_state.get("num_layers", 1), | |
| help="Material layer count", | |
| key="construction_num_layers_input" | |
| ) | |
| material_names = [m.name for m in materials] | |
| default_material = material_names[0] if material_names else "" | |
| layers = form_state.get("layers", [{"material_name": default_material, "thickness": 0.1}]) | |
| if len(layers) < num_layers: | |
| layers.extend([{"material_name": default_material, "thickness": 0.1} for _ in range(num_layers - len(layers))]) | |
| elif len(layers) > num_layers: | |
| layers = layers[:num_layers] | |
| form_state["layers"] = layers | |
| form_state["num_layers"] = num_layers | |
| for i in range(num_layers): | |
| st.write(f"Layer {i + 1} (Exterior to Interior)") | |
| layer = layers[i] | |
| material_name = st.selectbox( | |
| f"Material {i+1}", | |
| material_names, | |
| index=material_names.index(layer.get("material_name", default_material)) if layer.get("material_name", default_material) in material_names else 0, | |
| key=f"cons_mat_{i}_{num_layers}", | |
| help="Layer material" | |
| ) | |
| thickness = st.number_input( | |
| f"Thickness {i+1} (m)", | |
| min_value=0.001, | |
| value=layer.get("thickness", 0.1), | |
| key=f"cons_thick_{i}_{num_layers}", | |
| help="Layer thickness" | |
| ) | |
| layers[i] = {"material_name": material_name, "thickness": thickness} | |
| form_state["layers"] = layers | |
| st.session_state.construction_form_state = form_state | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| if st.form_submit_button("Update Layers"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.construction_action.get("id") != action_id: | |
| st.session_state.construction_action = {"action": "update_layers", "id": action_id} | |
| new_layers = [] | |
| for i in range(num_layers): | |
| if i < len(layers) and layers[i].get("material_name") in material_names: | |
| new_layers.append(layers[i]) | |
| else: | |
| new_layers.append({"material_name": default_material, "thickness": 0.1}) | |
| form_state["layers"] = new_layers | |
| form_state["num_layers"] = num_layers | |
| st.session_state.construction_form_state = form_state | |
| st.session_state.construction_action = {"action": None, "id": None} | |
| st.session_state.rerun_pending = True | |
| with col2: | |
| if st.form_submit_button("Preview U-Value"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.construction_action.get("id") != action_id: | |
| st.session_state.construction_action = {"action": "preview_uvalue", "id": action_id} | |
| if layers: | |
| valid_layers = [] | |
| for layer in layers: | |
| material_name = layer.get("material_name") | |
| thickness = layer.get("thickness", 0.1) | |
| material = next((m for m in materials if m.name == material_name), None) | |
| if material: | |
| valid_layers.append({"material": material, "thickness": thickness}) | |
| if valid_layers: | |
| r_total = sum(layer["thickness"] / layer["material"].conductivity for layer in valid_layers) | |
| u_value = 1 / r_total if r_total > 0 else 0.1 | |
| st.write(f"Calculated U-Value: {u_value:.3f} W/m²·K") | |
| else: | |
| st.warning("No valid materials selected for U-Value calculation.") | |
| else: | |
| st.warning("No layers defined to calculate U-Value.") | |
| st.session_state.construction_action = {"action": None, "id": None} | |
| with col3: | |
| if st.form_submit_button("Save Construction"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.construction_action.get("id") != action_id: | |
| st.session_state.construction_action = {"action": "save", "id": action_id} | |
| if not name or not name.strip(): | |
| st.error("Construction name cannot be empty.") | |
| elif (name in st.session_state.project_constructions or name in self.material_library.library_constructions) and (not is_edit or name != original_name): | |
| st.error(f"Construction '{name}' already exists.") | |
| elif not layers or any(not layer.get("material_name") for layer in layers): | |
| st.error("All layers must have a valid material.") | |
| else: | |
| try: | |
| save_as_new = is_edit and name != original_name | |
| valid_layers = [] | |
| for layer in layers: | |
| material_name = layer.get("material_name") | |
| thickness = layer.get("thickness", 0.1) | |
| material = next((m for m in materials if m.name == material_name), None) | |
| if material: | |
| valid_layers.append({"material": material, "thickness": thickness}) | |
| else: | |
| st.error(f"Material '{material_name}' not found.") | |
| break | |
| else: | |
| new_construction = Construction(name, component_type, valid_layers, is_library=False) | |
| if is_edit and not save_as_new: | |
| success, message = self.material_library.edit_project_construction( | |
| original_name, new_construction, st.session_state.project_constructions, st.session_state.components | |
| ) | |
| else: | |
| success, message = self.material_library.add_project_construction(new_construction, st.session_state.project_constructions) | |
| if success: | |
| st.success(message) | |
| st.session_state.construction_editor = {} | |
| st.session_state.construction_form_state = { | |
| "name": "", | |
| "component_type": "Wall", | |
| "num_layers": 1, | |
| "layers": [{"material_name": default_material, "thickness": 0.1}] | |
| } | |
| st.session_state.construction_action = {"action": None, "id": None} | |
| st.session_state.rerun_trigger = None | |
| st.session_state.rerun_pending = True | |
| else: | |
| st.error(f"Failed to save construction: {message}") | |
| except Exception as e: | |
| st.error(f"Error saving construction: {str(e)}") | |
| st.subheader("Project Constructions") | |
| try: | |
| construction_df = self.material_library.to_dataframe("constructions", | |
| project_constructions=st.session_state.project_constructions, | |
| only_project=True) | |
| if not construction_df.empty: | |
| project_constructions = construction_df | |
| if construction_filter != "All": | |
| project_constructions = project_constructions[project_constructions['Component Type'] == construction_filter] | |
| st.dataframe(project_constructions, use_container_width=True) | |
| else: | |
| st.write("No project constructions to display.") | |
| except Exception as e: | |
| st.error(f"Error displaying project constructions: {str(e)}") | |
| st.write("No project constructions to display.") | |
| with tab3: | |
| st.subheader("Glazing") | |
| col1, col2 = st.columns([3, 2]) | |
| with col1: | |
| filter_options = ["All", "None"] | |
| glazing_filter = st.selectbox("Filter Glazing", filter_options, key="glazing_filter") | |
| st.subheader("Library Glazing") | |
| with st.container(): | |
| library_glazing = getattr(self.material_library, 'library_glazing_materials', {}) | |
| library_glazing_list = list(library_glazing.values()) if library_glazing else [] | |
| if glazing_filter == "None": | |
| library_glazing_list = [] | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**SHGC**") | |
| cols[2].write("**U-Value (W/m²·K)**") | |
| cols[3].write("**Preview**") | |
| cols[4].write("**Copy**") | |
| for glazing in library_glazing_list: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(glazing.name) | |
| cols[1].write(f"{glazing.shgc:.2f}") | |
| cols[2].write(f"{glazing.u_value:.3f}") | |
| if cols[3].button("Preview", key=f"preview_lib_glz_{glazing.name}"): | |
| if st.session_state.get("rerun_trigger") != f"preview_glz_{glazing.name}": | |
| st.session_state.rerun_trigger = f"preview_glz_{glazing.name}" | |
| st.session_state.glazing_editor = { | |
| "name": glazing.name, | |
| "shgc": glazing.shgc, | |
| "u_value": glazing.u_value, | |
| "h_o": glazing.h_o, | |
| "is_edit": False | |
| } | |
| st.session_state.glazing_form_state = { | |
| "name": glazing.name, | |
| "shgc": glazing.shgc, | |
| "u_value": glazing.u_value, | |
| "h_o": glazing.h_o | |
| } | |
| st.session_state.active_tab = "Glazing" | |
| if cols[4].button("Copy", key=f"copy_lib_glz_{glazing.name}"): | |
| new_name = f"{glazing.name}_Project" | |
| counter = 1 | |
| while new_name in st.session_state.project_glazing_materials or new_name in getattr(self.material_library, 'library_glazing_materials', {}): | |
| new_name = f"{glazing.name}_Project_{counter}" | |
| counter += 1 | |
| new_glazing = GlazingMaterial( | |
| name=new_name, | |
| shgc=glazing.shgc, | |
| u_value=glazing.u_value, | |
| h_o=glazing.h_o, | |
| is_library=False | |
| ) | |
| st.session_state.project_glazing_materials[new_name] = new_glazing | |
| st.success(f"Glazing material '{new_name}' copied!") | |
| st.session_state.rerun_pending = True | |
| st.subheader("Project Glazing") | |
| with st.container(): | |
| if st.session_state.project_glazing_materials: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**SHGC**") | |
| cols[2].write("**U-Value (W/m²·K)**") | |
| cols[3].write("**Edit**") | |
| cols[4].write("**Delete**") | |
| for glazing in st.session_state.project_glazing_materials.values(): | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(glazing.name) | |
| cols[1].write(f"{glazing.shgc:.2f}") | |
| cols[2].write(f"{glazing.u_value:.3f}") | |
| if cols[3].button("Edit", key=f"edit_proj_glz_{glazing.name}"): | |
| if st.session_state.get("rerun_trigger") != f"edit_glz_{glazing.name}": | |
| st.session_state.rerun_trigger = f"edit_glz_{glazing.name}" | |
| st.session_state.glazing_editor = { | |
| "name": glazing.name, | |
| "shgc": glazing.shgc, | |
| "u_value": glazing.u_value, | |
| "h_o": glazing.h_o, | |
| "is_edit": True, | |
| "original_name": glazing.name | |
| } | |
| st.session_state.glazing_form_state = { | |
| "name": glazing.name, | |
| "shgc": glazing.shgc, | |
| "u_value": glazing.u_value, | |
| "h_o": glazing.h_o | |
| } | |
| st.session_state.active_tab = "Glazing" | |
| if cols[4].button("Delete", key=f"delete_proj_glz_{glazing.name}"): | |
| if any(comp.glazing_material and comp.glazing_material.name == glazing.name | |
| for comp_list in st.session_state.components.values() for comp in comp_list): | |
| st.error(f"Glazing material '{glazing.name}' is used in components and cannot be deleted.") | |
| else: | |
| del st.session_state.project_glazing_materials[glazing.name] | |
| st.success(f"Glazing material '{glazing.name}' deleted!") | |
| st.session_state.rerun_pending = True | |
| else: | |
| st.write("No project glazing materials added.") | |
| with col2: | |
| st.subheader("Glazing Editor/Creator") | |
| with st.container(): | |
| with st.form("glazing_editor_form", clear_on_submit=False): | |
| editor_state = st.session_state.get("glazing_editor", {}) | |
| form_state = st.session_state.get("glazing_form_state", { | |
| "name": "", | |
| "shgc": 0.7, | |
| "u_value": 5.0, | |
| "h_o": 23.0 | |
| }) | |
| is_edit = editor_state.get("is_edit", False) | |
| original_name = editor_state.get("original_name", "") | |
| name = st.text_input( | |
| "Glazing Name", | |
| value=form_state.get("name", editor_state.get("name", "")), | |
| help="Unique glazing identifier", | |
| key="glazing_name_input" | |
| ) | |
| shgc = st.number_input( | |
| "Solar Heat Gain Coefficient (SHGC)", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=form_state.get("shgc", editor_state.get("shgc", 0.7)), | |
| help="Fraction of solar radiation admitted", | |
| key="glazing_shgc_input" | |
| ) | |
| u_value = st.number_input( | |
| "U-Value (W/m²·K)", | |
| min_value=0.1, | |
| value=form_state.get("u_value", editor_state.get("u_value", 5.0)), | |
| help="Thermal transmittance", | |
| key="glazing_u_value_input" | |
| ) | |
| h_o = st.number_input( | |
| "Exterior Surface Conductance (W/m²·K)", | |
| min_value=0.0, | |
| value=form_state.get("h_o", editor_state.get("h_o", 23.0)), | |
| help="Exterior surface heat transfer coefficient", | |
| key="glazing_h_o_input" | |
| ) | |
| if st.form_submit_button("Save Glazing"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.glazing_action.get("id") != action_id: | |
| st.session_state.glazing_action = {"action": "save", "id": action_id} | |
| st.session_state.glazing_form_state = { | |
| "name": name, | |
| "shgc": shgc, | |
| "u_value": u_value, | |
| "h_o": h_o | |
| } | |
| if not name or not name.strip(): | |
| st.error("Glazing name cannot be empty.") | |
| elif (name in st.session_state.project_glazing_materials or name in getattr(self.material_library, 'library_glazing_materials', {})) and (not is_edit or name != original_name): | |
| st.error(f"Glazing material '{name}' already exists.") | |
| else: | |
| try: | |
| save_as_new = is_edit and name != original_name | |
| new_glazing = GlazingMaterial( | |
| name=name, | |
| shgc=shgc, | |
| u_value=u_value, | |
| h_o=h_o, | |
| is_library=False | |
| ) | |
| if is_edit and not save_as_new: | |
| st.session_state.project_glazing_materials[original_name] = new_glazing | |
| st.success(f"Glazing material '{name}' updated!") | |
| else: | |
| st.session_state.project_glazing_materials[name] = new_glazing | |
| st.success(f"Glazing material '{name}' added!") | |
| st.session_state.glazing_editor = {} | |
| st.session_state.glazing_form_state = { | |
| "name": "", | |
| "shgc": 0.7, | |
| "u_value": 5.0, | |
| "h_o": 23.0 | |
| } | |
| st.session_state.glazing_action = {"action": None, "id": None} | |
| st.session_state.rerun_trigger = None | |
| st.session_state.rerun_pending = True | |
| except Exception as e: | |
| st.error(f"Error saving glazing material: {str(e)}") | |
| st.subheader("Project Glazing") | |
| if st.session_state.project_glazing_materials: | |
| try: | |
| glazing_df = self.material_library.to_dataframe("glazing", | |
| project_glazing_materials=st.session_state.project_glazing_materials, | |
| only_project=True) | |
| if glazing_df.empty: | |
| # Fallback: Construct DataFrame manually | |
| glazing_data = [ | |
| { | |
| "Name": g.name, | |
| "SHGC": g.shgc, | |
| "U-Value (W/m²·K)": g.u_value, | |
| "Exterior Conductance (W/m²·K)": g.h_o | |
| } | |
| for g in st.session_state.project_glazing_materials.values() | |
| ] | |
| glazing_df = pd.DataFrame(glazing_data) | |
| if not glazing_df.empty: | |
| st.dataframe(glazing_df, use_container_width=True) | |
| else: | |
| st.write("No project glazing materials to display.") | |
| except Exception as e: | |
| # Fallback: Construct DataFrame manually | |
| glazing_data = [ | |
| { | |
| "Name": g.name, | |
| "SHGC": g.shgc, | |
| "U-Value (W/m²·K)": g.u_value, | |
| "Exterior Conductance (W/m²·K)": g.h_o | |
| } | |
| for g in st.session_state.project_glazing_materials.values() | |
| ] | |
| glazing_df = pd.DataFrame(glazing_data) | |
| if not glazing_df.empty: | |
| st.dataframe(glazing_df, use_container_width=True) | |
| else: | |
| st.error(f"Error displaying project glazing materials: {str(e)}") | |
| st.write("No project glazing materials to display.") | |
| else: | |
| st.write("No project glazing materials to display.") | |
| with tab4: | |
| st.subheader("Doors") | |
| col1, col2 = st.columns([3, 2]) | |
| with col1: | |
| filter_options = ["All", "None"] | |
| door_filter = st.selectbox("Filter Doors", filter_options, key="door_filter") | |
| st.subheader("Library Doors") | |
| with st.container(): | |
| library_doors = getattr(self.material_library, 'library_door_materials', {}) | |
| library_door_list = list(library_doors.values()) if library_doors else [] | |
| if door_filter == "None": | |
| library_door_list = [] | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**U-Value (W/m²·K)**") | |
| cols[2].write("**Solar Abs.**") | |
| cols[3].write("**Preview**") | |
| cols[4].write("**Copy**") | |
| for door in library_door_list: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(door.name) | |
| cols[1].write(f"{door.u_value:.3f}") | |
| cols[2].write(f"{door.solar_absorption:.2f}") | |
| if cols[3].button("Preview", key=f"preview_lib_door_{door.name}"): | |
| if st.session_state.get("rerun_trigger") != f"preview_door_{door.name}": | |
| st.session_state.rerun_trigger = f"preview_door_{door.name}" | |
| st.session_state.door_editor = { | |
| "name": door.name, | |
| "u_value": door.u_value, | |
| "solar_absorption": door.solar_absorption, | |
| "is_edit": False | |
| } | |
| st.session_state.door_form_state = { | |
| "name": door.name, | |
| "u_value": door.u_value, | |
| "solar_absorption": door.solar_absorption | |
| } | |
| st.session_state.active_tab = "Doors" | |
| if cols[4].button("Copy", key=f"copy_lib_door_{door.name}"): | |
| new_name = f"{door.name}_Project" | |
| counter = 1 | |
| while new_name in st.session_state.project_door_materials or new_name in getattr(self.material_library, 'library_door_materials', {}): | |
| new_name = f"{door.name}_Project_{counter}" | |
| counter += 1 | |
| new_door = DoorMaterial( | |
| name=new_name, | |
| u_value=door.u_value, | |
| solar_absorption=door.solar_absorption, | |
| is_library=False | |
| ) | |
| st.session_state.project_door_materials[new_name] = new_door | |
| st.success(f"Door material '{new_name}' copied!") | |
| st.session_state.rerun_pending = True | |
| st.subheader("Project Doors") | |
| with st.container(): | |
| if st.session_state.project_door_materials: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**U-Value (W/m²·K)**") | |
| cols[2].write("**Solar Abs.**") | |
| cols[3].write("**Edit**") | |
| cols[4].write("**Delete**") | |
| for door in st.session_state.project_door_materials.values(): | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(door.name) | |
| cols[1].write(f"{door.u_value:.3f}") | |
| cols[2].write(f"{door.solar_absorption:.2f}") | |
| if cols[3].button("Edit", key=f"edit_proj_door_{door.name}"): | |
| if st.session_state.get("rerun_trigger") != f"edit_door_{door.name}": | |
| st.session_state.rerun_trigger = f"edit_door_{door.name}" | |
| st.session_state.door_editor = { | |
| "name": door.name, | |
| "u_value": door.u_value, | |
| "solar_absorption": door.solar_absorption, | |
| "is_edit": True, | |
| "original_name": door.name | |
| } | |
| st.session_state.door_form_state = { | |
| "name": door.name, | |
| "u_value": door.u_value, | |
| "solar_absorption": door.solar_absorption | |
| } | |
| st.session_state.active_tab = "Doors" | |
| if cols[4].button("Delete", key=f"delete_proj_door_{door.name}"): | |
| if any(comp.door_material and comp.door_material.name == door.name | |
| for comp_list in st.session_state.components.values() for comp in comp_list): | |
| st.error(f"Door material '{door.name}' is used in components and cannot be deleted.") | |
| else: | |
| del st.session_state.project_door_materials[door.name] | |
| st.success(f"Door material '{door.name}' deleted!") | |
| st.session_state.rerun_pending = True | |
| else: | |
| st.write("No project door materials added.") | |
| with col2: | |
| st.subheader("Door Editor/Creator") | |
| with st.container(): | |
| with st.form("door_editor_form", clear_on_submit=False): | |
| editor_state = st.session_state.get("door_editor", {}) | |
| form_state = st.session_state.get("door_form_state", { | |
| "name": "", | |
| "u_value": 2.0, | |
| "solar_absorption": 0.6 | |
| }) | |
| is_edit = editor_state.get("is_edit", False) | |
| original_name = editor_state.get("original_name", "") | |
| name = st.text_input( | |
| "Door Name", | |
| value=form_state.get("name", editor_state.get("name", "")), | |
| help="Unique door identifier", | |
| key="door_name_input" | |
| ) | |
| u_value = st.number_input( | |
| "U-Value (W/m²·K)", | |
| min_value=0.1, | |
| value=form_state.get("u_value", editor_state.get("u_value", 2.0)), | |
| help="Thermal transmittance", | |
| key="door_u_value_input" | |
| ) | |
| solar_absorption = st.number_input( | |
| "Solar Absorption", | |
| min_value=0.0, | |
| max_value=1.0, | |
| value=form_state.get("solar_absorption", editor_state.get("solar_absorption", 0.6)), | |
| help="Fraction of solar radiation absorbed", | |
| key="door_solar_absorption_input" | |
| ) | |
| if st.form_submit_button("Save Door"): | |
| action_id = str(uuid.uuid4()) | |
| if st.session_state.door_action.get("id") != action_id: | |
| st.session_state.door_action = {"action": "save", "id": action_id} | |
| st.session_state.door_form_state = { | |
| "name": name, | |
| "u_value": u_value, | |
| "solar_absorption": solar_absorption | |
| } | |
| if not name or not name.strip(): | |
| st.error("Door name cannot be empty.") | |
| elif (name in st.session_state.project_door_materials or name in getattr(self.material_library, 'library_door_materials', {})) and (not is_edit or name != original_name): | |
| st.error(f"Door material '{name}' already exists.") | |
| else: | |
| try: | |
| save_as_new = is_edit and name != original_name | |
| new_door = DoorMaterial( | |
| name=name, | |
| u_value=u_value, | |
| solar_absorption=solar_absorption, | |
| is_library=False | |
| ) | |
| if is_edit and not save_as_new: | |
| st.session_state.project_door_materials[original_name] = new_door | |
| st.success(f"Door material '{name}' updated!") | |
| else: | |
| st.session_state.project_door_materials[name] = new_door | |
| st.success(f"Door material '{name}' added!") | |
| st.session_state.door_editor = {} | |
| st.session_state.door_form_state = { | |
| "name": "", | |
| "u_value": 2.0, | |
| "solar_absorption": 0.6 | |
| } | |
| st.session_state.door_action = {"action": None, "id": None} | |
| st.session_state.rerun_trigger = None | |
| st.session_state.rerun_pending = True | |
| except Exception as e: | |
| st.error(f"Error saving door material: {str(e)}") | |
| st.subheader("Project Doors") | |
| if st.session_state.project_door_materials: | |
| try: | |
| door_df = self.material_library.to_dataframe("doors", | |
| project_door_materials=st.session_state.project_door_materials, | |
| only_project=True) | |
| if door_df.empty: | |
| # Fallback: Construct DataFrame manually | |
| door_data = [ | |
| { | |
| "Name": d.name, | |
| "U-Value (W/m²·K)": d.u_value, | |
| "Solar Absorption": d.solar_absorption | |
| } | |
| for d in st.session_state.project_door_materials.values() | |
| ] | |
| door_df = pd.DataFrame(door_data) | |
| if not door_df.empty: | |
| st.dataframe(door_df, use_container_width=True) | |
| else: | |
| st.write("No project door materials to display.") | |
| except Exception as e: | |
| # Fallback: Construct DataFrame manually | |
| door_data = [ | |
| { | |
| "Name": d.name, | |
| "U-Value (W/m²·K)": d.u_value, | |
| "Solar Absorption": d.solar_absorption | |
| } | |
| for d in st.session_state.project_door_materials.values() | |
| ] | |
| door_df = pd.DataFrame(door_data) | |
| if not door_df.empty: | |
| st.dataframe(door_df, use_container_width=True) | |
| else: | |
| st.error(f"Error displaying project door materials: {str(e)}") | |
| st.write("No project door materials to display.") | |
| else: | |
| st.write("No project door materials to display.") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.button("Back to Climate Data and Design Requirements", key="material_back_to_climate", | |
| on_click=lambda: setattr(st.session_state, "page", "Climate Data and Design Requirements")) | |
| with col2: | |
| st.button("Continue to Building Components", key="material_to_components", | |
| on_click=lambda: setattr(st.session_state, "page", "Building Components")) | |
| def display_building_components(self): | |
| st.title("Building Components") | |
| st.write("Define walls, roofs, floors, windows, doors, and skylights based on ASHRAE 2005 Handbook of Fundamentals.") | |
| tabs = st.tabs(["Walls", "Roofs", "Floors", "Windows", "Doors", "Skylights"]) | |
| if 'components_rerun_pending' not in st.session_state: | |
| st.session_state.components_rerun_pending = False | |
| if st.session_state.components_rerun_pending: | |
| st.session_state.components_rerun_pending = False | |
| st.rerun() | |
| # Initialize components dictionary if not present | |
| if 'components' not in st.session_state: | |
| st.session_state.components = { | |
| 'walls': [], | |
| 'roofs': [], | |
| 'floors': [], | |
| 'windows': [], | |
| 'doors': [], | |
| 'skylights': [] | |
| } | |
| orientation_map = {'A': 0.0, 'B': 180.0, 'C': 90.0, 'D': 270.0} | |
| for tab, comp_type in zip(tabs, ComponentType): | |
| tab_name = comp_type.value + "s" | |
| with tab: | |
| col1, col2 = st.columns([3, 2]) | |
| components = st.session_state.components.get(tab_name.lower(), []) | |
| with col1: | |
| st.subheader(f"Saved {tab_name}") | |
| if comp_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]: | |
| available_items = list(st.session_state.project_constructions.values()) + list(self.material_library.library_constructions.values()) | |
| elif comp_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| available_items = list(st.session_state.project_glazing_materials.values()) + list(getattr(self.material_library, 'library_glazing_materials', {}).values()) | |
| else: # Doors | |
| available_items = list(st.session_state.project_door_materials.values()) + list(getattr(self.material_library, 'library_door_materials', {}).values()) | |
| if components: | |
| if comp_type in [ComponentType.WALL, ComponentType.WINDOW]: | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**U-Value**") | |
| cols[2].write("**Area (m²)**") | |
| cols[3].write("**Elevation**") | |
| cols[4].write("**Rotation (°)**") | |
| cols[5].write("**Tilt (°)**") | |
| cols[6].write("**Edit**") | |
| cols[7].write("**Delete**") | |
| for idx, comp in enumerate(components): | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write(comp.name) | |
| cols[1].write(f"{comp.u_value:.3f}") | |
| cols[2].write(f"{comp.area:.2f}") | |
| cols[3].write(comp.elevation if comp.elevation in ['A', 'B', 'C', 'D'] else "N/A") | |
| cols[4].write(f"{comp.rotation:.1f}") | |
| cols[5].write(f"{comp.tilt:.1f}") | |
| edit_key = f"edit_{tab_name.lower()}_{comp.name}_{idx}" | |
| delete_key = f"delete_{tab_name.lower()}_{comp.name}_{idx}" | |
| with cols[6].container(): | |
| editor_data = { | |
| "index": idx, | |
| "name": comp.name, | |
| "area": comp.area, | |
| "elevation": comp.elevation, | |
| "rotation": comp.rotation, | |
| "tilt": comp.tilt, | |
| "is_edit": True | |
| } | |
| if comp_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]: | |
| editor_data["construction"] = comp.construction.name if comp.construction else "" | |
| elif comp_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| editor_data["glazing_material"] = comp.glazing_material.name if comp.glazing_material else "" | |
| else: # Doors | |
| editor_data["door_material"] = comp.door_material.name if comp.door_material else "" | |
| if st.button("Edit", key=edit_key): | |
| st.session_state[f"{tab_name.lower()}_editor"] = editor_data | |
| st.session_state[f"{tab_name.lower()}_action"] = {"action": "edit", "id": str(uuid.uuid4())} | |
| st.session_state.components_rerun_pending = True | |
| with cols[7].container(): | |
| if st.button("Delete", key=delete_key): | |
| st.session_state.components[tab_name.lower()].pop(idx) | |
| st.success(f"{tab_name[:-1]} '{comp.name}' deleted!") | |
| st.session_state[f"{tab_name.lower()}_action"] = {"action": "delete", "id": str(uuid.uuid4())} | |
| st.session_state.components_rerun_pending = True | |
| elif comp_type in [ComponentType.ROOF, ComponentType.SKYLIGHT]: | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**U-Value**") | |
| cols[2].write("**Area (m²)**") | |
| cols[3].write("**Orientation (°)**") | |
| cols[4].write("**Tilt (°)**") | |
| cols[5].write("**Edit**") | |
| cols[6].write("**Delete**") | |
| for idx, comp in enumerate(components): | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write(comp.name) | |
| cols[1].write(f"{comp.u_value:.3f}") | |
| cols[2].write(f"{comp.area:.2f}") | |
| cols[3].write(f"{comp.orientation:.1f}") | |
| cols[4].write(f"{comp.tilt:.1f}") | |
| edit_key = f"edit_{tab_name.lower()}_{comp.name}_{idx}" | |
| delete_key = f"delete_{tab_name.lower()}_{comp.name}_{idx}" | |
| with cols[5].container(): | |
| editor_data = { | |
| "index": idx, | |
| "name": comp.name, | |
| "area": comp.area, | |
| "orientation": comp.orientation, | |
| "tilt": comp.tilt, | |
| "is_edit": True | |
| } | |
| if comp_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]: | |
| editor_data["construction"] = comp.construction.name if comp.construction else "" | |
| elif comp_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| editor_data["glazing_material"] = comp.glazing_material.name if comp.glazing_material else "" | |
| else: # Doors | |
| editor_data["door_material"] = comp.door_material.name if comp.door_material else "" | |
| if st.button("Edit", key=edit_key): | |
| st.session_state[f"{tab_name.lower()}_editor"] = editor_data | |
| st.session_state[f"{tab_name.lower()}_action"] = {"action": "edit", "id": str(uuid.uuid4())} | |
| st.session_state.components_rerun_pending = True | |
| with cols[6].container(): | |
| if st.button("Delete", key=delete_key): | |
| st.session_state.components[tab_name.lower()].pop(idx) | |
| st.success(f"{tab_name[:-1]} '{comp.name}' deleted!") | |
| st.session_state[f"{tab_name.lower()}_action"] = {"action": "delete", "id": str(uuid.uuid4())} | |
| st.session_state.components_rerun_pending = True | |
| elif comp_type == ComponentType.FLOOR: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**U-Value**") | |
| cols[2].write("**Area (m²)**") | |
| cols[3].write("**Edit**") | |
| cols[4].write("**Delete**") | |
| for idx, comp in enumerate(components): | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(comp.name) | |
| cols[1].write(f"{comp.u_value:.3f}") | |
| cols[2].write(f"{comp.area:.2f}") | |
| edit_key = f"edit_{tab_name.lower()}_{comp.name}_{idx}" | |
| delete_key = f"delete_{tab_name.lower()}_{comp.name}_{idx}" | |
| with cols[3].container(): | |
| editor_data = { | |
| "index": idx, | |
| "name": comp.name, | |
| "area": comp.area, | |
| "is_edit": True | |
| } | |
| if comp_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]: | |
| editor_data["construction"] = comp.construction.name if comp.construction else "" | |
| elif comp_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| editor_data["glazing_material"] = comp.glazing_material.name if comp.glazing_material else "" | |
| else: # Doors | |
| editor_data["door_material"] = comp.door_material.name if comp.door_material else "" | |
| if st.button("Edit", key=edit_key): | |
| st.session_state[f"{tab_name.lower()}_editor"] = editor_data | |
| st.session_state[f"{tab_name.lower()}_action"] = {"action": "edit", "id": str(uuid.uuid4())} | |
| st.session_state.components_rerun_pending = True | |
| with cols[4].container(): | |
| if st.button("Delete", key=delete_key): | |
| st.session_state.components[tab_name.lower()].pop(idx) | |
| st.success(f"{tab_name[:-1]} '{comp.name}' deleted!") | |
| st.session_state[f"{tab_name.lower()}_action"] = {"action": "delete", "id": str(uuid.uuid4())} | |
| st.session_state.components_rerun_pending = True | |
| else: # Doors | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**U-Value**") | |
| cols[2].write("**Solar Abs.**") | |
| cols[3].write("**Elevation**") | |
| cols[4].write("**Rotation (°)**") | |
| cols[5].write("**Tilt (°)**") | |
| cols[6].write("**Edit**") | |
| cols[7].write("**Delete**") | |
| for idx, comp in enumerate(components): | |
| cols = st.columns([2, 1, 1, 1, 1, 1, 1, 1]) | |
| cols[0].write(comp.name) | |
| cols[1].write(f"{comp.u_value:.3f}") | |
| cols[2].write(f"{comp.solar_absorptivity:.2f}") | |
| cols[3].write(comp.elevation if comp.elevation in ['A', 'B', 'C', 'D'] else "N/A") | |
| cols[4].write(f"{comp.rotation:.1f}") | |
| cols[5].write(f"{comp.tilt:.1f}") | |
| edit_key = f"edit_{tab_name.lower()}_{comp.name}_{idx}" | |
| delete_key = f"delete_{tab_name.lower()}_{comp.name}_{idx}" | |
| with cols[6].container(): | |
| editor_data = { | |
| "index": idx, | |
| "name": comp.name, | |
| "area": comp.area, | |
| "elevation": comp.elevation, | |
| "rotation": comp.rotation, | |
| "tilt": comp.tilt, | |
| "is_edit": True | |
| } | |
| if comp_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]: | |
| editor_data["construction"] = comp.construction.name if comp.construction else "" | |
| elif comp_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| editor_data["glazing_material"] = comp.glazing_material.name if comp.glazing_material else "" | |
| else: # Doors | |
| editor_data["door_material"] = comp.door_material.name if comp.door_material else "" | |
| if st.button("Edit", key=edit_key): | |
| st.session_state[f"{tab_name.lower()}_editor"] = editor_data | |
| st.session_state[f"{tab_name.lower()}_action"] = {"action": "edit", "id": str(uuid.uuid4())} | |
| st.session_state.components_rerun_pending = True | |
| with cols[7].container(): | |
| if st.button("Delete", key=delete_key): | |
| st.session_state.components[tab_name.lower()].pop(idx) | |
| st.success(f"{tab_name[:-1]} '{comp.name}' deleted!") | |
| st.session_state[f"{tab_name.lower()}_action"] = {"action": "delete", "id": str(uuid.uuid4())} | |
| st.session_state.components_rerun_pending = True | |
| else: | |
| st.write(f"No {tab_name.lower()} defined.") | |
| with col2: | |
| st.subheader(f"{tab_name[:-1]} Editor/Creator") | |
| with st.container(): | |
| with st.form(f"{tab_name.lower()}_editor_form", clear_on_submit=True): | |
| editor_state = st.session_state.get(f"{tab_name.lower()}_editor", {}) | |
| is_edit = editor_state.get("is_edit", False) | |
| name = st.text_input( | |
| "Name", | |
| value=editor_state.get("name", f"{tab_name[:-1]} {len(components) + 1}"), | |
| help="Unique identifier for the component.", | |
| key=f"{tab_name.lower()}_name_input" | |
| ) | |
| item_names = [item.name for item in available_items] | |
| default_item = editor_state.get("construction", editor_state.get("glazing_material", editor_state.get("door_material", item_names[0] if item_names else ""))) | |
| item_name = st.selectbox( | |
| "Construction" if comp_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR] | |
| else "Glazing Material" if comp_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT] | |
| else "Door Material", | |
| item_names, | |
| index=item_names.index(default_item) if default_item in item_names else 0, | |
| help="Select the material/construction from the Material Library.", | |
| key=f"{tab_name.lower()}_item_input" | |
| ) | |
| item = next((i for i in available_items if i.name == item_name), None) | |
| area = st.number_input( | |
| "Area (square meters)", | |
| min_value=0.1, | |
| value=editor_state.get("area", 10.0), | |
| help="Surface area of the component in square meters.", | |
| key=f"{tab_name.lower()}_area_input" | |
| ) | |
| if comp_type in [ComponentType.WALL, ComponentType.WINDOW, ComponentType.DOOR]: | |
| elevation = st.selectbox( | |
| "Elevation", | |
| ["A", "B", "C", "D"], | |
| index=["A", "B", "C", "D"].index(editor_state.get("elevation", "A")) | |
| if editor_state.get("elevation") in ["A", "B", "C", "D"] else 0, | |
| help="Elevation defines the facade direction (A=North, B=South, C=East, D=West, relative to the building’s rotation angle).", | |
| key=f"{tab_name.lower()}_elevation_input" | |
| ) | |
| rotation = st.number_input( | |
| "Rotation (°)", | |
| min_value=-45.0, | |
| max_value=45.0, | |
| value=editor_state.get("rotation", 0.0), | |
| step=0.1, | |
| help="Rotation adjusts the component’s angle relative to the elevation for precise positioning (-45° to 45°).", | |
| key=f"{tab_name.lower()}_rotation_input" | |
| ) | |
| tilt = st.number_input( | |
| "Tilt (°)", | |
| min_value=0.0, | |
| max_value=180.0, | |
| value=editor_state.get("tilt", 90.0), | |
| step=0.1, | |
| help="Tilt defines the angle from the horizontal plane (0°=upward, 90°=vertical, 180°=downward).", | |
| key=f"{tab_name.lower()}_tilt_input" | |
| ) | |
| orientation = orientation_map.get(elevation, 0.0) + rotation | |
| elif comp_type in [ComponentType.ROOF, ComponentType.SKYLIGHT]: | |
| orientation = st.number_input( | |
| "Orientation (°)", | |
| min_value=0.0, | |
| max_value=360.0, | |
| value=editor_state.get("orientation", 0.0), | |
| step=0.1, | |
| help="Orientation defines the component’s angle relative to North (0°=North, 90°=East).", | |
| key=f"{tab_name.lower()}_orientation_input" | |
| ) | |
| rotation = 0.0 | |
| tilt = st.number_input( | |
| "Tilt (°)", | |
| min_value=0.0, | |
| max_value=90.0, | |
| value=editor_state.get("tilt", 0.0), | |
| step=0.1, | |
| help="Tilt defines the angle from the horizontal plane (0°=flat, 90°=vertical).", | |
| key=f"{tab_name.lower()}_tilt_input" | |
| ) | |
| elevation = None | |
| else: # Floors | |
| orientation = None | |
| rotation = 0.0 | |
| tilt = 0.0 | |
| elevation = None | |
| if st.form_submit_button("Save"): | |
| action_id = str(uuid.uuid4()) | |
| if not name.strip(): | |
| st.error("Name cannot be empty.") | |
| elif any(c.name == name and (not is_edit or c != components[editor_state.get("index")]) for c in components): | |
| st.error(f"A {tab_name[:-1].lower()} with the name '{name}' already exists.") | |
| elif not item: | |
| st.error("Please select a valid construction/material.") | |
| else: | |
| try: | |
| component_args = { | |
| "name": name, | |
| "component_type": comp_type, | |
| "area": area, | |
| "elevation": elevation, | |
| "orientation": orientation, | |
| "rotation": rotation, | |
| "tilt": tilt | |
| } | |
| if comp_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]: | |
| component_args["construction"] = item | |
| elif comp_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| component_args["glazing_material"] = item | |
| component_args["shgc"] = item.shgc if item else 0.7 | |
| else: # Doors | |
| component_args["door_material"] = item | |
| new_component = Component(**component_args) | |
| if is_edit: | |
| components[editor_state["index"]] = new_component | |
| st.success(f"{tab_name[:-1]} '{name}' updated!") | |
| else: | |
| components.append(new_component) | |
| st.success(f"{tab_name[:-1]} '{name}' added!") | |
| st.session_state[f"{tab_name.lower()}_editor"] = {} | |
| st.session_state.components_rerun_pending = True | |
| except Exception as e: | |
| st.error(f"Error saving component: {str(e)}") | |
| st.subheader(f"Project {tab_name}") | |
| try: | |
| comp_data = [] | |
| for comp in components: | |
| comp_dict = { | |
| "Name": comp.name, | |
| "Construction/Material": (comp.construction.name if comp.construction else | |
| comp.glazing_material.name if comp.glazing_material else | |
| comp.door_material.name if comp.door_material else "N/A"), | |
| "Area (m²)": comp.area, | |
| "U-Value (W/m²·K)": comp.u_value | |
| } | |
| if comp_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]: | |
| comp_dict["Thermal Mass"] = comp.construction.get_thermal_mass().value if comp.construction else "N/A" | |
| if comp_type in [ComponentType.WALL, ComponentType.WINDOW, ComponentType.DOOR]: | |
| comp_dict["Elevation"] = comp.elevation if comp.elevation in ['A', 'B', 'C', 'D'] else "N/A" | |
| comp_dict["Rotation (°)"] = comp.rotation | |
| comp_dict["Tilt (°)"] = comp.tilt | |
| elif comp_type in [ComponentType.ROOF, ComponentType.SKYLIGHT]: | |
| comp_dict["Orientation (°)"] = comp.orientation | |
| comp_dict["Tilt (°)"] = comp.tilt | |
| if comp_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| comp_dict["SHGC"] = comp.shgc if comp.glazing_material else "N/A" | |
| if comp_type == ComponentType.DOOR: | |
| comp_dict["Solar Absorptivity"] = comp.solar_absorptivity | |
| comp_data.append(comp_dict) | |
| comp_df = pd.DataFrame(comp_data) | |
| if not comp_df.empty: | |
| st.dataframe(comp_df, use_container_width=True) | |
| else: | |
| st.write(f"No project {tab_name.lower()} data to display.") | |
| except Exception as e: | |
| st.error(f"Error displaying project {tab_name.lower()}: {str(e)}") | |
| st.write(f"No project {tab_name.lower()} to display.") | |
| # Navigation buttons outside the loop | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.button("Back to Material Library", key="components_back_to_material", | |
| on_click=lambda: setattr(st.session_state, "page", "Material Library")) | |
| with col2: | |
| st.button("Continue to Internal Loads", key="components_to_internal", | |
| on_click=lambda: setattr(st.session_state, "page", "Internal Loads")) | |
| def display_internal_loads(self): | |
| st.title("Internal Loads") | |
| st.write("Define internal heat gains from people, lighting, equipment, ventilation, and infiltration based on ASHRAE 2005 Handbook.") | |
| # Check if building type is set | |
| building_type = st.session_state.building_info.get("building_type") | |
| if not building_type: | |
| st.error("Please select a building type in Building Information.") | |
| st.button("Go to Building Information", key="internal_to_building", | |
| on_click=lambda: setattr(st.session_state, "page", "Building Information")) | |
| return | |
| # Initialize rerun_pending flag | |
| if 'internal_loads_rerun_pending' not in st.session_state: | |
| st.session_state.internal_loads_rerun_pending = False | |
| # Check for rerun trigger | |
| if st.session_state.internal_loads_rerun_pending: | |
| st.session_state.internal_loads_rerun_pending = False | |
| st.rerun() | |
| # Import internal loads data | |
| from data.internal_loads import ( | |
| PEOPLE_ACTIVITY_LEVELS, DIVERSITY_FACTORS, LPD_VALUES, LIGHTING_FIXTURE_TYPES, | |
| EQUIPMENT_HEAT_GAINS, VENTILATION_RATES, INFILTRATION_SETTINGS | |
| ) | |
| # Initialize session state for internal loads if not present | |
| if 'internal_loads' not in st.session_state: | |
| st.session_state.internal_loads = { | |
| 'people': [], | |
| 'lighting': [], | |
| 'equipment': None, | |
| 'ventilation': None, | |
| 'infiltration': None | |
| } | |
| # Create tabs | |
| tabs = st.tabs(["People", "Lighting", "Equipment", "Ventilation", "Infiltration"]) | |
| # People Tab | |
| with tabs[0]: | |
| st.subheader("People") | |
| col1, col2 = st.columns([3, 2]) | |
| with col1: | |
| st.write("**Saved People Groups**") | |
| people = st.session_state.internal_loads.get("people", []) | |
| if people: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**Number**") | |
| cols[2].write("**Activity Level**") | |
| cols[3].write("**Edit**") | |
| cols[4].write("**Delete**") | |
| for idx, person in enumerate(people): | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(person["name"]) | |
| cols[1].write(str(person["num_people"])) | |
| cols[2].write(person["activity_level"]) | |
| if cols[3].button("Edit", key=f"edit_person_{idx}"): | |
| st.session_state.people_editor = { | |
| "index": idx, | |
| "name": person["name"], | |
| "num_people": person["num_people"], | |
| "activity_level": person["activity_level"], | |
| "clo_summer": person["clo_summer"], | |
| "clo_winter": person["clo_winter"], | |
| "is_edit": True | |
| } | |
| st.session_state.internal_loads_rerun_pending = True | |
| if cols[4].button("Delete", key=f"delete_person_{idx}"): | |
| st.session_state.internal_loads["people"].pop(idx) | |
| st.success(f"People group '{person['name']}' deleted!") | |
| st.session_state.internal_loads_rerun_pending = True | |
| else: | |
| st.write("No people groups defined.") | |
| with col2: | |
| st.subheader("Add/Edit People Group") | |
| with st.form("people_form", clear_on_submit=True): | |
| editor_state = st.session_state.get("people_editor", {}) | |
| is_edit = editor_state.get("is_edit", False) | |
| name = st.text_input( | |
| "Group Name", | |
| value=editor_state.get("name", f"People Group {len(people) + 1}"), | |
| help="Unique name for the people group." | |
| ) | |
| num_people = st.number_input( | |
| "Number of People", | |
| min_value=1.0, | |
| value=float(editor_state.get("num_people", 10)), | |
| step=1.0, | |
| help="Number of people in the group." | |
| ) | |
| activity_level = st.selectbox( | |
| "Activity Level", | |
| list(PEOPLE_ACTIVITY_LEVELS.keys()), | |
| index=list(PEOPLE_ACTIVITY_LEVELS.keys()).index(editor_state.get("activity_level", "Seated, at Rest (Quiet, Reading, Writing)")) | |
| if editor_state.get("activity_level") in PEOPLE_ACTIVITY_LEVELS else 0, | |
| help="Select the activity level for the group." | |
| ) | |
| clo_summer = st.number_input( | |
| "Clothing Insulation Summer (clo)", | |
| min_value=0.0, | |
| value=float(editor_state.get("clo_summer", 0.5)), | |
| step=0.1, | |
| help="Clothing insulation value for summer." | |
| ) | |
| clo_winter = st.number_input( | |
| "Clothing Insulation Winter (clo)", | |
| min_value=0.0, | |
| value=float(editor_state.get("clo_winter", 1.0)), | |
| step=0.1, | |
| help="Clothing insulation value for winter." | |
| ) | |
| if st.form_submit_button("Save"): | |
| if not name.strip(): | |
| st.error("Group Name cannot be empty.") | |
| elif any(p["name"] == name and (not is_edit or st.session_state.internal_loads["people"][editor_state.get("index")]["name"] != name) | |
| for p in people): | |
| st.error("A people group with this name already exists.") | |
| else: | |
| new_group = { | |
| "name": name, | |
| "num_people": num_people, | |
| "activity_level": activity_level, | |
| "clo_summer": clo_summer, | |
| "clo_winter": clo_winter, | |
| "activity_data": PEOPLE_ACTIVITY_LEVELS[activity_level], | |
| "diversity_factor": DIVERSITY_FACTORS.get(building_type, 1.0) | |
| } | |
| if is_edit: | |
| st.session_state.internal_loads["people"][editor_state["index"]] = new_group | |
| st.success(f"People group '{name}' updated!") | |
| else: | |
| st.session_state.internal_loads["people"].append(new_group) | |
| st.success(f"People group '{name}' added!") | |
| st.session_state.people_editor = {} | |
| st.session_state.internal_loads_rerun_pending = True | |
| # Detailed Table for People | |
| st.subheader("Detailed People Groups") | |
| if people: | |
| people_data = [] | |
| for p in people: | |
| activity_data = p["activity_data"] | |
| people_data.append({ | |
| "Name": p["name"], | |
| "Number of People": p["num_people"], | |
| "Activity Level": p["activity_level"], | |
| "Metabolic Rate (met)": activity_data["metabolic_rate_met"], | |
| "Metabolic Rate (W/person)": activity_data["metabolic_rate_w"], | |
| "Sensible Heat Min (W)": activity_data["sensible_min_w"], | |
| "Sensible Heat Max (W)": activity_data["sensible_max_w"], | |
| "Latent Heat Min (W)": activity_data["latent_min_w"], | |
| "Latent Heat Max (W)": activity_data["latent_max_w"], | |
| "Clothing Insulation Summer (clo)": p["clo_summer"], | |
| "Clothing Insulation Winter (clo)": p["clo_winter"], | |
| "Diversity Factor": p["diversity_factor"] | |
| }) | |
| st.dataframe(pd.DataFrame(people_data), use_container_width=True) | |
| else: | |
| st.write("No people groups to display.") | |
| # Lighting Tab | |
| with tabs[1]: | |
| st.subheader("Lighting") | |
| col1, col2 = st.columns([3, 2]) | |
| with col1: | |
| st.write("**Saved Lighting Entries**") | |
| lighting = st.session_state.internal_loads.get("lighting", []) | |
| if lighting: | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write("**Name**") | |
| cols[1].write("**LPD (W/m²)**") | |
| cols[2].write("**Operating Hours**") | |
| cols[3].write("**Edit**") | |
| cols[4].write("**Delete**") | |
| for idx, light in enumerate(lighting): | |
| cols = st.columns([2, 1, 1, 1, 1]) | |
| cols[0].write(light["name"]) | |
| cols[1].write(f"{light['lpd']:.2f}") | |
| cols[2].write(str(light["operating_hours"])) | |
| if cols[3].button("Edit", key=f"edit_light_{idx}"): | |
| st.session_state.lighting_editor = { | |
| "index": idx, | |
| "name": light["name"], | |
| "lpd": light["lpd"], | |
| "operating_hours": light["operating_hours"], | |
| "fixture_type": light["fixture_type"], | |
| "is_edit": True | |
| } | |
| st.session_state.internal_loads_rerun_pending = True | |
| if cols[4].button("Delete", key=f"delete_light_{idx}"): | |
| st.session_state.internal_loads["lighting"].pop(idx) | |
| st.success(f"Lighting entry '{light['name']}' deleted!") | |
| st.session_state.internal_loads_rerun_pending = True | |
| else: | |
| st.write("No lighting entries defined.") | |
| with col2: | |
| st.subheader("Add/Edit Lighting Entry") | |
| with st.form("lighting_form", clear_on_submit=True): | |
| editor_state = st.session_state.get("lighting_editor", {}) | |
| is_edit = editor_state.get("is_edit", False) | |
| name = st.text_input( | |
| "Name", | |
| value=editor_state.get("name", f"Lighting {len(lighting) + 1}"), | |
| help="Unique name for the lighting entry." | |
| ) | |
| lpd = st.number_input( | |
| "Lighting Power Density (LPD) (W/m²)", | |
| min_value=0.0, | |
| value=float(editor_state.get("lpd", LPD_VALUES.get(building_type, 10.0))), | |
| step=0.1, | |
| help="Lighting power density in W/m²." | |
| ) | |
| operating_hours = st.slider( | |
| "Operating Hours (hours/day)", | |
| min_value=0, | |
| max_value=24, | |
| value=editor_state.get("operating_hours", 8), | |
| help="Daily operating hours for lighting." | |
| ) | |
| fixture_type = st.selectbox( | |
| "Fixture Type", | |
| list(LIGHTING_FIXTURE_TYPES.keys()), | |
| index=list(LIGHTING_FIXTURE_TYPES.keys()).index(editor_state.get("fixture_type", "Fluorescent")) | |
| if editor_state.get("fixture_type") in LIGHTING_FIXTURE_TYPES else 0, | |
| help="Select the lighting fixture type." | |
| ) | |
| if st.form_submit_button("Save"): | |
| if not name.strip(): | |
| st.error("Name cannot be empty.") | |
| elif any(l["name"] == name and (not is_edit or st.session_state.internal_loads["lighting"][editor_state.get("index")]["name"] != name) | |
| for l in lighting): | |
| st.error("A lighting entry with this name already exists.") | |
| else: | |
| new_entry = { | |
| "name": name, | |
| "lpd": lpd, | |
| "operating_hours": operating_hours, | |
| "fixture_type": fixture_type, | |
| "fixture_data": LIGHTING_FIXTURE_TYPES[fixture_type] | |
| } | |
| if is_edit: | |
| st.session_state.internal_loads["lighting"][editor_state["index"]] = new_entry | |
| st.success(f"Lighting entry '{name}' updated!") | |
| else: | |
| st.session_state.internal_loads["lighting"].append(new_entry) | |
| st.success(f"Lighting entry '{name}' added!") | |
| st.session_state.lighting_editor = {} | |
| st.session_state.internal_loads_rerun_pending = True | |
| # Detailed Table for Lighting | |
| st.subheader("Detailed Lighting Entries") | |
| if lighting: | |
| lighting_data = [] | |
| for l in lighting: | |
| fixture_data = l["fixture_data"] | |
| lighting_data.append({ | |
| "Name": l["name"], | |
| "LPD (W/m²)": l["lpd"], | |
| "Operating Hours": l["operating_hours"], | |
| "Fixture Type": l["fixture_type"], | |
| "Radiative (%)": fixture_data["radiative"], | |
| "Convective (%)": fixture_data["convective"] | |
| }) | |
| st.dataframe(pd.DataFrame(lighting_data), use_container_width=True) | |
| else: | |
| st.write("No lighting entries to display.") | |
| # Equipment Tab | |
| with tabs[2]: | |
| st.subheader("Equipment") | |
| equipment = st.session_state.internal_loads.get("equipment") | |
| if equipment: | |
| st.write(f"**Saved Equipment Settings**") | |
| st.write(f"Sensible Heat (%): {equipment['sensible']}") | |
| st.write(f"Latent Heat (%): {equipment['latent']}") | |
| st.write(f"Convective Heat (%): {equipment['convective']}") | |
| st.write(f"Radiant Split (%): {equipment['radiant']}") | |
| if st.button("Edit Equipment", key="edit_equipment"): | |
| st.session_state.equipment_editor = { | |
| "sensible": equipment["sensible"], | |
| "latent": equipment["latent"], | |
| "convective": equipment["convective"], | |
| "radiant": equipment["radiant"], | |
| "is_edit": True | |
| } | |
| st.session_state.internal_loads_rerun_pending = True | |
| else: | |
| st.write("No equipment settings defined.") | |
| st.subheader("Set Equipment Heat Gains") | |
| with st.form("equipment_form", clear_on_submit=True): | |
| editor_state = st.session_state.get("equipment_editor", {}) | |
| is_edit = editor_state.get("is_edit", False) | |
| sensible = st.number_input( | |
| "Sensible Heat (%)", | |
| min_value=0.0, | |
| max_value=100.0, | |
| value=float(editor_state.get("sensible", EQUIPMENT_HEAT_GAINS.get(building_type, {}).get("sensible", 70.0))), | |
| step=1.0, | |
| help="Percentage of sensible heat gain." | |
| ) | |
| latent = st.number_input( | |
| "Latent Heat (%)", | |
| min_value=0.0, | |
| max_value=100.0, | |
| value=float(editor_state.get("latent", EQUIPMENT_HEAT_GAINS.get(building_type, {}).get("latent", 30.0))), | |
| step=1.0, | |
| help="Percentage of latent heat gain." | |
| ) | |
| convective = st.number_input( | |
| "Convective Heat (%)", | |
| min_value=0.0, | |
| max_value=100.0, | |
| value=float(editor_state.get("convective", EQUIPMENT_HEAT_GAINS.get(building_type, {}).get("convective", 50.0))), | |
| step=1.0, | |
| help="Percentage of convective heat gain." | |
| ) | |
| radiant = st.number_input( | |
| "Radiant Split (%)", | |
| min_value=0.0, | |
| max_value=100.0, | |
| value=float(editor_state.get("radiant", EQUIPMENT_HEAT_GAINS.get(building_type, {}).get("radiant", 50.0))), | |
| step=1.0, | |
| help="Percentage of radiant split of sensible heat." | |
| ) | |
| if st.form_submit_button("Save"): | |
| new_settings = { | |
| "sensible": sensible, | |
| "latent": latent, | |
| "convective": convective, | |
| "radiant": radiant | |
| } | |
| st.session_state.internal_loads["equipment"] = new_settings | |
| st.success("Equipment settings saved!") | |
| st.session_state.equipment_editor = {} | |
| st.session_state.internal_loads_rerun_pending = True | |
| # Detailed Table for Equipment | |
| st.subheader("Equipment Settings") | |
| if equipment: | |
| equipment_data = [{ | |
| "Sensible Heat (%)": equipment["sensible"], | |
| "Latent Heat (%)": equipment["latent"], | |
| "Convective Heat (%)": equipment["convective"], | |
| "Radiant Split (%)": equipment["radiant"] | |
| }] | |
| st.dataframe(pd.DataFrame(equipment_data), use_container_width=True) | |
| else: | |
| st.write("No equipment settings to display.") | |
| # Ventilation Tab | |
| with tabs[3]: | |
| st.subheader("Ventilation") | |
| ventilation = st.session_state.internal_loads.get("ventilation") | |
| if ventilation: | |
| st.write(f"**Saved Ventilation Settings**") | |
| st.write(f"Ventilation Rate for Space (L/s/m²): {ventilation['space_rate']}") | |
| st.write(f"Ventilation Rate for People (L/s/person): {ventilation['people_rate']}") | |
| st.write(f"Ventilation Type: {ventilation['type']}") | |
| if st.button("Edit Ventilation", key="edit_ventilation"): | |
| st.session_state.ventilation_editor = { | |
| "space_rate": ventilation["space_rate"], | |
| "people_rate": ventilation["people_rate"], | |
| "type": ventilation["type"], | |
| "is_edit": True | |
| } | |
| st.session_state.internal_loads_rerun_pending = True | |
| else: | |
| st.write("No ventilation settings defined.") | |
| st.subheader("Set Ventilation Settings") | |
| with st.form("ventilation_form", clear_on_submit=True): | |
| editor_state = st.session_state.get("ventilation_editor", {}) | |
| is_edit = editor_state.get("is_edit", False) | |
| space_rate = st.number_input( | |
| "Ventilation Rate for Space (L/s/m²)", | |
| min_value=0.0, | |
| value=float(editor_state.get("space_rate", VENTILATION_RATES.get(building_type, {}).get("area_rate", 0.3))), | |
| step=0.1, | |
| help="Ventilation rate per square meter." | |
| ) | |
| people_rate = st.number_input( | |
| "Ventilation Rate for People (L/s/person)", | |
| min_value=0.0, | |
| value=float(editor_state.get("people_rate", VENTILATION_RATES.get(building_type, {}).get("people_rate", 2.5))), | |
| step=0.1, | |
| help="Ventilation rate per person." | |
| ) | |
| ventilation_type = st.selectbox( | |
| "Ventilation Type", | |
| ["Natural Ventilation", "Mechanical Ventilation", "Mixed-Mode Ventilation"], | |
| index=["Natural Ventilation", "Mechanical Ventilation", "Mixed-Mode Ventilation"].index(editor_state.get("type", "Mechanical Ventilation")) | |
| if editor_state.get("type") in ["Natural Ventilation", "Mechanical Ventilation", "Mixed-Mode Ventilation"] else 0, | |
| help="Select the type of ventilation." | |
| ) | |
| if st.form_submit_button("Save"): | |
| new_settings = { | |
| "space_rate": space_rate, | |
| "people_rate": people_rate, | |
| "type": ventilation_type | |
| } | |
| st.session_state.internal_loads["ventilation"] = new_settings | |
| st.success("Ventilation settings saved!") | |
| st.session_state.ventilation_editor = {} | |
| st.session_state.internal_loads_rerun_pending = True | |
| # Detailed Table for Ventilation | |
| st.subheader("Ventilation Settings") | |
| if ventilation: | |
| ventilation_data = [{ | |
| "Space Rate (L/s/m²)": ventilation["space_rate"], | |
| "People Rate (L/s/person)": ventilation["people_rate"], | |
| "Type": ventilation["type"] | |
| }] | |
| st.dataframe(pd.DataFrame(ventilation_data), use_container_width=True) | |
| else: | |
| st.write("No ventilation settings to display.") | |
| # Infiltration Tab | |
| with tabs[4]: | |
| st.subheader("Infiltration") | |
| infiltration = st.session_state.internal_loads.get("infiltration") | |
| if infiltration: | |
| st.write(f"**Saved Infiltration Settings**") | |
| st.write(f"Method: {infiltration['method']}") | |
| for key, value in infiltration['settings'].items(): | |
| st.write(f"{key}: {value}") | |
| if st.button("Edit Infiltration", key="edit_infiltration"): | |
| st.session_state.infiltration_editor = { | |
| "method": infiltration["method"], | |
| "settings": infiltration["settings"], | |
| "is_edit": True | |
| } | |
| st.session_state.internal_loads_rerun_pending = True | |
| else: | |
| st.write("No infiltration settings defined.") | |
| st.subheader("Set Infiltration Settings") | |
| with st.form("infiltration_form", clear_on_submit=True): | |
| editor_state = st.session_state.get("infiltration_editor", {}) | |
| is_edit = editor_state.get("is_edit", False) | |
| method = st.selectbox( | |
| "Infiltration Method", | |
| ["ACH", "Crack Flow", "Empirical Equations"], | |
| index=["ACH", "Crack Flow", "Empirical Equations"].index(editor_state.get("method", "ACH")) | |
| if editor_state.get("method") in ["ACH", "Crack Flow", "Empirical Equations"] else 0, | |
| help="Select the infiltration calculation method." | |
| ) | |
| # Dynamic fields based on method | |
| if method == "ACH": | |
| rate = st.number_input( | |
| "ACH Rate", | |
| min_value=0.0, | |
| value=float(editor_state.get("settings", {}).get("rate", INFILTRATION_SETTINGS["ACH"].get(building_type, {}).get("rate", 0.5))), | |
| step=0.1, | |
| help="Air changes per hour." | |
| ) | |
| settings = {"rate": rate} | |
| elif method == "Crack Flow": | |
| ela = st.number_input( | |
| "Effective Leakage Area (ELA) (m²/m²)", | |
| min_value=0.0, | |
| value=float(editor_state.get("settings", {}).get("ela", INFILTRATION_SETTINGS["Crack Flow"].get(building_type, {}).get("ela", 0.0001))), | |
| step=0.0001, | |
| help="Effective leakage area per square meter of wall." | |
| ) | |
| settings = {"ela": ela} | |
| else: # Empirical Equations | |
| c = st.number_input( | |
| "Coefficient C", | |
| min_value=0.0, | |
| value=float(editor_state.get("settings", {}).get("c", INFILTRATION_SETTINGS["Empirical Equations"].get(building_type, {}).get("c", 0.1))), | |
| step=0.01, | |
| help="Coefficient for empirical equation." | |
| ) | |
| n = st.number_input( | |
| "Exponent n", | |
| min_value=0.0, | |
| value=float(editor_state.get("settings", {}).get("n", INFILTRATION_SETTINGS["Empirical Equations"].get(building_type, {}).get("n", 0.65))), | |
| step=0.01, | |
| help="Exponent for empirical equation." | |
| ) | |
| settings = {"c": c, "n": n} | |
| if st.form_submit_button("Save"): | |
| new_settings = { | |
| "method": method, | |
| "settings": settings | |
| } | |
| st.session_state.internal_loads["infiltration"] = new_settings | |
| st.success("Infiltration settings saved!") | |
| st.session_state.infiltration_editor = {} | |
| st.session_state.internal_loads_rerun_pending = True | |
| # Detailed Table for Infiltration | |
| st.subheader("Infiltration Settings") | |
| if infiltration: | |
| infiltration_data = [{ | |
| "Method": infiltration["method"], | |
| **infiltration["settings"] | |
| }] | |
| st.dataframe(pd.DataFrame(infiltration_data), use_container_width=True) | |
| else: | |
| st.write("No infiltration settings to display.") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.button("Back to Building Components", key="internal_back_to_components", | |
| on_click=lambda: setattr(st.session_state, "page", "Building Components")) | |
| with col2: | |
| st.button("Continue to Calculation Results", key="internal_to_results", | |
| on_click=lambda: setattr(st.session_state, "page", "Calculation Results")) | |
| def display_calculation_results(self): | |
| st.title("Calculation Results") | |
| st.write("Configure simulation settings and view cooling and heating load calculations based on ASHRAE CTF/TFM methods.") | |
| if not st.session_state.get("climate_data"): | |
| st.error("Please upload climate data in the Climate Data section.") | |
| st.button("Go to Climate Data", key="results_to_climate", | |
| on_click=lambda: setattr(st.session_state, "page", "Climate Data and Design Requirements")) | |
| return | |
| if not any(st.session_state.components.values()): | |
| st.error("Please define building components in the Building Components section.") | |
| st.button("Go to Building Components", key="results_to_components", | |
| on_click=lambda: setattr(st.session_state, "page", "Building Components")) | |
| return | |
| # Location Information Form | |
| st.subheader("Location Information") | |
| with st.form("location_info_form"): | |
| country = st.text_input("Country", value=st.session_state.climate_data.get("country", "")) | |
| city = st.text_input("City", value=st.session_state.climate_data.get("city", "")) | |
| state_province = st.text_input("State/Province", value=st.session_state.climate_data.get("state_province", "")) | |
| latitude = st.number_input("Latitude (°)", value=st.session_state.climate_data.get("latitude", 0.0)) | |
| longitude = st.number_input("Longitude (°)", value=st.session_state.climate_data.get("longitude", 0.0)) | |
| elevation = st.number_input("Elevation (m)", value=st.session_state.climate_data.get("elevation", 0.0)) | |
| time_zone = st.number_input("Time Zone (UTC offset)", value=st.session_state.climate_data.get("time_zone", 0.0)) | |
| save_location_button = st.form_submit_button("Save Location") | |
| if save_location_button: | |
| st.session_state.climate_data.update({ | |
| "country": country, | |
| "city": city, | |
| "state_province": state_province, | |
| "latitude": latitude, | |
| "longitude": longitude, | |
| "elevation": elevation, | |
| "time_zone": time_zone | |
| }) | |
| st.success("Location information saved!") | |
| # Simulation Period Controls | |
| st.subheader("Simulation Period") | |
| typical_extreme_periods = st.session_state.climate_data.get("typical_extreme_periods", {}) | |
| sim_type_options = [ | |
| "Full Year", | |
| "From Date to Date", | |
| "Heating Only", | |
| "Cooling Only", | |
| "Summer Extreme", | |
| "Summer Typical", | |
| "Winter Extreme", | |
| "Winter Typical" | |
| ] | |
| sim_type = st.selectbox( | |
| "Simulation Type", | |
| sim_type_options, | |
| index=sim_type_options.index(st.session_state.sim_period["type"]) | |
| if st.session_state.sim_period["type"] in sim_type_options else 0, | |
| key="sim_type" | |
| ) | |
| sim_period = {"type": sim_type} | |
| if sim_type == "From Date to Date": | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| start_date = st.date_input("Start Date", value=pd.to_datetime("2025-01-01"), key="start_date") | |
| with col2: | |
| end_date = st.date_input("End Date", value=pd.to_datetime("2025-12-31"), key="end_date") | |
| sim_period["start_date"] = start_date | |
| sim_period["end_date"] = end_date | |
| elif sim_type in ["Summer Extreme", "Summer Typical", "Winter Extreme", "Winter Typical"]: | |
| period_key = sim_type.lower().replace(" ", "_") | |
| if period_key in typical_extreme_periods: | |
| period = typical_extreme_periods[period_key] | |
| start_date = pd.to_datetime(f"2025-{period['start']['month']}-{period['start']['day']}") | |
| end_date = pd.to_datetime(f"2025-{period['end']['month']}-{period['end']['day']}") | |
| st.write(f"Period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}") | |
| sim_period["start_date"] = start_date | |
| sim_period["end_date"] = end_date | |
| else: | |
| st.warning(f"No data available for {sim_type}. Please check climate data.") | |
| sim_period["start_date"] = pd.to_datetime("2025-01-01") | |
| sim_period["end_date"] = pd.to_datetime("2025-12-31") | |
| # Indoor Conditions Controls | |
| st.subheader("Indoor Conditions") | |
| indoor_type = st.selectbox( | |
| "Indoor Conditions Type", | |
| ["Fixed", "Time-varying", "Adaptive"], | |
| index=["Fixed", "Time-varying", "Adaptive"].index(st.session_state.indoor_conditions["type"]) | |
| if st.session_state.indoor_conditions["type"] in ["Fixed", "Time-varying", "Adaptive"] else 0, | |
| key="indoor_type" | |
| ) | |
| indoor_conditions = {"type": indoor_type} | |
| if indoor_type == "Fixed": | |
| st.write("Cooling Setpoint") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| cooling_temp = st.number_input( | |
| "Cooling Indoor Temperature (°C)", | |
| min_value=15.0, | |
| max_value=30.0, | |
| value=st.session_state.indoor_conditions.get("cooling_setpoint", {}).get("temperature", 24.0), | |
| key="cooling_fixed_temp" | |
| ) | |
| with col2: | |
| cooling_rh = st.number_input( | |
| "Cooling Indoor Relative Humidity (%)", | |
| min_value=0.0, | |
| max_value=100.0, | |
| value=st.session_state.indoor_conditions.get("cooling_setpoint", {}).get("rh", 50.0), | |
| key="cooling_fixed_rh" | |
| ) | |
| st.write("Heating Setpoint") | |
| col3, col4 = st.columns(2) | |
| with col3: | |
| heating_temp = st.number_input( | |
| "Heating Indoor Temperature (°C)", | |
| min_value=15.0, | |
| max_value=30.0, | |
| value=st.session_state.indoor_conditions.get("heating_setpoint", {}).get("temperature", 22.0), | |
| key="heating_fixed_temp" | |
| ) | |
| with col4: | |
| heating_rh = st.number_input( | |
| "Heating Indoor Relative Humidity (%)", | |
| min_value=0.0, | |
| max_value=100.0, | |
| value=st.session_state.indoor_conditions.get("heating_setpoint", {}).get("rh", 50.0), | |
| key="heating_fixed_rh" | |
| ) | |
| indoor_conditions["cooling_setpoint"] = {"temperature": cooling_temp, "rh": cooling_rh} | |
| indoor_conditions["heating_setpoint"] = {"temperature": heating_temp, "rh": heating_rh} | |
| elif indoor_type == "Time-varying": | |
| st.write("Define hourly schedule (0-23 hours) for Cooling and Heating Setpoints") | |
| cooling_schedule = [] | |
| heating_schedule = [] | |
| for hour in range(24): | |
| with st.expander(f"Hour {hour}", expanded=False): | |
| st.write("Cooling Setpoint") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| cooling_temp = st.number_input( | |
| f"Cooling Temperature (°C) at Hour {hour}", | |
| min_value=15.0, | |
| max_value=30.0, | |
| value=24.0, | |
| key=f"cooling_schedule_temp_{hour}" | |
| ) | |
| with col2: | |
| cooling_rh = st.number_input( | |
| f"Cooling RH (%) at Hour {hour}", | |
| min_value=0.0, | |
| max_value=100.0, | |
| value=50.0, | |
| key=f"cooling_schedule_rh_{hour}" | |
| ) | |
| st.write("Heating Setpoint") | |
| col3, col4 = st.columns(2) | |
| with col3: | |
| heating_temp = st.number_input( | |
| f"Heating Temperature (°C) at Hour {hour}", | |
| min_value=15.0, | |
| max_value=30.0, | |
| value=22.0, | |
| key=f"heating_schedule_temp_{hour}" | |
| ) | |
| with col4: | |
| heating_rh = st.number_input( | |
| f"Heating RH (%) at Hour {hour}", | |
| min_value=0.0, | |
| max_value=100.0, | |
| value=50.0, | |
| key=f"heating_schedule_rh_{hour}" | |
| ) | |
| cooling_schedule.append({"hour": hour, "temperature": cooling_temp, "rh": cooling_rh}) | |
| heating_schedule.append({"hour": hour, "temperature": heating_temp, "rh": heating_rh}) | |
| indoor_conditions["cooling_schedule"] = cooling_schedule | |
| indoor_conditions["heating_schedule"] = heating_schedule | |
| else: # Adaptive | |
| st.write("Adaptive comfort model (ASHRAE 55) will be used, adjusting temperature based on outdoor conditions.") | |
| indoor_conditions["rh"] = 50.0 | |
| # HVAC System Controls | |
| st.subheader("HVAC System Settings") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| cop = st.number_input( | |
| "Coefficient of Performance (COP)", | |
| min_value=1.0, | |
| max_value=6.0, | |
| value=st.session_state.hvac_settings.get("cop", 3.5), | |
| step=0.1, | |
| key="hvac_cop" | |
| ) | |
| with col2: | |
| num_periods = st.number_input( | |
| "Number of Operating Periods", | |
| min_value=1, | |
| max_value=5, | |
| value=len(st.session_state.hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])), | |
| step=1, | |
| key="num_operating_periods" | |
| ) | |
| operating_hours = [] | |
| for i in range(int(num_periods)): | |
| st.write(f"Operating Period {i+1}") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| start_hour = st.slider( | |
| f"Start Hour (Period {i+1})", | |
| min_value=0, | |
| max_value=23, | |
| value=st.session_state.hvac_settings["operating_hours"][i]["start"] if i < len(st.session_state.hvac_settings["operating_hours"]) else 8, | |
| key=f"start_hour_{i}" | |
| ) | |
| with col2: | |
| end_hour = st.slider( | |
| f"End Hour (Period {i+1})", | |
| min_value=start_hour, | |
| max_value=23, | |
| value=st.session_state.hvac_settings["operating_hours"][i]["end"] if i < len(st.session_state.hvac_settings["operating_hours"]) else 18, | |
| key=f"end_hour_{i}" | |
| ) | |
| operating_hours.append({"start": start_hour, "end": end_hour}) | |
| # Save settings to session state | |
| st.session_state.sim_period = sim_period | |
| st.session_state.indoor_conditions = indoor_conditions | |
| st.session_state.hvac_settings = { | |
| "cop": cop, | |
| "operating_hours": operating_hours | |
| } | |
| # Run Simulation Button | |
| if st.button("Run Simulation", key="run_simulation"): | |
| climate_data = st.session_state.climate_data.get("hourly_data", []) | |
| if not climate_data: | |
| st.error("No valid climate data available.") | |
| return | |
| with st.spinner("Running simulation..."): | |
| loads = self.tfm.calculate_tfm_loads( | |
| st.session_state.components, | |
| climate_data, | |
| st.session_state.indoor_conditions, | |
| st.session_state.internal_loads, | |
| st.session_state.building_info, | |
| st.session_state.sim_period, | |
| st.session_state.hvac_settings | |
| ) | |
| st.session_state.calculation_results["cooling"] = loads | |
| st.session_state.calculation_results["heating"] = loads | |
| st.success("Simulation completed!") | |
| # Display Results | |
| if st.session_state.calculation_results.get("cooling") and st.session_state.calculation_results.get("heating"): | |
| df = pd.DataFrame(st.session_state.calculation_results["cooling"]) | |
| if df.empty: | |
| st.error("No load calculations available.") | |
| return | |
| # Equipment Sizing | |
| st.subheader("Equipment Sizing") | |
| peak_cooling_load = df["total_cooling"].max() if "total_cooling" in df else 0.0 | |
| peak_heating_load = df["total_heating"].max() if "total_heating" in df else 0.0 | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.metric("Cooling Equipment Size", f"{peak_cooling_load:.2f} kW", help="Peak hourly cooling load") | |
| with col2: | |
| st.metric("Heating Equipment Size", f"{peak_heating_load:.2f} kW", help="Peak hourly heating load") | |
| # Pie Charts for Load Breakdown | |
| st.subheader("Load Breakdown") | |
| cooling_totals = { | |
| "Conduction": df["conduction_cooling"].sum(), | |
| "Solar": df["solar"].sum(), | |
| "Internal": df["internal"].sum(), | |
| "Ventilation": df["ventilation_cooling"].sum(), | |
| "Infiltration": df["infiltration_cooling"].sum() | |
| } | |
| heating_totals = { | |
| "Conduction": df["conduction_heating"].sum(), | |
| "Ventilation": df["ventilation_heating"].sum(), | |
| "Infiltration": df["infiltration_heating"].sum() | |
| } | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| fig_cooling = go.Figure(data=[ | |
| go.Pie(labels=list(cooling_totals.keys()), values=list(cooling_totals.values())) | |
| ]) | |
| fig_cooling.update_layout(title="Cooling Load Breakdown (kWh)", width=400, height=400) | |
| st.plotly_chart(fig_cooling, use_container_width=True) | |
| with col2: | |
| fig_heating = go.Figure(data=[ | |
| go.Pie(labels=list(heating_totals.keys()), values=list(heating_totals.values())) | |
| ]) | |
| fig_heating.update_layout(title="Heating Load Breakdown (kWh)", width=400, height=400) | |
| st.plotly_chart(fig_heating, use_container_width=True) | |
| # Monthly Loads Bar Chart | |
| st.subheader("Monthly Heating and Cooling Loads") | |
| df["month_name"] = df["month"].map({ | |
| 1: "Jan", 2: "Feb", 3: "Mar", 4: "Apr", 5: "May", 6: "Jun", | |
| 7: "Jul", 8: "Aug", 9: "Sep", 10: "Oct", 11: "Nov", 12: "Dec" | |
| }) | |
| monthly_loads = df.groupby("month_name")[["total_cooling", "total_heating"]].sum().reindex( | |
| ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] | |
| ) | |
| fig_monthly = go.Figure(data=[ | |
| go.Bar(name="Cooling Load (kWh)", x=monthly_loads.index, y=monthly_loads["total_cooling"]), | |
| go.Bar(name="Heating Load (kWh)", x=monthly_loads.index, y=monthly_loads["total_heating"]) | |
| ]) | |
| fig_monthly.update_layout( | |
| title="Monthly Heating and Cooling Loads", | |
| xaxis_title="Month", | |
| yaxis_title="Load (kWh)", | |
| barmode="group", | |
| width=800, | |
| height=400 | |
| ) | |
| st.plotly_chart(fig_monthly, use_container_width=True) | |
| # Detailed Load Summary Table | |
| st.subheader("Load Summary") | |
| summary_row = { | |
| "hour": "Total", | |
| "month_name": "", | |
| "conduction_cooling": df["conduction_cooling"].sum(), | |
| "conduction_heating": df["conduction_heating"].sum(), | |
| "solar": df["solar"].sum(), | |
| "internal": df["internal"].sum(), | |
| "ventilation_cooling": df["ventilation_cooling"].sum(), | |
| "ventilation_heating": df["ventilation_heating"].sum(), | |
| "infiltration_cooling": df["infiltration_cooling"].sum(), | |
| "infiltration_heating": df["infiltration_heating"].sum(), | |
| "total_cooling": df["total_cooling"].sum(), | |
| "total_heating": df["total_heating"].sum() | |
| } | |
| display_df = df[["hour", "month_name", "conduction_cooling", "conduction_heating", "solar", | |
| "internal", "ventilation_cooling", "ventilation_heating", | |
| "infiltration_cooling", "infiltration_heating", "total_cooling", "total_heating"]] | |
| display_df = pd.concat([display_df, pd.DataFrame([summary_row])], ignore_index=True) | |
| st.dataframe(display_df.rename(columns={"month_name": "Month"}), use_container_width=True) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.button("Back to Internal Loads", key="results_back_to_internal", | |
| on_click=lambda: setattr(st.session_state, "page", "Internal Loads")) | |
| with col2: | |
| st.button("Continue to Export Data", key="results_to_export", | |
| on_click=lambda: setattr(st.session_state, "page", "Export Data")) | |
| def display_export_data(self): | |
| st.title("Export Data") | |
| st.write("Export building data, components, loads, and calculations in various formats.") | |
| export_format = st.selectbox("Export Format", ["JSON", "CSV", "Excel"]) | |
| data = { | |
| "building_info": st.session_state.building_info, | |
| "climate_data": { | |
| "country": st.session_state.climate_data.get("country", ""), | |
| "city": st.session_state.climate_data.get("city", ""), | |
| "summary": st.session_state.climate_data.get("summary", {}) | |
| }, | |
| "components": { | |
| key: [{ | |
| "name": comp.name, | |
| "type": comp.component_type.value, | |
| "area": comp.area, | |
| "elevation": comp.elevation, | |
| "orientation": comp.orientation, | |
| "rotation": comp.rotation, | |
| "tilt": comp.tilt, | |
| "construction": comp.construction.name if comp.construction else None, | |
| "glazing_material": comp.glazing_material.name if comp.glazing_material else None, | |
| "door_material": comp.door_material.name if comp.door_material else None, | |
| "u_value": comp.u_value, | |
| "shgc": comp.shgc, | |
| "solar_absorptivity": comp.solar_absorptivity, | |
| "orientation_angle": comp.orientation_angle | |
| } for comp in comp_list] | |
| for key, comp_list in st.session_state.components.items() | |
| }, | |
| "internal_loads": st.session_state.internal_loads, | |
| "calculation_results": st.session_state.calculation_results | |
| } | |
| if export_format == "JSON": | |
| json_str = json.dumps(data, indent=4) | |
| st.download_button( | |
| label="Download JSON", | |
| data=json_str, | |
| file_name="hvac_calculator_data.json", | |
| mime="application/json" | |
| ) | |
| elif export_format == "CSV": | |
| csv_buffer = io.StringIO() | |
| writer = csv.writer(csv_buffer) | |
| writer.writerow(["Section", "Key", "Value"]) | |
| for section, section_data in data.items(): | |
| if isinstance(section_data, dict): | |
| for key, value in section_data.items(): | |
| writer.writerow([section, key, str(value)]) | |
| else: | |
| writer.writerow([section, "", str(section_data)]) | |
| st.download_button( | |
| label="Download CSV", | |
| data=csv_buffer.getvalue(), | |
| file_name="hvac_calculator_data.csv", | |
| mime="text/csv" | |
| ) | |
| elif export_format == "Excel": | |
| output = io.BytesIO() | |
| with pd.ExcelWriter(output, engine="xlsxwriter") as writer: | |
| for section, section_data in data.items(): | |
| if isinstance(section_data, dict): | |
| if section == "components": | |
| for comp_type, comp_list in section_data.items(): | |
| df = pd.DataFrame(comp_list) | |
| if not df.empty: | |
| df.to_excel(writer, sheet_name=f"Components_{comp_type}", index=False) | |
| elif section == "internal_loads": | |
| for load_type, load_list in section_data.items(): | |
| df = pd.DataFrame(load_list) | |
| if not df.empty: | |
| df.to_excel(writer, sheet_name=f"Loads_{load_type}", index=False) | |
| elif section == "calculation_results": | |
| for calc_type, calc_data in section_data.items(): | |
| if calc_data: | |
| df = pd.DataFrame(calc_data) | |
| df.to_excel(writer, sheet_name=f"Results_{calc_type}", index=False) | |
| else: | |
| df = pd.DataFrame.from_dict(section_data, orient="index").reset_index() | |
| df.columns = ["Key", "Value"] | |
| df.to_excel(writer, sheet_name=section, index=False) | |
| st.download_button( | |
| label="Download Excel", | |
| data=output.getvalue(), | |
| file_name="hvac_calculator_data.xlsx", | |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | |
| ) | |
| st.button("Back to Calculation Results", key="export_back_to_results", | |
| on_click=lambda: setattr(st.session_state, "page", "Calculation Results")) | |
| if __name__ == "__main__": | |
| app = HVACCalculator() |