mabuseif commited on
Commit
2cd3970
·
verified ·
1 Parent(s): 0f76324

Upload 23 files

Browse files
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
- # HVAC Load Calculator
 
13
 
14
- A modern web tool for calculating HVAC cooling and heating loads based on the ASHRAE method.
 
15
 
16
- ## Features
17
-
18
- - **Separate Calculators**: Independent cooling and heating load calculators
19
- - **Step-by-Step Input Forms**: Guided process with validation
20
- - **Reference Data**: Comprehensive material properties and location data
21
- - **Visual Results**: Charts and tables for load components
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 required information in each step
31
- 3. Review any warnings that appear (you can proceed with warnings)
32
- 4. Calculate results and analyze the output
33
- 5. Export results for your assignments
34
-
35
- ## Cooling Load Calculator
36
-
37
- The cooling load calculator helps determine the amount of heat that needs to be removed from a space to maintain comfort conditions. It accounts for:
38
-
39
- - Conduction through building envelope
40
- - Solar radiation through windows
41
- - Internal heat gains (people, equipment, lighting)
42
- - Infiltration and ventilation
43
-
44
- ## Heating Load Calculator
45
-
46
- The heating load calculator helps determine the amount of heat that needs to be added to a space to maintain comfort conditions. It accounts for:
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 file for HVAC Load Calculator
3
 
4
- This is the main entry point for the HVAC Load Calculator web application.
5
- It sets up the Streamlit interface and navigation between different pages.
6
  """
7
 
8
  import streamlit as st
9
- import os
10
- import sys
11
  from pathlib import Path
 
 
12
 
13
- # Add the parent directory to sys.path to import modules
14
  sys.path.append(os.path.dirname(os.path.abspath(__file__)))
15
 
16
- # Import pages
17
  from pages.cooling_calculator import cooling_calculator
18
  from pages.heating_calculator import heating_calculator
 
 
19
 
20
- # Set page configuration
21
- st.set_page_config(
22
- page_title="HVAC Load Calculator",
23
- page_icon="🔥❄️",
24
- layout="wide",
25
- initial_sidebar_state="expanded"
26
- )
27
 
28
- # Define main function
29
  def main():
30
- """Main function for the HVAC Load Calculator web application."""
 
 
 
31
 
32
- # Add custom CSS
33
- st.markdown("""
34
- <style>
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
- # Sidebar navigation
56
- st.sidebar.title("HVAC Load Calculator")
57
- st.sidebar.image("https://img.icons8.com/fluency/96/air-conditioner.png", width=100)
58
 
59
- # Navigation options
60
  page = st.sidebar.radio(
61
  "Select Calculator",
62
- ["Home", "Cooling Load Calculator", "Heating Load Calculator"]
63
  )
64
 
65
- # Display selected page
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
- # Footer
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
- ASHRAE Cooling Load Calculation Module
3
 
4
- This module implements the ASHRAE method for calculating cooling loads in residential buildings.
5
- It calculates the sensible cooling load and then applies a factor of 1.3 to account for latent load.
6
  """
7
 
8
- import numpy as np
9
- import pandas as pd
10
-
11
-
12
  class CoolingLoadCalculator:
13
  """
14
- A class to calculate cooling loads using the ASHRAE method.
 
 
 
 
15
  """
16
-
17
- def __init__(self):
18
- """Initialize the cooling load calculator with default values."""
19
- # Default values for internal heat gains (W)
20
- self.heat_gain_per_person = 75
21
- self.heat_gain_kitchen = 1000
22
-
23
- # Specific heat capacity of air × density of air
24
- self.air_heat_factor = 0.33
25
-
26
- def calculate_conduction_heat_gain(self, area, u_value, temp_diff):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  """
28
- Calculate conduction heat gain through building components.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  Args:
31
- area (float): Area of the building component in m²
32
  u_value (float): U-value of the component in W/m²°C
33
- temp_diff (float): Temperature difference (outside - inside) in °C
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): Number of air changes per hour
61
- temp_diff (float): Temperature difference (outside - inside) in °C
62
 
63
  Returns:
64
  float: Heat gain in Watts
65
  """
66
- return self.air_heat_factor * volume * air_changes * temp_diff
67
-
68
- def calculate_internal_heat_gain(self, num_people, has_kitchen=False, equipment_watts=0):
 
 
 
 
 
 
 
 
 
 
69
  """
70
- Calculate internal heat gain from people, kitchen, and equipment.
71
 
72
  Args:
73
- num_people (int): Number of occupants
74
- has_kitchen (bool): Whether the space includes a kitchen
75
- equipment_watts (float): Additional equipment heat gain in Watts
 
76
 
77
  Returns:
78
- float: Heat gain in Watts
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
- Get the Solar Heat Gain Factor based on orientation, glass type, and climate.
 
87
 
88
- Args:
89
- orientation (str): Window orientation ('north', 'east', 'south', 'west')
90
- glass_type (str): Type of glass ('single', 'double', 'low_e')
91
- daily_range (str): Daily temperature range ('low', 'medium', 'high')
92
- latitude (str): Latitude category ('low', 'medium', 'high')
93
-
94
- Returns:
95
- float: Solar Heat Gain Factor in W/m²
96
- """
97
- # This is a simplified version - in a real implementation, this would use lookup tables
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
- # Adjustments for glass type
110
- glass_factors = {
111
- 'single': 1.0,
112
- 'double': 0.85,
113
- 'low_e': 0.65
114
- }
115
 
116
- # Adjustments for latitude
117
- latitude_factors = {
118
- 'low': 1.1, # Closer to equator
119
- 'medium': 1.0, # Mid latitudes
120
- 'high': 0.9 # Closer to poles
121
- }
 
 
 
 
122
 
123
- # Adjustments for daily temperature range
124
- range_factors = {
125
- 'low': 0.95, # Less than 8.5°C
126
- 'medium': 1.0, # Between 8.5°C and 14°C
127
- 'high': 1.05 # Over 14°C
128
- }
129
 
130
- # Calculate the adjusted SHGF
131
- base_value = base_values.get(orientation.lower(), 350) # Default to south if not found
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 base_value * glass_factor * latitude_factor * range_factor
137
-
138
- def calculate_total_cooling_load(self, building_components, windows, infiltration, internal_gains):
 
 
 
 
 
 
 
139
  """
140
- Calculate the total cooling load including latent 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: Dictionary with sensible load, latent load, and total cooling load in Watts
150
  """
151
- # Calculate conduction heat gain through building components
152
- conduction_gain = sum(
153
- self.calculate_conduction_heat_gain(comp['area'], comp['u_value'], comp['temp_diff'])
154
- for comp in building_components
155
- )
156
 
157
- # Calculate solar and conduction heat gain through windows
158
- window_conduction_gain = 0
159
- window_solar_gain = 0
 
 
 
160
 
161
- for window in windows:
162
- # Conduction through glass
163
- window_conduction_gain += self.calculate_conduction_heat_gain(
164
- window['area'], window['u_value'], window['temp_diff']
165
  )
166
 
167
- # Solar radiation through glass
168
- shgf = self.get_solar_heat_gain_factor(
169
- window['orientation'],
170
- window['glass_type'],
171
- window.get('daily_range', 'medium'),
172
- window.get('latitude', 'medium')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- internal_gains['num_people'],
189
- internal_gains.get('has_kitchen', False),
190
- internal_gains.get('equipment_watts', 0)
 
191
  )
192
 
193
- # Calculate sensible cooling load
194
- sensible_load = conduction_gain + window_conduction_gain + window_solar_gain + infiltration_gain + internal_gain
195
 
196
- # Calculate total cooling load (including latent load)
197
- latent_load = sensible_load * 0.3 # 30% of sensible load for latent load
198
- total_load = sensible_load * 1.3 # Factor of 1.3 to account for latent load
199
 
200
- return {
201
- 'conduction_gain': conduction_gain,
202
- 'window_conduction_gain': window_conduction_gain,
203
- 'window_solar_gain': window_solar_gain,
204
- 'infiltration_gain': infiltration_gain,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  'internal_gain': internal_gain,
206
- 'sensible_load': sensible_load,
207
- 'latent_load': latent_load,
208
- 'total_load': total_load
 
 
 
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 'temp_diff' for each component
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 total heating load
176
- total_load = total_conduction_loss + infiltration_loss
 
 
 
 
 
 
 
 
 
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=True
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
- walls_df = walls_df[['name', 'Material', 'area', 'u_value']]
299
- walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)']
 
 
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=True
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.qualitative.Set2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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='indianred'
 
 
 
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='lightsalmon'
 
 
 
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=True
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
- walls_df = walls_df[['name', 'Material', 'area', 'u_value']]
284
- walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)']
 
 
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=True
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.qualitative.Set2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.qualitative.Set3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.qualitative.Pastel2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- pandas==2.0.0
2
- streamlit==1.32.0
3
- plotly==5.18.0
4
- numpy==1.24.3
 
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=True
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=True
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