Spaces:
Sleeping
Sleeping
""" | |
Heating Load Calculator Page | |
This module implements the heating load calculator interface for the HVAC Load Calculator web application. | |
It provides a step-by-step form for inputting building information and calculates heating loads | |
using the ASHRAE method. | |
""" | |
import streamlit as st | |
import pandas as pd | |
import numpy as np | |
import plotly.express as px | |
import plotly.graph_objects as go | |
import json | |
import os | |
import sys | |
from pathlib import Path | |
from datetime import datetime | |
# Add the parent directory to sys.path to import modules | |
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
# Import custom modules | |
from heating_load import HeatingLoadCalculator | |
from reference_data import ReferenceData | |
from utils.validation import validate_input, ValidationWarning | |
from utils.export import export_data | |
def load_session_state(): | |
"""Initialize or load session state variables.""" | |
# Initialize session state for form data | |
if 'heating_form_data' not in st.session_state: | |
st.session_state.heating_form_data = { | |
'building_info': {}, | |
'building_envelope': {}, | |
'windows': {}, | |
'ventilation': {}, | |
'occupancy': {}, | |
'results': {} | |
} | |
# Initialize session state for validation warnings | |
if 'heating_warnings' not in st.session_state: | |
st.session_state.heating_warnings = { | |
'building_info': [], | |
'building_envelope': [], | |
'windows': [], | |
'ventilation': [], | |
'occupancy': [] | |
} | |
# Initialize session state for form completion status | |
if 'heating_completed' not in st.session_state: | |
st.session_state.heating_completed = { | |
'building_info': False, | |
'building_envelope': False, | |
'windows': False, | |
'ventilation': False, | |
'occupancy': False | |
} | |
# Initialize session state for calculation results | |
if 'heating_results' not in st.session_state: | |
st.session_state.heating_results = None | |
def building_info_form(ref_data): | |
""" | |
Form for building information. | |
Args: | |
ref_data: Reference data object | |
""" | |
st.subheader("Building Information") | |
st.write("Enter general building information, location, and design temperatures.") | |
# Get location options from reference data | |
location_options = {loc_id: loc_data['name'] for loc_id, loc_data in ref_data.locations.items()} | |
col1, col2 = st.columns(2) | |
with col1: | |
# Building name | |
building_name = st.text_input( | |
"Building Name", | |
value=st.session_state.heating_form_data['building_info'].get('building_name', ''), | |
help="Enter a name for this building or project" | |
) | |
# Location selection | |
location = st.selectbox( | |
"Location", | |
options=list(location_options.keys()), | |
format_func=lambda x: location_options[x], | |
index=list(location_options.keys()).index(st.session_state.heating_form_data['building_info'].get('location', 'sydney')) if st.session_state.heating_form_data['building_info'].get('location') in location_options else 0, | |
help="Select the location of the building" | |
) | |
# Get climate data for selected location | |
location_data = ref_data.get_location_data(location) | |
# Indoor design temperature | |
indoor_temp = st.number_input( | |
"Indoor Design Temperature (°C)", | |
value=float(st.session_state.heating_form_data['building_info'].get('indoor_temp', 21.0)), | |
min_value=15.0, | |
max_value=25.0, | |
step=0.5, | |
help="Recommended indoor design temperature for heating is 21°C for living areas and 17°C for bedrooms" | |
) | |
with col2: | |
# Building type | |
building_type = st.selectbox( | |
"Building Type", | |
options=["Residential", "Small Office", "Educational", "Other"], | |
index=["Residential", "Small Office", "Educational", "Other"].index(st.session_state.heating_form_data['building_info'].get('building_type', 'Residential')), | |
help="Select the type of building" | |
) | |
# Outdoor design temperature (with default from location data) | |
outdoor_temp = st.number_input( | |
"Outdoor Design Temperature (°C)", | |
value=float(st.session_state.heating_form_data['building_info'].get('outdoor_temp', location_data['winter_design_temp'])), | |
min_value=-10.0, | |
max_value=15.0, | |
step=0.5, | |
help=f"Default value is based on selected location ({location_data['name']})" | |
) | |
# Building dimensions | |
st.subheader("Building Dimensions") | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
length = st.number_input( | |
"Length (m)", | |
value=float(st.session_state.heating_form_data['building_info'].get('length', 10.0)), | |
min_value=1.0, | |
step=0.1, | |
help="Building length in meters" | |
) | |
with col2: | |
width = st.number_input( | |
"Width (m)", | |
value=float(st.session_state.heating_form_data['building_info'].get('width', 8.0)), | |
min_value=1.0, | |
step=0.1, | |
help="Building width in meters" | |
) | |
with col3: | |
height = st.number_input( | |
"Height (m)", | |
value=float(st.session_state.heating_form_data['building_info'].get('height', 2.7)), | |
min_value=1.0, | |
step=0.1, | |
help="Floor-to-ceiling height in meters" | |
) | |
# Calculate floor area and volume | |
floor_area = length * width | |
volume = floor_area * height | |
st.info(f"Floor Area: {floor_area:.2f} m² | Volume: {volume:.2f} m³") | |
# Save form data to session state | |
form_data = { | |
'building_name': building_name, | |
'building_type': building_type, | |
'location': location, | |
'location_name': location_data['name'], | |
'indoor_temp': indoor_temp, | |
'outdoor_temp': outdoor_temp, | |
'length': length, | |
'width': width, | |
'height': height, | |
'floor_area': floor_area, | |
'volume': volume, | |
'temp_diff': indoor_temp - outdoor_temp | |
} | |
# Validate inputs | |
warnings = [] | |
# Check if building name is provided | |
if not building_name: | |
warnings.append(ValidationWarning("Building name is empty", "Consider adding a building name for reference")) | |
# Check if temperature difference is reasonable | |
if form_data['temp_diff'] <= 0: | |
warnings.append(ValidationWarning( | |
"Invalid temperature difference", | |
"Indoor temperature should be higher than outdoor temperature for heating load calculation", | |
is_critical=False # Changed to non-critical to allow proceeding with warnings | |
)) | |
# Check if dimensions are reasonable | |
if floor_area > 500: | |
warnings.append(ValidationWarning( | |
"Large floor area", | |
"Floor area exceeds 500 m², verify if this is correct for a residential building" | |
)) | |
if height < 2.4 or height > 3.5: | |
warnings.append(ValidationWarning( | |
"Unusual ceiling height", | |
"Typical residential ceiling heights are between 2.4m and 3.5m" | |
)) | |
# Save warnings to session state | |
st.session_state.heating_warnings['building_info'] = warnings | |
# Display warnings if any | |
if warnings: | |
st.warning("Please review the following warnings:") | |
for warning in warnings: | |
st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else "")) | |
st.write(f" Suggestion: {warning.suggestion}") | |
# Save form data regardless of warnings | |
st.session_state.heating_form_data['building_info'] = form_data | |
# Mark this step as completed if there are no critical warnings | |
st.session_state.heating_completed['building_info'] = not any(w.is_critical for w in warnings) | |
# Navigation buttons | |
col1, col2 = st.columns([1, 1]) | |
with col2: | |
next_button = st.button("Next: Building Envelope →", key="heating_building_info_next") | |
if next_button: | |
st.session_state.heating_active_tab = "building_envelope" | |
st.experimental_rerun() | |
def building_envelope_form(ref_data): | |
""" | |
Form for building envelope information. | |
Args: | |
ref_data: Reference data object | |
""" | |
st.subheader("Building Envelope") | |
st.write("Enter information about walls, roof, and floor construction.") | |
# Get building dimensions from previous step | |
building_info = st.session_state.heating_form_data['building_info'] | |
length = building_info.get('length', 10.0) | |
width = building_info.get('width', 8.0) | |
height = building_info.get('height', 2.7) | |
temp_diff = building_info.get('temp_diff', 16.5) | |
# Calculate default areas | |
default_wall_area = 2 * (length + width) * height | |
default_roof_area = length * width | |
default_floor_area = length * width | |
# Initialize envelope data if not already in session state | |
if 'walls' not in st.session_state.heating_form_data['building_envelope']: | |
st.session_state.heating_form_data['building_envelope']['walls'] = [] | |
if 'roof' not in st.session_state.heating_form_data['building_envelope']: | |
st.session_state.heating_form_data['building_envelope']['roof'] = {} | |
if 'floor' not in st.session_state.heating_form_data['building_envelope']: | |
st.session_state.heating_form_data['building_envelope']['floor'] = {} | |
# Walls section | |
st.write("### Walls") | |
# Get wall material options from reference data | |
wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()} | |
# Add custom option | |
wall_material_options["custom_walls"] = "Custom Wall (User-defined)" | |
# Display existing wall entries | |
if st.session_state.heating_form_data['building_envelope']['walls']: | |
st.write("Current walls:") | |
walls_df = pd.DataFrame(st.session_state.heating_form_data['building_envelope']['walls']) | |
walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown")) | |
# Add orientation column with default value if not present | |
walls_df['orientation'] = walls_df['orientation'].fillna('not specified') | |
walls_df = walls_df[['name', 'Material', 'area', 'u_value', 'orientation']] | |
walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)', 'Orientation'] | |
st.dataframe(walls_df) | |
# Add new wall form | |
st.write("Add a new wall:") | |
col1, col2 = st.columns(2) | |
with col1: | |
wall_name = st.text_input("Wall Name", value="", key="new_wall_name_heating") | |
wall_material = st.selectbox( | |
"Wall Material", | |
options=list(wall_material_options.keys()), | |
format_func=lambda x: wall_material_options[x], | |
key="new_wall_material_heating" | |
) | |
# Add wall orientation selection | |
wall_orientation = st.selectbox( | |
"Wall Orientation", | |
options=["north", "east", "south", "west"], | |
key="new_wall_orientation_heating" | |
) | |
# Get material properties | |
material_data = ref_data.get_material_by_type("walls", wall_material) | |
u_value = material_data['u_value'] | |
# Add custom U-value input if custom material is selected | |
if wall_material == "custom_walls": | |
u_value = st.number_input( | |
"Custom U-Value (W/m²°C)", | |
value=1.0, | |
min_value=0.1, | |
max_value=5.0, | |
step=0.1, | |
key="custom_wall_u_value_heating" | |
) | |
# Store custom material in session state | |
if "custom_materials" not in st.session_state: | |
st.session_state.custom_materials = {} | |
st.session_state.custom_materials["walls"] = { | |
"name": "Custom Wall", | |
"u_value": u_value, | |
"r_value": 1.0 / u_value if u_value > 0 else 1.0, | |
"description": "Custom wall with user-defined properties" | |
} | |
with col2: | |
wall_area = st.number_input( | |
"Wall Area (m²)", | |
value=default_wall_area / 4, # Default to 1/4 of total wall area as a starting point | |
min_value=0.1, | |
step=0.1, | |
key="new_wall_area_heating" | |
) | |
st.write(f"Material U-Value: {u_value} W/m²°C") | |
st.write(f"Heat Loss: {u_value * wall_area * temp_diff:.2f} W") | |
# Add wall button | |
if st.button("Add Wall", key="add_wall_heating"): | |
new_wall = { | |
'name': wall_name if wall_name else f"Wall {len(st.session_state.heating_form_data['building_envelope']['walls']) + 1}", | |
'material_id': wall_material, | |
'area': wall_area, | |
'u_value': u_value, | |
'temp_diff': temp_diff, | |
'orientation': wall_orientation # Add orientation to wall data | |
} | |
st.session_state.heating_form_data['building_envelope']['walls'].append(new_wall) | |
st.experimental_rerun() | |
# Roof section | |
st.write("### Roof") | |
# Get roof material options from reference data | |
roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()} | |
# Add custom option | |
roof_material_options["custom_roofs"] = "Custom Roof (User-defined)" | |
col1, col2 = st.columns(2) | |
with col1: | |
roof_material = st.selectbox( | |
"Roof Material", | |
options=list(roof_material_options.keys()), | |
format_func=lambda x: roof_material_options[x], | |
index=list(roof_material_options.keys()).index(st.session_state.heating_form_data['building_envelope'].get('roof', {}).get('material_id', 'metal_deck_insulated')) if st.session_state.heating_form_data['building_envelope'].get('roof', {}).get('material_id') in roof_material_options else 0 | |
) | |
# Get material properties | |
material_data = ref_data.get_material_by_type("roofs", roof_material) | |
roof_u_value = material_data['u_value'] | |
# Add custom U-value input if custom material is selected | |
if roof_material == "custom_roofs": | |
roof_u_value = st.number_input( | |
"Custom Roof U-Value (W/m²°C)", | |
value=1.0, | |
min_value=0.1, | |
max_value=5.0, | |
step=0.1, | |
key="custom_roof_u_value_heating" | |
) | |
# Store custom material in session state | |
if "custom_materials" not in st.session_state: | |
st.session_state.custom_materials = {} | |
st.session_state.custom_materials["roofs"] = { | |
"name": "Custom Roof", | |
"u_value": roof_u_value, | |
"r_value": 1.0 / roof_u_value if roof_u_value > 0 else 1.0, | |
"description": "Custom roof with user-defined properties" | |
} | |
with col2: | |
roof_area = st.number_input( | |
"Roof Area (m²)", | |
value=float(st.session_state.heating_form_data['building_envelope'].get('roof', {}).get('area', default_roof_area)), | |
min_value=0.1, | |
step=0.1, | |
key="roof_area_heating" | |
) | |
st.write(f"Material U-Value: {roof_u_value} W/m²°C") | |
st.write(f"Heat Loss: {roof_u_value * roof_area * temp_diff:.2f} W") | |
# Save roof data | |
st.session_state.heating_form_data['building_envelope']['roof'] = { | |
'material_id': roof_material, | |
'area': roof_area, | |
'u_value': roof_u_value, | |
'temp_diff': temp_diff | |
} | |
# Floor section | |
st.write("### Floor") | |
# Get floor material options from reference data | |
floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()} | |
# Add custom option | |
floor_material_options["custom_floors"] = "Custom Floor (User-defined)" | |
col1, col2 = st.columns(2) | |
with col1: | |
floor_material = st.selectbox( | |
"Floor Material", | |
options=list(floor_material_options.keys()), | |
format_func=lambda x: floor_material_options[x], | |
index=list(floor_material_options.keys()).index(st.session_state.heating_form_data['building_envelope'].get('floor', {}).get('material_id', 'concrete_slab_ground')) if st.session_state.heating_form_data['building_envelope'].get('floor', {}).get('material_id') in floor_material_options else 0 | |
) | |
# Get material properties | |
material_data = ref_data.get_material_by_type("floors", floor_material) | |
floor_u_value = material_data['u_value'] | |
# Add custom U-value input if custom material is selected | |
if floor_material == "custom_floors": | |
floor_u_value = st.number_input( | |
"Custom Floor U-Value (W/m²°C)", | |
value=1.0, | |
min_value=0.1, | |
max_value=5.0, | |
step=0.1, | |
key="custom_floor_u_value_heating" | |
) | |
# Store custom material in session state | |
if "custom_materials" not in st.session_state: | |
st.session_state.custom_materials = {} | |
st.session_state.custom_materials["floors"] = { | |
"name": "Custom Floor", | |
"u_value": floor_u_value, | |
"r_value": 1.0 / floor_u_value if floor_u_value > 0 else 1.0, | |
"description": "Custom floor with user-defined properties" | |
} | |
with col2: | |
floor_area = st.number_input( | |
"Floor Area (m²)", | |
value=float(st.session_state.heating_form_data['building_envelope'].get('floor', {}).get('area', default_floor_area)), | |
min_value=0.1, | |
step=0.1, | |
key="floor_area_heating" | |
) | |
st.write(f"Material U-Value: {floor_u_value} W/m²°C") | |
st.write(f"Heat Loss: {floor_u_value * floor_area * temp_diff:.2f} W") | |
# Save floor data | |
st.session_state.heating_form_data['building_envelope']['floor'] = { | |
'material_id': floor_material, | |
'area': floor_area, | |
'u_value': floor_u_value, | |
'temp_diff': temp_diff | |
} | |
# Validate inputs | |
warnings = [] | |
# Check if walls are defined | |
if not st.session_state.heating_form_data['building_envelope']['walls']: | |
warnings.append(ValidationWarning( | |
"No walls defined", | |
"Add at least one wall to continue", | |
is_critical=False # Changed to non-critical to allow proceeding with warnings | |
)) | |
# Check if total wall area is reasonable | |
total_wall_area = sum(wall['area'] for wall in st.session_state.heating_form_data['building_envelope']['walls']) | |
expected_wall_area = 2 * (length + width) * height | |
if total_wall_area < expected_wall_area * 0.8 or total_wall_area > expected_wall_area * 1.2: | |
warnings.append(ValidationWarning( | |
"Unusual wall area", | |
f"Total wall area ({total_wall_area:.2f} m²) differs significantly from the expected area ({expected_wall_area:.2f} m²) based on building dimensions" | |
)) | |
# Check if roof area matches floor area | |
if abs(roof_area - floor_area) > 1.0: | |
warnings.append(ValidationWarning( | |
"Roof area doesn't match floor area", | |
"For a simple building, roof area should approximately match floor area" | |
)) | |
# Save warnings to session state | |
st.session_state.heating_warnings['building_envelope'] = warnings | |
# Display warnings if any | |
if warnings: | |
st.warning("Please review the following warnings:") | |
for warning in warnings: | |
st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else "")) | |
st.write(f" Suggestion: {warning.suggestion}") | |
# Mark this step as completed if there are no critical warnings | |
st.session_state.heating_completed['building_envelope'] = not any(w.is_critical for w in warnings) | |
# Navigation buttons | |
col1, col2 = st.columns([1, 1]) | |
with col1: | |
prev_button = st.button("← Back: Building Information", key="heating_building_envelope_prev") | |
if prev_button: | |
st.session_state.heating_active_tab = "building_info" | |
st.experimental_rerun() | |
with col2: | |
next_button = st.button("Next: Windows & Doors →", key="heating_building_envelope_next") | |
if next_button: | |
st.session_state.heating_active_tab = "windows" | |
st.experimental_rerun() | |
def windows_form(ref_data): | |
""" | |
Form for windows and doors information. | |
Args: | |
ref_data: Reference data object | |
""" | |
st.subheader("Windows & Doors") | |
st.write("Enter information about windows and doors.") | |
# Get temperature difference from building info | |
temp_diff = st.session_state.heating_form_data['building_info'].get('temp_diff', 16.5) | |
# Initialize windows data if not already in session state | |
if 'windows' not in st.session_state.heating_form_data['windows']: | |
st.session_state.heating_form_data['windows']['windows'] = [] | |
if 'doors' not in st.session_state.heating_form_data['windows']: | |
st.session_state.heating_form_data['windows']['doors'] = [] | |
# Windows section | |
st.write("### Windows") | |
# Get glass type options from reference data | |
glass_type_options = {glass_id: glass_data['name'] for glass_id, glass_data in ref_data.glass_types.items()} | |
# Display existing window entries | |
if st.session_state.heating_form_data['windows']['windows']: | |
st.write("Current windows:") | |
windows_df = pd.DataFrame(st.session_state.heating_form_data['windows']['windows']) | |
windows_df['Glass Type'] = windows_df['glass_type'].map(lambda x: glass_type_options.get(x, "Unknown")) | |
windows_df = windows_df[['name', 'orientation', 'Glass Type', 'area', 'u_value']] | |
windows_df.columns = ['Name', 'Orientation', 'Glass Type', 'Area (m²)', 'U-Value (W/m²°C)'] | |
st.dataframe(windows_df) | |
# Add new window form | |
st.write("Add a new window:") | |
col1, col2 = st.columns(2) | |
with col1: | |
window_name = st.text_input("Window Name", value="", key="new_window_name_heating") | |
orientation = st.selectbox( | |
"Orientation", | |
options=["north", "east", "south", "west", "horizontal"], | |
key="new_window_orientation_heating" | |
) | |
glass_type = st.selectbox( | |
"Glass Type", | |
options=list(glass_type_options.keys()), | |
format_func=lambda x: glass_type_options[x], | |
key="new_window_glass_type_heating" | |
) | |
# Get glass properties | |
glass_data = ref_data.get_glass_type(glass_type) | |
window_u_value = glass_data['u_value'] | |
with col2: | |
window_area = st.number_input( | |
"Window Area (m²)", | |
value=2.0, | |
min_value=0.1, | |
step=0.1, | |
key="new_window_area_heating" | |
) | |
st.write(f"Glass U-Value: {window_u_value} W/m²°C") | |
st.write(f"Heat Loss: {window_u_value * window_area * temp_diff:.2f} W") | |
# Add window button | |
if st.button("Add Window", key="add_window_heating"): | |
new_window = { | |
'name': window_name if window_name else f"Window {len(st.session_state.heating_form_data['windows']['windows']) + 1}", | |
'orientation': orientation, | |
'glass_type': glass_type, | |
'area': window_area, | |
'u_value': window_u_value, | |
'temp_diff': temp_diff | |
} | |
st.session_state.heating_form_data['windows']['windows'].append(new_window) | |
st.experimental_rerun() | |
# Doors section | |
st.write("### Doors") | |
# Display existing door entries | |
if st.session_state.heating_form_data['windows']['doors']: | |
st.write("Current doors:") | |
doors_df = pd.DataFrame(st.session_state.heating_form_data['windows']['doors']) | |
doors_df = doors_df[['name', 'type', 'area', 'u_value']] | |
doors_df.columns = ['Name', 'Type', 'Area (m²)', 'U-Value (W/m²°C)'] | |
st.dataframe(doors_df) | |
# Add new door form | |
st.write("Add a new door:") | |
col1, col2 = st.columns(2) | |
with col1: | |
door_name = st.text_input("Door Name", value="", key="new_door_name_heating") | |
door_type = st.selectbox( | |
"Door Type", | |
options=["Solid wood", "Hollow core", "Glass", "Insulated"], | |
key="new_door_type_heating" | |
) | |
# Set U-value based on door type | |
door_u_values = { | |
"Solid wood": 2.0, | |
"Hollow core": 2.5, | |
"Glass": 5.0, | |
"Insulated": 1.2 | |
} | |
door_u_value = door_u_values[door_type] | |
with col2: | |
door_area = st.number_input( | |
"Door Area (m²)", | |
value=2.0, | |
min_value=0.1, | |
step=0.1, | |
key="new_door_area_heating" | |
) | |
st.write(f"Door U-Value: {door_u_value} W/m²°C") | |
st.write(f"Heat Loss: {door_u_value * door_area * temp_diff:.2f} W") | |
# Add door button | |
if st.button("Add Door", key="add_door_heating"): | |
new_door = { | |
'name': door_name if door_name else f"Door {len(st.session_state.heating_form_data['windows']['doors']) + 1}", | |
'type': door_type, | |
'area': door_area, | |
'u_value': door_u_value, | |
'temp_diff': temp_diff | |
} | |
st.session_state.heating_form_data['windows']['doors'].append(new_door) | |
st.experimental_rerun() | |
# Validate inputs | |
warnings = [] | |
# Check if windows are defined | |
if not st.session_state.heating_form_data['windows']['windows']: | |
warnings.append(ValidationWarning( | |
"No windows defined", | |
"Add at least one window to continue" | |
)) | |
# Check window-to-wall ratio | |
if st.session_state.heating_form_data['windows']['windows']: | |
total_window_area = sum(window['area'] for window in st.session_state.heating_form_data['windows']['windows']) | |
total_wall_area = sum(wall['area'] for wall in st.session_state.heating_form_data['building_envelope']['walls']) | |
window_wall_ratio = total_window_area / total_wall_area if total_wall_area > 0 else 0 | |
if window_wall_ratio > 0.6: | |
warnings.append(ValidationWarning( | |
"High window-to-wall ratio", | |
f"Window-to-wall ratio is {window_wall_ratio:.2f}, which is unusually high. Typical ratios are 0.2-0.4." | |
)) | |
# Save warnings to session state | |
st.session_state.heating_warnings['windows'] = warnings | |
# Display warnings if any | |
if warnings: | |
st.warning("Please review the following warnings:") | |
for warning in warnings: | |
st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else "")) | |
st.write(f" Suggestion: {warning.suggestion}") | |
# Mark this step as completed if there are no critical warnings | |
st.session_state.heating_completed['windows'] = not any(w.is_critical for w in warnings) | |
# Navigation buttons | |
col1, col2 = st.columns([1, 1]) | |
with col1: | |
prev_button = st.button("← Back: Building Envelope", key="heating_windows_prev") | |
if prev_button: | |
st.session_state.heating_active_tab = "building_envelope" | |
st.experimental_rerun() | |
with col2: | |
next_button = st.button("Next: Ventilation →", key="heating_windows_next") | |
if next_button: | |
st.session_state.heating_active_tab = "ventilation" | |
st.experimental_rerun() | |
def ventilation_form(ref_data): | |
""" | |
Form for ventilation and infiltration information. | |
Args: | |
ref_data: Reference data object | |
""" | |
st.subheader("Ventilation & Infiltration") | |
st.write("Enter information about ventilation and infiltration rates.") | |
# Get building info | |
building_info = st.session_state.heating_form_data['building_info'] | |
volume = building_info.get('volume', 216.0) | |
temp_diff = building_info.get('temp_diff', 16.5) | |
# Initialize ventilation data if not already in session state | |
if 'infiltration' not in st.session_state.heating_form_data['ventilation']: | |
st.session_state.heating_form_data['ventilation']['infiltration'] = { | |
'air_changes': 0.5 | |
} | |
if 'ventilation' not in st.session_state.heating_form_data['ventilation']: | |
st.session_state.heating_form_data['ventilation']['ventilation'] = { | |
'type': 'natural', | |
'air_changes': 0.0 | |
} | |
# Initialize internal loads data if not already in session state | |
if 'internal_loads' not in st.session_state.heating_form_data: | |
st.session_state.heating_form_data['internal_loads'] = {} | |
if 'occupants' not in st.session_state.heating_form_data['internal_loads']: | |
st.session_state.heating_form_data['internal_loads']['occupants'] = { | |
'count': 4, | |
'activity_level': 'seated_resting' | |
} | |
if 'lighting' not in st.session_state.heating_form_data['internal_loads']: | |
st.session_state.heating_form_data['internal_loads']['lighting'] = { | |
'type': 'led', | |
'power_density': 5.0 # W/m² | |
} | |
if 'appliances' not in st.session_state.heating_form_data['internal_loads']: | |
st.session_state.heating_form_data['internal_loads']['appliances'] = { | |
'kitchen': True, | |
'living_room': True, | |
'bedroom': True, | |
'office': False | |
} | |
# Infiltration section | |
st.write("### Infiltration") | |
st.write("Infiltration is the unintended air leakage through the building envelope.") | |
infiltration_ach = st.slider( | |
"Infiltration Rate (air changes per hour)", | |
value=float(st.session_state.heating_form_data['ventilation']['infiltration'].get('air_changes', 0.5)), | |
min_value=0.1, | |
max_value=2.0, | |
step=0.1, | |
help="Typical values: 0.5 ACH for modern construction, 1.0 ACH for average construction, 1.5+ ACH for older buildings", | |
key="infiltration_ach_heating" | |
) | |
# Calculate infiltration heat loss | |
infiltration_heat_loss = 0.33 * volume * infiltration_ach * temp_diff | |
st.write(f"Infiltration heat loss: {infiltration_heat_loss:.2f} W") | |
# Save infiltration data | |
st.session_state.heating_form_data['ventilation']['infiltration'] = { | |
'air_changes': infiltration_ach, | |
'volume': volume, | |
'temp_diff': temp_diff, | |
'heat_loss': infiltration_heat_loss | |
} | |
# Ventilation section | |
st.write("### Ventilation") | |
st.write("Ventilation is the intentional introduction of outside air into the building.") | |
# Internal Loads section | |
st.write("### Internal Loads") | |
st.write("Internal loads are heat sources inside the building that reduce heating requirements.") | |
# Occupants section | |
st.write("#### Occupants") | |
col1, col2 = st.columns(2) | |
with col1: | |
occupant_count = st.number_input( | |
"Number of Occupants", | |
value=int(st.session_state.heating_form_data['internal_loads']['occupants'].get('count', 4)), | |
min_value=1, | |
step=1, | |
key="occupant_count_heating" | |
) | |
with col2: | |
# Get activity level options from reference data | |
activity_options = {act_id: act_data['name'] for act_id, act_data in ref_data.internal_loads['people'].items()} | |
activity_level = st.selectbox( | |
"Activity Level", | |
options=list(activity_options.keys()), | |
format_func=lambda x: activity_options[x], | |
index=list(activity_options.keys()).index(st.session_state.heating_form_data['internal_loads']['occupants'].get('activity_level', 'seated_resting')) if st.session_state.heating_form_data['internal_loads']['occupants'].get('activity_level') in activity_options else 0, | |
key="activity_level_heating" | |
) | |
# Get heat gain per person | |
activity_data = ref_data.get_internal_load('people', activity_level) | |
sensible_heat_pp = activity_data['sensible_heat'] | |
latent_heat_pp = activity_data['latent_heat'] | |
total_heat_pp = sensible_heat_pp + latent_heat_pp | |
st.write(f"Heat gain per person: {total_heat_pp} W ({sensible_heat_pp} W sensible + {latent_heat_pp} W latent)") | |
st.write(f"Total occupant heat gain: {total_heat_pp * occupant_count} W") | |
# Save occupants data | |
st.session_state.heating_form_data['internal_loads']['occupants'] = { | |
'count': occupant_count, | |
'activity_level': activity_level, | |
'sensible_heat_pp': sensible_heat_pp, | |
'latent_heat_pp': latent_heat_pp, | |
'total_heat_gain': total_heat_pp * occupant_count | |
} | |
# Lighting section | |
st.write("#### Lighting") | |
col1, col2 = st.columns(2) | |
with col1: | |
# Get lighting type options from reference data | |
lighting_options = {light_id: light_data['name'] for light_id, light_data in ref_data.internal_loads['lighting'].items()} | |
lighting_type = st.selectbox( | |
"Lighting Type", | |
options=list(lighting_options.keys()), | |
format_func=lambda x: lighting_options[x], | |
index=list(lighting_options.keys()).index(st.session_state.heating_form_data['internal_loads']['lighting'].get('type', 'led')) if st.session_state.heating_form_data['internal_loads']['lighting'].get('type') in lighting_options else 0, | |
key="lighting_type_heating" | |
) | |
with col2: | |
lighting_power_density = st.number_input( | |
"Lighting Power Density (W/m²)", | |
value=float(st.session_state.heating_form_data['internal_loads']['lighting'].get('power_density', 5.0)), | |
min_value=1.0, | |
max_value=20.0, | |
step=0.5, | |
help="Typical values: Residential 5-10 W/m², Office 10-15 W/m²", | |
key="lighting_power_density_heating" | |
) | |
# Get lighting heat factor | |
lighting_data = ref_data.get_internal_load('lighting', lighting_type) | |
lighting_heat_factor = lighting_data['heat_factor'] | |
# Calculate lighting heat gain | |
floor_area = st.session_state.heating_form_data['building_info'].get('floor_area', 80.0) | |
lighting_heat_gain = lighting_power_density * floor_area * lighting_heat_factor | |
st.write(f"Lighting heat factor: {lighting_heat_factor}") | |
st.write(f"Total lighting heat gain: {lighting_heat_gain:.2f} W") | |
# Save lighting data | |
st.session_state.heating_form_data['internal_loads']['lighting'] = { | |
'type': lighting_type, | |
'power_density': lighting_power_density, | |
'heat_factor': lighting_heat_factor, | |
'total_heat_gain': lighting_heat_gain | |
} | |
# Equipment section | |
st.write("#### Equipment") | |
st.write("Select the equipment present in your space:") | |
col1, col2 = st.columns(2) | |
with col1: | |
has_kitchen = st.checkbox( | |
"Kitchen Appliances", | |
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('kitchen', True), | |
help="Refrigerator, stove, microwave, etc.", | |
key="has_kitchen_heating" | |
) | |
has_living_room = st.checkbox( | |
"Living Room Equipment", | |
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('living_room', True), | |
help="TV, audio equipment, etc.", | |
key="has_living_room_heating" | |
) | |
with col2: | |
has_bedroom = st.checkbox( | |
"Bedroom Equipment", | |
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('bedroom', True), | |
help="TV, chargers, etc.", | |
key="has_bedroom_heating" | |
) | |
has_office = st.checkbox( | |
"Office Equipment", | |
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('office', False), | |
help="Computer, printer, etc.", | |
key="has_office_heating" | |
) | |
# Calculate equipment heat gain | |
equipment_watts = 0 | |
if has_kitchen: | |
equipment_watts += 1000 # Kitchen appliances | |
if has_living_room: | |
equipment_watts += 300 # Living room equipment | |
if has_bedroom: | |
equipment_watts += 150 # Bedroom equipment | |
if has_office: | |
equipment_watts += 450 # Office equipment | |
st.write(f"Total equipment heat gain: {equipment_watts} W") | |
# Save appliances data | |
st.session_state.heating_form_data['internal_loads']['appliances'] = { | |
'kitchen': has_kitchen, | |
'living_room': has_living_room, | |
'bedroom': has_bedroom, | |
'office': has_office, | |
'total_heat_gain': equipment_watts | |
} | |
# Calculate total internal heat gain | |
total_internal_gain = ( | |
st.session_state.heating_form_data['internal_loads']['occupants']['total_heat_gain'] + | |
st.session_state.heating_form_data['internal_loads']['lighting']['total_heat_gain'] + | |
st.session_state.heating_form_data['internal_loads']['appliances']['total_heat_gain'] | |
) | |
st.write(f"Total internal heat gain: {total_internal_gain:.2f} W") | |
# Save total internal gain | |
st.session_state.heating_form_data['internal_loads']['total_heat_gain'] = total_internal_gain | |
col1, col2 = st.columns(2) | |
with col1: | |
ventilation_type = st.selectbox( | |
"Ventilation Type", | |
options=["natural", "mechanical", "mixed"], | |
format_func=lambda x: x.capitalize(), | |
index=["natural", "mechanical", "mixed"].index(st.session_state.heating_form_data['ventilation']['ventilation'].get('type', 'natural')), | |
key="ventilation_type_heating" | |
) | |
with col2: | |
ventilation_ach = st.number_input( | |
"Ventilation Rate (air changes per hour)", | |
value=float(st.session_state.heating_form_data['ventilation']['ventilation'].get('air_changes', 0.0)), | |
min_value=0.0, | |
max_value=5.0, | |
step=0.1, | |
help="Typical values: 0.35-1.0 ACH for residential buildings", | |
key="ventilation_ach_heating" | |
) | |
# Calculate ventilation heat loss | |
ventilation_heat_loss = 0.33 * volume * ventilation_ach * temp_diff | |
st.write(f"Ventilation heat loss: {ventilation_heat_loss:.2f} W") | |
# Save ventilation data | |
st.session_state.heating_form_data['ventilation']['ventilation'] = { | |
'type': ventilation_type, | |
'air_changes': ventilation_ach, | |
'volume': volume, | |
'temp_diff': temp_diff, | |
'heat_loss': ventilation_heat_loss | |
} | |
# Calculate total ventilation and infiltration heat loss | |
total_ventilation_loss = infiltration_heat_loss + ventilation_heat_loss | |
st.info(f"Total Ventilation & Infiltration Heat Loss: {total_ventilation_loss:.2f} W") | |
# Save total ventilation loss | |
st.session_state.heating_form_data['ventilation']['total_loss'] = total_ventilation_loss | |
# Validate inputs | |
warnings = [] | |
# Check if infiltration rate is reasonable | |
if infiltration_ach < 0.3: | |
warnings.append(ValidationWarning( | |
"Low infiltration rate", | |
"Infiltration rate below 0.3 ACH is unusually low for most buildings." | |
)) | |
elif infiltration_ach > 1.5: | |
warnings.append(ValidationWarning( | |
"High infiltration rate", | |
"Infiltration rate above 1.5 ACH indicates a leaky building envelope." | |
)) | |
# Check if ventilation rate is reasonable | |
if ventilation_ach > 0 and ventilation_ach < 0.35: | |
warnings.append(ValidationWarning( | |
"Low ventilation rate", | |
"Ventilation rate below 0.35 ACH may not provide adequate fresh air." | |
)) | |
elif ventilation_ach > 2.0: | |
warnings.append(ValidationWarning( | |
"High ventilation rate", | |
"Ventilation rate above 2.0 ACH is unusually high for residential buildings." | |
)) | |
# Save warnings to session state | |
st.session_state.heating_warnings['ventilation'] = warnings | |
# Display warnings if any | |
if warnings: | |
st.warning("Please review the following warnings:") | |
for warning in warnings: | |
st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else "")) | |
st.write(f" Suggestion: {warning.suggestion}") | |
# Mark this step as completed if there are no critical warnings | |
st.session_state.heating_completed['ventilation'] = not any(w.is_critical for w in warnings) | |
# Navigation buttons | |
col1, col2 = st.columns([1, 1]) | |
with col1: | |
prev_button = st.button("← Back: Windows & Doors", key="heating_ventilation_prev") | |
if prev_button: | |
st.session_state.heating_active_tab = "windows" | |
st.experimental_rerun() | |
with col2: | |
next_button = st.button("Next: Occupancy →", key="heating_ventilation_next") | |
if next_button: | |
st.session_state.heating_active_tab = "occupancy" | |
st.experimental_rerun() | |
def occupancy_form(ref_data): | |
""" | |
Form for occupancy information. | |
Args: | |
ref_data: Reference data object | |
""" | |
st.subheader("Occupancy Information") | |
st.write("Enter information about occupancy patterns and heating degree days.") | |
# Get location from building info | |
location = st.session_state.heating_form_data['building_info'].get('location', 'sydney') | |
location_name = st.session_state.heating_form_data['building_info'].get('location_name', 'Sydney') | |
# Initialize occupancy data if not already in session state | |
if 'occupancy_type' not in st.session_state.heating_form_data['occupancy']: | |
st.session_state.heating_form_data['occupancy']['occupancy_type'] = 'continuous' | |
if 'heating_degree_days' not in st.session_state.heating_form_data['occupancy']: | |
# Get default HDD from reference data | |
calculator = HeatingLoadCalculator() | |
default_hdd = calculator.get_heating_degree_days(location) | |
st.session_state.heating_form_data['occupancy']['heating_degree_days'] = default_hdd | |
# Occupancy section | |
st.write("### Occupancy Pattern") | |
# Get occupancy options from reference data | |
occupancy_options = {occ_id: occ_data['name'] for occ_id, occ_data in ref_data.occupancy_factors.items()} | |
occupancy_type = st.selectbox( | |
"Occupancy Type", | |
options=list(occupancy_options.keys()), | |
format_func=lambda x: occupancy_options[x], | |
index=list(occupancy_options.keys()).index(st.session_state.heating_form_data['occupancy'].get('occupancy_type', 'continuous')) if st.session_state.heating_form_data['occupancy'].get('occupancy_type') in occupancy_options else 0, | |
help="Select the occupancy pattern that best describes how the building is used" | |
) | |
# Get occupancy factor | |
occupancy_data = ref_data.get_occupancy_factor(occupancy_type) | |
occupancy_factor = occupancy_data['factor'] | |
st.write(f"Occupancy correction factor: {occupancy_factor}") | |
st.write(f"Description: {occupancy_data['description']}") | |
# Save occupancy data | |
st.session_state.heating_form_data['occupancy']['occupancy_type'] = occupancy_type | |
st.session_state.heating_form_data['occupancy']['occupancy_factor'] = occupancy_factor | |
# Heating degree days section | |
st.write("### Heating Degree Days") | |
st.write("Heating degree days are used to estimate annual heating energy requirements.") | |
col1, col2 = st.columns(2) | |
with col1: | |
base_temp = st.selectbox( | |
"Base Temperature", | |
options=[18, 15.5, 12], | |
index=[18, 15.5, 12].index(st.session_state.heating_form_data['occupancy'].get('base_temp', 18)) if st.session_state.heating_form_data['occupancy'].get('base_temp') in [18, 15.5, 12] else 0, | |
help="Base temperature for heating degree days calculation" | |
) | |
with col2: | |
# Get default HDD from reference data | |
calculator = HeatingLoadCalculator() | |
default_hdd = calculator.get_heating_degree_days(location, base_temp) | |
heating_degree_days = st.number_input( | |
"Heating Degree Days", | |
value=float(st.session_state.heating_form_data['occupancy'].get('heating_degree_days', default_hdd)), | |
min_value=0.0, | |
step=10.0, | |
help=f"Default value for {location_name} at base {base_temp}°C: {default_hdd}" | |
) | |
st.write(f"Heating degree days represent the sum of daily temperature differences between the base temperature and the average daily temperature when it falls below the base temperature.") | |
# Save heating degree days data | |
st.session_state.heating_form_data['occupancy']['base_temp'] = base_temp | |
st.session_state.heating_form_data['occupancy']['heating_degree_days'] = heating_degree_days | |
# Validate inputs | |
warnings = [] | |
# Check if heating degree days are reasonable | |
if heating_degree_days == 0: | |
warnings.append(ValidationWarning( | |
"Zero heating degree days", | |
"With zero heating degree days, annual heating energy will be zero." | |
)) | |
elif heating_degree_days < 100 and base_temp == 18: | |
warnings.append(ValidationWarning( | |
"Very low heating degree days", | |
f"Heating degree days below 100 at base {base_temp}°C is unusually low for most locations." | |
)) | |
elif heating_degree_days > 3000: | |
warnings.append(ValidationWarning( | |
"Very high heating degree days", | |
"Heating degree days above 3000 is unusually high for most locations." | |
)) | |
# Save warnings to session state | |
st.session_state.heating_warnings['occupancy'] = warnings | |
# Display warnings if any | |
if warnings: | |
st.warning("Please review the following warnings:") | |
for warning in warnings: | |
st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else "")) | |
st.write(f" Suggestion: {warning.suggestion}") | |
# Mark this step as completed if there are no critical warnings | |
st.session_state.heating_completed['occupancy'] = not any(w.is_critical for w in warnings) | |
# Navigation buttons | |
col1, col2 = st.columns([1, 1]) | |
with col1: | |
prev_button = st.button("← Back: Ventilation", key="heating_occupancy_prev") | |
if prev_button: | |
st.session_state.heating_active_tab = "ventilation" | |
st.experimental_rerun() | |
with col2: | |
calculate_button = st.button("Calculate Results →", key="heating_occupancy_calculate") | |
if calculate_button: | |
# Calculate heating load | |
calculate_heating_load() | |
st.session_state.heating_active_tab = "results" | |
st.experimental_rerun() | |
def calculate_heating_load(): | |
"""Calculate heating load based on input data.""" | |
# Create calculator instance | |
calculator = HeatingLoadCalculator() | |
# Get form data | |
form_data = st.session_state.heating_form_data | |
# Prepare building components for calculation | |
building_components = [] | |
# Add walls | |
for wall in form_data['building_envelope'].get('walls', []): | |
building_components.append({ | |
'name': wall['name'], | |
'area': wall['area'], | |
'u_value': wall['u_value'], | |
'temp_diff': wall['temp_diff'] | |
}) | |
# Add roof | |
roof = form_data['building_envelope'].get('roof', {}) | |
if roof: | |
building_components.append({ | |
'name': 'Roof', | |
'area': roof['area'], | |
'u_value': roof['u_value'], | |
'temp_diff': roof['temp_diff'] | |
}) | |
# Add floor | |
floor = form_data['building_envelope'].get('floor', {}) | |
if floor: | |
building_components.append({ | |
'name': 'Floor', | |
'area': floor['area'], | |
'u_value': floor['u_value'], | |
'temp_diff': floor['temp_diff'] | |
}) | |
# Add windows | |
for window in form_data['windows'].get('windows', []): | |
building_components.append({ | |
'name': window['name'], | |
'area': window['area'], | |
'u_value': window['u_value'], | |
'temp_diff': window['temp_diff'] | |
}) | |
# Add doors | |
for door in form_data['windows'].get('doors', []): | |
building_components.append({ | |
'name': door['name'], | |
'area': door['area'], | |
'u_value': door['u_value'], | |
'temp_diff': door['temp_diff'] | |
}) | |
# Prepare infiltration data | |
infiltration = form_data['ventilation'].get('infiltration', {}) | |
ventilation = form_data['ventilation'].get('ventilation', {}) | |
infiltration_data = { | |
'volume': infiltration.get('volume', 0), | |
'air_changes': infiltration.get('air_changes', 0) + ventilation.get('air_changes', 0), | |
'temp_diff': infiltration.get('temp_diff', 0) | |
} | |
# Prepare internal loads data | |
internal_loads = None | |
if 'internal_loads' in form_data: | |
internal_loads = { | |
'num_people': form_data['internal_loads']['occupants'].get('count', 0), | |
'has_kitchen': form_data['internal_loads']['appliances'].get('kitchen', False), | |
'equipment_watts': form_data['internal_loads']['appliances'].get('total_heat_gain', 0) | |
} | |
# Calculate heating load | |
results = calculator.calculate_total_heating_load( | |
building_components=building_components, | |
infiltration=infiltration_data, | |
internal_gains=internal_loads | |
) | |
# Calculate annual heating requirement | |
location = form_data['building_info'].get('location', 'sydney') | |
occupancy_type = form_data['occupancy'].get('occupancy_type', 'continuous') | |
base_temp = form_data['occupancy'].get('base_temp', 18) | |
annual_results = calculator.calculate_annual_heating_requirement( | |
results['total_load'], | |
location, | |
occupancy_type, | |
base_temp | |
) | |
# Combine results | |
combined_results = {**results, **annual_results} | |
# Save results to session state | |
st.session_state.heating_results = combined_results | |
# Add timestamp | |
st.session_state.heating_results['timestamp'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
# Add building info | |
st.session_state.heating_results['building_info'] = form_data['building_info'] | |
return combined_results | |
def results_page(): | |
"""Display calculation results.""" | |
st.subheader("Heating Load Calculation Results") | |
# Check if results are available | |
if not st.session_state.heating_results: | |
st.warning("No calculation results available. Please complete the input forms and calculate results.") | |
return | |
# Get results | |
results = st.session_state.heating_results | |
# Display summary | |
st.write("### Summary") | |
col1, col2 = st.columns(2) | |
with col1: | |
st.metric("Total Heating Load", f"{results['total_load']:.2f} W") | |
# Convert to kW | |
total_load_kw = results['total_load'] / 1000 | |
st.metric("Total Heating Load", f"{total_load_kw:.2f} kW") | |
# Annual heating energy | |
st.metric("Annual Heating Energy", f"{results['annual_energy_kwh']:.2f} kWh") | |
with col2: | |
# Calculate heating load per area | |
floor_area = results['building_info'].get('floor_area', 80.0) | |
heating_load_per_area = results['total_load'] / floor_area | |
st.metric("Heating Load per Area", f"{heating_load_per_area:.2f} W/m²") | |
# Annual heating energy per area | |
annual_energy_per_area = results['annual_energy_kwh'] / floor_area | |
st.metric("Annual Heating Energy per Area", f"{annual_energy_per_area:.2f} kWh/m²") | |
# Equipment sizing recommendation | |
# Add 10% safety factor | |
recommended_size = total_load_kw * 1.1 | |
st.metric("Recommended Equipment Size", f"{recommended_size:.2f} kW") | |
# Display load breakdown | |
st.write("### Load Breakdown") | |
# Prepare data for pie chart | |
component_losses = results['component_losses'] | |
# Create pie chart for component losses | |
fig = px.pie( | |
values=list(component_losses.values()), | |
names=list(component_losses.keys()), | |
title="Heating Load Components", | |
color_discrete_sequence=px.colors.sequential.Viridis, | |
hole=0.4, # Create a donut chart for better readability | |
labels={'label': 'Component', 'value': 'Heat Loss (W)'} | |
) | |
# Improve layout and formatting | |
fig.update_traces( | |
textposition='inside', | |
textinfo='percent+label', | |
hoverinfo='label+percent+value', | |
marker=dict(line=dict(color='#FFFFFF', width=2)) | |
) | |
# Improve layout | |
fig.update_layout( | |
legend_title_text='Building Components', | |
font=dict(size=14), | |
title_font=dict(size=18), | |
title_x=0.5, # Center the title | |
margin=dict(t=50, b=50, l=50, r=50) | |
) | |
st.plotly_chart(fig) | |
# Display load components in a table | |
load_components = { | |
'Conduction (Building Envelope)': results['total_conduction_loss'] - results.get('infiltration_loss', 0), | |
'Infiltration & Ventilation': results.get('infiltration_loss', 0) | |
} | |
# Add internal gains and solar gains if available | |
if 'internal_gain' in results and results['internal_gain'] > 0: | |
load_components['Internal Gains (reduction)'] = -results['internal_gain'] | |
if 'wall_solar_gain' in results and results['wall_solar_gain'] > 0: | |
load_components['Solar Gains (reduction)'] = -results['wall_solar_gain'] | |
load_df = pd.DataFrame({ | |
'Component': list(load_components.keys()), | |
'Load (W)': list(load_components.values()), | |
'Percentage (%)': [abs(value) / results['total_load'] * 100 for value in load_components.values()] | |
}) | |
st.dataframe(load_df.style.format({ | |
'Load (W)': '{:.2f}', | |
'Percentage (%)': '{:.2f}' | |
})) | |
# Display detailed results | |
st.write("### Detailed Results") | |
# Create tabs for different result sections | |
tabs = st.tabs([ | |
"Building Components", | |
"Ventilation", | |
"Annual Energy" | |
]) | |
with tabs[0]: | |
st.subheader("Building Component Heat Losses") | |
# Create dataframe from component losses | |
components_data = [] | |
for name, loss in component_losses.items(): | |
# Find the component in the original data to get area and U-value | |
component = None | |
for comp in st.session_state.heating_form_data['building_envelope'].get('walls', []): | |
if comp['name'] == name: | |
component = comp | |
break | |
if name == 'Roof': | |
component = st.session_state.heating_form_data['building_envelope'].get('roof', {}) | |
elif name == 'Floor': | |
component = st.session_state.heating_form_data['building_envelope'].get('floor', {}) | |
# Check windows and doors | |
if not component: | |
for window in st.session_state.heating_form_data['windows'].get('windows', []): | |
if window['name'] == name: | |
component = window | |
break | |
if not component: | |
for door in st.session_state.heating_form_data['windows'].get('doors', []): | |
if door['name'] == name: | |
component = door | |
break | |
if component: | |
components_data.append({ | |
'Component': name, | |
'Area (m²)': component.get('area', 0), | |
'U-Value (W/m²°C)': component.get('u_value', 0), | |
'Temperature Difference (°C)': component.get('temp_diff', 0), | |
'Heat Loss (W)': loss | |
}) | |
else: | |
components_data.append({ | |
'Component': name, | |
'Area (m²)': 0, | |
'U-Value (W/m²°C)': 0, | |
'Temperature Difference (°C)': 0, | |
'Heat Loss (W)': loss | |
}) | |
# Create dataframe | |
components_df = pd.DataFrame(components_data) | |
# Display table | |
st.dataframe(components_df.style.format({ | |
'Area (m²)': '{:.2f}', | |
'U-Value (W/m²°C)': '{:.2f}', | |
'Temperature Difference (°C)': '{:.2f}', | |
'Heat Loss (W)': '{:.2f}' | |
})) | |
# Create bar chart | |
fig = px.bar( | |
components_df, | |
x='Component', | |
y='Heat Loss (W)', | |
title="Heat Loss by Building Component", | |
color='Component', | |
color_discrete_sequence=px.colors.sequential.Viridis, | |
text='Heat Loss (W)' | |
) | |
# Improve layout and formatting | |
fig.update_traces( | |
texttemplate='%{text:.1f} W', | |
textposition='outside', | |
hovertemplate='<b>%{x}</b><br>Heat Loss: %{y:.1f} W<extra></extra>' | |
) | |
# Improve layout | |
fig.update_layout( | |
xaxis_title="Building Component", | |
yaxis_title="Heat Loss (W)", | |
font=dict(size=14), | |
title_font=dict(size=18), | |
title_x=0.5, # Center the title | |
margin=dict(t=50, b=50, l=50, r=50), | |
xaxis={'categoryorder':'total descending'} # Sort by highest heat loss | |
) | |
st.plotly_chart(fig) | |
with tabs[1]: | |
st.subheader("Ventilation & Infiltration Heat Losses") | |
# Get ventilation data | |
ventilation_data = st.session_state.heating_form_data['ventilation'] | |
# Create dataframe | |
ventilation_df = pd.DataFrame([ | |
{ | |
'Source': 'Infiltration', | |
'Air Changes per Hour': ventilation_data['infiltration']['air_changes'], | |
'Volume (m³)': ventilation_data['infiltration']['volume'], | |
'Temperature Difference (°C)': ventilation_data['infiltration']['temp_diff'], | |
'Heat Loss (W)': ventilation_data['infiltration']['heat_loss'] | |
}, | |
{ | |
'Source': 'Ventilation', | |
'Air Changes per Hour': ventilation_data['ventilation']['air_changes'], | |
'Volume (m³)': ventilation_data['ventilation']['volume'], | |
'Temperature Difference (°C)': ventilation_data['ventilation']['temp_diff'], | |
'Heat Loss (W)': ventilation_data['ventilation']['heat_loss'] | |
} | |
]) | |
# Display table | |
st.dataframe(ventilation_df.style.format({ | |
'Air Changes per Hour': '{:.2f}', | |
'Volume (m³)': '{:.2f}', | |
'Temperature Difference (°C)': '{:.2f}', | |
'Heat Loss (W)': '{:.2f}' | |
})) | |
# Create bar chart | |
fig = px.bar( | |
ventilation_df, | |
x='Source', | |
y='Heat Loss (W)', | |
title="Ventilation & Infiltration Heat Losses", | |
color='Source', | |
color_discrete_sequence=px.colors.sequential.Plasma, | |
text='Heat Loss (W)' | |
) | |
# Improve layout and formatting | |
fig.update_traces( | |
texttemplate='%{text:.1f} W', | |
textposition='outside', | |
hovertemplate='<b>%{x}</b><br>Heat Loss: %{y:.1f} W<br>Air Changes: %{customdata[0]:.2f} ACH<extra></extra>' | |
) | |
# Add custom data for hover | |
fig.update_traces(customdata=ventilation_df[['Air Changes per Hour']]) | |
# Improve layout | |
fig.update_layout( | |
xaxis_title="Ventilation Source", | |
yaxis_title="Heat Loss (W)", | |
font=dict(size=14), | |
title_font=dict(size=18), | |
title_x=0.5, # Center the title | |
margin=dict(t=50, b=50, l=50, r=50) | |
) | |
st.plotly_chart(fig) | |
with tabs[2]: | |
st.subheader("Annual Heating Energy") | |
# Get occupancy data | |
occupancy_data = st.session_state.heating_form_data['occupancy'] | |
# Create dataframe | |
annual_data = pd.DataFrame([ | |
{ | |
'Parameter': 'Heating Degree Days', | |
'Value': results['heating_degree_days'], | |
'Unit': 'HDD' | |
}, | |
{ | |
'Parameter': 'Base Temperature', | |
'Value': occupancy_data['base_temp'], | |
'Unit': '°C' | |
}, | |
{ | |
'Parameter': 'Occupancy Type', | |
'Value': occupancy_data['occupancy_type'].capitalize(), | |
'Unit': '' | |
}, | |
{ | |
'Parameter': 'Correction Factor', | |
'Value': results['correction_factor'], | |
'Unit': '' | |
}, | |
{ | |
'Parameter': 'Annual Heating Energy', | |
'Value': results['annual_energy_kwh'], | |
'Unit': 'kWh' | |
}, | |
{ | |
'Parameter': 'Annual Heating Energy', | |
'Value': results['annual_energy_mj'], | |
'Unit': 'MJ' | |
} | |
]) | |
# Display table | |
st.dataframe(annual_data.style.format({ | |
'Value': lambda x: f"{x:.2f}" if isinstance(x, (int, float)) else str(x) | |
})) | |
# Create bar chart for monthly distribution (estimated) | |
# This is a simplified distribution based on heating degree days | |
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] | |
# Get location | |
location = st.session_state.heating_form_data['building_info'].get('location', 'sydney') | |
# Simplified monthly distribution factors based on hemisphere | |
# Southern hemisphere: winter is June-August | |
# Northern hemisphere: winter is December-February | |
southern_hemisphere = ['sydney', 'melbourne', 'brisbane', 'perth', 'adelaide', 'hobart', 'darwin', 'canberra', 'mildura'] | |
if location.lower() in southern_hemisphere: | |
# Southern hemisphere distribution | |
monthly_factors = [0.02, 0.01, 0.03, 0.08, 0.12, 0.16, 0.18, 0.16, 0.12, 0.08, 0.03, 0.01] | |
else: | |
# Northern hemisphere distribution | |
monthly_factors = [0.18, 0.16, 0.12, 0.08, 0.03, 0.01, 0.01, 0.01, 0.03, 0.08, 0.12, 0.17] | |
# Calculate monthly energy | |
monthly_energy = [results['annual_energy_kwh'] * factor for factor in monthly_factors] | |
# Create dataframe | |
monthly_df = pd.DataFrame({ | |
'Month': months, | |
'Energy (kWh)': monthly_energy | |
}) | |
# Create bar chart | |
fig = px.bar( | |
monthly_df, | |
x='Month', | |
y='Energy (kWh)', | |
title="Estimated Monthly Heating Energy Distribution", | |
color_discrete_sequence=['indianred'] | |
) | |
st.plotly_chart(fig) | |
# Export options | |
st.write("### Export Options") | |
col1, col2 = st.columns(2) | |
with col1: | |
if st.button("Export Results as CSV", key="export_csv_heating"): | |
# Create a CSV file with results | |
csv_data = export_data(st.session_state.heating_form_data, st.session_state.heating_results, format='csv') | |
# Provide download link | |
st.download_button( | |
label="Download CSV", | |
data=csv_data, | |
file_name=f"heating_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", | |
mime="text/csv" | |
) | |
with col2: | |
if st.button("Export Results as JSON", key="export_json_heating"): | |
# Create a JSON file with results | |
json_data = export_data(st.session_state.heating_form_data, st.session_state.heating_results, format='json') | |
# Provide download link | |
st.download_button( | |
label="Download JSON", | |
data=json_data, | |
file_name=f"heating_load_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", | |
mime="application/json" | |
) | |
# Navigation buttons | |
col1, col2 = st.columns([1, 1]) | |
with col1: | |
prev_button = st.button("← Back: Occupancy", key="heating_results_prev") | |
if prev_button: | |
st.session_state.heating_active_tab = "occupancy" | |
st.experimental_rerun() | |
with col2: | |
recalculate_button = st.button("Recalculate", key="heating_results_recalculate") | |
if recalculate_button: | |
# Recalculate heating load | |
calculate_heating_load() | |
st.experimental_rerun() | |
def heating_calculator(): | |
"""Main function for the heating load calculator page.""" | |
st.title("Heating Load Calculator") | |
# Initialize reference data | |
ref_data = ReferenceData() | |
# Initialize session state | |
load_session_state() | |
# Initialize active tab if not already set | |
if 'heating_active_tab' not in st.session_state: | |
st.session_state.heating_active_tab = "building_info" | |
# Create tabs for different steps | |
tabs = st.tabs([ | |
"1. Building Information", | |
"2. Building Envelope", | |
"3. Windows & Doors", | |
"4. Ventilation", | |
"5. Occupancy", | |
"6. Results" | |
]) | |
# Add direct navigation buttons at the top | |
st.write("### Navigation") | |
st.write("Click on any button below to navigate directly to that section:") | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
if st.button("1. Building Information", key="direct_nav_heating_info"): | |
st.session_state.heating_active_tab = "building_info" | |
st.experimental_rerun() | |
if st.button("2. Building Envelope", key="direct_nav_heating_envelope"): | |
st.session_state.heating_active_tab = "building_envelope" | |
st.experimental_rerun() | |
with col2: | |
if st.button("3. Windows & Doors", key="direct_nav_heating_windows"): | |
st.session_state.heating_active_tab = "windows" | |
st.experimental_rerun() | |
if st.button("4. Ventilation", key="direct_nav_heating_ventilation"): | |
st.session_state.heating_active_tab = "ventilation" | |
st.experimental_rerun() | |
with col3: | |
if st.button("5. Occupancy", key="direct_nav_heating_occupancy"): | |
st.session_state.heating_active_tab = "occupancy" | |
st.experimental_rerun() | |
if st.button("6. Results", key="direct_nav_heating_results"): | |
# Only enable if all previous steps are completed | |
if all(st.session_state.heating_completed.values()): | |
st.session_state.heating_active_tab = "results" | |
st.experimental_rerun() | |
else: | |
st.warning("Please complete all previous steps before viewing results.") | |
# Display the active tab | |
with tabs[0]: | |
if st.session_state.heating_active_tab == "building_info": | |
building_info_form(ref_data) | |
with tabs[1]: | |
if st.session_state.heating_active_tab == "building_envelope": | |
building_envelope_form(ref_data) | |
with tabs[2]: | |
if st.session_state.heating_active_tab == "windows": | |
windows_form(ref_data) | |
with tabs[3]: | |
if st.session_state.heating_active_tab == "ventilation": | |
ventilation_form(ref_data) | |
with tabs[4]: | |
if st.session_state.heating_active_tab == "occupancy": | |
occupancy_form(ref_data) | |
with tabs[5]: | |
if st.session_state.heating_active_tab == "results": | |
results_page() | |
if __name__ == "__main__": | |
heating_calculator() | |