mabuseif commited on
Commit
705acaf
·
verified ·
1 Parent(s): fa218dc

Upload 32 files

Browse files
Files changed (2) hide show
  1. app/component_selection_redesign.py +1321 -0
  2. app/main.py +544 -401
app/component_selection_redesign.py ADDED
@@ -0,0 +1,1321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Component selection interface for HVAC Load Calculator.
3
+ This module provides the UI components for selecting building components.
4
+ This is a redesigned version with simplified grouping by orientation.
5
+ """
6
+
7
+ import streamlit as st
8
+ import pandas as pd
9
+ import numpy as np
10
+ from typing import Dict, List, Any, Optional, Tuple
11
+ import json
12
+ import os
13
+ import uuid
14
+
15
+ # Import data models
16
+ from data.building_components import Wall, Roof, Floor, Window, Door, Orientation, ComponentType
17
+ from utils.component_library import ComponentLibrary
18
+ from utils.u_value_calculator import UValueCalculator
19
+
20
+
21
+ class ComponentSelectionRedesigned:
22
+ """Class for redesigned component selection interface with orientation-based grouping."""
23
+
24
+ def __init__(self):
25
+ """Initialize component selection interface."""
26
+ self.component_library = ComponentLibrary()
27
+ self.u_value_calculator = UValueCalculator()
28
+
29
+ def display(self) -> None:
30
+ """Display component selection interface in Streamlit."""
31
+ # Check for pending actions in session state and process them
32
+ self._process_pending_actions(st.session_state)
33
+ self.display_component_selection(st.session_state)
34
+
35
+ def _process_pending_actions(self, session_state: Dict[str, Any]) -> None:
36
+ """
37
+ Process any pending actions stored in session state.
38
+
39
+ Args:
40
+ session_state: Streamlit session state
41
+ """
42
+ # Ensure components structure exists
43
+ if "components" not in session_state:
44
+ session_state["components"] = {
45
+ 'walls': [],
46
+ 'roofs': [],
47
+ 'floors': [],
48
+ 'windows': [],
49
+ 'doors': []
50
+ }
51
+
52
+ # Initialize pending actions if not exists
53
+ if "pending_actions" not in session_state:
54
+ session_state["pending_actions"] = {}
55
+
56
+ # Ensure all required pending action keys exist
57
+ required_keys = [
58
+ "add_wall", "delete_wall",
59
+ "add_roof", "delete_roof",
60
+ "add_floor", "delete_floor",
61
+ "add_window", "delete_window",
62
+ "add_door", "delete_door"
63
+ ]
64
+
65
+ for key in required_keys:
66
+ if key not in session_state["pending_actions"]:
67
+ session_state["pending_actions"][key] = None
68
+
69
+ # Process add wall action
70
+ if session_state["pending_actions"].get("add_wall") is not None:
71
+ new_wall = session_state["pending_actions"]["add_wall"]
72
+ session_state["components"]["walls"].append(new_wall)
73
+ session_state["pending_actions"]["add_wall"] = None
74
+
75
+ # Process delete wall action
76
+ if session_state["pending_actions"].get("delete_wall") is not None:
77
+ wall_index = session_state["pending_actions"]["delete_wall"]
78
+ if 0 <= wall_index < len(session_state["components"]["walls"]):
79
+ session_state["components"]["walls"].pop(wall_index)
80
+ session_state["pending_actions"]["delete_wall"] = None
81
+
82
+ # Process add roof action
83
+ if session_state["pending_actions"].get("add_roof") is not None:
84
+ new_roof = session_state["pending_actions"]["add_roof"]
85
+ session_state["components"]["roofs"].append(new_roof)
86
+ session_state["pending_actions"]["add_roof"] = None
87
+
88
+ # Process delete roof action
89
+ if session_state["pending_actions"].get("delete_roof") is not None:
90
+ roof_index = session_state["pending_actions"]["delete_roof"]
91
+ if 0 <= roof_index < len(session_state["components"]["roofs"]):
92
+ session_state["components"]["roofs"].pop(roof_index)
93
+ session_state["pending_actions"]["delete_roof"] = None
94
+
95
+ # Process add floor action
96
+ if session_state["pending_actions"].get("add_floor") is not None:
97
+ new_floor = session_state["pending_actions"]["add_floor"]
98
+ session_state["components"]["floors"].append(new_floor)
99
+ session_state["pending_actions"]["add_floor"] = None
100
+
101
+ # Process delete floor action
102
+ if session_state["pending_actions"].get("delete_floor") is not None:
103
+ floor_index = session_state["pending_actions"]["delete_floor"]
104
+ if 0 <= floor_index < len(session_state["components"]["floors"]):
105
+ session_state["components"]["floors"].pop(floor_index)
106
+ session_state["pending_actions"]["delete_floor"] = None
107
+
108
+ # Process add window action
109
+ if session_state["pending_actions"].get("add_window") is not None:
110
+ new_window = session_state["pending_actions"]["add_window"]
111
+ session_state["components"]["windows"].append(new_window)
112
+ session_state["pending_actions"]["add_window"] = None
113
+
114
+ # Process delete window action
115
+ if session_state["pending_actions"].get("delete_window") is not None:
116
+ window_index = session_state["pending_actions"]["delete_window"]
117
+ if 0 <= window_index < len(session_state["components"]["windows"]):
118
+ session_state["components"]["windows"].pop(window_index)
119
+ session_state["pending_actions"]["delete_window"] = None
120
+
121
+ # Process add door action
122
+ if session_state["pending_actions"].get("add_door") is not None:
123
+ new_door = session_state["pending_actions"]["add_door"]
124
+ session_state["components"]["doors"].append(new_door)
125
+ session_state["pending_actions"]["add_door"] = None
126
+
127
+ # Process delete door action
128
+ if session_state["pending_actions"].get("delete_door") is not None:
129
+ door_index = session_state["pending_actions"]["delete_door"]
130
+ if 0 <= door_index < len(session_state["components"]["doors"]):
131
+ session_state["components"]["doors"].pop(door_index)
132
+ session_state["pending_actions"]["delete_door"] = None
133
+
134
+ def display_component_selection(self, session_state: Dict[str, Any]) -> None:
135
+ """
136
+ Display component selection interface in Streamlit.
137
+
138
+ Args:
139
+ session_state: Streamlit session state for storing form data
140
+ """
141
+ st.header("Building Components")
142
+
143
+ # Initialize components in session state if not exists
144
+ if "components" not in session_state:
145
+ session_state["components"] = {
146
+ "walls": [],
147
+ "roofs": [],
148
+ "floors": [],
149
+ "windows": [],
150
+ "doors": []
151
+ }
152
+
153
+ # Initialize form submission flags if not exists
154
+ if "form_submitted" not in session_state:
155
+ session_state["form_submitted"] = {
156
+ "wall_form": False,
157
+ "roof_form": False,
158
+ "floor_form": False,
159
+ "window_form": False,
160
+ "door_form": False
161
+ }
162
+
163
+ # Display components grouped by orientation
164
+ self._display_components_by_orientation(session_state)
165
+
166
+ # Display roof and floor components (using building info area)
167
+ self._display_roof_floor_components(session_state)
168
+
169
+ # Add component buttons
170
+ st.subheader("Add New Components")
171
+ col1, col2, col3, col4, col5 = st.columns(5)
172
+
173
+ with col1:
174
+ if st.button("Add Wall"):
175
+ session_state["add_component_type"] = "wall"
176
+
177
+ with col2:
178
+ if st.button("Add Window"):
179
+ session_state["add_component_type"] = "window"
180
+
181
+ with col3:
182
+ if st.button("Add Door"):
183
+ session_state["add_component_type"] = "door"
184
+
185
+ with col4:
186
+ if st.button("Add Roof"):
187
+ session_state["add_component_type"] = "roof"
188
+
189
+ with col5:
190
+ if st.button("Add Floor"):
191
+ session_state["add_component_type"] = "floor"
192
+
193
+ # Display appropriate form based on selected component type
194
+ if "add_component_type" in session_state:
195
+ component_type = session_state["add_component_type"]
196
+
197
+ if component_type == "wall":
198
+ self._display_wall_form(session_state)
199
+ elif component_type == "window":
200
+ self._display_window_form(session_state)
201
+ elif component_type == "door":
202
+ self._display_door_form(session_state)
203
+ elif component_type == "roof":
204
+ self._display_roof_form(session_state)
205
+ elif component_type == "floor":
206
+ self._display_floor_form(session_state)
207
+
208
+ def _display_components_by_orientation(self, session_state: Dict[str, Any]) -> None:
209
+ """
210
+ Display components grouped by orientation.
211
+
212
+ Args:
213
+ session_state: Streamlit session state
214
+ """
215
+ st.subheader("Components by Orientation")
216
+
217
+ # Get all components
218
+ walls = session_state["components"]["walls"]
219
+ windows = session_state["components"]["windows"]
220
+ doors = session_state["components"]["doors"]
221
+
222
+ # Group components by orientation
223
+ orientations = [
224
+ Orientation.NORTH,
225
+ Orientation.NORTHEAST,
226
+ Orientation.EAST,
227
+ Orientation.SOUTHEAST,
228
+ Orientation.SOUTH,
229
+ Orientation.SOUTHWEST,
230
+ Orientation.WEST,
231
+ Orientation.NORTHWEST
232
+ ]
233
+
234
+ # Check if there are any components to display
235
+ has_components = len(walls) > 0 or len(windows) > 0 or len(doors) > 0
236
+
237
+ if not has_components:
238
+ st.info("No components added yet. Use the buttons below to add components.")
239
+ return
240
+
241
+ # Display components for each orientation
242
+ for orientation in orientations:
243
+ # Filter components by orientation
244
+ orientation_walls = [wall for wall in walls if wall.orientation == orientation]
245
+ orientation_windows = [window for window in windows if window.orientation == orientation]
246
+ orientation_doors = [door for door in doors if door.orientation == orientation]
247
+
248
+ # Skip if no components for this orientation
249
+ if not orientation_walls and not orientation_windows and not orientation_doors:
250
+ continue
251
+
252
+ # Display orientation header
253
+ st.write(f"### {orientation.value} Components")
254
+
255
+ # Create data for all components in this orientation
256
+ component_data = []
257
+
258
+ # Add walls
259
+ for i, wall in enumerate(orientation_walls):
260
+ component_data.append({
261
+ "ID": f"W{i+1}",
262
+ "Type": "Wall",
263
+ "Name": wall.name,
264
+ "U-Value (W/m²K)": f"{wall.u_value:.3f}",
265
+ "Area (m²)": f"{wall.area:.2f}",
266
+ "Group": wall.wall_group
267
+ })
268
+
269
+ # Add windows
270
+ for i, window in enumerate(orientation_windows):
271
+ component_data.append({
272
+ "ID": f"G{i+1}",
273
+ "Type": "Window",
274
+ "Name": window.name,
275
+ "U-Value (W/m²K)": f"{window.u_value:.3f}",
276
+ "Area (m²)": f"{window.area:.2f}",
277
+ "SHGC": f"{window.shgc:.2f}"
278
+ })
279
+
280
+ # Add doors
281
+ for i, door in enumerate(orientation_doors):
282
+ component_data.append({
283
+ "ID": f"D{i+1}",
284
+ "Type": "Door",
285
+ "Name": door.name,
286
+ "U-Value (W/m²K)": f"{door.u_value:.3f}",
287
+ "Area (m²)": f"{door.area:.2f}",
288
+ "Material": door.door_type
289
+ })
290
+
291
+ # Display components table
292
+ if component_data:
293
+ component_df = pd.DataFrame(component_data)
294
+ st.dataframe(component_df)
295
+
296
+ # Add edit/delete options
297
+ col1, col2, col3 = st.columns(3)
298
+
299
+ with col1:
300
+ # Wall edit/delete
301
+ if orientation_walls:
302
+ wall_to_modify = st.selectbox(
303
+ f"Select wall ({orientation.value}):",
304
+ options=range(len(orientation_walls)),
305
+ format_func=lambda x: f"Wall #{x+1}: {orientation_walls[x].name}",
306
+ key=f"wall_select_{orientation.value}"
307
+ )
308
+
309
+ edit_col, delete_col = st.columns(2)
310
+ with edit_col:
311
+ if st.button("Edit", key=f"edit_wall_{orientation.value}"):
312
+ # Find the global index of this wall
313
+ global_index = walls.index(orientation_walls[wall_to_modify])
314
+ session_state["wall_to_edit"] = global_index
315
+ session_state["add_component_type"] = "wall"
316
+
317
+ with delete_col:
318
+ if st.button("Delete", key=f"delete_wall_{orientation.value}"):
319
+ # Find the global index of this wall
320
+ global_index = walls.index(orientation_walls[wall_to_modify])
321
+ session_state["pending_actions"]["delete_wall"] = global_index
322
+
323
+ with col2:
324
+ # Window edit/delete
325
+ if orientation_windows:
326
+ window_to_modify = st.selectbox(
327
+ f"Select window ({orientation.value}):",
328
+ options=range(len(orientation_windows)),
329
+ format_func=lambda x: f"Window #{x+1}: {orientation_windows[x].name}",
330
+ key=f"window_select_{orientation.value}"
331
+ )
332
+
333
+ edit_col, delete_col = st.columns(2)
334
+ with edit_col:
335
+ if st.button("Edit", key=f"edit_window_{orientation.value}"):
336
+ # Find the global index of this window
337
+ global_index = windows.index(orientation_windows[window_to_modify])
338
+ session_state["window_to_edit"] = global_index
339
+ session_state["add_component_type"] = "window"
340
+
341
+ with delete_col:
342
+ if st.button("Delete", key=f"delete_window_{orientation.value}"):
343
+ # Find the global index of this window
344
+ global_index = windows.index(orientation_windows[window_to_modify])
345
+ session_state["pending_actions"]["delete_window"] = global_index
346
+
347
+ with col3:
348
+ # Door edit/delete
349
+ if orientation_doors:
350
+ door_to_modify = st.selectbox(
351
+ f"Select door ({orientation.value}):",
352
+ options=range(len(orientation_doors)),
353
+ format_func=lambda x: f"Door #{x+1}: {orientation_doors[x].name}",
354
+ key=f"door_select_{orientation.value}"
355
+ )
356
+
357
+ edit_col, delete_col = st.columns(2)
358
+ with edit_col:
359
+ if st.button("Edit", key=f"edit_door_{orientation.value}"):
360
+ # Find the global index of this door
361
+ global_index = doors.index(orientation_doors[door_to_modify])
362
+ session_state["door_to_edit"] = global_index
363
+ session_state["add_component_type"] = "door"
364
+
365
+ with delete_col:
366
+ if st.button("Delete", key=f"delete_door_{orientation.value}"):
367
+ # Find the global index of this door
368
+ global_index = doors.index(orientation_doors[door_to_modify])
369
+ session_state["pending_actions"]["delete_door"] = global_index
370
+
371
+ st.markdown("---")
372
+
373
+ def _display_roof_floor_components(self, session_state: Dict[str, Any]) -> None:
374
+ """
375
+ Display roof and floor components.
376
+
377
+ Args:
378
+ session_state: Streamlit session state
379
+ """
380
+ st.subheader("Roof and Floor Components")
381
+
382
+ # Get roof and floor components
383
+ roofs = session_state["components"]["roofs"]
384
+ floors = session_state["components"]["floors"]
385
+
386
+ # Check if there are any components to display
387
+ has_components = len(roofs) > 0 or len(floors) > 0
388
+
389
+ if not has_components:
390
+ st.info("No roof or floor components added yet. Use the buttons below to add components.")
391
+ return
392
+
393
+ # Display roof components
394
+ if roofs:
395
+ st.write("### Roof Components")
396
+
397
+ # Create data for roof components
398
+ roof_data = []
399
+ for i, roof in enumerate(roofs):
400
+ roof_data.append({
401
+ "ID": i + 1,
402
+ "Name": roof.name,
403
+ "Type": roof.roof_type,
404
+ "U-Value (W/m²K)": f"{roof.u_value:.3f}",
405
+ "Area (m²)": f"{roof.area:.2f}",
406
+ "Group": roof.roof_group
407
+ })
408
+
409
+ # Display roof table
410
+ roof_df = pd.DataFrame(roof_data)
411
+ st.dataframe(roof_df)
412
+
413
+ # Add edit/delete options
414
+ col1, col2 = st.columns(2)
415
+ with col1:
416
+ roof_to_modify = st.selectbox(
417
+ "Select roof to modify:",
418
+ options=range(len(roofs)),
419
+ format_func=lambda x: f"Roof #{x+1}: {roofs[x].name}"
420
+ )
421
+
422
+ with col2:
423
+ edit_col, delete_col = st.columns(2)
424
+ with edit_col:
425
+ if st.button("Edit Roof"):
426
+ session_state["roof_to_edit"] = roof_to_modify
427
+ session_state["add_component_type"] = "roof"
428
+
429
+ with delete_col:
430
+ if st.button("Delete Roof"):
431
+ session_state["pending_actions"]["delete_roof"] = roof_to_modify
432
+
433
+ # Display floor components
434
+ if floors:
435
+ st.write("### Floor Components")
436
+
437
+ # Create data for floor components
438
+ floor_data = []
439
+ for i, floor in enumerate(floors):
440
+ floor_data.append({
441
+ "ID": i + 1,
442
+ "Name": floor.name,
443
+ "Type": floor.floor_type,
444
+ "U-Value (W/m²K)": f"{floor.u_value:.3f}",
445
+ "Area (m²)": f"{floor.area:.2f}",
446
+ "Ground Contact": "Yes" if floor.is_ground_contact else "No"
447
+ })
448
+
449
+ # Display floor table
450
+ floor_df = pd.DataFrame(floor_data)
451
+ st.dataframe(floor_df)
452
+
453
+ # Add edit/delete options
454
+ col1, col2 = st.columns(2)
455
+ with col1:
456
+ floor_to_modify = st.selectbox(
457
+ "Select floor to modify:",
458
+ options=range(len(floors)),
459
+ format_func=lambda x: f"Floor #{x+1}: {floors[x].name}"
460
+ )
461
+
462
+ with col2:
463
+ edit_col, delete_col = st.columns(2)
464
+ with edit_col:
465
+ if st.button("Edit Floor"):
466
+ session_state["floor_to_edit"] = floor_to_modify
467
+ session_state["add_component_type"] = "floor"
468
+
469
+ with delete_col:
470
+ if st.button("Delete Floor"):
471
+ session_state["pending_actions"]["delete_floor"] = floor_to_modify
472
+
473
+ st.markdown("---")
474
+
475
+ def _display_wall_form(self, session_state: Dict[str, Any]) -> None:
476
+ """
477
+ Display wall form for adding or editing a wall.
478
+
479
+ Args:
480
+ session_state: Streamlit session state
481
+ """
482
+ # Check if we're editing an existing wall
483
+ editing_wall = "wall_to_edit" in session_state
484
+ wall_to_edit = session_state.get("wall_to_edit", None)
485
+
486
+ # Get the wall to edit if we're editing
487
+ wall = None
488
+ if editing_wall and wall_to_edit is not None:
489
+ wall = session_state["components"]["walls"][wall_to_edit]
490
+
491
+ # Form title
492
+ if editing_wall:
493
+ st.subheader(f"Edit Wall: {wall.name if wall else ''}")
494
+ else:
495
+ st.subheader("Add New Wall")
496
+
497
+ # Wall form
498
+ with st.form(key="wall_form"):
499
+ # Wall name
500
+ wall_name = st.text_input(
501
+ "Wall Name:",
502
+ value=wall.name if wall else ""
503
+ )
504
+
505
+ # Wall type selection
506
+ col1, col2 = st.columns(2)
507
+
508
+ with col1:
509
+ # Get preset wall types
510
+ preset_walls = self.component_library.get_preset_components_by_type(ComponentType.WALL)
511
+ preset_wall_names = [wall.name for wall in preset_walls]
512
+
513
+ wall_type = st.selectbox(
514
+ "Wall Type:",
515
+ options=["Custom"] + preset_wall_names,
516
+ index=preset_wall_names.index(wall.wall_type) + 1 if wall and wall.wall_type in preset_wall_names else 0
517
+ )
518
+
519
+ # U-value
520
+ with col2:
521
+ if wall_type == "Custom":
522
+ u_value = st.number_input(
523
+ "U-Value (W/m²K):",
524
+ min_value=0.0,
525
+ max_value=10.0,
526
+ value=wall.u_value if wall else 0.5,
527
+ step=0.01
528
+ )
529
+ else:
530
+ # Get U-value from preset wall
531
+ selected_wall = next((w for w in preset_walls if w.name == wall_type), None)
532
+ u_value = selected_wall.u_value if selected_wall else 0.5
533
+ st.write(f"U-Value: {u_value:.3f} W/m²K")
534
+
535
+ # Wall dimensions
536
+ col1, col2 = st.columns(2)
537
+
538
+ with col1:
539
+ wall_height = st.number_input(
540
+ "Wall Height (m):",
541
+ min_value=0.1,
542
+ max_value=50.0,
543
+ value=3.0 if not wall else (wall.area / 10.0), # Assuming width of 10m if editing
544
+ step=0.1
545
+ )
546
+
547
+ with col2:
548
+ wall_width = st.number_input(
549
+ "Wall Width (m):",
550
+ min_value=0.1,
551
+ max_value=100.0,
552
+ value=10.0 if not wall else (wall.area / wall_height),
553
+ step=0.1
554
+ )
555
+
556
+ # Calculate wall area
557
+ wall_area = wall_height * wall_width
558
+ st.write(f"Wall Area: {wall_area:.2f} m²")
559
+
560
+ # Wall orientation
561
+ orientation_options = [
562
+ Orientation.NORTH,
563
+ Orientation.NORTHEAST,
564
+ Orientation.EAST,
565
+ Orientation.SOUTHEAST,
566
+ Orientation.SOUTH,
567
+ Orientation.SOUTHWEST,
568
+ Orientation.WEST,
569
+ Orientation.NORTHWEST
570
+ ]
571
+
572
+ orientation = st.selectbox(
573
+ "Orientation:",
574
+ options=orientation_options,
575
+ index=orientation_options.index(wall.orientation) if wall else 0,
576
+ format_func=lambda x: x.value
577
+ )
578
+
579
+ # Wall color
580
+ color_options = ["Light", "Medium", "Dark"]
581
+ color = st.selectbox(
582
+ "Color:",
583
+ options=color_options,
584
+ index=color_options.index(wall.color) if wall and wall.color in color_options else 1
585
+ )
586
+
587
+ # Submit button
588
+ submit_label = "Update Wall" if editing_wall else "Add Wall"
589
+ submit = st.form_submit_button(submit_label)
590
+
591
+ if submit:
592
+ # Create or update wall
593
+ if wall_type == "Custom":
594
+ # Create custom wall
595
+ new_wall = Wall(
596
+ id=f"custom_wall_{str(uuid.uuid4())[:8]}",
597
+ name=wall_name,
598
+ component_type=ComponentType.WALL,
599
+ u_value=u_value,
600
+ area=wall_area,
601
+ orientation=orientation,
602
+ color=color,
603
+ wall_type="Custom",
604
+ wall_group="Custom"
605
+ )
606
+ else:
607
+ # Clone preset wall and update properties
608
+ selected_wall = next((w for w in preset_walls if w.name == wall_type), None)
609
+ if selected_wall:
610
+ new_wall = Wall(
611
+ id=f"custom_wall_{str(uuid.uuid4())[:8]}",
612
+ name=wall_name,
613
+ component_type=ComponentType.WALL,
614
+ u_value=selected_wall.u_value,
615
+ area=wall_area,
616
+ orientation=orientation,
617
+ color=color,
618
+ wall_type=selected_wall.wall_type,
619
+ wall_group=selected_wall.wall_group,
620
+ material_layers=selected_wall.material_layers
621
+ )
622
+
623
+ # Add or update wall in session state
624
+ if editing_wall:
625
+ session_state["components"]["walls"][wall_to_edit] = new_wall
626
+ del session_state["wall_to_edit"]
627
+ else:
628
+ # Queue the add action in pending_actions instead of doing it immediately
629
+ session_state["pending_actions"]["add_wall"] = new_wall
630
+
631
+ # Clear the component type selection
632
+ del session_state["add_component_type"]
633
+ st.experimental_rerun()
634
+
635
+ # Cancel button outside the form
636
+ if st.button("Cancel"):
637
+ if editing_wall:
638
+ del session_state["wall_to_edit"]
639
+ del session_state["add_component_type"]
640
+ st.experimental_rerun()
641
+
642
+ def _display_window_form(self, session_state: Dict[str, Any]) -> None:
643
+ """
644
+ Display window form for adding or editing a window.
645
+
646
+ Args:
647
+ session_state: Streamlit session state
648
+ """
649
+ # Check if we're editing an existing window
650
+ editing_window = "window_to_edit" in session_state
651
+ window_to_edit = session_state.get("window_to_edit", None)
652
+
653
+ # Get the window to edit if we're editing
654
+ window = None
655
+ if editing_window and window_to_edit is not None:
656
+ window = session_state["components"]["windows"][window_to_edit]
657
+
658
+ # Form title
659
+ if editing_window:
660
+ st.subheader(f"Edit Window: {window.name if window else ''}")
661
+ else:
662
+ st.subheader("Add New Window")
663
+
664
+ # Window form
665
+ with st.form(key="window_form"):
666
+ # Window name
667
+ window_name = st.text_input(
668
+ "Window Name:",
669
+ value=window.name if window else ""
670
+ )
671
+
672
+ # Window type selection
673
+ col1, col2 = st.columns(2)
674
+
675
+ with col1:
676
+ # Get preset window types
677
+ preset_windows = self.component_library.get_preset_components_by_type(ComponentType.WINDOW)
678
+ preset_window_names = [window.name for window in preset_windows]
679
+
680
+ window_type = st.selectbox(
681
+ "Window Type:",
682
+ options=["Custom"] + preset_window_names,
683
+ index=0
684
+ )
685
+
686
+ # U-value and SHGC
687
+ with col2:
688
+ if window_type == "Custom":
689
+ u_value = st.number_input(
690
+ "U-Value (W/m²K):",
691
+ min_value=0.0,
692
+ max_value=10.0,
693
+ value=window.u_value if window else 2.8,
694
+ step=0.01
695
+ )
696
+ else:
697
+ # Get U-value from preset window
698
+ selected_window = next((w for w in preset_windows if w.name == window_type), None)
699
+ u_value = selected_window.u_value if selected_window else 2.8
700
+ st.write(f"U-Value: {u_value:.3f} W/m²K")
701
+
702
+ # SHGC
703
+ shgc = st.number_input(
704
+ "Solar Heat Gain Coefficient (SHGC):",
705
+ min_value=0.0,
706
+ max_value=1.0,
707
+ value=window.shgc if window else 0.7,
708
+ step=0.01
709
+ )
710
+
711
+ # Window dimensions
712
+ col1, col2 = st.columns(2)
713
+
714
+ with col1:
715
+ window_height = st.number_input(
716
+ "Window Height (m):",
717
+ min_value=0.1,
718
+ max_value=10.0,
719
+ value=1.2 if not window else (window.area / 1.5), # Assuming width of 1.5m if editing
720
+ step=0.1
721
+ )
722
+
723
+ with col2:
724
+ window_width = st.number_input(
725
+ "Window Width (m):",
726
+ min_value=0.1,
727
+ max_value=10.0,
728
+ value=1.5 if not window else (window.area / window_height),
729
+ step=0.1
730
+ )
731
+
732
+ # Calculate window area
733
+ window_area = window_height * window_width
734
+ st.write(f"Window Area: {window_area:.2f} m²")
735
+
736
+ # Window orientation
737
+ orientation_options = [
738
+ Orientation.NORTH,
739
+ Orientation.NORTHEAST,
740
+ Orientation.EAST,
741
+ Orientation.SOUTHEAST,
742
+ Orientation.SOUTH,
743
+ Orientation.SOUTHWEST,
744
+ Orientation.WEST,
745
+ Orientation.NORTHWEST
746
+ ]
747
+
748
+ orientation = st.selectbox(
749
+ "Orientation:",
750
+ options=orientation_options,
751
+ index=orientation_options.index(window.orientation) if window else 0,
752
+ format_func=lambda x: x.value
753
+ )
754
+
755
+ # Shading options
756
+ has_shading = st.checkbox(
757
+ "Has Shading",
758
+ value=window.has_shading if window else False
759
+ )
760
+
761
+ if has_shading:
762
+ shading_type_options = ["Internal", "External", "Between-glass"]
763
+ shading_type = st.selectbox(
764
+ "Shading Type:",
765
+ options=shading_type_options,
766
+ index=0 if not window or not window.shading_type else shading_type_options.index(window.shading_type)
767
+ )
768
+
769
+ shading_coefficient = st.slider(
770
+ "Shading Coefficient (0-1):",
771
+ min_value=0.0,
772
+ max_value=1.0,
773
+ value=window.shading_coefficient if window else 0.7,
774
+ step=0.01
775
+ )
776
+ else:
777
+ shading_type = None
778
+ shading_coefficient = 1.0
779
+
780
+ # Submit button
781
+ submit_label = "Update Window" if editing_window else "Add Window"
782
+ submit = st.form_submit_button(submit_label)
783
+
784
+ if submit:
785
+ # Create or update window
786
+ if window_type == "Custom":
787
+ # Create custom window
788
+ new_window = Window(
789
+ id=f"custom_window_{str(uuid.uuid4())[:8]}",
790
+ name=window_name,
791
+ component_type=ComponentType.WINDOW,
792
+ u_value=u_value,
793
+ area=window_area,
794
+ orientation=orientation,
795
+ shgc=shgc,
796
+ has_shading=has_shading,
797
+ shading_type=shading_type,
798
+ shading_coefficient=shading_coefficient
799
+ )
800
+ else:
801
+ # Clone preset window and update properties
802
+ selected_window = next((w for w in preset_windows if w.name == window_type), None)
803
+ if selected_window:
804
+ new_window = Window(
805
+ id=f"custom_window_{str(uuid.uuid4())[:8]}",
806
+ name=window_name,
807
+ component_type=ComponentType.WINDOW,
808
+ u_value=selected_window.u_value,
809
+ area=window_area,
810
+ orientation=orientation,
811
+ shgc=selected_window.shgc if hasattr(selected_window, 'shgc') else shgc,
812
+ has_shading=has_shading,
813
+ shading_type=shading_type,
814
+ shading_coefficient=shading_coefficient
815
+ )
816
+
817
+ # Add or update window in session state
818
+ if editing_window:
819
+ session_state["components"]["windows"][window_to_edit] = new_window
820
+ del session_state["window_to_edit"]
821
+ else:
822
+ # Queue the add action in pending_actions instead of doing it immediately
823
+ session_state["pending_actions"]["add_window"] = new_window
824
+
825
+ # Clear the component type selection
826
+ del session_state["add_component_type"]
827
+ st.experimental_rerun()
828
+
829
+ # Cancel button outside the form
830
+ if st.button("Cancel"):
831
+ if editing_window:
832
+ del session_state["window_to_edit"]
833
+ del session_state["add_component_type"]
834
+ st.experimental_rerun()
835
+
836
+ def _display_door_form(self, session_state: Dict[str, Any]) -> None:
837
+ """
838
+ Display door form for adding or editing a door.
839
+
840
+ Args:
841
+ session_state: Streamlit session state
842
+ """
843
+ # Check if we're editing an existing door
844
+ editing_door = "door_to_edit" in session_state
845
+ door_to_edit = session_state.get("door_to_edit", None)
846
+
847
+ # Get the door to edit if we're editing
848
+ door = None
849
+ if editing_door and door_to_edit is not None:
850
+ door = session_state["components"]["doors"][door_to_edit]
851
+
852
+ # Form title
853
+ if editing_door:
854
+ st.subheader(f"Edit Door: {door.name if door else ''}")
855
+ else:
856
+ st.subheader("Add New Door")
857
+
858
+ # Door form
859
+ with st.form(key="door_form"):
860
+ # Door name
861
+ door_name = st.text_input(
862
+ "Door Name:",
863
+ value=door.name if door else ""
864
+ )
865
+
866
+ # Door type selection
867
+ col1, col2 = st.columns(2)
868
+
869
+ with col1:
870
+ # Get preset door types
871
+ preset_doors = self.component_library.get_preset_components_by_type(ComponentType.DOOR)
872
+ preset_door_names = [door.name for door in preset_doors]
873
+
874
+ door_type = st.selectbox(
875
+ "Door Type:",
876
+ options=["Custom"] + preset_door_names,
877
+ index=0
878
+ )
879
+
880
+ # U-value
881
+ with col2:
882
+ if door_type == "Custom":
883
+ u_value = st.number_input(
884
+ "U-Value (W/m²K):",
885
+ min_value=0.0,
886
+ max_value=10.0,
887
+ value=door.u_value if door else 2.0,
888
+ step=0.01
889
+ )
890
+ else:
891
+ # Get U-value from preset door
892
+ selected_door = next((d for d in preset_doors if d.name == door_type), None)
893
+ u_value = selected_door.u_value if selected_door else 2.0
894
+ st.write(f"U-Value: {u_value:.3f} W/m²K")
895
+
896
+ # Door dimensions
897
+ col1, col2 = st.columns(2)
898
+
899
+ with col1:
900
+ door_height = st.number_input(
901
+ "Door Height (m):",
902
+ min_value=0.1,
903
+ max_value=5.0,
904
+ value=2.0 if not door else (door.area / 0.9), # Assuming width of 0.9m if editing
905
+ step=0.1
906
+ )
907
+
908
+ with col2:
909
+ door_width = st.number_input(
910
+ "Door Width (m):",
911
+ min_value=0.1,
912
+ max_value=5.0,
913
+ value=0.9 if not door else (door.area / door_height),
914
+ step=0.1
915
+ )
916
+
917
+ # Calculate door area
918
+ door_area = door_height * door_width
919
+ st.write(f"Door Area: {door_area:.2f} m²")
920
+
921
+ # Door orientation
922
+ orientation_options = [
923
+ Orientation.NORTH,
924
+ Orientation.NORTHEAST,
925
+ Orientation.EAST,
926
+ Orientation.SOUTHEAST,
927
+ Orientation.SOUTH,
928
+ Orientation.SOUTHWEST,
929
+ Orientation.WEST,
930
+ Orientation.NORTHWEST
931
+ ]
932
+
933
+ orientation = st.selectbox(
934
+ "Orientation:",
935
+ options=orientation_options,
936
+ index=orientation_options.index(door.orientation) if door else 0,
937
+ format_func=lambda x: x.value
938
+ )
939
+
940
+ # Submit button
941
+ submit_label = "Update Door" if editing_door else "Add Door"
942
+ submit = st.form_submit_button(submit_label)
943
+
944
+ if submit:
945
+ # Create or update door
946
+ if door_type == "Custom":
947
+ # Create custom door
948
+ new_door = Door(
949
+ id=f"custom_door_{str(uuid.uuid4())[:8]}",
950
+ name=door_name,
951
+ component_type=ComponentType.DOOR,
952
+ u_value=u_value,
953
+ area=door_area,
954
+ orientation=orientation,
955
+ door_type="Custom"
956
+ )
957
+ else:
958
+ # Clone preset door and update properties
959
+ selected_door = next((d for d in preset_doors if d.name == door_type), None)
960
+ if selected_door:
961
+ new_door = Door(
962
+ id=f"custom_door_{str(uuid.uuid4())[:8]}",
963
+ name=door_name,
964
+ component_type=ComponentType.DOOR,
965
+ u_value=selected_door.u_value,
966
+ area=door_area,
967
+ orientation=orientation,
968
+ door_type=selected_door.door_type
969
+ )
970
+
971
+ # Add or update door in session state
972
+ if editing_door:
973
+ session_state["components"]["doors"][door_to_edit] = new_door
974
+ del session_state["door_to_edit"]
975
+ else:
976
+ # Queue the add action in pending_actions instead of doing it immediately
977
+ session_state["pending_actions"]["add_door"] = new_door
978
+
979
+ # Clear the component type selection
980
+ del session_state["add_component_type"]
981
+ st.experimental_rerun()
982
+
983
+ # Cancel button outside the form
984
+ if st.button("Cancel"):
985
+ if editing_door:
986
+ del session_state["door_to_edit"]
987
+ del session_state["add_component_type"]
988
+ st.experimental_rerun()
989
+
990
+ def _display_roof_form(self, session_state: Dict[str, Any]) -> None:
991
+ """
992
+ Display roof form for adding or editing a roof.
993
+
994
+ Args:
995
+ session_state: Streamlit session state
996
+ """
997
+ # Check if we're editing an existing roof
998
+ editing_roof = "roof_to_edit" in session_state
999
+ roof_to_edit = session_state.get("roof_to_edit", None)
1000
+
1001
+ # Get the roof to edit if we're editing
1002
+ roof = None
1003
+ if editing_roof and roof_to_edit is not None:
1004
+ roof = session_state["components"]["roofs"][roof_to_edit]
1005
+
1006
+ # Form title
1007
+ if editing_roof:
1008
+ st.subheader(f"Edit Roof: {roof.name if roof else ''}")
1009
+ else:
1010
+ st.subheader("Add New Roof")
1011
+
1012
+ # Get building floor area from building info if available
1013
+ building_area = 0.0
1014
+ if "building_info" in session_state and "floor_area" in session_state["building_info"]:
1015
+ building_area = session_state["building_info"]["floor_area"]
1016
+
1017
+ # Roof form
1018
+ with st.form(key="roof_form"):
1019
+ # Roof name
1020
+ roof_name = st.text_input(
1021
+ "Roof Name:",
1022
+ value=roof.name if roof else ""
1023
+ )
1024
+
1025
+ # Roof type selection
1026
+ col1, col2 = st.columns(2)
1027
+
1028
+ with col1:
1029
+ # Get preset roof types
1030
+ preset_roofs = self.component_library.get_preset_components_by_type(ComponentType.ROOF)
1031
+ preset_roof_names = [roof.name for roof in preset_roofs]
1032
+
1033
+ roof_type = st.selectbox(
1034
+ "Roof Type:",
1035
+ options=["Custom"] + preset_roof_names,
1036
+ index=0
1037
+ )
1038
+
1039
+ # U-value
1040
+ with col2:
1041
+ if roof_type == "Custom":
1042
+ u_value = st.number_input(
1043
+ "U-Value (W/m²K):",
1044
+ min_value=0.0,
1045
+ max_value=10.0,
1046
+ value=roof.u_value if roof else 0.25,
1047
+ step=0.01
1048
+ )
1049
+ else:
1050
+ # Get U-value from preset roof
1051
+ selected_roof = next((r for r in preset_roofs if r.name == roof_type), None)
1052
+ u_value = selected_roof.u_value if selected_roof else 0.25
1053
+ st.write(f"U-Value: {u_value:.3f} W/m²K")
1054
+
1055
+ # Roof area
1056
+ if building_area > 0:
1057
+ st.write(f"Building Floor Area: {building_area:.2f} m²")
1058
+ use_building_area = st.checkbox(
1059
+ "Use Building Floor Area",
1060
+ value=True
1061
+ )
1062
+
1063
+ if use_building_area:
1064
+ roof_area = building_area
1065
+ st.write(f"Roof Area: {roof_area:.2f} m²")
1066
+ else:
1067
+ roof_area = st.number_input(
1068
+ "Roof Area (m²):",
1069
+ min_value=0.1,
1070
+ max_value=10000.0,
1071
+ value=roof.area if roof else building_area,
1072
+ step=1.0
1073
+ )
1074
+ else:
1075
+ roof_area = st.number_input(
1076
+ "Roof Area (m²):",
1077
+ min_value=0.1,
1078
+ max_value=10000.0,
1079
+ value=roof.area if roof else 100.0,
1080
+ step=1.0
1081
+ )
1082
+
1083
+ # Roof color
1084
+ color_options = ["Light", "Medium", "Dark"]
1085
+ color = st.selectbox(
1086
+ "Color:",
1087
+ options=color_options,
1088
+ index=color_options.index(roof.color) if roof and roof.color in color_options else 1
1089
+ )
1090
+
1091
+ # Roof pitch
1092
+ roof_pitch = st.number_input(
1093
+ "Roof Pitch (degrees):",
1094
+ min_value=0.0,
1095
+ max_value=60.0,
1096
+ value=roof.pitch if roof else 0.0,
1097
+ step=1.0
1098
+ )
1099
+
1100
+ # Submit button
1101
+ submit_label = "Update Roof" if editing_roof else "Add Roof"
1102
+ submit = st.form_submit_button(submit_label)
1103
+
1104
+ if submit:
1105
+ # Create or update roof
1106
+ if roof_type == "Custom":
1107
+ # Create custom roof
1108
+ new_roof = Roof(
1109
+ id=f"custom_roof_{str(uuid.uuid4())[:8]}",
1110
+ name=roof_name,
1111
+ component_type=ComponentType.ROOF,
1112
+ u_value=u_value,
1113
+ area=roof_area,
1114
+ orientation=Orientation.HORIZONTAL,
1115
+ color=color,
1116
+ roof_type="Custom",
1117
+ roof_group="Custom",
1118
+ pitch=roof_pitch
1119
+ )
1120
+ else:
1121
+ # Clone preset roof and update properties
1122
+ selected_roof = next((r for r in preset_roofs if r.name == roof_type), None)
1123
+ if selected_roof:
1124
+ new_roof = Roof(
1125
+ id=f"custom_roof_{str(uuid.uuid4())[:8]}",
1126
+ name=roof_name,
1127
+ component_type=ComponentType.ROOF,
1128
+ u_value=selected_roof.u_value,
1129
+ area=roof_area,
1130
+ orientation=Orientation.HORIZONTAL,
1131
+ color=color,
1132
+ roof_type=selected_roof.roof_type,
1133
+ roof_group=selected_roof.roof_group,
1134
+ pitch=roof_pitch,
1135
+ material_layers=selected_roof.material_layers
1136
+ )
1137
+
1138
+ # Add or update roof in session state
1139
+ if editing_roof:
1140
+ session_state["components"]["roofs"][roof_to_edit] = new_roof
1141
+ del session_state["roof_to_edit"]
1142
+ else:
1143
+ # Queue the add action in pending_actions instead of doing it immediately
1144
+ session_state["pending_actions"]["add_roof"] = new_roof
1145
+
1146
+ # Clear the component type selection
1147
+ del session_state["add_component_type"]
1148
+ st.experimental_rerun()
1149
+
1150
+ # Cancel button outside the form
1151
+ if st.button("Cancel"):
1152
+ if editing_roof:
1153
+ del session_state["roof_to_edit"]
1154
+ del session_state["add_component_type"]
1155
+ st.experimental_rerun()
1156
+
1157
+ def _display_floor_form(self, session_state: Dict[str, Any]) -> None:
1158
+ """
1159
+ Display floor form for adding or editing a floor.
1160
+
1161
+ Args:
1162
+ session_state: Streamlit session state
1163
+ """
1164
+ # Check if we're editing an existing floor
1165
+ editing_floor = "floor_to_edit" in session_state
1166
+ floor_to_edit = session_state.get("floor_to_edit", None)
1167
+
1168
+ # Get the floor to edit if we're editing
1169
+ floor = None
1170
+ if editing_floor and floor_to_edit is not None:
1171
+ floor = session_state["components"]["floors"][floor_to_edit]
1172
+
1173
+ # Form title
1174
+ if editing_floor:
1175
+ st.subheader(f"Edit Floor: {floor.name if floor else ''}")
1176
+ else:
1177
+ st.subheader("Add New Floor")
1178
+
1179
+ # Get building floor area from building info if available
1180
+ building_area = 0.0
1181
+ if "building_info" in session_state and "floor_area" in session_state["building_info"]:
1182
+ building_area = session_state["building_info"]["floor_area"]
1183
+
1184
+ # Floor form
1185
+ with st.form(key="floor_form"):
1186
+ # Floor name
1187
+ floor_name = st.text_input(
1188
+ "Floor Name:",
1189
+ value=floor.name if floor else ""
1190
+ )
1191
+
1192
+ # Floor type selection
1193
+ col1, col2 = st.columns(2)
1194
+
1195
+ with col1:
1196
+ # Get preset floor types
1197
+ preset_floors = self.component_library.get_preset_components_by_type(ComponentType.FLOOR)
1198
+ preset_floor_names = [floor.name for floor in preset_floors]
1199
+
1200
+ floor_type = st.selectbox(
1201
+ "Floor Type:",
1202
+ options=["Custom"] + preset_floor_names,
1203
+ index=0
1204
+ )
1205
+
1206
+ # U-value
1207
+ with col2:
1208
+ if floor_type == "Custom":
1209
+ u_value = st.number_input(
1210
+ "U-Value (W/m²K):",
1211
+ min_value=0.0,
1212
+ max_value=10.0,
1213
+ value=floor.u_value if floor else 0.25,
1214
+ step=0.01
1215
+ )
1216
+ else:
1217
+ # Get U-value from preset floor
1218
+ selected_floor = next((f for f in preset_floors if f.name == floor_type), None)
1219
+ u_value = selected_floor.u_value if selected_floor else 0.25
1220
+ st.write(f"U-Value: {u_value:.3f} W/m²K")
1221
+
1222
+ # Floor area
1223
+ if building_area > 0:
1224
+ st.write(f"Building Floor Area: {building_area:.2f} m²")
1225
+ use_building_area = st.checkbox(
1226
+ "Use Building Floor Area",
1227
+ value=True
1228
+ )
1229
+
1230
+ if use_building_area:
1231
+ floor_area = building_area
1232
+ st.write(f"Floor Area: {floor_area:.2f} m²")
1233
+ else:
1234
+ floor_area = st.number_input(
1235
+ "Floor Area (m²):",
1236
+ min_value=0.1,
1237
+ max_value=10000.0,
1238
+ value=floor.area if floor else building_area,
1239
+ step=1.0
1240
+ )
1241
+ else:
1242
+ floor_area = st.number_input(
1243
+ "Floor Area (m²):",
1244
+ min_value=0.1,
1245
+ max_value=10000.0,
1246
+ value=floor.area if floor else 100.0,
1247
+ step=1.0
1248
+ )
1249
+
1250
+ # Ground contact
1251
+ is_ground_contact = st.checkbox(
1252
+ "Is in contact with ground",
1253
+ value=floor.is_ground_contact if floor else True
1254
+ )
1255
+
1256
+ # Perimeter length (for slab-on-grade floors)
1257
+ if is_ground_contact:
1258
+ perimeter_length = st.number_input(
1259
+ "Perimeter Length (m):",
1260
+ min_value=0.0,
1261
+ max_value=1000.0,
1262
+ value=floor.perimeter_length if floor else (4 * (floor_area ** 0.5)), # Estimate perimeter as 4 * sqrt(area) for a square
1263
+ step=1.0
1264
+ )
1265
+ else:
1266
+ perimeter_length = 0.0
1267
+
1268
+ # Submit button
1269
+ submit_label = "Update Floor" if editing_floor else "Add Floor"
1270
+ submit = st.form_submit_button(submit_label)
1271
+
1272
+ if submit:
1273
+ # Create or update floor
1274
+ if floor_type == "Custom":
1275
+ # Create custom floor
1276
+ new_floor = Floor(
1277
+ id=f"custom_floor_{str(uuid.uuid4())[:8]}",
1278
+ name=floor_name,
1279
+ component_type=ComponentType.FLOOR,
1280
+ u_value=u_value,
1281
+ area=floor_area,
1282
+ orientation=Orientation.HORIZONTAL,
1283
+ floor_type="Custom",
1284
+ is_ground_contact=is_ground_contact,
1285
+ perimeter_length=perimeter_length
1286
+ )
1287
+ else:
1288
+ # Clone preset floor and update properties
1289
+ selected_floor = next((f for f in preset_floors if f.name == floor_type), None)
1290
+ if selected_floor:
1291
+ new_floor = Floor(
1292
+ id=f"custom_floor_{str(uuid.uuid4())[:8]}",
1293
+ name=floor_name,
1294
+ component_type=ComponentType.FLOOR,
1295
+ u_value=selected_floor.u_value,
1296
+ area=floor_area,
1297
+ orientation=Orientation.HORIZONTAL,
1298
+ floor_type=selected_floor.floor_type,
1299
+ is_ground_contact=is_ground_contact,
1300
+ perimeter_length=perimeter_length,
1301
+ material_layers=selected_floor.material_layers
1302
+ )
1303
+
1304
+ # Add or update floor in session state
1305
+ if editing_floor:
1306
+ session_state["components"]["floors"][floor_to_edit] = new_floor
1307
+ del session_state["floor_to_edit"]
1308
+ else:
1309
+ # Queue the add action in pending_actions instead of doing it immediately
1310
+ session_state["pending_actions"]["add_floor"] = new_floor
1311
+
1312
+ # Clear the component type selection
1313
+ del session_state["add_component_type"]
1314
+ st.experimental_rerun()
1315
+
1316
+ # Cancel button outside the form
1317
+ if st.button("Cancel"):
1318
+ if editing_floor:
1319
+ del session_state["floor_to_edit"]
1320
+ del session_state["add_component_type"]
1321
+ st.experimental_rerun()
app/main.py CHANGED
@@ -23,7 +23,7 @@ from typing import Dict, List, Any, Optional, Tuple
23
 
24
  # Import application modules
25
  from app.building_info_form import BuildingInfoForm
26
- from app.component_selection import ComponentSelection
27
  from app.results_display import ResultsDisplay
28
  from app.data_validation import DataValidation
29
  from app.data_persistence import DataPersistence
@@ -51,20 +51,7 @@ from utils.time_based_visualization import TimeBasedVisualization
51
 
52
 
53
  class HVACCalculator:
54
- """
55
- Main HVAC Calculator application class.
56
-
57
- This class initializes the Streamlit application and manages the navigation
58
- between different sections of the calculator.
59
-
60
- Attributes:
61
- building_info_form (BuildingInfoForm): Building information input form
62
- component_selection (ComponentSelection): Component selection interface
63
- results_display (ResultsDisplay): Results display module
64
- data_validation (DataValidation): Data validation module
65
- data_persistence (DataPersistence): Data persistence module
66
- data_export (DataExport): Data export module
67
- """
68
 
69
  def __init__(self):
70
  """Initialize the HVAC Calculator application."""
@@ -76,256 +63,216 @@ class HVACCalculator:
76
  initial_sidebar_state="expanded"
77
  )
78
 
79
- # Initialize session state if not exists
80
- if 'page' not in st.session_state:
81
- st.session_state.page = 'Building Information'
82
-
83
- if 'building_info' not in st.session_state:
84
- st.session_state.building_info = {}
85
-
86
- if 'components' not in st.session_state:
87
- st.session_state.components = {
88
- 'walls': [],
89
- 'roofs': [],
90
- 'floors': [],
91
- 'windows': [],
92
- 'doors': []
93
- }
94
 
95
- if 'internal_loads' not in st.session_state:
96
- st.session_state.internal_loads = {
97
- 'people': [],
98
- 'lighting': [],
99
- 'equipment': []
100
- }
101
 
102
- if 'calculation_results' not in st.session_state:
 
 
 
 
 
 
 
 
 
 
103
  st.session_state.calculation_results = {
104
- 'cooling': {},
105
- 'heating': {}
 
106
  }
107
-
108
- if 'scenarios' not in st.session_state:
109
- st.session_state.scenarios = []
110
-
111
- # Initialize application modules
112
  self.building_info_form = BuildingInfoForm()
113
- self.component_selection = ComponentSelection()
114
  self.results_display = ResultsDisplay()
115
  self.data_validation = DataValidation()
116
  self.data_persistence = DataPersistence()
117
  self.data_export = DataExport()
118
 
119
- # Initialize calculation modules
 
 
 
 
 
 
 
 
 
 
 
120
  self.cooling_load = CoolingLoad()
121
  self.heating_load = HeatingLoad()
122
-
123
- # Set up the application layout
124
- self.setup_layout()
 
125
 
126
  def setup_layout(self):
127
- """Set up the application layout with sidebar navigation."""
128
- # Application title
129
- st.sidebar.title("HVAC Load Calculator")
130
- st.sidebar.markdown("---")
131
-
132
- # Navigation
133
- st.sidebar.subheader("Navigation")
134
- pages = [
135
- "Building Information",
136
- "Climate Data",
137
- "Building Components",
138
- "Internal Loads",
139
- "Calculation Results",
140
- "Export Data"
141
- ]
142
 
143
- selected_page = st.sidebar.radio("Go to", pages, index=pages.index(st.session_state.page))
 
144
 
145
- # Update session state if page changed
146
- if selected_page != st.session_state.page:
147
- st.session_state.page = selected_page
148
 
149
- # Add calculate button in sidebar
150
- if st.sidebar.button("Run Calculations"):
151
- self.run_calculations()
152
-
153
- # Display the selected page
154
  self.display_page(st.session_state.page)
155
-
156
- # Footer
157
- st.sidebar.markdown("---")
158
- st.sidebar.info(
159
- "HVAC Load Calculator v1.0.0\n\n"
160
- "Based on ASHRAE calculation methods\n\n"
161
- 2025"
162
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  def display_page(self, page: str):
165
  """
166
  Display the selected page.
167
 
168
  Args:
169
- page (str): The page to display
170
  """
171
- if page == "Building Information":
172
  self.building_info_form.display()
173
- elif page == "Climate Data":
174
- self.display_climate_data()
175
- elif page == "Building Components":
176
  self.component_selection.display()
177
- elif page == "Internal Loads":
178
  self.display_internal_loads()
179
- elif page == "Calculation Results":
180
  self.results_display.display()
181
- elif page == "Export Data":
182
  self.data_export.display()
183
 
184
- def display_climate_data(self):
185
- """Display the climate data page."""
186
- st.title("Climate Data")
187
-
188
- # Check if building information is available
189
- if not st.session_state.building_info:
190
- st.warning("Please enter building information first.")
191
- st.button("Go to Building Information", on_click=self.navigate_to, args=["Building Information"])
192
- return
193
-
194
- # Get climate zone from building information
195
- climate_zone = st.session_state.building_info.get('climate_zone')
196
- if not climate_zone:
197
- st.error("Climate zone not found in building information.")
198
- st.button("Go to Building Information", on_click=self.navigate_to, args=["Building Information"])
199
- return
200
-
201
- # Display climate zone information
202
- st.subheader(f"Climate Zone: {climate_zone}")
203
-
204
- # Get design conditions
205
- design_conditions = ClimateData.get_design_conditions(climate_zone)
206
-
207
- # Display design conditions
208
- st.subheader("Design Conditions")
209
- col1, col2 = st.columns(2)
210
-
211
- with col1:
212
- st.write("Summer Design Conditions")
213
- summer_data = pd.DataFrame({
214
- "Parameter": ["Dry-Bulb Temperature", "Wet-Bulb Temperature", "Dew Point"],
215
- "Value": [
216
- f"{design_conditions['summer']['db']} °C",
217
- f"{design_conditions['summer']['wb']} °C",
218
- f"{design_conditions['summer']['dp']} °C"
219
- ]
220
- })
221
- st.table(summer_data)
222
-
223
- with col2:
224
- st.write("Winter Design Conditions")
225
- winter_data = pd.DataFrame({
226
- "Parameter": ["Dry-Bulb Temperature", "Relative Humidity"],
227
- "Value": [
228
- f"{design_conditions['winter']['db']} °C",
229
- f"{design_conditions['winter']['rh']} %"
230
- ]
231
- })
232
- st.table(winter_data)
233
-
234
- # Get monthly temperature data
235
- monthly_temps = ClimateData.get_monthly_temperatures(climate_zone)
236
-
237
- # Prepare data for plotting
238
- months = list(range(1, 13))
239
- avg_temps = [monthly_temps[m]['avg_db'] for m in months]
240
- max_temps = [monthly_temps[m]['max_db'] for m in months]
241
- min_temps = [monthly_temps[m]['min_db'] for m in months]
242
-
243
- # Plot monthly temperature data
244
- st.subheader("Monthly Temperature Data")
245
- fig = go.Figure()
246
-
247
- fig.add_trace(go.Scatter(
248
- x=months,
249
- y=max_temps,
250
- mode='lines+markers',
251
- name='Maximum Temperature',
252
- line=dict(color='red')
253
- ))
254
-
255
- fig.add_trace(go.Scatter(
256
- x=months,
257
- y=avg_temps,
258
- mode='lines+markers',
259
- name='Average Temperature',
260
- line=dict(color='green')
261
- ))
262
-
263
- fig.add_trace(go.Scatter(
264
- x=months,
265
- y=min_temps,
266
- mode='lines+markers',
267
- name='Minimum Temperature',
268
- line=dict(color='blue')
269
- ))
270
-
271
- fig.update_layout(
272
- title='Monthly Temperature Data',
273
- xaxis_title='Month',
274
- yaxis_title='Temperature (°C)',
275
- xaxis=dict(
276
- tickmode='array',
277
- tickvals=months,
278
- ticktext=['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
279
- )
280
- )
281
-
282
- st.plotly_chart(fig, use_container_width=True)
283
-
284
  def display_internal_loads(self):
285
- """Display the internal loads page."""
286
- st.title("Internal Loads")
287
 
288
- # Check if building information is available
289
- if not st.session_state.building_info:
290
- st.warning("Please enter building information first.")
291
- st.button("Go to Building Information", on_click=self.navigate_to, args=["Building Information"])
292
- return
 
 
293
 
294
  # Create tabs for different internal load types
295
  tab1, tab2, tab3 = st.tabs(["People", "Lighting", "Equipment"])
296
 
297
  with tab1:
298
- # Display people loads
299
- st.subheader("People Loads")
300
 
301
  # Display existing people loads
302
- if st.session_state.internal_loads.get('people'):
303
  st.write("Existing People Loads:")
304
- people_data = []
305
 
306
- for i, people_load in enumerate(st.session_state.internal_loads['people']):
 
 
307
  people_data.append({
308
- "Space": people_load.get('space', 'Unknown'),
309
- "Count": people_load.get('count', 0),
310
- "Activity Level": people_load.get('activity_level', 'Seated, light work')
 
 
 
 
311
  })
312
-
313
- # Add delete button
314
- if st.button(f"Delete {people_load.get('space', 'Unknown')}", key=f"delete_people_{i}"):
315
- st.session_state.internal_loads['people'].pop(i)
316
- st.experimental_rerun()
317
 
318
- # Display as table
319
- st.table(pd.DataFrame(people_data))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
- # Add new people load
322
  st.write("Add New People Load:")
323
- with st.form("add_people_form"):
324
- space_name = st.text_input("Space Name", "Main Room")
325
- people_count = st.number_input("Number of People", min_value=1, value=10)
326
- activity_level = st.selectbox(
327
- "Activity Level",
328
- options=[
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  "Seated, resting",
330
  "Seated, light work",
331
  "Seated, moderate work",
@@ -334,242 +281,438 @@ class HVACCalculator:
334
  "Walking, light work",
335
  "Walking, moderate work",
336
  "Heavy work"
337
- ],
338
- index=1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  )
340
 
341
- submitted = st.form_submit_button("Add People Load")
 
 
342
 
343
- if submitted:
344
- # Add new people load to session state
345
- st.session_state.internal_loads['people'].append({
346
- 'space': space_name,
347
- 'count': people_count,
348
- 'activity_level': activity_level
349
- })
 
 
 
 
 
350
 
351
- st.success(f"Added {people_count} people in {space_name}")
 
 
 
 
 
352
 
353
  with tab2:
354
- # Display lighting loads
355
- st.subheader("Lighting Loads")
356
 
357
  # Display existing lighting loads
358
- if st.session_state.internal_loads.get('lighting'):
359
  st.write("Existing Lighting Loads:")
360
- lighting_data = []
361
 
362
- for i, lighting_load in enumerate(st.session_state.internal_loads['lighting']):
 
 
363
  lighting_data.append({
364
- "Space": lighting_load.get('space', 'Unknown'),
365
- "Power (W)": lighting_load.get('power', 0),
366
- "Usage Factor": lighting_load.get('usage_factor', 1.0)
 
 
 
 
367
  })
368
-
369
- # Add delete button
370
- if st.button(f"Delete {lighting_load.get('space', 'Unknown')}", key=f"delete_lighting_{i}"):
371
- st.session_state.internal_loads['lighting'].pop(i)
372
- st.experimental_rerun()
373
 
374
- # Display as table
375
- st.table(pd.DataFrame(lighting_data))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
- # Add new lighting load
378
  st.write("Add New Lighting Load:")
379
- with st.form("add_lighting_form"):
380
- space_name = st.text_input("Space Name", "Main Room", key="lighting_space")
381
- power = st.number_input("Power (W)", min_value=0.0, value=500.0)
382
- usage_factor = st.slider("Usage Factor", min_value=0.0, max_value=1.0, value=1.0, step=0.1)
383
-
384
- submitted = st.form_submit_button("Add Lighting Load")
385
-
386
- if submitted:
387
- # Add new lighting load to session state
388
- st.session_state.internal_loads['lighting'].append({
389
- 'space': space_name,
390
- 'power': power,
391
- 'usage_factor': usage_factor
392
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
 
394
- st.success(f"Added {power} W lighting in {space_name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
396
  with tab3:
397
- # Display equipment loads
398
- st.subheader("Equipment Loads")
399
 
400
  # Display existing equipment loads
401
- if st.session_state.internal_loads.get('equipment'):
402
  st.write("Existing Equipment Loads:")
403
- equipment_data = []
404
 
405
- for i, equipment_load in enumerate(st.session_state.internal_loads['equipment']):
 
 
406
  equipment_data.append({
407
- "Space": equipment_load.get('space', 'Unknown'),
408
- "Equipment": equipment_load.get('equipment', 'Unknown'),
409
- "Power (W)": equipment_load.get('power', 0),
410
- "Usage Factor": equipment_load.get('usage_factor', 1.0)
 
 
411
  })
412
-
413
- # Add delete button
414
- if st.button(f"Delete {equipment_load.get('equipment', 'Unknown')}", key=f"delete_equipment_{i}"):
415
- st.session_state.internal_loads['equipment'].pop(i)
416
- st.experimental_rerun()
417
 
418
- # Display as table
419
- st.table(pd.DataFrame(equipment_data))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
 
421
- # Add new equipment load
422
  st.write("Add New Equipment Load:")
423
- with st.form("add_equipment_form"):
424
- space_name = st.text_input("Space Name", "Main Room", key="equipment_space")
425
- equipment_name = st.text_input("Equipment Name", "Computer")
426
- power = st.number_input("Power (W)", min_value=0.0, value=200.0, key="equipment_power")
427
- usage_factor = st.slider("Usage Factor", min_value=0.0, max_value=1.0, value=0.8, step=0.1, key="equipment_usage")
428
-
429
- submitted = st.form_submit_button("Add Equipment Load")
430
-
431
- if submitted:
432
- # Add new equipment load to session state
433
- st.session_state.internal_loads['equipment'].append({
434
- 'space': space_name,
435
- 'equipment': equipment_name,
436
- 'power': power,
437
- 'usage_factor': usage_factor
438
- })
439
-
440
- st.success(f"Added {equipment_name} ({power} W) in {space_name}")
441
-
442
- # Display summary of internal loads
443
- st.subheader("Internal Loads Summary")
444
-
445
- # Calculate total people
446
- total_people = 0
447
- for people_load in st.session_state.internal_loads.get('people', []):
448
- total_people += people_load.get('count', 0)
449
-
450
- # Calculate total lighting power
451
- total_lighting_power = 0
452
- for lighting_load in st.session_state.internal_loads.get('lighting', []):
453
- total_lighting_power += lighting_load.get('power', 0) * lighting_load.get('usage_factor', 1.0)
454
-
455
- # Calculate total equipment power
456
- total_equipment_power = 0
457
- for equipment_load in st.session_state.internal_loads.get('equipment', []):
458
- total_equipment_power += equipment_load.get('power', 0) * equipment_load.get('usage_factor', 1.0)
459
-
460
- # Display metrics
461
- col1, col2, col3 = st.columns(3)
462
-
463
- with col1:
464
- st.metric("Total People", f"{total_people}")
465
-
466
- with col2:
467
- st.metric("Total Lighting Power", f"{total_lighting_power:.1f} W")
468
-
469
- with col3:
470
- st.metric("Total Equipment Power", f"{total_equipment_power:.1f} W")
471
-
472
- # Display pie chart of internal loads
473
- if total_lighting_power > 0 or total_equipment_power > 0:
474
- # Estimate people load (assuming 100W per person)
475
- people_power = total_people * 100
476
 
477
- # Create pie chart
478
- fig = px.pie(
479
- values=[people_power, total_lighting_power, total_equipment_power],
480
- names=['People', 'Lighting', 'Equipment'],
481
- title='Internal Loads Distribution'
482
- )
483
 
484
- st.plotly_chart(fig, use_container_width=True)
485
-
486
- def navigate_to(self, page: str):
487
- """
488
- Navigate to the specified page.
489
-
490
- Args:
491
- page (str): The page to navigate to
492
- """
493
- st.session_state.page = page
494
- st.experimental_rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
  def run_calculations(self):
497
- """Run HVAC load calculations and update session state with results."""
498
- # Check if required data is available
 
 
 
 
 
 
 
499
  if not self.data_validation.validate_calculation_inputs(st.session_state):
500
- st.error("Cannot run calculations. Please complete all required inputs first.")
501
  return
502
 
503
- # Get building components and design conditions
504
- building_components = st.session_state.components
505
  building_info = st.session_state.building_info
 
506
  internal_loads = st.session_state.internal_loads
507
 
508
- # Extract design conditions
509
- design_conditions = building_info.get('design_conditions', {})
510
-
511
- # Create climate data dictionary for cooling load calculation
512
  climate_data = {
513
- 'outdoor_db': design_conditions.get('summer_outdoor_db', 35),
514
- 'outdoor_wb': design_conditions.get('summer_outdoor_wb', 25),
515
- 'indoor_db': design_conditions.get('summer_indoor_db', 24),
516
- 'indoor_rh': design_conditions.get('summer_indoor_rh', 50),
517
- 'ground_temp': design_conditions.get('summer_ground_temp', 20),
518
- 'daily_range': design_conditions.get('summer_daily_range', 8),
519
- 'month': design_conditions.get('summer_design_month', 7),
520
- 'latitude': building_info.get('latitude', 40)
 
521
  }
522
 
523
- # Create outdoor conditions dictionary for heating load calculation
524
- outdoor_conditions = {
525
- 'db': design_conditions.get('winter_outdoor_db', 0),
526
- 'rh': design_conditions.get('winter_outdoor_rh', 80),
527
- 'ground_temp': design_conditions.get('winter_ground_temp', 10)
528
- }
 
 
 
 
 
 
 
 
529
 
530
- # Create indoor conditions dictionary for heating load calculation
531
- indoor_conditions = {
532
- 'db': design_conditions.get('winter_indoor_db', 22),
533
- 'rh': design_conditions.get('winter_indoor_rh', 40)
534
  }
535
 
536
- # Run cooling load calculations with correct parameters
537
- cooling_results = self.cooling_load.calculate_total_cooling_load(
538
- building_components=building_components,
539
- building_info=building_info,
540
- climate_data=climate_data,
541
- internal_loads=internal_loads,
542
- hour=design_conditions.get('summer_design_hour', 15)
543
- )
544
 
545
- # Run heating load calculations with correct parameters
546
  heating_results = self.heating_load.calculate_design_heating_load(
547
- building_components=building_components,
548
- outdoor_conditions=outdoor_conditions,
549
- indoor_conditions=indoor_conditions,
550
- internal_loads=internal_loads,
551
- safety_factor=design_conditions.get('heating_safety_factor', 1.15)
 
 
 
 
 
552
  )
553
 
554
- # Calculate load per area
555
- floor_area = building_info.get('floor_area', 100) # Default to 100 m² if not specified
556
-
557
- cooling_results['load_per_area'] = cooling_results['total_load'] * 1000 / floor_area # Convert kW to W/m²
558
- heating_results['load_per_area'] = heating_results['total_load'] * 1000 / floor_area # Convert kW to W/m²
 
 
559
 
560
- # Update session state with calculation results
561
  st.session_state.calculation_results = {
562
- 'cooling': cooling_results,
563
- 'heating': heating_results
 
564
  }
565
 
566
- # Show success message
567
- st.success("Calculations completed successfully!")
568
-
569
  # Navigate to results page
570
- self.navigate_to("Calculation Results")
 
571
 
572
 
 
573
  if __name__ == "__main__":
574
- # Create and run the HVAC Calculator application
575
  app = HVACCalculator()
 
23
 
24
  # Import application modules
25
  from app.building_info_form import BuildingInfoForm
26
+ from app.component_selection_redesign import ComponentSelectionRedesigned
27
  from app.results_display import ResultsDisplay
28
  from app.data_validation import DataValidation
29
  from app.data_persistence import DataPersistence
 
51
 
52
 
53
  class HVACCalculator:
54
+ """Main HVAC Calculator application class."""
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  def __init__(self):
57
  """Initialize the HVAC Calculator application."""
 
63
  initial_sidebar_state="expanded"
64
  )
65
 
66
+ # Initialize session state
67
+ self._initialize_session_state()
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ # Initialize application modules
70
+ self._initialize_modules()
 
 
 
 
71
 
72
+ # Setup application layout
73
+ self.setup_layout()
74
+
75
+ def _initialize_session_state(self):
76
+ """Initialize Streamlit session state variables."""
77
+ # Initialize page navigation
78
+ if "page" not in st.session_state:
79
+ st.session_state.page = "building_info"
80
+
81
+ # Initialize calculation results
82
+ if "calculation_results" not in st.session_state:
83
  st.session_state.calculation_results = {
84
+ "cooling_load": {},
85
+ "heating_load": {},
86
+ "psychrometrics": {}
87
  }
88
+
89
+ def _initialize_modules(self):
90
+ """Initialize application modules."""
91
+ # Application modules
 
92
  self.building_info_form = BuildingInfoForm()
93
+ self.component_selection = ComponentSelectionRedesigned()
94
  self.results_display = ResultsDisplay()
95
  self.data_validation = DataValidation()
96
  self.data_persistence = DataPersistence()
97
  self.data_export = DataExport()
98
 
99
+ # Data modules
100
+ self.reference_data = ReferenceData()
101
+ self.climate_data = ClimateData()
102
+ self.ashrae_tables = ASHRAETables()
103
+
104
+ # Utility modules
105
+ self.component_library = ComponentLibrary()
106
+ self.u_value_calculator = UValueCalculator()
107
+ self.shading_system = ShadingSystem()
108
+ self.area_calculation_system = AreaCalculationSystem()
109
+ self.psychrometrics = Psychrometrics()
110
+ self.heat_transfer = HeatTransfer()
111
  self.cooling_load = CoolingLoad()
112
  self.heating_load = HeatingLoad()
113
+ self.component_visualization = ComponentVisualization()
114
+ self.scenario_comparison = ScenarioComparison()
115
+ self.psychrometric_visualization = PsychrometricVisualization()
116
+ self.time_based_visualization = TimeBasedVisualization()
117
 
118
  def setup_layout(self):
119
+ """Setup the application layout."""
120
+ # Display header
121
+ st.title("HVAC Load Calculator")
122
+ st.markdown("A comprehensive tool for calculating heating and cooling loads using ASHRAE methods.")
 
 
 
 
 
 
 
 
 
 
 
123
 
124
+ # Setup sidebar
125
+ self.setup_sidebar()
126
 
127
+ # Run calculations when button is clicked
128
+ self.run_calculations()
 
129
 
130
+ # Display current page
 
 
 
 
131
  self.display_page(st.session_state.page)
132
+
133
+ def setup_sidebar(self):
134
+ """Setup the application sidebar."""
135
+ with st.sidebar:
136
+ st.header("Navigation")
137
+
138
+ # Navigation buttons
139
+ if st.button("Building Information", key="nav_building_info"):
140
+ st.session_state.page = "building_info"
141
+
142
+ if st.button("Building Components", key="nav_components"):
143
+ st.session_state.page = "components"
144
+
145
+ if st.button("Internal Loads", key="nav_internal_loads"):
146
+ st.session_state.page = "internal_loads"
147
+
148
+ if st.button("Calculation Results", key="nav_results"):
149
+ st.session_state.page = "results"
150
+
151
+ if st.button("Export Data", key="nav_export"):
152
+ st.session_state.page = "export"
153
+
154
+ st.markdown("---")
155
+
156
+ # Run calculations button
157
+ if st.button("Run Calculations", key="run_calculations"):
158
+ st.session_state.run_calculations = True
159
+
160
+ st.markdown("---")
161
+
162
+ # Display application info
163
+ st.subheader("About")
164
+ st.write("HVAC Load Calculator v1.0.0")
165
+ st.write("© 2025 Manus AI")
166
 
167
  def display_page(self, page: str):
168
  """
169
  Display the selected page.
170
 
171
  Args:
172
+ page: Page to display
173
  """
174
+ if page == "building_info":
175
  self.building_info_form.display()
176
+ elif page == "components":
 
 
177
  self.component_selection.display()
178
+ elif page == "internal_loads":
179
  self.display_internal_loads()
180
+ elif page == "results":
181
  self.results_display.display()
182
+ elif page == "export":
183
  self.data_export.display()
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  def display_internal_loads(self):
186
+ """Display internal loads interface."""
187
+ st.header("Internal Loads")
188
 
189
+ # Initialize internal loads in session state if not exists
190
+ if "internal_loads" not in st.session_state:
191
+ st.session_state.internal_loads = {
192
+ "people": [],
193
+ "lighting": [],
194
+ "equipment": []
195
+ }
196
 
197
  # Create tabs for different internal load types
198
  tab1, tab2, tab3 = st.tabs(["People", "Lighting", "Equipment"])
199
 
200
  with tab1:
201
+ st.subheader("People")
 
202
 
203
  # Display existing people loads
204
+ if st.session_state.internal_loads["people"]:
205
  st.write("Existing People Loads:")
 
206
 
207
+ # Create a table of existing people loads
208
+ people_data = []
209
+ for i, load in enumerate(st.session_state.internal_loads["people"]):
210
  people_data.append({
211
+ "ID": i + 1,
212
+ "Zone": load.get("zone", "Main Zone"),
213
+ "Number of People": load.get("count", 0),
214
+ "Activity Level": load.get("activity", "Seated, light work"),
215
+ "Sensible Heat (W/person)": load.get("sensible_heat", 70),
216
+ "Latent Heat (W/person)": load.get("latent_heat", 45),
217
+ "Schedule": load.get("schedule", "8 AM - 6 PM")
218
  })
 
 
 
 
 
219
 
220
+ people_df = pd.DataFrame(people_data)
221
+ st.dataframe(people_df)
222
+
223
+ # Add edit and delete buttons for people loads
224
+ col1, col2 = st.columns(2)
225
+ with col1:
226
+ people_to_edit = st.selectbox(
227
+ "Select people load to edit:",
228
+ options=range(1, len(st.session_state.internal_loads["people"]) + 1),
229
+ format_func=lambda x: f"People Load #{x}: {st.session_state.internal_loads['people'][x-1].get('zone', 'Main Zone')}"
230
+ )
231
+
232
+ with col2:
233
+ edit_col, delete_col = st.columns(2)
234
+ with edit_col:
235
+ if st.button("Edit People Load", key="edit_people"):
236
+ st.session_state.people_to_edit = people_to_edit - 1
237
+
238
+ with delete_col:
239
+ if st.button("Delete People Load", key="delete_people"):
240
+ st.session_state.internal_loads["people"].pop(people_to_edit - 1)
241
 
242
+ # Add new people load form
243
  st.write("Add New People Load:")
244
+
245
+ # Check if we're editing an existing people load
246
+ editing_people = "people_to_edit" in st.session_state
247
+ people_to_edit = st.session_state.get("people_to_edit", None)
248
+
249
+ # Get the people load to edit if we're editing
250
+ people_load = None
251
+ if editing_people and people_to_edit is not None:
252
+ people_load = st.session_state.internal_loads["people"][people_to_edit]
253
+
254
+ # People load form
255
+ with st.form(key="people_form"):
256
+ # Zone name
257
+ zone_name = st.text_input(
258
+ "Zone Name:",
259
+ value=people_load.get("zone", "Main Zone") if people_load else "Main Zone"
260
+ )
261
+
262
+ # Number of people
263
+ col1, col2 = st.columns(2)
264
+
265
+ with col1:
266
+ people_count = st.number_input(
267
+ "Number of People:",
268
+ min_value=1,
269
+ max_value=1000,
270
+ value=people_load.get("count", 1) if people_load else 1,
271
+ step=1
272
+ )
273
+
274
+ with col2:
275
+ activity_options = [
276
  "Seated, resting",
277
  "Seated, light work",
278
  "Seated, moderate work",
 
281
  "Walking, light work",
282
  "Walking, moderate work",
283
  "Heavy work"
284
+ ]
285
+
286
+ activity = st.selectbox(
287
+ "Activity Level:",
288
+ options=activity_options,
289
+ index=activity_options.index(people_load.get("activity", "Seated, light work")) if people_load and people_load.get("activity") in activity_options else 1
290
+ )
291
+
292
+ # Heat gains
293
+ col1, col2 = st.columns(2)
294
+
295
+ with col1:
296
+ sensible_heat = st.number_input(
297
+ "Sensible Heat (W/person):",
298
+ min_value=0,
299
+ max_value=500,
300
+ value=people_load.get("sensible_heat", 70) if people_load else 70,
301
+ step=5
302
+ )
303
+
304
+ with col2:
305
+ latent_heat = st.number_input(
306
+ "Latent Heat (W/person):",
307
+ min_value=0,
308
+ max_value=500,
309
+ value=people_load.get("latent_heat", 45) if people_load else 45,
310
+ step=5
311
+ )
312
+
313
+ # Schedule
314
+ schedule = st.text_input(
315
+ "Schedule (e.g., 8 AM - 6 PM):",
316
+ value=people_load.get("schedule", "8 AM - 6 PM") if people_load else "8 AM - 6 PM"
317
  )
318
 
319
+ # Submit button
320
+ submit_label = "Update People Load" if editing_people else "Add People Load"
321
+ submit = st.form_submit_button(submit_label)
322
 
323
+ if submit:
324
+ # Create or update people load
325
+ new_people_load = {
326
+ "zone": zone_name,
327
+ "count": people_count,
328
+ "activity": activity,
329
+ "sensible_heat": sensible_heat,
330
+ "latent_heat": latent_heat,
331
+ "schedule": schedule,
332
+ "total_sensible": people_count * sensible_heat,
333
+ "total_latent": people_count * latent_heat
334
+ }
335
 
336
+ # Add or update people load in session state
337
+ if editing_people:
338
+ st.session_state.internal_loads["people"][people_to_edit] = new_people_load
339
+ del st.session_state.people_to_edit
340
+ else:
341
+ st.session_state.internal_loads["people"].append(new_people_load)
342
 
343
  with tab2:
344
+ st.subheader("Lighting")
 
345
 
346
  # Display existing lighting loads
347
+ if st.session_state.internal_loads["lighting"]:
348
  st.write("Existing Lighting Loads:")
 
349
 
350
+ # Create a table of existing lighting loads
351
+ lighting_data = []
352
+ for i, load in enumerate(st.session_state.internal_loads["lighting"]):
353
  lighting_data.append({
354
+ "ID": i + 1,
355
+ "Zone": load.get("zone", "Main Zone"),
356
+ "Type": load.get("type", "Fluorescent"),
357
+ "Power (W)": load.get("power", 0),
358
+ "Power Density (W/m²)": load.get("power_density", 0),
359
+ "Area (m²)": load.get("area", 0),
360
+ "Schedule": load.get("schedule", "8 AM - 6 PM")
361
  })
 
 
 
 
 
362
 
363
+ lighting_df = pd.DataFrame(lighting_data)
364
+ st.dataframe(lighting_df)
365
+
366
+ # Add edit and delete buttons for lighting loads
367
+ col1, col2 = st.columns(2)
368
+ with col1:
369
+ lighting_to_edit = st.selectbox(
370
+ "Select lighting load to edit:",
371
+ options=range(1, len(st.session_state.internal_loads["lighting"]) + 1),
372
+ format_func=lambda x: f"Lighting Load #{x}: {st.session_state.internal_loads['lighting'][x-1].get('zone', 'Main Zone')}"
373
+ )
374
+
375
+ with col2:
376
+ edit_col, delete_col = st.columns(2)
377
+ with edit_col:
378
+ if st.button("Edit Lighting Load", key="edit_lighting"):
379
+ st.session_state.lighting_to_edit = lighting_to_edit - 1
380
+
381
+ with delete_col:
382
+ if st.button("Delete Lighting Load", key="delete_lighting"):
383
+ st.session_state.internal_loads["lighting"].pop(lighting_to_edit - 1)
384
 
385
+ # Add new lighting load form
386
  st.write("Add New Lighting Load:")
387
+
388
+ # Check if we're editing an existing lighting load
389
+ editing_lighting = "lighting_to_edit" in st.session_state
390
+ lighting_to_edit = st.session_state.get("lighting_to_edit", None)
391
+
392
+ # Get the lighting load to edit if we're editing
393
+ lighting_load = None
394
+ if editing_lighting and lighting_to_edit is not None:
395
+ lighting_load = st.session_state.internal_loads["lighting"][lighting_to_edit]
396
+
397
+ # Lighting load form
398
+ with st.form(key="lighting_form"):
399
+ # Zone name
400
+ zone_name = st.text_input(
401
+ "Zone Name:",
402
+ value=lighting_load.get("zone", "Main Zone") if lighting_load else "Main Zone",
403
+ key="lighting_zone"
404
+ )
405
+
406
+ # Lighting type
407
+ lighting_types = ["Incandescent", "Fluorescent", "LED", "Halogen", "Other"]
408
+ lighting_type = st.selectbox(
409
+ "Lighting Type:",
410
+ options=lighting_types,
411
+ index=lighting_types.index(lighting_load.get("type", "Fluorescent")) if lighting_load and lighting_load.get("type") in lighting_types else 1
412
+ )
413
+
414
+ # Input method selection
415
+ input_method = st.radio(
416
+ "Input Method:",
417
+ options=["Total Power", "Power Density"],
418
+ index=0 if not lighting_load or "power" in lighting_load else 1
419
+ )
420
+
421
+ if input_method == "Total Power":
422
+ # Total power input
423
+ col1, col2 = st.columns(2)
424
+
425
+ with col1:
426
+ power = st.number_input(
427
+ "Total Power (W):",
428
+ min_value=0,
429
+ max_value=100000,
430
+ value=lighting_load.get("power", 0) if lighting_load else 0,
431
+ step=10
432
+ )
433
+
434
+ with col2:
435
+ area = st.number_input(
436
+ "Area (m²):",
437
+ min_value=0.0,
438
+ max_value=10000.0,
439
+ value=lighting_load.get("area", 0) if lighting_load else 0,
440
+ step=1.0
441
+ )
442
+
443
+ # Calculate power density
444
+ if area > 0:
445
+ power_density = power / area
446
+ else:
447
+ power_density = 0
448
+
449
+ st.write(f"Power Density: {power_density:.2f} W/m²")
450
+ else:
451
+ # Power density input
452
+ col1, col2 = st.columns(2)
453
+
454
+ with col1:
455
+ power_density = st.number_input(
456
+ "Power Density (W/m²):",
457
+ min_value=0.0,
458
+ max_value=100.0,
459
+ value=lighting_load.get("power_density", 0) if lighting_load else 0,
460
+ step=0.1
461
+ )
462
+
463
+ with col2:
464
+ area = st.number_input(
465
+ "Area (m²):",
466
+ min_value=0.0,
467
+ max_value=10000.0,
468
+ value=lighting_load.get("area", 0) if lighting_load else 0,
469
+ step=1.0
470
+ )
471
 
472
+ # Calculate total power
473
+ power = power_density * area
474
+ st.write(f"Total Power: {power:.2f} W")
475
+
476
+ # Schedule
477
+ schedule = st.text_input(
478
+ "Schedule (e.g., 8 AM - 6 PM):",
479
+ value=lighting_load.get("schedule", "8 AM - 6 PM") if lighting_load else "8 AM - 6 PM",
480
+ key="lighting_schedule"
481
+ )
482
+
483
+ # Submit button
484
+ submit_label = "Update Lighting Load" if editing_lighting else "Add Lighting Load"
485
+ submit = st.form_submit_button(submit_label)
486
+
487
+ if submit:
488
+ # Create or update lighting load
489
+ new_lighting_load = {
490
+ "zone": zone_name,
491
+ "type": lighting_type,
492
+ "power": power,
493
+ "power_density": power_density,
494
+ "area": area,
495
+ "schedule": schedule
496
+ }
497
+
498
+ # Add or update lighting load in session state
499
+ if editing_lighting:
500
+ st.session_state.internal_loads["lighting"][lighting_to_edit] = new_lighting_load
501
+ del st.session_state.lighting_to_edit
502
+ else:
503
+ st.session_state.internal_loads["lighting"].append(new_lighting_load)
504
 
505
  with tab3:
506
+ st.subheader("Equipment")
 
507
 
508
  # Display existing equipment loads
509
+ if st.session_state.internal_loads["equipment"]:
510
  st.write("Existing Equipment Loads:")
 
511
 
512
+ # Create a table of existing equipment loads
513
+ equipment_data = []
514
+ for i, load in enumerate(st.session_state.internal_loads["equipment"]):
515
  equipment_data.append({
516
+ "ID": i + 1,
517
+ "Zone": load.get("zone", "Main Zone"),
518
+ "Type": load.get("type", "Office Equipment"),
519
+ "Sensible Heat (W)": load.get("sensible_heat", 0),
520
+ "Latent Heat (W)": load.get("latent_heat", 0),
521
+ "Schedule": load.get("schedule", "8 AM - 6 PM")
522
  })
 
 
 
 
 
523
 
524
+ equipment_df = pd.DataFrame(equipment_data)
525
+ st.dataframe(equipment_df)
526
+
527
+ # Add edit and delete buttons for equipment loads
528
+ col1, col2 = st.columns(2)
529
+ with col1:
530
+ equipment_to_edit = st.selectbox(
531
+ "Select equipment load to edit:",
532
+ options=range(1, len(st.session_state.internal_loads["equipment"]) + 1),
533
+ format_func=lambda x: f"Equipment Load #{x}: {st.session_state.internal_loads['equipment'][x-1].get('zone', 'Main Zone')}"
534
+ )
535
+
536
+ with col2:
537
+ edit_col, delete_col = st.columns(2)
538
+ with edit_col:
539
+ if st.button("Edit Equipment Load", key="edit_equipment"):
540
+ st.session_state.equipment_to_edit = equipment_to_edit - 1
541
+
542
+ with delete_col:
543
+ if st.button("Delete Equipment Load", key="delete_equipment"):
544
+ st.session_state.internal_loads["equipment"].pop(equipment_to_edit - 1)
545
 
546
+ # Add new equipment load form
547
  st.write("Add New Equipment Load:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
 
549
+ # Check if we're editing an existing equipment load
550
+ editing_equipment = "equipment_to_edit" in st.session_state
551
+ equipment_to_edit = st.session_state.get("equipment_to_edit", None)
 
 
 
552
 
553
+ # Get the equipment load to edit if we're editing
554
+ equipment_load = None
555
+ if editing_equipment and equipment_to_edit is not None:
556
+ equipment_load = st.session_state.internal_loads["equipment"][equipment_to_edit]
557
+
558
+ # Equipment load form
559
+ with st.form(key="equipment_form"):
560
+ # Zone name
561
+ zone_name = st.text_input(
562
+ "Zone Name:",
563
+ value=equipment_load.get("zone", "Main Zone") if equipment_load else "Main Zone",
564
+ key="equipment_zone"
565
+ )
566
+
567
+ # Equipment type
568
+ equipment_types = ["Office Equipment", "Kitchen Equipment", "Medical Equipment", "Industrial Equipment", "Other"]
569
+ equipment_type = st.selectbox(
570
+ "Equipment Type:",
571
+ options=equipment_types,
572
+ index=equipment_types.index(equipment_load.get("type", "Office Equipment")) if equipment_load and equipment_load.get("type") in equipment_types else 0
573
+ )
574
+
575
+ # Heat gains
576
+ col1, col2 = st.columns(2)
577
+
578
+ with col1:
579
+ sensible_heat = st.number_input(
580
+ "Sensible Heat (W):",
581
+ min_value=0,
582
+ max_value=100000,
583
+ value=equipment_load.get("sensible_heat", 0) if equipment_load else 0,
584
+ step=10
585
+ )
586
+
587
+ with col2:
588
+ latent_heat = st.number_input(
589
+ "Latent Heat (W):",
590
+ min_value=0,
591
+ max_value=100000,
592
+ value=equipment_load.get("latent_heat", 0) if equipment_load else 0,
593
+ step=10
594
+ )
595
+
596
+ # Schedule
597
+ schedule = st.text_input(
598
+ "Schedule (e.g., 8 AM - 6 PM):",
599
+ value=equipment_load.get("schedule", "8 AM - 6 PM") if equipment_load else "8 AM - 6 PM",
600
+ key="equipment_schedule"
601
+ )
602
+
603
+ # Submit button
604
+ submit_label = "Update Equipment Load" if editing_equipment else "Add Equipment Load"
605
+ submit = st.form_submit_button(submit_label)
606
+
607
+ if submit:
608
+ # Create or update equipment load
609
+ new_equipment_load = {
610
+ "zone": zone_name,
611
+ "type": equipment_type,
612
+ "sensible_heat": sensible_heat,
613
+ "latent_heat": latent_heat,
614
+ "schedule": schedule
615
+ }
616
+
617
+ # Add or update equipment load in session state
618
+ if editing_equipment:
619
+ st.session_state.internal_loads["equipment"][equipment_to_edit] = new_equipment_load
620
+ del st.session_state.equipment_to_edit
621
+ else:
622
+ st.session_state.internal_loads["equipment"].append(new_equipment_load)
623
 
624
  def run_calculations(self):
625
+ """Run HVAC load calculations."""
626
+ # Check if calculations should be run
627
+ if not hasattr(st.session_state, "run_calculations") or not st.session_state.run_calculations:
628
+ return
629
+
630
+ # Reset the flag
631
+ st.session_state.run_calculations = False
632
+
633
+ # Validate inputs before running calculations
634
  if not self.data_validation.validate_calculation_inputs(st.session_state):
635
+ st.error("Please fill in all required information before running calculations.")
636
  return
637
 
638
+ # Get input data from session state
 
639
  building_info = st.session_state.building_info
640
+ components = st.session_state.components
641
  internal_loads = st.session_state.internal_loads
642
 
643
+ # Get climate data
 
 
 
644
  climate_data = {
645
+ "location": building_info.get("location", ""),
646
+ "outdoor_temp": building_info.get("summer_outdoor_temp", 35.0),
647
+ "outdoor_humidity": building_info.get("summer_outdoor_humidity", 50.0),
648
+ "indoor_temp": building_info.get("summer_indoor_temp", 24.0),
649
+ "indoor_humidity": building_info.get("summer_indoor_humidity", 50.0),
650
+ "daily_range": building_info.get("daily_temp_range", 8.0),
651
+ "latitude": building_info.get("latitude", "40N"),
652
+ "month": building_info.get("design_month", 7),
653
+ "hour": building_info.get("design_hour", 15)
654
  }
655
 
656
+ # Calculate cooling load
657
+ cooling_results = self.cooling_load.calculate_total_cooling_load(
658
+ walls=components.get("walls", []),
659
+ roofs=components.get("roofs", []),
660
+ floors=components.get("floors", []),
661
+ windows=components.get("windows", []),
662
+ doors=components.get("doors", []),
663
+ people=internal_loads.get("people", []),
664
+ lighting=internal_loads.get("lighting", []),
665
+ equipment=internal_loads.get("equipment", []),
666
+ infiltration_rate=building_info.get("infiltration_rate", 0.5),
667
+ floor_area=building_info.get("floor_area", 100.0),
668
+ climate_data=climate_data
669
+ )
670
 
671
+ # Get heating climate data
672
+ heating_outdoor_conditions = {
673
+ "temperature": building_info.get("winter_outdoor_temp", -10.0),
674
+ "humidity": building_info.get("winter_outdoor_humidity", 80.0)
675
  }
676
 
677
+ heating_indoor_conditions = {
678
+ "temperature": building_info.get("winter_indoor_temp", 21.0),
679
+ "humidity": building_info.get("winter_indoor_humidity", 30.0)
680
+ }
 
 
 
 
681
 
682
+ # Calculate heating load
683
  heating_results = self.heating_load.calculate_design_heating_load(
684
+ walls=components.get("walls", []),
685
+ roofs=components.get("roofs", []),
686
+ floors=components.get("floors", []),
687
+ windows=components.get("windows", []),
688
+ doors=components.get("doors", []),
689
+ infiltration_rate=building_info.get("infiltration_rate", 0.5),
690
+ floor_area=building_info.get("floor_area", 100.0),
691
+ outdoor_conditions=heating_outdoor_conditions,
692
+ indoor_conditions=heating_indoor_conditions,
693
+ safety_factor=building_info.get("heating_safety_factor", 10.0)
694
  )
695
 
696
+ # Calculate psychrometric properties
697
+ psychrometric_results = self.psychrometrics.calculate_psychrometric_properties(
698
+ outdoor_temp=climate_data["outdoor_temp"],
699
+ outdoor_humidity=climate_data["outdoor_humidity"],
700
+ indoor_temp=climate_data["indoor_temp"],
701
+ indoor_humidity=climate_data["indoor_humidity"]
702
+ )
703
 
704
+ # Store results in session state
705
  st.session_state.calculation_results = {
706
+ "cooling_load": cooling_results,
707
+ "heating_load": heating_results,
708
+ "psychrometrics": psychrometric_results
709
  }
710
 
 
 
 
711
  # Navigate to results page
712
+ st.session_state.page = "results"
713
+ st.success("Calculations completed successfully!")
714
 
715
 
716
+ # Run the application
717
  if __name__ == "__main__":
 
718
  app = HVACCalculator()