Spaces:
Sleeping
Sleeping
""" | |
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() |