HVAC-03 / app /main.py
mabuseif's picture
Update app/main.py
f02f762 verified
"""
HVAC Calculator Code Documentation.
Updated 2025-05-02: Integrated skylights, surface color, glazing type, frame type, and drapery adjustments from main_new.py.
Updated 2025-05-02: Enhanced per Plan.txt to include winter design temperature, humidity, building height, ventilation rate, internal load enhancements, and calculation parameters.
Updated 2025-05-09: Fixed latitude parsing to return string (e.g., "24N") to match ASHRAE table keys and added group validation.
Updated 2025-05-09: Corrected group validation to use alphabetical groups (A-H for walls, A-G for roofs) and enhanced stale component handling.
Updated 2025-05-10: Aligned latitude parsing with cooling_load.py's validate_latitude and updated wall groups to A-H to match cooling_load.py.
"""
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import json
import pycountry
import os
import sys
from typing import Dict, List, Any, Optional, Tuple
import uuid
# Import application modules
from app.building_info_form import BuildingInfoForm
from app.component_selection import ComponentSelectionInterface, Orientation, ComponentType, Wall, Roof, Floor, Window, Door, Skylight, GlazingType, FrameType
from app.results_display import ResultsDisplay
from app.data_validation import DataValidation
from app.data_persistence import DataPersistence
from app.data_export import DataExport
# Import data modules
from data.reference_data import ReferenceData
from data.climate_data import ClimateData, ClimateLocation
from data.ashrae_tables import ASHRAETables
from data.building_components import Wall as WallModel, Roof as RoofModel, Skylight as SkylightModel
# Import utility modules
from utils.u_value_calculator import UValueCalculator
from utils.shading_system import ShadingSystem
from utils.area_calculation_system import AreaCalculationSystem
from utils.psychrometrics import Psychrometrics
from utils.heat_transfer import HeatTransferCalculations
from utils.cooling_load import CoolingLoadCalculator
from utils.heating_load import HeatingLoadCalculator
from utils.component_visualization import ComponentVisualization
from utils.scenario_comparison import ScenarioComparisonVisualization
from utils.psychrometric_visualization import PsychrometricVisualization
from utils.time_based_visualization import TimeBasedVisualization
from data.drapery import Drapery
# NEW: ASHRAE 62.1 Ventilation Rates (Table 6.1)
VENTILATION_RATES = {
"Office": {"people_rate": 2.5, "area_rate": 0.3}, # L/s/person, L/s/m²
"Classroom": {"people_rate": 5.0, "area_rate": 0.9},
"Retail": {"people_rate": 3.8, "area_rate": 0.9},
"Restaurant": {"people_rate": 5.0, "area_rate": 1.8},
"Custom": {"people_rate": 0.0, "area_rate": 0.0}
}
# Valid wall and roof groups for ASHRAE CLTD tables (aligned with cooling_load.py)
VALID_WALL_GROUPS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
VALID_ROOF_GROUPS = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
class HVACCalculator:
def __init__(self):
st.set_page_config(
page_title="HVAC Load Calculator",
page_icon="🌡️",
layout="wide",
initial_sidebar_state="expanded"
)
# Initialize session state
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": ""}
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 'saved_scenarios' not in st.session_state:
st.session_state.saved_scenarios = {}
if 'climate_data' not in st.session_state:
st.session_state.climate_data = {}
if 'debug_mode' not in st.session_state:
st.session_state.debug_mode = False
# Initialize modules
self.building_info_form = BuildingInfoForm()
self.component_selection = ComponentSelectionInterface()
self.results_display = ResultsDisplay()
self.data_validation = DataValidation()
self.data_persistence = DataPersistence()
self.data_export = DataExport()
self.cooling_calculator = CoolingLoadCalculator()
self.heating_calculator = HeatingLoadCalculator()
# Initialize Drapery with UI inputs from session state
self.drapery = Drapery(
openness=st.session_state.get('drapery_openness', 'Semi-Open'),
color=st.session_state.get('drapery_color', 'Medium'),
fullness=st.session_state.get('drapery_fullness', 1.5),
enabled=st.session_state.get('drapery_enabled', True),
shading_device=st.session_state.get('shading_device', 'Drapes')
)
# Persist ClimateData in session_state
if 'climate_data_obj' not in st.session_state:
st.session_state.climate_data_obj = ClimateData()
self.climate_data = st.session_state.climate_data_obj
# Load default climate data if locations are empty
try:
if not self.climate_data.locations:
self.climate_data = ClimateData.from_json("/home/user/app/climate_data.json")
st.session_state.climate_data_obj = self.climate_data
except FileNotFoundError:
st.warning("Default climate data file not found. Please enter climate data manually.")
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",
"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 v1.0.2\n\n"
"Based on ASHRAE steady-state calculation methods\n\n"
"Developed by: Dr Majed Abuseif\n\n"
"School of Architecture and Built Environment\n\n"
"Deakin University\n\n"
"© 2025"
)
def display_page(self, page: str):
if page == "Building Information":
self.building_info_form.display_building_info_form(st.session_state)
elif page == "Climate Data":
self.climate_data.display_climate_input(st.session_state)
elif page == "Building Components":
self.component_selection.display_component_selection(st.session_state)
elif page == "Internal Loads":
self.display_internal_loads()
elif page == "Calculation Results":
self.display_calculation_results()
elif page == "Export Data":
self.data_export.display()
def generate_climate_id(self, country: str, city: str) -> str:
"""Generate a climate ID from country and city names."""
try:
country = country.strip().title()
city = city.strip().title()
if len(country) < 2 or len(city) < 3:
raise ValueError("Country and city names must be at least 2 and 3 characters long, respectively.")
return f"{country[:2].upper()}-{city[:3].upper()}"
except Exception as e:
raise ValueError(f"Invalid country or city name: {str(e)}")
def validate_calculation_inputs(self) -> Tuple[bool, str]:
"""Validate inputs for cooling and heating calculations."""
building_info = st.session_state.get('building_info', {})
components = st.session_state.get('components', {})
climate_data = st.session_state.get('climate_data', {})
# Check building info
if not building_info.get('floor_area', 0) > 0:
return False, "Floor area must be positive."
if not any(components.get(key, []) for key in ['walls', 'roofs', 'windows', 'skylights']):
return False, "At least one wall, roof, window, or skylight must be defined."
# Validate climate data
if not climate_data:
return False, "Climate data is missing."
if not self.climate_data.validate_climate_data(climate_data):
return False, "Invalid climate data format or values."
# Validate and fix component groups
for component_type in ['walls', 'roofs']:
for comp in components.get(component_type, []):
if component_type == 'walls':
wall_group = str(getattr(comp, 'wall_group', 'A')).upper()
if wall_group not in VALID_WALL_GROUPS:
st.warning(f"Invalid wall group '{wall_group}' for {comp.name}. Setting to 'A'.")
comp.wall_group = 'A'
if st.session_state.get('debug_mode', False):
st.write(f"Debug: Wall {comp.name} group set to {comp.wall_group}")
if component_type == 'roofs':
roof_group = str(getattr(comp, 'roof_group', 'A')).upper()
if roof_group not in VALID_ROOF_GROUPS:
st.warning(f"Invalid roof group '{roof_group}' for {comp.name}. Setting to 'A'.")
comp.roof_group = 'A'
if st.session_state.get('debug_mode', False):
st.write(f"Debug: Roof {comp.name} group set to {comp.roof_group}")
# Validate components
for component_type in ['walls', 'roofs', 'windows', 'doors', 'floors', 'skylights']:
for comp in components.get(component_type, []):
if comp.area <= 0:
return False, f"Invalid area for {component_type}: {comp.name}"
if comp.u_value <= 0:
return False, f"Invalid U-value for {component_type}: {comp.name}"
if component_type == 'floors' and getattr(comp, 'ground_contact', False):
if not -10 <= comp.ground_temperature_c <= 40:
return False, f"Ground temperature for {comp.name} must be between -10°C and 40°C"
if getattr(comp, 'perimeter', 0) < 0:
return False, f"Perimeter for {comp.name} cannot be negative"
if component_type in ['walls', 'roofs']:
if not 0.1 <= getattr(comp, 'solar_absorptivity', 0.6) <= 1.0:
return False, f"Invalid solar absorptivity for {component_type}: {comp.name} (must be 0.1-1.0)"
if component_type in ['windows', 'skylights']:
if getattr(comp, 'shgc', 0) <= 0:
return False, f"Invalid SHGC for {component_type}: {comp.name}"
if getattr(comp, 'glazing_type', None) is None:
return False, f"Glazing type missing for {component_type}: {comp.name}"
if getattr(comp, 'frame_type', None) is None:
return False, f"Frame type missing for {component_type}: {comp.name}"
# Validate ventilation rate
if building_info.get('ventilation_rate', 0) < 0:
return False, "Ventilation rate cannot be negative"
if building_info.get('zone_type', '') == 'Custom' and building_info.get('ventilation_rate', 0) == 0:
return False, "Custom ventilation rate must be specified"
# Validate new inputs
if not -50 <= building_info.get('winter_temp', -10) <= 20:
return False, "Winter design temperature must be -50 to 20°C"
if not 0 <= building_info.get('outdoor_rh', 50) <= 100:
return False, "Outdoor relative humidity must be 0-100%"
if not 0 <= building_info.get('indoor_rh', 50) <= 100:
return False, "Indoor relative humidity must be 0-100%"
if not 0 <= building_info.get('building_height', 3) <= 100:
return False, "Building height must be 0-100m"
return True, "Inputs valid."
def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]:
"""Validate if a new internal load is unique and within limits."""
loads = st.session_state.internal_loads.get(load_type, [])
max_loads = 50
if len(loads) >= max_loads:
return False, f"Maximum of {max_loads} {load_type} loads reached."
for existing_load in loads:
if load_type == 'people':
if (existing_load['name'] == new_load['name'] and
existing_load['num_people'] == new_load['num_people'] and
existing_load['activity_level'] == new_load['activity_level'] and
existing_load['zone_type'] == new_load['zone_type'] and
existing_load['hours_in_operation'] == new_load['hours_in_operation'] and
existing_load['latent_gain'] == new_load['latent_gain']):
return False, f"Duplicate people load '{new_load['name']}' already exists."
elif load_type == 'lighting':
if (existing_load['name'] == new_load['name'] and
existing_load['power'] == new_load['power'] and
existing_load['usage_factor'] == new_load['usage_factor'] and
existing_load['zone_type'] == new_load['zone_type'] and
existing_load['hours_in_operation'] == new_load['hours_in_operation']):
return False, f"Duplicate lighting load '{new_load['name']}' already exists."
elif load_type == 'equipment':
if (existing_load['name'] == new_load['name'] and
existing_load['power'] == new_load['power'] and
existing_load['usage_factor'] == new_load['usage_factor'] and
existing_load['radiation_fraction'] == new_load['radiation_fraction'] and
existing_load['zone_type'] == new_load['zone_type'] and
existing_load['hours_in_operation'] == new_load['hours_in_operation']):
return False, f"Duplicate equipment load '{new_load['name']}' already exists."
return True, "Valid load."
def parse_latitude(self, latitude: Any) -> str:
"""Parse latitude from string or number to ASHRAE table format (e.g., '24N')."""
try:
# Use cooling_calculator's validate_latitude for consistency
return self.cooling_calculator.validate_latitude(latitude)
except Exception as e:
st.error(f"Invalid latitude: {latitude}. Using default 32N.")
return "32N"
def display_internal_loads(self):
st.title("Internal Loads")
if st.button("Reset All Internal Loads"):
st.session_state.internal_loads = {'people': [], 'lighting': [], 'equipment': []}
st.success("All internal loads reset!")
st.rerun()
tabs = st.tabs(["People", "Lighting", "Equipment", "Ventilation"])
with tabs[0]:
st.subheader("People")
with st.form("people_form"):
num_people = st.number_input(
"Number of People",
min_value=0,
value=0,
step=1,
help="Total number of occupants in the building"
)
activity_level = st.selectbox(
"Activity Level",
["Seated/Resting", "Light Work", "Moderate Work", "Heavy Work"],
help="Select typical activity level (affects internal heat gains per ASHRAE)"
)
zone_type = st.selectbox(
"Zone Type",
["A", "B", "C", "D"],
help="Select zone type for CLF accuracy per ASHRAE"
)
hours_in_operation = st.selectbox(
"Hours Occupied",
["2h", "4h", "6h"],
help="Select hours of occupancy for CLF calculations"
)
latent_gain = st.number_input(
"Latent Gain per Person (Btu/h)",
min_value=0.0,
max_value=500.0,
value=200.0,
step=10.0,
help="Latent heat gain per person per ASHRAE"
)
people_name = st.text_input("Name", value="Occupants")
if st.form_submit_button("Add People Load"):
people_load = {
"id": f"people_{len(st.session_state.internal_loads['people'])}",
"name": people_name,
"num_people": num_people,
"activity_level": activity_level,
"zone_type": zone_type,
"hours_in_operation": hours_in_operation,
"latent_gain": latent_gain
}
is_valid, message = self.validate_internal_load('people', people_load)
if is_valid:
st.session_state.internal_loads['people'].append(people_load)
st.success("People load added!")
st.rerun()
else:
st.error(message)
if st.session_state.internal_loads['people']:
people_df = pd.DataFrame(st.session_state.internal_loads['people'])
st.dataframe(people_df, use_container_width=True)
selected_people = st.multiselect(
"Select People Loads to Delete",
[load['id'] for load in st.session_state.internal_loads['people']]
)
if st.button("Delete Selected People Loads"):
st.session_state.internal_loads['people'] = [
load for load in st.session_state.internal_loads['people']
if load['id'] not in selected_people
]
st.success("Selected people loads deleted!")
st.rerun()
with tabs[1]:
st.subheader("Lighting")
with st.form("lighting_form"):
power = st.number_input(
"Power (W)",
min_value=0.0,
value=1000.0,
step=100.0,
help="Total lighting power consumption"
)
usage_factor = st.number_input(
"Usage Factor",
min_value=0.0,
max_value=1.0,
value=0.8,
step=0.1,
help="Fraction of time lighting is in use (0 to 1)"
)
zone_type = st.selectbox(
"Zone Type",
["A", "B", "C", "D"],
help="Select zone type for CLF accuracy per ASHRAE"
)
hours_in_operation = st.selectbox(
"Hours On",
["8h", "10h", "12h"],
help="Select hours of lighting operation for CLF calculations"
)
lighting_name = st.text_input("Name", value="General Lighting")
if st.form_submit_button("Add Lighting Load"):
lighting_load = {
"id": f"lighting_{len(st.session_state.internal_loads['lighting'])}",
"name": lighting_name,
"power": power,
"usage_factor": usage_factor,
"zone_type": zone_type,
"hours_in_operation": hours_in_operation
}
is_valid, message = self.validate_internal_load('lighting', lighting_load)
if is_valid:
st.session_state.internal_loads['lighting'].append(lighting_load)
st.success("Lighting load added!")
st.rerun()
else:
st.error(message)
if st.session_state.internal_loads['lighting']:
lighting_df = pd.DataFrame(st.session_state.internal_loads['lighting'])
st.dataframe(lighting_df, use_container_width=True)
selected_lighting = st.multiselect(
"Select Lighting Loads to Delete",
[load['id'] for load in st.session_state.internal_loads['lighting']]
)
if st.button("Delete Selected Lighting Loads"):
st.session_state.internal_loads['lighting'] = [
load for load in st.session_state.internal_loads['lighting']
if load['id'] not in selected_lighting
]
st.success("Selected lighting loads deleted!")
st.rerun()
with tabs[2]:
st.subheader("Equipment")
with st.form("equipment_form"):
power = st.number_input(
"Power (W)",
min_value=0.0,
value=500.0,
step=100.0,
help="Total equipment power consumption"
)
usage_factor = st.number_input(
"Usage Factor",
min_value=0.0,
max_value=1.0,
value=0.7,
step=0.1,
help="Fraction of time equipment is in use (0 to 1)"
)
radiation_fraction = st.number_input(
"Radiation Fraction",
min_value=0.0,
max_value=1.0,
value=0.3,
step=0.1,
help="Fraction of heat gain radiated to surroundings"
)
zone_type = st.selectbox(
"Zone Type",
["A", "B", "C", "D"],
help="Select zone type for CLF accuracy per ASHRAE"
)
hours_in_operation = st.selectbox(
"Hours Operated",
["2h", "4h", "6h"],
help="Select hours of equipment operation for CLF calculations"
)
equipment_name = st.text_input("Name", value="Office Equipment")
if st.form_submit_button("Add Equipment Load"):
equipment_load = {
"id": f"equipment_{len(st.session_state.internal_loads['equipment'])}",
"name": equipment_name,
"power": power,
"usage_factor": usage_factor,
"radiation_fraction": radiation_fraction,
"zone_type": zone_type,
"hours_in_operation": hours_in_operation
}
is_valid, message = self.validate_internal_load('equipment', equipment_load)
if is_valid:
st.session_state.internal_loads['equipment'].append(equipment_load)
st.success("Equipment load added!")
st.rerun()
else:
st.error(message)
if st.session_state.internal_loads['equipment']:
equipment_df = pd.DataFrame(st.session_state.internal_loads['equipment'])
st.dataframe(equipment_df, use_container_width=True)
selected_equipment = st.multiselect(
"Select Equipment Loads to Delete",
[load['id'] for load in st.session_state.internal_loads['equipment']]
)
if st.button("Delete Selected Equipment Loads"):
st.session_state.internal_loads['equipment'] = [
load for load in st.session_state.internal_loads['equipment']
if load['id'] not in selected_equipment
]
st.success("Selected equipment loads deleted!")
st.rerun()
with tabs[3]:
st.subheader("Ventilation Requirements (ASHRAE 62.1)")
with st.form("ventilation_form"):
col1, col2 = st.columns(2)
with col1:
zone_type = st.selectbox(
"Zone Type",
["Office", "Classroom", "Retail", "Restaurant", "Custom"],
help="Select building zone type for ASHRAE 62.1 ventilation rates"
)
ventilation_method = st.selectbox(
"Ventilation Method",
["Constant Volume", "Demand-Controlled"],
help="Constant Volume uses fixed rate; Demand-Controlled adjusts based on occupancy"
)
with col2:
if zone_type == "Custom":
people_rate = st.number_input(
"Ventilation Rate per Person (L/s/person)",
min_value=0.0,
value=2.5,
step=0.1,
help="Custom ventilation rate per person (ASHRAE 62.1)"
)
area_rate = st.number_input(
"Ventilation Rate per Area (L/s/m²)",
min_value=0.0,
value=0.3,
step=0.1,
help="Custom ventilation rate per floor area (ASHRAE 62.1)"
)
ventilation_rate = st.number_input(
"Ventilation Rate (m³/s)",
min_value=0.0,
max_value=10.0,
value=0.0,
step=0.1,
help="Total ventilation rate for custom zone type"
)
else:
people_rate = VENTILATION_RATES[zone_type]["people_rate"]
area_rate = VENTILATION_RATES[zone_type]["area_rate"]
st.write(f"People Rate: {people_rate} L/s/person (ASHRAE 62.1)")
st.write(f"Area Rate: {area_rate} L/s/m² (ASHRAE 62.1)")
ventilation_rate = st.number_input(
"Ventilation Rate (m³/s)",
min_value=0.0,
max_value=10.0,
value=0.0,
step=0.1,
help="Total ventilation rate (override ASHRAE defaults if needed)"
)
if st.form_submit_button("Save Ventilation Settings"):
total_people = sum(load['num_people'] for load in st.session_state.internal_loads.get('people', []))
floor_area = st.session_state.building_info.get('floor_area', 100.0)
calculated_rate = (
(total_people * people_rate + floor_area * area_rate) / 1000 # Convert L/s to m³/s
)
final_rate = ventilation_rate if ventilation_rate > 0 else calculated_rate
if ventilation_method == 'Demand-Controlled':
final_rate *= 0.75 # Reduce by 25% for DCV
st.session_state.building_info.update({
'zone_type': zone_type,
'ventilation_method': ventilation_method,
'ventilation_rate': final_rate
})
st.success(f"Ventilation settings saved! Total rate: {final_rate:.3f} m³/s")
col1, col2 = st.columns(2)
with col1:
st.button(
"Back to Building Components",
on_click=lambda: setattr(st.session_state, "page", "Building Components")
)
with col2:
st.button(
"Continue to Calculation Results",
on_click=lambda: setattr(st.session_state, "page", "Calculation Results")
)
def calculate_cooling(self) -> Tuple[bool, str, Dict]:
"""
Calculate cooling loads using CoolingLoadCalculator.
Returns: (success, message, results)
"""
try:
# Validate inputs
valid, message = self.validate_calculation_inputs()
if not valid:
return False, message, {}
# Gather inputs
building_components = st.session_state.get('components', {})
internal_loads = st.session_state.get('internal_loads', {})
building_info = st.session_state.get('building_info', {})
# Check climate data
if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
return False, "Please enter climate data in the 'Climate Data' page.", {}
# Extract climate data
country = building_info.get('country', '').strip().title()
city = building_info.get('city', '').strip().title()
if not country or not city:
return False, "Country and city must be set in Building Information.", {}
climate_id = self.generate_climate_id(country, city)
location = self.climate_data.get_location_by_id(climate_id, st.session_state)
if not location:
available_locations = list(self.climate_data.locations.keys())[:5]
return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
# Validate climate data
if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']):
return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
# NEW: Month-to-day mapping per Plan.txt
month_to_day = {
"Jan": 15, "Feb": 45, "Mar": 74, "Apr": 105, "May": 135, "Jun": 166,
"Jul": 196, "Aug": 227, "Sep": 258, "Oct": 288, "Nov": 319, "Dec": 350
}
# Validate latitude using cooling_calculator
raw_latitude = location.get('latitude', '32N')
latitude = self.cooling_calculator.validate_latitude(raw_latitude)
if st.session_state.get('debug_mode', False):
st.write(f"Debug: Raw latitude: {raw_latitude}, Validated latitude: {latitude}")
# Format conditions
outdoor_conditions = {
'temperature': location['summer_design_temp_db'],
'relative_humidity': building_info.get('outdoor_rh', location['monthly_humidity'].get('Jul', 50.0)),
'ground_temperature': location['monthly_temps'].get('Jul', 20.0),
'month': 'Jul',
'latitude': latitude,
'wind_speed': building_info.get('wind_speed', 4.0),
'day_of_year': month_to_day.get('Jul', 182)
}
indoor_conditions = {
'temperature': building_info.get('indoor_temp', 24.0),
'relative_humidity': building_info.get('indoor_rh', 50.0)
}
if st.session_state.get('debug_mode', False):
st.write("Debug: Cooling Input State", {
'climate_id': climate_id,
'outdoor_conditions': outdoor_conditions,
'indoor_conditions': indoor_conditions,
'components': {k: len(v) for k, v in building_components.items()},
'internal_loads': {
'people': len(internal_loads.get('people', [])),
'lighting': len(internal_loads.get('lighting', [])),
'equipment': len(internal_loads.get('equipment', []))
},
'building_info': building_info
})
# Format internal loads
formatted_internal_loads = {
'people': {
'number': sum(load['num_people'] for load in internal_loads.get('people', [])),
'activity_level': internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'),
'operating_hours': internal_loads.get('people', [{}])[0].get('hours_in_operation', '8h'),
'zone_type': internal_loads.get('people', [{}])[0].get('zone_type', 'A'),
'latent_gain': internal_loads.get('people', [{}])[0].get('latent_gain', 200.0)
},
'lights': {
'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
'special_allowance': 0.1,
'hours_operation': internal_loads.get('lighting', [{}])[0].get('hours_in_operation', '8h'),
'zone_type': internal_loads.get('lighting', [{}])[0].get('zone_type', 'A')
},
'equipment': {
'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
'radiation_factor': internal_loads.get('equipment', [{}])[0].get('radiation_fraction', 0.3),
'hours_operation': internal_loads.get('equipment', [{}])[0].get('hours_in_operation', '8h'),
'zone_type': internal_loads.get('equipment', [{}])[0].get('zone_type', 'A')
},
'infiltration': {
'flow_rate': building_info.get('infiltration_rate', 0.5),
'height': building_info.get('building_height', 3.0),
'crack_length': building_info.get('crack_length', 10.0)
},
'ventilation': {
'flow_rate': building_info.get('ventilation_rate', 0.1)
},
'operating_hours': building_info.get('operating_hours', '8:00-18:00')
}
# Calculate hourly loads
hourly_loads = self.cooling_calculator.calculate_hourly_cooling_loads(
building_components=building_components,
outdoor_conditions=outdoor_conditions,
indoor_conditions=indoor_conditions,
internal_loads=formatted_internal_loads,
building_volume=building_info.get('floor_area', 100.0) * building_info.get('building_height', 3.0)
)
if not hourly_loads:
return False, "Cooling hourly loads calculation failed. Check input data.", {}
# Get design loads
design_loads = self.cooling_calculator.calculate_design_cooling_load(hourly_loads)
if not design_loads:
return False, "Cooling design loads calculation failed. Check input data.", {}
# Get summary
summary = self.cooling_calculator.calculate_cooling_load_summary(design_loads)
if not summary:
return False, "Cooling load summary calculation failed. Check input data.", {}
# Ensure summary has all required keys
if 'total' not in summary:
if 'total_sensible' in summary and 'total_latent' in summary:
summary['total'] = summary['total_sensible'] + summary['total_latent']
else:
total_load = sum(value for key, value in design_loads.items() if key != 'design_hour')
summary = {
'total_sensible': total_load * 0.7,
'total_latent': total_load * 0.3,
'total': total_load
}
# Format results for results_display.py
floor_area = building_info.get('floor_area', 100.0) or 100.0
results = {
'total_load': summary['total'] / 1000, # kW
'sensible_load': summary['total_sensible'] / 1000, # kW
'latent_load': summary['total_latent'] / 1000, # kW
'load_per_area': summary['total'] / floor_area, # W/m²
'component_loads': {
'walls': design_loads['walls'] / 1000,
'roof': design_loads['roofs'] / 1000,
'windows': (design_loads['windows_conduction'] + design_loads['windows_solar']) / 1000,
'doors': design_loads['doors'] / 1000,
'skylights': design_loads.get('skylights', 0) / 1000,
'people': (design_loads['people_sensible'] + design_loads['people_latent']) / 1000,
'lighting': design_loads['lights'] / 1000,
'equipment': (design_loads['equipment_sensible'] + design_loads['equipment_latent']) / 1000,
'infiltration': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000,
'ventilation': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000
},
'detailed_loads': {
'walls': [],
'roofs': [],
'windows': [],
'doors': [],
'skylights': [],
'internal': [],
'infiltration': {
'air_flow': formatted_internal_loads['infiltration']['flow_rate'],
'sensible_load': design_loads['infiltration_sensible'] / 1000,
'latent_load': design_loads['infiltration_latent'] / 1000,
'total_load': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000
},
'ventilation': {
'air_flow': formatted_internal_loads['ventilation']['flow_rate'],
'sensible_load': design_loads['ventilation_sensible'] / 1000,
'latent_load': design_loads['infiltration_latent'] / 1000,
'total_load': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000
}
},
'building_info': building_info
}
# Populate detailed loads
for wall in building_components.get('walls', []):
try:
load = self.cooling_calculator.calculate_wall_cooling_load(
wall=wall,
outdoor_temp=outdoor_conditions['temperature'],
indoor_temp=indoor_conditions['temperature'],
month=outdoor_conditions['month'],
hour=design_loads['design_hour'],
latitude=outdoor_conditions['latitude'],
solar_absorptivity=wall.solar_absorptivity
)
if st.session_state.get('debug_mode', False):
st.write("Debug: Wall CLTD Inputs", {
'wall_name': wall.name,
'element_type': 'wall',
'group': wall.wall_group,
'orientation': wall.orientation.value,
'hour': design_loads['design_hour'],
'latitude': outdoor_conditions['latitude'],
'solar_absorptivity': wall.solar_absorptivity
})
try:
lat_value = float(outdoor_conditions['latitude'].replace('N', ''))
if st.session_state.get('debug_mode', False):
st.write(f"Debug: Converted wall latitude {outdoor_conditions['latitude']} to {lat_value} for get_cltd")
except ValueError:
lat_value = 32.0
if st.session_state.get('debug_mode', False):
st.error(f"Invalid latitude format in wall load: {outdoor_conditions['latitude']}. Defaulting to 32.0")
results['detailed_loads']['walls'].append({
'name': wall.name,
'orientation': wall.orientation.value,
'area': wall.area,
'u_value': wall.u_value,
'solar_absorptivity': wall.solar_absorptivity,
'cltd': self.cooling_calculator.ashrae_tables.get_cltd(
element_type='wall',
group=wall.wall_group,
orientation=wall.orientation.value,
hour=design_loads['design_hour'],
latitude=lat_value,
solar_absorptivity=wall.solar_absorptivity
),
'load': load / 1000
})
except TypeError as te:
st.error(f"Type error in wall CLTD calculation for {wall.name}: {str(te)}")
return False, f"Type error in wall CLTD calculation: {str(te)}", {}
except Exception as e:
if st.session_state.get('debug_mode', False):
st.error(f"Error in wall CLTD calculation for {wall.name}: {str(e)}")
return False, f"Error in wall CLTD calculation: {str(e)}", {}
for roof in building_components.get('roofs', []):
try:
load = self.cooling_calculator.calculate_roof_cooling_load(
roof=roof,
outdoor_temp=outdoor_conditions['temperature'],
indoor_temp=indoor_conditions['temperature'],
month=outdoor_conditions['month'],
hour=design_loads['design_hour'],
latitude=outdoor_conditions['latitude'],
solar_absorptivity=roof.solar_absorptivity
)
if st.session_state.get('debug_mode', False):
st.write("Debug: Roof CLTD Inputs", {
'roof_name': roof.name,
'element_type': 'roof',
'group': roof.roof_group,
'orientation': roof.orientation.value,
'hour': design_loads['design_hour'],
'latitude': outdoor_conditions['latitude'],
'solar_absorptivity': roof.solar_absorptivity
})
try:
lat_value = float(outdoor_conditions['latitude'].replace('N', ''))
if st.session_state.get('debug_mode', False):
st.write(f"Debug: Converted roof latitude {outdoor_conditions['latitude']} to {lat_value} for get_cltd")
except ValueError:
lat_value = 32.0
if st.session_state.get('debug_mode', False):
st.error(f"Invalid latitude format in roof load: {outdoor_conditions['latitude']}. Defaulting to 32.0")
results['detailed_loads']['roofs'].append({
'name': roof.name,
'orientation': roof.orientation.value,
'area': roof.area,
'u_value': roof.u_value,
'solar_absorptivity': roof.solar_absorptivity,
'cltd': self.cooling_calculator.ashrae_tables.get_cltd(
element_type='roof',
group=roof.roof_group,
orientation=roof.orientation.value,
hour=design_loads['design_hour'],
latitude=lat_value,
solar_absorptivity=roof.solar_absorptivity
),
'load': load / 1000
})
except TypeError as te:
st.error(f"Type error in roof CLTD calculation for {roof.name}: {str(te)}")
return False, f"Type error in roof CLTD calculation: {str(te)}", {}
except Exception as e:
if st.session_state.get('debug_mode', False):
st.error(f"Error in roof CLTD calculation for {roof.name}: {str(e)}")
return False, f"Error in roof CLTD calculation: {str(e)}", {}
for window in building_components.get('windows', []):
adjusted_shgc = window.shgc # Default to base SHGC
if hasattr(window, 'drapery_type') and window.drapery_type and self.drapery.enabled:
try:
adjusted_shgc = self.drapery.get_shading_coefficient(window.shgc)
if st.session_state.get('debug_mode', False):
st.write(f"Debug: Window {window.name} adjusted SHGC: {adjusted_shgc}")
except Exception as e:
if st.session_state.get('debug_mode', False):
st.error(f"Error adjusting SHGC for window {window.name}: {str(e)}")
adjusted_shgc = window.shgc
load_dict = self.cooling_calculator.calculate_window_cooling_load(
window=window,
outdoor_temp=outdoor_conditions['temperature'],
indoor_temp=indoor_conditions['temperature'],
month=outdoor_conditions['month'],
hour=design_loads['design_hour'],
latitude=outdoor_conditions['latitude'],
shading_coefficient=window.shading_coefficient,
adjusted_shgc=adjusted_shgc,
glazing_type=window.glazing_type,
frame_type=window.frame_type
)
if 'total' not in load_dict:
if 'conduction' in load_dict and 'solar' in load_dict:
load_dict['total'] = load_dict['conduction'] + load_dict['solar']
else:
load_dict['total'] = window.u_value * window.area * (outdoor_conditions['temperature'] - indoor_conditions['temperature'])
results['detailed_loads']['windows'].append({
'name': window.name,
'orientation': window.orientation.value,
'area': window.area,
'u_value': window.u_value,
'shgc': window.shgc,
'adjusted_shgc': adjusted_shgc,
'glazing_type': window.glazing_type,
'frame_type': window.frame_type,
'drapery_type': window.drapery_type if hasattr(window, 'drapery_type') else 'None',
'shading_device': window.shading_device,
'shading_coefficient': window.shading_coefficient,
'scl': self.cooling_calculator.ashrae_tables.get_scl(
latitude=outdoor_conditions['latitude'],
month=outdoor_conditions['month'].lower(),
orientation=window.orientation.value,
hour=design_loads['design_hour']
) * 3.15459, # Convert Btu/h-ft² to W/m²
'load': load_dict['total'] / 1000
})
for door in building_components.get('doors', []):
load = self.cooling_calculator.calculate_door_cooling_load(
door=door,
outdoor_temp=outdoor_conditions['temperature'],
indoor_temp=indoor_conditions['temperature']
)
results['detailed_loads']['doors'].append({
'name': door.name,
'orientation': door.orientation.value,
'area': door.area,
'u_value': door.u_value,
'cltd': outdoor_conditions['temperature'] - indoor_conditions['temperature'],
'load': load / 1000
})
for skylight in building_components.get('skylights', []):
adjusted_shgc = skylight.shgc # Default to base SHGC
if hasattr(skylight, 'drapery_type') and skylight.drapery_type and self.drapery.enabled:
try:
adjusted_shgc = self.drapery.get_shading_coefficient(skylight.shgc)
if st.session_state.get('debug_mode', False):
st.write(f"Debug: Skylight {skylight.name} adjusted SHGC: {adjusted_shgc}")
except Exception as e:
if st.session_state.get('debug_mode', False):
st.error(f"Error adjusting SHGC for skylight {skylight.name}: {str(e)}")
adjusted_shgc = skylight.shgc
load_dict = self.cooling_calculator.calculate_skylight_cooling_load(
skylight=skylight,
outdoor_temp=outdoor_conditions['temperature'],
indoor_temp=indoor_conditions['temperature'],
month=outdoor_conditions['month'],
hour=design_loads['design_hour'],
latitude=outdoor_conditions['latitude'],
shading_coefficient=skylight.shading_coefficient,
adjusted_shgc=adjusted_shgc,
glazing_type=skylight.glazing_type,
frame_type=skylight.frame_type
)
if 'total' not in load_dict:
if 'conduction' in load_dict and 'solar' in load_dict:
load_dict['total'] = load_dict['conduction'] + load_dict['solar']
else:
load_dict['total'] = skylight.u_value * skylight.area * (outdoor_conditions['temperature'] - indoor_conditions['temperature'])
results['detailed_loads']['skylights'].append({
'name': skylight.name,
'area': skylight.area,
'u_value': skylight.u_value,
'shgc': skylight.shgc,
'adjusted_shgc': adjusted_shgc,
'glazing_type': skylight.glazing_type,
'frame_type': skylight.frame_type,
'drapery_type': skylight.drapery_type if hasattr(skylight, 'drapery_type') else 'None',
'shading_coefficient': skylight.shading_coefficient,
'scl': self.cooling_calculator.ashrae_tables.get_scl(
latitude=outdoor_conditions['latitude'],
month=outdoor_conditions['month'].lower(),
orientation='Horizontal',
hour=design_loads['design_hour']
) * 3.15459, # Convert Btu/h-ft² to W/m²
'load': load_dict['total'] / 1000
})
for load_type, key in [('people', 'people'), ('lighting', 'lights'), ('equipment', 'equipment')]:
for load in internal_loads.get(key, []):
if load_type == 'people':
load_dict = self.cooling_calculator.calculate_people_cooling_load(
num_people=load['num_people'],
activity_level=load['activity_level'],
hour=design_loads['design_hour'],
latent_gain=load.get('latent_gain', 200.0)
)
if 'total' not in load_dict and ('sensible' in load_dict or 'latent' in load_dict):
load_dict['total'] = load_dict.get('sensible', 0) + load_dict.get('latent', 0)
elif load_type == 'lighting':
light_load = self.cooling_calculator.calculate_lights_cooling_load(
power=load['power'],
use_factor=load['usage_factor'],
special_allowance=0.1,
hour=design_loads['design_hour']
)
load_dict = {'total': light_load if light_load is not None else 0}
else:
load_dict = self.cooling_calculator.calculate_equipment_cooling_load(
power=load['power'],
use_factor=load['usage_factor'],
radiation_factor=load['radiation_fraction'],
hour=design_loads['design_hour']
)
if 'total' not in load_dict and ('sensible' in load_dict or 'latent' in load_dict):
load_dict['total'] = load_dict.get('sensible', 0) + load_dict.get('latent', 0)
results['detailed_loads']['internal'].append({
'type': load_type.capitalize(),
'name': load['name'],
'quantity': load.get('num_people', load.get('power', 1)),
'heat_gain': load_dict.get('sensible', load_dict.get('total', 0)),
'clf': self.cooling_calculator.ashrae_tables.get_clf_people(
zone_type=load.get('zone_type', 'A'),
hours_occupied=load.get('hours_in_operation', '6h'),
hour=design_loads['design_hour']
) if load_type == 'people' else 1.0,
'load': load_dict.get('total', 0) / 1000
})
if st.session_state.get('debug_mode', False):
st.write("Debug: Cooling Results", {
'total_load': results.get('total_load', 'N/A'),
'component_loads': results.get('component_loads', 'N/A'),
'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
})
return True, "Cooling calculation completed.", results
except ValueError as ve:
st.error(f"Input error in cooling calculation: {str(ve)}")
return False, f"Input error: {str(ve)}", {}
except KeyError as ke:
st.error(f"Missing data in cooling calculation: {str(ke)}")
return False, f"Missing data: {str(ke)}", {}
except Exception as e:
st.error(f"Unexpected error in cooling calculation: {str(e)}")
return False, f"Unexpected error: {str(e)}", {}
def calculate_heating(self) -> Tuple[bool, str, Dict]:
"""
Calculate heating loads using HeatingLoadCalculator.
Returns: (success, message, results)
"""
try:
# Validate inputs
valid, message = self.validate_calculation_inputs()
if not valid:
return False, message, {}
# Gather inputs
building_components = st.session_state.get('components', {})
internal_loads = st.session_state.get('internal_loads', {})
building_info = st.session_state.get('building_info', {})
# Check climate data
if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
return False, "Please enter climate data in the 'Climate Data' page.", {}
# Extract climate data
country = building_info.get('country', '').strip().title()
city = building_info.get('city', '').strip().title()
if not country or not city:
return False, "Country and city must be set in Building Information.", {}
climate_id = self.generate_climate_id(country, city)
location = self.climate_data.get_location_by_id(climate_id, st.session_state)
if not location:
available_locations = list(self.climate_data.locations.keys())[:5]
return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
# Validate climate data
if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']):
return False, f"Invalid climate data for {climate_id}. Missing required fields.", {}
# Calculate ground temperature
ground_contact_floors = [f for f in building_components.get('floors', []) if getattr(f, 'ground_contact', False)]
ground_temperature = (
sum(f.ground_temperature_c for f in ground_contact_floors) / len(ground_contact_floors)
if ground_contact_floors else
location['monthly_temps'].get('Jan', 10.0)
)
if not -10 <= ground_temperature <= 40:
return False, f"Invalid ground temperature: {ground_temperature}°C", {}
# Skip heating calculation if outdoor temp exceeds indoor temp
indoor_temp = building_info.get('indoor_temp', 21.0)
outdoor_temp = building_info.get('winter_temp', location['winter_design_temp'])
if outdoor_temp >= indoor_temp:
results = {
'total_load': 0.0,
'load_per_area': 0.0,
'design_heat_loss': 0.0,
'safety_factor': 115.0,
'component_loads': {
'walls': 0.0,
'roof': 0.0,
'floor': 0.0,
'windows': 0.0,
'doors': 0.0,
'skylights': 0.0,
'infiltration': 0.0,
'ventilation': 0.0
},
'detailed_loads': {
'walls': [],
'roofs': [],
'floors': [],
'windows': [],
'doors': [],
'skylights': [],
'infiltration': {'air_flow': 0.0, 'delta_t': 0.0, 'load': 0.0},
'ventilation': {'air_flow': 0.0, 'delta_t': 0.0, 'load': 0.0}
},
'building_info': building_info
}
return True, "No heating required (outdoor temp exceeds indoor temp).", results
# Format conditions
outdoor_conditions = {
'design_temperature': outdoor_temp,
'design_relative_humidity': building_info.get('outdoor_rh', location['monthly_humidity'].get('Jan', 80.0)),
'ground_temperature': ground_temperature,
'wind_speed': building_info.get('wind_speed', 4.0)
}
indoor_conditions = {
'temperature': indoor_temp,
'relative_humidity': building_info.get('indoor_rh', 40.0)
}
if st.session_state.get('debug_mode', False):
st.write("Debug: Heating Input State", {
'climate_id': climate_id,
'outdoor_conditions': outdoor_conditions,
'indoor_conditions': indoor_conditions,
'components': {k: len(v) for k, v in building_components.items()},
'internal_loads': {
'people': len(internal_loads.get('people', [])),
'lighting': len(internal_loads.get('lighting', [])),
'equipment': len(internal_loads.get('equipment', []))
},
'building_info': building_info
})
# Activity-level-based sensible gains
ACTIVITY_GAINS = {
'Seated/Resting': 70.0, # W/person
'Light Work': 85.0,
'Moderate Work': 100.0,
'Heavy Work': 150.0
}
# Format internal loads
formatted_internal_loads = {
'people': {
'number': sum(load['num_people'] for load in internal_loads.get('people', [])),
'sensible_gain': ACTIVITY_GAINS.get(
internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'),
70.0
),
'latent_gain': internal_loads.get('people', [{}])[0].get('latent_gain', 200.0),
'operating_hours': internal_loads.get('people', [{}])[0].get('hours_in_operation', '8h'),
'zone_type': internal_loads.get('people', [{}])[0].get('zone_type', 'A')
},
'lights': {
'power': sum(load['power'] for load in internal_loads.get('lighting', [])),
'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8),
'hours_operation': internal_loads.get('lighting', [{}])[0].get('hours_in_operation', '8h'),
'zone_type': internal_loads.get('lighting', [{}])[0].get('zone_type', 'A')
},
'equipment': {
'power': sum(load['power'] for load in internal_loads.get('equipment', [])),
'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7),
'hours_operation': internal_loads.get('equipment', [{}])[0].get('hours_in_operation', '8h'),
'zone_type': internal_loads.get('equipment', [{}])[0].get('zone_type', 'A')
},
'infiltration': {
'flow_rate': building_info.get('infiltration_rate', 0.05),
'height': building_info.get('building_height', 3.0),
'crack_length': building_info.get('crack_length', 10.0)
},
'ventilation': {
'flow_rate': building_info.get('ventilation_rate', 0.1)
},
'usage_factor': 0.7,
'operating_hours': building_info.get('operating_hours', '8:00-18:00')
}
# Calculate design loads
design_loads = self.heating_calculator.calculate_design_heating_load(
building_components=building_components,
outdoor_conditions=outdoor_conditions,
indoor_conditions=indoor_conditions,
internal_loads=formatted_internal_loads
)
if not design_loads:
return False, "Heating design loads calculation failed. Check input data.", {}
# Get summary
summary = self.heating_calculator.calculate_heating_load_summary(design_loads)
if not summary:
return False, "Heating load summary calculation failed. Check input data.", {}
# Format results
floor_area = building_info.get('floor_area', 100.0) or 100.0
results = {
'total_load': summary['total'] / 1000, # kW
'load_per_area': summary['total'] / floor_area, # W/m²
'design_heat_loss': summary['subtotal'] / 1000, # kW
'safety_factor': summary['safety_factor'] * 100, # %
'component_loads': {
'walls': design_loads['walls'] / 1000,
'roof': design_loads['roofs'] / 1000,
'floor': design_loads['floors'] / 1000,
'windows': design_loads['windows'] / 1000,
'doors': design_loads['doors'] / 1000,
'skylights': design_loads.get('skylights', 0) / 1000,
'infiltration': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000,
'ventilation': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000
},
'detailed_loads': {
'walls': [],
'roofs': [],
'floors': [],
'windows': [],
'doors': [],
'skylights': [],
'infiltration': {
'air_flow': formatted_internal_loads['infiltration']['flow_rate'],
'delta_t': indoor_conditions['temperature'] - outdoor_conditions['design_temperature'],
'load': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000
},
'ventilation': {
'air_flow': formatted_internal_loads['ventilation']['flow_rate'],
'delta_t': indoor_conditions['temperature'] - outdoor_conditions['design_temperature'],
'load': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000
}
},
'building_info': building_info
}
# Populate detailed loads
delta_t = indoor_conditions['temperature'] - outdoor_conditions['design_temperature']
for wall in building_components.get('walls', []):
load = self.heating_calculator.calculate_wall_heating_load(
wall=wall,
outdoor_temp=outdoor_conditions['design_temperature'],
indoor_temp=indoor_conditions['temperature']
)
results['detailed_loads']['walls'].append({
'name': wall.name,
'orientation': wall.orientation.value,
'area': wall.area,
'u_value': wall.u_value,
'solar_absorptivity': wall.solar_absorptivity,
'delta_t': delta_t,
'load': load / 1000
})
for roof in building_components.get('roofs', []):
load = self.heating_calculator.calculate_roof_heating_load(
roof=roof,
outdoor_temp=outdoor_conditions['design_temperature'],
indoor_temp=indoor_conditions['temperature']
)
results['detailed_loads']['roofs'].append({
'name': roof.name,
'orientation': roof.orientation.value,
'area': roof.area,
'u_value': wall.u_value,
'solar_absorptivity': roof.solar_absorptivity,
'delta_t': delta_t,
'load': load / 1000
})
for floor in building_components.get('floors', []):
load = self.heating_calculator.calculate_floor_heating_load(
floor=floor,
ground_temp=outdoor_conditions['ground_temperature'],
indoor_temp=indoor_conditions['temperature']
)
results['detailed_loads']['floors'].append({
'name': floor.name,
'area': floor.area,
'u_value': floor.u_value,
'delta_t': indoor_conditions['temperature'] - outdoor_conditions['ground_temperature'],
'load': load / 1000
})
for window in building_components.get('windows', []):
load = self.heating_calculator.calculate_window_heating_load(
window=window,
outdoor_temp=outdoor_conditions['design_temperature'],
indoor_temp=indoor_conditions['temperature'],
frame_type=window.frame_type
)
results['detailed_loads']['windows'].append({
'name': window.name,
'orientation': wall.orientation.value,
'area': window.area,
'u_value': window.u_value,
'glazing_type': window.glazing_type,
'frame_type': window.frame_type,
'delta_t': delta_t,
'load': load / 1000
})
for door in building_components.get('doors', []):
load = self.heating_calculator.calculate_door_heating_load(
door=door,
outdoor_temp=outdoor_conditions['design_temperature'],
indoor_temp=indoor_conditions['temperature']
)
results['detailed_loads']['doors'].append({
'name': door.name,
'orientation': door.orientation.value,
'area': door.area,
'u_value': door.u_value,
'delta_t': delta_t,
'load': load / 1000
})
for skylight in building_components.get('skylights', []):
load = self.heating_calculator.calculate_skylight_heating_load(
skylight=skylight,
outdoor_temp=outdoor_conditions['design_temperature'],
indoor_temp=indoor_conditions['temperature'],
frame_type=skylight.frame_type
)
results['detailed_loads']['skylights'].append({
'name': skylight.name,
'area': skylight.area,
'u_value': skylight.u_value,
'glazing_type': skylight.glazing_type,
'frame_type': skylight.frame_type,
'delta_t': delta_t,
'load': load / 1000
})
if st.session_state.get('debug_mode', False):
st.write("Debug: Heating Results", {
'total_load': results.get('total_load', 'N/A'),
'component_loads': results.get('component_loads', 'N/A'),
'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
})
return True, "Heating calculation completed.", results
except ValueError as ve:
st.error(f"Input error in heating calculation: {str(ve)}")
return False, f"Input error: {str(ve)}", {}
except KeyError as ke:
st.error(f"Missing data in heating calculation: {str(ke)}")
return False, f"Missing data: {str(ke)}", {}
except Exception as e:
st.error(f"Unexpected error in heating calculation: {str(e)}")
return False, f"Unexpected error: {str(e)}", {}
def display_calculation_results(self):
st.title("Calculation Results")
col1, col2 = st.columns(2)
with col1:
calculate_button = st.button("Calculate Loads")
with col2:
st.session_state.debug_mode = st.checkbox("Debug Mode", value=st.session_state.get('debug_mode', False))
if calculate_button:
# Reset results
st.session_state.calculation_results = {'cooling': {}, 'heating': {}}
with st.spinner("Calculating loads..."):
# Calculate cooling load
cooling_success, cooling_message, cooling_results = self.calculate_cooling()
if cooling_success:
st.session_state.calculation_results['cooling'] = cooling_results
st.success(cooling_message)
else:
st.error(cooling_message)
# Calculate heating load
heating_success, heating_message, heating_results = self.calculate_heating()
if heating_success:
st.session_state.calculation_results['heating'] = heating_results
st.success(heating_message)
else:
st.error(heating_message)
# Display results
self.results_display.display_results(st.session_state)
# Navigation
col1, col2 = st.columns(2)
with col1:
st.button(
"Back to Internal Loads",
on_click=lambda: setattr(st.session_state, "page", "Internal Loads")
)
with col2:
st.button(
"Continue to Export Data",
on_click=lambda: setattr(st.session_state, "page", "Export Data")
)
if __name__ == "__main__":
app = HVACCalculator()