mabuseif commited on
Commit
fb5a701
·
verified ·
1 Parent(s): 4bab960

Upload 4 files

Browse files
app/main.py CHANGED
@@ -1,720 +1,15 @@
1
- """
2
- HVAC Calculator Code Documentation
3
-
4
- This module contains the main Streamlit application for the HVAC Calculator.
5
- It provides a comprehensive interface for calculating heating and cooling loads
6
- using ASHRAE methods.
7
-
8
- Author: Manus AI
9
- Date: March 2025
10
- Version: 1.0.0
11
- """
12
-
13
  import streamlit as st
14
  import pandas as pd
15
  import numpy as np
16
- import plotly.graph_objects as go
17
- import plotly.express as px
18
- import matplotlib.pyplot as plt
19
- import json
20
  import os
21
  import sys
22
- 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_redesign import ComponentSelectionRedesigned
27
- from app.results_display import ResultsDisplay
28
- from app.data_validation import DataValidation
29
- from app.data_persistence import DataPersistence
30
- from app.data_export import DataExport
31
-
32
- # Import data modules
33
- from data.building_components import Wall, Roof, Floor, Window, Door, Orientation, ComponentType
34
- from data.reference_data import ReferenceData
35
- from data.climate_data import ClimateData
36
- from data.ashrae_tables import ASHRAETables
37
-
38
- # Import utility modules
39
- from utils.component_library import ComponentLibrary
40
- from utils.u_value_calculator import UValueCalculator
41
- from utils.shading_system import ShadingSystem
42
- from utils.area_calculation_system import AreaCalculationSystem
43
- from utils.psychrometrics import Psychrometrics
44
- from utils.heat_transfer import HeatTransfer
45
- from utils.cooling_load import CoolingLoad
46
- from utils.heating_load import HeatingLoad
47
- from utils.component_visualization import ComponentVisualization
48
- from utils.scenario_comparison import ScenarioComparison
49
- from utils.psychrometric_visualization import PsychrometricVisualization
50
- from utils.time_based_visualization import TimeBasedVisualization
51
-
52
 
53
- class HVACCalculator:
54
- """Main HVAC Calculator application class."""
55
-
56
- def __init__(self):
57
- """Initialize the HVAC Calculator application."""
58
- # Set page configuration
59
- st.set_page_config(
60
- page_title="HVAC Load Calculator",
61
- page_icon="🌡️",
62
- layout="wide",
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
- # Initialize calculation trigger
90
- if "should_run_calculations" not in st.session_state:
91
- st.session_state.should_run_calculations = False
92
-
93
- def _initialize_modules(self):
94
- """Initialize application modules."""
95
- # Application modules
96
- self.building_info_form = BuildingInfoForm()
97
- self.component_selection = ComponentSelectionRedesigned()
98
- self.results_display = ResultsDisplay()
99
- self.data_validation = DataValidation()
100
- self.data_persistence = DataPersistence()
101
- self.data_export = DataExport()
102
-
103
- # Data modules
104
- self.reference_data = ReferenceData()
105
- self.climate_data = ClimateData()
106
- self.ashrae_tables = ASHRAETables()
107
-
108
- # Utility modules
109
- self.component_library = ComponentLibrary()
110
- self.u_value_calculator = UValueCalculator()
111
- self.shading_system = ShadingSystem()
112
- self.area_calculation_system = AreaCalculationSystem()
113
- self.psychrometrics = Psychrometrics()
114
- self.heat_transfer = HeatTransfer()
115
- self.cooling_load = CoolingLoad()
116
- self.heating_load = HeatingLoad()
117
- self.component_visualization = ComponentVisualization()
118
- self.scenario_comparison = ScenarioComparison()
119
- self.psychrometric_visualization = PsychrometricVisualization()
120
- self.time_based_visualization = TimeBasedVisualization()
121
-
122
- def setup_layout(self):
123
- """Setup the application layout."""
124
- # Display header
125
- st.title("HVAC Load Calculator")
126
- st.markdown("A comprehensive tool for calculating heating and cooling loads using ASHRAE methods.")
127
-
128
- # Setup sidebar
129
- self.setup_sidebar()
130
-
131
- # Run calculations when triggered
132
- if st.session_state.should_run_calculations:
133
- self.run_calculations()
134
- # Reset the flag after running calculations
135
- st.session_state.should_run_calculations = False
136
-
137
- # Display current page
138
- self.display_page(st.session_state.page)
139
-
140
- def setup_sidebar(self):
141
- """Setup the application sidebar."""
142
- with st.sidebar:
143
- st.header("Navigation")
144
-
145
- # Navigation buttons
146
- if st.button("Building Information", key="nav_building_info"):
147
- st.session_state.page = "building_info"
148
-
149
- if st.button("Building Components", key="nav_components"):
150
- st.session_state.page = "components"
151
-
152
- if st.button("Internal Loads", key="nav_internal_loads"):
153
- st.session_state.page = "internal_loads"
154
-
155
- if st.button("Calculation Results", key="nav_results"):
156
- st.session_state.page = "results"
157
-
158
- if st.button("Export Data", key="nav_export"):
159
- st.session_state.page = "export"
160
-
161
- st.markdown("---")
162
-
163
- # Run calculations button - using a callback function
164
- def on_run_calculations_click():
165
- st.session_state.should_run_calculations = True
166
-
167
- st.button("Run Calculations", key="run_calc_button", on_click=on_run_calculations_click)
168
-
169
- st.markdown("---")
170
-
171
- # Display application info
172
- st.subheader("About")
173
- st.write("HVAC Load Calculator v1.0.0")
174
- st.write("© 2025 Manus AI")
175
-
176
- def display_page(self, page: str):
177
- """
178
- Display the selected page.
179
-
180
- Args:
181
- page: Page to display
182
- """
183
- if page == "building_info":
184
- self.building_info_form.display()
185
- elif page == "components":
186
- self.component_selection.display()
187
- elif page == "internal_loads":
188
- self.display_internal_loads()
189
- elif page == "results":
190
- self.results_display.display()
191
- elif page == "export":
192
- self.data_export.display()
193
-
194
- def display_internal_loads(self):
195
- """Display internal loads interface."""
196
- st.header("Internal Loads")
197
-
198
- # Initialize internal loads in session state if not exists
199
- if "internal_loads" not in st.session_state:
200
- st.session_state.internal_loads = {
201
- "people": [],
202
- "lighting": [],
203
- "equipment": []
204
- }
205
-
206
- # Create tabs for different internal load types
207
- tab1, tab2, tab3 = st.tabs(["People", "Lighting", "Equipment"])
208
-
209
- with tab1:
210
- st.subheader("People")
211
-
212
- # Display existing people loads
213
- if st.session_state.internal_loads["people"]:
214
- st.write("Existing People Loads:")
215
-
216
- # Create a table of existing people loads
217
- people_data = []
218
- for i, load in enumerate(st.session_state.internal_loads["people"]):
219
- people_data.append({
220
- "ID": i + 1,
221
- "Zone": load.get("zone", "Main Zone"),
222
- "Number of People": load.get("count", 0),
223
- "Activity Level": load.get("activity", "Seated, light work"),
224
- "Sensible Heat (W/person)": load.get("sensible_heat", 70),
225
- "Latent Heat (W/person)": load.get("latent_heat", 45),
226
- "Schedule": load.get("schedule", "8 AM - 6 PM")
227
- })
228
-
229
- people_df = pd.DataFrame(people_data)
230
- st.dataframe(people_df)
231
-
232
- # Add edit and delete buttons for people loads
233
- col1, col2 = st.columns(2)
234
- with col1:
235
- people_to_edit = st.selectbox(
236
- "Select people load to edit:",
237
- options=range(1, len(st.session_state.internal_loads["people"]) + 1),
238
- format_func=lambda x: f"People Load #{x}: {st.session_state.internal_loads['people'][x-1].get('zone', 'Main Zone')}"
239
- )
240
-
241
- with col2:
242
- edit_col, delete_col = st.columns(2)
243
- with edit_col:
244
- if st.button("Edit People Load", key="edit_people"):
245
- st.session_state.people_to_edit = people_to_edit - 1
246
-
247
- with delete_col:
248
- if st.button("Delete People Load", key="delete_people"):
249
- st.session_state.internal_loads["people"].pop(people_to_edit - 1)
250
-
251
- # Add new people load form
252
- st.write("Add New People Load:")
253
-
254
- # Check if we're editing an existing people load
255
- editing_people = "people_to_edit" in st.session_state
256
- people_to_edit = st.session_state.get("people_to_edit", None)
257
-
258
- # Get the people load to edit if we're editing
259
- people_load = None
260
- if editing_people and people_to_edit is not None:
261
- people_load = st.session_state.internal_loads["people"][people_to_edit]
262
-
263
- # People load form
264
- with st.form(key="people_form"):
265
- # Zone name
266
- zone_name = st.text_input(
267
- "Zone Name:",
268
- value=people_load.get("zone", "Main Zone") if people_load else "Main Zone"
269
- )
270
-
271
- # Number of people
272
- col1, col2 = st.columns(2)
273
-
274
- with col1:
275
- people_count = st.number_input(
276
- "Number of People:",
277
- min_value=1.0,
278
- max_value=1000.0,
279
- value=float(people_load.get("count", 1)) if people_load else 1.0,
280
- step=1.0
281
- )
282
-
283
- with col2:
284
- activity_options = [
285
- "Seated, resting",
286
- "Seated, light work",
287
- "Seated, moderate work",
288
- "Standing, light work",
289
- "Standing, moderate work",
290
- "Walking, light work",
291
- "Walking, moderate work",
292
- "Heavy work"
293
- ]
294
-
295
- activity = st.selectbox(
296
- "Activity Level:",
297
- options=activity_options,
298
- index=activity_options.index(people_load.get("activity", "Seated, light work")) if people_load and people_load.get("activity") in activity_options else 1
299
- )
300
-
301
- # Heat gains
302
- col1, col2 = st.columns(2)
303
-
304
- with col1:
305
- sensible_heat = st.number_input(
306
- "Sensible Heat (W/person):",
307
- min_value=0.0,
308
- max_value=500.0,
309
- value=float(people_load.get("sensible_heat", 70)) if people_load else 70.0,
310
- step=5.0
311
- )
312
-
313
- with col2:
314
- latent_heat = st.number_input(
315
- "Latent Heat (W/person):",
316
- min_value=0.0,
317
- max_value=500.0,
318
- value=float(people_load.get("latent_heat", 45)) if people_load else 45.0,
319
- step=5.0
320
- )
321
-
322
- # Schedule
323
- schedule = st.text_input(
324
- "Schedule (e.g., 8 AM - 6 PM):",
325
- value=people_load.get("schedule", "8 AM - 6 PM") if people_load else "8 AM - 6 PM"
326
- )
327
-
328
- # Submit button
329
- submit_label = "Update People Load" if editing_people else "Add People Load"
330
- submit = st.form_submit_button(submit_label)
331
-
332
- if submit:
333
- # Create or update people load
334
- new_people_load = {
335
- "zone": zone_name,
336
- "count": int(people_count),
337
- "activity": activity,
338
- "sensible_heat": float(sensible_heat),
339
- "latent_heat": float(latent_heat),
340
- "schedule": schedule,
341
- "total_sensible": int(people_count) * float(sensible_heat),
342
- "total_latent": int(people_count) * float(latent_heat)
343
- }
344
-
345
- # Add or update people load in session state
346
- if editing_people:
347
- st.session_state.internal_loads["people"][people_to_edit] = new_people_load
348
- del st.session_state.people_to_edit
349
- else:
350
- st.session_state.internal_loads["people"].append(new_people_load)
351
-
352
- with tab2:
353
- st.subheader("Lighting")
354
-
355
- # Display existing lighting loads
356
- if st.session_state.internal_loads["lighting"]:
357
- st.write("Existing Lighting Loads:")
358
-
359
- # Create a table of existing lighting loads
360
- lighting_data = []
361
- for i, load in enumerate(st.session_state.internal_loads["lighting"]):
362
- lighting_data.append({
363
- "ID": i + 1,
364
- "Zone": load.get("zone", "Main Zone"),
365
- "Type": load.get("type", "Fluorescent"),
366
- "Power (W)": load.get("power", 0),
367
- "Power Density (W/m²)": load.get("power_density", 0),
368
- "Area (m²)": load.get("area", 0),
369
- "Schedule": load.get("schedule", "8 AM - 6 PM")
370
- })
371
-
372
- lighting_df = pd.DataFrame(lighting_data)
373
- st.dataframe(lighting_df)
374
-
375
- # Add edit and delete buttons for lighting loads
376
- col1, col2 = st.columns(2)
377
- with col1:
378
- lighting_to_edit = st.selectbox(
379
- "Select lighting load to edit:",
380
- options=range(1, len(st.session_state.internal_loads["lighting"]) + 1),
381
- format_func=lambda x: f"Lighting Load #{x}: {st.session_state.internal_loads['lighting'][x-1].get('zone', 'Main Zone')}"
382
- )
383
-
384
- with col2:
385
- edit_col, delete_col = st.columns(2)
386
- with edit_col:
387
- if st.button("Edit Lighting Load", key="edit_lighting"):
388
- st.session_state.lighting_to_edit = lighting_to_edit - 1
389
-
390
- with delete_col:
391
- if st.button("Delete Lighting Load", key="delete_lighting"):
392
- st.session_state.internal_loads["lighting"].pop(lighting_to_edit - 1)
393
-
394
- # Add new lighting load form
395
- st.write("Add New Lighting Load:")
396
-
397
- # Check if we're editing an existing lighting load
398
- editing_lighting = "lighting_to_edit" in st.session_state
399
- lighting_to_edit = st.session_state.get("lighting_to_edit", None)
400
-
401
- # Get the lighting load to edit if we're editing
402
- lighting_load = None
403
- if editing_lighting and lighting_to_edit is not None:
404
- lighting_load = st.session_state.internal_loads["lighting"][lighting_to_edit]
405
-
406
- # Lighting load form
407
- with st.form(key="lighting_form"):
408
- # Zone name
409
- zone_name = st.text_input(
410
- "Zone Name:",
411
- value=lighting_load.get("zone", "Main Zone") if lighting_load else "Main Zone",
412
- key="lighting_zone"
413
- )
414
-
415
- # Lighting type
416
- lighting_types = ["Incandescent", "Fluorescent", "LED", "Halogen", "Other"]
417
- lighting_type = st.selectbox(
418
- "Lighting Type:",
419
- options=lighting_types,
420
- index=lighting_types.index(lighting_load.get("type", "Fluorescent")) if lighting_load and lighting_load.get("type") in lighting_types else 1
421
- )
422
-
423
- # Input method selection
424
- input_method = st.radio(
425
- "Input Method:",
426
- options=["Total Power", "Power Density"],
427
- index=0 if not lighting_load or "power" in lighting_load else 1
428
- )
429
-
430
- if input_method == "Total Power":
431
- # Total power input
432
- col1, col2 = st.columns(2)
433
-
434
- with col1:
435
- power = st.number_input(
436
- "Total Power (W):",
437
- min_value=0.0,
438
- max_value=100000.0,
439
- value=float(lighting_load.get("power", 0)) if lighting_load else 0.0,
440
- step=10.0
441
- )
442
-
443
- with col2:
444
- area = st.number_input(
445
- "Area (m²):",
446
- min_value=0.0,
447
- max_value=10000.0,
448
- value=float(lighting_load.get("area", 0)) if lighting_load else 0.0,
449
- step=1.0
450
- )
451
-
452
- # Calculate power density
453
- if area > 0:
454
- power_density = power / area
455
- else:
456
- power_density = 0.0
457
-
458
- st.write(f"Power Density: {power_density:.2f} W/m²")
459
- else:
460
- # Power density input
461
- col1, col2 = st.columns(2)
462
-
463
- with col1:
464
- power_density = st.number_input(
465
- "Power Density (W/m²):",
466
- min_value=0.0,
467
- max_value=100.0,
468
- value=float(lighting_load.get("power_density", 0)) if lighting_load else 0.0,
469
- step=0.1
470
- )
471
-
472
- with col2:
473
- area = st.number_input(
474
- "Area (m²):",
475
- min_value=0.0,
476
- max_value=10000.0,
477
- value=float(lighting_load.get("area", 0)) if lighting_load else 0.0,
478
- step=1.0
479
- )
480
-
481
- # Calculate total power
482
- power = power_density * area
483
- st.write(f"Total Power: {power:.2f} W")
484
-
485
- # Schedule
486
- schedule = st.text_input(
487
- "Schedule (e.g., 8 AM - 6 PM):",
488
- value=lighting_load.get("schedule", "8 AM - 6 PM") if lighting_load else "8 AM - 6 PM",
489
- key="lighting_schedule"
490
- )
491
-
492
- # Submit button
493
- submit_label = "Update Lighting Load" if editing_lighting else "Add Lighting Load"
494
- submit = st.form_submit_button(submit_label)
495
-
496
- if submit:
497
- # Create or update lighting load
498
- new_lighting_load = {
499
- "zone": zone_name,
500
- "type": lighting_type,
501
- "power": float(power),
502
- "power_density": float(power_density),
503
- "area": float(area),
504
- "schedule": schedule
505
- }
506
-
507
- # Add or update lighting load in session state
508
- if editing_lighting:
509
- st.session_state.internal_loads["lighting"][lighting_to_edit] = new_lighting_load
510
- del st.session_state.lighting_to_edit
511
- else:
512
- st.session_state.internal_loads["lighting"].append(new_lighting_load)
513
-
514
- with tab3:
515
- st.subheader("Equipment")
516
-
517
- # Display existing equipment loads
518
- if st.session_state.internal_loads["equipment"]:
519
- st.write("Existing Equipment Loads:")
520
-
521
- # Create a table of existing equipment loads
522
- equipment_data = []
523
- for i, load in enumerate(st.session_state.internal_loads["equipment"]):
524
- equipment_data.append({
525
- "ID": i + 1,
526
- "Zone": load.get("zone", "Main Zone"),
527
- "Type": load.get("type", "Office Equipment"),
528
- "Sensible Heat (W)": load.get("sensible_heat", 0),
529
- "Latent Heat (W)": load.get("latent_heat", 0),
530
- "Schedule": load.get("schedule", "8 AM - 6 PM")
531
- })
532
-
533
- equipment_df = pd.DataFrame(equipment_data)
534
- st.dataframe(equipment_df)
535
-
536
- # Add edit and delete buttons for equipment loads
537
- col1, col2 = st.columns(2)
538
- with col1:
539
- equipment_to_edit = st.selectbox(
540
- "Select equipment load to edit:",
541
- options=range(1, len(st.session_state.internal_loads["equipment"]) + 1),
542
- format_func=lambda x: f"Equipment Load #{x}: {st.session_state.internal_loads['equipment'][x-1].get('zone', 'Main Zone')}"
543
- )
544
-
545
- with col2:
546
- edit_col, delete_col = st.columns(2)
547
- with edit_col:
548
- if st.button("Edit Equipment Load", key="edit_equipment"):
549
- st.session_state.equipment_to_edit = equipment_to_edit - 1
550
-
551
- with delete_col:
552
- if st.button("Delete Equipment Load", key="delete_equipment"):
553
- st.session_state.internal_loads["equipment"].pop(equipment_to_edit - 1)
554
-
555
- # Add new equipment load form
556
- st.write("Add New Equipment Load:")
557
-
558
- # Check if we're editing an existing equipment load
559
- editing_equipment = "equipment_to_edit" in st.session_state
560
- equipment_to_edit = st.session_state.get("equipment_to_edit", None)
561
-
562
- # Get the equipment load to edit if we're editing
563
- equipment_load = None
564
- if editing_equipment and equipment_to_edit is not None:
565
- equipment_load = st.session_state.internal_loads["equipment"][equipment_to_edit]
566
-
567
- # Equipment load form
568
- with st.form(key="equipment_form"):
569
- # Zone name
570
- zone_name = st.text_input(
571
- "Zone Name:",
572
- value=equipment_load.get("zone", "Main Zone") if equipment_load else "Main Zone",
573
- key="equipment_zone"
574
- )
575
-
576
- # Equipment type
577
- equipment_types = ["Office Equipment", "Kitchen Equipment", "Medical Equipment", "Industrial Equipment", "Other"]
578
- equipment_type = st.selectbox(
579
- "Equipment Type:",
580
- options=equipment_types,
581
- index=equipment_types.index(equipment_load.get("type", "Office Equipment")) if equipment_load and equipment_load.get("type") in equipment_types else 0
582
- )
583
-
584
- # Heat gains
585
- col1, col2 = st.columns(2)
586
-
587
- with col1:
588
- sensible_heat = st.number_input(
589
- "Sensible Heat (W):",
590
- min_value=0.0,
591
- max_value=100000.0,
592
- value=float(equipment_load.get("sensible_heat", 0)) if equipment_load else 0.0,
593
- step=10.0
594
- )
595
-
596
- with col2:
597
- latent_heat = st.number_input(
598
- "Latent Heat (W):",
599
- min_value=0.0,
600
- max_value=100000.0,
601
- value=float(equipment_load.get("latent_heat", 0)) if equipment_load else 0.0,
602
- step=10.0
603
- )
604
-
605
- # Schedule
606
- schedule = st.text_input(
607
- "Schedule (e.g., 8 AM - 6 PM):",
608
- value=equipment_load.get("schedule", "8 AM - 6 PM") if equipment_load else "8 AM - 6 PM",
609
- key="equipment_schedule"
610
- )
611
-
612
- # Submit button
613
- submit_label = "Update Equipment Load" if editing_equipment else "Add Equipment Load"
614
- submit = st.form_submit_button(submit_label)
615
-
616
- if submit:
617
- # Create or update equipment load
618
- new_equipment_load = {
619
- "zone": zone_name,
620
- "type": equipment_type,
621
- "sensible_heat": float(sensible_heat),
622
- "latent_heat": float(latent_heat),
623
- "schedule": schedule
624
- }
625
-
626
- # Add or update equipment load in session state
627
- if editing_equipment:
628
- st.session_state.internal_loads["equipment"][equipment_to_edit] = new_equipment_load
629
- del st.session_state.equipment_to_edit
630
- else:
631
- st.session_state.internal_loads["equipment"].append(new_equipment_load)
632
-
633
- def run_calculations(self):
634
- """Run HVAC load calculations."""
635
- # Validate inputs before running calculations
636
- if not self.data_validation.validate_calculation_inputs(st.session_state):
637
- st.error("Please fill in all required information before running calculations.")
638
- return
639
-
640
- # Get input data from session state
641
- building_info = st.session_state.building_info
642
- components = st.session_state.components
643
- internal_loads = st.session_state.internal_loads
644
-
645
- # Get climate data
646
- climate_data = {
647
- "location": building_info.get("location", ""),
648
- "outdoor_temp": building_info.get("summer_outdoor_temp", 35.0),
649
- "outdoor_humidity": building_info.get("summer_outdoor_humidity", 50.0),
650
- "indoor_temp": building_info.get("summer_indoor_temp", 24.0),
651
- "indoor_humidity": building_info.get("summer_indoor_humidity", 50.0),
652
- "daily_range": building_info.get("daily_temp_range", 8.0),
653
- "latitude": building_info.get("latitude", "40N"),
654
- "month": building_info.get("design_month", 7),
655
- "hour": building_info.get("design_hour", 15)
656
- }
657
-
658
- # Calculate cooling load
659
- cooling_results = self.cooling_load.calculate_total_cooling_load(
660
- walls=components.get("walls", []),
661
- roofs=components.get("roofs", []),
662
- floors=components.get("floors", []),
663
- windows=components.get("windows", []),
664
- doors=components.get("doors", []),
665
- people=internal_loads.get("people", []),
666
- lighting=internal_loads.get("lighting", []),
667
- equipment=internal_loads.get("equipment", []),
668
- infiltration_rate=building_info.get("infiltration_rate", 0.5),
669
- floor_area=building_info.get("floor_area", 100.0),
670
- climate_data=climate_data
671
- )
672
-
673
- # Get heating climate data
674
- heating_outdoor_conditions = {
675
- "temperature": building_info.get("winter_outdoor_temp", -10.0),
676
- "humidity": building_info.get("winter_outdoor_humidity", 80.0)
677
- }
678
-
679
- heating_indoor_conditions = {
680
- "temperature": building_info.get("winter_indoor_temp", 21.0),
681
- "humidity": building_info.get("winter_indoor_humidity", 30.0)
682
- }
683
-
684
- # Calculate heating load
685
- heating_results = self.heating_load.calculate_design_heating_load(
686
- walls=components.get("walls", []),
687
- roofs=components.get("roofs", []),
688
- floors=components.get("floors", []),
689
- windows=components.get("windows", []),
690
- doors=components.get("doors", []),
691
- infiltration_rate=building_info.get("infiltration_rate", 0.5),
692
- floor_area=building_info.get("floor_area", 100.0),
693
- outdoor_conditions=heating_outdoor_conditions,
694
- indoor_conditions=heating_indoor_conditions,
695
- safety_factor=building_info.get("heating_safety_factor", 10.0)
696
- )
697
-
698
- # Calculate psychrometric properties
699
- psychrometric_results = self.psychrometrics.calculate_psychrometric_properties(
700
- outdoor_temp=climate_data["outdoor_temp"],
701
- outdoor_humidity=climate_data["outdoor_humidity"],
702
- indoor_temp=climate_data["indoor_temp"],
703
- indoor_humidity=climate_data["indoor_humidity"]
704
- )
705
-
706
- # Store results in session state
707
- st.session_state.calculation_results = {
708
- "cooling_load": cooling_results,
709
- "heating_load": heating_results,
710
- "psychrometrics": psychrometric_results
711
- }
712
-
713
- # Navigate to results page
714
- st.session_state.page = "results"
715
- st.success("Calculations completed successfully!")
716
 
 
 
717
 
718
  # Run the application
719
  if __name__ == "__main__":
720
- app = HVACCalculator()
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
 
 
 
 
4
  import os
5
  import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
+ # Add parent directory to path to import modules
8
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ # Import the main application
11
+ from hvac_calculator_file_upload import HVACCalculatorFileUpload
12
 
13
  # Run the application
14
  if __name__ == "__main__":
15
+ app = HVACCalculatorFileUpload()
data/ashrae_tables.py CHANGED
@@ -1,453 +1,48 @@
1
  """
2
- ASHRAE tables module for HVAC Load Calculator.
3
- This module implements CLTD, SCL, CLF tables and interpolation functions for load calculations.
4
- """
5
-
6
- from typing import Dict, List, Any, Optional, Tuple
7
- import pandas as pd
8
- import numpy as np
9
- import os
10
- import json
11
- from enum import Enum
12
-
13
- # Define paths
14
- DATA_DIR = os.path.dirname(os.path.abspath(__file__))
15
-
16
-
17
- class WallGroup(Enum):
18
- """Enumeration for ASHRAE wall groups."""
19
- A = "A" # Light construction
20
- B = "B"
21
- C = "C"
22
- D = "D"
23
- E = "E"
24
- F = "F"
25
- G = "G"
26
- H = "H" # Heavy construction
27
- CUSTOM = "Custom" # Added for custom wall types
28
-
29
-
30
- class RoofGroup(Enum):
31
- """Enumeration for ASHRAE roof groups."""
32
- A = "A" # Light construction
33
- B = "B"
34
- C = "C"
35
- D = "D"
36
- E = "E"
37
- F = "F"
38
- G = "G" # Heavy construction
39
- CUSTOM = "Custom" # Added for custom roof types
40
-
41
-
42
- class Orientation(Enum):
43
- """Enumeration for building component orientations."""
44
- N = "North"
45
- NE = "Northeast"
46
- E = "East"
47
- SE = "Southeast"
48
- S = "South"
49
- SW = "Southwest"
50
- W = "West"
51
- NW = "Northwest"
52
- HOR = "Horizontal" # For roofs and floors
53
 
 
 
 
54
 
55
  class ASHRAETables:
56
- """Class for ASHRAE tables and interpolation functions."""
57
 
58
  def __init__(self):
59
- """Initialize ASHRAE tables."""
60
- # Create default CLTD tables for walls with hardcoded values
61
- # These are simplified default values that will be used when CSV files are not available
62
-
63
- # Create a default DataFrame for wall CLTD values
64
- # Columns are orientations, rows are hours (0-23)
65
- default_wall_cltd = pd.DataFrame({
66
- "N": [2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 10, 9, 8, 6, 5, 4, 4, 3, 2],
67
- "NE": [2, 1, 0, 0, 0, 2, 5, 8, 10, 11, 10, 9, 8, 8, 7, 7, 6, 5, 5, 4, 3, 3, 2, 2],
68
- "E": [3, 2, 1, 0, 0, 1, 3, 7, 11, 14, 16, 16, 15, 14, 12, 10, 8, 7, 6, 5, 5, 4, 4, 3],
69
- "SE": [3, 2, 1, 0, 0, 0, 1, 4, 7, 10, 13, 15, 16, 16, 15, 13, 10, 8, 7, 6, 5, 4, 4, 3],
70
- "S": [3, 2, 1, 0, 0, 0, 0, 1, 2, 4, 6, 9, 12, 14, 15, 15, 14, 12, 9, 7, 6, 5, 4, 3],
71
- "SW": [3, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 6, 8, 11, 14, 16, 17, 16, 14, 11, 8, 6, 5, 4],
72
- "W": [3, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 8, 10, 13, 15, 16, 15, 13, 10, 7, 5, 4],
73
- "NW": [2, 1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 12, 10, 8, 6, 4, 3]
74
- }, index=range(24))
75
-
76
- # Create a default DataFrame for roof CLTD values
77
- # Column is HOR (horizontal), rows are hours (0-23)
78
- default_roof_cltd = pd.DataFrame({
79
- "HOR": [3, 2, 1, 0, 0, 0, 1, 3, 6, 9, 13, 16, 19, 21, 22, 21, 19, 16, 13, 10, 8, 6, 5, 4]
80
- }, index=range(24))
81
-
82
- # Initialize CLTD tables for walls with default values
83
- self.cltd_wall = {
84
- "A": default_wall_cltd.copy(),
85
- "B": default_wall_cltd.copy(),
86
- "C": default_wall_cltd.copy(),
87
- "D": default_wall_cltd.copy(),
88
- "E": default_wall_cltd.copy(),
89
- "F": default_wall_cltd.copy(),
90
- "G": default_wall_cltd.copy(),
91
- "H": default_wall_cltd.copy()
92
- }
93
-
94
- # Initialize CLTD tables for roofs with default values
95
- self.cltd_roof = {
96
- "A": default_roof_cltd.copy(),
97
- "B": default_roof_cltd.copy(),
98
- "C": default_roof_cltd.copy(),
99
- "D": default_roof_cltd.copy(),
100
- "E": default_roof_cltd.copy(),
101
- "F": default_roof_cltd.copy(),
102
- "G": default_roof_cltd.copy()
103
- }
104
-
105
- # Create default SCL table for glass
106
- # Columns are orientations, rows are latitudes
107
- self.scl_glass = pd.DataFrame({
108
- "N": [40, 45, 50, 55, 60],
109
- "NE": [80, 85, 90, 95, 100],
110
- "E": [120, 125, 130, 135, 140],
111
- "SE": [110, 115, 120, 125, 130],
112
- "S": [95, 100, 105, 110, 115],
113
- "SW": [110, 115, 120, 125, 130],
114
- "W": [120, 125, 130, 135, 140],
115
- "NW": [80, 85, 90, 95, 100],
116
- "HOR": [160, 170, 180, 190, 200]
117
- }, index=["24N", "32N", "40N", "48N", "56N"])
118
-
119
- # Create default CLF tables for internal loads
120
- # Columns are zone types, rows are hours (0-23)
121
- default_clf = pd.DataFrame({
122
- "A": [0.08, 0.06, 0.04, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
123
- "B": [0.15, 0.11, 0.08, 0.06, 0.04, 0.03, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
124
- "C": [0.24, 0.18, 0.14, 0.11, 0.08, 0.06, 0.05, 0.04, 0.03, 0.03, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
125
- "D": [0.40, 0.30, 0.23, 0.17, 0.13, 0.10, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02]
126
- }, index=range(24))
127
-
128
- self.clf_people = default_clf.copy()
129
- self.clf_lighting = default_clf.copy()
130
- self.clf_equipment = default_clf.copy()
131
-
132
- # Load correction factors
133
- self.color_correction = {
134
- "Dark": 0,
135
- "Medium": -2,
136
- "Light": -4
137
- }
138
-
139
- self.month_correction = {
140
- 1: -8, # January
141
- 2: -7, # February
142
- 3: -5, # March
143
- 4: -2, # April
144
- 5: 1, # May
145
- 6: 3, # June
146
- 7: 4, # July
147
- 8: 3, # August
148
- 9: 1, # September
149
- 10: -2, # October
150
- 11: -5, # November
151
- 12: -7 # December
152
- }
153
-
154
- self.latitude_correction = {
155
- "24N": {
156
- 1: -3, 2: -3, 3: -2, 4: 0, 5: 2, 6: 3, 7: 3, 8: 2, 9: 0, 10: -2, 11: -3, 12: -3
157
- },
158
- "32N": {
159
- 1: -2, 2: -2, 3: -1, 4: 0, 5: 1, 6: 2, 7: 2, 8: 1, 9: 0, 10: -1, 11: -2, 12: -2
160
- },
161
- "40N": {
162
- 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0
163
- },
164
- "48N": {
165
- 1: 2, 2: 2, 3: 1, 4: 0, 5: -1, 6: -2, 7: -2, 8: -1, 9: 0, 10: 1, 11: 2, 12: 2
166
- },
167
- "56N": {
168
- 1: 3, 2: 3, 3: 2, 4: 0, 5: -2, 6: -3, 7: -3, 8: -2, 9: 0, 10: 2, 11: 3, 12: 3
169
- }
170
- }
171
-
172
- def get_color_correction(self, color: str) -> float:
173
- """
174
- Get color correction factor.
175
-
176
- Args:
177
- color: Color of the surface (Dark, Medium, Light)
178
-
179
- Returns:
180
- Color correction factor
181
- """
182
- return self.color_correction.get(color, 0)
183
 
184
- def get_month_correction(self, month: int) -> float:
185
- """
186
- Get month correction factor.
187
-
188
- Args:
189
- month: Month (1-12)
190
-
191
- Returns:
192
- Month correction factor
193
- """
194
- return self.month_correction.get(month, 0)
195
-
196
- def get_latitude_correction(self, latitude: str, month: int) -> float:
197
- """
198
- Get latitude correction factor.
199
-
200
- Args:
201
- latitude: Latitude (24N, 32N, 40N, 48N, 56N)
202
- month: Month (1-12)
203
-
204
- Returns:
205
- Latitude correction factor
206
- """
207
- if latitude in self.latitude_correction:
208
- return self.latitude_correction[latitude].get(month, 0)
209
  return 0
210
 
211
- def get_cltd_wall(self, wall_group: str, orientation: str, hour: int) -> float:
212
- """
213
- Get CLTD value for a wall.
214
-
215
- Args:
216
- wall_group: Wall group (A-H)
217
- orientation: Wall orientation (N, NE, E, SE, S, SW, W, NW)
218
- hour: Hour of the day (0-23)
219
-
220
- Returns:
221
- CLTD value for the specified wall and hour
222
- """
223
- # Handle custom wall group by using group D (medium construction) as default
224
- if wall_group not in self.cltd_wall:
225
- if wall_group == "Custom":
226
- wall_group = "D" # Use group D (medium construction) for custom walls
227
- else:
228
- raise ValueError(f"Invalid wall group: {wall_group}")
229
-
230
- # Convert orientation to abbreviation if needed
231
- orientation_map = {
232
- "North": "N", "Northeast": "NE", "East": "E", "Southeast": "SE",
233
- "South": "S", "Southwest": "SW", "West": "W", "Northwest": "NW"
234
- }
235
- orientation_abbr = orientation_map.get(orientation, orientation)
236
-
237
- if orientation_abbr not in self.cltd_wall[wall_group].columns:
238
- raise ValueError(f"Invalid orientation: {orientation}")
239
-
240
- if hour < 0 or hour > 23:
241
- raise ValueError(f"Invalid hour: {hour}")
242
-
243
- # Get CLTD value
244
- return self.cltd_wall[wall_group].loc[hour, orientation_abbr]
245
-
246
- def get_cltd_roof(self, roof_group: str, hour: int) -> float:
247
- """
248
- Get CLTD value for a roof.
249
-
250
- Args:
251
- roof_group: Roof group (A-G)
252
- hour: Hour of the day (0-23)
253
-
254
- Returns:
255
- CLTD value for the specified roof and hour
256
- """
257
- # Handle custom roof group by using group C (medium construction) as default
258
- if roof_group not in self.cltd_roof:
259
- if roof_group == "Custom":
260
- roof_group = "C" # Use group C (medium construction) for custom roofs
261
- else:
262
- raise ValueError(f"Invalid roof group: {roof_group}")
263
-
264
- if hour < 0 or hour > 23:
265
- raise ValueError(f"Invalid hour: {hour}")
266
-
267
- # Get CLTD value
268
- return self.cltd_roof[roof_group].loc[hour, "HOR"]
269
-
270
- def get_scl_glass(self, orientation: str, latitude: str, month: int) -> float:
271
- """
272
- Get SCL value for glass.
273
-
274
- Args:
275
- orientation: Glass orientation (N, NE, E, SE, S, SW, W, NW, HOR)
276
- latitude: Latitude (24N, 32N, 40N, 48N, 56N)
277
- month: Month (1-12)
278
-
279
- Returns:
280
- SCL value for the specified glass
281
- """
282
- # Validate inputs
283
- if orientation not in self.scl_glass.columns:
284
- raise ValueError(f"Invalid orientation: {orientation}")
285
-
286
- if latitude not in self.scl_glass.index:
287
- # Default to 40N if latitude not found
288
- latitude = "40N"
289
-
290
- # Get SCL value
291
- scl = self.scl_glass.loc[latitude, orientation]
292
-
293
- # Apply month correction
294
- month_correction = self.get_month_correction(month)
295
-
296
- return scl + month_correction
297
-
298
- def get_scl(self, orientation: str, hour: int, latitude: str, month: int = 7) -> float:
299
- """
300
- Get SCL value for glass (compatibility method).
301
-
302
- Args:
303
- orientation: Glass orientation (N, NE, E, SE, S, SW, W, NW, HOR)
304
- hour: Hour of the day (0-23) - not used in this implementation
305
- latitude: Latitude (24N, 32N, 40N, 48N, 56N)
306
- month: Month (1-12), defaults to July (7)
307
-
308
- Returns:
309
- SCL value for the specified glass
310
- """
311
- # This is a compatibility method that calls get_scl_glass
312
- # The hour parameter is ignored as SCL values are daily maximums
313
- return self.get_scl_glass(orientation, latitude, month)
314
-
315
- def get_clf_people(self, hour: int, zone_type: str) -> float:
316
- """
317
- Get CLF value for people.
318
-
319
- Args:
320
- hour: Hour of the day (0-23)
321
- zone_type: Zone type (A-D)
322
-
323
- Returns:
324
- CLF value for people at the specified hour and zone type
325
- """
326
- # Validate inputs
327
- if zone_type not in self.clf_people.columns:
328
- # Default to zone type B if not found
329
- zone_type = "B"
330
-
331
- if hour < 0 or hour > 23:
332
- raise ValueError(f"Invalid hour: {hour}")
333
-
334
- # Get CLF value
335
- return self.clf_people.loc[hour, zone_type]
336
 
337
- def get_clf_lighting(self, hour: int, zone_type: str) -> float:
338
- """
339
- Get CLF value for lighting.
340
-
341
- Args:
342
- hour: Hour of the day (0-23)
343
- zone_type: Zone type (A-D)
344
-
345
- Returns:
346
- CLF value for lighting at the specified hour and zone type
347
- """
348
- # Validate inputs
349
- if zone_type not in self.clf_lighting.columns:
350
- # Default to zone type B if not found
351
- zone_type = "B"
352
-
353
- if hour < 0 or hour > 23:
354
- raise ValueError(f"Invalid hour: {hour}")
355
-
356
- # Get CLF value
357
- return self.clf_lighting.loc[hour, zone_type]
358
 
359
- def get_clf_equipment(self, hour: int, zone_type: str) -> float:
360
- """
361
- Get CLF value for equipment.
362
-
363
- Args:
364
- hour: Hour of the day (0-23)
365
- zone_type: Zone type (A-D)
366
-
367
- Returns:
368
- CLF value for equipment at the specified hour and zone type
369
- """
370
- # Validate inputs
371
- if zone_type not in self.clf_equipment.columns:
372
- # Default to zone type B if not found
373
- zone_type = "B"
374
-
375
- if hour < 0 or hour > 23:
376
- raise ValueError(f"Invalid hour: {hour}")
377
-
378
- # Get CLF value
379
- return self.clf_equipment.loc[hour, zone_type]
380
 
381
- def calculate_corrected_cltd_wall(self, wall_group: str, orientation: str, hour: int,
382
- color: str, month: int, latitude: str,
383
- indoor_temp: float, outdoor_temp: float) -> float:
384
- """
385
- Calculate corrected CLTD value for a wall.
386
-
387
- Args:
388
- wall_group: Wall group (A-H)
389
- orientation: Wall orientation (N, NE, E, SE, S, SW, W, NW)
390
- hour: Hour of the day (0-23)
391
- color: Color of the wall (Dark, Medium, Light)
392
- month: Month (1-12)
393
- latitude: Latitude (24N, 32N, 40N, 48N, 56N)
394
- indoor_temp: Indoor design temperature (°C)
395
- outdoor_temp: Outdoor design temperature (°C)
396
-
397
- Returns:
398
- Corrected CLTD value for the specified wall and hour
399
- """
400
- # Get base CLTD value
401
- cltd = self.get_cltd_wall(wall_group, orientation, hour)
402
-
403
- # Apply corrections
404
- color_correction = self.get_color_correction(color)
405
- month_correction = self.get_month_correction(month)
406
- latitude_correction = self.get_latitude_correction(latitude, month)
407
-
408
- # Temperature correction
409
- temp_correction = (indoor_temp - 25.5) + (outdoor_temp - 35.0)
410
-
411
- # Calculate corrected CLTD
412
- corrected_cltd = cltd + color_correction + month_correction + latitude_correction + temp_correction
413
-
414
- # Ensure CLTD is not negative
415
- return max(0, corrected_cltd)
416
 
417
- def calculate_corrected_cltd_roof(self, roof_group: str, hour: int,
418
- color: str, month: int, latitude: str,
419
- indoor_temp: float, outdoor_temp: float) -> float:
420
- """
421
- Calculate corrected CLTD value for a roof.
422
-
423
- Args:
424
- roof_group: Roof group (A-G)
425
- hour: Hour of the day (0-23)
426
- color: Color of the roof (Dark, Medium, Light)
427
- month: Month (1-12)
428
- latitude: Latitude (24N, 32N, 40N, 48N, 56N)
429
- indoor_temp: Indoor design temperature (°C)
430
- outdoor_temp: Outdoor design temperature (°C)
431
-
432
- Returns:
433
- Corrected CLTD value for the specified roof and hour
434
- """
435
- # Get base CLTD value
436
- cltd = self.get_cltd_roof(roof_group, hour)
437
-
438
- # Apply corrections
439
- color_correction = self.get_color_correction(color)
440
- month_correction = self.get_month_correction(month)
441
- latitude_correction = self.get_latitude_correction(latitude, month)
442
-
443
- # Temperature correction
444
- temp_correction = (indoor_temp - 25.5) + (outdoor_temp - 35.0)
445
-
446
- # Calculate corrected CLTD
447
- corrected_cltd = cltd + color_correction + month_correction + latitude_correction + temp_correction
448
-
449
- # Ensure CLTD is not negative
450
- return max(0, corrected_cltd)
451
 
452
- # Create an instance of ASHRAETables for other modules to import
453
  ashrae_tables = ASHRAETables()
 
1
  """
2
+ ASHRAETables class for the file upload version
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ This file contains a minimal implementation of the ASHRAETables class
5
+ needed for the HVAC Calculator File Upload application to work.
6
+ """
7
 
8
  class ASHRAETables:
9
+ """Simplified ASHRAETables class for the file upload version."""
10
 
11
  def __init__(self):
12
+ """Initialize the ASHRAETables class."""
13
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ def get_cltd_wall(self, wall_group, orientation, hour):
16
+ """Placeholder for the actual CLTD wall method."""
17
+ # This method is not actually used in the file upload version
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  return 0
19
 
20
+ def get_cltd_roof(self, roof_group, hour):
21
+ """Placeholder for the actual CLTD roof method."""
22
+ # This method is not actually used in the file upload version
23
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ def get_scl_glass(self, orientation, hour, latitude):
26
+ """Placeholder for the actual SCL glass method."""
27
+ # This method is not actually used in the file upload version
28
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ def get_scl(self, orientation, hour, latitude):
31
+ """Placeholder for the actual SCL method."""
32
+ # This method is not actually used in the file upload version
33
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ def calculate_corrected_cltd_wall(self, wall_group, orientation, hour,
36
+ outdoor_temp, indoor_temp, latitude, month):
37
+ """Placeholder for the actual corrected CLTD wall method."""
38
+ # This method is not actually used in the file upload version
39
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ def calculate_corrected_cltd_roof(self, roof_group, hour,
42
+ outdoor_temp, indoor_temp, latitude, month):
43
+ """Placeholder for the actual corrected CLTD roof method."""
44
+ # This method is not actually used in the file upload version
45
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ # Create an instance for other modules to import
48
  ashrae_tables = ASHRAETables()
hvac_calculator_file_upload.py ADDED
@@ -0,0 +1,1062 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HVAC Calculator with File Upload Interface
3
+
4
+ This module contains a simplified Streamlit application for the HVAC Calculator
5
+ that uses an Excel file upload approach instead of manual data entry.
6
+
7
+ Author: Manus AI
8
+ Date: March 2025
9
+ Version: 2.0.0
10
+ """
11
+
12
+ import streamlit as st
13
+ import pandas as pd
14
+ import numpy as np
15
+ import plotly.graph_objects as go
16
+ import plotly.express as px
17
+ import matplotlib.pyplot as plt
18
+ import io
19
+ import os
20
+ import sys
21
+ from typing import Dict, List, Any, Optional, Tuple
22
+
23
+ # Import utility modules
24
+ from utils.cooling_load import CoolingLoad
25
+ from utils.heating_load import HeatingLoad
26
+ from utils.psychrometrics import Psychrometrics
27
+ from utils.psychrometric_visualization import PsychrometricVisualization
28
+ from data.ashrae_tables import ASHRAETables
29
+
30
+ class HVACCalculatorFileUpload:
31
+ """HVAC Calculator application with file upload interface."""
32
+
33
+ def __init__(self):
34
+ """Initialize the HVAC Calculator application."""
35
+ # Set page configuration
36
+ st.set_page_config(
37
+ page_title="HVAC Load Calculator",
38
+ page_icon="🌡️",
39
+ layout="wide",
40
+ initial_sidebar_state="expanded"
41
+ )
42
+
43
+ # Initialize session state
44
+ self._initialize_session_state()
45
+
46
+ # Initialize utility modules
47
+ self._initialize_modules()
48
+
49
+ # Setup application layout
50
+ self.setup_layout()
51
+
52
+ def _initialize_session_state(self):
53
+ """Initialize Streamlit session state variables."""
54
+ # Initialize page navigation
55
+ if "page" not in st.session_state:
56
+ st.session_state.page = "upload"
57
+
58
+ # Initialize data storage
59
+ if "uploaded_data" not in st.session_state:
60
+ st.session_state.uploaded_data = None
61
+
62
+ # Initialize calculation results
63
+ if "calculation_results" not in st.session_state:
64
+ st.session_state.calculation_results = {
65
+ "cooling_load": {},
66
+ "heating_load": {},
67
+ "psychrometrics": {}
68
+ }
69
+
70
+ def _initialize_modules(self):
71
+ """Initialize utility modules."""
72
+ self.cooling_load = CoolingLoad()
73
+ self.heating_load = HeatingLoad()
74
+ self.psychrometrics = Psychrometrics()
75
+ self.psychrometric_visualization = PsychrometricVisualization()
76
+ self.ashrae_tables = ASHRAETables()
77
+
78
+ def setup_layout(self):
79
+ """Setup the application layout."""
80
+ # Display header
81
+ st.title("HVAC Load Calculator")
82
+ st.markdown("A simplified tool for calculating heating and cooling loads using ASHRAE methods.")
83
+
84
+ # Setup sidebar
85
+ self.setup_sidebar()
86
+
87
+ # Display current page
88
+ if st.session_state.page == "upload":
89
+ self.display_upload_page()
90
+ elif st.session_state.page == "results":
91
+ self.display_results_page()
92
+ elif st.session_state.page == "data":
93
+ self.display_data_page()
94
+
95
+ def setup_sidebar(self):
96
+ """Setup the application sidebar."""
97
+ with st.sidebar:
98
+ st.header("Navigation")
99
+
100
+ # Navigation buttons
101
+ if st.button("Upload Data", key="nav_upload"):
102
+ st.session_state.page = "upload"
103
+
104
+ if st.button("View Results", key="nav_results"):
105
+ if st.session_state.uploaded_data is None:
106
+ st.error("Please upload data first.")
107
+ else:
108
+ st.session_state.page = "results"
109
+
110
+ if st.button("View Uploaded Data", key="nav_data"):
111
+ if st.session_state.uploaded_data is None:
112
+ st.error("Please upload data first.")
113
+ else:
114
+ st.session_state.page = "data"
115
+
116
+ st.markdown("---")
117
+
118
+ # Run calculations button
119
+ if st.button("Run Calculations", key="run_calculations"):
120
+ if st.session_state.uploaded_data is None:
121
+ st.error("Please upload data first.")
122
+ else:
123
+ self.run_calculations()
124
+
125
+ st.markdown("---")
126
+
127
+ # Download template button
128
+ if st.download_button(
129
+ label="Download Template",
130
+ data=open("hvac_calculator_template.xlsx", "rb").read(),
131
+ file_name="hvac_calculator_template.xlsx",
132
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
133
+ ):
134
+ st.success("Template downloaded successfully!")
135
+
136
+ st.markdown("---")
137
+
138
+ # Display application info
139
+ st.subheader("About")
140
+ st.write("HVAC Load Calculator v2.0.0")
141
+ st.write("© 2025 Manus AI")
142
+
143
+ def display_upload_page(self):
144
+ """Display the file upload interface."""
145
+ st.header("Upload Data")
146
+
147
+ st.write("""
148
+ Please upload your HVAC data using the Excel template. If you don't have the template,
149
+ you can download it using the 'Download Template' button in the sidebar.
150
+ """)
151
+
152
+ uploaded_file = st.file_uploader(
153
+ "Upload your Excel file:",
154
+ type=["xlsx"],
155
+ help="Upload the filled HVAC calculator template Excel file."
156
+ )
157
+
158
+ if uploaded_file is not None:
159
+ try:
160
+ # Process the uploaded file
161
+ data = self.process_uploaded_file(uploaded_file)
162
+
163
+ # Store the processed data in session state
164
+ st.session_state.uploaded_data = data
165
+
166
+ # Display success message
167
+ st.success("File uploaded and processed successfully!")
168
+
169
+ # Add a button to view the data
170
+ if st.button("View Uploaded Data"):
171
+ st.session_state.page = "data"
172
+
173
+ # Add a button to run calculations
174
+ if st.button("Run Calculations Now"):
175
+ self.run_calculations()
176
+ st.session_state.page = "results"
177
+
178
+ except Exception as e:
179
+ st.error(f"Error processing the uploaded file: {str(e)}")
180
+ st.error("Please make sure you're using the correct template format.")
181
+
182
+ def process_uploaded_file(self, uploaded_file) -> Dict[str, Any]:
183
+ """
184
+ Process the uploaded Excel file and extract the data.
185
+
186
+ Args:
187
+ uploaded_file: The uploaded Excel file
188
+
189
+ Returns:
190
+ Dictionary containing the extracted data
191
+ """
192
+ # Read the Excel file
193
+ xls = pd.ExcelFile(uploaded_file)
194
+
195
+ # Initialize data dictionary
196
+ data = {
197
+ "building_info": {},
198
+ "components": {
199
+ "walls": [],
200
+ "windows": [],
201
+ "doors": [],
202
+ "roofs": [],
203
+ "floors": []
204
+ },
205
+ "internal_loads": {
206
+ "people": [],
207
+ "lighting": [],
208
+ "equipment": []
209
+ }
210
+ }
211
+
212
+ # Process Building Information sheet
213
+ building_info_df = pd.read_excel(xls, "Building Information", header=0)
214
+ for _, row in building_info_df.iterrows():
215
+ param = row["Parameter*"]
216
+ value = row["Value*"]
217
+ if pd.notna(param) and pd.notna(value):
218
+ # Convert parameter name to snake_case for consistency
219
+ param_key = param.lower().replace(" ", "_")
220
+ data["building_info"][param_key] = value
221
+
222
+ # Process Walls sheet
223
+ walls_df = pd.read_excel(xls, "Walls", header=0)
224
+ for _, row in walls_df.iterrows():
225
+ if pd.notna(row["Wall Name*"]) and pd.notna(row["Orientation*"]):
226
+ wall = {
227
+ "name": row["Wall Name*"],
228
+ "orientation": row["Orientation*"],
229
+ "height": row["Height (m)*"],
230
+ "width": row["Width (m)*"],
231
+ "area": row["Area (m²"] if pd.notna(row["Area (m²"]) else row["Height (m)*"] * row["Width (m)*"],
232
+ "u_value": row["U-Value (W/m²K)*"],
233
+ "wall_group": row["Wall Group*"],
234
+ "notes": row["Notes"] if pd.notna(row["Notes"]) else ""
235
+ }
236
+ data["components"]["walls"].append(wall)
237
+
238
+ # Process Windows sheet
239
+ windows_df = pd.read_excel(xls, "Windows", header=0)
240
+ for _, row in windows_df.iterrows():
241
+ if pd.notna(row["Window Name*"]) and pd.notna(row["Orientation*"]):
242
+ has_shading = row["Has Shading"] == "Yes" if pd.notna(row["Has Shading"]) else False
243
+ window = {
244
+ "name": row["Window Name*"],
245
+ "orientation": row["Orientation*"],
246
+ "height": row["Height (m)*"],
247
+ "width": row["Width (m)*"],
248
+ "area": row["Area (m²"] if pd.notna(row["Area (m²"]) else row["Height (m)*"] * row["Width (m)*"],
249
+ "u_value": row["U-Value (W/m²K)*"],
250
+ "shgc": row["SHGC*"],
251
+ "has_shading": has_shading,
252
+ "shading_type": row["Shading Type"] if has_shading and pd.notna(row["Shading Type"]) else None,
253
+ "shading_coefficient": row["Shading Coefficient"] if has_shading and pd.notna(row["Shading Coefficient"]) else 1.0,
254
+ "notes": row["Notes"] if pd.notna(row["Notes"]) else ""
255
+ }
256
+ data["components"]["windows"].append(window)
257
+
258
+ # Process Doors sheet
259
+ doors_df = pd.read_excel(xls, "Doors", header=0)
260
+ for _, row in doors_df.iterrows():
261
+ if pd.notna(row["Door Name*"]) and pd.notna(row["Orientation*"]):
262
+ door = {
263
+ "name": row["Door Name*"],
264
+ "orientation": row["Orientation*"],
265
+ "height": row["Height (m)*"],
266
+ "width": row["Width (m)*"],
267
+ "area": row["Area (m²"] if pd.notna(row["Area (m²"]) else row["Height (m)*"] * row["Width (m)*"],
268
+ "u_value": row["U-Value (W/m²K)*"],
269
+ "notes": row["Notes"] if pd.notna(row["Notes"]) else ""
270
+ }
271
+ data["components"]["doors"].append(door)
272
+
273
+ # Process Roof and Floor sheet
274
+ roof_floor_df = pd.read_excel(xls, "Roof and Floor", header=0)
275
+ for _, row in roof_floor_df.iterrows():
276
+ if pd.notna(row["Component*"]) and pd.notna(row["Area (m²)*"]):
277
+ component = {
278
+ "name": row["Component*"],
279
+ "area": row["Area (m²)*"],
280
+ "u_value": row["U-Value (W/m²K)*"],
281
+ "group": row["Group*"] if row["Component*"] == "Roof" else None,
282
+ "notes": row["Notes"] if pd.notna(row["Notes"]) else ""
283
+ }
284
+
285
+ if row["Component*"] == "Roof":
286
+ data["components"]["roofs"].append(component)
287
+ elif row["Component*"] == "Floor":
288
+ data["components"]["floors"].append(component)
289
+
290
+ # Process Internal Loads sheet
291
+ internal_loads_df = pd.read_excel(xls, "Internal Loads", header=None)
292
+
293
+ # Find the sections for people, lighting, and equipment
294
+ people_start = internal_loads_df[internal_loads_df[0] == "People Loads"].index[0]
295
+ lighting_start = internal_loads_df[internal_loads_df[0] == "Lighting Loads"].index[0]
296
+ equipment_start = internal_loads_df[internal_loads_df[0] == "Equipment Loads"].index[0]
297
+
298
+ # Process People Loads
299
+ people_df = pd.read_excel(xls, "Internal Loads", header=people_start+1, nrows=lighting_start-people_start-3)
300
+ for _, row in people_df.iterrows():
301
+ if pd.notna(row["Zone Name*"]) and pd.notna(row["Number of People*"]):
302
+ people_load = {
303
+ "zone": row["Zone Name*"],
304
+ "count": row["Number of People*"],
305
+ "activity": row["Activity Level*"],
306
+ "sensible_heat": row["Sensible Heat (W/person)*"],
307
+ "latent_heat": row["Latent Heat (W/person)*"],
308
+ "schedule": row["Schedule*"],
309
+ "notes": row["Notes"] if pd.notna(row["Notes"]) else "",
310
+ "total_sensible": row["Number of People*"] * row["Sensible Heat (W/person)*"],
311
+ "total_latent": row["Number of People*"] * row["Latent Heat (W/person)*"]
312
+ }
313
+ data["internal_loads"]["people"].append(people_load)
314
+
315
+ # Process Lighting Loads
316
+ lighting_df = pd.read_excel(xls, "Internal Loads", header=lighting_start+1, nrows=equipment_start-lighting_start-3)
317
+ for _, row in lighting_df.iterrows():
318
+ if pd.notna(row["Zone Name*"]) and pd.notna(row["Total Power (W)*"]):
319
+ power_density = row["Power Density (W/m²)"] if pd.notna(row["Power Density (W/m²)"]) else row["Total Power (W)*"] / row["Area (m²)*"]
320
+ lighting_load = {
321
+ "zone": row["Zone Name*"],
322
+ "type": row["Lighting Type*"],
323
+ "power": row["Total Power (W)*"],
324
+ "area": row["Area (m²)*"],
325
+ "power_density": power_density,
326
+ "schedule": row["Schedule*"],
327
+ "notes": row["Notes"] if pd.notna(row["Notes"]) else ""
328
+ }
329
+ data["internal_loads"]["lighting"].append(lighting_load)
330
+
331
+ # Process Equipment Loads
332
+ equipment_df = pd.read_excel(xls, "Internal Loads", header=equipment_start+1)
333
+ for _, row in equipment_df.iterrows():
334
+ if pd.notna(row["Zone Name*"]) and pd.notna(row["Sensible Heat (W)*"]):
335
+ equipment_load = {
336
+ "zone": row["Zone Name*"],
337
+ "type": row["Equipment Type*"],
338
+ "sensible_heat": row["Sensible Heat (W)*"],
339
+ "latent_heat": row["Latent Heat (W)*"],
340
+ "schedule": row["Schedule*"],
341
+ "notes": row["Notes"] if pd.notna(row["Notes"]) else ""
342
+ }
343
+ data["internal_loads"]["equipment"].append(equipment_load)
344
+
345
+ return data
346
+
347
+ def display_data_page(self):
348
+ """Display the uploaded data."""
349
+ if st.session_state.uploaded_data is None:
350
+ st.error("No data has been uploaded. Please upload data first.")
351
+ return
352
+
353
+ st.header("Uploaded Data")
354
+
355
+ # Create tabs for different data sections
356
+ tabs = st.tabs([
357
+ "Building Info", "Walls", "Windows", "Doors",
358
+ "Roof & Floor", "People", "Lighting", "Equipment"
359
+ ])
360
+
361
+ # Display Building Information
362
+ with tabs[0]:
363
+ st.subheader("Building Information")
364
+ building_info = st.session_state.uploaded_data["building_info"]
365
+ building_info_df = pd.DataFrame({
366
+ "Parameter": building_info.keys(),
367
+ "Value": building_info.values()
368
+ })
369
+ st.dataframe(building_info_df, use_container_width=True)
370
+
371
+ # Display Walls
372
+ with tabs[1]:
373
+ st.subheader("Walls")
374
+ walls = st.session_state.uploaded_data["components"]["walls"]
375
+ if walls:
376
+ walls_df = pd.DataFrame(walls)
377
+ st.dataframe(walls_df, use_container_width=True)
378
+ else:
379
+ st.info("No wall data available.")
380
+
381
+ # Display Windows
382
+ with tabs[2]:
383
+ st.subheader("Windows")
384
+ windows = st.session_state.uploaded_data["components"]["windows"]
385
+ if windows:
386
+ windows_df = pd.DataFrame(windows)
387
+ st.dataframe(windows_df, use_container_width=True)
388
+ else:
389
+ st.info("No window data available.")
390
+
391
+ # Display Doors
392
+ with tabs[3]:
393
+ st.subheader("Doors")
394
+ doors = st.session_state.uploaded_data["components"]["doors"]
395
+ if doors:
396
+ doors_df = pd.DataFrame(doors)
397
+ st.dataframe(doors_df, use_container_width=True)
398
+ else:
399
+ st.info("No door data available.")
400
+
401
+ # Display Roof and Floor
402
+ with tabs[4]:
403
+ st.subheader("Roof and Floor")
404
+ roofs = st.session_state.uploaded_data["components"]["roofs"]
405
+ floors = st.session_state.uploaded_data["components"]["floors"]
406
+
407
+ if roofs:
408
+ st.write("Roof Components:")
409
+ roofs_df = pd.DataFrame(roofs)
410
+ st.dataframe(roofs_df, use_container_width=True)
411
+ else:
412
+ st.info("No roof data available.")
413
+
414
+ if floors:
415
+ st.write("Floor Components:")
416
+ floors_df = pd.DataFrame(floors)
417
+ st.dataframe(floors_df, use_container_width=True)
418
+ else:
419
+ st.info("No floor data available.")
420
+
421
+ # Display People Loads
422
+ with tabs[5]:
423
+ st.subheader("People Loads")
424
+ people = st.session_state.uploaded_data["internal_loads"]["people"]
425
+ if people:
426
+ people_df = pd.DataFrame(people)
427
+ st.dataframe(people_df, use_container_width=True)
428
+ else:
429
+ st.info("No people load data available.")
430
+
431
+ # Display Lighting Loads
432
+ with tabs[6]:
433
+ st.subheader("Lighting Loads")
434
+ lighting = st.session_state.uploaded_data["internal_loads"]["lighting"]
435
+ if lighting:
436
+ lighting_df = pd.DataFrame(lighting)
437
+ st.dataframe(lighting_df, use_container_width=True)
438
+ else:
439
+ st.info("No lighting load data available.")
440
+
441
+ # Display Equipment Loads
442
+ with tabs[7]:
443
+ st.subheader("Equipment Loads")
444
+ equipment = st.session_state.uploaded_data["internal_loads"]["equipment"]
445
+ if equipment:
446
+ equipment_df = pd.DataFrame(equipment)
447
+ st.dataframe(equipment_df, use_container_width=True)
448
+ else:
449
+ st.info("No equipment load data available.")
450
+
451
+ def display_results_page(self):
452
+ """Display the calculation results."""
453
+ if st.session_state.uploaded_data is None:
454
+ st.error("No data has been uploaded. Please upload data first.")
455
+ return
456
+
457
+ if not st.session_state.calculation_results["cooling_load"]:
458
+ st.warning("Calculations have not been run yet. Please run calculations first.")
459
+ if st.button("Run Calculations Now"):
460
+ self.run_calculations()
461
+ return
462
+
463
+ st.header("Calculation Results")
464
+
465
+ # Create tabs for different result sections
466
+ tabs = st.tabs([
467
+ "Cooling Load", "Heating Load", "Psychrometric Analysis", "Load Breakdown"
468
+ ])
469
+
470
+ # Display Cooling Load Results
471
+ with tabs[0]:
472
+ st.subheader("Cooling Load Results")
473
+
474
+ cooling_results = st.session_state.calculation_results["cooling_load"]
475
+
476
+ # Display total cooling load
477
+ col1, col2, col3 = st.columns(3)
478
+ with col1:
479
+ st.metric(
480
+ label="Total Cooling Load",
481
+ value=f"{cooling_results.get('total_load', 0):.2f} kW"
482
+ )
483
+
484
+ with col2:
485
+ st.metric(
486
+ label="Sensible Cooling Load",
487
+ value=f"{cooling_results.get('sensible_load', 0):.2f} kW"
488
+ )
489
+
490
+ with col3:
491
+ st.metric(
492
+ label="Latent Cooling Load",
493
+ value=f"{cooling_results.get('latent_load', 0):.2f} kW"
494
+ )
495
+
496
+ # Display cooling load per area
497
+ st.metric(
498
+ label="Cooling Load per Area",
499
+ value=f"{cooling_results.get('load_per_area', 0):.2f} W/m²"
500
+ )
501
+
502
+ # Display cooling load breakdown
503
+ if "component_loads" in cooling_results:
504
+ st.subheader("Cooling Load Breakdown")
505
+
506
+ component_loads = cooling_results["component_loads"]
507
+
508
+ # Create a DataFrame for the component loads
509
+ load_data = []
510
+ for component, load in component_loads.items():
511
+ load_data.append({
512
+ "Component": component.capitalize(),
513
+ "Load (kW)": load,
514
+ "Percentage": load / cooling_results["total_load"] * 100 if cooling_results["total_load"] > 0 else 0
515
+ })
516
+
517
+ load_df = pd.DataFrame(load_data)
518
+
519
+ # Display the load breakdown table
520
+ st.dataframe(load_df, use_container_width=True)
521
+
522
+ # Create a pie chart for the load breakdown
523
+ fig = px.pie(
524
+ load_df,
525
+ values="Load (kW)",
526
+ names="Component",
527
+ title="Cooling Load Distribution"
528
+ )
529
+ st.plotly_chart(fig, use_container_width=True)
530
+
531
+ # Display Heating Load Results
532
+ with tabs[1]:
533
+ st.subheader("Heating Load Results")
534
+
535
+ heating_results = st.session_state.calculation_results["heating_load"]
536
+
537
+ # Display total heating load
538
+ col1, col2 = st.columns(2)
539
+ with col1:
540
+ st.metric(
541
+ label="Total Heating Load",
542
+ value=f"{heating_results.get('total_load', 0):.2f} kW"
543
+ )
544
+
545
+ with col2:
546
+ st.metric(
547
+ label="Heating Load per Area",
548
+ value=f"{heating_results.get('load_per_area', 0):.2f} W/m²"
549
+ )
550
+
551
+ # Display design heat loss
552
+ col1, col2 = st.columns(2)
553
+ with col1:
554
+ st.metric(
555
+ label="Design Heat Loss",
556
+ value=f"{heating_results.get('design_heat_loss', 0):.2f} kW"
557
+ )
558
+
559
+ with col2:
560
+ st.metric(
561
+ label="Safety Factor",
562
+ value=f"{heating_results.get('safety_factor', 0):.2f} %"
563
+ )
564
+
565
+ # Display heating load breakdown
566
+ if "component_loads" in heating_results:
567
+ st.subheader("Heating Load Breakdown")
568
+
569
+ component_loads = heating_results["component_loads"]
570
+
571
+ # Create a DataFrame for the component loads
572
+ load_data = []
573
+ for component, load in component_loads.items():
574
+ load_data.append({
575
+ "Component": component.capitalize(),
576
+ "Load (kW)": load,
577
+ "Percentage": load / heating_results["total_load"] * 100 if heating_results["total_load"] > 0 else 0
578
+ })
579
+
580
+ load_df = pd.DataFrame(load_data)
581
+
582
+ # Display the load breakdown table
583
+ st.dataframe(load_df, use_container_width=True)
584
+
585
+ # Create a pie chart for the load breakdown
586
+ fig = px.pie(
587
+ load_df,
588
+ values="Load (kW)",
589
+ names="Component",
590
+ title="Heating Load Distribution"
591
+ )
592
+ st.plotly_chart(fig, use_container_width=True)
593
+
594
+ # Display Psychrometric Analysis
595
+ with tabs[2]:
596
+ st.subheader("Psychrometric Analysis")
597
+
598
+ psychrometric_results = st.session_state.calculation_results["psychrometrics"]
599
+
600
+ # Display psychrometric properties
601
+ col1, col2 = st.columns(2)
602
+
603
+ with col1:
604
+ st.subheader("Outdoor Conditions")
605
+ outdoor = psychrometric_results.get("outdoor", {})
606
+
607
+ st.metric(
608
+ label="Dry Bulb Temperature",
609
+ value=f"{outdoor.get('dry_bulb', 0):.1f} °C"
610
+ )
611
+
612
+ st.metric(
613
+ label="Relative Humidity",
614
+ value=f"{outdoor.get('relative_humidity', 0):.1f} %"
615
+ )
616
+
617
+ st.metric(
618
+ label="Humidity Ratio",
619
+ value=f"{outdoor.get('humidity_ratio', 0):.4f} kg/kg"
620
+ )
621
+
622
+ st.metric(
623
+ label="Enthalpy",
624
+ value=f"{outdoor.get('enthalpy', 0):.1f} kJ/kg"
625
+ )
626
+
627
+ with col2:
628
+ st.subheader("Indoor Conditions")
629
+ indoor = psychrometric_results.get("indoor", {})
630
+
631
+ st.metric(
632
+ label="Dry Bulb Temperature",
633
+ value=f"{indoor.get('dry_bulb', 0):.1f} °C"
634
+ )
635
+
636
+ st.metric(
637
+ label="Relative Humidity",
638
+ value=f"{indoor.get('relative_humidity', 0):.1f} %"
639
+ )
640
+
641
+ st.metric(
642
+ label="Humidity Ratio",
643
+ value=f"{indoor.get('humidity_ratio', 0):.4f} kg/kg"
644
+ )
645
+
646
+ st.metric(
647
+ label="Enthalpy",
648
+ value=f"{indoor.get('enthalpy', 0):.1f} kJ/kg"
649
+ )
650
+
651
+ # Display psychrometric chart
652
+ st.subheader("Psychrometric Chart")
653
+
654
+ # Create a placeholder for the psychrometric chart
655
+ # In a real implementation, this would use the PsychrometricVisualization class
656
+ st.info("Psychrometric chart visualization would be displayed here.")
657
+
658
+ # Display Load Breakdown
659
+ with tabs[3]:
660
+ st.subheader("Load Breakdown by Component")
661
+
662
+ cooling_results = st.session_state.calculation_results["cooling_load"]
663
+ heating_results = st.session_state.calculation_results["heating_load"]
664
+
665
+ # Create a DataFrame for the component loads
666
+ if "component_loads" in cooling_results and "component_loads" in heating_results:
667
+ cooling_loads = cooling_results["component_loads"]
668
+ heating_loads = heating_results["component_loads"]
669
+
670
+ # Combine the component loads
671
+ components = set(list(cooling_loads.keys()) + list(heating_loads.keys()))
672
+
673
+ load_data = []
674
+ for component in components:
675
+ load_data.append({
676
+ "Component": component.capitalize(),
677
+ "Cooling Load (kW)": cooling_loads.get(component, 0),
678
+ "Heating Load (kW)": heating_loads.get(component, 0)
679
+ })
680
+
681
+ load_df = pd.DataFrame(load_data)
682
+
683
+ # Display the load breakdown table
684
+ st.dataframe(load_df, use_container_width=True)
685
+
686
+ # Create a bar chart for the load comparison
687
+ fig = px.bar(
688
+ load_df,
689
+ x="Component",
690
+ y=["Cooling Load (kW)", "Heating Load (kW)"],
691
+ title="Cooling and Heating Load Comparison",
692
+ barmode="group"
693
+ )
694
+ st.plotly_chart(fig, use_container_width=True)
695
+
696
+ def run_calculations(self):
697
+ """Run HVAC load calculations based on the uploaded data."""
698
+ if st.session_state.uploaded_data is None:
699
+ st.error("No data has been uploaded. Please upload data first.")
700
+ return
701
+
702
+ try:
703
+ # Get input data from uploaded data
704
+ data = st.session_state.uploaded_data
705
+ building_info = data["building_info"]
706
+ components = data["components"]
707
+ internal_loads = data["internal_loads"]
708
+
709
+ # Get climate data
710
+ climate_data = {
711
+ "location": building_info.get("location", ""),
712
+ "outdoor_temp": building_info.get("summer_outdoor_temperature", 35.0),
713
+ "outdoor_humidity": building_info.get("summer_outdoor_humidity", 50.0),
714
+ "indoor_temp": building_info.get("summer_indoor_temperature", 24.0),
715
+ "indoor_humidity": building_info.get("summer_indoor_humidity", 50.0),
716
+ "daily_range": building_info.get("daily_temperature_range", 8.0),
717
+ "latitude": building_info.get("latitude", "40N"),
718
+ "month": building_info.get("design_month", 7),
719
+ "hour": building_info.get("design_hour", 15)
720
+ }
721
+
722
+ # Calculate cooling load
723
+ cooling_results = self.calculate_cooling_load(
724
+ components=components,
725
+ internal_loads=internal_loads,
726
+ building_info=building_info,
727
+ climate_data=climate_data
728
+ )
729
+
730
+ # Get heating climate data
731
+ heating_outdoor_conditions = {
732
+ "temperature": building_info.get("winter_outdoor_temperature", -10.0),
733
+ "humidity": building_info.get("winter_outdoor_humidity", 80.0)
734
+ }
735
+
736
+ heating_indoor_conditions = {
737
+ "temperature": building_info.get("winter_indoor_temperature", 21.0),
738
+ "humidity": building_info.get("winter_indoor_humidity", 30.0)
739
+ }
740
+
741
+ # Calculate heating load
742
+ heating_results = self.calculate_heating_load(
743
+ components=components,
744
+ building_info=building_info,
745
+ outdoor_conditions=heating_outdoor_conditions,
746
+ indoor_conditions=heating_indoor_conditions
747
+ )
748
+
749
+ # Calculate psychrometric properties
750
+ psychrometric_results = self.calculate_psychrometric_properties(
751
+ outdoor_temp=climate_data["outdoor_temp"],
752
+ outdoor_humidity=climate_data["outdoor_humidity"],
753
+ indoor_temp=climate_data["indoor_temp"],
754
+ indoor_humidity=climate_data["indoor_humidity"]
755
+ )
756
+
757
+ # Store results in session state
758
+ st.session_state.calculation_results = {
759
+ "cooling_load": cooling_results,
760
+ "heating_load": heating_results,
761
+ "psychrometrics": psychrometric_results
762
+ }
763
+
764
+ # Navigate to results page
765
+ st.session_state.page = "results"
766
+ st.success("Calculations completed successfully!")
767
+
768
+ except Exception as e:
769
+ st.error(f"Error running calculations: {str(e)}")
770
+ st.error("Please check your input data and try again.")
771
+
772
+ def calculate_cooling_load(self, components, internal_loads, building_info, climate_data):
773
+ """
774
+ Calculate cooling load based on the uploaded data.
775
+
776
+ This is a simplified version that mimics the actual calculation logic.
777
+ In a real implementation, this would use the CoolingLoad class.
778
+
779
+ Args:
780
+ components: Dictionary of building components
781
+ internal_loads: Dictionary of internal loads
782
+ building_info: Dictionary of building information
783
+ climate_data: Dictionary of climate data
784
+
785
+ Returns:
786
+ Dictionary containing cooling load results
787
+ """
788
+ # Initialize component loads
789
+ component_loads = {
790
+ "walls": 0,
791
+ "windows": 0,
792
+ "doors": 0,
793
+ "roof": 0,
794
+ "floor": 0,
795
+ "people": 0,
796
+ "lighting": 0,
797
+ "equipment": 0,
798
+ "infiltration": 0
799
+ }
800
+
801
+ # Calculate wall loads
802
+ for wall in components["walls"]:
803
+ # Simplified calculation: U-value * Area * Temperature difference
804
+ temp_diff = climate_data["outdoor_temp"] - climate_data["indoor_temp"]
805
+ wall_load = wall["u_value"] * wall["area"] * temp_diff / 1000 # Convert to kW
806
+ component_loads["walls"] += wall_load
807
+
808
+ # Calculate window loads
809
+ for window in components["windows"]:
810
+ # Conduction load
811
+ temp_diff = climate_data["outdoor_temp"] - climate_data["indoor_temp"]
812
+ window_cond_load = window["u_value"] * window["area"] * temp_diff / 1000
813
+
814
+ # Solar load (simplified)
815
+ solar_factor = 200 # W/m² (simplified solar radiation)
816
+ shading_coef = window["shading_coefficient"] if window["has_shading"] else 1.0
817
+ window_solar_load = window["area"] * window["shgc"] * solar_factor * shading_coef / 1000
818
+
819
+ component_loads["windows"] += window_cond_load + window_solar_load
820
+
821
+ # Calculate door loads
822
+ for door in components["doors"]:
823
+ temp_diff = climate_data["outdoor_temp"] - climate_data["indoor_temp"]
824
+ door_load = door["u_value"] * door["area"] * temp_diff / 1000
825
+ component_loads["doors"] += door_load
826
+
827
+ # Calculate roof load
828
+ for roof in components["roofs"]:
829
+ # Simplified calculation with solar factor
830
+ temp_diff = (climate_data["outdoor_temp"] + 15) - climate_data["indoor_temp"] # Add 15°C for solar effect
831
+ roof_load = roof["u_value"] * roof["area"] * temp_diff / 1000
832
+ component_loads["roof"] += roof_load
833
+
834
+ # Calculate floor load
835
+ for floor in components["floors"]:
836
+ # Simplified calculation
837
+ temp_diff = climate_data["outdoor_temp"] - climate_data["indoor_temp"]
838
+ floor_load = floor["u_value"] * floor["area"] * temp_diff * 0.5 / 1000 # 50% of full temp diff
839
+ component_loads["floor"] += floor_load
840
+
841
+ # Calculate people loads
842
+ for people in internal_loads["people"]:
843
+ sensible_load = people["total_sensible"] / 1000 # Convert to kW
844
+ latent_load = people["total_latent"] / 1000 # Convert to kW
845
+ component_loads["people"] += sensible_load + latent_load
846
+
847
+ # Calculate lighting loads
848
+ for lighting in internal_loads["lighting"]:
849
+ lighting_load = lighting["power"] / 1000 # Convert to kW
850
+ component_loads["lighting"] += lighting_load
851
+
852
+ # Calculate equipment loads
853
+ for equipment in internal_loads["equipment"]:
854
+ sensible_load = equipment["sensible_heat"] / 1000 # Convert to kW
855
+ latent_load = equipment["latent_heat"] / 1000 # Convert to kW
856
+ component_loads["equipment"] += sensible_load + latent_load
857
+
858
+ # Calculate infiltration load
859
+ floor_area = building_info.get("floor_area", 100.0)
860
+ building_height = building_info.get("building_height", 3.0)
861
+ infiltration_rate = building_info.get("infiltration_rate", 0.5)
862
+ volume = floor_area * building_height
863
+
864
+ # Sensible infiltration load
865
+ air_density = 1.2 # kg/m³
866
+ specific_heat = 1.005 # kJ/kg·K
867
+ temp_diff = climate_data["outdoor_temp"] - climate_data["indoor_temp"]
868
+ sensible_inf = volume * infiltration_rate * air_density * specific_heat * temp_diff / 3600
869
+
870
+ # Latent infiltration load (simplified)
871
+ latent_heat = 2450 # kJ/kg
872
+ humidity_diff = 0.005 # kg/kg (simplified)
873
+ latent_inf = volume * infiltration_rate * air_density * latent_heat * humidity_diff / 3600
874
+
875
+ component_loads["infiltration"] = sensible_inf + latent_inf
876
+
877
+ # Calculate total loads
878
+ sensible_load = (
879
+ component_loads["walls"] +
880
+ component_loads["windows"] +
881
+ component_loads["doors"] +
882
+ component_loads["roof"] +
883
+ component_loads["floor"] +
884
+ component_loads["people"] * 0.7 + # Assume 70% of people load is sensible
885
+ component_loads["lighting"] +
886
+ component_loads["equipment"] * 0.9 + # Assume 90% of equipment load is sensible
887
+ sensible_inf
888
+ )
889
+
890
+ latent_load = (
891
+ component_loads["people"] * 0.3 + # Assume 30% of people load is latent
892
+ component_loads["equipment"] * 0.1 + # Assume 10% of equipment load is latent
893
+ latent_inf
894
+ )
895
+
896
+ total_load = sensible_load + latent_load
897
+
898
+ # Apply safety factor
899
+ safety_factor = building_info.get("cooling_safety_factor", 10.0) / 100
900
+ total_load_with_safety = total_load * (1 + safety_factor)
901
+
902
+ # Calculate load per area
903
+ load_per_area = total_load_with_safety * 1000 / floor_area # W/m²
904
+
905
+ # Return results
906
+ return {
907
+ "total_load": total_load_with_safety,
908
+ "sensible_load": sensible_load * (1 + safety_factor),
909
+ "latent_load": latent_load * (1 + safety_factor),
910
+ "load_per_area": load_per_area,
911
+ "component_loads": component_loads
912
+ }
913
+
914
+ def calculate_heating_load(self, components, building_info, outdoor_conditions, indoor_conditions):
915
+ """
916
+ Calculate heating load based on the uploaded data.
917
+
918
+ This is a simplified version that mimics the actual calculation logic.
919
+ In a real implementation, this would use the HeatingLoad class.
920
+
921
+ Args:
922
+ components: Dictionary of building components
923
+ building_info: Dictionary of building information
924
+ outdoor_conditions: Dictionary of outdoor conditions
925
+ indoor_conditions: Dictionary of indoor conditions
926
+
927
+ Returns:
928
+ Dictionary containing heating load results
929
+ """
930
+ # Initialize component loads
931
+ component_loads = {
932
+ "walls": 0,
933
+ "windows": 0,
934
+ "doors": 0,
935
+ "roof": 0,
936
+ "floor": 0,
937
+ "infiltration": 0
938
+ }
939
+
940
+ # Calculate temperature difference
941
+ temp_diff = indoor_conditions["temperature"] - outdoor_conditions["temperature"]
942
+
943
+ # Calculate wall loads
944
+ for wall in components["walls"]:
945
+ wall_load = wall["u_value"] * wall["area"] * temp_diff / 1000 # Convert to kW
946
+ component_loads["walls"] += wall_load
947
+
948
+ # Calculate window loads
949
+ for window in components["windows"]:
950
+ window_load = window["u_value"] * window["area"] * temp_diff / 1000
951
+ component_loads["windows"] += window_load
952
+
953
+ # Calculate door loads
954
+ for door in components["doors"]:
955
+ door_load = door["u_value"] * door["area"] * temp_diff / 1000
956
+ component_loads["doors"] += door_load
957
+
958
+ # Calculate roof load
959
+ for roof in components["roofs"]:
960
+ roof_load = roof["u_value"] * roof["area"] * temp_diff / 1000
961
+ component_loads["roof"] += roof_load
962
+
963
+ # Calculate floor load
964
+ for floor in components["floors"]:
965
+ floor_load = floor["u_value"] * floor["area"] * temp_diff * 0.5 / 1000 # 50% of full temp diff
966
+ component_loads["floor"] += floor_load
967
+
968
+ # Calculate infiltration load
969
+ floor_area = building_info.get("floor_area", 100.0)
970
+ building_height = building_info.get("building_height", 3.0)
971
+ infiltration_rate = building_info.get("infiltration_rate", 0.5)
972
+ volume = floor_area * building_height
973
+
974
+ # Sensible infiltration load
975
+ air_density = 1.2 # kg/m³
976
+ specific_heat = 1.005 # kJ/kg·K
977
+ infiltration_load = volume * infiltration_rate * air_density * specific_heat * temp_diff / 3600
978
+ component_loads["infiltration"] = infiltration_load
979
+
980
+ # Calculate total load
981
+ total_load = sum(component_loads.values())
982
+
983
+ # Apply safety factor
984
+ safety_factor = building_info.get("heating_safety_factor", 10.0) / 100
985
+ total_load_with_safety = total_load * (1 + safety_factor)
986
+
987
+ # Calculate load per area
988
+ load_per_area = total_load_with_safety * 1000 / floor_area # W/m²
989
+
990
+ # Return results
991
+ return {
992
+ "total_load": total_load_with_safety,
993
+ "load_per_area": load_per_area,
994
+ "design_heat_loss": total_load,
995
+ "safety_factor": building_info.get("heating_safety_factor", 10.0),
996
+ "component_loads": component_loads
997
+ }
998
+
999
+ def calculate_psychrometric_properties(self, outdoor_temp, outdoor_humidity, indoor_temp, indoor_humidity):
1000
+ """
1001
+ Calculate psychrometric properties.
1002
+
1003
+ This is a simplified version that mimics the actual calculation logic.
1004
+ In a real implementation, this would use the Psychrometrics class.
1005
+
1006
+ Args:
1007
+ outdoor_temp: Outdoor dry bulb temperature (°C)
1008
+ outdoor_humidity: Outdoor relative humidity (%)
1009
+ indoor_temp: Indoor dry bulb temperature (°C)
1010
+ indoor_humidity: Indoor relative humidity (%)
1011
+
1012
+ Returns:
1013
+ Dictionary containing psychrometric properties
1014
+ """
1015
+ # Simplified psychrometric calculations
1016
+
1017
+ # Function to calculate humidity ratio
1018
+ def calculate_humidity_ratio(temp, rh):
1019
+ # Simplified calculation
1020
+ # Saturation pressure (kPa)
1021
+ p_sat = 0.611 * np.exp(17.27 * temp / (temp + 237.3))
1022
+ # Partial pressure of water vapor (kPa)
1023
+ p_w = p_sat * rh / 100
1024
+ # Humidity ratio (kg/kg)
1025
+ w = 0.622 * p_w / (101.325 - p_w)
1026
+ return w
1027
+
1028
+ # Function to calculate enthalpy
1029
+ def calculate_enthalpy(temp, w):
1030
+ # Simplified calculation
1031
+ # Enthalpy (kJ/kg)
1032
+ h = 1.005 * temp + w * (2501 + 1.86 * temp)
1033
+ return h
1034
+
1035
+ # Calculate outdoor properties
1036
+ outdoor_w = calculate_humidity_ratio(outdoor_temp, outdoor_humidity)
1037
+ outdoor_h = calculate_enthalpy(outdoor_temp, outdoor_w)
1038
+
1039
+ # Calculate indoor properties
1040
+ indoor_w = calculate_humidity_ratio(indoor_temp, indoor_humidity)
1041
+ indoor_h = calculate_enthalpy(indoor_temp, indoor_w)
1042
+
1043
+ # Return results
1044
+ return {
1045
+ "outdoor": {
1046
+ "dry_bulb": outdoor_temp,
1047
+ "relative_humidity": outdoor_humidity,
1048
+ "humidity_ratio": outdoor_w,
1049
+ "enthalpy": outdoor_h
1050
+ },
1051
+ "indoor": {
1052
+ "dry_bulb": indoor_temp,
1053
+ "relative_humidity": indoor_humidity,
1054
+ "humidity_ratio": indoor_w,
1055
+ "enthalpy": indoor_h
1056
+ }
1057
+ }
1058
+
1059
+
1060
+ # Run the application
1061
+ if __name__ == "__main__":
1062
+ app = HVACCalculatorFileUpload()
utils/utility_modules.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Minimal utility modules for HVAC Calculator File Upload version
3
+
4
+ This file contains minimal implementations of the utility classes needed
5
+ for the HVAC Calculator File Upload application to work.
6
+ """
7
+
8
+ class CoolingLoad:
9
+ """Simplified CoolingLoad class for the file upload version."""
10
+
11
+ def __init__(self):
12
+ """Initialize the CoolingLoad class."""
13
+ pass
14
+
15
+ def calculate_total_cooling_load(self, **kwargs):
16
+ """Placeholder for the actual cooling load calculation method."""
17
+ # This method is not actually used in the file upload version
18
+ # The calculation is done directly in the HVACCalculatorFileUpload class
19
+ return {}
20
+
21
+ class HeatingLoad:
22
+ """Simplified HeatingLoad class for the file upload version."""
23
+
24
+ def __init__(self):
25
+ """Initialize the HeatingLoad class."""
26
+ pass
27
+
28
+ def calculate_design_heating_load(self, **kwargs):
29
+ """Placeholder for the actual heating load calculation method."""
30
+ # This method is not actually used in the file upload version
31
+ # The calculation is done directly in the HVACCalculatorFileUpload class
32
+ return {}
33
+
34
+ class Psychrometrics:
35
+ """Simplified Psychrometrics class for the file upload version."""
36
+
37
+ def __init__(self):
38
+ """Initialize the Psychrometrics class."""
39
+ pass
40
+
41
+ def calculate_properties(self, **kwargs):
42
+ """Placeholder for the actual psychrometric properties calculation method."""
43
+ # This method is not actually used in the file upload version
44
+ # The calculation is done directly in the HVACCalculatorFileUpload class
45
+ return {}
46
+
47
+ class PsychrometricVisualization:
48
+ """Simplified PsychrometricVisualization class for the file upload version."""
49
+
50
+ def __init__(self):
51
+ """Initialize the PsychrometricVisualization class."""
52
+ pass
53
+
54
+ def display_psychrometric_chart(self, **kwargs):
55
+ """Placeholder for the actual psychrometric chart display method."""
56
+ # This method is not actually used in the file upload version
57
+ # The visualization is done directly in the HVACCalculatorFileUpload class
58
+ pass