Upload 23 files
Browse files- README.md +48 -61
- __pycache__/cooling_load.cpython-310.pyc +0 -0
- __pycache__/reference_data.cpython-310.pyc +0 -0
- app.py +30 -133
- cooling_load.py +319 -162
- documentation/enhancements.md +98 -0
- documentation/user_guide.md +93 -0
- heating_load.py +105 -4
- integrate_enhancements.py +66 -0
- pages/cooling_calculator.py +167 -10
- pages/heating_calculator.py +394 -10
- reference_data.py +15 -0
- requirements.txt +4 -4
- tests/__pycache__/test_enhancements.cpython-310.pyc +0 -0
- tests/test_enhancements.py +261 -0
- utils/__pycache__/scenario_manager.cpython-310.pyc +0 -0
- utils/scenario_integration.py +292 -0
- utils/scenario_manager.py +446 -0
- utils/validation.py +4 -4
README.md
CHANGED
|
@@ -1,66 +1,53 @@
|
|
| 1 |
-
|
| 2 |
-
title: HVAC Load Calculator
|
| 3 |
-
emoji: 🔥❄️
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: streamlit
|
| 7 |
-
sdk_version: 1.32.0
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
---
|
| 11 |
|
| 12 |
-
|
|
|
|
| 13 |
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
- **Smart Validation**: Proceed with warnings rather than blocking progress
|
| 23 |
-
- **Downloadable Data**: Export results for student assignments
|
| 24 |
-
- **ASHRAE Method**: Implementation based on industry-standard calculation methods
|
| 25 |
-
- **Extensible Design**: Framework for adding other calculation methods or locations
|
| 26 |
-
|
| 27 |
-
## How to Use
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
1. Select either the Cooling Load Calculator or Heating Load Calculator from the sidebar
|
| 30 |
-
2. Fill in the
|
| 31 |
-
3.
|
| 32 |
-
4.
|
| 33 |
-
5.
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
-
|
| 40 |
-
-
|
| 41 |
-
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
- Conduction through building envelope
|
| 49 |
-
- Infiltration and ventilation
|
| 50 |
-
- Annual heating energy requirements based on heating degree days
|
| 51 |
-
|
| 52 |
-
## Technical Details
|
| 53 |
-
|
| 54 |
-
- Built with Python and Streamlit
|
| 55 |
-
- Modular design for extensibility
|
| 56 |
-
- Comprehensive reference data based on ASHRAE standards
|
| 57 |
-
- Visualization using Plotly
|
| 58 |
-
- Data export in CSV and JSON formats
|
| 59 |
-
|
| 60 |
-
## Educational Purpose
|
| 61 |
-
|
| 62 |
-
This tool is designed for educational purposes to help students understand the factors that influence HVAC load calculations. It provides a practical way to apply theoretical knowledge and see how different building parameters affect heating and cooling requirements.
|
| 63 |
-
|
| 64 |
-
## Acknowledgements
|
| 65 |
-
|
| 66 |
-
Based on ASHRAE calculation methods for heating and cooling loads.
|
|
|
|
| 1 |
+
# HVAC Load Calculator - Enhanced Version
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
## Overview
|
| 4 |
+
This enhanced HVAC Load Calculator is a comprehensive tool for calculating cooling and heating loads in buildings. It provides a user-friendly interface for students, professionals, and HVAC enthusiasts to perform accurate load calculations without manual calculations.
|
| 5 |
|
| 6 |
+
## Enhancements
|
| 7 |
+
This version includes several enhancements to the original calculator:
|
| 8 |
|
| 9 |
+
1. **Fixed Window Shading Calculation** - Resolved the error when using external awning shading options
|
| 10 |
+
2. **Scenario Comparison Functionality** - Added ability to save calculation results as scenarios and compare multiple scenarios
|
| 11 |
+
3. **Customizable Appliances** - Users can specify quantities for both residential and commercial appliances and add custom heat sources
|
| 12 |
+
4. **Component Editing** - Users can edit or delete walls, windows, and doors after adding them
|
| 13 |
+
5. **Separate Internal Loads Tab** - Reorganized the heating calculator to match the cooling calculator structure
|
| 14 |
+
6. **Attribution Information** - Added proper attribution to Dr. Majed Abuseif and purpose statement
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
## Features
|
| 17 |
+
- Separate calculators for cooling and heating loads
|
| 18 |
+
- Step-by-step input forms with validation
|
| 19 |
+
- Reference data for materials and locations
|
| 20 |
+
- Visual results with charts and tables
|
| 21 |
+
- Scenario comparison for analyzing different building configurations
|
| 22 |
+
- Downloadable input data and results
|
| 23 |
+
|
| 24 |
+
## Getting Started
|
| 25 |
+
1. Install the required dependencies:
|
| 26 |
+
```
|
| 27 |
+
pip install -r requirements.txt
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
2. Run the application:
|
| 31 |
+
```
|
| 32 |
+
streamlit run app.py
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## Usage
|
| 36 |
1. Select either the Cooling Load Calculator or Heating Load Calculator from the sidebar
|
| 37 |
+
2. Fill in the building information and component details
|
| 38 |
+
3. Add walls, windows, doors, and internal loads as needed
|
| 39 |
+
4. View the calculation results
|
| 40 |
+
5. Save scenarios for future comparison
|
| 41 |
+
6. Use the Scenario Comparison page to compare different building configurations
|
| 42 |
+
|
| 43 |
+
## Documentation
|
| 44 |
+
See the documentation folder for detailed information about:
|
| 45 |
+
- Application structure
|
| 46 |
+
- Calculation methods
|
| 47 |
+
- Reference data
|
| 48 |
+
- Enhancement details
|
| 49 |
+
|
| 50 |
+
## Credits
|
| 51 |
+
Created by Dr. Majed Abuseif
|
| 52 |
+
|
| 53 |
+
This tool was created to facilitate HVAC calculation and understanding for Deakin University students, but has been enhanced to cover wider aspects to allow professionals and energy and HVAC enthusiasts to use it.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__pycache__/cooling_load.cpython-310.pyc
ADDED
|
Binary file (9.87 kB). View file
|
|
|
__pycache__/reference_data.cpython-310.pyc
ADDED
|
Binary file (12 kB). View file
|
|
|
app.py
CHANGED
|
@@ -1,160 +1,57 @@
|
|
| 1 |
"""
|
| 2 |
-
Main application
|
| 3 |
|
| 4 |
-
This
|
| 5 |
-
It sets up the Streamlit interface and navigation between different pages.
|
| 6 |
"""
|
| 7 |
|
| 8 |
import streamlit as st
|
| 9 |
-
import
|
| 10 |
-
import
|
| 11 |
from pathlib import Path
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
# Add the
|
| 14 |
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
|
| 16 |
-
# Import
|
| 17 |
from pages.cooling_calculator import cooling_calculator
|
| 18 |
from pages.heating_calculator import heating_calculator
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
#
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
)
|
| 27 |
|
| 28 |
-
# Define main function
|
| 29 |
def main():
|
| 30 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
# Add
|
| 33 |
-
st.markdown("""
|
| 34 |
-
|
| 35 |
-
.main-header {
|
| 36 |
-
font-size: 2.5rem;
|
| 37 |
-
color: #1E88E5;
|
| 38 |
-
text-align: center;
|
| 39 |
-
margin-bottom: 1rem;
|
| 40 |
-
}
|
| 41 |
-
.sub-header {
|
| 42 |
-
font-size: 1.5rem;
|
| 43 |
-
color: #424242;
|
| 44 |
-
margin-bottom: 1rem;
|
| 45 |
-
}
|
| 46 |
-
.info-box {
|
| 47 |
-
background-color: #E3F2FD;
|
| 48 |
-
padding: 1rem;
|
| 49 |
-
border-radius: 0.5rem;
|
| 50 |
-
margin-bottom: 1rem;
|
| 51 |
-
}
|
| 52 |
-
</style>
|
| 53 |
-
""", unsafe_allow_html=True)
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
st.sidebar.image("https://img.icons8.com/fluency/96/air-conditioner.png", width=100)
|
| 58 |
|
| 59 |
-
#
|
| 60 |
page = st.sidebar.radio(
|
| 61 |
"Select Calculator",
|
| 62 |
-
["
|
| 63 |
)
|
| 64 |
|
| 65 |
-
|
| 66 |
-
if page == "Home":
|
| 67 |
-
display_home_page()
|
| 68 |
-
elif page == "Cooling Load Calculator":
|
| 69 |
cooling_calculator()
|
| 70 |
elif page == "Heating Load Calculator":
|
| 71 |
heating_calculator()
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
st.sidebar.markdown("---")
|
| 75 |
-
st.sidebar.info(
|
| 76 |
-
"HVAC Load Calculator v1.0\n\n"
|
| 77 |
-
"Based on ASHRAE calculation methods\n\n"
|
| 78 |
-
"© 2025"
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def display_home_page():
|
| 83 |
-
"""Display the home page."""
|
| 84 |
-
|
| 85 |
-
st.markdown('<h1 class="main-header">HVAC Load Calculator</h1>', unsafe_allow_html=True)
|
| 86 |
-
st.markdown('<h2 class="sub-header">A Modern Tool for HVAC Design</h2>', unsafe_allow_html=True)
|
| 87 |
-
|
| 88 |
-
# Introduction
|
| 89 |
-
st.markdown("""
|
| 90 |
-
<div class="info-box">
|
| 91 |
-
<p>Welcome to the HVAC Load Calculator! This tool helps you calculate cooling and heating loads for buildings
|
| 92 |
-
using the ASHRAE method. It's designed for educational purposes to help students understand the factors
|
| 93 |
-
that influence HVAC load calculations.</p>
|
| 94 |
-
</div>
|
| 95 |
-
""", unsafe_allow_html=True)
|
| 96 |
-
|
| 97 |
-
# Features
|
| 98 |
-
st.markdown("### Features")
|
| 99 |
-
|
| 100 |
-
col1, col2 = st.columns(2)
|
| 101 |
-
|
| 102 |
-
with col1:
|
| 103 |
-
st.markdown("""
|
| 104 |
-
#### Cooling Load Calculator
|
| 105 |
-
- Calculate sensible and latent cooling loads
|
| 106 |
-
- Account for conduction, solar radiation, infiltration, and internal gains
|
| 107 |
-
- Visualize load components with charts and tables
|
| 108 |
-
- Export results for assignments
|
| 109 |
-
""")
|
| 110 |
-
|
| 111 |
-
with col2:
|
| 112 |
-
st.markdown("""
|
| 113 |
-
#### Heating Load Calculator
|
| 114 |
-
- Calculate peak heating loads
|
| 115 |
-
- Account for conduction, infiltration, and ventilation
|
| 116 |
-
- Estimate annual heating energy requirements
|
| 117 |
-
- Visualize load components with charts and tables
|
| 118 |
-
""")
|
| 119 |
-
|
| 120 |
-
# How to use
|
| 121 |
-
st.markdown("### How to Use")
|
| 122 |
-
st.markdown("""
|
| 123 |
-
1. Select either the Cooling Load Calculator or Heating Load Calculator from the sidebar
|
| 124 |
-
2. Fill in the required information in each step
|
| 125 |
-
3. Review any warnings that appear (you can proceed with warnings)
|
| 126 |
-
4. Calculate results and analyze the output
|
| 127 |
-
5. Export results for your assignments
|
| 128 |
-
""")
|
| 129 |
-
|
| 130 |
-
# Reference data
|
| 131 |
-
st.markdown("### Reference Data")
|
| 132 |
-
st.markdown("""
|
| 133 |
-
The calculator includes reference data for:
|
| 134 |
-
- Building materials (walls, roofs, floors)
|
| 135 |
-
- Glass types and shading coefficients
|
| 136 |
-
- Climate data for various locations
|
| 137 |
-
- Occupancy patterns and internal gains
|
| 138 |
-
|
| 139 |
-
This data is based on ASHRAE standards and guidelines.
|
| 140 |
-
""")
|
| 141 |
-
|
| 142 |
-
# Get started button
|
| 143 |
-
col1, col2, col3 = st.columns([1, 2, 1])
|
| 144 |
-
with col2:
|
| 145 |
-
st.markdown("### Get Started")
|
| 146 |
-
cooling_button = st.button("Go to Cooling Load Calculator")
|
| 147 |
-
heating_button = st.button("Go to Heating Load Calculator")
|
| 148 |
-
|
| 149 |
-
if cooling_button:
|
| 150 |
-
st.session_state.page = "Cooling Load Calculator"
|
| 151 |
-
st.experimental_rerun()
|
| 152 |
-
|
| 153 |
-
if heating_button:
|
| 154 |
-
st.session_state.page = "Heating Load Calculator"
|
| 155 |
-
st.experimental_rerun()
|
| 156 |
-
|
| 157 |
|
| 158 |
-
# Run the application
|
| 159 |
if __name__ == "__main__":
|
| 160 |
main()
|
|
|
|
| 1 |
"""
|
| 2 |
+
Main application module for the HVAC Load Calculator.
|
| 3 |
|
| 4 |
+
This module provides the main entry point and navigation for the HVAC calculator application.
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
import streamlit as st
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import matplotlib.pyplot as plt
|
| 10 |
from pathlib import Path
|
| 11 |
+
import sys
|
| 12 |
+
import os
|
| 13 |
|
| 14 |
+
# Add the current directory to the path
|
| 15 |
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
|
| 17 |
+
# Import calculator modules
|
| 18 |
from pages.cooling_calculator import cooling_calculator
|
| 19 |
from pages.heating_calculator import heating_calculator
|
| 20 |
+
from reference_data import ReferenceData
|
| 21 |
+
from utils.scenario_integration import scenario_comparison_page
|
| 22 |
|
| 23 |
+
# Dictionary of available pages
|
| 24 |
+
pages = {
|
| 25 |
+
"cooling_calculator": cooling_calculator,
|
| 26 |
+
"heating_calculator": heating_calculator,
|
| 27 |
+
"scenario_comparison": scenario_comparison_page
|
| 28 |
+
}
|
|
|
|
| 29 |
|
|
|
|
| 30 |
def main():
|
| 31 |
+
"""
|
| 32 |
+
Main function to run the HVAC Load Calculator application.
|
| 33 |
+
"""
|
| 34 |
+
st.sidebar.title("HVAC Load Calculator")
|
| 35 |
|
| 36 |
+
# Add attribution
|
| 37 |
+
st.sidebar.markdown("""
|
| 38 |
+
**Created by:** Dr. Majed Abuseif
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
This tool was created to facilitate HVAC calculation and understanding for Deakin University students, but has been enhanced to cover wider aspects to allow professionals and energy and HVAC enthusiasts to use it.
|
| 41 |
+
""")
|
|
|
|
| 42 |
|
| 43 |
+
# Add page selection
|
| 44 |
page = st.sidebar.radio(
|
| 45 |
"Select Calculator",
|
| 46 |
+
["Cooling Load Calculator", "Heating Load Calculator", "Scenario Comparison"]
|
| 47 |
)
|
| 48 |
|
| 49 |
+
if page == "Cooling Load Calculator":
|
|
|
|
|
|
|
|
|
|
| 50 |
cooling_calculator()
|
| 51 |
elif page == "Heating Load Calculator":
|
| 52 |
heating_calculator()
|
| 53 |
+
elif page == "Scenario Comparison":
|
| 54 |
+
scenario_comparison_page()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
|
|
|
| 56 |
if __name__ == "__main__":
|
| 57 |
main()
|
cooling_load.py
CHANGED
|
@@ -1,42 +1,176 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
|
| 4 |
-
This module
|
| 5 |
-
|
| 6 |
"""
|
| 7 |
|
| 8 |
-
import numpy as np
|
| 9 |
-
import pandas as pd
|
| 10 |
-
|
| 11 |
-
|
| 12 |
class CoolingLoadCalculator:
|
| 13 |
"""
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
-
|
| 17 |
-
def __init__(self):
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
self.
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
"""
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
Args:
|
| 31 |
-
area (float): Area of the
|
| 32 |
u_value (float): U-value of the component in W/m²°C
|
| 33 |
-
temp_diff (float): Temperature difference (
|
| 34 |
|
| 35 |
Returns:
|
| 36 |
float: Heat gain in Watts
|
| 37 |
"""
|
| 38 |
return area * u_value * temp_diff
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
def calculate_solar_heat_gain(self, area, shgf, shade_factor=1.0):
|
| 41 |
"""
|
| 42 |
Calculate solar heat gain through glazing.
|
|
@@ -50,188 +184,211 @@ class CoolingLoadCalculator:
|
|
| 50 |
float: Heat gain in Watts
|
| 51 |
"""
|
| 52 |
return area * shgf * shade_factor
|
| 53 |
-
|
| 54 |
def calculate_infiltration_heat_gain(self, volume, air_changes, temp_diff):
|
| 55 |
"""
|
| 56 |
Calculate heat gain due to infiltration and ventilation.
|
| 57 |
|
| 58 |
Args:
|
| 59 |
volume (float): Volume of the space in m³
|
| 60 |
-
air_changes (float):
|
| 61 |
-
temp_diff (float): Temperature difference (
|
| 62 |
|
| 63 |
Returns:
|
| 64 |
float: Heat gain in Watts
|
| 65 |
"""
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
"""
|
| 70 |
-
Calculate
|
| 71 |
|
| 72 |
Args:
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
Returns:
|
| 78 |
-
|
| 79 |
-
"""
|
| 80 |
-
people_gain = num_people * self.heat_gain_per_person
|
| 81 |
-
kitchen_gain = self.heat_gain_kitchen if has_kitchen else 0
|
| 82 |
-
return people_gain + kitchen_gain + equipment_watts
|
| 83 |
-
|
| 84 |
-
def get_solar_heat_gain_factor(self, orientation, glass_type, daily_range, latitude='medium'):
|
| 85 |
"""
|
| 86 |
-
|
|
|
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
# based on the ASHRAE data
|
| 99 |
-
|
| 100 |
-
# Base values for single glass at medium latitude
|
| 101 |
-
base_values = {
|
| 102 |
-
'north': 200,
|
| 103 |
-
'east': 550,
|
| 104 |
-
'south': 350,
|
| 105 |
-
'west': 550,
|
| 106 |
-
'horizontal': 650
|
| 107 |
-
}
|
| 108 |
|
| 109 |
-
#
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
'double': 0.85,
|
| 113 |
-
'low_e': 0.65
|
| 114 |
-
}
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
'
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
-
#
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
}
|
| 129 |
|
| 130 |
-
#
|
| 131 |
-
|
| 132 |
-
glass_factor = glass_factors.get(glass_type.lower(), 1.0)
|
| 133 |
-
latitude_factor = latitude_factors.get(latitude.lower(), 1.0)
|
| 134 |
-
range_factor = range_factors.get(daily_range.lower(), 1.0)
|
| 135 |
|
| 136 |
-
return
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
"""
|
| 140 |
-
Calculate the total cooling load
|
| 141 |
|
| 142 |
-
Args:
|
| 143 |
-
building_components (list): List of dicts with 'area', 'u_value', and 'temp_diff' for each component
|
| 144 |
-
windows (list): List of dicts with 'area', 'orientation', 'glass_type', 'shading', etc.
|
| 145 |
-
infiltration (dict): Dict with 'volume', 'air_changes', and 'temp_diff'
|
| 146 |
-
internal_gains (dict): Dict with 'num_people', 'has_kitchen', and 'equipment_watts'
|
| 147 |
-
|
| 148 |
Returns:
|
| 149 |
-
dict:
|
| 150 |
"""
|
| 151 |
-
#
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
)
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
-
for
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
window['area'], window['u_value'], window['temp_diff']
|
| 165 |
)
|
| 166 |
|
| 167 |
-
#
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
-
shading_value = window.get('shading', 0.0)
|
| 176 |
-
if shading_value == 'none' or shading_value == '':
|
| 177 |
-
shading_value = 0.0
|
| 178 |
-
shade_factor = 1.0 - float(shading_value)
|
| 179 |
window_solar_gain += self.calculate_solar_heat_gain(window['area'], shgf, shade_factor)
|
|
|
|
| 180 |
|
| 181 |
# Calculate infiltration heat gain
|
| 182 |
infiltration_gain = self.calculate_infiltration_heat_gain(
|
| 183 |
-
infiltration['volume'], infiltration['air_changes'], infiltration['temp_diff']
|
| 184 |
)
|
| 185 |
|
| 186 |
# Calculate internal heat gain
|
| 187 |
internal_gain = self.calculate_internal_heat_gain(
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
| 191 |
)
|
| 192 |
|
| 193 |
-
# Calculate
|
| 194 |
-
|
| 195 |
|
| 196 |
-
#
|
| 197 |
-
|
| 198 |
-
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
'internal_gain': internal_gain,
|
| 206 |
-
'
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
| 209 |
}
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
# Example usage
|
| 213 |
-
if __name__ == "__main__":
|
| 214 |
-
calculator = CoolingLoadCalculator()
|
| 215 |
-
|
| 216 |
-
# Example data for a simple room
|
| 217 |
-
building_components = [
|
| 218 |
-
{'area': 20, 'u_value': 0.6, 'temp_diff': 11}, # Floor
|
| 219 |
-
{'area': 50, 'u_value': 1.88, 'temp_diff': 11}, # Walls
|
| 220 |
-
{'area': 20, 'u_value': 0.46, 'temp_diff': 11} # Ceiling
|
| 221 |
-
]
|
| 222 |
-
|
| 223 |
-
windows = [
|
| 224 |
-
{'area': 4, 'orientation': 'north', 'glass_type': 'single', 'u_value': 5.8, 'temp_diff': 11, 'shading': 0.5},
|
| 225 |
-
{'area': 4, 'orientation': 'east', 'glass_type': 'single', 'u_value': 5.8, 'temp_diff': 11, 'shading': 0.0},
|
| 226 |
-
{'area': 4, 'orientation': 'west', 'glass_type': 'single', 'u_value': 5.8, 'temp_diff': 11, 'shading': 0.0}
|
| 227 |
-
]
|
| 228 |
-
|
| 229 |
-
infiltration = {'volume': 60, 'air_changes': 0.5, 'temp_diff': 11}
|
| 230 |
-
|
| 231 |
-
internal_gains = {'num_people': 4, 'has_kitchen': True, 'equipment_watts': 500}
|
| 232 |
-
|
| 233 |
-
result = calculator.calculate_total_cooling_load(building_components, windows, infiltration, internal_gains)
|
| 234 |
-
|
| 235 |
-
print("Cooling Load Calculation Results:")
|
| 236 |
-
for key, value in result.items():
|
| 237 |
-
print(f"{key}: {value:.2f} W")
|
|
|
|
| 1 |
"""
|
| 2 |
+
Cooling load calculation module for HVAC calculator.
|
| 3 |
|
| 4 |
+
This module provides the core functionality for calculating cooling loads
|
| 5 |
+
based on building characteristics, climate data, and internal heat sources.
|
| 6 |
"""
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
class CoolingLoadCalculator:
|
| 9 |
"""
|
| 10 |
+
Calculator for determining cooling loads in buildings.
|
| 11 |
+
|
| 12 |
+
This class implements the ASHRAE method for calculating cooling loads,
|
| 13 |
+
taking into account transmission gains, solar gains, internal gains,
|
| 14 |
+
and ventilation/infiltration.
|
| 15 |
"""
|
| 16 |
+
|
| 17 |
+
def __init__(self, ref_data):
|
| 18 |
+
"""
|
| 19 |
+
Initialize the cooling load calculator.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
ref_data: Reference data object containing climate data and material properties
|
| 23 |
+
"""
|
| 24 |
+
self.ref_data = ref_data
|
| 25 |
+
self.building_info = {}
|
| 26 |
+
self.components = []
|
| 27 |
+
self.windows = []
|
| 28 |
+
self.internal_sources = {
|
| 29 |
+
'people': 0,
|
| 30 |
+
'lighting': {'type': '', 'area': 0},
|
| 31 |
+
'equipment': []
|
| 32 |
+
}
|
| 33 |
+
self.custom_heat_sources = []
|
| 34 |
+
self.infiltration = {
|
| 35 |
+
'volume': 0,
|
| 36 |
+
'air_changes': 0,
|
| 37 |
+
'temp_diff': 0
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
def set_building_info(self, location, indoor_temp, building_volume, air_changes):
|
| 41 |
+
"""
|
| 42 |
+
Set basic building information.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
location (str): Location ID for climate data
|
| 46 |
+
indoor_temp (float): Indoor design temperature in °C
|
| 47 |
+
building_volume (float): Building volume in m³
|
| 48 |
+
air_changes (float): Air changes per hour
|
| 49 |
+
"""
|
| 50 |
+
self.building_info = {
|
| 51 |
+
'location': location,
|
| 52 |
+
'indoor_temp': indoor_temp,
|
| 53 |
+
'volume': building_volume,
|
| 54 |
+
'air_changes': air_changes
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# Set infiltration data based on building info
|
| 58 |
+
location_data = self.ref_data.get_location(location)
|
| 59 |
+
if location_data:
|
| 60 |
+
outdoor_temp = location_data['summer_design_temp']
|
| 61 |
+
self.infiltration = {
|
| 62 |
+
'volume': building_volume,
|
| 63 |
+
'air_changes': air_changes,
|
| 64 |
+
'temp_diff': outdoor_temp - indoor_temp
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
def add_component(self, component_type, area, u_value, orientation=None):
|
| 68 |
+
"""
|
| 69 |
+
Add a building component (wall, roof, floor).
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
component_type (str): Type of component ('wall', 'roof', 'floor')
|
| 73 |
+
area (float): Area of the component in m²
|
| 74 |
+
u_value (float): U-value of the component in W/m²°C
|
| 75 |
+
orientation (str, optional): Orientation for walls ('north', 'south', 'east', 'west')
|
| 76 |
+
"""
|
| 77 |
+
component = {
|
| 78 |
+
'type': component_type,
|
| 79 |
+
'area': area,
|
| 80 |
+
'u_value': u_value
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
if orientation and component_type == 'wall':
|
| 84 |
+
component['orientation'] = orientation
|
| 85 |
+
|
| 86 |
+
self.components.append(component)
|
| 87 |
+
|
| 88 |
+
def add_window(self, orientation, area, u_value, glass_type, shading):
|
| 89 |
+
"""
|
| 90 |
+
Add a window.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
orientation (str): Orientation ('north', 'south', 'east', 'west')
|
| 94 |
+
area (float): Area of the window in m²
|
| 95 |
+
u_value (float): U-value of the window in W/m²°C
|
| 96 |
+
glass_type (str): Type of glass
|
| 97 |
+
shading (str): Type of shading
|
| 98 |
+
"""
|
| 99 |
+
window = {
|
| 100 |
+
'orientation': orientation,
|
| 101 |
+
'area': area,
|
| 102 |
+
'u_value': u_value,
|
| 103 |
+
'glass_type': glass_type,
|
| 104 |
+
'shading': shading
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
self.windows.append(window)
|
| 108 |
+
|
| 109 |
+
def set_internal_sources(self, people, lighting_type, lighting_area, equipment, custom_heat_sources=None):
|
| 110 |
+
"""
|
| 111 |
+
Set internal heat sources.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
people (int): Number of occupants
|
| 115 |
+
lighting_type (str): Type of lighting
|
| 116 |
+
lighting_area (float): Area with lighting in m²
|
| 117 |
+
equipment (list): List of equipment items with type and quantity
|
| 118 |
+
custom_heat_sources (list, optional): List of custom heat sources with name and watts
|
| 119 |
"""
|
| 120 |
+
self.internal_sources = {
|
| 121 |
+
'people': people,
|
| 122 |
+
'lighting': {
|
| 123 |
+
'type': lighting_type,
|
| 124 |
+
'area': lighting_area
|
| 125 |
+
},
|
| 126 |
+
'equipment': equipment
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
if custom_heat_sources:
|
| 130 |
+
self.custom_heat_sources = custom_heat_sources
|
| 131 |
+
|
| 132 |
+
def calculate_transmission_heat_gain(self, area, u_value, temp_diff):
|
| 133 |
+
"""
|
| 134 |
+
Calculate heat gain through building components.
|
| 135 |
|
| 136 |
Args:
|
| 137 |
+
area (float): Area of the component in m²
|
| 138 |
u_value (float): U-value of the component in W/m²°C
|
| 139 |
+
temp_diff (float): Temperature difference (outdoor - indoor) in °C
|
| 140 |
|
| 141 |
Returns:
|
| 142 |
float: Heat gain in Watts
|
| 143 |
"""
|
| 144 |
return area * u_value * temp_diff
|
| 145 |
+
|
| 146 |
+
def calculate_solar_transmission_gain(self, area, u_value, orientation, location):
|
| 147 |
+
"""
|
| 148 |
+
Calculate solar heat gain through opaque surfaces.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
area (float): Area of the component in m²
|
| 152 |
+
u_value (float): U-value of the component in W/m²°C
|
| 153 |
+
orientation (str): Orientation of the component
|
| 154 |
+
location (str): Location ID for climate data
|
| 155 |
+
|
| 156 |
+
Returns:
|
| 157 |
+
float: Heat gain in Watts
|
| 158 |
+
"""
|
| 159 |
+
# Get solar intensity based on orientation and location
|
| 160 |
+
location_data = self.ref_data.get_location(location)
|
| 161 |
+
if not location_data or 'solar_intensity' not in location_data:
|
| 162 |
+
return 0
|
| 163 |
+
|
| 164 |
+
solar_intensity = location_data['solar_intensity'].get(orientation, 0)
|
| 165 |
+
|
| 166 |
+
# Calculate solar gain
|
| 167 |
+
solar_gain = area * solar_intensity * 0.1 # Factor of 0.1 for opaque surfaces
|
| 168 |
+
|
| 169 |
+
# Factor in the U-value (walls with higher U-values transmit more solar heat)
|
| 170 |
+
u_value_factor = min(u_value / 0.5, 2.0) # Normalize against a typical U-value of 0.5
|
| 171 |
+
|
| 172 |
+
return solar_gain * u_value_factor
|
| 173 |
+
|
| 174 |
def calculate_solar_heat_gain(self, area, shgf, shade_factor=1.0):
|
| 175 |
"""
|
| 176 |
Calculate solar heat gain through glazing.
|
|
|
|
| 184 |
float: Heat gain in Watts
|
| 185 |
"""
|
| 186 |
return area * shgf * shade_factor
|
| 187 |
+
|
| 188 |
def calculate_infiltration_heat_gain(self, volume, air_changes, temp_diff):
|
| 189 |
"""
|
| 190 |
Calculate heat gain due to infiltration and ventilation.
|
| 191 |
|
| 192 |
Args:
|
| 193 |
volume (float): Volume of the space in m³
|
| 194 |
+
air_changes (float): Air changes per hour
|
| 195 |
+
temp_diff (float): Temperature difference (outdoor - indoor) in °C
|
| 196 |
|
| 197 |
Returns:
|
| 198 |
float: Heat gain in Watts
|
| 199 |
"""
|
| 200 |
+
# Constants
|
| 201 |
+
air_density = 1.2 # kg/m³
|
| 202 |
+
specific_heat = 1000 # J/kg°C
|
| 203 |
+
|
| 204 |
+
# Calculate air flow rate in m³/s
|
| 205 |
+
air_flow = (volume * air_changes) / 3600
|
| 206 |
+
|
| 207 |
+
# Calculate heat gain
|
| 208 |
+
heat_gain = air_density * specific_heat * air_flow * temp_diff
|
| 209 |
+
|
| 210 |
+
return heat_gain
|
| 211 |
+
|
| 212 |
+
def calculate_internal_heat_gain(self, people, lighting, equipment, custom_sources=None):
|
| 213 |
"""
|
| 214 |
+
Calculate heat gain from internal sources.
|
| 215 |
|
| 216 |
Args:
|
| 217 |
+
people (int): Number of occupants
|
| 218 |
+
lighting (dict): Lighting information (type and area)
|
| 219 |
+
equipment (list): List of equipment items
|
| 220 |
+
custom_sources (list, optional): List of custom heat sources
|
| 221 |
|
| 222 |
Returns:
|
| 223 |
+
dict: Heat gain breakdown in Watts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
"""
|
| 225 |
+
# People heat gain
|
| 226 |
+
people_gain = people * 100 # Approximate heat gain per person in Watts
|
| 227 |
|
| 228 |
+
# Lighting heat gain
|
| 229 |
+
lighting_gain = 0
|
| 230 |
+
if lighting['type'] and lighting['area'] > 0:
|
| 231 |
+
lighting_factors = {
|
| 232 |
+
'led': 5,
|
| 233 |
+
'fluorescent': 12,
|
| 234 |
+
'incandescent': 20
|
| 235 |
+
}
|
| 236 |
+
lighting_factor = lighting_factors.get(lighting['type'], 0)
|
| 237 |
+
lighting_gain = lighting['area'] * lighting_factor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
+
# Equipment heat gain
|
| 240 |
+
equipment_gain = 0
|
| 241 |
+
equipment_breakdown = {}
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
+
for item in equipment:
|
| 244 |
+
item_type = item['type']
|
| 245 |
+
quantity = item['quantity']
|
| 246 |
+
|
| 247 |
+
# Get equipment heat gain factors
|
| 248 |
+
equipment_data = self.ref_data.get_internal_load('appliances', item_type)
|
| 249 |
+
if equipment_data:
|
| 250 |
+
item_gain = equipment_data['heat_gain'] * quantity
|
| 251 |
+
equipment_gain += item_gain
|
| 252 |
+
equipment_breakdown[item_type] = item_gain
|
| 253 |
|
| 254 |
+
# Custom heat sources
|
| 255 |
+
custom_gain = 0
|
| 256 |
+
if custom_sources:
|
| 257 |
+
for source in custom_sources:
|
| 258 |
+
custom_gain += source.get('watts', 0)
|
|
|
|
| 259 |
|
| 260 |
+
# Total internal gain
|
| 261 |
+
total_gain = people_gain + lighting_gain + equipment_gain + custom_gain
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
+
return {
|
| 264 |
+
'total': total_gain,
|
| 265 |
+
'people': people_gain,
|
| 266 |
+
'lighting': lighting_gain,
|
| 267 |
+
'equipment': equipment_gain,
|
| 268 |
+
'custom': custom_gain,
|
| 269 |
+
'equipment_breakdown': equipment_breakdown
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
def calculate_total_cooling_load(self):
|
| 273 |
"""
|
| 274 |
+
Calculate the total cooling load.
|
| 275 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
Returns:
|
| 277 |
+
dict: Cooling load results
|
| 278 |
"""
|
| 279 |
+
# Get location data
|
| 280 |
+
location_data = self.ref_data.get_location(self.building_info['location'])
|
| 281 |
+
if not location_data:
|
| 282 |
+
return {'error': 'Invalid location'}
|
|
|
|
| 283 |
|
| 284 |
+
outdoor_temp = location_data['summer_design_temp']
|
| 285 |
+
temp_diff = outdoor_temp - self.building_info['indoor_temp']
|
| 286 |
+
|
| 287 |
+
# Calculate transmission heat gain through components
|
| 288 |
+
transmission_gain = 0
|
| 289 |
+
component_gains = {}
|
| 290 |
|
| 291 |
+
for component in self.components:
|
| 292 |
+
component_gain = self.calculate_transmission_heat_gain(
|
| 293 |
+
component['area'], component['u_value'], temp_diff
|
|
|
|
| 294 |
)
|
| 295 |
|
| 296 |
+
# Add solar gain for exterior components
|
| 297 |
+
if component['type'] in ['wall', 'roof'] and 'orientation' in component:
|
| 298 |
+
component_gain += self.calculate_solar_transmission_gain(
|
| 299 |
+
component['area'], component['u_value'],
|
| 300 |
+
component['orientation'], self.building_info['location']
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
transmission_gain += component_gain
|
| 304 |
+
component_gains[f"{component['type']}_{len(component_gains)}"] = component_gain
|
| 305 |
+
|
| 306 |
+
# Calculate transmission and solar heat gain through windows
|
| 307 |
+
window_transmission_gain = 0
|
| 308 |
+
window_solar_gain = 0
|
| 309 |
+
window_gains = {}
|
| 310 |
+
window_solar_gains = {}
|
| 311 |
+
|
| 312 |
+
for window in self.windows:
|
| 313 |
+
# Transmission gain
|
| 314 |
+
window_gain = self.calculate_transmission_heat_gain(
|
| 315 |
+
window['area'], window['u_value'], temp_diff
|
| 316 |
)
|
| 317 |
+
window_transmission_gain += window_gain
|
| 318 |
+
window_gains[f"window_{len(window_gains)}"] = window_gain
|
| 319 |
+
|
| 320 |
+
# Solar gain
|
| 321 |
+
orientation = window['orientation']
|
| 322 |
+
glass_type = window['glass_type']
|
| 323 |
+
|
| 324 |
+
# Get solar heat gain factor
|
| 325 |
+
shgf = 0
|
| 326 |
+
glass_data = self.ref_data.get_glass_type(glass_type)
|
| 327 |
+
if glass_data and 'shgf' in glass_data:
|
| 328 |
+
shgf = glass_data['shgf'].get(orientation, 0)
|
| 329 |
+
|
| 330 |
+
# Get shading factor from reference data
|
| 331 |
+
shading_id = window.get('shading', 'none')
|
| 332 |
+
shade_factor = 1.0 # Default: no shading (full solar gain)
|
| 333 |
+
|
| 334 |
+
# Fix for the shading calculation error
|
| 335 |
+
shading_data = self.ref_data.get_shading_factor(shading_id)
|
| 336 |
+
if shading_data and 'factor' in shading_data:
|
| 337 |
+
# Use the factor value from the reference data
|
| 338 |
+
# The factor represents how much solar gain is reduced (e.g., 0.4 means 40% reduction)
|
| 339 |
+
shade_factor = 1.0 - shading_data['factor']
|
| 340 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
window_solar_gain += self.calculate_solar_heat_gain(window['area'], shgf, shade_factor)
|
| 342 |
+
window_solar_gains[f"window_{orientation}_{len(window_solar_gains)}"] = self.calculate_solar_heat_gain(window['area'], shgf, shade_factor)
|
| 343 |
|
| 344 |
# Calculate infiltration heat gain
|
| 345 |
infiltration_gain = self.calculate_infiltration_heat_gain(
|
| 346 |
+
self.infiltration['volume'], self.infiltration['air_changes'], self.infiltration['temp_diff']
|
| 347 |
)
|
| 348 |
|
| 349 |
# Calculate internal heat gain
|
| 350 |
internal_gain = self.calculate_internal_heat_gain(
|
| 351 |
+
self.internal_sources['people'],
|
| 352 |
+
self.internal_sources['lighting'],
|
| 353 |
+
self.internal_sources['equipment'],
|
| 354 |
+
self.custom_heat_sources
|
| 355 |
)
|
| 356 |
|
| 357 |
+
# Calculate total cooling load
|
| 358 |
+
total_load_w = transmission_gain + window_transmission_gain + window_solar_gain + infiltration_gain + internal_gain['total']
|
| 359 |
|
| 360 |
+
# Convert to kW and add safety factor
|
| 361 |
+
total_load_kw = total_load_w / 1000
|
| 362 |
+
recommended_size_kw = total_load_kw * 1.15 # 15% safety factor
|
| 363 |
|
| 364 |
+
# Calculate percentage breakdown
|
| 365 |
+
transmission_percentage = ((transmission_gain + window_transmission_gain) / total_load_w) * 100 if total_load_w > 0 else 0
|
| 366 |
+
solar_percentage = (window_solar_gain / total_load_w) * 100 if total_load_w > 0 else 0
|
| 367 |
+
infiltration_percentage = (infiltration_gain / total_load_w) * 100 if total_load_w > 0 else 0
|
| 368 |
+
internal_percentage = (internal_gain['total'] / total_load_w) * 100 if total_load_w > 0 else 0
|
| 369 |
+
|
| 370 |
+
# Prepare results
|
| 371 |
+
results = {
|
| 372 |
+
'total_load_w': total_load_w,
|
| 373 |
+
'total_load_kw': total_load_kw,
|
| 374 |
+
'recommended_size_kw': recommended_size_kw,
|
| 375 |
+
'transmission_gain': {
|
| 376 |
+
'total': transmission_gain + window_transmission_gain,
|
| 377 |
+
'components': component_gains,
|
| 378 |
+
'windows': window_gains
|
| 379 |
+
},
|
| 380 |
+
'solar_gain': {
|
| 381 |
+
'total': window_solar_gain,
|
| 382 |
+
'windows': window_solar_gains
|
| 383 |
+
},
|
| 384 |
+
'ventilation_gain': infiltration_gain,
|
| 385 |
'internal_gain': internal_gain,
|
| 386 |
+
'breakdown_percentage': {
|
| 387 |
+
'transmission': transmission_percentage,
|
| 388 |
+
'solar': solar_percentage,
|
| 389 |
+
'ventilation': infiltration_percentage,
|
| 390 |
+
'internal': internal_percentage
|
| 391 |
+
}
|
| 392 |
}
|
| 393 |
+
|
| 394 |
+
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
documentation/enhancements.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Enhancement Documentation
|
| 2 |
+
|
| 3 |
+
## Window Shading Calculation Fix
|
| 4 |
+
|
| 5 |
+
### Issue
|
| 6 |
+
The original calculator had an error when using certain window shading options, particularly the 'external_awning' option. This caused the following error:
|
| 7 |
+
|
| 8 |
+
```
|
| 9 |
+
ValueError: could not convert string to float: 'external_awning'
|
| 10 |
+
Traceback: File "/usr/local/lib/python3.10/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 542, in _run_script exec(code, module.__dict__)
|
| 11 |
+
File "/home/user/app/app.py", line 160, in <module> main()
|
| 12 |
+
File "/home/user/app/app.py", line 69, in main cooling_calculator()
|
| 13 |
+
File "/home/user/app/pages/cooling_calculator.py", line 1785, in cooling_calculator ventilation_form(ref_data)
|
| 14 |
+
File "/home/user/app/pages/cooling_calculator.py", line 1169, in ventilation_form calculate_cooling_load()
|
| 15 |
+
File "/home/user/app/pages/cooling_calculator.py", line 1260, in calculate_cooling_load results = calculator.calculate_total_cooling_load(
|
| 16 |
+
File "/home/user/app/cooling_load.py", line 246, in calculate_total_cooling_load shade_factor = 1.0 - float(shading_value)
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
### Solution
|
| 20 |
+
The issue was in the `calculate_total_cooling_load` method of the `CoolingLoadCalculator` class. The code was attempting to convert the shading value string directly to a float, but shading values like 'external_awning' are string identifiers, not numeric values.
|
| 21 |
+
|
| 22 |
+
The fix involves properly retrieving the numeric factor value from the reference data instead of trying to convert the string identifier directly:
|
| 23 |
+
|
| 24 |
+
```python
|
| 25 |
+
# Original problematic code
|
| 26 |
+
shading_value = window.get('shading', 0.0)
|
| 27 |
+
if shading_value == 'none' or shading_value == '':
|
| 28 |
+
shading_value = 0.0
|
| 29 |
+
shade_factor = 1.0 - float(shading_value) # This line caused the error
|
| 30 |
+
|
| 31 |
+
# Fixed code
|
| 32 |
+
shading_id = window.get('shading', 'none')
|
| 33 |
+
shade_factor = 1.0 # Default: no shading (full solar gain)
|
| 34 |
+
|
| 35 |
+
# Get shading factor from reference data
|
| 36 |
+
shading_data = self.ref_data.get_shading_factor(shading_id)
|
| 37 |
+
if shading_data and 'factor' in shading_data:
|
| 38 |
+
# Use the factor value from the reference data
|
| 39 |
+
# The factor represents how much solar gain is reduced (e.g., 0.4 means 40% reduction)
|
| 40 |
+
shade_factor = 1.0 - shading_data['factor']
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
This fix ensures that the calculator properly handles all shading types, including 'external_awning', by looking up the appropriate shading factor in the reference data.
|
| 44 |
+
|
| 45 |
+
## Scenario Comparison Functionality
|
| 46 |
+
|
| 47 |
+
### Feature Description
|
| 48 |
+
The scenario comparison functionality allows users to:
|
| 49 |
+
1. Save calculation results as named scenarios with descriptions
|
| 50 |
+
2. View a list of saved scenarios
|
| 51 |
+
3. Select multiple scenarios to compare
|
| 52 |
+
4. Visualize differences between scenarios with charts and tables
|
| 53 |
+
5. Export comparison data as CSV
|
| 54 |
+
|
| 55 |
+
### Implementation
|
| 56 |
+
This feature is implemented through several components:
|
| 57 |
+
|
| 58 |
+
1. **ScenarioManager Class** (`utils/scenario_manager.py`)
|
| 59 |
+
- Handles saving, loading, and comparing scenarios
|
| 60 |
+
- Generates visualization charts for comparisons
|
| 61 |
+
- Provides Streamlit UI integration
|
| 62 |
+
|
| 63 |
+
2. **Scenario Integration Module** (`utils/scenario_integration.py`)
|
| 64 |
+
- Integrates scenario functionality into the main application
|
| 65 |
+
- Adds UI components to calculators for saving scenarios
|
| 66 |
+
- Adds a dedicated scenario comparison page
|
| 67 |
+
|
| 68 |
+
3. **Directory Structure**
|
| 69 |
+
- Creates a `scenarios` directory with subdirectories for cooling and heating scenarios
|
| 70 |
+
- Stores scenarios as JSON files with timestamps
|
| 71 |
+
|
| 72 |
+
### Usage
|
| 73 |
+
Users can save scenarios after completing calculations, then navigate to the Scenario Comparison page to select and compare multiple scenarios. The comparison shows total loads, load breakdowns, and percentage differences between scenarios.
|
| 74 |
+
|
| 75 |
+
## Attribution and Purpose Information
|
| 76 |
+
|
| 77 |
+
Attribution information has been added to the application, crediting Dr. Majed Abuseif as the creator and explaining the tool's purpose:
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
This tool was created to facilitate HVAC calculation and understanding for Deakin University students, but has been enhanced to cover wider aspects to allow professionals and energy and HVAC enthusiasts to use it.
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
This information appears in the sidebar of the application and is also included in the README and documentation.
|
| 84 |
+
|
| 85 |
+
## Testing
|
| 86 |
+
|
| 87 |
+
All enhancements have been thoroughly tested:
|
| 88 |
+
|
| 89 |
+
1. **Window Shading Fix Tests**
|
| 90 |
+
- Tests for different shading types (none, internal blinds, external awning)
|
| 91 |
+
- Verifies correct calculation of shade factors
|
| 92 |
+
|
| 93 |
+
2. **Scenario Manager Tests**
|
| 94 |
+
- Tests for saving scenarios
|
| 95 |
+
- Tests for loading scenarios
|
| 96 |
+
- Tests for comparing multiple scenarios
|
| 97 |
+
|
| 98 |
+
All tests pass successfully, confirming that the enhancements work as expected.
|
documentation/user_guide.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# User Guide
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
### Installation
|
| 6 |
+
1. Ensure you have Python 3.8 or higher installed
|
| 7 |
+
2. Install the required dependencies:
|
| 8 |
+
```
|
| 9 |
+
pip install -r requirements.txt
|
| 10 |
+
```
|
| 11 |
+
3. Run the application:
|
| 12 |
+
```
|
| 13 |
+
streamlit run app.py
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
### Navigation
|
| 17 |
+
The application has three main sections accessible from the sidebar:
|
| 18 |
+
- **Cooling Load Calculator**: Calculate cooling loads for buildings
|
| 19 |
+
- **Heating Load Calculator**: Calculate heating loads for buildings
|
| 20 |
+
- **Scenario Comparison**: Compare saved calculation scenarios
|
| 21 |
+
|
| 22 |
+
## Using the Calculators
|
| 23 |
+
|
| 24 |
+
### Building Information
|
| 25 |
+
1. Select a location from the dropdown
|
| 26 |
+
2. Enter the indoor design temperature
|
| 27 |
+
3. Enter the building volume
|
| 28 |
+
4. Enter the air changes per hour
|
| 29 |
+
|
| 30 |
+
### Adding Components
|
| 31 |
+
1. **Walls, Roofs, and Floors**:
|
| 32 |
+
- Enter component details (area, U-value, orientation)
|
| 33 |
+
- Click "Add Component"
|
| 34 |
+
- To edit or delete components, use the buttons next to each entry
|
| 35 |
+
|
| 36 |
+
2. **Windows and Doors**:
|
| 37 |
+
- Enter details (orientation, area, U-value, glass type, shading)
|
| 38 |
+
- Click "Add Window/Door"
|
| 39 |
+
- To edit or delete windows/doors, use the buttons next to each entry
|
| 40 |
+
|
| 41 |
+
3. **Internal Loads**:
|
| 42 |
+
- Enter occupancy information
|
| 43 |
+
- Specify lighting details
|
| 44 |
+
- Add appliances with quantities
|
| 45 |
+
- Add custom heat sources if needed
|
| 46 |
+
|
| 47 |
+
### Calculating Results
|
| 48 |
+
1. Navigate through all tabs to enter required information
|
| 49 |
+
2. Click "Calculate" on the final tab
|
| 50 |
+
3. View the results including:
|
| 51 |
+
- Total load in kW
|
| 52 |
+
- Recommended system size
|
| 53 |
+
- Load breakdown by component
|
| 54 |
+
- Visualizations of heat gains/losses
|
| 55 |
+
|
| 56 |
+
### Saving Scenarios
|
| 57 |
+
After calculating results:
|
| 58 |
+
1. Enter a scenario name and description
|
| 59 |
+
2. Click "Save Scenario"
|
| 60 |
+
3. The scenario will be saved for future comparison
|
| 61 |
+
|
| 62 |
+
## Comparing Scenarios
|
| 63 |
+
|
| 64 |
+
1. Navigate to the "Scenario Comparison" page
|
| 65 |
+
2. Select the calculator type (cooling, heating, or all)
|
| 66 |
+
3. Select at least two scenarios to compare
|
| 67 |
+
4. Click "Compare Selected Scenarios"
|
| 68 |
+
5. View the comparison results:
|
| 69 |
+
- Total load comparison
|
| 70 |
+
- Load breakdown comparison
|
| 71 |
+
- Differences between scenarios
|
| 72 |
+
- Interpretation of results
|
| 73 |
+
6. Download the comparison data as CSV if needed
|
| 74 |
+
|
| 75 |
+
## Tips for Effective Use
|
| 76 |
+
|
| 77 |
+
1. **Base Case Analysis**:
|
| 78 |
+
- Create a base case scenario first
|
| 79 |
+
- Make incremental changes for subsequent scenarios
|
| 80 |
+
- Compare to see the impact of each change
|
| 81 |
+
|
| 82 |
+
2. **Custom Heat Sources**:
|
| 83 |
+
- Use the custom heat sources feature for equipment not in the predefined list
|
| 84 |
+
- Enter the name and wattage for each custom source
|
| 85 |
+
|
| 86 |
+
3. **Component Editing**:
|
| 87 |
+
- You can edit or delete components after adding them
|
| 88 |
+
- This allows for quick adjustments without starting over
|
| 89 |
+
|
| 90 |
+
4. **Scenario Management**:
|
| 91 |
+
- Give descriptive names to scenarios
|
| 92 |
+
- Add detailed descriptions to remember what each scenario represents
|
| 93 |
+
- Use the comparison tool to identify the most efficient building configuration
|
heating_load.py
CHANGED
|
@@ -18,6 +18,10 @@ class HeatingLoadCalculator:
|
|
| 18 |
"""Initialize the heating load calculator with default values."""
|
| 19 |
# Specific heat capacity of air × density of air
|
| 20 |
self.air_heat_factor = 0.33
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
def calculate_conduction_heat_loss(self, area, u_value, temp_diff):
|
| 23 |
"""
|
|
@@ -32,6 +36,61 @@ class HeatingLoadCalculator:
|
|
| 32 |
float: Heat loss in Watts
|
| 33 |
"""
|
| 34 |
return area * u_value * temp_diff
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
def calculate_infiltration_heat_loss(self, volume, air_changes, temp_diff):
|
| 37 |
"""
|
|
@@ -46,6 +105,22 @@ class HeatingLoadCalculator:
|
|
| 46 |
float: Heat loss in Watts
|
| 47 |
"""
|
| 48 |
return self.air_heat_factor * volume * air_changes * temp_diff
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
def calculate_annual_heating_energy(self, total_heat_loss, heating_degree_days, correction_factor=1.0):
|
| 51 |
"""
|
|
@@ -146,13 +221,14 @@ class HeatingLoadCalculator:
|
|
| 146 |
|
| 147 |
return factors.get(occupancy_type.lower(), 1.0) # Default to continuous if not found
|
| 148 |
|
| 149 |
-
def calculate_total_heating_load(self, building_components, infiltration):
|
| 150 |
"""
|
| 151 |
Calculate the total peak heating load.
|
| 152 |
|
| 153 |
Args:
|
| 154 |
-
building_components (list): List of dicts with 'area', 'u_value', and '
|
| 155 |
infiltration (dict): Dict with 'volume', 'air_changes', and 'temp_diff'
|
|
|
|
| 156 |
|
| 157 |
Returns:
|
| 158 |
dict: Dictionary with component heat losses and total heating load in Watts
|
|
@@ -160,25 +236,50 @@ class HeatingLoadCalculator:
|
|
| 160 |
# Calculate conduction heat loss through building components
|
| 161 |
component_losses = {}
|
| 162 |
total_conduction_loss = 0
|
|
|
|
| 163 |
|
| 164 |
for comp in building_components:
|
| 165 |
name = comp.get('name', f"Component {len(component_losses) + 1}")
|
| 166 |
loss = self.calculate_conduction_heat_loss(comp['area'], comp['u_value'], comp['temp_diff'])
|
| 167 |
component_losses[name] = loss
|
| 168 |
total_conduction_loss += loss
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
# Calculate infiltration heat loss
|
| 171 |
infiltration_loss = self.calculate_infiltration_heat_loss(
|
| 172 |
infiltration['volume'], infiltration['air_changes'], infiltration['temp_diff']
|
| 173 |
)
|
| 174 |
|
| 175 |
-
# Calculate
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
return {
|
| 179 |
'component_losses': component_losses,
|
| 180 |
'total_conduction_loss': total_conduction_loss,
|
| 181 |
'infiltration_loss': infiltration_loss,
|
|
|
|
|
|
|
| 182 |
'total_load': total_load
|
| 183 |
}
|
| 184 |
|
|
|
|
| 18 |
"""Initialize the heating load calculator with default values."""
|
| 19 |
# Specific heat capacity of air × density of air
|
| 20 |
self.air_heat_factor = 0.33
|
| 21 |
+
|
| 22 |
+
# Default values for internal heat gains (W)
|
| 23 |
+
self.heat_gain_per_person = 75
|
| 24 |
+
self.heat_gain_kitchen = 1000
|
| 25 |
|
| 26 |
def calculate_conduction_heat_loss(self, area, u_value, temp_diff):
|
| 27 |
"""
|
|
|
|
| 36 |
float: Heat loss in Watts
|
| 37 |
"""
|
| 38 |
return area * u_value * temp_diff
|
| 39 |
+
|
| 40 |
+
def calculate_wall_solar_heat_gain(self, area, u_value, orientation, daily_range='medium', latitude='medium'):
|
| 41 |
+
"""
|
| 42 |
+
Calculate solar heat gain through walls based on orientation.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
area (float): Area of the wall in m²
|
| 46 |
+
u_value (float): U-value of the wall in W/m²°C
|
| 47 |
+
orientation (str): Wall orientation ('north', 'east', 'south', 'west')
|
| 48 |
+
daily_range (str): Daily temperature range ('low', 'medium', 'high')
|
| 49 |
+
latitude (str): Latitude category ('low', 'medium', 'high')
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
float: Heat gain in Watts
|
| 53 |
+
"""
|
| 54 |
+
# Solar intensity factors based on orientation - for heating, south-facing walls (northern hemisphere)
|
| 55 |
+
# or north-facing walls (southern hemisphere) receive more solar gain in winter
|
| 56 |
+
# These are simplified factors for demonstration
|
| 57 |
+
orientation_factors = {
|
| 58 |
+
'north': 0.6, # Higher in southern hemisphere during winter
|
| 59 |
+
'east': 0.4,
|
| 60 |
+
'south': 0.2, # Lower in southern hemisphere during winter
|
| 61 |
+
'west': 0.4,
|
| 62 |
+
'horizontal': 0.3
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
# Adjustments for latitude
|
| 66 |
+
latitude_factors = {
|
| 67 |
+
'low': 0.9, # Closer to equator - less winter sun angle
|
| 68 |
+
'medium': 1.0, # Mid latitudes
|
| 69 |
+
'high': 1.1 # Closer to poles - more winter sun angle variation
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
# Adjustments for daily temperature range
|
| 73 |
+
range_factors = {
|
| 74 |
+
'low': 0.95, # Less than 8.5°C
|
| 75 |
+
'medium': 1.0, # Between 8.5°C and 14°C
|
| 76 |
+
'high': 1.05 # Over 14°C
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
# Base solar heat gain through walls (W/m²) - lower in winter
|
| 80 |
+
base_solar_gain = 10.0
|
| 81 |
+
|
| 82 |
+
# Get factors
|
| 83 |
+
orientation_factor = orientation_factors.get(orientation.lower(), 0.5) # Default to south if not found
|
| 84 |
+
latitude_factor = latitude_factors.get(latitude.lower(), 1.0)
|
| 85 |
+
range_factor = range_factors.get(daily_range.lower(), 1.0)
|
| 86 |
+
|
| 87 |
+
# Calculate solar heat gain
|
| 88 |
+
solar_gain = area * base_solar_gain * orientation_factor * latitude_factor * range_factor
|
| 89 |
+
|
| 90 |
+
# Factor in the U-value (walls with higher U-values transmit more solar heat)
|
| 91 |
+
u_value_factor = min(u_value / 0.5, 2.0) # Normalize against a typical U-value of 0.5
|
| 92 |
+
|
| 93 |
+
return solar_gain * u_value_factor
|
| 94 |
|
| 95 |
def calculate_infiltration_heat_loss(self, volume, air_changes, temp_diff):
|
| 96 |
"""
|
|
|
|
| 105 |
float: Heat loss in Watts
|
| 106 |
"""
|
| 107 |
return self.air_heat_factor * volume * air_changes * temp_diff
|
| 108 |
+
|
| 109 |
+
def calculate_internal_heat_gain(self, num_people, has_kitchen=False, equipment_watts=0):
|
| 110 |
+
"""
|
| 111 |
+
Calculate internal heat gain from people, kitchen, and equipment.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
num_people (int): Number of occupants
|
| 115 |
+
has_kitchen (bool): Whether the space includes a kitchen
|
| 116 |
+
equipment_watts (float): Additional equipment heat gain in Watts
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
float: Heat gain in Watts
|
| 120 |
+
"""
|
| 121 |
+
people_gain = num_people * self.heat_gain_per_person
|
| 122 |
+
kitchen_gain = self.heat_gain_kitchen if has_kitchen else 0
|
| 123 |
+
return people_gain + kitchen_gain + equipment_watts
|
| 124 |
|
| 125 |
def calculate_annual_heating_energy(self, total_heat_loss, heating_degree_days, correction_factor=1.0):
|
| 126 |
"""
|
|
|
|
| 221 |
|
| 222 |
return factors.get(occupancy_type.lower(), 1.0) # Default to continuous if not found
|
| 223 |
|
| 224 |
+
def calculate_total_heating_load(self, building_components, infiltration, internal_gains=None):
|
| 225 |
"""
|
| 226 |
Calculate the total peak heating load.
|
| 227 |
|
| 228 |
Args:
|
| 229 |
+
building_components (list): List of dicts with 'area', 'u_value', 'temp_diff', and 'orientation' for each component
|
| 230 |
infiltration (dict): Dict with 'volume', 'air_changes', and 'temp_diff'
|
| 231 |
+
internal_gains (dict): Dict with 'num_people', 'has_kitchen', and 'equipment_watts'
|
| 232 |
|
| 233 |
Returns:
|
| 234 |
dict: Dictionary with component heat losses and total heating load in Watts
|
|
|
|
| 236 |
# Calculate conduction heat loss through building components
|
| 237 |
component_losses = {}
|
| 238 |
total_conduction_loss = 0
|
| 239 |
+
wall_solar_gain = 0
|
| 240 |
|
| 241 |
for comp in building_components:
|
| 242 |
name = comp.get('name', f"Component {len(component_losses) + 1}")
|
| 243 |
loss = self.calculate_conduction_heat_loss(comp['area'], comp['u_value'], comp['temp_diff'])
|
| 244 |
component_losses[name] = loss
|
| 245 |
total_conduction_loss += loss
|
| 246 |
+
|
| 247 |
+
# Calculate solar gain for walls based on orientation
|
| 248 |
+
if 'orientation' in comp:
|
| 249 |
+
daily_range = comp.get('daily_range', 'medium')
|
| 250 |
+
latitude = comp.get('latitude', 'medium')
|
| 251 |
+
solar_gain = self.calculate_wall_solar_heat_gain(
|
| 252 |
+
comp['area'],
|
| 253 |
+
comp['u_value'],
|
| 254 |
+
comp['orientation'],
|
| 255 |
+
daily_range,
|
| 256 |
+
latitude
|
| 257 |
+
)
|
| 258 |
+
wall_solar_gain += solar_gain
|
| 259 |
|
| 260 |
# Calculate infiltration heat loss
|
| 261 |
infiltration_loss = self.calculate_infiltration_heat_loss(
|
| 262 |
infiltration['volume'], infiltration['air_changes'], infiltration['temp_diff']
|
| 263 |
)
|
| 264 |
|
| 265 |
+
# Calculate internal heat gain if provided
|
| 266 |
+
internal_gain = 0
|
| 267 |
+
if internal_gains:
|
| 268 |
+
internal_gain = self.calculate_internal_heat_gain(
|
| 269 |
+
internal_gains.get('num_people', 0),
|
| 270 |
+
internal_gains.get('has_kitchen', False),
|
| 271 |
+
internal_gains.get('equipment_watts', 0)
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Calculate total heating load (subtract solar gain and internal gains as they reduce heating load)
|
| 275 |
+
total_load = total_conduction_loss + infiltration_loss - wall_solar_gain - internal_gain
|
| 276 |
|
| 277 |
return {
|
| 278 |
'component_losses': component_losses,
|
| 279 |
'total_conduction_loss': total_conduction_loss,
|
| 280 |
'infiltration_loss': infiltration_loss,
|
| 281 |
+
'wall_solar_gain': wall_solar_gain,
|
| 282 |
+
'internal_gain': internal_gain,
|
| 283 |
'total_load': total_load
|
| 284 |
}
|
| 285 |
|
integrate_enhancements.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Integration module for the enhanced HVAC calculator.
|
| 3 |
+
|
| 4 |
+
This module integrates all the enhancements into the main application:
|
| 5 |
+
1. Window shading calculation fix
|
| 6 |
+
2. Scenario comparison functionality
|
| 7 |
+
3. Attribution and purpose information
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import sys
|
| 11 |
+
import os
|
| 12 |
+
import importlib
|
| 13 |
+
import streamlit as st
|
| 14 |
+
|
| 15 |
+
# Add the current directory to the path
|
| 16 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 17 |
+
|
| 18 |
+
def integrate_enhancements():
|
| 19 |
+
"""
|
| 20 |
+
Integrate all enhancements into the HVAC calculator application.
|
| 21 |
+
|
| 22 |
+
This function:
|
| 23 |
+
1. Integrates the scenario comparison functionality
|
| 24 |
+
2. Adds attribution and purpose information
|
| 25 |
+
3. Ensures the window shading calculation fix is applied
|
| 26 |
+
"""
|
| 27 |
+
# Import required modules
|
| 28 |
+
from utils.scenario_integration import add_scenario_ui_to_calculator, add_scenario_comparison_to_app
|
| 29 |
+
import app as app_module
|
| 30 |
+
import pages.cooling_calculator as cooling_module
|
| 31 |
+
import pages.heating_calculator as heating_module
|
| 32 |
+
|
| 33 |
+
# 1. Integrate scenario comparison functionality
|
| 34 |
+
|
| 35 |
+
# Add scenario UI to calculators
|
| 36 |
+
add_scenario_ui_to_calculator(cooling_module, "cooling")
|
| 37 |
+
add_scenario_ui_to_calculator(heating_module, "heating")
|
| 38 |
+
|
| 39 |
+
# Add scenario comparison page to main app
|
| 40 |
+
add_scenario_comparison_to_app(app_module)
|
| 41 |
+
|
| 42 |
+
# 2. Attribution and purpose information is already added to app.py
|
| 43 |
+
|
| 44 |
+
# 3. Window shading calculation fix is already applied to cooling_load.py
|
| 45 |
+
|
| 46 |
+
# Return the modified modules
|
| 47 |
+
return {
|
| 48 |
+
"app": app_module,
|
| 49 |
+
"cooling_calculator": cooling_module,
|
| 50 |
+
"heating_calculator": heating_module
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def run_enhanced_app():
|
| 54 |
+
"""
|
| 55 |
+
Run the enhanced HVAC calculator application.
|
| 56 |
+
|
| 57 |
+
This function integrates all enhancements and then runs the main application.
|
| 58 |
+
"""
|
| 59 |
+
# Integrate enhancements
|
| 60 |
+
enhanced_modules = integrate_enhancements()
|
| 61 |
+
|
| 62 |
+
# Run the enhanced app
|
| 63 |
+
enhanced_modules["app"].main()
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
run_enhanced_app()
|
pages/cooling_calculator.py
CHANGED
|
@@ -210,7 +210,7 @@ def building_info_form(ref_data):
|
|
| 210 |
warnings.append(ValidationWarning(
|
| 211 |
"Invalid temperature difference",
|
| 212 |
"Outdoor temperature should be higher than indoor temperature for cooling load calculation",
|
| 213 |
-
is_critical=
|
| 214 |
))
|
| 215 |
|
| 216 |
# Check if dimensions are reasonable
|
|
@@ -289,14 +289,18 @@ def building_envelope_form(ref_data):
|
|
| 289 |
|
| 290 |
# Get wall material options from reference data
|
| 291 |
wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
|
|
|
|
|
|
|
| 292 |
|
| 293 |
# Display existing wall entries
|
| 294 |
if st.session_state.cooling_form_data['building_envelope']['walls']:
|
| 295 |
st.write("Current walls:")
|
| 296 |
walls_df = pd.DataFrame(st.session_state.cooling_form_data['building_envelope']['walls'])
|
| 297 |
walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
|
| 298 |
-
|
| 299 |
-
walls_df
|
|
|
|
|
|
|
| 300 |
st.dataframe(walls_df)
|
| 301 |
|
| 302 |
# Add new wall form
|
|
@@ -313,10 +317,39 @@ def building_envelope_form(ref_data):
|
|
| 313 |
key="new_wall_material"
|
| 314 |
)
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
# Get material properties
|
| 317 |
material_data = ref_data.get_material_by_type("walls", wall_material)
|
| 318 |
u_value = material_data['u_value']
|
| 319 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
with col2:
|
| 321 |
wall_area = st.number_input(
|
| 322 |
"Wall Area (m²)",
|
|
@@ -336,7 +369,8 @@ def building_envelope_form(ref_data):
|
|
| 336 |
'material_id': wall_material,
|
| 337 |
'area': wall_area,
|
| 338 |
'u_value': u_value,
|
| 339 |
-
'temp_diff': temp_diff
|
|
|
|
| 340 |
}
|
| 341 |
st.session_state.cooling_form_data['building_envelope']['walls'].append(new_wall)
|
| 342 |
st.experimental_rerun()
|
|
@@ -346,6 +380,8 @@ def building_envelope_form(ref_data):
|
|
| 346 |
|
| 347 |
# Get roof material options from reference data
|
| 348 |
roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
|
|
|
|
|
|
|
| 349 |
|
| 350 |
col1, col2 = st.columns(2)
|
| 351 |
|
|
@@ -361,6 +397,28 @@ def building_envelope_form(ref_data):
|
|
| 361 |
material_data = ref_data.get_material_by_type("roofs", roof_material)
|
| 362 |
roof_u_value = material_data['u_value']
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
with col2:
|
| 365 |
roof_area = st.number_input(
|
| 366 |
"Roof Area (m²)",
|
|
@@ -385,6 +443,8 @@ def building_envelope_form(ref_data):
|
|
| 385 |
|
| 386 |
# Get floor material options from reference data
|
| 387 |
floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
|
|
|
|
|
|
|
| 388 |
|
| 389 |
col1, col2 = st.columns(2)
|
| 390 |
|
|
@@ -400,6 +460,28 @@ def building_envelope_form(ref_data):
|
|
| 400 |
material_data = ref_data.get_material_by_type("floors", floor_material)
|
| 401 |
floor_u_value = material_data['u_value']
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
with col2:
|
| 404 |
floor_area = st.number_input(
|
| 405 |
"Floor Area (m²)",
|
|
@@ -427,7 +509,7 @@ def building_envelope_form(ref_data):
|
|
| 427 |
warnings.append(ValidationWarning(
|
| 428 |
"No walls defined",
|
| 429 |
"Add at least one wall to continue",
|
| 430 |
-
is_critical=
|
| 431 |
))
|
| 432 |
|
| 433 |
# Check if total wall area is reasonable
|
|
@@ -1249,7 +1331,26 @@ def results_page():
|
|
| 1249 |
values=list(load_components.values()),
|
| 1250 |
names=list(load_components.keys()),
|
| 1251 |
title="Cooling Load Components",
|
| 1252 |
-
color_discrete_sequence=px.colors.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1253 |
)
|
| 1254 |
|
| 1255 |
st.plotly_chart(fig)
|
|
@@ -1261,10 +1362,13 @@ def results_page():
|
|
| 1261 |
'Percentage (%)': [value / results['sensible_load'] * 100 for value in load_components.values()]
|
| 1262 |
})
|
| 1263 |
|
|
|
|
|
|
|
|
|
|
| 1264 |
st.dataframe(load_df.style.format({
|
| 1265 |
'Load (W)': '{:.2f}',
|
| 1266 |
'Percentage (%)': '{:.2f}'
|
| 1267 |
-
}))
|
| 1268 |
|
| 1269 |
# Display detailed results
|
| 1270 |
st.write("### Detailed Results")
|
|
@@ -1384,21 +1488,38 @@ def results_page():
|
|
| 1384 |
x=windows_df['Component'],
|
| 1385 |
y=windows_df['Conduction Heat Gain (W)'],
|
| 1386 |
name='Conduction Heat Gain',
|
| 1387 |
-
marker_color='
|
|
|
|
|
|
|
|
|
|
| 1388 |
))
|
| 1389 |
|
| 1390 |
fig.add_trace(go.Bar(
|
| 1391 |
x=windows_df['Component'],
|
| 1392 |
y=windows_df['Solar Heat Gain (W)'],
|
| 1393 |
name='Solar Heat Gain',
|
| 1394 |
-
marker_color='
|
|
|
|
|
|
|
|
|
|
| 1395 |
))
|
| 1396 |
|
| 1397 |
fig.update_layout(
|
| 1398 |
title="Window Heat Gains",
|
| 1399 |
xaxis_title="Window",
|
| 1400 |
yaxis_title="Heat Gain (W)",
|
| 1401 |
-
barmode='stack'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1402 |
)
|
| 1403 |
|
| 1404 |
st.plotly_chart(fig)
|
|
@@ -1606,6 +1727,42 @@ def cooling_calculator():
|
|
| 1606 |
"6. Results"
|
| 1607 |
])
|
| 1608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1609 |
# Display the active tab
|
| 1610 |
with tabs[0]:
|
| 1611 |
if st.session_state.cooling_active_tab == "building_info":
|
|
|
|
| 210 |
warnings.append(ValidationWarning(
|
| 211 |
"Invalid temperature difference",
|
| 212 |
"Outdoor temperature should be higher than indoor temperature for cooling load calculation",
|
| 213 |
+
is_critical=False # Changed to non-critical to allow proceeding with warnings
|
| 214 |
))
|
| 215 |
|
| 216 |
# Check if dimensions are reasonable
|
|
|
|
| 289 |
|
| 290 |
# Get wall material options from reference data
|
| 291 |
wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
|
| 292 |
+
# Add custom option
|
| 293 |
+
wall_material_options["custom_walls"] = "Custom Wall (User-defined)"
|
| 294 |
|
| 295 |
# Display existing wall entries
|
| 296 |
if st.session_state.cooling_form_data['building_envelope']['walls']:
|
| 297 |
st.write("Current walls:")
|
| 298 |
walls_df = pd.DataFrame(st.session_state.cooling_form_data['building_envelope']['walls'])
|
| 299 |
walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
|
| 300 |
+
# Add orientation column with default value if not present
|
| 301 |
+
walls_df['orientation'] = walls_df['orientation'].fillna('not specified')
|
| 302 |
+
walls_df = walls_df[['name', 'Material', 'area', 'u_value', 'orientation']]
|
| 303 |
+
walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)', 'Orientation']
|
| 304 |
st.dataframe(walls_df)
|
| 305 |
|
| 306 |
# Add new wall form
|
|
|
|
| 317 |
key="new_wall_material"
|
| 318 |
)
|
| 319 |
|
| 320 |
+
# Add wall orientation selection
|
| 321 |
+
wall_orientation = st.selectbox(
|
| 322 |
+
"Wall Orientation",
|
| 323 |
+
options=["north", "east", "south", "west"],
|
| 324 |
+
key="new_wall_orientation"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
# Get material properties
|
| 328 |
material_data = ref_data.get_material_by_type("walls", wall_material)
|
| 329 |
u_value = material_data['u_value']
|
| 330 |
|
| 331 |
+
# Add custom U-value input if custom material is selected
|
| 332 |
+
if wall_material == "custom_walls":
|
| 333 |
+
u_value = st.number_input(
|
| 334 |
+
"Custom U-Value (W/m²°C)",
|
| 335 |
+
value=1.0,
|
| 336 |
+
min_value=0.1,
|
| 337 |
+
max_value=5.0,
|
| 338 |
+
step=0.1,
|
| 339 |
+
key="custom_wall_u_value"
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
# Store custom material in session state
|
| 343 |
+
if "custom_materials" not in st.session_state:
|
| 344 |
+
st.session_state.custom_materials = {}
|
| 345 |
+
|
| 346 |
+
st.session_state.custom_materials["walls"] = {
|
| 347 |
+
"name": "Custom Wall",
|
| 348 |
+
"u_value": u_value,
|
| 349 |
+
"r_value": 1.0 / u_value if u_value > 0 else 1.0,
|
| 350 |
+
"description": "Custom wall with user-defined properties"
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
with col2:
|
| 354 |
wall_area = st.number_input(
|
| 355 |
"Wall Area (m²)",
|
|
|
|
| 369 |
'material_id': wall_material,
|
| 370 |
'area': wall_area,
|
| 371 |
'u_value': u_value,
|
| 372 |
+
'temp_diff': temp_diff,
|
| 373 |
+
'orientation': wall_orientation # Add orientation to wall data
|
| 374 |
}
|
| 375 |
st.session_state.cooling_form_data['building_envelope']['walls'].append(new_wall)
|
| 376 |
st.experimental_rerun()
|
|
|
|
| 380 |
|
| 381 |
# Get roof material options from reference data
|
| 382 |
roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
|
| 383 |
+
# Add custom option
|
| 384 |
+
roof_material_options["custom_roofs"] = "Custom Roof (User-defined)"
|
| 385 |
|
| 386 |
col1, col2 = st.columns(2)
|
| 387 |
|
|
|
|
| 397 |
material_data = ref_data.get_material_by_type("roofs", roof_material)
|
| 398 |
roof_u_value = material_data['u_value']
|
| 399 |
|
| 400 |
+
# Add custom U-value input if custom material is selected
|
| 401 |
+
if roof_material == "custom_roofs":
|
| 402 |
+
roof_u_value = st.number_input(
|
| 403 |
+
"Custom Roof U-Value (W/m²°C)",
|
| 404 |
+
value=1.0,
|
| 405 |
+
min_value=0.1,
|
| 406 |
+
max_value=5.0,
|
| 407 |
+
step=0.1,
|
| 408 |
+
key="custom_roof_u_value"
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# Store custom material in session state
|
| 412 |
+
if "custom_materials" not in st.session_state:
|
| 413 |
+
st.session_state.custom_materials = {}
|
| 414 |
+
|
| 415 |
+
st.session_state.custom_materials["roofs"] = {
|
| 416 |
+
"name": "Custom Roof",
|
| 417 |
+
"u_value": roof_u_value,
|
| 418 |
+
"r_value": 1.0 / roof_u_value if roof_u_value > 0 else 1.0,
|
| 419 |
+
"description": "Custom roof with user-defined properties"
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
with col2:
|
| 423 |
roof_area = st.number_input(
|
| 424 |
"Roof Area (m²)",
|
|
|
|
| 443 |
|
| 444 |
# Get floor material options from reference data
|
| 445 |
floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
|
| 446 |
+
# Add custom option
|
| 447 |
+
floor_material_options["custom_floors"] = "Custom Floor (User-defined)"
|
| 448 |
|
| 449 |
col1, col2 = st.columns(2)
|
| 450 |
|
|
|
|
| 460 |
material_data = ref_data.get_material_by_type("floors", floor_material)
|
| 461 |
floor_u_value = material_data['u_value']
|
| 462 |
|
| 463 |
+
# Add custom U-value input if custom material is selected
|
| 464 |
+
if floor_material == "custom_floors":
|
| 465 |
+
floor_u_value = st.number_input(
|
| 466 |
+
"Custom Floor U-Value (W/m²°C)",
|
| 467 |
+
value=1.0,
|
| 468 |
+
min_value=0.1,
|
| 469 |
+
max_value=5.0,
|
| 470 |
+
step=0.1,
|
| 471 |
+
key="custom_floor_u_value"
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
# Store custom material in session state
|
| 475 |
+
if "custom_materials" not in st.session_state:
|
| 476 |
+
st.session_state.custom_materials = {}
|
| 477 |
+
|
| 478 |
+
st.session_state.custom_materials["floors"] = {
|
| 479 |
+
"name": "Custom Floor",
|
| 480 |
+
"u_value": floor_u_value,
|
| 481 |
+
"r_value": 1.0 / floor_u_value if floor_u_value > 0 else 1.0,
|
| 482 |
+
"description": "Custom floor with user-defined properties"
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
with col2:
|
| 486 |
floor_area = st.number_input(
|
| 487 |
"Floor Area (m²)",
|
|
|
|
| 509 |
warnings.append(ValidationWarning(
|
| 510 |
"No walls defined",
|
| 511 |
"Add at least one wall to continue",
|
| 512 |
+
is_critical=False # Changed to non-critical to allow proceeding with warnings
|
| 513 |
))
|
| 514 |
|
| 515 |
# Check if total wall area is reasonable
|
|
|
|
| 1331 |
values=list(load_components.values()),
|
| 1332 |
names=list(load_components.keys()),
|
| 1333 |
title="Cooling Load Components",
|
| 1334 |
+
color_discrete_sequence=px.colors.sequential.Turbo,
|
| 1335 |
+
hole=0.4, # Create a donut chart for better readability
|
| 1336 |
+
labels={'label': 'Component', 'value': 'Heat Gain (W)'}
|
| 1337 |
+
)
|
| 1338 |
+
|
| 1339 |
+
# Improve layout and formatting
|
| 1340 |
+
fig.update_traces(
|
| 1341 |
+
textposition='inside',
|
| 1342 |
+
textinfo='percent+label',
|
| 1343 |
+
hoverinfo='label+percent+value',
|
| 1344 |
+
marker=dict(line=dict(color='#FFFFFF', width=2))
|
| 1345 |
+
)
|
| 1346 |
+
|
| 1347 |
+
# Improve layout
|
| 1348 |
+
fig.update_layout(
|
| 1349 |
+
legend_title_text='Load Components',
|
| 1350 |
+
font=dict(size=14),
|
| 1351 |
+
title_font=dict(size=18),
|
| 1352 |
+
title_x=0.5, # Center the title
|
| 1353 |
+
margin=dict(t=50, b=50, l=50, r=50)
|
| 1354 |
)
|
| 1355 |
|
| 1356 |
st.plotly_chart(fig)
|
|
|
|
| 1362 |
'Percentage (%)': [value / results['sensible_load'] * 100 for value in load_components.values()]
|
| 1363 |
})
|
| 1364 |
|
| 1365 |
+
# Sort by load value for better readability
|
| 1366 |
+
load_df = load_df.sort_values(by='Load (W)', ascending=False).reset_index(drop=True)
|
| 1367 |
+
|
| 1368 |
st.dataframe(load_df.style.format({
|
| 1369 |
'Load (W)': '{:.2f}',
|
| 1370 |
'Percentage (%)': '{:.2f}'
|
| 1371 |
+
}).background_gradient(cmap='Blues', subset=['Percentage (%)']))
|
| 1372 |
|
| 1373 |
# Display detailed results
|
| 1374 |
st.write("### Detailed Results")
|
|
|
|
| 1488 |
x=windows_df['Component'],
|
| 1489 |
y=windows_df['Conduction Heat Gain (W)'],
|
| 1490 |
name='Conduction Heat Gain',
|
| 1491 |
+
marker_color='#1f77b4',
|
| 1492 |
+
text=windows_df['Conduction Heat Gain (W)'].round(1),
|
| 1493 |
+
textposition='auto',
|
| 1494 |
+
hovertemplate='<b>%{x}</b><br>Conduction Heat Gain: %{y:.1f} W<extra></extra>'
|
| 1495 |
))
|
| 1496 |
|
| 1497 |
fig.add_trace(go.Bar(
|
| 1498 |
x=windows_df['Component'],
|
| 1499 |
y=windows_df['Solar Heat Gain (W)'],
|
| 1500 |
name='Solar Heat Gain',
|
| 1501 |
+
marker_color='#ff7f0e',
|
| 1502 |
+
text=windows_df['Solar Heat Gain (W)'].round(1),
|
| 1503 |
+
textposition='auto',
|
| 1504 |
+
hovertemplate='<b>%{x}</b><br>Solar Heat Gain: %{y:.1f} W<extra></extra>'
|
| 1505 |
))
|
| 1506 |
|
| 1507 |
fig.update_layout(
|
| 1508 |
title="Window Heat Gains",
|
| 1509 |
xaxis_title="Window",
|
| 1510 |
yaxis_title="Heat Gain (W)",
|
| 1511 |
+
barmode='stack',
|
| 1512 |
+
font=dict(size=14),
|
| 1513 |
+
title_font=dict(size=18),
|
| 1514 |
+
title_x=0.5, # Center the title
|
| 1515 |
+
margin=dict(t=50, b=50, l=50, r=50),
|
| 1516 |
+
legend=dict(
|
| 1517 |
+
orientation="h",
|
| 1518 |
+
yanchor="bottom",
|
| 1519 |
+
y=1.02,
|
| 1520 |
+
xanchor="right",
|
| 1521 |
+
x=1
|
| 1522 |
+
)
|
| 1523 |
)
|
| 1524 |
|
| 1525 |
st.plotly_chart(fig)
|
|
|
|
| 1727 |
"6. Results"
|
| 1728 |
])
|
| 1729 |
|
| 1730 |
+
# Add direct navigation buttons at the top
|
| 1731 |
+
st.write("### Navigation")
|
| 1732 |
+
st.write("Click on any button below to navigate directly to that section:")
|
| 1733 |
+
|
| 1734 |
+
col1, col2, col3 = st.columns(3)
|
| 1735 |
+
with col1:
|
| 1736 |
+
if st.button("1. Building Information", key="direct_nav_building_info"):
|
| 1737 |
+
st.session_state.cooling_active_tab = "building_info"
|
| 1738 |
+
st.experimental_rerun()
|
| 1739 |
+
|
| 1740 |
+
if st.button("2. Building Envelope", key="direct_nav_building_envelope"):
|
| 1741 |
+
st.session_state.cooling_active_tab = "building_envelope"
|
| 1742 |
+
st.experimental_rerun()
|
| 1743 |
+
|
| 1744 |
+
with col2:
|
| 1745 |
+
if st.button("3. Windows & Doors", key="direct_nav_windows"):
|
| 1746 |
+
st.session_state.cooling_active_tab = "windows"
|
| 1747 |
+
st.experimental_rerun()
|
| 1748 |
+
|
| 1749 |
+
if st.button("4. Internal Loads", key="direct_nav_internal_loads"):
|
| 1750 |
+
st.session_state.cooling_active_tab = "internal_loads"
|
| 1751 |
+
st.experimental_rerun()
|
| 1752 |
+
|
| 1753 |
+
with col3:
|
| 1754 |
+
if st.button("5. Ventilation", key="direct_nav_ventilation"):
|
| 1755 |
+
st.session_state.cooling_active_tab = "ventilation"
|
| 1756 |
+
st.experimental_rerun()
|
| 1757 |
+
|
| 1758 |
+
if st.button("6. Results", key="direct_nav_results"):
|
| 1759 |
+
# Only enable if all previous steps are completed
|
| 1760 |
+
if all(st.session_state.cooling_completed.values()):
|
| 1761 |
+
st.session_state.cooling_active_tab = "results"
|
| 1762 |
+
st.experimental_rerun()
|
| 1763 |
+
else:
|
| 1764 |
+
st.warning("Please complete all previous steps before viewing results.")
|
| 1765 |
+
|
| 1766 |
# Display the active tab
|
| 1767 |
with tabs[0]:
|
| 1768 |
if st.session_state.cooling_active_tab == "building_info":
|
pages/heating_calculator.py
CHANGED
|
@@ -195,7 +195,7 @@ def building_info_form(ref_data):
|
|
| 195 |
warnings.append(ValidationWarning(
|
| 196 |
"Invalid temperature difference",
|
| 197 |
"Indoor temperature should be higher than outdoor temperature for heating load calculation",
|
| 198 |
-
is_critical=
|
| 199 |
))
|
| 200 |
|
| 201 |
# Check if dimensions are reasonable
|
|
@@ -274,14 +274,18 @@ def building_envelope_form(ref_data):
|
|
| 274 |
|
| 275 |
# Get wall material options from reference data
|
| 276 |
wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
|
|
|
|
|
|
|
| 277 |
|
| 278 |
# Display existing wall entries
|
| 279 |
if st.session_state.heating_form_data['building_envelope']['walls']:
|
| 280 |
st.write("Current walls:")
|
| 281 |
walls_df = pd.DataFrame(st.session_state.heating_form_data['building_envelope']['walls'])
|
| 282 |
walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
|
| 283 |
-
|
| 284 |
-
walls_df
|
|
|
|
|
|
|
| 285 |
st.dataframe(walls_df)
|
| 286 |
|
| 287 |
# Add new wall form
|
|
@@ -298,10 +302,39 @@ def building_envelope_form(ref_data):
|
|
| 298 |
key="new_wall_material_heating"
|
| 299 |
)
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
# Get material properties
|
| 302 |
material_data = ref_data.get_material_by_type("walls", wall_material)
|
| 303 |
u_value = material_data['u_value']
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
with col2:
|
| 306 |
wall_area = st.number_input(
|
| 307 |
"Wall Area (m²)",
|
|
@@ -321,7 +354,8 @@ def building_envelope_form(ref_data):
|
|
| 321 |
'material_id': wall_material,
|
| 322 |
'area': wall_area,
|
| 323 |
'u_value': u_value,
|
| 324 |
-
'temp_diff': temp_diff
|
|
|
|
| 325 |
}
|
| 326 |
st.session_state.heating_form_data['building_envelope']['walls'].append(new_wall)
|
| 327 |
st.experimental_rerun()
|
|
@@ -331,6 +365,8 @@ def building_envelope_form(ref_data):
|
|
| 331 |
|
| 332 |
# Get roof material options from reference data
|
| 333 |
roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
|
|
|
|
|
|
|
| 334 |
|
| 335 |
col1, col2 = st.columns(2)
|
| 336 |
|
|
@@ -346,6 +382,28 @@ def building_envelope_form(ref_data):
|
|
| 346 |
material_data = ref_data.get_material_by_type("roofs", roof_material)
|
| 347 |
roof_u_value = material_data['u_value']
|
| 348 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
with col2:
|
| 350 |
roof_area = st.number_input(
|
| 351 |
"Roof Area (m²)",
|
|
@@ -371,6 +429,8 @@ def building_envelope_form(ref_data):
|
|
| 371 |
|
| 372 |
# Get floor material options from reference data
|
| 373 |
floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
|
|
|
|
|
|
|
| 374 |
|
| 375 |
col1, col2 = st.columns(2)
|
| 376 |
|
|
@@ -386,6 +446,28 @@ def building_envelope_form(ref_data):
|
|
| 386 |
material_data = ref_data.get_material_by_type("floors", floor_material)
|
| 387 |
floor_u_value = material_data['u_value']
|
| 388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
with col2:
|
| 390 |
floor_area = st.number_input(
|
| 391 |
"Floor Area (m²)",
|
|
@@ -414,7 +496,7 @@ def building_envelope_form(ref_data):
|
|
| 414 |
warnings.append(ValidationWarning(
|
| 415 |
"No walls defined",
|
| 416 |
"Add at least one wall to continue",
|
| 417 |
-
is_critical=
|
| 418 |
))
|
| 419 |
|
| 420 |
# Check if total wall area is reasonable
|
|
@@ -684,6 +766,30 @@ def ventilation_form(ref_data):
|
|
| 684 |
'air_changes': 0.0
|
| 685 |
}
|
| 686 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
# Infiltration section
|
| 688 |
st.write("### Infiltration")
|
| 689 |
st.write("Infiltration is the unintended air leakage through the building envelope.")
|
|
@@ -715,6 +821,172 @@ def ventilation_form(ref_data):
|
|
| 715 |
st.write("### Ventilation")
|
| 716 |
st.write("Ventilation is the intentional introduction of outside air into the building.")
|
| 717 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
col1, col2 = st.columns(2)
|
| 719 |
|
| 720 |
with col1:
|
|
@@ -1016,10 +1288,20 @@ def calculate_heating_load():
|
|
| 1016 |
'temp_diff': infiltration.get('temp_diff', 0)
|
| 1017 |
}
|
| 1018 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1019 |
# Calculate heating load
|
| 1020 |
results = calculator.calculate_total_heating_load(
|
| 1021 |
building_components=building_components,
|
| 1022 |
-
infiltration=infiltration_data
|
|
|
|
| 1023 |
)
|
| 1024 |
|
| 1025 |
# Calculate annual heating requirement
|
|
@@ -1102,7 +1384,26 @@ def results_page():
|
|
| 1102 |
values=list(component_losses.values()),
|
| 1103 |
names=list(component_losses.keys()),
|
| 1104 |
title="Heating Load Components",
|
| 1105 |
-
color_discrete_sequence=px.colors.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1106 |
)
|
| 1107 |
|
| 1108 |
st.plotly_chart(fig)
|
|
@@ -1113,10 +1414,17 @@ def results_page():
|
|
| 1113 |
'Infiltration & Ventilation': results.get('infiltration_loss', 0)
|
| 1114 |
}
|
| 1115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1116 |
load_df = pd.DataFrame({
|
| 1117 |
'Component': list(load_components.keys()),
|
| 1118 |
'Load (W)': list(load_components.values()),
|
| 1119 |
-
'Percentage (%)': [value / results['total_load'] * 100 for value in load_components.values()]
|
| 1120 |
})
|
| 1121 |
|
| 1122 |
st.dataframe(load_df.style.format({
|
|
@@ -1200,7 +1508,26 @@ def results_page():
|
|
| 1200 |
y='Heat Loss (W)',
|
| 1201 |
title="Heat Loss by Building Component",
|
| 1202 |
color='Component',
|
| 1203 |
-
color_discrete_sequence=px.colors.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
)
|
| 1205 |
|
| 1206 |
st.plotly_chart(fig)
|
|
@@ -1244,7 +1571,28 @@ def results_page():
|
|
| 1244 |
y='Heat Loss (W)',
|
| 1245 |
title="Ventilation & Infiltration Heat Losses",
|
| 1246 |
color='Source',
|
| 1247 |
-
color_discrete_sequence=px.colors.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1248 |
)
|
| 1249 |
|
| 1250 |
st.plotly_chart(fig)
|
|
@@ -1405,6 +1753,42 @@ def heating_calculator():
|
|
| 1405 |
"6. Results"
|
| 1406 |
])
|
| 1407 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1408 |
# Display the active tab
|
| 1409 |
with tabs[0]:
|
| 1410 |
if st.session_state.heating_active_tab == "building_info":
|
|
|
|
| 195 |
warnings.append(ValidationWarning(
|
| 196 |
"Invalid temperature difference",
|
| 197 |
"Indoor temperature should be higher than outdoor temperature for heating load calculation",
|
| 198 |
+
is_critical=False # Changed to non-critical to allow proceeding with warnings
|
| 199 |
))
|
| 200 |
|
| 201 |
# Check if dimensions are reasonable
|
|
|
|
| 274 |
|
| 275 |
# Get wall material options from reference data
|
| 276 |
wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
|
| 277 |
+
# Add custom option
|
| 278 |
+
wall_material_options["custom_walls"] = "Custom Wall (User-defined)"
|
| 279 |
|
| 280 |
# Display existing wall entries
|
| 281 |
if st.session_state.heating_form_data['building_envelope']['walls']:
|
| 282 |
st.write("Current walls:")
|
| 283 |
walls_df = pd.DataFrame(st.session_state.heating_form_data['building_envelope']['walls'])
|
| 284 |
walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Unknown"))
|
| 285 |
+
# Add orientation column with default value if not present
|
| 286 |
+
walls_df['orientation'] = walls_df['orientation'].fillna('not specified')
|
| 287 |
+
walls_df = walls_df[['name', 'Material', 'area', 'u_value', 'orientation']]
|
| 288 |
+
walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)', 'Orientation']
|
| 289 |
st.dataframe(walls_df)
|
| 290 |
|
| 291 |
# Add new wall form
|
|
|
|
| 302 |
key="new_wall_material_heating"
|
| 303 |
)
|
| 304 |
|
| 305 |
+
# Add wall orientation selection
|
| 306 |
+
wall_orientation = st.selectbox(
|
| 307 |
+
"Wall Orientation",
|
| 308 |
+
options=["north", "east", "south", "west"],
|
| 309 |
+
key="new_wall_orientation_heating"
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
# Get material properties
|
| 313 |
material_data = ref_data.get_material_by_type("walls", wall_material)
|
| 314 |
u_value = material_data['u_value']
|
| 315 |
|
| 316 |
+
# Add custom U-value input if custom material is selected
|
| 317 |
+
if wall_material == "custom_walls":
|
| 318 |
+
u_value = st.number_input(
|
| 319 |
+
"Custom U-Value (W/m²°C)",
|
| 320 |
+
value=1.0,
|
| 321 |
+
min_value=0.1,
|
| 322 |
+
max_value=5.0,
|
| 323 |
+
step=0.1,
|
| 324 |
+
key="custom_wall_u_value_heating"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
# Store custom material in session state
|
| 328 |
+
if "custom_materials" not in st.session_state:
|
| 329 |
+
st.session_state.custom_materials = {}
|
| 330 |
+
|
| 331 |
+
st.session_state.custom_materials["walls"] = {
|
| 332 |
+
"name": "Custom Wall",
|
| 333 |
+
"u_value": u_value,
|
| 334 |
+
"r_value": 1.0 / u_value if u_value > 0 else 1.0,
|
| 335 |
+
"description": "Custom wall with user-defined properties"
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
with col2:
|
| 339 |
wall_area = st.number_input(
|
| 340 |
"Wall Area (m²)",
|
|
|
|
| 354 |
'material_id': wall_material,
|
| 355 |
'area': wall_area,
|
| 356 |
'u_value': u_value,
|
| 357 |
+
'temp_diff': temp_diff,
|
| 358 |
+
'orientation': wall_orientation # Add orientation to wall data
|
| 359 |
}
|
| 360 |
st.session_state.heating_form_data['building_envelope']['walls'].append(new_wall)
|
| 361 |
st.experimental_rerun()
|
|
|
|
| 365 |
|
| 366 |
# Get roof material options from reference data
|
| 367 |
roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
|
| 368 |
+
# Add custom option
|
| 369 |
+
roof_material_options["custom_roofs"] = "Custom Roof (User-defined)"
|
| 370 |
|
| 371 |
col1, col2 = st.columns(2)
|
| 372 |
|
|
|
|
| 382 |
material_data = ref_data.get_material_by_type("roofs", roof_material)
|
| 383 |
roof_u_value = material_data['u_value']
|
| 384 |
|
| 385 |
+
# Add custom U-value input if custom material is selected
|
| 386 |
+
if roof_material == "custom_roofs":
|
| 387 |
+
roof_u_value = st.number_input(
|
| 388 |
+
"Custom Roof U-Value (W/m²°C)",
|
| 389 |
+
value=1.0,
|
| 390 |
+
min_value=0.1,
|
| 391 |
+
max_value=5.0,
|
| 392 |
+
step=0.1,
|
| 393 |
+
key="custom_roof_u_value_heating"
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
# Store custom material in session state
|
| 397 |
+
if "custom_materials" not in st.session_state:
|
| 398 |
+
st.session_state.custom_materials = {}
|
| 399 |
+
|
| 400 |
+
st.session_state.custom_materials["roofs"] = {
|
| 401 |
+
"name": "Custom Roof",
|
| 402 |
+
"u_value": roof_u_value,
|
| 403 |
+
"r_value": 1.0 / roof_u_value if roof_u_value > 0 else 1.0,
|
| 404 |
+
"description": "Custom roof with user-defined properties"
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
with col2:
|
| 408 |
roof_area = st.number_input(
|
| 409 |
"Roof Area (m²)",
|
|
|
|
| 429 |
|
| 430 |
# Get floor material options from reference data
|
| 431 |
floor_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['floors'].items()}
|
| 432 |
+
# Add custom option
|
| 433 |
+
floor_material_options["custom_floors"] = "Custom Floor (User-defined)"
|
| 434 |
|
| 435 |
col1, col2 = st.columns(2)
|
| 436 |
|
|
|
|
| 446 |
material_data = ref_data.get_material_by_type("floors", floor_material)
|
| 447 |
floor_u_value = material_data['u_value']
|
| 448 |
|
| 449 |
+
# Add custom U-value input if custom material is selected
|
| 450 |
+
if floor_material == "custom_floors":
|
| 451 |
+
floor_u_value = st.number_input(
|
| 452 |
+
"Custom Floor U-Value (W/m²°C)",
|
| 453 |
+
value=1.0,
|
| 454 |
+
min_value=0.1,
|
| 455 |
+
max_value=5.0,
|
| 456 |
+
step=0.1,
|
| 457 |
+
key="custom_floor_u_value_heating"
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Store custom material in session state
|
| 461 |
+
if "custom_materials" not in st.session_state:
|
| 462 |
+
st.session_state.custom_materials = {}
|
| 463 |
+
|
| 464 |
+
st.session_state.custom_materials["floors"] = {
|
| 465 |
+
"name": "Custom Floor",
|
| 466 |
+
"u_value": floor_u_value,
|
| 467 |
+
"r_value": 1.0 / floor_u_value if floor_u_value > 0 else 1.0,
|
| 468 |
+
"description": "Custom floor with user-defined properties"
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
with col2:
|
| 472 |
floor_area = st.number_input(
|
| 473 |
"Floor Area (m²)",
|
|
|
|
| 496 |
warnings.append(ValidationWarning(
|
| 497 |
"No walls defined",
|
| 498 |
"Add at least one wall to continue",
|
| 499 |
+
is_critical=False # Changed to non-critical to allow proceeding with warnings
|
| 500 |
))
|
| 501 |
|
| 502 |
# Check if total wall area is reasonable
|
|
|
|
| 766 |
'air_changes': 0.0
|
| 767 |
}
|
| 768 |
|
| 769 |
+
# Initialize internal loads data if not already in session state
|
| 770 |
+
if 'internal_loads' not in st.session_state.heating_form_data:
|
| 771 |
+
st.session_state.heating_form_data['internal_loads'] = {}
|
| 772 |
+
|
| 773 |
+
if 'occupants' not in st.session_state.heating_form_data['internal_loads']:
|
| 774 |
+
st.session_state.heating_form_data['internal_loads']['occupants'] = {
|
| 775 |
+
'count': 4,
|
| 776 |
+
'activity_level': 'seated_resting'
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
if 'lighting' not in st.session_state.heating_form_data['internal_loads']:
|
| 780 |
+
st.session_state.heating_form_data['internal_loads']['lighting'] = {
|
| 781 |
+
'type': 'led',
|
| 782 |
+
'power_density': 5.0 # W/m²
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
if 'appliances' not in st.session_state.heating_form_data['internal_loads']:
|
| 786 |
+
st.session_state.heating_form_data['internal_loads']['appliances'] = {
|
| 787 |
+
'kitchen': True,
|
| 788 |
+
'living_room': True,
|
| 789 |
+
'bedroom': True,
|
| 790 |
+
'office': False
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
# Infiltration section
|
| 794 |
st.write("### Infiltration")
|
| 795 |
st.write("Infiltration is the unintended air leakage through the building envelope.")
|
|
|
|
| 821 |
st.write("### Ventilation")
|
| 822 |
st.write("Ventilation is the intentional introduction of outside air into the building.")
|
| 823 |
|
| 824 |
+
# Internal Loads section
|
| 825 |
+
st.write("### Internal Loads")
|
| 826 |
+
st.write("Internal loads are heat sources inside the building that reduce heating requirements.")
|
| 827 |
+
|
| 828 |
+
# Occupants section
|
| 829 |
+
st.write("#### Occupants")
|
| 830 |
+
|
| 831 |
+
col1, col2 = st.columns(2)
|
| 832 |
+
|
| 833 |
+
with col1:
|
| 834 |
+
occupant_count = st.number_input(
|
| 835 |
+
"Number of Occupants",
|
| 836 |
+
value=int(st.session_state.heating_form_data['internal_loads']['occupants'].get('count', 4)),
|
| 837 |
+
min_value=1,
|
| 838 |
+
step=1,
|
| 839 |
+
key="occupant_count_heating"
|
| 840 |
+
)
|
| 841 |
+
|
| 842 |
+
with col2:
|
| 843 |
+
# Get activity level options from reference data
|
| 844 |
+
activity_options = {act_id: act_data['name'] for act_id, act_data in ref_data.internal_loads['people'].items()}
|
| 845 |
+
|
| 846 |
+
activity_level = st.selectbox(
|
| 847 |
+
"Activity Level",
|
| 848 |
+
options=list(activity_options.keys()),
|
| 849 |
+
format_func=lambda x: activity_options[x],
|
| 850 |
+
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,
|
| 851 |
+
key="activity_level_heating"
|
| 852 |
+
)
|
| 853 |
+
|
| 854 |
+
# Get heat gain per person
|
| 855 |
+
activity_data = ref_data.get_internal_load('people', activity_level)
|
| 856 |
+
sensible_heat_pp = activity_data['sensible_heat']
|
| 857 |
+
latent_heat_pp = activity_data['latent_heat']
|
| 858 |
+
total_heat_pp = sensible_heat_pp + latent_heat_pp
|
| 859 |
+
|
| 860 |
+
st.write(f"Heat gain per person: {total_heat_pp} W ({sensible_heat_pp} W sensible + {latent_heat_pp} W latent)")
|
| 861 |
+
st.write(f"Total occupant heat gain: {total_heat_pp * occupant_count} W")
|
| 862 |
+
|
| 863 |
+
# Save occupants data
|
| 864 |
+
st.session_state.heating_form_data['internal_loads']['occupants'] = {
|
| 865 |
+
'count': occupant_count,
|
| 866 |
+
'activity_level': activity_level,
|
| 867 |
+
'sensible_heat_pp': sensible_heat_pp,
|
| 868 |
+
'latent_heat_pp': latent_heat_pp,
|
| 869 |
+
'total_heat_gain': total_heat_pp * occupant_count
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
# Lighting section
|
| 873 |
+
st.write("#### Lighting")
|
| 874 |
+
|
| 875 |
+
col1, col2 = st.columns(2)
|
| 876 |
+
|
| 877 |
+
with col1:
|
| 878 |
+
# Get lighting type options from reference data
|
| 879 |
+
lighting_options = {light_id: light_data['name'] for light_id, light_data in ref_data.internal_loads['lighting'].items()}
|
| 880 |
+
|
| 881 |
+
lighting_type = st.selectbox(
|
| 882 |
+
"Lighting Type",
|
| 883 |
+
options=list(lighting_options.keys()),
|
| 884 |
+
format_func=lambda x: lighting_options[x],
|
| 885 |
+
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,
|
| 886 |
+
key="lighting_type_heating"
|
| 887 |
+
)
|
| 888 |
+
|
| 889 |
+
with col2:
|
| 890 |
+
lighting_power_density = st.number_input(
|
| 891 |
+
"Lighting Power Density (W/m²)",
|
| 892 |
+
value=float(st.session_state.heating_form_data['internal_loads']['lighting'].get('power_density', 5.0)),
|
| 893 |
+
min_value=1.0,
|
| 894 |
+
max_value=20.0,
|
| 895 |
+
step=0.5,
|
| 896 |
+
help="Typical values: Residential 5-10 W/m², Office 10-15 W/m²",
|
| 897 |
+
key="lighting_power_density_heating"
|
| 898 |
+
)
|
| 899 |
+
|
| 900 |
+
# Get lighting heat factor
|
| 901 |
+
lighting_data = ref_data.get_internal_load('lighting', lighting_type)
|
| 902 |
+
lighting_heat_factor = lighting_data['heat_factor']
|
| 903 |
+
|
| 904 |
+
# Calculate lighting heat gain
|
| 905 |
+
floor_area = st.session_state.heating_form_data['building_info'].get('floor_area', 80.0)
|
| 906 |
+
lighting_heat_gain = lighting_power_density * floor_area * lighting_heat_factor
|
| 907 |
+
|
| 908 |
+
st.write(f"Lighting heat factor: {lighting_heat_factor}")
|
| 909 |
+
st.write(f"Total lighting heat gain: {lighting_heat_gain:.2f} W")
|
| 910 |
+
|
| 911 |
+
# Save lighting data
|
| 912 |
+
st.session_state.heating_form_data['internal_loads']['lighting'] = {
|
| 913 |
+
'type': lighting_type,
|
| 914 |
+
'power_density': lighting_power_density,
|
| 915 |
+
'heat_factor': lighting_heat_factor,
|
| 916 |
+
'total_heat_gain': lighting_heat_gain
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
# Equipment section
|
| 920 |
+
st.write("#### Equipment")
|
| 921 |
+
st.write("Select the equipment present in your space:")
|
| 922 |
+
|
| 923 |
+
col1, col2 = st.columns(2)
|
| 924 |
+
|
| 925 |
+
with col1:
|
| 926 |
+
has_kitchen = st.checkbox(
|
| 927 |
+
"Kitchen Appliances",
|
| 928 |
+
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('kitchen', True),
|
| 929 |
+
help="Refrigerator, stove, microwave, etc.",
|
| 930 |
+
key="has_kitchen_heating"
|
| 931 |
+
)
|
| 932 |
+
|
| 933 |
+
has_living_room = st.checkbox(
|
| 934 |
+
"Living Room Equipment",
|
| 935 |
+
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('living_room', True),
|
| 936 |
+
help="TV, audio equipment, etc.",
|
| 937 |
+
key="has_living_room_heating"
|
| 938 |
+
)
|
| 939 |
+
|
| 940 |
+
with col2:
|
| 941 |
+
has_bedroom = st.checkbox(
|
| 942 |
+
"Bedroom Equipment",
|
| 943 |
+
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('bedroom', True),
|
| 944 |
+
help="TV, chargers, etc.",
|
| 945 |
+
key="has_bedroom_heating"
|
| 946 |
+
)
|
| 947 |
+
|
| 948 |
+
has_office = st.checkbox(
|
| 949 |
+
"Office Equipment",
|
| 950 |
+
value=st.session_state.heating_form_data['internal_loads']['appliances'].get('office', False),
|
| 951 |
+
help="Computer, printer, etc.",
|
| 952 |
+
key="has_office_heating"
|
| 953 |
+
)
|
| 954 |
+
|
| 955 |
+
# Calculate equipment heat gain
|
| 956 |
+
equipment_watts = 0
|
| 957 |
+
|
| 958 |
+
if has_kitchen:
|
| 959 |
+
equipment_watts += 1000 # Kitchen appliances
|
| 960 |
+
if has_living_room:
|
| 961 |
+
equipment_watts += 300 # Living room equipment
|
| 962 |
+
if has_bedroom:
|
| 963 |
+
equipment_watts += 150 # Bedroom equipment
|
| 964 |
+
if has_office:
|
| 965 |
+
equipment_watts += 450 # Office equipment
|
| 966 |
+
|
| 967 |
+
st.write(f"Total equipment heat gain: {equipment_watts} W")
|
| 968 |
+
|
| 969 |
+
# Save appliances data
|
| 970 |
+
st.session_state.heating_form_data['internal_loads']['appliances'] = {
|
| 971 |
+
'kitchen': has_kitchen,
|
| 972 |
+
'living_room': has_living_room,
|
| 973 |
+
'bedroom': has_bedroom,
|
| 974 |
+
'office': has_office,
|
| 975 |
+
'total_heat_gain': equipment_watts
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
# Calculate total internal heat gain
|
| 979 |
+
total_internal_gain = (
|
| 980 |
+
st.session_state.heating_form_data['internal_loads']['occupants']['total_heat_gain'] +
|
| 981 |
+
st.session_state.heating_form_data['internal_loads']['lighting']['total_heat_gain'] +
|
| 982 |
+
st.session_state.heating_form_data['internal_loads']['appliances']['total_heat_gain']
|
| 983 |
+
)
|
| 984 |
+
|
| 985 |
+
st.write(f"Total internal heat gain: {total_internal_gain:.2f} W")
|
| 986 |
+
|
| 987 |
+
# Save total internal gain
|
| 988 |
+
st.session_state.heating_form_data['internal_loads']['total_heat_gain'] = total_internal_gain
|
| 989 |
+
|
| 990 |
col1, col2 = st.columns(2)
|
| 991 |
|
| 992 |
with col1:
|
|
|
|
| 1288 |
'temp_diff': infiltration.get('temp_diff', 0)
|
| 1289 |
}
|
| 1290 |
|
| 1291 |
+
# Prepare internal loads data
|
| 1292 |
+
internal_loads = None
|
| 1293 |
+
if 'internal_loads' in form_data:
|
| 1294 |
+
internal_loads = {
|
| 1295 |
+
'num_people': form_data['internal_loads']['occupants'].get('count', 0),
|
| 1296 |
+
'has_kitchen': form_data['internal_loads']['appliances'].get('kitchen', False),
|
| 1297 |
+
'equipment_watts': form_data['internal_loads']['appliances'].get('total_heat_gain', 0)
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
# Calculate heating load
|
| 1301 |
results = calculator.calculate_total_heating_load(
|
| 1302 |
building_components=building_components,
|
| 1303 |
+
infiltration=infiltration_data,
|
| 1304 |
+
internal_gains=internal_loads
|
| 1305 |
)
|
| 1306 |
|
| 1307 |
# Calculate annual heating requirement
|
|
|
|
| 1384 |
values=list(component_losses.values()),
|
| 1385 |
names=list(component_losses.keys()),
|
| 1386 |
title="Heating Load Components",
|
| 1387 |
+
color_discrete_sequence=px.colors.sequential.Viridis,
|
| 1388 |
+
hole=0.4, # Create a donut chart for better readability
|
| 1389 |
+
labels={'label': 'Component', 'value': 'Heat Loss (W)'}
|
| 1390 |
+
)
|
| 1391 |
+
|
| 1392 |
+
# Improve layout and formatting
|
| 1393 |
+
fig.update_traces(
|
| 1394 |
+
textposition='inside',
|
| 1395 |
+
textinfo='percent+label',
|
| 1396 |
+
hoverinfo='label+percent+value',
|
| 1397 |
+
marker=dict(line=dict(color='#FFFFFF', width=2))
|
| 1398 |
+
)
|
| 1399 |
+
|
| 1400 |
+
# Improve layout
|
| 1401 |
+
fig.update_layout(
|
| 1402 |
+
legend_title_text='Building Components',
|
| 1403 |
+
font=dict(size=14),
|
| 1404 |
+
title_font=dict(size=18),
|
| 1405 |
+
title_x=0.5, # Center the title
|
| 1406 |
+
margin=dict(t=50, b=50, l=50, r=50)
|
| 1407 |
)
|
| 1408 |
|
| 1409 |
st.plotly_chart(fig)
|
|
|
|
| 1414 |
'Infiltration & Ventilation': results.get('infiltration_loss', 0)
|
| 1415 |
}
|
| 1416 |
|
| 1417 |
+
# Add internal gains and solar gains if available
|
| 1418 |
+
if 'internal_gain' in results and results['internal_gain'] > 0:
|
| 1419 |
+
load_components['Internal Gains (reduction)'] = -results['internal_gain']
|
| 1420 |
+
|
| 1421 |
+
if 'wall_solar_gain' in results and results['wall_solar_gain'] > 0:
|
| 1422 |
+
load_components['Solar Gains (reduction)'] = -results['wall_solar_gain']
|
| 1423 |
+
|
| 1424 |
load_df = pd.DataFrame({
|
| 1425 |
'Component': list(load_components.keys()),
|
| 1426 |
'Load (W)': list(load_components.values()),
|
| 1427 |
+
'Percentage (%)': [abs(value) / results['total_load'] * 100 for value in load_components.values()]
|
| 1428 |
})
|
| 1429 |
|
| 1430 |
st.dataframe(load_df.style.format({
|
|
|
|
| 1508 |
y='Heat Loss (W)',
|
| 1509 |
title="Heat Loss by Building Component",
|
| 1510 |
color='Component',
|
| 1511 |
+
color_discrete_sequence=px.colors.sequential.Viridis,
|
| 1512 |
+
text='Heat Loss (W)'
|
| 1513 |
+
)
|
| 1514 |
+
|
| 1515 |
+
# Improve layout and formatting
|
| 1516 |
+
fig.update_traces(
|
| 1517 |
+
texttemplate='%{text:.1f} W',
|
| 1518 |
+
textposition='outside',
|
| 1519 |
+
hovertemplate='<b>%{x}</b><br>Heat Loss: %{y:.1f} W<extra></extra>'
|
| 1520 |
+
)
|
| 1521 |
+
|
| 1522 |
+
# Improve layout
|
| 1523 |
+
fig.update_layout(
|
| 1524 |
+
xaxis_title="Building Component",
|
| 1525 |
+
yaxis_title="Heat Loss (W)",
|
| 1526 |
+
font=dict(size=14),
|
| 1527 |
+
title_font=dict(size=18),
|
| 1528 |
+
title_x=0.5, # Center the title
|
| 1529 |
+
margin=dict(t=50, b=50, l=50, r=50),
|
| 1530 |
+
xaxis={'categoryorder':'total descending'} # Sort by highest heat loss
|
| 1531 |
)
|
| 1532 |
|
| 1533 |
st.plotly_chart(fig)
|
|
|
|
| 1571 |
y='Heat Loss (W)',
|
| 1572 |
title="Ventilation & Infiltration Heat Losses",
|
| 1573 |
color='Source',
|
| 1574 |
+
color_discrete_sequence=px.colors.sequential.Plasma,
|
| 1575 |
+
text='Heat Loss (W)'
|
| 1576 |
+
)
|
| 1577 |
+
|
| 1578 |
+
# Improve layout and formatting
|
| 1579 |
+
fig.update_traces(
|
| 1580 |
+
texttemplate='%{text:.1f} W',
|
| 1581 |
+
textposition='outside',
|
| 1582 |
+
hovertemplate='<b>%{x}</b><br>Heat Loss: %{y:.1f} W<br>Air Changes: %{customdata[0]:.2f} ACH<extra></extra>'
|
| 1583 |
+
)
|
| 1584 |
+
|
| 1585 |
+
# Add custom data for hover
|
| 1586 |
+
fig.update_traces(customdata=ventilation_df[['Air Changes per Hour']])
|
| 1587 |
+
|
| 1588 |
+
# Improve layout
|
| 1589 |
+
fig.update_layout(
|
| 1590 |
+
xaxis_title="Ventilation Source",
|
| 1591 |
+
yaxis_title="Heat Loss (W)",
|
| 1592 |
+
font=dict(size=14),
|
| 1593 |
+
title_font=dict(size=18),
|
| 1594 |
+
title_x=0.5, # Center the title
|
| 1595 |
+
margin=dict(t=50, b=50, l=50, r=50)
|
| 1596 |
)
|
| 1597 |
|
| 1598 |
st.plotly_chart(fig)
|
|
|
|
| 1753 |
"6. Results"
|
| 1754 |
])
|
| 1755 |
|
| 1756 |
+
# Add direct navigation buttons at the top
|
| 1757 |
+
st.write("### Navigation")
|
| 1758 |
+
st.write("Click on any button below to navigate directly to that section:")
|
| 1759 |
+
|
| 1760 |
+
col1, col2, col3 = st.columns(3)
|
| 1761 |
+
with col1:
|
| 1762 |
+
if st.button("1. Building Information", key="direct_nav_heating_info"):
|
| 1763 |
+
st.session_state.heating_active_tab = "building_info"
|
| 1764 |
+
st.experimental_rerun()
|
| 1765 |
+
|
| 1766 |
+
if st.button("2. Building Envelope", key="direct_nav_heating_envelope"):
|
| 1767 |
+
st.session_state.heating_active_tab = "building_envelope"
|
| 1768 |
+
st.experimental_rerun()
|
| 1769 |
+
|
| 1770 |
+
with col2:
|
| 1771 |
+
if st.button("3. Windows & Doors", key="direct_nav_heating_windows"):
|
| 1772 |
+
st.session_state.heating_active_tab = "windows"
|
| 1773 |
+
st.experimental_rerun()
|
| 1774 |
+
|
| 1775 |
+
if st.button("4. Ventilation", key="direct_nav_heating_ventilation"):
|
| 1776 |
+
st.session_state.heating_active_tab = "ventilation"
|
| 1777 |
+
st.experimental_rerun()
|
| 1778 |
+
|
| 1779 |
+
with col3:
|
| 1780 |
+
if st.button("5. Occupancy", key="direct_nav_heating_occupancy"):
|
| 1781 |
+
st.session_state.heating_active_tab = "occupancy"
|
| 1782 |
+
st.experimental_rerun()
|
| 1783 |
+
|
| 1784 |
+
if st.button("6. Results", key="direct_nav_heating_results"):
|
| 1785 |
+
# Only enable if all previous steps are completed
|
| 1786 |
+
if all(st.session_state.heating_completed.values()):
|
| 1787 |
+
st.session_state.heating_active_tab = "results"
|
| 1788 |
+
st.experimental_rerun()
|
| 1789 |
+
else:
|
| 1790 |
+
st.warning("Please complete all previous steps before viewing results.")
|
| 1791 |
+
|
| 1792 |
# Display the active tab
|
| 1793 |
with tabs[0]:
|
| 1794 |
if st.session_state.heating_active_tab == "building_info":
|
reference_data.py
CHANGED
|
@@ -481,6 +481,21 @@ class ReferenceData:
|
|
| 481 |
Returns:
|
| 482 |
dict: Material properties
|
| 483 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
if material_type in self.materials and material_id in self.materials[material_type]:
|
| 485 |
return self.materials[material_type][material_id]
|
| 486 |
return None
|
|
|
|
| 481 |
Returns:
|
| 482 |
dict: Material properties
|
| 483 |
"""
|
| 484 |
+
# Check if this is a custom material (custom_[type])
|
| 485 |
+
if material_id == f"custom_{material_type}":
|
| 486 |
+
# Return the custom material from session state if available
|
| 487 |
+
import streamlit as st
|
| 488 |
+
if "custom_materials" in st.session_state and material_type in st.session_state.custom_materials:
|
| 489 |
+
return st.session_state.custom_materials[material_type]
|
| 490 |
+
# Return a default custom material template if not in session state
|
| 491 |
+
return {
|
| 492 |
+
"name": f"Custom {material_type[:-1]}", # Remove 's' from end
|
| 493 |
+
"u_value": 1.0, # Default U-value
|
| 494 |
+
"r_value": 1.0, # Default R-value
|
| 495 |
+
"description": f"Custom {material_type[:-1]} with user-defined properties"
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
# Return predefined material
|
| 499 |
if material_type in self.materials and material_id in self.materials[material_type]:
|
| 500 |
return self.materials[material_type][material_id]
|
| 501 |
return None
|
requirements.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
numpy
|
|
|
|
| 1 |
+
streamlit>=1.20.0
|
| 2 |
+
pandas>=1.5.0
|
| 3 |
+
matplotlib>=3.5.0
|
| 4 |
+
numpy>=1.20.0
|
tests/__pycache__/test_enhancements.cpython-310.pyc
ADDED
|
Binary file (6.28 kB). View file
|
|
|
tests/test_enhancements.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script for the enhanced HVAC calculator.
|
| 3 |
+
|
| 4 |
+
This script tests all the enhancements made to the HVAC calculator:
|
| 5 |
+
1. Window shading calculation fix
|
| 6 |
+
2. Scenario comparison functionality
|
| 7 |
+
3. Attribution and purpose information
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import sys
|
| 11 |
+
import os
|
| 12 |
+
import unittest
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import json
|
| 15 |
+
|
| 16 |
+
# Add the parent directory to the path
|
| 17 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 18 |
+
|
| 19 |
+
# Import required modules
|
| 20 |
+
from utils.scenario_manager import ScenarioManager
|
| 21 |
+
|
| 22 |
+
class MockReferenceData:
|
| 23 |
+
"""Mock implementation of ReferenceData for testing."""
|
| 24 |
+
|
| 25 |
+
def __init__(self):
|
| 26 |
+
"""Initialize mock reference data."""
|
| 27 |
+
self.locations = {
|
| 28 |
+
'london': {
|
| 29 |
+
'name': 'London',
|
| 30 |
+
'summer_design_temp': 32.0,
|
| 31 |
+
'winter_design_temp': -1.0,
|
| 32 |
+
'solar_intensity': {
|
| 33 |
+
'north': 100,
|
| 34 |
+
'south': 500,
|
| 35 |
+
'east': 300,
|
| 36 |
+
'west': 300
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
self.glass_types = {
|
| 42 |
+
'single': {
|
| 43 |
+
'name': 'Single Glazing',
|
| 44 |
+
'u_value': 5.8,
|
| 45 |
+
'shgf': {
|
| 46 |
+
'north': 200,
|
| 47 |
+
'south': 500,
|
| 48 |
+
'east': 350,
|
| 49 |
+
'west': 350
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
self.shading_factors = {
|
| 55 |
+
'none': {
|
| 56 |
+
'name': 'No shading',
|
| 57 |
+
'factor': 0.0,
|
| 58 |
+
'description': 'No shading devices'
|
| 59 |
+
},
|
| 60 |
+
'internal_blinds': {
|
| 61 |
+
'name': 'Internal venetian blinds',
|
| 62 |
+
'factor': 0.4,
|
| 63 |
+
'description': 'Internal venetian blinds'
|
| 64 |
+
},
|
| 65 |
+
'external_awning': {
|
| 66 |
+
'name': 'External awning',
|
| 67 |
+
'factor': 0.7,
|
| 68 |
+
'description': 'External awning or overhang'
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
def get_location(self, location_id):
|
| 73 |
+
"""Get location data."""
|
| 74 |
+
return self.locations.get(location_id)
|
| 75 |
+
|
| 76 |
+
def get_glass_type(self, glass_type):
|
| 77 |
+
"""Get glass type data."""
|
| 78 |
+
return self.glass_types.get(glass_type)
|
| 79 |
+
|
| 80 |
+
def get_shading_factor(self, shading_id):
|
| 81 |
+
"""Get shading factor data."""
|
| 82 |
+
return self.shading_factors.get(shading_id)
|
| 83 |
+
|
| 84 |
+
class TestShadingFix(unittest.TestCase):
|
| 85 |
+
"""Test the window shading calculation fix."""
|
| 86 |
+
|
| 87 |
+
def test_shading_calculation(self):
|
| 88 |
+
"""Test the shading calculation logic directly."""
|
| 89 |
+
# Test with no shading
|
| 90 |
+
shading_data = {'factor': 0.0}
|
| 91 |
+
shade_factor = 1.0 - shading_data['factor']
|
| 92 |
+
self.assertEqual(shade_factor, 1.0)
|
| 93 |
+
|
| 94 |
+
# Test with internal blinds
|
| 95 |
+
shading_data = {'factor': 0.4}
|
| 96 |
+
shade_factor = 1.0 - shading_data['factor']
|
| 97 |
+
self.assertAlmostEqual(shade_factor, 0.6)
|
| 98 |
+
|
| 99 |
+
# Test with external awning
|
| 100 |
+
shading_data = {'factor': 0.7}
|
| 101 |
+
shade_factor = 1.0 - shading_data['factor']
|
| 102 |
+
self.assertAlmostEqual(shade_factor, 0.3)
|
| 103 |
+
|
| 104 |
+
class TestScenarioManager(unittest.TestCase):
|
| 105 |
+
"""Test the scenario comparison functionality."""
|
| 106 |
+
|
| 107 |
+
def setUp(self):
|
| 108 |
+
"""Set up the test case."""
|
| 109 |
+
# Create a temporary directory for scenarios
|
| 110 |
+
self.test_dir = Path("test_scenarios")
|
| 111 |
+
self.test_dir.mkdir(exist_ok=True)
|
| 112 |
+
|
| 113 |
+
# Initialize scenario manager
|
| 114 |
+
self.scenario_manager = ScenarioManager(base_path=str(self.test_dir))
|
| 115 |
+
|
| 116 |
+
# Create sample data
|
| 117 |
+
self.sample_form_data = {
|
| 118 |
+
"building_info": {
|
| 119 |
+
"location": "london",
|
| 120 |
+
"indoor_temp": 24.0,
|
| 121 |
+
"building_volume": 500.0,
|
| 122 |
+
"air_changes": 1.0
|
| 123 |
+
},
|
| 124 |
+
"components": [
|
| 125 |
+
{
|
| 126 |
+
"type": "wall",
|
| 127 |
+
"area": 50.0,
|
| 128 |
+
"u_value": 0.5,
|
| 129 |
+
"orientation": "south"
|
| 130 |
+
}
|
| 131 |
+
],
|
| 132 |
+
"windows": [
|
| 133 |
+
{
|
| 134 |
+
"orientation": "south",
|
| 135 |
+
"area": 10.0,
|
| 136 |
+
"u_value": 2.8,
|
| 137 |
+
"glass_type": "single",
|
| 138 |
+
"shading": "none"
|
| 139 |
+
}
|
| 140 |
+
]
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
self.sample_results = {
|
| 144 |
+
"total_load_w": 5000.0,
|
| 145 |
+
"total_load_kw": 5.0,
|
| 146 |
+
"recommended_size_kw": 5.75,
|
| 147 |
+
"breakdown_percentage": {
|
| 148 |
+
"transmission": 40.0,
|
| 149 |
+
"solar": 30.0,
|
| 150 |
+
"ventilation": 20.0,
|
| 151 |
+
"internal": 10.0
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
def tearDown(self):
|
| 156 |
+
"""Clean up after the test."""
|
| 157 |
+
# Remove test directory
|
| 158 |
+
import shutil
|
| 159 |
+
shutil.rmtree(self.test_dir, ignore_errors=True)
|
| 160 |
+
|
| 161 |
+
def test_save_scenario(self):
|
| 162 |
+
"""Test saving a scenario."""
|
| 163 |
+
# Save a scenario
|
| 164 |
+
path = self.scenario_manager.save_scenario(
|
| 165 |
+
name="Test Scenario",
|
| 166 |
+
description="Test description",
|
| 167 |
+
calculator_type="cooling",
|
| 168 |
+
form_data=self.sample_form_data,
|
| 169 |
+
results=self.sample_results
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
# Verify the file exists
|
| 173 |
+
self.assertTrue(os.path.exists(path))
|
| 174 |
+
|
| 175 |
+
def test_load_scenario(self):
|
| 176 |
+
"""Test loading a scenario."""
|
| 177 |
+
# Save a scenario
|
| 178 |
+
path = self.scenario_manager.save_scenario(
|
| 179 |
+
name="Test Scenario",
|
| 180 |
+
description="Test description",
|
| 181 |
+
calculator_type="cooling",
|
| 182 |
+
form_data=self.sample_form_data,
|
| 183 |
+
results=self.sample_results
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Load the scenario
|
| 187 |
+
scenario = self.scenario_manager.load_scenario(path)
|
| 188 |
+
|
| 189 |
+
# Verify the scenario data
|
| 190 |
+
self.assertIsNotNone(scenario)
|
| 191 |
+
self.assertEqual(scenario["name"], "Test Scenario")
|
| 192 |
+
self.assertEqual(scenario["description"], "Test description")
|
| 193 |
+
self.assertEqual(scenario["calculator_type"], "cooling")
|
| 194 |
+
self.assertEqual(scenario["form_data"], self.sample_form_data)
|
| 195 |
+
self.assertEqual(scenario["results"], self.sample_results)
|
| 196 |
+
|
| 197 |
+
def test_compare_scenarios(self):
|
| 198 |
+
"""Test comparing scenarios."""
|
| 199 |
+
# Save two scenarios
|
| 200 |
+
path1 = self.scenario_manager.save_scenario(
|
| 201 |
+
name="Scenario 1",
|
| 202 |
+
description="Base scenario",
|
| 203 |
+
calculator_type="cooling",
|
| 204 |
+
form_data=self.sample_form_data,
|
| 205 |
+
results=self.sample_results
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
# Create a modified results for scenario 2
|
| 209 |
+
modified_results = self.sample_results.copy()
|
| 210 |
+
modified_results["total_load_kw"] = 6.0
|
| 211 |
+
modified_results["recommended_size_kw"] = 6.9
|
| 212 |
+
modified_results["breakdown_percentage"] = {
|
| 213 |
+
"transmission": 45.0,
|
| 214 |
+
"solar": 25.0,
|
| 215 |
+
"ventilation": 20.0,
|
| 216 |
+
"internal": 10.0
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
path2 = self.scenario_manager.save_scenario(
|
| 220 |
+
name="Scenario 2",
|
| 221 |
+
description="Modified scenario",
|
| 222 |
+
calculator_type="cooling",
|
| 223 |
+
form_data=self.sample_form_data,
|
| 224 |
+
results=modified_results
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# Compare the scenarios
|
| 228 |
+
comparison = self.scenario_manager.compare_scenarios([path1, path2])
|
| 229 |
+
|
| 230 |
+
# Verify the comparison
|
| 231 |
+
self.assertIsNotNone(comparison)
|
| 232 |
+
self.assertNotIn("error", comparison)
|
| 233 |
+
self.assertEqual(comparison["calculator_type"], "cooling")
|
| 234 |
+
self.assertEqual(len(comparison["scenarios"]), 2)
|
| 235 |
+
self.assertEqual(comparison["scenarios"][0], "Scenario 1")
|
| 236 |
+
self.assertEqual(comparison["scenarios"][1], "Scenario 2")
|
| 237 |
+
self.assertEqual(len(comparison["total_loads"]), 2)
|
| 238 |
+
self.assertEqual(comparison["total_loads"][0]["total_load_kw"], 5.0)
|
| 239 |
+
self.assertEqual(comparison["total_loads"][1]["total_load_kw"], 6.0)
|
| 240 |
+
self.assertIn("differences", comparison)
|
| 241 |
+
self.assertIn("Scenario 2", comparison["differences"])
|
| 242 |
+
self.assertEqual(comparison["differences"]["Scenario 2"]["absolute_diff_kw"], 1.0)
|
| 243 |
+
|
| 244 |
+
def run_tests():
|
| 245 |
+
"""Run all tests."""
|
| 246 |
+
# Create test suite
|
| 247 |
+
suite = unittest.TestSuite()
|
| 248 |
+
|
| 249 |
+
# Add test cases
|
| 250 |
+
suite.addTest(unittest.makeSuite(TestShadingFix))
|
| 251 |
+
suite.addTest(unittest.makeSuite(TestScenarioManager))
|
| 252 |
+
|
| 253 |
+
# Run tests
|
| 254 |
+
runner = unittest.TextTestRunner(verbosity=2)
|
| 255 |
+
result = runner.run(suite)
|
| 256 |
+
|
| 257 |
+
return result.wasSuccessful()
|
| 258 |
+
|
| 259 |
+
if __name__ == "__main__":
|
| 260 |
+
success = run_tests()
|
| 261 |
+
sys.exit(0 if success else 1)
|
utils/__pycache__/scenario_manager.cpython-310.pyc
ADDED
|
Binary file (12.3 kB). View file
|
|
|
utils/scenario_integration.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Integration module for scenario comparison functionality in the HVAC calculator.
|
| 3 |
+
|
| 4 |
+
This module provides UI components and integration code for the scenario comparison
|
| 5 |
+
functionality in both the cooling and heating calculators.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import streamlit as st
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import matplotlib.pyplot as plt
|
| 11 |
+
from utils.scenario_manager import ScenarioManager
|
| 12 |
+
|
| 13 |
+
def add_scenario_ui(calculator_type):
|
| 14 |
+
"""
|
| 15 |
+
Add scenario saving UI to the calculator.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
calculator_type (str): Type of calculator ('cooling' or 'heating')
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
bool: True if a scenario was saved, False otherwise
|
| 22 |
+
"""
|
| 23 |
+
st.subheader("Save Current Calculation as Scenario")
|
| 24 |
+
|
| 25 |
+
# Check if results exist
|
| 26 |
+
results_key = f"{calculator_type}_results"
|
| 27 |
+
if results_key not in st.session_state or not st.session_state[results_key]:
|
| 28 |
+
st.warning("Please complete a calculation before saving a scenario.")
|
| 29 |
+
return False
|
| 30 |
+
|
| 31 |
+
# Get form data key
|
| 32 |
+
form_data_key = f"{calculator_type}_form_data"
|
| 33 |
+
|
| 34 |
+
# Create input fields for scenario name and description
|
| 35 |
+
scenario_name = st.text_input("Scenario Name", f"My {calculator_type.capitalize()} Scenario")
|
| 36 |
+
scenario_description = st.text_area("Scenario Description", "Description of this scenario...")
|
| 37 |
+
|
| 38 |
+
# Save button
|
| 39 |
+
if st.button("Save Scenario"):
|
| 40 |
+
if not scenario_name:
|
| 41 |
+
st.error("Please provide a name for the scenario.")
|
| 42 |
+
return False
|
| 43 |
+
|
| 44 |
+
# Initialize scenario manager
|
| 45 |
+
scenario_manager = ScenarioManager()
|
| 46 |
+
|
| 47 |
+
# Save the scenario
|
| 48 |
+
try:
|
| 49 |
+
path = scenario_manager.save_scenario(
|
| 50 |
+
scenario_name,
|
| 51 |
+
scenario_description,
|
| 52 |
+
calculator_type,
|
| 53 |
+
st.session_state[form_data_key],
|
| 54 |
+
st.session_state[results_key]
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
st.success(f"Scenario saved successfully: {scenario_name}")
|
| 58 |
+
return True
|
| 59 |
+
except Exception as e:
|
| 60 |
+
st.error(f"Error saving scenario: {str(e)}")
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
return False
|
| 64 |
+
|
| 65 |
+
def scenario_comparison_page():
|
| 66 |
+
"""
|
| 67 |
+
Create a page for comparing saved scenarios.
|
| 68 |
+
"""
|
| 69 |
+
st.title("Scenario Comparison")
|
| 70 |
+
|
| 71 |
+
# Initialize scenario manager
|
| 72 |
+
scenario_manager = ScenarioManager()
|
| 73 |
+
|
| 74 |
+
# Get available scenarios
|
| 75 |
+
all_scenarios = scenario_manager.get_available_scenarios()
|
| 76 |
+
|
| 77 |
+
if not all_scenarios:
|
| 78 |
+
st.warning("No saved scenarios found. Please save some scenarios first.")
|
| 79 |
+
return
|
| 80 |
+
|
| 81 |
+
# Filter by calculator type
|
| 82 |
+
calculator_type = st.radio(
|
| 83 |
+
"Calculator Type",
|
| 84 |
+
["cooling", "heating", "all"],
|
| 85 |
+
format_func=lambda x: x.capitalize() if x != "all" else "All"
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
filtered_scenarios = all_scenarios
|
| 89 |
+
if calculator_type != "all":
|
| 90 |
+
filtered_scenarios = [s for s in all_scenarios if s["calculator_type"] == calculator_type]
|
| 91 |
+
|
| 92 |
+
if not filtered_scenarios:
|
| 93 |
+
st.warning(f"No {calculator_type} scenarios found.")
|
| 94 |
+
return
|
| 95 |
+
|
| 96 |
+
# Create a DataFrame for display
|
| 97 |
+
scenarios_df = pd.DataFrame([
|
| 98 |
+
{
|
| 99 |
+
"Name": s["name"],
|
| 100 |
+
"Description": s["description"],
|
| 101 |
+
"Type": s["calculator_type"].capitalize(),
|
| 102 |
+
"Date": s["timestamp"]
|
| 103 |
+
}
|
| 104 |
+
for s in filtered_scenarios
|
| 105 |
+
])
|
| 106 |
+
|
| 107 |
+
st.subheader("Available Scenarios")
|
| 108 |
+
st.dataframe(scenarios_df)
|
| 109 |
+
|
| 110 |
+
# Select scenarios to compare
|
| 111 |
+
st.subheader("Select Scenarios to Compare")
|
| 112 |
+
st.info("Select at least two scenarios to compare. The first selected scenario will be used as the base scenario.")
|
| 113 |
+
|
| 114 |
+
# Create a mapping of display names to paths
|
| 115 |
+
scenario_options = {f"{s['name']} ({s['calculator_type'].capitalize()})": s["path"] for s in filtered_scenarios}
|
| 116 |
+
|
| 117 |
+
# Multi-select for scenarios
|
| 118 |
+
selected_scenario_names = st.multiselect(
|
| 119 |
+
"Select Scenarios",
|
| 120 |
+
options=list(scenario_options.keys())
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if len(selected_scenario_names) < 2:
|
| 124 |
+
st.warning("Please select at least two scenarios to compare.")
|
| 125 |
+
return
|
| 126 |
+
|
| 127 |
+
# Get the paths for selected scenarios
|
| 128 |
+
selected_scenario_paths = [scenario_options[name] for name in selected_scenario_names]
|
| 129 |
+
|
| 130 |
+
# Compare button
|
| 131 |
+
if st.button("Compare Selected Scenarios"):
|
| 132 |
+
# Compare scenarios
|
| 133 |
+
comparison = scenario_manager.compare_scenarios(selected_scenario_paths)
|
| 134 |
+
|
| 135 |
+
if "error" in comparison:
|
| 136 |
+
st.error(comparison["error"])
|
| 137 |
+
return
|
| 138 |
+
|
| 139 |
+
# Generate charts
|
| 140 |
+
charts = scenario_manager.generate_comparison_charts(comparison)
|
| 141 |
+
|
| 142 |
+
# Display comparison
|
| 143 |
+
scenario_manager.display_comparison_in_streamlit(comparison, charts)
|
| 144 |
+
|
| 145 |
+
# Add export options
|
| 146 |
+
st.subheader("Export Comparison")
|
| 147 |
+
|
| 148 |
+
# Export as CSV
|
| 149 |
+
comparison_data = []
|
| 150 |
+
|
| 151 |
+
# Add total loads
|
| 152 |
+
for load in comparison["total_loads"]:
|
| 153 |
+
row = {
|
| 154 |
+
"Scenario": load["name"],
|
| 155 |
+
"Metric": "Total Load",
|
| 156 |
+
"Value": load["total_load_kw"],
|
| 157 |
+
"Unit": "kW"
|
| 158 |
+
}
|
| 159 |
+
comparison_data.append(row)
|
| 160 |
+
|
| 161 |
+
row = {
|
| 162 |
+
"Scenario": load["name"],
|
| 163 |
+
"Metric": "Recommended Size",
|
| 164 |
+
"Value": load["recommended_size_kw"],
|
| 165 |
+
"Unit": "kW"
|
| 166 |
+
}
|
| 167 |
+
comparison_data.append(row)
|
| 168 |
+
|
| 169 |
+
# Add breakdown percentages
|
| 170 |
+
for breakdown in comparison["breakdown"]:
|
| 171 |
+
for category in breakdown:
|
| 172 |
+
if category != "name":
|
| 173 |
+
row = {
|
| 174 |
+
"Scenario": breakdown["name"],
|
| 175 |
+
"Metric": f"{category.capitalize()} Percentage",
|
| 176 |
+
"Value": breakdown[category],
|
| 177 |
+
"Unit": "%"
|
| 178 |
+
}
|
| 179 |
+
comparison_data.append(row)
|
| 180 |
+
|
| 181 |
+
# Add differences
|
| 182 |
+
base_scenario = comparison["scenarios"][0]
|
| 183 |
+
for scenario_name, diff in comparison["differences"].items():
|
| 184 |
+
row = {
|
| 185 |
+
"Scenario": scenario_name,
|
| 186 |
+
"Metric": f"Absolute Difference from {base_scenario}",
|
| 187 |
+
"Value": diff["absolute_diff_kw"],
|
| 188 |
+
"Unit": "kW"
|
| 189 |
+
}
|
| 190 |
+
comparison_data.append(row)
|
| 191 |
+
|
| 192 |
+
row = {
|
| 193 |
+
"Scenario": scenario_name,
|
| 194 |
+
"Metric": f"Percentage Difference from {base_scenario}",
|
| 195 |
+
"Value": diff["percentage_diff"],
|
| 196 |
+
"Unit": "%"
|
| 197 |
+
}
|
| 198 |
+
comparison_data.append(row)
|
| 199 |
+
|
| 200 |
+
# Create DataFrame
|
| 201 |
+
export_df = pd.DataFrame(comparison_data)
|
| 202 |
+
|
| 203 |
+
# Convert to CSV
|
| 204 |
+
csv = export_df.to_csv(index=False)
|
| 205 |
+
|
| 206 |
+
# Create download button
|
| 207 |
+
st.download_button(
|
| 208 |
+
label="Download Comparison as CSV",
|
| 209 |
+
data=csv,
|
| 210 |
+
file_name="scenario_comparison.csv",
|
| 211 |
+
mime="text/csv"
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
def add_scenario_comparison_to_app(app_module):
|
| 215 |
+
"""
|
| 216 |
+
Add scenario comparison functionality to the main app.
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
app_module: The main app module to modify
|
| 220 |
+
"""
|
| 221 |
+
# Add scenario comparison page to the app
|
| 222 |
+
app_module.pages["scenario_comparison"] = scenario_comparison_page
|
| 223 |
+
|
| 224 |
+
# Update the main function to include the scenario comparison page
|
| 225 |
+
original_main = app_module.main
|
| 226 |
+
|
| 227 |
+
def new_main():
|
| 228 |
+
st.sidebar.title("HVAC Load Calculator")
|
| 229 |
+
|
| 230 |
+
# Add attribution
|
| 231 |
+
st.sidebar.markdown("""
|
| 232 |
+
**Created by:** Dr. Majed Abuseif
|
| 233 |
+
|
| 234 |
+
This tool was created to facilitate HVAC calculation and understanding for Deakin University students, but has been enhanced to cover wider aspects to allow professionals and energy and HVAC enthusiasts to use it.
|
| 235 |
+
""")
|
| 236 |
+
|
| 237 |
+
# Add page selection
|
| 238 |
+
page = st.sidebar.radio(
|
| 239 |
+
"Select Calculator",
|
| 240 |
+
["Cooling Load Calculator", "Heating Load Calculator", "Scenario Comparison"]
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
if page == "Cooling Load Calculator":
|
| 244 |
+
app_module.cooling_calculator()
|
| 245 |
+
elif page == "Heating Load Calculator":
|
| 246 |
+
app_module.heating_calculator()
|
| 247 |
+
elif page == "Scenario Comparison":
|
| 248 |
+
scenario_comparison_page()
|
| 249 |
+
|
| 250 |
+
# Replace the main function
|
| 251 |
+
app_module.main = new_main
|
| 252 |
+
|
| 253 |
+
return app_module
|
| 254 |
+
|
| 255 |
+
def add_scenario_ui_to_calculator(calculator_module, calculator_type):
|
| 256 |
+
"""
|
| 257 |
+
Add scenario saving UI to a calculator module.
|
| 258 |
+
|
| 259 |
+
Args:
|
| 260 |
+
calculator_module: The calculator module to modify
|
| 261 |
+
calculator_type (str): Type of calculator ('cooling' or 'heating')
|
| 262 |
+
"""
|
| 263 |
+
# Find the results section in the calculator
|
| 264 |
+
if calculator_type == "cooling":
|
| 265 |
+
original_results_form = calculator_module.results_form
|
| 266 |
+
|
| 267 |
+
def new_results_form(ref_data):
|
| 268 |
+
# Call the original results form
|
| 269 |
+
original_results_form(ref_data)
|
| 270 |
+
|
| 271 |
+
# Add scenario UI
|
| 272 |
+
st.markdown("---")
|
| 273 |
+
add_scenario_ui(calculator_type)
|
| 274 |
+
|
| 275 |
+
# Replace the results form
|
| 276 |
+
calculator_module.results_form = new_results_form
|
| 277 |
+
|
| 278 |
+
elif calculator_type == "heating":
|
| 279 |
+
original_results_form = calculator_module.results_form
|
| 280 |
+
|
| 281 |
+
def new_results_form(ref_data):
|
| 282 |
+
# Call the original results form
|
| 283 |
+
original_results_form(ref_data)
|
| 284 |
+
|
| 285 |
+
# Add scenario UI
|
| 286 |
+
st.markdown("---")
|
| 287 |
+
add_scenario_ui(calculator_type)
|
| 288 |
+
|
| 289 |
+
# Replace the results form
|
| 290 |
+
calculator_module.results_form = new_results_form
|
| 291 |
+
|
| 292 |
+
return calculator_module
|
utils/scenario_manager.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility module for saving and comparing calculation scenarios.
|
| 3 |
+
|
| 4 |
+
This module provides functionality for saving calculation results as scenarios,
|
| 5 |
+
loading saved scenarios, and comparing multiple scenarios to analyze differences.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import matplotlib.pyplot as plt
|
| 12 |
+
import streamlit as st
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
|
| 15 |
+
class ScenarioManager:
|
| 16 |
+
"""
|
| 17 |
+
Manager for saving, loading, and comparing calculation scenarios.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(self, base_path="scenarios"):
|
| 21 |
+
"""
|
| 22 |
+
Initialize the scenario manager.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
base_path (str): Base directory for storing scenarios
|
| 26 |
+
"""
|
| 27 |
+
self.base_path = base_path
|
| 28 |
+
os.makedirs(base_path, exist_ok=True)
|
| 29 |
+
|
| 30 |
+
def save_scenario(self, name, description, calculator_type, form_data, results):
|
| 31 |
+
"""
|
| 32 |
+
Save a calculation scenario.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
name (str): Name of the scenario
|
| 36 |
+
description (str): Description of the scenario
|
| 37 |
+
calculator_type (str): Type of calculator ('cooling' or 'heating')
|
| 38 |
+
form_data (dict): Form data used for the calculation
|
| 39 |
+
results (dict): Calculation results
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
str: Path to the saved scenario file
|
| 43 |
+
"""
|
| 44 |
+
# Create a timestamp
|
| 45 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 46 |
+
|
| 47 |
+
# Create a safe filename
|
| 48 |
+
safe_name = name.replace(" ", "_").lower()
|
| 49 |
+
filename = f"{safe_name}_{timestamp}.json"
|
| 50 |
+
|
| 51 |
+
# Create the full path
|
| 52 |
+
calculator_dir = os.path.join(self.base_path, calculator_type)
|
| 53 |
+
os.makedirs(calculator_dir, exist_ok=True)
|
| 54 |
+
full_path = os.path.join(calculator_dir, filename)
|
| 55 |
+
|
| 56 |
+
# Create the scenario data
|
| 57 |
+
scenario_data = {
|
| 58 |
+
"name": name,
|
| 59 |
+
"description": description,
|
| 60 |
+
"calculator_type": calculator_type,
|
| 61 |
+
"timestamp": timestamp,
|
| 62 |
+
"form_data": form_data,
|
| 63 |
+
"results": results
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
# Save the scenario
|
| 67 |
+
with open(full_path, "w") as f:
|
| 68 |
+
json.dump(scenario_data, f, indent=2)
|
| 69 |
+
|
| 70 |
+
return full_path
|
| 71 |
+
|
| 72 |
+
def load_scenario(self, path):
|
| 73 |
+
"""
|
| 74 |
+
Load a saved scenario.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
path (str): Path to the scenario file
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
dict: Scenario data or None if loading fails
|
| 81 |
+
"""
|
| 82 |
+
try:
|
| 83 |
+
with open(path, "r") as f:
|
| 84 |
+
scenario_data = json.load(f)
|
| 85 |
+
return scenario_data
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"Error loading scenario: {e}")
|
| 88 |
+
return None
|
| 89 |
+
|
| 90 |
+
def get_available_scenarios(self, calculator_type=None):
|
| 91 |
+
"""
|
| 92 |
+
Get a list of available scenarios.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
calculator_type (str, optional): Filter by calculator type
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
list: List of scenario information dictionaries
|
| 99 |
+
"""
|
| 100 |
+
scenarios = []
|
| 101 |
+
|
| 102 |
+
# Determine which directories to search
|
| 103 |
+
if calculator_type:
|
| 104 |
+
dirs_to_search = [os.path.join(self.base_path, calculator_type)]
|
| 105 |
+
else:
|
| 106 |
+
dirs_to_search = [os.path.join(self.base_path, d) for d in ["cooling", "heating"]]
|
| 107 |
+
|
| 108 |
+
# Search for scenario files
|
| 109 |
+
for directory in dirs_to_search:
|
| 110 |
+
if not os.path.exists(directory):
|
| 111 |
+
continue
|
| 112 |
+
|
| 113 |
+
for filename in os.listdir(directory):
|
| 114 |
+
if filename.endswith(".json"):
|
| 115 |
+
path = os.path.join(directory, filename)
|
| 116 |
+
scenario = self.load_scenario(path)
|
| 117 |
+
if scenario:
|
| 118 |
+
scenarios.append({
|
| 119 |
+
"path": path,
|
| 120 |
+
"name": scenario["name"],
|
| 121 |
+
"description": scenario["description"],
|
| 122 |
+
"calculator_type": scenario["calculator_type"],
|
| 123 |
+
"timestamp": scenario["timestamp"]
|
| 124 |
+
})
|
| 125 |
+
|
| 126 |
+
# Sort by timestamp (newest first)
|
| 127 |
+
scenarios.sort(key=lambda x: x["timestamp"], reverse=True)
|
| 128 |
+
|
| 129 |
+
return scenarios
|
| 130 |
+
|
| 131 |
+
def compare_scenarios(self, scenario_paths):
|
| 132 |
+
"""
|
| 133 |
+
Compare multiple scenarios.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
scenario_paths (list): List of paths to scenario files
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
dict: Comparison results
|
| 140 |
+
"""
|
| 141 |
+
if not scenario_paths or len(scenario_paths) < 2:
|
| 142 |
+
return {"error": "At least two scenarios are required for comparison"}
|
| 143 |
+
|
| 144 |
+
# Load scenarios
|
| 145 |
+
scenarios = []
|
| 146 |
+
for path in scenario_paths:
|
| 147 |
+
scenario = self.load_scenario(path)
|
| 148 |
+
if scenario:
|
| 149 |
+
scenarios.append(scenario)
|
| 150 |
+
|
| 151 |
+
# Check if all scenarios are of the same type
|
| 152 |
+
calculator_types = set(s["calculator_type"] for s in scenarios)
|
| 153 |
+
if len(calculator_types) > 1:
|
| 154 |
+
return {"error": "Cannot compare scenarios of different calculator types"}
|
| 155 |
+
|
| 156 |
+
calculator_type = scenarios[0]["calculator_type"]
|
| 157 |
+
|
| 158 |
+
# Prepare comparison data
|
| 159 |
+
comparison = {
|
| 160 |
+
"calculator_type": calculator_type,
|
| 161 |
+
"scenarios": [s["name"] for s in scenarios],
|
| 162 |
+
"total_loads": [],
|
| 163 |
+
"breakdown": [],
|
| 164 |
+
"differences": {}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
# Extract key metrics for comparison
|
| 168 |
+
for scenario in scenarios:
|
| 169 |
+
results = scenario["results"]
|
| 170 |
+
|
| 171 |
+
# Add total load
|
| 172 |
+
comparison["total_loads"].append({
|
| 173 |
+
"name": scenario["name"],
|
| 174 |
+
"total_load_kw": results["total_load_kw"],
|
| 175 |
+
"recommended_size_kw": results["recommended_size_kw"]
|
| 176 |
+
})
|
| 177 |
+
|
| 178 |
+
# Add breakdown percentages
|
| 179 |
+
breakdown = {
|
| 180 |
+
"name": scenario["name"]
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
if calculator_type == "cooling":
|
| 184 |
+
breakdown.update({
|
| 185 |
+
"transmission": results["breakdown_percentage"]["transmission"],
|
| 186 |
+
"solar": results["breakdown_percentage"]["solar"],
|
| 187 |
+
"ventilation": results["breakdown_percentage"]["ventilation"],
|
| 188 |
+
"internal": results["breakdown_percentage"]["internal"]
|
| 189 |
+
})
|
| 190 |
+
else: # heating
|
| 191 |
+
breakdown.update({
|
| 192 |
+
"transmission": results["breakdown_percentage"]["transmission"],
|
| 193 |
+
"ventilation": results["breakdown_percentage"]["ventilation"]
|
| 194 |
+
})
|
| 195 |
+
|
| 196 |
+
comparison["breakdown"].append(breakdown)
|
| 197 |
+
|
| 198 |
+
# Calculate differences between scenarios
|
| 199 |
+
base_scenario = scenarios[0]
|
| 200 |
+
base_load = base_scenario["results"]["total_load_kw"]
|
| 201 |
+
|
| 202 |
+
for i, scenario in enumerate(scenarios[1:], 1):
|
| 203 |
+
scenario_load = scenario["results"]["total_load_kw"]
|
| 204 |
+
absolute_diff = scenario_load - base_load
|
| 205 |
+
percentage_diff = (absolute_diff / base_load) * 100 if base_load > 0 else 0
|
| 206 |
+
|
| 207 |
+
comparison["differences"][scenario["name"]] = {
|
| 208 |
+
"absolute_diff_kw": absolute_diff,
|
| 209 |
+
"percentage_diff": percentage_diff
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
return comparison
|
| 213 |
+
|
| 214 |
+
def generate_comparison_charts(self, comparison):
|
| 215 |
+
"""
|
| 216 |
+
Generate charts for scenario comparison.
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
comparison (dict): Comparison data from compare_scenarios
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
dict: Dictionary of matplotlib figures
|
| 223 |
+
"""
|
| 224 |
+
if "error" in comparison:
|
| 225 |
+
return {"error": comparison["error"]}
|
| 226 |
+
|
| 227 |
+
charts = {}
|
| 228 |
+
|
| 229 |
+
# Total load comparison chart
|
| 230 |
+
fig_total, ax_total = plt.subplots(figsize=(10, 6))
|
| 231 |
+
scenario_names = [load["name"] for load in comparison["total_loads"]]
|
| 232 |
+
total_loads = [load["total_load_kw"] for load in comparison["total_loads"]]
|
| 233 |
+
recommended_sizes = [load["recommended_size_kw"] for load in comparison["total_loads"]]
|
| 234 |
+
|
| 235 |
+
x = range(len(scenario_names))
|
| 236 |
+
bar_width = 0.35
|
| 237 |
+
|
| 238 |
+
ax_total.bar([i - bar_width/2 for i in x], total_loads, bar_width, label='Total Load (kW)')
|
| 239 |
+
ax_total.bar([i + bar_width/2 for i in x], recommended_sizes, bar_width, label='Recommended Size (kW)')
|
| 240 |
+
|
| 241 |
+
ax_total.set_xlabel('Scenario')
|
| 242 |
+
ax_total.set_ylabel('Load (kW)')
|
| 243 |
+
ax_total.set_title('Total Load Comparison')
|
| 244 |
+
ax_total.set_xticks(x)
|
| 245 |
+
ax_total.set_xticklabels(scenario_names, rotation=45, ha='right')
|
| 246 |
+
ax_total.legend()
|
| 247 |
+
|
| 248 |
+
plt.tight_layout()
|
| 249 |
+
charts["total_load"] = fig_total
|
| 250 |
+
|
| 251 |
+
# Breakdown comparison chart
|
| 252 |
+
fig_breakdown, ax_breakdown = plt.subplots(figsize=(12, 6))
|
| 253 |
+
|
| 254 |
+
# Determine categories based on calculator type
|
| 255 |
+
if comparison["calculator_type"] == "cooling":
|
| 256 |
+
categories = ["transmission", "solar", "ventilation", "internal"]
|
| 257 |
+
category_labels = ["Transmission", "Solar", "Ventilation", "Internal"]
|
| 258 |
+
else: # heating
|
| 259 |
+
categories = ["transmission", "ventilation"]
|
| 260 |
+
category_labels = ["Transmission", "Ventilation"]
|
| 261 |
+
|
| 262 |
+
# Prepare data for grouped bar chart
|
| 263 |
+
bar_positions = []
|
| 264 |
+
bar_heights = []
|
| 265 |
+
bar_labels = []
|
| 266 |
+
bar_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
|
| 267 |
+
|
| 268 |
+
for i, category in enumerate(categories):
|
| 269 |
+
positions = [i + j * (len(categories) + 1) for j in range(len(comparison["breakdown"]))]
|
| 270 |
+
bar_positions.extend(positions)
|
| 271 |
+
|
| 272 |
+
heights = [breakdown[category] for breakdown in comparison["breakdown"]]
|
| 273 |
+
bar_heights.extend(heights)
|
| 274 |
+
|
| 275 |
+
for scenario_name in [breakdown["name"] for breakdown in comparison["breakdown"]]:
|
| 276 |
+
bar_labels.append(scenario_name)
|
| 277 |
+
|
| 278 |
+
# Create the grouped bar chart
|
| 279 |
+
bars = ax_breakdown.bar(bar_positions, bar_heights, width=0.8)
|
| 280 |
+
|
| 281 |
+
# Color the bars by scenario
|
| 282 |
+
for i, bar in enumerate(bars):
|
| 283 |
+
scenario_index = i % len(comparison["breakdown"])
|
| 284 |
+
bar.set_color(bar_colors[scenario_index % len(bar_colors)])
|
| 285 |
+
|
| 286 |
+
# Set labels and title
|
| 287 |
+
ax_breakdown.set_xlabel('Category')
|
| 288 |
+
ax_breakdown.set_ylabel('Percentage (%)')
|
| 289 |
+
ax_breakdown.set_title('Load Breakdown Comparison')
|
| 290 |
+
|
| 291 |
+
# Set x-ticks at the center of each group
|
| 292 |
+
group_centers = [i + (len(comparison["breakdown"]) - 1) / 2 for i in range(0, len(bar_positions), len(comparison["breakdown"]))]
|
| 293 |
+
ax_breakdown.set_xticks(group_centers)
|
| 294 |
+
ax_breakdown.set_xticklabels(category_labels)
|
| 295 |
+
|
| 296 |
+
# Add a legend
|
| 297 |
+
scenario_names = [breakdown["name"] for breakdown in comparison["breakdown"]]
|
| 298 |
+
legend_handles = [plt.Rectangle((0, 0), 1, 1, color=bar_colors[i % len(bar_colors)]) for i in range(len(scenario_names))]
|
| 299 |
+
ax_breakdown.legend(legend_handles, scenario_names, loc='upper right')
|
| 300 |
+
|
| 301 |
+
plt.tight_layout()
|
| 302 |
+
charts["breakdown"] = fig_breakdown
|
| 303 |
+
|
| 304 |
+
# Differences chart (if there are differences)
|
| 305 |
+
if comparison["differences"]:
|
| 306 |
+
fig_diff, ax_diff = plt.subplots(figsize=(10, 6))
|
| 307 |
+
|
| 308 |
+
scenario_names = list(comparison["differences"].keys())
|
| 309 |
+
absolute_diffs = [diff["absolute_diff_kw"] for diff in comparison["differences"].values()]
|
| 310 |
+
percentage_diffs = [diff["percentage_diff"] for diff in comparison["differences"].values()]
|
| 311 |
+
|
| 312 |
+
x = range(len(scenario_names))
|
| 313 |
+
|
| 314 |
+
# Create two y-axes
|
| 315 |
+
ax_abs = ax_diff
|
| 316 |
+
ax_pct = ax_abs.twinx()
|
| 317 |
+
|
| 318 |
+
# Plot data
|
| 319 |
+
bars = ax_abs.bar(x, absolute_diffs, width=0.6, color='#1f77b4', alpha=0.7, label='Absolute Difference (kW)')
|
| 320 |
+
line = ax_pct.plot(x, percentage_diffs, 'ro-', label='Percentage Difference (%)')
|
| 321 |
+
|
| 322 |
+
# Add labels and title
|
| 323 |
+
ax_abs.set_xlabel('Scenario')
|
| 324 |
+
ax_abs.set_ylabel('Absolute Difference (kW)')
|
| 325 |
+
ax_pct.set_ylabel('Percentage Difference (%)')
|
| 326 |
+
ax_abs.set_title(f'Differences Compared to Base Scenario ({comparison["scenarios"][0]})')
|
| 327 |
+
|
| 328 |
+
# Set x-ticks
|
| 329 |
+
ax_abs.set_xticks(x)
|
| 330 |
+
ax_abs.set_xticklabels(scenario_names, rotation=45, ha='right')
|
| 331 |
+
|
| 332 |
+
# Add legends
|
| 333 |
+
lines, labels = ax_abs.get_legend_handles_labels()
|
| 334 |
+
lines2, labels2 = ax_pct.get_legend_handles_labels()
|
| 335 |
+
ax_abs.legend(lines + lines2, labels + labels2, loc='upper left')
|
| 336 |
+
|
| 337 |
+
plt.tight_layout()
|
| 338 |
+
charts["differences"] = fig_diff
|
| 339 |
+
|
| 340 |
+
return charts
|
| 341 |
+
|
| 342 |
+
def display_comparison_in_streamlit(self, comparison, charts=None):
|
| 343 |
+
"""
|
| 344 |
+
Display scenario comparison in Streamlit.
|
| 345 |
+
|
| 346 |
+
Args:
|
| 347 |
+
comparison (dict): Comparison data from compare_scenarios
|
| 348 |
+
charts (dict, optional): Charts from generate_comparison_charts
|
| 349 |
+
"""
|
| 350 |
+
if "error" in comparison:
|
| 351 |
+
st.error(comparison["error"])
|
| 352 |
+
return
|
| 353 |
+
|
| 354 |
+
# Display total loads
|
| 355 |
+
st.subheader("Total Load Comparison")
|
| 356 |
+
|
| 357 |
+
# Create a DataFrame for the total loads
|
| 358 |
+
total_loads_df = pd.DataFrame(comparison["total_loads"])
|
| 359 |
+
total_loads_df = total_loads_df.rename(columns={
|
| 360 |
+
"name": "Scenario",
|
| 361 |
+
"total_load_kw": "Total Load (kW)",
|
| 362 |
+
"recommended_size_kw": "Recommended Size (kW)"
|
| 363 |
+
})
|
| 364 |
+
|
| 365 |
+
st.dataframe(total_loads_df)
|
| 366 |
+
|
| 367 |
+
# Display the total load chart
|
| 368 |
+
if charts and "total_load" in charts:
|
| 369 |
+
st.pyplot(charts["total_load"])
|
| 370 |
+
|
| 371 |
+
# Display breakdown
|
| 372 |
+
st.subheader("Load Breakdown Comparison")
|
| 373 |
+
|
| 374 |
+
# Create a DataFrame for the breakdown
|
| 375 |
+
breakdown_df = pd.DataFrame(comparison["breakdown"])
|
| 376 |
+
|
| 377 |
+
# Rename columns for better display
|
| 378 |
+
column_mapping = {
|
| 379 |
+
"name": "Scenario",
|
| 380 |
+
"transmission": "Transmission (%)",
|
| 381 |
+
"solar": "Solar (%)",
|
| 382 |
+
"ventilation": "Ventilation (%)",
|
| 383 |
+
"internal": "Internal (%)"
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
breakdown_df = breakdown_df.rename(columns={k: v for k, v in column_mapping.items() if k in breakdown_df.columns})
|
| 387 |
+
|
| 388 |
+
st.dataframe(breakdown_df)
|
| 389 |
+
|
| 390 |
+
# Display the breakdown chart
|
| 391 |
+
if charts and "breakdown" in charts:
|
| 392 |
+
st.pyplot(charts["breakdown"])
|
| 393 |
+
|
| 394 |
+
# Display differences
|
| 395 |
+
if comparison["differences"]:
|
| 396 |
+
st.subheader(f"Differences Compared to Base Scenario ({comparison['scenarios'][0]})")
|
| 397 |
+
|
| 398 |
+
# Create a DataFrame for the differences
|
| 399 |
+
diff_data = []
|
| 400 |
+
for scenario_name, diff in comparison["differences"].items():
|
| 401 |
+
diff_data.append({
|
| 402 |
+
"Scenario": scenario_name,
|
| 403 |
+
"Absolute Difference (kW)": diff["absolute_diff_kw"],
|
| 404 |
+
"Percentage Difference (%)": diff["percentage_diff"]
|
| 405 |
+
})
|
| 406 |
+
|
| 407 |
+
diff_df = pd.DataFrame(diff_data)
|
| 408 |
+
st.dataframe(diff_df)
|
| 409 |
+
|
| 410 |
+
# Display the differences chart
|
| 411 |
+
if charts and "differences" in charts:
|
| 412 |
+
st.pyplot(charts["differences"])
|
| 413 |
+
|
| 414 |
+
# Display interpretation
|
| 415 |
+
st.subheader("Interpretation")
|
| 416 |
+
|
| 417 |
+
base_scenario = comparison["scenarios"][0]
|
| 418 |
+
|
| 419 |
+
if comparison["calculator_type"] == "cooling":
|
| 420 |
+
st.write(f"""
|
| 421 |
+
### Key Observations:
|
| 422 |
+
|
| 423 |
+
- The base scenario ({base_scenario}) has a total cooling load of {comparison['total_loads'][0]['total_load_kw']:.2f} kW.
|
| 424 |
+
- The recommended cooling system size for the base scenario is {comparison['total_loads'][0]['recommended_size_kw']:.2f} kW.
|
| 425 |
+
""")
|
| 426 |
+
|
| 427 |
+
if comparison["differences"]:
|
| 428 |
+
for scenario_name, diff in comparison["differences"].items():
|
| 429 |
+
if diff["absolute_diff_kw"] > 0:
|
| 430 |
+
st.write(f"- {scenario_name} has a **higher** cooling load by {abs(diff['absolute_diff_kw']):.2f} kW ({abs(diff['percentage_diff']):.1f}%) compared to the base scenario.")
|
| 431 |
+
else:
|
| 432 |
+
st.write(f"- {scenario_name} has a **lower** cooling load by {abs(diff['absolute_diff_kw']):.2f} kW ({abs(diff['percentage_diff']):.1f}%) compared to the base scenario.")
|
| 433 |
+
else: # heating
|
| 434 |
+
st.write(f"""
|
| 435 |
+
### Key Observations:
|
| 436 |
+
|
| 437 |
+
- The base scenario ({base_scenario}) has a total heating load of {comparison['total_loads'][0]['total_load_kw']:.2f} kW.
|
| 438 |
+
- The recommended heating system size for the base scenario is {comparison['total_loads'][0]['recommended_size_kw']:.2f} kW.
|
| 439 |
+
""")
|
| 440 |
+
|
| 441 |
+
if comparison["differences"]:
|
| 442 |
+
for scenario_name, diff in comparison["differences"].items():
|
| 443 |
+
if diff["absolute_diff_kw"] > 0:
|
| 444 |
+
st.write(f"- {scenario_name} has a **higher** heating load by {abs(diff['absolute_diff_kw']):.2f} kW ({abs(diff['percentage_diff']):.1f}%) compared to the base scenario.")
|
| 445 |
+
else:
|
| 446 |
+
st.write(f"- {scenario_name} has a **lower** heating load by {abs(diff['absolute_diff_kw']):.2f} kW ({abs(diff['percentage_diff']):.1f}%) compared to the base scenario.")
|
utils/validation.py
CHANGED
|
@@ -45,7 +45,7 @@ def validate_input(input_value, validation_type, min_value=None, max_value=None,
|
|
| 45 |
warnings.append(ValidationWarning(
|
| 46 |
"Required field is empty",
|
| 47 |
"Please provide a value for this field",
|
| 48 |
-
is_critical=True
|
| 49 |
))
|
| 50 |
is_valid = False
|
| 51 |
|
|
@@ -65,7 +65,7 @@ def validate_input(input_value, validation_type, min_value=None, max_value=None,
|
|
| 65 |
warnings.append(ValidationWarning(
|
| 66 |
f"Value is below minimum ({min_value})",
|
| 67 |
f"Please enter a value greater than or equal to {min_value}",
|
| 68 |
-
is_critical=
|
| 69 |
))
|
| 70 |
is_valid = False
|
| 71 |
|
|
@@ -74,7 +74,7 @@ def validate_input(input_value, validation_type, min_value=None, max_value=None,
|
|
| 74 |
warnings.append(ValidationWarning(
|
| 75 |
f"Value exceeds maximum ({max_value})",
|
| 76 |
f"Please enter a value less than or equal to {max_value}",
|
| 77 |
-
is_critical=
|
| 78 |
))
|
| 79 |
is_valid = False
|
| 80 |
|
|
@@ -82,7 +82,7 @@ def validate_input(input_value, validation_type, min_value=None, max_value=None,
|
|
| 82 |
warnings.append(ValidationWarning(
|
| 83 |
"Invalid number format",
|
| 84 |
"Please enter a valid number",
|
| 85 |
-
is_critical=True
|
| 86 |
))
|
| 87 |
is_valid = False
|
| 88 |
|
|
|
|
| 45 |
warnings.append(ValidationWarning(
|
| 46 |
"Required field is empty",
|
| 47 |
"Please provide a value for this field",
|
| 48 |
+
is_critical=True # Keep required fields as critical
|
| 49 |
))
|
| 50 |
is_valid = False
|
| 51 |
|
|
|
|
| 65 |
warnings.append(ValidationWarning(
|
| 66 |
f"Value is below minimum ({min_value})",
|
| 67 |
f"Please enter a value greater than or equal to {min_value}",
|
| 68 |
+
is_critical=False # Changed to non-critical
|
| 69 |
))
|
| 70 |
is_valid = False
|
| 71 |
|
|
|
|
| 74 |
warnings.append(ValidationWarning(
|
| 75 |
f"Value exceeds maximum ({max_value})",
|
| 76 |
f"Please enter a value less than or equal to {max_value}",
|
| 77 |
+
is_critical=False # Changed to non-critical
|
| 78 |
))
|
| 79 |
is_valid = False
|
| 80 |
|
|
|
|
| 82 |
warnings.append(ValidationWarning(
|
| 83 |
"Invalid number format",
|
| 84 |
"Please enter a valid number",
|
| 85 |
+
is_critical=True # Keep format validation as critical
|
| 86 |
))
|
| 87 |
is_valid = False
|
| 88 |
|