| """ |
| Main application module for HVAC Load Calculator. |
| """ |
|
|
| import streamlit as st |
| import pandas as pd |
| import numpy as np |
| import plotly.graph_objects as go |
| import plotly.express as px |
| import matplotlib.pyplot as plt |
| import json |
| import pycountry |
| import os |
| import sys |
| from typing import Dict, List, Any, Optional, Tuple |
|
|
| |
| from app.building_info_form import BuildingInfoForm |
| from app.component_selection import ComponentSelectionInterface, Orientation, ComponentType, Wall, Roof, Floor, Window, Door |
| from app.results_display import ResultsDisplay |
| from app.data_validation import DataValidation |
| from app.data_persistence import DataPersistence |
| from app.data_export import DataExport |
|
|
| |
| from data.reference_data import ReferenceData |
| from data.climate_data import ClimateData |
| from data.ashrae_tables import ASHRAETables |
|
|
| |
| 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 |
|
|
| class HVACCalculator: |
| def __init__(self): |
| st.set_page_config( |
| page_title="HVAC Load Calculator", |
| page_icon="🌡️", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
| |
| if 'page' not in st.session_state: |
| st.session_state.page = 'Building Information' |
| |
| if 'building_info' not in st.session_state: |
| st.session_state.building_info = {"project_name": ""} |
| |
| if 'components' not in st.session_state: |
| st.session_state.components = { |
| 'walls': [], |
| 'roofs': [], |
| 'floors': [], |
| 'windows': [], |
| 'doors': [] |
| } |
| |
| 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 'scenarios' not in st.session_state: |
| st.session_state.scenarios = [] |
| |
| if 'climate_data' not in st.session_state: |
| st.session_state.climate_data = {} |
| |
| 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() |
| |
| 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.0\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.display_climate_data() |
| 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 calculate_cooling(self) -> Tuple[bool, str, Dict]: |
| """ |
| Calculate cooling loads using CoolingLoadCalculator. |
| Returns: (success, message, results) |
| """ |
| try: |
| |
| building_components = st.session_state.get('components', {}) |
| internal_loads = st.session_state.get('internal_loads', {}) |
| climate_data = st.session_state.get('climate_data', {}) |
| building_info = st.session_state.get('building_info', {}) |
| |
| |
| outdoor_conditions = { |
| 'temperature': climate_data.get('summer_outdoor_db', 35.0), |
| 'relative_humidity': climate_data.get('summer_outdoor_rh', 50.0), |
| 'ground_temperature': climate_data.get('ground_temperature', 20.0), |
| 'month': climate_data.get('design_month', 'Jul'), |
| 'latitude': climate_data.get('latitude', '40N') |
| } |
| indoor_conditions = { |
| 'temperature': building_info.get('indoor_temp', 24.0), |
| 'relative_humidity': building_info.get('indoor_rh', 50.0) |
| } |
| |
| |
| 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'), |
| 'hours_occupancy': f"{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)}h" |
| }, |
| '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': f"{internal_loads.get('lighting', [{}])[0].get('hours_in_operation', 8)}h", |
| 'lighting_type': internal_loads.get('lighting', [{}])[0].get('lighting_type', 'Fluorescent') |
| }, |
| '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': f"{internal_loads.get('equipment', [{}])[0].get('hours_in_operation', 8)}h", |
| 'equipment_type': internal_loads.get('equipment', [{}])[0].get('equipment_type', 'General') |
| }, |
| 'infiltration': {'flow_rate': building_info.get('infiltration_rate', 0.05)}, |
| 'ventilation': {'flow_rate': building_info.get('ventilation_rate', 0.1)} |
| } |
| |
| |
| 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 |
| ) |
| |
| |
| design_loads = self.cooling_calculator.calculate_design_cooling_load(hourly_loads) |
| |
| |
| summary = self.cooling_calculator.calculate_cooling_load_summary(design_loads) |
| |
| |
| floor_area = building_info.get('floor_area', 100.0) or 100.0 |
| results = { |
| 'total_load': summary['total'] / 1000, |
| 'sensible_load': summary['total_sensible'] / 1000, |
| 'latent_load': summary['total_latent'] / 1000, |
| 'load_per_area': summary['total'] / floor_area, |
| '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, |
| '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': [], |
| '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['ventilation_latent'] / 1000, |
| 'total_load': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000 |
| } |
| } |
| } |
| |
| |
| for wall in building_components.get('walls', []): |
| 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'] |
| ) |
| results['detailed_loads']['walls'].append({ |
| 'name': wall.name, |
| 'orientation': wall.orientation.value, |
| 'area': wall.area, |
| 'u_value': wall.u_value, |
| 'cltd': self.cooling_calculator.ashrae_tables.calculate_corrected_cltd_wall( |
| wall_group=wall.wall_group, |
| orientation=wall.orientation.value, |
| hour=design_loads['design_hour'], |
| color='Dark', |
| month=outdoor_conditions['month'], |
| latitude=outdoor_conditions['latitude'], |
| indoor_temp=indoor_conditions['temperature'], |
| outdoor_temp=outdoor_conditions['temperature'] |
| ), |
| 'load': load / 1000 |
| }) |
| |
| for roof in building_components.get('roofs', []): |
| 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'] |
| ) |
| results['detailed_loads']['roofs'].append({ |
| 'name': roof.name, |
| 'orientation': roof.orientation.value, |
| 'area': roof.area, |
| 'u_value': roof.u_value, |
| 'cltd': self.cooling_calculator.ashrae_tables.calculate_corrected_cltd_roof( |
| roof_group=roof.roof_group, |
| hour=design_loads['design_hour'], |
| color='Dark', |
| month=outdoor_conditions['month'], |
| latitude=outdoor_conditions['latitude'], |
| indoor_temp=indoor_conditions['temperature'], |
| outdoor_temp=outdoor_conditions['temperature'] |
| ), |
| 'load': load / 1000 |
| }) |
| |
| for window in building_components.get('windows', []): |
| 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=f"{outdoor_conditions['latitude']}_{outdoor_conditions['month'].upper()}" |
| ) |
| results['detailed_loads']['windows'].append({ |
| 'name': window.name, |
| 'orientation': window.orientation.value, |
| 'area': window.area, |
| 'u_value': window.u_value, |
| 'shgc': window.shgc, |
| 'scl': self.cooling_calculator.ashrae_tables.get_scl( |
| window.orientation.value, |
| design_loads['design_hour'], |
| f"{outdoor_conditions['latitude']}_{outdoor_conditions['month'].upper()}" |
| ), |
| '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 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'], |
| hours_occupancy=f"{load['hours_in_operation']}h", |
| hour=design_loads['design_hour'] |
| ) |
| |
| results['detailed_loads']['internal'].append({ |
| 'type': load_type.capitalize(), |
| 'name': load['name'], |
| 'quantity': load['num_people'], |
| 'activity_level': load_dict.get('activity_level', load['activity_level']), |
| 'sensible_gain_per_person': load_dict.get('sensible_gain_per_person', 0), |
| 'latent_gain_per_person': load_dict.get('latent_gain_per_person', 0), |
| 'heat_gain': load_dict.get('sensible', 0), |
| 'clf': self.cooling_calculator.ashrae_tables.get_clf_people( |
| design_loads['design_hour'], |
| f"{load['hours_in_operation']}h" |
| ), |
| 'load': load_dict['total'] / 1000 |
| }) |
| elif load_type == 'lighting': |
| load_dict = self.cooling_calculator.calculate_lights_cooling_load( |
| power=load['power'], |
| use_factor=load['usage_factor'], |
| special_allowance=0.1, |
| hours_operation=f"{load['hours_in_operation']}h", |
| hour=design_loads['design_hour'], |
| lighting_type=load.get('lighting_type', 'Fluorescent') |
| ) |
| results['detailed_loads']['internal'].append({ |
| 'type': load_type.capitalize(), |
| 'name': load['name'], |
| 'quantity': load['power'], |
| 'lighting_type': load_dict.get('lighting_type', 'Fluorescent'), |
| 'heat_emission_factor': load_dict.get('heat_emission_factor', 0.85), |
| 'heat_gain': load_dict['total'], |
| 'clf': load_dict.get('clf', 1.0), |
| 'load': load_dict['total'] / 1000 |
| }) |
| else: |
| load_dict = self.cooling_calculator.calculate_equipment_cooling_load( |
| power=load['power'], |
| use_factor=load['usage_factor'], |
| radiation_factor=load['radiation_fraction'], |
| hours_operation=f"{load['hours_in_operation']}h", |
| hour=design_loads['design_hour'], |
| equipment_type=load.get('equipment_type', 'General') |
| ) |
| results['detailed_loads']['internal'].append({ |
| 'type': load_type.capitalize(), |
| 'name': load['name'], |
| 'quantity': load['power'], |
| 'equipment_type': load_dict.get('equipment_type', 'General'), |
| 'sensible_factor': load_dict.get('sensible_factor', 1.0), |
| 'latent_factor': load_dict.get('latent_factor', 0.0), |
| 'heat_gain': load_dict.get('sensible', load_dict['total']), |
| 'clf': load_dict.get('clf', 1.0), |
| 'load': load_dict['total'] / 1000 |
| }) |
| |
| return True, "Cooling calculation completed.", results |
| except Exception as e: |
| return False, f"Cooling calculation error: {str(e)}", {} |
| |
| def calculate_heating(self) -> Tuple[bool, str, Dict]: |
| """ |
| Calculate heating loads using HeatingLoadCalculator. |
| Returns: (success, message, results) |
| """ |
| try: |
| |
| building_components = st.session_state.get('components', {}) |
| internal_loads = st.session_state.get('internal_loads', {}) |
| climate_data = st.session_state.get('climate_data', {}) |
| building_info = st.session_state.get('building_info', {}) |
| |
| |
| outdoor_conditions = { |
| 'design_temperature': climate_data.get('winter_outdoor_db', -10.0), |
| 'design_relative_humidity': climate_data.get('winter_outdoor_rh', 80.0), |
| 'ground_temperature': climate_data.get('ground_temperature', 10.0) |
| } |
| indoor_conditions = { |
| 'temperature': building_info.get('indoor_temp', 21.0), |
| 'relative_humidity': building_info.get('indoor_rh', 40.0) |
| } |
| |
| |
| formatted_internal_loads = { |
| 'people': { |
| 'number': sum(load['num_people'] for load in internal_loads.get('people', [])), |
| 'sensible_gain': 70 |
| }, |
| 'lights': { |
| 'power': sum(load['power'] for load in internal_loads.get('lighting', [])), |
| 'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8) |
| }, |
| 'equipment': { |
| 'power': sum(load['power'] for load in internal_loads.get('equipment', [])), |
| 'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7) |
| }, |
| 'infiltration': {'flow_rate': building_info.get('infiltration_rate', 0.05)}, |
| 'ventilation': {'flow_rate': building_info.get('ventilation_rate', 0.1)}, |
| 'usage_factor': 0.7 |
| } |
| |
| |
| 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 |
| ) |
| |
| |
| summary = self.heating_calculator.calculate_heating_load_summary(design_loads) |
| |
| |
| floor_area = building_info.get('floor_area', 100.0) or 100.0 |
| results = { |
| 'total_load': summary['total'] / 1000, |
| 'load_per_area': summary['total'] / floor_area, |
| 'design_heat_loss': summary['subtotal'] / 1000, |
| '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, |
| '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': [], |
| '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 |
| } |
| } |
| } |
| |
| |
| 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, |
| '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': roof.u_value, |
| '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'] |
| ) |
| results['detailed_loads']['windows'].append({ |
| 'name': window.name, |
| 'orientation': window.orientation.value, |
| 'area': window.area, |
| 'u_value': window.u_value, |
| '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 |
| }) |
| |
| return True, "Heating calculation completed.", results |
| except Exception as e: |
| return False, f"Heating calculation error: {str(e)}", {} |
| |
| def display_calculation_results(self): |
| """ |
| Display calculation results with separate tabs for cooling and heating. |
| """ |
| st.title("Calculation Results") |
| |
| |
| debug = st.checkbox("Show debug information") |
| |
| |
| cooling_tab, heating_tab = st.tabs(["Cooling Load", "Heating Load"]) |
| |
| |
| with cooling_tab: |
| st.subheader("Cooling Load") |
| success, message, cooling_results = self.calculate_cooling() |
| |
| if debug: |
| st.write("Cooling Inputs:", { |
| 'components': st.session_state.get('components', {}), |
| 'internal_loads': st.session_state.get('internal_loads', {}), |
| 'climate_data': st.session_state.get('climate_data', {}), |
| 'building_info': st.session_state.get('building_info', {}) |
| }) |
| st.write("Cooling Results:", cooling_results) |
| |
| if not success: |
| st.error(message) |
| elif not cooling_results: |
| st.warning("No cooling load data available. Please ensure all inputs are provided.") |
| else: |
| |
| st.session_state.calculation_results['cooling'] = cooling_results |
| |
| self.results_display._display_summary_results(st.session_state) |
| |
| cooling_details = [] |
| for wall in cooling_results['detailed_loads']['walls']: |
| cooling_details.append({ |
| "Component Type": "Wall", |
| "Name": wall["name"], |
| "Orientation": wall["orientation"], |
| "Area (m²)": wall["area"], |
| "U-Value (W/m²·K)": wall["u_value"], |
| "CLTD (°C)": wall["cltd"], |
| "Load (kW)": wall["load"] |
| }) |
| for roof in cooling_results['detailed_loads']['roofs']: |
| cooling_details.append({ |
| "Component Type": "Roof", |
| "Name": roof["name"], |
| "Orientation": roof["orientation"], |
| "Area (m²)": roof["area"], |
| "U-Value (W/m²·K)": roof["u_value"], |
| "CLTD (°C)": roof["cltd"], |
| "Load (kW)": roof["load"] |
| }) |
| for window in cooling_results['detailed_loads']['windows']: |
| cooling_details.append({ |
| "Component Type": "Window", |
| "Name": window["name"], |
| "Orientation": window["orientation"], |
| "Area (m²)": window["area"], |
| "U-Value (W/m²·K)": window["u_value"], |
| "SHGC": window["shgc"], |
| "SCL (W/m²)": window["scl"], |
| "Load (kW)": window["load"] |
| }) |
| for door in cooling_results['detailed_loads']['doors']: |
| cooling_details.append({ |
| "Component Type": "Door", |
| "Name": door["name"], |
| "Orientation": door["orientation"], |
| "Area (m²)": door["area"], |
| "U-Value (W/m²·K)": door["u_value"], |
| "CLTD (°C)": door["cltd"], |
| "Load (kW)": door["load"] |
| }) |
| for internal_load in cooling_results['detailed_loads']['internal']: |
| |
| if internal_load["type"] == "People": |
| cooling_details.append({ |
| "Component Type": internal_load["type"], |
| "Name": internal_load["name"], |
| "Quantity": internal_load["quantity"], |
| "Activity Level": internal_load.get("activity_level", "N/A"), |
| "Sensible Gain (W/person)": internal_load.get("sensible_gain_per_person", "N/A"), |
| "Latent Gain (W/person)": internal_load.get("latent_gain_per_person", "N/A"), |
| "Heat Gain (W)": internal_load["heat_gain"], |
| "CLF": internal_load["clf"], |
| "Load (kW)": internal_load["load"] |
| }) |
| |
| elif internal_load["type"] == "Lighting": |
| cooling_details.append({ |
| "Component Type": internal_load["type"], |
| "Name": internal_load["name"], |
| "Power (W)": internal_load["quantity"], |
| "Lighting Type": internal_load.get("lighting_type", "N/A"), |
| "Heat Emission Factor": internal_load.get("heat_emission_factor", "N/A"), |
| "Heat Gain (W)": internal_load["heat_gain"], |
| "CLF": internal_load["clf"], |
| "Load (kW)": internal_load["load"] |
| }) |
| |
| elif internal_load["type"] == "Equipment": |
| cooling_details.append({ |
| "Component Type": internal_load["type"], |
| "Name": internal_load["name"], |
| "Power (W)": internal_load["quantity"], |
| "Equipment Type": internal_load.get("equipment_type", "N/A"), |
| "Sensible Factor": internal_load.get("sensible_factor", "N/A"), |
| "Latent Factor": internal_load.get("latent_factor", "N/A"), |
| "Heat Gain (W)": internal_load["heat_gain"], |
| "CLF": internal_load["clf"], |
| "Load (kW)": internal_load["load"] |
| }) |
| else: |
| cooling_details.append({ |
| "Component Type": internal_load["type"], |
| "Name": internal_load["name"], |
| "Quantity": internal_load["quantity"], |
| "Heat Gain (W)": internal_load["heat_gain"], |
| "CLF": internal_load["clf"], |
| "Load (kW)": internal_load["load"] |
| }) |
| cooling_details.append({ |
| "Component Type": "Infiltration", |
| "Name": "Air Infiltration", |
| "Air Flow (m³/s)": cooling_results['detailed_loads']['infiltration']['air_flow'], |
| "Sensible Load (kW)": cooling_results['detailed_loads']['infiltration']['sensible_load'], |
| "Latent Load (kW)": cooling_results['detailed_loads']['infiltration']['latent_load'], |
| "Load (kW)": cooling_results['detailed_loads']['infiltration']['total_load'] |
| }) |
| cooling_details.append({ |
| "Component Type": "Ventilation", |
| "Name": "Fresh Air", |
| "Air Flow (m³/s)": cooling_results['detailed_loads']['ventilation']['air_flow'], |
| "Sensible Load (kW)": cooling_results['detailed_loads']['ventilation']['sensible_load'], |
| "Latent Load (kW)": cooling_results['detailed_loads']['ventilation']['latent_load'], |
| "Load (kW)": cooling_results['detailed_loads']['ventilation']['total_load'] |
| }) |
| cooling_df = pd.DataFrame(cooling_details) |
| st.dataframe(cooling_df, use_container_width=True) |
| |
| |
| with heating_tab: |
| st.subheader("Heating Load") |
| success, message, heating_results = self.calculate_heating() |
| |
| if debug: |
| st.write("Heating Inputs:", { |
| 'components': st.session_state.get('components', {}), |
| 'internal_loads': st.session_state.get('internal_loads', {}), |
| 'climate_data': st.session_state.get('climate_data', {}), |
| 'building_info': st.session_state.get('building_info', {}) |
| }) |
| st.write("Heating Results:", heating_results) |
| |
| if not success: |
| st.error(message) |
| elif not heating_results: |
| st.warning("No heating load data available. Please ensure all inputs are provided.") |
| else: |
| |
| st.session_state.calculation_results['heating'] = heating_results |
| |
| self.results_display._display_summary_results(st.session_state) |
| |
| heating_details = [] |
| for wall in heating_results['detailed_loads']['walls']: |
| heating_details.append({ |
| "Component Type": "Wall", |
| "Name": wall["name"], |
| "Orientation": wall["orientation"], |
| "Area (m²)": wall["area"], |
| "U-Value (W/m²·K)": wall["u_value"], |
| "Temperature Difference (°C)": wall["delta_t"], |
| "Load (kW)": wall["load"] |
| }) |
| for roof in heating_results['detailed_loads']['roofs']: |
| heating_details.append({ |
| "Component Type": "Roof", |
| "Name": roof["name"], |
| "Orientation": roof["orientation"], |
| "Area (m²)": roof["area"], |
| "U-Value (W/m²·K)": roof["u_value"], |
| "Temperature Difference (°C)": roof["delta_t"], |
| "Load (kW)": roof["load"] |
| }) |
| for floor in heating_results['detailed_loads']['floors']: |
| heating_details.append({ |
| "Component Type": "Floor", |
| "Name": floor["name"], |
| "Area (m²)": floor["area"], |
| "U-Value (W/m²·K)": floor["u_value"], |
| "Temperature Difference (°C)": floor["delta_t"], |
| "Load (kW)": floor["load"] |
| }) |
| for window in heating_results['detailed_loads']['windows']: |
| heating_details.append({ |
| "Component Type": "Window", |
| "Name": window["name"], |
| "Orientation": window["orientation"], |
| "Area (m²)": window["area"], |
| "U-Value (W/m²·K)": window["u_value"], |
| "Temperature Difference (°C)": window["delta_t"], |
| "Load (kW)": window["load"] |
| }) |
| for door in heating_results['detailed_loads']['doors']: |
| heating_details.append({ |
| "Component Type": "Door", |
| "Name": door["name"], |
| "Orientation": door["orientation"], |
| "Area (m²)": door["area"], |
| "U-Value (W/m²·K)": door["u_value"], |
| "Temperature Difference (°C)": door["delta_t"], |
| "Load (kW)": door["load"] |
| }) |
| heating_details.append({ |
| "Component Type": "Infiltration", |
| "Name": "Air Infiltration", |
| "Air Flow (m³/s)": heating_results['detailed_loads']['infiltration']['air_flow'], |
| "Temperature Difference (°C)": heating_results['detailed_loads']['infiltration']['delta_t'], |
| "Load (kW)": heating_results['detailed_loads']['infiltration']['load'] |
| }) |
| heating_details.append({ |
| "Component Type": "Ventilation", |
| "Name": "Fresh Air", |
| "Air Flow (m³/s)": heating_results['detailed_loads']['ventilation']['air_flow'], |
| "Temperature Difference (°C)": heating_results['detailed_loads']['ventilation']['delta_t'], |
| "Load (kW)": heating_results['detailed_loads']['ventilation']['load'] |
| }) |
| heating_df = pd.DataFrame(heating_details) |
| st.dataframe(heating_df, use_container_width=True) |
| |
| |
| col1, col2 = st.columns(2) |
| with col1: |
| st.button("Back to Internal Loads", on_click=self.navigate_to, args=["Internal Loads"]) |
| with col2: |
| st.button("Continue to Export Data", on_click=self.navigate_to, args=["Export Data"]) |
| |
| def display_climate_data(self): |
| climate_data = ClimateData() |
| climate_data.display_climate_input(st.session_state) |
| |
| if climate_data.locations: |
| st.session_state["climate_data"] = climate_data.locations |
| |
| def display_internal_loads(self): |
| st.title("Internal Loads") |
| |
| if not any(st.session_state.components.values()): |
| st.warning("Please define building components first.") |
| st.button("Go to Building Components", on_click=self.navigate_to, args=["Building Components"]) |
| return |
| |
| tabs = st.tabs(["People", "Lighting", "Equipment"]) |
| |
| with tabs[0]: |
| self.display_people_loads() |
| |
| with tabs[1]: |
| self.display_lighting_loads() |
| |
| with tabs[2]: |
| self.display_equipment_loads() |
| |
| self.display_internal_loads_summary() |
| |
| col1, col2 = st.columns(2) |
| with col1: |
| st.button("Back to Building Components", on_click=self.navigate_to, args=["Building Components"]) |
| with col2: |
| is_valid, messages = self.data_validation.validate_internal_loads(st.session_state.internal_loads) |
| if messages: |
| self.data_validation.display_validation_messages(messages) |
| if is_valid: |
| st.button("Continue to Calculation Results", on_click=self.navigate_to, args=["Calculation Results"]) |
| else: |
| st.button("Continue to Calculation Results", disabled=True) |
| |
| def display_people_loads(self): |
| st.subheader("People") |
| |
| |
| st.info(""" |
| **Activity Level Heat Gains:** |
| - Seated/Resting: 70W sensible, 45W latent |
| - Light work: 75W sensible, 55W latent |
| - Medium work: 85W sensible, 80W latent |
| - Heavy work: 95W sensible, 145W latent |
| |
| The activity level selection directly affects the heat gain calculations. |
| """) |
| |
| with st.form("people_load_form"): |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| name = st.text_input("Name", "Occupants") |
| num_people = st.number_input("Number of People", min_value=1, value=10) |
| |
| with col2: |
| activity_level = st.selectbox( |
| "Activity Level", |
| options=["Seated/Resting", "Light work", "Medium work", "Heavy work"] |
| ) |
| zone_type = st.selectbox( |
| "Zone Type", |
| options=["Office", "Residential", "Retail", "Educational", "Healthcare"] |
| ) |
| |
| hours_in_operation = st.slider("Hours in Operation", min_value=1, max_value=24, value=8) |
| |
| submitted = st.form_submit_button("Add People Load") |
| |
| if submitted: |
| people_load = { |
| "id": f"people_{len(st.session_state.internal_loads['people'])}", |
| "name": name, |
| "num_people": num_people, |
| "activity_level": activity_level, |
| "zone_type": zone_type, |
| "hours_in_operation": hours_in_operation |
| } |
| |
| st.session_state.internal_loads['people'].append(people_load) |
| st.success(f"Added {name} with {num_people} people at {activity_level} activity level") |
| |
| if st.session_state.internal_loads['people']: |
| st.subheader("Existing People Loads") |
| |
| people_data = [] |
| for load in st.session_state.internal_loads['people']: |
| people_data.append({ |
| "Name": load['name'], |
| "Number of People": load['num_people'], |
| "Activity Level": load['activity_level'], |
| "Zone Type": load['zone_type'], |
| "Hours in Operation": load['hours_in_operation'], |
| "Actions": load['id'] |
| }) |
| |
| df = pd.DataFrame(people_data) |
| |
| for i, row in df.iterrows(): |
| col1, col2, col3, col4, col5, col6, col7 = st.columns([2, 1, 2, 2, 1, 1, 1]) |
| |
| with col1: |
| st.write(row["Name"]) |
| with col2: |
| st.write(row["Number of People"]) |
| with col3: |
| st.write(row["Activity Level"]) |
| with col4: |
| st.write(row["Zone Type"]) |
| with col5: |
| st.write(row["Hours in Operation"]) |
| with col6: |
| if st.button("Edit", key=f"edit_{row['Actions']}"): |
| st.session_state.editing_people = row['Actions'] |
| with col7: |
| if st.button("Delete", key=f"delete_{row['Actions']}"): |
| st.session_state.internal_loads['people'] = [ |
| load for load in st.session_state.internal_loads['people'] |
| if load['id'] != row['Actions'] |
| ] |
| st.experimental_rerun() |
| |
| def display_lighting_loads(self): |
| st.subheader("Lighting") |
| |
| |
| st.info(""" |
| **Lighting Type Heat Emission Factors:** |
| - LED: 80% of power converted to heat |
| - Fluorescent: 85% of power converted to heat |
| - Halogen: 95% of power converted to heat |
| - Incandescent: 98% of power converted to heat |
| |
| The lighting type selection directly affects the heat gain calculations. |
| """) |
| |
| with st.form("lighting_load_form"): |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| name = st.text_input("Name", "General Lighting") |
| power = st.number_input("Power (W)", min_value=0.0, value=1000.0) |
| lighting_type = st.selectbox( |
| "Lighting Type", |
| options=["LED", "Fluorescent", "Halogen", "Incandescent"] |
| ) |
| |
| with col2: |
| usage_factor = st.slider("Usage Factor", min_value=0.0, max_value=1.0, value=0.8, step=0.1) |
| zone_type = st.selectbox( |
| "Zone Type", |
| options=["Office", "Residential", "Retail", "Educational", "Healthcare"] |
| ) |
| |
| hours_in_operation = st.slider("Hours in Operation", min_value=1, max_value=24, value=8) |
| |
| submitted = st.form_submit_button("Add Lighting Load") |
| |
| if submitted: |
| lighting_load = { |
| "id": f"lighting_{len(st.session_state.internal_loads['lighting'])}", |
| "name": name, |
| "power": power, |
| "lighting_type": lighting_type, |
| "usage_factor": usage_factor, |
| "zone_type": zone_type, |
| "hours_in_operation": hours_in_operation |
| } |
| |
| st.session_state.internal_loads['lighting'].append(lighting_load) |
| st.success(f"Added {name} with {power} W ({lighting_type})") |
| |
| if st.session_state.internal_loads['lighting']: |
| st.subheader("Existing Lighting Loads") |
| |
| lighting_data = [] |
| for load in st.session_state.internal_loads['lighting']: |
| lighting_data.append({ |
| "Name": load['name'], |
| "Power (W)": load['power'], |
| "Lighting Type": load.get('lighting_type', 'Fluorescent'), |
| "Usage Factor": load['usage_factor'], |
| "Zone Type": load['zone_type'], |
| "Hours in Operation": load['hours_in_operation'], |
| "Actions": load['id'] |
| }) |
| |
| df = pd.DataFrame(lighting_data) |
| |
| for i, row in df.iterrows(): |
| col1, col2, col3, col4, col5, col6, col7, col8 = st.columns([2, 1, 1.5, 1, 2, 1, 1, 1]) |
| |
| with col1: |
| st.write(row["Name"]) |
| with col2: |
| st.write(f"{row['Power (W)']:.1f}") |
| with col3: |
| st.write(row["Lighting Type"]) |
| with col4: |
| st.write(f"{row['Usage Factor']:.1f}") |
| with col5: |
| st.write(row["Zone Type"]) |
| with col6: |
| st.write(row["Hours in Operation"]) |
| with col7: |
| if st.button("Edit", key=f"edit_{row['Actions']}"): |
| st.session_state.editing_lighting = row['Actions'] |
| with col8: |
| if st.button("Delete", key=f"delete_{row['Actions']}"): |
| st.session_state.internal_loads['lighting'] = [ |
| load for load in st.session_state.internal_loads['lighting'] |
| if load['id'] != row['Actions'] |
| ] |
| st.experimental_rerun() |
| |
| def display_equipment_loads(self): |
| st.subheader("Equipment") |
| |
| |
| st.info(""" |
| **Equipment Type Heat Factors:** |
| - General: 100% sensible, 0% latent |
| - Computer: 95% sensible, 0% latent |
| - Kitchen: 80% sensible, 20% latent |
| - Medical: 90% sensible, 10% latent |
| - Laboratory: 85% sensible, 15% latent |
| |
| The equipment type selection directly affects the heat gain calculations. |
| """) |
| |
| with st.form("equipment_load_form"): |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| name = st.text_input("Name", "Office Equipment") |
| power = st.number_input("Power (W)", min_value=0.0, value=500.0) |
| equipment_type = st.selectbox( |
| "Equipment Type", |
| options=["General", "Computer", "Kitchen", "Medical", "Laboratory"] |
| ) |
| |
| with col2: |
| usage_factor = st.slider("Usage Factor", min_value=0.0, max_value=1.0, value=0.7, step=0.1) |
| radiation_fraction = st.slider("Radiation Fraction", min_value=0.0, max_value=1.0, value=0.3, step=0.1) |
| zone_type = st.selectbox( |
| "Zone Type", |
| options=["Office", "Residential", "Retail", "Educational", "Healthcare"] |
| ) |
| |
| hours_in_operation = st.slider("Hours in Operation", min_value=1, max_value=24, value=8) |
| |
| submitted = st.form_submit_button("Add Equipment Load") |
| |
| if submitted: |
| equipment_load = { |
| "id": f"equipment_{len(st.session_state.internal_loads['equipment'])}", |
| "name": name, |
| "power": power, |
| "equipment_type": equipment_type, |
| "usage_factor": usage_factor, |
| "radiation_fraction": radiation_fraction, |
| "zone_type": zone_type, |
| "hours_in_operation": hours_in_operation |
| } |
| |
| st.session_state.internal_loads['equipment'].append(equipment_load) |
| st.success(f"Added {name} with {power} W ({equipment_type})") |
| |
| if st.session_state.internal_loads['equipment']: |
| st.subheader("Existing Equipment Loads") |
| |
| equipment_data = [] |
| for load in st.session_state.internal_loads['equipment']: |
| equipment_data.append({ |
| "Name": load['name'], |
| "Power (W)": load['power'], |
| "Equipment Type": load.get('equipment_type', 'General'), |
| "Usage Factor": load['usage_factor'], |
| "Radiation Fraction": load['radiation_fraction'], |
| "Zone Type": load['zone_type'], |
| "Hours in Operation": load['hours_in_operation'], |
| "Actions": load['id'] |
| }) |
| |
| df = pd.DataFrame(equipment_data) |
| |
| for i, row in df.iterrows(): |
| col1, col2, col3, col4, col5, col6, col7, col8, col9 = st.columns([2, 1, 1.5, 1, 1, 1.5, 1, 1, 1]) |
| |
| with col1: |
| st.write(row["Name"]) |
| with col2: |
| st.write(f"{row['Power (W)']:.1f}") |
| with col3: |
| st.write(row["Equipment Type"]) |
| with col4: |
| st.write(f"{row['Usage Factor']:.1f}") |
| with col5: |
| st.write(f"{row['Radiation Fraction']:.1f}") |
| with col6: |
| st.write(row["Zone Type"]) |
| with col7: |
| st.write(row["Hours in Operation"]) |
| with col8: |
| if st.button("Edit", key=f"edit_{row['Actions']}"): |
| st.session_state.editing_equipment = row['Actions'] |
| with col9: |
| if st.button("Delete", key=f"delete_{row['Actions']}"): |
| st.session_state.internal_loads['equipment'] = [ |
| load for load in st.session_state.internal_loads['equipment'] |
| if load['id'] != row['Actions'] |
| ] |
| st.experimental_rerun() |
| |
| def display_internal_loads_summary(self): |
| st.subheader("Internal Loads Summary") |
| |
| if not any(st.session_state.internal_loads.values()): |
| st.info("No internal loads defined yet.") |
| return |
| |
| total_people = sum(load['num_people'] for load in st.session_state.internal_loads['people']) |
| total_lighting_power = sum(load['power'] * load['usage_factor'] for load in st.session_state.internal_loads['lighting']) |
| total_equipment_power = sum(load['power'] * load['usage_factor'] for load in st.session_state.internal_loads['equipment']) |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.metric("Total People", f"{total_people}") |
| |
| with col2: |
| st.metric("Total Lighting Power", f"{total_lighting_power:.1f} W") |
| |
| with col3: |
| st.metric("Total Equipment Power", f"{total_equipment_power:.1f} W") |
| |
| if total_lighting_power > 0 or total_equipment_power > 0: |
| |
| people_power = 0 |
| for load in st.session_state.internal_loads['people']: |
| activity_level = load['activity_level'] |
| num_people = load['num_people'] |
| |
| |
| if activity_level == "Seated/Resting": |
| people_power += num_people * (70 + 45) |
| elif activity_level == "Light work": |
| people_power += num_people * (75 + 55) |
| elif activity_level == "Medium work": |
| people_power += num_people * (85 + 80) |
| elif activity_level == "Heavy work": |
| people_power += num_people * (95 + 145) |
| else: |
| people_power += num_people * 115 |
| |
| |
| adjusted_lighting_power = 0 |
| for load in st.session_state.internal_loads['lighting']: |
| lighting_type = load.get('lighting_type', 'Fluorescent') |
| power = load['power'] * load['usage_factor'] |
| |
| |
| if lighting_type == "LED": |
| adjusted_lighting_power += power * 0.80 |
| elif lighting_type == "Fluorescent": |
| adjusted_lighting_power += power * 0.85 |
| elif lighting_type == "Halogen": |
| adjusted_lighting_power += power * 0.95 |
| elif lighting_type == "Incandescent": |
| adjusted_lighting_power += power * 0.98 |
| else: |
| adjusted_lighting_power += power * 0.85 |
| |
| |
| adjusted_equipment_power = 0 |
| for load in st.session_state.internal_loads['equipment']: |
| equipment_type = load.get('equipment_type', 'General') |
| power = load['power'] * load['usage_factor'] |
| |
| |
| if equipment_type == "General": |
| adjusted_equipment_power += power * 1.0 |
| elif equipment_type == "Computer": |
| adjusted_equipment_power += power * 0.95 |
| elif equipment_type == "Kitchen": |
| adjusted_equipment_power += power * 1.0 |
| elif equipment_type == "Medical": |
| adjusted_equipment_power += power * 1.0 |
| elif equipment_type == "Laboratory": |
| adjusted_equipment_power += power * 1.0 |
| else: |
| adjusted_equipment_power += power * 1.0 |
| |
| fig = px.pie( |
| values=[people_power, adjusted_lighting_power, adjusted_equipment_power], |
| names=['People', 'Lighting', 'Equipment'], |
| title='Internal Loads Distribution' |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
| |
| def navigate_to(self, page: str): |
| st.session_state.page = page |
| st.experimental_rerun() |
|
|
| if __name__ == "__main__": |
| app = HVACCalculator() |
|
|