mabuseif's picture
Upload main.py
fb47cc8 verified
"""
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()