mabuseif commited on
Commit
4f3c166
·
verified ·
1 Parent(s): 0c4836f

Upload 6 files

Browse files
app.py CHANGED
@@ -1,47 +1,53 @@
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;
@@ -49,112 +55,33 @@ def main():
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
  import streamlit as st
 
2
  import sys
3
+ import os
4
  from pathlib import Path
5
 
6
  # Add the parent directory to sys.path to import modules
7
  sys.path.append(os.path.dirname(os.path.abspath(__file__)))
8
 
9
  # Import pages
10
+ from pages.cooling_calculator_enhanced import cooling_calculator
11
+ from pages.heating_calculator_enhanced import heating_calculator
 
 
 
 
 
 
 
 
12
 
 
13
  def main():
14
+ """Main function for the HVAC Load Calculator application."""
15
+ # Set page config
16
+ st.set_page_config(
17
+ page_title="HVAC Load Calculator",
18
+ page_icon="🔥❄️",
19
+ layout="wide",
20
+ initial_sidebar_state="expanded"
21
+ )
22
 
23
+ # Custom CSS
24
  st.markdown("""
25
  <style>
26
+ .main .block-container {
27
+ padding-top: 2rem;
28
+ padding-bottom: 2rem;
29
+ }
30
+ h1, h2, h3 {
31
  color: #1E88E5;
 
 
32
  }
33
+ .stTabs [data-baseweb="tab-list"] {
34
+ gap: 2px;
35
+ }
36
+ .stTabs [data-baseweb="tab"] {
37
+ height: 50px;
38
+ white-space: pre-wrap;
39
+ background-color: #F0F2F6;
40
+ border-radius: 4px 4px 0 0;
41
+ gap: 1px;
42
+ padding-top: 10px;
43
+ padding-bottom: 10px;
44
+ }
45
+ .stTabs [aria-selected="true"] {
46
+ background-color: #1E88E5;
47
+ color: white;
48
+ }
49
+ .stButton>button {
50
+ width: 100%;
51
  }
52
  .info-box {
53
  background-color: #E3F2FD;
 
55
  border-radius: 0.5rem;
56
  margin-bottom: 1rem;
57
  }
58
+ .warning-box {
59
+ background-color: #FFEBEE;
60
+ padding: 1rem;
61
+ border-radius: 0.5rem;
62
+ margin-bottom: 1rem;
63
+ }
64
+ .success-box {
65
+ background-color: #E8F5E9;
66
+ padding: 1rem;
67
+ border-radius: 0.5rem;
68
+ margin-bottom: 1rem;
69
+ }
70
  </style>
71
  """, unsafe_allow_html=True)
72
 
73
+ # Application header
74
+ st.title("HVAC Load Calculator")
75
+ st.write("A comprehensive tool for calculating cooling and heating loads based on ASHRAE methods.")
76
 
77
+ # Create tabs for cooling and heating calculators
78
+ tab1, tab2 = st.tabs(["Cooling Load Calculator", "Heating Load Calculator"])
 
 
 
79
 
80
+ with tab1:
 
 
 
81
  cooling_calculator()
 
 
82
 
83
+ with tab2:
84
+ heating_calculator()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
 
86
  if __name__ == "__main__":
87
  main()
cooling_calculator.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ }
2
+
3
+ # Initialize session state for form completion status
4
+ if 'cooling_completed' not in st.session_state:
5
+ st.session_state.cooling_completed = {
6
+ 'building_info': False,
7
+ 'building_envelope': False,
8
+ 'windows': False,
9
+ 'internal_loads': False,
10
+ 'ventilation': False
11
+ }
12
+
13
+ # Initialize session state for calculation results
14
+ if 'cooling_results' not in st.session_state:
15
+ st.session_state.cooling_results = None
16
+
17
+
18
+ def building_info_form(ref_data):
19
+ """
20
+ Form for building information.
21
+
22
+ Args:
23
+ ref_data: Reference data object
24
+ """
25
+ st.subheader("Building Information")
26
+ st.write("Enter general building information, location, and design temperatures.")
27
+
28
+ # Get location options from reference data
29
+ location_options = {loc_id: loc_data['name'] for loc_id, loc_data in ref_data.locations.items()}
30
+
31
+ col1, col2 = st.columns(2)
32
+
33
+ with col1:
34
+ # Building name
35
+ building_name = st.text_input(
36
+ "Building Name",
37
+ value=st.session_state.cooling_form_data['building_info'].get('building_name', ''),
38
+ help="Enter a name for this building or project"
39
+ )
40
+
41
+ # Location selection
42
+ location = st.selectbox(
43
+ "Location",
44
+ options=list(location_options.keys()),
45
+ format_func=lambda x: location_options[x],
46
+ index=list(location_options.keys()).index(st.session_state.cooling_form_data['building_info'].get('location', 'sydney')) if st.session_state.cooling_form_data['building_info'].get('location') in location_options else 0,
47
+ help="Select the location of the building"
48
+ )
49
+
50
+ # Get climate data for selected location
cooling_calculator_enhanced.py ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cooling Load Calculator Page (Enhanced Version)
3
+
4
+ This module implements the cooling load calculator interface for the HVAC Load Calculator web application.
5
+ It provides a step-by-step form for inputting building information and calculates cooling loads
6
+ using the ASHRAE method.
7
+
8
+ Enhancements:
9
+ - Improved navigation to allow editing previous stages
10
+ - Added custom U-value input capability
11
+ - Enhanced visualization components
12
+ """
13
+
14
+ import streamlit as st
15
+ import pandas as pd
16
+ import numpy as np
17
+ import plotly.express as px
18
+ import plotly.graph_objects as go
19
+ import json
20
+ import os
21
+ import sys
22
+ from pathlib import Path
23
+ from datetime import datetime
24
+
25
+ # Add the parent directory to sys.path to import modules
26
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
+
28
+ # Import custom modules
29
+ from cooling_load import CoolingLoadCalculator
30
+ from reference_data import ReferenceData
31
+ from utils.validation import validate_input, ValidationWarning
32
+ from utils.export import export_data
33
+
34
+
35
+ def load_session_state():
36
+ """Initialize or load session state variables."""
37
+ # Initialize session state for form data
38
+ if 'cooling_form_data' not in st.session_state:
39
+ st.session_state.cooling_form_data = {
40
+ 'building_info': {},
41
+ 'building_envelope': {},
42
+ 'windows': {},
43
+ 'internal_loads': {},
44
+ 'ventilation': {},
45
+ 'results': {}
46
+ }
47
+
48
+ # Initialize session state for validation warnings
49
+ if 'cooling_warnings' not in st.session_state:
50
+ st.session_state.cooling_warnings = {
51
+ 'building_info': [],
52
+ 'building_envelope': [],
53
+ 'windows': [],
54
+ 'internal_loads': [],
55
+ 'ventilation': []
56
+ }
57
+
58
+ # Initialize session state for form completion status
59
+ if 'cooling_completed' not in st.session_state:
60
+ st.session_state.cooling_completed = {
61
+ 'building_info': False,
62
+ 'building_envelope': False,
63
+ 'windows': False,
64
+ 'internal_loads': False,
65
+ 'ventilation': False
66
+ }
67
+
68
+ # Initialize session state for calculation results
69
+ if 'cooling_results' not in st.session_state:
70
+ st.session_state.cooling_results = None
71
+
72
+ # Initialize active tab if not already set
73
+ if 'cooling_active_tab' not in st.session_state:
74
+ st.session_state.cooling_active_tab = "building_info"
75
+
76
+
77
+ def building_info_form(ref_data):
78
+ """
79
+ Form for building information.
80
+
81
+ Args:
82
+ ref_data: Reference data object
83
+ """
84
+ st.subheader("Building Information")
85
+ st.write("Enter general building information, location, and design temperatures.")
86
+
87
+ # Get location options from reference data
88
+ location_options = {loc_id: loc_data['name'] for loc_id, loc_data in ref_data.locations.items()}
89
+
90
+ col1, col2 = st.columns(2)
91
+
92
+ with col1:
93
+ # Building name
94
+ building_name = st.text_input(
95
+ "Building Name",
96
+ value=st.session_state.cooling_form_data['building_info'].get('building_name', ''),
97
+ help="Enter a name for this building or project",
98
+ key="cooling_building_name"
99
+ )
100
+
101
+ # Location selection
102
+ location = st.selectbox(
103
+ "Location",
104
+ options=list(location_options.keys()),
105
+ format_func=lambda x: location_options[x],
106
+ index=list(location_options.keys()).index(st.session_state.cooling_form_data['building_info'].get('location', 'sydney')) if st.session_state.cooling_form_data['building_info'].get('location') in location_options else 0,
107
+ help="Select the location of the building",
108
+ key="cooling_location"
109
+ )
110
+
111
+ # Get climate data for selected location
112
+ location_data = ref_data.get_location_data(location)
113
+
114
+ # Indoor design temperature
115
+ indoor_temp = st.number_input(
116
+ "Indoor Design Temperature (°C)",
117
+ value=float(st.session_state.cooling_form_data['building_info'].get('indoor_temp', 24.0)),
118
+ min_value=20.0,
119
+ max_value=30.0,
120
+ step=0.5,
121
+ help="Recommended indoor design temperature for cooling is 24°C",
122
+ key="cooling_indoor_temp"
123
+ )
124
+
125
+ with col2:
126
+ # Building type
127
+ building_type = st.selectbox(
128
+ "Building Type",
129
+ options=["Residential", "Small Office", "Educational", "Other"],
130
+ index=["Residential", "Small Office", "Educational", "Other"].index(st.session_state.cooling_form_data['building_info'].get('building_type', 'Residential')),
131
+ help="Select the type of building",
132
+ key="cooling_building_type"
133
+ )
134
+
135
+ # Outdoor design temperature (with default from location data)
136
+ outdoor_temp = st.number_input(
137
+ "Outdoor Design Temperature (°C)",
138
+ value=float(st.session_state.cooling_form_data['building_info'].get('outdoor_temp', location_data['summer_design_temp'])),
139
+ min_value=20.0,
140
+ max_value=45.0,
141
+ step=0.5,
142
+ help=f"Default value is based on selected location ({location_data['name']})",
143
+ key="cooling_outdoor_temp"
144
+ )
145
+
146
+ # Building dimensions
147
+ st.subheader("Building Dimensions")
148
+
149
+ col1, col2, col3 = st.columns(3)
150
+
151
+ with col1:
152
+ length = st.number_input(
153
+ "Length (m)",
154
+ value=float(st.session_state.cooling_form_data['building_info'].get('length', 10.0)),
155
+ min_value=1.0,
156
+ step=0.1,
157
+ help="Building length in meters",
158
+ key="cooling_length"
159
+ )
160
+
161
+ with col2:
162
+ width = st.number_input(
163
+ "Width (m)",
164
+ value=float(st.session_state.cooling_form_data['building_info'].get('width', 8.0)),
165
+ min_value=1.0,
166
+ step=0.1,
167
+ help="Building width in meters",
168
+ key="cooling_width"
169
+ )
170
+
171
+ with col3:
172
+ height = st.number_input(
173
+ "Height (m)",
174
+ value=float(st.session_state.cooling_form_data['building_info'].get('height', 2.7)),
175
+ min_value=1.0,
176
+ step=0.1,
177
+ help="Floor-to-ceiling height in meters",
178
+ key="cooling_height"
179
+ )
180
+
181
+ # Calculate floor area and volume
182
+ floor_area = length * width
183
+ volume = floor_area * height
184
+
185
+ st.info(f"Floor Area: {floor_area:.2f} m² | Volume: {volume:.2f} m³")
186
+
187
+ # Save form data to session state
188
+ form_data = {
189
+ 'building_name': building_name,
190
+ 'building_type': building_type,
191
+ 'location': location,
192
+ 'location_name': location_data['name'],
193
+ 'indoor_temp': indoor_temp,
194
+ 'outdoor_temp': outdoor_temp,
195
+ 'length': length,
196
+ 'width': width,
197
+ 'height': height,
198
+ 'floor_area': floor_area,
199
+ 'volume': volume,
200
+ 'temp_diff': outdoor_temp - indoor_temp
201
+ }
202
+
203
+ # Validate inputs
204
+ warnings = []
205
+
206
+ # Check if building name is provided
207
+ if not building_name:
208
+ warnings.append(ValidationWarning("Building name is empty", "Consider adding a building name for reference"))
209
+
210
+ # Check if temperature difference is reasonable
211
+ if form_data['temp_diff'] <= 0:
212
+ warnings.append(ValidationWarning(
213
+ "Invalid temperature difference",
214
+ "Outdoor temperature should be higher than indoor temperature for cooling load calculation",
215
+ is_critical=True
216
+ ))
217
+
218
+ # Check if dimensions are reasonable
219
+ if floor_area > 500:
220
+ warnings.append(ValidationWarning(
221
+ "Large floor area",
222
+ "Floor area exceeds 500 m², verify if this is correct for a residential building"
223
+ ))
224
+
225
+ if height < 2.4 or height > 3.5:
226
+ warnings.append(ValidationWarning(
227
+ "Unusual ceiling height",
228
+ "Typical residential ceiling heights are between 2.4m and 3.5m"
229
+ ))
230
+
231
+ # Save warnings to session state
232
+ st.session_state.cooling_warnings['building_info'] = warnings
233
+
234
+ # Display warnings if any
235
+ if warnings:
236
+ st.warning("Please review the following warnings:")
237
+ for warning in warnings:
238
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
239
+ st.write(f" Suggestion: {warning.suggestion}")
240
+
241
+ # Save form data regardless of warnings
242
+ st.session_state.cooling_form_data['building_info'] = form_data
243
+
244
+ # Mark this step as completed if there are no critical warnings
245
+ st.session_state.cooling_completed['building_info'] = not any(w.is_critical for w in warnings)
246
+
247
+ # Navigation buttons
248
+ col1, col2 = st.columns([1, 1])
249
+
250
+ with col2:
251
+ next_button = st.button("Next: Building Envelope →", key="cooling_building_info_next")
252
+ if next_button:
253
+ st.session_state.cooling_active_tab = "building_envelope"
254
+ st.experimental_rerun()
255
+
256
+
257
+ def building_envelope_form(ref_data):
258
+ """
259
+ Form for building envelope information.
260
+
261
+ Args:
262
+ ref_data: Reference data object
263
+ """
264
+ st.subheader("Building Envelope")
265
+ st.write("Enter information about walls, roof, and floor construction.")
266
+
267
+ # Get building dimensions from previous step
268
+ building_info = st.session_state.cooling_form_data['building_info']
269
+ length = building_info.get('length', 10.0)
270
+ width = building_info.get('width', 8.0)
271
+ height = building_info.get('height', 2.7)
272
+ temp_diff = building_info.get('temp_diff', 11.0)
273
+
274
+ # Calculate default areas
275
+ default_wall_area = 2 * (length + width) * height
276
+ default_roof_area = length * width
277
+ default_floor_area = length * width
278
+
279
+ # Initialize envelope data if not already in session state
280
+ if 'walls' not in st.session_state.cooling_form_data['building_envelope']:
281
+ st.session_state.cooling_form_data['building_envelope']['walls'] = []
282
+
283
+ if 'roof' not in st.session_state.cooling_form_data['building_envelope']:
284
+ st.session_state.cooling_form_data['building_envelope']['roof'] = {}
285
+
286
+ if 'floor' not in st.session_state.cooling_form_data['building_envelope']:
287
+ st.session_state.cooling_form_data['building_envelope']['floor'] = {}
288
+
289
+ # Walls section
290
+ st.write("### Walls")
291
+
292
+ # Get wall material options from reference data
293
+ wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
294
+ wall_material_options['custom'] = "Custom U-value"
295
+
296
+ # Display existing wall entries
297
+ if st.session_state.cooling_form_data['building_envelope']['walls']:
298
+ st.write("Current walls:")
299
+ walls_df = pd.DataFrame(st.session_state.cooling_form_data['building_envelope']['walls'])
300
+ walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Custom"))
301
+ walls_df = walls_df[['name', 'Material', 'area', 'u_value']]
302
+ walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)']
303
+ st.dataframe(walls_df)
304
+
305
+ # Add edit/delete buttons for existing walls
306
+ if st.button("Edit/Delete Walls", key="edit_walls_cooling"):
307
+ st.session_state.cooling_edit_walls = True
308
+
309
+ if st.session_state.get('cooling_edit_walls', False):
310
+ st.write("Select a wall to edit or delete:")
311
+ for i, wall in enumerate(st.session_state.cooling_form_data['building_envelope']['walls']):
312
+ col1, col2 = st.columns([3, 1])
313
+ with col1:
314
+ st.write(f"{wall['name']} - {wall_material_options.get(wall['material_id'], 'Custom')}, {wall['area']:.2f} m², U-value: {wall['u_value']:.2f}")
315
+ with col2:
316
+ if st.button("Delete", key=f"delete_wall_{i}_cooling"):
317
+ st.session_state.cooling_form_data['building_envelope']['walls'].pop(i)
318
+ st.experimental_rerun()
319
+
320
+ if st.button("Done Editing", key="done_editing_walls_cooling"):
321
+ st.session_state.cooling_edit_walls = False
322
+ st.experimental_rerun()
323
+
324
+ # Add new wall form
325
+ st.write("Add a new wall:")
326
+
327
+ col1, col2 = st.columns(2)
328
+
329
+ with col1:
330
+ wall_name = st.text_input("Wall Name", value="", key="new_wall_name_cooling")
331
+ wall_material = st.selectbox(
332
+ "Wall Material",
333
+ options=list(wall_material_options.keys()),
334
+ format_func=lambda x: wall_material_options[x],
335
+ key="new_wall_material_cooling"
336
+ )
337
+
338
+ # Get material properties or allow custom U-value
339
+ if wall_material == 'custom':
340
+ u_value = st.number_input(
341
+ "Custom U-Value (W/m²°C)",
342
+ value=1.0,
343
+ min_value=0.1,
344
+ max_value=5.0,
345
+ step=0.1,
346
+ key="new_wall_custom_u_value_cooling"
347
+ )
348
+ st.info("Typical U-values for walls range from 0.15 to 3.0 W/m²°C")
349
+ else:
350
+ material_data = ref_data.get_material_by_type("walls", wall_material)
351
+ u_value = material_data['u_value']
352
+ st.info(f"Material: {material_data['name']}, U-Value: {u_value} W/m²°C")
353
+
354
+ with col2:
355
+ wall_area = st.number_input(
356
+ "Wall Area (m²)",
357
+ value=default_wall_area / 4, # Default to 1/4 of total wall area as a starting point
358
+ min_value=0.1,
359
+ step=0.1,
360
+ key="new_wall_area_cooling"
361
+ )
362
+
363
+ st.write(f"Heat Gain: {u_value * wall_area * temp_diff:.2f} W")
364
+
365
+ # Add wall button
366
+ if st.button("Add Wall", key="add_wall_cooling"):
367
+ new_wall = {
368
+ 'name': wall_name if wall_name else f"Wall {len(st.session_state.cooling_form_data['building_envelope']['walls']) + 1}",
369
+ 'material_id': wall_material,
370
+ 'area': wall_area,
371
+ 'u_value': u_value,
372
+ 'temp_diff': temp_diff
373
+ }
374
+ st.session_state.cooling_form_data['building_envelope']['walls'].append(new_wall)
375
+ st.experimental_rerun()
376
+
377
+ # Roof section
378
+ st.write("### Roof")
379
+
380
+ # Get roof material options from reference data
381
+ roof_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['roofs'].items()}
382
+ roof_material_options['custom'] = "Custom U-value"
383
+
384
+ col1, col2 = st.columns(2)
385
+
386
+ with col1:
387
+ roof_material = st.selectbox(
388
+ "Roof Material",
389
+ options=list(roof_material_options.keys()),
390
+ format_func=lambda x: roof_material_options[x],
391
+ index=list(roof_material_options.keys()).index(st.session_state.cooling_form_data['building_envelope'].get('roof', {}).get('material_id', 'metal_deck_insulated')) if st.session_state.cooling_form_data['building_envelope'].get('roof', {}).get('material_id') in roof_material_options else 0,
392
+ key="roof_material_cooling"
393
+ )
394
+
395
+ # Get material properties or allow custom U-value
396
+ if roof_material == 'custom':
397
+ roof_u_value = st.number_input(
398
+ "Custom U-Value (W/m²°C)",
399
+ value=float(st.session_state.cooling_form_data['building_envelope'].get('roof', {}).get('u_value', 0.5)),
400
+ min_value=0.1,
401
+ max_value=5.0,
402
+ step=0.1,
403
+ key="roof_custom_u_value_cooling"
404
+ )
405
+ st.info("Typical U-values for roofs range from 0.15 to 2.0 W/m²°C")
406
+ else:
407
+ material_data = ref_data.get_material_by_type("roofs", roof_material)
408
+ roof_u_value = material_data['u_value']
409
+ st.info(f"Material: {material_data['name']}, U-Value: {roof_u_value} W/m²°C")
410
+
411
+ with col2:
412
+ roof_area = st.number_input(
413
+ "Roof Area (m²)",
414
+ value=float(st.sess<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>
heating_calculator.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Heating Load Calculator Page
3
+
4
+ This module implements the heating load calculator interface for the HVAC Load Calculator web application.
5
+ It provides a step-by-step form for inputting building information and calculates heating loads
6
+ using the ASHRAE method.
7
+ """
8
+
9
+ import streamlit as st
10
+ import pandas as pd
11
+ import numpy as np
12
+ import plotly.express as px
13
+ import plotly.graph_objects as go
14
+ import json
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+
20
+ # Add the parent directory to sys.path to import modules
21
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
22
+
23
+ # Import custom modules
24
+ from heating_load import HeatingLoadCalculator
25
+ from reference_data import ReferenceData
26
+ from utils.validation import validate_input, ValidationWarning
27
+ from utils.export import export_data
28
+
29
+
30
+ def load_session_state():
31
+ """Initialize or load session state variables."""
32
+ # Initialize session state for form data
33
+ if 'heating_form_data' not in st.session_state:
34
+ st.session_state.heating_form_data = {
35
+ 'building_info': {},
36
+ 'building_envelope': {},
37
+ 'windows': {},
38
+ 'ventilation': {},
39
+ 'occupancy': {},
40
+ 'results': {}
41
+ }
42
+
43
+ # Initialize session state for validation warnings
44
+ if 'heating_warnings' not in st.session_state:
45
+ st.session_state.heating_warnings = {
46
+ 'building_info': [],
47
+ 'building_envelope': [],
48
+ 'windows': [],
49
+ 'ventilation': [],
50
+ 'occupancy': []
heating_calculator_enhanced.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Heating Load Calculator Page (Enhanced Version)
3
+
4
+ This module implements the heating load calculator interface for the HVAC Load Calculator web application.
5
+ It provides a step-by-step form for inputting building information and calculates heating loads
6
+ using the ASHRAE method.
7
+
8
+ Enhancements:
9
+ - Improved navigation to allow editing previous stages
10
+ - Added custom U-value input capability
11
+ - Included internal loads as heating contributors
12
+ - Enhanced visualization components
13
+ """
14
+
15
+ import streamlit as st
16
+ import pandas as pd
17
+ import numpy as np
18
+ import plotly.express as px
19
+ import plotly.graph_objects as go
20
+ import json
21
+ import os
22
+ import sys
23
+ from pathlib import Path
24
+ from datetime import datetime
25
+
26
+ # Add the parent directory to sys.path to import modules
27
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28
+
29
+ # Import custom modules
30
+ from heating_load import HeatingLoadCalculator
31
+ from reference_data import ReferenceData
32
+ from utils.validation import validate_input, ValidationWarning
33
+ from utils.export import export_data
34
+
35
+
36
+ def load_session_state():
37
+ """Initialize or load session state variables."""
38
+ # Initialize session state for form data
39
+ if 'heating_form_data' not in st.session_state:
40
+ st.session_state.heating_form_data = {
41
+ 'building_info': {},
42
+ 'building_envelope': {},
43
+ 'windows': {},
44
+ 'ventilation': {},
45
+ 'internal_loads': {}, # Added internal loads section
46
+ 'results': {}
47
+ }
48
+
49
+ # Initialize session state for validation warnings
50
+ if 'heating_warnings' not in st.session_state:
51
+ st.session_state.heating_warnings = {
52
+ 'building_info': [],
53
+ 'building_envelope': [],
54
+ 'windows': [],
55
+ 'ventilation': [],
56
+ 'internal_loads': [] # Added internal loads section
57
+ }
58
+
59
+ # Initialize session state for form completion status
60
+ if 'heating_completed' not in st.session_state:
61
+ st.session_state.heating_completed = {
62
+ 'building_info': False,
63
+ 'building_envelope': False,
64
+ 'windows': False,
65
+ 'ventilation': False,
66
+ 'internal_loads': False # Added internal loads section
67
+ }
68
+
69
+ # Initialize session state for calculation results
70
+ if 'heating_results' not in st.session_state:
71
+ st.session_state.heating_results = None
72
+
73
+ # Initialize active tab if not already set
74
+ if 'heating_active_tab' not in st.session_state:
75
+ st.session_state.heating_active_tab = "building_info"
76
+
77
+
78
+ def building_info_form(ref_data):
79
+ """
80
+ Form for building information.
81
+
82
+ Args:
83
+ ref_data: Reference data object
84
+ """
85
+ st.subheader("Building Information")
86
+ st.write("Enter general building information, location, and design temperatures.")
87
+
88
+ # Get location options from reference data
89
+ location_options = {loc_id: loc_data['name'] for loc_id, loc_data in ref_data.locations.items()}
90
+
91
+ col1, col2 = st.columns(2)
92
+
93
+ with col1:
94
+ # Building name
95
+ building_name = st.text_input(
96
+ "Building Name",
97
+ value=st.session_state.heating_form_data['building_info'].get('building_name', ''),
98
+ help="Enter a name for this building or project",
99
+ key="heating_building_name"
100
+ )
101
+
102
+ # Location selection
103
+ location = st.selectbox(
104
+ "Location",
105
+ options=list(location_options.keys()),
106
+ format_func=lambda x: location_options[x],
107
+ index=list(location_options.keys()).index(st.session_state.heating_form_data['building_info'].get('location', 'sydney')) if st.session_state.heating_form_data['building_info'].get('location') in location_options else 0,
108
+ help="Select the location of the building",
109
+ key="heating_location"
110
+ )
111
+
112
+ # Get climate data for selected location
113
+ location_data = ref_data.get_location_data(location)
114
+
115
+ # Indoor design temperature
116
+ indoor_temp = st.number_input(
117
+ "Indoor Design Temperature (°C)",
118
+ value=float(st.session_state.heating_form_data['building_info'].get('indoor_temp', 21.0)),
119
+ min_value=15.0,
120
+ max_value=25.0,
121
+ step=0.5,
122
+ help="Recommended indoor design temperature for heating is 21°C",
123
+ key="heating_indoor_temp"
124
+ )
125
+
126
+ with col2:
127
+ # Building type
128
+ building_type = st.selectbox(
129
+ "Building Type",
130
+ options=["Residential", "Small Office", "Educational", "Other"],
131
+ index=["Residential", "Small Office", "Educational", "Other"].index(st.session_state.heating_form_data['building_info'].get('building_type', 'Residential')),
132
+ help="Select the type of building",
133
+ key="heating_building_type"
134
+ )
135
+
136
+ # Outdoor design temperature (with default from location data)
137
+ outdoor_temp = st.number_input(
138
+ "Outdoor Design Temperature (°C)",
139
+ value=float(st.session_state.heating_form_data['building_info'].get('outdoor_temp', location_data['winter_design_temp'])),
140
+ min_value=-20.0,
141
+ max_value=15.0,
142
+ step=0.5,
143
+ help=f"Default value is based on selected location ({location_data['name']})",
144
+ key="heating_outdoor_temp"
145
+ )
146
+
147
+ # Building dimensions
148
+ st.subheader("Building Dimensions")
149
+
150
+ col1, col2, col3 = st.columns(3)
151
+
152
+ with col1:
153
+ length = st.number_input(
154
+ "Length (m)",
155
+ value=float(st.session_state.heating_form_data['building_info'].get('length', 10.0)),
156
+ min_value=1.0,
157
+ step=0.1,
158
+ help="Building length in meters",
159
+ key="heating_length"
160
+ )
161
+
162
+ with col2:
163
+ width = st.number_input(
164
+ "Width (m)",
165
+ value=float(st.session_state.heating_form_data['building_info'].get('width', 8.0)),
166
+ min_value=1.0,
167
+ step=0.1,
168
+ help="Building width in meters",
169
+ key="heating_width"
170
+ )
171
+
172
+ with col3:
173
+ height = st.number_input(
174
+ "Height (m)",
175
+ value=float(st.session_state.heating_form_data['building_info'].get('height', 2.7)),
176
+ min_value=1.0,
177
+ step=0.1,
178
+ help="Floor-to-ceiling height in meters",
179
+ key="heating_height"
180
+ )
181
+
182
+ # Calculate floor area and volume
183
+ floor_area = length * width
184
+ volume = floor_area * height
185
+
186
+ st.info(f"Floor Area: {floor_area:.2f} m² | Volume: {volume:.2f} m³")
187
+
188
+ # Save form data to session state
189
+ form_data = {
190
+ 'building_name': building_name,
191
+ 'building_type': building_type,
192
+ 'location': location,
193
+ 'location_name': location_data['name'],
194
+ 'indoor_temp': indoor_temp,
195
+ 'outdoor_temp': outdoor_temp,
196
+ 'length': length,
197
+ 'width': width,
198
+ 'height': height,
199
+ 'floor_area': floor_area,
200
+ 'volume': volume,
201
+ 'temp_diff': indoor_temp - outdoor_temp
202
+ }
203
+
204
+ # Validate inputs
205
+ warnings = []
206
+
207
+ # Check if building name is provided
208
+ if not building_name:
209
+ warnings.append(ValidationWarning("Building name is empty", "Consider adding a building name for reference"))
210
+
211
+ # Check if temperature difference is reasonable
212
+ if form_data['temp_diff'] <= 0:
213
+ warnings.append(ValidationWarning(
214
+ "Invalid temperature difference",
215
+ "Indoor temperature should be higher than outdoor temperature for heating load calculation",
216
+ is_critical=True
217
+ ))
218
+
219
+ # Check if dimensions are reasonable
220
+ if floor_area > 500:
221
+ warnings.append(ValidationWarning(
222
+ "Large floor area",
223
+ "Floor area exceeds 500 m², verify if this is correct for a residential building"
224
+ ))
225
+
226
+ if height < 2.4 or height > 3.5:
227
+ warnings.append(ValidationWarning(
228
+ "Unusual ceiling height",
229
+ "Typical residential ceiling heights are between 2.4m and 3.5m"
230
+ ))
231
+
232
+ # Save warnings to session state
233
+ st.session_state.heating_warnings['building_info'] = warnings
234
+
235
+ # Display warnings if any
236
+ if warnings:
237
+ st.warning("Please review the following warnings:")
238
+ for warning in warnings:
239
+ st.write(f"- {warning.message}" + (" (Critical)" if warning.is_critical else ""))
240
+ st.write(f" Suggestion: {warning.suggestion}")
241
+
242
+ # Save form data regardless of warnings
243
+ st.session_state.heating_form_data['building_info'] = form_data
244
+
245
+ # Mark this step as completed if there are no critical warnings
246
+ st.session_state.heating_completed['building_info'] = not any(w.is_critical for w in warnings)
247
+
248
+ # Navigation buttons
249
+ col1, col2 = st.columns([1, 1])
250
+
251
+ with col2:
252
+ next_button = st.button("Next: Building Envelope →", key="heating_building_info_next")
253
+ if next_button:
254
+ st.session_state.heating_active_tab = "building_envelope"
255
+ st.experimental_rerun()
256
+
257
+
258
+ def building_envelope_form(ref_data):
259
+ """
260
+ Form for building envelope information.
261
+
262
+ Args:
263
+ ref_data: Reference data object
264
+ """
265
+ st.subheader("Building Envelope")
266
+ st.write("Enter information about walls, roof, and floor construction.")
267
+
268
+ # Get building dimensions from previous step
269
+ building_info = st.session_state.heating_form_data['building_info']
270
+ length = building_info.get('length', 10.0)
271
+ width = building_info.get('width', 8.0)
272
+ height = building_info.get('height', 2.7)
273
+ temp_diff = building_info.get('temp_diff', 21.0)
274
+
275
+ # Calculate default areas
276
+ default_wall_area = 2 * (length + width) * height
277
+ default_roof_area = length * width
278
+ default_floor_area = length * width
279
+
280
+ # Initialize envelope data if not already in session state
281
+ if 'walls' not in st.session_state.heating_form_data['building_envelope']:
282
+ st.session_state.heating_form_data['building_envelope']['walls'] = []
283
+
284
+ if 'roof' not in st.session_state.heating_form_data['building_envelope']:
285
+ st.session_state.heating_form_data['building_envelope']['roof'] = {}
286
+
287
+ if 'floor' not in st.session_state.heating_form_data['building_envelope']:
288
+ st.session_state.heating_form_data['building_envelope']['floor'] = {}
289
+
290
+ # Walls section
291
+ st.write("### Walls")
292
+
293
+ # Get wall material options from reference data
294
+ wall_material_options = {mat_id: mat_data['name'] for mat_id, mat_data in ref_data.materials['walls'].items()}
295
+ wall_material_options['custom'] = "Custom U-value"
296
+
297
+ # Display existing wall entries
298
+ if st.session_state.heating_form_data['building_envelope']['walls']:
299
+ st.write("Current walls:")
300
+ walls_df = pd.DataFrame(st.session_state.heating_form_data['building_envelope']['walls'])
301
+ walls_df['Material'] = walls_df['material_id'].map(lambda x: wall_material_options.get(x, "Custom"))
302
+ walls_df = walls_df[['name', 'Material', 'area', 'u_value']]
303
+ walls_df.columns = ['Name', 'Material', 'Area (m²)', 'U-Value (W/m²°C)']
304
+ st.dataframe(walls_df)
305
+
306
+ # Add edit/delete buttons for existing walls
307
+ if st.button("Edit/Delete Walls", key="edit_walls_heating"):
308
+ st.session_state.heating_edit_walls = True
309
+
310
+ if st.session_state.get('heating_edit_walls', False):
311
+ st.write("Select a wall to edit or delete:")
312
+ for i, wall in enumerate(st.session_state.heating_form_data['building_envelope']['walls']):
313
+ col1, col2 = st.columns([3, 1])
314
+ with col1:
315
+ st.write(f"{wall['name']} - {wall_material_options.get(wall['material_id'], 'Custom')}, {wall['area']:.2f} m², U-value: {wall['u_value']:.2f}")
316
+ with col2:
317
+ if st.button("Delete", key=f"delete_wall_{i}_heating"):
318
+ st.session_state.heating_form_data['building_envelope']['walls'].pop(i)
319
+ st.experimental_rerun()
320
+
321
+ if st.button("Done Editing", key="done_editing_walls_heating"):
322
+ st.session_state.heating_edit_walls = False
323
+ st.experimental_rerun()
324
+
325
+ # Add new wall form
326
+ st.write("Add a new wall:")
327
+
328
+ col1, col2 = st.columns(2)
329
+
330
+ with col1:
331
+ wall_name = st.text_input("Wall Name", value="", key="new_wall_name_heating")
332
+ wall_material = st.selectbox(
333
+ "Wall Material",
334
+ options=list(wall_material_options.keys()),
335
+ format_func=lambda x: wall_material_options[x],
336
+ key="new_wall_material_heating"
337
+ )
338
+
339
+ # Get material properties or allow custom U-value
340
+ if wall_material == 'custom':
341
+ u_value = st.number_input(
342
+ "Custom U-Value (W/m²°C)",
343
+ value=1.0,
344
+ min_value=0.1,
345
+ max_value=5.0,
346
+ step=0.1,
347
+ key="new_wall_custom_u_value_heating"
348
+ )
349
+ st.info("Typical U-values for walls range from 0.15 to 3.0 W/m²°C")
350
+ else:
351
+ material_data = ref_data.get_material_by_type("walls", wall_material)
352
+ u_value = material_data['u_value']
353
+ st.info(f"Material: {material_data['name']}, U-Value: {u_value} W/m²°C")
354
+
355
+ with col2:
356
+ wall_area = st.number_input(
357
+ "Wall Area (m²)",
358
+ value=default_wall_area / 4, # Default to 1/4 of total wall area as a starting point
359
+ min_value=0.1,
360
+ step=0.1,
361
+ key="new_wall_area_heating"
362
+ )
363
+
364
+ st.write(f"Heat Loss: {u_value * wall_area * temp_diff:.2f} W")
365
+
366
+ # Add wall button
367
+ if st.button("Add Wall", key="add_wall_heating"):
368
+ new_wall = {
369
+ 'name': wall_name if wall_name else f"Wall {len(st.session_state.heating_form_data['building_envelope']['walls']) + 1}",
370
+ 'material_id': wall_material,
371
+ 'area': wall_area,
372
+ 'u_value': u_value,
373
+ 'temp_diff': temp_diff
374
+ }
375
+ st.session_state.heating_form_data['building_envelope']['walls'].append(new_wall)
376
+ st.experimental_rerun()
377
+
378
+ # Roof section
379
+ st.write("### Roof")
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
+ roof_material_options['custom'] = "Custom U-value"
384
+
385
+ col1, col2 = st.columns(2)
386
+
387
+ with col1:
388
+ roof_material = st.selectbox(
389
+ "Roof Material",
390
+ options=list(roof_material_options.keys()),
391
+ format_func=lambda x: roof_material_options[x],
392
+ index=list(roof_material_options.keys()).index(st.session_state.heating_form_data['building_envelope'].get('roof', {}).get('material_id', 'metal_deck_insulated')) if st.session_state.heating_form_data['building_envelope'].get('roof', {}).get('material_id') in roof_material_options else 0,
393
+ key="roof_material_heating"
394
+ )
395
+
396
+ # Get material properties or allow custom U-value
397
+ if roof_material == 'custom':
398
+ roof_u_value = st.number_input(
399
+ "Custom U-Value (W/m²°C)",
400
+ value=float(st.session_state.heating_form_data['building_envelope'].get('roof', {}).get('u_value', 0.5)),
401
+ min_value=0.1,
402
+ max_value=5.0,
403
+ step=0.1,
404
+ key="roof_custom_u_value_heating"
405
+ )
406
+ st.info("Typical U-values for roofs range from 0.15 to 2.0 W/m²°C")
407
+ else:
408
+ material_data = ref_data.get_material_by_type("roofs", roof_material)
409
+ roof_u_value = material_data['u_value']
410
+ st.info(f"Material: {material_data['name']}, U-Value:<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>
visualization.py ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Visualization utilities for HVAC Load Calculator
3
+
4
+ This module provides enhanced visualization functions for creating interactive
5
+ and informative charts for the HVAC Load Calculator application.
6
+ """
7
+
8
+ import plotly.express as px
9
+ import plotly.graph_objects as go
10
+ import pandas as pd
11
+ import numpy as np
12
+
13
+
14
+ def create_load_breakdown_chart(load_components, title="Load Breakdown"):
15
+ """
16
+ Create an enhanced pie chart for load components breakdown.
17
+
18
+ Args:
19
+ load_components (dict): Dictionary of load components and their values
20
+ title (str): Chart title
21
+
22
+ Returns:
23
+ plotly.graph_objects.Figure: Interactive pie chart
24
+ """
25
+ # Remove zero values
26
+ load_components = {k: v for k, v in load_components.items() if v > 0}
27
+
28
+ # Create figure
29
+ fig = go.Figure()
30
+
31
+ # Add pie chart
32
+ fig.add_trace(go.Pie(
33
+ labels=list(load_components.keys()),
34
+ values=list(load_components.values()),
35
+ textinfo='label+percent',
36
+ insidetextorientation='radial',
37
+ marker=dict(
38
+ colors=px.colors.qualitative.Bold,
39
+ line=dict(color='white', width=2)
40
+ ),
41
+ pull=[0.05 if x == max(load_components.values()) else 0 for x in load_components.values()],
42
+ hovertemplate='<b>%{label}</b><br>%{value:.1f} W<br>%{percent}<extra></extra>'
43
+ ))
44
+
45
+ # Update layout
46
+ fig.update_layout(
47
+ title={
48
+ 'text': title,
49
+ 'y': 0.95,
50
+ 'x': 0.5,
51
+ 'xanchor': 'center',
52
+ 'yanchor': 'top',
53
+ 'font': dict(size=20)
54
+ },
55
+ legend=dict(
56
+ orientation="h",
57
+ yanchor="bottom",
58
+ y=-0.2,
59
+ xanchor="center",
60
+ x=0.5,
61
+ font=dict(size=12)
62
+ ),
63
+ height=500,
64
+ margin=dict(t=80, b=80, l=40, r=40),
65
+ paper_bgcolor='rgba(0,0,0,0)',
66
+ plot_bgcolor='rgba(0,0,0,0)'
67
+ )
68
+
69
+ return fig
70
+
71
+
72
+ def create_component_bar_chart(df, x_col, y_col, color_col=None, title="Component Breakdown"):
73
+ """
74
+ Create an enhanced bar chart for component breakdown.
75
+
76
+ Args:
77
+ df (pd.DataFrame): DataFrame containing the data
78
+ x_col (str): Column name for x-axis
79
+ y_col (str): Column name for y-axis
80
+ color_col (str, optional): Column name for color grouping
81
+ title (str): Chart title
82
+
83
+ Returns:
84
+ plotly.graph_objects.Figure: Interactive bar chart
85
+ """
86
+ # Create figure
87
+ if color_col:
88
+ fig = px.bar(
89
+ df,
90
+ x=x_col,
91
+ y=y_col,
92
+ color=color_col,
93
+ title=title,
94
+ color_discrete_sequence=px.colors.qualitative.Bold,
95
+ height=500,
96
+ text=y_col
97
+ )
98
+ else:
99
+ fig = px.bar(
100
+ df,
101
+ x=x_col,
102
+ y=y_col,
103
+ title=title,
104
+ color_discrete_sequence=px.colors.qualitative.Bold,
105
+ height=500,
106
+ text=y_col
107
+ )
108
+
109
+ # Update layout
110
+ fig.update_layout(
111
+ xaxis_title=x_col,
112
+ yaxis_title=y_col,
113
+ legend_title=color_col if color_col else "",
114
+ font=dict(size=12),
115
+ xaxis={'categoryorder': 'total descending'},
116
+ paper_bgcolor='rgba(0,0,0,0)',
117
+ plot_bgcolor='rgba(0,0,0,0)',
118
+ hovermode="x unified"
119
+ )
120
+
121
+ # Add data labels
122
+ fig.update_traces(
123
+ texttemplate='%{y:.1f}',
124
+ textposition='outside',
125
+ hovertemplate='<b>%{x}</b><br>%{y:.1f}<extra></extra>'
126
+ )
127
+
128
+ # Add grid lines
129
+ fig.update_yaxes(
130
+ showgrid=True,
131
+ gridwidth=1,
132
+ gridcolor='rgba(211,211,211,0.3)'
133
+ )
134
+
135
+ return fig
136
+
137
+
138
+ def create_stacked_bar_chart(df, x_col, y_cols, names, title="Stacked Bar Chart"):
139
+ """
140
+ Create an enhanced stacked bar chart.
141
+
142
+ Args:
143
+ df (pd.DataFrame): DataFrame containing the data
144
+ x_col (str): Column name for x-axis
145
+ y_cols (list): List of column names for y-axis values
146
+ names (list): List of names for each y-column
147
+ title (str): Chart title
148
+
149
+ Returns:
150
+ plotly.graph_objects.Figure: Interactive stacked bar chart
151
+ """
152
+ # Create figure
153
+ fig = go.Figure()
154
+
155
+ # Add bars for each y column
156
+ for i, y_col in enumerate(y_cols):
157
+ fig.add_trace(go.Bar(
158
+ x=df[x_col],
159
+ y=df[y_col],
160
+ name=names[i],
161
+ hovertemplate=f'<b>{names[i]}</b>: %{{y:.1f}}<extra></extra>'
162
+ ))
163
+
164
+ # Update layout
165
+ fig.update_layout(
166
+ title=title,
167
+ xaxis_title=x_col,
168
+ yaxis_title="Value",
169
+ barmode='stack',
170
+ height=500,
171
+ legend=dict(
172
+ orientation="h",
173
+ yanchor="bottom",
174
+ y=1.02,
175
+ xanchor="center",
176
+ x=0.5
177
+ ),
178
+ paper_bgcolor='rgba(0,0,0,0)',
179
+ plot_bgcolor='rgba(0,0,0,0)',
180
+ hovermode="x unified"
181
+ )
182
+
183
+ # Add grid lines
184
+ fig.update_yaxes(
185
+ showgrid=True,
186
+ gridwidth=1,
187
+ gridcolor='rgba(211,211,211,0.3)'
188
+ )
189
+
190
+ return fig
191
+
192
+
193
+ def create_grouped_bar_chart(df, x_col, y_cols, names, title="Grouped Bar Chart"):
194
+ """
195
+ Create an enhanced grouped bar chart.
196
+
197
+ Args:
198
+ df (pd.DataFrame): DataFrame containing the data
199
+ x_col (str): Column name for x-axis
200
+ y_cols (list): List of column names for y-axis values
201
+ names (list): List of names for each y-column
202
+ title (str): Chart title
203
+
204
+ Returns:
205
+ plotly.graph_objects.Figure: Interactive grouped bar chart
206
+ """
207
+ # Create figure
208
+ fig = go.Figure()
209
+
210
+ # Add bars for each y column
211
+ for i, y_col in enumerate(y_cols):
212
+ fig.add_trace(go.Bar(
213
+ x=df[x_col],
214
+ y=df[y_col],
215
+ name=names[i],
216
+ hovertemplate=f'<b>{names[i]}</b>: %{{y:.1f}}<extra></extra>'
217
+ ))
218
+
219
+ # Update layout
220
+ fig.update_layout(
221
+ title=title,
222
+ xaxis_title=x_col,
223
+ yaxis_title="Value",
224
+ barmode='group',
225
+ height=500,
226
+ legend=dict(
227
+ orientation="h",
228
+ yanchor="bottom",
229
+ y=1.02,
230
+ xanchor="center",
231
+ x=0.5
232
+ ),
233
+ paper_bgcolor='rgba(0,0,0,0)',
234
+ plot_bgcolor='rgba(0,0,0,0)',
235
+ hovermode="x unified"
236
+ )
237
+
238
+ # Add grid lines
239
+ fig.update_yaxes(
240
+ showgrid=True,
241
+ gridwidth=1,
242
+ gridcolor='rgba(211,211,211,0.3)'
243
+ )
244
+
245
+ return fig
246
+
247
+
248
+ def create_heat_map_chart(df, x_col, y_col, z_col, title="Heat Map"):
249
+ """
250
+ Create an enhanced heat map chart.
251
+
252
+ Args:
253
+ df (pd.DataFrame): DataFrame containing the data
254
+ x_col (str): Column name for x-axis
255
+ y_col (str): Column name for y-axis
256
+ z_col (str): Column name for z-axis (color)
257
+ title (str): Chart title
258
+
259
+ Returns:
260
+ plotly.graph_objects.Figure: Interactive heat map chart
261
+ """
262
+ # Create figure
263
+ fig = px.density_heatmap(
264
+ df,
265
+ x=x_col,
266
+ y=y_col,
267
+ z=z_col,
268
+ title=title,
269
+ color_continuous_scale="Viridis",
270
+ height=500
271
+ )
272
+
273
+ # Update layout
274
+ fig.update_layout(
275
+ xaxis_title=x_col,
276
+ yaxis_title=y_col,
277
+ font=dict(size=12),
278
+ paper_bgcolor='rgba(0,0,0,0)',
279
+ plot_bgcolor='rgba(0,0,0,0)'
280
+ )
281
+
282
+ return fig
283
+
284
+
285
+ def create_line_chart(df, x_col, y_cols, names, title="Line Chart"):
286
+ """
287
+ Create an enhanced line chart.
288
+
289
+ Args:
290
+ df (pd.DataFrame): DataFrame containing the data
291
+ x_col (str): Column name for x-axis
292
+ y_cols (list): List of column names for y-axis values
293
+ names (list): List of names for each y-column
294
+ title (str): Chart title
295
+
296
+ Returns:
297
+ plotly.graph_objects.Figure: Interactive line chart
298
+ """
299
+ # Create figure
300
+ fig = go.Figure()
301
+
302
+ # Add lines for each y column
303
+ for i, y_col in enumerate(y_cols):
304
+ fig.add_trace(go.Scatter(
305
+ x=df[x_col],
306
+ y=df[y_col],
307
+ mode='lines+markers',
308
+ name=names[i],
309
+ hovertemplate=f'<b>{names[i]}</b>: %{{y:.1f}}<extra></extra>'
310
+ ))
311
+
312
+ # Update layout
313
+ fig.update_layout(
314
+ title=title,
315
+ xaxis_title=x_col,
316
+ yaxis_title="Value",
317
+ height=500,
318
+ legend=dict(
319
+ orientation="h",
320
+ yanchor="bottom",
321
+ y=1.02,
322
+ xanchor="center",
323
+ x=0.5
324
+ ),
325
+ paper_bgcolor='rgba(0,0,0,0)',
326
+ plot_bgcolor='rgba(0,0,0,0)',
327
+ hovermode="x unified"
328
+ )
329
+
330
+ # Add grid lines
331
+ fig.update_yaxes(
332
+ showgrid=True,
333
+ gridwidth=1,
334
+ gridcolor='rgba(211,211,211,0.3)'
335
+ )
336
+
337
+ fig.update_xaxes(
338
+ showgrid=True,
339
+ gridwidth=1,
340
+ gridcolor='rgba(211,211,211,0.3)'
341
+ )
342
+
343
+ return fig
344
+
345
+
346
+ def create_sankey_diagram(nodes, links, title="Energy Flow"):
347
+ """
348
+ Create a Sankey diagram for energy flow visualization.
349
+
350
+ Args:
351
+ nodes (list): List of node labels
352
+ links (dict): Dictionary with source, target, and value lists
353
+ title (str): Chart title
354
+
355
+ Returns:
356
+ plotly.graph_objects.Figure: Interactive Sankey diagram
357
+ """
358
+ # Create figure
359
+ fig = go.Figure(data=[go.Sankey(
360
+ node=dict(
361
+ pad=15,
362
+ thickness=20,
363
+ line=dict(color="black", width=0.5),
364
+ label=nodes,
365
+ color="blue"
366
+ ),
367
+ link=dict(
368
+ source=links['source'],
369
+ target=links['target'],
370
+ value=links['value'],
371
+ hovertemplate='%{source.label} → %{target.label}: %{value:.1f} W<extra></extra>'
372
+ )
373
+ )])
374
+
375
+ # Update layout
376
+ fig.update_layout(
377
+ title=title,
378
+ font=dict(size=12),
379
+ height=600,
380
+ paper_bgcolor='rgba(0,0,0,0)',
381
+ plot_bgcolor='rgba(0,0,0,0)'
382
+ )
383
+
384
+ return fig
385
+
386
+
387
+ def create_gauge_chart(value, min_val, max_val, title="Gauge", threshold_values=None, threshold_colors=None):
388
+ """
389
+ Create a gauge chart for displaying a value within a range.
390
+
391
+ Args:
392
+ value (float): Value to display
393
+ min_val (float): Minimum value of the range
394
+ max_val (float): Maximum value of the range
395
+ title (str): Chart title
396
+ threshold_values (list, optional): List of threshold values
397
+ threshold_colors (list, optional): List of colors for each threshold
398
+
399
+ Returns:
400
+ plotly.graph_objects.Figure: Interactive gauge chart
401
+ """
402
+ # Set default thresholds if not provided
403
+ if threshold_values is None:
404
+ threshold_values = [min_val, (min_val + max_val) / 2, max_val]
405
+
406
+ if threshold_colors is None:
407
+ threshold_colors = ["green", "yellow", "red"]
408
+
409
+ # Create figure
410
+ fig = go.Figure(go.Indicator(
411
+ mode="gauge+number",
412
+ value=value,
413
+ domain={'x': [0, 1], 'y': [0, 1]},
414
+ title={'text': title},
415
+ gauge={
416
+ 'axis': {'range': [min_val, max_val]},
417
+ 'bar': {'color': "darkblue"},
418
+ 'steps': [
419
+ {'range': [threshold_values[i], threshold_values[i+1]], 'color': threshold_colors[i]}
420
+ for i in range(len(threshold_values)-1)
421
+ ],
422
+ 'threshold': {
423
+ 'line': {'color': "red", 'width': 4},
424
+ 'thickness': 0.75,
425
+ 'value': value
426
+ }
427
+ }
428
+ ))
429
+
430
+ # Update layout
431
+ fig.update_layout(
432
+ height=300,
433
+ paper_bgcolor='rgba(0,0,0,0)',
434
+ plot_bgcolor='rgba(0,0,0,0)'
435
+ )
436
+
437
+ return fig
438
+
439
+
440
+ def create_enhanced_results_visualization(results):
441
+ """
442
+ Create enhanced visualizations for HVAC load calculation results.
443
+
444
+ Args:
445
+ results (dict): Dictionary containing calculation results
446
+
447
+ Returns:
448
+ dict: Dictionary of plotly figures
449
+ """
450
+ figures = {}
451
+
452
+ # Prepare data for load breakdown pie chart
453
+ load_components = {
454
+ 'Walls': results.get('wall_loss', 0),
455
+ 'Roof': results.get('roof_loss', 0),
456
+ 'Floor': results.get('floor_loss', 0),
457
+ 'Windows & Doors': results.get('window_loss', 0),
458
+ 'Infiltration': results.get('infiltration_loss', 0),
459
+ 'Ventilation': results.get('ventilation_loss', 0) - results.get('infiltration_loss', 0)
460
+ }
461
+
462
+ # Create load breakdown pie chart
463
+ figures['load_breakdown'] = create_load_breakdown_chart(
464
+ load_components,
465
+ title="Heating Load Components"
466
+ )
467
+
468
+ # Create energy flow Sankey diagram
469
+ if 'internal_gain' in results and results['internal_gain'] > 0:
470
+ # Create nodes and links for Sankey diagram
471
+ nodes = [
472
+ "Walls", "Roof", "Floor", "Windows & Doors",
473
+ "Infiltration", "Ventilation", "Internal Gains",
474
+ "Total Heat Loss", "Net Heating Load"
475
+ ]
476
+
477
+ links = {
478
+ 'source': [0, 1, 2, 3, 4, 5, 7, 6],
479
+ 'target': [7, 7, 7, 7, 7, 7, 8, 8],
480
+ 'value': [
481
+ load_components['Walls'],
482
+ load_components['Roof'],
483
+ load_components['Floor'],
484
+ load_components['Windows & Doors'],
485
+ load_components['Infiltration'],
486
+ load_components['Ventilation'],
487
+ results['internal_gain'],
488
+ results['total_heat_loss']
489
+ ]
490
+ }
491
+
492
+ figures['energy_flow'] = create_sankey_diagram(
493
+ nodes,
494
+ links,
495
+ title="Heating Energy Flow"
496
+ )
497
+
498
+ # Create gauge chart for heating load per area
499
+ if 'net_heating_load' in results and 'building_info' in results:
500
+ floor_area = results['building_info'].get('floor_area', 80.0)
501
+ heating_load_per_area = results['net_heating_load'] / floor_area
502
+
503
+ figures['load_per_area_gauge'] = create_gauge_chart(
504
+ heating_load_per_area,
505
+ 0,
506
+ 200,
507
+ title="Heating Load per Area (W/m²)",
508
+ threshold_values=[0, 50, 100, 150, 200],
509
+ threshold_colors=["green", "lightgreen", "yellow", "orange", "red"]
510
+ )
511
+
512
+ return figures